remove old quest system and assets, replace with v1 of new quest system
This commit is contained in:
@@ -56,6 +56,8 @@ NetIndexFirstBitSegment=16
|
||||
+GameplayTagList=(Tag="SaveGameTags.InteractionStage",DevComment="")
|
||||
+GameplayTagList=(Tag="SaveGameTags.Locked",DevComment="")
|
||||
+GameplayTagList=(Tag="SetByCaller.Effects.ExpModifier",DevComment="")
|
||||
+GameplayTagList=(Tag="Quests",DevComment="Identity of a quest. Each quest data asset is tagged with Quests.<Name>, and quests reference each other by these tags.")
|
||||
+GameplayTagList=(Tag="Quests.Signals",DevComment="Signals a quest step waits on that no system in the game announces, fired from blueprint with NotifyQuestSignal.")
|
||||
+GameplayTagList=(Tag="Skins",DevComment="")
|
||||
+GameplayTagList=(Tag="Skins.Eleri.Body",DevComment="")
|
||||
+GameplayTagList=(Tag="Skins.Eleri.Default",DevComment="")
|
||||
|
||||
BIN
Content/GameData/ItemData.uasset
LFS
BIN
Content/GameData/ItemData.uasset
LFS
Binary file not shown.
BIN
Content/GameData/NpcData.uasset
LFS
BIN
Content/GameData/NpcData.uasset
LFS
Binary file not shown.
Binary file not shown.
BIN
Content/GameData/Quests/DA_Quest_Trish_WarmWelcome.uasset
LFS
Normal file
BIN
Content/GameData/Quests/DA_Quest_Trish_WarmWelcome.uasset
LFS
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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