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

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:

  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:

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

  1. 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
    
  2. Run tests:

    bazel test //src/test/scala/...
    
  3. 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