osb/source/frontend/StarMainInterface.cpp
Kae 888cde79ef feat: middle click objects in the inventory to open their interface
makes carrying around shop objects easier

might need to restrict the allowed interaction types more, as some may break due to the source entity being the player
2024-03-27 16:00:13 +11:00

1607 lines
66 KiB
C++

#include "StarMainInterface.hpp"
#include "StarJsonExtra.hpp"
#include "StarLogging.hpp"
#include "StarLexicalCast.hpp"
#include "StarContainerInterface.hpp"
#include "StarCraftingInterface.hpp"
#include "StarMerchantInterface.hpp"
#include "StarRoot.hpp"
#include "StarUniverseClient.hpp"
#include "StarCodexInterface.hpp"
#include "StarSongbookInterface.hpp"
#include "StarQuestInterface.hpp"
#include "StarQuestManager.hpp"
#include "StarPopupInterface.hpp"
#include "StarConfirmationDialog.hpp"
#include "StarJoinRequestDialog.hpp"
#include "StarImageMetadataDatabase.hpp"
#include "StarGuiReader.hpp"
#include "StarPaneManager.hpp"
#include "StarClientCommandProcessor.hpp"
#include "StarChat.hpp"
#include "StarOptionsMenu.hpp"
#include "StarActionBar.hpp"
#include "StarWireInterface.hpp"
#include "StarTeamBar.hpp"
#include "StarStatusPane.hpp"
#include "StarCanvasWidget.hpp"
#include "StarLabelWidget.hpp"
#include "StarItemSlotWidget.hpp"
#include "StarPlayer.hpp"
#include "StarPlayerLog.hpp"
#include "StarMonster.hpp"
#include "StarItemDrop.hpp"
#include "StarAssets.hpp"
#include "StarPlayerInventory.hpp"
#include "StarCelestialDatabase.hpp"
#include "StarItem.hpp"
#include "StarAiInterface.hpp"
#include "StarDrawable.hpp"
#include "StarFireableItem.hpp"
#include "StarClientContext.hpp"
#include "StarToolUserEntity.hpp"
#include "StarTeleportDialog.hpp"
#include "StarCinematic.hpp"
#include "StarNameplatePainter.hpp"
#include "StarQuestIndicatorPainter.hpp"
#include "StarScriptPane.hpp"
#include "StarContainerEntity.hpp"
#include "StarWarpTargetEntity.hpp"
#include "StarPlayerUniverseMap.hpp"
#include "StarWorldTemplate.hpp"
#include "StarRadioMessagePopup.hpp"
#include "StarAiTypes.hpp"
#include "StarActiveItem.hpp"
#include "StarInspectionTool.hpp"
#include "StarQuestTracker.hpp"
#include "StarContainerInteractor.hpp"
#include "StarChatBubbleManager.hpp"
#include "StarNpc.hpp"
namespace Star {
GuiMessage::GuiMessage() : message(), cooldown(), springState() {}
GuiMessage::GuiMessage(String const& message, float cooldown, float spring)
: message(message), cooldown(cooldown), springState(spring) {}
MainInterface::MainInterface(UniverseClientPtr client, WorldPainterPtr painter, CinematicPtr cinematicOverlay)
: m_guiContext(GuiContext::singletonPtr())
, m_config(MainInterfaceConfig::loadFromAssets())
, m_client(std::move(client))
, m_worldPainter(std::move(painter))
, m_cinematicOverlay(std::move(cinematicOverlay))
, m_containerInteractor(make_shared<ContainerInteractor>())
{
GuiReader itemSlotReader;
m_cursorItem = convert<ItemSlotWidget>(itemSlotReader.makeSingle("cursorItemSlot", m_config->cursorItemSlot));
m_planetNameTimer = GameTimer(m_config->planetNameTime);
m_debugSpatialClearTimer = GameTimer(m_config->debugSpatialClearTime);
m_debugMapClearTimer = GameTimer(m_config->debugMapClearTime);
m_stickyTargetingTimer = GameTimer(m_config->monsterHealthBarTime);
m_inventoryWindow = make_shared<InventoryPane>(this, m_client->mainPlayer(), m_containerInteractor);
m_paneManager.registerPane(MainInterfacePanes::Inventory, PaneLayer::Window, m_inventoryWindow, [this](PanePtr const&) {
if (auto player = m_client->mainPlayer())
player->clearSwap();
if (m_containerPane) {
m_containerPane->dismiss();
m_containerPane = {};
m_containerInteractor->closeContainer();
}
for (EntityId id : m_interactionScriptPanes.keys()) {
if (m_paneManager.isDisplayed(m_interactionScriptPanes[id]) && as<ScriptPane>(m_interactionScriptPanes[id])->openWithInventory())
m_interactionScriptPanes[id]->dismiss();
}
});
m_overflowMessage = make_shared<GuiMessage>("", 0);
m_plainCraftingWindow = make_shared<CraftingPane>(m_client->worldClient(), m_client->mainPlayer(), JsonObject{{"filter", JsonArray{"plain"}}}, m_client->mainPlayer()->entityId());
m_paneManager.registerPane(MainInterfacePanes::CraftingPlain, PaneLayer::Window, m_plainCraftingWindow);
m_paneManager.registerPane(MainInterfacePanes::EscapeDialog, PaneLayer::ModalWindow, createEscapeDialog());
auto songbookInterface = make_shared<SongbookInterface>(m_client->mainPlayer());
m_paneManager.registerPane(MainInterfacePanes::Songbook, PaneLayer::Window, songbookInterface);
m_questLogInterface = make_shared<QuestLogInterface>(m_client->questManager(), m_client->mainPlayer(), m_cinematicOverlay, m_client);
m_paneManager.registerPane(MainInterfacePanes::QuestLog, PaneLayer::Window, m_questLogInterface);
auto aiInterface = make_shared<AiInterface>(m_client, m_cinematicOverlay, &m_paneManager);
m_paneManager.registerPane(MainInterfacePanes::Ai, PaneLayer::Window, aiInterface);
m_codexInterface = make_shared<CodexInterface>(m_client->mainPlayer());
m_paneManager.registerPane(MainInterfacePanes::Codex, PaneLayer::Window, m_codexInterface);
m_optionsMenu = make_shared<OptionsMenu>(&m_paneManager);
m_paneManager.registerPane(MainInterfacePanes::Options, PaneLayer::ModalWindow, m_optionsMenu);
m_popupInterface = make_shared<PopupInterface>();
m_paneManager.registerPane(MainInterfacePanes::Popup, PaneLayer::Window, m_popupInterface);
m_confirmationDialog = make_shared<ConfirmationDialog>();
m_paneManager.registerPane(MainInterfacePanes::Confirmation, PaneLayer::ModalWindow, m_confirmationDialog);
m_joinRequestDialog = make_shared<JoinRequestDialog>();
m_paneManager.registerPane(MainInterfacePanes::JoinRequest, PaneLayer::ModalWindow, m_joinRequestDialog);
m_actionBar = make_shared<ActionBar>(&m_paneManager, m_client->mainPlayer());
m_paneManager.registerPane(MainInterfacePanes::ActionBar, PaneLayer::Hud, m_actionBar);
m_questTracker = make_shared<QuestTrackerPane>();
m_paneManager.registerPane(MainInterfacePanes::QuestTracker, PaneLayer::Hud, m_questTracker);
m_mmUpgrade = make_shared<ScriptPane>(m_client, "/interface/scripted/mmupgrade/mmupgradegui.config");
m_paneManager.registerPane(MainInterfacePanes::MmUpgrade, PaneLayer::Window, m_mmUpgrade);
m_collections = make_shared<ScriptPane>(m_client, "/interface/scripted/collections/collectionsgui.config");
m_paneManager.registerPane(MainInterfacePanes::Collections, PaneLayer::Window, m_collections);
m_chat = make_shared<Chat>(m_client);
m_paneManager.registerPane(MainInterfacePanes::Chat, PaneLayer::Hud, m_chat);
m_clientCommandProcessor = make_shared<ClientCommandProcessor>(m_client, m_cinematicOverlay, &m_paneManager, m_config->macroCommands);
m_radioMessagePopup = make_shared<RadioMessagePopup>();
m_paneManager.registerPane(MainInterfacePanes::RadioMessagePopup, PaneLayer::Hud, m_radioMessagePopup);
m_wireInterface = make_shared<WirePane>(m_client->worldClient(), m_client->mainPlayer(), m_worldPainter);
m_paneManager.registerPane(MainInterfacePanes::WireInterface, PaneLayer::World, m_wireInterface);
m_client->mainPlayer()->setWireConnector(m_wireInterface.get());
auto teamBar = make_shared<TeamBar>(this, m_client);
m_paneManager.registerPane(MainInterfacePanes::TeamBar, PaneLayer::Hud, teamBar);
auto statusPane = make_shared<StatusPane>(&m_paneManager, m_client);
m_paneManager.registerPane(MainInterfacePanes::StatusPane, PaneLayer::Hud, statusPane);
auto planetName = make_shared<Pane>();
m_planetText = make_shared<LabelWidget>();
m_planetText->setFontSize(m_config->planetNameFontSize);
m_planetText->setFontMode(FontMode::Normal);
m_planetText->setAnchor(HorizontalAnchor::HMidAnchor, VerticalAnchor::VMidAnchor);
m_planetText->setDirectives(m_config->planetNameDirectives);
planetName->disableScissoring();
planetName->setPosition(m_config->planetNameOffset);
planetName->setAnchor(PaneAnchor::Center);
planetName->addChild("planetText", m_planetText);
m_paneManager.registerPane(MainInterfacePanes::PlanetText, PaneLayer::Hud, planetName);
m_nameplatePainter = make_shared<NameplatePainter>();
m_questIndicatorPainter = make_shared<QuestIndicatorPainter>(m_client);
m_chatBubbleManager = make_shared<ChatBubbleManager>();
m_paneManager.displayRegisteredPane(MainInterfacePanes::ActionBar);
m_paneManager.displayRegisteredPane(MainInterfacePanes::Chat);
m_paneManager.displayRegisteredPane(MainInterfacePanes::TeamBar);
m_paneManager.displayRegisteredPane(MainInterfacePanes::StatusPane);
}
MainInterface::~MainInterface() {
m_paneManager.dismissAllPanes();
}
MainInterface::RunningState MainInterface::currentState() const {
return m_state;
}
MainInterfacePaneManager* MainInterface::paneManager() {
return &m_paneManager;
}
bool MainInterface::escapeDialogOpen() const {
return m_paneManager.registeredPaneIsDisplayed(MainInterfacePanes::EscapeDialog) || m_paneManager.registeredPaneIsDisplayed(MainInterfacePanes::Options);
}
void MainInterface::openCraftingWindow(Json const& config, EntityId sourceEntityId) {
if (m_craftingWindow && m_paneManager.isDisplayed(m_craftingWindow)) {
m_paneManager.dismissPane(m_craftingWindow);
bool fromPlayer = false;
if (auto player = m_client->mainPlayer())
fromPlayer = player->inWorld() && player->entityId() == sourceEntityId;
if (m_craftingWindow->sourceEntityId() == sourceEntityId) {
m_craftingWindow.reset();
return;
}
}
m_craftingWindow = make_shared<CraftingPane>(m_client->worldClient(), m_client->mainPlayer(), config, sourceEntityId);
m_paneManager.displayPane(PaneLayer::Window, m_craftingWindow, [this](PanePtr const&) {
if (auto player = m_client->mainPlayer())
player->clearSwap();
});
}
void MainInterface::openMerchantWindow(Json const& config, EntityId sourceEntityId) {
if (m_merchantWindow && m_paneManager.isDisplayed(m_merchantWindow)) {
m_paneManager.dismissPane(m_merchantWindow);
bool fromPlayer = false;
if (auto player = m_client->mainPlayer())
fromPlayer = player->inWorld() && player->entityId() == sourceEntityId;
if (!fromPlayer && m_merchantWindow->sourceEntityId() == sourceEntityId) {
m_merchantWindow.reset();
return;
}
}
bool openWithInventory = config.getBool("openWithInventory", true);
m_merchantWindow = make_shared<MerchantPane>(m_client->worldClient(), m_client->mainPlayer(), config, sourceEntityId);
m_paneManager.displayPane(PaneLayer::Window,
m_merchantWindow,
[this, openWithInventory](PanePtr const&) {
if (auto player = m_client->mainPlayer())
player->clearSwap();
if (openWithInventory)
m_paneManager.dismissRegisteredPane(MainInterfacePanes::Inventory);
});
if (openWithInventory)
m_paneManager.displayRegisteredPane(MainInterfacePanes::Inventory);
m_paneManager.bringPaneAdjacent(m_paneManager.registeredPane(MainInterfacePanes::Inventory),
m_merchantWindow, Root::singleton().assets()->json("/interface.config:bringAdjacentWindowGap").toFloat());
}
void MainInterface::togglePlainCraftingWindow() {
m_paneManager.toggleRegisteredPane(MainInterfacePanes::CraftingPlain);
if (m_craftingWindow && m_craftingWindow->isDisplayed()
&& m_craftingWindow != m_paneManager.registeredPane(MainInterfacePanes::CraftingPlain))
m_paneManager.dismissPane(m_craftingWindow);
m_craftingWindow = m_plainCraftingWindow;
}
bool MainInterface::windowsOpen() const {
return (bool)m_paneManager.topPane({PaneLayer::Window});
}
MerchantPanePtr MainInterface::activeMerchantPane() const {
if (m_paneManager.isDisplayed(m_merchantWindow))
return m_merchantWindow;
else
return {};
}
bool MainInterface::handleInputEvent(InputEvent const& event) {
auto player = m_client->mainPlayer();
auto inv = player->inventory();
auto& root = Root::singleton();
if (auto mouseMove = event.ptr<MouseMoveEvent>())
m_cursorScreenPos = mouseMove->mousePosition;
if (m_paneManager.sendInputEvent(event)) {
if (!event.is<MouseButtonUpEvent>() && !event.is<KeyUpEvent>())
return true;
}
if (event.is<KeyDownEvent>()) {
if (m_chat->hasFocus()) {
if (m_guiContext->actions(event).contains(InterfaceAction::ChatSendLine)) {
doChat(m_chat->currentChat(), true);
m_chat->clearCurrentChat();
m_chat->stopChat();
return true;
}
} else if (!m_paneManager.keyboardCapturedPane()) {
Maybe<InventorySlot> swapSlot;
for (auto action : m_guiContext->actions(event)) {
switch (action) {
default:
break;
case InterfaceAction::GuiShifting:
m_guiContext->setShiftHeld(true);
break;
case InterfaceAction::ChatBegin:
m_chat->startChat();
break;
case InterfaceAction::InterfaceHideHud:
m_disableHud = !m_disableHud;
break;
case InterfaceAction::InterfaceRepeatCommand:
if (!m_lastCommand.empty())
doChat(m_lastCommand, false);
break;
case InterfaceAction::InterfaceToggleFullscreen:
m_optionsMenu->toggleFullscreen();
break;
case InterfaceAction::InterfaceReload:
root.reload();
root.fullyLoad();
break;
case InterfaceAction::ChatBeginCommand:
m_chat->startCommand();
break;
case InterfaceAction::InterfaceEscapeMenu:
m_paneManager.toggleRegisteredPane(MainInterfacePanes::EscapeDialog);
break;
case InterfaceAction::InterfaceInventory:
m_paneManager.toggleRegisteredPane(MainInterfacePanes::Inventory);
break;
case InterfaceAction::InterfaceCodex:
m_paneManager.toggleRegisteredPane(MainInterfacePanes::Codex);
break;
case InterfaceAction::InterfaceQuest:
m_paneManager.toggleRegisteredPane(MainInterfacePanes::QuestLog);
break;
case InterfaceAction::InterfaceCrafting:
togglePlainCraftingWindow();
break;
}
}
}
return false;
} else if (auto keyUp = event.ptr<KeyUpEvent>()) {
if (m_guiContext->actionsForKey(keyUp->key).contains(InterfaceAction::GuiShifting))
m_guiContext->setShiftHeld(false);
return false;
} else if (auto mouseDown = event.ptr<MouseButtonDownEvent>()) {
auto mouseButton = mouseDown->mouseButton;
if (mouseButton >= MouseButton::Left && mouseButton <= MouseButton::Right
&& overlayClick(mouseDown->mousePosition, mouseDown->mouseButton)) {
return true;
} else {
if (mouseButton == MouseButton::Left)
player->beginPrimaryFire();
if (mouseButton == MouseButton::Right)
player->beginAltFire();
if (mouseButton == MouseButton::Middle)
player->beginTrigger();
}
} else if (auto mouseUp = event.ptr<MouseButtonUpEvent>()) {
if (mouseUp->mouseButton == MouseButton::Left)
player->endPrimaryFire();
if (mouseUp->mouseButton == MouseButton::Right)
player->endAltFire();
if (mouseUp->mouseButton == MouseButton::Middle)
player->endTrigger();
}
bool captured = false;
for (auto& pair : m_canvases)
captured |= pair.second->sendEvent(event);
return captured;
}
bool MainInterface::inputFocus() const {
return (bool)m_paneManager.keyboardCapturedPane();
}
bool MainInterface::textInputActive() const {
return m_paneManager.keyboardCapturedForTextInput();
}
void MainInterface::handleInteractAction(InteractAction interactAction) {
auto assets = Root::singleton().assets();
auto world = m_client->worldClient();
if (interactAction.type == InteractActionType::OpenContainer) {
// If we're currently displaying this container, close it.
if (m_containerPane && m_containerInteractor->openContainerId() == interactAction.entityId) {
m_paneManager.dismissPane(m_containerPane);
return;
}
// If we're currently displaying another container, close it before we open.
if (m_containerPane)
m_paneManager.dismissPane(m_containerPane);
auto containerEntity = world->get<ContainerEntity>(interactAction.entityId);
if (!containerEntity)
return;
m_containerInteractor->openContainer(containerEntity);
m_paneManager.displayRegisteredPane(MainInterfacePanes::Inventory);
m_containerPane = make_shared<ContainerPane>(world, m_client->mainPlayer(), m_containerInteractor);
m_paneManager.displayPane(PaneLayer::Window, m_containerPane, [this](PanePtr const&) {
if (auto player = m_client->mainPlayer())
player->clearSwap();
m_paneManager.dismissRegisteredPane(MainInterfacePanes::Inventory);
});
m_paneManager.bringPaneAdjacent(m_paneManager.registeredPane(MainInterfacePanes::Inventory),
m_containerPane, Root::singleton().assets()->json("/interface.config:bringAdjacentWindowGap").toFloat());
} else if (interactAction.type == InteractActionType::SitDown) {
m_client->mainPlayer()->lounge(interactAction.entityId, interactAction.data.toUInt());
} else if (interactAction.type == InteractActionType::OpenCraftingInterface) {
if (!world->entity(interactAction.entityId))
return;
openCraftingWindow(interactAction.data, interactAction.entityId);
} else if (interactAction.type == InteractActionType::OpenSongbookInterface) {
m_paneManager.displayRegisteredPane(MainInterfacePanes::Songbook);
} else if (interactAction.type == InteractActionType::OpenNpcCraftingInterface) {
if (!world->entity(interactAction.entityId))
return;
openCraftingWindow(interactAction.data, interactAction.entityId);
} else if (interactAction.type == InteractActionType::OpenMerchantInterface) {
if (!world->entity(interactAction.entityId))
return;
openMerchantWindow(interactAction.data, interactAction.entityId);
} else if (interactAction.type == InteractActionType::OpenAiInterface) {
as<AiInterface>(m_paneManager.registeredPane(MainInterfacePanes::Ai))->setSourceEntityId(interactAction.entityId);
m_paneManager.displayRegisteredPane(MainInterfacePanes::Ai);
} else if (interactAction.type == InteractActionType::OpenTeleportDialog) {
if (m_teleportDialog)
m_teleportDialog->dismiss();
if (!m_client->canTeleport())
return;
auto currentLocation = TeleportBookmark();
auto config = assets->fetchJson(interactAction.data);
if (config.getBool("canBookmark", false)) {
if (auto entity = world->entity(interactAction.entityId)) {
if (auto uniqueEntityId = entity->uniqueId()) {
auto worldTemplate = m_client->worldClient()->currentTemplate();
String icon, planetName;
if (m_client->playerWorld().is<ClientShipWorldId>()) {
icon = "ship";
planetName = "Player Ship";
} else if (m_client->playerWorld().is<CelestialWorldId>()) {
icon = worldTemplate->worldParameters()->typeName;
planetName = worldTemplate->worldName();
} else if (m_client->playerWorld().is<InstanceWorldId>()) {
icon = worldTemplate->worldParameters()->typeName;
planetName = worldTemplate->worldName();
} else {
icon = "default";
planetName = "???";
}
currentLocation = TeleportBookmark {
{m_client->playerWorld(), SpawnTargetUniqueEntity(*uniqueEntityId)},
planetName,
config.getString("bookmarkName", ""),
icon
};
if (!m_client->mainPlayer()->universeMap()->teleportBookmarks().contains(currentLocation) || !config.getBool("canTeleport", true)) {
auto editBookmarkDialog = make_shared<EditBookmarkDialog>(m_client->mainPlayer()->universeMap());
editBookmarkDialog->setBookmark(currentLocation);
m_paneManager.displayPane(PaneLayer::ModalWindow, editBookmarkDialog);
return;
}
}
}
}
if (config.getBool("canTeleport", true)) {
m_teleportDialog = make_shared<TeleportDialog>(m_client, &m_paneManager, interactAction.data, interactAction.entityId, currentLocation);
m_paneManager.displayPane(PaneLayer::ModalWindow, m_teleportDialog);
}
} else if (interactAction.type == InteractActionType::ShowPopup) {
m_paneManager.displayRegisteredPane(MainInterfacePanes::Popup);
m_popupInterface->displayMessage(interactAction.data.getString("message"), interactAction.data.getString("title", ""), interactAction.data.getString("subtitle", ""), interactAction.data.optString("sound"));
} else if (interactAction.type == InteractActionType::ScriptPane) {
auto sourceEntity = interactAction.entityId;
// dismiss if there's already a scriptpane open for this source entity
if (sourceEntity != NullEntityId && m_interactionScriptPanes.contains(sourceEntity) && m_paneManager.isDisplayed(m_interactionScriptPanes[sourceEntity]))
m_paneManager.dismissPane(m_interactionScriptPanes[sourceEntity]);
ScriptPanePtr scriptPane = make_shared<ScriptPane>(m_client, interactAction.data, sourceEntity);
displayScriptPane(scriptPane, sourceEntity);
} else if (interactAction.type == InteractActionType::Message) {
m_client->mainPlayer()->receiveMessage(connectionForEntity(interactAction.entityId),
interactAction.data.getString("messageType"), interactAction.data.getArray("messageArgs"));
}
}
void MainInterface::preUpdate(float dt) {
auto player = m_client->mainPlayer();
if (!m_client->paused())
player->aim(cursorWorldPosition());
}
void MainInterface::update(float dt) {
m_paneManager.update(dt);
m_cursor.update(dt);
m_questLogInterface->pollDialog(&m_paneManager);
if (!m_paneManager.topPane({PaneLayer::ModalWindow}) && m_codexInterface->showNewCodex())
m_paneManager.displayRegisteredPane(MainInterfacePanes::Codex);
auto player = m_client->mainPlayer();
auto cursorWorldPos = cursorWorldPosition();
if (player->wireToolInUse()) {
m_paneManager.displayRegisteredPane(MainInterfacePanes::WireInterface);
player->setWireConnector(m_wireInterface.get());
} else {
m_paneManager.dismissRegisteredPane(MainInterfacePanes::WireInterface);
}
// update inventory pane items, to know if item slots changed
m_inventoryWindow->updateItems();
// update mouseover target
EntityId newMouseOverTarget = NullEntityId;
m_stickyTargetingTimer.tick(dt);
auto mouseoverEntities = m_client->worldClient()->query<DamageBarEntity>(RectF::withCenter(cursorWorldPos, Vec2F(1, 1)), [=](shared_ptr<DamageBarEntity> const& entity) {
return entity != player
&& entity->damageBar() == DamageBarType::Default
&& (entity->getTeam().type == TeamType::Enemy || entity->getTeam().type == TeamType::PVP)
&& m_client->worldClient()->lightLevel(entity->position()) > 0;
});
sortByComputedValue(mouseoverEntities, [&](DamageBarEntityPtr const& a) {
return m_client->worldClient()->geometry().diff(a->position(), cursorWorldPos).magnitude();
});
if (mouseoverEntities.size() > 0) {
newMouseOverTarget = mouseoverEntities[0]->entityId();
} else if (m_lastMouseoverTarget == NullEntityId && player->lastDamagedTarget() != NullEntityId && player->timeSinceLastGaveDamage() < m_stickyTargetingTimer.time / 2) {
if (auto targetEntity = as<DamageBarEntity>(m_client->worldClient()->entity(player->lastDamagedTarget()))) {
if (targetEntity->damageBar() == DamageBarType::Default && (targetEntity->getTeam().type == TeamType::Enemy || targetEntity->getTeam().type == TeamType::PVP)) {
newMouseOverTarget = targetEntity->entityId();
}
}
}
if (newMouseOverTarget != NullEntityId && newMouseOverTarget != m_lastMouseoverTarget) {
m_lastMouseoverTarget = newMouseOverTarget;
m_portraitScale = 0;
m_stickyTargetingTimer.reset();
}
if (m_stickyTargetingTimer.ready())
m_lastMouseoverTarget = NullEntityId;
// special damage bar entity
if (m_specialDamageBarTarget != NullEntityId) {
auto damageBarEntity = as<DamageBarEntity>(m_client->worldClient()->entity(m_specialDamageBarTarget));
if (damageBarEntity && damageBarEntity->damageBar() == DamageBarType::Special) {
float targetHealth = damageBarEntity->health() / damageBarEntity->maxHealth();
float fillSpeed = 1.0f / Root::singleton().assets()->json("/interface.config:specialDamageBar.fillTime").toFloat();
if (abs(targetHealth - m_specialDamageBarValue) < fillSpeed * dt)
m_specialDamageBarValue = targetHealth;
else
m_specialDamageBarValue += copysign(1.0f, targetHealth - m_specialDamageBarValue) * fillSpeed * dt;
} else {
m_specialDamageBarTarget = NullEntityId;
}
}
if (m_specialDamageBarTarget == NullEntityId)
m_specialDamageBarValue = 0.0f;
if (m_specialDamageBarTarget == NullEntityId && m_client->mainPlayer()->inWorld()) {
List<DamageBarEntityPtr> specialDamageTargets;
m_client->worldClient()->forAllEntities([&specialDamageTargets](EntityPtr const& entity) {
if (auto damageBarEntity = as<DamageBarEntity>(entity))
if (damageBarEntity->damageBar() == DamageBarType::Special)
specialDamageTargets.append(damageBarEntity);
});
sortByComputedValue(specialDamageTargets, [&](DamageBarEntityPtr entity) {
return m_client->worldClient()->geometry().diff(entity->position(), m_client->mainPlayer()->position());
});
if (specialDamageTargets.size() > 0)
m_specialDamageBarTarget = specialDamageTargets[0]->entityId();
}
for (auto const& message : m_client->mainPlayer()->pullQueuedMessages())
queueMessage(message);
auto chatHeight = (m_chat->active() && m_chat->visible() > 0.1) ? m_chat->size()[1] : 0;
m_radioMessagePopup->setChatHeight(chatHeight);
if (!m_cinematicOverlay || m_cinematicOverlay->completed()) {
if (m_client->mainPlayer()->interruptRadioMessage())
m_radioMessagePopup->interrupt();
if (!m_radioMessagePopup->messageActive()) {
if (auto radioMessage = m_client->mainPlayer()->pullPendingRadioMessage()) {
m_radioMessagePopup->setMessage(*radioMessage);
m_paneManager.displayRegisteredPane(MainInterfacePanes::RadioMessagePopup);
ChatReceivedMessage message = {
{MessageContext::RadioMessage},
ServerConnectionId,
Text::stripEscapeCodes(radioMessage->senderName),
Text::stripEscapeCodes(radioMessage->text),
Text::stripEscapeCodes(radioMessage->portraitImage.replace("<frame>", "0"))
};
m_chat->addMessages({message}, false);
} else {
m_paneManager.dismissRegisteredPane(MainInterfacePanes::RadioMessagePopup);
}
}
m_client->mainPlayer()->setInCinematic(false);
} else {
m_client->mainPlayer()->setInCinematic(true);
}
for (auto const& drop : m_client->mainPlayer()->pullQueuedItemDrops())
queueItemPickupText(drop);
m_chat->addMessages(m_client->pullChatMessages());
if (auto worldClient = m_client->worldClient()) {
if (worldClient->inWorld()) {
if (auto cinematic = m_client->mainPlayer()->pullPendingCinematic()) {
if (*cinematic)
m_cinematicOverlay->load(Root::singleton().assets()->fetchJson(cinematic.take()));
else
m_cinematicOverlay->stop();
}
}
}
if (!m_confirmationDialog->isDisplayed()) {
if (auto confirmation = m_client->mainPlayer()->pullPendingConfirmation()) {
m_paneManager.displayRegisteredPane(MainInterfacePanes::Confirmation);
m_confirmationDialog->displayConfirmation(confirmation->first, confirmation->second);
}
} else {
auto confirmationSource = m_confirmationDialog->sourceEntityId();
if (confirmationSource && !m_client->worldClient()->playerCanReachEntity(*confirmationSource))
m_confirmationDialog->dismiss();
}
if (!m_joinRequestDialog->isDisplayed()) {
if (auto req = m_queuedJoinRequests.maybeTakeLast()) {
m_paneManager.displayRegisteredPane(MainInterfacePanes::JoinRequest);
m_joinRequestDialog->displayRequest(req->first, [req](P2PJoinRequestReply reply) mutable {
req->second.fulfill(reply);
});
}
}
for (EntityId id : m_interactionScriptPanes.keys()) {
if (!m_paneManager.isDisplayed(m_interactionScriptPanes[id]))
m_interactionScriptPanes.remove(id);
}
if (!m_messages.contains(m_overflowMessage))
m_messageOverflow = 0;
unsigned maxMessages = m_messageOverflow == 0 ? m_config->maxMessageCount : m_config->maxMessageCount + 1; // exclude overflow message
if (m_messages.size() > maxMessages) {
if (m_messageOverflow == 0) {
m_messages.prepend(m_overflowMessage);
}
m_messageOverflow++;
m_overflowMessage->message = m_config->overflowMessageText.replace("<count>", toString(m_messageOverflow));
m_overflowMessage->cooldown = m_config->messageTime;
if (auto oldest = m_messages.sorted([](GuiMessagePtr a, GuiMessagePtr b) { return a->cooldown < b->cooldown; }).maybeFirst())
m_overflowMessage->cooldown = oldest.value()->cooldown;
if (auto bottom = m_messages.filtered([this](GuiMessagePtr m) { return m != m_overflowMessage; }).maybeFirst())
bottom.value()->cooldown = 0;
}
for (auto it = m_messages.begin(); it != m_messages.end();) {
auto& message = *it;
message->cooldown -= dt;
if (message->cooldown < 0)
it = m_messages.erase(it);
else
it++;
}
for (auto it = m_itemDropMessages.begin(); it != m_itemDropMessages.end();) {
auto& message = *it;
if (message.second.second->cooldown < 0)
it = m_itemDropMessages.erase(it);
else
it++;
}
bool playerInWorld = m_client->mainPlayer()->inWorld();
if (m_cinematicOverlay->completed()) {
if (m_planetNameTimer.tick(dt)) {
m_paneManager.dismissRegisteredPane(MainInterfacePanes::PlanetText);
} else {
if (playerInWorld) {
String worldName;
if (auto worldTemplate = m_client->worldClient()->currentTemplate())
worldName = worldTemplate->worldName();
if (!worldName.empty()) {
m_planetText->setText(strf(m_config->planetNameFormatString.utf8Ptr(), worldName));
m_paneManager.displayRegisteredPane(MainInterfacePanes::PlanetText);
}
}
Color textColor = Color::White; // probably need to make this jsonable
float fadeTimer = m_planetNameTimer.timer;
if (fadeTimer < m_config->planetNameFadeTime)
textColor.setAlphaF(fadeTimer / m_config->planetNameFadeTime);
m_planetText->setColor(textColor);
}
} else if (!playerInWorld) {
m_planetNameTimer.reset();
m_paneManager.dismissRegisteredPane(MainInterfacePanes::PlanetText);
}
for (auto& containerResult : m_containerInteractor->pullContainerResults()) {
if (!m_containerPane || !m_containerPane->giveContainerResult(containerResult)) {
if (!m_inventoryWindow->giveContainerResult(containerResult)) {
for (auto item : containerResult) {
if (m_containerInteractor->containerOpen())
m_containerInteractor->addToContainer(m_client->mainPlayer()->pickupItems(item));
else
m_client->mainPlayer()->giveItem(item);
}
}
}
}
if (auto currentQuest = m_client->questManager()->currentQuest()) {
m_paneManager.displayRegisteredPane(MainInterfacePanes::QuestTracker);
m_questTracker->setQuest(*currentQuest);
} else {
m_paneManager.dismissRegisteredPane(MainInterfacePanes::QuestTracker);
}
updateCursor();
m_nameplatePainter->update(dt, m_client->worldClient(), m_worldPainter->camera(), m_client->worldClient()->interactiveHighlightMode());
m_questIndicatorPainter->update(dt, m_client->worldClient(), m_worldPainter->camera());
m_chatBubbleManager->setCamera(m_worldPainter->camera());
if (auto worldClient = m_client->worldClient()) {
auto chatActions = worldClient->pullPendingChatActions();
auto portraitActions = chatActions.filtered([](ChatAction action) { return action.is<PortraitChatAction>(); });
for (auto action : portraitActions) {
PortraitChatAction portraitAction = action.get<PortraitChatAction>();
String name;
if (auto npc = as<Npc>(worldClient->entity(portraitAction.entity)))
name = npc->name();
ChatReceivedMessage message = {
{ MessageContext::World },
ServerConnectionId,
Text::stripEscapeCodes(name),
Text::stripEscapeCodes(portraitAction.text),
Text::stripEscapeCodes(portraitAction.portrait.replace("<frame>", "0"))
};
m_chat->addMessages({message}, false);
}
m_chatBubbleManager->addChatActions(chatActions);
m_chatBubbleManager->update(dt, worldClient);
}
if (auto container = m_client->worldClient()->get<ContainerEntity>(m_containerInteractor->openContainerId())) {
if (!m_client->worldClient()->playerCanReachEntity(container->entityId())
|| !container->isInteractive())
m_containerInteractor->closeContainer();
}
if (m_paneManager.topPane({PaneLayer::Window, PaneLayer::ModalWindow}))
m_client->mainPlayer()->setBusyState(PlayerBusyState::Menu);
else if (m_chat->hasFocus())
m_client->mainPlayer()->setBusyState(PlayerBusyState::Chatting);
else
m_client->mainPlayer()->setBusyState(PlayerBusyState::None);
for (auto& pair : m_canvases) {
pair.second->setPosition(Vec2I());
if (pair.second->ignoreInterfaceScale())
pair.second->setSize(Vec2I(m_guiContext->windowSize()));
else
pair.second->setSize(Vec2I(m_guiContext->windowInterfaceSize()));
pair.second->update(dt);
}
}
void MainInterface::renderInWorldElements() {
if (m_disableHud)
return;
m_guiContext->setDefaultFont();
m_guiContext->setFontProcessingDirectives("");
m_guiContext->setFontColor(Vec4B::filled(255));
m_questIndicatorPainter->render();
m_nameplatePainter->render();
m_chatBubbleManager->render();
}
void MainInterface::render() {
if (m_disableHud)
return;
m_guiContext->setDefaultFont();
m_guiContext->setFontProcessingDirectives("");
m_guiContext->setFontColor(Vec4B::filled(255));
renderBreath();
renderMessages();
renderMonsterHealthBar();
renderSpecialDamageBar();
renderMainBar();
renderDebug();
RectI screenRect = RectI::withSize(Vec2I(), Vec2I(m_guiContext->windowSize()));
for (auto& pair : m_canvases)
pair.second->render(screenRect);
renderWindows();
renderCursor();
}
Vec2F MainInterface::cursorWorldPosition() const {
return m_worldPainter->camera().screenToWorld(Vec2F(m_cursorScreenPos));
}
bool MainInterface::isDebugDisplayed() {
return m_clientCommandProcessor->debugDisplayEnabled();
}
void MainInterface::doChat(String const& chat, bool addToHistory) {
if (chat.empty())
return;
if (chat.beginsWith("/")) {
m_lastCommand = chat;
for (auto const& result : m_clientCommandProcessor->handleCommand(chat))
m_chat->addLine(result);
} else {
m_client->sendChat(chat, m_chat->sendMode());
}
if (addToHistory)
m_chat->addHistory(chat);
}
void MainInterface::queueMessage(String const& message, Maybe<float> cooldown, float spring) {
auto guiMessage = make_shared<GuiMessage>(message, cooldown.value(m_config->messageTime), spring);
m_messages.append(guiMessage);
}
void MainInterface::queueMessage(String const& message) {
queueMessage(message, m_config->messageTime, 0.0f);
}
void MainInterface::queueJoinRequest(pair<String, RpcPromiseKeeper<P2PJoinRequestReply>> request)
{
m_queuedJoinRequests.push_back(request);
}
void MainInterface::queueItemPickupText(ItemPtr const& item) {
auto descriptor = item->descriptor();
if (m_itemDropMessages.contains(descriptor.singular())) {
auto countMessPair = m_itemDropMessages.get(descriptor.singular());
auto newCount = item->count() + countMessPair.first;
auto message = countMessPair.second;
message->message = strf("{} - {}", item->friendlyName(), newCount);
message->cooldown = m_config->messageTime;
m_itemDropMessages[descriptor.singular()] = {newCount, message};
} else {
auto message = make_shared<GuiMessage>(strf("{} - {}", item->friendlyName(), item->count()), m_config->messageTime);
m_messages.append(message);
m_itemDropMessages[descriptor.singular()] = {item->count(), message};
}
}
bool MainInterface::fixedCamera() const {
return m_clientCommandProcessor->fixedCameraEnabled();
}
void MainInterface::warpToOrbitedWorld(bool deploy) {
if (m_client->canBeamDown(deploy)) {
if (deploy)
m_client->warpPlayer(WarpAlias::OrbitedWorld, true, "deploy", true);
else
m_client->warpPlayer(WarpAlias::OrbitedWorld, true, "beam");
return;
}
m_guiContext->playAudio("/sfx/interface/clickon_error.ogg");
}
void MainInterface::warpToOwnShip() {
if (m_client->canBeamUp()) {
warpTo(WarpAlias::OwnShip);
} else {
m_guiContext->playAudio("/sfx/interface/clickon_error.ogg");
}
}
void MainInterface::warpTo(WarpAction const& warpAction) {
if (m_client->beamUpRule() == BeamUpRule::AnywhereWithWarning) {
if (m_confirmationDialog->isDisplayed())
m_confirmationDialog->dismiss();
m_paneManager.displayRegisteredPane(MainInterfacePanes::Confirmation);
m_confirmationDialog->displayConfirmation("/interface/windowconfig/beamupconfirmation.config", [this, warpAction] (Widget*) {
m_client->warpPlayer(warpAction, true, "beam");
}, [](Widget*) {});
} else {
m_client->warpPlayer(warpAction, true, "beam");
}
}
CanvasWidgetPtr MainInterface::fetchCanvas(String const& canvasName, bool ignoreInterfaceScale) {
CanvasWidgetPtr canvas;
if (auto canvasPtr = m_canvases.ptr(canvasName))
canvas = *canvasPtr;
else {
m_canvases.emplace(canvasName, canvas = make_shared<CanvasWidget>());
canvas->setPosition(Vec2I());
if (ignoreInterfaceScale)
canvas->setSize(Vec2I(m_guiContext->windowSize()));
else
canvas->setSize(Vec2I(m_guiContext->windowInterfaceSize()));
}
canvas->setIgnoreInterfaceScale(ignoreInterfaceScale);
return canvas;
}
// For when the player swaps characters. We need to completely reload ScriptPanes,
// because a lot of ScriptPanes do not expect the character to suddenly change and may break or spill data over.
void MainInterface::takeScriptPanes(List<ScriptPaneInfo>& out) {
m_paneManager.dismissWhere([&](PanePtr const& pane) {
if (auto scriptPane = as<ScriptPane>(pane)) {
if (scriptPane->isDismissed())
return false;
auto sourceEntityId = scriptPane->sourceEntityId();
m_interactionScriptPanes.remove(sourceEntityId);
auto& info = out.emplaceAppend();
info.scriptPane = scriptPane;
info.config = scriptPane->rawConfig();
info.sourceEntityId = sourceEntityId;
info.visible = scriptPane->visibility();
info.position = scriptPane->relativePosition();
return true;
}
return false;
});
}
void MainInterface::reviveScriptPanes(List<ScriptPaneInfo>& panes) {
for (auto& info : panes) { // this is evil and stupid
info.scriptPane->~ScriptPane();
new(info.scriptPane.get()) ScriptPane(m_client, info.config, info.sourceEntityId);
info.scriptPane->setVisibility(info.visible);
displayScriptPane(info.scriptPane, info.sourceEntityId);
info.scriptPane->setPosition(info.position);
}
}
PanePtr MainInterface::createEscapeDialog() {
auto assets = Root::singleton().assets();
auto escapeDialog = make_shared<Pane>();
auto escapeDialogPtr = escapeDialog.get();
GuiReader escapeDialogReader;
escapeDialogReader.registerCallback("returnToGame", [escapeDialogPtr](Widget*) {
escapeDialogPtr->dismiss();
});
escapeDialogReader.registerCallback("showOptions", [escapeDialogPtr, this](Widget*) {
escapeDialogPtr->dismiss();
m_paneManager.displayRegisteredPane(MainInterfacePanes::Options);
});
escapeDialogReader.registerCallback("saveAndQuit", [escapeDialogPtr, this](Widget*) {
m_state = ReturnToTitle;
escapeDialogPtr->dismiss();
});
escapeDialogReader.construct(assets->json("/interface.config:escapeDialog"), escapeDialogPtr);
escapeDialog->fetchChild<LabelWidget>("lblversion")->setText(strf("OpenStarbound - {} ({})", StarVersionString, StarArchitectureString));
return escapeDialog;
}
float MainInterface::interfaceScale() const {
return m_guiContext->interfaceScale();
}
unsigned MainInterface::windowHeight() const {
return m_guiContext->windowHeight();
}
unsigned MainInterface::windowWidth() const {
return m_guiContext->windowWidth();
}
Vec2I MainInterface::mainBarPosition() const {
return Vec2I(windowWidth(), windowHeight()) - m_config->mainBarSize * interfaceScale();
}
void MainInterface::renderBreath() {
auto assets = Root::singleton().assets();
auto imgMetadata = Root::singleton().imageMetadataDatabase();
Vec2I breathBarSize = Vec2I(Vec2F(m_guiContext->textureSize("/interface/breath/empty.png")) * interfaceScale());
Vec2I breathOffset = jsonToVec2I(assets->json("/interface.config:breathPos"));
Vec2F breathBackgroundCenterPos(windowWidth() * 0.5f + breathOffset[0] * interfaceScale(), windowHeight() - breathOffset[1] * interfaceScale());
Vec2F breathBarPos = breathBackgroundCenterPos + Vec2F(jsonToVec2I(assets->json("/interface.config:breathBarPos")) * interfaceScale());
float breath = m_client->mainPlayer()->breath();
float breathMax = m_client->mainPlayer()->maxBreath();
size_t blocks = round((10 * breath) / breathMax);
if (blocks < 10) {
String breathPath = "/interface/breath/breath.png";
m_guiContext->drawQuad(breathPath, RectF::withCenter(breathBackgroundCenterPos, Vec2F(imgMetadata->imageSize(breathPath)) * interfaceScale()));
for (size_t i = 0; i < 10; i++) {
if (i >= blocks) {
if (blocks == 0 && Time::monotonicMilliseconds() % 500 > 250)
m_guiContext->drawQuad("/interface/breath/warning.png", breathBarPos + Vec2F(breathBarSize[0] * i, 0), interfaceScale());
else
m_guiContext->drawQuad("/interface/breath/empty.png", breathBarPos + Vec2F(breathBarSize[0] * i, 0), interfaceScale());
} else {
m_guiContext->drawQuad("/interface/breath/breathbar.png", breathBarPos + Vec2F(breathBarSize[0] * i, 0), interfaceScale());
}
}
}
}
void MainInterface::renderMessages() {
if (m_messages.empty())
return;
Vec2F totalOffset = {};
auto imgMetadata = Root::singleton().imageMetadataDatabase();
unsigned bottomOffset = Root::singleton().configuration()->getPath("inventory.bottomActionBar").optBool().value(false) ? 32 : 0;
for (auto& message : m_messages) {
Vec2F hiddenOffset = Vec2F(m_config->messageHiddenOffset);
Vec2F activeOffset = Vec2F(m_config->messageActiveOffset);
if (bottomOffset) {
activeOffset[1] += bottomOffset;
bottomOffset = 0;
}
Vec2F messageOffset = lerp(message->springState, Vec2F(), activeOffset - hiddenOffset);
totalOffset += messageOffset;
messageOffset = totalOffset + hiddenOffset;
Vec2F backgroundCenterPos = Vec2F(windowWidth() * 0.5f + messageOffset[0] * interfaceScale(), messageOffset[1] * interfaceScale());
Vec2F backgroundTextCenterPos = backgroundCenterPos + Vec2F(m_config->messageTextContainerOffset * interfaceScale());
Vec2F messageTextOffset = backgroundTextCenterPos + Vec2F(m_config->messageTextOffset * interfaceScale());
if (message->cooldown > m_config->messageHideTime)
message->springState = (message->springState * m_config->messageWindowSpring + 1.0f) / (m_config->messageWindowSpring + 1.0f);
else
message->springState = (message->springState * m_config->messageWindowSpring) / (m_config->messageWindowSpring + 1.0f);
m_guiContext->drawQuad(m_config->messageTextContainer,
RectF::withCenter(backgroundTextCenterPos, Vec2F(imgMetadata->imageSize(m_config->messageTextContainer) * interfaceScale())));
m_guiContext->setFont(m_config->font);
m_guiContext->setFontSize(m_config->fontSize);
m_guiContext->setFontColor(Color::White.toRgba());
m_guiContext->renderText(message->message, {messageTextOffset, HorizontalAnchor::HMidAnchor, VerticalAnchor::VMidAnchor});
}
}
void MainInterface::renderMonsterHealthBar() {
auto assets = Root::singleton().assets();
auto imgMetadata = Root::singleton().imageMetadataDatabase();
if (m_lastMouseoverTarget != NullEntityId && !m_stickyTargetingTimer.ready()) {
auto world = m_client->worldClient();
auto entity = world->entity(m_lastMouseoverTarget);
auto showDamageEntity = as<DamageBarEntity>(entity);
if (!showDamageEntity) {
m_lastMouseoverTarget = NullEntityId;
return;
}
Vec2F backgroundCenterPos = Vec2F(windowWidth() / 2.0f, windowHeight());
auto container = assets->json("/interface.config:monsterHealth.container").toString();
auto offset = jsonToVec2F(assets->json("/interface.config:monsterHealth.offset")) * interfaceScale();
m_guiContext->drawQuad(container, RectF::withCenter(backgroundCenterPos + offset, Vec2F(imgMetadata->imageSize(container) * interfaceScale())));
auto nameTextOffset = jsonToVec2F(assets->json("/interface.config:monsterHealth.nameTextOffset")) * interfaceScale();
m_guiContext->setFont(m_config->font);
m_guiContext->setFontSize(m_config->fontSize);
m_guiContext->setFontColor(Color::White.toRgba());
m_guiContext->renderText(showDamageEntity->name(), backgroundCenterPos + nameTextOffset);
auto empty = assets->json("/interface.config:monsterHealth.progressEmpty").toString();
auto filled = assets->json("/interface.config:monsterHealth.progressFilled").toString();
auto progressBarOffset = jsonToVec2F(assets->json("/interface.config:monsterHealth.progressBarOffset")) * interfaceScale();
auto chunks = assets->json("/interface.config:monsterHealth.progressChunks").toInt();
int blocks = round(showDamageEntity->health() / showDamageEntity->maxHealth() * chunks);
Vec2F barPos = backgroundCenterPos + progressBarOffset;
Vec2F barItemOffset = Vec2F(imgMetadata->imageSize(filled)) * interfaceScale();
barItemOffset[1] = 0;
m_guiContext->drawQuad(empty, RectF::withSize(backgroundCenterPos + barPos, Vec2F(imgMetadata->imageSize(empty) * interfaceScale())));
for (int i = 0; i < blocks; i++)
m_guiContext->drawQuad(filled, barPos + barItemOffset * i, interfaceScale());
auto portraitOffset = jsonToVec2F(assets->json("/interface.config:monsterHealth.portraitOffset")) * interfaceScale();
auto portraitScale = assets->json("/interface.config:monsterHealth.portraitScale").toFloat() * interfaceScale();
auto portraitScissorRect = jsonToRectF(assets->json("/interface.config:monsterHealth.portraitScissorRect")).scaled(interfaceScale());
auto rect = portraitScissorRect.translated(backgroundCenterPos + portraitOffset);
m_guiContext->setInterfaceScissorRect(RectI(RectF(rect).scaled(1.0f / interfaceScale())));
auto portraitMaxSize = jsonToVec2I(assets->json("/interface.config:monsterHealth.portraitMaxSize"));
List<Drawable> portrait = showDamageEntity->portrait(PortraitMode::Full);
auto bounds = Drawable::boundBoxAll(portrait, true);
if (m_portraitScale == 0)
m_portraitScale = max<int>(1, ceil(max(bounds.size().x() / portraitMaxSize.x(), bounds.size().y() / portraitMaxSize.y())));
Drawable::translateAll(portrait, {-bounds.xMin() - (bounds.width() * 0.5f), -bounds.yMin() }); // crop out whitespace, align bottom center
Drawable::scaleAll(portrait, 1.0f / m_portraitScale);
for (auto drawable : portrait)
m_guiContext->drawDrawable(std::move(drawable), backgroundCenterPos + portraitOffset, portraitScale);
m_guiContext->resetInterfaceScissorRect();
}
}
void MainInterface::renderSpecialDamageBar() {
if (m_specialDamageBarTarget == NullEntityId)
return;
auto assets = Root::singleton().assets();
auto imgMetadata = Root::singleton().imageMetadataDatabase();
if (auto target = as<DamageBarEntity>(m_client->worldClient()->entity(m_specialDamageBarTarget))) {
Vec2F bottomCenter = Vec2F(windowWidth() / 2.0f, 0);
auto barConfig = assets->json("/interface.config:specialDamageBar");
auto background = barConfig.getString("background");
auto backgroundOffset = jsonToVec2F(barConfig.get("backgroundOffset")) * interfaceScale();
auto screenPos = RectF::withSize(bottomCenter + backgroundOffset, Vec2F(imgMetadata->imageSize(background) * interfaceScale()));
m_guiContext->drawQuad(background, screenPos);
auto fill = barConfig.getString("fill");
auto fillOffset = jsonToVec2F(barConfig.get("fillOffset")) * interfaceScale();
Vec2F size = Vec2F(barConfig.getInt("fillWidth") * m_specialDamageBarValue, imgMetadata->imageSize(fill).y());
m_guiContext->drawQuad(fill, RectF::withSize(bottomCenter + fillOffset, size * interfaceScale()));
auto nameOffset = jsonToVec2F(barConfig.get("nameOffset")) * interfaceScale();
m_guiContext->setFontColor(jsonToColor(barConfig.get("nameColor")).toRgba());
m_guiContext->setFontSize(barConfig.getUInt("nameSize"));
m_guiContext->setFontProcessingDirectives(barConfig.getString("nameDirectives"));
m_guiContext->renderText(target->name(), TextPositioning(bottomCenter + nameOffset, HorizontalAnchor::HMidAnchor, VerticalAnchor::BottomAnchor));
m_guiContext->setFontProcessingDirectives("");
}
}
void MainInterface::renderMainBar() {
Vec2I barPos = mainBarPosition();
m_cursorTooltip = {};
auto assets = Root::singleton().assets();
Vec2I inventoryButtonPos = barPos + m_config->mainBarInventoryButtonOffset * interfaceScale();
if (m_paneManager.registeredPaneIsDisplayed(MainInterfacePanes::Inventory)) {
if (overButton(m_config->mainBarInventoryButtonPoly, m_cursorScreenPos)) {
m_guiContext->drawQuad(m_config->inventoryImageOpenHover, Vec2F(inventoryButtonPos), interfaceScale());
m_cursorTooltip = assets->json("/interface.config:cursorTooltip.inventoryText").toString();
} else {
m_guiContext->drawQuad(m_config->inventoryImageOpen, Vec2F(inventoryButtonPos), interfaceScale());
}
} else if (overButton(m_config->mainBarInventoryButtonPoly, m_cursorScreenPos)) {
if (m_inventoryWindow->containsNewItems())
m_guiContext->drawQuad(m_config->inventoryImageGlowHover, Vec2F(inventoryButtonPos), interfaceScale());
else
m_guiContext->drawQuad(m_config->inventoryImageHover, Vec2F(inventoryButtonPos), interfaceScale());
m_cursorTooltip = assets->json("/interface.config:cursorTooltip.inventoryText").toString();
} else {
if (m_inventoryWindow->containsNewItems())
m_guiContext->drawQuad(m_config->inventoryImageGlow, Vec2F(inventoryButtonPos), interfaceScale());
else
m_guiContext->drawQuad(m_config->inventoryImage, Vec2F(inventoryButtonPos), interfaceScale());
}
auto drawStateButton = [this](MainInterfacePanes paneType, Vec2I pos, PolyI poly,
String image, String hoverImage, String openImage, String hoverOpenImage, String toolTip) {
if (m_paneManager.registeredPaneIsDisplayed(paneType)) {
if (overButton(poly, m_cursorScreenPos)) {
m_guiContext->drawQuad(hoverOpenImage, Vec2F(pos), interfaceScale());
m_cursorTooltip = toolTip;
} else {
m_guiContext->drawQuad(openImage, Vec2F(pos), interfaceScale());
}
} else if (overButton(poly, m_cursorScreenPos)) {
m_guiContext->drawQuad(hoverImage, Vec2F(pos), interfaceScale());
m_cursorTooltip = toolTip;
} else {
m_guiContext->drawQuad(image, Vec2F(pos), interfaceScale());
}
};
Vec2I craftButtonPos = barPos + m_config->mainBarCraftButtonOffset * interfaceScale();
drawStateButton(MainInterfacePanes::CraftingPlain,
craftButtonPos,
m_config->mainBarCraftButtonPoly,
m_config->craftImage,
m_config->craftImageHover,
m_config->craftImageOpen,
m_config->craftImageOpenHover,
assets->json("/interface.config:cursorTooltip.craftingText").toString());
Vec2I codexButtonPos = barPos + m_config->mainBarCodexButtonOffset * interfaceScale();
drawStateButton(MainInterfacePanes::Codex,
codexButtonPos,
m_config->mainBarCodexButtonPoly,
m_config->codexImage,
m_config->codexImageHover,
m_config->codexImageOpen,
m_config->codexImageHoverOpen,
assets->json("/interface.config:cursorTooltip.codexText").toString());
Vec2I mmUpgradeButtonPos = barPos + m_config->mainBarMmUpgradeButtonOffset * interfaceScale();
if (m_client->mainPlayer()->inventory()->essentialItem(EssentialItem::BeamAxe)) {
drawStateButton(MainInterfacePanes::MmUpgrade,
mmUpgradeButtonPos,
m_config->mainBarMmUpgradeButtonPoly,
m_config->mmUpgradeImage,
m_config->mmUpgradeImageHover,
m_config->mmUpgradeImageOpen,
m_config->mmUpgradeImageHoverOpen,
assets->json("/interface.config:cursorTooltip.mmUpgradeText").toString());
} else {
drawStateButton(MainInterfacePanes::MmUpgrade,
mmUpgradeButtonPos,
m_config->mainBarMmUpgradeButtonPoly,
m_config->mmUpgradeImageDisabled,
m_config->mmUpgradeImageDisabled,
m_config->mmUpgradeImageDisabled,
m_config->mmUpgradeImageDisabled,
assets->json("/interface.config:cursorTooltip.disabledText").toString());
}
Vec2I collectionsButtonPos = barPos + m_config->mainBarCollectionsButtonOffset * interfaceScale();
drawStateButton(MainInterfacePanes::Collections,
collectionsButtonPos,
m_config->mainBarCollectionsButtonPoly,
m_config->collectionsImage,
m_config->collectionsImageHover,
m_config->collectionsImageOpen,
m_config->collectionsImageHoverOpen,
assets->json("/interface.config:cursorTooltip.collectionsText").toString());
// when the player can't deploy or beam, show the deploy button disabled
// when the player can beam up they can't deploy down, show beaming up button in deploy button's place
// when the player can only deploy, only show deploy button
// when the player can deploy or beam down, show both buttons
Vec2F deployButtonPos(barPos + m_config->mainBarDeployButtonOffset * interfaceScale());
if (m_client->canBeamUp()) {
if (overButton(m_config->mainBarDeployButtonPoly, m_cursorScreenPos)) {
m_guiContext->drawQuad(m_config->beamUpImageHover, deployButtonPos, interfaceScale());
m_cursorTooltip = assets->json("/interface.config:cursorTooltip.beamUpText").toString();
} else {
m_guiContext->drawQuad(m_config->beamUpImage, deployButtonPos, interfaceScale());
}
} else if (m_client->canBeamDown(true)) {
if (overButton(m_config->mainBarDeployButtonPoly, m_cursorScreenPos)) {
m_guiContext->drawQuad(m_config->deployImageHover, deployButtonPos, interfaceScale());
m_cursorTooltip = assets->json("/interface.config:cursorTooltip.deployText").toString();
} else {
m_guiContext->drawQuad(m_config->deployImage, deployButtonPos, interfaceScale());
}
} else {
m_guiContext->drawQuad(m_config->deployImageDisabled, deployButtonPos, interfaceScale());
}
Vec2F beamButtonPos(barPos + m_config->mainBarBeamButtonOffset * interfaceScale());
if (m_client->canBeamDown()) {
if (overButton(m_config->mainBarBeamButtonPoly, m_cursorScreenPos)) {
m_guiContext->drawQuad(m_config->beamDownImageHover, beamButtonPos, interfaceScale());
m_cursorTooltip = assets->json("/interface.config:cursorTooltip.beamDownText").toString();
} else {
m_guiContext->drawQuad(m_config->beamDownImage, beamButtonPos, interfaceScale());
}
}
Vec2I questLogButtonPos = barPos + m_config->mainBarQuestLogButtonOffset * interfaceScale();
drawStateButton(MainInterfacePanes::QuestLog,
questLogButtonPos,
m_config->mainBarQuestLogButtonPoly,
m_config->questLogImage,
m_config->questLogImageHover,
m_config->questLogImageOpen,
m_config->questLogImageHoverOpen,
assets->json("/interface.config:cursorTooltip.questsText").toString());
}
void MainInterface::renderWindows() {
m_paneManager.render();
}
void MainInterface::renderDebug() {
if (!isDebugDisplayed()) {
SpatialLogger::clear();
m_debugTextRect = RectF::null();
LogMap::clear();
SpatialLogger::setObserved(false);
return;
}
SpatialLogger::setObserved(true);
if (m_clientCommandProcessor->debugHudEnabled()) {
auto assets = Root::singleton().assets();
m_guiContext->setFontSize(m_config->debugFontSize);
m_guiContext->setFont(m_config->debugFont);
m_guiContext->setLineSpacing(0.5f);
m_guiContext->setFontProcessingDirectives(m_config->debugFontDirectives);
m_guiContext->setFontColor(Color::White.toRgba());
m_guiContext->setFontMode(FontMode::Normal);
bool clearMap = m_debugMapClearTimer.wrapTick();
auto logMapValues = LogMap::getValues();
if (clearMap)
LogMap::clear();
List<String> formatted;
formatted.reserve(logMapValues.size());
int counter = 0;
for (auto const& pair : logMapValues) {
TextPositioning positioning = { Vec2F(m_config->debugOffset[0], windowHeight() - m_config->debugOffset[1] - m_config->fontSize * interfaceScale() * counter++) };
String& text = formatted.emplace_back(strf("{}^lightgray;:^green,set; {}", pair.first, pair.second));
m_debugTextRect.combine(m_guiContext->determineTextSize(text, positioning).padded(m_config->debugBackgroundPad));
}
if (!m_debugTextRect.isNull()) {
RenderQuad& quad = m_guiContext->renderer()->immediatePrimitives()
.emplace_back(std::in_place_type_t<RenderQuad>(), m_debugTextRect, m_config->debugBackgroundColor.toRgba(), 0.0f).get<RenderQuad>();
quad.b.color[3] = quad.c.color[3] = 0;
};
m_debugTextRect = RectF::null();
for (size_t index = 0; index != formatted.size(); ++index) {
TextPositioning positioning = { Vec2F(m_config->debugOffset[0], windowHeight() - m_config->debugOffset[1] - m_config->fontSize * interfaceScale() * index) };
m_guiContext->renderText(formatted[index], positioning);
}
m_guiContext->setFontSize(8);
m_guiContext->setDefaultFont();
m_guiContext->setDefaultLineSpacing();
m_guiContext->setFontColor(Vec4B::filled(255));
m_guiContext->setFontProcessingDirectives("");
}
auto const& camera = m_worldPainter->camera();
bool clearSpatial = m_debugSpatialClearTimer.wrapTick();
for (auto const& line : SpatialLogger::getLines("world", clearSpatial)) {
Vec2F begin = camera.worldToScreen(line.begin);
Vec2F end = camera.worldGeometry().diff(line.end, line.begin) * camera.pixelRatio() * TilePixels + begin;
m_guiContext->drawLine(begin, end, line.color, 1);
}
for (auto const& line : SpatialLogger::getLines("screen", clearSpatial))
m_guiContext->drawLine(Vec2F(line.begin), Vec2F(line.end), line.color, 1);
for (auto const& point : SpatialLogger::getPoints("world", clearSpatial)) {
auto position = camera.worldToScreen(point.position);
m_guiContext->drawLine(position + Vec2F(-2, -2), position + Vec2F(-2, 2), point.color, 1);
m_guiContext->drawLine(position + Vec2F(-2, 2), position + Vec2F(2, 2), point.color, 1);
m_guiContext->drawLine(position + Vec2F(2, 2), position + Vec2F(2, -2), point.color, 1);
m_guiContext->drawLine(position + Vec2F(2, -2), position + Vec2F(-2, -2), point.color, 1);
}
for (auto const& point : SpatialLogger::getPoints("screen", clearSpatial)) {
auto position = point.position;
m_guiContext->drawLine(position + Vec2F(-2, -2), position + Vec2F(-2, 2), point.color, 1);
m_guiContext->drawLine(position + Vec2F(-2, 2), position + Vec2F(2, 2), point.color, 1);
m_guiContext->drawLine(position + Vec2F(2, 2), position + Vec2F(2, -2), point.color, 1);
m_guiContext->drawLine(position + Vec2F(2, -2), position + Vec2F(-2, -2), point.color, 1);
}
m_guiContext->setFontSize(m_config->debugFontSize);
for (auto const& logText : SpatialLogger::getText("world", clearSpatial)) {
m_guiContext->setFontColor(logText.color);
m_guiContext->renderText(logText.text.utf8Ptr(), camera.worldToScreen(logText.position));
}
for (auto const& logText : SpatialLogger::getText("screen", clearSpatial)) {
m_guiContext->setFontColor(logText.color);
m_guiContext->renderText(logText.text.utf8Ptr(), logText.position);
}
m_guiContext->setFontColor(Vec4B::filled(255));
}
void MainInterface::updateCursor() {
Maybe<String> cursorOverride = m_actionBar->cursorOverride(m_cursorScreenPos);
if (!cursorOverride) {
if (auto pane = m_paneManager.getPaneAt(m_cursorScreenPos / interfaceScale())) {
cursorOverride = cursorOverride.orMaybe(pane->cursorOverride(m_cursorScreenPos / interfaceScale()));
} else {
auto player = m_client->mainPlayer();
if (auto anchorState = m_client->mainPlayer()->loungingIn()) {
if (auto loungeable = m_client->worldClient()->get<LoungeableEntity>(anchorState->entityId)) {
if (auto loungeAnchor = loungeable->loungeAnchor(anchorState->positionIndex))
cursorOverride = cursorOverride.orMaybe(loungeAnchor->cursorOverride);
}
}
if (!cursorOverride) {
for (auto item : {player->primaryHandItem(), player->altHandItem()}) {
if (auto activeItem = as<ActiveItem>(item)) {
if (auto cursor = activeItem->cursor()) {
cursorOverride = cursor;
break;
}
} else if (auto inspectionTool = as<InspectionTool>(item)) {
cursorOverride = String("/cursors/inspect.cursor");
break;
}
}
}
}
}
if (cursorOverride)
m_cursor.setCursor(cursorOverride.take());
else
m_cursor.resetCursor();
}
void MainInterface::renderCursor() {
// if we're currently playing a cinematic, we should not render the mouse.
if (m_cinematicOverlay && !m_cinematicOverlay->completed())
return m_guiContext->applicationController()->setCursorVisible(false);
Vec2I cursorPos = m_cursorScreenPos;
Vec2I cursorSize = m_cursor.size();
Vec2I cursorOffset = m_cursor.offset();
unsigned int cursorScale = m_cursor.scale(interfaceScale());
Drawable cursorDrawable = m_cursor.drawable();
cursorPos[0] -= cursorOffset[0] * cursorScale;
cursorPos[1] -= (cursorSize[1] - cursorOffset[1]) * cursorScale;
if (!m_guiContext->trySetCursor(cursorDrawable, cursorOffset, cursorScale))
m_guiContext->drawDrawable(cursorDrawable, Vec2F(cursorPos), cursorScale);
if (m_cursorTooltip) {
auto assets = Root::singleton().assets();
auto imgDb = Root::singleton().imageMetadataDatabase();
auto backgroundImage = assets->json("/interface.config:cursorTooltip.background").toString();
auto rawCursorOffset = jsonToVec2I(assets->json("/interface.config:cursorTooltip.offset"));
Vec2I tooltipSize = Vec2I(imgDb->imageSize(backgroundImage)) * interfaceScale();
Vec2I cursorOffset = (Vec2I{0, -m_cursor.size().y()} + rawCursorOffset) * cursorScale;
Vec2I tooltipOffset = m_cursorScreenPos + cursorOffset;
size_t fontSize = assets->json("/interface.config:cursorTooltip.fontSize").toUInt();
String font = assets->json("/interface.config:cursorTooltip.font").toString();
Vec4B fontColor = jsonToColor(assets->json("/interface.config:cursorTooltip.color")).toRgba();
m_guiContext->drawQuad(backgroundImage, Vec2F(tooltipOffset) + Vec2F(-tooltipSize.x(), 0), interfaceScale());
m_guiContext->setFontSize(fontSize);
m_guiContext->setFontColor(fontColor);
m_guiContext->setFont(font);
m_guiContext->renderText(*m_cursorTooltip,
TextPositioning(Vec2F(tooltipOffset) + Vec2F(-tooltipSize.x(), tooltipSize.y()) / 2,
HorizontalAnchor::HMidAnchor,
VerticalAnchor::VMidAnchor));
}
m_cursorItem->setPosition(m_cursorScreenPos / interfaceScale() + m_config->inventoryItemMouseOffset);
if (auto swapItem = m_client->mainPlayer()->inventory()->swapSlotItem())
m_cursorItem->setItem(swapItem);
else
m_cursorItem->setItem({});
m_cursorItem->render(RectI::withSize({}, {(int)windowWidth(), (int)windowHeight()}));
m_guiContext->resetInterfaceScissorRect();
}
bool MainInterface::overButton(PolyI buttonPoly, Vec2I const& mousePos) const {
Vec2I barPos = mainBarPosition();
buttonPoly.translate(barPos);
buttonPoly.scale(interfaceScale(), barPos);
return buttonPoly.contains(mousePos);
}
bool MainInterface::overlayClick(Vec2I const& mousePos, MouseButton) {
PolyI mainBarPoly = m_config->mainBarPoly;
Vec2I barPos = mainBarPosition();
mainBarPoly.translate(barPos);
mainBarPoly.scale(interfaceScale(), barPos);
if (overButton(m_config->mainBarInventoryButtonPoly, mousePos)) {
m_paneManager.toggleRegisteredPane(MainInterfacePanes::Inventory);
return true;
}
if (overButton(m_config->mainBarCraftButtonPoly, mousePos)) {
togglePlainCraftingWindow();
return true;
}
if (overButton(m_config->mainBarCodexButtonPoly, mousePos)) {
m_paneManager.toggleRegisteredPane(MainInterfacePanes::Codex);
return true;
}
if (overButton(m_config->mainBarDeployButtonPoly, mousePos)) {
if (m_client->canBeamDown(true))
warpToOrbitedWorld(true);
else if (m_client->canBeamUp())
warpToOwnShip();
return true;
}
if (overButton(m_config->mainBarBeamButtonPoly, mousePos)) {
if (m_client->canBeamDown())
warpToOrbitedWorld();
return true;
}
if (overButton(m_config->mainBarQuestLogButtonPoly, mousePos)) {
m_paneManager.toggleRegisteredPane(MainInterfacePanes::QuestLog);
return true;
}
if (overButton(m_config->mainBarMmUpgradeButtonPoly, mousePos)) {
if (m_client->mainPlayer()->inventory()->essentialItem(EssentialItem::BeamAxe))
m_paneManager.toggleRegisteredPane(MainInterfacePanes::MmUpgrade);
return true;
}
if (overButton(m_config->mainBarCollectionsButtonPoly, mousePos)) {
m_paneManager.toggleRegisteredPane(MainInterfacePanes::Collections);
return true;
}
return false;
}
void MainInterface::displayScriptPane(ScriptPanePtr& scriptPane, EntityId sourceEntity) {
// keep any number of script panes open with null source entities
if (sourceEntity != NullEntityId)
m_interactionScriptPanes[sourceEntity] = scriptPane;
PaneLayer layer = PaneLayer::Window;
if (auto layerName = scriptPane->config().optString("paneLayer"))
layer = PaneLayerNames.getLeft(*layerName);
if (scriptPane->openWithInventory()) {
m_paneManager.displayPane(layer, scriptPane, [this](PanePtr const&) {
if (auto player = m_client->mainPlayer())
player->clearSwap();
m_paneManager.dismissRegisteredPane(MainInterfacePanes::Inventory);
});
m_paneManager.displayRegisteredPane(MainInterfacePanes::Inventory);
m_paneManager.bringPaneAdjacent(m_paneManager.registeredPane(MainInterfacePanes::Inventory),
scriptPane, Root::singleton().assets()->json("/interface.config:bringAdjacentWindowGap").toFloat());
} else {
m_paneManager.displayPane(layer, scriptPane);
}
}
}