Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/frontend/StarMainInterface.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'source/frontend/StarMainInterface.cpp')
-rw-r--r--source/frontend/StarMainInterface.cpp1472
1 files changed, 1472 insertions, 0 deletions
diff --git a/source/frontend/StarMainInterface.cpp b/source/frontend/StarMainInterface.cpp
new file mode 100644
index 0000000..1f007a5
--- /dev/null
+++ b/source/frontend/StarMainInterface.cpp
@@ -0,0 +1,1472 @@
+#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 "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)
+ : message(message), cooldown(cooldown), springState(0) {}
+
+MainInterface::MainInterface(UniverseClientPtr client, WorldPainterPtr painter, CinematicPtr cinematicOverlay) {
+ m_state = Running;
+
+ m_guiContext = GuiContext::singletonPtr();
+
+ m_client = client;
+ m_worldPainter = painter;
+ m_cinematicOverlay = cinematicOverlay;
+
+ m_disableHud = false;
+
+ m_cursorScreenPos = Vec2I();
+ m_state = Running;
+
+ m_config = MainInterfaceConfig::loadFromAssets();
+
+ 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_debugTextRect = RectF::null();
+
+ m_lastMouseoverTarget = NullEntityId;
+ 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&) {
+ m_client->mainPlayer()->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->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>(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);
+ 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&) {
+ m_client->mainPlayer()->clearSwap();
+ });
+}
+
+void MainInterface::openMerchantWindow(Json const& config, EntityId sourceEntityId) {
+ if (m_merchantWindow && m_paneManager.isDisplayed(m_merchantWindow)) {
+ m_paneManager.dismissPane(m_merchantWindow);
+ if (m_merchantWindow->sourceEntityId() == sourceEntityId) {
+ m_merchantWindow.reset();
+ return;
+ }
+ }
+
+ m_merchantWindow = make_shared<MerchantPane>(m_client->worldClient(), m_client->mainPlayer(), config, sourceEntityId);
+ m_paneManager.displayPane(PaneLayer::Window,
+ m_merchantWindow,
+ [this](PanePtr const&) {
+ m_client->mainPlayer()->clearSwap();
+ m_paneManager.dismissRegisteredPane(MainInterfacePanes::Inventory);
+ });
+ 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>()) {
+ if (mouseDown->mouseButton == MouseButton::Left || mouseDown->mouseButton == MouseButton::Right
+ || mouseDown->mouseButton == MouseButton::Middle)
+ overlayClick(mouseDown->mousePosition, mouseDown->mouseButton);
+
+ } 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();
+ }
+
+ return true;
+}
+
+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&) {
+ m_client->mainPlayer()->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);
+ // keep any number of script panes open with null source entities
+ if (sourceEntity != NullEntityId)
+ m_interactionScriptPanes[sourceEntity] = scriptPane;
+
+ if (scriptPane->openWithInventory()) {
+ m_paneManager.displayPane(PaneLayer::Window, scriptPane, [this](PanePtr const&) {
+ m_client->mainPlayer()->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(PaneLayer::Window, scriptPane);
+ }
+ } else if (interactAction.type == InteractActionType::Message) {
+ m_client->mainPlayer()->receiveMessage(connectionForEntity(interactAction.entityId),
+ interactAction.data.getString("messageType"), interactAction.data.getArray("messageArgs"));
+ }
+}
+
+void MainInterface::update() {
+ m_paneManager.update();
+
+ 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 (!m_client->paused())
+ player->aim(cursorWorldPos);
+ 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();
+ 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 * WorldTimestep)
+ m_specialDamageBarValue = targetHealth;
+ else
+ m_specialDamageBarValue += copysign(1.0f, targetHealth - m_specialDamageBarValue) * fillSpeed * WorldTimestep;
+ } 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>", strf("%s", 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 -= WorldTimestep;
+ 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++;
+ }
+
+ auto worldId = m_client->playerWorld();
+ if (worldId.is<CelestialWorldId>()) {
+ if (m_planetNameTimer.tick())
+ m_paneManager.dismissRegisteredPane(MainInterfacePanes::PlanetText);
+ else
+ m_paneManager.displayRegisteredPane(MainInterfacePanes::PlanetText);
+
+ if (auto parameters = m_client->celestialDatabase()->parameters(worldId.get<CelestialWorldId>()))
+ m_planetText->setText(strf(m_config->planetNameFormatString.utf8Ptr(), parameters->name()));
+
+ Color textColor = Color::White; // probably need to make this jsonable
+ float fadeTimer = m_planetNameTimer.percent();
+ if (fadeTimer < m_config->planetNameFadeTime)
+ textColor.setAlphaF(fadeTimer / m_config->planetNameFadeTime);
+
+ m_planetText->setColor(textColor);
+
+ } else {
+ m_paneManager.dismissRegisteredPane(MainInterfacePanes::PlanetText);
+ m_planetNameTimer.reset();
+ }
+
+ 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(m_client->worldClient(), m_worldPainter->camera(), m_client->worldClient()->interactiveHighlightMode());
+ m_questIndicatorPainter->update(m_client->worldClient(), 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(worldClient);
+ }
+ m_chatBubbleManager->setCamera(m_worldPainter->camera());
+
+ 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);
+}
+
+void MainInterface::renderInWorldElements() {
+ if (m_disableHud)
+ return;
+
+ m_guiContext->setFontColor(Vec4B::filled(255));
+ m_questIndicatorPainter->render();
+ m_nameplatePainter->render();
+ m_chatBubbleManager->render();
+}
+
+void MainInterface::render() {
+ if (m_disableHud)
+ return;
+
+ m_guiContext->setFontColor(Vec4B::filled(255));
+ renderBreath();
+ renderMessages();
+ renderMonsterHealthBar();
+ renderSpecialDamageBar();
+ renderMainBar();
+
+ renderWindows();
+ renderCursor();
+
+ renderDebug();
+}
+
+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) {
+ auto guiMessage = make_shared<GuiMessage>(message, m_config->messageTime);
+ m_messages.append(guiMessage);
+}
+
+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("%s - %s", item->friendlyName(), newCount);
+ message->cooldown = m_config->messageTime;
+ m_itemDropMessages[descriptor.singular()] = {newCount, message};
+ } else {
+ auto message = make_shared<GuiMessage>(strf("%s - %s", 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");
+ }
+}
+
+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("Starbound - %s (%s)", 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) {
+ m_guiContext->drawQuad("/interface/breath/breath.png",
+ RectF::withCenter(breathBackgroundCenterPos, Vec2F(imgMetadata->imageSize("/interface/breath/breath.png")) * 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() {
+ Vec2F totalOffset = {};
+ auto imgMetadata = Root::singleton().imageMetadataDatabase();
+ for (auto& message : m_messages) {
+ Vec2F hiddenOffset = Vec2F(m_config->messageHiddenOffset);
+ Vec2F messageOffset = lerp(message->springState, Vec2F(), Vec2F(m_config->messageActiveOffset) - 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->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->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(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();
+ return;
+ }
+
+ auto assets = Root::singleton().assets();
+ m_guiContext->setFontSize(m_config->debugFontSize);
+ int counter = 0;
+ m_guiContext->setFontColor(Color::Green.toRgba());
+
+ bool clearMap = m_debugMapClearTimer.wrapTick();
+ auto logMapValues = LogMap::getValues();
+ if (clearMap)
+ LogMap::clear();
+
+ for (auto const& pair : logMapValues) {
+ TextPositioning positioning = {Vec2F(m_config->debugOffset[0], windowHeight() - m_config->debugOffset[1] - m_config->fontSize * interfaceScale() * counter)};
+ m_debugTextRect.combine(m_guiContext->determineTextSize(strf("%s: %s", pair.first, pair.second), positioning).padded(m_config->debugBackgroundPad));
+ ++counter;
+ }
+
+ if (!m_debugTextRect.isNull())
+ m_guiContext->drawQuad(m_debugTextRect, m_config->debugBackgroundColor.toRgba());
+
+ if (clearMap)
+ m_debugTextRect = RectF::null();
+
+ counter = 0;
+ for (auto const& pair : logMapValues) {
+ TextPositioning positioning = {Vec2F(m_config->debugOffset[0], windowHeight() - m_config->debugOffset[1] - m_config->fontSize * interfaceScale() * counter)};
+ m_guiContext->renderText(strf("%s: %s", pair.first, pair.second), positioning);
+ ++counter;
+ }
+ m_guiContext->setFontColor(Vec4B::filled(255));
+
+ 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(assets->json("/interface.config:debugFontSize").toInt());
+ 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_cursor.update(WorldTimestep);
+
+ Vec2I cursorPos = m_cursorScreenPos;
+ Vec2I cursorSize = m_cursor.size();
+ Vec2I cursorOffset = m_cursor.offset();
+ cursorPos[0] -= cursorOffset[0] * interfaceScale();
+ cursorPos[1] -= (cursorSize[1] - cursorOffset[1]) * interfaceScale();
+ m_guiContext->drawDrawable(m_cursor.drawable(), Vec2F(cursorPos), interfaceScale());
+
+ 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) * interfaceScale();
+ Vec2I tooltipOffset = m_cursorScreenPos + cursorOffset;
+ size_t fontSize = assets->json("/interface.config:cursorTooltip.fontSize").toUInt();
+ 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->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);
+}
+
+void MainInterface::overlayClick(Vec2I const& mousePos, MouseButton 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;
+ }
+
+ if (overButton(m_config->mainBarCraftButtonPoly, mousePos)) {
+ togglePlainCraftingWindow();
+ return;
+ }
+
+ if (overButton(m_config->mainBarCodexButtonPoly, mousePos)) {
+ m_paneManager.toggleRegisteredPane(MainInterfacePanes::Codex);
+ return;
+ }
+
+ if (overButton(m_config->mainBarDeployButtonPoly, mousePos)) {
+ if (m_client->canBeamDown(true))
+ warpToOrbitedWorld(true);
+ else if (m_client->canBeamUp())
+ warpToOwnShip();
+ return;
+ }
+
+ if (overButton(m_config->mainBarBeamButtonPoly, mousePos)) {
+ if (m_client->canBeamDown())
+ warpToOrbitedWorld();
+ return;
+ }
+
+ if (overButton(m_config->mainBarQuestLogButtonPoly, mousePos)) {
+ m_paneManager.toggleRegisteredPane(MainInterfacePanes::QuestLog);
+ return;
+ }
+
+ if (overButton(m_config->mainBarMmUpgradeButtonPoly, mousePos)) {
+ if (m_client->mainPlayer()->inventory()->essentialItem(EssentialItem::BeamAxe))
+ m_paneManager.toggleRegisteredPane(MainInterfacePanes::MmUpgrade);
+ return;
+ }
+
+ if (overButton(m_config->mainBarCollectionsButtonPoly, mousePos)) {
+ m_paneManager.toggleRegisteredPane(MainInterfacePanes::Collections);
+ return;
+ }
+
+ if (mouseButton == MouseButton::Left)
+ m_client->mainPlayer()->beginPrimaryFire();
+ if (mouseButton == MouseButton::Right)
+ m_client->mainPlayer()->beginAltFire();
+ if (mouseButton == MouseButton::Middle)
+ m_client->mainPlayer()->beginTrigger();
+}
+
+} \ No newline at end of file