Гайд по созданию квестов в M&B 2: Bannerlord
Оригинальная статья - https://forums.taleworlds.com/index.php?threads/creating-a-quest.415596/
Создание и регистрирование CampaignBehavior
Перед тем, как создать квест, вам нужно создать новый класс TaleWorlds.CampaignSystem.CampaignBehavior.
(Некоторые квесты используют класс TaleWorlds.CampaignSystem.CampaignBehavior.SaveableCampaignBehaviorTypeDefiner, но не понятно, для чего оно использовано). Оба класса будут помогать сериализовать ваши квесты, имея возможность сохранить/загрузить их, это будет сделано полностью автоматически. Также неизвестно, зачем нужен один CampaignBehavior на каждый квест, но TaleWorlds так делает, поэтому будьте уверены, что вы создаёте CampaignBehavior на каждый квест.
Сперва создайте класс, который называется MyCampaignBehavior, который наследует из CampaignBehaviorBase, и реализует все абстрактные функции.
В вашем субмодульном классе, переопределите функцию OnGameStart. Эта функция вызывается при запуске игры (из загрузки или после создания персонажа), здесь мы собираемся зарегистрировать всё ваше поведение кампании (campaign behaviors):
C#:
protected override void OnGameStart(Game inGame, IGameStarter inGameStarter)
{
base.OnGameStart(inGame, inGameStarter);
// Make sure we only load the quest in StoryMode.
// This is not necessary, only if don't want your quest to start in a a game mode that's not the StoryMode (Like multiplayer)
if (!(inGame.GameType is StoryMode.CampaignStoryMode))
return;
CampaignGameStarter campaignGameStarter = (CampaignGameStarter) inGameStarter;
campaignGameStarter.AddBehavior(new MyCampainBehavior());
}
Вот пример квеста, который запускается при входе в поселение:Примечание: в этом примере я решил запускать квест, когда игрок входит в поселение, но теоретически вы можете создать и начать квест в любом месте, где захотите. Это не обязательно должно быть в классе CampainBehavior.
C#:
class MyCampainBehavior : CampaignBehaviorBase
{
public override void RegisterEvents()
{
CampaignEvents.SettlementEntered.AddNonSerializedListener(this, SettlementEnteredEvent);
}
// Event triggered when any party enters a settlement
private void SettlementEnteredEvent(MobileParty arg1, Settlement arg2, Hero arg3)
{
if (arg3 != Hero.MainHero)
return;
// Do not repeat this quest. (Currently don't know how to check completed quests)
if (Campaign.Current.QuestManager.Quests.Any(q => q.GetType() == typeof(MyCampainQuest)))
return;
Hero quest_giver = Hero.MainHero;
// Find nearest village around the player that is not the current settlement
IEnumerable<Settlement> settlements = Settlement.FindSettlementsAroundPosition(quest_giver.GetPosition().AsVec2, 200.0f)
.OrderBy(s => s.GetTrackDistanceToMainAgent())
.Where(s => s.IsVillage && s != arg2);
// Start the quest in that settlement
Settlement settlement = settlements.First();
if (settlement != null)
new MyCampainQuest(quest_giver, settlement).StartQuest();
}
public override void SyncData(IDataStore dataStore)
{
// Not sure what this does. Multiplayer maybe?
}
public class MyCampainQuestBehaviorTypeDefiner : CampaignBehaviorBase.SaveableCampaignBehaviorTypeDefiner
{
public MyCampainQuestBehaviorTypeDefiner()
// This number is the SaveID, supposed to be a unique identifier
: base(846_000_000)
{
}
protected override void DefineClassTypes()
{
// Your quest goes here, second argument is the SaveID
AddClassDefinition(typeof(MyCampainBehavior.MyCampainQuest), 1);
}
}
}
Создание самого квеста
Чтобы создать класс, создайте новый внутренний класс для MyCampaignBehavior c названием MyCampaignQuest, который наследует из QuestBase.
Вам нужно переопределить несколько малых абстрактных методов, но это должно быть легко для понимания.
SetDialogs() - это функция, где вы будете настраивать все диалоги квеста. Её нужно вызывать дважды. Один раз в конструктор, и один в InitializeQuestOnGameLoad. Неизвестно почему, но так делает TaleWorlds (нужно больше информации).
Работа диалогов требует отдельного обучения, я не собираюсь вдаваться в подробности. Я добавил несколько комментариев ниже кода, чтобы все было понятно.
В следующем примере, я создал простой диалог, который идёт двумя способами:
1. Позитивный: Квест успешно завершен.
2. Негативный: Добавлена новая задача (пройти расстояние x единиц), до завершения квеста.
C#:
// Could also use StoryModeQuestBase, but it's a lot more restrictive
internal class MyCampainQuest : QuestBase
{
[SaveableField(1)]
private bool _metAntiImperialMentor;
[SaveableField(2)]
protected float _TotalDistanceTraveled;
// Save JournalLog so we can update it or just keep track of it
protected JournalLog _Task1;
protected readonly int _DistanceToTravel = 100;
protected Vec2 _PreviousPos;
private TextObject _StartQuestLog
{
get
{
TextObject parent = new TextObject("Find and meet {HERO.LINK} and tell him he is your dad. He is currently in {SETTLEMENT}.");
StringHelpers.SetCharacterProperties("HERO", StoryMode.StoryMode.Current.MainStoryLine.AntiImperialMentor.CharacterObject, null, parent);
parent.SetTextVariable("SETTLEMENT", StoryMode.StoryMode.Current.MainStoryLine.AntiImperialMentor.CurrentSettlement.EncyclopediaLinkWithName);
return parent;
}
}
private TextObject _EndQuestLog
{
get
{
TextObject parent = new TextObject("You talked with {HERO.LINK}.");
StringHelpers.SetCharacterProperties("HERO", StoryMode.StoryMode.Current.MainStoryLine.AntiImperialMentor.CharacterObject, null, parent);
return parent;
}
}
private TextObject _YouShouldRunLog
{
get
{
TextObject parent = new TextObject("Your \"dad\" wants you to run {DISTANCE} units.");
parent.SetTextVariable("DISTANCE", _DistanceToTravel);
return parent;
}
}
private TextObject _YouShouldRunText
{
get
{
TextObject parent = new TextObject("How dare you? How about you run {DISTANCE} units, just for fun.");
parent.SetTextVariable("DISTANCE", _DistanceToTravel);
return parent;
}
}
public override TextObject Title
{
get
{
TextObject parent = new TextObject("Meet with your \"dad\" {HERO.NAME}");
StringHelpers.SetCharacterProperties("HERO", StoryMode.StoryMode.Current.MainStoryLine.AntiImperialMentor.CharacterObject, (TextObject) null, parent);
return parent;
}
}
public override bool IsRemainingTimeHidden
{
get
{
return false;
}
}
public MyCampainQuest(Hero questGiver, Settlement settlement)
: base("my_campain_story_mode_quest", questGiver, duration: CampaignTime.DaysFromNow(20), rewardGold: -100 )
{
_metAntiImperialMentor = false;
SetDialogs();
HeroHelper.SpawnHeroForTheFirstTime(StoryMode.StoryMode.Current.MainStoryLine.AntiImperialMentor, settlement);
AddTrackedObject(settlement);
AddLog(_StartQuestLog);
AddTwoWayContinuousLog(new TextObject("This is a two way continuous Log. We are not going to use it."), new TextObject("Task 2"), 0, 10);
}
protected override void RegisterEvents()
{
CampaignEvents.TickEvent.AddNonSerializedListener(this, Tick);
}
protected void Tick(float dt)
{
if (_Task1 != null && !_Task1.HasBeenCompleted())
{
// Compute total distance traveled on the map.
Vec2 current_pos = Hero.MainHero.GetPosition().AsVec2;
_TotalDistanceTraveled += current_pos.Distance(_PreviousPos);
_Task1.UpdateCurrentProgress((int)Math.Floor(_TotalDistanceTraveled));
_PreviousPos = current_pos;
}
}
protected override void InitializeQuestOnGameLoad()
{
SetDialogs();
}
protected override void SetDialogs()
{
// Dialog to offer the quest?
OfferDialogFlow = DialogFlow.CreateDialogFlow(QuestManager.QuestOfferToken, 100)
.NpcLine(new TextObject("{=!}You shouldn't see this."))
.Condition(() => Hero.OneToOneConversationHero == QuestGiver)
.CloseDialog();
// Dialog to discuss the quest?
DiscussDialogFlow = DialogFlow.CreateDialogFlow(QuestManager.QuestDiscussToken, 100)
.NpcLine(new TextObject("{=!}You shouldn't see this."))
.Condition(() => Hero.OneToOneConversationHero == QuestGiver)
.CloseDialog();
// Dialog when starting a conversation with the AntiImperialMentor if we have NOT met him
DialogFlow unmet_dialog = DialogFlow.CreateDialogFlow(QuestManager.NpcLordStartToken, 110)
.NpcLine(new TextObject("So. Who are you, and what brings you to me?"))
// This whole dialog will only start if the following conditions are met
.Condition(() => Hero.OneToOneConversationHero != null && Hero.OneToOneConversationHero == StoryMode.StoryMode.Current.MainStoryLine.AntiImperialMentor && !_metAntiImperialMentor)
.PlayerLine(new TextObject("Hi dad, it's me, your boy."))
.NpcLine(new TextObject("Is that true? Well, that is interesting."))
// Player option: Starts with BeginPlayerOptions() and ends with EndPlayerOptions()
.BeginPlayerOptions()
// PlayerOption will trigger the following dialog flow up to the next CloseDialog() when selected
.PlayerOption(new TextObject("Yes, it's really me dad."), null)
.NpcLine(new TextObject("Good. I don't have anything to say."))
.Consequence(new ConversationSentence.OnConsequenceDelegate(GoodBoy))
.CloseDialog()
.PlayerOption(new TextObject("Actually I was just joking."), null)
.Consequence(new ConversationSentence.OnConsequenceDelegate(BadBoy))
.NpcLine(_YouShouldRunText)
.NpcLine(new TextObject("Now, GO!"))
.CloseDialog()
.EndPlayerOptions()
.CloseDialog();
// Dialog when starting a conversation with the AntiImperialMentor if we have met him
DialogFlow met_dialog = DialogFlow.CreateDialogFlow(QuestManager.NpcLordStartToken, 110)
.NpcLine(new TextObject("So, did you run like I told you to?"))
// This whole dialog will only start if the following conditions are met
.Condition(() => Hero.OneToOneConversationHero != null && Hero.OneToOneConversationHero == StoryMode.StoryMode.Current.MainStoryLine.AntiImperialMentor && _metAntiImperialMentor)
.BeginPlayerOptions()
.PlayerOption(new TextObject("Yes I did."), null)
.Condition(() => _Task1.HasBeenCompleted())
.NpcLine(new TextObject("Ok Great. Now leave"))
.Consequence(() => Campaign.Current.ConversationManager.ConversationEndOneShot += new Action(CompleteQuestWithSuccess))
.CloseDialog()
.PlayerOption(new TextObject("Actuall no, I didn't."), null)
.NpcLine(new TextObject("Don't come back until you did."))
.CloseDialog()
.EndPlayerOptions()
.CloseDialog();
Campaign.Current.ConversationManager.AddDialogFlow(unmet_dialog, this);
Campaign.Current.ConversationManager.AddDialogFlow(met_dialog, this);
}
private void GoodBoy()
{
AddLog(new TextObject("You were a good boy"));
Campaign.Current.ConversationManager.ConversationEndOneShot += new Action(CompleteQuestWithSuccess);
_metAntiImperialMentor = true;
}
private void BadBoy()
{
AddLog(new TextObject("You were a bad boy"));
_Task1 = AddDiscreteLog(_YouShouldRunLog, new TextObject("Units ran."), currentProgress: 0, targetProgress: _DistanceToTravel);
_PreviousPos = Hero.MainHero.GetPosition().AsVec2;
_TotalDistanceTraveled = 0.0f;
_metAntiImperialMentor = true;
}
// Called after the quest is finished
protected override void OnFinalize()
{
base.OnFinalize();
AddLog(_EndQuestLog);
}
}
Журнал логов (journal logs)
Журнал лога - это запись в логе заданий, которую вы видите при открытии Книги Квестов. Его можно использовать для отображения текстового сообщения или шкалы прогресса. Здесь доступны несколько типов журналов:
Синий: регулярный лог, он отображает текст. AddLog(new TextObject("This is a regular Log"));
Бирюзовый: двухсторонний лог. AddTwoWayContinuousLog(task_text, new TextObject("Task 2"), 0, 10);
Фиолетовый: дискретный журнал, показывает шкалу прогресса. AddDiscreteLog(_YouShouldRunLog, new TextObject("Units ran."), currentProgress: 0, targetProgress: _DistanceToTravel);
На этом скриншоте также показана ещё пара вещей:
Желтый: Название квеста
Розовый: Оставшиеся время, отключается при переопределении IsRemainingTimeHidden
Зелёный: Квестодатель
События квеста:
Как объяснилось ранее, запустить квест так же легко, как создать новый объект MyCampaignQuest и название StartQuest().
Вы можете начать новый квест, когда захотите, если игра была полностью загружена. (Точная отправная точка неизвестна).
Если вы попытаетесь создать квест до этого момента, тот фактически квест будет создан, но его не будет видно. Однако логика квеста все равно будет работать.
Вы можете завершить квест, вызвав любую из этих функций:
- CompleteQuestWithBetrayal
- CompleteQuestWithCancel
- CompleteQuestWithFail
- CompleteQuestWithTimeOut
- CompleteQuestWithSuccess (защищено)
Которая, в свою очередь, может вызывать следующие события, которые могут быть переопределены из класса QuestBase:
Это можно использовать, чтобы начать дополнительные квесты, обновить лог квеста и т.д.
- OnCanceled()
- OnFailed()
- OnBetrayal()
- OnCompleteWithSuccess()
- OnFinalize()
- OnStartQuest()
- OnTimedOut()
Комментариев 0
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.