* Enable battalion diversity quest generation (PR 3/3) Adds quest creation logic for BattalionDiversityQuest to QuestCreationUtils. Quest availability: - Only if province currently has 1-2 battalion types - This encourages players to diversify their army composition Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Clarify PR dependency structure in quest docs PR 2 (client) and PR 3 (generation) both depend on PR 1 (types), but are independent of each other. They can be developed in parallel and merged in either order after PR 1. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
8.2 KiB
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:
-
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.
-
PR 2 - Client Handling: Add client-side display code (C#/Unity) that can render the new quest type.
-
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:
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):
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:
// 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:
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):
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:
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:
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:
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:
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:
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:
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
-
Build all modified targets:
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 -
Run tests:
bazel test //src/test/scala/... -
Verify proto/Scala parity:
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