# Adding New Quest Types This document describes how to add new quest types to Eagle. Quests are tasks that unaffiliated heroes want factions to complete before they'll join. ## Deployment Strategy: Three-PR Approach When adding new quest types, use a three-PR strategy to ensure clients never see quests they don't understand: 1. **PR 1 - Types Only (Server)**: Add proto definitions, Scala types, converters, and all handling logic (fulfillment, failure, LLM prompts). Do NOT generate the quests yet. 2. **PR 2 - Client Handling**: Add client-side display code (C#/Unity) that can render the new quest type. 3. **PR 3 - Quest Generation (Server)**: Add the actual quest creation logic to `QuestCreationUtils.scala`. **PR Dependencies:** ``` PR 1 (types) ├── PR 2 (client) └── PR 3 (generation) ``` PR 2 and PR 3 both depend on PR 1, but are independent of each other. They can be developed in parallel after PR 1 is merged, and merged in either order. The key constraint is that PR 3 should not be deployed before PR 2, so clients understand the quest type before the server starts generating it. ## Files to Modify ### Server-Side (Scala) #### 1. Proto Definition **File:** `src/main/protobuf/net/eagle0/eagle/common/unaffiliated_hero_quest.proto` Add a new message type for your quest and add it to the `QuestDetails` oneof: ```protobuf message MyNewQuest { int32 some_field = 1; string another_field = 2; } // In the QuestDetails message, add to the oneof: oneof sealed_value { // ... existing quests ... MyNewQuest my_new_quest = XX; // Use next available field number } ``` #### 2. Scala Case Class **File:** `src/main/scala/net/eagle0/eagle/model/state/quest/Quest.scala` Add a case class (or case object for quests with no parameters): ```scala case class MyNewQuest( someField: Int, anotherField: String ) extends Quest // For quests with multi-part completion, extend ComponentQuest: case class MyComponentQuest( override val componentCount: Int, override val componentsFulfilled: Int, targetValue: Int ) extends ComponentQuest { override def withComponentsFulfilled(componentsFulfilled: Int): Quest = this.copy(componentsFulfilled = componentsFulfilled) } ``` #### 3. Proto Converter **File:** `src/main/scala/net/eagle0/eagle/model/proto_converters/QuestConverter.scala` Add conversions in both `toProto` and `fromProto` methods: ```scala // In toProto: case q: MyNewQuest => QuestProto(details = MyNewQuestProto(q.someField, q.anotherField)) // In fromProto: case MyNewQuestProto(someField, anotherField, _) => MyNewQuest(someField, anotherField) ``` #### 4. Quest Fulfillment Check **File:** `src/main/scala/net/eagle0/eagle/library/actions/impl/action/CheckForFulfilledQuestsAction.scala` Add a case to `didFulfillQuest` that returns `true` when the quest conditions are met: ```scala case MyNewQuest(someField, anotherField) => // Return true if quest is fulfilled someConditionIsMet(province, someField) ``` For `ComponentQuest` subclasses, the base case `case q: ComponentQuest => q.componentsFulfilled >= q.componentCount` handles fulfillment automatically. #### 5. Quest Failure Check **File:** `src/main/scala/net/eagle0/eagle/library/actions/impl/action/CheckForFailedQuestsAction.scala` Add a case to `isQuestFailed` if your quest can fail (e.g., if a required faction is destroyed): ```scala case MyNewQuest(someField, _) => // Return true if quest can no longer be completed !factionWithId(someField).exists(_.isActive) ``` Many quests use the default case that returns `false` (quest never fails automatically). #### 6. LLM Prompt Generators **File:** `src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators/DivineMessagePromptGenerator.scala` Add a case to `describeQuest` for when a soothsayer reveals the quest: ```scala case MyNewQuest(someField, anotherField) => TextGenerationSuccess( s"$divinedHeroName wants ${faction.name} to do something with $someField." ) ``` **File:** `src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators/QuestEndedGeneratorUtilities.scala` Add a case to `pastTenseQuestDescription` for quest fulfilled/failed narratives: ```scala case MyNewQuest(someField, anotherField) => for { unaffiliatedHeroName <- unaffiliatedHeroNameResult } yield s"$unaffiliatedHeroName wanted ${faction.name} to do something with $someField." ``` #### 7. Quest Creation (PR 3 only) **File:** `src/main/scala/net/eagle0/eagle/library/util/quest_creation/QuestCreationUtils.scala` Add a quest creator function and register it in `availableQuests`: ```scala private def myNewQuests( province: ProvinceT, allProvinces: Vector[ProvinceT], @unused factions: Vector[FactionT], @unused battalions: Vector[BattalionT], functionalRandom: FunctionalRandom ): RandomState[Vector[Quest]] = { // Return empty if quest shouldn't be available if (!someCondition) RandomState(Vector(), functionalRandom) else { functionalRandom.nextIntInclusive(minValue, maxValue).map { value => Vector(MyNewQuest(value, "something")) } } } // Add to availableQuests list: def availableQuests(...) = functionalRandom.nextFlatMap( Vector[QuestCreator]( // ... existing creators ... myNewQuests ) ) { ... } ``` #### 8. Optional: Quest Command Selectors (AI auto-completion) **Directory:** `src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors/` If you want AI players to automatically work toward completing your quest, create a command chooser. ### Client-Side (C#/Unity) #### 1. Quest Type Display Name **File:** `src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Eagle/DisplayNames.cs` Add a case to `QuestTypeString`: ```csharp case SealedValueOneofCase.MyNewQuest: return "My New Quest"; ``` #### 2. Quest Description String **File:** `src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Eagle/Table Rows/UnaffiliatedHeroRowController.cs` Add a case to `ShortQuestString` for displaying the quest in the UI: ```csharp case SealedValueOneofCase.MyNewQuest: { var details = quest.Details.MyNewQuest; return $"Do something with {details.SomeField}"; } ``` For quests involving heroes (where you want dynamic name updates), add a case to `SetQuestText` instead. ### Build Files After making changes, run: ```bash bazel run gazelle ``` This updates BUILD.bazel files with any new dependencies. ## Quest Type Categories ### Simple Quests Quests with fixed completion conditions (e.g., `AllianceQuest`, `SuppressRiotByForceQuest`). ### Component Quests Quests with multi-part completion that track progress via `componentsFulfilled` / `componentCount` (e.g., `AlmsToProvinceQuest`, `DevelopProvincesQuest`). Extend `ComponentQuest` and implement `withComponentsFulfilled`. ### Action Quests Quests completed by specific player actions rather than state checks. Return `false` in `didFulfillQuest` and handle completion in the relevant command handler (e.g., `ExecutePrisonerQuest` is fulfilled in prisoner management command). ## Testing 1. Build all modified targets: ```bash bazel build //src/main/scala/net/eagle0/eagle/model/state/quest:quest bazel build //src/main/scala/net/eagle0/eagle/model/proto_converters:quest_converter ``` 2. Run tests: ```bash bazel test //src/test/scala/... ``` 3. Verify proto/Scala parity: ```bash bazel test //src/test/scala/net/eagle0/eagle/model/action_result/types:action_result_type_parity_test ``` ## Current Quest Types (30 total) - **Diplomacy**: AllianceQuest, TruceWithFactionQuest, TruceCountQuest, DefeatFactionQuest - **Development**: ImproveAgricultureQuest, ImproveEconomyQuest, ImproveInfrastructureQuest, TotalDevelopmentQuest - **Expansion**: SpecificExpansionQuest, ExpandToProvincesQuest - **Military**: GrandArmyQuest, UpgradeBattalionQuest, FightBeastsAloneQuest - **Resources**: WealthQuest, AlmsToProvinceQuest, AlmsAcrossRealmQuest, GiveToHeroesInProvinceQuest, GiveToHeroesAcrossRealmQuest - **Personnel**: DismissSpecificVassalQuest, RescueImprisonedLeaderQuest - **Prisoner**: ExecutePrisonerQuest, ExilePrisonerQuest, ReleasePrisonerQuest, ReturnPrisonerQuest - **Province Orders**: DevelopProvincesQuest, MobilizeProvincesQuest - **Events**: SuppressRiotByForceQuest