mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 21:35:42 +00:00
* 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>
243 lines
8.2 KiB
Markdown
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
|