Add WinBattlesQuest type (PR 1/3) (#5868)

* Add WinBattlesQuest type (PR 1/3)

Adds a new quest type where a hero asks the player to win X battles
(where X is 2-4). This is a ComponentQuest that tracks progress.

- Proto: Add WinBattlesQuest message and field
- Quest.scala: Add WinBattlesQuest case class extending ComponentQuest
- QuestConverter: Handle WinBattlesQuest toProto/fromProto
- ResolveBattleAction: Increment quest counter when winning battles
- CheckForFulfilledQuestsAction: Quest is fulfilled via ComponentQuest
  handling when componentsFulfilled >= componentCount

This quest never fails.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add WinBattlesQuest cases to prompt generators

The exhaustive pattern matching on Quest types requires adding cases
to all prompt generators when a new quest type is added.

Also updates CLAUDE.md to require running full test suite before pushing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 15:07:01 -08:00
committed by GitHub
co-authored by Claude Opus 4.5
parent f3dea0306d
commit c583e216d3
8 changed files with 76 additions and 3 deletions
+4
View File
@@ -110,6 +110,10 @@ bazel run gazelle # Update Go build files
1. **If you modified any BUILD.bazel file:** Run `bazel run gazelle` and stage any changes it makes
2. **If you modified C++ or C# files:** Run `clang-format -i` on the modified files
3. **If you modified Scala files:** scalafmt will run automatically via pre-commit hook
4. **ALWAYS run the full test suite for your language changes before pushing:**
- For Scala changes: `bazel test //src/test/scala/...`
- For C++ changes: `bazel test //src/test/cpp/...`
- This catches exhaustive pattern match errors and other compile-time failures that single-target builds miss
The pre-commit hook runs gazelle but only checks if it succeeds - it does NOT verify the BUILD files are in canonical format. The `gazelle_test` will fail if deps are not alphabetically sorted. **Always run gazelle manually after BUILD file changes.**
@@ -51,6 +51,7 @@ message QuestDetails {
BetrayAllyQuest betray_ally_quest = 34;
BorderSecurityQuest border_security_quest = 35;
WinBattleOutnumberedQuest win_battle_outnumbered_quest = 36;
WinBattlesQuest win_battles_quest = 37;
}
}
@@ -194,4 +195,7 @@ message BorderSecurityQuest {
}
message WinBattleOutnumberedQuest {
}
message WinBattlesQuest {
}
@@ -211,6 +211,7 @@ case class CheckForFulfilledQuestsAction(
}
case WinBattleOutnumberedQuest =>
false // action quest - fulfilled in ResolveBattleAction
// Note: WinBattlesQuest is handled by ComponentQuest case above
}
private def incrementProvinceOrderQuestProgress(
@@ -21,7 +21,7 @@ import net.eagle0.eagle.model.state.battalion.{BattalionT, EventForBattalionBack
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.hero.{EventForHeroBackstory, EventForHeroBackstoryT, HeroT}
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.quest.{DefeatFactionQuest, Quest, WinBattleOutnumberedQuest}
import net.eagle0.eagle.model.state.quest.{DefeatFactionQuest, Quest, WinBattleOutnumberedQuest, WinBattlesQuest}
import net.eagle0.eagle.model.state.shardok_battle.{BattleType, ShardokBattle, ShardokPlayer}
import net.eagle0.eagle.model.state.unaffiliated_hero.{RecruitmentInfo, UnaffiliatedHeroType}
import net.eagle0.eagle.model.state.unaffiliated_hero.concrete.UnaffiliatedHeroC
@@ -157,6 +157,34 @@ case class ResolveBattleAction(
case _ => false
}
private def winBattlesQuestIncrementResults(
startingState: GameState,
winningFids: Set[FactionId]
): Vector[ActionResultT] = {
val changedProvinces = winningFids.toVector
.flatMap(fid => FactionUtils.provinces(fid, startingState.provinces.values.toVector))
.flatMap { province =>
QuestFulfillmentUtils.counterIncrementedCP(
province = province,
incrementAmount = 1,
check = (uh, _) =>
uh.quest.exists {
case q: WinBattlesQuest => q.componentsFulfilled < q.componentCount
case _ => false
}
)
}
Option
.when(changedProvinces.nonEmpty) {
ActionResultC(
actionResultType = BattleEnded,
changedProvinces = changedProvinces
)
}
.toVector
}
private def playerTroopCount(
player: ShardokPlayer,
battalions: Map[BattalionId, BattalionT]
@@ -190,7 +218,11 @@ case class ResolveBattleAction(
val wasOutnumbered = winnerTroopCount < loserTroopCount && winners.nonEmpty
winners
// Increment WinBattlesQuest counters for all winners
val winBattlesIncrementResults = winBattlesQuestIncrementResults(startingState, winningFids)
// Check for quest fulfillments
val questFulfillmentResults = winners
.flatMap(rsp => FactionUtils.provinces(rsp.eagleFid, startingState.provinces.values.toVector))
.flatMap(p =>
QuestFulfillmentUtils.fulfilledQuestResults(
@@ -206,6 +238,8 @@ case class ResolveBattleAction(
currentDate = startingState.currentDate.get
)
)
winBattlesIncrementResults ++ questFulfillmentResults
}
}
@@ -288,6 +288,11 @@ case class DivineMessagePromptGenerator(
TextGenerationSuccess(
s"$divinedHeroName wants to see ${faction.name} prove their valor by winning a battle while outnumbered."
)
case q: WinBattlesQuest =>
TextGenerationSuccess(
s"$divinedHeroName wants to see ${faction.name} prove their military prowess by winning ${q.componentCount} battles."
)
}
override def generate: TextGenerationResult = for {
@@ -295,6 +295,11 @@ object QuestEndedGeneratorUtilities {
for {
unaffiliatedHeroName <- unaffiliatedHeroNameResult
} yield s"$unaffiliatedHeroName wanted ${faction.name} to prove their valor by winning a battle while outnumbered."
case q: WinBattlesQuest =>
for {
unaffiliatedHeroName <- unaffiliatedHeroNameResult
} yield s"$unaffiliatedHeroName wanted ${faction.name} to prove their military prowess by winning ${q.componentCount} battles."
}
}
}
@@ -36,7 +36,8 @@ import net.eagle0.eagle.common.unaffiliated_hero_quest.{
TruceWithFactionQuest as TruceWithFactionQuestProto,
UpgradeBattalionQuest as UpgradeBattalionQuestProto,
WealthQuest as WealthQuestProto,
WinBattleOutnumberedQuest as WinBattleOutnumberedQuestProto
WinBattleOutnumberedQuest as WinBattleOutnumberedQuestProto,
WinBattlesQuest as WinBattlesQuestProto
}
import net.eagle0.eagle.model.state.quest.*
import net.eagle0.eagle.model.state.BattalionTypeId
@@ -196,6 +197,12 @@ object QuestConverter {
QuestProto(details = BorderSecurityQuestProto(q.targetProvinceId))
case WinBattleOutnumberedQuest =>
QuestProto(details = WinBattleOutnumberedQuestProto())
case q: WinBattlesQuest =>
QuestProto(
details = WinBattlesQuestProto(),
componentCount = q.componentCount,
componentsFulfilled = q.componentsFulfilled
)
}
def fromProto(questProto: QuestProto): Quest =
@@ -375,6 +382,11 @@ object QuestConverter {
BorderSecurityQuest(targetProvinceId)
case WinBattleOutnumberedQuestProto(_ /* unknownFieldSet */ ) =>
WinBattleOutnumberedQuest
case WinBattlesQuestProto(_ /* unknownFieldSet */ ) =>
WinBattlesQuest(
componentCount = questProto.componentCount,
componentsFulfilled = questProto.componentsFulfilled
)
case QuestDetailsProto.Empty =>
throw new IllegalArgumentException("Empty quest details")
}
@@ -158,3 +158,11 @@ case class BetrayAllyQuest(targetFactionId: FactionId) extends Quest
case class BorderSecurityQuest(targetProvinceId: ProvinceId) extends Quest
case object WinBattleOutnumberedQuest extends Quest
case class WinBattlesQuest(
override val componentCount: Int,
override val componentsFulfilled: Int
) extends ComponentQuest {
override def withComponentsFulfilled(componentsFulfilled: Int): Quest =
this.copy(componentsFulfilled = componentsFulfilled)
}