This commit is contained in:
Aria 2025-03-21 22:23:30 +11:00
commit 9c94d113d3
Signed by untrusted user who does not match committer: aria
GPG key ID: 19AB7AA462B8AB3B
10260 changed files with 1237388 additions and 0 deletions

View 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})

View 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());
}
}

View 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

View 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));
}
}

View 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

View 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));
}
}

View 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

View 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 {};
}
}

View 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

View 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();
}
}

View 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

View 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"));
}
}

View 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

View 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);
}
}

View 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

View 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;
}
}

View 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

View 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();
}
}

View 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

View 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");
}
}

View 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

View 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());
}
}
}
}

View 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

View 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();
}
}

View 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

View 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 {};
}
}

View 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

View 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);
}
}
}
}
}

View 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

View 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();
}
}

View 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

View 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();
}
}

View 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

View 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")));
}
}

View 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

View 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);
}
}
}

View 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

View 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));
}
}

View 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

View 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));
}
}
}
}
}

View 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

View 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();
}
}

View 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

View 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();
}
}

View 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

File diff suppressed because it is too large Load diff

View 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

View 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;
}
}

View 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

View 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);
}
}

View 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

View 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();
}
}
}
}

View 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

View 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());
}
}

View 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

View 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;
}
}

View 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

View 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);
}
}

View 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

View 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);
}
}

View 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

View 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));
}
}
}

View 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

View 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();
}
}

View 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

View 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;
}
}

View 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

View 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();
}
}
}

View 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

View 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);
}
}

View 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

View 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;
}
}

View 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

View 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;
}
}

View 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

View 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));
}
}

View 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

View 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();
}
}

View 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

View 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();
}
}
}

View 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

View 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();
}
}

View 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

View 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;
}
}

View 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

View 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;
}
}

View 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