4b0bc220e4
Context-specific (like per-world) timescales can also be added later
403 lines
15 KiB
C++
403 lines
15 KiB
C++
#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(float dt) {
|
|
if (!m_client->playerOnOwnShip())
|
|
dismiss();
|
|
|
|
Pane::update(dt);
|
|
|
|
m_showCrewButton->setVisibility(m_currentPage == AiPages::StatusPage);
|
|
m_showMissionsButton->setVisibility(m_currentPage == AiPages::StatusPage);
|
|
m_backButton->setVisibility(m_currentPage != AiPages::StatusPage);
|
|
|
|
m_staticAnimation.update(dt);
|
|
m_scanlineAnimation.update(dt);
|
|
|
|
if (m_currentSpeech) {
|
|
m_textLength += m_currentSpeech->speedModifier * m_aiDatabase->charactersPerSecond() * dt;
|
|
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(dt * m_currentSpeech->speedModifier);
|
|
} else {
|
|
setFaceAnimation("idle");
|
|
m_faceAnimation.second.update(dt);
|
|
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));
|
|
}
|
|
|
|
}
|