v1.4.4
This commit is contained in:
commit
9c94d113d3
10260 changed files with 1237388 additions and 0 deletions
105
source/frontend/CMakeLists.txt
Normal file
105
source/frontend/CMakeLists.txt
Normal file
|
@ -0,0 +1,105 @@
|
|||
INCLUDE_DIRECTORIES (
|
||||
${STAR_EXTERN_INCLUDES}
|
||||
${STAR_CORE_INCLUDES}
|
||||
${STAR_BASE_INCLUDES}
|
||||
${STAR_GAME_INCLUDES}
|
||||
${STAR_PLATFORM_INCLUDES}
|
||||
${STAR_APPLICATION_INCLUDES}
|
||||
${STAR_RENDERING_INCLUDES}
|
||||
${STAR_WINDOWING_INCLUDES}
|
||||
${STAR_FRONTEND_INCLUDES}
|
||||
)
|
||||
|
||||
SET (star_frontend_HEADERS
|
||||
StarActionBar.hpp
|
||||
StarAiInterface.hpp
|
||||
StarBookmarkInterface.hpp
|
||||
StarChat.hpp
|
||||
StarCharCreation.hpp
|
||||
StarCharSelection.hpp
|
||||
StarChatBubbleSeparation.hpp
|
||||
StarChatBubbleManager.hpp
|
||||
StarCinematic.hpp
|
||||
StarClientCommandProcessor.hpp
|
||||
StarCodexInterface.hpp
|
||||
StarConfirmationDialog.hpp
|
||||
StarContainerInterface.hpp
|
||||
StarContainerInteractor.hpp
|
||||
StarCraftingInterface.hpp
|
||||
StarErrorScreen.hpp
|
||||
StarGraphicsMenu.hpp
|
||||
StarInventory.hpp
|
||||
StarInterfaceCursor.hpp
|
||||
StarItemTooltip.hpp
|
||||
StarJoinRequestDialog.hpp
|
||||
StarKeybindingsMenu.hpp
|
||||
StarMainInterface.hpp
|
||||
StarMainInterfaceTypes.hpp
|
||||
StarMainMixer.hpp
|
||||
StarMerchantInterface.hpp
|
||||
StarModsMenu.hpp
|
||||
StarNameplatePainter.hpp
|
||||
StarOptionsMenu.hpp
|
||||
StarPopupInterface.hpp
|
||||
StarQuestIndicatorPainter.hpp
|
||||
StarQuestInterface.hpp
|
||||
StarQuestTracker.hpp
|
||||
StarRadioMessagePopup.hpp
|
||||
StarTeamBar.hpp
|
||||
StarTitleScreen.hpp
|
||||
StarScriptPane.hpp
|
||||
StarSimpleTooltip.hpp
|
||||
StarSongbookInterface.hpp
|
||||
StarStatusPane.hpp
|
||||
StarTeleportDialog.hpp
|
||||
StarWidgetLuaBindings.hpp
|
||||
StarWireInterface.hpp
|
||||
)
|
||||
|
||||
SET (star_frontend_SOURCES
|
||||
StarActionBar.cpp
|
||||
StarAiInterface.cpp
|
||||
StarBookmarkInterface.cpp
|
||||
StarChat.cpp
|
||||
StarCharCreation.cpp
|
||||
StarCharSelection.cpp
|
||||
StarChatBubbleSeparation.cpp
|
||||
StarChatBubbleManager.cpp
|
||||
StarCinematic.cpp
|
||||
StarClientCommandProcessor.cpp
|
||||
StarCodexInterface.cpp
|
||||
StarConfirmationDialog.cpp
|
||||
StarContainerInterface.cpp
|
||||
StarContainerInteractor.cpp
|
||||
StarCraftingInterface.cpp
|
||||
StarErrorScreen.cpp
|
||||
StarGraphicsMenu.cpp
|
||||
StarInventory.cpp
|
||||
StarInterfaceCursor.cpp
|
||||
StarItemTooltip.cpp
|
||||
StarJoinRequestDialog.cpp
|
||||
StarKeybindingsMenu.cpp
|
||||
StarMainInterface.cpp
|
||||
StarMainInterfaceTypes.cpp
|
||||
StarMainMixer.cpp
|
||||
StarMerchantInterface.cpp
|
||||
StarModsMenu.cpp
|
||||
StarNameplatePainter.cpp
|
||||
StarOptionsMenu.cpp
|
||||
StarPopupInterface.cpp
|
||||
StarQuestIndicatorPainter.cpp
|
||||
StarQuestInterface.cpp
|
||||
StarQuestTracker.cpp
|
||||
StarRadioMessagePopup.cpp
|
||||
StarTeamBar.cpp
|
||||
StarTitleScreen.cpp
|
||||
StarScriptPane.cpp
|
||||
StarSimpleTooltip.cpp
|
||||
StarSongbookInterface.cpp
|
||||
StarStatusPane.cpp
|
||||
StarTeleportDialog.cpp
|
||||
StarWidgetLuaBindings.cpp
|
||||
StarWireInterface.cpp
|
||||
)
|
||||
|
||||
ADD_LIBRARY (star_frontend OBJECT ${star_frontend_SOURCES} ${star_frontend_HEADERS})
|
317
source/frontend/StarActionBar.cpp
Normal file
317
source/frontend/StarActionBar.cpp
Normal file
|
@ -0,0 +1,317 @@
|
|||
#include "StarActionBar.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarItemTooltip.hpp"
|
||||
#include "StarUniverseClient.hpp"
|
||||
#include "StarItemGridWidget.hpp"
|
||||
#include "StarItemSlotWidget.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarPaneManager.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarPlayerInventory.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarItem.hpp"
|
||||
#include "StarMerchantInterface.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
ActionBar::ActionBar(MainInterfacePaneManager* paneManager, PlayerPtr player) {
|
||||
m_paneManager = paneManager;
|
||||
m_player = move(player);
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
m_config = assets->json("/interface/windowconfig/actionbar.config");
|
||||
|
||||
m_actionBarSelectOffset = jsonToVec2I(m_config.get("actionBarSelectOffset"));
|
||||
m_switchSounds = jsonToStringList(m_config.get("sounds").get("switch"));
|
||||
|
||||
GuiReader reader;
|
||||
|
||||
for (uint8_t i = 0; i < m_player->inventory()->customBarIndexes(); ++i) {
|
||||
reader.registerCallback(strf("customBar%sL", i + 1), bind(&ActionBar::customBarClick, this, i, true));
|
||||
reader.registerCallback(strf("customBar%sR", i + 1), bind(&ActionBar::customBarClick, this, i, false));
|
||||
|
||||
reader.registerCallback(strf("customBar%sL.right", i + 1), bind(&ActionBar::customBarClickRight, this, i, true));
|
||||
reader.registerCallback(strf("customBar%sR.right", i + 1), bind(&ActionBar::customBarClickRight, this, i, false));
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < EssentialItemCount; ++i)
|
||||
reader.registerCallback(strf("essentialBar%s", i + 1), bind(&ActionBar::essentialBarClick, this, i));
|
||||
|
||||
reader.registerCallback("pickupToActionBar", [=](Widget* widget) {
|
||||
auto button = as<ButtonWidget>(widget);
|
||||
Root::singleton().configuration()->setPath("inventory.pickupToActionBar", button->isChecked());
|
||||
});
|
||||
|
||||
reader.registerCallback("swapCustomBar", [this](Widget*) {
|
||||
swapCustomBar();
|
||||
});
|
||||
|
||||
reader.construct(m_config.get("paneLayout"), this);
|
||||
|
||||
for (uint8_t i = 0; i < m_player->inventory()->customBarIndexes(); ++i) {
|
||||
auto customBarLeft = fetchChild<ItemSlotWidget>(strf("customBar%sL", i + 1));
|
||||
auto customBarRight = fetchChild<ItemSlotWidget>(strf("customBar%sR", i + 1));
|
||||
auto customBarLeftOverlay = fetchChild<ImageWidget>(strf("customBar%sLOverlay", i + 1));
|
||||
auto customBarRightOverlay = fetchChild<ImageWidget>(strf("customBar%sROverlay", i + 1));
|
||||
|
||||
TextPositioning countPosition = {jsonToVec2F(m_config.get("countMidAnchor")), HorizontalAnchor::HMidAnchor};
|
||||
customBarLeft->setCountPosition(countPosition);
|
||||
customBarLeft->setCountFontMode(FontMode::Shadow);
|
||||
customBarRight->setCountPosition(countPosition);
|
||||
customBarRight->setCountFontMode(FontMode::Shadow);
|
||||
|
||||
m_customBarWidgets.append({customBarLeft, customBarRight, customBarLeftOverlay, customBarRightOverlay});
|
||||
}
|
||||
m_customSelectedWidget = fetchChild<ImageWidget>("customSelect");
|
||||
|
||||
for (uint8_t i = 0; i < EssentialItemCount; ++i)
|
||||
m_essentialBarWidgets.append(fetchChild<ItemSlotWidget>(strf("essentialBar%s", i + 1)));
|
||||
m_essentialSelectedWidget = fetchChild<ImageWidget>("essentialSelect");
|
||||
}
|
||||
|
||||
PanePtr ActionBar::createTooltip(Vec2I const& screenPosition) {
|
||||
ItemPtr item;
|
||||
auto tryItemWidget = [&](ItemSlotWidgetPtr const& isw) {
|
||||
if (isw->screenBoundRect().contains(screenPosition))
|
||||
item = isw->item();
|
||||
};
|
||||
|
||||
for (auto const& p : m_customBarWidgets) {
|
||||
tryItemWidget(p.left);
|
||||
tryItemWidget(p.right);
|
||||
}
|
||||
|
||||
for (auto const& w : m_essentialBarWidgets)
|
||||
tryItemWidget(w);
|
||||
|
||||
if (!item)
|
||||
return {};
|
||||
|
||||
return ItemTooltipBuilder::buildItemTooltip(item, m_player);
|
||||
}
|
||||
|
||||
bool ActionBar::sendEvent(InputEvent const& event) {
|
||||
if (Pane::sendEvent(event))
|
||||
return true;
|
||||
|
||||
auto inventory = m_player->inventory();
|
||||
|
||||
auto customBarIndexes = inventory->customBarIndexes();
|
||||
if (auto mouseWheel = event.ptr<MouseWheelEvent>()) {
|
||||
auto abl = inventory->selectedActionBarLocation();
|
||||
|
||||
int index = 0;
|
||||
if (!abl) {
|
||||
if (mouseWheel->mouseWheel == MouseWheel::Down)
|
||||
index = 0;
|
||||
else
|
||||
index = customBarIndexes + EssentialItemCount - 1;
|
||||
} else {
|
||||
if (auto cbi = abl.ptr<CustomBarIndex>()) {
|
||||
if (*cbi < customBarIndexes / 2)
|
||||
index = *cbi;
|
||||
else
|
||||
index = *cbi + EssentialItemCount;
|
||||
} else {
|
||||
index = customBarIndexes / 2 + (int)abl.get<EssentialItem>();
|
||||
}
|
||||
|
||||
if (mouseWheel->mouseWheel == MouseWheel::Down)
|
||||
index = pmod(index + 1, customBarIndexes + EssentialItemCount);
|
||||
else
|
||||
index = pmod(index - 1, customBarIndexes + EssentialItemCount);
|
||||
}
|
||||
|
||||
if (index < customBarIndexes / 2)
|
||||
abl = (CustomBarIndex)index;
|
||||
else if (index < customBarIndexes / 2 + EssentialItemCount)
|
||||
abl = (EssentialItem)(index - customBarIndexes / 2);
|
||||
else
|
||||
abl = (CustomBarIndex)(index - EssentialItemCount);
|
||||
|
||||
inventory->selectActionBarLocation(abl);
|
||||
context()->playAudio(RandomSource().randFrom(m_switchSounds));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.is<MouseMoveEvent>()) {
|
||||
m_customBarHover.reset();
|
||||
Vec2I screenPosition = *GuiContext::singleton().mousePosition(event);
|
||||
for (uint8_t i = 0; i < customBarIndexes; ++i) {
|
||||
if (m_customBarWidgets[i].left->screenBoundRect().contains(screenPosition))
|
||||
m_customBarHover = make_pair((CustomBarIndex)i, false);
|
||||
else if (m_customBarWidgets[i].right->screenBoundRect().contains(screenPosition))
|
||||
m_customBarHover = make_pair((CustomBarIndex)i, true);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto action : context()->actions(event)) {
|
||||
if (action >= InterfaceAction::InterfaceBar1 && action <= InterfaceAction::InterfaceBar6)
|
||||
inventory->selectActionBarLocation((CustomBarIndex)((int)action - (int)InterfaceAction::InterfaceBar1));
|
||||
|
||||
if (action >= InterfaceAction::EssentialBar1 && action <= InterfaceAction::EssentialBar4)
|
||||
inventory->selectActionBarLocation((EssentialItem)((int)action - (int)InterfaceAction::EssentialBar1));
|
||||
|
||||
if (action == InterfaceAction::InterfaceDeselectHands) {
|
||||
if (auto previousSelectedLocation = inventory->selectedActionBarLocation()) {
|
||||
m_emptyHandsPreviousActionBarLocation = inventory->selectedActionBarLocation();
|
||||
inventory->selectActionBarLocation({});
|
||||
} else {
|
||||
inventory->selectActionBarLocation(take(m_emptyHandsPreviousActionBarLocation));
|
||||
}
|
||||
}
|
||||
|
||||
if (action == InterfaceAction::InterfaceChangeBarGroup)
|
||||
swapCustomBar();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ActionBar::update() {
|
||||
auto inventory = m_player->inventory();
|
||||
auto abl = inventory->selectedActionBarLocation();
|
||||
if (abl.is<CustomBarIndex>()) {
|
||||
auto overlayLoc = m_customBarWidgets.at(abl.get<CustomBarIndex>()).left->position();
|
||||
m_customSelectedWidget->setPosition(overlayLoc + m_actionBarSelectOffset);
|
||||
m_customSelectedWidget->show();
|
||||
m_essentialSelectedWidget->hide();
|
||||
} else if (abl.is<EssentialItem>()) {
|
||||
auto overlayLoc = m_essentialBarWidgets.at((size_t)abl.get<EssentialItem>())->position();
|
||||
m_essentialSelectedWidget->setPosition(overlayLoc + m_actionBarSelectOffset);
|
||||
m_essentialSelectedWidget->show();
|
||||
m_customSelectedWidget->hide();
|
||||
} else {
|
||||
m_essentialSelectedWidget->hide();
|
||||
m_customSelectedWidget->hide();
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < EssentialItemCount; ++i)
|
||||
m_essentialBarWidgets[i]->setItem(inventory->essentialItem((EssentialItem)i));
|
||||
|
||||
for (uint8_t i = 0; i < inventory->customBarIndexes(); ++i) {
|
||||
// If there is no swap slot item being hovered over the custom bar, then
|
||||
// simply set the left and right item widgets in the custom bar to the
|
||||
// primary and secondary items. If the primary item is two handed, the
|
||||
// secondary item will be null and the primary hand item is drawn dimmed in
|
||||
// the right hand slot. If there IS a swap slot item being hovered over
|
||||
// this spot in the custom bar, things are more complex. Instead of
|
||||
// showing what is currently in the custom bar, a preview of what WOULD
|
||||
// happen when linking is shown, except both the left and right item
|
||||
// widgets are always shown with no count and always dimmed to indicate
|
||||
// that it is just a preview.
|
||||
|
||||
ItemPtr primaryItem;
|
||||
ItemPtr secondaryItem;
|
||||
|
||||
if (auto slot = inventory->customBarPrimarySlot(i))
|
||||
primaryItem = inventory->itemsAt(*slot);
|
||||
|
||||
if (auto slot = inventory->customBarSecondarySlot(i))
|
||||
secondaryItem = inventory->itemsAt(*slot);
|
||||
|
||||
bool primaryPreview = false;
|
||||
bool secondaryPreview = false;
|
||||
|
||||
ItemPtr swapSlotItem = inventory->swapSlotItem();
|
||||
if (swapSlotItem && m_customBarHover && m_customBarHover->first == i) {
|
||||
if (!m_customBarHover->second || swapSlotItem->twoHanded()) {
|
||||
if (!primaryItem && swapSlotItem == secondaryItem)
|
||||
secondaryItem = {};
|
||||
primaryItem = swapSlotItem;
|
||||
primaryPreview = true;
|
||||
} else {
|
||||
if (itemSafeTwoHanded(primaryItem))
|
||||
primaryItem = {};
|
||||
if (!secondaryItem && swapSlotItem == primaryItem)
|
||||
primaryItem = {};
|
||||
secondaryItem = swapSlotItem;
|
||||
secondaryPreview = true;
|
||||
}
|
||||
}
|
||||
|
||||
auto& widgets = m_customBarWidgets[i];
|
||||
widgets.left->setItem(primaryItem);
|
||||
if (primaryPreview) {
|
||||
widgets.left->showDurability(false);
|
||||
widgets.left->showCount(false);
|
||||
widgets.leftOverlay->show();
|
||||
} else {
|
||||
widgets.left->showDurability(true);
|
||||
widgets.left->showCount(true);
|
||||
widgets.leftOverlay->hide();
|
||||
}
|
||||
|
||||
if (itemSafeTwoHanded(primaryItem)) {
|
||||
widgets.right->setItem(primaryItem);
|
||||
widgets.right->showDurability(false);
|
||||
widgets.right->showCount(false);
|
||||
widgets.rightOverlay->show();
|
||||
} else {
|
||||
widgets.right->setItem(secondaryItem);
|
||||
if (secondaryPreview) {
|
||||
widgets.right->showDurability(false);
|
||||
widgets.right->showCount(false);
|
||||
widgets.rightOverlay->show();
|
||||
} else {
|
||||
widgets.right->showDurability(true);
|
||||
widgets.right->showCount(true);
|
||||
widgets.rightOverlay->hide();
|
||||
}
|
||||
}
|
||||
|
||||
widgets.left->setHighlightEnabled(!widgets.left->item() && swapSlotItem);
|
||||
widgets.right->setHighlightEnabled(!widgets.right->item() && swapSlotItem);
|
||||
}
|
||||
|
||||
fetchChild<ButtonWidget>("pickupToActionBar")->setChecked(Root::singleton().configuration()->getPath("inventory.pickupToActionBar").toBool());
|
||||
fetchChild<ButtonWidget>("swapCustomBar")->setChecked(m_player->inventory()->customBarGroup() != 0);
|
||||
}
|
||||
|
||||
Maybe<String> ActionBar::cursorOverride(Vec2I const&) {
|
||||
if (m_customBarHover && m_player->inventory()->swapSlotItem())
|
||||
return m_config.getString("linkCursor");
|
||||
return {};
|
||||
}
|
||||
|
||||
void ActionBar::customBarClick(uint8_t index, bool primary) {
|
||||
if (auto swapItem = m_player->inventory()->swapSlotItem()) {
|
||||
if (primary || itemSafeTwoHanded(swapItem))
|
||||
m_player->inventory()->setCustomBarPrimarySlot(index, InventorySlot(SwapSlot()));
|
||||
else
|
||||
m_player->inventory()->setCustomBarSecondarySlot(index, InventorySlot(SwapSlot()));
|
||||
|
||||
m_player->inventory()->clearSwap();
|
||||
|
||||
} else {
|
||||
m_player->inventory()->selectActionBarLocation(index);
|
||||
}
|
||||
}
|
||||
|
||||
void ActionBar::customBarClickRight(uint8_t index, bool primary) {
|
||||
if (m_paneManager->registeredPaneIsDisplayed(MainInterfacePanes::Inventory)) {
|
||||
auto inventory = m_player->inventory();
|
||||
auto primarySlot = inventory->customBarPrimarySlot(index);
|
||||
auto secondarySlot = inventory->customBarSecondarySlot(index);
|
||||
|
||||
if (primary || (primarySlot && itemSafeTwoHanded(inventory->itemsAt(*primarySlot))))
|
||||
inventory->setCustomBarPrimarySlot(index, {});
|
||||
else
|
||||
inventory->setCustomBarSecondarySlot(index, {});
|
||||
}
|
||||
}
|
||||
|
||||
void ActionBar::essentialBarClick(uint8_t index) {
|
||||
m_player->inventory()->selectActionBarLocation((EssentialItem)index);
|
||||
}
|
||||
|
||||
void ActionBar::swapCustomBar() {
|
||||
m_player->inventory()->setCustomBarGroup((m_player->inventory()->customBarGroup() + 1) % m_player->inventory()->customBarGroups());
|
||||
}
|
||||
|
||||
}
|
61
source/frontend/StarActionBar.hpp
Normal file
61
source/frontend/StarActionBar.hpp
Normal file
|
@ -0,0 +1,61 @@
|
|||
#ifndef STAR_ACTIONBAR_HPP
|
||||
#define STAR_ACTIONBAR_HPP
|
||||
|
||||
#include "StarInventoryTypes.hpp"
|
||||
#include "StarMainInterfaceTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(MainInterface);
|
||||
STAR_CLASS(UniverseClient);
|
||||
STAR_CLASS(Player);
|
||||
STAR_CLASS(Item);
|
||||
STAR_CLASS(ItemSlotWidget);
|
||||
STAR_CLASS(ImageWidget);
|
||||
|
||||
STAR_CLASS(ActionBar);
|
||||
|
||||
class ActionBar : public Pane {
|
||||
public:
|
||||
ActionBar(MainInterfacePaneManager* paneManager, PlayerPtr player);
|
||||
|
||||
PanePtr createTooltip(Vec2I const& screenPosition) override;
|
||||
bool sendEvent(InputEvent const& event) override;
|
||||
|
||||
void update() override;
|
||||
|
||||
Maybe<String> cursorOverride(Vec2I const& screenPosition) override;
|
||||
|
||||
private:
|
||||
struct CustomBarEntry {
|
||||
ItemSlotWidgetPtr left;
|
||||
ItemSlotWidgetPtr right;
|
||||
ImageWidgetPtr leftOverlay;
|
||||
ImageWidgetPtr rightOverlay;
|
||||
};
|
||||
|
||||
void customBarClick(uint8_t index, bool primary);
|
||||
void customBarClickRight(uint8_t index, bool primary);
|
||||
void essentialBarClick(uint8_t index);
|
||||
void swapCustomBar();
|
||||
|
||||
MainInterfacePaneManager* m_paneManager;
|
||||
PlayerPtr m_player;
|
||||
Json m_config;
|
||||
|
||||
Vec2I m_actionBarSelectOffset;
|
||||
StringList m_switchSounds;
|
||||
|
||||
List<CustomBarEntry> m_customBarWidgets;
|
||||
ImageWidgetPtr m_customSelectedWidget;
|
||||
|
||||
List<ItemSlotWidgetPtr> m_essentialBarWidgets;
|
||||
ImageWidgetPtr m_essentialSelectedWidget;
|
||||
|
||||
SelectedActionBarLocation m_emptyHandsPreviousActionBarLocation;
|
||||
Maybe<pair<CustomBarIndex, bool>> m_customBarHover;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
402
source/frontend/StarAiInterface.cpp
Normal file
402
source/frontend/StarAiInterface.cpp
Normal file
|
@ -0,0 +1,402 @@
|
|||
#include "StarAiInterface.hpp"
|
||||
#include "StarLexicalCast.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarJsonRpc.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarContainerEntity.hpp"
|
||||
#include "StarItemBag.hpp"
|
||||
#include "StarItemDatabase.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarPlayerCompanions.hpp"
|
||||
#include "StarPlayerInventory.hpp"
|
||||
#include "StarQuests.hpp"
|
||||
#include "StarQuestManager.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarUniverseClient.hpp"
|
||||
#include "StarPlayerStorage.hpp"
|
||||
#include "StarClientContext.hpp"
|
||||
#include "StarCanvasWidget.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarImageStretchWidget.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarListWidget.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarOrderedSet.hpp"
|
||||
#include "StarAiDatabase.hpp"
|
||||
#include "StarTabSet.hpp"
|
||||
#include "StarPlayerTech.hpp"
|
||||
#include "StarPlayerBlueprints.hpp"
|
||||
#include "StarItemSlotWidget.hpp"
|
||||
#include "StarStackWidget.hpp"
|
||||
#include "StarCinematic.hpp"
|
||||
#include "StarWorldClient.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
AiInterface::AiInterface(UniverseClientPtr client, CinematicPtr cinematic, MainInterfacePaneManager* paneManager) {
|
||||
m_client = client;
|
||||
m_cinematic = cinematic;
|
||||
m_paneManager = paneManager;
|
||||
|
||||
m_textLength = 0.0;
|
||||
m_textMaxLength = 0.0;
|
||||
|
||||
m_aiDatabase = Root::singleton().aiDatabase();
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
GuiReader reader;
|
||||
reader.registerCallback("close", bind(mem_fn(&AiInterface::dismiss), this));
|
||||
reader.registerCallback("missionItemList", bind(mem_fn(&AiInterface::selectMission), this));
|
||||
reader.registerCallback("startMission", bind(mem_fn(&AiInterface::startMission), this));
|
||||
reader.registerCallback("crewItemList", bind(mem_fn(&AiInterface::selectRecruit), this));
|
||||
reader.registerCallback("dismissRecruit", bind(mem_fn(&AiInterface::dismissRecruit), this));
|
||||
reader.registerCallback("showMissions", bind(mem_fn(&AiInterface::showMissions), this));
|
||||
reader.registerCallback("showCrew", bind(mem_fn(&AiInterface::showCrew), this));
|
||||
reader.registerCallback("goBack", bind(mem_fn(&AiInterface::goBack), this));
|
||||
|
||||
reader.construct(assets->json("/interface/ai/ai.config:guiConfig"), this);
|
||||
|
||||
m_mainStack = fetchChild<StackWidget>("mainStack");
|
||||
m_missionStack = findChild<StackWidget>("missionStack");
|
||||
m_crewStack = findChild<StackWidget>("crewStack");
|
||||
|
||||
m_breadcrumbLeftPadding = assets->json("/interface/ai/ai.config:breadcrumbLeftPadding").toInt();
|
||||
m_breadcrumbRightPadding = assets->json("/interface/ai/ai.config:breadcrumbRightPadding").toInt();
|
||||
m_homeBreadcrumbBackground = fetchChild<ImageStretchWidget>("homeBreadcrumbBg");
|
||||
m_pageBreadcrumbBackground = fetchChild<ImageStretchWidget>("pageBreadcrumbBg");
|
||||
m_itemBreadcrumbBackground = fetchChild<ImageStretchWidget>("itemBreadcrumbBg");
|
||||
|
||||
m_homeBreadcrumbWidget = fetchChild<LabelWidget>("homeBreadcrumb");
|
||||
m_pageBreadcrumbWidget = fetchChild<LabelWidget>("pageBreadcrumb");
|
||||
m_itemBreadcrumbWidget = fetchChild<LabelWidget>("itemBreadcrumb");
|
||||
|
||||
m_aiFaceCanvasWidget = findChild<CanvasWidget>("aiFaceCanvas");
|
||||
m_missionListWidget = findChild<ListWidget>("missionItemList");
|
||||
m_crewListWidget = findChild<ListWidget>("crewItemList");
|
||||
|
||||
m_showMissionsButton = findChild<ButtonWidget>("showMissions");
|
||||
m_showCrewButton = findChild<ButtonWidget>("showCrew");
|
||||
m_backButton = findChild<ButtonWidget>("backButton");
|
||||
|
||||
m_missionNameLabel = findChild<LabelWidget>("missionName");
|
||||
m_missionIcon = findChild<ImageWidget>("missionIcon");
|
||||
m_startMissionButton = findChild<ButtonWidget>("startMission");
|
||||
|
||||
m_recruitNameLabel = findChild<LabelWidget>("recruitName");
|
||||
m_recruitIcon = findChild<ImageWidget>("recruitIcon");
|
||||
m_dismissRecruitButton = findChild<ButtonWidget>("dismissRecruit");
|
||||
|
||||
m_species = m_client->mainPlayer()->species();
|
||||
m_staticAnimation = m_aiDatabase->staticAnimation(m_species);
|
||||
m_scanlineAnimation = m_aiDatabase->scanlineAnimation();
|
||||
|
||||
m_missionBreadcrumbText = assets->json("/interface/ai/ai.config:missionBreadcrumbText").toString();
|
||||
m_missionDeployText = assets->json("/interface/ai/ai.config:missionDeployText").toString();
|
||||
m_crewBreadcrumbText = assets->json("/interface/ai/ai.config:crewBreadcrumbText").toString();
|
||||
m_defaultRecruitName = assets->json("/interface/ai/ai.config:defaultRecruitName").toString();
|
||||
m_defaultRecruitDescription = assets->json("/interface/ai/ai.config:defaultRecruitDescription").toString();
|
||||
}
|
||||
|
||||
void AiInterface::update() {
|
||||
if (!m_client->playerOnOwnShip())
|
||||
dismiss();
|
||||
|
||||
Pane::update();
|
||||
|
||||
m_showCrewButton->setVisibility(m_currentPage == AiPages::StatusPage);
|
||||
m_showMissionsButton->setVisibility(m_currentPage == AiPages::StatusPage);
|
||||
m_backButton->setVisibility(m_currentPage != AiPages::StatusPage);
|
||||
|
||||
m_staticAnimation.update(WorldTimestep);
|
||||
m_scanlineAnimation.update(WorldTimestep);
|
||||
|
||||
if (m_currentSpeech) {
|
||||
m_textLength += m_currentSpeech->speedModifier * m_aiDatabase->charactersPerSecond() * WorldTimestep;
|
||||
m_currentTextWidget->setText(m_currentSpeech->text);
|
||||
m_currentTextWidget->setTextCharLimit(min(m_textMaxLength, floor(m_textLength)));
|
||||
|
||||
if (m_textLength < m_textMaxLength) {
|
||||
setFaceAnimation(m_currentSpeech->animation);
|
||||
if (!m_chatterSound || m_chatterSound->finished()) {
|
||||
auto assets = Root::singleton().assets();
|
||||
m_chatterSound = make_shared<AudioInstance>(*assets->audio(assets->json("/interface/ai/ai.config:chatterSound").toString()));
|
||||
m_chatterSound->setLoops(-1);
|
||||
GuiContext::singleton().playAudio(m_chatterSound);
|
||||
}
|
||||
} else {
|
||||
setFaceAnimation("idle");
|
||||
if (m_chatterSound)
|
||||
m_chatterSound->stop();
|
||||
}
|
||||
m_faceAnimation.second.update(WorldTimestep * m_currentSpeech->speedModifier);
|
||||
} else {
|
||||
setFaceAnimation("idle");
|
||||
m_faceAnimation.second.update(WorldTimestep);
|
||||
if (m_chatterSound)
|
||||
m_chatterSound->stop();
|
||||
}
|
||||
|
||||
// If the enabled missions list changes, update the mission list
|
||||
auto aiState = m_client->mainPlayer()->aiState();
|
||||
if (aiState.availableMissions.values() != m_availableMissions
|
||||
|| aiState.completedMissions.values() != m_completedMissions) {
|
||||
m_availableMissions = aiState.availableMissions.values();
|
||||
m_completedMissions = aiState.completedMissions.values();
|
||||
populateMissions();
|
||||
}
|
||||
|
||||
auto crew = m_client->mainPlayer()->companions()->getCompanions("crew");
|
||||
if (crew != m_crew) {
|
||||
m_crew = crew;
|
||||
populateCrew();
|
||||
}
|
||||
|
||||
m_aiFaceCanvasWidget->clear();
|
||||
m_aiFaceCanvasWidget->drawDrawable(m_faceAnimation.second.drawable(1.0f), Vec2F(0, 0));
|
||||
m_aiFaceCanvasWidget->drawDrawable(m_staticAnimation.drawable(1.0f), Vec2F(0, 0));
|
||||
m_aiFaceCanvasWidget->drawDrawable(m_scanlineAnimation.drawable(1.0f), Vec2F(0, 0));
|
||||
}
|
||||
|
||||
void AiInterface::updateBreadcrumbs() {
|
||||
// Home breadcrumb
|
||||
auto width = m_homeBreadcrumbWidget->size()[0];
|
||||
width += m_breadcrumbLeftPadding + m_breadcrumbRightPadding;
|
||||
m_homeBreadcrumbBackground->setSize(Vec2I(width, m_homeBreadcrumbBackground->size()[1]));
|
||||
|
||||
// Middle breadcrumb
|
||||
if (m_currentPage != AiPages::StatusPage) {
|
||||
Vec2I pagePosition = m_homeBreadcrumbBackground->position() + Vec2I(width, 0);
|
||||
m_pageBreadcrumbWidget->setPosition(Vec2I(pagePosition[0] + m_breadcrumbLeftPadding, m_homeBreadcrumbWidget->position()[1]));
|
||||
m_pageBreadcrumbBackground->setPosition(pagePosition - Vec2I(m_breadcrumbRightPadding, 0));
|
||||
width = m_pageBreadcrumbWidget->size()[0] + (2 * m_breadcrumbRightPadding) + m_breadcrumbLeftPadding; // Add right padding twice because of the overlap to the left
|
||||
m_pageBreadcrumbBackground->setSize(Vec2I(width, m_pageBreadcrumbBackground->size()[1]));
|
||||
|
||||
// End breadcrumb
|
||||
if (m_currentPage == AiPages::MissionPage || m_currentPage == AiPages::CrewPage) {
|
||||
Vec2I itemPosition = m_pageBreadcrumbBackground->position() + Vec2I(width, 0);
|
||||
m_itemBreadcrumbWidget->setPosition(Vec2I(itemPosition[0] + m_breadcrumbLeftPadding, m_homeBreadcrumbWidget->position()[1]));
|
||||
m_itemBreadcrumbBackground->setPosition(itemPosition - Vec2I(m_breadcrumbRightPadding, 0));
|
||||
width = m_itemBreadcrumbWidget->size()[0] + (2 * m_breadcrumbRightPadding) + m_breadcrumbLeftPadding;
|
||||
m_itemBreadcrumbBackground->setSize(Vec2I(width, m_itemBreadcrumbBackground->size()[1]));
|
||||
}
|
||||
}
|
||||
|
||||
// Visibility
|
||||
m_pageBreadcrumbBackground->setVisibility(m_currentPage != AiPages::StatusPage);
|
||||
m_pageBreadcrumbWidget->setVisibility(m_currentPage != AiPages::StatusPage);
|
||||
m_itemBreadcrumbBackground->setVisibility(m_currentPage == AiPages::MissionPage || m_currentPage == AiPages::CrewPage);
|
||||
m_itemBreadcrumbWidget->setVisibility(m_currentPage == AiPages::MissionPage || m_currentPage == AiPages::CrewPage);
|
||||
}
|
||||
|
||||
void AiInterface::displayed() {
|
||||
if (!m_client->playerOnOwnShip())
|
||||
return;
|
||||
|
||||
Pane::displayed();
|
||||
|
||||
showStatus();
|
||||
}
|
||||
|
||||
void AiInterface::dismissed() {
|
||||
if (m_chatterSound)
|
||||
m_chatterSound->stop();
|
||||
|
||||
m_selectedMission = {};
|
||||
m_selectedRecruit = {};
|
||||
|
||||
Pane::dismissed();
|
||||
}
|
||||
|
||||
void AiInterface::setSourceEntityId(EntityId sourceEntityId) {
|
||||
m_sourceEntityId = sourceEntityId;
|
||||
}
|
||||
|
||||
void AiInterface::showStatus() {
|
||||
m_currentPage = AiPages::StatusPage;
|
||||
setCurrentSpeech("shipStatusText", m_aiDatabase->shipStatus(m_client->clientContext()->shipUpgrades().shipLevel));
|
||||
m_mainStack->showPage(0);
|
||||
|
||||
updateBreadcrumbs();
|
||||
}
|
||||
|
||||
void AiInterface::showMissions() {
|
||||
m_currentPage = AiPages::MissionList;
|
||||
m_currentSpeech = {};
|
||||
m_pageBreadcrumbWidget->setText(m_missionBreadcrumbText);
|
||||
|
||||
populateMissions();
|
||||
|
||||
m_missionListWidget->clearSelected();
|
||||
m_mainStack->showPage(1);
|
||||
m_missionStack->showPage(0);
|
||||
if (m_availableMissions.empty() && m_completedMissions.empty()) {
|
||||
m_missionStack->showPage(0);
|
||||
setCurrentSpeech("noMissionsText", m_aiDatabase->noMissionsSpeech());
|
||||
} else {
|
||||
m_missionStack->showPage(1);
|
||||
}
|
||||
|
||||
updateBreadcrumbs();
|
||||
}
|
||||
|
||||
void AiInterface::selectMission() {
|
||||
size_t selectedItem = m_missionListWidget->selectedItem();
|
||||
|
||||
Maybe<String> mission = {};
|
||||
if (selectedItem < m_availableMissions.size())
|
||||
mission = m_availableMissions.at(selectedItem);
|
||||
else if (selectedItem < m_availableMissions.size() + m_completedMissions.size())
|
||||
mission = m_completedMissions.at(selectedItem - m_availableMissions.size());
|
||||
|
||||
m_selectedMission = mission;
|
||||
|
||||
if (m_selectedMission) {
|
||||
m_currentPage = AiPages::MissionPage;
|
||||
|
||||
auto mission = m_aiDatabase->mission(*m_selectedMission);
|
||||
auto missionText = mission.speciesText.value(m_species, mission.speciesText.value("default"));
|
||||
setCurrentSpeech("missionText", missionText.selectSpeech);
|
||||
m_missionNameLabel->setText(missionText.buttonText);
|
||||
m_missionIcon->setImage(mission.icon);
|
||||
|
||||
m_itemBreadcrumbWidget->setText(m_missionDeployText);
|
||||
|
||||
m_missionStack->showPage(2);
|
||||
|
||||
updateBreadcrumbs();
|
||||
}
|
||||
}
|
||||
|
||||
void AiInterface::showCrew() {
|
||||
m_currentPage = AiPages::CrewList;
|
||||
m_currentSpeech = {};
|
||||
m_pageBreadcrumbWidget->setText(m_crewBreadcrumbText);
|
||||
|
||||
populateMissions();
|
||||
|
||||
m_crewListWidget->clearSelected();
|
||||
m_mainStack->showPage(2);
|
||||
if (m_crew.empty()) {
|
||||
m_crewStack->showPage(0);
|
||||
setCurrentSpeech("noCrewText", m_aiDatabase->noCrewSpeech());
|
||||
} else {
|
||||
m_crewStack->showPage(1);
|
||||
}
|
||||
|
||||
updateBreadcrumbs();
|
||||
}
|
||||
|
||||
void AiInterface::selectRecruit() {
|
||||
CompanionPtr recruit = {};
|
||||
size_t selectedItem = m_crewListWidget->selectedItem();
|
||||
if (selectedItem < m_crew.size())
|
||||
recruit = m_crew.at(selectedItem);
|
||||
|
||||
m_selectedRecruit = recruit;
|
||||
|
||||
if (m_selectedRecruit) {
|
||||
m_currentPage = AiPages::CrewPage;
|
||||
|
||||
auto speech = m_selectedRecruit->description().value(m_defaultRecruitDescription);
|
||||
setCurrentSpeech("recruitText", {m_aiDatabase->defaultAnimation(), speech, 1.0f});
|
||||
m_recruitNameLabel->setText(m_selectedRecruit->name().value(m_defaultRecruitName));
|
||||
m_recruitIcon->setDrawables(m_selectedRecruit->portrait());
|
||||
|
||||
m_itemBreadcrumbWidget->setText(recruit->name().value(m_defaultRecruitName));
|
||||
|
||||
m_crewStack->showPage(2);
|
||||
|
||||
updateBreadcrumbs();
|
||||
}
|
||||
}
|
||||
|
||||
void AiInterface::populateMissions() {
|
||||
m_missionListWidget->clear();
|
||||
for (auto const& missionName : m_availableMissions) {
|
||||
auto widget = m_missionListWidget->addItem();
|
||||
auto label = widget->fetchChild<LabelWidget>("itemName");
|
||||
auto icon = widget->fetchChild<ImageWidget>("itemIcon");
|
||||
|
||||
auto const& mission = m_aiDatabase->mission(missionName);
|
||||
icon->setImage(mission.icon);
|
||||
auto const& missionText = mission.speciesText.value(m_species, mission.speciesText.value("default"));
|
||||
label->setText(missionText.buttonText);
|
||||
}
|
||||
for (auto const& missionName : m_completedMissions) {
|
||||
auto widget = m_missionListWidget->addItem();
|
||||
auto label = widget->fetchChild<LabelWidget>("itemName");
|
||||
auto icon = widget->fetchChild<ImageWidget>("itemIcon");
|
||||
|
||||
auto const& mission = m_aiDatabase->mission(missionName);
|
||||
icon->setImage(mission.icon);
|
||||
auto const& missionText = mission.speciesText.value(m_species, mission.speciesText.value("default"));
|
||||
label->setText(missionText.repeatButtonText);
|
||||
}
|
||||
m_missionListWidget->setSelected(NPos);
|
||||
}
|
||||
|
||||
void AiInterface::startMission() {
|
||||
if (m_selectedMission) {
|
||||
auto const& mission = m_aiDatabase->mission(*m_selectedMission);
|
||||
m_client->warpPlayer(
|
||||
WarpToWorld{InstanceWorldId(mission.missionUniqueWorld, m_client->teamUuid()), {}},
|
||||
true,
|
||||
mission.warpAnimation.value("default"),
|
||||
mission.warpDeploy.value(false));
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
void AiInterface::populateCrew() {
|
||||
m_crewListWidget->clear();
|
||||
for (auto const& recruit : m_crew) {
|
||||
auto widget = m_crewListWidget->addItem();
|
||||
auto label = widget->fetchChild<LabelWidget>("itemName");
|
||||
auto icon = widget->fetchChild<ImageWidget>("itemIcon");
|
||||
|
||||
icon->setDrawables(recruit->portrait());
|
||||
label->setText(recruit->name().value(m_defaultRecruitName));
|
||||
}
|
||||
m_crewListWidget->setSelected(NPos);
|
||||
}
|
||||
|
||||
void AiInterface::dismissRecruit() {
|
||||
if (!m_selectedRecruit)
|
||||
return;
|
||||
|
||||
Uuid podUuid = m_selectedRecruit->podUuid();
|
||||
m_client->mainPlayer()->companions()->dismissCompanion("crew", podUuid);
|
||||
|
||||
m_crewStack->showPage(1);
|
||||
}
|
||||
|
||||
void AiInterface::goBack() {
|
||||
if (m_currentPage == AiPages::MissionPage)
|
||||
showMissions();
|
||||
else if (m_currentPage == AiPages::CrewPage)
|
||||
showCrew();
|
||||
else if (m_currentPage == AiPages::MissionList || m_currentPage == AiPages::CrewList)
|
||||
showStatus();
|
||||
|
||||
updateBreadcrumbs();
|
||||
}
|
||||
|
||||
void AiInterface::setFaceAnimation(String const& name) {
|
||||
if (m_faceAnimation.first != name)
|
||||
m_faceAnimation = {name, m_aiDatabase->animation(m_species, name)};
|
||||
}
|
||||
|
||||
void AiInterface::setCurrentSpeech(String const& textWidget, AiSpeech speech) {;
|
||||
m_currentSpeech = move(speech);
|
||||
m_textLength = 0.0;
|
||||
m_textMaxLength = Text::stripEscapeCodes(m_currentSpeech->text).size();
|
||||
m_currentTextWidget = findChild<LabelWidget>(textWidget);
|
||||
m_currentTextWidget->setText("");
|
||||
}
|
||||
|
||||
void AiInterface::giveBlueprint(String const& blueprintName) {
|
||||
m_client->mainPlayer()->addBlueprint(ItemDescriptor(blueprintName));
|
||||
}
|
||||
|
||||
}
|
145
source/frontend/StarAiInterface.hpp
Normal file
145
source/frontend/StarAiInterface.hpp
Normal file
|
@ -0,0 +1,145 @@
|
|||
#ifndef STAR_AI_INTERFACE_HPP
|
||||
#define STAR_AI_INTERFACE_HPP
|
||||
|
||||
#include "StarAiTypes.hpp"
|
||||
#include "StarGameTimers.hpp"
|
||||
#include "StarWarping.hpp"
|
||||
#include "StarAnimation.hpp"
|
||||
#include "StarItemDescriptor.hpp"
|
||||
#include "StarPane.hpp"
|
||||
#include "StarMainInterfaceTypes.hpp"
|
||||
#include "StarTechDatabase.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(UniverseClient);
|
||||
STAR_CLASS(AiDatabase);
|
||||
STAR_CLASS(Cinematic);
|
||||
STAR_CLASS(LabelWidget);
|
||||
STAR_CLASS(ImageWidget);
|
||||
STAR_CLASS(ImageStretchWidget);
|
||||
STAR_CLASS(CanvasWidget);
|
||||
STAR_CLASS(ListWidget);
|
||||
STAR_CLASS(ButtonWidget);
|
||||
STAR_CLASS(QuestManager);
|
||||
STAR_CLASS(StackWidget);
|
||||
STAR_CLASS(TabSetWidget);
|
||||
STAR_CLASS(Companion);
|
||||
|
||||
STAR_CLASS(AiInterface);
|
||||
|
||||
STAR_EXCEPTION(AiInterfaceException, StarException);
|
||||
|
||||
class AiInterface : public Pane {
|
||||
public:
|
||||
AiInterface(UniverseClientPtr client, CinematicPtr cinematic, MainInterfacePaneManager* paneManager);
|
||||
|
||||
void update() override;
|
||||
|
||||
void displayed() override;
|
||||
void dismissed() override;
|
||||
|
||||
void setSourceEntityId(EntityId sourceEntityId);
|
||||
|
||||
private:
|
||||
enum class AiPages : uint8_t {
|
||||
StatusPage,
|
||||
MissionList,
|
||||
MissionPage,
|
||||
CrewList,
|
||||
CrewPage
|
||||
};
|
||||
|
||||
void updateBreadcrumbs();
|
||||
void showStatus();
|
||||
|
||||
void populateMissions();
|
||||
void showMissions();
|
||||
void selectMission();
|
||||
void startMission();
|
||||
|
||||
void populateCrew();
|
||||
void showCrew();
|
||||
void selectRecruit();
|
||||
void dismissRecruit();
|
||||
|
||||
void goBack();
|
||||
|
||||
void setFaceAnimation(String const& name);
|
||||
void setCurrentSpeech(String const& textWidget, AiSpeech speech);
|
||||
|
||||
void giveBlueprint(String const& blueprintName);
|
||||
|
||||
AiPages m_currentPage;
|
||||
|
||||
UniverseClientPtr m_client;
|
||||
CinematicPtr m_cinematic;
|
||||
MainInterfacePaneManager* m_paneManager;
|
||||
QuestManagerPtr m_questManager;
|
||||
|
||||
EntityId m_sourceEntityId;
|
||||
|
||||
AiDatabaseConstPtr m_aiDatabase;
|
||||
|
||||
Animation m_staticAnimation;
|
||||
Animation m_scanlineAnimation;
|
||||
pair<String, Animation> m_faceAnimation;
|
||||
|
||||
AudioInstancePtr m_chatterSound;
|
||||
|
||||
StackWidgetPtr m_mainStack;
|
||||
StackWidgetPtr m_missionStack;
|
||||
StackWidgetPtr m_crewStack;
|
||||
|
||||
ButtonWidgetPtr m_showMissionsButton;
|
||||
ButtonWidgetPtr m_showCrewButton;
|
||||
ButtonWidgetPtr m_backButton;
|
||||
|
||||
int m_breadcrumbLeftPadding;
|
||||
int m_breadcrumbRightPadding;
|
||||
ImageStretchWidgetPtr m_homeBreadcrumbBackground;
|
||||
ImageStretchWidgetPtr m_pageBreadcrumbBackground;
|
||||
ImageStretchWidgetPtr m_itemBreadcrumbBackground;
|
||||
LabelWidgetPtr m_homeBreadcrumbWidget;
|
||||
LabelWidgetPtr m_pageBreadcrumbWidget;
|
||||
LabelWidgetPtr m_itemBreadcrumbWidget;
|
||||
|
||||
LabelWidgetPtr m_currentTextWidget;
|
||||
|
||||
CanvasWidgetPtr m_aiFaceCanvasWidget;
|
||||
LabelWidgetPtr m_shipStatusTextWidget;
|
||||
|
||||
ListWidgetPtr m_missionListWidget;
|
||||
LabelWidgetPtr m_missionNameLabel;
|
||||
ImageWidgetPtr m_missionIcon;
|
||||
|
||||
ListWidgetPtr m_crewListWidget;
|
||||
LabelWidgetPtr m_recruitNameLabel;
|
||||
ImageWidgetPtr m_recruitIcon;
|
||||
|
||||
String m_species;
|
||||
|
||||
String m_missionBreadcrumbText;
|
||||
String m_missionDeployText;
|
||||
String m_crewBreadcrumbText;
|
||||
String m_defaultRecruitName;
|
||||
String m_defaultRecruitDescription;
|
||||
|
||||
StringList m_availableMissions;
|
||||
StringList m_completedMissions;
|
||||
Maybe<String> m_selectedMission;
|
||||
|
||||
List<CompanionPtr> m_crew;
|
||||
CompanionPtr m_selectedRecruit;
|
||||
|
||||
Maybe<AiSpeech> m_currentSpeech;
|
||||
float m_textLength;
|
||||
float m_textMaxLength;
|
||||
|
||||
ButtonWidgetPtr m_startMissionButton;
|
||||
ButtonWidgetPtr m_dismissRecruitButton;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
81
source/frontend/StarBookmarkInterface.cpp
Normal file
81
source/frontend/StarBookmarkInterface.cpp
Normal file
|
@ -0,0 +1,81 @@
|
|||
#include "StarBookmarkInterface.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarTextBoxWidget.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
EditBookmarkDialog::EditBookmarkDialog(PlayerUniverseMapPtr playerUniverseMap) {
|
||||
m_playerUniverseMap = playerUniverseMap;
|
||||
|
||||
GuiReader reader;
|
||||
auto assets = Root::singleton().assets();
|
||||
reader.registerCallback("ok", [this](Widget*) { ok(); });
|
||||
reader.registerCallback("remove", [this](Widget*) { remove(); });
|
||||
reader.registerCallback("close", [this](Widget*) { close(); });
|
||||
reader.registerCallback("name", [](Widget*) {});
|
||||
reader.construct(assets->json("/interface/windowconfig/editbookmark.config:paneLayout"), this);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
void EditBookmarkDialog::setBookmark(TeleportBookmark bookmark) {
|
||||
m_bookmark = bookmark;
|
||||
|
||||
m_isNew = true;
|
||||
for (auto& existing : m_playerUniverseMap->teleportBookmarks()) {
|
||||
if (existing == bookmark) {
|
||||
m_bookmark.bookmarkName = existing.bookmarkName;
|
||||
m_isNew = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EditBookmarkDialog::show() {
|
||||
Pane::show();
|
||||
|
||||
if (m_isNew) {
|
||||
fetchChild<LabelWidget>("lblTitle")->setText("NEW BOOKMARK");
|
||||
fetchChild<ButtonWidget>("remove")->hide();
|
||||
} else {
|
||||
fetchChild<LabelWidget>("lblTitle")->setText("EDIT BOOKMARK");
|
||||
fetchChild<ButtonWidget>("remove")->show();
|
||||
}
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
fetchChild<ImageWidget>("imgIcon")->setImage(strf("/interface/bookmarks/icons/%s.png", m_bookmark.icon));
|
||||
|
||||
fetchChild<LabelWidget>("lblPlanetName")->setText(m_bookmark.targetName);
|
||||
fetchChild<TextBoxWidget>("name")->setText(m_bookmark.bookmarkName, false);
|
||||
fetchChild<TextBoxWidget>("name")->focus();
|
||||
}
|
||||
|
||||
void EditBookmarkDialog::ok() {
|
||||
m_bookmark.bookmarkName = fetchChild<TextBoxWidget>("name")->getText();
|
||||
if (m_bookmark.bookmarkName.empty())
|
||||
m_bookmark.bookmarkName = m_bookmark.targetName;
|
||||
if (!m_isNew)
|
||||
m_playerUniverseMap->removeTeleportBookmark(m_bookmark);
|
||||
m_playerUniverseMap->addTeleportBookmark(m_bookmark);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
void EditBookmarkDialog::remove() {
|
||||
m_playerUniverseMap->removeTeleportBookmark(m_bookmark);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
void EditBookmarkDialog::close() {
|
||||
dismiss();
|
||||
}
|
||||
|
||||
void setupBookmarkEntry(WidgetPtr const& entry, TeleportBookmark const& bookmark) {
|
||||
entry->fetchChild<LabelWidget>("name")->setText(bookmark.bookmarkName);
|
||||
entry->fetchChild<LabelWidget>("planetName")->setText(bookmark.targetName);
|
||||
entry->fetchChild<ImageWidget>("icon")->setImage(strf("/interface/bookmarks/icons/%s.png", bookmark.icon));
|
||||
}
|
||||
|
||||
}
|
33
source/frontend/StarBookmarkInterface.hpp
Normal file
33
source/frontend/StarBookmarkInterface.hpp
Normal file
|
@ -0,0 +1,33 @@
|
|||
#ifndef STAR_BOOKMARK_INTERFACE_HPP
|
||||
#define STAR_BOOKMARK_INTERFACE_HPP
|
||||
|
||||
#include "StarPlayerUniverseMap.hpp"
|
||||
#include "StarPane.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(EditBookmarkDialog);
|
||||
|
||||
class EditBookmarkDialog : public Pane {
|
||||
public:
|
||||
EditBookmarkDialog(PlayerUniverseMapPtr playerUniverseMap);
|
||||
|
||||
virtual void show() override;
|
||||
|
||||
void setBookmark(TeleportBookmark bookmark);
|
||||
|
||||
void ok();
|
||||
void remove();
|
||||
void close();
|
||||
|
||||
private:
|
||||
PlayerUniverseMapPtr m_playerUniverseMap;
|
||||
TeleportBookmark m_bookmark;
|
||||
|
||||
bool m_isNew;
|
||||
};
|
||||
|
||||
void setupBookmarkEntry(WidgetPtr const& entry, TeleportBookmark const& bookmark);
|
||||
}
|
||||
|
||||
#endif
|
448
source/frontend/StarCharCreation.cpp
Normal file
448
source/frontend/StarCharCreation.cpp
Normal file
|
@ -0,0 +1,448 @@
|
|||
#include "StarCharCreation.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarNameGenerator.hpp"
|
||||
#include "StarLogging.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarWorldClient.hpp"
|
||||
#include "StarSpeciesDatabase.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarPortraitWidget.hpp"
|
||||
#include "StarTextBoxWidget.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarArmors.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarPlayerFactory.hpp"
|
||||
#include "StarItemDatabase.hpp"
|
||||
#include "StarPlayerInventory.hpp"
|
||||
#include "StarPlayerLog.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
CharCreationPane::CharCreationPane(std::function<void(PlayerPtr)> requestCloseFunc) {
|
||||
auto& root = Root::singleton();
|
||||
|
||||
m_speciesList = jsonToStringList(root.assets()->json("/interface/windowconfig/charcreation.config:speciesOrdering"));
|
||||
|
||||
GuiReader guiReader;
|
||||
guiReader.registerCallback("cancel", [=](Widget*) { requestCloseFunc({}); });
|
||||
guiReader.registerCallback("saveChar", [=](Widget*) {
|
||||
if (fetchChild<ButtonWidget>("btnSkipIntro")->isChecked())
|
||||
m_previewPlayer->log()->setIntroComplete(true);
|
||||
requestCloseFunc(m_previewPlayer);
|
||||
createPlayer();
|
||||
randomize();
|
||||
randomizeName();
|
||||
});
|
||||
|
||||
guiReader.registerCallback("mainSkinColor.up", [=](Widget*) {
|
||||
m_bodyColor++;
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("mainSkinColor.down", [=](Widget*) {
|
||||
m_bodyColor--;
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("alty.up", [=](Widget*) {
|
||||
m_alty++;
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("alty.down", [=](Widget*) {
|
||||
m_alty--;
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("hairStyle.up", [=](Widget*) {
|
||||
m_hairChoice++;
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("hairStyle.down", [=](Widget*) {
|
||||
m_hairChoice--;
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("shirt.up", [=](Widget*) {
|
||||
m_shirtChoice++;
|
||||
fetchChild<ButtonWidget>("btnToggleClothing")->setChecked(true);
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("shirt.down", [=](Widget*) {
|
||||
m_shirtChoice--;
|
||||
fetchChild<ButtonWidget>("btnToggleClothing")->setChecked(true);
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("pants.up", [=](Widget*) {
|
||||
m_pantsChoice++;
|
||||
fetchChild<ButtonWidget>("btnToggleClothing")->setChecked(true);
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("pants.down", [=](Widget*) {
|
||||
m_pantsChoice--;
|
||||
fetchChild<ButtonWidget>("btnToggleClothing")->setChecked(true);
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("heady.up", [=](Widget*) {
|
||||
m_heady++;
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("heady.down", [=](Widget*) {
|
||||
m_heady--;
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("shirtColor.up", [=](Widget*) {
|
||||
m_shirtColor++;
|
||||
fetchChild<ButtonWidget>("btnToggleClothing")->setChecked(true);
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("shirtColor.down", [=](Widget*) {
|
||||
m_shirtColor--;
|
||||
fetchChild<ButtonWidget>("btnToggleClothing")->setChecked(true);
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("pantsColor.up", [=](Widget*) {
|
||||
m_pantsColor++;
|
||||
fetchChild<ButtonWidget>("btnToggleClothing")->setChecked(true);
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("pantsColor.down", [=](Widget*) {
|
||||
m_pantsColor--;
|
||||
fetchChild<ButtonWidget>("btnToggleClothing")->setChecked(true);
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("personality.up", [=](Widget*) {
|
||||
m_personality++;
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("personality.down", [=](Widget*) {
|
||||
m_personality--;
|
||||
changed();
|
||||
});
|
||||
guiReader.registerCallback("toggleClothing", [=](Widget*) {
|
||||
changed();
|
||||
});
|
||||
|
||||
guiReader.registerCallback("randomName", [=](Widget*) { randomizeName(); });
|
||||
guiReader.registerCallback("randomize", [=](Widget*) { randomize(); });
|
||||
|
||||
guiReader.registerCallback("name", [=](Widget* object) { nameBoxCallback(object); });
|
||||
|
||||
guiReader.registerCallback("species", [=](Widget* button) {
|
||||
size_t speciesChoice = convert<ButtonWidget>(button)->buttonGroupId();
|
||||
if (speciesChoice < m_speciesList.size() && speciesChoice != m_speciesChoice) {
|
||||
m_speciesChoice = speciesChoice;
|
||||
randomize();
|
||||
randomizeName();
|
||||
}
|
||||
});
|
||||
guiReader.registerCallback("gender", [=](Widget* button) {
|
||||
m_genderChoice = convert<ButtonWidget>(button)->buttonGroupId();
|
||||
changed();
|
||||
});
|
||||
|
||||
guiReader.registerCallback("mode", [=](Widget* button) {
|
||||
m_modeChoice = convert<ButtonWidget>(button)->buttonGroupId();
|
||||
changed();
|
||||
});
|
||||
|
||||
guiReader.construct(root.assets()->json("/interface/windowconfig/charcreation.config:paneLayout"), this);
|
||||
|
||||
createPlayer();
|
||||
|
||||
RandomSource random;
|
||||
m_speciesChoice = random.randu32() % m_speciesList.size();
|
||||
m_genderChoice = random.randu32();
|
||||
m_modeChoice = 1;
|
||||
randomize();
|
||||
randomizeName();
|
||||
}
|
||||
|
||||
void CharCreationPane::createPlayer() {
|
||||
m_previewPlayer = Root::singleton().playerFactory()->create();
|
||||
try {
|
||||
auto portrait = fetchChild<PortraitWidget>("charPreview");
|
||||
if ((bool)portrait) {
|
||||
portrait->setEntity(m_previewPlayer);
|
||||
} else {
|
||||
throw CharCreationException("The charPreview portrait has the wrong type.");
|
||||
}
|
||||
} catch (CharCreationException const& e) {
|
||||
Logger::error("Character Preview portrait was not found in the json specification. %s", outputException(e, false));
|
||||
}
|
||||
}
|
||||
|
||||
void CharCreationPane::randomize() {
|
||||
RandomSource random;
|
||||
m_bodyColor = random.randu32();
|
||||
m_hairChoice = random.randu32();
|
||||
m_alty = random.randu32();
|
||||
m_heady = random.randu32();
|
||||
m_shirtChoice = random.randu32();
|
||||
m_shirtColor = random.randu32();
|
||||
m_pantsChoice = random.randu32();
|
||||
m_pantsColor = random.randu32();
|
||||
m_personality = random.randu32();
|
||||
changed();
|
||||
}
|
||||
|
||||
void CharCreationPane::tick() {
|
||||
Pane::tick();
|
||||
if (!active())
|
||||
return;
|
||||
if (!m_previewPlayer)
|
||||
return;
|
||||
m_previewPlayer->animatePortrait();
|
||||
}
|
||||
|
||||
bool CharCreationPane::sendEvent(InputEvent const& event) {
|
||||
if (active() && m_previewPlayer) {
|
||||
if (event.is<KeyDownEvent>()) {
|
||||
auto actions = context()->actions(event);
|
||||
if (actions.contains(InterfaceAction::EmoteBlabbering))
|
||||
m_previewPlayer->addEmote(HumanoidEmote::Blabbering);
|
||||
if (actions.contains(InterfaceAction::EmoteShouting))
|
||||
m_previewPlayer->addEmote(HumanoidEmote::Shouting);
|
||||
if (actions.contains(InterfaceAction::EmoteHappy))
|
||||
m_previewPlayer->addEmote(HumanoidEmote::Happy);
|
||||
if (actions.contains(InterfaceAction::EmoteSad))
|
||||
m_previewPlayer->addEmote(HumanoidEmote::Sad);
|
||||
if (actions.contains(InterfaceAction::EmoteNeutral))
|
||||
m_previewPlayer->addEmote(HumanoidEmote::NEUTRAL);
|
||||
if (actions.contains(InterfaceAction::EmoteLaugh))
|
||||
m_previewPlayer->addEmote(HumanoidEmote::Laugh);
|
||||
if (actions.contains(InterfaceAction::EmoteAnnoyed))
|
||||
m_previewPlayer->addEmote(HumanoidEmote::Annoyed);
|
||||
if (actions.contains(InterfaceAction::EmoteOh))
|
||||
m_previewPlayer->addEmote(HumanoidEmote::Oh);
|
||||
if (actions.contains(InterfaceAction::EmoteOooh))
|
||||
m_previewPlayer->addEmote(HumanoidEmote::OOOH);
|
||||
if (actions.contains(InterfaceAction::EmoteBlink))
|
||||
m_previewPlayer->addEmote(HumanoidEmote::Blink);
|
||||
if (actions.contains(InterfaceAction::EmoteWink))
|
||||
m_previewPlayer->addEmote(HumanoidEmote::Wink);
|
||||
if (actions.contains(InterfaceAction::EmoteEat))
|
||||
m_previewPlayer->addEmote(HumanoidEmote::Eat);
|
||||
if (actions.contains(InterfaceAction::EmoteSleep))
|
||||
m_previewPlayer->addEmote(HumanoidEmote::Sleep);
|
||||
}
|
||||
}
|
||||
return Pane::sendEvent(event);
|
||||
}
|
||||
|
||||
void CharCreationPane::randomizeName() {
|
||||
auto species = Root::singleton().speciesDatabase()->species(m_speciesList[m_speciesChoice]);
|
||||
auto tb = fetchChild<TextBoxWidget>("name");
|
||||
auto genderOption = species->options().genderOptions.wrap(m_genderChoice);
|
||||
int limiter = 100;
|
||||
while (!tb->setText(Root::singleton().nameGenerator()->generateName(species->nameGen(genderOption.gender)))) {
|
||||
if (limiter == 0)
|
||||
break;
|
||||
limiter--;
|
||||
}
|
||||
changed();
|
||||
}
|
||||
|
||||
void CharCreationPane::changed() {
|
||||
auto& root = Root::singleton();
|
||||
|
||||
auto textBox = fetchChild<TextBoxWidget>("name");
|
||||
auto speciesDefinition = Root::singleton().speciesDatabase()->species(m_speciesList[m_speciesChoice]);
|
||||
auto species = speciesDefinition->options();
|
||||
auto genderOptions = species.genderOptions.wrap(m_genderChoice);
|
||||
int genderIdx = pmod<int64_t>(m_genderChoice, species.genderOptions.size());
|
||||
|
||||
auto labels = speciesDefinition->charGenTextLabels();
|
||||
|
||||
fetchChild<LabelWidget>("labelMainSkinColor")->setText(labels[0]);
|
||||
fetchChild<LabelWidget>("labelHairStyle")->setText(labels[1]);
|
||||
fetchChild<LabelWidget>("labelShirt")->setText(labels[2]);
|
||||
fetchChild<LabelWidget>("labelPants")->setText(labels[3]);
|
||||
if (!labels[4].empty()) {
|
||||
fetchChild<LabelWidget>("labelAlty")->setText(labels[4]);
|
||||
fetchChild<LabelWidget>("labelAlty")->show();
|
||||
fetchChild<Widget>("alty")->show();
|
||||
} else {
|
||||
fetchChild<LabelWidget>("labelAlty")->hide();
|
||||
fetchChild<Widget>("alty")->hide();
|
||||
}
|
||||
fetchChild<LabelWidget>("labelHeady")->setText(labels[5]);
|
||||
fetchChild<LabelWidget>("labelShirtColor")->setText(labels[6]);
|
||||
fetchChild<LabelWidget>("labelPantsColor")->setText(labels[7]);
|
||||
fetchChild<LabelWidget>("labelPortrait")->setText(labels[8]);
|
||||
fetchChild<LabelWidget>("labelPersonality")->setText(labels[9]);
|
||||
|
||||
fetchChild<ButtonWidget>(strf("species.%s", m_speciesChoice))->check();
|
||||
fetchChild<ButtonWidget>(strf("gender.%s", genderIdx))->check();
|
||||
auto modeButton = fetchChild<ButtonWidget>(strf("mode.%s", m_modeChoice));
|
||||
modeButton->check();
|
||||
setLabel("labelMode", modeButton->data().getString("description", "fail"));
|
||||
|
||||
// Update the gender images for the new species
|
||||
for (size_t i = 0; i < species.genderOptions.size(); i++)
|
||||
fetchChild<ButtonWidget>(strf("gender.%s", i))->setOverlayImage(species.genderOptions[i].image);
|
||||
|
||||
for (auto const& nameDefPair : root.speciesDatabase()->allSpecies()) {
|
||||
String name;
|
||||
SpeciesDefinitionPtr def;
|
||||
std::tie(name, def) = nameDefPair;
|
||||
// NOTE: Probably not hot enough to matter, but this contains and indexOf makes this loop
|
||||
// O(n^2). This is less than ideal.
|
||||
if (m_speciesList.contains(name)) {
|
||||
auto bw = fetchChild<ButtonWidget>(strf("species.%s", m_speciesList.indexOf(name)));
|
||||
if (bw)
|
||||
bw->setOverlayImage(def->options().genderOptions[genderIdx].characterImage);
|
||||
}
|
||||
}
|
||||
|
||||
auto portrait = fetchChild<PortraitWidget>("charPreview");
|
||||
if (fetchChild<ButtonWidget>("btnToggleClothing")->isChecked())
|
||||
portrait->setMode(PortraitMode::Full);
|
||||
else
|
||||
portrait->setMode(PortraitMode::FullNude);
|
||||
|
||||
auto gender = species.genderOptions.wrap(m_genderChoice);
|
||||
auto bodyColor = species.bodyColorDirectives.wrap(m_bodyColor);
|
||||
|
||||
String altColor;
|
||||
|
||||
if (species.altOptionAsUndyColor) {
|
||||
// undyColor
|
||||
altColor = species.undyColorDirectives.wrap(m_alty);
|
||||
}
|
||||
|
||||
auto hair = gender.hairOptions.wrap(m_hairChoice);
|
||||
String hairColor = bodyColor;
|
||||
if (species.headOptionAsHairColor && species.altOptionAsHairColor) {
|
||||
hairColor = species.hairColorDirectives.wrap(m_heady);
|
||||
hairColor += species.undyColorDirectives.wrap(m_alty);
|
||||
} else if (species.headOptionAsHairColor) {
|
||||
hairColor = species.hairColorDirectives.wrap(m_heady);
|
||||
}
|
||||
|
||||
if (species.hairColorAsBodySubColor)
|
||||
bodyColor += hairColor;
|
||||
|
||||
String facialHair;
|
||||
String facialHairGroup;
|
||||
String facialHairDirective;
|
||||
if (species.headOptionAsFacialhair) {
|
||||
facialHair = gender.facialHairOptions.wrap(m_heady);
|
||||
facialHairGroup = gender.facialHairGroup;
|
||||
facialHairDirective = hairColor;
|
||||
}
|
||||
|
||||
String facialMask;
|
||||
String facialMaskGroup;
|
||||
String facialMaskDirective;
|
||||
if (species.altOptionAsFacialMask) {
|
||||
facialMask = gender.facialMaskOptions.wrap(m_alty);
|
||||
facialMaskGroup = gender.facialMaskGroup;
|
||||
facialMaskDirective = "";
|
||||
}
|
||||
if (species.bodyColorAsFacialMaskSubColor)
|
||||
facialMaskDirective += bodyColor;
|
||||
if (species.altColorAsFacialMaskSubColor)
|
||||
facialMaskDirective += altColor;
|
||||
|
||||
auto shirt = gender.shirtOptions.wrap(m_shirtChoice);
|
||||
auto pants = gender.pantsOptions.wrap(m_pantsChoice);
|
||||
|
||||
m_previewPlayer->setModeType((PlayerMode)m_modeChoice);
|
||||
|
||||
m_previewPlayer->setName(textBox->getText());
|
||||
|
||||
m_previewPlayer->setSpecies(species.species);
|
||||
m_previewPlayer->setBodyDirectives(bodyColor + altColor);
|
||||
|
||||
m_previewPlayer->setGender(GenderNames.getLeft(gender.name));
|
||||
|
||||
m_previewPlayer->setHairType(gender.hairGroup, hair);
|
||||
m_previewPlayer->setHairDirectives(hairColor);
|
||||
|
||||
m_previewPlayer->setEmoteDirectives(bodyColor + altColor);
|
||||
|
||||
m_previewPlayer->setFacialHair(facialHairGroup, facialHair, facialHairDirective);
|
||||
|
||||
m_previewPlayer->setFacialMask(facialMaskGroup, facialMask, facialMaskDirective);
|
||||
|
||||
auto personality = speciesDefinition->personalities().wrap(m_personality);
|
||||
m_previewPlayer->setPersonality(personality);
|
||||
|
||||
setShirt(shirt, m_shirtColor);
|
||||
setPants(pants, m_pantsColor);
|
||||
|
||||
m_previewPlayer->finalizeCreation();
|
||||
}
|
||||
|
||||
void CharCreationPane::setShirt(String const& shirt, size_t colorIndex) {
|
||||
auto& root = Root::singleton();
|
||||
|
||||
while (m_previewPlayer->inventory()->chestArmor())
|
||||
m_previewPlayer->inventory()->consumeSlot(InventorySlot(EquipmentSlot::Chest));
|
||||
if (!shirt.empty()) {
|
||||
m_previewPlayer->inventory()->addItems(
|
||||
root.itemDatabase()->item({shirt, 1, JsonObject{{"colorIndex", colorIndex}}}));
|
||||
}
|
||||
m_previewPlayer->refreshEquipment();
|
||||
}
|
||||
|
||||
void CharCreationPane::setPants(String const& pants, size_t colorIndex) {
|
||||
auto& root = Root::singleton();
|
||||
|
||||
while (m_previewPlayer->inventory()->legsArmor())
|
||||
m_previewPlayer->inventory()->consumeSlot(InventorySlot(EquipmentSlot::Legs));
|
||||
if (!pants.empty()) {
|
||||
m_previewPlayer->inventory()->addItems(
|
||||
root.itemDatabase()->item({pants, 1, JsonObject{{"colorIndex", colorIndex}}}));
|
||||
}
|
||||
m_previewPlayer->refreshEquipment();
|
||||
}
|
||||
|
||||
void CharCreationPane::nameBoxCallback(Widget* object) {
|
||||
if (as<TextBoxWidget>(object))
|
||||
changed();
|
||||
else
|
||||
throw GuiException("Invalid object type, expected TextBoxWidget.");
|
||||
}
|
||||
|
||||
PanePtr CharCreationPane::createTooltip(Vec2I const& screenPosition) {
|
||||
// what's under my cursor
|
||||
if (WidgetPtr child = getChildAt(screenPosition)) {
|
||||
// is it a species button ?
|
||||
if (child->parent()->name() == "species") {
|
||||
// which species is it ?
|
||||
size_t speciesIndex = convert<ButtonWidget>(child)->buttonGroupId();
|
||||
|
||||
// no tooltips for unassigned button indices
|
||||
if (speciesIndex >= m_speciesList.size())
|
||||
return {};
|
||||
|
||||
String speciesName = m_speciesList[speciesIndex];
|
||||
Star::SpeciesDefinitionPtr speciesDefinition = Root::singleton().speciesDatabase()->species(speciesName);
|
||||
|
||||
// make a tooltip from the config file
|
||||
PanePtr tooltip = make_shared<Pane>();
|
||||
tooltip->removeAllChildren();
|
||||
GuiReader reader;
|
||||
auto& root = Root::singleton();
|
||||
String tooltipKind = "/interface/tooltips/species.tooltip";
|
||||
reader.construct(root.assets()->json(tooltipKind), tooltip.get());
|
||||
|
||||
// find out the gender option block from the currently selected gender
|
||||
auto genderOption = speciesDefinition->options().genderOptions.wrap(m_genderChoice);
|
||||
// makes an icon out of the default gendered character image
|
||||
WidgetPtr titleIcon = make_shared<ImageWidget>(genderOption.characterImage);
|
||||
|
||||
// read the description out of the already loaded species database.
|
||||
String title = speciesDefinition->tooltip().title;
|
||||
String subTitle = speciesDefinition->tooltip().subTitle;
|
||||
tooltip->setTitle(titleIcon, title, subTitle);
|
||||
|
||||
tooltip->setLabel("descriptionLabel", speciesDefinition->tooltip().description);
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
61
source/frontend/StarCharCreation.hpp
Normal file
61
source/frontend/StarCharCreation.hpp
Normal file
|
@ -0,0 +1,61 @@
|
|||
#ifndef _STAR_CHAR_CREATION_H_
|
||||
#define _STAR_CHAR_CREATION_H_
|
||||
|
||||
#include "StarPane.hpp"
|
||||
#include "StarImageProcessing.hpp"
|
||||
#include "StarHumanoid.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
class Player;
|
||||
typedef shared_ptr<Player> PlayerPtr;
|
||||
|
||||
STAR_EXCEPTION(CharCreationException, StarException);
|
||||
|
||||
STAR_CLASS(CharCreationPane);
|
||||
class CharCreationPane : public Pane {
|
||||
public:
|
||||
// The callback here is either called with null (when the user hits the
|
||||
// cancel button) or the newly created player (when the user hits the save
|
||||
// button).
|
||||
CharCreationPane(function<void(PlayerPtr)> requestCloseFunc);
|
||||
|
||||
void randomize();
|
||||
void randomizeName();
|
||||
|
||||
virtual void tick() override;
|
||||
virtual bool sendEvent(InputEvent const& event) override;
|
||||
|
||||
virtual PanePtr createTooltip(Vec2I const&) override;
|
||||
|
||||
private:
|
||||
void nameBoxCallback(Widget* object);
|
||||
|
||||
void changed();
|
||||
|
||||
void createPlayer();
|
||||
|
||||
void setShirt(String const& shirt, size_t colorIndex);
|
||||
void setPants(String const& pants, size_t colorIndex);
|
||||
|
||||
PlayerPtr m_previewPlayer;
|
||||
|
||||
StringList m_speciesList;
|
||||
|
||||
size_t m_speciesChoice;
|
||||
size_t m_genderChoice;
|
||||
size_t m_modeChoice;
|
||||
size_t m_bodyColor;
|
||||
size_t m_alty;
|
||||
size_t m_hairChoice;
|
||||
size_t m_heady;
|
||||
size_t m_shirtChoice;
|
||||
size_t m_shirtColor;
|
||||
size_t m_pantsChoice;
|
||||
size_t m_pantsColor;
|
||||
size_t m_personality;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
104
source/frontend/StarCharSelection.cpp
Normal file
104
source/frontend/StarCharSelection.cpp
Normal file
|
@ -0,0 +1,104 @@
|
|||
#include "StarCharSelection.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarLargeCharPlateWidget.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarRandom.hpp"
|
||||
#include "StarInputEvent.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
CharSelectionPane::CharSelectionPane(PlayerStoragePtr playerStorage,
|
||||
CreateCharCallback createCallback,
|
||||
SelectCharacterCallback selectCallback,
|
||||
DeleteCharacterCallback deleteCallback)
|
||||
: m_playerStorage(playerStorage),
|
||||
m_downScroll(0),
|
||||
m_createCallback(createCallback),
|
||||
m_selectCallback(selectCallback),
|
||||
m_deleteCallback(deleteCallback) {
|
||||
auto& root = Root::singleton();
|
||||
|
||||
GuiReader guiReader;
|
||||
|
||||
guiReader.registerCallback("playerUpButton", [=](Widget*) { shiftCharacters(-1); });
|
||||
guiReader.registerCallback("playerDownButton", [=](Widget*) { shiftCharacters(1); });
|
||||
guiReader.registerCallback("charSelector1", [=](Widget*) { selectCharacter(0); });
|
||||
guiReader.registerCallback("charSelector2", [=](Widget*) { selectCharacter(1); });
|
||||
guiReader.registerCallback("charSelector3", [=](Widget*) { selectCharacter(2); });
|
||||
guiReader.registerCallback("charSelector4", [=](Widget*) { selectCharacter(3); });
|
||||
|
||||
guiReader.construct(root.assets()->json("/interface/windowconfig/charselection.config"), this);
|
||||
}
|
||||
|
||||
bool CharSelectionPane::sendEvent(InputEvent const& event) {
|
||||
if (m_visible) {
|
||||
if (auto mouseWheel = event.ptr<MouseWheelEvent>()) {
|
||||
if (inMember(*context()->mousePosition(event))) {
|
||||
if (mouseWheel->mouseWheel == MouseWheel::Down)
|
||||
shiftCharacters(1);
|
||||
else if (mouseWheel->mouseWheel == MouseWheel::Up)
|
||||
shiftCharacters(-1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Pane::sendEvent(event);
|
||||
}
|
||||
|
||||
void CharSelectionPane::show() {
|
||||
Pane::show();
|
||||
|
||||
m_downScroll = 0;
|
||||
updateCharacterPlates();
|
||||
}
|
||||
|
||||
void CharSelectionPane::shiftCharacters(int shift) {
|
||||
m_downScroll = std::max<int>(std::min<int>(m_downScroll + shift, m_playerStorage->playerCount() - 3), 0);
|
||||
updateCharacterPlates();
|
||||
}
|
||||
|
||||
void CharSelectionPane::selectCharacter(unsigned buttonIndex) {
|
||||
if (auto playerUuid = m_playerStorage->playerUuidAt(m_downScroll + buttonIndex)) {
|
||||
auto player = m_playerStorage->loadPlayer(*playerUuid);
|
||||
if (player->isPermaDead() && !player->isAdmin()) {
|
||||
auto sound = Random::randValueFrom(
|
||||
Root::singleton().assets()->json("/interface.config:buttonClickFailSound").toArray(), "")
|
||||
.toString();
|
||||
if (!sound.empty())
|
||||
context()->playAudio(sound);
|
||||
} else
|
||||
m_selectCallback(player);
|
||||
} else
|
||||
m_createCallback();
|
||||
}
|
||||
|
||||
void CharSelectionPane::updateCharacterPlates() {
|
||||
auto updatePlayerLine = [this](String name, unsigned scrollPosition) {
|
||||
auto charSelector = fetchChild<LargeCharPlateWidget>(name);
|
||||
if (auto playerUuid = m_playerStorage->playerUuidAt(scrollPosition)) {
|
||||
charSelector->setPlayer(m_playerStorage->loadPlayer(*playerUuid));
|
||||
charSelector->enableDelete([this, playerUuid](Widget*) { m_deleteCallback(*playerUuid); });
|
||||
} else {
|
||||
charSelector->setPlayer(PlayerPtr());
|
||||
charSelector->disableDelete();
|
||||
}
|
||||
};
|
||||
|
||||
updatePlayerLine("charSelector1", m_downScroll + 0);
|
||||
updatePlayerLine("charSelector2", m_downScroll + 1);
|
||||
updatePlayerLine("charSelector3", m_downScroll + 2);
|
||||
updatePlayerLine("charSelector4", m_downScroll + 3);
|
||||
|
||||
if (m_downScroll > 0)
|
||||
fetchChild("playerUpButton")->show();
|
||||
else
|
||||
fetchChild("playerUpButton")->hide();
|
||||
|
||||
if (m_downScroll < m_playerStorage->playerCount() - 3)
|
||||
fetchChild("playerDownButton")->show();
|
||||
else
|
||||
fetchChild("playerDownButton")->hide();
|
||||
}
|
||||
|
||||
}
|
38
source/frontend/StarCharSelection.hpp
Normal file
38
source/frontend/StarCharSelection.hpp
Normal file
|
@ -0,0 +1,38 @@
|
|||
#ifndef STAR_CHAR_SELECTION_HPP
|
||||
#define STAR_CHAR_SELECTION_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
#include "StarPlayerStorage.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(PlayerStorage);
|
||||
|
||||
class CharSelectionPane : public Pane {
|
||||
public:
|
||||
typedef function<void()> CreateCharCallback;
|
||||
typedef function<void(PlayerPtr const&)> SelectCharacterCallback;
|
||||
typedef function<void(Uuid)> DeleteCharacterCallback;
|
||||
|
||||
CharSelectionPane(PlayerStoragePtr playerStorage, CreateCharCallback createCallback,
|
||||
SelectCharacterCallback selectCallback, DeleteCharacterCallback deleteCallback);
|
||||
|
||||
bool sendEvent(InputEvent const& event) override;
|
||||
void show() override;
|
||||
void updateCharacterPlates();
|
||||
|
||||
private:
|
||||
void shiftCharacters(int movement);
|
||||
void selectCharacter(unsigned buttonIndex);
|
||||
|
||||
PlayerStoragePtr m_playerStorage;
|
||||
unsigned m_downScroll;
|
||||
|
||||
CreateCharCallback m_createCallback;
|
||||
SelectCharacterCallback m_selectCallback;
|
||||
DeleteCharacterCallback m_deleteCallback;
|
||||
};
|
||||
typedef shared_ptr<CharSelectionPane> CharSelectionPanePtr;
|
||||
}
|
||||
|
||||
#endif
|
382
source/frontend/StarChat.cpp
Normal file
382
source/frontend/StarChat.cpp
Normal file
|
@ -0,0 +1,382 @@
|
|||
#include "StarChat.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarUniverseClient.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarTextBoxWidget.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarImageStretchWidget.hpp"
|
||||
#include "StarCanvasWidget.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarLogging.hpp"
|
||||
#include "StarPlayerStorage.hpp"
|
||||
#include "StarTeamClient.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
Chat::Chat(UniverseClientPtr client) : m_client(client) {
|
||||
m_chatPrevIndex = 0;
|
||||
m_historyOffset = 0;
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
m_timeChatLastActive = Time::monotonicMilliseconds();
|
||||
m_fontSize = assets->json("/interface/chat/chat.config:config.font.baseSize").toInt();
|
||||
m_chatLineHeight = assets->json("/interface/chat/chat.config:config.lineHeight").toFloat();
|
||||
m_chatVisTime = assets->json("/interface/chat/chat.config:config.visTime").toFloat();
|
||||
m_fadeRate = assets->json("/interface/chat/chat.config:config.fadeRate").toDouble();
|
||||
m_chatHistoryLimit = assets->json("/interface/chat/chat.config:config.chatHistoryLimit").toInt();
|
||||
|
||||
m_portraitTextOffset = jsonToVec2I(assets->json("/interface/chat/chat.config:config.portraitTextOffset"));
|
||||
m_portraitImageOffset = jsonToVec2I(assets->json("/interface/chat/chat.config:config.portraitImageOffset"));
|
||||
m_portraitScale = assets->json("/interface/chat/chat.config:config.portraitScale").toFloat();
|
||||
m_portraitVerticalMargin = assets->json("/interface/chat/chat.config:config.portraitVerticalMargin").toFloat();
|
||||
m_portraitBackground = assets->json("/interface/chat/chat.config:config.portraitBackground").toString();
|
||||
|
||||
m_bodyHeight = assets->json("/interface/chat/chat.config:config.bodyHeight").toInt();
|
||||
m_expandedBodyHeight = assets->json("/interface/chat/chat.config:config.expandedBodyHeight").toInt();
|
||||
|
||||
m_colorCodes[MessageContext::Local] = assets->json("/interface/chat/chat.config:config.colors.local").toString();
|
||||
m_colorCodes[MessageContext::Party] = assets->json("/interface/chat/chat.config:config.colors.party").toString();
|
||||
m_colorCodes[MessageContext::Broadcast] = assets->json("/interface/chat/chat.config:config.colors.broadcast").toString();
|
||||
m_colorCodes[MessageContext::Whisper] = assets->json("/interface/chat/chat.config:config.colors.whisper").toString();
|
||||
m_colorCodes[MessageContext::CommandResult] = assets->json("/interface/chat/chat.config:config.colors.commandResult").toString();
|
||||
m_colorCodes[MessageContext::RadioMessage] = assets->json("/interface/chat/chat.config:config.colors.radioMessage").toString();
|
||||
m_colorCodes[MessageContext::World] = assets->json("/interface/chat/chat.config:config.colors.world").toString();
|
||||
|
||||
GuiReader reader;
|
||||
|
||||
reader.registerCallback("textBox", [=](Widget*) { startChat(); });
|
||||
reader.registerCallback("upButton", [=](Widget*) { scrollUp(); });
|
||||
reader.registerCallback("downButton", [=](Widget*) { scrollDown(); });
|
||||
reader.registerCallback("bottomButton", [=](Widget*) { scrollBottom(); });
|
||||
|
||||
reader.registerCallback("filterGroup", [=](Widget* widget) {
|
||||
Json data = as<ButtonWidget>(widget)->data();
|
||||
auto filter = data.getArray("filter", {});
|
||||
m_modeFilter.clear();
|
||||
for (auto mode : filter)
|
||||
m_modeFilter.insert(MessageContextModeNames.getLeft(mode.toString()));
|
||||
m_sendMode = ChatSendModeNames.getLeft(data.getString("sendMode", "Broadcast"));
|
||||
m_historyOffset = 0;
|
||||
});
|
||||
|
||||
m_sendMode = ChatSendMode::Broadcast;
|
||||
|
||||
reader.construct(assets->json("/interface/chat/chat.config:gui"), this);
|
||||
|
||||
m_textBox = fetchChild<TextBoxWidget>("textBox");
|
||||
m_say = fetchChild<LabelWidget>("say");
|
||||
|
||||
m_chatLog = fetchChild<CanvasWidget>("chatLog");
|
||||
|
||||
m_bottomButton = fetchChild<ButtonWidget>("bottomButton");
|
||||
m_upButton = fetchChild<ButtonWidget>("upButton");
|
||||
|
||||
m_chatHistory.appendAll(m_client->playerStorage()->getMetadata("chatHistory").opt().apply(jsonToStringList).value());
|
||||
|
||||
show();
|
||||
|
||||
updateBottomButton();
|
||||
|
||||
m_background = fetchChild<ImageStretchWidget>("background");
|
||||
m_defaultHeight = m_background->size()[1];
|
||||
m_expanded = false;
|
||||
updateSize();
|
||||
}
|
||||
|
||||
void Chat::update() {
|
||||
Pane::update();
|
||||
|
||||
auto team = m_client->teamClient()->currentTeam();
|
||||
for (auto button : fetchChild<ButtonGroup>("filterGroup")->buttons()) {
|
||||
auto mode = ChatSendModeNames.getLeft(button->data().getString("sendMode", "Broadcast"));
|
||||
if (!team.isValid() && m_sendMode == ChatSendMode::Party && mode == ChatSendMode::Broadcast)
|
||||
button->check();
|
||||
if (mode == ChatSendMode::Party)
|
||||
button->setEnabled(team.isValid());
|
||||
}
|
||||
}
|
||||
|
||||
void Chat::startChat() {
|
||||
show();
|
||||
m_textBox->focus();
|
||||
}
|
||||
|
||||
void Chat::startCommand() {
|
||||
show();
|
||||
m_textBox->setText("/");
|
||||
m_textBox->focus();
|
||||
}
|
||||
|
||||
bool Chat::hasFocus() const {
|
||||
return m_textBox->hasFocus();
|
||||
}
|
||||
|
||||
void Chat::stopChat() {
|
||||
m_textBox->setText("");
|
||||
m_textBox->blur();
|
||||
m_timeChatLastActive = Time::monotonicMilliseconds();
|
||||
}
|
||||
|
||||
String Chat::currentChat() const {
|
||||
return m_textBox->getText();
|
||||
}
|
||||
|
||||
void Chat::setCurrentChat(String const& chat) {
|
||||
m_textBox->setText(chat);
|
||||
}
|
||||
|
||||
void Chat::clearCurrentChat() {
|
||||
m_textBox->setText("");
|
||||
m_chatPrevIndex = 0;
|
||||
}
|
||||
|
||||
ChatSendMode Chat::sendMode() const {
|
||||
return m_sendMode;
|
||||
}
|
||||
|
||||
void Chat::incrementIndex() {
|
||||
if (!m_chatHistory.empty()) {
|
||||
m_chatPrevIndex = std::min(m_chatPrevIndex + 1, (unsigned)m_chatHistory.size());
|
||||
m_textBox->setText(m_chatHistory.at(m_chatPrevIndex - 1));
|
||||
}
|
||||
}
|
||||
|
||||
void Chat::decrementIndex() {
|
||||
if (m_chatPrevIndex > 1 && !m_chatHistory.empty()) {
|
||||
--m_chatPrevIndex;
|
||||
m_textBox->setText(m_chatHistory.at(m_chatPrevIndex - 1));
|
||||
} else {
|
||||
m_chatPrevIndex = 0;
|
||||
m_textBox->setText("");
|
||||
}
|
||||
}
|
||||
|
||||
void Chat::addLine(String const& text, bool showPane) {
|
||||
ChatReceivedMessage message = {{MessageContext::CommandResult}, ServerConnectionId, "", text};
|
||||
addMessages({message}, showPane);
|
||||
}
|
||||
|
||||
void Chat::addMessages(List<ChatReceivedMessage> const& messages, bool showPane) {
|
||||
if (messages.empty())
|
||||
return;
|
||||
|
||||
GuiContext& guiContext = GuiContext::singleton();
|
||||
|
||||
for (auto const& message : messages) {
|
||||
Maybe<unsigned> wrapWidth;
|
||||
if (message.portrait.empty())
|
||||
wrapWidth = m_chatLog->size()[0];
|
||||
|
||||
guiContext.setFontSize(m_fontSize);
|
||||
StringList lines;
|
||||
if (message.fromNick != "" && message.portrait == "")
|
||||
lines = guiContext.wrapInterfaceText(strf("<%s> %s", message.fromNick, message.text), wrapWidth);
|
||||
else
|
||||
lines = guiContext.wrapInterfaceText(message.text, wrapWidth);
|
||||
|
||||
for (size_t i = 0; i < lines.size(); ++i) {
|
||||
m_receivedMessages.prepend({
|
||||
message.context.mode,
|
||||
message.portrait,
|
||||
move(lines[i])
|
||||
});
|
||||
}
|
||||
|
||||
if (message.fromNick != "")
|
||||
Logger::info("Chat: <%s> %s", message.fromNick, message.text);
|
||||
else
|
||||
Logger::info("Chat: %s", message.text);
|
||||
}
|
||||
|
||||
if (showPane) {
|
||||
m_timeChatLastActive = Time::monotonicMilliseconds();
|
||||
show();
|
||||
}
|
||||
|
||||
m_receivedMessages.resize(std::min((unsigned)m_receivedMessages.size(), m_chatHistoryLimit));
|
||||
}
|
||||
|
||||
void Chat::addHistory(String const& chat) {
|
||||
if (m_chatHistory.size() > 0 && m_chatHistory.get(0).equals(chat))
|
||||
return;
|
||||
|
||||
m_chatHistory.prepend(chat);
|
||||
m_chatHistory.resize(std::min((unsigned)m_chatHistory.size(), m_chatHistoryLimit));
|
||||
m_timeChatLastActive = Time::monotonicMilliseconds();
|
||||
m_client->playerStorage()->setMetadata("chatHistory", JsonArray::from(m_chatHistory));
|
||||
}
|
||||
|
||||
void Chat::renderImpl() {
|
||||
Pane::renderImpl();
|
||||
if (m_textBox->hasFocus())
|
||||
m_timeChatLastActive = Time::monotonicMilliseconds();
|
||||
Vec4B fade = {255, 255, 255, 255};
|
||||
fade[3] = (uint8_t)(visible() * 255);
|
||||
if (!visible()) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
Color fadeGreen = Color::Green;
|
||||
fadeGreen.setAlpha(fade[3]);
|
||||
m_say->setColor(fadeGreen);
|
||||
|
||||
m_chatLog->clear();
|
||||
Vec2I chatMin;
|
||||
int messageIndex = -m_historyOffset;
|
||||
|
||||
GuiContext& guiContext = GuiContext::singleton();
|
||||
guiContext.setFontSize(m_fontSize);
|
||||
guiContext.setLineSpacing(m_chatLineHeight);
|
||||
for (auto message : m_receivedMessages) {
|
||||
if (!m_modeFilter.empty() && !m_modeFilter.contains(message.mode))
|
||||
continue;
|
||||
|
||||
messageIndex++;
|
||||
if (messageIndex <= 0)
|
||||
continue;
|
||||
if (chatMin[1] > m_chatLog->size()[1])
|
||||
break;
|
||||
|
||||
String channelColorCode = "^reset";
|
||||
if (m_colorCodes.contains(message.mode))
|
||||
channelColorCode = m_colorCodes[message.mode];
|
||||
channelColorCode += "^set;";
|
||||
|
||||
String messageString = channelColorCode + message.text;
|
||||
|
||||
float messageHeight = 0;
|
||||
float lineHeightMargin = + ((m_chatLineHeight * m_fontSize) - m_fontSize);
|
||||
unsigned wrapWidth = m_chatLog->size()[0];
|
||||
|
||||
if (message.portrait != "") {
|
||||
TextPositioning tp = {Vec2F(chatMin + m_portraitTextOffset), HorizontalAnchor::LeftAnchor, VerticalAnchor::VMidAnchor, (wrapWidth - m_portraitTextOffset[0])};
|
||||
Vec2F textSize = guiContext.determineInterfaceTextSize(messageString, tp).size().floor();
|
||||
Vec2F portraitSize = Vec2F(guiContext.textureSize(m_portraitBackground));
|
||||
messageHeight = max(portraitSize[1] + m_portraitVerticalMargin, textSize[1] + lineHeightMargin);
|
||||
|
||||
// Draw both image and text anchored left and centered vertically
|
||||
auto imagePosition = chatMin + Vec2I(0, floor(messageHeight / 2)) - Vec2I(0, floor(portraitSize[1] / 2));
|
||||
m_chatLog->drawImage(m_portraitBackground, Vec2F(imagePosition), 1.0f, fade);
|
||||
m_chatLog->drawImage(message.portrait, Vec2F(imagePosition + m_portraitImageOffset), m_portraitScale, fade);
|
||||
tp.pos += Vec2F(0, floor(messageHeight / 2));
|
||||
m_chatLog->drawText(messageString, tp, m_fontSize, fade, FontMode::Normal, m_chatLineHeight);
|
||||
|
||||
} else {
|
||||
TextPositioning tp = {Vec2F(chatMin), HorizontalAnchor::LeftAnchor, VerticalAnchor::BottomAnchor, wrapWidth};
|
||||
messageHeight = guiContext.determineInterfaceTextSize(messageString, tp).size()[1] + lineHeightMargin;
|
||||
|
||||
auto fadeColor = fade;
|
||||
fadeColor[3] /= 2;
|
||||
m_chatLog->drawText(messageString, tp, m_fontSize, fadeColor, FontMode::Normal, m_chatLineHeight);
|
||||
}
|
||||
|
||||
chatMin[1] += ceil(messageHeight);
|
||||
}
|
||||
|
||||
guiContext.setDefaultLineSpacing();
|
||||
}
|
||||
|
||||
void Chat::hide() {
|
||||
stopChat();
|
||||
Pane::hide();
|
||||
}
|
||||
|
||||
float Chat::visible() const {
|
||||
double difference = (Time::monotonicMilliseconds() - m_timeChatLastActive) / 1000.0;
|
||||
if (difference < m_chatVisTime)
|
||||
return 1;
|
||||
return clamp<float>(1 - (difference - m_chatVisTime) / m_fadeRate, 0, 1);
|
||||
}
|
||||
|
||||
bool Chat::sendEvent(InputEvent const& event) {
|
||||
if (active()) {
|
||||
if (hasFocus()) {
|
||||
if (event.is<KeyDownEvent>()) {
|
||||
auto actions = context()->actions(event);
|
||||
if (actions.contains(InterfaceAction::ChatStop)) {
|
||||
stopChat();
|
||||
return true;
|
||||
} else if (actions.contains(InterfaceAction::ChatPreviousLine)) {
|
||||
incrementIndex();
|
||||
return true;
|
||||
} else if (actions.contains(InterfaceAction::ChatNextLine)) {
|
||||
decrementIndex();
|
||||
return true;
|
||||
} else if (actions.contains(InterfaceAction::ChatPageDown)) {
|
||||
scrollDown();
|
||||
return true;
|
||||
} else if (actions.contains(InterfaceAction::ChatPageUp)) {
|
||||
scrollUp();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auto mouseWheel = event.ptr<MouseWheelEvent>()) {
|
||||
if (inMember(*context()->mousePosition(event))) {
|
||||
if (mouseWheel->mouseWheel == MouseWheel::Down)
|
||||
scrollDown();
|
||||
else
|
||||
scrollUp();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.is<MouseMoveEvent>() && inMember(*context()->mousePosition(event)))
|
||||
m_timeChatLastActive = Time::monotonicMilliseconds();
|
||||
|
||||
if (event.is<MouseButtonDownEvent>()) {
|
||||
if (m_chatLog->inMember(*context()->mousePosition(event))) {
|
||||
m_expanded = !m_expanded;
|
||||
updateSize();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Pane::sendEvent(event);
|
||||
}
|
||||
|
||||
void Chat::scrollUp() {
|
||||
auto shownMessages = m_receivedMessages.filtered([=](LogMessage msg) {
|
||||
return (m_modeFilter.empty() || m_modeFilter.contains(msg.mode));
|
||||
});
|
||||
|
||||
m_historyOffset = std::max(0, std::min((int)shownMessages.size() - 1, m_historyOffset + 1));
|
||||
m_timeChatLastActive = Time::monotonicMilliseconds();
|
||||
updateBottomButton();
|
||||
}
|
||||
|
||||
void Chat::scrollDown() {
|
||||
m_historyOffset = std::max(0, m_historyOffset - 1);
|
||||
m_timeChatLastActive = Time::monotonicMilliseconds();
|
||||
updateBottomButton();
|
||||
}
|
||||
|
||||
void Chat::scrollBottom() {
|
||||
m_historyOffset = 0;
|
||||
m_timeChatLastActive = Time::monotonicMilliseconds();
|
||||
updateBottomButton();
|
||||
}
|
||||
|
||||
void Chat::updateSize() {
|
||||
auto height = m_expanded ? m_expandedBodyHeight : m_bodyHeight;
|
||||
m_background->setSize(Vec2I(m_background->size()[0], m_defaultHeight + height));
|
||||
m_chatLog->setSize(Vec2I(m_chatLog->size()[0], height));
|
||||
m_upButton->setPosition(Vec2I(m_upButton->position()[0], m_chatLog->position()[1] + m_chatLog->size()[1] - m_upButton->size()[1]));
|
||||
determineSizeFromChildren();
|
||||
}
|
||||
|
||||
void Chat::updateBottomButton() {
|
||||
auto assets = Root::singleton().assets();
|
||||
auto bottomConfig = assets->json("/interface/chat/chat.config:bottom");
|
||||
if (m_historyOffset == 0)
|
||||
m_bottomButton->setImages(bottomConfig.get("atbottom").getString("base"), bottomConfig.get("atbottom").getString("hover"));
|
||||
else
|
||||
m_bottomButton->setImages(bottomConfig.get("scrolling").getString("base"), bottomConfig.get("scrolling").getString("hover"));
|
||||
}
|
||||
|
||||
}
|
100
source/frontend/StarChat.hpp
Normal file
100
source/frontend/StarChat.hpp
Normal file
|
@ -0,0 +1,100 @@
|
|||
#ifndef STAR_CHAT_HPP
|
||||
#define STAR_CHAT_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
#include "StarChatTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(UniverseClient);
|
||||
STAR_CLASS(TextBoxWidget);
|
||||
STAR_CLASS(LabelWidget);
|
||||
STAR_CLASS(ButtonWidget);
|
||||
STAR_CLASS(ImageStretchWidget);
|
||||
STAR_CLASS(CanvasWidget);
|
||||
STAR_CLASS(Chat);
|
||||
|
||||
class Chat : public Pane {
|
||||
public:
|
||||
Chat(UniverseClientPtr client);
|
||||
|
||||
void startChat();
|
||||
void startCommand();
|
||||
bool hasFocus() const override;
|
||||
virtual bool sendEvent(InputEvent const& event) override;
|
||||
void stopChat();
|
||||
virtual void renderImpl() override;
|
||||
virtual void hide() override;
|
||||
|
||||
virtual void update() override;
|
||||
|
||||
void addLine(String const& text, bool showPane = true);
|
||||
void addMessages(List<ChatReceivedMessage> const& messages, bool showPane = true);
|
||||
void addHistory(String const& chat);
|
||||
|
||||
String currentChat() const;
|
||||
void setCurrentChat(String const& chat);
|
||||
void clearCurrentChat();
|
||||
|
||||
ChatSendMode sendMode() const;
|
||||
|
||||
void incrementIndex();
|
||||
void decrementIndex();
|
||||
|
||||
float visible() const;
|
||||
|
||||
void scrollUp();
|
||||
void scrollDown();
|
||||
void scrollBottom();
|
||||
|
||||
private:
|
||||
struct LogMessage {
|
||||
MessageContext::Mode mode;
|
||||
String portrait;
|
||||
String text;
|
||||
};
|
||||
|
||||
void updateBottomButton();
|
||||
|
||||
UniverseClientPtr m_client;
|
||||
|
||||
TextBoxWidgetPtr m_textBox;
|
||||
LabelWidgetPtr m_say;
|
||||
ButtonWidgetPtr m_bottomButton;
|
||||
ButtonWidgetPtr m_upButton;
|
||||
Deque<String> m_chatHistory;
|
||||
unsigned m_chatPrevIndex;
|
||||
int64_t m_timeChatLastActive;
|
||||
float m_chatVisTime;
|
||||
float m_fadeRate;
|
||||
unsigned m_fontSize;
|
||||
float m_chatLineHeight;
|
||||
unsigned m_chatHistoryLimit;
|
||||
int m_historyOffset;
|
||||
|
||||
CanvasWidgetPtr m_chatLog;
|
||||
|
||||
ImageStretchWidgetPtr m_background;
|
||||
int m_defaultHeight;
|
||||
int m_bodyHeight;
|
||||
int m_expandedBodyHeight;
|
||||
bool m_expanded;
|
||||
|
||||
void updateSize();
|
||||
|
||||
Vec2I m_portraitTextOffset;
|
||||
Vec2I m_portraitImageOffset;
|
||||
float m_portraitScale;
|
||||
int m_portraitVerticalMargin;
|
||||
String m_portraitBackground;
|
||||
|
||||
Map<MessageContext::Mode, String> m_colorCodes;
|
||||
Deque<LogMessage> m_receivedMessages;
|
||||
|
||||
Set<MessageContext::Mode> m_modeFilter;
|
||||
ChatSendMode m_sendMode;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
344
source/frontend/StarChatBubbleManager.cpp
Normal file
344
source/frontend/StarChatBubbleManager.cpp
Normal file
|
@ -0,0 +1,344 @@
|
|||
#include "StarChatBubbleManager.hpp"
|
||||
#include "StarJson.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarConfiguration.hpp"
|
||||
#include "StarWorldClient.hpp"
|
||||
#include "StarChattyEntity.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarAssetTextureGroup.hpp"
|
||||
#include "StarImageMetadataDatabase.hpp"
|
||||
#include "StarGuiContext.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
ChatBubbleManager::ChatBubbleManager()
|
||||
: m_textTemplate(Vec2F()), m_portraitTextTemplate(Vec2F()) {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
m_guiContext = GuiContext::singletonPtr();
|
||||
|
||||
auto jsonData = assets->json("/interface/windowconfig/chatbubbles.config");
|
||||
|
||||
m_color = jsonToColor(jsonData.get("textColor"));
|
||||
|
||||
m_fontSize = jsonData.getInt("fontSize");
|
||||
m_textPadding = jsonToVec2F(jsonData.get("textPadding"));
|
||||
|
||||
m_zoom = jsonData.getInt("textZoom");
|
||||
m_bubbleOffset = jsonToVec2F(jsonData.get("bubbleOffset"));
|
||||
m_maxAge = jsonData.getFloat("maxAge");
|
||||
m_portraitMaxAge = jsonData.getFloat("portraitMaxAge");
|
||||
|
||||
unsigned textWrapWidth = jsonData.getUInt("textWrapWidth");
|
||||
m_textTemplate = TextPositioning{Vec2F(), HorizontalAnchor::HMidAnchor, VerticalAnchor::TopAnchor, textWrapWidth * m_zoom};
|
||||
|
||||
m_interBubbleMargin = jsonData.getFloat("interBubbleMargin");
|
||||
|
||||
m_maxMessagePerEntity = jsonData.getInt("maxMessagePerEntity");
|
||||
|
||||
m_bubbles.setTweenFactor(jsonData.getFloat("tweenFactor"));
|
||||
m_bubbles.setMovementThreshold(jsonData.getFloat("movementThreshold"));
|
||||
|
||||
m_portraitBackgroundImage = jsonData.getString("portraitBackgroundImage");
|
||||
m_portraitMoreImage = jsonData.getString("portraitMoreImage");
|
||||
m_portraitMorePosition = jsonToVec2I(jsonData.get("portraitMorePosition"));
|
||||
m_portraitBackgroundSize = jsonToVec2I(jsonData.get("portraitBackgroundSize"));
|
||||
m_portraitPosition = jsonToVec2I(jsonData.get("portraitPosition"));
|
||||
m_portraitSize = jsonToVec2I(jsonData.get("portraitSize"));
|
||||
m_portraitTextPosition = jsonToVec2I(jsonData.get("portraitTextPosition"));
|
||||
m_portraitTextWidth = jsonData.getUInt("portraitTextWidth");
|
||||
m_portraitChatterFramerate = jsonData.getFloat("portraitChatterFramerate");
|
||||
m_portraitChatterDuration = jsonData.getFloat("portraitChatterDuration");
|
||||
|
||||
m_portraitTextTemplate = TextPositioning{Vec2F(m_portraitTextPosition), HorizontalAnchor::LeftAnchor, VerticalAnchor::TopAnchor, m_portraitTextWidth * m_zoom};
|
||||
|
||||
// This is a factor(0.0 - 1.0) based on the window size.
|
||||
// 0.0 is directly over the player, 1.0 is the edge of the window
|
||||
m_furthestVisibleTextDistance = jsonData.getFloat("furthestTextDistance");
|
||||
|
||||
String textFadeFunctionName = jsonData.getString("textFadeFunction");
|
||||
m_textFadeFunction = Root::singleton().functionDatabase()->function(textFadeFunctionName);
|
||||
|
||||
String bubbleFadeFunctionName = jsonData.getString("bubbleFadeFunction");
|
||||
m_bubbleFadeFunction = Root::singleton().functionDatabase()->function(bubbleFadeFunctionName);
|
||||
}
|
||||
|
||||
void ChatBubbleManager::setCamera(WorldCamera const& camera) {
|
||||
float oldPixelRatio = m_camera.pixelRatio();
|
||||
m_camera = camera;
|
||||
if (m_camera.pixelRatio() != oldPixelRatio) {
|
||||
List<ChatAction> actions;
|
||||
m_bubbles.forEach([&actions](BubbleState<Bubble> const& state, Bubble& bubble) {
|
||||
actions.append(SayChatAction{bubble.entity, bubble.text, state.idealDestination, bubble.config});
|
||||
});
|
||||
m_bubbles.clear();
|
||||
for (auto portraitBubble : m_portraitBubbles)
|
||||
actions.append(PortraitChatAction{
|
||||
portraitBubble.entity,
|
||||
portraitBubble.portrait,
|
||||
portraitBubble.text,
|
||||
portraitBubble.position,
|
||||
portraitBubble.config
|
||||
});
|
||||
m_portraitBubbles.clear();
|
||||
addChatActions(actions, true);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatBubbleManager::update(WorldClientPtr world) {
|
||||
m_bubbles.forEach([this, &world](BubbleState<Bubble>& bubbleState, Bubble& bubble) {
|
||||
bubble.age += WorldTimestep;
|
||||
if (auto entity = world->get<ChattyEntity>(bubble.entity)) {
|
||||
bubble.onscreen = m_camera.worldGeometry().rectIntersectsRect(
|
||||
m_camera.worldScreenRect(), entity->metaBoundBox().translated(entity->position()));
|
||||
bubbleState.idealDestination = m_camera.worldToScreen(entity->mouthPosition() + m_bubbleOffset);
|
||||
}
|
||||
});
|
||||
|
||||
for (auto& portraitBubble : m_portraitBubbles) {
|
||||
portraitBubble.age += WorldTimestep;
|
||||
if (auto entity = world->entity(portraitBubble.entity)) {
|
||||
portraitBubble.onscreen = m_camera.worldGeometry().rectIntersectsRect(m_camera.worldScreenRect(), entity->metaBoundBox().translated(entity->position()));
|
||||
if (auto chatter = as<ChattyEntity>(entity))
|
||||
portraitBubble.position = chatter->mouthPosition();
|
||||
else
|
||||
portraitBubble.position = entity->position();
|
||||
}
|
||||
}
|
||||
|
||||
Map<EntityId, int> count;
|
||||
filter(m_portraitBubbles, [&](PortraitBubble const& portraitBubble) -> bool {
|
||||
count[portraitBubble.entity] += m_maxMessagePerEntity;
|
||||
if (count[portraitBubble.entity] > m_maxMessagePerEntity)
|
||||
return false;
|
||||
if (world->get<ChattyEntity>(portraitBubble.entity))
|
||||
return portraitBubble.age < m_portraitMaxAge;
|
||||
return false;
|
||||
});
|
||||
|
||||
m_bubbles.filter([&](BubbleState<Bubble> const&, Bubble const& bubble) -> bool {
|
||||
if (++count[bubble.entity] > m_maxMessagePerEntity)
|
||||
return false;
|
||||
if (world->get<ChattyEntity>(bubble.entity))
|
||||
return bubble.age < m_maxAge;
|
||||
return false;
|
||||
});
|
||||
|
||||
m_bubbles.update();
|
||||
}
|
||||
|
||||
uint8_t ChatBubbleManager::calcDistanceFadeAlpha(Vec2F bubbleScreenPosition, StoredFunctionPtr fadeFunction) const {
|
||||
// first calculate bubble position as a factor, distance from center to edge
|
||||
// of screen (0.0-1.0)
|
||||
float halfScreenwidth = m_camera.screenSize()[0] * 0.5f;
|
||||
float distanceFactor = (fabsf(bubbleScreenPosition[0] - halfScreenwidth)) / halfScreenwidth;
|
||||
|
||||
// that distance factor is divided by the max allowable distance
|
||||
// to re-space the distance as a 0 - 1 over the max allowable distance
|
||||
distanceFactor = clamp(distanceFactor / m_furthestVisibleTextDistance, 0.0f, 1.0f);
|
||||
|
||||
int alpha = fadeFunction->evaluate(distanceFactor);
|
||||
return clamp(alpha, 0, 255);
|
||||
}
|
||||
|
||||
void ChatBubbleManager::render() {
|
||||
if (m_bubbles.empty() && m_portraitBubbles.empty())
|
||||
return;
|
||||
if (!Root::singleton().configuration()->get("speechBubbles").toBool())
|
||||
return;
|
||||
|
||||
m_bubbles.forEach([this](BubbleState<Bubble> const& state, Bubble& bubble) {
|
||||
if (bubble.onscreen) {
|
||||
int alpha = calcDistanceFadeAlpha(state.currentPosition, m_bubbleFadeFunction);
|
||||
if (alpha) {
|
||||
for (auto const& bubbleImage : bubble.backgroundImages)
|
||||
drawBubbleImage(state.currentPosition, bubbleImage, m_zoom, alpha);
|
||||
for (auto const& bubbleText : bubble.bubbleText)
|
||||
drawBubbleText(state.currentPosition, bubbleText, m_zoom, alpha, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (auto portraitBubble : m_portraitBubbles) {
|
||||
if (portraitBubble.onscreen) {
|
||||
Vec2F screenPos = m_camera.worldToScreen(portraitBubble.position + m_bubbleOffset);
|
||||
int frame = 0;
|
||||
if (portraitBubble.age <= m_portraitChatterDuration)
|
||||
frame = int((portraitBubble.age / m_portraitChatterFramerate) * 2) % 2;
|
||||
// 255 here because portrait bubbles are always full opacity
|
||||
for (auto const& bubbleImage : portraitBubble.backgroundImages)
|
||||
drawBubbleImage(screenPos, make_tuple(get<0>(bubbleImage).replace("<frame>", toString(frame)), get<1>(bubbleImage)), m_zoom, 255);
|
||||
// 255 here because portrait bubbles are always full opacity
|
||||
for (auto const& bubbleText : portraitBubble.bubbleText)
|
||||
drawBubbleText(screenPos, bubbleText, m_zoom, 255, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChatBubbleManager::addChatActions(List<ChatAction> chatActions, bool silent) {
|
||||
auto assets = Root::singleton().assets();
|
||||
auto config = assets->json("/interface/windowconfig/chatbubbles.config");
|
||||
|
||||
float partSize = config.getFloat("partSize");
|
||||
|
||||
for (auto action : chatActions) {
|
||||
Json config = JsonObject{};
|
||||
Vec2F position;
|
||||
|
||||
if (action.is<SayChatAction>()) {
|
||||
auto sayAction = action.get<SayChatAction>();
|
||||
config = sayAction.config.optObject().value(JsonObject{});
|
||||
position = sayAction.position;
|
||||
|
||||
// TODO: Get rid of this stupid fucking bullshit, this is the ugliest
|
||||
// fragilest pointlessest horseshit code in the codebase. It wouldn't
|
||||
// bother me so bad if it weren't so fucking easy to do right.
|
||||
m_guiContext->setFontSize(m_fontSize, m_zoom);
|
||||
auto result = m_guiContext->determineTextSize(sayAction.text, m_textTemplate);
|
||||
float textWidth = result.width() / m_zoom + m_textPadding[0];
|
||||
float textHeight = result.height() / m_zoom + m_textPadding[1];
|
||||
|
||||
Vec2I innerTiles = Vec2I::ceil(Vec2F((textWidth + 4) / partSize, (textHeight + 3) / partSize));
|
||||
if (innerTiles[0] % 2 == 0)
|
||||
innerTiles[0] += 1;
|
||||
if (innerTiles[0] < 3)
|
||||
innerTiles[0] = 3;
|
||||
int middleIdx = (innerTiles[0] - 1) / 2;
|
||||
|
||||
List<BubbleImage> backgroundImages;
|
||||
if (config.getBool("drawBorder", true)) {
|
||||
for (int y = 0; y < innerTiles[1]; y++) {
|
||||
for (int x = 0; x < innerTiles[0]; x++) {
|
||||
auto partPosition = [partSize](int x, int y) {
|
||||
return Vec2F(x * partSize, y * partSize);
|
||||
};
|
||||
if (y == 0) {
|
||||
if (x == 0) {
|
||||
backgroundImages.append(make_tuple("/interface/chatbubbles/cornerBottomLeft.png", partPosition(x, y)));
|
||||
} else if (x == innerTiles[0] - 1) {
|
||||
backgroundImages.append(make_tuple("/interface/chatbubbles/cornerBottomRight.png", partPosition(x, y)));
|
||||
} else {
|
||||
if (middleIdx == x)
|
||||
backgroundImages.append(make_tuple("/interface/chatbubbles/point.png", partPosition(x, y - 1)));
|
||||
else
|
||||
backgroundImages.append(make_tuple("/interface/chatbubbles/sideDown.png", partPosition(x, y)));
|
||||
}
|
||||
} else if (y == innerTiles[1] - 1) {
|
||||
if (x == 0)
|
||||
backgroundImages.append(make_tuple("/interface/chatbubbles/cornerTopLeft.png", partPosition(x, y)));
|
||||
else if (x == innerTiles[0] - 1)
|
||||
backgroundImages.append(make_tuple("/interface/chatbubbles/cornerTopRight.png", partPosition(x, y)));
|
||||
else
|
||||
backgroundImages.append(make_tuple("/interface/chatbubbles/sideUp.png", partPosition(x, y)));
|
||||
} else {
|
||||
if (x == 0)
|
||||
backgroundImages.append(make_tuple("/interface/chatbubbles/sideLeft.png", partPosition(x, y)));
|
||||
else if (x == innerTiles[0] - 1)
|
||||
backgroundImages.append(make_tuple("/interface/chatbubbles/sideRight.png", partPosition(x, y)));
|
||||
else
|
||||
backgroundImages.append(make_tuple("/interface/chatbubbles/center.png", partPosition(x, y)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float textMultiLineShift = textHeight;
|
||||
float horizontalCenter = partSize * innerTiles[0] * 0.5f;
|
||||
float verticalShift = (partSize * innerTiles[1] - textMultiLineShift) * 0.5f + textMultiLineShift;
|
||||
Vec2F position = Vec2F(horizontalCenter, verticalShift);
|
||||
List<BubbleText> bubbleTexts;
|
||||
auto fontSize = config.getUInt("fontSize", m_fontSize);
|
||||
auto color = config.opt("color").apply(jsonToColor).value(m_color);
|
||||
bubbleTexts.append(make_tuple(sayAction.text, fontSize, color.toRgba(), true, position));
|
||||
|
||||
for (auto& backgroundImage : backgroundImages)
|
||||
get<1>(backgroundImage) += Vec2F(-horizontalCenter, partSize);
|
||||
for (auto& bubbleText : bubbleTexts)
|
||||
get<4>(bubbleText) += Vec2F(-horizontalCenter, partSize);
|
||||
|
||||
auto pos = m_camera.worldToScreen(sayAction.position + m_bubbleOffset);
|
||||
RectF boundBox = fold(backgroundImages, RectF::null(), [pos, this](RectF const& boundBox, BubbleImage const& bubbleImage) {
|
||||
return boundBox.combined(bubbleImageRect(pos, bubbleImage, m_zoom));
|
||||
});
|
||||
Bubble bubble = {sayAction.entity, sayAction.text, sayAction.config, 0, move(backgroundImages), move(bubbleTexts), false};
|
||||
List<BubbleState<Bubble>> oldBubbles = m_bubbles.filtered([&sayAction](BubbleState<Bubble> const&, Bubble const& bubble) {
|
||||
return bubble.entity == sayAction.entity;
|
||||
});
|
||||
m_bubbles.filter([&sayAction](BubbleState<Bubble> const&, Bubble const& bubble) { return bubble.entity != sayAction.entity; });
|
||||
m_bubbles.addBubble(pos, boundBox, move(bubble), m_interBubbleMargin * m_zoom);
|
||||
oldBubbles.sort([](BubbleState<Bubble> const& a, BubbleState<Bubble> const& b) { return a.contents.age < b.contents.age; });
|
||||
for (auto bubble : oldBubbles.slice(0, m_maxMessagePerEntity - 1))
|
||||
m_bubbles.addBubble(bubble.idealDestination, bubble.boundBox, bubble.contents, 0);
|
||||
|
||||
} else if (action.is<PortraitChatAction>()) {
|
||||
auto portraitAction = action.get<PortraitChatAction>();
|
||||
config = portraitAction.config.optObject().value(JsonObject{});
|
||||
position = portraitAction.position;
|
||||
|
||||
List<BubbleImage> backgroundImages;
|
||||
backgroundImages.append(make_tuple(m_portraitBackgroundImage, Vec2F()));
|
||||
if (config.getBool("drawMoreIndicator", false))
|
||||
backgroundImages.append(make_tuple(m_portraitMoreImage, Vec2F(m_portraitMorePosition)));
|
||||
backgroundImages.append(make_tuple(portraitAction.portrait, Vec2F(m_portraitPosition)));
|
||||
List<BubbleText> bubbleTexts;
|
||||
bubbleTexts.append(make_tuple(portraitAction.text, m_fontSize, m_color.toRgba(), false, Vec2F(m_portraitTextPosition)));
|
||||
|
||||
for (auto& backgroundImage : backgroundImages)
|
||||
get<1>(backgroundImage) += Vec2F(-m_portraitBackgroundSize[0] / 2, 0);
|
||||
for (auto& bubbleText : bubbleTexts)
|
||||
get<4>(bubbleText) += Vec2F(-m_portraitBackgroundSize[0] / 2, 0);
|
||||
|
||||
m_portraitBubbles.prepend({
|
||||
portraitAction.entity,
|
||||
portraitAction.portrait,
|
||||
portraitAction.text,
|
||||
portraitAction.position,
|
||||
portraitAction.config,
|
||||
0,
|
||||
move(backgroundImages),
|
||||
move(bubbleTexts),
|
||||
false
|
||||
});
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
if (auto sound = config.optString("sound")) {
|
||||
auto assets = Root::singleton().assets();
|
||||
AudioInstancePtr audioInstance = make_shared<AudioInstance>(*assets->audio(*sound));
|
||||
audioInstance->setPosition(position);
|
||||
m_guiContext->playAudio(audioInstance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RectF ChatBubbleManager::bubbleImageRect(Vec2F screenPos, BubbleImage const& bubbleImage, int pixelRatio) {
|
||||
auto imgMetadata = Root::singleton().imageMetadataDatabase();
|
||||
auto image = get<0>(bubbleImage);
|
||||
return RectF::withSize(screenPos + get<1>(bubbleImage) * pixelRatio, Vec2F(imgMetadata->imageSize(image)) * pixelRatio);
|
||||
}
|
||||
|
||||
void ChatBubbleManager::drawBubbleImage(Vec2F screenPos, BubbleImage const& bubbleImage, int pixelRatio, int alpha) {
|
||||
auto image = get<0>(bubbleImage);
|
||||
auto offset = get<1>(bubbleImage) * pixelRatio;
|
||||
m_guiContext->drawQuad(image, screenPos + offset, pixelRatio, {255, 255, 255, alpha});
|
||||
}
|
||||
|
||||
void ChatBubbleManager::drawBubbleText(Vec2F screenPos, BubbleText const& bubbleText, int pixelRatio, int alpha, bool isPortrait) {
|
||||
Vec4B const& baseColor = get<2>(bubbleText);
|
||||
|
||||
// use the alpha as a blend value for the text colour as pulled from data.
|
||||
Vec4B const& displayColor = Vec4B(baseColor[0], baseColor[1], baseColor[2], (baseColor[3] * alpha) / 255);
|
||||
|
||||
m_guiContext->setFontColor(displayColor);
|
||||
m_guiContext->setFontSize(get<1>(bubbleText), m_zoom);
|
||||
|
||||
auto offset = get<4>(bubbleText) * pixelRatio;
|
||||
TextPositioning tp = isPortrait ? m_portraitTextTemplate : m_textTemplate;
|
||||
tp.pos = screenPos + offset;
|
||||
|
||||
m_guiContext->renderText(get<0>(bubbleText), tp);
|
||||
}
|
||||
|
||||
}
|
100
source/frontend/StarChatBubbleManager.hpp
Normal file
100
source/frontend/StarChatBubbleManager.hpp
Normal file
|
@ -0,0 +1,100 @@
|
|||
#ifndef STAR_CHAT_BUBBLE_MANAGER_HPP
|
||||
#define STAR_CHAT_BUBBLE_MANAGER_HPP
|
||||
|
||||
#include "StarChatAction.hpp"
|
||||
#include "StarTextPainter.hpp"
|
||||
#include "StarWorldCamera.hpp"
|
||||
#include "StarChatBubbleSeparation.hpp"
|
||||
#include "StarStoredFunctions.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(GuiContext);
|
||||
STAR_CLASS(AssetTextureGroup);
|
||||
STAR_CLASS(WorldClient);
|
||||
STAR_CLASS(ChatBubbleManager);
|
||||
|
||||
class ChatBubbleManager {
|
||||
public:
|
||||
ChatBubbleManager();
|
||||
|
||||
void setCamera(WorldCamera const& camera);
|
||||
|
||||
void addChatActions(List<ChatAction> chatActions, bool silent = false);
|
||||
|
||||
void update(WorldClientPtr world);
|
||||
void render();
|
||||
|
||||
private:
|
||||
typedef tuple<String, Vec2F> BubbleImage;
|
||||
typedef tuple<String, unsigned, Vec4B, bool, Vec2F> BubbleText;
|
||||
|
||||
struct Bubble {
|
||||
EntityId entity;
|
||||
String text;
|
||||
Json config;
|
||||
float age;
|
||||
List<BubbleImage> backgroundImages;
|
||||
List<BubbleText> bubbleText;
|
||||
bool onscreen;
|
||||
};
|
||||
|
||||
struct PortraitBubble {
|
||||
EntityId entity;
|
||||
String portrait;
|
||||
String text;
|
||||
Vec2F position;
|
||||
Json config;
|
||||
float age;
|
||||
List<BubbleImage> backgroundImages;
|
||||
List<BubbleText> bubbleText;
|
||||
bool onscreen;
|
||||
};
|
||||
|
||||
// Calculate the alpha for a speech bubble based on distance from player to
|
||||
// edge of screen
|
||||
uint8_t calcDistanceFadeAlpha(Vec2F bubbleScreenPosition, StoredFunctionPtr fadeFunction) const;
|
||||
|
||||
RectF bubbleImageRect(Vec2F screenPos, BubbleImage const& bubbleImage, int pixelRatio);
|
||||
void drawBubbleImage(Vec2F screenPos, BubbleImage const& bubbleImage, int pixelRatio, int alpha);
|
||||
void drawBubbleText(Vec2F screenPos, BubbleText const& bubbleText, int pixelRatio, int alpha, bool isPortrait);
|
||||
|
||||
GuiContext* m_guiContext;
|
||||
|
||||
WorldCamera m_camera;
|
||||
|
||||
TextPositioning m_textTemplate;
|
||||
TextPositioning m_portraitTextTemplate;
|
||||
Color m_color;
|
||||
int m_fontSize;
|
||||
Vec2F m_textPadding;
|
||||
|
||||
BubbleSeparator<Bubble> m_bubbles;
|
||||
int m_zoom;
|
||||
Vec2F m_bubbleOffset;
|
||||
float m_maxAge;
|
||||
float m_portraitMaxAge;
|
||||
float m_interBubbleMargin;
|
||||
int m_maxMessagePerEntity;
|
||||
|
||||
Deque<PortraitBubble> m_portraitBubbles;
|
||||
String m_portraitBackgroundImage;
|
||||
String m_portraitMoreImage;
|
||||
Vec2I m_portraitMorePosition;
|
||||
Vec2I m_portraitBackgroundSize;
|
||||
Vec2I m_portraitPosition;
|
||||
Vec2I m_portraitSize;
|
||||
Vec2I m_portraitTextPosition;
|
||||
unsigned m_portraitTextWidth;
|
||||
float m_portraitChatterFramerate;
|
||||
float m_portraitChatterDuration;
|
||||
|
||||
float m_furthestVisibleTextDistance; // 0.0 is directly over the player, 1.0
|
||||
// is the edge of the window
|
||||
StoredFunctionPtr m_textFadeFunction;
|
||||
StoredFunctionPtr m_bubbleFadeFunction;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
74
source/frontend/StarChatBubbleSeparation.cpp
Normal file
74
source/frontend/StarChatBubbleSeparation.cpp
Normal file
|
@ -0,0 +1,74 @@
|
|||
#include "StarChatBubbleSeparation.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
bool compareLeft(RectF const& a, RectF const& b) {
|
||||
return a.xMin() < b.xMin();
|
||||
}
|
||||
|
||||
bool compareRight(RectF const& a, RectF const& b) {
|
||||
return a.xMax() > b.xMax();
|
||||
}
|
||||
|
||||
bool compareOverlapLeft(RectF const& newBox, RectF const& fixedBox) {
|
||||
return newBox.xMax() < fixedBox.xMin();
|
||||
}
|
||||
|
||||
bool compareOverlapRight(RectF const& newBox, RectF const& fixedBox) {
|
||||
return newBox.xMin() > fixedBox.xMax();
|
||||
}
|
||||
|
||||
template <typename Compare>
|
||||
void appendHorizontalOverlaps(List<RectF>& overlaps,
|
||||
List<RectF> const& boxes,
|
||||
List<RectF>::iterator leftBound,
|
||||
Compare compare,
|
||||
RectF const& box) {
|
||||
auto i = leftBound;
|
||||
if (i == boxes.begin())
|
||||
return;
|
||||
--i;
|
||||
while (!compare(box, *i)) {
|
||||
overlaps.append(*i);
|
||||
if (i == boxes.begin())
|
||||
return;
|
||||
--i;
|
||||
}
|
||||
}
|
||||
|
||||
RectF separateBubble(List<RectF>& sortedLeftEdges, List<RectF>& sortedRightEdges, RectF box) {
|
||||
// We have to maintain two lists of boxes: one sorted by the left edges and
|
||||
// one
|
||||
// by the right edges. This is because boxes can be different sizes, and
|
||||
// if we only check one edge, appendHorizontalOverlaps can miss any boxes
|
||||
// whose projections onto the X axis entirely contain other boxes'.
|
||||
auto leftOverlapBound = upper_bound(sortedLeftEdges.begin(), sortedLeftEdges.end(), box, compareOverlapLeft);
|
||||
auto rightOverlapBound = upper_bound(sortedRightEdges.begin(), sortedRightEdges.end(), box, compareOverlapRight);
|
||||
|
||||
List<RectF> horizontalOverlaps;
|
||||
appendHorizontalOverlaps(horizontalOverlaps, sortedLeftEdges, leftOverlapBound, compareOverlapRight, box);
|
||||
appendHorizontalOverlaps(horizontalOverlaps, sortedRightEdges, rightOverlapBound, compareOverlapLeft, box);
|
||||
|
||||
// horizontalOverlaps now consists of the boxes that (when projected onto the
|
||||
// X axis)
|
||||
// overlap with 'box'.
|
||||
|
||||
while (true) {
|
||||
// While box is overlapping any other boxes, raise its Y value.
|
||||
List<RectF> overlappingBoxes =
|
||||
horizontalOverlaps.filtered([&box](RectF const& overlapper) { return box.intersects(overlapper, false); });
|
||||
if (overlappingBoxes.empty())
|
||||
break;
|
||||
RectF overlapBoundBox = fold(overlappingBoxes, box, [](RectF const& a, RectF const& b) { return a.combined(b); });
|
||||
auto height = box.height();
|
||||
box.setYMin(overlapBoundBox.yMax());
|
||||
box.setYMax(box.yMin() + height);
|
||||
}
|
||||
|
||||
sortedLeftEdges.insertSorted(box, compareLeft);
|
||||
sortedRightEdges.insertSorted(box, compareRight);
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
}
|
181
source/frontend/StarChatBubbleSeparation.hpp
Normal file
181
source/frontend/StarChatBubbleSeparation.hpp
Normal file
|
@ -0,0 +1,181 @@
|
|||
#ifndef STAR_CHAT_BUBBLE_SEPARATION_HPP
|
||||
#define STAR_CHAT_BUBBLE_SEPARATION_HPP
|
||||
|
||||
#include "StarRect.hpp"
|
||||
#include "StarList.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
template <typename T>
|
||||
struct BubbleState {
|
||||
T contents;
|
||||
// The destination is the position the bubble is being pulled towards,
|
||||
// ignoring the positions of all other bubbles (so it could overlap).
|
||||
// This is the position of the entity.
|
||||
Vec2F idealDestination;
|
||||
// The idealDestination is the position of the entity now, while the
|
||||
// currentDestination is only updated once the idealDestination passes a
|
||||
// minimum distance. (So that the algorithm is not re-run for sub-pixel
|
||||
// position changes.)
|
||||
Vec2F currentDestination;
|
||||
// The bound box of the nametag if it was at the destination.
|
||||
RectF boundBox;
|
||||
// The position for the bubble chosen by the algorithm (which it may not
|
||||
// have fully moved to yet).
|
||||
Vec2F separatedPosition;
|
||||
// The bound box of the bubble around the separatedPosition.
|
||||
RectF separatedBox;
|
||||
// Where the bubble is now, which could be anywhere en route to the
|
||||
// separatedPosition.
|
||||
Vec2F currentPosition;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class BubbleSeparator {
|
||||
public:
|
||||
using Bubble = BubbleState<T>;
|
||||
|
||||
BubbleSeparator(float tweenFactor = 0.5f, float movementThreshold = 2.0f);
|
||||
|
||||
float tweenFactor() const;
|
||||
void setTweenFactor(float tweenFactor);
|
||||
|
||||
float movementThreshold() const;
|
||||
void setMovementThreshold(float movementThreshold);
|
||||
|
||||
void addBubble(Vec2F position, RectF boundBox, T contents, unsigned margin = 0);
|
||||
|
||||
void filter(function<bool(Bubble const&, T&)> func);
|
||||
List<Bubble> filtered(function<bool(Bubble const&, T const&)> func);
|
||||
void forEach(function<void(Bubble&, T&)> func);
|
||||
|
||||
void update();
|
||||
void clear();
|
||||
bool empty() const;
|
||||
|
||||
private:
|
||||
static bool compareBubbleY(Bubble const& a, Bubble const& b);
|
||||
|
||||
float m_tweenFactor;
|
||||
float m_movementThreshold;
|
||||
List<Bubble> m_bubbles;
|
||||
List<RectF> m_sortedLeftEdges;
|
||||
List<RectF> m_sortedRightEdges;
|
||||
};
|
||||
|
||||
// Shifts box upwards until it is not overlapping any of the boxes in
|
||||
// sortedLeftEdges
|
||||
// and sortedRightEdges.
|
||||
// The resulting box is returned and inserted into sortedLeftEdges and
|
||||
// sortedRightEdges.
|
||||
// The two lists contain all the chat bubbles that have been separated, sorted
|
||||
// by
|
||||
// the X positions of their left and right edges respectively.
|
||||
RectF separateBubble(List<RectF>& sortedLeftEdges, List<RectF>& sortedRightEdges, RectF box);
|
||||
|
||||
template <typename T>
|
||||
BubbleSeparator<T>::BubbleSeparator(float tweenFactor, float movementThreshold)
|
||||
: m_tweenFactor(tweenFactor), m_movementThreshold(movementThreshold), m_sortedLeftEdges(), m_sortedRightEdges() {}
|
||||
|
||||
template <typename T>
|
||||
float BubbleSeparator<T>::tweenFactor() const {
|
||||
return m_tweenFactor;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void BubbleSeparator<T>::setTweenFactor(float tweenFactor) {
|
||||
m_tweenFactor = tweenFactor;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
float BubbleSeparator<T>::movementThreshold() const {
|
||||
return m_movementThreshold;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void BubbleSeparator<T>::setMovementThreshold(float movementThreshold) {
|
||||
m_movementThreshold = movementThreshold;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void BubbleSeparator<T>::addBubble(Vec2F position, RectF boundBox, T contents, unsigned margin) {
|
||||
boundBox.setYMax(boundBox.yMax() + margin);
|
||||
RectF separated = separateBubble(m_sortedLeftEdges, m_sortedRightEdges, boundBox);
|
||||
Vec2F separatedPosition = position + separated.min() - boundBox.min();
|
||||
Bubble bubble = Bubble{contents, position, position, boundBox, separatedPosition, separated, separatedPosition};
|
||||
m_bubbles.insertSorted(move(bubble), &BubbleSeparator<T>::compareBubbleY);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void BubbleSeparator<T>::filter(function<bool(Bubble const&, T&)> func) {
|
||||
m_bubbles.filter([this, func](Bubble& bubble) {
|
||||
if (!func(bubble, bubble.contents)) {
|
||||
m_sortedLeftEdges.remove(bubble.separatedBox);
|
||||
m_sortedRightEdges.remove(bubble.separatedBox);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
List<BubbleState<T>> BubbleSeparator<T>::filtered(function<bool(Bubble const&, T const&)> func) {
|
||||
return m_bubbles.filtered([func](Bubble const& bubble) { return func(bubble, bubble.contents); });
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void BubbleSeparator<T>::forEach(function<void(Bubble&, T&)> func) {
|
||||
bool anyMoved = false;
|
||||
m_bubbles.exec([this, func, &anyMoved](Bubble& bubble) {
|
||||
RectF oldBoundBox = bubble.boundBox;
|
||||
|
||||
func(bubble, bubble.contents);
|
||||
|
||||
Vec2F sizeDelta = bubble.boundBox.size() - oldBoundBox.size();
|
||||
Vec2F positionDelta = bubble.idealDestination - bubble.currentDestination;
|
||||
if (sizeDelta.magnitude() > m_movementThreshold || positionDelta.magnitude() > m_movementThreshold) {
|
||||
m_sortedLeftEdges.remove(bubble.separatedBox);
|
||||
m_sortedRightEdges.remove(bubble.separatedBox);
|
||||
RectF boundBox = bubble.boundBox.translated(positionDelta);
|
||||
RectF separated = separateBubble(m_sortedLeftEdges, m_sortedRightEdges, boundBox);
|
||||
anyMoved = true;
|
||||
bubble = Bubble{bubble.contents,
|
||||
bubble.idealDestination,
|
||||
bubble.idealDestination,
|
||||
boundBox,
|
||||
bubble.idealDestination + separated.min() - boundBox.min(),
|
||||
separated,
|
||||
bubble.currentPosition + positionDelta};
|
||||
}
|
||||
});
|
||||
if (anyMoved)
|
||||
m_bubbles.sort(&BubbleSeparator<T>::compareBubbleY);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void BubbleSeparator<T>::update() {
|
||||
m_bubbles.exec([this](Bubble& bubble) {
|
||||
Vec2F delta = bubble.separatedPosition - bubble.currentPosition;
|
||||
bubble.currentPosition += m_tweenFactor * delta;
|
||||
});
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void BubbleSeparator<T>::clear() {
|
||||
m_bubbles.clear();
|
||||
m_sortedLeftEdges.clear();
|
||||
m_sortedRightEdges.clear();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool BubbleSeparator<T>::empty() const {
|
||||
return m_bubbles.empty();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool BubbleSeparator<T>::compareBubbleY(Bubble const& a, Bubble const& b) {
|
||||
return a.currentDestination[1] < b.currentDestination[1];
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
562
source/frontend/StarCinematic.cpp
Normal file
562
source/frontend/StarCinematic.cpp
Normal file
|
@ -0,0 +1,562 @@
|
|||
#include "StarCinematic.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarWorldClient.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarGuiContext.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
const float vWidth = 960.0f;
|
||||
const float vHeight = 540.0f;
|
||||
|
||||
Cinematic::Cinematic() {
|
||||
m_completionTime = 0;
|
||||
m_completable = false;
|
||||
m_suppressInput = false;
|
||||
}
|
||||
|
||||
void Cinematic::load(Json const& definition) {
|
||||
stop();
|
||||
|
||||
for (auto timeSkipDefinition : definition.getArray("timeSkips", JsonArray())) {
|
||||
TimeSkip timeSkip;
|
||||
timeSkip.availableTime = timeSkipDefinition.getFloat("available");
|
||||
timeSkip.skipToTime = timeSkipDefinition.getFloat("skipTo");
|
||||
m_timeSkips.append(timeSkip);
|
||||
}
|
||||
sort(m_timeSkips, [](TimeSkip const& a, TimeSkip const& b) -> bool {
|
||||
return a.availableTime < b.availableTime;
|
||||
});
|
||||
|
||||
for (auto cameraDefinition : definition.getArray("camera", JsonArray())) {
|
||||
CameraKeyFrame keyFrame;
|
||||
keyFrame.timecode = cameraDefinition.getFloat("timecode");
|
||||
keyFrame.zoom = cameraDefinition.getFloat("zoom", 1.0f);
|
||||
if (cameraDefinition.contains("pan"))
|
||||
keyFrame.pan = jsonToVec2F(cameraDefinition.get("pan"));
|
||||
m_cameraKeyFrames.append(keyFrame);
|
||||
}
|
||||
|
||||
for (auto panelDefinition : definition.getArray("panels")) {
|
||||
PanelPtr panel = make_shared<Panel>();
|
||||
panel->useCamera = panelDefinition.getBool("useCamera", true);
|
||||
panel->drawables = panelDefinition.getArray("drawables", {});
|
||||
panel->animationFrames = panelDefinition.getInt("animationFrames", std::numeric_limits<int>::max());
|
||||
panel->text = panelDefinition.getString("text", "");
|
||||
panel->textPosition = TextPositioning(panelDefinition.get("textPosition", JsonObject()));
|
||||
panel->fontColor = panelDefinition.opt("fontColor").apply(jsonToVec4B).value(Vec4B(255, 255, 255, 255));
|
||||
panel->fontSize = panelDefinition.getUInt("fontSize", 8);
|
||||
panel->avatar = panelDefinition.getString("avatar", "");
|
||||
panel->startTime = panelDefinition.getFloat("startTime", 0);
|
||||
panel->endTime = panelDefinition.getFloat("endTime", 0);
|
||||
panel->loopTime = panelDefinition.getFloat("loopTime", 0);
|
||||
for (auto keyframeDefinition : panelDefinition.getArray("keyframes")) {
|
||||
KeyFrame keyframe;
|
||||
keyframe.timecode = keyframeDefinition.getFloat("timecode");
|
||||
keyframe.command = keyframeDefinition;
|
||||
panel->keyFrames.append(keyframe);
|
||||
float endTimecode = panel->endTime > 0 ? std::min(panel->endTime, panel->startTime + keyframe.timecode) : panel->startTime + keyframe.timecode;
|
||||
m_completionTime = std::max(m_completionTime, endTimecode);
|
||||
}
|
||||
m_panels.append(panel);
|
||||
}
|
||||
|
||||
for (auto audioDefinition : definition.getArray("audio")) {
|
||||
AudioCue cue;
|
||||
cue.timecode = audioDefinition.getFloat("timecode");
|
||||
cue.loops = audioDefinition.getInt("loops", 0);
|
||||
cue.endTimecode = audioDefinition.getFloat("endTimecode", 0);
|
||||
cue.resource = audioDefinition.getString("resource");
|
||||
m_audioCues.append(cue);
|
||||
m_completionTime = std::max(m_completionTime, cue.timecode);
|
||||
m_completionTime = std::max(m_completionTime, cue.endTimecode);
|
||||
}
|
||||
m_activeAudio = std::vector<AudioInstancePtr>(m_audioCues.size());
|
||||
|
||||
if (definition.contains("offset"))
|
||||
m_offset = jsonToVec2F(definition.get("offset"));
|
||||
else
|
||||
m_offset = {};
|
||||
|
||||
if (definition.contains("backgroundColor"))
|
||||
m_backgroundColor = jsonToVec4B(definition.get("backgroundColor"));
|
||||
else
|
||||
m_backgroundColor = {};
|
||||
m_backgroundFadeTime = definition.getFloat("backgroundFadeTime", 0);
|
||||
|
||||
m_completionTime += 2 * m_backgroundFadeTime;
|
||||
|
||||
m_scissor = definition.getBool("scissor", true);
|
||||
m_letterbox = definition.getBool("letterbox", true);
|
||||
m_skippable = definition.getBool("skippable", true);
|
||||
m_suppressInput = definition.getBool("suppressInput", true);
|
||||
m_muteSfx = definition.getBool("muteSfx", false);
|
||||
m_muteMusic = definition.getBool("muteMusic", false);
|
||||
|
||||
m_timer.reset();
|
||||
m_timer.start();
|
||||
}
|
||||
|
||||
void Cinematic::setPlayer(PlayerPtr player) {
|
||||
m_player = player;
|
||||
}
|
||||
|
||||
void Cinematic::update() {
|
||||
m_currentTimeSkip = {};
|
||||
for (auto timeSkip : m_timeSkips) {
|
||||
if (currentTimecode() >= timeSkip.availableTime && currentTimecode() < timeSkip.skipToTime)
|
||||
m_currentTimeSkip = timeSkip;
|
||||
}
|
||||
}
|
||||
|
||||
bool Cinematic::completed() const {
|
||||
for (size_t i = 0; i < m_audioCues.size(); ++i) {
|
||||
if (m_activeAudio[i] && !m_activeAudio[i]->finished())
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_timer.time() >= m_completionTime;
|
||||
}
|
||||
|
||||
bool Cinematic::completable() const {
|
||||
return m_completable;
|
||||
}
|
||||
|
||||
void Cinematic::render() {
|
||||
if (completed())
|
||||
return;
|
||||
|
||||
auto& guiContext = GuiContext::singleton();
|
||||
auto mixer = guiContext.mixer();
|
||||
auto renderer = guiContext.renderer();
|
||||
auto textPainter = guiContext.textPainter();
|
||||
|
||||
m_windowSize = Vec2F(renderer->screenSize());
|
||||
Vec2F screenWindowSize = Vec2F(vWidth * m_drawableScale, vHeight * m_drawableScale);
|
||||
m_drawableScale = std::min(m_windowSize[0] / vWidth, m_windowSize[1] / vHeight);
|
||||
m_drawableTranslation = Vec2F(0.5f * (m_windowSize[0] - vWidth * m_drawableScale), 0.5f * (m_windowSize[1] - vHeight * m_drawableScale));
|
||||
m_scissorRect = RectI::withSize(Vec2I::floor((m_windowSize / 2) - (screenWindowSize / 2)), Vec2I::ceil(screenWindowSize));
|
||||
|
||||
updateCamera(m_timer.time());
|
||||
|
||||
float fadeFactor = 1.0;
|
||||
if (m_backgroundFadeTime > 0) {
|
||||
if (m_timer.time() < m_backgroundFadeTime)
|
||||
fadeFactor = m_timer.time() / m_backgroundFadeTime;
|
||||
else if (m_completionTime - m_timer.time() < m_backgroundFadeTime)
|
||||
fadeFactor = max<float>(0.0f, m_completionTime - m_timer.time()) / m_backgroundFadeTime;
|
||||
}
|
||||
|
||||
if (m_backgroundColor) {
|
||||
Vec4B backgroundColor = m_backgroundColor.get();
|
||||
backgroundColor[3] *= fadeFactor;
|
||||
renderer->render(renderFlatRect(RectF::withSize(Vec2F(0, 0), m_windowSize), backgroundColor, 0.0f));
|
||||
}
|
||||
|
||||
if (m_letterbox && !m_backgroundColor) {
|
||||
Vec4B letterboxColor = Vec4B(0, 0, 0, 255 * fadeFactor);
|
||||
if (m_windowSize[0] / vWidth > m_windowSize[1] / vHeight) {
|
||||
renderer->render(renderFlatRect(RectF(0, 0, m_scissorRect.xMin(), m_windowSize[1]), letterboxColor, 0.0f));
|
||||
renderer->render(renderFlatRect(RectF(m_scissorRect.xMax(), 0, m_windowSize[0], m_windowSize[1]), letterboxColor, 0.0f));
|
||||
} else {
|
||||
renderer->render(renderFlatRect(RectF(0, 0, m_windowSize[0], m_scissorRect.yMin()), letterboxColor, 0.0f));
|
||||
renderer->render(renderFlatRect(RectF(0, m_scissorRect.yMax(), m_windowSize[0], m_windowSize[1]), letterboxColor, 0.0f));
|
||||
}
|
||||
}
|
||||
|
||||
if (fadeFactor < 1.0f)
|
||||
return;
|
||||
|
||||
if (m_scissor)
|
||||
renderer->setScissorRect(m_scissorRect);
|
||||
|
||||
String playerSpecies = "";
|
||||
if (m_player)
|
||||
playerSpecies = m_player->species();
|
||||
|
||||
for (auto panel : m_panels) {
|
||||
float drawableScale = m_drawableScale;
|
||||
Vec2F drawableTranslation = m_drawableTranslation;
|
||||
if (panel->useCamera) {
|
||||
drawableScale *= m_cameraZoom;
|
||||
drawableTranslation += m_cameraPan * drawableScale;
|
||||
}
|
||||
|
||||
auto values = determinePanelValues(panel, currentTimecode());
|
||||
if (values.completable)
|
||||
m_completable = true;
|
||||
if (!values.alpha)
|
||||
continue;
|
||||
auto frame = strf("%s", ((int)values.frame) % panel->animationFrames);
|
||||
auto alphaColor = Color::rgbaf(1.0f, 1.0f, 1.0f, values.alpha);
|
||||
for (auto const& d : panel->drawables) {
|
||||
Drawable drawable = Drawable(d.set("image", d.getString("image").replaceTags(StringMap<String>{{"species", playerSpecies}, {"frame", frame}})));
|
||||
drawable.translate(m_offset);
|
||||
drawable.scale(values.zoom);
|
||||
drawable.translate(values.position);
|
||||
drawable.color *= alphaColor;
|
||||
drawDrawable(move(drawable), drawableScale, drawableTranslation);
|
||||
}
|
||||
if (!panel->avatar.empty() && m_player) {
|
||||
for (auto drawable : m_player->portrait(PortraitModeNames.getLeft(panel->avatar))) {
|
||||
drawable.translate(m_offset);
|
||||
drawable.scale(values.zoom);
|
||||
drawable.translate(values.position);
|
||||
drawable.color *= alphaColor;
|
||||
drawDrawable(move(drawable), drawableScale, drawableTranslation);
|
||||
}
|
||||
}
|
||||
if (!panel->text.empty()) {
|
||||
textPainter->setFontSize(floor(panel->fontSize * drawableScale));
|
||||
auto fontColor = panel->fontColor;
|
||||
fontColor[3] *= values.alpha;
|
||||
textPainter->setFontColor(fontColor);
|
||||
Vec2F position = (m_offset + values.position + Vec2F(panel->textPosition.pos)) * drawableScale + drawableTranslation;
|
||||
TextPositioning tp = TextPositioning(position, panel->textPosition.hAnchor, panel->textPosition.vAnchor, {}, {});
|
||||
if (panel->textPosition.wrapWidth)
|
||||
tp.wrapWidth = floor(panel->textPosition.wrapWidth.get() * drawableScale);
|
||||
if (values.textPercentage < 1.0)
|
||||
tp.charLimit = floor(panel->text.length() * values.textPercentage);
|
||||
textPainter->renderText(panel->text, tp);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_scissor)
|
||||
renderer->setScissorRect({});
|
||||
|
||||
for (size_t i = 0; i < m_audioCues.size(); ++i) {
|
||||
if (m_audioCues[i].endTimecode > 0 && m_audioCues[i].endTimecode <= currentTimecode()) {
|
||||
if (!m_activeAudio[i])
|
||||
continue;
|
||||
m_activeAudio[i]->stop();
|
||||
} else if (m_audioCues[i].timecode <= currentTimecode()) {
|
||||
if (m_activeAudio[i])
|
||||
continue;
|
||||
AudioInstancePtr audioInstance = make_shared<AudioInstance>(*Root::singleton().assets()->audio(m_audioCues[i].resource));
|
||||
audioInstance->setLoops(m_audioCues[i].loops);
|
||||
audioInstance->setMixerGroup(MixerGroup::Cinematic);
|
||||
mixer->play(audioInstance);
|
||||
m_activeAudio[i] = audioInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Cinematic::drawDrawable(Drawable const& drawable, float drawableScale, Vec2F const& drawableTranslation) {
|
||||
auto& guiContext = GuiContext::singleton();
|
||||
auto renderer = guiContext.renderer();
|
||||
auto textureGroup = guiContext.assetTextureGroup();
|
||||
|
||||
if (drawable.isImage()) {
|
||||
auto const& imagePart = drawable.imagePart();
|
||||
auto texture = textureGroup->loadTexture(imagePart.image);
|
||||
auto textureSize = Vec2F(texture->size());
|
||||
|
||||
RectF imageRect(Vec2F(), textureSize);
|
||||
|
||||
Vec2F screenTranslation = drawable.position * drawableScale + drawableTranslation;
|
||||
|
||||
Vec2F lowerLeft =
|
||||
imagePart.transformation.transformVec2(Vec2F(imageRect.xMin(), imageRect.yMin())) * drawableScale
|
||||
+ screenTranslation;
|
||||
Vec2F lowerRight =
|
||||
imagePart.transformation.transformVec2(Vec2F(imageRect.xMax(), imageRect.yMin())) * drawableScale
|
||||
+ screenTranslation;
|
||||
Vec2F upperRight =
|
||||
imagePart.transformation.transformVec2(Vec2F(imageRect.xMax(), imageRect.yMax())) * drawableScale
|
||||
+ screenTranslation;
|
||||
Vec2F upperLeft =
|
||||
imagePart.transformation.transformVec2(Vec2F(imageRect.xMin(), imageRect.yMax())) * drawableScale
|
||||
+ screenTranslation;
|
||||
|
||||
Vec4B drawableColor = drawable.color.toRgba();
|
||||
|
||||
renderer->render(RenderQuad{move(texture),
|
||||
RenderVertex{lowerLeft, Vec2F(0, 0), drawableColor, 0.0f},
|
||||
RenderVertex{lowerRight, Vec2F(textureSize[0], 0), drawableColor, 0.0f},
|
||||
RenderVertex{upperRight, Vec2F(textureSize[0], textureSize[1]), drawableColor, 0.0f},
|
||||
RenderVertex{upperLeft, Vec2F(0, textureSize[1]), drawableColor, 0.0f}});
|
||||
} else {
|
||||
starAssert(drawable.part.empty());
|
||||
}
|
||||
}
|
||||
|
||||
void Cinematic::updateCamera(float timecode) {
|
||||
float startZoom = 1.0f;
|
||||
float startZoomTimecode = -1;
|
||||
float endZoom = startZoom;
|
||||
float endZoomTimecode = startZoomTimecode;
|
||||
|
||||
Vec2F startPan = {0, 0};
|
||||
float startPanTimecode = -1;
|
||||
Vec2F endPan = startPan;
|
||||
float endPanTimecode = startPanTimecode;
|
||||
|
||||
for (auto keyframe : m_cameraKeyFrames) {
|
||||
if (keyframe.timecode <= timecode) {
|
||||
startZoom = keyframe.zoom;
|
||||
startZoomTimecode = keyframe.timecode;
|
||||
}
|
||||
if (endZoomTimecode < timecode) {
|
||||
endZoom = keyframe.zoom;
|
||||
endZoomTimecode = keyframe.timecode;
|
||||
}
|
||||
|
||||
if (keyframe.timecode <= timecode) {
|
||||
startPan = keyframe.pan;
|
||||
startPanTimecode = keyframe.timecode;
|
||||
}
|
||||
if (endPanTimecode < timecode) {
|
||||
endPan = keyframe.pan;
|
||||
endPanTimecode = keyframe.timecode;
|
||||
}
|
||||
}
|
||||
|
||||
if (startZoom == endZoom)
|
||||
m_cameraZoom = startZoom;
|
||||
else if (timecode <= startZoomTimecode)
|
||||
m_cameraZoom = startZoom;
|
||||
else if (timecode >= endZoomTimecode)
|
||||
m_cameraZoom = endZoom;
|
||||
else
|
||||
m_cameraZoom = lerp((timecode - startZoomTimecode) / (endZoomTimecode - startZoomTimecode), startZoom, endZoom);
|
||||
|
||||
if (startPan == endPan)
|
||||
m_cameraPan = startPan;
|
||||
else if (timecode <= startPanTimecode)
|
||||
m_cameraPan = startPan;
|
||||
else if (timecode >= endPanTimecode)
|
||||
m_cameraPan = endPan;
|
||||
else
|
||||
m_cameraPan = lerp((timecode - startPanTimecode) / (endPanTimecode - startPanTimecode), startPan, endPan);
|
||||
}
|
||||
|
||||
float Cinematic::currentTimecode() const {
|
||||
return std::min((float)m_timer.time() - m_backgroundFadeTime, m_completionTime - 2 * m_backgroundFadeTime);
|
||||
}
|
||||
|
||||
Cinematic::PanelValues Cinematic::determinePanelValues(PanelPtr panel, float timecode) {
|
||||
if (panel->endTime != 0) {
|
||||
if (timecode > panel->endTime) {
|
||||
Cinematic::PanelValues result;
|
||||
result.alpha = 0;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (panel->startTime != 0) {
|
||||
if (timecode < panel->startTime) {
|
||||
Cinematic::PanelValues result;
|
||||
result.alpha = 0;
|
||||
return result;
|
||||
} else {
|
||||
timecode -= panel->startTime;
|
||||
}
|
||||
}
|
||||
|
||||
if (panel->loopTime != 0) {
|
||||
timecode = fmod(timecode, panel->loopTime);
|
||||
}
|
||||
|
||||
float startZoom = 0;
|
||||
float startZoomTimecode = -1;
|
||||
float endZoom = startZoom;
|
||||
float endZoomTimecode = startZoomTimecode;
|
||||
|
||||
float startAlpha = 0;
|
||||
float startAlphaTimecode = -1;
|
||||
float endAlpha = startAlpha;
|
||||
float endAlphaTimecode = startAlphaTimecode;
|
||||
|
||||
Vec2F startPosition = {};
|
||||
float startPositionTimecode = -1;
|
||||
Vec2F endPosition = startPosition;
|
||||
float endPositionTimecode = startPositionTimecode;
|
||||
|
||||
float startFrame = 0;
|
||||
float startFrameTimecode = -1;
|
||||
float endFrame = startFrame;
|
||||
float endFrameTimecode = startFrameTimecode;
|
||||
|
||||
float startTextPercentage = 1;
|
||||
float startTextPercentageTimecode = -1;
|
||||
float endTextPercentage = 1;
|
||||
float endTextPercentageTimecode = -1;
|
||||
|
||||
bool completable = false;
|
||||
|
||||
for (auto keyframe : panel->keyFrames) {
|
||||
if (keyframe.command.contains("complete")) {
|
||||
if (keyframe.command.getBool("complete"))
|
||||
if (keyframe.timecode <= timecode)
|
||||
completable = true;
|
||||
}
|
||||
|
||||
if (keyframe.command.contains("zoom")) {
|
||||
float zoom = keyframe.command.getFloat("zoom");
|
||||
if (keyframe.timecode <= timecode) {
|
||||
startZoom = zoom;
|
||||
startZoomTimecode = keyframe.timecode;
|
||||
}
|
||||
if (endZoomTimecode < timecode) {
|
||||
endZoom = zoom;
|
||||
endZoomTimecode = keyframe.timecode;
|
||||
}
|
||||
}
|
||||
|
||||
if (keyframe.command.contains("alpha")) {
|
||||
float alpha = keyframe.command.getFloat("alpha");
|
||||
if (keyframe.timecode <= timecode) {
|
||||
startAlpha = alpha;
|
||||
startAlphaTimecode = keyframe.timecode;
|
||||
}
|
||||
if (endAlphaTimecode < timecode) {
|
||||
endAlpha = alpha;
|
||||
endAlphaTimecode = keyframe.timecode;
|
||||
}
|
||||
}
|
||||
|
||||
if (keyframe.command.contains("position")) {
|
||||
Vec2F position = jsonToVec2F(keyframe.command.get("position"));
|
||||
if (keyframe.timecode <= timecode) {
|
||||
startPosition = position;
|
||||
startPositionTimecode = keyframe.timecode;
|
||||
}
|
||||
if (endPositionTimecode < timecode) {
|
||||
endPosition = position;
|
||||
endPositionTimecode = keyframe.timecode;
|
||||
}
|
||||
}
|
||||
|
||||
if (keyframe.command.contains("frame")) {
|
||||
float frame = keyframe.command.getFloat("frame");
|
||||
if (keyframe.timecode <= timecode) {
|
||||
startFrame = frame;
|
||||
startFrameTimecode = keyframe.timecode;
|
||||
}
|
||||
if (endFrameTimecode < timecode) {
|
||||
endFrame = frame;
|
||||
endFrameTimecode = keyframe.timecode;
|
||||
}
|
||||
}
|
||||
|
||||
if (keyframe.command.contains("textPercentage")) {
|
||||
float textPercentage = keyframe.command.getFloat("textPercentage");
|
||||
if (keyframe.timecode <= timecode) {
|
||||
startTextPercentage = textPercentage;
|
||||
startTextPercentageTimecode = keyframe.timecode;
|
||||
}
|
||||
if (endTextPercentageTimecode < timecode) {
|
||||
endTextPercentage = textPercentage;
|
||||
endTextPercentageTimecode = keyframe.timecode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cinematic::PanelValues result;
|
||||
|
||||
result.completable = completable;
|
||||
|
||||
if (startZoom == endZoom)
|
||||
result.zoom = startZoom;
|
||||
else if (timecode <= startZoomTimecode)
|
||||
result.zoom = startZoom;
|
||||
else if (timecode >= endZoomTimecode)
|
||||
result.zoom = endZoom;
|
||||
else
|
||||
result.zoom = lerp((timecode - startZoomTimecode) / (endZoomTimecode - startZoomTimecode), startZoom, endZoom);
|
||||
|
||||
if (startAlpha == endAlpha)
|
||||
result.alpha = startAlpha;
|
||||
else if (timecode <= startAlphaTimecode)
|
||||
result.alpha = startAlpha;
|
||||
else if (timecode >= endAlphaTimecode)
|
||||
result.alpha = endAlpha;
|
||||
else
|
||||
result.alpha = lerp((timecode - startAlphaTimecode) / (endAlphaTimecode - startAlphaTimecode), startAlpha, endAlpha);
|
||||
|
||||
if (startPosition == endPosition)
|
||||
result.position = startPosition;
|
||||
else if (timecode <= startPositionTimecode)
|
||||
result.position = startPosition;
|
||||
else if (timecode >= endPositionTimecode)
|
||||
result.position = endPosition;
|
||||
else
|
||||
result.position = lerp((timecode - startPositionTimecode) / (endPositionTimecode - startPositionTimecode), startPosition, endPosition);
|
||||
|
||||
if (startFrame == endFrame)
|
||||
result.frame = startFrame;
|
||||
else if (timecode <= startFrameTimecode)
|
||||
result.frame = startFrame;
|
||||
else if (timecode >= endFrameTimecode)
|
||||
result.frame = endFrame;
|
||||
else
|
||||
result.frame = lerp((timecode - startFrameTimecode) / (endFrameTimecode - startFrameTimecode), startFrame, endFrame);
|
||||
|
||||
if (startTextPercentage == endTextPercentage)
|
||||
result.textPercentage = startTextPercentage;
|
||||
else if (timecode <= startTextPercentageTimecode)
|
||||
result.textPercentage = startTextPercentage;
|
||||
else if (timecode >= endTextPercentageTimecode)
|
||||
result.textPercentage = endTextPercentage;
|
||||
else
|
||||
result.textPercentage =
|
||||
lerp((timecode - startTextPercentageTimecode) / (endTextPercentageTimecode - startTextPercentageTimecode),
|
||||
startTextPercentage,
|
||||
endTextPercentage);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void Cinematic::setTime(float timecode) {
|
||||
m_timer.setTime(timecode + m_backgroundFadeTime);
|
||||
}
|
||||
|
||||
void Cinematic::stop() {
|
||||
m_timeSkips.clear();
|
||||
m_cameraKeyFrames.clear();
|
||||
m_panels.clear();
|
||||
m_completionTime = 0;
|
||||
m_timer.stop();
|
||||
m_timer.reset();
|
||||
for (size_t i = 0; i < m_audioCues.size(); ++i) {
|
||||
if (m_activeAudio[i])
|
||||
m_activeAudio[i]->stop();
|
||||
}
|
||||
m_audioCues.clear();
|
||||
m_activeAudio.clear();
|
||||
m_completable = false;
|
||||
m_suppressInput = false;
|
||||
}
|
||||
|
||||
bool Cinematic::handleInputEvent(InputEvent const& event) {
|
||||
if (completed())
|
||||
return false;
|
||||
if (event.is<MouseButtonUpEvent>() || event.is<KeyUpEvent>())
|
||||
return false;
|
||||
if (event.is<KeyDownEvent>()) {
|
||||
if (m_currentTimeSkip) {
|
||||
setTime(m_currentTimeSkip.take().skipToTime);
|
||||
return true;
|
||||
} else if (m_skippable && GuiContext::singleton().actions(event).contains(InterfaceAction::CinematicSkip)) {
|
||||
stop();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return m_suppressInput;
|
||||
}
|
||||
|
||||
bool Cinematic::suppressInput() const {
|
||||
return m_suppressInput && !completed();
|
||||
}
|
||||
|
||||
bool Cinematic::muteSfx() const {
|
||||
return m_muteSfx && !completed();
|
||||
}
|
||||
|
||||
bool Cinematic::muteMusic() const {
|
||||
return m_muteMusic && !completed();
|
||||
}
|
||||
|
||||
}
|
143
source/frontend/StarCinematic.hpp
Normal file
143
source/frontend/StarCinematic.hpp
Normal file
|
@ -0,0 +1,143 @@
|
|||
#ifndef STAR_CINEMATIC_HPP
|
||||
#define STAR_CINEMATIC_HPP
|
||||
|
||||
#include "StarTime.hpp"
|
||||
#include "StarRenderer.hpp"
|
||||
#include "StarDrawable.hpp"
|
||||
#include "StarWorldCamera.hpp"
|
||||
#include "StarInputEvent.hpp"
|
||||
#include "StarTextPainter.hpp"
|
||||
#include "StarMixer.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(Cinematic);
|
||||
STAR_CLASS(Player);
|
||||
|
||||
class Cinematic {
|
||||
public:
|
||||
Cinematic();
|
||||
|
||||
void load(Json const& definition);
|
||||
|
||||
void setPlayer(PlayerPtr player);
|
||||
|
||||
void update();
|
||||
void render();
|
||||
|
||||
bool completed() const;
|
||||
bool completable() const;
|
||||
|
||||
// this won't synchronize audio, so it should only be used for testing
|
||||
void setTime(float timecode);
|
||||
|
||||
void stop();
|
||||
|
||||
bool handleInputEvent(InputEvent const& event);
|
||||
bool suppressInput() const;
|
||||
|
||||
bool muteSfx() const;
|
||||
bool muteMusic() const;
|
||||
|
||||
private:
|
||||
struct TimeSkip {
|
||||
float availableTime;
|
||||
float skipToTime;
|
||||
};
|
||||
|
||||
struct KeyFrame {
|
||||
float timecode;
|
||||
Json command;
|
||||
};
|
||||
|
||||
struct CameraKeyFrame {
|
||||
float timecode;
|
||||
float zoom;
|
||||
Vec2F pan;
|
||||
};
|
||||
|
||||
struct Panel {
|
||||
bool useCamera;
|
||||
String avatar;
|
||||
JsonArray drawables;
|
||||
int animationFrames;
|
||||
String text;
|
||||
TextPositioning textPosition;
|
||||
Vec4B fontColor;
|
||||
unsigned fontSize;
|
||||
List<KeyFrame> keyFrames;
|
||||
float startTime;
|
||||
float endTime;
|
||||
float loopTime;
|
||||
};
|
||||
typedef shared_ptr<Panel> PanelPtr;
|
||||
|
||||
struct PanelValues {
|
||||
float zoom;
|
||||
float alpha;
|
||||
Vec2F position;
|
||||
float frame;
|
||||
float textPercentage;
|
||||
bool completable;
|
||||
};
|
||||
|
||||
struct AudioCue {
|
||||
AudioCue() : timecode(), endTimecode() {}
|
||||
String resource;
|
||||
int loops;
|
||||
float timecode;
|
||||
float endTimecode;
|
||||
};
|
||||
|
||||
void drawDrawable(Drawable const& drawable, float drawableScale, Vec2F const& drawableTranslation);
|
||||
|
||||
void updateCamera(float timecode);
|
||||
|
||||
// since the clock time includes the background fade in/out time, this function gives the adjusted
|
||||
// timecode to use for events within the cinematic
|
||||
float currentTimecode() const;
|
||||
|
||||
PanelValues determinePanelValues(PanelPtr panel, float timecode);
|
||||
|
||||
List<TimeSkip> m_timeSkips;
|
||||
Maybe<TimeSkip> m_currentTimeSkip;
|
||||
|
||||
List<CameraKeyFrame> m_cameraKeyFrames;
|
||||
List<PanelPtr> m_panels;
|
||||
List<AudioCue> m_audioCues;
|
||||
std::vector<AudioInstancePtr> m_activeAudio;
|
||||
|
||||
// these include the time for background fades so they may not reflect the completion timecode
|
||||
Clock m_timer;
|
||||
float m_completionTime;
|
||||
|
||||
Maybe<Vec4B> m_backgroundColor;
|
||||
float m_backgroundFadeTime;
|
||||
|
||||
float m_cameraZoom;
|
||||
Vec2F m_cameraPan;
|
||||
|
||||
float m_drawableScale;
|
||||
Vec2F m_drawableTranslation;
|
||||
Vec2F m_windowSize;
|
||||
RectI m_scissorRect;
|
||||
|
||||
bool m_scissor;
|
||||
bool m_letterbox;
|
||||
|
||||
PlayerPtr m_player;
|
||||
|
||||
Vec2F m_offset;
|
||||
|
||||
bool m_skippable;
|
||||
bool m_suppressInput;
|
||||
|
||||
bool m_muteSfx;
|
||||
bool m_muteMusic;
|
||||
|
||||
bool m_completable;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
375
source/frontend/StarClientCommandProcessor.cpp
Normal file
375
source/frontend/StarClientCommandProcessor.cpp
Normal file
|
@ -0,0 +1,375 @@
|
|||
#include "StarClientCommandProcessor.hpp"
|
||||
#include "StarItem.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarItemDatabase.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarPlayerTech.hpp"
|
||||
#include "StarPlayerInventory.hpp"
|
||||
#include "StarPlayerLog.hpp"
|
||||
#include "StarWorldClient.hpp"
|
||||
#include "StarAiInterface.hpp"
|
||||
#include "StarQuestInterface.hpp"
|
||||
#include "StarStatistics.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
ClientCommandProcessor::ClientCommandProcessor(UniverseClientPtr universeClient, CinematicPtr cinematicOverlay,
|
||||
MainInterfacePaneManager* paneManager, StringMap<StringList> macroCommands)
|
||||
: m_universeClient(move(universeClient)), m_cinematicOverlay(move(cinematicOverlay)),
|
||||
m_paneManager(paneManager), m_macroCommands(move(macroCommands)) {
|
||||
m_builtinCommands = {
|
||||
{"reload", bind(&ClientCommandProcessor::reload, this)},
|
||||
{"whoami", bind(&ClientCommandProcessor::whoami, this)},
|
||||
{"gravity", bind(&ClientCommandProcessor::gravity, this)},
|
||||
{"debug", bind(&ClientCommandProcessor::debug, this)},
|
||||
{"boxes", bind(&ClientCommandProcessor::boxes, this)},
|
||||
{"fullbright", bind(&ClientCommandProcessor::fullbright, this)},
|
||||
{"setGravity", bind(&ClientCommandProcessor::setGravity, this, _1)},
|
||||
{"resetGravity", bind(&ClientCommandProcessor::resetGravity, this)},
|
||||
{"fixedCamera", bind(&ClientCommandProcessor::fixedCamera, this)},
|
||||
{"monochromeLighting", bind(&ClientCommandProcessor::monochromeLighting, this)},
|
||||
{"radioMessage", bind(&ClientCommandProcessor::radioMessage, this, _1)},
|
||||
{"clearRadioMessages", bind(&ClientCommandProcessor::clearRadioMessages, this)},
|
||||
{"clearCinematics", bind(&ClientCommandProcessor::clearCinematics, this)},
|
||||
{"startQuest", bind(&ClientCommandProcessor::startQuest, this, _1)},
|
||||
{"completeQuest", bind(&ClientCommandProcessor::completeQuest, this, _1)},
|
||||
{"failQuest", bind(&ClientCommandProcessor::failQuest, this, _1)},
|
||||
{"previewNewQuest", bind(&ClientCommandProcessor::previewNewQuest, this, _1)},
|
||||
{"previewQuestComplete", bind(&ClientCommandProcessor::previewQuestComplete, this, _1)},
|
||||
{"previewQuestFailed", bind(&ClientCommandProcessor::previewQuestFailed, this, _1)},
|
||||
{"clearScannedObjects", bind(&ClientCommandProcessor::clearScannedObjects, this)},
|
||||
{"played", bind(&ClientCommandProcessor::playTime, this)},
|
||||
{"deaths", bind(&ClientCommandProcessor::deathCount, this)},
|
||||
{"cinema", bind(&ClientCommandProcessor::cinema, this, _1)},
|
||||
{"suicide", bind(&ClientCommandProcessor::suicide, this)},
|
||||
{"naked", bind(&ClientCommandProcessor::naked, this)},
|
||||
{"resetAchievements", bind(&ClientCommandProcessor::resetAchievements, this)},
|
||||
{"statistic", bind(&ClientCommandProcessor::statistic, this, _1)},
|
||||
{"giveessentialitem", bind(&ClientCommandProcessor::giveEssentialItem, this, _1)},
|
||||
{"maketechavailable", bind(&ClientCommandProcessor::makeTechAvailable, this, _1)},
|
||||
{"enabletech", bind(&ClientCommandProcessor::enableTech, this, _1)},
|
||||
{"upgradeship", bind(&ClientCommandProcessor::upgradeShip, this, _1)}
|
||||
};
|
||||
}
|
||||
|
||||
bool ClientCommandProcessor::adminCommandAllowed() const {
|
||||
return Root::singleton().configuration()->get("allowAdminCommandsFromAnyone").toBool() ||
|
||||
m_universeClient->mainPlayer()->isAdmin();
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::previewQuestPane(StringList const& arguments, function<PanePtr(QuestPtr)> createPane) {
|
||||
Maybe<String> templateId = {};
|
||||
templateId = arguments[0];
|
||||
if (auto quest = createPreviewQuest(*templateId, arguments.at(1), arguments.at(2), m_universeClient->mainPlayer().get())) {
|
||||
auto pane = createPane(quest);
|
||||
m_paneManager->displayPane(PaneLayer::ModalWindow, pane);
|
||||
return "Previewed quest";
|
||||
}
|
||||
return "No such quest";
|
||||
}
|
||||
|
||||
StringList ClientCommandProcessor::handleCommand(String const& commandLine) {
|
||||
try {
|
||||
if (!commandLine.beginsWith("/"))
|
||||
throw StarException("ClientCommandProcessor expected command, does not start with '/'");
|
||||
|
||||
String allArguments = commandLine.substr(1);
|
||||
String command = allArguments.extract();
|
||||
auto arguments = m_parser.tokenizeToStringList(allArguments);
|
||||
|
||||
StringList result;
|
||||
if (auto builtinCommand = m_builtinCommands.maybe(command)) {
|
||||
result.append((*builtinCommand)(arguments));
|
||||
} else if (auto macroCommand = m_macroCommands.maybe(command)) {
|
||||
for (auto const& c : *macroCommand) {
|
||||
if (c.beginsWith("/"))
|
||||
result.appendAll(handleCommand(c));
|
||||
else
|
||||
result.append(c);
|
||||
}
|
||||
} else {
|
||||
m_universeClient->sendChat(commandLine, ChatSendMode::Broadcast);
|
||||
}
|
||||
return result;
|
||||
} catch (ShellParsingException const& e) {
|
||||
Logger::error("Shell parsing exception: %s", outputException(e, false));
|
||||
return {"Shell parsing exception"};
|
||||
} catch (std::exception const& e) {
|
||||
Logger::error("Exception caught handling client command %s: %s", commandLine, outputException(e, true));
|
||||
return {strf("Exception caught handling client command %s", commandLine)};
|
||||
}
|
||||
}
|
||||
|
||||
bool ClientCommandProcessor::debugDisplayEnabled() const {
|
||||
return m_debugDisplayEnabled;
|
||||
}
|
||||
|
||||
bool ClientCommandProcessor::fixedCameraEnabled() const {
|
||||
return m_fixedCameraEnabled;
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::reload() {
|
||||
Root::singleton().reload();
|
||||
return "Client Star::Root reloaded";
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::whoami() {
|
||||
return strf("Client: You are %s. You are %san Admin.",
|
||||
m_universeClient->mainPlayer()->name(), m_universeClient->mainPlayer()->isAdmin() ? "" : "not ");
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::gravity() {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
return strf("%s", m_universeClient->worldClient()->gravity(m_universeClient->mainPlayer()->position()));
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::debug() {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
m_debugDisplayEnabled = !m_debugDisplayEnabled;
|
||||
return strf("Debug display %s", m_debugDisplayEnabled ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::boxes() {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
return strf("Geometry debug display %s",
|
||||
m_universeClient->worldClient()->toggleCollisionDebug()
|
||||
? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::fullbright() {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
return strf("Fullbright render lighting %s",
|
||||
m_universeClient->worldClient()->toggleFullbright()
|
||||
? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::setGravity(StringList const& arguments) {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
m_universeClient->worldClient()->overrideGravity(lexicalCast<float>(arguments.at(0)));
|
||||
return strf("Gravity set to %s, the change is LOCAL ONLY", arguments.at(0));
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::resetGravity() {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
m_universeClient->worldClient()->resetGravity();
|
||||
return "Gravity reset";
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::fixedCamera() {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
m_fixedCameraEnabled = !m_fixedCameraEnabled;
|
||||
return strf("Fixed camera %s", m_fixedCameraEnabled ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::monochromeLighting() {
|
||||
bool monochrome = !Root::singleton().configuration()->get("monochromeLighting").toBool();
|
||||
Root::singleton().configuration()->set("monochromeLighting", monochrome);
|
||||
return strf("Monochrome lighting %s", monochrome ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::radioMessage(StringList const& arguments) {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
if (arguments.size() != 1)
|
||||
return "Must provide one argument";
|
||||
|
||||
m_universeClient->mainPlayer()->queueRadioMessage(arguments.at(0));
|
||||
return "Queued radio message";
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::clearRadioMessages() {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
m_universeClient->mainPlayer()->log()->clearRadioMessages();
|
||||
return "Player radio message records cleared!";
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::clearCinematics() {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
m_universeClient->mainPlayer()->log()->clearCinematics();
|
||||
return "Player cinematic records cleared!";
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::startQuest(StringList const& arguments) {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
auto questArc = QuestArcDescriptor::fromJson(Json::parse(arguments.at(0)));
|
||||
m_universeClient->questManager()->offer(make_shared<Quest>(questArc, 0, m_universeClient->mainPlayer().get()));
|
||||
return "Quest started";
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::completeQuest(StringList const& arguments) {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
m_universeClient->questManager()->getQuest(arguments.at(0))->complete();
|
||||
return strf("Quest %s complete", arguments.at(0));
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::failQuest(StringList const& arguments) {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
m_universeClient->questManager()->getQuest(arguments.at(0))->fail();
|
||||
return strf("Quest %s failed", arguments.at(0));
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::previewNewQuest(StringList const& arguments) {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
return previewQuestPane(arguments, [this](QuestPtr const& quest) {
|
||||
return make_shared<NewQuestInterface>(m_universeClient->questManager(), quest, m_universeClient->mainPlayer());
|
||||
});
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::previewQuestComplete(StringList const& arguments) {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
return previewQuestPane(arguments, [this](QuestPtr const& quest) {
|
||||
return make_shared<QuestCompleteInterface>(quest, m_universeClient->mainPlayer(), CinematicPtr{});
|
||||
});
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::previewQuestFailed(StringList const& arguments) {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
return previewQuestPane(arguments, [this](QuestPtr const& quest) {
|
||||
return make_shared<QuestFailedInterface>(quest, m_universeClient->mainPlayer());
|
||||
});
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::clearScannedObjects() {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
m_universeClient->mainPlayer()->log()->clearScannedObjects();
|
||||
return "Player scanned objects cleared!";
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::playTime() {
|
||||
return strf("Total play time: %s", Time::printDuration(m_universeClient->mainPlayer()->log()->playTime()));
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::deathCount() {
|
||||
auto deaths = m_universeClient->mainPlayer()->log()->deathCount();
|
||||
return strf("Total deaths: %s%s", deaths, deaths == 0 ? ". Well done!" : "");
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::cinema(StringList const& arguments) {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
m_cinematicOverlay->load(Root::singleton().assets()->json(arguments.at(0)));
|
||||
if (arguments.size() > 1)
|
||||
m_cinematicOverlay->setTime(lexicalCast<float>(arguments.at(1)));
|
||||
return strf("Started cinematic %s at %s", arguments.at(0), arguments.size() > 1 ? arguments.at(1) : "beginning");
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::suicide() {
|
||||
m_universeClient->mainPlayer()->kill();
|
||||
return "You are now dead";
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::naked() {
|
||||
auto playerInventory = m_universeClient->mainPlayer()->inventory();
|
||||
for (auto slot : EquipmentSlotNames.leftValues())
|
||||
playerInventory->addItems(playerInventory->addToBags(playerInventory->takeSlot(slot)));
|
||||
return "You are now naked";
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::resetAchievements() {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
if (m_universeClient->statistics()->reset()) {
|
||||
return "Achievements reset";
|
||||
}
|
||||
return "Unable to reset achievements";
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::statistic(StringList const& arguments) {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
StringList values;
|
||||
for (String const& statName : arguments) {
|
||||
values.append(strf("%s = %s", statName, m_universeClient->statistics()->stat(statName)));
|
||||
}
|
||||
return values.join("\n");
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::giveEssentialItem(StringList const& arguments) {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
if (arguments.size() < 2)
|
||||
return "Not enough arguments to /giveessentialitem";
|
||||
|
||||
try {
|
||||
auto item = Root::singleton().itemDatabase()->item(ItemDescriptor(arguments.at(0)));
|
||||
auto slot = EssentialItemNames.getLeft(arguments.at(1));
|
||||
m_universeClient->mainPlayer()->inventory()->setEssentialItem(slot, item);
|
||||
return strf("Put %s in player slot %s", item->name(), arguments.at(1));
|
||||
} catch (MapException e) {
|
||||
return strf("Invalid essential item slot %s.", arguments.at(1));
|
||||
}
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::makeTechAvailable(StringList const& arguments) {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
if (arguments.size() == 0)
|
||||
return "Not enouch arguments to /maketechavailable";
|
||||
|
||||
m_universeClient->mainPlayer()->techs()->makeAvailable(arguments.at(0));
|
||||
return strf("Added %s to player's visible techs", arguments.at(0));
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::enableTech(StringList const& arguments) {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
if (arguments.size() == 0)
|
||||
return "Not enouch arguments to /enabletech";
|
||||
|
||||
m_universeClient->mainPlayer()->techs()->makeAvailable(arguments.at(0));
|
||||
m_universeClient->mainPlayer()->techs()->enable(arguments.at(0));
|
||||
return strf("Player tech %s enabled", arguments.at(0));
|
||||
}
|
||||
|
||||
String ClientCommandProcessor::upgradeShip(StringList const& arguments) {
|
||||
if (!adminCommandAllowed())
|
||||
return "You must be an admin to use this command.";
|
||||
|
||||
if (arguments.size() == 0)
|
||||
return "Not enouch arguments to /upgradeship";
|
||||
|
||||
auto shipUpgrades = Json::parseJson(arguments.at(0));
|
||||
m_universeClient->rpcInterface()->invokeRemote("ship.applyShipUpgrades", shipUpgrades);
|
||||
return strf("Upgraded ship");
|
||||
}
|
||||
|
||||
}
|
73
source/frontend/StarClientCommandProcessor.hpp
Normal file
73
source/frontend/StarClientCommandProcessor.hpp
Normal file
|
@ -0,0 +1,73 @@
|
|||
#ifndef STAR_CLIENT_COMMAND_PROCESSOR_HPP
|
||||
#define STAR_CLIENT_COMMAND_PROCESSOR_HPP
|
||||
|
||||
#include "StarShellParser.hpp"
|
||||
#include "StarLuaComponents.hpp"
|
||||
#include "StarLuaRoot.hpp"
|
||||
#include "StarUniverseClient.hpp"
|
||||
#include "StarQuestManager.hpp"
|
||||
#include "StarCinematic.hpp"
|
||||
#include "StarMainInterfaceTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
class ClientCommandProcessor {
|
||||
public:
|
||||
ClientCommandProcessor(UniverseClientPtr universeClient, CinematicPtr cinematicOverlay,
|
||||
MainInterfacePaneManager* paneManager, StringMap<StringList> macroCommands);
|
||||
|
||||
StringList handleCommand(String const& commandLine);
|
||||
|
||||
bool debugDisplayEnabled() const;
|
||||
bool fixedCameraEnabled() const;
|
||||
|
||||
private:
|
||||
bool adminCommandAllowed() const;
|
||||
String previewQuestPane(StringList const& arguments, function<PanePtr(QuestPtr)> createPane);
|
||||
|
||||
String reload();
|
||||
String whoami();
|
||||
String gravity();
|
||||
String debug();
|
||||
String boxes();
|
||||
String fullbright();
|
||||
String setGravity(StringList const& arguments);
|
||||
String resetGravity();
|
||||
String fixedCamera();
|
||||
String monochromeLighting();
|
||||
String radioMessage(StringList const& arguments);
|
||||
String clearRadioMessages();
|
||||
String clearCinematics();
|
||||
String startQuest(StringList const& arguments);
|
||||
String completeQuest(StringList const& arguments);
|
||||
String failQuest(StringList const& arguments);
|
||||
String previewNewQuest(StringList const& arguments);
|
||||
String previewQuestComplete(StringList const& arguments);
|
||||
String previewQuestFailed(StringList const& arguments);
|
||||
String clearScannedObjects();
|
||||
String playTime();
|
||||
String deathCount();
|
||||
String cinema(StringList const& arguments);
|
||||
String suicide();
|
||||
String naked();
|
||||
String resetAchievements();
|
||||
String statistic(StringList const& arguments);
|
||||
String giveEssentialItem(StringList const& arguments);
|
||||
String makeTechAvailable(StringList const& arguments);
|
||||
String enableTech(StringList const& arguments);
|
||||
String upgradeShip(StringList const& arguments);
|
||||
|
||||
UniverseClientPtr m_universeClient;
|
||||
CinematicPtr m_cinematicOverlay;
|
||||
MainInterfacePaneManager* m_paneManager;
|
||||
CaseInsensitiveStringMap<function<String(StringList const&)>> m_builtinCommands;
|
||||
StringMap<StringList> m_macroCommands;
|
||||
ShellParser m_parser;
|
||||
LuaBaseComponent m_scriptComponent;
|
||||
bool m_debugDisplayEnabled = false;
|
||||
bool m_fixedCameraEnabled = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
159
source/frontend/StarCodexInterface.cpp
Normal file
159
source/frontend/StarCodexInterface.cpp
Normal file
|
@ -0,0 +1,159 @@
|
|||
#include "StarCodexInterface.hpp"
|
||||
#include "StarCodex.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarListWidget.hpp"
|
||||
#include "StarStackWidget.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarButtonGroup.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
CodexInterface::CodexInterface(PlayerPtr player) {
|
||||
m_player = player;
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
GuiReader reader;
|
||||
|
||||
reader.registerCallback("close", [=](Widget*) { dismiss(); });
|
||||
reader.registerCallback("prevButton", [=](Widget*) { backwardPage(); });
|
||||
reader.registerCallback("nextButton", [=](Widget*) { forwardPage(); });
|
||||
reader.registerCallback("selectCodex", [=](Widget*) { showSelectedContents(); });
|
||||
reader.registerCallback("updateSpecies", [=](Widget*) { updateSpecies(); });
|
||||
|
||||
reader.construct(assets->json("/interface/windowconfig/codex.config:paneLayout"), this);
|
||||
|
||||
m_speciesTabs = fetchChild<ButtonGroupWidget>("speciesTabs");
|
||||
m_selectLabel = fetchChild<LabelWidget>("selectLabel");
|
||||
m_titleLabel = fetchChild<LabelWidget>("titleLabel");
|
||||
m_bookList = fetchChild<ListWidget>("scrollArea.bookList");
|
||||
m_pageContent = fetchChild<LabelWidget>("pageText");
|
||||
m_pageLabelWidget = fetchChild<LabelWidget>("pageLabel");
|
||||
m_pageNumberWidget = fetchChild<LabelWidget>("pageNum");
|
||||
m_prevPageButton = fetchChild<ButtonWidget>("prevButton");
|
||||
m_nextPageButton = fetchChild<ButtonWidget>("nextButton");
|
||||
|
||||
m_selectText = assets->json("/interface/windowconfig/codex.config:selectText").toString();
|
||||
|
||||
m_currentPage = 0;
|
||||
updateSpecies();
|
||||
setupPageText();
|
||||
}
|
||||
|
||||
void CodexInterface::show() {
|
||||
Pane::show();
|
||||
updateCodexList();
|
||||
}
|
||||
|
||||
void CodexInterface::tick() {
|
||||
updateCodexList();
|
||||
}
|
||||
|
||||
void CodexInterface::showSelectedContents() {
|
||||
if (m_bookList->selectedItem() == NPos || m_bookList->selectedItem() >= m_codexList.size())
|
||||
return;
|
||||
|
||||
showContents(m_codexList[m_bookList->selectedItem()].first);
|
||||
}
|
||||
|
||||
void CodexInterface::showContents(String const& codexId) {
|
||||
CodexConstPtr result;
|
||||
for (auto entry : m_codexList)
|
||||
if (entry.first->id() == codexId) {
|
||||
result = entry.first;
|
||||
break;
|
||||
}
|
||||
if (result)
|
||||
showContents(result);
|
||||
}
|
||||
|
||||
void CodexInterface::showContents(CodexConstPtr codex) {
|
||||
if (m_player->codexes()->markCodexRead(codex->id()))
|
||||
updateCodexList();
|
||||
m_currentCodex = codex;
|
||||
m_currentPage = 0;
|
||||
setupPageText();
|
||||
}
|
||||
|
||||
void CodexInterface::forwardPage() {
|
||||
if (m_currentCodex && m_currentPage < m_currentCodex->pageCount() - 1) {
|
||||
++m_currentPage;
|
||||
setupPageText();
|
||||
}
|
||||
}
|
||||
|
||||
void CodexInterface::backwardPage() {
|
||||
if (m_currentCodex && m_currentPage > 0) {
|
||||
--m_currentPage;
|
||||
setupPageText();
|
||||
}
|
||||
}
|
||||
|
||||
bool CodexInterface::showNewCodex() {
|
||||
if (auto newCodex = m_player->codexes()->firstNewCodex()) {
|
||||
for (auto button : m_speciesTabs->buttons()) {
|
||||
if (button->data().getString("species") == newCodex->species()) {
|
||||
m_speciesTabs->select(m_speciesTabs->id(button));
|
||||
break;
|
||||
}
|
||||
}
|
||||
showContents(newCodex);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void CodexInterface::updateSpecies() {
|
||||
String newSpecies = "other";
|
||||
if (auto speciesButton = m_speciesTabs->checkedButton())
|
||||
newSpecies = speciesButton->data().getString("species");
|
||||
if (newSpecies != m_currentSpecies) {
|
||||
m_currentCodex = {};
|
||||
m_currentSpecies = newSpecies;
|
||||
m_bookList->clearSelected();
|
||||
setupPageText();
|
||||
}
|
||||
m_selectLabel->setText(m_selectText.replaceTags(StringMap<String>{{"species", m_currentSpecies}}).titleCase());
|
||||
}
|
||||
|
||||
void CodexInterface::setupPageText() {
|
||||
if (m_currentCodex) {
|
||||
m_pageContent->setText(m_currentCodex->page(m_currentPage));
|
||||
m_pageLabelWidget->show();
|
||||
m_pageNumberWidget->setText(strf("%d of %d", m_currentPage + 1, m_currentCodex->pageCount()));
|
||||
m_titleLabel->setText(m_currentCodex->title());
|
||||
m_nextPageButton->setEnabled(m_currentPage < m_currentCodex->pageCount() - 1);
|
||||
m_prevPageButton->setEnabled(m_currentPage > 0);
|
||||
} else {
|
||||
m_pageContent->setText("");
|
||||
m_pageLabelWidget->hide();
|
||||
m_pageNumberWidget->setText("");
|
||||
m_titleLabel->setText("");
|
||||
m_nextPageButton->disable();
|
||||
m_prevPageButton->disable();
|
||||
}
|
||||
}
|
||||
|
||||
void CodexInterface::updateCodexList() {
|
||||
auto newCodexList = m_player->codexes()->codexes();
|
||||
filter(newCodexList, [&](auto const& p) {
|
||||
return p.first->species() == m_currentSpecies;
|
||||
});
|
||||
if (m_codexList != newCodexList) {
|
||||
m_bookList->removeAllChildren();
|
||||
m_codexList = newCodexList;
|
||||
for (auto entry : m_codexList) {
|
||||
auto newEntry = m_bookList->addItem();
|
||||
newEntry->fetchChild<LabelWidget>("bookName")->setText(entry.first->title());
|
||||
newEntry->fetchChild<ImageWidget>("bookIcon")->setImage(entry.first->icon());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
67
source/frontend/StarCodexInterface.hpp
Normal file
67
source/frontend/StarCodexInterface.hpp
Normal file
|
@ -0,0 +1,67 @@
|
|||
#ifndef STAR_CODEX_INTERFACE_HPP
|
||||
#define STAR_CODEX_INTERFACE_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
#include "StarPlayerCodexes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(Player);
|
||||
STAR_CLASS(JsonRpcInterface);
|
||||
STAR_CLASS(StackWidget);
|
||||
STAR_CLASS(ListWidget);
|
||||
STAR_CLASS(LabelWidget);
|
||||
STAR_CLASS(ButtonWidget);
|
||||
STAR_CLASS(ButtonGroupWidget);
|
||||
STAR_CLASS(Codex);
|
||||
|
||||
STAR_CLASS(CodexInterface);
|
||||
class CodexInterface : public Pane {
|
||||
public:
|
||||
CodexInterface(PlayerPtr player);
|
||||
|
||||
virtual void show() override;
|
||||
virtual void tick() override;
|
||||
|
||||
void showTitles();
|
||||
void showSelectedContents();
|
||||
void showContents(String const& codexId);
|
||||
void showContents(CodexConstPtr codex);
|
||||
|
||||
void forwardPage();
|
||||
void backwardPage();
|
||||
|
||||
bool showNewCodex();
|
||||
|
||||
private:
|
||||
void updateSpecies();
|
||||
void setupPageText();
|
||||
void updateCodexList();
|
||||
|
||||
StackWidgetPtr m_stack;
|
||||
|
||||
ListWidgetPtr m_bookList;
|
||||
|
||||
CodexConstPtr m_currentCodex;
|
||||
size_t m_currentPage;
|
||||
|
||||
ButtonGroupWidgetPtr m_speciesTabs;
|
||||
LabelWidgetPtr m_selectLabel;
|
||||
LabelWidgetPtr m_titleLabel;
|
||||
LabelWidgetPtr m_pageContent;
|
||||
LabelWidgetPtr m_pageLabelWidget;
|
||||
LabelWidgetPtr m_pageNumberWidget;
|
||||
ButtonWidgetPtr m_prevPageButton;
|
||||
ButtonWidgetPtr m_nextPageButton;
|
||||
ButtonWidgetPtr m_backButton;
|
||||
|
||||
String m_selectText;
|
||||
String m_currentSpecies;
|
||||
|
||||
PlayerPtr m_player;
|
||||
List<PlayerCodexes::CodexEntry> m_codexList;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
96
source/frontend/StarConfirmationDialog.cpp
Normal file
96
source/frontend/StarConfirmationDialog.cpp
Normal file
|
@ -0,0 +1,96 @@
|
|||
#include "StarConfirmationDialog.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarRandom.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
ConfirmationDialog::ConfirmationDialog() {}
|
||||
|
||||
void ConfirmationDialog::displayConfirmation(Json const& dialogConfig, RpcPromiseKeeper<Json> resultPromise) {
|
||||
m_resultPromise = resultPromise;
|
||||
displayConfirmation(dialogConfig, [this] (Widget*) { m_resultPromise->fulfill(true); }, [this] (Widget*) { m_resultPromise->fulfill(false); } );
|
||||
}
|
||||
|
||||
void ConfirmationDialog::displayConfirmation(Json const& dialogConfig, WidgetCallbackFunc okCallback, WidgetCallbackFunc cancelCallback) {
|
||||
Json config;
|
||||
if (dialogConfig.isType(Json::Type::String))
|
||||
config = Root::singleton().assets()->json(dialogConfig.toString());
|
||||
else
|
||||
config = dialogConfig;
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
removeAllChildren();
|
||||
|
||||
GuiReader reader;
|
||||
|
||||
m_okCallback = move(okCallback);
|
||||
m_cancelCallback = move(cancelCallback);
|
||||
|
||||
reader.registerCallback("close", bind(&ConfirmationDialog::dismiss, this));
|
||||
reader.registerCallback("cancel", bind(&ConfirmationDialog::dismiss, this));
|
||||
reader.registerCallback("ok", bind(&ConfirmationDialog::ok, this));
|
||||
|
||||
m_confirmed = false;
|
||||
|
||||
String paneLayoutPath =
|
||||
config.optString("paneLayout").value("/interface/windowconfig/confirmation.config:paneLayout");
|
||||
reader.construct(assets->json(paneLayoutPath), this);
|
||||
|
||||
ImageWidgetPtr titleIcon = {};
|
||||
if (config.contains("icon"))
|
||||
titleIcon = make_shared<ImageWidget>(config.getString("icon"));
|
||||
|
||||
setTitle(titleIcon, config.getString("title", ""), config.getString("subtitle", ""));
|
||||
fetchChild<LabelWidget>("message")->setText(config.getString("message"));
|
||||
|
||||
if (config.contains("okCaption"))
|
||||
fetchChild<ButtonWidget>("ok")->setText(config.getString("okCaption"));
|
||||
if (config.contains("cancelCaption"))
|
||||
fetchChild<ButtonWidget>("cancel")->setText(config.getString("cancelCaption"));
|
||||
|
||||
m_sourceEntityId = config.optInt("sourceEntityId");
|
||||
|
||||
for (auto image : config.optObject("images").value({})) {
|
||||
auto widget = fetchChild<ImageWidget>(image.first);
|
||||
if (image.second.isType(Json::Type::String))
|
||||
widget->setImage(image.second.toString());
|
||||
else
|
||||
widget->setDrawables(image.second.toArray().transformed(construct<Drawable>()));
|
||||
}
|
||||
|
||||
for (auto label : config.optObject("labels").value({})) {
|
||||
auto widget = fetchChild<LabelWidget>(label.first);
|
||||
widget->setText(label.second.toString());
|
||||
}
|
||||
|
||||
show();
|
||||
auto sound = Random::randValueFrom(Root::singleton().assets()->json("/interface/windowconfig/confirmation.config:onShowSound").toArray(), "").toString();
|
||||
|
||||
if (!sound.empty())
|
||||
context()->playAudio(sound);
|
||||
}
|
||||
|
||||
Maybe<EntityId> ConfirmationDialog::sourceEntityId() {
|
||||
return m_sourceEntityId;
|
||||
}
|
||||
|
||||
void ConfirmationDialog::dismissed() {
|
||||
if (!m_confirmed)
|
||||
m_cancelCallback(this);
|
||||
|
||||
Pane::dismissed();
|
||||
}
|
||||
|
||||
void ConfirmationDialog::ok() {
|
||||
m_okCallback(this);
|
||||
m_confirmed = true;
|
||||
dismiss();
|
||||
}
|
||||
|
||||
}
|
38
source/frontend/StarConfirmationDialog.hpp
Normal file
38
source/frontend/StarConfirmationDialog.hpp
Normal file
|
@ -0,0 +1,38 @@
|
|||
#ifndef STAR_CONFIRMATION_DIALOG_HPP
|
||||
#define STAR_CONFIRMATION_DIALOG_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
#include "StarRpcPromise.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(ConfirmationDialog);
|
||||
|
||||
class ConfirmationDialog : public Pane {
|
||||
public:
|
||||
ConfirmationDialog();
|
||||
|
||||
virtual ~ConfirmationDialog() {}
|
||||
|
||||
void displayConfirmation(Json const& dialogConfig, RpcPromiseKeeper<Json> resultPromise);
|
||||
void displayConfirmation(Json const& dialogConfig, WidgetCallbackFunc okCallback, WidgetCallbackFunc cancelCallback);
|
||||
|
||||
Maybe<EntityId> sourceEntityId();
|
||||
|
||||
void dismissed() override;
|
||||
|
||||
private:
|
||||
void ok();
|
||||
|
||||
WidgetCallbackFunc m_okCallback;
|
||||
WidgetCallbackFunc m_cancelCallback;
|
||||
bool m_confirmed;
|
||||
|
||||
Maybe<EntityId> m_sourceEntityId;
|
||||
|
||||
Maybe<RpcPromiseKeeper<Json>> m_resultPromise;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
92
source/frontend/StarContainerInteractor.cpp
Normal file
92
source/frontend/StarContainerInteractor.cpp
Normal file
|
@ -0,0 +1,92 @@
|
|||
#include "StarContainerInteractor.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
void ContainerInteractor::openContainer(ContainerEntityPtr containerEntity) {
|
||||
if (m_openContainer && m_openContainer->inWorld())
|
||||
m_openContainer->containerClose();
|
||||
m_openContainer = move(containerEntity);
|
||||
if (m_openContainer) {
|
||||
starAssert(m_openContainer->inWorld());
|
||||
m_openContainer->containerOpen();
|
||||
}
|
||||
}
|
||||
|
||||
void ContainerInteractor::closeContainer() {
|
||||
openContainer({});
|
||||
}
|
||||
|
||||
bool ContainerInteractor::containerOpen() const {
|
||||
return openContainerId() != NullEntityId;
|
||||
}
|
||||
|
||||
EntityId ContainerInteractor::openContainerId() const {
|
||||
if (m_openContainer && !m_openContainer->inWorld())
|
||||
m_openContainer = {};
|
||||
|
||||
if (m_openContainer)
|
||||
return m_openContainer->entityId();
|
||||
|
||||
return NullEntityId;
|
||||
}
|
||||
|
||||
ContainerEntityPtr const& ContainerInteractor::openContainer() const {
|
||||
if (m_openContainer && !m_openContainer->inWorld())
|
||||
m_openContainer = {};
|
||||
|
||||
if (!m_openContainer)
|
||||
throw StarException("ContainerInteractor has no open container");
|
||||
|
||||
return m_openContainer;
|
||||
}
|
||||
|
||||
List<ContainerResult> ContainerInteractor::pullContainerResults() {
|
||||
List<List<ItemPtr>> results;
|
||||
eraseWhere(m_pendingResults, [&results](auto& promise) {
|
||||
if (auto res = promise.result())
|
||||
results.append(res.take());
|
||||
return promise.finished();
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
void ContainerInteractor::swapInContainer(size_t slot, ItemPtr const& items) {
|
||||
m_pendingResults.append(openContainer()->swapItems(slot, items).wrap(resultFromItem));
|
||||
}
|
||||
|
||||
void ContainerInteractor::addToContainer(ItemPtr const& items) {
|
||||
m_pendingResults.append(openContainer()->addItems(items).wrap(resultFromItem));
|
||||
}
|
||||
|
||||
void ContainerInteractor::takeFromContainerSlot(size_t slot, size_t count) {
|
||||
m_pendingResults.append(openContainer()->takeItems(slot, count).wrap(resultFromItem));
|
||||
}
|
||||
|
||||
void ContainerInteractor::applyAugmentInContainer(size_t slot, ItemPtr const& augment) {
|
||||
m_pendingResults.append(openContainer()->applyAugment(slot, augment).wrap(resultFromItem));
|
||||
}
|
||||
|
||||
void ContainerInteractor::startCraftingInContainer() {
|
||||
openContainer()->startCrafting();
|
||||
}
|
||||
|
||||
void ContainerInteractor::stopCraftingInContainer() {
|
||||
openContainer()->stopCrafting();
|
||||
}
|
||||
|
||||
void ContainerInteractor::burnContainer() {
|
||||
openContainer()->burnContainerContents();
|
||||
}
|
||||
|
||||
void ContainerInteractor::clearContainer() {
|
||||
m_pendingResults.append(openContainer()->clearContainer());
|
||||
}
|
||||
|
||||
ContainerResult ContainerInteractor::resultFromItem(ItemPtr const& item) {
|
||||
if (item)
|
||||
return {item};
|
||||
else
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
46
source/frontend/StarContainerInteractor.hpp
Normal file
46
source/frontend/StarContainerInteractor.hpp
Normal file
|
@ -0,0 +1,46 @@
|
|||
#ifndef STAR_CONTAINER_INTERACTION_HPP
|
||||
#define STAR_CONTAINER_INTERACTION_HPP
|
||||
|
||||
#include "StarContainerEntity.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(ContainerInteractor);
|
||||
|
||||
typedef List<ItemPtr> ContainerResult;
|
||||
|
||||
class ContainerInteractor {
|
||||
public:
|
||||
void openContainer(ContainerEntityPtr containerEntity);
|
||||
void closeContainer();
|
||||
|
||||
bool containerOpen() const;
|
||||
|
||||
// Returns NullEntityId if no container is open
|
||||
EntityId openContainerId() const;
|
||||
|
||||
// Throws exception if there is no currently open container.
|
||||
ContainerEntityPtr const& openContainer() const;
|
||||
|
||||
List<ContainerResult> pullContainerResults();
|
||||
|
||||
void swapInContainer(size_t slot, ItemPtr const& items);
|
||||
void addToContainer(ItemPtr const& items);
|
||||
void takeFromContainerSlot(size_t slot, size_t count);
|
||||
void applyAugmentInContainer(size_t slot, ItemPtr const& augment);
|
||||
|
||||
void startCraftingInContainer();
|
||||
void stopCraftingInContainer();
|
||||
void burnContainer();
|
||||
void clearContainer();
|
||||
|
||||
private:
|
||||
static ContainerResult resultFromItem(ItemPtr const& items);
|
||||
|
||||
mutable ContainerEntityPtr m_openContainer;
|
||||
List<RpcPromise<ContainerResult>> m_pendingResults;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
303
source/frontend/StarContainerInterface.cpp
Normal file
303
source/frontend/StarContainerInterface.cpp
Normal file
|
@ -0,0 +1,303 @@
|
|||
#include "StarContainerInterface.hpp"
|
||||
#include "StarCasting.hpp"
|
||||
#include "StarContainerEntity.hpp"
|
||||
#include "StarWorldClient.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarItemTooltip.hpp"
|
||||
#include "StarItemGridWidget.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarPaneManager.hpp"
|
||||
#include "StarFuelWidget.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarItemDatabase.hpp"
|
||||
#include "StarObject.hpp"
|
||||
#include "StarPlayerInventory.hpp"
|
||||
#include "StarConfigLuaBindings.hpp"
|
||||
#include "StarPlayerLuaBindings.hpp"
|
||||
#include "StarStatusControllerLuaBindings.hpp"
|
||||
#include "StarWidgetLuaBindings.hpp"
|
||||
#include "StarAugmentItem.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
ContainerPane::ContainerPane(WorldClientPtr worldClient, PlayerPtr player, ContainerInteractorPtr containerInteractor) {
|
||||
m_worldClient = worldClient;
|
||||
m_player = player;
|
||||
m_containerInteractor = move(containerInteractor);
|
||||
|
||||
auto container = m_containerInteractor->openContainer();
|
||||
auto guiConfig = container->containerGuiConfig();
|
||||
|
||||
if (auto scripts = guiConfig.opt("scripts").apply(jsonToStringList)) {
|
||||
if (!m_script) {
|
||||
m_script.emplace();
|
||||
m_script->setScripts(*scripts);
|
||||
}
|
||||
m_script->addCallbacks("widget", LuaBindings::makeWidgetCallbacks(this, &m_reader));
|
||||
m_script->addCallbacks("config", LuaBindings::makeConfigCallbacks( [guiConfig](String const& name, Json const& def) {
|
||||
return guiConfig.query(name, def);
|
||||
}));
|
||||
m_script->addCallbacks("player", LuaBindings::makePlayerCallbacks(m_player.get()));
|
||||
m_script->addCallbacks("status", LuaBindings::makeStatusControllerCallbacks(m_player->statusController()));
|
||||
|
||||
LuaCallbacks containerPaneCallbacks;
|
||||
containerPaneCallbacks.registerCallback("containerEntityId", [this]() -> Maybe<EntityId> {
|
||||
return m_containerInteractor->openContainerId();
|
||||
});
|
||||
containerPaneCallbacks.registerCallback("playerEntityId", [this]() { return m_player->entityId(); });
|
||||
containerPaneCallbacks.registerCallback("dismiss", [this]() { dismiss(); });
|
||||
m_script->addCallbacks("pane", containerPaneCallbacks);
|
||||
|
||||
m_script->setUpdateDelta(guiConfig.getUInt("scriptDelta", 1));
|
||||
}
|
||||
|
||||
auto rightClickCallback = [this](size_t index) {
|
||||
if (m_expectingSwap != ExpectingSwap::None)
|
||||
return;
|
||||
|
||||
if (ItemPtr slotItem = m_containerInteractor->openContainer()->itemBag()->at(index)) {
|
||||
auto swapItem = m_player->inventory()->swapSlotItem();
|
||||
if (!swapItem || swapItem->empty() || swapItem->couldStack(slotItem)) {
|
||||
size_t count = swapItem ? swapItem->couldStack(slotItem) : slotItem->maxStack();
|
||||
if (context()->shiftHeld())
|
||||
count = max(1, min<int>(count, slotItem->count() / 2));
|
||||
else
|
||||
count = 1;
|
||||
|
||||
m_containerInteractor->takeFromContainerSlot(index, count);
|
||||
m_expectingSwap = ExpectingSwap::SwapSlotStack;
|
||||
} else if (is<AugmentItem>(swapItem)) {
|
||||
m_containerInteractor->applyAugmentInContainer(index, swapItem);
|
||||
m_player->inventory()->setSwapSlotItem({});
|
||||
m_expectingSwap = ExpectingSwap::SwapSlot;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
m_reader.registerCallback("close", [this](Widget*) { dismiss(); });
|
||||
m_reader.registerCallback("itemGrid", [=](Widget* paneObj) {
|
||||
if (auto itemGrid = as<ItemGridWidget>(paneObj))
|
||||
swapSlot(itemGrid);
|
||||
else
|
||||
throw GuiException("Invalid object type, expected ItemGridWidget.");
|
||||
});
|
||||
m_reader.registerCallback("itemGrid.right", [=](Widget* paneObj) {
|
||||
if (auto itemGrid = as<ItemGridWidget>(paneObj))
|
||||
rightClickCallback(itemGrid->selectedIndex());
|
||||
else
|
||||
throw GuiException("Invalid object type, expected ItemGridWidget.");
|
||||
});
|
||||
|
||||
m_reader.registerCallback("itemGrid2", [=](Widget* paneObj) {
|
||||
if (auto itemGrid = as<ItemGridWidget>(paneObj))
|
||||
swapSlot(itemGrid);
|
||||
else
|
||||
throw GuiException("Invalid object type, expected ItemGridWidget.");
|
||||
});
|
||||
m_reader.registerCallback("itemGrid2.right", [=](Widget* paneObj) {
|
||||
if (auto itemGrid = as<ItemGridWidget>(paneObj))
|
||||
rightClickCallback(itemGrid->selectedIndex());
|
||||
else
|
||||
throw GuiException("Invalid object type, expected ItemGridWidget.");
|
||||
});
|
||||
|
||||
m_reader.registerCallback("outputItemGrid", [=](Widget* paneObj) {
|
||||
if (auto itemGrid = as<ItemGridWidget>(paneObj))
|
||||
swapSlot(itemGrid);
|
||||
else
|
||||
throw GuiException("Invalid object type, expected ItemGridWidget.");
|
||||
});
|
||||
m_reader.registerCallback("outputItemGrid.right", [=](Widget* paneObj) {
|
||||
if (auto itemGrid = as<ItemGridWidget>(paneObj))
|
||||
rightClickCallback(itemGrid->selectedIndex());
|
||||
else
|
||||
throw GuiException("Invalid object type, expected ItemGridWidget.");
|
||||
});
|
||||
|
||||
m_reader.registerCallback("toggleCrafting", [=](Widget*) { toggleCrafting(); });
|
||||
|
||||
m_reader.registerCallback("clear", [=](Widget*) { clear(); });
|
||||
m_reader.registerCallback("burn", [=](Widget*) { burn(); });
|
||||
|
||||
for (auto const& callbackName : jsonToStringList(guiConfig.get("scriptWidgetCallbacks", JsonArray{}))) {
|
||||
m_reader.registerCallback(callbackName, [this, callbackName](Widget* widget) {
|
||||
m_script->invoke(callbackName, widget->name(), widget->data());
|
||||
});
|
||||
}
|
||||
|
||||
m_reader.construct(guiConfig.get("gui"), this);
|
||||
|
||||
if (auto countWidget = fetchChild<LabelWidget>("count"))
|
||||
countWidget->setText(countWidget->text().replace("<slots>", strf("%s", container->containerSize())));
|
||||
|
||||
m_itemBag = make_shared<ItemBag>(container->containerSize());
|
||||
auto items = container->containerItems();
|
||||
|
||||
fetchChild<ItemGridWidget>("itemGrid")->setItemBag(m_itemBag);
|
||||
if (containsChild("itemGrid2"))
|
||||
fetchChild<ItemGridWidget>("itemGrid2")->setItemBag(m_itemBag);
|
||||
if (containsChild("outputItemGrid"))
|
||||
fetchChild<ItemGridWidget>("outputItemGrid")->setItemBag(m_itemBag);
|
||||
|
||||
if (container->iconItem()) {
|
||||
auto itemDatabase = Root::singleton().itemDatabase();
|
||||
auto iconItem = itemDatabase->item(container->iconItem());
|
||||
auto icon = make_shared<ItemSlotWidget>(iconItem, "/interface/inventory/portrait.png");
|
||||
icon->showDurability(false);
|
||||
icon->showRarity(false);
|
||||
icon->setBackingImageAffinity(true, true);
|
||||
setTitle(icon, container->containerDescription(), container->containerSubTitle());
|
||||
}
|
||||
|
||||
if (containsChild("objectImage"))
|
||||
if (auto containerObject = as<Object>(m_containerInteractor->openContainer()))
|
||||
fetchChild<ImageWidget>("objectImage")->setDrawables(containerObject->cursorHintDrawables());
|
||||
|
||||
m_expectingSwap = ExpectingSwap::None;
|
||||
}
|
||||
|
||||
void ContainerPane::displayed() {
|
||||
Pane::displayed();
|
||||
|
||||
m_expectingSwap = ExpectingSwap::None;
|
||||
|
||||
if (m_script) {
|
||||
if (m_worldClient && m_worldClient->inWorld())
|
||||
m_script->init(m_worldClient.get());
|
||||
|
||||
m_script->invoke("displayed");
|
||||
}
|
||||
}
|
||||
|
||||
void ContainerPane::dismissed() {
|
||||
Pane::dismissed();
|
||||
|
||||
if (m_script) {
|
||||
m_script->invoke("dismissed");
|
||||
m_script->uninit();
|
||||
}
|
||||
}
|
||||
|
||||
bool ContainerPane::giveContainerResult(ContainerResult result) {
|
||||
if (m_expectingSwap == ExpectingSwap::None)
|
||||
return false;
|
||||
|
||||
for (auto item : result) {
|
||||
auto inv = m_player->inventory();
|
||||
m_player->triggerPickupEvents(item);
|
||||
|
||||
if (m_expectingSwap == ExpectingSwap::SwapSlot) {
|
||||
m_player->clearSwap();
|
||||
inv->setSwapSlotItem(item);
|
||||
} else if (m_expectingSwap == ExpectingSwap::SwapSlotStack) {
|
||||
auto swapItem = inv->swapSlotItem();
|
||||
if (swapItem && swapItem->stackWith(item)) {
|
||||
continue;
|
||||
} else {
|
||||
inv->clearSwap();
|
||||
inv->setSwapSlotItem(item);
|
||||
}
|
||||
} else {
|
||||
m_containerInteractor->addToContainer(inv->addItems(item));
|
||||
}
|
||||
}
|
||||
|
||||
m_expectingSwap = ExpectingSwap::None;
|
||||
return true;
|
||||
}
|
||||
|
||||
PanePtr ContainerPane::createTooltip(Vec2I const& screenPosition) {
|
||||
ItemPtr item;
|
||||
if (auto child = getChildAt(screenPosition)) {
|
||||
if (auto itemSlot = as<ItemSlotWidget>(child))
|
||||
item = itemSlot->item();
|
||||
if (auto itemGrid = as<ItemGridWidget>(child))
|
||||
item = itemGrid->itemAt(screenPosition);
|
||||
}
|
||||
if (item)
|
||||
return ItemTooltipBuilder::buildItemTooltip(item, m_player);
|
||||
return {};
|
||||
}
|
||||
|
||||
void ContainerPane::swapSlot(ItemGridWidget* grid) {
|
||||
auto inv = m_player->inventory();
|
||||
if (context()->shiftHeld()) {
|
||||
auto containerItem = grid->selectedItem();
|
||||
if (containerItem && inv->itemsCanFit(containerItem) >= containerItem->count()) {
|
||||
m_containerInteractor->swapInContainer(grid->selectedIndex(), {});
|
||||
m_expectingSwap = ExpectingSwap::Inventory;
|
||||
}
|
||||
} else {
|
||||
m_containerInteractor->swapInContainer(grid->selectedIndex(), inv->swapSlotItem());
|
||||
inv->setSwapSlotItem({});
|
||||
m_expectingSwap = ExpectingSwap::SwapSlot;
|
||||
}
|
||||
}
|
||||
|
||||
void ContainerPane::startCrafting() {
|
||||
m_containerInteractor->startCraftingInContainer();
|
||||
}
|
||||
|
||||
void ContainerPane::stopCrafting() {
|
||||
m_containerInteractor->stopCraftingInContainer();
|
||||
}
|
||||
|
||||
void ContainerPane::toggleCrafting() {
|
||||
if (m_containerInteractor->openContainer()->isCrafting())
|
||||
stopCrafting();
|
||||
else
|
||||
startCrafting();
|
||||
}
|
||||
|
||||
void ContainerPane::clear() {
|
||||
m_containerInteractor->clearContainer();
|
||||
}
|
||||
|
||||
void ContainerPane::burn() {
|
||||
m_containerInteractor->burnContainer();
|
||||
}
|
||||
|
||||
void ContainerPane::update() {
|
||||
Pane::update();
|
||||
|
||||
if (m_script)
|
||||
m_script->update(m_script->updateDt());
|
||||
|
||||
m_itemBag->clearItems();
|
||||
|
||||
if (!m_containerInteractor->containerOpen()) {
|
||||
dismiss();
|
||||
|
||||
} else {
|
||||
auto container = m_containerInteractor->openContainer();
|
||||
|
||||
for (size_t i = 0; i < m_itemBag->size(); ++i)
|
||||
m_itemBag->putItems(i, container->containerItems()[i]);
|
||||
|
||||
if (container->isInteractive()) {
|
||||
if (auto itemGrid = fetchChild<ItemGridWidget>("itemGrid")) {
|
||||
itemGrid->setProgress(container->craftingProgress());
|
||||
itemGrid->updateAllItemSlots();
|
||||
}
|
||||
if (auto itemGrid = fetchChild<ItemGridWidget>("itemGrid2")) {
|
||||
itemGrid->setProgress(container->craftingProgress());
|
||||
itemGrid->updateAllItemSlots();
|
||||
}
|
||||
|
||||
if (auto fuelGauge = fetchChild<FuelWidget>("fuelGauge")) {
|
||||
fuelGauge->setCurrentFuelLevel(m_worldClient->getProperty("ship.fuel", 0).toUInt());
|
||||
fuelGauge->setMaxFuelLevel(m_worldClient->getProperty("ship.maxFuel", 0).toUInt());
|
||||
float totalFuelAmount = 0;
|
||||
for (auto& item : container->containerItems()) {
|
||||
if (item)
|
||||
totalFuelAmount += item->instanceValue("fuelAmount", 0).toUInt() * item->count();
|
||||
}
|
||||
fuelGauge->setPotentialFuelAmount(totalFuelAmount);
|
||||
fuelGauge->setRequestedFuelAmount(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
61
source/frontend/StarContainerInterface.hpp
Normal file
61
source/frontend/StarContainerInterface.hpp
Normal file
|
@ -0,0 +1,61 @@
|
|||
#ifndef STAR_CONTAINER_INTERFACE_HPP
|
||||
#define STAR_CONTAINER_INTERFACE_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
#include "StarLuaComponents.hpp"
|
||||
#include "StarContainerInteractor.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(ContainerEntity);
|
||||
STAR_CLASS(Player);
|
||||
STAR_CLASS(WorldClient);
|
||||
STAR_CLASS(Item);
|
||||
STAR_CLASS(ItemGridWidget);
|
||||
STAR_CLASS(ItemBag);
|
||||
STAR_CLASS(ContainerPane);
|
||||
|
||||
class ContainerPane : public Pane {
|
||||
public:
|
||||
ContainerPane(WorldClientPtr worldClient, PlayerPtr player, ContainerInteractorPtr containerInteractor);
|
||||
|
||||
void displayed() override;
|
||||
void dismissed() override;
|
||||
PanePtr createTooltip(Vec2I const& screenPosition) override;
|
||||
|
||||
bool giveContainerResult(ContainerResult result);
|
||||
|
||||
protected:
|
||||
void update() override;
|
||||
|
||||
private:
|
||||
enum class ExpectingSwap {
|
||||
None,
|
||||
Inventory,
|
||||
SwapSlot,
|
||||
SwapSlotStack
|
||||
};
|
||||
|
||||
void swapSlot(ItemGridWidget* grid);
|
||||
void startCrafting();
|
||||
void stopCrafting();
|
||||
void toggleCrafting();
|
||||
void clear();
|
||||
void burn();
|
||||
|
||||
WorldClientPtr m_worldClient;
|
||||
PlayerPtr m_player;
|
||||
ContainerInteractorPtr m_containerInteractor;
|
||||
ItemBagPtr m_itemBag;
|
||||
|
||||
ExpectingSwap m_expectingSwap;
|
||||
|
||||
GuiReader m_reader;
|
||||
|
||||
Maybe<LuaWorldComponent<LuaUpdatableComponent<LuaBaseComponent>>> m_script;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
773
source/frontend/StarCraftingInterface.cpp
Normal file
773
source/frontend/StarCraftingInterface.cpp
Normal file
|
@ -0,0 +1,773 @@
|
|||
#include "StarCraftingInterface.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarLexicalCast.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarItemTooltip.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarContainerEntity.hpp"
|
||||
#include "StarWorldClient.hpp"
|
||||
#include "StarPlayerBlueprints.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarPaneManager.hpp"
|
||||
#include "StarPortraitWidget.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarTextBoxWidget.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarListWidget.hpp"
|
||||
#include "StarImageStretchWidget.hpp"
|
||||
#include "StarItemSlotWidget.hpp"
|
||||
#include "StarConfiguration.hpp"
|
||||
#include "StarObjectItem.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarItemDatabase.hpp"
|
||||
#include "StarObjectDatabase.hpp"
|
||||
#include "StarPlayerInventory.hpp"
|
||||
#include "StarPlayerLog.hpp"
|
||||
#include "StarMixer.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
CraftingPane::CraftingPane(WorldClientPtr worldClient, PlayerPtr player, Json const& settings, EntityId sourceEntityId) {
|
||||
m_worldClient = move(worldClient);
|
||||
m_player = move(player);
|
||||
m_blueprints = m_player->blueprints();
|
||||
m_recipeAutorefreshCooldown = 0;
|
||||
m_sourceEntityId = sourceEntityId;
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
// get the config data for this crafting pane, default to "bare hands" crafting
|
||||
auto baseConfig = settings.get("config", "/interface/windowconfig/crafting.config");
|
||||
m_settings = jsonMerge(assets->fetchJson(baseConfig), settings);
|
||||
m_filter = StringSet::from(jsonToStringList(m_settings.get("filter", JsonArray())));
|
||||
|
||||
GuiReader reader;
|
||||
reader.registerCallback("spinCount.up", [=](Widget*) {
|
||||
if (m_count < maxCraft())
|
||||
m_count++;
|
||||
else
|
||||
m_count = 1;
|
||||
countChanged();
|
||||
});
|
||||
|
||||
reader.registerCallback("spinCount.down", [=](Widget*) {
|
||||
if (m_count > 1)
|
||||
m_count--;
|
||||
else
|
||||
m_count = std::max(maxCraft(), 1);
|
||||
countChanged();
|
||||
});
|
||||
|
||||
reader.registerCallback("tbSpinCount", [=](Widget*) { countTextChanged(); });
|
||||
|
||||
reader.registerCallback("close", [=](Widget*) { dismiss(); });
|
||||
|
||||
reader.registerCallback("btnCraft", [=](Widget*) { toggleCraft(); });
|
||||
reader.registerCallback("btnStopCraft", [=](Widget*) { toggleCraft(); });
|
||||
|
||||
reader.registerCallback("btnFilterHaveMaterials", [=](Widget*) {
|
||||
Root::singleton().configuration()->setPath("crafting.filterHaveMaterials", m_filterHaveMaterials->isChecked());
|
||||
updateAvailableRecipes();
|
||||
});
|
||||
|
||||
reader.registerCallback("filter", [=](Widget*) { updateAvailableRecipes(); });
|
||||
|
||||
reader.registerCallback("categories", [=](Widget*) { updateAvailableRecipes(); });
|
||||
|
||||
reader.registerCallback("rarities", [=](Widget*) { updateAvailableRecipes(); });
|
||||
|
||||
reader.registerCallback("btnUpgrade", [=](Widget*) { upgradeTable(); });
|
||||
|
||||
// this is where the GUI gets built and the buttons begin to have existence.
|
||||
// all possible callbacks must exist by this point
|
||||
|
||||
Json paneLayout = m_settings.get("paneLayout");
|
||||
paneLayout = jsonMerge(paneLayout, m_settings.get("paneLayoutOverride", {}));
|
||||
reader.construct(paneLayout, this);
|
||||
|
||||
if (auto upgradeButton = fetchChild<ButtonWidget>("btnUpgrade")) {
|
||||
upgradeButton->disable();
|
||||
Maybe<JsonArray> recipeData = m_settings.optArray("upgradeMaterials");
|
||||
|
||||
// create a recipe out of the listed upgrade materials.
|
||||
// for ease of creating a tooltip later.
|
||||
if (recipeData) {
|
||||
m_upgradeRecipe = ItemRecipe();
|
||||
for (auto ingredient : *recipeData)
|
||||
m_upgradeRecipe->inputs.append(ItemDescriptor(ingredient.getString("item"), ingredient.getUInt("count"), {}));
|
||||
upgradeButton->setVisibility(true);
|
||||
} else {
|
||||
upgradeButton->setVisibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
m_guiList = fetchChild<ListWidget>("scrollArea.itemList");
|
||||
m_textBox = fetchChild<TextBoxWidget>("tbSpinCount");
|
||||
|
||||
m_filterHaveMaterials = fetchChild<ButtonWidget>("btnFilterHaveMaterials");
|
||||
if (m_filterHaveMaterials)
|
||||
m_filterHaveMaterials->setChecked(Root::singleton().configuration()->getPath("crafting.filterHaveMaterials").toBool());
|
||||
|
||||
fetchChild<ButtonWidget>("btnCraft")->disable();
|
||||
if (auto spinCountUp = fetchChild<ButtonWidget>("spinCount.up"))
|
||||
spinCountUp->disable();
|
||||
if (auto spinCountDown = fetchChild<ButtonWidget>("spinCount.down"))
|
||||
spinCountDown->disable();
|
||||
|
||||
m_displayedRecipe = NPos;
|
||||
updateAvailableRecipes();
|
||||
|
||||
m_crafting = false;
|
||||
m_count = 1;
|
||||
countChanged();
|
||||
|
||||
if (m_settings.getBool("titleFromEntity", false) && sourceEntityId != NullEntityId) {
|
||||
auto entity = m_worldClient->entity(sourceEntityId);
|
||||
|
||||
if (auto container = as<ContainerEntity>(entity)) {
|
||||
if (container->iconItem()) {
|
||||
auto itemDatabase = Root::singleton().itemDatabase();
|
||||
auto iconItem = itemDatabase->item(container->iconItem());
|
||||
auto icon = make_shared<ItemSlotWidget>(iconItem, "/interface/inventory/portrait.png");
|
||||
String title = this->title();
|
||||
if (title.empty())
|
||||
title = container->containerDescription();
|
||||
String subTitle = this->subTitle();
|
||||
if (subTitle.empty())
|
||||
subTitle = container->containerSubTitle();
|
||||
icon->showRarity(false);
|
||||
setTitle(icon, title, subTitle);
|
||||
}
|
||||
}
|
||||
if (auto portaitEntity = as<PortraitEntity>(entity)) {
|
||||
auto portrait = make_shared<PortraitWidget>(portaitEntity, PortraitMode::Bust);
|
||||
portrait->setIconMode();
|
||||
String title = this->title();
|
||||
if (title.empty())
|
||||
title = portaitEntity->name();
|
||||
String subTitle = this->subTitle();
|
||||
setTitle(portrait, title, subTitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CraftingPane::displayed() {
|
||||
Pane::displayed();
|
||||
|
||||
if (auto filterWidget = fetchChild<TextBoxWidget>("filter")) {
|
||||
filterWidget->setText("");
|
||||
filterWidget->blur();
|
||||
}
|
||||
|
||||
updateAvailableRecipes();
|
||||
|
||||
// unlock any recipes specified
|
||||
if (auto recipeUnlocks = m_settings.opt("initialRecipeUnlocks")) {
|
||||
for (String itemName : jsonToStringList(*recipeUnlocks))
|
||||
m_player->addBlueprint(ItemDescriptor(itemName));
|
||||
}
|
||||
}
|
||||
|
||||
void CraftingPane::dismissed() {
|
||||
stopCrafting();
|
||||
Pane::dismissed();
|
||||
m_itemCache.clear();
|
||||
}
|
||||
|
||||
PanePtr CraftingPane::createTooltip(Vec2I const& screenPosition) {
|
||||
for (size_t i = 0; i < m_guiList->numChildren(); ++i) {
|
||||
auto entry = m_guiList->itemAt(i);
|
||||
if (entry->getChildAt(screenPosition)) {
|
||||
auto& recipe = m_recipesWidgetMap.getLeft(entry);
|
||||
return setupTooltip(recipe);
|
||||
}
|
||||
}
|
||||
|
||||
if (WidgetPtr child = getChildAt(screenPosition)) {
|
||||
if (child->name() == "btnUpgrade") {
|
||||
if (m_upgradeRecipe)
|
||||
return setupTooltip(*m_upgradeRecipe);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
EntityId CraftingPane::sourceEntityId() const {
|
||||
return m_sourceEntityId;
|
||||
}
|
||||
|
||||
void CraftingPane::upgradeTable() {
|
||||
if (m_sourceEntityId != NullEntityId) {
|
||||
// Checks that the upgrade path exists
|
||||
if (m_upgradeRecipe) {
|
||||
if (m_player->isAdmin() || ItemDatabase::canMakeRecipe(*m_upgradeRecipe, m_player->inventory()->availableItems(), m_player->inventory()->availableCurrencies())) {
|
||||
if (!m_player->isAdmin())
|
||||
consumeIngredients(*m_upgradeRecipe, 1);
|
||||
|
||||
// upgrade the old table
|
||||
m_worldClient->sendEntityMessage(m_sourceEntityId, "requestUpgrade");
|
||||
|
||||
// unlock any recipes specified
|
||||
if (auto recipeUnlocks = m_settings.opt("upgradeRecipeUnlocks")) {
|
||||
for (String itemName : jsonToStringList(*recipeUnlocks))
|
||||
m_player->addBlueprint(ItemDescriptor(itemName));
|
||||
}
|
||||
|
||||
// this closes the interface window
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t CraftingPane::itemCount(List<ItemPtr> const& store, ItemDescriptor const& item) {
|
||||
auto itemDb = Root::singleton().itemDatabase();
|
||||
return itemDb->getCountOfItem(store, item);
|
||||
}
|
||||
|
||||
void CraftingPane::update() {
|
||||
// shut down if we can't reach the table anymore.
|
||||
if (m_sourceEntityId != NullEntityId) {
|
||||
auto sourceEntity = as<TileEntity>(m_worldClient->entity(m_sourceEntityId));
|
||||
if (!sourceEntity || !m_worldClient->playerCanReachEntity(m_sourceEntityId) || !sourceEntity->isInteractive()) {
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// similarly if the player is dead
|
||||
if (m_player->isDead()) {
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
// has the selected recipe changed ?
|
||||
bool changedHighlight = (m_displayedRecipe != m_guiList->selectedItem());
|
||||
|
||||
if (changedHighlight) {
|
||||
stopCrafting(); // TODO: allow viewing other recipes without interrupting crafting
|
||||
|
||||
m_displayedRecipe = m_guiList->selectedItem();
|
||||
countTextChanged();
|
||||
|
||||
auto recipe = recipeFromSelectedWidget();
|
||||
|
||||
if (recipe.isNull()) {
|
||||
fetchChild<Widget>("description")->removeAllChildren();
|
||||
} else {
|
||||
auto description = fetchChild<Widget>("description");
|
||||
description->removeAllChildren();
|
||||
|
||||
auto item = Root::singleton().itemDatabase()->item(recipe.output);
|
||||
ItemTooltipBuilder::buildItemDescription(description, item);
|
||||
}
|
||||
}
|
||||
|
||||
// crafters gonna craft
|
||||
while (m_crafting && m_craftTimer.wrapTick()) {
|
||||
craft(1);
|
||||
}
|
||||
|
||||
// update crafting icon, progress and buttons
|
||||
if (auto currentRecipeIcon = fetchChild<ItemSlotWidget>("currentRecipeIcon")) {
|
||||
auto recipe = recipeFromSelectedWidget();
|
||||
if (recipe.isNull()) {
|
||||
currentRecipeIcon->setItem(nullptr);
|
||||
} else {
|
||||
auto single = recipe.output.singular();
|
||||
ItemPtr item = m_itemCache[single];
|
||||
currentRecipeIcon->setItem(item);
|
||||
|
||||
if (m_crafting)
|
||||
currentRecipeIcon->setProgress(1.0f - m_craftTimer.percent());
|
||||
else
|
||||
currentRecipeIcon->setProgress(1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
--m_recipeAutorefreshCooldown;
|
||||
|
||||
// changed recipe or auto update time
|
||||
if (changedHighlight || (m_recipeAutorefreshCooldown <= 0)) {
|
||||
updateAvailableRecipes();
|
||||
updateCraftButtons();
|
||||
}
|
||||
|
||||
setLabel("lblPlayerMoney", strf("%s", (int)m_player->currency("money")));
|
||||
|
||||
Pane::update();
|
||||
}
|
||||
|
||||
void CraftingPane::updateCraftButtons() {
|
||||
auto normalizedBag = m_player->inventory()->availableItems();
|
||||
auto availableCurrencies = m_player->inventory()->availableCurrencies();
|
||||
|
||||
auto recipe = recipeFromSelectedWidget();
|
||||
bool recipeAvailable = !recipe.isNull() && (m_player->isAdmin() || ItemDatabase::canMakeRecipe(recipe, normalizedBag, availableCurrencies));
|
||||
|
||||
fetchChild<ButtonWidget>("btnCraft")->setEnabled(recipeAvailable);
|
||||
if (auto spinCountUp = fetchChild<ButtonWidget>("spinCount.up"))
|
||||
spinCountUp->setEnabled(recipeAvailable);
|
||||
if (auto spinCountDown = fetchChild<ButtonWidget>("spinCount.down"))
|
||||
spinCountDown->setEnabled(recipeAvailable);
|
||||
|
||||
if (auto stopCraftButton = fetchChild<ButtonWidget>("btnStopCraft")) {
|
||||
stopCraftButton->setVisibility(m_crafting);
|
||||
fetchChild<ButtonWidget>("btnCraft")->setVisibility(!m_crafting);
|
||||
}
|
||||
|
||||
if (auto upgradeButton = fetchChild<ButtonWidget>("btnUpgrade")) {
|
||||
bool canUpgrade = (m_upgradeRecipe && (m_player->isAdmin() || ItemDatabase::canMakeRecipe(*m_upgradeRecipe, normalizedBag, availableCurrencies)));
|
||||
upgradeButton->setEnabled(canUpgrade);
|
||||
}
|
||||
}
|
||||
|
||||
void CraftingPane::updateAvailableRecipes() {
|
||||
m_recipeAutorefreshCooldown = 30;
|
||||
|
||||
StringSet categoryFilter;
|
||||
if (auto categoriesGroup = fetchChild<ButtonGroupWidget>("categories")) {
|
||||
if (auto selectedCategories = categoriesGroup->checkedButton()) {
|
||||
for (auto group : selectedCategories->data().getArray("filter"))
|
||||
categoryFilter.add(group.toString());
|
||||
}
|
||||
}
|
||||
|
||||
HashSet<Rarity> rarityFilter;
|
||||
if (auto raritiesGroup = fetchChild<ButtonGroupWidget>("rarities")) {
|
||||
if (auto selectedRarities = raritiesGroup->checkedButton()) {
|
||||
for (auto entry : jsonToStringSet(selectedRarities->data().getArray("rarity")))
|
||||
rarityFilter.add(RarityNames.getLeft(entry));
|
||||
}
|
||||
}
|
||||
|
||||
String filterText;
|
||||
if (auto filterWidget = fetchChild<TextBoxWidget>("filter"))
|
||||
filterText = filterWidget->getText();
|
||||
|
||||
m_recipes = determineRecipes();
|
||||
|
||||
size_t currentOffset = 0;
|
||||
|
||||
ItemRecipe selectedRecipe;
|
||||
if (m_guiList->selectedWidget())
|
||||
selectedRecipe = m_recipesWidgetMap.getLeft(m_guiList->selectedWidget());
|
||||
|
||||
HashMap<ItemDescriptor, uint64_t> normalizedBag = m_player->inventory()->availableItems();
|
||||
|
||||
m_guiList->clear();
|
||||
|
||||
for (auto const& recipe : m_recipes) {
|
||||
auto widget = m_recipesWidgetMap.valueRight(recipe);
|
||||
if (widget) {
|
||||
m_guiList->addItem(widget);
|
||||
} else {
|
||||
widget = m_guiList->addItem();
|
||||
m_recipesWidgetMap.add(recipe, widget);
|
||||
}
|
||||
|
||||
setupWidget(widget, recipe, normalizedBag);
|
||||
|
||||
if (selectedRecipe == recipe) {
|
||||
m_guiList->setSelected(currentOffset);
|
||||
}
|
||||
|
||||
currentOffset++;
|
||||
}
|
||||
}
|
||||
|
||||
void CraftingPane::setupWidget(WidgetPtr const& widget, ItemRecipe const& recipe, HashMap<ItemDescriptor, uint64_t> const& normalizedBag) {
|
||||
auto& root = Root::singleton();
|
||||
|
||||
auto single = recipe.output.singular();
|
||||
ItemPtr item = m_itemCache[single];
|
||||
if (!item) {
|
||||
item = root.itemDatabase()->item(single);
|
||||
m_itemCache[single] = item;
|
||||
}
|
||||
|
||||
bool unavailable = false;
|
||||
size_t price = recipe.currencyInputs.value("money", 0);
|
||||
|
||||
if (!m_player->isAdmin()) {
|
||||
for (auto const& p : recipe.currencyInputs) {
|
||||
if (m_player->currency(p.first) < p.second)
|
||||
unavailable = true;
|
||||
}
|
||||
|
||||
auto itemDb = Root::singleton().itemDatabase();
|
||||
for (auto const& input : recipe.inputs) {
|
||||
if (itemDb->getCountOfItem(normalizedBag, input, recipe.matchInputParameters) < input.count())
|
||||
unavailable = true;
|
||||
}
|
||||
}
|
||||
|
||||
String name = item->friendlyName();
|
||||
if (recipe.output.count() > 1)
|
||||
name = strf("%s (x%s)", name, recipe.output.count());
|
||||
|
||||
auto itemName = widget->fetchChild<LabelWidget>("itemName");
|
||||
auto notcraftableoverlay = widget->fetchChild<ImageWidget>("notcraftableoverlay");
|
||||
|
||||
itemName->setText(name);
|
||||
|
||||
if (unavailable) {
|
||||
itemName->setColor(Color::Gray);
|
||||
notcraftableoverlay->show();
|
||||
} else {
|
||||
itemName->setColor(Color::White);
|
||||
notcraftableoverlay->hide();
|
||||
}
|
||||
|
||||
if (price > 0) {
|
||||
widget->setLabel("priceLabel", strf("%s", price));
|
||||
if (auto icon = widget->fetchChild<ImageWidget>("moneyIcon"))
|
||||
icon->setVisibility(true);
|
||||
} else {
|
||||
widget->setLabel("priceLabel", "");
|
||||
if (auto icon = widget->fetchChild<ImageWidget>("moneyIcon"))
|
||||
icon->setVisibility(false);
|
||||
}
|
||||
|
||||
if (auto newIndicator = widget->fetchChild<ImageWidget>("newIcon")) {
|
||||
if (m_blueprints->isNew(recipe.output.singular())) {
|
||||
newIndicator->show();
|
||||
widget->setLabel("priceLabel", "");
|
||||
if (auto icon = widget->fetchChild<ImageWidget>("moneyIcon"))
|
||||
icon->setVisibility(false);
|
||||
} else {
|
||||
newIndicator->hide();
|
||||
}
|
||||
}
|
||||
|
||||
widget->fetchChild<ItemSlotWidget>("itemIcon")->setItem(item);
|
||||
widget->show();
|
||||
}
|
||||
|
||||
PanePtr CraftingPane::setupTooltip(ItemRecipe const& recipe) {
|
||||
auto& root = Root::singleton();
|
||||
|
||||
auto tooltip = make_shared<Pane>();
|
||||
GuiReader reader;
|
||||
reader.construct(root.assets()->json("/interface/craftingtooltip/craftingtooltip.config"), tooltip.get());
|
||||
|
||||
auto guiList = tooltip->fetchChild<ListWidget>("itemList");
|
||||
guiList->clear();
|
||||
|
||||
auto normalizedBag = m_player->inventory()->availableItems();
|
||||
|
||||
auto itemDb = root.itemDatabase();
|
||||
|
||||
auto addIngredient = [guiList](ItemPtr const& item, size_t availableCount, size_t requiredCount) {
|
||||
auto widget = guiList->addItem();
|
||||
widget->fetchChild<LabelWidget>("itemName")->setText(item->friendlyName());
|
||||
auto countWidget = widget->fetchChild<LabelWidget>("count");
|
||||
countWidget->setText(strf("%s/%s", availableCount, requiredCount));
|
||||
if (availableCount < requiredCount)
|
||||
countWidget->setColor(Color::Red);
|
||||
else
|
||||
countWidget->setColor(Color::Green);
|
||||
widget->fetchChild<ItemSlotWidget>("itemIcon")->setItem(item);
|
||||
widget->show();
|
||||
};
|
||||
|
||||
auto currenciesConfig = root.assets()->json("/currencies.config");
|
||||
for (auto const& p : recipe.currencyInputs) {
|
||||
if (p.second > 0) {
|
||||
auto currencyItem = root.itemDatabase()->item(ItemDescriptor(currenciesConfig.get(p.first).getString("representativeItem")));
|
||||
addIngredient(currencyItem, m_player->currency(p.first), p.second);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& input : recipe.inputs) {
|
||||
auto item = root.itemDatabase()->item(input.singular());
|
||||
size_t itemCount = itemDb->getCountOfItem(normalizedBag, input, recipe.matchInputParameters);
|
||||
addIngredient(item, itemCount, input.count());
|
||||
}
|
||||
|
||||
auto background = tooltip->fetchChild<ImageStretchWidget>("background");
|
||||
background->setSize(background->size() + Vec2I(0, guiList->size()[1]));
|
||||
|
||||
auto title = tooltip->fetchChild<LabelWidget>("title");
|
||||
title->setPosition(title->position() + Vec2I(0, guiList->size()[1]));
|
||||
|
||||
tooltip->setSize(background->size());
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
bool CraftingPane::consumeIngredients(ItemRecipe& recipe, int count) {
|
||||
auto itemDb = Root::singleton().itemDatabase();
|
||||
|
||||
auto normalizedBag = m_player->inventory()->availableItems();
|
||||
auto availableCurrencies = m_player->inventory()->availableCurrencies();
|
||||
|
||||
// make sure we still have the currencies and items avaialable
|
||||
for (auto const& p : recipe.currencyInputs) {
|
||||
uint64_t countRequired = p.second * count;
|
||||
if (availableCurrencies.value(p.first) < countRequired) {
|
||||
updateAvailableRecipes();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (auto input : recipe.inputs) {
|
||||
size_t countRequired = input.count() * count;
|
||||
if (itemDb->getCountOfItem(normalizedBag, input, recipe.matchInputParameters) < countRequired) {
|
||||
updateAvailableRecipes();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// actually consume the currencies and items
|
||||
for (auto const& p : recipe.currencyInputs) {
|
||||
if (p.second > 0)
|
||||
m_player->inventory()->consumeCurrency(p.first, p.second * count);
|
||||
}
|
||||
for (auto input : recipe.inputs) {
|
||||
if (count > 0)
|
||||
m_player->inventory()->consumeItems(ItemDescriptor(input.name(), input.count() * count, input.parameters()), recipe.matchInputParameters);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void CraftingPane::stopCrafting() {
|
||||
if (m_craftingSound)
|
||||
m_craftingSound->stop();
|
||||
m_crafting = false;
|
||||
}
|
||||
|
||||
void CraftingPane::toggleCraft() {
|
||||
if (m_crafting) {
|
||||
stopCrafting();
|
||||
} else {
|
||||
auto recipe = recipeFromSelectedWidget();
|
||||
if (recipe.duration > 0 && !m_settings.getBool("disableTimer", false)) {
|
||||
m_crafting = true;
|
||||
m_craftTimer = GameTimer(recipe.duration);
|
||||
|
||||
if (auto craftingSound = m_settings.optString("craftingSound")) {
|
||||
auto assets = Root::singleton().assets();
|
||||
m_craftingSound = make_shared<AudioInstance>(*assets->audio(*craftingSound));
|
||||
m_craftingSound->setLoops(-1);
|
||||
GuiContext::singleton().playAudio(m_craftingSound);
|
||||
}
|
||||
} else {
|
||||
craft(m_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CraftingPane::craft(int count) {
|
||||
auto& root = Root::singleton();
|
||||
|
||||
if (m_guiList->selectedItem() != NPos) {
|
||||
auto recipe = recipeFromSelectedWidget();
|
||||
|
||||
if (!m_player->isAdmin() && !consumeIngredients(recipe, count)) {
|
||||
stopCrafting();
|
||||
return;
|
||||
}
|
||||
|
||||
ItemDescriptor itemDescriptor = recipe.output;
|
||||
int remainingItemCount = itemDescriptor.count() * count;
|
||||
while (remainingItemCount > 0) {
|
||||
auto craftedItem = root.itemDatabase()->item(itemDescriptor.singular().multiply(remainingItemCount));
|
||||
remainingItemCount -= craftedItem->count();
|
||||
m_player->giveItem(craftedItem);
|
||||
|
||||
for (auto collectable : recipe.collectables)
|
||||
m_player->addCollectable(collectable.first, collectable.second);
|
||||
}
|
||||
|
||||
m_blueprints->markAsRead(recipe.output.singular());
|
||||
}
|
||||
|
||||
updateAvailableRecipes();
|
||||
|
||||
m_count -= count;
|
||||
if (m_count <= 0) {
|
||||
m_count = 1;
|
||||
stopCrafting();
|
||||
}
|
||||
countChanged();
|
||||
|
||||
updateCraftButtons();
|
||||
}
|
||||
|
||||
void CraftingPane::countTextChanged() {
|
||||
if (m_textBox) {
|
||||
int appropriateDefaultCount = 1;
|
||||
try {
|
||||
if (!m_textBox->getText().replace("x", "").size()) {
|
||||
m_count = appropriateDefaultCount;
|
||||
} else {
|
||||
m_count = clamp<int>(lexicalCast<int>(m_textBox->getText().replace("x", "")), appropriateDefaultCount, maxCraft());
|
||||
countChanged();
|
||||
}
|
||||
} catch (BadLexicalCast const&) {
|
||||
m_count = appropriateDefaultCount;
|
||||
countChanged();
|
||||
}
|
||||
} else {
|
||||
m_count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void CraftingPane::countChanged() {
|
||||
if (m_textBox)
|
||||
m_textBox->setText(strf("x%s", m_count), false);
|
||||
}
|
||||
|
||||
List<ItemRecipe> CraftingPane::determineRecipes() {
|
||||
HashSet<ItemRecipe> recipes;
|
||||
auto itemDb = Root::singleton().itemDatabase();
|
||||
|
||||
StringSet categoryFilter;
|
||||
if (auto categoriesGroup = fetchChild<ButtonGroupWidget>("categories")) {
|
||||
if (auto selectedCategories = categoriesGroup->checkedButton()) {
|
||||
for (auto group : selectedCategories->data().getArray("filter"))
|
||||
categoryFilter.add(group.toString());
|
||||
}
|
||||
}
|
||||
|
||||
HashSet<Rarity> rarityFilter;
|
||||
if (auto raritiesGroup = fetchChild<ButtonGroupWidget>("rarities")) {
|
||||
if (auto selectedRarities = raritiesGroup->checkedButton()) {
|
||||
for (auto entry : jsonToStringSet(selectedRarities->data().getArray("rarity")))
|
||||
rarityFilter.add(RarityNames.getLeft(entry));
|
||||
}
|
||||
}
|
||||
|
||||
String filterText;
|
||||
if (auto filterWidget = fetchChild<TextBoxWidget>("filter"))
|
||||
filterText = filterWidget->getText();
|
||||
|
||||
bool filterHaveMaterials = false;
|
||||
if (m_filterHaveMaterials)
|
||||
filterHaveMaterials = m_filterHaveMaterials->isChecked();
|
||||
|
||||
if (m_settings.getBool("printer", false)) {
|
||||
auto objectDatabase = Root::singleton().objectDatabase();
|
||||
|
||||
StringList itemList;
|
||||
if (m_player->isAdmin())
|
||||
itemList = objectDatabase->allObjects();
|
||||
else
|
||||
itemList = StringList::from(m_player->log()->scannedObjects());
|
||||
|
||||
filter(itemList, [objectDatabase, itemDb](String const& itemName) {
|
||||
if (objectDatabase->isObject(itemName)) {
|
||||
if (auto objectConfig = objectDatabase->getConfig(itemName))
|
||||
return objectConfig->printable && itemDb->hasItem(itemName);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
float printTime = m_settings.getFloat("printTime", 0);
|
||||
float printFactor = m_settings.getFloat("printCostFactor", 1.0);
|
||||
for (auto itemName : itemList) {
|
||||
ItemRecipe recipe;
|
||||
recipe.output = ItemDescriptor(itemName, 1);
|
||||
auto recipeItem = itemDb->item(recipe.output);
|
||||
int itemPrice = int(recipeItem->price() * printFactor);
|
||||
recipe.currencyInputs["money"] = itemPrice;
|
||||
recipe.outputRarity = recipeItem->rarity();
|
||||
recipe.duration = printTime;
|
||||
recipe.guiFilterString = ItemDatabase::guiFilterString(recipeItem);
|
||||
recipe.groups = StringSet{objectDatabase->getConfig(itemName)->category};
|
||||
recipes.add(recipe);
|
||||
}
|
||||
} else if (m_settings.contains("recipes")) {
|
||||
for (auto entry : m_settings.getArray("recipes")) {
|
||||
if (entry.type() == Json::Type::String)
|
||||
recipes.addAll(itemDb->recipesForOutputItem(entry.toString()));
|
||||
else
|
||||
recipes.add(itemDb->parseRecipe(entry));
|
||||
}
|
||||
|
||||
if (filterHaveMaterials)
|
||||
recipes.addAll(itemDb->recipesFromSubset(m_player->inventory()->availableItems(), m_player->inventory()->availableCurrencies(), take(recipes), m_filter));
|
||||
} else {
|
||||
if (filterHaveMaterials)
|
||||
recipes.addAll(itemDb->recipesFromBagContents(m_player->inventory()->availableItems(), m_player->inventory()->availableCurrencies(), m_filter));
|
||||
else
|
||||
recipes.addAll(itemDb->allRecipes(m_filter));
|
||||
}
|
||||
|
||||
if (!m_player->isAdmin() && m_settings.getBool("requiresBlueprint", true)) {
|
||||
auto tempRecipes = take(recipes);
|
||||
for (auto const& recipe : tempRecipes) {
|
||||
if (m_blueprints->isKnown(recipe.output))
|
||||
recipes.add(recipe);
|
||||
}
|
||||
}
|
||||
|
||||
if (!categoryFilter.empty()) {
|
||||
auto temprecipes = take(recipes);
|
||||
for (auto const& recipe : temprecipes) {
|
||||
if (recipe.groups.hasIntersection(categoryFilter))
|
||||
recipes.add(recipe);
|
||||
}
|
||||
}
|
||||
|
||||
if (!rarityFilter.empty()) {
|
||||
auto temprecipes = take(recipes);
|
||||
for (auto const& recipe : temprecipes) {
|
||||
if (recipe.output) {
|
||||
if (rarityFilter.contains(recipe.outputRarity))
|
||||
recipes.add(recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!filterText.empty()) {
|
||||
auto bits = filterText.toLower().splitAny(" ,.?*\\+/|\t");
|
||||
auto temprecipes = take(recipes);
|
||||
for (auto const& recipe : temprecipes) {
|
||||
if (recipe.output) {
|
||||
bool match = true;
|
||||
auto guiFilterString = recipe.guiFilterString;
|
||||
for (auto const& bit : bits) {
|
||||
match &= guiFilterString.contains(bit);
|
||||
if (!match)
|
||||
break;
|
||||
}
|
||||
if (match)
|
||||
recipes.add(recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<ItemRecipe> sortedRecipes = recipes.values();
|
||||
auto itemDatabase = Root::singleton().itemDatabase();
|
||||
sortByComputedValue(sortedRecipes, [itemDatabase](ItemRecipe const& recipe) {
|
||||
return make_tuple(itemDatabase->itemFriendlyName(recipe.output.name()).trim().toLower(), recipe.output.name());
|
||||
});
|
||||
|
||||
return sortedRecipes;
|
||||
}
|
||||
|
||||
int CraftingPane::maxCraft() {
|
||||
if (m_player->isAdmin())
|
||||
return 1000;
|
||||
auto itemDb = Root::singleton().itemDatabase();
|
||||
int res = 0;
|
||||
if (m_guiList->selectedItem() != NPos && m_guiList->selectedItem() < m_recipes.size()) {
|
||||
HashMap<ItemDescriptor, uint64_t> normalizedBag = m_player->inventory()->availableItems();
|
||||
auto selectedRecipe = recipeFromSelectedWidget();
|
||||
res = itemDb->maxCraftableInBag(normalizedBag, m_player->inventory()->availableCurrencies(), selectedRecipe);
|
||||
res = std::min(res, 1000);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
ItemRecipe CraftingPane::recipeFromSelectedWidget() const {
|
||||
auto pane = m_guiList->selectedWidget();
|
||||
if (pane && m_recipesWidgetMap.hasRightValue(pane)) {
|
||||
return m_recipesWidgetMap.getLeft(pane);
|
||||
}
|
||||
return ItemRecipe();
|
||||
}
|
||||
|
||||
}
|
85
source/frontend/StarCraftingInterface.hpp
Normal file
85
source/frontend/StarCraftingInterface.hpp
Normal file
|
@ -0,0 +1,85 @@
|
|||
#ifndef STAR_CRAFTING_INTERFACE_HPP
|
||||
#define STAR_CRAFTING_INTERFACE_HPP
|
||||
|
||||
#include "StarWorldPainter.hpp"
|
||||
#include "StarWorldClient.hpp"
|
||||
#include "StarItemRecipe.hpp"
|
||||
#include "StarPane.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(WorldClient);
|
||||
STAR_CLASS(PlayerBlueprints);
|
||||
STAR_CLASS(ListWidget);
|
||||
STAR_CLASS(TextBoxWidget);
|
||||
STAR_CLASS(ButtonWidget);
|
||||
STAR_CLASS(LabelWidget);
|
||||
STAR_CLASS(AudioInstance);
|
||||
|
||||
STAR_CLASS(CraftingPane);
|
||||
|
||||
class CraftingPane : public Pane {
|
||||
public:
|
||||
CraftingPane(
|
||||
WorldClientPtr worldClient, PlayerPtr player, Json const& settings, EntityId sourceEntityId = NullEntityId);
|
||||
|
||||
void displayed() override;
|
||||
void dismissed() override;
|
||||
PanePtr createTooltip(Vec2I const& screenPosition) override;
|
||||
|
||||
EntityId sourceEntityId() const;
|
||||
|
||||
private:
|
||||
void upgradeTable();
|
||||
|
||||
List<ItemRecipe> determineRecipes();
|
||||
|
||||
virtual void update() override;
|
||||
void updateCraftButtons();
|
||||
void updateAvailableRecipes();
|
||||
bool consumeIngredients(ItemRecipe& recipe, int count);
|
||||
void stopCrafting();
|
||||
void toggleCraft();
|
||||
void craft(int count);
|
||||
void countChanged();
|
||||
void countTextChanged();
|
||||
int maxCraft();
|
||||
void setupList(WidgetPtr widget, ItemRecipe const& recipe);
|
||||
ItemRecipe recipeFromSelectedWidget() const;
|
||||
void setupWidget(WidgetPtr const& widget, ItemRecipe const& recipe, HashMap<ItemDescriptor, uint64_t> const& normalizedBag);
|
||||
|
||||
PanePtr setupTooltip(ItemRecipe const& recipe);
|
||||
|
||||
size_t itemCount(List<ItemPtr> const& store, ItemDescriptor const& item);
|
||||
|
||||
WorldClientPtr m_worldClient;
|
||||
PlayerPtr m_player;
|
||||
PlayerBlueprintsPtr m_blueprints;
|
||||
|
||||
bool m_crafting;
|
||||
GameTimer m_craftTimer;
|
||||
AudioInstancePtr m_craftingSound;
|
||||
int m_count;
|
||||
List<ItemRecipe> m_recipes;
|
||||
BiHashMap<ItemRecipe, WidgetPtr> m_recipesWidgetMap; // maps ItemRecipe to guiList WidgetPtrs
|
||||
|
||||
ListWidgetPtr m_guiList;
|
||||
TextBoxWidgetPtr m_textBox;
|
||||
ButtonWidgetPtr m_filterHaveMaterials;
|
||||
size_t m_displayedRecipe;
|
||||
|
||||
StringSet m_filter;
|
||||
|
||||
int m_recipeAutorefreshCooldown;
|
||||
|
||||
HashMap<ItemDescriptor, ItemPtr> m_itemCache;
|
||||
|
||||
EntityId m_sourceEntityId;
|
||||
Json m_settings;
|
||||
|
||||
Maybe<ItemRecipe> m_upgradeRecipe;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
97
source/frontend/StarErrorScreen.cpp
Normal file
97
source/frontend/StarErrorScreen.cpp
Normal file
|
@ -0,0 +1,97 @@
|
|||
#include "StarErrorScreen.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarPaneManager.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
ErrorScreen::ErrorScreen() {
|
||||
m_paneManager = make_shared<PaneManager>();
|
||||
|
||||
m_accepted = false;
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
m_guiContext = GuiContext::singletonPtr();
|
||||
|
||||
m_errorPane = make_shared<Pane>();
|
||||
GuiReader reader;
|
||||
reader.registerCallback("btnOk", [this](Widget*) {
|
||||
m_accepted = true;
|
||||
});
|
||||
reader.construct(assets->json("/interface/windowconfig/error.config:paneLayout"), m_errorPane.get());
|
||||
|
||||
m_paneManager->displayPane(PaneLayer::Window, m_errorPane, [this](PanePtr) {
|
||||
m_accepted = true;
|
||||
});
|
||||
}
|
||||
|
||||
void ErrorScreen::setMessage(String const& errorMessage) {
|
||||
m_errorPane->fetchChild<LabelWidget>("labelError")->setText(errorMessage);
|
||||
m_accepted = false;
|
||||
}
|
||||
|
||||
bool ErrorScreen::accepted() {
|
||||
return m_accepted;
|
||||
}
|
||||
|
||||
void ErrorScreen::render() {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
for (auto backdropImage : assets->json("/interface/windowconfig/title.config:backdropImages").toArray()) {
|
||||
Vec2F offset = jsonToVec2F(backdropImage.get(0)) * interfaceScale();
|
||||
String image = backdropImage.getString(1);
|
||||
float scale = backdropImage.getFloat(2);
|
||||
Vec2F imageSize = Vec2F(m_guiContext->textureSize(image)) * interfaceScale() * scale;
|
||||
|
||||
Vec2F lowerLeft = Vec2F(windowWidth() / 2.0f, windowHeight());
|
||||
lowerLeft[0] -= imageSize[0] / 2;
|
||||
lowerLeft[1] -= imageSize[1];
|
||||
lowerLeft += offset;
|
||||
RectF screenCoords(lowerLeft, lowerLeft + imageSize);
|
||||
m_guiContext->drawQuad(image, screenCoords);
|
||||
}
|
||||
|
||||
m_paneManager->render();
|
||||
renderCursor();
|
||||
}
|
||||
|
||||
bool ErrorScreen::handleInputEvent(InputEvent const& event) {
|
||||
if (auto mouseMove = event.ptr<MouseMoveEvent>())
|
||||
m_cursorScreenPos = mouseMove->mousePosition;
|
||||
|
||||
return m_paneManager->sendInputEvent(event);
|
||||
}
|
||||
|
||||
void ErrorScreen::update() {
|
||||
m_paneManager->update();
|
||||
}
|
||||
|
||||
void ErrorScreen::renderCursor() {
|
||||
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());
|
||||
}
|
||||
|
||||
float ErrorScreen::interfaceScale() const {
|
||||
return m_guiContext->interfaceScale();
|
||||
}
|
||||
|
||||
unsigned ErrorScreen::windowHeight() const {
|
||||
return m_guiContext->windowHeight();
|
||||
}
|
||||
|
||||
unsigned ErrorScreen::windowWidth() const {
|
||||
return m_guiContext->windowWidth();
|
||||
}
|
||||
|
||||
}
|
51
source/frontend/StarErrorScreen.hpp
Normal file
51
source/frontend/StarErrorScreen.hpp
Normal file
|
@ -0,0 +1,51 @@
|
|||
#ifndef STAR_ERROR_SCREEN_HPP
|
||||
#define STAR_ERROR_SCREEN_HPP
|
||||
|
||||
#include "StarVector.hpp"
|
||||
#include "StarString.hpp"
|
||||
#include "StarInterfaceCursor.hpp"
|
||||
#include "StarInputEvent.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(Pane);
|
||||
STAR_CLASS(PaneManager);
|
||||
STAR_CLASS(GuiContext);
|
||||
|
||||
STAR_CLASS(ErrorScreen);
|
||||
|
||||
class ErrorScreen {
|
||||
public:
|
||||
ErrorScreen();
|
||||
|
||||
// Resets accepted
|
||||
void setMessage(String const& message);
|
||||
|
||||
bool accepted();
|
||||
|
||||
void render();
|
||||
|
||||
bool handleInputEvent(InputEvent const& event);
|
||||
void update();
|
||||
|
||||
private:
|
||||
void renderCursor();
|
||||
|
||||
void back();
|
||||
|
||||
float interfaceScale() const;
|
||||
unsigned windowHeight() const;
|
||||
unsigned windowWidth() const;
|
||||
|
||||
GuiContext* m_guiContext;
|
||||
PaneManagerPtr m_paneManager;
|
||||
PanePtr m_errorPane;
|
||||
|
||||
bool m_accepted;
|
||||
Vec2I m_cursorScreenPos;
|
||||
InterfaceCursor m_cursor;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
194
source/frontend/StarGraphicsMenu.cpp
Normal file
194
source/frontend/StarGraphicsMenu.cpp
Normal file
|
@ -0,0 +1,194 @@
|
|||
#include "StarGraphicsMenu.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarConfiguration.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarListWidget.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarSliderBar.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarOrderedSet.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
GraphicsMenu::GraphicsMenu() {
|
||||
GuiReader reader;
|
||||
reader.registerCallback("cancel",
|
||||
[&](Widget*) {
|
||||
dismiss();
|
||||
});
|
||||
reader.registerCallback("accept",
|
||||
[&](Widget*) {
|
||||
apply();
|
||||
applyWindowSettings();
|
||||
});
|
||||
reader.registerCallback("resSlider", [=](Widget*) {
|
||||
Vec2U res = m_resList[fetchChild<SliderBarWidget>("resSlider")->val()];
|
||||
m_localChanges.set("fullscreenResolution", jsonFromVec2U(res));
|
||||
syncGui();
|
||||
});
|
||||
reader.registerCallback("zoomSlider", [=](Widget*) {
|
||||
auto slider = fetchChild<SliderBarWidget>("zoomSlider");
|
||||
m_localChanges.set("zoomLevel", m_zoomList[slider->val()]);
|
||||
Root::singleton().configuration()->set("zoomLevel", m_zoomList[slider->val()]);
|
||||
syncGui();
|
||||
});
|
||||
reader.registerCallback("speechBubbleCheckbox", [=](Widget*) {
|
||||
auto button = fetchChild<ButtonWidget>("speechBubbleCheckbox");
|
||||
m_localChanges.set("speechBubbles", button->isChecked());
|
||||
Root::singleton().configuration()->set("speechBubbles", button->isChecked());
|
||||
syncGui();
|
||||
});
|
||||
reader.registerCallback("interactiveHighlightCheckbox", [=](Widget*) {
|
||||
auto button = fetchChild<ButtonWidget>("interactiveHighlightCheckbox");
|
||||
m_localChanges.set("interactiveHighlight", button->isChecked());
|
||||
Root::singleton().configuration()->set("interactiveHighlight", button->isChecked());
|
||||
syncGui();
|
||||
});
|
||||
reader.registerCallback("fullscreenCheckbox", [=](Widget*) {
|
||||
bool checked = fetchChild<ButtonWidget>("fullscreenCheckbox")->isChecked();
|
||||
m_localChanges.set("fullscreen", checked);
|
||||
if (checked)
|
||||
m_localChanges.set("borderless", !checked);
|
||||
syncGui();
|
||||
});
|
||||
reader.registerCallback("borderlessCheckbox", [=](Widget*) {
|
||||
bool checked = fetchChild<ButtonWidget>("borderlessCheckbox")->isChecked();
|
||||
m_localChanges.set("borderless", checked);
|
||||
if (checked)
|
||||
m_localChanges.set("fullscreen", !checked);
|
||||
syncGui();
|
||||
});
|
||||
reader.registerCallback("textureLimitCheckbox", [=](Widget*) {
|
||||
m_localChanges.set("limitTextureAtlasSize", fetchChild<ButtonWidget>("textureLimitCheckbox")->isChecked());
|
||||
syncGui();
|
||||
});
|
||||
reader.registerCallback("multiTextureCheckbox", [=](Widget*) {
|
||||
m_localChanges.set("useMultiTexturing", fetchChild<ButtonWidget>("multiTextureCheckbox")->isChecked());
|
||||
syncGui();
|
||||
});
|
||||
reader.registerCallback("monochromeCheckbox", [=](Widget*) {
|
||||
bool checked = fetchChild<ButtonWidget>("monochromeCheckbox")->isChecked();
|
||||
m_localChanges.set("monochromeLighting", checked);
|
||||
Root::singleton().configuration()->set("monochromeLighting", checked);
|
||||
syncGui();
|
||||
});
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
Json paneLayout = assets->json("/interface/windowconfig/graphicsmenu.config:paneLayout");
|
||||
|
||||
m_resList = jsonToVec2UList(assets->json("/interface/windowconfig/graphicsmenu.config:resolutionList"));
|
||||
m_zoomList = jsonToFloatList(assets->json("/interface/windowconfig/graphicsmenu.config:zoomList"));
|
||||
|
||||
reader.construct(paneLayout, this);
|
||||
|
||||
fetchChild<SliderBarWidget>("resSlider")->setRange(0, m_resList.size() - 1, 1);
|
||||
fetchChild<SliderBarWidget>("zoomSlider")->setRange(0, m_zoomList.size() - 1, 1);
|
||||
|
||||
initConfig();
|
||||
syncGui();
|
||||
}
|
||||
|
||||
void GraphicsMenu::show() {
|
||||
Pane::show();
|
||||
initConfig();
|
||||
syncGui();
|
||||
}
|
||||
|
||||
void GraphicsMenu::dismissed() {
|
||||
Pane::dismissed();
|
||||
}
|
||||
|
||||
void GraphicsMenu::toggleFullscreen() {
|
||||
bool fullscreen = m_localChanges.get("fullscreen").toBool();
|
||||
bool borderless = m_localChanges.get("borderless").toBool();
|
||||
|
||||
m_localChanges.set("fullscreen", !(fullscreen || borderless));
|
||||
Root::singleton().configuration()->set("fullscreen", !(fullscreen || borderless));
|
||||
|
||||
m_localChanges.set("borderless", false);
|
||||
Root::singleton().configuration()->set("borderless", false);
|
||||
|
||||
applyWindowSettings();
|
||||
syncGui();
|
||||
}
|
||||
|
||||
StringList const GraphicsMenu::ConfigKeys = {
|
||||
"fullscreenResolution",
|
||||
"zoomLevel",
|
||||
"speechBubbles",
|
||||
"interactiveHighlight",
|
||||
"fullscreen",
|
||||
"borderless",
|
||||
"limitTextureAtlasSize",
|
||||
"useMultiTexturing",
|
||||
"monochromeLighting"
|
||||
};
|
||||
|
||||
void GraphicsMenu::initConfig() {
|
||||
auto configuration = Root::singleton().configuration();
|
||||
|
||||
for (auto key : ConfigKeys) {
|
||||
m_localChanges.set(key, configuration->get(key));
|
||||
}
|
||||
}
|
||||
|
||||
void GraphicsMenu::syncGui() {
|
||||
Vec2U res = jsonToVec2U(m_localChanges.get("fullscreenResolution"));
|
||||
auto resSlider = fetchChild<SliderBarWidget>("resSlider");
|
||||
auto resIt = std::lower_bound(m_resList.begin(), m_resList.end(), res, [&](Vec2U const& a, Vec2U const& b) {
|
||||
return a[0] * a[1] < b[0] * b[1]; // sort by number of pixels
|
||||
});
|
||||
if (resIt != m_resList.end()) {
|
||||
size_t resIndex = resIt - m_resList.begin();
|
||||
resIndex = std::min(resIndex, m_resList.size() - 1);
|
||||
resSlider->setVal(resIndex, false);
|
||||
} else {
|
||||
resSlider->setVal(m_resList.size() - 1);
|
||||
}
|
||||
fetchChild<LabelWidget>("resValueLabel")->setText(strf("%dx%d", res[0], res[1]));
|
||||
|
||||
auto zoomSlider = fetchChild<SliderBarWidget>("zoomSlider");
|
||||
auto zoomIt = std::lower_bound(m_zoomList.begin(), m_zoomList.end(), m_localChanges.get("zoomLevel").toFloat());
|
||||
if (zoomIt != m_zoomList.end()) {
|
||||
size_t zoomIndex = zoomIt - m_zoomList.begin();
|
||||
zoomIndex = std::min(zoomIndex, m_resList.size() - 1);
|
||||
zoomSlider->setVal(zoomIndex, false);
|
||||
} else {
|
||||
zoomSlider->setVal(m_zoomList.size() - 1);
|
||||
}
|
||||
fetchChild<LabelWidget>("zoomValueLabel")->setText(strf("%dx", m_localChanges.get("zoomLevel").toInt()));
|
||||
|
||||
|
||||
fetchChild<ButtonWidget>("speechBubbleCheckbox")->setChecked(m_localChanges.get("speechBubbles").toBool());
|
||||
fetchChild<ButtonWidget>("interactiveHighlightCheckbox")->setChecked(m_localChanges.get("interactiveHighlight").toBool());
|
||||
fetchChild<ButtonWidget>("fullscreenCheckbox")->setChecked(m_localChanges.get("fullscreen").toBool());
|
||||
fetchChild<ButtonWidget>("borderlessCheckbox")->setChecked(m_localChanges.get("borderless").toBool());
|
||||
fetchChild<ButtonWidget>("textureLimitCheckbox")->setChecked(m_localChanges.get("limitTextureAtlasSize").toBool());
|
||||
fetchChild<ButtonWidget>("multiTextureCheckbox")->setChecked(m_localChanges.get("useMultiTexturing").optBool().value(true));
|
||||
fetchChild<ButtonWidget>("monochromeCheckbox")->setChecked(m_localChanges.get("monochromeLighting").toBool());
|
||||
}
|
||||
|
||||
void GraphicsMenu::apply() {
|
||||
auto configuration = Root::singleton().configuration();
|
||||
for (auto p : m_localChanges) {
|
||||
configuration->set(p.first, p.second);
|
||||
}
|
||||
}
|
||||
|
||||
void GraphicsMenu::applyWindowSettings() {
|
||||
auto configuration = Root::singleton().configuration();
|
||||
auto appController = GuiContext::singleton().applicationController();
|
||||
if (configuration->get("fullscreen").toBool())
|
||||
appController->setFullscreenWindow(jsonToVec2U(configuration->get("fullscreenResolution")));
|
||||
else if (configuration->get("borderless").toBool())
|
||||
appController->setBorderlessWindow();
|
||||
else if (configuration->get("maximized").toBool())
|
||||
appController->setMaximizedWindow();
|
||||
else
|
||||
appController->setNormalWindow(jsonToVec2U(configuration->get("windowedResolution")));
|
||||
}
|
||||
|
||||
}
|
36
source/frontend/StarGraphicsMenu.hpp
Normal file
36
source/frontend/StarGraphicsMenu.hpp
Normal file
|
@ -0,0 +1,36 @@
|
|||
#ifndef STAR_GRAPHICS_MENU_HPP
|
||||
#define STAR_GRAPHICS_MENU_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(GraphicsMenu);
|
||||
|
||||
class GraphicsMenu : public Pane {
|
||||
public:
|
||||
GraphicsMenu();
|
||||
|
||||
void show() override;
|
||||
void dismissed() override;
|
||||
|
||||
void toggleFullscreen();
|
||||
|
||||
private:
|
||||
static StringList const ConfigKeys;
|
||||
|
||||
void initConfig();
|
||||
void syncGui();
|
||||
|
||||
void apply();
|
||||
void applyWindowSettings();
|
||||
|
||||
List<Vec2U> m_resList;
|
||||
List<float> m_zoomList;
|
||||
|
||||
JsonObject m_localChanges;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
62
source/frontend/StarInterfaceCursor.cpp
Normal file
62
source/frontend/StarInterfaceCursor.cpp
Normal file
|
@ -0,0 +1,62 @@
|
|||
#include "StarInterfaceCursor.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarImageMetadataDatabase.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
InterfaceCursor::InterfaceCursor() {
|
||||
resetCursor();
|
||||
}
|
||||
|
||||
void InterfaceCursor::resetCursor() {
|
||||
auto& root = Root::singleton();
|
||||
auto assets = root.assets();
|
||||
setCursor(assets->json("/interface.config:defaultCursor").toString());
|
||||
}
|
||||
|
||||
void InterfaceCursor::setCursor(String const& configFile) {
|
||||
if (m_configFile == configFile)
|
||||
return;
|
||||
|
||||
m_configFile = configFile;
|
||||
|
||||
auto& root = Root::singleton();
|
||||
auto assets = root.assets();
|
||||
auto imageMetadata = root.imageMetadataDatabase();
|
||||
|
||||
auto config = assets->json(m_configFile);
|
||||
|
||||
m_offset = jsonToVec2I(config.get("offset"));
|
||||
if (config.contains("image")) {
|
||||
m_drawable = config.getString("image");
|
||||
m_size = Vec2I{imageMetadata->imageSize(config.getString("image"))};
|
||||
} else {
|
||||
m_drawable = Animation(config.get("animation"), "/interface");
|
||||
m_size = Vec2I(m_drawable.get<Animation>().drawable(1.0f).boundBox(false).size());
|
||||
}
|
||||
}
|
||||
|
||||
Drawable InterfaceCursor::drawable() const {
|
||||
if (m_drawable.is<String>())
|
||||
return Drawable::makeImage(m_drawable.get<String>(), 1.0f, false, {});
|
||||
else
|
||||
return m_drawable.get<Animation>().drawable(1.0f);
|
||||
}
|
||||
|
||||
Vec2I InterfaceCursor::size() const {
|
||||
return m_size;
|
||||
}
|
||||
|
||||
Vec2I InterfaceCursor::offset() const {
|
||||
return m_offset;
|
||||
}
|
||||
|
||||
void InterfaceCursor::update(float dt) {
|
||||
if (m_drawable.is<Animation>()) {
|
||||
m_drawable.get<Animation>().update(dt);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
35
source/frontend/StarInterfaceCursor.hpp
Normal file
35
source/frontend/StarInterfaceCursor.hpp
Normal file
|
@ -0,0 +1,35 @@
|
|||
#ifndef STAR_INTERFACE_CURSOR_HPP
|
||||
#define STAR_INTERFACE_CURSOR_HPP
|
||||
|
||||
#include "StarJson.hpp"
|
||||
#include "StarAnimation.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
class InterfaceCursor {
|
||||
public:
|
||||
InterfaceCursor();
|
||||
|
||||
// Sets the cursor to the default defined in interface.config
|
||||
void resetCursor();
|
||||
|
||||
// Sets the cursor config to the given config IF the config is different than
|
||||
// the current one. Expects a full asset path to the cursor config.
|
||||
void setCursor(String const& configFile);
|
||||
|
||||
Drawable drawable() const;
|
||||
Vec2I size() const;
|
||||
Vec2I offset() const;
|
||||
|
||||
void update(float dt);
|
||||
|
||||
private:
|
||||
String m_configFile;
|
||||
Vec2I m_offset;
|
||||
Vec2I m_size;
|
||||
MVariant<String, Animation> m_drawable;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
426
source/frontend/StarInventory.cpp
Normal file
426
source/frontend/StarInventory.cpp
Normal file
|
@ -0,0 +1,426 @@
|
|||
#include "StarInventory.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarItemTooltip.hpp"
|
||||
#include "StarSimpleTooltip.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarUniverseClient.hpp"
|
||||
#include "StarItemGridWidget.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarPortraitWidget.hpp"
|
||||
#include "StarPaneManager.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarPlayerInventory.hpp"
|
||||
#include "StarPlayerCompanions.hpp"
|
||||
#include "StarWorldClient.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarItem.hpp"
|
||||
#include "StarMainInterface.hpp"
|
||||
#include "StarMerchantInterface.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarStatistics.hpp"
|
||||
#include "StarAugmentItem.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
InventoryPane::InventoryPane(MainInterface* parent, PlayerPtr player, ContainerInteractorPtr containerInteractor) {
|
||||
m_parent = parent;
|
||||
m_player = move(player);
|
||||
m_containerInteractor = move(containerInteractor);
|
||||
|
||||
GuiReader invWindowReader;
|
||||
auto config = Root::singleton().assets()->json("/interface/windowconfig/playerinventory.config");
|
||||
|
||||
auto leftClickCallback = [this](String const& bagType, Widget* widget) {
|
||||
auto itemGrid = convert<ItemGridWidget>(widget);
|
||||
InventorySlot inventorySlot = BagSlot(bagType, itemGrid->selectedIndex());
|
||||
|
||||
if (context()->shiftHeld()) {
|
||||
if (auto sourceItem = itemGrid->selectedItem()) {
|
||||
if (auto activeMerchantPane = m_parent->activeMerchantPane()) {
|
||||
auto remainder = activeMerchantPane->addItems(m_player->inventory()->takeSlot(inventorySlot));
|
||||
if (remainder && !remainder->empty())
|
||||
m_player->inventory()->setItem(inventorySlot, remainder);
|
||||
} else if (m_containerInteractor->containerOpen()) {
|
||||
m_player->inventory()->takeSlot(inventorySlot);
|
||||
m_containerInteractor->addToContainer(sourceItem);
|
||||
m_containerSource = inventorySlot;
|
||||
m_expectingSwap = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m_player->inventory()->shiftSwap(inventorySlot);
|
||||
}
|
||||
};
|
||||
|
||||
auto rightClickCallback = [this](InventorySlot slot) {
|
||||
if (ItemPtr slotItem = m_player->inventory()->itemsAt(slot)) {
|
||||
auto swapItem = m_player->inventory()->swapSlotItem();
|
||||
if (!swapItem || swapItem->empty() || swapItem->couldStack(slotItem)) {
|
||||
uint64_t count = swapItem ? swapItem->couldStack(slotItem) : slotItem->maxStack();
|
||||
if (context()->shiftHeld())
|
||||
count = max(1, min<int>(count, slotItem->count() / 2));
|
||||
else
|
||||
count = 1;
|
||||
|
||||
if (auto taken = slotItem->take(count)) {
|
||||
if (swapItem)
|
||||
swapItem->stackWith(taken);
|
||||
else
|
||||
m_player->inventory()->setSwapSlotItem(taken);
|
||||
}
|
||||
} else if (auto augment = as<AugmentItem>(swapItem)) {
|
||||
if (auto augmented = augment->applyTo(slotItem))
|
||||
m_player->inventory()->setItem(slot, augmented);
|
||||
}
|
||||
}
|
||||
};
|
||||
auto bagGridCallback = [rightClickCallback](String const& bagType, Widget* widget) {
|
||||
auto slot = BagSlot(bagType, convert<ItemGridWidget>(widget)->selectedIndex());
|
||||
rightClickCallback(slot);
|
||||
};
|
||||
|
||||
Json itemBagConfig = config.get("bagConfig");
|
||||
auto bagOrder = itemBagConfig.toObject().keys().sorted([&itemBagConfig](String const& a, String const& b) {
|
||||
return itemBagConfig.get(a).getInt("order", 0) < itemBagConfig.get(b).getInt("order", 0);
|
||||
});
|
||||
for (auto name : bagOrder) {
|
||||
auto itemGrid = itemBagConfig.get(name).getString("itemGrid");
|
||||
invWindowReader.registerCallback(itemGrid, bind(leftClickCallback, name, _1));
|
||||
invWindowReader.registerCallback(strf("%s.right", itemGrid), bind(bagGridCallback, name, _1));
|
||||
}
|
||||
|
||||
invWindowReader.registerCallback("close", [=](Widget*) {
|
||||
dismiss();
|
||||
});
|
||||
|
||||
invWindowReader.registerCallback("sort", [=](Widget*) {
|
||||
m_player->inventory()->condenseBagStacks(m_selectedTab);
|
||||
m_player->inventory()->sortBag(m_selectedTab);
|
||||
// Don't show sorted items as new items
|
||||
m_itemGrids[m_selectedTab]->updateItemState();
|
||||
m_itemGrids[m_selectedTab]->clearChangedSlots();
|
||||
});
|
||||
|
||||
invWindowReader.registerCallback("gridModeSelector", [=](Widget* widget) {
|
||||
auto selected = convert<ButtonWidget>(widget)->data().toString();
|
||||
selectTab(m_tabButtonData.keyOf(selected));
|
||||
});
|
||||
|
||||
auto registerSlotCallbacks = [&](String name, InventorySlot slot) {
|
||||
invWindowReader.registerCallback(name, [=](Widget* paneObj) {
|
||||
if (as<ItemSlotWidget>(paneObj))
|
||||
m_player->inventory()->shiftSwap(slot);
|
||||
else
|
||||
throw GuiException("Invalid object type, expected ItemSlotWidget");
|
||||
});
|
||||
invWindowReader.registerCallback(name + ".right", [=](Widget* paneObj) {
|
||||
if (as<ItemSlotWidget>(paneObj))
|
||||
rightClickCallback(slot);
|
||||
else
|
||||
throw GuiException("Invalid object type, expected ItemSlotWidget");
|
||||
});
|
||||
};
|
||||
|
||||
for (auto const p : EquipmentSlotNames)
|
||||
registerSlotCallbacks(p.second, p.first);
|
||||
registerSlotCallbacks("trash", TrashSlot());
|
||||
|
||||
invWindowReader.construct(config.get("paneLayout"), this);
|
||||
|
||||
m_trashSlot = fetchChild<ItemSlotWidget>("trash");
|
||||
m_trashBurn = GameTimer(config.get("trashBurnTimeout").toFloat());
|
||||
|
||||
m_disabledTechOverlays.append(fetchChild<ImageWidget>("techHeadDisabled"));
|
||||
m_disabledTechOverlays.append(fetchChild<ImageWidget>("techBodyDisabled"));
|
||||
m_disabledTechOverlays.append(fetchChild<ImageWidget>("techLegsDisabled"));
|
||||
|
||||
for (auto const p : EquipmentSlotNames) {
|
||||
if (auto itemSlot = fetchChild<ItemSlotWidget>(p.second))
|
||||
itemSlot->setItem(m_player->inventory()->itemsAt(p.first));
|
||||
}
|
||||
|
||||
for (auto name : bagOrder) {
|
||||
auto itemTab = itemBagConfig.get(name);
|
||||
m_itemGrids[name] = fetchChild<ItemGridWidget>(itemTab.getString("itemGrid"));
|
||||
m_itemGrids[name]->setItemBag(m_player->inventory()->bagContents(name));
|
||||
m_itemGrids[name]->hide();
|
||||
m_newItemMarkers[name] = fetchChild<Widget>(itemTab.getString("newItemMarker"));
|
||||
m_tabButtonData[name] = itemTab.getString("tabButtonData");
|
||||
}
|
||||
selectTab(bagOrder[0]);
|
||||
|
||||
auto centralPortrait = fetchChild<PortraitWidget>("portrait");
|
||||
centralPortrait->setEntity(m_player);
|
||||
|
||||
auto portrait = make_shared<PortraitWidget>(m_player, PortraitMode::Bust);
|
||||
portrait->setIconMode();
|
||||
setTitle(portrait, m_player->name(), config.getString("subtitle"));
|
||||
|
||||
m_expectingSwap = false;
|
||||
|
||||
if (auto item = m_player->inventory()->swapSlotItem())
|
||||
m_currentSwapSlotItem = item->descriptor();
|
||||
m_pickUpSounds = jsonToStringList(config.get("sounds").get("pickup"));
|
||||
m_putDownSounds = jsonToStringList(config.get("sounds").get("putdown"));
|
||||
}
|
||||
|
||||
void InventoryPane::displayed() {
|
||||
Pane::displayed();
|
||||
m_expectingSwap = false;
|
||||
|
||||
for (auto grid : m_itemGrids)
|
||||
grid.second->updateItemState();
|
||||
m_itemGrids[m_selectedTab]->indicateChangedSlots();
|
||||
}
|
||||
|
||||
PanePtr InventoryPane::createTooltip(Vec2I const& screenPosition) {
|
||||
ItemPtr item;
|
||||
if (auto child = getChildAt(screenPosition)) {
|
||||
if (auto itemSlot = as<ItemSlotWidget>(child)) {
|
||||
item = itemSlot->item();
|
||||
if (!item) {
|
||||
auto widgetData = itemSlot->data();
|
||||
if (widgetData && widgetData.type() == Json::Type::Object) {
|
||||
if (auto text = widgetData.optString("tooltipText"))
|
||||
return SimpleTooltipBuilder::buildTooltip(*text);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (auto itemGrid = as<ItemGridWidget>(child))
|
||||
item = itemGrid->itemAt(screenPosition);
|
||||
}
|
||||
if (item)
|
||||
return ItemTooltipBuilder::buildItemTooltip(item, m_player);
|
||||
|
||||
auto techDatabase = Root::singleton().techDatabase();
|
||||
for (auto const& p : TechTypeNames) {
|
||||
if (auto techIcon = fetchChild<ImageWidget>(strf("tech%s", p.second))) {
|
||||
if (techIcon->screenBoundRect().contains(screenPosition)) {
|
||||
if (auto techModule = m_player->techs()->equippedTechs().maybe(p.first))
|
||||
return SimpleTooltipBuilder::buildTooltip(techDatabase->tech(*techModule).description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool InventoryPane::giveContainerResult(ContainerResult result) {
|
||||
if (!m_expectingSwap)
|
||||
return false;
|
||||
|
||||
for (auto item : result) {
|
||||
auto inv = m_player->inventory();
|
||||
m_player->triggerPickupEvents(item);
|
||||
|
||||
auto remainder = inv->stackWith(m_containerSource, item);
|
||||
if (remainder && !remainder->empty())
|
||||
m_player->giveItem(remainder);
|
||||
}
|
||||
|
||||
m_expectingSwap = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void InventoryPane::updateItems() {
|
||||
for (auto p : m_itemGrids)
|
||||
p.second->updateItemState();
|
||||
}
|
||||
|
||||
bool InventoryPane::containsNewItems() const {
|
||||
for (auto p : m_itemGrids) {
|
||||
if (p.second->slotsChanged())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void InventoryPane::update() {
|
||||
auto inventory = m_player->inventory();
|
||||
auto context = Widget::context();
|
||||
|
||||
HashSet<ItemPtr> customBarItems;
|
||||
for (uint8_t i = 0; i < inventory->customBarIndexes(); ++i) {
|
||||
if (auto primarySlot = inventory->customBarPrimarySlot(i)) {
|
||||
if (auto primaryItem = inventory->itemsAt(*primarySlot))
|
||||
customBarItems.add(primaryItem);
|
||||
}
|
||||
if (auto secondarySlot = inventory->customBarSecondarySlot(i)) {
|
||||
if (auto secondaryItem = inventory->itemsAt(*secondarySlot))
|
||||
customBarItems.add(secondaryItem);
|
||||
}
|
||||
}
|
||||
|
||||
m_trashSlot->setItem(inventory->itemsAt(TrashSlot()));
|
||||
m_trashSlot->showLinkIndicator(customBarItems.contains(m_trashSlot->item()));
|
||||
if (auto trashItem = m_trashSlot->item()) {
|
||||
if (m_trashBurn.tick() && trashItem->count() > 0) {
|
||||
m_player->statistics()->recordEvent("trashItem", JsonObject{
|
||||
{"itemName", trashItem->name()},
|
||||
{"count", trashItem->count()},
|
||||
{"category", trashItem->category()}
|
||||
});
|
||||
trashItem->take(trashItem->count());
|
||||
}
|
||||
} else {
|
||||
m_trashBurn.reset();
|
||||
}
|
||||
m_trashSlot->setProgress(m_trashBurn.timer / m_trashBurn.time);
|
||||
|
||||
for (auto const& p : EquipmentSlotNames) {
|
||||
if (auto itemSlot = fetchChild<ItemSlotWidget>(p.second)) {
|
||||
itemSlot->setItem(inventory->itemsAt(p.first));
|
||||
itemSlot->showLinkIndicator(customBarItems.contains(itemSlot->item()));
|
||||
}
|
||||
}
|
||||
|
||||
auto techDatabase = Root::singleton().techDatabase();
|
||||
for (auto const& p : TechTypeNames) {
|
||||
if (auto techIcon = fetchChild<ImageWidget>(strf("tech%s", p.second))) {
|
||||
if (auto techModule = m_player->techs()->equippedTechs().maybe(p.first))
|
||||
techIcon->setImage(techDatabase->tech(*techModule).icon);
|
||||
else
|
||||
techIcon->setImage("");
|
||||
}
|
||||
}
|
||||
|
||||
if (ItemPtr swapSlot = inventory->swapSlotItem()) {
|
||||
for (auto pair : m_itemGrids) {
|
||||
if (pair.first != m_selectedTab && PlayerInventory::itemAllowedInBag(swapSlot, pair.first)) {
|
||||
selectTab(pair.first);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto p : m_itemGrids) {
|
||||
p.second->updateItemState();
|
||||
for (size_t i = 0; i < p.second->itemSlots(); ++i) {
|
||||
auto itemWidget = p.second->itemWidgetAt(i);
|
||||
itemWidget->showLinkIndicator(customBarItems.contains(itemWidget->item()));
|
||||
}
|
||||
}
|
||||
|
||||
m_itemGrids[m_selectedTab]->clearChangedSlots();
|
||||
|
||||
for (auto pair : m_newItemMarkers) {
|
||||
if (m_itemGrids[pair.first]->slotsChanged())
|
||||
pair.second->show();
|
||||
else
|
||||
pair.second->hide();
|
||||
}
|
||||
|
||||
for (auto techOverlay : m_disabledTechOverlays)
|
||||
techOverlay->setVisibility(m_player->techOverridden());
|
||||
|
||||
auto healthLabel = fetchChild<LabelWidget>("healthtext");
|
||||
healthLabel->setText(strf("%d", m_player->maxHealth()));
|
||||
auto energyLabel = fetchChild<LabelWidget>("energytext");
|
||||
energyLabel->setText(strf("%d", m_player->maxEnergy()));
|
||||
auto weaponLabel = fetchChild<LabelWidget>("weapontext");
|
||||
weaponLabel->setText(strf("%d%%", ceil(m_player->powerMultiplier() * 100)));
|
||||
auto defenseLabel = fetchChild<LabelWidget>("defensetext");
|
||||
if (m_player->protection() == 0)
|
||||
defenseLabel->setText("--");
|
||||
else
|
||||
defenseLabel->setText(strf("%d", ceil(m_player->protection())));
|
||||
|
||||
auto moneyLabel = fetchChild<LabelWidget>("lblMoney");
|
||||
moneyLabel->setText(strf("%d", m_player->currency("money")));
|
||||
|
||||
if (m_player->currency("essence") > 0) {
|
||||
fetchChild<ImageWidget>("imgEssenceIcon")->show();
|
||||
auto essenceLabel = fetchChild<LabelWidget>("lblEssence");
|
||||
essenceLabel->show();
|
||||
essenceLabel->setText(strf("%d", m_player->currency("essence")));
|
||||
} else {
|
||||
fetchChild<ImageWidget>("imgEssenceIcon")->hide();
|
||||
fetchChild<LabelWidget>("lblEssence")->hide();
|
||||
}
|
||||
|
||||
auto config = Root::singleton().assets()->json("/interface/windowconfig/playerinventory.config");
|
||||
|
||||
auto pets = m_player->companions()->getCompanions("pets");
|
||||
if (pets.size() > 0) {
|
||||
auto pet = pets.first();
|
||||
auto companionImage = fetchChild<ImageWidget>("companionSlot");
|
||||
companionImage->setVisibility(true);
|
||||
|
||||
companionImage->setDrawables(pet->portrait());
|
||||
auto nameLabel = fetchChild<LabelWidget>("companionName");
|
||||
if (auto name = pet->name()) {
|
||||
nameLabel->setText(pet->name()->toUpper());
|
||||
} else {
|
||||
nameLabel->setText(config.getString("defaultPetNameLabel"));
|
||||
}
|
||||
|
||||
auto attackLabel = fetchChild<LabelWidget>("companionAttackStat");
|
||||
if (auto attack = pet->stat("attack")) {
|
||||
attackLabel->setText(strf("%.0f", *attack));
|
||||
} else {
|
||||
attackLabel->setText("");
|
||||
}
|
||||
|
||||
auto defenseLabel = fetchChild<LabelWidget>("companionDefenseStat");
|
||||
if (auto defense = pet->stat("defense")) {
|
||||
defenseLabel->setText(strf("%.0f", *defense));
|
||||
} else {
|
||||
defenseLabel->setText("");
|
||||
}
|
||||
|
||||
if (containsChild("companionHealthBar")) {
|
||||
auto healthBar = fetchChild<ProgressWidget>("companionHealthBar");
|
||||
Maybe<float> health = pet->resource("health");
|
||||
Maybe<float> healthMax = pet->resourceMax("health");
|
||||
if (health && healthMax) {
|
||||
healthBar->setCurrentProgressLevel(*health);
|
||||
healthBar->setMaxProgressLevel(*healthMax);
|
||||
} else {
|
||||
healthBar->setCurrentProgressLevel(0);
|
||||
healthBar->setMaxProgressLevel(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fetchChild<ImageWidget>("companionSlot")->setVisibility(false);
|
||||
|
||||
fetchChild<LabelWidget>("companionName")->setText(config.getString("defaultPetNameLabel"));
|
||||
|
||||
fetchChild<LabelWidget>("companionAttackStat")->setText("");
|
||||
fetchChild<LabelWidget>("companionDefenseStat")->setText("");
|
||||
|
||||
if (containsChild("companionHealthBar")) {
|
||||
auto healthBar = fetchChild<ProgressWidget>("companionHealthBar");
|
||||
healthBar->setCurrentProgressLevel(0);
|
||||
healthBar->setMaxProgressLevel(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (auto item = inventory->swapSlotItem()) {
|
||||
if (!m_currentSwapSlotItem || !item->matches(*m_currentSwapSlotItem, true) || item->count() > m_currentSwapSlotItem->count())
|
||||
context->playAudio(RandomSource().randFrom(m_pickUpSounds));
|
||||
else if (item->count() < m_currentSwapSlotItem->count())
|
||||
context->playAudio(RandomSource().randFrom(m_putDownSounds));
|
||||
|
||||
m_currentSwapSlotItem = item->descriptor();
|
||||
} else {
|
||||
if (m_currentSwapSlotItem)
|
||||
context->playAudio(RandomSource().randFrom(m_putDownSounds));
|
||||
m_currentSwapSlotItem = {};
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryPane::selectTab(String const& selected) {
|
||||
for (auto grid : m_itemGrids)
|
||||
grid.second->hide();
|
||||
m_selectedTab = selected;
|
||||
m_itemGrids[m_selectedTab]->show();
|
||||
m_itemGrids[m_selectedTab]->indicateChangedSlots();
|
||||
|
||||
auto tabs = fetchChild<ButtonGroupWidget>("gridModeSelector");
|
||||
for (auto button : tabs->buttons())
|
||||
if (button->data().toString().equalsIgnoreCase(m_tabButtonData[selected]))
|
||||
tabs->select(tabs->id(button));
|
||||
}
|
||||
|
||||
}
|
67
source/frontend/StarInventory.hpp
Normal file
67
source/frontend/StarInventory.hpp
Normal file
|
@ -0,0 +1,67 @@
|
|||
#ifndef STAR_INVENTORY_HPP
|
||||
#define STAR_INVENTORY_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
#include "StarInventoryTypes.hpp"
|
||||
#include "StarItemDescriptor.hpp"
|
||||
#include "StarPlayerTech.hpp"
|
||||
#include "StarGameTimers.hpp"
|
||||
#include "StarContainerInteractor.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(MainInterface);
|
||||
STAR_CLASS(UniverseClient);
|
||||
STAR_CLASS(Player);
|
||||
STAR_CLASS(Item);
|
||||
STAR_CLASS(ItemSlotWidget);
|
||||
STAR_CLASS(ItemGridWidget);
|
||||
STAR_CLASS(ImageWidget);
|
||||
STAR_CLASS(Widget);
|
||||
STAR_CLASS(InventoryPane);
|
||||
|
||||
class InventoryPane : public Pane {
|
||||
public:
|
||||
InventoryPane(MainInterface* parent, PlayerPtr player, ContainerInteractorPtr containerInteractor);
|
||||
|
||||
void displayed() override;
|
||||
PanePtr createTooltip(Vec2I const& screenPosition) override;
|
||||
|
||||
bool giveContainerResult(ContainerResult result);
|
||||
|
||||
// update only item grids, to see if they have had their slots changed
|
||||
// this is a little hacky and should probably be checked in the player inventory instead
|
||||
void updateItems();
|
||||
bool containsNewItems() const;
|
||||
|
||||
protected:
|
||||
virtual void update() override;
|
||||
void selectTab(String const& selected);
|
||||
|
||||
private:
|
||||
MainInterface* m_parent;
|
||||
PlayerPtr m_player;
|
||||
ContainerInteractorPtr m_containerInteractor;
|
||||
|
||||
bool m_expectingSwap;
|
||||
InventorySlot m_containerSource;
|
||||
|
||||
GameTimer m_trashBurn;
|
||||
ItemSlotWidgetPtr m_trashSlot;
|
||||
|
||||
Map<String, ItemGridWidgetPtr> m_itemGrids;
|
||||
Map<String, String> m_tabButtonData;
|
||||
|
||||
Map<String, WidgetPtr> m_newItemMarkers;
|
||||
String m_selectedTab;
|
||||
|
||||
StringList m_pickUpSounds;
|
||||
StringList m_putDownSounds;
|
||||
Maybe<ItemDescriptor> m_currentSwapSlotItem;
|
||||
|
||||
List<ImageWidgetPtr> m_disabledTechOverlays;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
221
source/frontend/StarItemTooltip.cpp
Normal file
221
source/frontend/StarItemTooltip.cpp
Normal file
|
@ -0,0 +1,221 @@
|
|||
#include "StarItemTooltip.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarPane.hpp"
|
||||
#include "StarListWidget.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarStoredFunctions.hpp"
|
||||
#include "StarObjectItem.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarItemSlotWidget.hpp"
|
||||
#include "StarPreviewableItem.hpp"
|
||||
#include "StarFireableItem.hpp"
|
||||
#include "StarStatusEffectItem.hpp"
|
||||
#include "StarObject.hpp"
|
||||
#include "StarLogging.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarObjectDatabase.hpp"
|
||||
#include "StarStatusEffectDatabase.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
PanePtr ItemTooltipBuilder::buildItemTooltip(ItemPtr const& item, PlayerPtr const& viewer) {
|
||||
if (!item) {
|
||||
return {};
|
||||
} else {
|
||||
PanePtr tooltip = make_shared<Pane>();
|
||||
tooltip->removeAllChildren();
|
||||
|
||||
String title;
|
||||
String subTitle;
|
||||
|
||||
String tooltipKind = item->tooltipKind();
|
||||
|
||||
if (tooltipKind.empty())
|
||||
tooltipKind = "base";
|
||||
if (!tooltipKind.endsWith(".tooltip"))
|
||||
tooltipKind = "/interface/tooltips/" + tooltipKind + ".tooltip";
|
||||
|
||||
buildItemDescriptionInner(tooltip, item, tooltipKind, title, subTitle, viewer);
|
||||
|
||||
auto titleIcon = make_shared<ItemSlotWidget>(item, "/interface/inventory/portrait.png");
|
||||
titleIcon->setBackingImageAffinity(true, true);
|
||||
titleIcon->showRarity(false);
|
||||
tooltip->setTitle(titleIcon, title, subTitle);
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
void ItemTooltipBuilder::buildItemDescription(WidgetPtr const& container, ItemPtr const& item) {
|
||||
String tooltipKind = item->tooltipKind();
|
||||
|
||||
if (tooltipKind.empty())
|
||||
tooltipKind = "base";
|
||||
if (!tooltipKind.endsWith(".itemdescription"))
|
||||
tooltipKind = "/interface/itemdescriptions/" + tooltipKind + ".itemdescription";
|
||||
|
||||
String title;
|
||||
String subTitle;
|
||||
buildItemDescriptionInner(container, item, tooltipKind, title, subTitle);
|
||||
}
|
||||
|
||||
String categoryDisplayName(String const& category) {
|
||||
Json categories = Root::singleton().assets()->json("/items/categories.config:labels");
|
||||
return categories.getString(category, category);
|
||||
}
|
||||
|
||||
void ItemTooltipBuilder::buildItemDescriptionInner(
|
||||
WidgetPtr const& container, ItemPtr const& item, String const& tooltipKind, String& title, String& subTitle, PlayerPtr const& viewer) {
|
||||
GuiReader reader;
|
||||
auto& root = Root::singleton();
|
||||
|
||||
title = item->friendlyName();
|
||||
subTitle = categoryDisplayName(item->category());
|
||||
String description = item->description();
|
||||
|
||||
reader.construct(root.assets()->json(tooltipKind), container.get());
|
||||
|
||||
if (container->containsChild("icon"))
|
||||
container->fetchChild<ItemSlotWidget>("icon")->setItem(item);
|
||||
|
||||
container->setLabel("nameLabel", item->name());
|
||||
container->setLabel("countLabel", strf("%s", item->count()));
|
||||
|
||||
container->setLabel("rarityLabel", RarityNames.getRight(item->rarity()).titleCase());
|
||||
|
||||
if (item->twoHanded())
|
||||
container->setLabel("handednessLabel", "2-Handed");
|
||||
else
|
||||
container->setLabel("handednessLabel", "1-Handed");
|
||||
|
||||
container->setLabel("countLabel", strf("%s", item->instanceValue("fuelAmount", 0).toUInt() * item->count()));
|
||||
container->setLabel("priceLabel", strf("%s", (int)item->price()));
|
||||
|
||||
if (auto objectItem = as<ObjectItem>(item)) {
|
||||
try {
|
||||
auto object = Root::singleton().objectDatabase()->createObject(objectItem->objectName(), objectItem->objectParameters());
|
||||
|
||||
if (container->containsChild("objectImage")) {
|
||||
auto drawables = object->cursorHintDrawables();
|
||||
container->fetchChild<ImageWidget>("objectImage")->setDrawables(drawables);
|
||||
}
|
||||
|
||||
if (objectItem->tooltipKind() == "container")
|
||||
container->setLabel("slotCountLabel", strf("Holds %s Items", objectItem->instanceValue("slotCount")));
|
||||
|
||||
title = object->shortDescription();
|
||||
subTitle = categoryDisplayName(object->category());
|
||||
description = object->description();
|
||||
} catch (StarException const& e) {
|
||||
Logger::error("Failed to instantiate object for object item tooltip. %s", outputException(e, false));
|
||||
}
|
||||
} else {
|
||||
if (container->containsChild("objectImage")) {
|
||||
if (auto previewable = as<PreviewableItem>(item)) {
|
||||
container->fetchChild<ImageWidget>("objectImage")->setDrawables(previewable->preview(viewer));
|
||||
} else {
|
||||
auto drawables = item->iconDrawables();
|
||||
container->fetchChild<ImageWidget>("objectImage")->setDrawables(drawables);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto tooltipFields = item->instanceValue("tooltipFields", JsonObject());
|
||||
for (auto const& pair : tooltipFields.iterateObject()) {
|
||||
if (pair.first.equalsIgnoreCase("subtitle"))
|
||||
subTitle = pair.second.toString();
|
||||
if (pair.first.endsWith("Label"))
|
||||
container->setLabel(pair.first, pair.second.type() == Json::Type::String ? pair.second.toString() : toString(pair.second));
|
||||
if (pair.first.endsWith("Image") && container->containsChild(pair.first)) {
|
||||
if (pair.second.isType(Json::Type::String))
|
||||
container->fetchChild<ImageWidget>(pair.first)->setImage(pair.second.toString());
|
||||
else
|
||||
container->fetchChild<ImageWidget>(pair.first)->setDrawables(pair.second.toArray().transformed(construct<Drawable>()));
|
||||
}
|
||||
}
|
||||
|
||||
if (auto fireable = as<FireableItem>(item)) {
|
||||
container->setLabel("cooldownTimeLabel", strf("%.2f", fireable->cooldownTime()));
|
||||
container->setLabel("windupTimeLabel", strf("%.2f", fireable->windupTime()));
|
||||
container->setLabel("speedLabel", strf("%.2f", 1.0f / (fireable->cooldownTime() + fireable->windupTime())));
|
||||
}
|
||||
|
||||
if (container->containsChild("largeImage")) {
|
||||
container->fetchChild<ImageWidget>("largeImage")->setImage(item->largeImage());
|
||||
}
|
||||
|
||||
container->setLabel("descriptionLabel", description);
|
||||
container->setLabel("friendlyNameLabel", title);
|
||||
|
||||
if (container->containsChild("statusList")) {
|
||||
auto statusList = container->fetchChild<ListWidget>("statusList");
|
||||
if (auto statusEffects = as<StatusEffectItem>(item)) {
|
||||
for (auto effect : statusEffects->statusEffects())
|
||||
describePersistentEffect(statusList, effect);
|
||||
}
|
||||
}
|
||||
|
||||
if (item->instanceValue("acceptsAugmentType", false)) {
|
||||
if (auto augmentLabel = container->fetchChild<LabelWidget>("augmentNameLabel")) {
|
||||
if (auto currentAugment = item->instanceValue("currentAugment")) {
|
||||
container->setLabel("augmentNameLabel", currentAugment.getString("displayName", "???"));
|
||||
if (auto augmentIcon = container->fetchChild<ImageWidget>("augmentIconImage"))
|
||||
augmentIcon->setImage(currentAugment.getString("displayIcon", ""));
|
||||
augmentLabel->setColor(Color::White);
|
||||
} else {
|
||||
container->setLabel("augmentNameLabel", "NO AUGMENT INSERTED");
|
||||
if (auto augmentIcon = container->fetchChild<ImageWidget>("augmentIconImage"))
|
||||
augmentIcon->setImage("");
|
||||
augmentLabel->setColor(Color::Gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
container->setLabel("title", title);
|
||||
container->setLabel("subTitle", subTitle);
|
||||
if (container->containsChild("titleIcon")) {
|
||||
auto titleIcon = container->fetchChild<ItemSlotWidget>("titleIcon");
|
||||
titleIcon->setItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemTooltipBuilder::describePersistentEffect(
|
||||
ListWidgetPtr const& container, PersistentStatusEffect const& effect) {
|
||||
if (auto uniqueStatusEffect = effect.ptr<UniqueStatusEffect>()) {
|
||||
auto statusEffectDatabase = Root::singleton().statusEffectDatabase();
|
||||
auto effectConfig = statusEffectDatabase->uniqueEffectConfig(*uniqueStatusEffect);
|
||||
if (effectConfig.icon) {
|
||||
auto listItem = container->addItem();
|
||||
listItem->setLabel("statusLabel", effectConfig.label);
|
||||
listItem->fetchChild<ImageWidget>("statusImage")->setImage(*effectConfig.icon);
|
||||
}
|
||||
} else if (auto modifierEffect = effect.ptr<StatModifier>()) {
|
||||
auto statsConfig = Root::singleton().assets()->json("/interface/stats/stats.config");
|
||||
if (auto baseMultiplier = modifierEffect->ptr<StatBaseMultiplier>()) {
|
||||
if (statsConfig.contains(baseMultiplier->statName)) {
|
||||
auto listItem = container->addItem();
|
||||
listItem->fetchChild<ImageWidget>("statusImage")
|
||||
->setImage(statsConfig.get(baseMultiplier->statName).getString("icon"));
|
||||
listItem->setLabel("statusLabel", strf("%s%%", (baseMultiplier->baseMultiplier - 1) * 100));
|
||||
}
|
||||
} else if (auto valueModifier = modifierEffect->ptr<StatValueModifier>()) {
|
||||
if (statsConfig.contains(valueModifier->statName)) {
|
||||
auto listItem = container->addItem();
|
||||
listItem->fetchChild<ImageWidget>("statusImage")
|
||||
->setImage(statsConfig.get(valueModifier->statName).getString("icon"));
|
||||
listItem->setLabel("statusLabel", strf("%s%s", valueModifier->value < 0 ? "-" : "", valueModifier->value));
|
||||
}
|
||||
} else if (auto effectiveMultiplier = modifierEffect->ptr<StatEffectiveMultiplier>()) {
|
||||
if (statsConfig.contains(effectiveMultiplier->statName)) {
|
||||
auto listItem = container->addItem();
|
||||
listItem->fetchChild<ImageWidget>("statusImage")
|
||||
->setImage(statsConfig.get(effectiveMultiplier->statName).getString("icon"));
|
||||
listItem->setLabel("statusLabel", strf("%s%%", (effectiveMultiplier->effectiveMultiplier - 1) * 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
28
source/frontend/StarItemTooltip.hpp
Normal file
28
source/frontend/StarItemTooltip.hpp
Normal file
|
@ -0,0 +1,28 @@
|
|||
#ifndef STAR_ITEM_TOOLTIP_HPP
|
||||
#define STAR_ITEM_TOOLTIP_HPP
|
||||
|
||||
#include "StarString.hpp"
|
||||
#include "StarStatusTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(Item);
|
||||
STAR_CLASS(Widget);
|
||||
STAR_CLASS(ListWidget);
|
||||
STAR_CLASS(Augment);
|
||||
STAR_CLASS(Pane);
|
||||
STAR_CLASS(Player);
|
||||
|
||||
namespace ItemTooltipBuilder {
|
||||
PanePtr buildItemTooltip(ItemPtr const& item, PlayerPtr const& viewer = {});
|
||||
|
||||
void buildItemDescription(WidgetPtr const& container, ItemPtr const& item);
|
||||
void buildItemDescriptionInner(
|
||||
WidgetPtr const& container, ItemPtr const& item, String const& tooltipKind, String& title, String& subtitle, PlayerPtr const& viewer = {});
|
||||
|
||||
void describePersistentEffect(ListWidgetPtr const& container, PersistentStatusEffect const& effect);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
52
source/frontend/StarJoinRequestDialog.cpp
Normal file
52
source/frontend/StarJoinRequestDialog.cpp
Normal file
|
@ -0,0 +1,52 @@
|
|||
#include "StarJoinRequestDialog.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarRandom.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
JoinRequestDialog::JoinRequestDialog() {}
|
||||
|
||||
void JoinRequestDialog::displayRequest(String const& userName, function<void(P2PJoinRequestReply)> callback) {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
removeAllChildren();
|
||||
|
||||
GuiReader reader;
|
||||
|
||||
m_callback = move(callback);
|
||||
|
||||
reader.registerCallback("yes", [this](Widget*){ reply(P2PJoinRequestReply::Yes); });
|
||||
reader.registerCallback("no", [this](Widget*){ reply(P2PJoinRequestReply::No); });
|
||||
reader.registerCallback("ignore", [this](Widget*){ reply(P2PJoinRequestReply::Ignore); });
|
||||
|
||||
m_confirmed = false;
|
||||
|
||||
Json config = assets->json("/interface/windowconfig/joinrequest.config");
|
||||
|
||||
reader.construct(config.get("paneLayout"), this);
|
||||
|
||||
String message = config.getString("joinMessage").replaceTags(StringMap<String>{{"username", userName}});
|
||||
fetchChild<LabelWidget>("message")->setText(message);
|
||||
|
||||
show();
|
||||
}
|
||||
|
||||
void JoinRequestDialog::reply(P2PJoinRequestReply reply) {
|
||||
m_confirmed = true;
|
||||
m_callback(reply);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
void JoinRequestDialog::dismissed() {
|
||||
if (!m_confirmed)
|
||||
m_callback(P2PJoinRequestReply::No);
|
||||
|
||||
Pane::dismissed();
|
||||
}
|
||||
|
||||
}
|
30
source/frontend/StarJoinRequestDialog.hpp
Normal file
30
source/frontend/StarJoinRequestDialog.hpp
Normal file
|
@ -0,0 +1,30 @@
|
|||
#ifndef STAR_JOIN_REQUEST_DIALOG_HPP
|
||||
#define STAR_JOIN_REQUEST_DIALOG_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
#include "StarRpcPromise.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(JoinRequestDialog);
|
||||
|
||||
class JoinRequestDialog : public Pane {
|
||||
public:
|
||||
JoinRequestDialog();
|
||||
|
||||
virtual ~JoinRequestDialog() {}
|
||||
|
||||
void displayRequest(String const& userName, function<void(P2PJoinRequestReply)> callback);
|
||||
|
||||
void dismissed() override;
|
||||
|
||||
private:
|
||||
void reply(P2PJoinRequestReply reply);
|
||||
|
||||
function<void(P2PJoinRequestReply)> m_callback;
|
||||
bool m_confirmed;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
244
source/frontend/StarKeybindingsMenu.cpp
Normal file
244
source/frontend/StarKeybindingsMenu.cpp
Normal file
|
@ -0,0 +1,244 @@
|
|||
#include "StarKeybindingsMenu.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarConfiguration.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarListWidget.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarOrderedSet.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
KeybindingsMenu::KeybindingsMenu() : m_activeKeybinding(nullptr) {
|
||||
GuiReader reader;
|
||||
reader.registerCallback("cancel",
|
||||
[&](Widget*) {
|
||||
revert();
|
||||
dismiss();
|
||||
});
|
||||
reader.registerCallback("accept",
|
||||
[&](Widget*) {
|
||||
apply();
|
||||
dismiss();
|
||||
});
|
||||
reader.registerCallback("setDefault", [&](Widget*) { resetDefaults(); });
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
m_maxBindings = assets->json("/interface/windowconfig/keybindingsmenu.config:maxBindings").toUInt();
|
||||
|
||||
Json paneLayout = assets->json("/interface/windowconfig/keybindingsmenu.config:paneLayout");
|
||||
reader.construct(paneLayout, this);
|
||||
|
||||
buildListsFromConfig();
|
||||
|
||||
m_currentMods = KeyMod::NoMod;
|
||||
}
|
||||
|
||||
KeyboardCaptureMode KeybindingsMenu::keyboardCaptured() const {
|
||||
return m_activeKeybinding ? KeyboardCaptureMode::KeyEvents : KeyboardCaptureMode::None;
|
||||
}
|
||||
|
||||
bool KeybindingsMenu::sendEvent(InputEvent const& event) {
|
||||
if (!m_visible)
|
||||
return false;
|
||||
|
||||
if (m_activeKeybinding) {
|
||||
if (m_context->actions(event).contains(InterfaceAction::KeybindingClear)) {
|
||||
clearActive();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_context->actions(event).contains(InterfaceAction::KeybindingCancel)) {
|
||||
exitActiveMode();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_activeKeybinding) {
|
||||
// HACK: I need to pass events only to the trash button first.
|
||||
if (m_activeKeybinding->parent()->fetchChild<ButtonWidget>("deleteBinding")->sendEvent(event))
|
||||
return true;
|
||||
|
||||
if (auto keyUp = event.ptr<KeyUpEvent>()) {
|
||||
if (Maybe<KeyMod> modKey = KeyChordMods.maybe(keyUp->key)) {
|
||||
m_currentMods &= ~*modKey;
|
||||
setKeybinding(KeyChord{keyUp->key, m_currentMods});
|
||||
return true;
|
||||
}
|
||||
} else if (auto keyDown = event.ptr<KeyDownEvent>()) {
|
||||
Maybe<KeyMod> modKey = KeyModNames.maybeLeft(KeyNames.getRight(keyDown->key));
|
||||
|
||||
if (modKey) {
|
||||
m_currentMods |= *modKey;
|
||||
return true;
|
||||
} else {
|
||||
setKeybinding(KeyChord{keyDown->key, m_currentMods});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_context->actions(event).contains(InterfaceAction::GuiClose)) {
|
||||
dismiss();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Pane::sendEvent(event))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void KeybindingsMenu::show() {
|
||||
m_origConfiguration = Root::singleton().configuration()->get("bindings");
|
||||
Pane::show();
|
||||
}
|
||||
|
||||
void KeybindingsMenu::dismissed() {
|
||||
exitActiveMode();
|
||||
Pane::dismissed();
|
||||
}
|
||||
|
||||
void KeybindingsMenu::buildListsFromConfig() {
|
||||
m_playerList = fetchChild<ListWidget>("categories.tabs.player.scrollArea.keyList");
|
||||
m_toolBarList = fetchChild<ListWidget>("categories.tabs.toolbar.scrollArea.keyList");
|
||||
m_gameList = fetchChild<ListWidget>("categories.tabs.game.scrollArea.keyList");
|
||||
|
||||
m_childToAction.clear();
|
||||
|
||||
auto doKeybindingsFor = [&](ListWidgetPtr const& list, Json const& keybinds) {
|
||||
list->clear();
|
||||
|
||||
list->registerMemberCallback("activateBinding", [this](Widget* widget) { activateBinding(widget); });
|
||||
|
||||
list->registerMemberCallback("deleteBinding", [this](Widget*) { clearActive(); });
|
||||
|
||||
auto config = Root::singleton().configuration();
|
||||
auto bindings = config->get("bindings");
|
||||
|
||||
for (auto const& keybind : keybinds.iterateArray()) {
|
||||
auto newListMember = list->addItem();
|
||||
auto actionString = keybind.get("action").toString();
|
||||
auto action = InterfaceActionNames.getLeft(actionString);
|
||||
List<KeyChord> inputDesc;
|
||||
try {
|
||||
for (auto const& bindingEntry : bindings.get(actionString).iterateArray())
|
||||
inputDesc.append(inputDescriptorFromJson(bindingEntry));
|
||||
} catch (StarException const& e) {
|
||||
Logger::warn("Could not load keybinding for %s. %s\n", actionString, e.what());
|
||||
}
|
||||
|
||||
m_childToAction.insert({newListMember->fetchChild<ButtonWidget>("boundKeys").get(), action});
|
||||
newListMember->fetchChild<LabelWidget>("actionName")->setText(keybind.getString("label"));
|
||||
newListMember->fetchChild<ButtonWidget>("boundKeys")->setText(StringList(inputDesc.transformed(printInputDescriptor)).join(", "));
|
||||
newListMember->fetchChild<ButtonWidget>("deleteBinding")->hide();
|
||||
}
|
||||
};
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
doKeybindingsFor(m_playerList, assets->json("/interface/windowconfig/keybindingsmenu.config:keyActions.player"));
|
||||
doKeybindingsFor(m_toolBarList, assets->json("/interface/windowconfig/keybindingsmenu.config:keyActions.toolbar"));
|
||||
doKeybindingsFor(m_gameList, assets->json("/interface/windowconfig/keybindingsmenu.config:keyActions.game"));
|
||||
}
|
||||
|
||||
bool KeybindingsMenu::activateBinding(Widget* widget) {
|
||||
exitActiveMode();
|
||||
|
||||
m_activeKeybinding = widget;
|
||||
m_activeKeybinding->parent()->fetchChild<ButtonWidget>("deleteBinding")->show();
|
||||
convert<ButtonWidget>(m_activeKeybinding)->setHighlighted(true);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void KeybindingsMenu::setKeybinding(KeyChord desc) {
|
||||
if (!m_activeKeybinding)
|
||||
return;
|
||||
|
||||
auto out = inputDescriptorToJson(desc);
|
||||
|
||||
auto config = Root::singleton().configuration();
|
||||
auto base = config->get("bindings");
|
||||
|
||||
auto action = m_childToAction.get(m_activeKeybinding);
|
||||
auto key = InterfaceActionNames.getRight(action);
|
||||
|
||||
auto bindings = OrderedHashSet<Json>::from(base.get(key).toArray());
|
||||
|
||||
if (bindings.contains(out))
|
||||
bindings.clear();
|
||||
|
||||
bindings.add(out);
|
||||
|
||||
if (bindings.size() > m_maxBindings)
|
||||
bindings.removeFirst();
|
||||
|
||||
base = base.set(key, JsonArray::from(bindings));
|
||||
|
||||
config->set("bindings", base);
|
||||
|
||||
String buttonText;
|
||||
|
||||
for (auto const& entry : base.get(key).iterateArray()) {
|
||||
auto stored = inputDescriptorFromJson(entry);
|
||||
buttonText = String::joinWith(", ", buttonText, printInputDescriptor(stored));
|
||||
}
|
||||
|
||||
convert<ButtonWidget>(m_activeKeybinding)->setText(buttonText);
|
||||
|
||||
apply();
|
||||
exitActiveMode();
|
||||
}
|
||||
|
||||
void KeybindingsMenu::clearActive() {
|
||||
if (!m_activeKeybinding)
|
||||
return;
|
||||
|
||||
auto config = Root::singleton().configuration();
|
||||
auto base = config->get("bindings").toObject();
|
||||
|
||||
auto action = m_childToAction.get(m_activeKeybinding);
|
||||
auto key = InterfaceActionNames.getRight(action);
|
||||
|
||||
base[key] = JsonArray{};
|
||||
config->set("bindings", base);
|
||||
|
||||
convert<ButtonWidget>(m_activeKeybinding)->setText("<Unbound>");
|
||||
|
||||
apply();
|
||||
exitActiveMode();
|
||||
}
|
||||
|
||||
void KeybindingsMenu::exitActiveMode() {
|
||||
if (!m_activeKeybinding)
|
||||
return;
|
||||
|
||||
m_activeKeybinding->parent()->fetchChild<ButtonWidget>("deleteBinding")->hide();
|
||||
convert<ButtonWidget>(m_activeKeybinding)->setHighlighted(false);
|
||||
m_activeKeybinding = nullptr;
|
||||
m_currentMods = KeyMod::NoMod;
|
||||
}
|
||||
|
||||
void KeybindingsMenu::apply() {
|
||||
m_context->refreshKeybindings();
|
||||
}
|
||||
|
||||
void KeybindingsMenu::revert() {
|
||||
Root::singleton().configuration()->set("bindings", m_origConfiguration);
|
||||
apply();
|
||||
|
||||
buildListsFromConfig();
|
||||
}
|
||||
|
||||
void KeybindingsMenu::resetDefaults() {
|
||||
auto config = Root::singleton().configuration();
|
||||
config->set("bindings", config->getDefault("bindings"));
|
||||
apply();
|
||||
|
||||
buildListsFromConfig();
|
||||
}
|
||||
|
||||
}
|
49
source/frontend/StarKeybindingsMenu.hpp
Normal file
49
source/frontend/StarKeybindingsMenu.hpp
Normal file
|
@ -0,0 +1,49 @@
|
|||
#ifndef STAR_KEYBINDINGS_MENU_HPP
|
||||
#define STAR_KEYBINDINGS_MENU_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(TabSetWidget);
|
||||
STAR_CLASS(ListWidget);
|
||||
STAR_CLASS(KeybindingsMenu);
|
||||
|
||||
class KeybindingsMenu : public Pane {
|
||||
public:
|
||||
KeybindingsMenu();
|
||||
|
||||
// We need to handle our own Esc dismissal
|
||||
KeyboardCaptureMode keyboardCaptured() const override;
|
||||
bool sendEvent(InputEvent const& event) override;
|
||||
|
||||
void show() override;
|
||||
void dismissed() override;
|
||||
|
||||
private:
|
||||
void buildListsFromConfig();
|
||||
bool activateBinding(Widget* widget);
|
||||
void setKeybinding(KeyChord desc);
|
||||
void clearActive();
|
||||
void exitActiveMode();
|
||||
void apply();
|
||||
void revert();
|
||||
void resetDefaults();
|
||||
|
||||
Widget* m_activeKeybinding;
|
||||
|
||||
Map<Widget*, InterfaceAction> m_childToAction;
|
||||
TabSetWidgetPtr m_tabSet;
|
||||
ListWidgetPtr m_playerList;
|
||||
ListWidgetPtr m_toolBarList;
|
||||
ListWidgetPtr m_gameList;
|
||||
|
||||
Json m_origConfiguration;
|
||||
|
||||
size_t m_maxBindings;
|
||||
KeyMod m_currentMods;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
1472
source/frontend/StarMainInterface.cpp
Normal file
1472
source/frontend/StarMainInterface.cpp
Normal file
File diff suppressed because it is too large
Load diff
214
source/frontend/StarMainInterface.hpp
Normal file
214
source/frontend/StarMainInterface.hpp
Normal file
|
@ -0,0 +1,214 @@
|
|||
#ifndef STAR_MAIN_INTERFACE_HPP
|
||||
#define STAR_MAIN_INTERFACE_HPP
|
||||
|
||||
#include "StarInventory.hpp"
|
||||
#include "StarInteractionTypes.hpp"
|
||||
#include "StarItemDescriptor.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
#include "StarInterfaceCursor.hpp"
|
||||
#include "StarMainInterfaceTypes.hpp"
|
||||
#include "StarWarping.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(UniverseClient);
|
||||
STAR_CLASS(WorldPainter);
|
||||
STAR_CLASS(Item);
|
||||
STAR_CLASS(Chat);
|
||||
STAR_CLASS(ClientCommandProcessor);
|
||||
STAR_CLASS(OptionsMenu);
|
||||
STAR_CLASS(WirePane);
|
||||
STAR_CLASS(ActionBar);
|
||||
STAR_CLASS(TeamBar);
|
||||
STAR_CLASS(StatusPane);
|
||||
STAR_CLASS(ContainerPane);
|
||||
STAR_CLASS(CraftingPane);
|
||||
STAR_CLASS(MerchantPane);
|
||||
STAR_CLASS(CodexInterface);
|
||||
STAR_CLASS(SongbookInterface);
|
||||
STAR_CLASS(QuestLogInterface);
|
||||
STAR_CLASS(AiInterface);
|
||||
STAR_CLASS(PopupInterface);
|
||||
STAR_CLASS(ConfirmationDialog);
|
||||
STAR_CLASS(JoinRequestDialog);
|
||||
STAR_CLASS(TeleportDialog);
|
||||
STAR_CLASS(LabelWidget);
|
||||
STAR_CLASS(Cinematic);
|
||||
STAR_CLASS(NameplatePainter);
|
||||
STAR_CLASS(QuestIndicatorPainter);
|
||||
STAR_CLASS(RadioMessagePopup);
|
||||
STAR_CLASS(Quest);
|
||||
STAR_CLASS(QuestTrackerPane);
|
||||
STAR_CLASS(ContainerInteractor);
|
||||
STAR_CLASS(ScriptPane);
|
||||
STAR_CLASS(ChatBubbleManager);
|
||||
|
||||
STAR_STRUCT(GuiMessage);
|
||||
STAR_CLASS(MainInterface);
|
||||
|
||||
struct GuiMessage {
|
||||
GuiMessage();
|
||||
GuiMessage(String const& message, float cooldown);
|
||||
|
||||
String message;
|
||||
float cooldown;
|
||||
float springState;
|
||||
};
|
||||
|
||||
class MainInterface {
|
||||
public:
|
||||
enum RunningState {
|
||||
Running,
|
||||
ReturnToTitle
|
||||
};
|
||||
|
||||
MainInterface(UniverseClientPtr client, WorldPainterPtr painter, CinematicPtr cinematicOverlay);
|
||||
|
||||
~MainInterface();
|
||||
|
||||
RunningState currentState() const;
|
||||
|
||||
MainInterfacePaneManager* paneManager();
|
||||
|
||||
bool escapeDialogOpen() const;
|
||||
|
||||
void openCraftingWindow(Json const& config, EntityId sourceEntityId = NullEntityId);
|
||||
void openMerchantWindow(Json const& config, EntityId sourceEntityId = NullEntityId);
|
||||
void togglePlainCraftingWindow();
|
||||
|
||||
bool windowsOpen() const;
|
||||
|
||||
MerchantPanePtr activeMerchantPane() const;
|
||||
|
||||
// Return true if this event was consumed or should be handled elsewhere.
|
||||
bool handleInputEvent(InputEvent const& event);
|
||||
// Return true if mouse / keyboard events are currently locked here
|
||||
bool inputFocus() const;
|
||||
// If input is focused, should MainInterface also accept text input events?
|
||||
bool textInputActive() const;
|
||||
|
||||
void handleInteractAction(InteractAction interactAction);
|
||||
|
||||
// Handles incoming client messages, aims main player, etc.
|
||||
void update();
|
||||
|
||||
// Render things e.g. quest indicators that should be drawn in the world
|
||||
// behind interface e.g. chat bubbles
|
||||
void renderInWorldElements();
|
||||
void render();
|
||||
|
||||
Vec2F cursorWorldPosition() const;
|
||||
|
||||
void toggleDebugDisplay();
|
||||
bool isDebugDisplayed();
|
||||
|
||||
void doChat(String const& chat, bool addToHistory);
|
||||
|
||||
void queueMessage(String const& message);
|
||||
void queueItemPickupText(ItemPtr const& item);
|
||||
void queueJoinRequest(pair<String, RpcPromiseKeeper<P2PJoinRequestReply>> request);
|
||||
|
||||
bool fixedCamera() const;
|
||||
|
||||
void warpToOrbitedWorld(bool deploy = false);
|
||||
void warpToOwnShip();
|
||||
void warpTo(WarpAction const& warpAction);
|
||||
|
||||
private:
|
||||
PanePtr createEscapeDialog();
|
||||
|
||||
float interfaceScale() const;
|
||||
unsigned windowHeight() const;
|
||||
unsigned windowWidth() const;
|
||||
Vec2I mainBarPosition() const;
|
||||
|
||||
void renderBreath();
|
||||
void renderMessages();
|
||||
void renderMonsterHealthBar();
|
||||
void renderSpecialDamageBar();
|
||||
void renderMainBar();
|
||||
void renderWindows();
|
||||
void renderDebug();
|
||||
|
||||
void updateCursor();
|
||||
void renderCursor();
|
||||
|
||||
bool overButton(PolyI buttonPoly, Vec2I const& mousePos) const;
|
||||
|
||||
void overlayClick(Vec2I const& mousePos, MouseButton mouseButton);
|
||||
|
||||
GuiContext* m_guiContext;
|
||||
MainInterfaceConfigConstPtr m_config;
|
||||
InterfaceCursor m_cursor;
|
||||
|
||||
RunningState m_state;
|
||||
|
||||
UniverseClientPtr m_client;
|
||||
WorldPainterPtr m_worldPainter;
|
||||
CinematicPtr m_cinematicOverlay;
|
||||
|
||||
MainInterfacePaneManager m_paneManager;
|
||||
|
||||
QuestLogInterfacePtr m_questLogInterface;
|
||||
|
||||
InventoryPanePtr m_inventoryWindow;
|
||||
CraftingPanePtr m_plainCraftingWindow;
|
||||
CraftingPanePtr m_craftingWindow;
|
||||
MerchantPanePtr m_merchantWindow;
|
||||
CodexInterfacePtr m_codexInterface;
|
||||
OptionsMenuPtr m_optionsMenu;
|
||||
ContainerPanePtr m_containerPane;
|
||||
PopupInterfacePtr m_popupInterface;
|
||||
ConfirmationDialogPtr m_confirmationDialog;
|
||||
JoinRequestDialogPtr m_joinRequestDialog;
|
||||
TeleportDialogPtr m_teleportDialog;
|
||||
QuestTrackerPanePtr m_questTracker;
|
||||
ScriptPanePtr m_mmUpgrade;
|
||||
ScriptPanePtr m_collections;
|
||||
Map<EntityId, PanePtr> m_interactionScriptPanes;
|
||||
|
||||
ChatPtr m_chat;
|
||||
ClientCommandProcessorPtr m_clientCommandProcessor;
|
||||
RadioMessagePopupPtr m_radioMessagePopup;
|
||||
WirePanePtr m_wireInterface;
|
||||
|
||||
ActionBarPtr m_actionBar;
|
||||
Vec2I m_cursorScreenPos;
|
||||
ItemSlotWidgetPtr m_cursorItem;
|
||||
Maybe<String> m_cursorTooltip;
|
||||
|
||||
LabelWidgetPtr m_planetText;
|
||||
GameTimer m_planetNameTimer;
|
||||
|
||||
GameTimer m_debugSpatialClearTimer;
|
||||
GameTimer m_debugMapClearTimer;
|
||||
RectF m_debugTextRect;
|
||||
|
||||
NameplatePainterPtr m_nameplatePainter;
|
||||
QuestIndicatorPainterPtr m_questIndicatorPainter;
|
||||
ChatBubbleManagerPtr m_chatBubbleManager;
|
||||
|
||||
bool m_disableHud;
|
||||
|
||||
String m_lastCommand;
|
||||
|
||||
LinkedList<GuiMessagePtr> m_messages;
|
||||
HashMap<ItemDescriptor, std::pair<size_t, GuiMessagePtr>> m_itemDropMessages;
|
||||
unsigned m_messageOverflow;
|
||||
GuiMessagePtr m_overflowMessage;
|
||||
|
||||
List<pair<String, RpcPromiseKeeper<P2PJoinRequestReply>>> m_queuedJoinRequests;
|
||||
|
||||
EntityId m_lastMouseoverTarget;
|
||||
GameTimer m_stickyTargetingTimer;
|
||||
int m_portraitScale;
|
||||
|
||||
EntityId m_specialDamageBarTarget;
|
||||
float m_specialDamageBarValue;
|
||||
|
||||
ContainerInteractorPtr m_containerInteractor;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
129
source/frontend/StarMainInterfaceTypes.cpp
Normal file
129
source/frontend/StarMainInterfaceTypes.cpp
Normal file
|
@ -0,0 +1,129 @@
|
|||
#include "StarMainInterfaceTypes.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarImageMetadataDatabase.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
MainInterfaceConfigPtr MainInterfaceConfig::loadFromAssets() {
|
||||
auto& root = Root::singleton();
|
||||
auto assets = root.assets();
|
||||
auto imageMetadata = root.imageMetadataDatabase();
|
||||
|
||||
auto config = make_shared<MainInterfaceConfig>();
|
||||
|
||||
config->fontSize = assets->json("/interface.config:font.baseSize").toInt();
|
||||
|
||||
config->inventoryImage = assets->json("/interface.config:mainBar.inventory.base").toString();
|
||||
config->inventoryImageHover = assets->json("/interface.config:mainBar.inventory.hover").toString();
|
||||
config->inventoryImageGlow = assets->json("/interface.config:mainBar.inventory.glow").toString();
|
||||
config->inventoryImageGlowHover = assets->json("/interface.config:mainBar.inventory.glowHover").toString();
|
||||
config->inventoryImageOpen = assets->json("/interface.config:mainBar.inventory.open").toString();
|
||||
config->inventoryImageOpenHover = assets->json("/interface.config:mainBar.inventory.openHover").toString();
|
||||
|
||||
config->beamDownImage = assets->json("/interface.config:mainBar.beam.base").toString();
|
||||
config->beamDownImageHover = assets->json("/interface.config:mainBar.beam.hover").toString();
|
||||
|
||||
config->deployImage = assets->json("/interface.config:mainBar.deploy.base").toString();
|
||||
config->deployImageHover = assets->json("/interface.config:mainBar.deploy.hover").toString();
|
||||
config->deployImageDisabled = assets->json("/interface.config:mainBar.deploy.disabled").toString();
|
||||
config->beamUpImage = assets->json("/interface.config:mainBar.beamUp.base").toString();
|
||||
config->beamUpImageHover = assets->json("/interface.config:mainBar.beamUp.hover").toString();
|
||||
|
||||
config->craftImage = assets->json("/interface.config:mainBar.craft.base").toString();
|
||||
config->craftImageHover = assets->json("/interface.config:mainBar.craft.hover").toString();
|
||||
config->craftImageOpen = assets->json("/interface.config:mainBar.craft.open").toString();
|
||||
config->craftImageOpenHover = assets->json("/interface.config:mainBar.craft.openHover").toString();
|
||||
|
||||
config->codexImage = assets->json("/interface.config:mainBar.codex.base").toString();
|
||||
config->codexImageHover = assets->json("/interface.config:mainBar.codex.hover").toString();
|
||||
config->codexImageOpen = assets->json("/interface.config:mainBar.codex.open").toString();
|
||||
config->codexImageHoverOpen = assets->json("/interface.config:mainBar.codex.openHover").toString();
|
||||
|
||||
config->questLogImage = assets->json("/interface.config:mainBar.questLog.base").toString();
|
||||
config->questLogImageHover = assets->json("/interface.config:mainBar.questLog.hover").toString();
|
||||
config->questLogImageOpen = assets->json("/interface.config:mainBar.questLog.open").toString();
|
||||
config->questLogImageHoverOpen = assets->json("/interface.config:mainBar.questLog.openHover").toString();
|
||||
|
||||
config->mmUpgradeImage = assets->json("/interface.config:mainBar.mmUpgrade.base").toString();
|
||||
config->mmUpgradeImageHover = assets->json("/interface.config:mainBar.mmUpgrade.hover").toString();
|
||||
config->mmUpgradeImageOpen = assets->json("/interface.config:mainBar.mmUpgrade.open").toString();
|
||||
config->mmUpgradeImageHoverOpen = assets->json("/interface.config:mainBar.mmUpgrade.openHover").toString();
|
||||
config->mmUpgradeImageDisabled = assets->json("/interface.config:mainBar.mmUpgrade.disabled").toString();
|
||||
|
||||
config->collectionsImage = assets->json("/interface.config:mainBar.collections.base").toString();
|
||||
config->collectionsImageHover = assets->json("/interface.config:mainBar.collections.hover").toString();
|
||||
config->collectionsImageOpen = assets->json("/interface.config:mainBar.collections.open").toString();
|
||||
config->collectionsImageHoverOpen = assets->json("/interface.config:mainBar.collections.openHover").toString();
|
||||
|
||||
config->mainBarInventoryButtonOffset = jsonToVec2I(assets->json("/interface.config:mainBar.inventory.pos"));
|
||||
config->mainBarCraftButtonOffset = jsonToVec2I(assets->json("/interface.config:mainBar.craft.pos"));
|
||||
config->mainBarBeamButtonOffset = jsonToVec2I(assets->json("/interface.config:mainBar.beam.pos"));
|
||||
config->mainBarDeployButtonOffset = jsonToVec2I(assets->json("/interface.config:mainBar.deploy.pos"));
|
||||
config->mainBarCodexButtonOffset = jsonToVec2I(assets->json("/interface.config:mainBar.codex.pos"));
|
||||
config->mainBarQuestLogButtonOffset = jsonToVec2I(assets->json("/interface.config:mainBar.questLog.pos"));
|
||||
config->mainBarMmUpgradeButtonOffset = jsonToVec2I(assets->json("/interface.config:mainBar.mmUpgrade.pos"));
|
||||
config->mainBarCollectionsButtonOffset = jsonToVec2I(assets->json("/interface.config:mainBar.collections.pos"));
|
||||
|
||||
config->mainBarInventoryButtonPoly = jsonToPolyI(assets->json("/interface.config:mainBar.inventory.poly"));
|
||||
config->mainBarCraftButtonPoly = jsonToPolyI(assets->json("/interface.config:mainBar.craft.poly"));
|
||||
config->mainBarBeamButtonPoly = jsonToPolyI(assets->json("/interface.config:mainBar.beam.poly"));
|
||||
config->mainBarDeployButtonPoly = jsonToPolyI(assets->json("/interface.config:mainBar.deploy.poly"));
|
||||
config->mainBarCodexButtonPoly = jsonToPolyI(assets->json("/interface.config:mainBar.codex.poly"));
|
||||
config->mainBarQuestLogButtonPoly = jsonToPolyI(assets->json("/interface.config:mainBar.questLog.poly"));
|
||||
config->mainBarMmUpgradeButtonPoly = jsonToPolyI(assets->json("/interface.config:mainBar.mmUpgrade.poly"));
|
||||
config->mainBarCollectionsButtonPoly = jsonToPolyI(assets->json("/interface.config:mainBar.collections.poly"));
|
||||
|
||||
config->mainBarSize = jsonToVec2I(assets->json("/interface.config:mainBar.size"));
|
||||
|
||||
config->itemCountRightAnchor = jsonToVec2I(assets->json("/interface.config:itemCountRightAnchor"));
|
||||
config->inventoryItemMouseOffset = jsonToVec2I(assets->json("/interface.config:inventoryItemMouseOffset"));
|
||||
|
||||
config->maxMessageCount = assets->json("/interface.config:maxMessageCount").toUInt();
|
||||
config->overflowMessageText = assets->json("/interface.config:overflowMessageText").toString();
|
||||
|
||||
config->messageBarPos = jsonToVec2I(assets->json("/interface.config:message.barPos"));
|
||||
config->messageItemOffset = jsonToVec2I(assets->json("/interface.config:message.itemOffset"));
|
||||
|
||||
config->messageTextContainer = assets->json("/interface.config:message.textContainer").toString();
|
||||
config->messageTextContainerOffset = jsonToVec2I(assets->json("/interface.config:message.textContainerOffset"));
|
||||
config->messageTextOffset = jsonToVec2I(assets->json("/interface.config:message.textOffset"));
|
||||
|
||||
config->messageTime = assets->json("/interface.config:message.showTime").toFloat();
|
||||
config->messageHideTime = assets->json("/interface.config:message.hideTime").toFloat();
|
||||
config->messageActiveOffset = jsonToVec2I(assets->json("/interface.config:message.offset"));
|
||||
config->messageHiddenOffset = jsonToVec2I(assets->json("/interface.config:message.offsetHidden"));
|
||||
config->messageHiddenOffsetBar = jsonToVec2I(assets->json("/interface.config:message.offsetHiddenBar"));
|
||||
config->messageWindowSpring = assets->json("/interface.config:message.windowSpring").toFloat();
|
||||
|
||||
config->monsterHealthBarTime = assets->json("/interface.config:monsterHealth.showTime").toFloat();
|
||||
|
||||
config->hungerIcon = assets->json("/interface.config:hungerIcon").toString();
|
||||
|
||||
config->planetNameTime = assets->json("/interface.config:planetNameTime").toFloat();
|
||||
config->planetNameFadeTime = assets->json("/interface.config:planetNameFadeTime").toFloat();
|
||||
config->planetNameFormatString = assets->json("/interface.config:planetNameFormatString").toString();
|
||||
config->planetNameFontSize = assets->json("/interface.config:font.planetSize").toInt();
|
||||
config->planetNameDirectives = assets->json("/interface.config:planetNameDirectives").toString();
|
||||
config->planetNameOffset = jsonToVec2I(assets->json("/interface.config:planetTextOffset"));
|
||||
|
||||
config->renderVirtualCursor = assets->json("/interface.config:renderVirtualCursor").toBool();
|
||||
config->cursorItemSlot = assets->json("/interface.config:cursorItemSlot");
|
||||
|
||||
config->debugOffset = jsonToVec2I(assets->json("/interface.config:debugOffset"));
|
||||
config->debugFontSize = assets->json("/interface.config:debugFontSize").toUInt();
|
||||
config->debugSpatialClearTime = assets->json("/interface.config:debugSpatialClearTime").toFloat();
|
||||
config->debugMapClearTime = assets->json("/interface.config:debugMapClearTime").toFloat();
|
||||
config->debugBackgroundColor = jsonToColor(assets->json("/interface.config:debugBackgroundColor"));
|
||||
config->debugBackgroundPad = assets->json("/interface.config:debugBackgroundPad").toUInt();
|
||||
|
||||
for (auto const& path : assets->scanExtension("macros")) {
|
||||
for (auto const& pair : assets->json(path).iterateObject())
|
||||
config->macroCommands.add(pair.first, jsonToStringList(pair.second));
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
}
|
156
source/frontend/StarMainInterfaceTypes.hpp
Normal file
156
source/frontend/StarMainInterfaceTypes.hpp
Normal file
|
@ -0,0 +1,156 @@
|
|||
#ifndef STAR_MAIN_INTERFACE_CONFIG_HPP
|
||||
#define STAR_MAIN_INTERFACE_CONFIG_HPP
|
||||
|
||||
#include "StarJson.hpp"
|
||||
#include "StarPoly.hpp"
|
||||
#include "StarBiMap.hpp"
|
||||
#include "StarRegisteredPaneManager.hpp"
|
||||
#include "StarAnimation.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_STRUCT(MainInterfaceConfig);
|
||||
|
||||
enum class MainInterfacePanes {
|
||||
EscapeDialog,
|
||||
Inventory,
|
||||
Codex,
|
||||
Cockpit,
|
||||
Tech,
|
||||
Songbook,
|
||||
Ai,
|
||||
Popup,
|
||||
Confirmation,
|
||||
JoinRequest,
|
||||
Options,
|
||||
QuestLog,
|
||||
ActionBar,
|
||||
TeamBar,
|
||||
StatusPane,
|
||||
Chat,
|
||||
WireInterface,
|
||||
PlanetText,
|
||||
RadioMessagePopup,
|
||||
CraftingPlain,
|
||||
QuestTracker,
|
||||
MmUpgrade,
|
||||
Collections
|
||||
};
|
||||
|
||||
typedef RegisteredPaneManager<MainInterfacePanes> MainInterfacePaneManager;
|
||||
|
||||
struct MainInterfaceConfig {
|
||||
static MainInterfaceConfigPtr loadFromAssets();
|
||||
|
||||
unsigned fontSize;
|
||||
|
||||
String inventoryImage;
|
||||
String inventoryImageHover;
|
||||
String inventoryImageGlow;
|
||||
String inventoryImageGlowHover;
|
||||
String inventoryImageOpen;
|
||||
String inventoryImageOpenHover;
|
||||
|
||||
String beamDownImage;
|
||||
String beamDownImageHover;
|
||||
|
||||
String deployImage;
|
||||
String deployImageHover;
|
||||
String deployImageDisabled;
|
||||
String beamUpImage;
|
||||
String beamUpImageHover;
|
||||
|
||||
String craftImage;
|
||||
String craftImageHover;
|
||||
String craftImageOpen;
|
||||
String craftImageOpenHover;
|
||||
|
||||
String codexImage;
|
||||
String codexImageHover;
|
||||
String codexImageOpen;
|
||||
String codexImageHoverOpen;
|
||||
|
||||
String questLogImage;
|
||||
String questLogImageHover;
|
||||
String questLogImageOpen;
|
||||
String questLogImageHoverOpen;
|
||||
|
||||
String mmUpgradeImage;
|
||||
String mmUpgradeImageHover;
|
||||
String mmUpgradeImageOpen;
|
||||
String mmUpgradeImageHoverOpen;
|
||||
String mmUpgradeImageDisabled;
|
||||
|
||||
String collectionsImage;
|
||||
String collectionsImageHover;
|
||||
String collectionsImageOpen;
|
||||
String collectionsImageHoverOpen;
|
||||
String collectionsImageDisabled;
|
||||
|
||||
Vec2I mainBarInventoryButtonOffset;
|
||||
Vec2I mainBarCraftButtonOffset;
|
||||
Vec2I mainBarCodexButtonOffset;
|
||||
Vec2I mainBarBeamButtonOffset;
|
||||
Vec2I mainBarDeployButtonOffset;
|
||||
Vec2I mainBarQuestLogButtonOffset;
|
||||
Vec2I mainBarMmUpgradeButtonOffset;
|
||||
Vec2I mainBarCollectionsButtonOffset;
|
||||
|
||||
PolyI mainBarInventoryButtonPoly;
|
||||
PolyI mainBarCraftButtonPoly;
|
||||
PolyI mainBarCodexButtonPoly;
|
||||
PolyI mainBarBeamButtonPoly;
|
||||
PolyI mainBarDeployButtonPoly;
|
||||
PolyI mainBarQuestLogButtonPoly;
|
||||
PolyI mainBarMmUpgradeButtonPoly;
|
||||
PolyI mainBarCollectionsButtonPoly;
|
||||
|
||||
PolyI mainBarPoly;
|
||||
Vec2I mainBarSize;
|
||||
|
||||
Vec2I itemCountRightAnchor;
|
||||
Vec2I inventoryItemMouseOffset;
|
||||
|
||||
unsigned maxMessageCount;
|
||||
String overflowMessageText;
|
||||
|
||||
Vec2I messageBarPos;
|
||||
Vec2I messageItemOffset;
|
||||
|
||||
String messageTextContainer;
|
||||
Vec2I messageTextContainerOffset;
|
||||
Vec2I messageTextOffset;
|
||||
|
||||
float messageTime;
|
||||
float messageHideTime;
|
||||
Vec2I messageActiveOffset;
|
||||
Vec2I messageHiddenOffset;
|
||||
Vec2I messageHiddenOffsetBar;
|
||||
float messageWindowSpring;
|
||||
float monsterHealthBarTime;
|
||||
|
||||
String hungerIcon;
|
||||
|
||||
float planetNameTime;
|
||||
float planetNameFadeTime;
|
||||
String planetNameFormatString;
|
||||
unsigned planetNameFontSize;
|
||||
String planetNameDirectives;
|
||||
Vec2I planetNameOffset;
|
||||
|
||||
bool renderVirtualCursor;
|
||||
Json cursorItemSlot;
|
||||
|
||||
Vec2I debugOffset;
|
||||
unsigned debugFontSize;
|
||||
float debugSpatialClearTime;
|
||||
float debugMapClearTime;
|
||||
Color debugBackgroundColor;
|
||||
int debugBackgroundPad;
|
||||
|
||||
StringMap<StringList> macroCommands;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
114
source/frontend/StarMainMixer.cpp
Normal file
114
source/frontend/StarMainMixer.cpp
Normal file
|
@ -0,0 +1,114 @@
|
|||
#include "StarMainMixer.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarConfiguration.hpp"
|
||||
#include "StarUniverseClient.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarWorldClient.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
MainMixer::MainMixer(unsigned sampleRate, unsigned channels) {
|
||||
m_mixer = make_shared<Mixer>(sampleRate, channels);
|
||||
}
|
||||
|
||||
void MainMixer::setUniverseClient(UniverseClientPtr universeClient) {
|
||||
m_universeClient = move(universeClient);
|
||||
}
|
||||
|
||||
void MainMixer::update(bool muteSfx, bool muteMusic) {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
auto updateGroupVolume = [&](MixerGroup group, bool muted, String const& settingName) {
|
||||
if (m_mutedGroups.contains(group) != muted) {
|
||||
if (muted) {
|
||||
m_mutedGroups.add(group);
|
||||
m_mixer->setGroupVolume(group, 0, 1.0f);
|
||||
} else {
|
||||
m_mutedGroups.remove(group);
|
||||
m_mixer->setGroupVolume(group, m_groupVolumes[group], 1.0f);
|
||||
}
|
||||
} else if (!m_mutedGroups.contains(group)) {
|
||||
float volumeSetting = Root::singleton().configuration()->get(settingName).toFloat() / 100;
|
||||
if (!m_groupVolumes.contains(group) || volumeSetting != m_groupVolumes[group]) {
|
||||
m_mixer->setGroupVolume(group, volumeSetting);
|
||||
m_groupVolumes[group] = volumeSetting;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateGroupVolume(MixerGroup::Effects, muteSfx, "sfxVol");
|
||||
updateGroupVolume(MixerGroup::Music, muteMusic, "musicVol");
|
||||
updateGroupVolume(MixerGroup::Cinematic, false, "sfxVol");
|
||||
|
||||
WorldClientPtr currentWorld;
|
||||
if (m_universeClient)
|
||||
currentWorld = m_universeClient->worldClient();
|
||||
|
||||
if (currentWorld) {
|
||||
for (auto audioInstance : currentWorld->pullPendingAudio()) {
|
||||
audioInstance->setMixerGroup(MixerGroup::Effects);
|
||||
m_mixer->play(audioInstance);
|
||||
}
|
||||
for (auto audioInstance : currentWorld->pullPendingMusic()) {
|
||||
audioInstance->setMixerGroup(MixerGroup::Music);
|
||||
m_mixer->play(audioInstance);
|
||||
}
|
||||
|
||||
if (m_universeClient && m_universeClient->mainPlayer()->underwater()) {
|
||||
if (!m_mixer->hasEffect("lowpass"))
|
||||
m_mixer->addEffect("lowpass", m_mixer->lowpass(32), 0.50f);
|
||||
if (!m_mixer->hasEffect("echo"))
|
||||
m_mixer->addEffect("echo", m_mixer->echo(0.2f, 0.6f, 0.4f), 0.50f);
|
||||
} else {
|
||||
if (m_mixer->hasEffect("lowpass"))
|
||||
m_mixer->removeEffect("lowpass", 0.5f);
|
||||
if (m_mixer->hasEffect("echo"))
|
||||
m_mixer->removeEffect("echo", 0.5f);
|
||||
}
|
||||
|
||||
float baseMaxDistance = assets->json("/sfx.config:baseMaxDistance").toFloat();
|
||||
Vec2F stereoAdjustmentRange = jsonToVec2F(assets->json("/sfx.config:stereoAdjustmentRange"));
|
||||
float attenuationGamma = assets->json("/sfx.config:attenuationGamma").toFloat();
|
||||
auto playerPos = m_universeClient->mainPlayer()->position();
|
||||
auto worldGeometry = currentWorld->geometry();
|
||||
|
||||
m_mixer->update([&](unsigned channel, Vec2F pos, float rangeMultiplier) {
|
||||
Vec2F diff = worldGeometry.diff(pos, playerPos);
|
||||
float diffMagnitude = diff.magnitude();
|
||||
if (diffMagnitude == 0.0f)
|
||||
return 0.0f;
|
||||
|
||||
Vec2F diffNorm = diff / diffMagnitude;
|
||||
|
||||
float stereoIncidence = channel == 0 ? -diffNorm[0] : diffNorm[0];
|
||||
|
||||
float maxDistance = baseMaxDistance * rangeMultiplier * lerp((stereoIncidence + 1.0f) / 2.0f, stereoAdjustmentRange[0], stereoAdjustmentRange[1]);
|
||||
|
||||
return pow(clamp(diffMagnitude / maxDistance, 0.0f, 1.0f), 1.0f / attenuationGamma);
|
||||
});
|
||||
|
||||
} else {
|
||||
if (m_mixer->hasEffect("lowpass"))
|
||||
m_mixer->removeEffect("lowpass", 0);
|
||||
if (m_mixer->hasEffect("echo"))
|
||||
m_mixer->removeEffect("echo", 0);
|
||||
|
||||
m_mixer->update();
|
||||
}
|
||||
}
|
||||
|
||||
MixerPtr MainMixer::mixer() const {
|
||||
return m_mixer;
|
||||
}
|
||||
|
||||
void MainMixer::setVolume(float volume, float rampTime) {
|
||||
m_mixer->setVolume(volume, rampTime);
|
||||
}
|
||||
|
||||
void MainMixer::read(int16_t* sampleData, size_t frameCount) {
|
||||
m_mixer->read(sampleData, frameCount);
|
||||
}
|
||||
|
||||
}
|
34
source/frontend/StarMainMixer.hpp
Normal file
34
source/frontend/StarMainMixer.hpp
Normal file
|
@ -0,0 +1,34 @@
|
|||
#ifndef STAR_MAIN_MIXER_HPP
|
||||
#define STAR_MAIN_MIXER_HPP
|
||||
|
||||
#include "StarMixer.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(UniverseClient);
|
||||
STAR_CLASS(MainMixer);
|
||||
|
||||
class MainMixer {
|
||||
public:
|
||||
MainMixer(unsigned sampleRate, unsigned channels);
|
||||
|
||||
void setUniverseClient(UniverseClientPtr universeClient);
|
||||
|
||||
void update(bool muteSfx = false, bool muteMusic = false);
|
||||
|
||||
MixerPtr mixer() const;
|
||||
|
||||
void setVolume(float volume, float rampTime = 0.0f);
|
||||
void read(int16_t* sampleData, size_t frameCount);
|
||||
|
||||
private:
|
||||
UniverseClientPtr m_universeClient;
|
||||
MixerPtr m_mixer;
|
||||
Set<MixerGroup> m_mutedGroups;
|
||||
Map<MixerGroup, float> m_groupVolumes;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
391
source/frontend/StarMerchantInterface.cpp
Normal file
391
source/frontend/StarMerchantInterface.cpp
Normal file
|
@ -0,0 +1,391 @@
|
|||
#include "StarMerchantInterface.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarLexicalCast.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarItemTooltip.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarWorldClient.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarTextBoxWidget.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarItemGridWidget.hpp"
|
||||
#include "StarListWidget.hpp"
|
||||
#include "StarTabSet.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarItemDatabase.hpp"
|
||||
#include "StarPlayerInventory.hpp"
|
||||
#include "StarItemBag.hpp"
|
||||
#include "StarQuestManager.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
MerchantPane::MerchantPane(
|
||||
WorldClientPtr worldClient, PlayerPtr player, Json const& settings, EntityId sourceEntityId) {
|
||||
m_worldClient = move(worldClient);
|
||||
m_player = move(player);
|
||||
m_sourceEntityId = sourceEntityId;
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
auto baseConfig = settings.get("config", "/interface/windowconfig/merchant.config");
|
||||
m_settings = jsonMerge(assets->fetchJson(baseConfig), settings);
|
||||
|
||||
m_refreshTimer = GameTimer(assets->json("/merchant.config:autoRefreshRate").toFloat());
|
||||
|
||||
m_buyFactor = m_settings.getFloat("buyFactor", assets->json("/merchant.config:defaultBuyFactor").toFloat());
|
||||
m_sellFactor = m_settings.getFloat("sellFactor", assets->json("/merchant.config:defaultSellFactor").toFloat());
|
||||
|
||||
m_itemBag = make_shared<ItemBag>(m_settings.getUInt("sellContainerSize"));
|
||||
|
||||
GuiReader reader;
|
||||
reader.registerCallback("spinCount.up", [=](Widget*) {
|
||||
if (m_selectedIndex != NPos) {
|
||||
if (m_buyCount < maxBuyCount())
|
||||
m_buyCount++;
|
||||
else
|
||||
m_buyCount = 1;
|
||||
} else {
|
||||
m_buyCount = 0;
|
||||
}
|
||||
countChanged();
|
||||
});
|
||||
|
||||
reader.registerCallback("spinCount.down", [=](Widget*) {
|
||||
if (m_selectedIndex != NPos) {
|
||||
if (m_buyCount > 1)
|
||||
m_buyCount--;
|
||||
else
|
||||
m_buyCount = std::max(maxBuyCount(), 1);
|
||||
} else {
|
||||
m_buyCount = 0;
|
||||
}
|
||||
countChanged();
|
||||
});
|
||||
|
||||
reader.registerCallback("countChanged", [=](Widget*) { countChanged(); });
|
||||
reader.registerCallback("parseCountText", [=](Widget*) { countTextChanged(); });
|
||||
|
||||
reader.registerCallback("buy", [=](Widget*) { buy(); });
|
||||
|
||||
reader.registerCallback("sell", [=](Widget*) { sell(); });
|
||||
|
||||
reader.registerCallback("close", [=](Widget*) { dismiss(); });
|
||||
|
||||
reader.registerCallback("itemGrid",
|
||||
[=](Widget*) {
|
||||
swapSlot();
|
||||
updateSellTotal();
|
||||
});
|
||||
|
||||
Json paneLayout = m_settings.get("paneLayout");
|
||||
paneLayout = jsonMerge(paneLayout, m_settings.get("paneLayoutOverride", {}));
|
||||
reader.construct(paneLayout, this);
|
||||
|
||||
m_tabSet = findChild<TabSetWidget>("buySellTabs");
|
||||
m_tabSet->setCallback([this](Widget*) {
|
||||
auto bgResult = getBG();
|
||||
if (m_tabSet->selectedTab() == 0)
|
||||
bgResult.body = m_settings.getString("buyBody");
|
||||
else
|
||||
bgResult.body = m_settings.getString("sellBody");
|
||||
setBG(bgResult);
|
||||
});
|
||||
m_itemGuiList = findChild<ListWidget>("itemList");
|
||||
m_countTextBox = findChild<TextBoxWidget>("tbCount");
|
||||
m_buyTotalLabel = findChild<LabelWidget>("lblBuyTotal");
|
||||
m_buyButton = findChild<ButtonWidget>("btnBuy");
|
||||
m_sellTotalLabel = findChild<LabelWidget>("lblSellTotal");
|
||||
m_sellButton = findChild<ButtonWidget>("btnSell");
|
||||
|
||||
m_itemGrid = findChild<ItemGridWidget>("itemGrid");
|
||||
m_itemGrid->setItemBag(m_itemBag);
|
||||
|
||||
buildItemList();
|
||||
|
||||
updateSelection();
|
||||
|
||||
updateSellTotal();
|
||||
}
|
||||
|
||||
void MerchantPane::displayed() {
|
||||
Pane::displayed();
|
||||
}
|
||||
|
||||
void MerchantPane::dismissed() {
|
||||
Pane::dismissed();
|
||||
|
||||
for (auto unsold : m_itemBag->takeAll())
|
||||
m_player->giveItem(unsold);
|
||||
|
||||
m_worldClient->sendEntityMessage(m_sourceEntityId, "onMerchantClosed");
|
||||
}
|
||||
|
||||
PanePtr MerchantPane::createTooltip(Vec2I const& screenPosition) {
|
||||
if (m_tabSet->selectedTab() == 0) {
|
||||
for (size_t i = 0; i < m_itemGuiList->numChildren(); ++i) {
|
||||
auto entry = m_itemGuiList->itemAt(i);
|
||||
if (entry->getChildAt(screenPosition)) {
|
||||
auto itemConfig = m_itemList.get(i);
|
||||
ItemPtr item = Root::singleton().itemDatabase()->item(ItemDescriptor(itemConfig.get("item")));
|
||||
return ItemTooltipBuilder::buildItemTooltip(item, m_player);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (auto item = m_itemGrid->itemAt(screenPosition))
|
||||
return ItemTooltipBuilder::buildItemTooltip(item, m_player);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void MerchantPane::update() {
|
||||
Pane::update();
|
||||
|
||||
if (!m_worldClient->playerCanReachEntity(m_sourceEntityId))
|
||||
dismiss();
|
||||
|
||||
if (m_refreshTimer.wrapTick()) {
|
||||
for (size_t i = 0; i < m_itemList.size(); ++i) {
|
||||
auto itemConfig = m_itemList.get(i);
|
||||
auto itemWidget = m_itemGuiList->itemAt(i);
|
||||
setupWidget(itemWidget, itemConfig);
|
||||
}
|
||||
updateBuyTotal();
|
||||
}
|
||||
|
||||
updateSelection();
|
||||
|
||||
m_itemGrid->updateAllItemSlots();
|
||||
}
|
||||
|
||||
EntityId MerchantPane::sourceEntityId() const {
|
||||
return m_sourceEntityId;
|
||||
}
|
||||
|
||||
ItemPtr MerchantPane::addItems(ItemPtr const& items) {
|
||||
if (m_tabSet->selectedTab() == 1) {
|
||||
auto remainder = m_itemBag->addItems(items);
|
||||
updateSellTotal();
|
||||
return remainder;
|
||||
} else {
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
void MerchantPane::swapSlot() {
|
||||
ItemPtr source = m_player->inventory()->swapSlotItem();
|
||||
auto inv = m_player->inventory();
|
||||
if (context()->shiftHeld()) {
|
||||
if (m_itemGrid->selectedItem()) {
|
||||
auto remainder = inv->addItems(m_itemBag->takeItems(m_itemGrid->selectedIndex()));
|
||||
if (remainder && !remainder->empty())
|
||||
m_itemBag->setItem(m_itemGrid->selectedIndex(), remainder);
|
||||
}
|
||||
} else {
|
||||
if (auto heldItem = m_player->inventory()->swapSlotItem())
|
||||
inv->setSwapSlotItem(m_itemBag->swapItems(m_itemGrid->selectedIndex(), heldItem));
|
||||
else
|
||||
inv->setSwapSlotItem(m_itemBag->takeItems(m_itemGrid->selectedIndex()));
|
||||
}
|
||||
}
|
||||
|
||||
void MerchantPane::buildItemList() {
|
||||
m_itemGuiList->clear();
|
||||
m_itemList = m_settings.getArray("items");
|
||||
|
||||
auto itemDatabase = Root::singleton().itemDatabase();
|
||||
filter(m_itemList, [&](Json const& itemConfig) {
|
||||
if (!itemDatabase->hasItem(ItemDescriptor(itemConfig.get("item")).name()))
|
||||
return false;
|
||||
|
||||
if (auto prerequisite = itemConfig.optString("prerequisiteQuest")) {
|
||||
if (!m_player->questManager()->hasCompleted(*prerequisite))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (auto quests = itemConfig.optArray("exclusiveQuests")) {
|
||||
for (auto quest : *quests) {
|
||||
if (m_player->questManager()->hasQuest(quest.toString()))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto prerequisite = itemConfig.optUInt("prerequisiteShipLevel")) {
|
||||
if (m_player->shipUpgrades().shipLevel < *prerequisite)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (auto maxLevel = itemConfig.optUInt("maxShipLevel")) {
|
||||
if (m_player->shipUpgrades().shipLevel > *maxLevel)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
for (auto itemConfig : m_itemList) {
|
||||
auto widget = m_itemGuiList->addItem();
|
||||
setupWidget(widget, itemConfig);
|
||||
}
|
||||
}
|
||||
|
||||
void MerchantPane::setupWidget(WidgetPtr const& widget, Json const& itemConfig) {
|
||||
auto& root = Root::singleton();
|
||||
auto assets = root.assets();
|
||||
ItemPtr item = root.itemDatabase()->item(ItemDescriptor(itemConfig.get("item")));
|
||||
|
||||
String name = item->friendlyName();
|
||||
if (item->count() > 1)
|
||||
name = strf("%s (x%s)", name, item->count());
|
||||
|
||||
auto itemName = widget->fetchChild<LabelWidget>("itemName");
|
||||
itemName->setText(name);
|
||||
|
||||
unsigned price = ceil(itemConfig.getInt("price", item->price()) * m_buyFactor);
|
||||
widget->setLabel("priceLabel", strf("%s", price));
|
||||
widget->setData(price);
|
||||
|
||||
bool unavailable = price > m_player->currency("money");
|
||||
auto unavailableoverlay = widget->fetchChild<ImageWidget>("unavailableoverlay");
|
||||
if (unavailable) {
|
||||
itemName->setColor(Color::Gray);
|
||||
unavailableoverlay->show();
|
||||
} else {
|
||||
itemName->setColor(Color::White);
|
||||
unavailableoverlay->hide();
|
||||
}
|
||||
|
||||
widget->fetchChild<ItemSlotWidget>("itemIcon")->setItem(item);
|
||||
widget->show();
|
||||
}
|
||||
|
||||
void MerchantPane::updateSelection() {
|
||||
if (m_selectedIndex != m_itemGuiList->selectedItem()) {
|
||||
m_selectedIndex = m_itemGuiList->selectedItem();
|
||||
|
||||
if (m_selectedIndex != NPos) {
|
||||
auto itemConfig = m_itemList.get(m_selectedIndex);
|
||||
m_selectedItem = Root::singleton().itemDatabase()->item(ItemDescriptor(itemConfig.get("item")));
|
||||
findChild<ButtonWidget>("spinCount.up")->enable();
|
||||
findChild<ButtonWidget>("spinCount.down")->enable();
|
||||
m_countTextBox->setColor(Color::White);
|
||||
m_buyCount = 1;
|
||||
} else {
|
||||
findChild<ButtonWidget>("spinCount.up")->disable();
|
||||
findChild<ButtonWidget>("spinCount.down")->disable();
|
||||
m_countTextBox->setColor(Color::Gray);
|
||||
m_buyCount = 0;
|
||||
}
|
||||
|
||||
countChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void MerchantPane::updateBuyTotal() {
|
||||
if (auto selected = m_itemGuiList->selectedWidget())
|
||||
m_buyTotal = selected->data().toUInt() * m_buyCount;
|
||||
else
|
||||
m_buyTotal = 0;
|
||||
|
||||
m_buyTotalLabel->setText(strf("%s", m_buyTotal));
|
||||
|
||||
if (m_selectedIndex != NPos && m_buyCount > 0)
|
||||
m_buyButton->enable();
|
||||
else
|
||||
m_buyButton->disable();
|
||||
|
||||
if (m_buyTotal > (int)m_player->inventory()->currency("money")) {
|
||||
m_buyTotalLabel->setColor(Color::Red);
|
||||
m_buyButton->disable();
|
||||
} else {
|
||||
m_buyTotalLabel->setColor(Color::White);
|
||||
}
|
||||
}
|
||||
|
||||
void MerchantPane::buy() {
|
||||
if (m_buyTotal > 0 && m_player->inventory()->consumeCurrency("money", m_buyTotal)) {
|
||||
auto countRemaining = m_buyCount;
|
||||
while (countRemaining > 0) {
|
||||
auto buyItem = m_selectedItem->clone();
|
||||
buyItem->setCount(m_selectedItem->count() * countRemaining);
|
||||
countRemaining -= buyItem->count();
|
||||
m_player->giveItem(buyItem);
|
||||
}
|
||||
|
||||
auto reportItem = m_selectedItem->clone();
|
||||
reportItem->setCount(reportItem->count() * m_buyCount, true);
|
||||
auto buySummary = JsonObject{{"item", reportItem->descriptor().toJson()}, {"total", m_buyTotal}};
|
||||
m_worldClient->sendEntityMessage(m_sourceEntityId, "onBuy", {buySummary});
|
||||
|
||||
auto& guiContext = GuiContext::singleton();
|
||||
guiContext.playAudio(Root::singleton().assets()->json("/merchant.config:buySound").toString());
|
||||
|
||||
buildItemList();
|
||||
|
||||
updateBuyTotal();
|
||||
}
|
||||
}
|
||||
|
||||
void MerchantPane::updateSellTotal() {
|
||||
m_sellTotal = 0;
|
||||
for (auto item : m_itemBag->items()) {
|
||||
if (item)
|
||||
m_sellTotal += round(item->price() * m_sellFactor);
|
||||
}
|
||||
m_sellTotalLabel->setText(strf("%s", m_sellTotal));
|
||||
if (m_sellTotal > 0)
|
||||
m_sellButton->enable();
|
||||
else
|
||||
m_sellButton->disable();
|
||||
}
|
||||
|
||||
void MerchantPane::sell() {
|
||||
if (m_sellTotal > 0) {
|
||||
auto sellSummary = JsonObject{{"items", m_itemBag->toJson()}, {"total", m_sellTotal}};
|
||||
m_worldClient->sendEntityMessage(m_sourceEntityId, "onSell", {sellSummary});
|
||||
|
||||
m_player->inventory()->addCurrency("money", m_sellTotal);
|
||||
m_itemBag->clearItems();
|
||||
updateSellTotal();
|
||||
|
||||
auto& guiContext = GuiContext::singleton();
|
||||
guiContext.playAudio(Root::singleton().assets()->json("/merchant.config:sellSound").toString());
|
||||
}
|
||||
}
|
||||
|
||||
int MerchantPane::maxBuyCount() {
|
||||
if (auto selected = m_itemGuiList->selectedWidget()) {
|
||||
auto assets = Root::singleton().assets();
|
||||
auto unitPrice = selected->data().toUInt();
|
||||
if (unitPrice == 0)
|
||||
return 1000;
|
||||
return min(1000, (int)floor(m_player->currency("money") / unitPrice));
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void MerchantPane::countChanged() {
|
||||
m_countTextBox->setText(strf("x%s", m_buyCount));
|
||||
updateBuyTotal();
|
||||
}
|
||||
|
||||
void MerchantPane::countTextChanged() {
|
||||
if (m_selectedIndex == NPos) {
|
||||
m_buyCount = 0;
|
||||
countChanged();
|
||||
} else {
|
||||
try {
|
||||
auto countString = m_countTextBox->getText().replace("x", "");
|
||||
if (countString.size()) {
|
||||
m_buyCount = clamp<int>(lexicalCast<int>(countString), 1, maxBuyCount());
|
||||
countChanged();
|
||||
}
|
||||
} catch (BadLexicalCast const&) {
|
||||
m_buyCount = 1;
|
||||
countChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
84
source/frontend/StarMerchantInterface.hpp
Normal file
84
source/frontend/StarMerchantInterface.hpp
Normal file
|
@ -0,0 +1,84 @@
|
|||
#ifndef STAR_MERCHANT_INTERFACE_HPP
|
||||
#define STAR_MERCHANT_INTERFACE_HPP
|
||||
|
||||
#include "StarWorldClient.hpp"
|
||||
#include "StarPane.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(WorldClient);
|
||||
STAR_CLASS(ItemBag);
|
||||
STAR_CLASS(ItemGridWidget);
|
||||
STAR_CLASS(ListWidget);
|
||||
STAR_CLASS(TextBoxWidget);
|
||||
STAR_CLASS(ButtonWidget);
|
||||
STAR_CLASS(LabelWidget);
|
||||
STAR_CLASS(TabSetWidget);
|
||||
|
||||
STAR_CLASS(MerchantPane);
|
||||
|
||||
class MerchantPane : public Pane {
|
||||
public:
|
||||
MerchantPane(WorldClientPtr worldClient, PlayerPtr player, Json const& settings, EntityId sourceEntityId = NullEntityId);
|
||||
|
||||
void displayed() override;
|
||||
void dismissed() override;
|
||||
PanePtr createTooltip(Vec2I const& screenPosition) override;
|
||||
|
||||
EntityId sourceEntityId() const;
|
||||
|
||||
ItemPtr addItems(ItemPtr const& items);
|
||||
|
||||
protected:
|
||||
void update() override;
|
||||
|
||||
private:
|
||||
void swapSlot();
|
||||
|
||||
void buildItemList();
|
||||
void setupWidget(WidgetPtr const& widget, Json const& itemConfig);
|
||||
void updateSelection();
|
||||
int itemPrice();
|
||||
void updateBuyTotal();
|
||||
void buy();
|
||||
|
||||
void updateSellTotal();
|
||||
void sell();
|
||||
|
||||
int maxBuyCount();
|
||||
void countChanged();
|
||||
void countTextChanged();
|
||||
|
||||
WorldClientPtr m_worldClient;
|
||||
PlayerPtr m_player;
|
||||
EntityId m_sourceEntityId;
|
||||
Json m_settings;
|
||||
|
||||
GameTimer m_refreshTimer;
|
||||
|
||||
JsonArray m_itemList;
|
||||
size_t m_selectedIndex;
|
||||
ItemPtr m_selectedItem;
|
||||
|
||||
float m_buyFactor;
|
||||
int m_buyTotal;
|
||||
float m_sellFactor;
|
||||
int m_sellTotal;
|
||||
|
||||
TabSetWidgetPtr m_tabSet;
|
||||
ListWidgetPtr m_itemGuiList;
|
||||
TextBoxWidgetPtr m_countTextBox;
|
||||
LabelWidgetPtr m_buyTotalLabel;
|
||||
ButtonWidgetPtr m_buyButton;
|
||||
LabelWidgetPtr m_sellTotalLabel;
|
||||
ButtonWidgetPtr m_sellButton;
|
||||
|
||||
ItemGridWidgetPtr m_itemGrid;
|
||||
ItemBagPtr m_itemBag;
|
||||
|
||||
int m_buyCount;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
119
source/frontend/StarModsMenu.cpp
Normal file
119
source/frontend/StarModsMenu.cpp
Normal file
|
@ -0,0 +1,119 @@
|
|||
#include "StarModsMenu.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarListWidget.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
ModsMenu::ModsMenu() {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
GuiReader reader;
|
||||
reader.registerCallback("linkbutton", bind(&ModsMenu::openLink, this));
|
||||
reader.registerCallback("workshopbutton", bind(&ModsMenu::openWorkshop, this));
|
||||
reader.construct(assets->json("/interface/modsmenu/modsmenu.config:paneLayout"), this);
|
||||
|
||||
m_assetsSources = assets->assetSources();
|
||||
m_modList = fetchChild<ListWidget>("mods.list");
|
||||
for (auto const& assetsSource : m_assetsSources) {
|
||||
auto modName = m_modList->addItem()->fetchChild<LabelWidget>("name");
|
||||
modName->setText(bestModName(assets->assetSourceMetadata(assetsSource), assetsSource));
|
||||
}
|
||||
|
||||
m_modName = findChild<LabelWidget>("modname");
|
||||
m_modAuthor = findChild<LabelWidget>("modauthor");
|
||||
m_modVersion = findChild<LabelWidget>("modversion");
|
||||
m_modPath = findChild<LabelWidget>("modpath");
|
||||
m_modDescription = findChild<LabelWidget>("moddescription");
|
||||
|
||||
m_linkButton = fetchChild<ButtonWidget>("linkbutton");
|
||||
m_copyLinkButton = fetchChild<ButtonWidget>("copylinkbutton");
|
||||
|
||||
auto linkLabel = fetchChild<LabelWidget>("linklabel");
|
||||
auto copyLinkLabel = fetchChild<LabelWidget>("copylinklabel");
|
||||
auto workshopLinkButton = fetchChild<ButtonWidget>("workshopbutton");
|
||||
|
||||
auto& guiContext = GuiContext::singleton();
|
||||
bool hasDesktopService = (bool)guiContext.applicationController()->desktopService();
|
||||
|
||||
workshopLinkButton->setEnabled(hasDesktopService);
|
||||
|
||||
m_linkButton->setVisibility(hasDesktopService);
|
||||
m_copyLinkButton->setVisibility(!hasDesktopService);
|
||||
|
||||
m_linkButton->setEnabled(false);
|
||||
m_copyLinkButton->setEnabled(false);
|
||||
|
||||
linkLabel->setVisibility(hasDesktopService);
|
||||
copyLinkLabel->setVisibility(!hasDesktopService);
|
||||
}
|
||||
|
||||
void ModsMenu::update() {
|
||||
Pane::update();
|
||||
|
||||
size_t selectedItem = m_modList->selectedItem();
|
||||
if (selectedItem == NPos) {
|
||||
m_modName->setText("");
|
||||
m_modAuthor->setText("");
|
||||
m_modVersion->setText("");
|
||||
m_modPath->setText("");
|
||||
m_modDescription->setText("");
|
||||
|
||||
} else {
|
||||
String assetsSource = m_assetsSources.at(selectedItem);
|
||||
JsonObject assetsSourceMetadata = Root::singleton().assets()->assetSourceMetadata(assetsSource);
|
||||
|
||||
m_modName->setText(bestModName(assetsSourceMetadata, assetsSource));
|
||||
m_modAuthor->setText(assetsSourceMetadata.value("author", "No Author Set").toString());
|
||||
m_modVersion->setText(assetsSourceMetadata.value("version", "No Version Set").toString());
|
||||
m_modPath->setText(assetsSource);
|
||||
m_modDescription->setText(assetsSourceMetadata.value("description", "").toString());
|
||||
|
||||
String link = assetsSourceMetadata.value("link", "").toString();
|
||||
|
||||
m_linkButton->setEnabled(!link.empty());
|
||||
m_copyLinkButton->setEnabled(!link.empty());
|
||||
}
|
||||
}
|
||||
|
||||
String ModsMenu::bestModName(JsonObject const& metadata, String const& sourcePath) {
|
||||
if (auto ptr = metadata.ptr("friendlyName"))
|
||||
return ptr->toString();
|
||||
if (auto ptr = metadata.ptr("name"))
|
||||
return ptr->toString();
|
||||
String baseName = File::baseName(sourcePath);
|
||||
if (baseName.contains("."))
|
||||
baseName.rextract(".");
|
||||
return baseName;
|
||||
}
|
||||
|
||||
void ModsMenu::openLink() {
|
||||
size_t selectedItem = m_modList->selectedItem();
|
||||
if (selectedItem == NPos)
|
||||
return;
|
||||
|
||||
String assetsSource = m_assetsSources.at(selectedItem);
|
||||
JsonObject assetsSourceMetadata = Root::singleton().assets()->assetSourceMetadata(assetsSource);
|
||||
String link = assetsSourceMetadata.value("link", "").toString();
|
||||
|
||||
if (link.empty())
|
||||
return;
|
||||
|
||||
auto& guiContext = GuiContext::singleton();
|
||||
if (auto desktopService = guiContext.applicationController()->desktopService())
|
||||
desktopService->openUrl(link);
|
||||
else
|
||||
guiContext.setClipboard(link);
|
||||
}
|
||||
|
||||
void ModsMenu::openWorkshop() {
|
||||
auto assets = Root::singleton().assets();
|
||||
auto& guiContext = GuiContext::singleton();
|
||||
if (auto desktopService = guiContext.applicationController()->desktopService())
|
||||
desktopService->openUrl(assets->json("/interface/modsmenu/modsmenu.config:workshopLink").toString());
|
||||
}
|
||||
|
||||
}
|
39
source/frontend/StarModsMenu.hpp
Normal file
39
source/frontend/StarModsMenu.hpp
Normal file
|
@ -0,0 +1,39 @@
|
|||
#ifndef STAR_MODS_MENU_HPP
|
||||
#define STAR_MODS_MENU_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(LabelWidget);
|
||||
STAR_CLASS(ButtonWidget);
|
||||
STAR_CLASS(ListWidget);
|
||||
|
||||
class ModsMenu : public Pane {
|
||||
public:
|
||||
ModsMenu();
|
||||
|
||||
void update() override;
|
||||
|
||||
private:
|
||||
static String bestModName(JsonObject const& metadata, String const& sourcePath);
|
||||
|
||||
void openLink();
|
||||
void openWorkshop();
|
||||
|
||||
StringList m_assetsSources;
|
||||
|
||||
ListWidgetPtr m_modList;
|
||||
LabelWidgetPtr m_modName;
|
||||
LabelWidgetPtr m_modAuthor;
|
||||
LabelWidgetPtr m_modVersion;
|
||||
LabelWidgetPtr m_modPath;
|
||||
LabelWidgetPtr m_modDescription;
|
||||
|
||||
ButtonWidgetPtr m_linkButton;
|
||||
ButtonWidgetPtr m_copyLinkButton;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
118
source/frontend/StarNameplatePainter.cpp
Normal file
118
source/frontend/StarNameplatePainter.cpp
Normal file
|
@ -0,0 +1,118 @@
|
|||
#include "StarNameplatePainter.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarNametagEntity.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarGuiContext.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
NameplatePainter::NameplatePainter() {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
Json nametagConfig = assets->json("/interface.config:nametag");
|
||||
m_opacityRate = nametagConfig.getFloat("opacityRate");
|
||||
m_offset = jsonToVec2F(nametagConfig.get("offset"));
|
||||
m_fontSize = nametagConfig.getFloat("fontSize");
|
||||
m_statusFontSize = nametagConfig.getFloat("statusFontSize");
|
||||
m_statusOffset = jsonToVec2F(nametagConfig.get("statusOffset"));
|
||||
m_statusColor = jsonToColor(nametagConfig.get("statusColor"));
|
||||
m_opacityBoost = nametagConfig.getFloat("opacityBoost");
|
||||
m_nametags.setTweenFactor(nametagConfig.getFloat("tweenFactor"));
|
||||
m_nametags.setMovementThreshold(nametagConfig.getFloat("movementThreshold"));
|
||||
}
|
||||
|
||||
void NameplatePainter::update(WorldClientPtr const& world, WorldCamera const& camera, bool inspectionMode) {
|
||||
m_camera = camera;
|
||||
|
||||
Set<EntityId> foundEntities;
|
||||
for (auto const& entity : world->query<NametagEntity>(camera.worldScreenRect())) {
|
||||
if (entity->isMaster() || !entity->displayNametag())
|
||||
continue;
|
||||
if (auto player = as<Player>(entity)) {
|
||||
if (player->isTeleporting())
|
||||
continue;
|
||||
}
|
||||
foundEntities.insert(entity->entityId());
|
||||
|
||||
if (!m_entitiesWithNametags.contains(entity->entityId())) {
|
||||
Nametag nametag = {entity->name(), entity->statusText(), entity->nametagColor(), 1.0f, entity->entityId()};
|
||||
RectF boundBox = determineBoundBox(Vec2F(), nametag);
|
||||
m_nametags.addBubble(Vec2F(), boundBox, move(nametag));
|
||||
}
|
||||
}
|
||||
|
||||
m_nametags.filter([&foundEntities](
|
||||
BubbleState<Nametag> const&, Nametag const& nametag) { return foundEntities.contains(nametag.entityId); });
|
||||
|
||||
m_nametags.forEach([&world, &camera, this, inspectionMode](BubbleState<Nametag>& bubbleState, Nametag& nametag) {
|
||||
if (auto entity = as<NametagEntity>(world->entity(nametag.entityId))) {
|
||||
bubbleState.idealDestination = camera.worldToScreen(entity->position()) + m_offset * camera.pixelRatio();
|
||||
bubbleState.boundBox = determineBoundBox(bubbleState.idealDestination, nametag);
|
||||
|
||||
nametag.statusText = entity->statusText();
|
||||
nametag.name = entity->name();
|
||||
nametag.color = entity->nametagColor();
|
||||
bool fullyOnScreen = world->geometry().rectContains(camera.worldScreenRect(), entity->position());
|
||||
if (inspectionMode)
|
||||
nametag.opacity = 1.0f;
|
||||
else if (fullyOnScreen)
|
||||
nametag.opacity = approach(0.0f, nametag.opacity, m_opacityRate);
|
||||
else
|
||||
nametag.opacity = approach(m_opacityBoost, nametag.opacity, m_opacityRate);
|
||||
}
|
||||
});
|
||||
|
||||
m_entitiesWithNametags = foundEntities;
|
||||
m_nametags.update();
|
||||
}
|
||||
|
||||
void NameplatePainter::render() {
|
||||
auto& context = GuiContext::singleton();
|
||||
|
||||
m_nametags.forEach([&context, this](BubbleState<Nametag> const& bubble, Nametag const& nametag) {
|
||||
if (nametag.opacity == 0.0f)
|
||||
return;
|
||||
|
||||
context.setFontSize(m_fontSize);
|
||||
|
||||
auto color = Color::rgb(nametag.color);
|
||||
color.setAlphaF(nametag.opacity);
|
||||
auto statusColor = m_statusColor;
|
||||
statusColor.setAlphaF(nametag.opacity);
|
||||
context.setFontColor(color.toRgba());
|
||||
context.setFontMode(FontMode::Shadow);
|
||||
|
||||
context.renderText(nametag.name, namePosition(bubble.currentPosition));
|
||||
|
||||
if (nametag.statusText) {
|
||||
context.setFontSize(m_statusFontSize);
|
||||
context.setFontColor(statusColor.toRgba());
|
||||
context.renderText(*nametag.statusText, statusPosition(bubble.currentPosition));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
TextPositioning NameplatePainter::namePosition(Vec2F bubblePosition) const {
|
||||
return TextPositioning(bubblePosition, HorizontalAnchor::HMidAnchor, VerticalAnchor::BottomAnchor);
|
||||
}
|
||||
|
||||
TextPositioning NameplatePainter::statusPosition(Vec2F bubblePosition) const {
|
||||
auto& context = GuiContext::singleton();
|
||||
return TextPositioning(
|
||||
bubblePosition + m_statusOffset * context.interfaceScale(),
|
||||
HorizontalAnchor::HMidAnchor, VerticalAnchor::BottomAnchor);
|
||||
}
|
||||
|
||||
RectF NameplatePainter::determineBoundBox(Vec2F bubblePosition, Nametag const& nametag) const {
|
||||
auto& context = GuiContext::singleton();
|
||||
context.setFontSize(m_fontSize);
|
||||
RectF nametagBox = context.determineTextSize(nametag.name, namePosition(bubblePosition));
|
||||
if (nametag.statusText) {
|
||||
context.setFontSize(m_statusFontSize);
|
||||
nametagBox.combine(context.determineTextSize(*nametag.statusText, statusPosition(bubblePosition)));
|
||||
}
|
||||
return nametagBox;
|
||||
}
|
||||
|
||||
}
|
50
source/frontend/StarNameplatePainter.hpp
Normal file
50
source/frontend/StarNameplatePainter.hpp
Normal file
|
@ -0,0 +1,50 @@
|
|||
#ifndef STAR_NAMEPLATE_PAINTER_HPP
|
||||
#define STAR_NAMEPLATE_PAINTER_HPP
|
||||
|
||||
#include "StarWorldClient.hpp"
|
||||
#include "StarWorldCamera.hpp"
|
||||
#include "StarChatBubbleSeparation.hpp"
|
||||
#include "StarTextPainter.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(WorldClient);
|
||||
STAR_CLASS(NameplatePainter);
|
||||
|
||||
class NameplatePainter {
|
||||
public:
|
||||
NameplatePainter();
|
||||
|
||||
void update(WorldClientPtr const& world, WorldCamera const& camera, bool inspectionMode);
|
||||
void render();
|
||||
|
||||
private:
|
||||
struct Nametag {
|
||||
String name;
|
||||
Maybe<String> statusText;
|
||||
Vec3B color;
|
||||
float opacity;
|
||||
EntityId entityId;
|
||||
};
|
||||
|
||||
TextPositioning namePosition(Vec2F bubblePosition) const;
|
||||
TextPositioning statusPosition(Vec2F bubblePosition) const;
|
||||
RectF determineBoundBox(Vec2F bubblePosition, Nametag const& nametag) const;
|
||||
|
||||
float m_opacityRate;
|
||||
Vec2F m_offset;
|
||||
float m_fontSize;
|
||||
float m_statusFontSize;
|
||||
Vec2F m_statusOffset;
|
||||
Color m_statusColor;
|
||||
float m_opacityBoost;
|
||||
|
||||
WorldCamera m_camera;
|
||||
|
||||
Set<EntityId> m_entitiesWithNametags;
|
||||
BubbleSeparator<Nametag> m_nametags;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
169
source/frontend/StarOptionsMenu.cpp
Normal file
169
source/frontend/StarOptionsMenu.cpp
Normal file
|
@ -0,0 +1,169 @@
|
|||
#include "StarOptionsMenu.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarLexicalCast.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarSliderBar.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarKeybindingsMenu.hpp"
|
||||
#include "StarGraphicsMenu.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
OptionsMenu::OptionsMenu(PaneManager* manager)
|
||||
: m_sfxRange(0, 100), m_musicRange(0, 100), m_paneManager(manager) {
|
||||
auto root = Root::singletonPtr();
|
||||
auto assets = root->assets();
|
||||
|
||||
GuiReader reader;
|
||||
|
||||
reader.registerCallback("sfxSlider", [=](Widget*) {
|
||||
updateSFXVol();
|
||||
});
|
||||
reader.registerCallback("musicSlider", [=](Widget*) {
|
||||
updateMusicVol();
|
||||
});
|
||||
reader.registerCallback("acceptButton", [=](Widget*) {
|
||||
for (auto k : ConfigKeys)
|
||||
root->configuration()->set(k, m_localChanges.get(k));
|
||||
|
||||
dismiss();
|
||||
});
|
||||
reader.registerCallback("tutorialMessagesCheckbox", [=](Widget*) {
|
||||
updateTutorialMessages();
|
||||
});
|
||||
reader.registerCallback("clientIPJoinableCheckbox", [=](Widget*) {
|
||||
updateClientIPJoinable();
|
||||
});
|
||||
reader.registerCallback("clientP2PJoinableCheckbox", [=](Widget*) {
|
||||
updateClientP2PJoinable();
|
||||
});
|
||||
reader.registerCallback("allowAssetsMismatchCheckbox", [=](Widget*) {
|
||||
updateAllowAssetsMismatch();
|
||||
});
|
||||
reader.registerCallback("backButton", [=](Widget*) {
|
||||
dismiss();
|
||||
});
|
||||
reader.registerCallback("showKeybindings", [=](Widget*) {
|
||||
displayControls();
|
||||
});
|
||||
reader.registerCallback("showGraphics", [=](Widget*) {
|
||||
displayGraphics();
|
||||
});
|
||||
|
||||
reader.construct(assets->json("/interface/optionsmenu/optionsmenu.config:paneLayout"), this);
|
||||
|
||||
m_sfxSlider = fetchChild<SliderBarWidget>("sfxSlider");
|
||||
m_musicSlider = fetchChild<SliderBarWidget>("musicSlider");
|
||||
m_tutorialMessagesButton = fetchChild<ButtonWidget>("tutorialMessagesCheckbox");
|
||||
m_clientIPJoinableButton = fetchChild<ButtonWidget>("clientIPJoinableCheckbox");
|
||||
m_clientP2PJoinableButton = fetchChild<ButtonWidget>("clientP2PJoinableCheckbox");
|
||||
m_allowAssetsMismatchButton = fetchChild<ButtonWidget>("allowAssetsMismatchCheckbox");
|
||||
|
||||
m_sfxLabel = fetchChild<LabelWidget>("sfxValueLabel");
|
||||
m_musicLabel = fetchChild<LabelWidget>("musicValueLabel");
|
||||
m_p2pJoinableLabel = fetchChild<LabelWidget>("clientP2PJoinableLabel");
|
||||
|
||||
m_sfxSlider->setRange(m_sfxRange, assets->json("/interface/optionsmenu/optionsmenu.config:sfxDelta").toInt());
|
||||
m_musicSlider->setRange(m_musicRange, assets->json("/interface/optionsmenu/optionsmenu.config:musicDelta").toInt());
|
||||
|
||||
m_keybindingsMenu = make_shared<KeybindingsMenu>();
|
||||
m_graphicsMenu = make_shared<GraphicsMenu>();
|
||||
|
||||
initConfig();
|
||||
}
|
||||
|
||||
void OptionsMenu::show() {
|
||||
initConfig();
|
||||
syncGuiToConf();
|
||||
|
||||
Pane::show();
|
||||
}
|
||||
|
||||
void OptionsMenu::toggleFullscreen() {
|
||||
m_graphicsMenu->toggleFullscreen();
|
||||
|
||||
syncGuiToConf();
|
||||
}
|
||||
|
||||
StringList const OptionsMenu::ConfigKeys = {
|
||||
"sfxVol",
|
||||
"musicVol",
|
||||
"tutorialMessages",
|
||||
"clientIPJoinable",
|
||||
"clientP2PJoinable",
|
||||
"allowAssetsMismatch"
|
||||
};
|
||||
|
||||
void OptionsMenu::initConfig() {
|
||||
auto configuration = Root::singleton().configuration();
|
||||
|
||||
for (auto k : ConfigKeys) {
|
||||
m_origConfig[k] = configuration->get(k);
|
||||
m_localChanges[k] = configuration->get(k);
|
||||
}
|
||||
}
|
||||
|
||||
void OptionsMenu::updateSFXVol() {
|
||||
m_localChanges.set("sfxVol", m_sfxSlider->val());
|
||||
Root::singleton().configuration()->set("sfxVol", m_sfxSlider->val());
|
||||
m_sfxLabel->setText(toString(m_sfxSlider->val()));
|
||||
}
|
||||
|
||||
void OptionsMenu::updateMusicVol() {
|
||||
m_localChanges.set("musicVol", {m_musicSlider->val()});
|
||||
Root::singleton().configuration()->set("musicVol", m_musicSlider->val());
|
||||
m_musicLabel->setText(toString(m_musicSlider->val()));
|
||||
}
|
||||
|
||||
|
||||
void OptionsMenu::updateTutorialMessages() {
|
||||
m_localChanges.set("tutorialMessages", m_tutorialMessagesButton->isChecked());
|
||||
Root::singleton().configuration()->set("tutorialMessages", m_tutorialMessagesButton->isChecked());
|
||||
}
|
||||
|
||||
void OptionsMenu::updateClientIPJoinable() {
|
||||
m_localChanges.set("clientIPJoinable", m_clientIPJoinableButton->isChecked());
|
||||
Root::singleton().configuration()->set("clientIPJoinable", m_clientIPJoinableButton->isChecked());
|
||||
}
|
||||
|
||||
void OptionsMenu::updateClientP2PJoinable() {
|
||||
m_localChanges.set("clientP2PJoinable", m_clientP2PJoinableButton->isChecked());
|
||||
Root::singleton().configuration()->set("clientP2PJoinable", m_clientP2PJoinableButton->isChecked());
|
||||
}
|
||||
|
||||
void OptionsMenu::updateAllowAssetsMismatch() {
|
||||
m_localChanges.set("allowAssetsMismatch", m_allowAssetsMismatchButton->isChecked());
|
||||
Root::singleton().configuration()->set("allowAssetsMismatch", m_allowAssetsMismatchButton->isChecked());
|
||||
}
|
||||
|
||||
void OptionsMenu::syncGuiToConf() {
|
||||
m_sfxSlider->setVal(m_localChanges.get("sfxVol").toInt(), false);
|
||||
m_sfxLabel->setText(toString(m_sfxSlider->val()));
|
||||
|
||||
m_musicSlider->setVal(m_localChanges.get("musicVol").toInt(), false);
|
||||
m_musicLabel->setText(toString(m_musicSlider->val()));
|
||||
|
||||
m_tutorialMessagesButton->setChecked(m_localChanges.get("tutorialMessages").toBool());
|
||||
m_clientIPJoinableButton->setChecked(m_localChanges.get("clientIPJoinable").toBool());
|
||||
m_clientP2PJoinableButton->setChecked(m_localChanges.get("clientP2PJoinable").toBool());
|
||||
m_allowAssetsMismatchButton->setChecked(m_localChanges.get("allowAssetsMismatch").toBool());
|
||||
|
||||
auto appController = GuiContext::singleton().applicationController();
|
||||
if (!appController->p2pNetworkingService()) {
|
||||
m_p2pJoinableLabel->setColor(Color::DarkGray);
|
||||
m_clientP2PJoinableButton->setEnabled(false);
|
||||
m_clientP2PJoinableButton->setChecked(false);
|
||||
}
|
||||
}
|
||||
|
||||
void OptionsMenu::displayControls() {
|
||||
m_paneManager->displayPane(PaneLayer::ModalWindow, m_keybindingsMenu);
|
||||
}
|
||||
|
||||
void OptionsMenu::displayGraphics() {
|
||||
m_paneManager->displayPane(PaneLayer::ModalWindow, m_graphicsMenu);
|
||||
}
|
||||
|
||||
}
|
68
source/frontend/StarOptionsMenu.hpp
Normal file
68
source/frontend/StarOptionsMenu.hpp
Normal file
|
@ -0,0 +1,68 @@
|
|||
#ifndef STAR_OPTIONS_MENU_HPP
|
||||
#define STAR_OPTIONS_MENU_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
#include "StarConfiguration.hpp"
|
||||
#include "StarMainInterfaceTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(SliderBarWidget);
|
||||
STAR_CLASS(ButtonWidget);
|
||||
STAR_CLASS(LabelWidget);
|
||||
STAR_CLASS(KeybindingsMenu);
|
||||
STAR_CLASS(GraphicsMenu);
|
||||
|
||||
STAR_CLASS(OptionsMenu);
|
||||
|
||||
class OptionsMenu : public Pane {
|
||||
public:
|
||||
OptionsMenu(PaneManager* manager);
|
||||
|
||||
virtual void show() override;
|
||||
|
||||
void toggleFullscreen();
|
||||
|
||||
private:
|
||||
static StringList const ConfigKeys;
|
||||
|
||||
void initConfig();
|
||||
|
||||
void updateSFXVol();
|
||||
void updateMusicVol();
|
||||
void updateTutorialMessages();
|
||||
void updateClientIPJoinable();
|
||||
void updateClientP2PJoinable();
|
||||
void updateAllowAssetsMismatch();
|
||||
|
||||
void syncGuiToConf();
|
||||
|
||||
void displayControls();
|
||||
void displayGraphics();
|
||||
|
||||
SliderBarWidgetPtr m_sfxSlider;
|
||||
SliderBarWidgetPtr m_musicSlider;
|
||||
ButtonWidgetPtr m_tutorialMessagesButton;
|
||||
ButtonWidgetPtr m_interactiveHighlightButton;
|
||||
ButtonWidgetPtr m_clientIPJoinableButton;
|
||||
ButtonWidgetPtr m_clientP2PJoinableButton;
|
||||
ButtonWidgetPtr m_allowAssetsMismatchButton;
|
||||
|
||||
LabelWidgetPtr m_sfxLabel;
|
||||
LabelWidgetPtr m_musicLabel;
|
||||
LabelWidgetPtr m_p2pJoinableLabel;
|
||||
|
||||
Vec2I m_sfxRange;
|
||||
Vec2I m_musicRange;
|
||||
|
||||
JsonObject m_origConfig;
|
||||
JsonObject m_localChanges;
|
||||
|
||||
KeybindingsMenuPtr m_keybindingsMenu;
|
||||
GraphicsMenuPtr m_graphicsMenu;
|
||||
PaneManager* m_paneManager;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
30
source/frontend/StarPopupInterface.cpp
Normal file
30
source/frontend/StarPopupInterface.cpp
Normal file
|
@ -0,0 +1,30 @@
|
|||
#include "StarPopupInterface.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarRandom.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
PopupInterface::PopupInterface() {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
GuiReader reader;
|
||||
|
||||
reader.registerCallback("close", [=](Widget*) { dismiss(); });
|
||||
reader.registerCallback("ok", [=](Widget*) { dismiss(); });
|
||||
|
||||
reader.construct(assets->json("/interface/windowconfig/popup.config:paneLayout"), this);
|
||||
}
|
||||
|
||||
void PopupInterface::displayMessage(String const& message, String const& title, String const& subtitle, Maybe<String> const& onShowSound) {
|
||||
setTitleString(title, subtitle);
|
||||
fetchChild<LabelWidget>("message")->setText(message);
|
||||
show();
|
||||
auto sound = onShowSound.value(Random::randValueFrom(Root::singleton().assets()->json("/interface/windowconfig/popup.config:onShowSound").toArray(), "").toString());
|
||||
if (!sound.empty())
|
||||
context()->playAudio(sound);
|
||||
}
|
||||
|
||||
}
|
22
source/frontend/StarPopupInterface.hpp
Normal file
22
source/frontend/StarPopupInterface.hpp
Normal file
|
@ -0,0 +1,22 @@
|
|||
#ifndef STAR_POPUP_INTERFACE_HPP
|
||||
#define STAR_POPUP_INTERFACE_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(PopupInterface);
|
||||
class PopupInterface : public Pane {
|
||||
public:
|
||||
PopupInterface();
|
||||
|
||||
virtual ~PopupInterface() {}
|
||||
|
||||
void displayMessage(String const& message, String const& title, String const& subtitle, Maybe<String> const& onShowSound = {});
|
||||
|
||||
private:
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
67
source/frontend/StarQuestIndicatorPainter.cpp
Normal file
67
source/frontend/StarQuestIndicatorPainter.cpp
Normal file
|
@ -0,0 +1,67 @@
|
|||
#include "StarQuestIndicatorPainter.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarGuiContext.hpp"
|
||||
#include "StarQuestManager.hpp"
|
||||
#include "StarWorldClient.hpp"
|
||||
#include "StarUniverseClient.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
QuestIndicatorPainter::QuestIndicatorPainter(UniverseClientPtr const& client) {
|
||||
m_client = client;
|
||||
}
|
||||
|
||||
AnimationPtr indicatorAnimation(String indicatorPath) {
|
||||
auto assets = Root::singleton().assets();
|
||||
return make_shared<Animation>(assets->json(indicatorPath), indicatorPath);
|
||||
}
|
||||
|
||||
void QuestIndicatorPainter::update(WorldClientPtr const& world, WorldCamera const& camera) {
|
||||
m_camera = camera;
|
||||
|
||||
Set<EntityId> foundIndicators;
|
||||
for (auto const& entity : world->query<Entity>(camera.worldScreenRect())) {
|
||||
auto indicator = m_client->questManager()->getQuestIndicator(entity);
|
||||
if (!indicator) continue;
|
||||
|
||||
foundIndicators.insert(entity->entityId());
|
||||
Vec2F screenPos = camera.worldToScreen(indicator->worldPosition);
|
||||
|
||||
if (auto currentIndicator = m_indicators.ptr(entity->entityId())) {
|
||||
currentIndicator->screenPos = screenPos;
|
||||
if (currentIndicator->indicatorName == indicator->indicatorImage) {
|
||||
currentIndicator->animation->update(WorldTimestep);
|
||||
} else {
|
||||
currentIndicator->indicatorName = indicator->indicatorImage;
|
||||
currentIndicator->animation = indicatorAnimation(indicator->indicatorImage);
|
||||
}
|
||||
} else {
|
||||
m_indicators[entity->entityId()] = Indicator {
|
||||
entity->entityId(),
|
||||
screenPos,
|
||||
indicator->indicatorImage,
|
||||
indicatorAnimation(indicator->indicatorImage)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
m_indicators = Map<EntityId, Indicator>::from(m_indicators.pairs().filtered([&foundIndicators](pair<EntityId, Indicator> indicator) {
|
||||
return foundIndicators.contains(indicator.first);
|
||||
}));
|
||||
}
|
||||
|
||||
Drawable QuestIndicatorPainter::Indicator::render(float pixelRatio) const {
|
||||
return animation->drawable(pixelRatio);
|
||||
}
|
||||
|
||||
void QuestIndicatorPainter::render() {
|
||||
auto& context = GuiContext::singleton();
|
||||
|
||||
for (auto const& indicator : m_indicators.values()) {
|
||||
Drawable drawable = indicator.render(m_camera.pixelRatio());
|
||||
drawable.fullbright = true;
|
||||
context.drawDrawable(drawable, Vec2F(indicator.screenPos), 1, Vec4B(255, 255, 255, 255));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
36
source/frontend/StarQuestIndicatorPainter.hpp
Normal file
36
source/frontend/StarQuestIndicatorPainter.hpp
Normal file
|
@ -0,0 +1,36 @@
|
|||
#ifndef STAR_QUEST_INDICATOR_PAINTER_HPP
|
||||
#define STAR_QUEST_INDICATOR_PAINTER_HPP
|
||||
|
||||
#include "StarWorldCamera.hpp"
|
||||
#include "StarWorldClient.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(UniverseClient);
|
||||
STAR_CLASS(QuestIndicatorPainter);
|
||||
|
||||
class QuestIndicatorPainter {
|
||||
public:
|
||||
QuestIndicatorPainter(UniverseClientPtr const& client);
|
||||
|
||||
void update(WorldClientPtr const& world, WorldCamera const& camera);
|
||||
void render();
|
||||
|
||||
private:
|
||||
struct Indicator {
|
||||
Drawable render(float pixelRatio) const;
|
||||
|
||||
EntityId entityId;
|
||||
Vec2F screenPos;
|
||||
String indicatorName;
|
||||
AnimationPtr animation;
|
||||
};
|
||||
|
||||
UniverseClientPtr m_client;
|
||||
WorldCamera m_camera;
|
||||
Map<EntityId, Indicator> m_indicators;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
447
source/frontend/StarQuestInterface.cpp
Normal file
447
source/frontend/StarQuestInterface.cpp
Normal file
|
@ -0,0 +1,447 @@
|
|||
#include "StarQuestInterface.hpp"
|
||||
#include "StarQuestManager.hpp"
|
||||
#include "StarCinematic.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarUniverseClient.hpp"
|
||||
#include "StarPaneManager.hpp"
|
||||
#include "StarListWidget.hpp"
|
||||
#include "StarItemGridWidget.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarItemDatabase.hpp"
|
||||
#include "StarRandom.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarVerticalLayout.hpp"
|
||||
#include "StarItemTooltip.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
QuestLogInterface::QuestLogInterface(QuestManagerPtr manager, PlayerPtr player, CinematicPtr cinematic, UniverseClientPtr client) {
|
||||
m_manager = manager;
|
||||
m_player = player;
|
||||
m_cinematic = cinematic;
|
||||
m_client = client;
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
auto config = assets->json("/interface/windowconfig/questlog.config");
|
||||
|
||||
m_trackLabel = config.getString("trackLabel");
|
||||
m_untrackLabel = config.getString("untrackLabel");
|
||||
|
||||
GuiReader reader;
|
||||
|
||||
reader.registerCallback("close", [=](Widget*) { dismiss(); });
|
||||
reader.registerCallback("btnToggleTracking",
|
||||
[=](Widget*) {
|
||||
toggleTracking();
|
||||
});
|
||||
reader.registerCallback("btnAbandon",
|
||||
[=](Widget*) {
|
||||
abandon();
|
||||
});
|
||||
reader.registerCallback("filter",
|
||||
[=](Widget*) {
|
||||
fetchData();
|
||||
});
|
||||
|
||||
reader.construct(config.get("paneLayout"), this);
|
||||
|
||||
auto mainQuestList = fetchChild<ListWidget>("scrollArea.verticalLayout.mainQuestList");
|
||||
auto sideQuestList = fetchChild<ListWidget>("scrollArea.verticalLayout.sideQuestList");
|
||||
mainQuestList->setCallback([sideQuestList](Widget* widget) {
|
||||
auto listWidget = as<ListWidget>(widget);
|
||||
if (listWidget->selectedItem() != NPos)
|
||||
sideQuestList->clearSelected();
|
||||
});
|
||||
sideQuestList->setCallback([mainQuestList](Widget* widget) {
|
||||
auto listWidget = as<ListWidget>(widget);
|
||||
if (listWidget->selectedItem() != NPos)
|
||||
mainQuestList->clearSelected();
|
||||
});
|
||||
mainQuestList->disableScissoring();
|
||||
sideQuestList->disableScissoring();
|
||||
|
||||
m_rewardItems = make_shared<ItemBag>(5);
|
||||
fetchChild<ItemGridWidget>("rewardItems")->setItemBag(m_rewardItems);
|
||||
|
||||
m_refreshRate = 30;
|
||||
m_refreshTimer = 0;
|
||||
}
|
||||
|
||||
void QuestLogInterface::pollDialog(PaneManager* paneManager) {
|
||||
if (paneManager->topPane({PaneLayer::ModalWindow}))
|
||||
return;
|
||||
|
||||
if (auto failableQuest = m_manager->getFirstFailableQuest()) {
|
||||
auto qfi = make_shared<QuestFailedInterface>(failableQuest.value(), m_player);
|
||||
(*failableQuest)->setDialogShown();
|
||||
paneManager->displayPane(PaneLayer::ModalWindow, qfi);
|
||||
} else if (auto completableQuest = m_manager->getFirstCompletableQuest()) {
|
||||
auto qci = make_shared<QuestCompleteInterface>(completableQuest.value(), m_player, m_cinematic);
|
||||
(*completableQuest)->setDialogShown();
|
||||
paneManager->displayPane(PaneLayer::ModalWindow, qci);
|
||||
} else if (auto newQuest = m_manager->getFirstNewQuest()) {
|
||||
auto nqd = make_shared<NewQuestInterface>(m_manager, newQuest.value(), m_player);
|
||||
paneManager->displayPane(PaneLayer::ModalWindow, nqd);
|
||||
}
|
||||
}
|
||||
|
||||
void QuestLogInterface::displayed() {
|
||||
Pane::displayed();
|
||||
tick();
|
||||
fetchData();
|
||||
}
|
||||
|
||||
void QuestLogInterface::tick() {
|
||||
Pane::tick();
|
||||
auto selected = getSelected();
|
||||
if (selected && m_manager->hasQuest(selected->data().toString())) {
|
||||
auto quest = m_manager->getQuest(selected->data().toString());
|
||||
|
||||
m_manager->markAsRead(quest->questId());
|
||||
|
||||
if (m_manager->isActive(quest->questId())) {
|
||||
fetchChild<ButtonWidget>("btnToggleTracking")->enable();
|
||||
fetchChild<ButtonWidget>("btnToggleTracking")->setText(m_manager->isCurrent(quest->questId()) ? m_untrackLabel : m_trackLabel);
|
||||
if (quest->canBeAbandoned())
|
||||
fetchChild<ButtonWidget>("btnAbandon")->enable();
|
||||
else
|
||||
fetchChild<ButtonWidget>("btnAbandon")->disable();
|
||||
} else {
|
||||
fetchChild<ButtonWidget>("btnToggleTracking")->disable();
|
||||
fetchChild<ButtonWidget>("btnToggleTracking")->setText(m_trackLabel);
|
||||
fetchChild<ButtonWidget>("btnAbandon")->disable();
|
||||
}
|
||||
fetchChild<LabelWidget>("lblQuestTitle")->setText(quest->title());
|
||||
fetchChild<LabelWidget>("lblQuestBody")->setText(quest->text());
|
||||
|
||||
auto portraitName = "Objective";
|
||||
auto imagePortrait = quest->portrait(portraitName);
|
||||
if (imagePortrait) {
|
||||
auto portraitTitleLabel = fetchChild<LabelWidget>("lblPortraitTitle");
|
||||
String portraitTitle = quest->portraitTitle(portraitName).value("");
|
||||
Maybe<unsigned> charLimit = portraitTitleLabel->getTextCharLimit();
|
||||
portraitTitleLabel->setText(portraitTitle);
|
||||
|
||||
Drawable::scaleAll(*imagePortrait, Vec2F(-1, 1));
|
||||
fetchChild<ImageWidget>("imgPortrait")->setDrawables(*imagePortrait);
|
||||
fetchChild<ImageWidget>("imgPolaroid")->setVisibility(true);
|
||||
fetchChild<ImageWidget>("imgPolaroidBack")->setVisibility(true);
|
||||
|
||||
} else {
|
||||
fetchChild<LabelWidget>("lblPortraitTitle")->setText("");
|
||||
fetchChild<ImageWidget>("imgPortrait")->setDrawables({});
|
||||
fetchChild<ImageWidget>("imgPolaroid")->setVisibility(false);
|
||||
fetchChild<ImageWidget>("imgPolaroidBack")->setVisibility(false);
|
||||
}
|
||||
|
||||
m_rewardItems->clearItems();
|
||||
if (quest->rewards().size() > 0) {
|
||||
fetchChild<LabelWidget>("lblRewards")->setVisibility(true);
|
||||
fetchChild<ItemGridWidget>("rewardItems")->setVisibility(true);
|
||||
for (auto const& reward : quest->rewards())
|
||||
m_rewardItems->addItems(reward->clone());
|
||||
} else {
|
||||
fetchChild<LabelWidget>("lblRewards")->setVisibility(false);
|
||||
fetchChild<ItemGridWidget>("rewardItems")->setVisibility(false);
|
||||
}
|
||||
} else {
|
||||
fetchChild<ButtonWidget>("btnToggleTracking")->disable();
|
||||
fetchChild<ButtonWidget>("btnToggleTracking")->setText(m_trackLabel);
|
||||
fetchChild<ButtonWidget>("btnAbandon")->disable();
|
||||
fetchChild<LabelWidget>("lblQuestTitle")->setText("");
|
||||
fetchChild<LabelWidget>("lblQuestBody")->setText("");
|
||||
fetchChild<LabelWidget>("lblPortraitTitle")->setText("");
|
||||
fetchChild<ImageWidget>("imgPortrait")->setDrawables({});
|
||||
fetchChild<LabelWidget>("lblRewards")->setVisibility(false);
|
||||
fetchChild<ItemGridWidget>("rewardItems")->setVisibility(false);
|
||||
fetchChild<ImageWidget>("imgPolaroid")->setVisibility(false);
|
||||
fetchChild<ImageWidget>("imgPolaroidBack")->setVisibility(false);
|
||||
m_rewardItems->clearItems();
|
||||
}
|
||||
|
||||
m_refreshTimer--;
|
||||
if (m_refreshTimer < 0) {
|
||||
fetchData();
|
||||
m_refreshTimer = m_refreshRate;
|
||||
}
|
||||
}
|
||||
|
||||
PanePtr QuestLogInterface::createTooltip(Vec2I const& screenPosition) {
|
||||
ItemPtr item;
|
||||
if (auto child = getChildAt(screenPosition)) {
|
||||
if (auto itemSlot = as<ItemSlotWidget>(child))
|
||||
item = itemSlot->item();
|
||||
if (auto itemGrid = as<ItemGridWidget>(child))
|
||||
item = itemGrid->itemAt(screenPosition);
|
||||
}
|
||||
if (item)
|
||||
return ItemTooltipBuilder::buildItemTooltip(item, m_player);
|
||||
return {};
|
||||
}
|
||||
|
||||
WidgetPtr QuestLogInterface::getSelected() {
|
||||
auto mainQuestList = fetchChild<ListWidget>("scrollArea.verticalLayout.mainQuestList");
|
||||
if (auto selected = mainQuestList->selectedWidget())
|
||||
return selected;
|
||||
|
||||
auto sideQuestList = fetchChild<ListWidget>("scrollArea.verticalLayout.sideQuestList");
|
||||
if (auto selected = sideQuestList->selectedWidget())
|
||||
return selected;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void QuestLogInterface::setSelected(WidgetPtr selected) {
|
||||
auto mainQuestList = fetchChild<ListWidget>("scrollArea.verticalLayout.mainQuestList");
|
||||
auto mainQuestListPos = mainQuestList->itemPosition(selected);
|
||||
if (mainQuestListPos != NPos) {
|
||||
mainQuestList->setSelected(mainQuestListPos);
|
||||
return;
|
||||
}
|
||||
|
||||
auto sideQuestList = fetchChild<ListWidget>("scrollArea.verticalLayout.sideQuestList");
|
||||
auto sideQuestListPos = sideQuestList->itemPosition(selected);
|
||||
if (sideQuestListPos != NPos) {
|
||||
sideQuestList->setSelected(sideQuestListPos);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void QuestLogInterface::toggleTracking() {
|
||||
if (auto selected = getSelected()) {
|
||||
String questId = selected->data().toString();
|
||||
if (!m_manager->isCurrent(questId))
|
||||
m_manager->setAsTracked(questId);
|
||||
else
|
||||
m_manager->setAsTracked({});
|
||||
}
|
||||
}
|
||||
|
||||
void QuestLogInterface::abandon() {
|
||||
if (auto selected = getSelected()) {
|
||||
m_manager->getQuest(selected->data().toString())->abandon();
|
||||
}
|
||||
}
|
||||
|
||||
void QuestLogInterface::fetchData() {
|
||||
auto filter = fetchChild<ButtonGroupWidget>("filter")->checkedButton()->data().toString();
|
||||
if (filter.equalsIgnoreCase("inProgress"))
|
||||
showQuests(m_manager->listActiveQuests());
|
||||
else if (filter.equalsIgnoreCase("completed"))
|
||||
showQuests(m_manager->listCompletedQuests());
|
||||
else
|
||||
throw StarException(strf("Unknown quest filter '%s'", filter));
|
||||
}
|
||||
|
||||
void QuestLogInterface::showQuests(List<QuestPtr> quests) {
|
||||
auto mainQuestList = fetchChild<ListWidget>("scrollArea.verticalLayout.mainQuestList");
|
||||
auto sideQuestList = fetchChild<ListWidget>("scrollArea.verticalLayout.sideQuestList");
|
||||
|
||||
auto mainQuestHeader = fetchChild<Widget>("scrollArea.verticalLayout.mainQuestHeader");
|
||||
auto sideQuestHeader = fetchChild<Widget>("scrollArea.verticalLayout.sideQuestHeader");
|
||||
|
||||
String selectedQuest;
|
||||
if (auto selected = getSelected())
|
||||
selectedQuest = selected->data().toString();
|
||||
else if (m_manager->currentQuest())
|
||||
selectedQuest = (*m_manager->currentQuest())->questId();
|
||||
|
||||
mainQuestList->clear();
|
||||
mainQuestHeader->hide();
|
||||
sideQuestList->clear();
|
||||
sideQuestHeader->hide();
|
||||
for (auto const& quest : quests) {
|
||||
WidgetPtr entry;
|
||||
if (quest->mainQuest()) {
|
||||
entry = mainQuestList->addItem();
|
||||
mainQuestHeader->show();
|
||||
} else {
|
||||
entry = sideQuestList->addItem();
|
||||
sideQuestHeader->show();
|
||||
}
|
||||
|
||||
entry->setData(quest->questId());
|
||||
entry->fetchChild<LabelWidget>("lblQuestEntry")->setText(quest->title());
|
||||
entry->fetchChild<ImageWidget>("imgNew")->setVisibility(quest->unread());
|
||||
entry->fetchChild<ImageWidget>("imgTracked")->setVisibility(m_manager->isCurrent(quest->questId()));
|
||||
|
||||
bool currentWorld = false;
|
||||
if (auto questWorld = quest->worldId()) {
|
||||
currentWorld = m_client->playerWorld() == *questWorld;
|
||||
}
|
||||
entry->fetchChild<ImageWidget>("imgCurrent")->setVisibility(currentWorld);
|
||||
|
||||
entry->fetchChild<ImageWidget>("imgPortrait")->setDrawables(quest->portrait("QuestStarted").value({}));
|
||||
entry->show();
|
||||
if (quest->questId() == selectedQuest)
|
||||
setSelected(entry);
|
||||
}
|
||||
|
||||
auto verticalLayout = fetchChild<VerticalLayout>("scrollArea.verticalLayout");
|
||||
verticalLayout->update();
|
||||
}
|
||||
|
||||
QuestPane::QuestPane(QuestPtr const& quest, PlayerPtr player) : Pane(), m_quest(quest), m_player(move(player)) {}
|
||||
|
||||
void QuestPane::commonSetup(Json config, String bodyText, String const& portraitName) {
|
||||
GuiReader reader;
|
||||
|
||||
reader.registerCallback("close", [=](Widget*) { close(); });
|
||||
reader.registerCallback("btnDecline", [=](Widget*) { close(); });
|
||||
reader.registerCallback("btnAccept", [=](Widget*) { accept(); });
|
||||
reader.construct(config.get("paneLayout"), this);
|
||||
|
||||
if (auto titleLabel = fetchChild<LabelWidget>("lblQuestTitle"))
|
||||
titleLabel->setText(m_quest->title());
|
||||
if (auto bodyLabel = fetchChild<LabelWidget>("lblQuestBody"))
|
||||
bodyLabel->setText(bodyText);
|
||||
|
||||
if (auto portraitImage = fetchChild<ImageWidget>("portraitImage")) {
|
||||
Maybe<List<Drawable>> portrait = m_quest->portrait(portraitName);
|
||||
portraitImage->setDrawables(portrait.value({}));
|
||||
}
|
||||
|
||||
if (auto portraitTitleLabel = fetchChild<LabelWidget>("portraitTitle")) {
|
||||
Maybe<String> portraitTitle = m_quest->portraitTitle(portraitName);
|
||||
String text = m_quest->portraitTitle(portraitName).value("");
|
||||
Maybe<unsigned> charLimit = portraitTitleLabel->getTextCharLimit();
|
||||
portraitTitleLabel->setText(text);
|
||||
}
|
||||
|
||||
if (auto rewardItemsWidget = fetchChild<ItemGridWidget>("rewardItems")) {
|
||||
auto rewardItems = make_shared<ItemBag>(5);
|
||||
for (auto const& reward : m_quest->rewards())
|
||||
rewardItems->addItems(reward->clone());
|
||||
rewardItemsWidget->setItemBag(rewardItems);
|
||||
}
|
||||
|
||||
auto sound = Random::randValueFrom(config.get("onShowSound").toArray(), "").toString();
|
||||
if (!sound.empty())
|
||||
context()->playAudio(sound);
|
||||
}
|
||||
|
||||
void QuestPane::close() {
|
||||
dismiss();
|
||||
}
|
||||
|
||||
void QuestPane::accept() {
|
||||
close();
|
||||
}
|
||||
|
||||
PanePtr QuestPane::createTooltip(Vec2I const& screenPosition) {
|
||||
ItemPtr item;
|
||||
if (auto child = getChildAt(screenPosition)) {
|
||||
if (auto itemSlot = as<ItemSlotWidget>(child))
|
||||
item = itemSlot->item();
|
||||
if (auto itemGrid = as<ItemGridWidget>(child))
|
||||
item = itemGrid->itemAt(screenPosition);
|
||||
}
|
||||
if (item)
|
||||
return ItemTooltipBuilder::buildItemTooltip(item, m_player);
|
||||
return {};
|
||||
}
|
||||
|
||||
NewQuestInterface::NewQuestInterface(QuestManagerPtr const& manager, QuestPtr const& quest, PlayerPtr player)
|
||||
: QuestPane(quest, move(player)), m_manager(manager), m_declined(false) {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
List<Drawable> objectivePortrait = m_quest->portrait("Objective").value({});
|
||||
bool shortDialog = objectivePortrait.size() == 0;
|
||||
|
||||
String configFile;
|
||||
if (shortDialog)
|
||||
configFile = m_quest->getTemplate()->newQuestGuiConfig.value(assets->json("/quests/quests.config:defaultGuiConfigs.newQuest").toString());
|
||||
else
|
||||
configFile = m_quest->getTemplate()->newQuestGuiConfig.value(assets->json("/quests/quests.config:defaultGuiConfigs.newQuestPortrait").toString());
|
||||
|
||||
Json config = assets->json(configFile);
|
||||
|
||||
commonSetup(config, m_quest->text(), "QuestStarted");
|
||||
|
||||
m_declined = m_quest->canBeAbandoned();
|
||||
if (!m_declined) {
|
||||
if (auto declineButton = fetchChild<ButtonWidget>("btnDecline"))
|
||||
declineButton->disable();
|
||||
}
|
||||
|
||||
if (!shortDialog) {
|
||||
if (auto objectivePortraitImage = fetchChild<ImageWidget>("objectivePortraitImage")) {
|
||||
Drawable::scaleAll(objectivePortrait, Vec2F(-1, 1));
|
||||
objectivePortraitImage->setDrawables(objectivePortrait);
|
||||
|
||||
String objectivePortraitTitle = m_quest->portraitTitle("Objective").value("");
|
||||
auto portraitLabel = fetchChild<LabelWidget>("objectivePortraitTitle");
|
||||
portraitLabel->setText(objectivePortraitTitle);
|
||||
portraitLabel->setVisibility(objectivePortrait.size() > 0);
|
||||
|
||||
fetchChild<ImageWidget>("imgPolaroid")->setVisibility(objectivePortrait.size() > 0);
|
||||
fetchChild<ImageWidget>("imgPolaroidBack")->setVisibility(objectivePortrait.size() > 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (auto rewardItemsWidget = fetchChild<ItemGridWidget>("rewardItems"))
|
||||
rewardItemsWidget->setVisibility(m_quest->rewards().size() > 0);
|
||||
if (auto rewardsLabel = fetchChild<LabelWidget>("lblRewards"))
|
||||
rewardsLabel->setVisibility(m_quest->rewards().size() > 0);
|
||||
}
|
||||
|
||||
void NewQuestInterface::close() {
|
||||
m_declined = true;
|
||||
dismiss();
|
||||
}
|
||||
|
||||
void NewQuestInterface::accept() {
|
||||
m_declined = false;
|
||||
dismiss();
|
||||
}
|
||||
|
||||
void NewQuestInterface::dismissed() {
|
||||
QuestPane::dismissed();
|
||||
if (m_declined && m_quest->canBeAbandoned()) {
|
||||
m_manager->getQuest(m_quest->questId())->declineOffer();
|
||||
} else {
|
||||
m_manager->getQuest(m_quest->questId())->start();
|
||||
}
|
||||
}
|
||||
|
||||
QuestCompleteInterface::QuestCompleteInterface(QuestPtr const& quest, PlayerPtr player, CinematicPtr cinematic)
|
||||
: QuestPane(quest, player) {
|
||||
auto assets = Root::singleton().assets();
|
||||
String configFile = m_quest->getTemplate()->questCompleteGuiConfig.value(assets->json("/quests/quests.config:defaultGuiConfigs.questComplete").toString());
|
||||
Json config = assets->json(configFile);
|
||||
|
||||
m_player = player;
|
||||
m_cinematic = cinematic;
|
||||
|
||||
commonSetup(config, m_quest->completionText(), "QuestComplete");
|
||||
|
||||
if (auto moneyLabel = fetchChild<LabelWidget>("lblMoneyAmount"))
|
||||
moneyLabel->setText(strf("%s", m_quest->money()));
|
||||
disableScissoring();
|
||||
}
|
||||
|
||||
void QuestCompleteInterface::close() {
|
||||
auto assets = Root::singleton().assets();
|
||||
if (m_quest->completionCinema() && m_cinematic) {
|
||||
String cinema = m_quest->completionCinema()->replaceTags(
|
||||
StringMap<String>{{"species", m_player->species()}, {"gender", GenderNames.getRight(m_player->gender())}});
|
||||
m_cinematic->load(assets->fetchJson(cinema));
|
||||
}
|
||||
dismiss();
|
||||
}
|
||||
|
||||
QuestFailedInterface::QuestFailedInterface(QuestPtr const& quest, PlayerPtr player) : QuestPane(quest, move(player)) {
|
||||
auto assets = Root::singleton().assets();
|
||||
String configFile = m_quest->getTemplate()->questFailedGuiConfig.value(assets->json("/quests/quests.config:defaultGuiConfigs.questFailed").toString());
|
||||
Json config = assets->json(configFile);
|
||||
commonSetup(config, m_quest->failureText(), "QuestFailed");
|
||||
disableScissoring();
|
||||
}
|
||||
|
||||
}
|
95
source/frontend/StarQuestInterface.hpp
Normal file
95
source/frontend/StarQuestInterface.hpp
Normal file
|
@ -0,0 +1,95 @@
|
|||
#ifndef STAR_QUEST_INTERFACE_HPP
|
||||
#define STAR_QUEST_INTERFACE_HPP
|
||||
|
||||
#include "StarQuests.hpp"
|
||||
#include "StarPane.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(QuestManager);
|
||||
STAR_CLASS(Player);
|
||||
STAR_CLASS(Cinematic);
|
||||
STAR_CLASS(UniverseClient);
|
||||
STAR_CLASS(PaneManager);
|
||||
STAR_CLASS(ItemBag);
|
||||
|
||||
class QuestLogInterface : public Pane {
|
||||
public:
|
||||
QuestLogInterface(QuestManagerPtr manager, PlayerPtr player, CinematicPtr cinematic, UniverseClientPtr client);
|
||||
virtual ~QuestLogInterface() {}
|
||||
|
||||
virtual void displayed() override;
|
||||
virtual void tick() override;
|
||||
virtual PanePtr createTooltip(Vec2I const& screenPosition) override;
|
||||
|
||||
void fetchData();
|
||||
|
||||
void pollDialog(PaneManager* paneManager);
|
||||
|
||||
private:
|
||||
WidgetPtr getSelected();
|
||||
void setSelected(WidgetPtr selected);
|
||||
void toggleTracking();
|
||||
void abandon();
|
||||
void showQuests(List<QuestPtr> quests);
|
||||
|
||||
QuestManagerPtr m_manager;
|
||||
PlayerPtr m_player;
|
||||
CinematicPtr m_cinematic;
|
||||
UniverseClientPtr m_client;
|
||||
|
||||
String m_trackLabel;
|
||||
String m_untrackLabel;
|
||||
|
||||
ItemBagPtr m_rewardItems;
|
||||
int m_refreshRate;
|
||||
int m_refreshTimer;
|
||||
};
|
||||
|
||||
class QuestPane : public Pane {
|
||||
protected:
|
||||
QuestPane(QuestPtr const& quest, PlayerPtr player);
|
||||
|
||||
void commonSetup(Json config, String bodyText, String const& portraitName);
|
||||
virtual void close();
|
||||
virtual void accept();
|
||||
virtual PanePtr createTooltip(Vec2I const& screenPosition) override;
|
||||
|
||||
QuestPtr m_quest;
|
||||
PlayerPtr m_player;
|
||||
};
|
||||
|
||||
class NewQuestInterface : public QuestPane {
|
||||
public:
|
||||
NewQuestInterface(QuestManagerPtr const& manager, QuestPtr const& quest, PlayerPtr player);
|
||||
|
||||
protected:
|
||||
void close() override;
|
||||
void accept() override;
|
||||
void dismissed() override;
|
||||
|
||||
private:
|
||||
QuestManagerPtr m_manager;
|
||||
bool m_declined;
|
||||
};
|
||||
|
||||
class QuestCompleteInterface : public QuestPane {
|
||||
public:
|
||||
QuestCompleteInterface(QuestPtr const& quest, PlayerPtr player, CinematicPtr cinematic);
|
||||
|
||||
protected:
|
||||
void close() override;
|
||||
|
||||
private:
|
||||
PlayerPtr m_player;
|
||||
CinematicPtr m_cinematic;
|
||||
};
|
||||
|
||||
class QuestFailedInterface : public QuestPane {
|
||||
public:
|
||||
QuestFailedInterface(QuestPtr const& quest, PlayerPtr player);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
186
source/frontend/StarQuestTracker.cpp
Normal file
186
source/frontend/StarQuestTracker.cpp
Normal file
|
@ -0,0 +1,186 @@
|
|||
#include "StarQuestTracker.hpp"
|
||||
#include "StarMathCommon.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarImageStretchWidget.hpp"
|
||||
#include "StarProgressWidget.hpp"
|
||||
#include "StarVerticalLayout.hpp"
|
||||
#include "StarQuests.hpp"
|
||||
#include "StarLogging.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
QuestTrackerPane::QuestTrackerPane() {
|
||||
auto assets = Root::singleton().assets();
|
||||
auto config = assets->json("/interface/questtracker/questtracker.config");
|
||||
|
||||
m_progressFrameImage = config.getString("progressFrameImage");
|
||||
m_expandedProgressFrameImage = config.getString("expandedProgressFrameImage");
|
||||
|
||||
m_compassFrameImage = config.getString("compassFrameImage");
|
||||
m_expandedCompassFrameImage = config.getString("expandedCompassFrameImage");
|
||||
|
||||
m_incompleteObjectiveTemplate = config.getString("incompleteObjectiveTemplate");
|
||||
m_completeObjectiveTemplate = config.getString("completeObjectiveTemplate");
|
||||
|
||||
m_expandedFrameMinHeight = config.getInt("expandedFrameMinHeight");
|
||||
m_expandedFramePadding = config.getInt("expandedFramePadding");
|
||||
|
||||
m_compassDirection = 0;
|
||||
m_compassSpeed = 0;
|
||||
m_compassAcceleration = config.getFloat("compassAcceleration");
|
||||
m_compassFriction = config.getFloat("compassFriction");
|
||||
|
||||
GuiReader reader;
|
||||
reader.construct(config.get("paneLayout"), this);
|
||||
|
||||
m_frame = fetchChild<ImageWidget>("imgFrame");
|
||||
m_expandedFrame = fetchChild<ImageStretchWidget>("imgFrameExpanded");
|
||||
|
||||
m_compassFrame = fetchChild<ImageWidget>("imgCompassFrame");
|
||||
m_compass = fetchChild<ImageWidget>("imgCompass");
|
||||
|
||||
m_questObjectiveList = fetchChild<LabelWidget>("lblQuestObjectiveList");
|
||||
|
||||
m_progress = fetchChild<ProgressWidget>("questProgress");
|
||||
m_progressFrame = fetchChild<ImageWidget>("imgProgressFrame");
|
||||
|
||||
setExpanded(false);
|
||||
|
||||
// allow things like the compass and progress bar to be drawn outside the background
|
||||
disableScissoring();
|
||||
m_progressFrame->disableScissoring();
|
||||
}
|
||||
|
||||
bool QuestTrackerPane::sendEvent(InputEvent const& event) {
|
||||
if (Pane::sendEvent(event))
|
||||
return true;
|
||||
|
||||
if (event.is<MouseButtonDownEvent>() && event.get<MouseButtonDownEvent>().mouseButton == MouseButton::Left) {
|
||||
auto mousePos = *context()->mousePosition(event);
|
||||
if ((m_expanded && m_expandedFrame->inMember(mousePos)) ||
|
||||
(!m_expanded && m_frame->inMember(mousePos))) {
|
||||
setExpanded(!m_expanded);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void QuestTrackerPane::update() {
|
||||
if (m_currentQuest) {
|
||||
if (auto objectiveList = m_currentQuest->objectiveList()) {
|
||||
if (objectiveList->size() == 0) {
|
||||
m_questObjectiveList->hide();
|
||||
} else {
|
||||
m_questObjectiveList->show();
|
||||
if (m_expanded) {
|
||||
String listText = "";
|
||||
for (auto objective : objectiveList.value()) {
|
||||
if (objective.get(1).toBool())
|
||||
listText += m_completeObjectiveTemplate.replaceTags(StringMap<String>{{"objective", objective.get(0).toString()}});
|
||||
else
|
||||
listText += m_incompleteObjectiveTemplate.replaceTags(StringMap<String>{{"objective", objective.get(0).toString()}});
|
||||
}
|
||||
m_questObjectiveList->setText(listText);
|
||||
} else {
|
||||
String displayObjective = "";
|
||||
for (auto objective : objectiveList.value()) {
|
||||
if (displayObjective.empty() || !objective.get(1).toBool()) {
|
||||
if (objective.get(1).toBool()) {
|
||||
displayObjective = m_completeObjectiveTemplate.replaceTags(StringMap<String>{{"objective", objective.get(0).toString()}});
|
||||
} else {
|
||||
displayObjective = m_incompleteObjectiveTemplate.replaceTags(StringMap<String>{{"objective", objective.get(0).toString()}});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
m_questObjectiveList->setText(displayObjective);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m_questObjectiveList->hide();
|
||||
m_questObjectiveList->setText("");
|
||||
}
|
||||
|
||||
if (m_expanded) {
|
||||
m_expandedFrame->show();
|
||||
m_frame->hide();
|
||||
|
||||
int frameHeight = max(m_expandedFrameMinHeight, m_questObjectiveList->size()[1] + m_expandedFramePadding * 2);
|
||||
m_expandedFrame->setSize(Vec2I(m_expandedFrame->size()[0], frameHeight));
|
||||
m_expandedFrame->setPosition(Vec2I(m_expandedFrame->relativePosition()[0], -frameHeight));
|
||||
} else {
|
||||
m_frame->show();
|
||||
m_expandedFrame->hide();
|
||||
}
|
||||
|
||||
if (auto progress = m_currentQuest->progress()) {
|
||||
m_progress->show();
|
||||
m_progressFrame->show();
|
||||
|
||||
if (m_expanded) {
|
||||
m_progressFrame->setImage(m_expandedProgressFrameImage);
|
||||
|
||||
m_progress->setDrawingOffset(Vec2I(0, -m_expandedFrame->size()[1]));
|
||||
m_progressFrame->setDrawingOffset(Vec2I(0, -m_expandedFrame->size()[1]));
|
||||
} else {
|
||||
m_progressFrame->setImage(m_progressFrameImage);
|
||||
|
||||
m_progress->setDrawingOffset(Vec2I(0, -m_frame->size()[1]));
|
||||
m_progressFrame->setDrawingOffset(Vec2I(0, -m_frame->size()[1]));
|
||||
}
|
||||
|
||||
m_progress->setCurrentProgressLevel(*progress);
|
||||
} else {
|
||||
m_progress->hide();
|
||||
m_progressFrame->hide();
|
||||
}
|
||||
|
||||
if (auto compassDirection = m_currentQuest->compassDirection()) {
|
||||
m_compass->show();
|
||||
m_compassFrame->show();
|
||||
|
||||
if (m_expanded || m_currentQuest->progress())
|
||||
m_compassFrame->setImage(m_expandedCompassFrameImage);
|
||||
else
|
||||
m_compassFrame->setImage(m_compassFrameImage);
|
||||
|
||||
float compassDiff = pfmod(*compassDirection - m_compassDirection, (float)Constants::pi * 2);
|
||||
if (abs(compassDiff) < m_compassAcceleration && abs(m_compassSpeed) < m_compassAcceleration) {
|
||||
m_compassSpeed = 0;
|
||||
m_compassDirection = *compassDirection;
|
||||
} else {
|
||||
if (abs(compassDiff) > Constants::pi)
|
||||
compassDiff -= copysign(2 * Constants::pi, compassDiff);
|
||||
float diffRatio = abs(compassDiff) / Constants::pi;
|
||||
float speedChange = m_compassAcceleration * diffRatio;
|
||||
|
||||
m_compassSpeed += copysign(speedChange, compassDiff);
|
||||
m_compassSpeed -= m_compassSpeed * m_compassFriction;
|
||||
m_compassDirection += m_compassSpeed;
|
||||
}
|
||||
|
||||
m_compass->setRotation(m_compassDirection);
|
||||
} else {
|
||||
m_compass->hide();
|
||||
m_compassFrame->hide();
|
||||
}
|
||||
}
|
||||
|
||||
Pane::update();
|
||||
}
|
||||
|
||||
void QuestTrackerPane::setQuest(QuestPtr const& quest) {
|
||||
m_currentQuest = quest;
|
||||
}
|
||||
|
||||
void QuestTrackerPane::setExpanded(bool expanded) {
|
||||
m_expanded = expanded;
|
||||
}
|
||||
|
||||
}
|
60
source/frontend/StarQuestTracker.hpp
Normal file
60
source/frontend/StarQuestTracker.hpp
Normal file
|
@ -0,0 +1,60 @@
|
|||
#ifndef STAR_QUEST_TRACKER_HPP
|
||||
#define STAR_QUEST_TRACKER_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(LabelWidget);
|
||||
STAR_CLASS(ImageWidget);
|
||||
STAR_CLASS(ImageStretchWidget);
|
||||
STAR_CLASS(ProgressWidget);
|
||||
STAR_CLASS(Quest);
|
||||
|
||||
class QuestTrackerPane : public Pane {
|
||||
public:
|
||||
QuestTrackerPane();
|
||||
|
||||
bool sendEvent(InputEvent const& event) override;
|
||||
void update() override;
|
||||
|
||||
void setQuest(QuestPtr const& quest);
|
||||
|
||||
private:
|
||||
void setExpanded(bool expanded);
|
||||
|
||||
ImageWidgetPtr m_frame;
|
||||
ImageStretchWidgetPtr m_expandedFrame;
|
||||
|
||||
LabelWidgetPtr m_questObjectiveList;
|
||||
|
||||
ImageWidgetPtr m_compassFrame;
|
||||
ImageWidgetPtr m_compass;
|
||||
|
||||
ImageWidgetPtr m_progressFrame;
|
||||
ProgressWidgetPtr m_progress;
|
||||
|
||||
int m_expandedFrameMinHeight;
|
||||
int m_expandedFramePadding;
|
||||
|
||||
float m_compassDirection;
|
||||
float m_compassSpeed;
|
||||
float m_compassAcceleration;
|
||||
float m_compassFriction;
|
||||
|
||||
QuestPtr m_currentQuest;
|
||||
bool m_expanded;
|
||||
|
||||
String m_compassFrameImage;
|
||||
String m_expandedCompassFrameImage;
|
||||
|
||||
String m_progressFrameImage;
|
||||
String m_expandedProgressFrameImage;
|
||||
|
||||
String m_incompleteObjectiveTemplate;
|
||||
String m_completeObjectiveTemplate;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
167
source/frontend/StarRadioMessagePopup.cpp
Normal file
167
source/frontend/StarRadioMessagePopup.cpp
Normal file
|
@ -0,0 +1,167 @@
|
|||
#include "StarRadioMessagePopup.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarLogging.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarInterpolation.hpp"
|
||||
#include "StarLexicalCast.hpp"
|
||||
#include "StarMixer.hpp"
|
||||
#include "StarTextPainter.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
RadioMessagePopup::RadioMessagePopup() {
|
||||
auto assets = Root::singleton().assets();
|
||||
auto config = assets->json("/interface/radiomessage/radiomessage.config");
|
||||
|
||||
GuiReader reader;
|
||||
reader.construct(config.get("paneLayout"), this);
|
||||
|
||||
m_messageLabel = fetchChild<LabelWidget>("lblMessage");
|
||||
m_portraitImage = fetchChild<ImageWidget>("imgPortrait");
|
||||
|
||||
m_backgroundImage = config.getString("backgroundImage");
|
||||
|
||||
m_animateInTime = config.getFloat("animateInTime");
|
||||
m_animateInImage = config.getString("animateInImage");
|
||||
m_animateInFrames = config.getInt("animateInFrames");
|
||||
|
||||
m_animateOutTime = config.getFloat("animateOutTime");
|
||||
m_animateOutImage = config.getString("animateOutImage");
|
||||
m_animateOutFrames = config.getInt("animateOutFrames");
|
||||
|
||||
m_chatOffset = jsonToVec2I(config.get("chatOffset"));
|
||||
m_chatStartPosition = m_chatOffset;
|
||||
m_chatEndPosition = m_chatOffset;
|
||||
|
||||
m_slideTime = config.getFloat("slideTime");
|
||||
m_slideTimer = m_slideTime;
|
||||
updateAnchorOffset();
|
||||
|
||||
enterStage(PopupStage::Hidden);
|
||||
}
|
||||
|
||||
void RadioMessagePopup::update() {
|
||||
if (messageActive()) {
|
||||
if (m_stageTimer.tick())
|
||||
nextPopupStage();
|
||||
|
||||
if (m_popupStage == PopupStage::AnimateIn) {
|
||||
int frame = floor((1.0f - m_stageTimer.percent()) * m_animateInFrames);
|
||||
setBG("", strf("%s:%s", m_animateInImage, frame), "");
|
||||
} else if (m_popupStage == PopupStage::ScrollText) {
|
||||
int frame =
|
||||
int((m_stageTimer.timer / m_message.portraitSpeed) * m_message.portraitFrames) % m_message.portraitFrames;
|
||||
m_portraitImage->setImage(m_message.portraitImage.replace("<frame>", toString(frame)));
|
||||
int textLength = floor(Text::stripEscapeCodes(m_message.text).length() * (1.0f - m_stageTimer.percent()));
|
||||
m_messageLabel->setTextCharLimit(textLength);
|
||||
} else if (m_popupStage == PopupStage::Persist) {
|
||||
// you're cool, just stay cool, cool person
|
||||
} else if (m_popupStage == PopupStage::AnimateOut) {
|
||||
int frame = floor((1.0f - m_stageTimer.percent()) * m_animateOutFrames);
|
||||
setBG("", strf("%s:%s", m_animateOutImage, frame), "");
|
||||
}
|
||||
|
||||
m_slideTimer = min(m_slideTimer + WorldTimestep, m_slideTime);
|
||||
updateAnchorOffset();
|
||||
}
|
||||
|
||||
Pane::update();
|
||||
}
|
||||
|
||||
void RadioMessagePopup::dismissed() {
|
||||
if (m_chatterSound)
|
||||
m_chatterSound->stop();
|
||||
Pane::dismissed();
|
||||
}
|
||||
|
||||
bool RadioMessagePopup::messageActive() {
|
||||
return m_popupStage != PopupStage::Hidden;
|
||||
}
|
||||
|
||||
void RadioMessagePopup::setMessage(RadioMessage message) {
|
||||
m_message = message;
|
||||
|
||||
if (!message.chatterSound.empty() && message.textSpeed > 0) {
|
||||
if (m_chatterSound)
|
||||
m_chatterSound->stop();
|
||||
auto assets = Root::singleton().assets();
|
||||
m_chatterSound = make_shared<AudioInstance>(*assets->audio(message.chatterSound));
|
||||
m_chatterSound->setLoops(-1);
|
||||
}
|
||||
|
||||
enterStage(PopupStage::AnimateIn);
|
||||
|
||||
updateAnchorOffset();
|
||||
}
|
||||
|
||||
void RadioMessagePopup::setChatHeight(int chatHeight) {
|
||||
auto endPosition = m_chatOffset + Vec2I(0, chatHeight);
|
||||
if (endPosition != m_chatEndPosition) {
|
||||
m_chatStartPosition = anchorOffset();
|
||||
m_chatEndPosition = endPosition;
|
||||
m_slideTimer = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void RadioMessagePopup::interrupt() {
|
||||
if (m_popupStage != PopupStage::Hidden && m_popupStage != PopupStage::AnimateOut)
|
||||
enterStage(PopupStage::AnimateOut);
|
||||
}
|
||||
|
||||
void RadioMessagePopup::updateAnchorOffset() {
|
||||
float slideRatio = m_slideTimer / m_slideTime;
|
||||
setAnchorOffset(Vec2I(int(m_chatStartPosition[0] * (1 - slideRatio) + m_chatEndPosition[0] * slideRatio),
|
||||
int(m_chatStartPosition[1] * (1 - slideRatio) + m_chatEndPosition[1] * slideRatio)));
|
||||
}
|
||||
|
||||
void RadioMessagePopup::nextPopupStage() {
|
||||
if (m_popupStage != PopupStage::Hidden)
|
||||
enterStage((PopupStage)((int)m_popupStage + 1));
|
||||
}
|
||||
|
||||
void RadioMessagePopup::enterStage(PopupStage newStage) {
|
||||
m_popupStage = newStage;
|
||||
if (m_popupStage == PopupStage::Hidden) {
|
||||
m_portraitImage->hide();
|
||||
m_messageLabel->hide();
|
||||
setBG("", strf("%s:0", m_animateInImage), "");
|
||||
} else if (m_popupStage == PopupStage::AnimateIn) {
|
||||
m_stageTimer = GameTimer(m_animateInTime);
|
||||
m_portraitImage->hide();
|
||||
m_messageLabel->hide();
|
||||
} else if (m_popupStage == PopupStage::ScrollText) {
|
||||
if (m_message.textSpeed == 0) {
|
||||
// skip this stage if text is instant
|
||||
enterStage(PopupStage::Persist);
|
||||
} else {
|
||||
m_stageTimer = GameTimer(Text::stripEscapeCodes(m_message.text).length() / m_message.textSpeed);
|
||||
m_portraitImage->show();
|
||||
m_messageLabel->show();
|
||||
m_messageLabel->setText(m_message.text);
|
||||
m_messageLabel->setTextCharLimit(0);
|
||||
setBG(m_backgroundImage);
|
||||
if (m_chatterSound)
|
||||
GuiContext::singleton().playAudio(m_chatterSound);
|
||||
}
|
||||
} else if (m_popupStage == PopupStage::Persist) {
|
||||
m_stageTimer = GameTimer(m_message.persistTime);
|
||||
m_portraitImage->show();
|
||||
m_portraitImage->setImage(m_message.portraitImage.replace("<frame>", "0"));
|
||||
m_messageLabel->show();
|
||||
m_messageLabel->setText(m_message.text);
|
||||
m_messageLabel->setTextCharLimit({});
|
||||
setBG(m_backgroundImage);
|
||||
if (m_chatterSound)
|
||||
m_chatterSound->stop();
|
||||
} else if (m_popupStage == PopupStage::AnimateOut) {
|
||||
m_stageTimer = GameTimer(m_animateOutTime);
|
||||
m_portraitImage->hide();
|
||||
m_messageLabel->hide();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
65
source/frontend/StarRadioMessagePopup.hpp
Normal file
65
source/frontend/StarRadioMessagePopup.hpp
Normal file
|
@ -0,0 +1,65 @@
|
|||
#ifndef STAR_RADIO_MESSAGE_POPUP_HPP
|
||||
#define STAR_RADIO_MESSAGE_POPUP_HPP
|
||||
|
||||
#include "StarGameTimers.hpp"
|
||||
#include "StarPane.hpp"
|
||||
#include "StarAiTypes.hpp"
|
||||
#include "StarRadioMessageDatabase.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(LabelWidget);
|
||||
STAR_CLASS(ImageWidget);
|
||||
STAR_CLASS(AudioInstance);
|
||||
|
||||
class RadioMessagePopup : public Pane {
|
||||
public:
|
||||
RadioMessagePopup();
|
||||
|
||||
void update() override;
|
||||
void dismissed() override;
|
||||
|
||||
bool messageActive();
|
||||
|
||||
void setMessage(RadioMessage message);
|
||||
void setChatHeight(int chatHeight);
|
||||
void interrupt();
|
||||
|
||||
private:
|
||||
enum PopupStage { AnimateIn, ScrollText, Persist, AnimateOut, Hidden };
|
||||
|
||||
void updateAnchorOffset();
|
||||
void nextPopupStage();
|
||||
void enterStage(PopupStage newStage);
|
||||
|
||||
PopupStage m_popupStage;
|
||||
GameTimer m_stageTimer;
|
||||
|
||||
LabelWidgetPtr m_messageLabel;
|
||||
ImageWidgetPtr m_portraitImage;
|
||||
|
||||
RadioMessage m_message;
|
||||
|
||||
String m_backgroundImage;
|
||||
|
||||
float m_animateInTime;
|
||||
String m_animateInImage;
|
||||
int m_animateInFrames;
|
||||
|
||||
float m_animateOutTime;
|
||||
String m_animateOutImage;
|
||||
int m_animateOutFrames;
|
||||
|
||||
Vec2I m_chatOffset;
|
||||
Vec2I m_chatStartPosition;
|
||||
Vec2I m_chatEndPosition;
|
||||
|
||||
float m_slideTimer;
|
||||
float m_slideTime;
|
||||
|
||||
AudioInstancePtr m_chatterSound;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
198
source/frontend/StarScriptPane.cpp
Normal file
198
source/frontend/StarScriptPane.cpp
Normal file
|
@ -0,0 +1,198 @@
|
|||
#include "StarScriptPane.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarConfigLuaBindings.hpp"
|
||||
#include "StarPlayerLuaBindings.hpp"
|
||||
#include "StarStatusControllerLuaBindings.hpp"
|
||||
#include "StarCelestialLuaBindings.hpp"
|
||||
#include "StarLuaGameConverters.hpp"
|
||||
#include "StarWorldClient.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarUniverseClient.hpp"
|
||||
#include "StarWidgetLuaBindings.hpp"
|
||||
#include "StarCanvasWidget.hpp"
|
||||
#include "StarItemTooltip.hpp"
|
||||
#include "StarItemGridWidget.hpp"
|
||||
#include "StarSimpleTooltip.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
ScriptPane::ScriptPane(UniverseClientPtr client, Json config, EntityId sourceEntityId) {
|
||||
auto& root = Root::singleton();
|
||||
auto assets = root.assets();
|
||||
|
||||
m_client = move(client);
|
||||
|
||||
if (config.type() == Json::Type::Object && config.contains("baseConfig")) {
|
||||
auto baseConfig = assets->fetchJson(config.getString("baseConfig"));
|
||||
m_config = jsonMerge(baseConfig, config);
|
||||
} else {
|
||||
m_config = assets->fetchJson(config);
|
||||
}
|
||||
m_sourceEntityId = sourceEntityId;
|
||||
|
||||
m_reader.registerCallback("close", [this](Widget*) { dismiss(); });
|
||||
|
||||
for (auto const& callbackName : jsonToStringList(m_config.get("scriptWidgetCallbacks", JsonArray{}))) {
|
||||
m_reader.registerCallback(callbackName, [this, callbackName](Widget* widget) {
|
||||
m_script.invoke(callbackName, widget->name(), widget->data());
|
||||
});
|
||||
}
|
||||
|
||||
m_reader.construct(assets->fetchJson(m_config.get("gui")), this);
|
||||
|
||||
for (auto pair : m_config.getObject("canvasClickCallbacks", {}))
|
||||
m_canvasClickCallbacks.set(findChild<CanvasWidget>(pair.first), pair.second.toString());
|
||||
for (auto pair : m_config.getObject("canvasKeyCallbacks", {}))
|
||||
m_canvasKeyCallbacks.set(findChild<CanvasWidget>(pair.first), pair.second.toString());
|
||||
|
||||
m_script.setScripts(jsonToStringList(m_config.get("scripts", JsonArray())));
|
||||
m_script.addCallbacks("pane", makePaneCallbacks());
|
||||
m_script.addCallbacks("widget", LuaBindings::makeWidgetCallbacks(this, &m_reader));
|
||||
m_script.addCallbacks("config", LuaBindings::makeConfigCallbacks( [this](String const& name, Json const& def) {
|
||||
return m_config.query(name, def);
|
||||
}));
|
||||
m_script.addCallbacks("player", LuaBindings::makePlayerCallbacks(m_client->mainPlayer().get()));
|
||||
m_script.addCallbacks("status", LuaBindings::makeStatusControllerCallbacks(m_client->mainPlayer()->statusController()));
|
||||
m_script.addCallbacks("celestial", LuaBindings::makeCelestialCallbacks(m_client.get()));
|
||||
m_script.setUpdateDelta(m_config.getUInt("scriptDelta", 1));
|
||||
}
|
||||
|
||||
void ScriptPane::displayed() {
|
||||
Pane::displayed();
|
||||
auto world = m_client->worldClient();
|
||||
if (world && world->inWorld())
|
||||
m_script.init(world.get());
|
||||
|
||||
m_script.invoke("displayed");
|
||||
}
|
||||
|
||||
void ScriptPane::dismissed() {
|
||||
Pane::dismissed();
|
||||
m_script.invoke("dismissed");
|
||||
m_script.uninit();
|
||||
}
|
||||
|
||||
void ScriptPane::tick() {
|
||||
Pane::tick();
|
||||
|
||||
if (m_sourceEntityId != NullEntityId && !m_client->worldClient()->playerCanReachEntity(m_sourceEntityId))
|
||||
dismiss();
|
||||
|
||||
for (auto p : m_canvasClickCallbacks) {
|
||||
for (auto const& clickEvent : p.first->pullClickEvents())
|
||||
m_script.invoke(p.second, jsonFromVec2I(clickEvent.position), (uint8_t)clickEvent.button, clickEvent.buttonDown);
|
||||
}
|
||||
for (auto p : m_canvasKeyCallbacks) {
|
||||
for (auto const& keyEvent : p.first->pullKeyEvents())
|
||||
m_script.invoke(p.second, (int)keyEvent.key, keyEvent.keyDown);
|
||||
}
|
||||
|
||||
m_playingSounds.filter([](pair<String, AudioInstancePtr> const& p) {
|
||||
return p.second->finished() == false;
|
||||
});
|
||||
|
||||
m_script.update(m_script.updateDt());
|
||||
}
|
||||
|
||||
bool ScriptPane::sendEvent(InputEvent const& event) {
|
||||
// Intercept GuiClose before the canvas child so GuiClose always closes
|
||||
// ScriptPanes without having to support it in the script.
|
||||
if (context()->actions(event).contains(InterfaceAction::GuiClose)) {
|
||||
dismiss();
|
||||
return true;
|
||||
}
|
||||
|
||||
return Pane::sendEvent(event);
|
||||
}
|
||||
|
||||
PanePtr ScriptPane::createTooltip(Vec2I const& screenPosition) {
|
||||
auto result = m_script.invoke<Json>("createTooltip", screenPosition);
|
||||
if (result && !result.value().isNull()) {
|
||||
if (result->type() == Json::Type::String) {
|
||||
return SimpleTooltipBuilder::buildTooltip(result->toString());
|
||||
} else {
|
||||
PanePtr tooltip = make_shared<Pane>();
|
||||
m_reader.construct(*result, tooltip.get());
|
||||
return tooltip;
|
||||
}
|
||||
} else {
|
||||
ItemPtr item;
|
||||
if (auto child = getChildAt(screenPosition)) {
|
||||
if (auto itemSlot = as<ItemSlotWidget>(child))
|
||||
item = itemSlot->item();
|
||||
if (auto itemGrid = as<ItemGridWidget>(child))
|
||||
item = itemGrid->itemAt(screenPosition);
|
||||
}
|
||||
if (item)
|
||||
return ItemTooltipBuilder::buildItemTooltip(item, m_client->mainPlayer());
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Maybe<String> ScriptPane::cursorOverride(Vec2I const& screenPosition) {
|
||||
auto result = m_script.invoke<Maybe<String>>("cursorOverride", screenPosition);
|
||||
if (result)
|
||||
return *result;
|
||||
else
|
||||
return {};
|
||||
}
|
||||
|
||||
LuaCallbacks ScriptPane::makePaneCallbacks() {
|
||||
LuaCallbacks callbacks;
|
||||
|
||||
callbacks.registerCallback("sourceEntity", [this]() { return m_sourceEntityId; });
|
||||
callbacks.registerCallback("dismiss", [this]() { dismiss(); });
|
||||
|
||||
callbacks.registerCallback("playSound",
|
||||
[this](String const& audio, Maybe<int> loops, Maybe<float> volume) {
|
||||
auto assets = Root::singleton().assets();
|
||||
auto config = Root::singleton().configuration();
|
||||
auto audioInstance = make_shared<AudioInstance>(*assets->audio(audio));
|
||||
audioInstance->setVolume(volume.value(1.0));
|
||||
audioInstance->setLoops(loops.value(0));
|
||||
auto& guiContext = GuiContext::singleton();
|
||||
guiContext.playAudio(audioInstance);
|
||||
m_playingSounds.append({audio, move(audioInstance)});
|
||||
});
|
||||
|
||||
callbacks.registerCallback("stopAllSounds", [this](String const& audio) {
|
||||
m_playingSounds.filter([audio](pair<String, AudioInstancePtr> const& p) {
|
||||
if (p.first == audio) {
|
||||
p.second->stop();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setTitle", [this](String const& title, String const& subTitle) {
|
||||
setTitleString(title, subTitle);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setTitleIcon", [this](String const& image) {
|
||||
if (auto icon = as<ImageWidget>(titleIcon()))
|
||||
icon->setImage(image);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("addWidget", [this](Json const& newWidgetConfig, Maybe<String> const& newWidgetName) {
|
||||
String name = newWidgetName.value(strf("%d", Random::randu64()));
|
||||
WidgetPtr newWidget = m_reader.makeSingle(name, newWidgetConfig);
|
||||
this->addChild(name, newWidget);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("removeWidget", [this](String const& widgetName) {
|
||||
this->removeChild(widgetName);
|
||||
});
|
||||
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
bool ScriptPane::openWithInventory() const {
|
||||
return m_config.getBool("openWithInventory", false);
|
||||
}
|
||||
|
||||
}
|
49
source/frontend/StarScriptPane.hpp
Normal file
49
source/frontend/StarScriptPane.hpp
Normal file
|
@ -0,0 +1,49 @@
|
|||
#ifndef STAR_SCRIPT_PANE_HPP
|
||||
#define STAR_SCRIPT_PANE_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
#include "StarLuaComponents.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(CanvasWidget);
|
||||
STAR_CLASS(ScriptPane);
|
||||
STAR_CLASS(UniverseClient);
|
||||
|
||||
class ScriptPane : public Pane {
|
||||
public:
|
||||
ScriptPane(UniverseClientPtr client, Json config, EntityId sourceEntityId = NullEntityId);
|
||||
|
||||
void displayed() override;
|
||||
void dismissed() override;
|
||||
|
||||
void tick() override;
|
||||
|
||||
bool sendEvent(InputEvent const& event) override;
|
||||
|
||||
PanePtr createTooltip(Vec2I const& screenPosition) override;
|
||||
Maybe<String> cursorOverride(Vec2I const& screenPosition) override;
|
||||
|
||||
bool openWithInventory() const;
|
||||
|
||||
private:
|
||||
LuaCallbacks makePaneCallbacks();
|
||||
|
||||
UniverseClientPtr m_client;
|
||||
EntityId m_sourceEntityId;
|
||||
Json m_config;
|
||||
|
||||
GuiReader m_reader;
|
||||
|
||||
Map<CanvasWidgetPtr, String> m_canvasClickCallbacks;
|
||||
Map<CanvasWidgetPtr, String> m_canvasKeyCallbacks;
|
||||
|
||||
LuaWorldComponent<LuaUpdatableComponent<LuaBaseComponent>> m_script;
|
||||
|
||||
List<pair<String, AudioInstancePtr>> m_playingSounds;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
23
source/frontend/StarSimpleTooltip.cpp
Normal file
23
source/frontend/StarSimpleTooltip.cpp
Normal file
|
@ -0,0 +1,23 @@
|
|||
#include "StarSimpleTooltip.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarPane.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
PanePtr SimpleTooltipBuilder::buildTooltip(String const& text) {
|
||||
PanePtr tooltip = make_shared<Pane>();
|
||||
tooltip->removeAllChildren();
|
||||
GuiReader reader;
|
||||
reader.construct(Root::singleton().assets()->json("/interface/tooltips/simpletooltip.tooltip"), tooltip.get());
|
||||
tooltip->setLabel("contentLabel", text);
|
||||
|
||||
auto stretchBackground = tooltip->fetchChild<Widget>("stretchBackground");
|
||||
stretchBackground->setSize(Vec2I{tooltip->fetchChild<Widget>("contentLabel")->size()[0] + 8, stretchBackground->size()[1]});
|
||||
tooltip->setSize(stretchBackground->size());
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
}
|
16
source/frontend/StarSimpleTooltip.hpp
Normal file
16
source/frontend/StarSimpleTooltip.hpp
Normal file
|
@ -0,0 +1,16 @@
|
|||
#ifndef STAR_SIMPLE_TOOLTIP_HPP
|
||||
#define STAR_SIMPLE_TOOLTIP_HPP
|
||||
|
||||
#include "StarString.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(Pane);
|
||||
|
||||
namespace SimpleTooltipBuilder {
|
||||
PanePtr buildTooltip(String const& text);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
61
source/frontend/StarSongbookInterface.cpp
Normal file
61
source/frontend/StarSongbookInterface.cpp
Normal file
|
@ -0,0 +1,61 @@
|
|||
#include "StarSongbookInterface.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarListWidget.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarTextBoxWidget.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
SongbookInterface::SongbookInterface(PlayerPtr player) {
|
||||
m_player = move(player);
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
GuiReader reader;
|
||||
|
||||
reader.registerCallback("close", [=](Widget*) { dismiss(); });
|
||||
reader.registerCallback("btnPlay",
|
||||
[=](Widget*) {
|
||||
if (play())
|
||||
dismiss();
|
||||
});
|
||||
reader.registerCallback("group", [=](Widget*) {});
|
||||
|
||||
reader.construct(assets->json("/interface/windowconfig/songbook.config:paneLayout"), this);
|
||||
|
||||
auto songList = fetchChild<ListWidget>("songs.list");
|
||||
|
||||
StringList files = assets->scan(".abc");
|
||||
sort(files, [](String const& a, String const& b) -> bool { return b.compare(a, String::CaseInsensitive) > 0; });
|
||||
for (auto s : files) {
|
||||
auto song = s.substr(7, s.length() - (7 + 4));
|
||||
auto widget = songList->addItem();
|
||||
widget->setData(s);
|
||||
auto songName = widget->fetchChild<LabelWidget>("songName");
|
||||
songName->setText(song);
|
||||
|
||||
widget->show();
|
||||
}
|
||||
}
|
||||
|
||||
bool SongbookInterface::play() {
|
||||
auto songList = fetchChild<ListWidget>("songs.list");
|
||||
auto songWidget = songList->selectedWidget();
|
||||
if (!songWidget)
|
||||
return false;
|
||||
auto songName = songWidget->data().toString();
|
||||
auto group = fetchChild<TextBoxWidget>("group")->getText();
|
||||
|
||||
JsonObject song;
|
||||
song["resource"] = songName;
|
||||
auto buffer = Root::singleton().assets()->bytes(songName);
|
||||
song["abc"] = String(buffer->ptr(), buffer->size());
|
||||
|
||||
m_player->songbook()->play(song, group);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
24
source/frontend/StarSongbookInterface.hpp
Normal file
24
source/frontend/StarSongbookInterface.hpp
Normal file
|
@ -0,0 +1,24 @@
|
|||
#ifndef STAR_SONGBOOK_INTERFACE_HPP
|
||||
#define STAR_SONGBOOK_INTERFACE_HPP
|
||||
|
||||
#include "StarSongbook.hpp"
|
||||
#include "StarPane.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(Player);
|
||||
|
||||
STAR_CLASS(SongbookInterface);
|
||||
|
||||
class SongbookInterface : public Pane {
|
||||
public:
|
||||
SongbookInterface(PlayerPtr player);
|
||||
|
||||
private:
|
||||
PlayerPtr m_player;
|
||||
bool play();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
92
source/frontend/StarStatusPane.cpp
Normal file
92
source/frontend/StarStatusPane.cpp
Normal file
|
@ -0,0 +1,92 @@
|
|||
#include "StarStatusPane.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarUniverseClient.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarStatusEffectDatabase.hpp"
|
||||
#include "StarImageMetadataDatabase.hpp"
|
||||
#include "StarImageProcessing.hpp"
|
||||
#include "StarSimpleTooltip.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
StatusPane::StatusPane(MainInterfacePaneManager* paneManager, UniverseClientPtr client) {
|
||||
m_paneManager = paneManager;
|
||||
m_client = client;
|
||||
m_player = m_client->mainPlayer();
|
||||
|
||||
m_guiContext = GuiContext::singletonPtr();
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
GuiReader reader;
|
||||
reader.construct(assets->json("/interface/windowconfig/statuspane.config:paneLayout"), this);
|
||||
disableScissoring();
|
||||
}
|
||||
|
||||
PanePtr StatusPane::createTooltip(Vec2I const& screenPosition) {
|
||||
auto interfaceScale = m_guiContext->interfaceScale();
|
||||
for (auto const& indicator : m_statusIndicators) {
|
||||
if (indicator.screenRect.contains(Vec2F(screenPosition * interfaceScale))) {
|
||||
if (!indicator.label.empty())
|
||||
return SimpleTooltipBuilder::buildTooltip(indicator.label);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void StatusPane::renderImpl() {
|
||||
Pane::renderImpl();
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
auto interfaceScale = m_guiContext->interfaceScale();
|
||||
auto imageMetadataDatabase = Root::singleton().imageMetadataDatabase();
|
||||
|
||||
String statusIconDarkenImage = assets->json("/interface.config:statusIconDarkenImage").toString();
|
||||
|
||||
for (auto const& entry : m_statusIndicators) {
|
||||
String image = entry.icon;
|
||||
if (entry.durationPercentage) {
|
||||
int imageHeight = imageMetadataDatabase->imageSize(image)[1];
|
||||
int yOffset = -(int)(*entry.durationPercentage * imageHeight);
|
||||
image += "?" + imageOperationToString(BlendImageOperation{
|
||||
BlendImageOperation::Multiply, {statusIconDarkenImage}, Vec2I(0, yOffset)});
|
||||
}
|
||||
m_guiContext->drawQuad(image, entry.screenRect.min(), interfaceScale);
|
||||
}
|
||||
}
|
||||
|
||||
void StatusPane::update() {
|
||||
Pane::update();
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
auto interfaceScale = m_guiContext->interfaceScale();
|
||||
int roundWindowHeight = ceil(windowHeight() / interfaceScale) * interfaceScale;
|
||||
|
||||
auto imageMetadataDatabase = Root::singleton().imageMetadataDatabase();
|
||||
auto statusEffectDatabase = Root::singleton().statusEffectDatabase();
|
||||
|
||||
Vec2I statusIconOffset = jsonToVec2I(assets->json("/interface.config:statusIconPos"));
|
||||
Vec2I statusIconPos = Vec2I(statusIconOffset[0] * interfaceScale, roundWindowHeight - statusIconOffset[1] * interfaceScale);
|
||||
Vec2I statusIconShift = jsonToVec2I(assets->json("/interface.config:statusIconShift")) * interfaceScale;
|
||||
|
||||
RectF boundRect = RectF::null();
|
||||
|
||||
m_statusIndicators.clear();
|
||||
for (auto const& pair : m_player->activeUniqueStatusEffectSummary()) {
|
||||
auto effectConfig = statusEffectDatabase->uniqueEffectConfig(pair.first);
|
||||
if (effectConfig.icon) {
|
||||
RectF rect = RectF::withSize(Vec2F(statusIconPos), Vec2F(imageMetadataDatabase->imageSize(*effectConfig.icon)) * interfaceScale);
|
||||
boundRect.combine(rect);
|
||||
m_statusIndicators.append(StatusEffectIndicator{*effectConfig.icon, pair.second, effectConfig.label, rect});
|
||||
statusIconPos += statusIconShift;
|
||||
}
|
||||
}
|
||||
|
||||
setPosition(Vec2I::round(boundRect.min() / interfaceScale));
|
||||
setSize(Vec2I::round(boundRect.size() / interfaceScale));
|
||||
}
|
||||
|
||||
}
|
41
source/frontend/StarStatusPane.hpp
Normal file
41
source/frontend/StarStatusPane.hpp
Normal file
|
@ -0,0 +1,41 @@
|
|||
#ifndef STAR_STATUSPANE_HPP
|
||||
#define STAR_STATUSPANE_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
#include "StarMainInterfaceTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(Player);
|
||||
STAR_CLASS(UniverseClient);
|
||||
STAR_CLASS(StatusPane);
|
||||
|
||||
class StatusPane : public Pane {
|
||||
public:
|
||||
StatusPane(MainInterfacePaneManager* paneManager, UniverseClientPtr client);
|
||||
|
||||
virtual PanePtr createTooltip(Vec2I const& screenPosition) override;
|
||||
|
||||
protected:
|
||||
virtual void renderImpl() override;
|
||||
virtual void update() override;
|
||||
|
||||
private:
|
||||
struct StatusEffectIndicator {
|
||||
String icon;
|
||||
Maybe<float> durationPercentage;
|
||||
String label;
|
||||
RectF screenRect;
|
||||
};
|
||||
|
||||
MainInterfacePaneManager* m_paneManager;
|
||||
UniverseClientPtr m_client;
|
||||
PlayerPtr m_player;
|
||||
|
||||
GuiContext* m_guiContext;
|
||||
List<StatusEffectIndicator> m_statusIndicators;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
393
source/frontend/StarTeamBar.cpp
Normal file
393
source/frontend/StarTeamBar.cpp
Normal file
|
@ -0,0 +1,393 @@
|
|||
#include "StarTeamBar.hpp"
|
||||
#include "StarMainInterface.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarUniverseClient.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarTeamClient.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarProgressWidget.hpp"
|
||||
#include "StarTextBoxWidget.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarWorldClient.hpp"
|
||||
#include "StarPortraitWidget.hpp"
|
||||
#include "StarMathCommon.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
TeamBar::TeamBar(MainInterface* mainInterface, UniverseClientPtr client) {
|
||||
m_mainInterface = mainInterface;
|
||||
m_client = client;
|
||||
|
||||
m_guiContext = GuiContext::singletonPtr();
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
m_teamInvite = make_shared<TeamInvite>(this);
|
||||
m_teamInvitation = make_shared<TeamInvitation>(this);
|
||||
m_teamMemberMenu = make_shared<TeamMemberMenu>(this);
|
||||
|
||||
m_nameFontSize = assets->json("/interface.config:font.nameSize").toInt();
|
||||
m_nameOffset = jsonToVec2F(assets->json("/interface.config:nameOffset"));
|
||||
|
||||
GuiReader reader;
|
||||
|
||||
reader.registerCallback("inviteButton", [this](Widget*) { inviteButton(); });
|
||||
reader.registerCallback("showSelfMenu", [this](Widget*) {
|
||||
if (!m_client->teamClient()->isMemberOfTeam())
|
||||
return;
|
||||
auto position = jsonToVec2I(Root::singleton().assets()->json("/interface/windowconfig/teambar.config:selfMenuOffset"));
|
||||
position[1] += windowHeight() / m_guiContext->interfaceScale();
|
||||
showMemberMenu(m_client->mainPlayer()->uuid(), position);
|
||||
});
|
||||
|
||||
reader.construct(assets->json("/interface/windowconfig/teambar.config:paneLayout"), this);
|
||||
|
||||
m_healthBar = fetchChild<ProgressWidget>("healthBar");
|
||||
m_energyBar = fetchChild<ProgressWidget>("energyBar");
|
||||
m_foodBar = fetchChild<ProgressWidget>("foodBar");
|
||||
|
||||
m_energyBarColor = jsonToColor(assets->json("/interface/windowconfig/teambar.config:energyBarColor"));
|
||||
m_energyBarRegenMixColor = jsonToColor(assets->json("/interface/windowconfig/teambar.config:energyBarRegenMixColor"));
|
||||
m_energyBarUnusableColor = jsonToColor(assets->json("/interface/windowconfig/teambar.config:energyBarUnusableColor"));
|
||||
|
||||
m_energyBar->setColor(m_energyBarColor);
|
||||
|
||||
auto playerPortrait = fetchChild<PortraitWidget>("portrait");
|
||||
playerPortrait->setEntity(as<PortraitEntity>(m_client->mainPlayer()));
|
||||
|
||||
fetchChild<LabelWidget>("name")->setText(m_client->mainPlayer()->name());
|
||||
|
||||
disableScissoring();
|
||||
}
|
||||
|
||||
bool TeamBar::sendEvent(InputEvent const& event) {
|
||||
if (event.is<MouseButtonDownEvent>()
|
||||
&& (event.get<MouseButtonDownEvent>().mouseButton == MouseButton::Left || event.get<MouseButtonDownEvent>().mouseButton == MouseButton::Right)) {
|
||||
if (m_teamMemberMenu->isDisplayed() && !m_teamMemberMenu->inMember(*context()->mousePosition(event)))
|
||||
m_teamMemberMenu->dismiss();
|
||||
}
|
||||
return Pane::sendEvent(event);
|
||||
}
|
||||
|
||||
void TeamBar::invitePlayer(String const& playerName) {
|
||||
m_client->teamClient()->invitePlayer(playerName);
|
||||
}
|
||||
|
||||
void TeamBar::acceptInvitation(Uuid const& inviterUuid) {
|
||||
m_client->teamClient()->acceptInvitation(inviterUuid);
|
||||
}
|
||||
|
||||
void TeamBar::update() {
|
||||
Pane::update();
|
||||
|
||||
updatePlayerResources();
|
||||
|
||||
auto teamClient = m_client->teamClient();
|
||||
|
||||
if (!m_teamInvitation->active()) {
|
||||
if (teamClient->hasInvitationPending()) {
|
||||
auto invitation = teamClient->pullInvitation();
|
||||
m_teamInvitation->open(invitation.first, invitation.second);
|
||||
if (!m_teamInvitation->isDisplayed())
|
||||
m_mainInterface->paneManager()->displayPane(PaneLayer::Window, m_teamInvitation);
|
||||
}
|
||||
}
|
||||
|
||||
if (teamClient->currentTeam() && !teamClient->isTeamLeader())
|
||||
m_teamInvite->dismiss();
|
||||
|
||||
fetchChild<ImageWidget>("leader")->setVisibility(teamClient->isTeamLeader());
|
||||
|
||||
buildTeamBar();
|
||||
}
|
||||
|
||||
void TeamBar::updatePlayerResources() {
|
||||
auto player = m_client->mainPlayer();
|
||||
|
||||
m_healthBar->setCurrentProgressLevel(player->healthPercentage());
|
||||
m_energyBar->setCurrentProgressLevel(player->energyPercentage());
|
||||
|
||||
if (player->modeConfig().hunger) {
|
||||
m_foodBar->setCurrentProgressLevel(player->foodPercentage());
|
||||
auto assets = Root::singleton().assets();
|
||||
if (player->foodPercentage() <= assets->json("/player.config:foodLowThreshold").toFloat()) {
|
||||
float flashTime = assets->json("/interface/windowconfig/teambar.config:foodBarFlashTime").toFloat();
|
||||
if (fmod(Time::monotonicTime(), flashTime * 2) < flashTime)
|
||||
m_foodBar->setOverlay(assets->json("/interface/windowconfig/teambar.config:foodBarFlashOverlay").toString());
|
||||
else
|
||||
m_foodBar->setOverlay("");
|
||||
} else {
|
||||
m_foodBar->setOverlay("");
|
||||
}
|
||||
} else {
|
||||
m_foodBar->hide();
|
||||
}
|
||||
|
||||
if (player->energyLocked()) {
|
||||
m_energyBar->setColor(m_energyBarUnusableColor);
|
||||
} else {
|
||||
m_energyBar->setColor(m_energyBarColor.mix(m_energyBarRegenMixColor, player->energyRegenBlockPercent()));
|
||||
}
|
||||
}
|
||||
|
||||
void TeamBar::inviteButton() {
|
||||
if (!m_teamInvite->isDisplayed())
|
||||
m_mainInterface->paneManager()->displayPane(PaneLayer::Window, m_teamInvite);
|
||||
}
|
||||
|
||||
void TeamBar::buildTeamBar() {
|
||||
auto teamClient = m_client->teamClient();
|
||||
auto player = m_client->mainPlayer();
|
||||
|
||||
auto list = fetchChild("list");
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
Vec2I offset;
|
||||
size_t controlIndex = 0;
|
||||
size_t memberIndex = 0;
|
||||
|
||||
float portraitScale = assets->json("/interface/windowconfig/teambar.config:memberPortraitScale").toFloat();
|
||||
int memberSize = assets->json("/interface/windowconfig/teambar.config:memberSize").toInt();
|
||||
int memberSpacing = assets->json("/interface/windowconfig/teambar.config:memberSpacing").toInt();
|
||||
|
||||
for (auto member : teamClient->members()) {
|
||||
if (member.uuid == player->uuid()) {
|
||||
memberIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
String cellName = strf("%s", controlIndex);
|
||||
WidgetPtr cell = list->fetchChild(cellName);
|
||||
|
||||
if (!cell) {
|
||||
GuiReader reader;
|
||||
cell = make_shared<Widget>();
|
||||
cell->disableScissoring();
|
||||
cell->markAsContainer();
|
||||
|
||||
reader.registerCallback("showMemberMenu", [this](Widget* widget) {
|
||||
auto position = widget->screenPosition() + jsonToVec2I(Root::singleton().assets()->json("/interface/windowconfig/teambar.config:memberMenuOffset"));
|
||||
showMemberMenu(Uuid(widget->parent()->data().toString()), position);
|
||||
});
|
||||
|
||||
reader.construct(assets->json("/interface/windowconfig/teambar.config:entry"), cell.get());
|
||||
|
||||
list->addChild(cellName, cell);
|
||||
}
|
||||
|
||||
offset[1] -= memberSize;
|
||||
cell->setPosition(offset);
|
||||
|
||||
cell->setData(member.uuid.hex());
|
||||
|
||||
cell->show();
|
||||
|
||||
if (!teamClient->isTeamLeader(member.uuid))
|
||||
cell->fetchChild<ImageWidget>("leader")->hide();
|
||||
else
|
||||
cell->fetchChild<ImageWidget>("leader")->show();
|
||||
|
||||
List<Drawable> drawables = member.portrait;
|
||||
Drawable::scaleAll(drawables, portraitScale);
|
||||
cell->fetchChild<ImageWidget>("portrait")->setDrawables(move(drawables));
|
||||
|
||||
if (member.world == m_client->playerWorld() && m_client->worldClient()) {
|
||||
auto mpos = member.position;
|
||||
if (auto entity = m_client->worldClient()->entity(member.entity))
|
||||
mpos = entity->position();
|
||||
auto direction = m_client->worldClient()->geometry().diff(mpos, player->position());
|
||||
|
||||
auto compassImage = cell->fetchChild<ImageWidget>("compass");
|
||||
compassImage->setRotation(direction.angle() - Constants::pi / 2.0f);
|
||||
compassImage->show();
|
||||
cell->fetchChild<ImageWidget>("compassoffworld")->hide();
|
||||
} else {
|
||||
cell->fetchChild<ImageWidget>("compass")->hide();
|
||||
cell->fetchChild<ImageWidget>("compassoffworld")->show();
|
||||
}
|
||||
|
||||
cell->fetchChild<ProgressWidget>("healthBar")->setCurrentProgressLevel(member.healthPercentage);
|
||||
cell->fetchChild<ProgressWidget>("energyBar")->setCurrentProgressLevel(member.energyPercentage);
|
||||
|
||||
offset[1] -= memberSpacing;
|
||||
controlIndex++;
|
||||
memberIndex++;
|
||||
}
|
||||
|
||||
auto inviteButton = fetchChild<ButtonWidget>("inviteButton");
|
||||
auto noInviteImage = fetchChild<ImageWidget>("noInviteImage");
|
||||
|
||||
Vec2I inviteOffset = list->position() + offset;
|
||||
inviteButton->setPosition(inviteOffset - Vec2I{0, inviteButton->size()[1]});
|
||||
noInviteImage->setPosition(inviteOffset - Vec2I{0, noInviteImage->size()[1]});
|
||||
|
||||
bool couldInvite = (!teamClient->currentTeam() || teamClient->isTeamLeader())
|
||||
&& m_client->teamClient()->members().size() < Root::singleton().configuration()->get("maxTeamSize").toUInt();
|
||||
inviteButton->setVisibility(couldInvite);
|
||||
inviteButton->setEnabled(!m_teamInvitation->active());
|
||||
noInviteImage->setVisibility(!couldInvite);
|
||||
|
||||
while (true) {
|
||||
String cellName = strf("%s", controlIndex);
|
||||
WidgetPtr cell = list->fetchChild(cellName);
|
||||
if (!cell)
|
||||
break;
|
||||
cell->hide();
|
||||
controlIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
void TeamBar::showMemberMenu(Uuid memberUuid, Vec2I position) {
|
||||
m_teamMemberMenu->open(memberUuid, position);
|
||||
if (!m_teamMemberMenu->isDisplayed())
|
||||
m_mainInterface->paneManager()->displayPane(PaneLayer::Window, m_teamMemberMenu);
|
||||
}
|
||||
|
||||
TeamInvite::TeamInvite(TeamBar* owner) {
|
||||
m_owner = owner;
|
||||
GuiReader reader;
|
||||
auto assets = Root::singleton().assets();
|
||||
reader.registerCallback("ok", [this](Widget*) { ok(); });
|
||||
reader.registerCallback("close", [this](Widget*) { close(); });
|
||||
reader.registerCallback("name", [](Widget*) {});
|
||||
reader.construct(assets->json("/interface/windowconfig/teaminvite.config:paneLayout"), this);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
void TeamInvite::show() {
|
||||
Pane::show();
|
||||
fetchChild<TextBoxWidget>("name")->setText("", false);
|
||||
fetchChild<TextBoxWidget>("name")->focus();
|
||||
}
|
||||
|
||||
void TeamInvite::ok() {
|
||||
m_owner->invitePlayer(fetchChild<TextBoxWidget>("name")->getText());
|
||||
dismiss();
|
||||
}
|
||||
|
||||
void TeamInvite::close() {
|
||||
dismiss();
|
||||
}
|
||||
|
||||
TeamInvitation::TeamInvitation(TeamBar* owner) {
|
||||
m_owner = owner;
|
||||
GuiReader reader;
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
reader.registerCallback("ok", [this](Widget*) { ok(); });
|
||||
reader.registerCallback("close", [this](Widget*) { close(); });
|
||||
|
||||
reader.construct(assets->json("/interface/windowconfig/teaminvitation.config:paneLayout"), this);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
void TeamInvitation::open(Uuid const& inviterUuid, String const& inviterName) {
|
||||
if (active())
|
||||
return;
|
||||
m_inviterUuid = inviterUuid;
|
||||
fetchChild<LabelWidget>("inviterName")->setText(inviterName);
|
||||
Pane::show();
|
||||
}
|
||||
|
||||
void TeamInvitation::ok() {
|
||||
m_owner->acceptInvitation(m_inviterUuid);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
void TeamInvitation::close() {
|
||||
dismiss();
|
||||
}
|
||||
|
||||
TeamMemberMenu::TeamMemberMenu(TeamBar* owner) {
|
||||
m_owner = owner;
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
GuiReader reader;
|
||||
reader.registerCallback("beamToShip", [this](Widget*) { beamToShip(); });
|
||||
reader.registerCallback("close", [this](Widget*) { close(); });
|
||||
reader.registerCallback("makeLeader", [this](Widget*) { makeLeader(); });
|
||||
reader.registerCallback("removeFromTeam", [this](Widget*) { removeFromTeam(); });
|
||||
reader.construct(assets->json("/interface/windowconfig/teammembermenu.config:paneLayout"), this);
|
||||
}
|
||||
|
||||
void TeamMemberMenu::open(Uuid memberUuid, Vec2I position) {
|
||||
if (active())
|
||||
return;
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
setPosition(position);
|
||||
|
||||
m_memberUuid = memberUuid;
|
||||
auto members = m_owner->m_client->teamClient()->members();
|
||||
for (auto member : members) {
|
||||
if (member.uuid == m_memberUuid) {
|
||||
fetchChild<LabelWidget>("name")->setText(member.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
updateWidgets();
|
||||
|
||||
Pane::show();
|
||||
}
|
||||
|
||||
void TeamMemberMenu::update() {
|
||||
auto stillValid = false;
|
||||
auto members = m_owner->m_client->teamClient()->members();
|
||||
for (auto member : members) {
|
||||
if (member.uuid == m_memberUuid) {
|
||||
stillValid = true;
|
||||
m_canBeam = member.warpMode != WarpMode::None && m_owner->m_client->canBeamToTeamShip();
|
||||
}
|
||||
}
|
||||
|
||||
if (!stillValid) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
updateWidgets();
|
||||
|
||||
Pane::update();
|
||||
}
|
||||
|
||||
void TeamMemberMenu::updateWidgets() {
|
||||
bool isLeader = m_owner->m_client->teamClient()->isTeamLeader();
|
||||
bool isSelf = m_owner->m_client->mainPlayer()->uuid() == m_memberUuid;
|
||||
|
||||
fetchChild<ButtonWidget>("beamToShip")->setEnabled(m_canBeam);
|
||||
fetchChild<ButtonWidget>("makeLeader")->setEnabled(isLeader && !isSelf);
|
||||
fetchChild<ButtonWidget>("removeFromTeam")->setEnabled(isLeader || isSelf);
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
if (isSelf)
|
||||
fetchChild<ButtonWidget>("removeFromTeam")->setText(assets->json("/interface/windowconfig/teammembermenu.config:removeSelfText").toString());
|
||||
else
|
||||
fetchChild<ButtonWidget>("removeFromTeam")->setText(assets->json("/interface/windowconfig/teammembermenu.config:removeOtherText").toString());
|
||||
}
|
||||
|
||||
void TeamMemberMenu::beamToShip() {
|
||||
if (m_canBeam)
|
||||
m_owner->m_mainInterface->warpTo(WarpToWorld{ClientShipWorldId(m_memberUuid), {}});
|
||||
dismiss();
|
||||
}
|
||||
|
||||
void TeamMemberMenu::close() {
|
||||
dismiss();
|
||||
}
|
||||
|
||||
void TeamMemberMenu::makeLeader() {
|
||||
m_owner->m_client->teamClient()->makeLeader(m_memberUuid);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
void TeamMemberMenu::removeFromTeam() {
|
||||
m_owner->m_client->teamClient()->removeFromTeam(m_memberUuid);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
}
|
115
source/frontend/StarTeamBar.hpp
Normal file
115
source/frontend/StarTeamBar.hpp
Normal file
|
@ -0,0 +1,115 @@
|
|||
#ifndef STAR_TEAMBAR_HPP
|
||||
#define STAR_TEAMBAR_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
#include "StarUuid.hpp"
|
||||
#include "StarMainInterfaceTypes.hpp"
|
||||
#include "StarProgressWidget.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(TeamBar);
|
||||
STAR_CLASS(MainInterface);
|
||||
STAR_CLASS(UniverseClient);
|
||||
STAR_CLASS(Player);
|
||||
|
||||
STAR_CLASS(TeamInvite);
|
||||
STAR_CLASS(TeamInvitation);
|
||||
STAR_CLASS(TeamMemberMenu);
|
||||
STAR_CLASS(TeamBar);
|
||||
|
||||
class TeamInvite : public Pane {
|
||||
public:
|
||||
TeamInvite(TeamBar* owner);
|
||||
|
||||
virtual void show() override;
|
||||
|
||||
private:
|
||||
TeamBar* m_owner;
|
||||
|
||||
void ok();
|
||||
void close();
|
||||
};
|
||||
|
||||
class TeamInvitation : public Pane {
|
||||
public:
|
||||
TeamInvitation(TeamBar* owner);
|
||||
|
||||
void open(Uuid const& inviterUuid, String const& inviterName);
|
||||
|
||||
private:
|
||||
TeamBar* m_owner;
|
||||
Uuid m_inviterUuid;
|
||||
|
||||
void ok();
|
||||
void close();
|
||||
};
|
||||
|
||||
class TeamMemberMenu : public Pane {
|
||||
public:
|
||||
TeamMemberMenu(TeamBar* owner);
|
||||
|
||||
void open(Uuid memberUuid, Vec2I position);
|
||||
|
||||
virtual void update() override;
|
||||
|
||||
private:
|
||||
void updateWidgets();
|
||||
|
||||
void close();
|
||||
void beamToShip();
|
||||
void makeLeader();
|
||||
void removeFromTeam();
|
||||
|
||||
TeamBar* m_owner;
|
||||
Uuid m_memberUuid;
|
||||
bool m_canBeam;
|
||||
};
|
||||
|
||||
class TeamBar : public Pane {
|
||||
public:
|
||||
TeamBar(MainInterface* mainInterface, UniverseClientPtr client);
|
||||
|
||||
bool sendEvent(InputEvent const& event) override;
|
||||
|
||||
void invitePlayer(String const& playerName);
|
||||
void acceptInvitation(Uuid const& inviterUuid);
|
||||
|
||||
protected:
|
||||
virtual void update() override;
|
||||
|
||||
private:
|
||||
void updatePlayerResources();
|
||||
|
||||
void inviteButton();
|
||||
|
||||
void buildTeamBar();
|
||||
|
||||
void showMemberMenu(Uuid memberUuid, Vec2I position);
|
||||
|
||||
MainInterface* m_mainInterface;
|
||||
UniverseClientPtr m_client;
|
||||
|
||||
GuiContext* m_guiContext;
|
||||
|
||||
int m_nameFontSize;
|
||||
Vec2F m_nameOffset;
|
||||
|
||||
TeamInvitePtr m_teamInvite;
|
||||
TeamInvitationPtr m_teamInvitation;
|
||||
TeamMemberMenuPtr m_teamMemberMenu;
|
||||
|
||||
ProgressWidgetPtr m_healthBar;
|
||||
ProgressWidgetPtr m_energyBar;
|
||||
ProgressWidgetPtr m_foodBar;
|
||||
|
||||
Color m_energyBarColor;
|
||||
Color m_energyBarRegenMixColor;
|
||||
Color m_energyBarUnusableColor;
|
||||
|
||||
friend class TeamMemberMenu;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
173
source/frontend/StarTeleportDialog.cpp
Normal file
173
source/frontend/StarTeleportDialog.cpp
Normal file
|
@ -0,0 +1,173 @@
|
|||
#include "StarTeleportDialog.hpp"
|
||||
#include "StarWorldClient.hpp"
|
||||
#include "StarUniverseClient.hpp"
|
||||
#include "StarClientContext.hpp"
|
||||
#include "StarCelestialDatabase.hpp"
|
||||
#include "StarTeamClient.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarQuestManager.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarPaneManager.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarImageWidget.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarListWidget.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
TeleportDialog::TeleportDialog(UniverseClientPtr client,
|
||||
PaneManager* paneManager,
|
||||
Json config,
|
||||
EntityId sourceEntityId,
|
||||
TeleportBookmark currentLocation) {
|
||||
m_client = client;
|
||||
m_paneManager = paneManager;
|
||||
m_sourceEntityId = sourceEntityId;
|
||||
m_currentLocation = currentLocation;
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
GuiReader reader;
|
||||
|
||||
reader.registerCallback("dismiss", bind(&Pane::dismiss, this));
|
||||
reader.registerCallback("teleport", bind(&TeleportDialog::teleport, this));
|
||||
reader.registerCallback("selectDestination", bind(&TeleportDialog::selectDestination, this));
|
||||
|
||||
reader.construct(assets->json("/interface/windowconfig/teleportdialog.config:paneLayout"), this);
|
||||
|
||||
config = assets->fetchJson(config);
|
||||
auto destList = fetchChild<ListWidget>("bookmarkList.bookmarkItemList");
|
||||
destList->registerMemberCallback("editBookmark", bind(&TeleportDialog::editBookmark, this));
|
||||
|
||||
for (auto dest : config.getArray("destinations", JsonArray())) {
|
||||
if (auto prerequisite = dest.optString("prerequisiteQuest")) {
|
||||
if (!m_client->mainPlayer()->questManager()->hasCompleted(*prerequisite))
|
||||
continue;
|
||||
}
|
||||
|
||||
auto warpAction = parseWarpAction(dest.getString("warpAction"));
|
||||
bool deploy = dest.getBool("deploy", false);
|
||||
if (warpAction == WarpAlias::OrbitedWorld && !m_client->canBeamDown(deploy))
|
||||
continue;
|
||||
|
||||
auto entry = destList->addItem();
|
||||
entry->fetchChild<LabelWidget>("name")->setText(dest.getString("name"));
|
||||
entry->fetchChild<LabelWidget>("planetName")->setText(dest.getString("planetName", ""));
|
||||
if (dest.contains("icon"))
|
||||
entry->fetchChild<ImageWidget>("icon")->setImage(
|
||||
strf("/interface/bookmarks/icons/%s.png", dest.getString("icon")));
|
||||
entry->fetchChild<ButtonWidget>("editButton")->hide();
|
||||
|
||||
if (dest.getBool("mission", false)) {
|
||||
// if the warpaction is for an instance world, set the uuid to the team uuid
|
||||
if (auto warpToWorld = warpAction.ptr<WarpToWorld>()) {
|
||||
if (auto worldId = warpToWorld->world.ptr<InstanceWorldId>())
|
||||
warpAction = WarpToWorld(InstanceWorldId(worldId->instance, m_client->teamUuid(), worldId->level), warpToWorld->target);
|
||||
}
|
||||
}
|
||||
|
||||
m_destinations.append({warpAction, deploy});
|
||||
}
|
||||
|
||||
String beamPartyMember = assets->json("/interface/windowconfig/teleportdialog.config:beamPartyMemberLabel").toString();
|
||||
String deployPartyMember = assets->json("/interface/windowconfig/teleportdialog.config:deployPartyMemberLabel").toString();
|
||||
String beamPartyMemberIcon = assets->json("/interface/windowconfig/teleportdialog.config:beamPartyMemberIcon").toString();
|
||||
String deployPartyMemberIcon = assets->json("/interface/windowconfig/teleportdialog.config:deployPartyMemberIcon").toString();
|
||||
|
||||
if (config.getBool("includePartyMembers", false)) {
|
||||
auto teamClient = m_client->teamClient();
|
||||
for (auto member : teamClient->members()) {
|
||||
if (member.uuid == m_client->mainPlayer()->uuid() || member.warpMode == WarpMode::None)
|
||||
continue;
|
||||
|
||||
auto entry = destList->addItem();
|
||||
entry->fetchChild<LabelWidget>("name")->setText(member.name);
|
||||
|
||||
if (member.warpMode == WarpMode::DeployOnly)
|
||||
entry->fetchChild<LabelWidget>("planetName")->setText(deployPartyMember);
|
||||
else
|
||||
entry->fetchChild<LabelWidget>("planetName")->setText(beamPartyMember);
|
||||
|
||||
if (member.warpMode == WarpMode::DeployOnly)
|
||||
entry->fetchChild<ImageWidget>("icon")->setImage(deployPartyMemberIcon);
|
||||
else
|
||||
entry->fetchChild<ImageWidget>("icon")->setImage(beamPartyMemberIcon);
|
||||
|
||||
entry->fetchChild<ButtonWidget>("editButton")->hide();
|
||||
|
||||
m_destinations.append({WarpToPlayer(member.uuid), member.warpMode == WarpMode::DeployOnly});
|
||||
}
|
||||
}
|
||||
|
||||
if (config.getBool("includePlayerBookmarks", false)) {
|
||||
auto teleportBookmarks = m_client->mainPlayer()->universeMap()->teleportBookmarks();
|
||||
|
||||
teleportBookmarks.sort([](auto const& a, auto const& b) { return a.bookmarkName.toLower() < b.bookmarkName.toLower(); });
|
||||
|
||||
for (auto bookmark : teleportBookmarks) {
|
||||
auto entry = destList->addItem();
|
||||
setupBookmarkEntry(entry, bookmark);
|
||||
if (bookmark == m_currentLocation) {
|
||||
destList->setEnabled(destList->itemPosition(entry), false);
|
||||
entry->fetchChild<ButtonWidget>("editButton")->setEnabled(false);
|
||||
}
|
||||
m_destinations.append({WarpToWorld(bookmark.target.first, bookmark.target.second), false});
|
||||
}
|
||||
}
|
||||
|
||||
fetchChild<ButtonWidget>("btnTeleport")->setEnabled(destList->selectedItem() != NPos);
|
||||
}
|
||||
|
||||
void TeleportDialog::tick() {
|
||||
if (!m_client->worldClient()->playerCanReachEntity(m_sourceEntityId))
|
||||
dismiss();
|
||||
}
|
||||
|
||||
void TeleportDialog::selectDestination() {
|
||||
auto destList = fetchChild<ListWidget>("bookmarkList.bookmarkItemList");
|
||||
fetchChild<ButtonWidget>("btnTeleport")->setEnabled(destList->selectedItem() != NPos);
|
||||
}
|
||||
|
||||
void TeleportDialog::teleport() {
|
||||
auto destList = fetchChild<ListWidget>("bookmarkList.bookmarkItemList");
|
||||
if (destList->selectedItem() != NPos) {
|
||||
auto& destination = m_destinations[destList->selectedItem()];
|
||||
auto warpAction = destination.first;
|
||||
bool deploy = destination.second;
|
||||
|
||||
auto warp = [this, deploy](WarpAction const& action, String const& animation = "default") {
|
||||
if (deploy)
|
||||
m_client->warpPlayer(action, true, "deploy", true);
|
||||
else
|
||||
m_client->warpPlayer(action, true, animation);
|
||||
};
|
||||
|
||||
m_client->worldClient()->sendEntityMessage(m_sourceEntityId, "onTeleport", {printWarpAction(warpAction)});
|
||||
if (warpAction.is<WarpAlias>() && warpAction.get<WarpAlias>() == WarpAlias::OrbitedWorld) {
|
||||
warp(take(destination).first, "beam");
|
||||
} else {
|
||||
warp(take(destination).first);
|
||||
}
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
void TeleportDialog::editBookmark() {
|
||||
auto destList = fetchChild<ListWidget>("bookmarkList.bookmarkItemList");
|
||||
if (destList->selectedItem() != NPos) {
|
||||
size_t selectedItem = destList->selectedItem();
|
||||
auto bookmarks = m_client->mainPlayer()->universeMap()->teleportBookmarks();
|
||||
bookmarks.sort([](auto const& a, auto const& b) { return a.bookmarkName.toLower() < b.bookmarkName.toLower(); });
|
||||
selectedItem = selectedItem - (m_destinations.size() - bookmarks.size());
|
||||
if (bookmarks.size() > selectedItem) {
|
||||
auto editBookmarkDialog = make_shared<EditBookmarkDialog>(m_client->mainPlayer()->universeMap());
|
||||
editBookmarkDialog->setBookmark(bookmarks[selectedItem]);
|
||||
m_paneManager->displayPane(PaneLayer::ModalWindow, editBookmarkDialog);
|
||||
}
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
38
source/frontend/StarTeleportDialog.hpp
Normal file
38
source/frontend/StarTeleportDialog.hpp
Normal file
|
@ -0,0 +1,38 @@
|
|||
#ifndef STAR_TELEPORTER_DIALOG_HPP
|
||||
#define STAR_TELEPORTER_DIALOG_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
#include "StarWarping.hpp"
|
||||
#include "StarPlayerUniverseMap.hpp"
|
||||
#include "StarBookmarkInterface.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(UniverseClient);
|
||||
STAR_CLASS(PaneManager);
|
||||
|
||||
class TeleportDialog : public Pane {
|
||||
public:
|
||||
TeleportDialog(UniverseClientPtr client,
|
||||
PaneManager* paneManager,
|
||||
Json config,
|
||||
EntityId sourceEntityId,
|
||||
TeleportBookmark currentLocation);
|
||||
|
||||
void tick() override;
|
||||
|
||||
void selectDestination();
|
||||
void teleport();
|
||||
void editBookmark();
|
||||
|
||||
private:
|
||||
EntityId m_sourceEntityId;
|
||||
UniverseClientPtr m_client;
|
||||
PaneManager* m_paneManager;
|
||||
List<pair<WarpAction, bool>> m_destinations;
|
||||
TeleportBookmark m_currentLocation;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
435
source/frontend/StarTitleScreen.cpp
Normal file
435
source/frontend/StarTitleScreen.cpp
Normal file
|
@ -0,0 +1,435 @@
|
|||
#include "StarTitleScreen.hpp"
|
||||
#include "StarEncode.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarGuiContext.hpp"
|
||||
#include "StarPaneManager.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarCharSelection.hpp"
|
||||
#include "StarCharCreation.hpp"
|
||||
#include "StarTextBoxWidget.hpp"
|
||||
#include "StarOptionsMenu.hpp"
|
||||
#include "StarModsMenu.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarCelestialDatabase.hpp"
|
||||
#include "StarEnvironmentPainter.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
TitleScreen::TitleScreen(PlayerStoragePtr playerStorage, MixerPtr mixer)
|
||||
: m_playerStorage(playerStorage), m_skipMultiPlayerConnection(false), m_mixer(mixer) {
|
||||
m_titleState = TitleState::Quit;
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
m_guiContext = GuiContext::singletonPtr();
|
||||
|
||||
m_celestialDatabase = make_shared<CelestialMasterDatabase>();
|
||||
auto randomWorld = m_celestialDatabase->findRandomWorld(10, 50, [this](CelestialCoordinate const& coordinate) {
|
||||
return is<TerrestrialWorldParameters>(m_celestialDatabase->parameters(coordinate)->visitableParameters());
|
||||
}).take();
|
||||
SkyParameters skyParameters(randomWorld, m_celestialDatabase);
|
||||
m_skyBackdrop = make_shared<Sky>(skyParameters, true);
|
||||
|
||||
m_musicTrack = make_shared<AmbientNoisesDescription>(assets->json("/interface/windowconfig/title.config:music").toObject(), "/");
|
||||
|
||||
initMainMenu();
|
||||
initCharSelectionMenu();
|
||||
initCharCreationMenu();
|
||||
initMultiPlayerMenu();
|
||||
initOptionsMenu();
|
||||
initModsMenu();
|
||||
|
||||
resetState();
|
||||
}
|
||||
|
||||
void TitleScreen::renderInit(RendererPtr renderer) {
|
||||
m_renderer = move(renderer);
|
||||
m_environmentPainter = make_shared<EnvironmentPainter>(m_renderer);
|
||||
}
|
||||
|
||||
void TitleScreen::render() {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
float pixelRatio = m_guiContext->interfaceScale();
|
||||
Vec2F screenSize = Vec2F(m_guiContext->windowSize());
|
||||
auto skyRenderData = m_skyBackdrop->renderData();
|
||||
m_environmentPainter->renderStars(pixelRatio, screenSize, skyRenderData);
|
||||
m_environmentPainter->renderDebrisFields(pixelRatio, screenSize, skyRenderData);
|
||||
m_environmentPainter->renderBackOrbiters(pixelRatio, screenSize, skyRenderData);
|
||||
m_environmentPainter->renderPlanetHorizon(pixelRatio, screenSize, skyRenderData);
|
||||
m_environmentPainter->renderFrontOrbiters(pixelRatio, screenSize, skyRenderData);
|
||||
m_environmentPainter->renderSky(screenSize, skyRenderData);
|
||||
|
||||
m_renderer->flush();
|
||||
|
||||
auto skyBackdropDarken = jsonToColor(assets->json("/interface/windowconfig/title.config:skyBackdropDarken"));
|
||||
m_renderer->render(renderFlatRect(RectF(0, 0, windowWidth(), windowHeight()), skyBackdropDarken.toRgba(), 0.0f));
|
||||
|
||||
m_renderer->flush();
|
||||
|
||||
for (auto backdropImage : assets->json("/interface/windowconfig/title.config:backdropImages").toArray()) {
|
||||
Vec2F offset = jsonToVec2F(backdropImage.get(0)) * interfaceScale();
|
||||
String image = backdropImage.getString(1);
|
||||
float scale = backdropImage.getFloat(2);
|
||||
Vec2F imageSize = Vec2F(m_guiContext->textureSize(image)) * interfaceScale() * scale;
|
||||
|
||||
Vec2F lowerLeft = Vec2F(windowWidth() / 2.0f, windowHeight());
|
||||
lowerLeft[0] -= imageSize[0] / 2;
|
||||
lowerLeft[1] -= imageSize[1];
|
||||
lowerLeft += offset;
|
||||
RectF screenCoords(lowerLeft, lowerLeft + imageSize);
|
||||
m_guiContext->drawQuad(image, screenCoords);
|
||||
}
|
||||
|
||||
m_renderer->flush();
|
||||
|
||||
m_paneManager.render();
|
||||
renderCursor();
|
||||
|
||||
m_renderer->flush();
|
||||
}
|
||||
|
||||
bool TitleScreen::handleInputEvent(InputEvent const& event) {
|
||||
if (auto mouseMove = event.ptr<MouseMoveEvent>())
|
||||
m_cursorScreenPos = mouseMove->mousePosition;
|
||||
|
||||
if (event.is<KeyDownEvent>()) {
|
||||
if (GuiContext::singleton().actions(event).contains(InterfaceAction::TitleBack)) {
|
||||
back();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return m_paneManager.sendInputEvent(event);
|
||||
}
|
||||
|
||||
void TitleScreen::update() {
|
||||
for (auto p : m_rightAnchoredButtons)
|
||||
p.first->setPosition(Vec2I(m_guiContext->windowWidth() / m_guiContext->interfaceScale(), 0) + p.second);
|
||||
m_mainMenu->determineSizeFromChildren();
|
||||
|
||||
m_skyBackdrop->update();
|
||||
m_environmentPainter->update();
|
||||
|
||||
m_paneManager.update();
|
||||
|
||||
if (!finishedState()) {
|
||||
if (auto audioSample = m_musicTrackManager.updateAmbient(m_musicTrack, m_skyBackdrop->isDayTime())) {
|
||||
audioSample->setMixerGroup(MixerGroup::Music);
|
||||
m_mixer->play(audioSample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool TitleScreen::textInputActive() const {
|
||||
return m_paneManager.keyboardCapturedForTextInput();
|
||||
}
|
||||
|
||||
TitleState TitleScreen::currentState() const {
|
||||
return m_titleState;
|
||||
}
|
||||
|
||||
bool TitleScreen::finishedState() const {
|
||||
switch (m_titleState) {
|
||||
case TitleState::StartSinglePlayer:
|
||||
case TitleState::StartMultiPlayer:
|
||||
case TitleState::Quit:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScreen::resetState() {
|
||||
switchState(TitleState::Main);
|
||||
}
|
||||
|
||||
void TitleScreen::goToMultiPlayerSelectCharacter(bool skipConnection) {
|
||||
m_skipMultiPlayerConnection = skipConnection;
|
||||
switchState(TitleState::MultiPlayerSelectCharacter);
|
||||
}
|
||||
|
||||
void TitleScreen::stopMusic() {
|
||||
m_musicTrackManager.cancelAll();
|
||||
}
|
||||
|
||||
PlayerPtr TitleScreen::currentlySelectedPlayer() const {
|
||||
return m_mainAppPlayer;
|
||||
}
|
||||
|
||||
String TitleScreen::multiPlayerAddress() const {
|
||||
return m_connectionAddress;
|
||||
}
|
||||
|
||||
void TitleScreen::setMultiPlayerAddress(String address) {
|
||||
m_multiPlayerMenu->fetchChild<TextBoxWidget>("address")->setText(address);
|
||||
m_connectionAddress = move(address);
|
||||
}
|
||||
|
||||
String TitleScreen::multiPlayerPort() const {
|
||||
return m_connectionPort;
|
||||
}
|
||||
|
||||
void TitleScreen::setMultiPlayerPort(String port) {
|
||||
m_multiPlayerMenu->fetchChild<TextBoxWidget>("port")->setText(port);
|
||||
m_connectionPort = move(port);
|
||||
}
|
||||
|
||||
String TitleScreen::multiPlayerAccount() const {
|
||||
return m_account;
|
||||
}
|
||||
|
||||
void TitleScreen::setMultiPlayerAccount(String account) {
|
||||
m_multiPlayerMenu->fetchChild<TextBoxWidget>("account")->setText(account);
|
||||
m_account = move(account);
|
||||
}
|
||||
|
||||
String TitleScreen::multiPlayerPassword() const {
|
||||
return m_password;
|
||||
}
|
||||
|
||||
void TitleScreen::setMultiPlayerPassword(String password) {
|
||||
m_multiPlayerMenu->fetchChild<TextBoxWidget>("password")->setText(password);
|
||||
m_password = move(password);
|
||||
}
|
||||
|
||||
void TitleScreen::initMainMenu() {
|
||||
m_mainMenu = make_shared<Pane>();
|
||||
auto backMenu = make_shared<Pane>();
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
StringMap<WidgetCallbackFunc> buttonCallbacks;
|
||||
buttonCallbacks["singleplayer"] = [=](Widget*) { switchState(TitleState::SinglePlayerSelectCharacter); };
|
||||
buttonCallbacks["multiplayer"] = [=](Widget*) { switchState(TitleState::MultiPlayerSelectCharacter); };
|
||||
buttonCallbacks["options"] = [=](Widget*) { switchState(TitleState::Options); };
|
||||
buttonCallbacks["quit"] = [=](Widget*) { switchState(TitleState::Quit); };
|
||||
buttonCallbacks["back"] = [=](Widget*) { back(); };
|
||||
buttonCallbacks["mods"] = [=](Widget*) { switchState(TitleState::Mods); };
|
||||
|
||||
for (auto buttonConfig : assets->json("/interface/windowconfig/title.config:mainMenuButtons").toArray()) {
|
||||
String key = buttonConfig.getString("key");
|
||||
String image = buttonConfig.getString("button");
|
||||
String imageHover = buttonConfig.getString("hover");
|
||||
Vec2I offset = jsonToVec2I(buttonConfig.get("offset"));
|
||||
WidgetCallbackFunc callback = buttonCallbacks.get(key);
|
||||
bool rightAnchored = buttonConfig.getBool("rightAnchored", false);
|
||||
|
||||
auto button = make_shared<ButtonWidget>(callback, image, imageHover, "", "");
|
||||
button->setPosition(offset);
|
||||
|
||||
if (rightAnchored)
|
||||
m_rightAnchoredButtons.append({button, offset});
|
||||
|
||||
if (key == "back")
|
||||
backMenu->addChild(key, button);
|
||||
else
|
||||
m_mainMenu->addChild(key, button);
|
||||
}
|
||||
|
||||
m_mainMenu->setAnchor(PaneAnchor::BottomLeft);
|
||||
m_mainMenu->lockPosition();
|
||||
|
||||
backMenu->determineSizeFromChildren();
|
||||
backMenu->setAnchor(PaneAnchor::BottomLeft);
|
||||
backMenu->lockPosition();
|
||||
|
||||
m_paneManager.registerPane("mainMenu", PaneLayer::Hud, m_mainMenu);
|
||||
m_paneManager.registerPane("backMenu", PaneLayer::Hud, backMenu);
|
||||
}
|
||||
|
||||
void TitleScreen::initCharSelectionMenu() {
|
||||
auto deleteDialog = make_shared<Pane>();
|
||||
|
||||
GuiReader reader;
|
||||
|
||||
reader.registerCallback("delete", [=](Widget*) { deleteDialog->dismiss(); });
|
||||
reader.registerCallback("cancel", [=](Widget*) { deleteDialog->dismiss(); });
|
||||
|
||||
reader.construct(Root::singleton().assets()->json("/interface/windowconfig/deletedialog.config"), deleteDialog.get());
|
||||
|
||||
auto charSelectionMenu = make_shared<CharSelectionPane>(m_playerStorage, [=]() {
|
||||
if (m_titleState == TitleState::SinglePlayerSelectCharacter)
|
||||
switchState(TitleState::SinglePlayerCreateCharacter);
|
||||
else if (m_titleState == TitleState::MultiPlayerSelectCharacter)
|
||||
switchState(TitleState::MultiPlayerCreateCharacter);
|
||||
}, [=](PlayerPtr mainPlayer) {
|
||||
m_mainAppPlayer = mainPlayer;
|
||||
m_playerStorage->moveToFront(m_mainAppPlayer->uuid());
|
||||
if (m_titleState == TitleState::SinglePlayerSelectCharacter) {
|
||||
switchState(TitleState::StartSinglePlayer);
|
||||
} else if (m_titleState == TitleState::MultiPlayerSelectCharacter) {
|
||||
if (m_skipMultiPlayerConnection)
|
||||
switchState(TitleState::StartMultiPlayer);
|
||||
else
|
||||
switchState(TitleState::MultiPlayerConnect);
|
||||
}
|
||||
}, [=](Uuid playerUuid) {
|
||||
auto deleteDialog = m_paneManager.registeredPane("deleteDialog");
|
||||
deleteDialog->fetchChild<ButtonWidget>("delete")->setCallback([=](Widget*) {
|
||||
m_playerStorage->deletePlayer(playerUuid);
|
||||
deleteDialog->dismiss();
|
||||
});
|
||||
m_paneManager.displayRegisteredPane("deleteDialog");
|
||||
});
|
||||
charSelectionMenu->setAnchor(PaneAnchor::Center);
|
||||
charSelectionMenu->lockPosition();
|
||||
|
||||
m_paneManager.registerPane("deleteDialog", PaneLayer::ModalWindow, deleteDialog, [=](PanePtr const&) {
|
||||
charSelectionMenu->updateCharacterPlates();
|
||||
});
|
||||
m_paneManager.registerPane("charSelectionMenu", PaneLayer::Hud, charSelectionMenu);
|
||||
}
|
||||
|
||||
void TitleScreen::initCharCreationMenu() {
|
||||
auto charCreationMenu = make_shared<CharCreationPane>([=](PlayerPtr newPlayer) {
|
||||
if (newPlayer) {
|
||||
m_mainAppPlayer = newPlayer;
|
||||
m_playerStorage->savePlayer(m_mainAppPlayer);
|
||||
m_playerStorage->moveToFront(m_mainAppPlayer->uuid());
|
||||
}
|
||||
back();
|
||||
});
|
||||
charCreationMenu->setAnchor(PaneAnchor::Center);
|
||||
charCreationMenu->lockPosition();
|
||||
|
||||
m_paneManager.registerPane("charCreationMenu", PaneLayer::Hud, charCreationMenu);
|
||||
}
|
||||
|
||||
void TitleScreen::initMultiPlayerMenu() {
|
||||
m_multiPlayerMenu = make_shared<Pane>();
|
||||
|
||||
GuiReader reader;
|
||||
|
||||
reader.registerCallback("address", [=](Widget* obj) {
|
||||
m_connectionAddress = convert<TextBoxWidget>(obj)->getText().trim();
|
||||
});
|
||||
|
||||
reader.registerCallback("port", [=](Widget* obj) {
|
||||
m_connectionPort = convert<TextBoxWidget>(obj)->getText().trim();
|
||||
});
|
||||
|
||||
reader.registerCallback("account", [=](Widget* obj) {
|
||||
m_account = convert<TextBoxWidget>(obj)->getText().trim();
|
||||
});
|
||||
|
||||
reader.registerCallback("password", [=](Widget* obj) {
|
||||
m_password = convert<TextBoxWidget>(obj)->getText().trim();
|
||||
});
|
||||
|
||||
reader.registerCallback("connect", [=](Widget*) {
|
||||
switchState(TitleState::StartMultiPlayer);
|
||||
});
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
reader.construct(assets->json("/interface/windowconfig/multiplayer.config"), m_multiPlayerMenu.get());
|
||||
|
||||
m_paneManager.registerPane("multiplayerMenu", PaneLayer::Hud, m_multiPlayerMenu);
|
||||
}
|
||||
|
||||
void TitleScreen::initOptionsMenu() {
|
||||
auto optionsMenu = make_shared<OptionsMenu>(&m_paneManager);
|
||||
optionsMenu->setAnchor(PaneAnchor::Center);
|
||||
optionsMenu->lockPosition();
|
||||
|
||||
m_paneManager.registerPane("optionsMenu", PaneLayer::Hud, optionsMenu, [this](PanePtr const&) {
|
||||
back();
|
||||
});
|
||||
}
|
||||
|
||||
void TitleScreen::initModsMenu() {
|
||||
auto modsMenu = make_shared<ModsMenu>();
|
||||
modsMenu->setAnchor(PaneAnchor::Center);
|
||||
modsMenu->lockPosition();
|
||||
|
||||
m_paneManager.registerPane("modsMenu", PaneLayer::Hud, modsMenu, [this](PanePtr const&) {
|
||||
back();
|
||||
});
|
||||
}
|
||||
|
||||
void TitleScreen::switchState(TitleState titleState) {
|
||||
if (m_titleState == titleState)
|
||||
return;
|
||||
|
||||
m_paneManager.dismissAllPanes();
|
||||
m_titleState = titleState;
|
||||
|
||||
// Clear the 'skip multi player connection' flag if we leave the multi player
|
||||
// menus
|
||||
if (m_titleState < TitleState::MultiPlayerSelectCharacter || m_titleState > TitleState::MultiPlayerConnect)
|
||||
m_skipMultiPlayerConnection = false;
|
||||
|
||||
if (titleState == TitleState::Main) {
|
||||
m_paneManager.displayRegisteredPane("mainMenu");
|
||||
} else {
|
||||
m_paneManager.displayRegisteredPane("backMenu");
|
||||
|
||||
if (titleState == TitleState::Options) {
|
||||
m_paneManager.displayRegisteredPane("optionsMenu");
|
||||
} if (titleState == TitleState::Mods) {
|
||||
m_paneManager.displayRegisteredPane("modsMenu");
|
||||
} else if (titleState == TitleState::SinglePlayerSelectCharacter) {
|
||||
m_paneManager.displayRegisteredPane("charSelectionMenu");
|
||||
} else if (titleState == TitleState::SinglePlayerCreateCharacter) {
|
||||
m_paneManager.displayRegisteredPane("charCreationMenu");
|
||||
} else if (titleState == TitleState::MultiPlayerSelectCharacter) {
|
||||
m_paneManager.displayRegisteredPane("charSelectionMenu");
|
||||
} else if (titleState == TitleState::MultiPlayerCreateCharacter) {
|
||||
m_paneManager.displayRegisteredPane("charCreationMenu");
|
||||
} else if (titleState == TitleState::MultiPlayerConnect) {
|
||||
m_paneManager.displayRegisteredPane("multiplayerMenu");
|
||||
if (auto addressWidget = m_multiPlayerMenu->fetchChild("address"))
|
||||
addressWidget->focus();
|
||||
}
|
||||
}
|
||||
|
||||
if (titleState == TitleState::Quit)
|
||||
m_musicTrackManager.cancelAll();
|
||||
}
|
||||
|
||||
void TitleScreen::back() {
|
||||
if (m_titleState == TitleState::Options)
|
||||
switchState(TitleState::Main);
|
||||
else if (m_titleState == TitleState::Mods)
|
||||
switchState(TitleState::Main);
|
||||
else if (m_titleState == TitleState::SinglePlayerSelectCharacter)
|
||||
switchState(TitleState::Main);
|
||||
else if (m_titleState == TitleState::SinglePlayerCreateCharacter)
|
||||
switchState(TitleState::SinglePlayerSelectCharacter);
|
||||
else if (m_titleState == TitleState::MultiPlayerSelectCharacter)
|
||||
switchState(TitleState::Main);
|
||||
else if (m_titleState == TitleState::MultiPlayerCreateCharacter)
|
||||
switchState(TitleState::MultiPlayerSelectCharacter);
|
||||
else if (m_titleState == TitleState::MultiPlayerConnect)
|
||||
switchState(TitleState::MultiPlayerSelectCharacter);
|
||||
}
|
||||
|
||||
void TitleScreen::renderCursor() {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
float TitleScreen::interfaceScale() const {
|
||||
return m_guiContext->interfaceScale();
|
||||
}
|
||||
|
||||
unsigned TitleScreen::windowHeight() const {
|
||||
return m_guiContext->windowHeight();
|
||||
}
|
||||
|
||||
unsigned TitleScreen::windowWidth() const {
|
||||
return m_guiContext->windowWidth();
|
||||
}
|
||||
|
||||
}
|
132
source/frontend/StarTitleScreen.hpp
Normal file
132
source/frontend/StarTitleScreen.hpp
Normal file
|
@ -0,0 +1,132 @@
|
|||
#ifndef STAR_TITLE_HPP
|
||||
#define STAR_TITLE_HPP
|
||||
|
||||
#include "StarSky.hpp"
|
||||
#include "StarAmbient.hpp"
|
||||
#include "StarRegisteredPaneManager.hpp"
|
||||
#include "StarInterfaceCursor.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(Player);
|
||||
STAR_CLASS(PlayerStorage);
|
||||
STAR_CLASS(CharCreationPane);
|
||||
STAR_CLASS(CharSelectionPane);
|
||||
STAR_CLASS(OptionsMenu);
|
||||
STAR_CLASS(ModsMenu);
|
||||
STAR_CLASS(GuiContext);
|
||||
STAR_CLASS(Pane);
|
||||
STAR_CLASS(PaneManager);
|
||||
STAR_CLASS(Mixer);
|
||||
STAR_CLASS(EnvironmentPainter);
|
||||
STAR_CLASS(CelestialMasterDatabase);
|
||||
STAR_CLASS(ButtonWidget);
|
||||
|
||||
STAR_CLASS(TitleScreen);
|
||||
|
||||
enum class TitleState {
|
||||
Main,
|
||||
Options,
|
||||
Mods,
|
||||
SinglePlayerSelectCharacter,
|
||||
SinglePlayerCreateCharacter,
|
||||
MultiPlayerSelectCharacter,
|
||||
MultiPlayerCreateCharacter,
|
||||
MultiPlayerConnect,
|
||||
StartSinglePlayer,
|
||||
StartMultiPlayer,
|
||||
Quit
|
||||
};
|
||||
|
||||
class TitleScreen {
|
||||
public:
|
||||
TitleScreen(PlayerStoragePtr playerStorage, MixerPtr mixer);
|
||||
|
||||
void renderInit(RendererPtr renderer);
|
||||
|
||||
void render();
|
||||
|
||||
bool handleInputEvent(InputEvent const& event);
|
||||
void update();
|
||||
|
||||
bool textInputActive() const;
|
||||
|
||||
TitleState currentState() const;
|
||||
// TitleState is StartSinglePlayer, StartMultiPlayer, or Quit
|
||||
bool finishedState() const;
|
||||
void resetState();
|
||||
// Switches to multi player select character screen immediately, skipping the
|
||||
// connection screen if 'skipConnection' is true. If the player backs out of
|
||||
// the multiplayer menu, the skip connection is forgotten.
|
||||
void goToMultiPlayerSelectCharacter(bool skipConnection);
|
||||
|
||||
void stopMusic();
|
||||
|
||||
PlayerPtr currentlySelectedPlayer() const;
|
||||
|
||||
String multiPlayerAddress() const;
|
||||
void setMultiPlayerAddress(String address);
|
||||
|
||||
String multiPlayerPort() const;
|
||||
void setMultiPlayerPort(String port);
|
||||
|
||||
String multiPlayerAccount() const;
|
||||
void setMultiPlayerAccount(String account);
|
||||
|
||||
String multiPlayerPassword() const;
|
||||
void setMultiPlayerPassword(String password);
|
||||
|
||||
private:
|
||||
void initMainMenu();
|
||||
void initCharSelectionMenu();
|
||||
void initCharCreationMenu();
|
||||
void initMultiPlayerMenu();
|
||||
void initOptionsMenu();
|
||||
void initModsMenu();
|
||||
|
||||
void renderCursor();
|
||||
|
||||
void switchState(TitleState titleState);
|
||||
void back();
|
||||
|
||||
float interfaceScale() const;
|
||||
unsigned windowHeight() const;
|
||||
unsigned windowWidth() const;
|
||||
|
||||
GuiContext* m_guiContext;
|
||||
|
||||
RendererPtr m_renderer;
|
||||
EnvironmentPainterPtr m_environmentPainter;
|
||||
PanePtr m_multiPlayerMenu;
|
||||
|
||||
RegisteredPaneManager<String> m_paneManager;
|
||||
|
||||
Vec2I m_cursorScreenPos;
|
||||
InterfaceCursor m_cursor;
|
||||
TitleState m_titleState;
|
||||
|
||||
PanePtr m_mainMenu;
|
||||
List<pair<ButtonWidgetPtr, Vec2I>> m_rightAnchoredButtons;
|
||||
|
||||
PlayerPtr m_mainAppPlayer;
|
||||
PlayerStoragePtr m_playerStorage;
|
||||
|
||||
bool m_skipMultiPlayerConnection;
|
||||
String m_connectionAddress;
|
||||
String m_connectionPort;
|
||||
String m_account;
|
||||
String m_password;
|
||||
|
||||
CelestialMasterDatabasePtr m_celestialDatabase;
|
||||
|
||||
MixerPtr m_mixer;
|
||||
|
||||
SkyPtr m_skyBackdrop;
|
||||
|
||||
AmbientNoisesDescriptionPtr m_musicTrack;
|
||||
AmbientManager m_musicTrackManager;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
411
source/frontend/StarWidgetLuaBindings.cpp
Normal file
411
source/frontend/StarWidgetLuaBindings.cpp
Normal file
|
@ -0,0 +1,411 @@
|
|||
#include "StarWidgetLuaBindings.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarLuaGameConverters.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarCanvasWidget.hpp"
|
||||
#include "StarLabelWidget.hpp"
|
||||
#include "StarListWidget.hpp"
|
||||
#include "StarButtonWidget.hpp"
|
||||
#include "StarButtonGroup.hpp"
|
||||
#include "StarTextBoxWidget.hpp"
|
||||
#include "StarProgressWidget.hpp"
|
||||
#include "StarSliderBar.hpp"
|
||||
#include "StarItemGridWidget.hpp"
|
||||
#include "StarItemSlotWidget.hpp"
|
||||
#include "StarItemDatabase.hpp"
|
||||
#include "StarFlowLayout.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
template <>
|
||||
struct LuaConverter<CanvasWidgetPtr> : LuaUserDataConverter<CanvasWidgetPtr> {};
|
||||
|
||||
template <>
|
||||
struct LuaUserDataMethods<CanvasWidgetPtr> {
|
||||
static LuaMethods<CanvasWidgetPtr> make() {
|
||||
LuaMethods<CanvasWidgetPtr> methods;
|
||||
|
||||
methods.registerMethodWithSignature<Vec2I, CanvasWidgetPtr>("size", mem_fn(&CanvasWidget::size));
|
||||
methods.registerMethodWithSignature<Vec2I, CanvasWidgetPtr>("mousePosition", mem_fn(&CanvasWidget::mousePosition));
|
||||
|
||||
methods.registerMethodWithSignature<void, CanvasWidgetPtr>("clear", mem_fn(&CanvasWidget::clear));
|
||||
|
||||
methods.registerMethod("drawImage",
|
||||
[](CanvasWidgetPtr canvasWidget, String image, Vec2F position, Maybe<float> scale, Maybe<Color> color, Maybe<bool> centered) {
|
||||
if (centered && *centered)
|
||||
canvasWidget->drawImageCentered(image, position, scale.value(1.0f), color.value(Color::White).toRgba());
|
||||
else
|
||||
canvasWidget->drawImage(image, position, scale.value(1.0f), color.value(Color::White).toRgba());
|
||||
});
|
||||
methods.registerMethod("drawImageDrawable",
|
||||
[](CanvasWidgetPtr canvasWidget, String image, Vec2F position, MVariant<Vec2F, float> scale, Maybe<Color> color, Maybe<float> rotation) {
|
||||
auto drawable = Drawable::makeImage(image, 1.0, true, {0.0, 0.0}, color.value(Color::White));
|
||||
if (auto s = scale.maybe<Vec2F>())
|
||||
drawable.transform(Mat3F::scaling(*s));
|
||||
else if(auto s = scale.maybe<float>())
|
||||
drawable.transform(Mat3F::scaling(*s));
|
||||
if (rotation)
|
||||
drawable.rotate(*rotation);
|
||||
canvasWidget->drawDrawable(drawable, position);
|
||||
});
|
||||
methods.registerMethod("drawImageRect",
|
||||
[](CanvasWidgetPtr canvasWidget, String image, RectF texCoords, RectF screenCoords, Maybe<Color> color) {
|
||||
canvasWidget->drawImageRect(image, texCoords, screenCoords, color.value(Color::White).toRgba());
|
||||
});
|
||||
methods.registerMethod("drawTiledImage",
|
||||
[](CanvasWidgetPtr canvasWidget, String image, Vec2D offset, RectF screenCoords, Maybe<float> scale, Maybe<Color> color) {
|
||||
canvasWidget->drawTiledImage(image, scale.value(1.0f), offset, screenCoords, color.value(Color::White).toRgba());
|
||||
});
|
||||
methods.registerMethod("drawLine",
|
||||
[](CanvasWidgetPtr canvasWidget, Vec2F begin, Vec2F end, Maybe<Color> color, Maybe<float> lineWidth) {
|
||||
canvasWidget->drawLine(begin, end, color.value(Color::White).toRgba(), lineWidth.value(1.0f));
|
||||
});
|
||||
methods.registerMethod("drawRect",
|
||||
[](CanvasWidgetPtr canvasWidget, RectF rect, Maybe<Color> color) {
|
||||
canvasWidget->drawRect(rect, color.value(Color::White).toRgba());
|
||||
});
|
||||
methods.registerMethod("drawPoly",
|
||||
[](CanvasWidgetPtr canvasWidget, PolyF poly, Maybe<Color> color, Maybe<float> lineWidth) {
|
||||
canvasWidget->drawPoly(poly, color.value(Color::White).toRgba(), lineWidth.value(1.0f));
|
||||
});
|
||||
methods.registerMethod("drawTriangles",
|
||||
[](CanvasWidgetPtr canvasWidget, List<PolyF> triangles, Maybe<Color> color) {
|
||||
auto tris = triangles.transformed([](PolyF const& poly) {
|
||||
if (poly.sides() != 3)
|
||||
throw StarException("Triangle must have exactly 3 sides");
|
||||
return tuple<Vec2F, Vec2F, Vec2F>(poly.vertex(0), poly.vertex(1), poly.vertex(2));
|
||||
});
|
||||
canvasWidget->drawTriangles(tris, color.value(Color::White).toRgba());
|
||||
});
|
||||
methods.registerMethod("drawText",
|
||||
[](CanvasWidgetPtr canvasWidget, String text, Json tp, unsigned fontSize, Maybe<Color> color) {
|
||||
canvasWidget->drawText(text, TextPositioning(tp), fontSize, color.value(Color::White).toRgba());
|
||||
});
|
||||
|
||||
return methods;
|
||||
}
|
||||
};
|
||||
|
||||
LuaCallbacks LuaBindings::makeWidgetCallbacks(Widget* parentWidget, GuiReader* reader) {
|
||||
LuaCallbacks callbacks;
|
||||
|
||||
// a bit miscellaneous, but put this here since widgets have access to gui context
|
||||
|
||||
callbacks.registerCallback("playSound",
|
||||
[parentWidget](String const& audio, Maybe<int> loops, Maybe<float> volume) {
|
||||
parentWidget->context()->playAudio(audio, loops.value(0), volume.value(1.0f));
|
||||
});
|
||||
|
||||
// widget userdata methods
|
||||
|
||||
callbacks.registerCallback("bindCanvas", [parentWidget](String const& widgetName) -> Maybe<CanvasWidgetPtr> {
|
||||
if (auto canvas = parentWidget->fetchChild<CanvasWidget>(widgetName))
|
||||
return canvas;
|
||||
return {};
|
||||
});
|
||||
|
||||
// generic widget callbacks
|
||||
|
||||
callbacks.registerCallback("getPosition", [parentWidget](String const& widgetName) -> Maybe<Vec2I> {
|
||||
if (auto widget = parentWidget->fetchChild<Widget>(widgetName))
|
||||
return widget->relativePosition();
|
||||
return {};
|
||||
});
|
||||
callbacks.registerCallback("setPosition", [parentWidget](String const& widgetName, Vec2I const& position) {
|
||||
if (auto widget = parentWidget->fetchChild<Widget>(widgetName))
|
||||
widget->setPosition(position);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("getSize", [parentWidget](String const& widgetName) -> Maybe<Vec2I> {
|
||||
if (auto widget = parentWidget->fetchChild<Widget>(widgetName))
|
||||
return widget->size();
|
||||
return {};
|
||||
});
|
||||
callbacks.registerCallback("setSize", [parentWidget](String const& widgetName, Vec2I const& size) {
|
||||
if (auto widget = parentWidget->fetchChild<Widget>(widgetName))
|
||||
widget->setSize(size);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setVisible", [parentWidget](String const& widgetName, bool visible) {
|
||||
if (auto widget = parentWidget->fetchChild<Widget>(widgetName))
|
||||
widget->setVisibility(visible);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("active", [parentWidget](String const& widgetName) -> Maybe<bool> {
|
||||
if (auto widget = parentWidget->fetchChild<Widget>(widgetName))
|
||||
return widget->active();
|
||||
return {};
|
||||
});
|
||||
|
||||
callbacks.registerCallback("focus", [parentWidget](String const& widgetName) {
|
||||
if (auto widget = parentWidget->fetchChild<Widget>(widgetName))
|
||||
widget->focus();
|
||||
});
|
||||
|
||||
callbacks.registerCallback("hasFocus", [parentWidget](String const& widgetName) -> Maybe<bool> {
|
||||
if (auto widget = parentWidget->fetchChild<Widget>(widgetName))
|
||||
return widget->hasFocus();
|
||||
return {};
|
||||
});
|
||||
|
||||
callbacks.registerCallback("blur", [parentWidget](String const& widgetName) {
|
||||
if (auto widget = parentWidget->fetchChild<Widget>(widgetName))
|
||||
widget->blur();
|
||||
});
|
||||
|
||||
callbacks.registerCallback("getData", [parentWidget](String const& widgetName) {
|
||||
if (auto widget = parentWidget->fetchChild<Widget>(widgetName))
|
||||
return widget->data();
|
||||
return Json();
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setData", [parentWidget](String const& widgetName, Json const& data) {
|
||||
if (auto widget = parentWidget->fetchChild<Widget>(widgetName))
|
||||
widget->setData(data);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("getChildAt", [parentWidget](Vec2I const& screenPosition) -> Maybe<String> {
|
||||
if (auto widget = parentWidget->getChildAt(screenPosition))
|
||||
return widget->fullName();
|
||||
else
|
||||
return{};
|
||||
});
|
||||
|
||||
callbacks.registerCallback("inMember", [parentWidget](String const& widgetName, Vec2I const& screenPosition) -> Maybe<bool> {
|
||||
if (auto widget = parentWidget->fetchChild<Widget>(widgetName))
|
||||
return widget->inMember(screenPosition);
|
||||
else
|
||||
return {};
|
||||
});
|
||||
|
||||
callbacks.registerCallback("addChild", [parentWidget, reader](String const& widgetName, Json const& newChildConfig, Maybe<String> const& newChildName) {
|
||||
if (auto widget = parentWidget->fetchChild<Widget>(widgetName)) {
|
||||
String name = newChildName.value(strf("%d", Random::randu64()));
|
||||
WidgetPtr newChild = reader->makeSingle(name, newChildConfig);
|
||||
widget->addChild(name, newChild);
|
||||
}
|
||||
});
|
||||
|
||||
callbacks.registerCallback("removeAllChildren", [parentWidget](String const& widgetName) {
|
||||
if (auto widget = parentWidget->fetchChild<Widget>(widgetName))
|
||||
widget->removeAllChildren();
|
||||
});
|
||||
|
||||
callbacks.registerCallback("removeChild", [parentWidget](String const& widgetName, String const& childName) {
|
||||
if (auto widget = parentWidget->fetchChild<Widget>(widgetName))
|
||||
widget->removeChild(childName);
|
||||
});
|
||||
|
||||
// callbacks only valid for specific widget types
|
||||
|
||||
callbacks.registerCallback("getText", [parentWidget](String const& widgetName) -> Maybe<String> {
|
||||
if (auto textBox = parentWidget->fetchChild<TextBoxWidget>(widgetName))
|
||||
return textBox->getText();
|
||||
return {};
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setText", [parentWidget](String const& widgetName, String const& text) {
|
||||
if (auto label = parentWidget->fetchChild<LabelWidget>(widgetName))
|
||||
label->setText(text);
|
||||
else if (auto button = parentWidget->fetchChild<ButtonWidget>(widgetName))
|
||||
button->setText(text);
|
||||
else if (auto textBox = parentWidget->fetchChild<TextBoxWidget>(widgetName))
|
||||
textBox->setText(text);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setFontColor", [parentWidget](String const& widgetName, Color const& color) {
|
||||
if (auto label = parentWidget->fetchChild<LabelWidget>(widgetName))
|
||||
label->setColor(color);
|
||||
else if (auto button = parentWidget->fetchChild<ButtonWidget>(widgetName))
|
||||
button->setFontColor(color);
|
||||
else if (auto textBox = parentWidget->fetchChild<TextBoxWidget>(widgetName))
|
||||
textBox->setColor(color);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setImage", [parentWidget](String const& widgetName, String const& imagePath) {
|
||||
if (auto image = parentWidget->fetchChild<ImageWidget>(widgetName))
|
||||
image->setImage(imagePath);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setImageScale", [parentWidget](String const& widgetName, float const& imageScale) {
|
||||
if (auto image = parentWidget->fetchChild<ImageWidget>(widgetName))
|
||||
image->setScale(imageScale);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setImageRotation", [parentWidget](String const& widgetName, float const& imageRotation) {
|
||||
if (auto image = parentWidget->fetchChild<ImageWidget>(widgetName))
|
||||
image->setRotation(imageRotation);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setButtonEnabled", [parentWidget](String const& widgetName, bool enabled) {
|
||||
if (auto button = parentWidget->fetchChild<ButtonWidget>(widgetName))
|
||||
button->setEnabled(enabled);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setButtonImage", [parentWidget](String const& widgetName, String const& baseImage) {
|
||||
if (auto button = parentWidget->fetchChild<ButtonWidget>(widgetName))
|
||||
button->setImages(baseImage);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setButtonImages", [parentWidget](String const& widgetName, Json const& imageSet) {
|
||||
if (auto button = parentWidget->fetchChild<ButtonWidget>(widgetName))
|
||||
button->setImages(imageSet.getString("base"), imageSet.getString("hover", ""), imageSet.getString("pressed", ""), imageSet.getString("disabled", ""));
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setButtonCheckedImages", [parentWidget](String const& widgetName, Json const& imageSet) {
|
||||
if (auto button = parentWidget->fetchChild<ButtonWidget>(widgetName))
|
||||
button->setCheckedImages(imageSet.getString("base"), imageSet.getString("hover", ""), imageSet.getString("pressed", ""), imageSet.getString("disabled", ""));
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setButtonOverlayImage", [parentWidget](String const& widgetName, String const& overlayImage) {
|
||||
if (auto button = parentWidget->fetchChild<ButtonWidget>(widgetName))
|
||||
button->setOverlayImage(overlayImage);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("getChecked", [parentWidget](String const& widgetName) -> Maybe<bool> {
|
||||
if (auto button = parentWidget->fetchChild<ButtonWidget>(widgetName))
|
||||
return button->isChecked();
|
||||
return {};
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setChecked", [parentWidget](String const& widgetName, bool checked) {
|
||||
if (auto button = parentWidget->fetchChild<ButtonWidget>(widgetName))
|
||||
button->setChecked(checked);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("getSelectedOption", [parentWidget](String const& widgetName) -> Maybe<int> {
|
||||
if (auto buttonGroup = parentWidget->fetchChild<ButtonGroupWidget>(widgetName))
|
||||
return buttonGroup->checkedId();
|
||||
return {};
|
||||
});
|
||||
|
||||
callbacks.registerCallback("getSelectedData", [parentWidget](String const& widgetName) -> Json {
|
||||
if (auto buttonGroup = parentWidget->fetchChild<ButtonGroupWidget>(widgetName)) {
|
||||
if (auto button = buttonGroup->checkedButton())
|
||||
return button->data();
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setSelectedOption", [parentWidget](String const& widgetName, Maybe<int> index) {
|
||||
if (auto buttonGroup = parentWidget->fetchChild<ButtonGroupWidget>(widgetName))
|
||||
buttonGroup->select(index.value(ButtonGroup::NoButton));
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setOptionEnabled", [parentWidget](String const& widgetName, int index, bool enabled) {
|
||||
if (auto buttonGroup = parentWidget->fetchChild<ButtonGroupWidget>(widgetName)) {
|
||||
if (auto button = buttonGroup->button(index))
|
||||
button->setEnabled(enabled);
|
||||
}
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setOptionVisible", [parentWidget](String const& widgetName, int index, bool visible) {
|
||||
if (auto buttonGroup = parentWidget->fetchChild<ButtonGroupWidget>(widgetName)) {
|
||||
if (auto button = buttonGroup->button(index))
|
||||
button->setVisibility(visible);
|
||||
}
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setProgress", [parentWidget](String const& widgetName, float const& value) {
|
||||
if (auto progress = parentWidget->fetchChild<ProgressWidget>(widgetName))
|
||||
progress->setCurrentProgressLevel(value);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setSliderEnabled", [parentWidget](String const& widgetName, bool enabled) {
|
||||
if (auto slider = parentWidget->fetchChild<SliderBarWidget>(widgetName))
|
||||
slider->setEnabled(enabled);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("getSliderValue", [parentWidget](String const& widgetName) -> Maybe<int> {
|
||||
if (auto slider = parentWidget->fetchChild<SliderBarWidget>(widgetName))
|
||||
return slider->val();
|
||||
return {};
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setSliderValue", [parentWidget](String const& widgetName, int newValue) {
|
||||
if (auto slider = parentWidget->fetchChild<SliderBarWidget>(widgetName))
|
||||
return slider->setVal(newValue);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setSliderRange", [parentWidget](String const& widgetName, int newMin, int newMax, Maybe<int> newDelta) {
|
||||
if (auto slider = parentWidget->fetchChild<SliderBarWidget>(widgetName))
|
||||
return slider->setRange(newMin, newMax, newDelta.value(1));
|
||||
});
|
||||
|
||||
callbacks.registerCallback("clearListItems", [parentWidget](String const& widgetName) {
|
||||
if (auto list = parentWidget->fetchChild<ListWidget>(widgetName))
|
||||
list->clear();
|
||||
});
|
||||
|
||||
callbacks.registerCallback("addListItem", [parentWidget](String const& widgetName) -> Maybe<String> {
|
||||
if (auto list = parentWidget->fetchChild<ListWidget>(widgetName)) {
|
||||
auto newItem = list->addItem();
|
||||
return newItem->name();
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
callbacks.registerCallback("removeListItem", [parentWidget](String const& widgetName, size_t at) {
|
||||
if (auto list = parentWidget->fetchChild<ListWidget>(widgetName))
|
||||
list->removeItem(at);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("getListSelected", [parentWidget](String const& widgetName) -> Maybe<String> {
|
||||
if (auto list = parentWidget->fetchChild<ListWidget>(widgetName))
|
||||
if (list->selectedItem() != NPos)
|
||||
return list->selectedWidget()->name();
|
||||
return {};
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setListSelected", [parentWidget](String const& widgetName, String const& selectedName) {
|
||||
if (auto list = parentWidget->fetchChild<ListWidget>(widgetName))
|
||||
if (auto selected = list->fetchChild(selectedName))
|
||||
list->setSelectedWidget(selected);
|
||||
});
|
||||
|
||||
callbacks.registerCallback("registerMemberCallback", [parentWidget](String const& widgetName, String const& name, LuaFunction callback) {
|
||||
if (auto list = parentWidget->fetchChild<ListWidget>(widgetName)){
|
||||
list->registerMemberCallback(name, [callback](Widget* widget) {
|
||||
callback.invoke(widget->name(), widget->data());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
callbacks.registerCallback("itemGridItems", [parentWidget](String const& widgetName) {
|
||||
if (auto itemGrid = parentWidget->fetchChild<ItemGridWidget>(widgetName))
|
||||
return itemGrid->bag()->toJson();
|
||||
return Json();
|
||||
});
|
||||
|
||||
callbacks.registerCallback("itemSlotItem", [parentWidget](String const& widgetName) -> Maybe<Json> {
|
||||
if (auto itemSlot = parentWidget->fetchChild<ItemSlotWidget>(widgetName)) {
|
||||
if (itemSlot->item())
|
||||
return itemSlot->item()->descriptor().toJson();
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setItemSlotItem", [parentWidget](String const& widgetName, Json const& item) {
|
||||
if (auto itemSlot = parentWidget->fetchChild<ItemSlotWidget>(widgetName)) {
|
||||
auto itemDb = Root::singleton().itemDatabase();
|
||||
itemSlot->setItem(itemDb->fromJson(item));
|
||||
}
|
||||
});
|
||||
|
||||
callbacks.registerCallback("setItemSlotProgress", [parentWidget](String const& widgetName, float progress) {
|
||||
if (auto itemSlot = parentWidget->fetchChild<ItemSlotWidget>(widgetName)) {
|
||||
itemSlot->setProgress(progress);
|
||||
}
|
||||
});
|
||||
|
||||
callbacks.registerCallback("addFlowImage", [parentWidget](String const& widgetName, String const& childName, String const& image) {
|
||||
if (auto flow = parentWidget->fetchChild<FlowLayout>(widgetName)) {
|
||||
WidgetPtr newChild = make_shared<ImageWidget>(image);
|
||||
flow->addChild(childName, newChild);
|
||||
}
|
||||
});
|
||||
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
}
|
17
source/frontend/StarWidgetLuaBindings.hpp
Normal file
17
source/frontend/StarWidgetLuaBindings.hpp
Normal file
|
@ -0,0 +1,17 @@
|
|||
#ifndef STAR_WIDGET_LUA_BINDINGS_HPP
|
||||
#define STAR_WIDGET_LUA_BINDINGS_HPP
|
||||
|
||||
#include "StarLua.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(Widget);
|
||||
|
||||
namespace LuaBindings {
|
||||
LuaCallbacks makeWidgetCallbacks(Widget* parentWidget, GuiReader* reader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
332
source/frontend/StarWireInterface.cpp
Normal file
332
source/frontend/StarWireInterface.cpp
Normal file
|
@ -0,0 +1,332 @@
|
|||
#include "StarWireInterface.hpp"
|
||||
#include "StarGuiReader.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarWorldClient.hpp"
|
||||
#include "StarWireEntity.hpp"
|
||||
#include "StarWorldGeometry.hpp"
|
||||
#include "StarWorldPainter.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarTools.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
WirePane::WirePane(WorldClientPtr worldClient, PlayerPtr player, WorldPainterPtr worldPainter) {
|
||||
m_worldClient = worldClient;
|
||||
m_player = player;
|
||||
m_worldPainter = worldPainter;
|
||||
|
||||
m_connecting = false;
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
GuiReader reader;
|
||||
reader.construct(assets->json("/interface/wires/wires.config:gui"), this);
|
||||
|
||||
m_insize = Vec2F(context()->textureSize("/interface/wires/inbound.png")) / TilePixels;
|
||||
m_outsize = Vec2F(context()->textureSize("/interface/wires/outbound.png")) / TilePixels;
|
||||
m_nodesize = Vec2F(1.8f, 1.8f);
|
||||
|
||||
setTitle({}, "", "Wire you looking at me like that?");
|
||||
disableScissoring();
|
||||
markAsContainer();
|
||||
}
|
||||
|
||||
void WirePane::reset() {
|
||||
m_connecting = false;
|
||||
}
|
||||
|
||||
void WirePane::update() {
|
||||
if (!active())
|
||||
return;
|
||||
if (!m_worldClient->inWorld()) {
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_connecting) {
|
||||
for (auto entity : m_worldClient->atTile<WireEntity>(m_sourceConnector.entityLocation)) {
|
||||
if (m_sourceConnector.nodeIndex < entity->nodeCount(m_sourceDirection))
|
||||
return;
|
||||
}
|
||||
|
||||
// stop pending connection if node has been removed
|
||||
m_connecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
void WirePane::renderWire(Vec2F from, Vec2F to, Color baseColor) {
|
||||
if (m_worldClient->isTileProtected(Vec2I::floor(from)) || m_worldClient->isTileProtected(Vec2I::floor(to)))
|
||||
return;
|
||||
|
||||
from = m_worldPainter->camera().worldToScreen(from);
|
||||
to = m_worldPainter->camera().worldToScreen(to);
|
||||
|
||||
auto rangeRand = [&](float dev, float min, float max) {
|
||||
return clamp<float>(Random::nrandf(dev, max), min, max);
|
||||
};
|
||||
|
||||
float m_beamWidthDev;
|
||||
float m_minBeamWidth;
|
||||
float m_maxBeamWidth;
|
||||
float m_beamTransDev;
|
||||
float m_minBeamTrans;
|
||||
float m_maxBeamTrans;
|
||||
float m_innerBrightnessScale;
|
||||
float m_firstStripeThickness;
|
||||
float m_secondStripeThickness;
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
JsonObject config = assets->json("/player.config:beamGunConfig").toObject();
|
||||
m_minBeamWidth = config.get("minBeamWidth").toFloat();
|
||||
m_maxBeamWidth = config.get("maxBeamWidth").toFloat();
|
||||
m_beamWidthDev = config.value("beamWidthDev", (m_maxBeamWidth - m_minBeamWidth) / 3).toFloat();
|
||||
m_minBeamTrans = config.get("minBeamTrans").toFloat();
|
||||
m_maxBeamTrans = config.get("maxBeamTrans").toFloat();
|
||||
m_beamTransDev = config.value("beamTransDev", (m_maxBeamTrans - m_minBeamTrans) / 3).toFloat();
|
||||
m_innerBrightnessScale = config.get("innerBrightnessScale").toFloat();
|
||||
m_firstStripeThickness = config.get("firstStripeThickness").toFloat();
|
||||
m_secondStripeThickness = config.get("secondStripeThickness").toFloat();
|
||||
|
||||
float lineThickness = m_worldPainter->camera().pixelRatio() * rangeRand(m_beamWidthDev, m_minBeamWidth, m_maxBeamWidth);
|
||||
float beamTransparency = rangeRand(m_beamTransDev, m_minBeamTrans, m_maxBeamTrans);
|
||||
baseColor.setAlphaF(baseColor.alphaF() * beamTransparency);
|
||||
Color innerStripe = baseColor;
|
||||
innerStripe.setValue(1 - (1 - innerStripe.value()) / m_innerBrightnessScale);
|
||||
innerStripe.setSaturation(innerStripe.saturation() / m_innerBrightnessScale);
|
||||
Color firstStripe = innerStripe;
|
||||
innerStripe.setValue(1 - (1 - innerStripe.value()) / m_innerBrightnessScale);
|
||||
innerStripe.setSaturation(innerStripe.saturation() / m_innerBrightnessScale);
|
||||
Color secondStripe = innerStripe;
|
||||
|
||||
context()->drawLine(from, to, baseColor.toRgba(), lineThickness);
|
||||
context()->drawLine(from, to, firstStripe.toRgba(), lineThickness * m_firstStripeThickness);
|
||||
context()->drawLine(from, to, secondStripe.toRgba(), lineThickness * m_secondStripeThickness);
|
||||
}
|
||||
|
||||
void WirePane::renderImpl() {
|
||||
if (!m_worldClient->inWorld())
|
||||
return;
|
||||
|
||||
auto region = RectF(m_worldClient->clientWindow());
|
||||
|
||||
auto const& camera = m_worldPainter->camera();
|
||||
auto highWire = Color::Red;
|
||||
auto lowWire = Color::Red.mix(Color::Black, 0.8f);
|
||||
auto white = Color::White.toRgba();
|
||||
float phase = 0.5f + 0.5f * std::sin((double)Time::monotonicMilliseconds() / 100.0);
|
||||
auto drawLineColor = Color::Red.mix(Color::White, phase);
|
||||
|
||||
for (auto entity : m_worldClient->query<WireEntity>(region)) {
|
||||
for (size_t i = 0; i < entity->nodeCount(WireDirection::Input); ++i) {
|
||||
Vec2I position = entity->tilePosition() + entity->nodePosition({WireDirection::Input, i});
|
||||
if (!m_worldClient->isTileProtected(position)) {
|
||||
context()->drawQuad("/interface/wires/inbound.png",
|
||||
camera.worldToScreen(centerOfTile(position) - (m_insize / 2.0f)),
|
||||
camera.pixelRatio(), white);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < entity->nodeCount(WireDirection::Output); ++i) {
|
||||
Vec2I position = entity->tilePosition() + entity->nodePosition({WireDirection::Output, i});
|
||||
if (!m_worldClient->isTileProtected(position)) {
|
||||
context()->drawQuad("/interface/wires/outbound.png",
|
||||
camera.worldToScreen(centerOfTile(position) - (m_outsize / 2.0f)),
|
||||
camera.pixelRatio(), white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HashSet<pair<WireConnection, WireConnection>> visitedConnections;
|
||||
for (auto entity : m_worldClient->query<WireEntity>(region)) {
|
||||
for (size_t i = 0; i < entity->nodeCount(WireDirection::Input); ++i) {
|
||||
Vec2I tilePosition = entity->tilePosition();
|
||||
Vec2I inPosition = tilePosition + entity->nodePosition({WireDirection::Input, i});
|
||||
|
||||
for (auto const& connection : entity->connectionsForNode({WireDirection::Input, i})) {
|
||||
visitedConnections.add({{tilePosition, i}, connection});
|
||||
|
||||
auto wire = lowWire;
|
||||
Vec2I outPosition = connection.entityLocation;
|
||||
if (auto sourceEntity = m_worldClient->atTile<WireEntity>(connection.entityLocation).get(0)) {
|
||||
outPosition += sourceEntity->nodePosition({WireDirection::Output, connection.nodeIndex});
|
||||
if (sourceEntity->nodeState(WireNode{WireDirection::Output, connection.nodeIndex}))
|
||||
wire = highWire;
|
||||
}
|
||||
|
||||
renderWire(centerOfTile(inPosition), centerOfTile(outPosition), wire);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < entity->nodeCount(WireDirection::Output); ++i) {
|
||||
Vec2I tilePosition = entity->tilePosition();
|
||||
Vec2I outPosition = tilePosition + entity->nodePosition({WireDirection::Output, i});
|
||||
|
||||
auto wire = lowWire;
|
||||
if (entity->nodeState({WireDirection::Output, i}))
|
||||
wire = highWire;
|
||||
|
||||
for (auto const& connection : entity->connectionsForNode({WireDirection::Output, i})) {
|
||||
visitedConnections.contains({connection, {tilePosition, i}});
|
||||
|
||||
Vec2I inPosition = connection.entityLocation;
|
||||
if (auto sourceEntity = m_worldClient->atTile<WireEntity>(connection.entityLocation).get(0))
|
||||
inPosition += sourceEntity->nodePosition({WireDirection::Input, connection.nodeIndex});
|
||||
|
||||
renderWire(centerOfTile(outPosition), centerOfTile(inPosition), wire);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_connecting) {
|
||||
Vec2F aimPos = m_worldPainter->camera().screenToWorld(Vec2F(m_mousePos) * m_context->interfaceScale());
|
||||
Vec2I sourcePosition = m_sourceConnector.entityLocation;
|
||||
if (auto sourceEntity = m_worldClient->atTile<WireEntity>(m_sourceConnector.entityLocation).get(0)) {
|
||||
if (m_sourceDirection == WireDirection::Input)
|
||||
sourcePosition += sourceEntity->nodePosition({WireDirection::Input, m_sourceConnector.nodeIndex});
|
||||
else
|
||||
sourcePosition += sourceEntity->nodePosition({WireDirection::Output, m_sourceConnector.nodeIndex});
|
||||
}
|
||||
renderWire(centerOfTile(sourcePosition), aimPos, drawLineColor);
|
||||
}
|
||||
}
|
||||
|
||||
bool WirePane::sendEvent(InputEvent const& event) {
|
||||
if (event.is<MouseMoveEvent>())
|
||||
m_mousePos = *context()->mousePosition(event);
|
||||
|
||||
if (event.is<MouseButtonDownEvent>())
|
||||
m_mousePos = *context()->mousePosition(event);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
WireConnector::SwingResult WirePane::swing(WorldGeometry const& geometry, Vec2F pos, FireMode mode) {
|
||||
pos = geometry.xwrap(pos);
|
||||
|
||||
if (m_worldClient->isTileProtected((Vec2I)pos)) {
|
||||
m_connecting = false;
|
||||
return Protected;
|
||||
}
|
||||
|
||||
RectF bounds = {pos - Vec2F(16, 16), pos + Vec2F(16, 16)};
|
||||
|
||||
if (mode == FireMode::Primary) {
|
||||
Maybe<WireConnection> matchNode;
|
||||
WireDirection matchDirection = WireDirection::Output;
|
||||
float bestDist = 10000;
|
||||
for (auto entity : m_worldClient->query<WireEntity>(bounds)) {
|
||||
for (size_t i = 0; i < entity->nodeCount(WireDirection::Input); ++i) {
|
||||
RectF inbounds = RectF::withSize(centerOfTile(entity->tilePosition() + entity->nodePosition({WireDirection::Input, i})) - (m_nodesize / 2.0f), m_nodesize);
|
||||
if (geometry.rectContains(inbounds, pos)) {
|
||||
if (!matchNode) {
|
||||
matchNode = WireConnection{entity->tilePosition(), i};
|
||||
matchDirection = WireDirection::Input;
|
||||
bestDist = geometry.diff(centerOfTile(entity->tilePosition() + entity->nodePosition({WireDirection::Input, i})), pos).magnitudeSquared();
|
||||
} else {
|
||||
float thisDist = geometry.diff(centerOfTile(entity->tilePosition() + entity->nodePosition({WireDirection::Input, i})), pos).magnitudeSquared();
|
||||
if (thisDist < bestDist) {
|
||||
matchNode = WireConnection{entity->tilePosition(), i};
|
||||
matchDirection = WireDirection::Input;
|
||||
bestDist = thisDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < entity->nodeCount(WireDirection::Output); ++i) {
|
||||
RectF outbounds = RectF::withSize(centerOfTile(entity->tilePosition() + entity->nodePosition({WireDirection::Output, i})) - (m_nodesize / 2.0f), m_nodesize);
|
||||
if (geometry.rectContains(outbounds, pos)) {
|
||||
if (!matchNode) {
|
||||
matchNode = WireConnection{entity->tilePosition(), i};
|
||||
matchDirection = WireDirection::Output;
|
||||
bestDist = geometry.diff(centerOfTile(entity->tilePosition() + entity->nodePosition({WireDirection::Output, i})), pos).magnitudeSquared();
|
||||
} else {
|
||||
float thisDist = geometry.diff(centerOfTile(entity->tilePosition() + entity->nodePosition({WireDirection::Output, i})), pos).magnitudeSquared();
|
||||
if (thisDist < bestDist) {
|
||||
matchNode = WireConnection{entity->tilePosition(), i};
|
||||
matchDirection = WireDirection::Output;
|
||||
bestDist = thisDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchNode) {
|
||||
if (m_connecting) {
|
||||
if (m_sourceDirection == matchDirection) {
|
||||
return Mismatch;
|
||||
} else if (m_sourceConnector.entityLocation == matchNode->entityLocation) {
|
||||
return Mismatch;
|
||||
} else {
|
||||
m_connecting = false;
|
||||
if (matchDirection == WireDirection::Output)
|
||||
m_worldClient->connectWire(*matchNode, m_sourceConnector);
|
||||
else
|
||||
m_worldClient->connectWire(m_sourceConnector, *matchNode);
|
||||
}
|
||||
} else {
|
||||
m_connecting = true;
|
||||
m_sourceDirection = matchDirection;
|
||||
m_sourceConnector = *matchNode;
|
||||
}
|
||||
return Connect;
|
||||
}
|
||||
|
||||
} else {
|
||||
m_connecting = false;
|
||||
|
||||
Maybe<WireNode> matchNode;
|
||||
Maybe<Vec2I> matchPosition;
|
||||
float bestDist = 10000;
|
||||
for (auto entity : m_worldClient->query<WireEntity>(bounds)) {
|
||||
for (size_t i = 0; i < entity->nodeCount(WireDirection::Input); ++i) {
|
||||
RectF inbounds = RectF::withSize(centerOfTile(entity->tilePosition() + entity->nodePosition({WireDirection::Input, i})) - (m_nodesize / 2.0f), m_nodesize);
|
||||
if (geometry.rectContains(inbounds, pos) && entity->connectionsForNode({WireDirection::Input, i}).size() > 0) {
|
||||
if (!matchNode) {
|
||||
matchPosition = entity->tilePosition();
|
||||
matchNode = WireNode{WireDirection::Input, i};
|
||||
bestDist = geometry.diff(centerOfTile(entity->tilePosition() + entity->nodePosition({WireDirection::Input, i})), pos).magnitudeSquared();
|
||||
} else {
|
||||
float thisDist = geometry.diff(centerOfTile(entity->tilePosition() + entity->nodePosition({WireDirection::Input, i})), pos).magnitudeSquared();
|
||||
if (thisDist < bestDist) {
|
||||
matchPosition = entity->tilePosition();
|
||||
matchNode = WireNode{WireDirection::Input, i};
|
||||
bestDist = thisDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < entity->nodeCount(WireDirection::Output); ++i) {
|
||||
RectF outbounds = RectF::withSize(centerOfTile(entity->tilePosition() + entity->nodePosition({WireDirection::Output, i})) - (m_nodesize / 2.0f), m_nodesize);
|
||||
if (geometry.rectContains(outbounds, pos) && entity->connectionsForNode({WireDirection::Output, i}).size() > 0) {
|
||||
if (!matchNode) {
|
||||
matchPosition = entity->tilePosition();
|
||||
matchNode = WireNode{WireDirection::Output, i};
|
||||
bestDist = geometry.diff(centerOfTile(entity->tilePosition() + entity->nodePosition({WireDirection::Output, i})), pos).magnitudeSquared();
|
||||
} else {
|
||||
float thisDist = geometry.diff(centerOfTile(entity->tilePosition() + entity->nodePosition({WireDirection::Output, i})), pos).magnitudeSquared();
|
||||
if (thisDist < bestDist) {
|
||||
matchPosition = entity->tilePosition();
|
||||
matchNode = WireNode{WireDirection::Output, i};
|
||||
bestDist = thisDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchNode) {
|
||||
m_worldClient->disconnectAllWires(*matchPosition, *matchNode);
|
||||
return Connect;
|
||||
}
|
||||
}
|
||||
return Nothing;
|
||||
}
|
||||
|
||||
bool WirePane::connecting() {
|
||||
return m_connecting;
|
||||
}
|
||||
|
||||
}
|
48
source/frontend/StarWireInterface.hpp
Normal file
48
source/frontend/StarWireInterface.hpp
Normal file
|
@ -0,0 +1,48 @@
|
|||
#ifndef STAR_WIRE_INTERFACE_HPP
|
||||
#define STAR_WIRE_INTERFACE_HPP
|
||||
|
||||
#include "StarPane.hpp"
|
||||
#include "StarWiring.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(WorldClient);
|
||||
STAR_CLASS(WorldPainter);
|
||||
STAR_CLASS(Player);
|
||||
STAR_CLASS(WirePane);
|
||||
|
||||
class WirePane : public Pane, public WireConnector {
|
||||
public:
|
||||
WirePane(WorldClientPtr worldClient, PlayerPtr player, WorldPainterPtr worldPainter);
|
||||
virtual ~WirePane() {}
|
||||
|
||||
virtual void update() override;
|
||||
virtual bool sendEvent(InputEvent const& event) override;
|
||||
|
||||
virtual SwingResult swing(WorldGeometry const& geometry, Vec2F position, FireMode mode) override;
|
||||
virtual bool connecting() override;
|
||||
|
||||
virtual void reset();
|
||||
|
||||
protected:
|
||||
void renderImpl() override;
|
||||
|
||||
private:
|
||||
void renderWire(Vec2F from, Vec2F to, Color baseColor);
|
||||
|
||||
WorldClientPtr m_worldClient;
|
||||
PlayerPtr m_player;
|
||||
WorldPainterPtr m_worldPainter;
|
||||
Vec2I m_mousePos;
|
||||
bool m_connecting;
|
||||
WireDirection m_sourceDirection;
|
||||
WireConnection m_sourceConnector;
|
||||
|
||||
Vec2F m_insize;
|
||||
Vec2F m_outsize;
|
||||
Vec2F m_nodesize;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
Loading…
Add table
Add a link
Reference in a new issue