Files
eagle0/docs/ADDING_NEW_QUESTS.md
df916f9c29 Enable battalion diversity quest generation (PR 3/3) (#5838)
* 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>
2026-02-04 09:11:03 -08:00

243 lines
8.2 KiB
Markdown

# 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