/// <summary> /// Adds quest heading text to the main quest's Dialogue, Journal, and HUD categories. /// </summary> /// <param name="questBuilder">QuestBuilder.</param> /// <param name="goal">Goal step, which may contain completion text.</param> protected virtual void AddQuestHeadings(QuestBuilder questBuilder, PlanStep goal) { var hasSuccessfulDialogueText = !StringField.IsNullOrEmpty(goal.action.actionText.completedText.dialogueText); var hasSuccessfulJournalText = !StringField.IsNullOrEmpty(goal.action.actionText.completedText.journalText); AddQuestHeading(questBuilder, QuestContentCategory.Dialogue, hasSuccessfulDialogueText); AddQuestHeading(questBuilder, QuestContentCategory.Journal, hasSuccessfulJournalText); AddQuestHeading(questBuilder, QuestContentCategory.HUD, false); }
public WorldModel worldModel; // Resulting world model. public Plan(Plan plan, PlanStep step, WorldModel worldModel) { this.steps = new List <PlanStep>(); if (plan != null) { this.steps.AddRange(plan.steps); } if (step != null) { steps.Add(step); } this.worldModel = worldModel; }
private PlanStep GetStep(Fact fact, Action action) { foreach (var step in masterStepList) { if (step.fact == fact && step.action == action) { return(step); } } var newStep = new PlanStep(fact, action); masterStepList.Add(newStep); return(newStep); }
protected virtual void AddTagsToDictionary(TagDictionary tagDictionary, PlanStep goal) { if (tagDictionary == null || goal == null) { return; } tagDictionary.dict.Add(QuestMachineTags.DOMAIN, StringField.GetStringValue(goal.fact.domainType.displayName)); tagDictionary.dict.Add(QuestMachineTags.ACTION, StringField.GetStringValue(goal.action.displayName)); tagDictionary.dict.Add(QuestMachineTags.TARGET, StringField.GetStringValue(goal.fact.entityType.displayName)); tagDictionary.dict.Add(QuestMachineTags.TARGETS, StringField.GetStringValue(goal.fact.entityType.pluralDisplayName)); tagDictionary.dict.Add(QuestMachineTags.TARGETDESCRIPTOR, goal.fact.entityType.GetDescriptor(goal.requiredCounterValue)); var completion = goal.action.completion; if (completion.mode == ActionCompletion.Mode.Counter) { tagDictionary.dict.Add(QuestMachineTags.COUNTERGOAL, goal.requiredCounterValue.ToString()); } }
// Currently use a BFS rather than A* because it produces better results since there's no good heuristic yet. private IEnumerator BFS(WorldModel initialWorldModel, PlanStep goal) { yield return(null); var Q = new Queue <Plan>(); // Queue the initial state: Q.Enqueue(new Plan(null, null, initialWorldModel)); int numStepsChecked = 0; int safeguard = 0; while (Q.Count > 0 && safeguard < 1000) { safeguard++; numStepsChecked++; if (numStepsChecked > maxStepsPerFrame) { numStepsChecked = 0; yield return(null); } var current = Q.Dequeue(); //--- For debugging: //var indent = string.Empty; //for (int i = 0; i < current.steps.Count; i++) { indent += " "; } //var lastStep = (current.steps.Count > 0) ? current.steps[current.steps.Count - 1] : null; //Debug.Log(indent + "Goal met (lastStep=" + lastStep + ")? " + current.worldModel.AreRequirementsMet(goal.action.requirements)); // If the current state meets the goal requirements, return a finished plan: if (current.worldModel.AreRequirementsMet(goal.action.requirements)) { plan = new Plan(current, goal, null); yield break; } // Otherwise queue up the actions that are valid in the current state: var lastStep = (current.steps.Count > 0) ? current.steps[current.steps.Count - 1] : null; foreach (var fact in current.worldModel.facts) { var actions = GetEntityActions(fact.entityType); if (actions == null) { continue; } foreach (var action in actions) { if (fact == null || fact.entityType == null || action == null) { continue; } if (lastStep != null && fact == lastStep.fact && action == lastStep.action) { continue; // Don't repeat last action. } if (!current.worldModel.AreRequirementsMet(action.requirements)) { continue; // If not valid, don't queue it. } //---Debug.Log(indent + action.name + " " + fact.entityType.name); var newWorldModel = new WorldModel(current.worldModel); newWorldModel.ApplyAction(fact, action); var newPlan = new Plan(current, GetStep(fact, action), newWorldModel); Q.Enqueue(newPlan); } } } if (QuestMachine.debug || detailedDebug) { Debug.Log("Quest Machine: [Generator] Could not create quest. Exceeded safeguard while generating plan to " + goal.action.name + " " + goal.fact.entityType.name + ".", entity); } }
private IEnumerator DetermineGoal() { // Search world model for most urgent fact: Fact[] mostUrgentFacts; var cumulativeUrgency = worldModel.ComputeUrgency(goalSelectionMode, out mostUrgentFacts, ignoreList, detailedDebug); var mostUrgentFact = ChooseWeightedMostUrgentFact(mostUrgentFacts); if (mostUrgentFact == null) { yield break; } if (cumulativeUrgency <= 0) { if (QuestMachine.debug || detailedDebug) { Debug.Log("Quest Machine: [Generator] " + entity.displayName + ": No facts are currently urgent for " + entity.displayName + ". Not generating a new quest.", entity); } yield break; } if (QuestMachine.debug || detailedDebug) { Debug.Log("Quest Machine: [Generator] " + entity.displayName + ": Most urgent fact: " + mostUrgentFact.count + " " + mostUrgentFact.entityType.name + " in " + mostUrgentFact.domainType.name, entity); } // Choose goal action to perform on that fact: float bestUrgency = Mathf.Infinity; Action bestAction = null; Motive bestMotive = null; var actions = GetEntityActions(mostUrgentFact.entityType); if (actions == null) { yield break; } int numChecks = 0; for (int i = 0; i < actions.Count; i++) { numChecks++; if (numChecks > maxGoalActionChecksPerFrame) { numChecks = 0; yield return(null); } var action = actions[i]; if (action == null) { continue; } var wm = new WorldModel(worldModel); wm.ApplyAction(mostUrgentFact, action); Fact newMostUrgentFact; var newUrgency = wm.ComputeUrgency(out newMostUrgentFact); var bestMotiveForAction = ChooseBestMotive(action.motives); var weightedUrgency = (bestMotiveForAction != null) ? (newUrgency - (GetDriveAlignment(bestMotiveForAction.driveValues) * newUrgency)) : newUrgency; if (weightedUrgency < bestUrgency) // Select goal action based on resulting urgency weighted by how well the motive aligns with the giver's drives. { bestUrgency = weightedUrgency; bestAction = action; bestMotive = bestMotiveForAction; } } if (bestAction == null) { yield break; } motive = bestMotive; goal = new PlanStep(mostUrgentFact, bestAction, Mathf.CeilToInt(mostUrgentFact.entityType.maxCountInAction.Evaluate(mostUrgentFact.count))); if (QuestMachine.debug || detailedDebug) { Debug.Log("Quest Machine: [Generator] " + entity.displayName + ": Goal: " + bestAction.name + " " + mostUrgentFact.count + " " + mostUrgentFact.entityType.name, entity); } }
/// <summary> /// Adds the quest's offer text . /// </summary> /// <param name="questBuilder">QuestBuilder.</param> /// <param name="mainTargetEntity">Target of the quest.</param> /// <param name="mainTargetDescriptor">Descriptor of the target (with count).</param> /// <param name="domainName">Domain where the target is located.</param> /// <param name="goal">Final step to complete quest.</param> /// <param name="motive">Motive for goal.</param> protected virtual void AddOfferText(QuestBuilder questBuilder, string mainTargetEntity, string mainTargetDescriptor, string domainName, PlanStep goal, Motive motive) { // Offer motive: var motiveText = (motive != null) ? StringField.GetStringValue(motive.text) : (goal.action.motives.Length > 0) ? StringField.GetStringValue(goal.action.motives[0].text) : StringField.GetStringValue(goal.action.actionText.activeText.dialogueText); motiveText = ReplaceStepTags(motiveText, mainTargetEntity, mainTargetDescriptor, domainName, string.Empty, 0); questBuilder.AddOfferContents(questBuilder.CreateTitleContent(), questBuilder.CreateBodyContent(motiveText)); }
/// <summary> /// Sets the quest's main info . /// </summary> /// <param name="questBuilder">QuestBuilder</param> /// <param name="questID">Quest ID.</param> /// <param name="title">Quest title.</param> /// <param name="group">Quest group.</param> /// <param name="goal">Final step to accomplish the plan.</param> protected virtual void SetMainInfo(QuestBuilder questBuilder, string questID, string title, StringField group, PlanStep goal) { questBuilder.quest.isTrackable = true; questBuilder.quest.showInTrackHUD = true; questBuilder.quest.icon = goal.fact.entityType.image; questBuilder.quest.group = new StringField(group); questBuilder.quest.goalEntityTypeName = goal.fact.entityType.name; }
/// <summary> /// Adds UI content to the return node. /// </summary> protected virtual void AddReturnNodeText(QuestBuilder questBuilder, QuestNode returnNode, QuestGiver questGiver, string mainTargetEntity, string mainTargetDescriptor, string domainName, PlanStep goal, string hudText) { var stateInfo = returnNode.stateInfoList[(int)QuestNodeState.Active]; QuestStateInfo.ValidateCategorizedContentListCount(stateInfo.categorizedContentList); var successText = ReplaceStepTags(StringField.GetStringValue(goal.action.actionText.successText), mainTargetEntity, mainTargetDescriptor, domainName, string.Empty, 0); var bodyText = questBuilder.CreateBodyContent(successText); var dialogueList = returnNode.stateInfoList[(int)QuestNodeState.Active].categorizedContentList[(int)QuestContentCategory.Dialogue]; dialogueList.contentList.Add(bodyText); var jrlText = "{Return to} " + questGiver.displayName; var jrlBodyText = questBuilder.CreateBodyContent(jrlText); var journalList = returnNode.stateInfoList[(int)QuestNodeState.Active].categorizedContentList[(int)QuestContentCategory.Journal]; journalList.contentList.Add(jrlBodyText); var hudBodyText = questBuilder.CreateBodyContent(hudText); var hudList = returnNode.stateInfoList[(int)QuestNodeState.Active].categorizedContentList[(int)QuestContentCategory.HUD]; hudList.contentList.Add(hudBodyText); }
/// <summary> /// Adds a final "return to quest giver" step. /// </summary> /// <returns>The return node.</returns> protected virtual QuestNode AddReturnNode(QuestBuilder questBuilder, QuestNode previousNode, QuestEntity entity, string mainTargetEntity, string mainTargetDescriptor, string domainName, PlanStep goal, int rewardsContentIndex = 9999) // Default to 9999 to not break any customer code using old signature. { var questGiver = entity.GetComponent <QuestGiver>(); var giverID = (questGiver != null) ? questGiver.id : ((entity != null) ? entity.displayName : null); var returnNode = questBuilder.AddDiscussQuestNode(previousNode, QuestMessageParticipant.QuestGiver, giverID); returnNode.id = new StringField("Return"); previousNode = returnNode; QuestStateInfo.ValidateStateInfoListCount(returnNode.stateInfoList); var hudText = "{Return to} " + questGiver.displayName; // Text when active: AddReturnNodeText(questBuilder, returnNode, questGiver, mainTargetEntity, mainTargetDescriptor, domainName, goal, hudText); // Add rewards content: var dialogueList = returnNode.stateInfoList[(int)QuestNodeState.Active].categorizedContentList[(int)QuestContentCategory.Dialogue]; var offerContentList = questBuilder.quest.offerContentList; for (int i = rewardsContentIndex; i < offerContentList.Count; i++) { var original = offerContentList[i]; var copy = QuestContent.Instantiate(original) as QuestContent; original.CloneSubassetsInto(copy); dialogueList.contentList.Add(copy); } var actionList = returnNode.GetStateInfo(QuestNodeState.Active).actionList; // Alert when active: AddReturnNodeAlert(questBuilder, returnNode, actionList, hudText); // Indicators: AddReturnNodeIndicators(questBuilder, returnNode, actionList, entity); return(previousNode); }
/// <summary> /// Adds the text for a step. /// </summary> protected virtual void AddStepNodeText(QuestBuilder questBuilder, QuestNode conditionNode, QuestStateInfo activeState, string targetEntity, string targetDescriptor, string domainName, string counterName, int requiredCounterValue, PlanStep step) { // Text for condition node's Active state: var taskText = ReplaceStepTags(step.action.actionText.activeText.dialogueText.value, targetEntity, targetDescriptor, domainName, counterName, requiredCounterValue); var bodyText = questBuilder.CreateBodyContent(taskText); var dialogueList = activeState.categorizedContentList[(int)QuestContentCategory.Dialogue]; dialogueList.contentList.Add(bodyText); var jrlText = ReplaceStepTags(step.action.actionText.activeText.journalText.value, targetEntity, targetDescriptor, domainName, counterName, requiredCounterValue); var jrlbodyText = questBuilder.CreateBodyContent(jrlText); var journalList = activeState.categorizedContentList[(int)QuestContentCategory.Journal]; journalList.contentList.Add(jrlbodyText); var hudText = ReplaceStepTags(step.action.actionText.activeText.hudText.value, targetEntity, targetDescriptor, domainName, counterName, requiredCounterValue); var hudbodyText = questBuilder.CreateBodyContent(hudText); var hudList = activeState.categorizedContentList[(int)QuestContentCategory.HUD]; hudList.contentList.Add(hudbodyText); }
/// <summary> /// Converts a plan generated by QuestGenerator into a quest. /// </summary> /// <param name="entity">Entity generating the quest (e.g., QuestGiver).</param> /// <param name="group">Optional quest group under which to categorize this quest.</param> /// <param name="goal">Final step to accomplish quest.</param> /// <param name="motive">Motive for the goal.</param> /// <param name="plan">Steps required to complete the goal step.</param> /// <param name="requireReturnToComplete">If true, add a quest node that requires quester to return to entity.</param> /// <param name="rewardsUIContents">UI content to show in the rewards offer section.</param> /// <param name="rewardSystems">Reward systems to use to </param> /// <returns></returns> public virtual Quest ConvertPlanToQuest(QuestEntity entity, StringField group, PlanStep goal, Motive motive, Plan plan, bool requireReturnToComplete, List <QuestContent> rewardsUIContents, List <RewardSystem> rewardSystems) { // Build title: var mainTargetEntity = goal.fact.entityType.name; var mainTargetDescriptor = goal.fact.entityType.GetDescriptor(goal.requiredCounterValue); var title = goal.action.displayName + " " + mainTargetDescriptor; var questID = title + " " + System.Guid.NewGuid(); var domainName = StringField.GetStringValue(goal.fact.domainType.displayName); // Start QuestBuilder: var questBuilder = new QuestBuilder(title, questID, title); SetMainInfo(questBuilder, questID, title, group, goal); AddTagsToDictionary(questBuilder.quest.tagDictionary, goal); // Offer: AddOfferText(questBuilder, mainTargetEntity, mainTargetDescriptor, domainName, goal, motive); var rewardsContentIndex = questBuilder.quest.offerContentList.Count; AddRewards(questBuilder, entity, goal, rewardsUIContents, rewardSystems); // Quest headings: AddQuestHeadings(questBuilder, goal); // Successful text (shown in journal / when talking again about successful quest): AddSuccessfulText(questBuilder, mainTargetEntity, mainTargetDescriptor, domainName, goal); // Add steps: var previousNode = AddSteps(questBuilder, domainName, goal, plan); // Add "return to giver" node: if (requireReturnToComplete) { previousNode = AddReturnNode(questBuilder, previousNode, entity, mainTargetEntity, mainTargetDescriptor, domainName, goal, rewardsContentIndex); } // Success node: questBuilder.AddSuccessNode(previousNode); return(questBuilder.ToQuest()); }
/// <summary> /// Adds the plan's steps. /// </summary> /// <param name="questBuilder">QuestBuilder.</param> /// <param name="domainName">Main target's domain.</param> /// <param name="goal">Goal step.</param> /// <param name="plan">List of steps that end with goal step.</param> /// <returns>The last node added.</returns> protected virtual QuestNode AddSteps(QuestBuilder questBuilder, string domainName, PlanStep goal, Plan plan) { var previousNode = questBuilder.GetStartNode(); var counterNames = new HashSet <string>(); for (int i = 0; i < plan.steps.Count; i++) { var step = plan.steps[i]; // Create next condition node: var targetEntity = step.fact.entityType.name; var targetDescriptor = step.fact.entityType.GetDescriptor(step.requiredCounterValue); var id = (i + 1).ToString(); var internalName = step.action.displayName + " " + targetDescriptor; var conditionNode = questBuilder.AddConditionNode(previousNode, id, internalName, ConditionCountMode.All); previousNode = conditionNode; // Variables for node text tag replacement: var counterName = string.Empty; int requiredCounterValue = 0; var completion = step.action.completion; if (completion.mode == ActionCompletion.Mode.Counter) { // Setup counter condition: counterName = goal.fact.entityType.pluralDisplayName.value + completion.baseCounterName.value; if (!counterNames.Contains(counterName)) { var counter = questBuilder.AddCounter(counterName, completion.initialValue, completion.minValue, completion.maxValue, false, completion.updateMode); foreach (var messageEvent in completion.messageEventList) { var counterMessageEvent = new QuestCounterMessageEvent(messageEvent.targetID, messageEvent.message, new StringField(StringField.GetStringValue(messageEvent.parameter).Replace("{TARGETENTITY}", targetEntity)), messageEvent.operation, messageEvent.literalValue); counter.messageEventList.Add(counterMessageEvent); } } counterName = goal.fact.entityType.pluralDisplayName.value + completion.baseCounterName.value; requiredCounterValue = Mathf.Min(step.requiredCounterValue, step.fact.count); questBuilder.AddCounterCondition(conditionNode, counterName, CounterValueConditionMode.AtLeast, requiredCounterValue); // Consider: Add action to reset counter to zero in case future nodes repeat the same counter? } else { // Setup message condition: questBuilder.AddMessageCondition(conditionNode, QuestMessageParticipant.Any, completion.senderID, QuestMessageParticipant.Any, completion.targetID, completion.message, new StringField(StringField.GetStringValue(completion.parameter).Replace("{TARGETENTITY}", targetEntity))); } var activeState = conditionNode.stateInfoList[(int)QuestNodeState.Active]; AddStepNodeText(questBuilder, conditionNode, activeState, targetEntity, targetDescriptor, domainName, counterName, requiredCounterValue, step); // Actions when active: if (!StringField.IsNullOrEmpty(step.action.actionText.activeText.alertText)) { // Alert action: var alertAction = questBuilder.CreateAlertAction(ReplaceStepTags(step.action.actionText.activeText.alertText.value, targetEntity, targetDescriptor, domainName, counterName, requiredCounterValue)); activeState.actionList.Add(alertAction); } // Send message action: if (!StringField.IsNullOrEmpty(step.action.sendMessageOnActive)) { var messageAction = questBuilder.CreateMessageAction(ReplaceStepTags(step.action.sendMessageOnActive.value, targetEntity, targetDescriptor, domainName, counterName, requiredCounterValue)); activeState.actionList.Add(messageAction); } // Actions when completed: if (!StringField.IsNullOrEmpty(step.action.sendMessageOnCompletion)) { var trueState = conditionNode.stateInfoList[(int)QuestNodeState.True]; var messageAction = questBuilder.CreateMessageAction(ReplaceStepTags(step.action.sendMessageOnCompletion.value, targetEntity, targetDescriptor, domainName, counterName, requiredCounterValue)); trueState.actionList.Add(messageAction); } } return(previousNode); }
/// <summary> /// Adds text to show in UIs after a quest has been successfully completed. /// </summary> /// <param name="questBuilder">QuestBuilder.</param> /// <param name="mainTargetEntity">Main target entity that quest is about.</param> /// <param name="mainTargetDescriptor">Target descriptor (with count).</param> /// <param name="domainName">Target's domain.</param> /// <param name="goal">Goal step in quest.</param> protected virtual void AddSuccessfulText(QuestBuilder questBuilder, string mainTargetEntity, string mainTargetDescriptor, string domainName, PlanStep goal) { var successful = questBuilder.quest.GetStateInfo(QuestState.Successful); var hasSuccessfulDialogueText = !StringField.IsNullOrEmpty(goal.action.actionText.completedText.dialogueText); var hasSuccessfulJournalText = !StringField.IsNullOrEmpty(goal.action.actionText.completedText.journalText); if (hasSuccessfulDialogueText) { var dlgText = questBuilder.CreateBodyContent(ReplaceStepTags(goal.action.actionText.completedText.dialogueText.value, mainTargetEntity, mainTargetDescriptor, domainName, string.Empty, 0)); questBuilder.AddContents(successful.GetContentList(QuestContentCategory.Dialogue), dlgText); } if (hasSuccessfulJournalText) { var jrlText = questBuilder.CreateBodyContent(ReplaceStepTags(goal.action.actionText.completedText.journalText.value, mainTargetEntity, mainTargetDescriptor, domainName, string.Empty, 0)); questBuilder.AddContents(successful.GetContentList(QuestContentCategory.Journal), jrlText); } }
/// <summary> /// Uses a list of reward systems to add rewards to the quest builder /// that are commensurate with the difficulty of the quest. /// </summary> /// <param name="questBuilder">QuestBuilder to receive reward offers.</param> /// <param name="entity">Quest giver entity.</param> /// <param name="goal">Goal step (determines quest difficulty).</param> /// <param name="rewardsUIContents">Text to introduce rewards.</param> /// <param name="rewardSystems">Reward systems from which to get rewards.</param> protected virtual void AddRewards(QuestBuilder questBuilder, QuestEntity entity, PlanStep goal, List <QuestContent> rewardsUIContents, List <RewardSystem> rewardSystems) { if (QuestGenerator.detailedDebug) { Debug.Log("Quest Machine: [Generator] Checking " + rewardSystems.Count + " reward systems for " + goal.fact.count + " " + goal.fact.entityType.name + " (level " + goal.fact.entityType.level + ") on " + entity.name, entity); } questBuilder.AddOfferContents(QuestContent.CloneList <QuestContent>(rewardsUIContents).ToArray()); var pointsRemaining = goal.fact.entityType.level * goal.fact.count; for (int i = 0; i < rewardSystems.Count; i++) { var rewardSystem = rewardSystems[i]; if (rewardSystem == null) { continue; } if (UnityEngine.Random.value > rewardSystem.probability) { continue; } pointsRemaining = rewardSystem.DetermineReward(pointsRemaining, questBuilder.quest, goal.fact.entityType); if (pointsRemaining <= 0) { break; } } }