remove old quest system and assets, replace with v1 of new quest system
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "NpcDatabase.h"
|
||||
|
||||
bool UNpcDatabase::FindNpc(FName NpcID, FNpc& Npc) {
|
||||
for(int i = 0; i < AllNpcData.Num(); i++) {
|
||||
if(AllNpcData[i].NpcID == NpcID) {
|
||||
Npc = AllNpcData[i];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "NpcDatabase.generated.h"
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FNpc {
|
||||
GENERATED_USTRUCT_BODY()
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Npc")
|
||||
FName NpcID;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Npc")
|
||||
FName NpcName;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class PROJECTELERI_API UNpcDatabase : public UDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Quest")
|
||||
TArray<FNpc> AllNpcData;
|
||||
|
||||
bool FindNpc(FName NpcID, FNpc& Npc);
|
||||
};
|
||||
@@ -1,357 +0,0 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "OrderManager.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
|
||||
// Sets default values
|
||||
AOrderManager::AOrderManager()
|
||||
{
|
||||
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
}
|
||||
|
||||
void AOrderManager::OnDayPassedResponse() {
|
||||
UE_LOG(LogTemp, Warning, TEXT("Day passed, checking from Order Manager"));
|
||||
//Get all orders that can be accepted in this month, increase counter if found
|
||||
//If the month didn't change do not check it again, simply take cached results
|
||||
QuestQueue.Empty();
|
||||
for (int i = 0; i < QuestDatabase->AllQuestsByNpc.Num(); ++i) {
|
||||
for (int j = 0; j < QuestDatabase->AllQuestsByNpc[i].QuestList.Num(); ++j) {
|
||||
UE_LOG(LogTemp, Warning, TEXT("Checking quest with ID: %s"), *QuestDatabase->AllQuestsByNpc[i].QuestList[j].QuestID.ToString());
|
||||
FQuestItem questCompletionInfo;
|
||||
if(CheckIfQuestComplete(QuestDatabase->AllQuestsByNpc[i].QuestList[j].QuestID,questCompletionInfo)) {
|
||||
if(QuestDatabase->AllQuestsByNpc[i].QuestList[j].PrerequisiteQuestID.IsNone()
|
||||
|| !FindQuest(QuestDatabase->AllQuestsByNpc[i].QuestList[j].QuestID,questCompletionInfo)) {
|
||||
FQuest NextQuest;
|
||||
if(questCompletionInfo.ResponseIndex == -1) {
|
||||
QuestDatabase->GetQuestData(questCompletionInfo.QuestID, NextQuest);
|
||||
}else {
|
||||
FQuest Current;
|
||||
QuestDatabase->GetQuestData(questCompletionInfo.QuestID, Current);
|
||||
QuestDatabase->GetQuestData(Current.ResponsesQuestLinks[questCompletionInfo.ResponseIndex], NextQuest);
|
||||
}
|
||||
UE_LOG(LogTemp, Warning, TEXT("Days passed currently: %d"), TimeOfDayActor->GetAllDaysPassed());
|
||||
UE_LOG(LogTemp, Warning, TEXT("Quest appears on day: %d"), QuestDatabase->AllQuestsByNpc[i].QuestList[j].DaysFromLastQuest);
|
||||
if(QuestDatabase->AllQuestsByNpc[i].QuestList[j].PrerequisiteQuestID.IsNone()) {
|
||||
if(TimeOfDayActor->GetAllDaysPassed() >= QuestDatabase->AllQuestsByNpc[i].QuestList[j].DaysFromLastQuest) {
|
||||
QuestQueue.Add(NextQuest);
|
||||
}
|
||||
break;
|
||||
}else {
|
||||
int32 passedDays = TimeOfDayActor->GetAllDaysPassed() - questCompletionInfo.QuestCompleteDay;
|
||||
if(passedDays > 0)
|
||||
QuestQueue.Add(NextQuest);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}else {
|
||||
if(QuestDatabase->AllQuestsByNpc[i].QuestList[j].PrerequisiteQuestID.IsNone()) {
|
||||
if(TimeOfDayActor->GetAllDaysPassed() >= QuestDatabase->AllQuestsByNpc[i].QuestList[j].DaysFromLastQuest) {
|
||||
QuestQueue.Add(QuestDatabase->AllQuestsByNpc[i].QuestList[j]);
|
||||
}
|
||||
break;
|
||||
}else {
|
||||
int32 passedDays = TimeOfDayActor->GetAllDaysPassed() - questCompletionInfo.QuestCompleteDay;
|
||||
if(passedDays > 0)
|
||||
QuestQueue.Add(QuestDatabase->AllQuestsByNpc[i].QuestList[j]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(QuestQueue.Num() >= 8)
|
||||
break;
|
||||
}
|
||||
UE_LOG(LogTemp, Warning, TEXT("Number of quests in the queue: %d"), QuestQueue.Num());
|
||||
|
||||
//Limit orders to 1 per NPC and maximum of 5 NPCs,
|
||||
//check logs to see how many orders player has left over from last day,
|
||||
//if they didn't complete anything only add 1/2 new orders, if they did then add up to 5
|
||||
/*if(QuestQueue.Num() > 0) {
|
||||
int32 questAddCounter = 1;
|
||||
if(QuestsCompletedLastDay > 1) {
|
||||
questAddCounter = 5;
|
||||
if(questAddCounter > QuestQueue.Num())
|
||||
questAddCounter = QuestQueue.Num();
|
||||
}
|
||||
for (int i = 0; i < questAddCounter; ++i) {
|
||||
AcceptQuest(QuestQueue[0].QuestID,TimeOfDayActor->GetAllDaysPassed());
|
||||
QuestQueue.RemoveAt(0);
|
||||
}
|
||||
QuestsCompletedLastDay = 2;
|
||||
}*/
|
||||
for (int i = 0; i < QuestQueue.Num(); ++i) {
|
||||
AcceptQuest(QuestQueue[i].QuestID,TimeOfDayActor->GetAllDaysPassed());
|
||||
}
|
||||
}
|
||||
|
||||
// Called when the game starts or when spawned
|
||||
void AOrderManager::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
TimeOfDayActor = Cast<ATimeOfDayManager, AActor>(UGameplayStatics::GetActorOfClass(GetWorld(), ATimeOfDayManager::StaticClass()));
|
||||
TimeOfDayActor->OnDayPassed.AddDynamic(this,&AOrderManager::OnDayPassedResponse);
|
||||
|
||||
}
|
||||
|
||||
// Called every frame
|
||||
void AOrderManager::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
|
||||
}
|
||||
|
||||
FQuest AOrderManager::FindQuest_Implementation(FName QuestID, bool& sucess) {
|
||||
sucess = false;
|
||||
FQuest Quest;
|
||||
if (QuestDatabase == nullptr) {return Quest;}
|
||||
for (int i = 0; i < QuestDatabase->AllQuestsByNpc.Num(); i++) {
|
||||
for (int j = 0; j < QuestDatabase->AllQuestsByNpc[i].QuestList.Num(); ++j) {
|
||||
if(QuestDatabase->AllQuestsByNpc[i].QuestList[j].QuestID == QuestID) {
|
||||
sucess = true;
|
||||
return QuestDatabase->AllQuestsByNpc[i].QuestList[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return Quest;
|
||||
}
|
||||
|
||||
TArray<FQuest> AOrderManager::GetAllAcceptedQuests(FName NpcID) {
|
||||
TArray<FQuest> acceptedQuests;
|
||||
for (int i = 0; i < QuestList.Num(); i++) {
|
||||
FQuest Data;
|
||||
if(QuestDatabase->GetQuestData(QuestList[i].QuestID,Data)
|
||||
&& Data.NpcID == NpcID) {
|
||||
FName ID = QuestList[i].QuestID;
|
||||
FQuest questToAdd;
|
||||
if(!QuestDatabase->GetQuestData(ID,questToAdd))
|
||||
continue;
|
||||
acceptedQuests.Add(questToAdd);
|
||||
}
|
||||
}
|
||||
return acceptedQuests;
|
||||
}
|
||||
|
||||
TArray<FNpcQuest> AOrderManager::GetAllDifferentNpcs() {
|
||||
TArray<FNpcQuest> allNpcQuests;
|
||||
for (int k = 0; k < QuestDatabase->AllQuestsByNpc.Num(); ++k) {
|
||||
TArray<FQuest> acceptedQuests = GetAllAcceptedQuests(QuestDatabase->AllQuestsByNpc[k].NpcID);
|
||||
for(int i = 0; i < acceptedQuests.Num(); i++) {
|
||||
int alreadyFound = -1;
|
||||
for(int j = 0; j < allNpcQuests.Num(); j++) {
|
||||
if(acceptedQuests[i].NpcID == allNpcQuests[j].NpcID)
|
||||
alreadyFound = j;
|
||||
}
|
||||
if(alreadyFound == -1) {
|
||||
FNpcQuest questToAdd;
|
||||
questToAdd.NpcID = acceptedQuests[i].NpcID;
|
||||
questToAdd.QuestIDList.Add(acceptedQuests[i].QuestID);
|
||||
allNpcQuests.Add(questToAdd);
|
||||
}else {
|
||||
allNpcQuests[alreadyFound].QuestIDList.Add(acceptedQuests[i].QuestID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Warning, TEXT("Returning %d quests"), allNpcQuests.Num());
|
||||
return allNpcQuests;
|
||||
}
|
||||
|
||||
FNpc AOrderManager::GetNpc(FName NpcID) {
|
||||
FNpc* npcToReturn = new FNpc();
|
||||
NpcDatabase->FindNpc(NpcID,*npcToReturn);
|
||||
return *npcToReturn;
|
||||
}
|
||||
|
||||
TArray<FNpcQuest> AOrderManager::GetQuestsByNpc() {
|
||||
TArray<FNpcQuest> allNpcQuests;
|
||||
for(int i = 0; i < QuestList.Num(); i++) {
|
||||
int alreadyFound = -1;
|
||||
FQuest questToCompare;
|
||||
for(int j = 0; j < allNpcQuests.Num(); j++) {
|
||||
if(QuestDatabase->GetQuestData(QuestList[i].QuestID,questToCompare)) {
|
||||
if(questToCompare.NpcID == allNpcQuests[j].NpcID)
|
||||
alreadyFound = j;
|
||||
}
|
||||
}
|
||||
if(alreadyFound == -1) {
|
||||
FNpcQuest questToAdd;
|
||||
questToAdd.NpcID = questToCompare.NpcID;
|
||||
questToAdd.QuestIDList.Add(questToCompare.QuestID);
|
||||
allNpcQuests.Add(questToAdd);
|
||||
}else {
|
||||
allNpcQuests[alreadyFound].QuestIDList.Add(questToCompare.QuestID);
|
||||
}
|
||||
}
|
||||
return allNpcQuests;
|
||||
}
|
||||
|
||||
bool AOrderManager::FindQuest(FName QuestID, FQuestItem& Quest) {
|
||||
for(int i = 0; i < QuestList.Num(); i++) {
|
||||
if(QuestList[i].QuestID == QuestID) {
|
||||
Quest = QuestList[i];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AOrderManager::ReadQuest(FName QuestID) {
|
||||
for (int i = 0; i < QuestList.Num(); ++i) {
|
||||
if(QuestList[i].QuestID == QuestID) {
|
||||
QuestList[i].ReadQuest = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AOrderManager::MarkQuestCompleted(FName QuestID) {
|
||||
for(int i = 0; i < QuestList.Num(); i++) {
|
||||
if((QuestList[i].QuestID == QuestID) && (!QuestList[i].IsCompleted)) {
|
||||
QuestList[i].IsCompleted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
//TODO
|
||||
//Implement update quest list event
|
||||
}
|
||||
|
||||
bool AOrderManager::CheckIfPrerequisiteQuestIsCompleted(FName QuestID) {
|
||||
int32 indexFound = -1;
|
||||
for (int i = 0; i < QuestList.Num(); ++i) {
|
||||
if(QuestList[i].QuestID == QuestID)
|
||||
indexFound = i;
|
||||
}
|
||||
if(QuestList[indexFound].IsCompleted)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AOrderManager::CheckIfQuestComplete(FName QuestID, FQuestItem& QuestItem) {
|
||||
for (int i = 0; i < QuestList.Num(); ++i) {
|
||||
if(QuestList[i].QuestID == QuestID) {
|
||||
QuestItem = QuestList[i];
|
||||
return QuestList[i].IsCompleted;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
TArray<FQuest> AOrderManager::GetIncompleteQuests() {
|
||||
TArray<FQuest> incomplete;
|
||||
for (int i = 0; i < QuestDatabase->AllQuestsByNpc.Num(); ++i) {
|
||||
for (int j = 0; j < QuestDatabase->AllQuestsByNpc[i].QuestList.Num(); ++j) {
|
||||
bool found = false;
|
||||
for (int k = 0; k < QuestList.Num(); ++k) {
|
||||
if(QuestDatabase->AllQuestsByNpc[i].QuestList[j].QuestID ==
|
||||
QuestList[j].QuestID && QuestList[j].IsCompleted)
|
||||
found = true;
|
||||
}
|
||||
if(!found)
|
||||
incomplete.Add(QuestDatabase->AllQuestsByNpc[i].QuestList[j]);
|
||||
}
|
||||
}
|
||||
return incomplete;
|
||||
}
|
||||
|
||||
void AOrderManager::AcceptQuest(FName QuestID, int32 currentDay, int32 response, TArray<FName> itemArray) {
|
||||
bool QuestFound = false;
|
||||
for(int i = 0; i < QuestList.Num(); i++) {
|
||||
if(QuestList[i].QuestID == QuestID) {
|
||||
QuestFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!QuestFound) {
|
||||
FQuestItem NewQuest;
|
||||
NewQuest.QuestID = QuestID;
|
||||
NewQuest.IsCompleted = false;
|
||||
NewQuest.QuestAcceptDay = currentDay;
|
||||
NewQuest.ResponseIndex = response;
|
||||
NewQuest.ItemIDsSubmitted = itemArray;
|
||||
QuestList.Add(NewQuest);
|
||||
//TODO
|
||||
//Implement update quest list event
|
||||
}
|
||||
}
|
||||
void AOrderManager::AcceptQuest(FName QuestID, int32 currentDay) {
|
||||
bool QuestFound = false;
|
||||
for(int i = 0; i < QuestList.Num(); i++) {
|
||||
if(QuestList[i].QuestID == QuestID) {
|
||||
QuestFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!QuestFound) {
|
||||
FQuestItem NewQuest;
|
||||
NewQuest.QuestID = QuestID;
|
||||
NewQuest.IsCompleted = false;
|
||||
NewQuest.QuestAcceptDay = currentDay;
|
||||
NewQuest.QuestCompleteDay = -1;
|
||||
QuestList.Add(NewQuest);
|
||||
//TODO
|
||||
//Implement update quest list event
|
||||
}
|
||||
}
|
||||
|
||||
bool AOrderManager::SubmitAnswerWithItems(FName QuestID, int32 ResponseIndex, TArray<FName> ItemIndexes) {
|
||||
FQuest questToLookAt;
|
||||
for (int i = 0; i < QuestList.Num(); ++i) {
|
||||
if(QuestList[i].QuestID == QuestID
|
||||
&& QuestDatabase->GetQuestData(QuestList[i].QuestID,questToLookAt)) {
|
||||
int itemsToCheck = ItemIndexes.Num();
|
||||
for (int j = 0; j < ItemIndexes.Num(); ++j) {
|
||||
for (int k = 0; k < questToLookAt.QuestItemRequest.Num(); ++k) {
|
||||
if(questToLookAt.QuestItemRequest[k].ItemID == ItemIndexes[j])
|
||||
itemsToCheck--;
|
||||
}
|
||||
}
|
||||
if(itemsToCheck == 0) {
|
||||
QuestList[i].ResponseIndex = ResponseIndex;
|
||||
QuestList[i].ItemIDsSubmitted = ItemIndexes;
|
||||
QuestList[i].IsCompleted = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AOrderManager::SubmitAnswer(FName QuestID, int32 ResponseIndex) {
|
||||
for (int i = 0; i < QuestList.Num(); ++i) {
|
||||
if(QuestList[i].QuestID == QuestID) {
|
||||
QuestList[i].ResponseIndex = ResponseIndex;
|
||||
QuestList[i].IsCompleted = true;
|
||||
UE_LOG(LogTemp, Warning, TEXT("Quest response recorded fot quest with ID:%s"), *QuestID.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FQuest AOrderManager::GetQuestInfo(FName QuestID) {
|
||||
FQuest quest;
|
||||
QuestDatabase->GetQuestData(QuestID, quest);
|
||||
return quest;
|
||||
}
|
||||
|
||||
TArray<FQuestItem> AOrderManager::GetAllAcceptedQuestForSaving() {
|
||||
TArray<FQuestItem> quests;
|
||||
quests = QuestList;
|
||||
return quests;
|
||||
}
|
||||
|
||||
void AOrderManager::LoadQuestsIntoMemory(TArray<FQuestItem> quests) {
|
||||
QuestList = quests;
|
||||
}
|
||||
|
||||
|
||||
FQuestItem AOrderManager::GetAcceptedQuestInfo(FName QuestID) {
|
||||
FQuestItem quest;
|
||||
for (int i = 0; i < QuestList.Num(); ++i) {
|
||||
if(QuestID == QuestList[i].QuestID) {
|
||||
quest = QuestList[i];
|
||||
}
|
||||
}
|
||||
return quest;
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "NpcDatabase.h"
|
||||
#include "QuestDatabase.h"
|
||||
#include "TimeOfDayManager.h"
|
||||
#include "QuestDatabase.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "OrderManager.generated.h"
|
||||
|
||||
UCLASS(Blueprintable)
|
||||
class PROJECTELERI_API AOrderManager : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// Sets default values for this actor's properties
|
||||
AOrderManager();
|
||||
|
||||
//TODO Save variable
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
int32 LastCheckedMonth;
|
||||
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TArray<FQuest> QuestQueue;
|
||||
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
ATimeOfDayManager* TimeOfDayActor;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly)
|
||||
UQuestDatabase* QuestDatabase;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly)
|
||||
UNpcDatabase* NpcDatabase;
|
||||
|
||||
UPROPERTY(EditAnywhere)
|
||||
int32 QuestsCompletedLastDay = 2;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Quest")
|
||||
TArray<FQuestItem> QuestList;
|
||||
|
||||
UFUNCTION()
|
||||
void OnDayPassedResponse();
|
||||
|
||||
protected:
|
||||
// Called when the game starts or when spawned
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
public:
|
||||
// Called every frame
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="Quests")
|
||||
FQuest FindQuest(FName QuestID, bool& sucess);
|
||||
FQuest FindQuest_Implementation(FName QuestID, bool& sucess);
|
||||
|
||||
bool FindQuest(FName QuestID, FQuestItem& Quest);
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void ReadQuest(FName QuestID);
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void MarkQuestCompleted(FName QuestID);
|
||||
bool CheckIfPrerequisiteQuestIsCompleted(FName QuestID);
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
bool CheckIfQuestComplete(FName QuestID, FQuestItem& QuestItem);
|
||||
|
||||
TArray<FQuest> GetIncompleteQuests();
|
||||
void AcceptQuest(FName QuestID, int32 currentDay, int32 response, TArray<FName> itemArray);
|
||||
void AcceptQuest(FName QuestID, int32 currentDay);
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
bool SubmitAnswerWithItems(FName QuestID, int32 ResponseIndex, TArray<FName> ItemIndexes);
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void SubmitAnswer(FName QuestID, int32 ResponseIndex);
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
FQuest GetQuestInfo(FName QuestID);
|
||||
|
||||
//Helpers for accepted quests
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
TArray<FQuestItem> GetAllAcceptedQuestForSaving();
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void LoadQuestsIntoMemory(TArray<FQuestItem> quests);
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
FQuestItem GetAcceptedQuestInfo(FName QuestID);
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
TArray<FQuest> GetAllAcceptedQuests(FName NpcID);
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
TArray<FNpcQuest> GetAllDifferentNpcs();
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
FNpc GetNpc(FName NpcID);
|
||||
|
||||
|
||||
//Helpers for unaccepted quests
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
TArray<FNpcQuest> GetQuestsByNpc();
|
||||
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
#include "PlayerQuest.h"
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -116,6 +116,13 @@ struct FQuests {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* The original, name based quest asset.
|
||||
*
|
||||
* Superseded by UQuestDataAsset, which keys a quest by gameplay tag instead of by FName, holds its steps
|
||||
* as data rather than as dialogue responses, and is driven by UQuestSubsystem. Left in place because
|
||||
* QuestData.uasset is content this still reads; a new quest belongs in a UQuestDataAsset.
|
||||
*/
|
||||
UCLASS()
|
||||
class PROJECTELERI_API UQuestDatabase : public UDataAsset
|
||||
{
|
||||
|
||||
@@ -44,9 +44,6 @@ public:
|
||||
|
||||
static UItemDatabase* Get(const UObject* WorldContext);
|
||||
|
||||
UPROPERTY(EditDefaultsOnly)
|
||||
UItemContainer* ItemDatabase;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Alchemy Settings")
|
||||
TMap<Element, int32> ElementStrength;
|
||||
|
||||
|
||||
@@ -54,6 +54,11 @@ void UEleriGameInstance::SaveGame()
|
||||
|
||||
if(!SaveGameData || !PlayerChar || !EleriGameState) return;
|
||||
|
||||
// Stamped here rather than in the constructor, because the same class is what a load deserializes
|
||||
// into: a version set by the constructor would make every save made before the field existed read
|
||||
// as a current one.
|
||||
SaveGameData->SaveVersion = UEleriSaveGame::CurrentSaveVersion;
|
||||
|
||||
SaveGameData->SaveData = EleriGameState->PrepareSaveData();
|
||||
|
||||
FAsyncSaveGameToSlotDelegate SavedDelegate;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "ProjectEleri/GameObjects/EleriBaseActor.h"
|
||||
#include "ProjectEleri/Ai/NpcScheduleComponent.h"
|
||||
#include "ProjectEleri/GameObjects/BaseCharacter.h"
|
||||
#include "ProjectEleri/QuestSystem/QuestSubsystem.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -250,6 +251,24 @@ void AEleriGameState::PropagateSaveData(UEleriSaveGame* InSaveData)
|
||||
}
|
||||
}
|
||||
|
||||
//Quests. Placed after the inventories above, because a step that is counted from the inventory is
|
||||
//read from it the moment the quest state is put back, and before the seed restore below, which the
|
||||
//comment on it asks of everything else.
|
||||
if (UQuestSubsystem* QuestSubsystem = UQuestSubsystem::Get(this))
|
||||
{
|
||||
//The version is only reported for now: a save made before any of this existed simply has no
|
||||
//quest data in it, and an empty list is exactly what a fresh quest state is, so there is
|
||||
//nothing to migrate yet. The field is what makes the first real change to the quest save
|
||||
//layout visible instead of silent.
|
||||
if (InSaveData->SaveVersion < UEleriSaveGame::CurrentSaveVersion)
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("Save was written with version %d, this build reads version %d."),
|
||||
InSaveData->SaveVersion, UEleriSaveGame::CurrentSaveVersion);
|
||||
}
|
||||
|
||||
QuestSubsystem->ApplySaveData(SaveData.QuestSaveData);
|
||||
}
|
||||
|
||||
//Seed delete and spawn should always go last
|
||||
PropagateAllSeedData();
|
||||
|
||||
@@ -317,6 +336,12 @@ const FSaveData& AEleriGameState::PrepareSaveData()
|
||||
SaveData.SetPlayerData(GetWorld());
|
||||
SaveData.SetAbilitySystemData(GetWorld());
|
||||
|
||||
//SAVE QUESTS
|
||||
if (UQuestSubsystem* QuestSubsystem = UQuestSubsystem::Get(this))
|
||||
{
|
||||
QuestSubsystem->PrepareSaveData(SaveData.QuestSaveData);
|
||||
}
|
||||
|
||||
return SaveData;
|
||||
}
|
||||
|
||||
|
||||
@@ -197,10 +197,6 @@ void AEleriPlayerController::SetupInputComponent()
|
||||
{
|
||||
PlayerEnhancedInputComponent->BindAction(BookAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnBookToggle);
|
||||
}
|
||||
if (OrdersAction)
|
||||
{
|
||||
PlayerEnhancedInputComponent->BindAction(OrdersAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnOrdersToggle);
|
||||
}
|
||||
if (CalendarAction)
|
||||
{
|
||||
PlayerEnhancedInputComponent->BindAction(CalendarAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnCalendarToggle);
|
||||
@@ -912,21 +908,6 @@ void AEleriPlayerController::OnBookToggle(const FInputActionValue &Value)
|
||||
OpenBookOfAlchemy(nullptr);
|
||||
}
|
||||
|
||||
void AEleriPlayerController::OnOrdersToggle(const FInputActionValue &Value)
|
||||
{
|
||||
if (Paused || IsDialogueOpen() || IsInMenu() || !MoveActorsComp || MoveActorsComp->IsRemovingMode())
|
||||
return;
|
||||
|
||||
if (OrdersOpen)
|
||||
{
|
||||
CloseOrdersUI();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void AEleriPlayerController::OnCalendarToggle(const FInputActionValue &Value)
|
||||
{
|
||||
if (Paused || IsDialogueOpen() || (IsInMenu() && !MainGameUI->IsCalendarOpen()) || !MoveActorsComp || MoveActorsComp->IsRemovingMode())
|
||||
@@ -1090,18 +1071,6 @@ void AEleriPlayerController::DisableBookAnimating()
|
||||
TickBookTimer = 0.6f;
|
||||
}
|
||||
|
||||
void AEleriPlayerController::CloseOrdersUI()
|
||||
{
|
||||
OrdersOpen = false;
|
||||
|
||||
OrdersUI->RemoveFromParent();
|
||||
if (MenuStack.Num() == 0)
|
||||
{
|
||||
SetShowMouseCursor(false);
|
||||
SetInputMode(FInputModeGameOnly());
|
||||
}
|
||||
}
|
||||
|
||||
UMainMenuWidget *AEleriPlayerController::OpenMainMenu()
|
||||
{
|
||||
if (!IsValid(MainGameUI))
|
||||
|
||||
@@ -11,11 +11,19 @@
|
||||
#include "ProjectEleri/Ai/NpcScheduleComponent.h"
|
||||
#include "ProjectEleri/GameObjects/BaseCharacter.h"
|
||||
|
||||
UEleriSaveGame::UEleriSaveGame()
|
||||
{
|
||||
// Deliberately not the current version: this runs for a load as well as for a new save, and an old
|
||||
// file would then look like a new one. The version is stamped by whoever writes the save.
|
||||
SaveVersion = 0;
|
||||
}
|
||||
|
||||
void FSaveData::Initialize()
|
||||
{
|
||||
BookProgression.Empty();
|
||||
InventoryData.Empty();
|
||||
QuestList.Empty();
|
||||
QuestSaveData.Empty();
|
||||
SeedBedData.Empty();
|
||||
PlacedActors.Empty();
|
||||
ActorSaveData.Empty();
|
||||
|
||||
@@ -90,11 +90,6 @@ private:
|
||||
UPROPERTY()
|
||||
UMainGameWidget* MainGameUI;
|
||||
|
||||
UPROPERTY()
|
||||
bool OrdersOpen;
|
||||
UPROPERTY()
|
||||
UUserWidget* OrdersUI;
|
||||
|
||||
UPROPERTY()
|
||||
bool TickBookAnimating = false;
|
||||
UPROPERTY()
|
||||
@@ -144,9 +139,6 @@ public:
|
||||
UPROPERTY(EditDefaultsOnly, Category = "UI")
|
||||
TSubclassOf<UMainGameWidget> MainGameWidgetBlueprint;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, Category = "UI")
|
||||
TSubclassOf<UUserWidget> OrdersBlueprint;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, Category = "UI")
|
||||
TSubclassOf<UEleriBaseWidget> AlchemyUiBlueprint;
|
||||
|
||||
@@ -195,8 +187,6 @@ public:
|
||||
UPROPERTY(EditDefaultsOnly, Category = "UI Input")
|
||||
UInputAction* BookAction;
|
||||
UPROPERTY(EditDefaultsOnly, Category = "UI Input")
|
||||
UInputAction* OrdersAction;
|
||||
UPROPERTY(EditDefaultsOnly, Category = "UI Input")
|
||||
UInputAction* CalendarAction;
|
||||
UPROPERTY(EditDefaultsOnly, Category = "UI Input")
|
||||
UInputAction* MapAction;
|
||||
@@ -331,7 +321,6 @@ public:
|
||||
void OnMainMenuToggle(const FInputActionValue& Value);
|
||||
void OnInventoryToggle(const FInputActionValue& Value);
|
||||
void OnBookToggle(const FInputActionValue& Value);
|
||||
void OnOrdersToggle(const FInputActionValue& Value);
|
||||
void OnCalendarToggle(const FInputActionValue& Value);
|
||||
void OnMapToggle(const FInputActionValue& Value);
|
||||
|
||||
@@ -356,9 +345,6 @@ public:
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void CloseBookOfAlchemy();
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void CloseOrdersUI();
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
UMainMenuWidget* OpenMainMenu();
|
||||
UFUNCTION(BlueprintCallable)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "../System/Subsystem/ObjectPersistenceSubsystem.h"
|
||||
#include "Interface/SaveableObjectInterface.h"
|
||||
#include "ProjectEleri/GameEventSystem/GameEventSubsystem.h"
|
||||
#include "ProjectEleri/QuestSystem/QuestTypes.h"
|
||||
#include "Data/GameplayStatEffect.h"
|
||||
#include "Data/StatForgeDefs.h"
|
||||
#include "ProjectEleri/Items/SeedBedActor.h"
|
||||
@@ -56,6 +57,17 @@ struct FSaveData {
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Quest")
|
||||
TArray<FQuestItem> QuestList;
|
||||
|
||||
/**
|
||||
* The quest system's own state, one entry per quest the player has touched.
|
||||
*
|
||||
* Kept next to the legacy list above rather than replacing it, because the order manager is still
|
||||
* the only thing writing that one and a save that carried both is a save both can read. An empty
|
||||
* array is what a fresh quest state is, so a save made before any of this existed loads through
|
||||
* here without needing a migration.
|
||||
*/
|
||||
UPROPERTY()
|
||||
TArray<FQuestSaveEntry> QuestSaveData;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seeds")
|
||||
TMap<int32, FSeedBedSaveData> SeedBedData;
|
||||
|
||||
@@ -106,8 +118,24 @@ class PROJECTELERI_API UEleriSaveGame : public USaveGame
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
|
||||
UEleriSaveGame();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FSaveData SaveData;
|
||||
|
||||
/**
|
||||
* Which layout this file was written with.
|
||||
*
|
||||
* Zero, the default, is what a save made before this field existed deserializes to: an absent
|
||||
* property is left at the value the constructor gave it, which is why the stamp is applied when a
|
||||
* save is written rather than being set in the constructor. Without that, every old save would read
|
||||
* as current and there would be no way to tell them apart.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Version")
|
||||
int32 SaveVersion = 0;
|
||||
|
||||
/** The layout this build writes. Raise it when something is added to FSaveData that an older save
|
||||
* cannot be read through, and handle the difference where that field is read back. */
|
||||
static constexpr int32 CurrentSaveVersion = 1;
|
||||
};
|
||||
|
||||
174
Source/ProjectEleri/QuestSystem/QuestDataAsset.cpp
Normal file
174
Source/ProjectEleri/QuestSystem/QuestDataAsset.cpp
Normal file
@@ -0,0 +1,174 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "QuestDataAsset.h"
|
||||
|
||||
#include "ProjectEleri/GameEventSystem/GameEventRequirements.h"
|
||||
#include "ProjectEleri/Items/DataAssets/ItemDataAsset.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
/** Every entry has to pass. An entry that is not a requirement at all is skipped rather than
|
||||
* failing the quest, so one unreadable condition cannot take a whole quest off the board. */
|
||||
bool AreRequirementsMet(const TArray<FInstancedStruct>& Requirements, const UObject* Context)
|
||||
{
|
||||
for (const FInstancedStruct& Requirement : Requirements)
|
||||
{
|
||||
const FGameEventRequirement* Typed = Requirement.GetPtr<FGameEventRequirement>();
|
||||
if (Typed == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Typed->Test(Context))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool UQuestDataAsset::AreOfferRequirementsMet(const UObject* TestContext) const
|
||||
{
|
||||
return AreRequirementsMet(OfferRequirements, TestContext);
|
||||
}
|
||||
|
||||
bool UQuestDataAsset::AreTurnInRequirementsMet(const UObject* TestContext) const
|
||||
{
|
||||
return AreRequirementsMet(TurnInRequirements, TestContext);
|
||||
}
|
||||
|
||||
int32 UQuestDataAsset::FindObjectiveIndex(FName ObjectiveId) const
|
||||
{
|
||||
if (ObjectiveId.IsNone())
|
||||
{
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
for (int32 Index = 0; Index < Objectives.Num(); ++Index)
|
||||
{
|
||||
if (Objectives[Index].ObjectiveId == ObjectiveId)
|
||||
{
|
||||
return Index;
|
||||
}
|
||||
}
|
||||
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
const FQuestObjective* UQuestDataAsset::FindObjective(FName ObjectiveId) const
|
||||
{
|
||||
const int32 Index = FindObjectiveIndex(ObjectiveId);
|
||||
return Objectives.IsValidIndex(Index) ? &Objectives[Index] : nullptr;
|
||||
}
|
||||
|
||||
bool UQuestDataAsset::HasDeliverStep() const
|
||||
{
|
||||
return Objectives.ContainsByPredicate(
|
||||
[](const FQuestObjective& Objective) { return Objective.StepType == EQuestStepType::DeliverItems; });
|
||||
}
|
||||
|
||||
int32 UQuestDataAsset::CountRequiredObjectives() const
|
||||
{
|
||||
int32 Count = 0;
|
||||
for (const FQuestObjective& Objective : Objectives)
|
||||
{
|
||||
if (!Objective.bOptional)
|
||||
{
|
||||
++Count;
|
||||
}
|
||||
}
|
||||
|
||||
return Count;
|
||||
}
|
||||
|
||||
bool UQuestDataAsset::IsObjectiveUnlocked(int32 Index, const FQuestRuntimeState& State) const
|
||||
{
|
||||
if (!bSequentialObjectives || Index <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Everything before it has to be done, optional steps included: "in order" means the step above is
|
||||
// finished, and an optional step that is still open is one the player has not done yet.
|
||||
if (!Objectives.IsValidIndex(Index))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int32 Earlier = 0; Earlier < Index; ++Earlier)
|
||||
{
|
||||
const FQuestObjectiveProgress* Progress = State.FindProgress(Objectives[Earlier].ObjectiveId);
|
||||
if (Progress == nullptr || !Progress->bComplete)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void UQuestDataAsset::CollectDeliverables(const FQuestRuntimeState& State, TArray<TPair<FPrimaryAssetId, int32>>& OutItems) const
|
||||
{
|
||||
for (const FQuestObjective& Objective : Objectives)
|
||||
{
|
||||
if (Objective.StepType != EQuestStepType::DeliverItems || Objective.bOptional)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FQuestObjectiveProgress* Progress = State.FindProgress(Objective.ObjectiveId);
|
||||
if (Progress == nullptr || !Progress->bComplete)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FPrimaryAssetId ItemId = Objective.TargetItem.LoadSynchronous()
|
||||
? Objective.TargetItem->GetPrimaryAssetId()
|
||||
: FPrimaryAssetId();
|
||||
|
||||
if (!ItemId.IsValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
OutItems.Emplace(ItemId, FMath::Max(1, Objective.RequiredCount));
|
||||
}
|
||||
}
|
||||
|
||||
bool UQuestDataAsset::IsValidQuest(FString& OutError) const
|
||||
{
|
||||
if (!QuestTag.IsValid())
|
||||
{
|
||||
OutError = TEXT("no quest tag");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Objectives.Num() == 0)
|
||||
{
|
||||
OutError = TEXT("no objectives");
|
||||
return false;
|
||||
}
|
||||
|
||||
TSet<FName> SeenIds;
|
||||
for (const FQuestObjective& Objective : Objectives)
|
||||
{
|
||||
if (Objective.ObjectiveId.IsNone())
|
||||
{
|
||||
OutError = TEXT("an objective has no id");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SeenIds.Contains(Objective.ObjectiveId))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("objective id '%s' is used twice"), *Objective.ObjectiveId.ToString());
|
||||
return false;
|
||||
}
|
||||
|
||||
SeenIds.Add(Objective.ObjectiveId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
120
Source/ProjectEleri/QuestSystem/QuestDataAsset.h
Normal file
120
Source/ProjectEleri/QuestSystem/QuestDataAsset.h
Normal file
@@ -0,0 +1,120 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "QuestTypes.h"
|
||||
|
||||
#include "QuestDataAsset.generated.h"
|
||||
|
||||
class UTexture2D;
|
||||
|
||||
/**
|
||||
* One quest, authored as an asset.
|
||||
*
|
||||
* A quest is a tag, a list of steps and a list of things that have to be true before it can be taken.
|
||||
* Everything else about it, the journal entry, the rewards and the conditions on individual steps, hangs
|
||||
* off those three, so a quest with no code of its own is a data asset drop and one line of dialogue.
|
||||
*
|
||||
* Steps are not a state machine. Each one listens to whatever the game already broadcasts for it, so an
|
||||
* InteractWith step is satisfied by the same interaction the player performs to open a chest, and two
|
||||
* quests watching the same NPC both count the same conversation without either of them owning it.
|
||||
*/
|
||||
UCLASS(BlueprintType, PrioritizeCategories = ("Quest", "Objectives", "Requirements", "Rewards"))
|
||||
class PROJECTELERI_API UQuestDataAsset : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Identity of the quest. Every lookup, every requirement and every save entry goes through this
|
||||
* tag, so it is the one field that has to be filled in. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Quest", meta = (GameplayTagFilter = "Quests"))
|
||||
FGameplayTag QuestTag;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Quest")
|
||||
FText QuestName;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Quest", meta = (MultiLine = true))
|
||||
FText QuestDescription;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Quest")
|
||||
TSoftObjectPtr<UTexture2D> QuestIcon;
|
||||
|
||||
/** The NPC this quest is handed out by. The journal uses it to say who to go back to, and a
|
||||
* dialogue can ask "do you have anything for me" through it. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Quest", meta = (ForceInlineRow, Categories = "NpcTags"))
|
||||
FGameplayTag QuestGiverTag;
|
||||
|
||||
/** Days the player has to hand the quest in. 0 means it never runs out. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Quest", meta = (ClampMin = 0))
|
||||
int32 DeadlineInDays = 0;
|
||||
|
||||
/** The steps, in the order the journal lists them. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Objectives", meta = (TitleProperty = "Description"))
|
||||
TArray<FQuestObjective> Objectives;
|
||||
|
||||
/** When set, a step starts listening only once the step before it is done. Off by default, so a
|
||||
* quest that asks for three separate things reads as one list of three rather than as a chain. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Objectives")
|
||||
bool bSequentialObjectives = false;
|
||||
|
||||
/** The items a DeliverItems step asks for are taken out of the inventory at the moment the quest
|
||||
* is handed in, which is what makes a delivery a delivery. Turn it off for a quest that only
|
||||
* wants to see the item and give it back. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Objectives")
|
||||
bool bConsumeDeliveredItems = true;
|
||||
|
||||
/** Everything that has to be true before this quest can be offered. A prerequisite quest is one of
|
||||
* these rather than a field of its own: "the fisherman's request", Completed. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Requirements", meta = (ExcludeBaseStruct, BaseStruct = "/Script/ProjectEleri.GameEventRequirement"))
|
||||
TArray<FInstancedStruct> OfferRequirements;
|
||||
|
||||
/** Checked a second time when the quest is handed in, for a quest whose conditions can lapse
|
||||
* between finishing the last step and getting back to the giver. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Requirements", meta = (ExcludeBaseStruct, BaseStruct = "/Script/ProjectEleri.GameEventRequirement"))
|
||||
TArray<FInstancedStruct> TurnInRequirements;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Rewards")
|
||||
FQuestRewardBundle Rewards;
|
||||
|
||||
/**
|
||||
* True when everything the quest asks for before it can be taken holds right now.
|
||||
*
|
||||
* TestContext is the player character, the same context the event system hands its requirements, so
|
||||
* a condition written for one system behaves identically in the other.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Eleri|Quest")
|
||||
bool AreOfferRequirementsMet(const UObject* TestContext) const;
|
||||
|
||||
/** Checked when the quest is handed in rather than when it is offered */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Eleri|Quest")
|
||||
bool AreTurnInRequirementsMet(const UObject* TestContext) const;
|
||||
|
||||
/** The step with this id, or null. Objective ids are unique inside one quest. */
|
||||
const FQuestObjective* FindObjective(FName ObjectiveId) const;
|
||||
|
||||
/** The step with this id and where it sits in Objectives, or INDEX_NONE */
|
||||
int32 FindObjectiveIndex(FName ObjectiveId) const;
|
||||
|
||||
/** True when the quest asks the player to hand anything over */
|
||||
bool HasDeliverStep() const;
|
||||
|
||||
/** Every step that has to be done for the quest to be handed in. Hidden and optional steps are
|
||||
* left out, since neither of them can hold a quest back. */
|
||||
int32 CountRequiredObjectives() const;
|
||||
|
||||
/** True when the step at Index may run: everything before it is done when the quest asks for its
|
||||
* steps in order, and it is free to run when it does not. */
|
||||
bool IsObjectiveUnlocked(int32 Index, const FQuestRuntimeState& State) const;
|
||||
|
||||
/** The items every finished DeliverItems step is asking for, resolved to what the inventory files
|
||||
* them under. Empty when there is nothing to hand over. */
|
||||
void CollectDeliverables(const FQuestRuntimeState& State, TArray<TPair<FPrimaryAssetId, int32>>& OutItems) const;
|
||||
|
||||
/** Says whether the asset is worth offering at all: a quest with no tag, no steps or a duplicate
|
||||
* step id would otherwise fail quietly in the middle of a playthrough. */
|
||||
bool IsValidQuest(FString& OutError) const;
|
||||
};
|
||||
134
Source/ProjectEleri/QuestSystem/QuestLocationMarker.cpp
Normal file
134
Source/ProjectEleri/QuestSystem/QuestLocationMarker.cpp
Normal file
@@ -0,0 +1,134 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "QuestLocationMarker.h"
|
||||
|
||||
#include "Components/SphereComponent.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "TimerManager.h"
|
||||
|
||||
#include "QuestSubsystem.h"
|
||||
|
||||
AQuestLocationMarker::AQuestLocationMarker()
|
||||
{
|
||||
// Nothing here needs a tick: the overlap is an event and the fallback is a timer, both of which cost
|
||||
// nothing while nothing is happening.
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
ArrivalSphere = CreateDefaultSubobject<USphereComponent>(TEXT("ArrivalSphere"));
|
||||
SetRootComponent(ArrivalSphere);
|
||||
|
||||
ArrivalSphere->SetSphereRadius(ArrivalRadius);
|
||||
ArrivalSphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
ArrivalSphere->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||
ArrivalSphere->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
|
||||
ArrivalSphere->SetGenerateOverlapEvents(true);
|
||||
ArrivalSphere->SetHiddenInGame(true);
|
||||
}
|
||||
|
||||
void AQuestLocationMarker::OnConstruction(const FTransform& Transform)
|
||||
{
|
||||
Super::OnConstruction(Transform);
|
||||
|
||||
// The authored radius is the radius the sphere has, so the volume drawn in the level is the volume
|
||||
// the game tests against.
|
||||
if (ArrivalSphere != nullptr)
|
||||
{
|
||||
ArrivalSphere->SetSphereRadius(FMath::Max(1.f, ArrivalRadius), false);
|
||||
}
|
||||
}
|
||||
|
||||
void AQuestLocationMarker::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
if (ArrivalSphere != nullptr)
|
||||
{
|
||||
ArrivalSphere->OnComponentBeginOverlap.AddDynamic(this, &AQuestLocationMarker::HandleBeginOverlap);
|
||||
ArrivalSphere->OnComponentEndOverlap.AddDynamic(this, &AQuestLocationMarker::HandleEndOverlap);
|
||||
}
|
||||
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().SetTimer(
|
||||
CheckTimerHandle, this, &AQuestLocationMarker::CheckPlayerDistance, FMath::Max(0.1f, CheckInterval), true);
|
||||
}
|
||||
|
||||
// Asked once now rather than waiting for the first interval, because a player who is already
|
||||
// standing on the marker when it spawns has not arrived anywhere, they are simply there.
|
||||
CheckPlayerDistance();
|
||||
}
|
||||
|
||||
void AQuestLocationMarker::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().ClearTimer(CheckTimerHandle);
|
||||
}
|
||||
|
||||
Super::EndPlay(EndPlayReason);
|
||||
}
|
||||
|
||||
void AQuestLocationMarker::HandleBeginOverlap(
|
||||
UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
|
||||
bool bFromSweep, const FHitResult& SweepResult)
|
||||
{
|
||||
CheckPlayerDistance();
|
||||
}
|
||||
|
||||
void AQuestLocationMarker::HandleEndOverlap(
|
||||
UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
|
||||
{
|
||||
CheckPlayerDistance();
|
||||
}
|
||||
|
||||
void AQuestLocationMarker::RefreshArrival()
|
||||
{
|
||||
CheckPlayerDistance();
|
||||
}
|
||||
|
||||
void AQuestLocationMarker::CheckPlayerDistance()
|
||||
{
|
||||
AActor* Player = FindPlayerActor();
|
||||
const float RadiusSquared = FMath::Square(FMath::Max(1.f, ArrivalRadius));
|
||||
|
||||
const bool bInside = Player != nullptr
|
||||
&& FVector::DistSquared(Player->GetActorLocation(), GetActorLocation()) <= RadiusSquared;
|
||||
|
||||
if (bInside == bPlayerInside)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bPlayerInside = bInside;
|
||||
|
||||
// Only the way in counts. Standing on a marker is one arrival however often it is noticed, and a
|
||||
// step that wants the player to come back has a count of its own to say so.
|
||||
if (bPlayerInside)
|
||||
{
|
||||
if (UQuestSubsystem* QuestSubsystem = UQuestSubsystem::Get(this))
|
||||
{
|
||||
QuestSubsystem->NotifyLocationReached(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AActor* AQuestLocationMarker::FindPlayerActor()
|
||||
{
|
||||
if (CachedPlayer.IsValid())
|
||||
{
|
||||
return CachedPlayer.Get();
|
||||
}
|
||||
|
||||
const UWorld* World = GetWorld();
|
||||
if (World == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AActor* Player = UGameplayStatics::GetPlayerCharacter(World, 0);
|
||||
CachedPlayer = Player;
|
||||
|
||||
return Player;
|
||||
}
|
||||
81
Source/ProjectEleri/QuestSystem/QuestLocationMarker.h
Normal file
81
Source/ProjectEleri/QuestSystem/QuestLocationMarker.h
Normal file
@@ -0,0 +1,81 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
|
||||
#include "QuestLocationMarker.generated.h"
|
||||
|
||||
class USphereComponent;
|
||||
|
||||
/**
|
||||
* A spot in the level a quest step can ask the player to stand on.
|
||||
*
|
||||
* This is the one piece of world furniture the quest system needs, and it exists because arrival is the
|
||||
* one thing the game does not already announce: there is no existing signal for "the player has come
|
||||
* near this place", so there has to be something in the level that can say it.
|
||||
*
|
||||
* The sphere is the arrival test, and it is resized to the radius the marker is authored with, so what
|
||||
* a designer sees in the level is exactly what the game acts on. The sphere alone is not enough: a
|
||||
* player who is already standing there when the level loads, or who arrives by a warp, crosses no
|
||||
* boundary that generates an overlap, so the marker also measures the distance on a slow repeating
|
||||
* check. Both paths run through the same one test, which means a single arrival is always announced
|
||||
* once and never twice.
|
||||
*/
|
||||
UCLASS(Blueprintable, BlueprintType)
|
||||
class PROJECTELERI_API AQuestLocationMarker : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
AQuestLocationMarker();
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
virtual void OnConstruction(const FTransform& Transform) override;
|
||||
|
||||
/** How close the player has to come for the arrival to count. The sphere is resized to match. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Quest", meta = (ClampMin = "1.0", UIMin = "1.0"))
|
||||
float ArrivalRadius = 400.f;
|
||||
|
||||
/** How often the distance is measured as a fallback to the overlap. Half a second, which is fast
|
||||
* enough that walking in and out again between two checks is the only way to miss an arrival, and
|
||||
* cheap enough that a level full of markers costs nothing. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Quest", meta = (ClampMin = "0.1", UIMax = "2.0"))
|
||||
float CheckInterval = 0.5f;
|
||||
|
||||
/** True while the player stands inside the marker. An arrival is announced on the way in only. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Quest")
|
||||
bool IsPlayerInside() const { return bPlayerInside; }
|
||||
|
||||
/** Re-runs the arrival test now, for a caller that has just moved the player and does not want to
|
||||
* wait for the next check. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Quest")
|
||||
void RefreshArrival();
|
||||
|
||||
protected:
|
||||
|
||||
UFUNCTION()
|
||||
void HandleBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
|
||||
|
||||
/** Measures where the player is against this marker and tells the quest subsystem when that has
|
||||
* changed from outside to inside */
|
||||
void CheckPlayerDistance();
|
||||
|
||||
AActor* FindPlayerActor();
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Quest")
|
||||
TObjectPtr<USphereComponent> ArrivalSphere;
|
||||
|
||||
/** The player pawn, cached because this is asked for twice a second per marker */
|
||||
TWeakObjectPtr<AActor> CachedPlayer;
|
||||
|
||||
FTimerHandle CheckTimerHandle;
|
||||
|
||||
bool bPlayerInside = false;
|
||||
};
|
||||
56
Source/ProjectEleri/QuestSystem/QuestRequirements.cpp
Normal file
56
Source/ProjectEleri/QuestSystem/QuestRequirements.cpp
Normal file
@@ -0,0 +1,56 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "QuestRequirements.h"
|
||||
|
||||
#include "QuestSubsystem.h"
|
||||
|
||||
int32 FGameEventRequirement_QuestStatus::GetStatusRank(EQuestStatus Status)
|
||||
{
|
||||
switch (Status)
|
||||
{
|
||||
case EQuestStatus::Unavailable:
|
||||
return 0;
|
||||
case EQuestStatus::Available:
|
||||
return 1;
|
||||
case EQuestStatus::Active:
|
||||
return 2;
|
||||
case EQuestStatus::ReadyToTurnIn:
|
||||
return 3;
|
||||
case EQuestStatus::Completed:
|
||||
return 4;
|
||||
default:
|
||||
// Failed and Abandoned. A quest in either of those states is not under way any more, so it
|
||||
// ranks with one nobody has taken rather than satisfying a requirement for a live quest.
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
bool FGameEventRequirement_QuestStatus::Test(const UObject* TestContext) const
|
||||
{
|
||||
if (!QuestTag.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const UQuestSubsystem* QuestSubsystem = UQuestSubsystem::Get(TestContext);
|
||||
if (QuestSubsystem == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const EQuestStatus Status = QuestSubsystem->GetQuestStatus(QuestTag);
|
||||
|
||||
// Failed and Abandoned are the two ends a quest can come to rest on, so asking about them is
|
||||
// asking about that exact outcome rather than about how far along the quest is.
|
||||
const bool bMatchesExactly = !bAtLeast
|
||||
|| RequiredStatus == EQuestStatus::Failed
|
||||
|| RequiredStatus == EQuestStatus::Abandoned;
|
||||
|
||||
if (bMatchesExactly)
|
||||
{
|
||||
return Status == RequiredStatus;
|
||||
}
|
||||
|
||||
return GetStatusRank(Status) >= GetStatusRank(RequiredStatus);
|
||||
}
|
||||
53
Source/ProjectEleri/QuestSystem/QuestRequirements.h
Normal file
53
Source/ProjectEleri/QuestSystem/QuestRequirements.h
Normal file
@@ -0,0 +1,53 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/NoExportTypes.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "ProjectEleri/GameEventSystem/GameEventRequirements.h"
|
||||
#include "QuestTypes.h"
|
||||
|
||||
#include "QuestRequirements.generated.h"
|
||||
|
||||
/**
|
||||
* Passes when a quest stands where the author asked for.
|
||||
*
|
||||
* This is the only requirement the quest system adds, and it is added to the event system's family
|
||||
* rather than to a family of its own so that a quest can be offered on the strength of an event having
|
||||
* run, a friendship having been earned and a quest having been handed in, all in one list.
|
||||
*
|
||||
* It also gives quest chains their prerequisites without a field for them: "the fisherman's request",
|
||||
* Completed, is a prerequisite, and it reads as one.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct PROJECTELERI_API FGameEventRequirement_QuestStatus : public FGameEventRequirement
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta = (ForceInlineRow, GameplayTagFilter = "Quests"))
|
||||
FGameplayTag QuestTag;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta = (ForceInlineRow))
|
||||
EQuestStatus RequiredStatus = EQuestStatus::Completed;
|
||||
|
||||
/**
|
||||
* When set, a status counts as reached as soon as the quest is at or past RequiredStatus on the way
|
||||
* a quest travels: Available, Active, ReadyToTurnIn, Completed.
|
||||
*
|
||||
* Both of the ways a quest can stop without being finished sit outside that ladder on purpose and
|
||||
* are always matched exactly, so a failed quest does not satisfy a requirement for one that is
|
||||
* under way. Leaving this off matches the one status asked for, which is what a requirement written
|
||||
* against Failed or Abandoned wants.
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta = (ForceInlineRow))
|
||||
bool bAtLeast = true;
|
||||
|
||||
virtual bool Test(const UObject* TestContext = nullptr) const override;
|
||||
|
||||
/** Where a status sits on the ladder bAtLeast walks. An abandoned or failed quest ranks as one
|
||||
* that was never taken, which is where it leaves the player. */
|
||||
static int32 GetStatusRank(EQuestStatus Status);
|
||||
};
|
||||
1449
Source/ProjectEleri/QuestSystem/QuestSubsystem.cpp
Normal file
1449
Source/ProjectEleri/QuestSystem/QuestSubsystem.cpp
Normal file
File diff suppressed because it is too large
Load Diff
330
Source/ProjectEleri/QuestSystem/QuestSubsystem.h
Normal file
330
Source/ProjectEleri/QuestSystem/QuestSubsystem.h
Normal file
@@ -0,0 +1,330 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
#include "Engine/WorldInitializationValues.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "QuestTypes.h"
|
||||
|
||||
#include "QuestSubsystem.generated.h"
|
||||
|
||||
class AActor;
|
||||
class AMyCharacter;
|
||||
class APlayerController;
|
||||
class ATimeOfDayManager;
|
||||
class UInventoryComponent;
|
||||
class UQuestDataAsset;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnQuestAssetsLoaded);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnQuestStatusChanged, UQuestDataAsset*, Quest, EQuestStatus, NewStatus);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FOnQuestObjectiveProgress, UQuestDataAsset*, Quest, FName, ObjectiveId, int32, CurrentCount, int32, RequiredCount);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnQuestObjectiveCompleted, UQuestDataAsset*, Quest, FName, ObjectiveId);
|
||||
|
||||
/**
|
||||
* Every quest in the project, and the state of the ones the player has touched.
|
||||
*
|
||||
* A game instance subsystem rather than a world one, for the same reason the event and dialogue
|
||||
* subsystems are: a quest outlives the level it was accepted in, and travelling must not lose it.
|
||||
*
|
||||
* Nothing here polls. Each step is satisfied by whatever already announces that kind of thing, so an
|
||||
* InteractWith step costs one delegate binding rather than an overlap test every frame:
|
||||
*
|
||||
* - TalkToNpc and DialogueEvent come from UDialogueSubsystem, which is already told who the player is
|
||||
* talking to and which gameplay events a line fires.
|
||||
* - InteractWith comes from the player character's own OnInteract, plus NotifyInteraction for callers
|
||||
* that know exactly what was used.
|
||||
* - AcquireItems and DeliverItems come from the player's inventory component, which already
|
||||
* broadcasts every change to it.
|
||||
* - LocationArrival comes from AQuestLocationMarker, which is the one thing that has to be placed in
|
||||
* the level to make a "go there" step work.
|
||||
* - Custom comes from NotifyQuestSignal.
|
||||
*
|
||||
* The bindings are re-made whenever the world or the possessed pawn changes, which is what keeps all
|
||||
* of this working across a level transition, a respawn and a save load.
|
||||
*/
|
||||
UCLASS()
|
||||
class PROJECTELERI_API UQuestSubsystem : public UGameInstanceSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
// ----- Lifetime ---------------------------------------------------------------
|
||||
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
|
||||
/** The subsystem of the game instance the context belongs to, or null when there is none. */
|
||||
static UQuestSubsystem* Get(const UObject* WorldContext);
|
||||
|
||||
// ----- Delegates --------------------------------------------------------------
|
||||
|
||||
/** Fired once the project's quest assets have been read, which is asynchronous because it goes
|
||||
* through the asset manager the way the event and dialogue subsystems do. */
|
||||
UPROPERTY(BlueprintAssignable, Category = "Eleri|Quest")
|
||||
FOnQuestAssetsLoaded OnQuestAssetsLoaded;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Eleri|Quest")
|
||||
FOnQuestStatusChanged OnQuestStatusChanged;
|
||||
|
||||
/** Fired whenever a step's count moves, including while a step is being counted up in place */
|
||||
UPROPERTY(BlueprintAssignable, Category = "Eleri|Quest")
|
||||
FOnQuestObjectiveProgress OnQuestObjectiveProgress;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Eleri|Quest")
|
||||
FOnQuestObjectiveCompleted OnQuestObjectiveCompleted;
|
||||
|
||||
// ----- Registry ---------------------------------------------------------------
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Eleri|Quest")
|
||||
bool QuestsLoaded() const { return bQuestsLoaded; }
|
||||
|
||||
/** Every quest asset in the project, in no particular order */
|
||||
const TArray<TObjectPtr<UQuestDataAsset>>& GetAllQuests() const { return QuestAssets; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Eleri|Quest")
|
||||
UQuestDataAsset* GetQuestByTag(FGameplayTag QuestTag) const;
|
||||
|
||||
// ----- Status -----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Where the quest stands.
|
||||
*
|
||||
* A quest nobody has accepted is Available when its offer requirements hold and Unavailable when
|
||||
* they do not, so a quest that becomes takeable the moment the player's friendship is high enough
|
||||
* is available without anything having to announce it.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Eleri|Quest")
|
||||
EQuestStatus GetQuestStatus(FGameplayTag QuestTag) const;
|
||||
|
||||
/** True while the quest is being worked on or waiting to be handed in */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Eleri|Quest")
|
||||
bool IsQuestInProgress(FGameplayTag QuestTag) const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Eleri|Quest")
|
||||
bool CanAcceptQuest(FGameplayTag QuestTag) const;
|
||||
|
||||
/** How far one step has got, 0 when nothing of that quest is running */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Eleri|Quest")
|
||||
int32 GetObjectiveProgress(FGameplayTag QuestTag, FName ObjectiveId) const;
|
||||
|
||||
// ----- Journal ----------------------------------------------------------------
|
||||
|
||||
/** Every quest the player has taken and not finished, ready to draw */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Quest")
|
||||
TArray<FQuestJournalEntry> GetTrackedQuestEntries() const;
|
||||
|
||||
/** Everything currently on offer, which is what a giver's "anything for me?" asks */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Quest")
|
||||
TArray<FQuestJournalEntry> GetOfferableQuestEntries() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Quest")
|
||||
TArray<FQuestJournalEntry> GetQuestEntries(EQuestStatus Status) const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Quest")
|
||||
FQuestJournalEntry BuildJournalEntry(const UQuestDataAsset* Quest) const;
|
||||
|
||||
// ----- What the player does ---------------------------------------------------
|
||||
|
||||
/** Puts a quest on offer once its requirements hold. Returns false when they do not. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Quest")
|
||||
bool OfferQuest(FGameplayTag QuestTag);
|
||||
|
||||
/** Accepts a quest. Its offer requirements are checked, so a quest can be started without being
|
||||
* offered first and a dialogue node does not have to care which of the two it calls. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Quest")
|
||||
bool StartQuest(FGameplayTag QuestTag);
|
||||
|
||||
/** Hands the quest in: takes the delivery items, grants the rewards and records the day. Only
|
||||
* works once every required step is done. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Quest")
|
||||
bool CompleteQuest(FGameplayTag QuestTag);
|
||||
|
||||
/** Drops the quest, keeping how far it had got in case it is taken up again */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Quest")
|
||||
bool AbandonQuest(FGameplayTag QuestTag);
|
||||
|
||||
/** Fails the quest outright, for a deadline that ran out or a story beat that moved on */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Quest")
|
||||
bool FailQuest(FGameplayTag QuestTag);
|
||||
|
||||
/**
|
||||
* Drives every Custom step waiting on the signal's tag from a blueprint.
|
||||
*
|
||||
* Source is optional and only narrows a signal down: a step that names a speaker ignores signals
|
||||
* from anybody else, which is how "the blacksmith told you the rumour" is authored without the
|
||||
* caller having to know which quest cares.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Quest")
|
||||
void NotifyQuestSignal(const FQuestSignal& Signal, AActor* Source);
|
||||
|
||||
// ----- What the rest of the game tells the quest system -----------------------
|
||||
//
|
||||
// The same entry points the bindings use, left public so a blueprint that already knows exactly
|
||||
// what happened does not have to go looking for the actor an event would only have implied.
|
||||
|
||||
/** Something was used. Interactable is the actor the player acted on. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Quest")
|
||||
void NotifyInteraction(AActor* Interactable, AActor* Interactor = nullptr);
|
||||
|
||||
/** A conversation began, or moved to another speaker */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Quest")
|
||||
void NotifyDialogueStarted(AActor* Speaker);
|
||||
|
||||
/** A line fired one of the dialogue gameplay events */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Quest")
|
||||
void NotifyDialogueEvent(FGameplayTag EventTag, AActor* Speaker);
|
||||
|
||||
/** Something in the player's inventory changed. Called by the inventory binding. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Quest")
|
||||
void NotifyInventoryChanged();
|
||||
|
||||
/** The player arrived at a quest marker. Called by AQuestLocationMarker. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Quest")
|
||||
void NotifyLocationReached(AActor* Marker);
|
||||
|
||||
/** Re-reads every running quest: the steps counted from the inventory, the steps a sequential
|
||||
* quest has just unlocked, and the deadlines. Called on a day change and after a load. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Quest")
|
||||
void RefreshActiveQuests();
|
||||
|
||||
// ----- Saving -----------------------------------------------------------------
|
||||
|
||||
/** Everything the player has done to the quests, ready to be written into the save */
|
||||
void PrepareSaveData(TArray<FQuestSaveEntry>& OutEntries) const;
|
||||
|
||||
/** Puts back what PrepareSaveData wrote. Works through the quest assets that are in the project
|
||||
* now, so a quest that has been deleted since the save was made is skipped instead of taking the
|
||||
* whole save down with it. */
|
||||
void ApplySaveData(const TArray<FQuestSaveEntry>& Entries);
|
||||
|
||||
protected:
|
||||
|
||||
// ----- Bindings ---------------------------------------------------------------
|
||||
|
||||
void HandlePostWorldInitialization(UWorld* World, const FWorldInitializationValues IVS);
|
||||
void HandleWorldBeginPlay();
|
||||
void HandleWorldCleanup(UWorld* World, bool bSessionEnded, bool bCleanupResources);
|
||||
|
||||
/** Makes sure everything this subsystem listens to points at the world the player is in now.
|
||||
* Cheap to call: it compares what is bound against what is alive and rebinds only on a change. */
|
||||
void EnsureGameplayBound(UWorld* World);
|
||||
|
||||
UFUNCTION()
|
||||
void HandlePossessedPawnChanged(APawn* OldPawn, APawn* NewPawn);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleDialogueSpeakerChanged(AActor* Speaker);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleDialogueGameplayEvent(FGameplayTag GameplayTag);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleInventoryUpdated();
|
||||
|
||||
UFUNCTION()
|
||||
void HandlePlayerInteract();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleDayPassed();
|
||||
|
||||
/** Lets go of every delegate this subsystem holds, dynamic and otherwise */
|
||||
void ReleaseGameplayBindings();
|
||||
|
||||
// ----- Assets -----------------------------------------------------------------
|
||||
|
||||
void QuestAssetsLoaded();
|
||||
|
||||
// ----- Progress ---------------------------------------------------------------
|
||||
|
||||
/** The record for a quest, created on the spot when there is not one yet */
|
||||
FQuestRuntimeState& FindOrAddState(FGameplayTag QuestTag);
|
||||
|
||||
/** Moves a quest to a new status and announces it. Does nothing when it is already there. */
|
||||
void SetQuestStatus(FQuestRuntimeState& State, EQuestStatus NewStatus);
|
||||
|
||||
/** Counts a step up, announcing it and completing it when it reaches what it asked for. Returns
|
||||
* true when something changed. */
|
||||
bool AdvanceObjective(FQuestRuntimeState& State, FName ObjectiveId, int32 Amount = 1);
|
||||
|
||||
/** Meters the steps whose progress is a property of the world rather than a tally: how many of
|
||||
* an item the player is carrying. Returns true when something changed. */
|
||||
bool RecountInventoryObjectives(FQuestRuntimeState& State);
|
||||
|
||||
/** Offers a trigger to every running quest, which counts the steps that match it */
|
||||
void HandleObjectiveTrigger(EQuestStepType StepType, AActor* Target, FGameplayTag GameplayTag);
|
||||
|
||||
/** True when the actor is what the step is waiting for, by way of the NPC tag it carries or the
|
||||
* actor the step names */
|
||||
bool DoesActorMatchObjective(const FQuestObjective& Objective, const AActor* Actor) const;
|
||||
|
||||
/** Marks every running quest that can be handed in, and fails the ones out of days */
|
||||
void UpdateQuestStatuses();
|
||||
|
||||
/** Hands the rewards over, takes the delivered items and records the day */
|
||||
void CompleteQuestInternal(FQuestRuntimeState& State);
|
||||
|
||||
/** Gives the player everything the bundle names, through the systems that already own each part:
|
||||
* the inventory, the game state's friendship, the exp library and the event subsystem. */
|
||||
void GrantRewards(const UQuestDataAsset& Quest);
|
||||
|
||||
/** True when every step that cannot be skipped is done. A quest with nothing but optional steps
|
||||
* counts as done the moment it is accepted, which is what it was written to say. */
|
||||
bool AreRequiredObjectivesComplete(const FQuestRuntimeState& State) const;
|
||||
|
||||
/** The world every trigger is bound in, or null when the subsystem is between worlds */
|
||||
UWorld* GetBoundWorld() const { return BoundWorld.Get(); }
|
||||
|
||||
/** The player character requirements are tested against, the same context the event system hands
|
||||
* its own requirements */
|
||||
UObject* GetTestContext() const;
|
||||
|
||||
/** The player's inventory, or null before there is a pawn */
|
||||
UInventoryComponent* GetPlayerInventory() const;
|
||||
|
||||
/** Days passed on the game's own clock, 0 when there is no clock in the world */
|
||||
int32 GetCurrentDay();
|
||||
|
||||
ATimeOfDayManager* GetTimeOfDayManager();
|
||||
|
||||
/** The NPC tag an actor carries on its schedule component, invalid when it has none */
|
||||
static FGameplayTag ResolveActorNpcTag(const AActor* Actor);
|
||||
|
||||
/** Quest assets, filled once the asset manager has finished reading them */
|
||||
UPROPERTY()
|
||||
TArray<TObjectPtr<UQuestDataAsset>> QuestAssets;
|
||||
|
||||
/** Where every quest the player has touched stands, keyed by its tag */
|
||||
UPROPERTY()
|
||||
TMap<FGameplayTag, FQuestRuntimeState> QuestStates;
|
||||
|
||||
UPROPERTY()
|
||||
bool bQuestsLoaded = false;
|
||||
|
||||
private:
|
||||
|
||||
/** The world the bindings below point into. Weak, because this subsystem outlives it. */
|
||||
TWeakObjectPtr<UWorld> BoundWorld;
|
||||
|
||||
TWeakObjectPtr<APlayerController> BoundPlayerController;
|
||||
TWeakObjectPtr<AMyCharacter> BoundCharacter;
|
||||
TWeakObjectPtr<UInventoryComponent> BoundInventory;
|
||||
TWeakObjectPtr<ATimeOfDayManager> CachedTimeOfDayManager;
|
||||
|
||||
/** Held so the world-level bindings can be taken apart one at a time */
|
||||
FDelegateHandle WorldBeginPlayHandle;
|
||||
FDelegateHandle PostWorldInitializationHandle;
|
||||
FDelegateHandle WorldCleanupHandle;
|
||||
|
||||
/** The paths the registry sweep asked the streamable manager for, read back when it is done */
|
||||
TArray<FSoftObjectPath> AssetPathsToLoad;
|
||||
|
||||
/**
|
||||
* True while a reward is being handed over. The inventory tells the subsystem about every change to
|
||||
* it, and a reward can be the very item a quest in flight is asking for: without this a reward would
|
||||
* be able to finish the quest it was paid for.
|
||||
*/
|
||||
bool bGrantingRewards = false;
|
||||
};
|
||||
49
Source/ProjectEleri/QuestSystem/QuestTypes.cpp
Normal file
49
Source/ProjectEleri/QuestSystem/QuestTypes.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "QuestTypes.h"
|
||||
|
||||
#include "ProjectEleri/GameEventSystem/GameEventRequirements.h"
|
||||
#include "ProjectEleri/Items/DataAssets/ItemDataAsset.h"
|
||||
|
||||
bool FQuestObjective::TestConditions(const UObject* TestContext) const
|
||||
{
|
||||
for (const FInstancedStruct& Condition : Conditions)
|
||||
{
|
||||
// GetPtr rather than Get, because a condition that is not a requirement at all is skipped
|
||||
// here instead of tripping the check inside Get: a step with one unreadable entry still runs,
|
||||
// which is the difference between a quest with a hole in it and a quest that cannot be played.
|
||||
const FGameEventRequirement* Requirement = Condition.GetPtr<FGameEventRequirement>();
|
||||
if (Requirement == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Requirement->Test(TestContext))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
FPrimaryAssetId FQuestItemReward::GetItemId() const
|
||||
{
|
||||
if (const UItemDataAsset* ItemAsset = Item.LoadSynchronous())
|
||||
{
|
||||
return ItemAsset->GetPrimaryAssetId();
|
||||
}
|
||||
|
||||
return FPrimaryAssetId();
|
||||
}
|
||||
|
||||
FText FQuestItemReward::GetDisplayName() const
|
||||
{
|
||||
if (const UItemDataAsset* ItemAsset = Item.LoadSynchronous())
|
||||
{
|
||||
return ItemAsset->ItemName;
|
||||
}
|
||||
|
||||
return FText::GetEmpty();
|
||||
}
|
||||
412
Source/ProjectEleri/QuestSystem/QuestTypes.h
Normal file
412
Source/ProjectEleri/QuestSystem/QuestTypes.h
Normal file
@@ -0,0 +1,412 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "StructUtils/InstancedStruct.h"
|
||||
#include "ProjectEleri/System/MainBlueprintFunctionLibrary.h"
|
||||
|
||||
#include "QuestTypes.generated.h"
|
||||
|
||||
class AActor;
|
||||
class UGameEventDataAsset;
|
||||
class UItemDataAsset;
|
||||
class UQuestDataAsset;
|
||||
|
||||
/**
|
||||
* Where a quest stands.
|
||||
*
|
||||
* Unavailable and Available are not progress: they are what the subsystem answers for a quest nobody
|
||||
* has accepted yet, Unavailable being the one whose offer requirements do not hold. The remaining five
|
||||
* are what a save stores, which is why Abandoned is a status of its own rather than the quest simply
|
||||
* disappearing. A quest the player walked away from is one they can still pick back up.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EQuestStatus : uint8
|
||||
{
|
||||
/** Offer requirements do not hold (yet), so the quest is not the player's to take */
|
||||
Unavailable UMETA(DisplayName = "Unavailable"),
|
||||
|
||||
/** Offer requirements hold and nobody has taken it */
|
||||
Available,
|
||||
|
||||
/** Taken and being worked on */
|
||||
Active,
|
||||
|
||||
/** Every required step is done, waiting to be handed in */
|
||||
ReadyToTurnIn UMETA(DisplayName = "Ready To Turn In"),
|
||||
|
||||
/** Handed in, rewards granted */
|
||||
Completed,
|
||||
|
||||
/** Ran out of time */
|
||||
Failed,
|
||||
|
||||
/** Deliberately dropped by the player */
|
||||
Abandoned
|
||||
};
|
||||
|
||||
/**
|
||||
* What has to happen for one step of a quest to be satisfied.
|
||||
*
|
||||
* Every type but Custom is watched by the subsystem itself, by binding to whatever already broadcasts
|
||||
* the event: the dialogue subsystem for TalkToNpc and DialogueEvent, the player's own interaction
|
||||
* delegate for InteractWith, the inventory for AcquireItems and DeliverItems, and the quest location
|
||||
* markers for LocationArrival. Custom covers everything the game does not already announce, and is
|
||||
* driven from a blueprint through NotifyQuestSignal.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EQuestStepType : uint8
|
||||
{
|
||||
/** Counted by NotifyQuestSignal carrying CustomSignalTag */
|
||||
Custom,
|
||||
|
||||
/** Talk to the NPC carrying TargetNpcTag, or start a dialogue with the actor in TargetActor */
|
||||
TalkToNpc UMETA(DisplayName = "Talk To Npc"),
|
||||
|
||||
/** A line fired TargetGameplayTag, whoever said it */
|
||||
DialogueEvent,
|
||||
|
||||
/** Interact with the NPC carrying TargetNpcTag, or with the actor in TargetActor */
|
||||
InteractWith,
|
||||
|
||||
/** Come within ArrivalRadius of the marker in TargetLocationMarker */
|
||||
LocationArrival UMETA(DisplayName = "Location Arrival"),
|
||||
|
||||
/** Carry RequiredCount of TargetItem. Counted from the inventory, so it never over-counts. */
|
||||
AcquireItems UMETA(DisplayName = "Acquire Items"),
|
||||
|
||||
/** Carry RequiredCount of TargetItem and hand it over when the quest is turned in */
|
||||
DeliverItems UMETA(DisplayName = "Deliver Items")
|
||||
};
|
||||
|
||||
/**
|
||||
* What a blueprint is allowed to tell the quest subsystem, so a step the game has no event for can
|
||||
* still be driven from one.
|
||||
*
|
||||
* One signal satisfies every active Custom step whose CustomSignalTag matches it, so two quests that
|
||||
* both wait for "the bell rang" need no wiring at all. ObjectiveId, when set, narrows the signal down
|
||||
* to that one step, which is what a quest with two similar custom steps wants. SourceNpcTag, when set,
|
||||
* narrows it to a speaker.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct PROJECTELERI_API FQuestSignal
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Signal", meta = (Categories = "Quests.Signals"))
|
||||
FGameplayTag SignalTag;
|
||||
|
||||
/** Optional. Empty means the signal counts for every step waiting on SignalTag. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Signal")
|
||||
FName ObjectiveId;
|
||||
|
||||
/** Optional. When set, a signal from any other NPC is ignored. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Signal", meta = (Categories = "NpcTags"))
|
||||
FGameplayTag SourceNpcTag;
|
||||
};
|
||||
|
||||
/**
|
||||
* One step of a quest, as authored.
|
||||
*
|
||||
* Which fields matter depends on StepType, and the panel hides the others so a step reads as the one
|
||||
* thing it is: a talk step has a speaker, an item step has an item, and neither shows the other's
|
||||
* fields. The conditions are checked every time the step is about to count, which is what makes a step
|
||||
* like "bring the herbs, but not before the rain stops" expressible with no new code at all.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct PROJECTELERI_API FQuestObjective
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Stable id. Referenced by signals, by the journal and by the save, so renaming one loses the
|
||||
* progress of every player part way through the step. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Step")
|
||||
FName ObjectiveId;
|
||||
|
||||
/** Shown in the journal */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Step", meta = (MultiLine = true))
|
||||
FText Description;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Step")
|
||||
EQuestStepType StepType = EQuestStepType::Custom;
|
||||
|
||||
/** How many times the step has to happen. Ignored by AcquireItems and DeliverItems, which count
|
||||
* what the inventory holds rather than how often something occurred. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Step", meta = (ClampMin = 1, EditCondition = "StepType!=EQuestStepType::AcquireItems && StepType!=EQuestStepType::DeliverItems"))
|
||||
int32 RequiredCount = 1;
|
||||
|
||||
/** Counted, but not required: the quest can be handed in without it */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Step")
|
||||
bool bOptional = false;
|
||||
|
||||
/** Tracked but never listed in the journal */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Step")
|
||||
bool bHidden = false;
|
||||
|
||||
/** TalkToNpc / InteractWith: the NPC tag that has to be talked to or used */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Step", meta = (Categories = "NpcTags", EditCondition = "StepType==EQuestStepType::TalkToNpc || StepType==EQuestStepType::InteractWith", EditConditionHides))
|
||||
FGameplayTag TargetNpcTag;
|
||||
|
||||
/** TalkToNpc / InteractWith / LocationArrival: the target, when it is not an NPC */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Step", meta = (EditCondition = "StepType==EQuestStepType::TalkToNpc || StepType==EQuestStepType::InteractWith || StepType==EQuestStepType::LocationArrival", EditConditionHides))
|
||||
TSoftObjectPtr<AActor> TargetActor;
|
||||
|
||||
/** DialogueEvent: the event a line has to fire. TalkToNpc accepts it too, which is how a
|
||||
* conversation with several outcomes is made to count. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Step", meta = (Categories = "Dialogue.Event", EditCondition = "StepType==EQuestStepType::DialogueEvent || StepType==EQuestStepType::TalkToNpc", EditConditionHides))
|
||||
FGameplayTag TargetGameplayTag;
|
||||
|
||||
/** LocationArrival: the marker to stand near. Its own radius is used unless ArrivalRadius
|
||||
* overrides it. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Step", meta = (EditCondition = "StepType==EQuestStepType::LocationArrival", EditConditionHides))
|
||||
TSoftObjectPtr<AActor> TargetLocationMarker;
|
||||
|
||||
/** LocationArrival: how close counts as arrived. 0 takes the marker's own radius. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Step", meta = (ClampMin = 0.f, EditCondition = "StepType==EQuestStepType::LocationArrival", EditConditionHides))
|
||||
float ArrivalRadius = 0.f;
|
||||
|
||||
/** AcquireItems / DeliverItems: the item that has to be carried */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Step", meta = (EditCondition = "StepType==EQuestStepType::AcquireItems || StepType==EQuestStepType::DeliverItems", EditConditionHides))
|
||||
TSoftObjectPtr<UItemDataAsset> TargetItem;
|
||||
|
||||
/** AcquireItems / DeliverItems: count only the high quality stack */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Step", meta = (EditCondition = "StepType==EQuestStepType::AcquireItems || StepType==EQuestStepType::DeliverItems", EditConditionHides))
|
||||
bool bHighQuality = false;
|
||||
|
||||
/** Custom: the signal this step waits for */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Step", meta = (Categories = "Quests.Signals", EditCondition = "StepType==EQuestStepType::Custom", EditConditionHides))
|
||||
FGameplayTag CustomSignalTag;
|
||||
|
||||
/** Checked every time the step is about to count. The same types the event system uses, so a
|
||||
* quest condition is written exactly the way an event condition is. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Step", meta = (ExcludeBaseStruct, BaseStruct = "/Script/ProjectEleri.GameEventRequirement"))
|
||||
TArray<FInstancedStruct> Conditions;
|
||||
|
||||
/** True when the step could count right now */
|
||||
bool TestConditions(const UObject* TestContext) const;
|
||||
|
||||
/** True for the step types nothing but a signal can satisfy */
|
||||
bool IsCustomStep() const { return StepType == EQuestStepType::Custom; }
|
||||
};
|
||||
|
||||
/** How far one step of a running quest has got */
|
||||
USTRUCT(BlueprintType)
|
||||
struct PROJECTELERI_API FQuestObjectiveProgress
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Matches FQuestObjective::ObjectiveId */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Step")
|
||||
FName ObjectiveId;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Step")
|
||||
int32 CurrentCount = 0;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Step")
|
||||
bool bComplete = false;
|
||||
|
||||
/** Set for the AcquireItems and DeliverItems steps, which are recomputed from the inventory
|
||||
* rather than counted up as things happen. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Step")
|
||||
bool bCountedFromInventory = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* A quest as the game is playing it.
|
||||
*
|
||||
* The data asset is a hard pointer because a running quest is meaningless without one: an asset that
|
||||
* has left the project takes its quest out of the world with it, which is exactly what the content
|
||||
* change that removed it asked for. The save stores the path instead, so an old save still loads.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct PROJECTELERI_API FQuestRuntimeState
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Quest")
|
||||
TObjectPtr<UQuestDataAsset> Quest = nullptr;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Quest")
|
||||
EQuestStatus Status = EQuestStatus::Unavailable;
|
||||
|
||||
/** Day the quest was accepted, on the clock's own count of days */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Quest")
|
||||
int32 AcceptedDay = 0;
|
||||
|
||||
/** Day the quest was handed in, 0 while it has not been */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Quest")
|
||||
int32 CompletedDay = 0;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Quest")
|
||||
TArray<FQuestObjectiveProgress> Objectives;
|
||||
|
||||
FQuestObjectiveProgress* FindProgress(FName ObjectiveId)
|
||||
{
|
||||
return Objectives.FindByPredicate(
|
||||
[ObjectiveId](const FQuestObjectiveProgress& Entry) { return Entry.ObjectiveId == ObjectiveId; });
|
||||
}
|
||||
|
||||
const FQuestObjectiveProgress* FindProgress(FName ObjectiveId) const
|
||||
{
|
||||
return Objectives.FindByPredicate(
|
||||
[ObjectiveId](const FQuestObjectiveProgress& Entry) { return Entry.ObjectiveId == ObjectiveId; });
|
||||
}
|
||||
};
|
||||
|
||||
/** One step of a running quest, as written to disk */
|
||||
USTRUCT()
|
||||
struct PROJECTELERI_API FQuestObjectiveSave
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY()
|
||||
FName ObjectiveId;
|
||||
|
||||
UPROPERTY()
|
||||
int32 CurrentCount = 0;
|
||||
|
||||
UPROPERTY()
|
||||
bool bComplete = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* One quest as written to disk.
|
||||
*
|
||||
* The asset is a soft path rather than a pointer so that a save made while a quest existed still loads
|
||||
* after that quest has been deleted: the entry is skipped with a warning instead of the whole save
|
||||
* being rejected, which is the only way an author can take a quest back out of a live project.
|
||||
*/
|
||||
USTRUCT()
|
||||
struct PROJECTELERI_API FQuestSaveEntry
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY()
|
||||
FSoftObjectPath QuestPath;
|
||||
|
||||
UPROPERTY()
|
||||
EQuestStatus Status = EQuestStatus::Unavailable;
|
||||
|
||||
UPROPERTY()
|
||||
int32 AcceptedDay = 0;
|
||||
|
||||
UPROPERTY()
|
||||
int32 CompletedDay = 0;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<FQuestObjectiveSave> Objectives;
|
||||
};
|
||||
|
||||
/** One item handed over when a quest is turned in */
|
||||
USTRUCT(BlueprintType)
|
||||
struct PROJECTELERI_API FQuestItemReward
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Item")
|
||||
TSoftObjectPtr<UItemDataAsset> Item;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Item", meta = (ClampMin = 1))
|
||||
int32 Amount = 1;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Item")
|
||||
bool bHighQuality = false;
|
||||
|
||||
/** The id the inventory files this item under, or an invalid id when Item is unset. Loading the
|
||||
* asset is what produces it, and the item database has every item in memory by the time a quest
|
||||
* can be turned in, so this is a lookup rather than a load in practice. */
|
||||
FPrimaryAssetId GetItemId() const;
|
||||
|
||||
/** Human readable name of the item, for the reward line of a journal entry */
|
||||
FText GetDisplayName() const;
|
||||
};
|
||||
|
||||
/**
|
||||
* Everything a quest gives the player when it is handed in.
|
||||
*
|
||||
* Gold and the crystals are items too, but they are addressed by tag the way the rest of the game
|
||||
* addresses them, through UEleriGameSettings::SpecialItemsMap. Experience goes through the same
|
||||
* ModifyExp the rest of the game uses, and events are started through the event subsystem, so a quest
|
||||
* reward has no path of its own into any of those systems.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct PROJECTELERI_API FQuestRewardBundle
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Items")
|
||||
TArray<FQuestItemReward> Items;
|
||||
|
||||
/** Tagged items: Items.Gold, Items.Crystals.* and anything else in the special items map */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Items", meta = (ForceInlineRow, Categories = "Items"))
|
||||
TMap<FGameplayTag, int32> SpecialItems;
|
||||
|
||||
/** Friendship with the NPCs the quest touched */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Rewards", meta = (ForceInlineRow, Categories = "Characters"))
|
||||
TMap<FGameplayTag, int32> Friendship;
|
||||
|
||||
/** Profession experience, by the same types the exp system uses */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Rewards", meta = (ForceInlineRow))
|
||||
TMap<EExpType, float> Experience;
|
||||
|
||||
/** Events this unlocks. Started through the event subsystem, so its own requirements still apply. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Rewards")
|
||||
TArray<TSoftObjectPtr<UGameEventDataAsset>> EventsToStart;
|
||||
};
|
||||
|
||||
/** One step of a quest as a widget reads it, with everything resolved and nothing to look up */
|
||||
USTRUCT(BlueprintType)
|
||||
struct PROJECTELERI_API FQuestObjectiveView
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Journal")
|
||||
FName ObjectiveId;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Journal")
|
||||
FText Description;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Journal")
|
||||
int32 CurrentCount = 0;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Journal")
|
||||
int32 RequiredCount = 1;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Journal")
|
||||
bool bComplete = false;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Journal")
|
||||
bool bOptional = false;
|
||||
};
|
||||
|
||||
/** One quest of the journal, ready to draw */
|
||||
USTRUCT(BlueprintType)
|
||||
struct PROJECTELERI_API FQuestJournalEntry
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Journal")
|
||||
TObjectPtr<UQuestDataAsset> Quest = nullptr;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Journal")
|
||||
FGameplayTag QuestTag;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Journal")
|
||||
FText QuestName;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Journal")
|
||||
FText QuestDescription;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Journal")
|
||||
EQuestStatus Status = EQuestStatus::Unavailable;
|
||||
|
||||
/** True when every required step is done and only the hand-in is left */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Journal")
|
||||
bool bReadyToTurnIn = false;
|
||||
|
||||
/** The steps worth showing, in the order the asset lists them */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Journal")
|
||||
TArray<FQuestObjectiveView> Objectives;
|
||||
};
|
||||
@@ -10,6 +10,7 @@ class UStatForgeComponent;
|
||||
class URetainerBox;
|
||||
class UMainGameWidget;
|
||||
class UInventoryComponent;
|
||||
class UQuestSubsystem;
|
||||
class ARemovableStaticMeshActor;
|
||||
class UEleriAbilitySystemComponent;
|
||||
|
||||
@@ -38,6 +39,9 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|HTTP")
|
||||
static void PostFeedback(const FString& Message);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Eleri|Quest", meta = (WorldContext = "WorldContextObject"))
|
||||
static UQuestSubsystem* GetQuestSubsystem(const UObject* WorldContextObject);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Eleri", meta = (WorldContext = "WorldContextObject"))
|
||||
static UInventoryComponent* GetPlayerInventory(const UObject* WorldContextObject);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "ProjectEleri/GameObjects/BaseCharacter.h"
|
||||
#include "Runtime/Online/HTTP/Public/HttpModule.h"
|
||||
#include "ProjectEleri/Settings/EleriGameSettings.h"
|
||||
#include "ProjectEleri/QuestSystem/QuestSubsystem.h"
|
||||
|
||||
void UMainBlueprintFunctionLibrary::PostFeedback(const FString& Message) {
|
||||
auto request = FHttpModule::Get().CreateRequest();
|
||||
@@ -41,6 +42,13 @@ UInventoryComponent* UMainBlueprintFunctionLibrary::GetPlayerInventory(const UOb
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UQuestSubsystem* UMainBlueprintFunctionLibrary::GetQuestSubsystem(const UObject* WorldContextObject)
|
||||
{
|
||||
// Everything a quest can be asked is a function on the subsystem, so this accessor is all a
|
||||
// blueprint needs: start one, hand one in, read the journal, or fire a signal a custom step waits on.
|
||||
return UQuestSubsystem::Get(WorldContextObject);
|
||||
}
|
||||
|
||||
UMainGameWidget* UMainBlueprintFunctionLibrary::GetMainGameWidget(UObject* WorldContextObject)
|
||||
{
|
||||
if (!WorldContextObject || !WorldContextObject->GetWorld())
|
||||
|
||||
Reference in New Issue
Block a user