Generate focus province quests (#8704)

* Add focus province quest type

* Generate focus province quests
This commit is contained in:
2026-07-18 09:09:18 -07:00
committed by GitHub
parent c2c18d8a07
commit 0c0ff85513
9 changed files with 243 additions and 8 deletions
@@ -353,6 +353,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:execute_prisoner_quest_command_chooser",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:exile_prisoner_quest_command_chooser",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:fight_beasts_alone_quest_command_chooser",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:focus_province_quest_command_chooser",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:give_to_heroes_across_realm_quest_command_chooser",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:give_to_heroes_in_province_quest_command_chooser",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:grand_army_quest_command_chooser",
@@ -14,6 +14,7 @@ import net.eagle0.eagle.library.util.command_choice_helpers.quest_command_select
ExecutePrisonerQuestCommandChooser,
ExilePrisonerQuestCommandChooser,
FightBeastsAloneQuestCommandChooser,
FocusProvinceQuestCommandChooser,
GiveToHeroesAcrossRealmQuestCommandChooser,
GiveToHeroesInProvinceQuestCommandChooser,
GrandArmyQuestCommandChooser,
@@ -43,6 +44,7 @@ import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.quest.{
AlmsAcrossRealmQuest,
DevelopProvincesQuest,
FocusProvinceQuest,
GiveToHeroesAcrossRealmQuest,
MobilizeProvincesQuest,
Quest,
@@ -77,6 +79,7 @@ object FulfillQuestsCommandSelector {
ApprehendOutlawQuestCommandChooser,
SpendOnFeastsQuestCommandChooser,
RestProvinceQuestCommandChooser,
FocusProvinceQuestCommandChooser,
DevelopProvincesQuestCommandChooser,
MobilizeProvincesQuestCommandChooser,
SendSuppliesQuestCommandChooser,
@@ -128,6 +131,7 @@ object FulfillQuestsCommandSelector {
case _: ReconSpecificProvincesQuest => true
case _: DevelopProvincesQuest => true
case _: MobilizeProvincesQuest => true
case _: FocusProvinceQuest => true
case _ => false
}
@@ -384,6 +384,28 @@ scala_library(
],
)
scala_library(
name = "focus_province_quest_command_chooser",
srcs = ["FocusProvinceQuestCommandChooser.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:__subpackages__",
],
exports = [
":quest_command_chooser",
],
deps = [
":quest_command_chooser",
":unaffiliated_hero_with_quest",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/quest",
],
)
scala_library(
name = "release_all_prisoners_quest_command_chooser",
srcs = ["ReleaseAllPrisonersQuestCommandChooser.scala"],
@@ -494,6 +516,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/command/common",
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
@@ -0,0 +1,40 @@
package net.eagle0.eagle.library.util.command_choice_helpers.quest_command_selectors
import net.eagle0.eagle.library.util.CommandSelection
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.IssueOrdersAvailable
import net.eagle0.eagle.model.state.command.selected.ProvinceOrder
import net.eagle0.eagle.model.state.command.selected.SelectedCommand.IssueOrdersSelected
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.quest.FocusProvinceQuest
import net.eagle0.eagle.FactionId
object FocusProvinceQuestCommandChooser extends DeterministicQuestCommandChooser {
override def chosenDeterministicCommand(
actingFactionId: FactionId,
gameState: GameState,
availableCommands: Vector[AvailableCommand],
uhsWithQuests: Vector[UnaffiliatedHeroWithQuest]
): Option[CommandSelection] = {
val targetProvinceIds = uhsWithQuests.map(_.quest).collect { case q: FocusProvinceQuest => q.targetProvinceId }
val currentFocus = gameState.factions.get(actingFactionId).flatMap(_.focusProvinceId)
if currentFocus.exists(targetProvinceIds.contains) then None
else
availableCommands.collectFirst { case command: IssueOrdersAvailable => command }.flatMap { issueOrders =>
targetProvinceIds.find(issueOrders.availableFocusProvinces.contains).map { targetProvinceId =>
CommandSelection(
actingFactionId = actingFactionId,
actingProvinceId = issueOrders.actingProvinceId,
available = issueOrders,
selected = IssueOrdersSelected(
newOrders = issueOrders.currentOrders.map(order => ProvinceOrder(order.provinceId, order.orderType)),
newFocusProvince = Some(targetProvinceId)
),
reason = "fulfill FocusProvince quest"
)
}
}
end if
}
}
@@ -60,7 +60,7 @@ object ProvinceOrderQuestHelper {
available = issueOrders,
selected = IssueOrdersSelected(
newOrders = newOrders,
newFocusProvince = issueOrders.availableFocusProvinces.headOption
newFocusProvince = gameState.factions.get(actingFactionId).flatMap(_.focusProvinceId)
),
reason = reason
)
@@ -45,6 +45,7 @@ import net.eagle0.eagle.model.state.quest.{
ExilePrisonerQuest,
ExpandToProvincesQuest,
FightBeastsAloneQuest,
FocusProvinceQuest,
GiveToHeroesAcrossRealmQuest,
GiveToHeroesInProvinceQuest,
GrandArmyQuest,
@@ -157,6 +158,7 @@ object QuestCreationUtils {
swearBrotherhoodWithHeroQuests,
QuestCreatorFrom(betrayAllyQuests),
QuestCreatorFrom(borderSecurityQuests),
focusProvinceQuests,
QuestCreatorFrom(winBattleOutnumberedQuests),
winBattlesQuests,
QuestCreatorFrom(apprehendOutlawQuests),
@@ -889,8 +891,38 @@ object QuestCreationUtils {
factions: Vector[FactionT],
@unused battalions: Vector[BattalionT]
): Vector[Quest] =
borderProvinces(province, allProvinces, factions)
.map(borderProvince => BorderSecurityQuest(targetProvinceId = borderProvince.id))
end borderSecurityQuests
private def focusProvinceQuests(
province: ProvinceT,
allProvinces: Vector[ProvinceT],
factions: Vector[FactionT],
@unused battalions: Vector[BattalionT],
functionalRandom: FunctionalRandom
): RandomState[Vector[Quest]] = {
val candidates = borderProvinces(province, allProvinces, factions)
if candidates.isEmpty then RandomState(Vector.empty, functionalRandom)
else
functionalRandom.nextIntInclusive(3, 6).map { targetMonths =>
candidates.map { borderProvince =>
FocusProvinceQuest(
componentCount = targetMonths,
componentsFulfilled = 0,
targetProvinceId = borderProvince.id
)
}
}
}
private def borderProvinces(
province: ProvinceT,
allProvinces: Vector[ProvinceT],
factions: Vector[FactionT]
): Vector[ProvinceT] = {
val factionId = province.getRulingFactionId
// Create a quest for each province that has neighbors controlled by non-allied factions
FactionUtils
.provinces(factionId, allProvinces)
.filter { factionProvince =>
@@ -903,8 +935,7 @@ object QuestCreationUtils {
}
}
}
.map(borderProvince => BorderSecurityQuest(targetProvinceId = borderProvince.id))
end borderSecurityQuests
}
private def winBattleOutnumberedQuests(
@unused province: ProvinceT,
@@ -7,8 +7,14 @@ import net.eagle0.eagle.library.util.CommandSelection
import net.eagle0.eagle.model.state.{GameType, RoundPhase}
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.*
import net.eagle0.eagle.model.state.command.common.{DiplomacyOptionType, ImprovementType}
import net.eagle0.eagle.model.state.command.selected.SelectedCommand.{AlmsSelected, DiplomacySelected, ImproveSelected}
import net.eagle0.eagle.model.state.command.common.{DiplomacyOptionType, ImprovementType, ProvinceOrderType}
import net.eagle0.eagle.model.state.command.selected.ProvinceOrder
import net.eagle0.eagle.model.state.command.selected.SelectedCommand.{
AlmsSelected,
DiplomacySelected,
ImproveSelected,
IssueOrdersSelected
}
import net.eagle0.eagle.model.state.faction.concrete.FactionC
import net.eagle0.eagle.model.state.faction.FactionRelationship
import net.eagle0.eagle.model.state.faction.FactionRelationship.RelationshipLevel.{Hostile, Truce}
@@ -21,6 +27,7 @@ import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.quest.{
AlmsAcrossRealmQuest,
AlmsToProvinceQuest,
FocusProvinceQuest,
ImproveAgricultureQuest,
TruceCountQuest,
TruceWithFactionQuest,
@@ -264,6 +271,72 @@ class FulfillQuestsCommandSelectorTest extends AnyFlatSpec with Matchers with Be
)
}
it should "set the target focus for a focus province quest in a vassal-ruled province" in {
val focusQuestUhs = Vector(
UnaffiliatedHeroC(
heroId = 96,
unaffiliatedHeroType = UnaffiliatedHeroType.Resident,
recruitmentInfo = RecruitmentInfo.HasQuest(
FocusProvinceQuest(
componentCount = 4,
componentsFulfilled = 0,
targetProvinceId = 22
)
)
)
)
val gameStateWithFocusQuest = gameState.copy(
provinces = gameState.provinces.updated(
22,
gameState
.provinces(22)
.updateWith(
rulingHeroId = Some(11),
rulingFactionHeroIds = Vector(11),
unaffiliatedHeroes = focusQuestUhs
)
)
)
val issueOrders = IssueOrdersAvailable(
currentOrders = Vector(
ProvinceOrders(16, ProvinceOrderType.Entrust),
ProvinceOrders(18, ProvinceOrderType.Develop),
ProvinceOrders(22, ProvinceOrderType.Mobilize)
),
availableOrders = Vector(
ProvinceOrderType.Develop,
ProvinceOrderType.Entrust,
ProvinceOrderType.Mobilize
),
availableFocusProvinces = Vector(16, 18, 22),
actingProvinceId = 16
)
FulfillQuestsCommandSelector
.chosenFulfillEasyQuestsCommand(
actingFactionId = actingFactionId,
gameState = gameStateWithFocusQuest,
availableCommands = Vector(issueOrders),
functionalRandom = SeededRandom(0xcafe)
)
.newValue should contain(
CommandSelection(
actingFactionId = actingFactionId,
actingProvinceId = 16,
available = issueOrders,
selected = IssueOrdersSelected(
newOrders = Vector(
ProvinceOrder(16, ProvinceOrderType.Entrust),
ProvinceOrder(18, ProvinceOrderType.Develop),
ProvinceOrder(22, ProvinceOrderType.Mobilize)
),
newFocusProvince = Some(22)
),
reason = "fulfill FocusProvince quest"
)
)
}
it should "ignore local province quests from vassal-ruled provinces" in {
val almsCommand = AlmsAvailable(
actingProvinceId = 22,
@@ -37,7 +37,8 @@ class DevelopProvincesQuestCommandChooserTest extends AnyFlatSpec with Matchers
private def createGameState(
province2Neighbors: Vector[Neighbor] = Vector.empty,
province3Neighbors: Vector[Neighbor] = Vector.empty
province3Neighbors: Vector[Neighbor] = Vector.empty,
focusProvinceId: Option[ProvinceId] = None
): GameState = {
val factionLeader = HeroC(id = factionLeaderId, factionId = Some(actingFactionId))
val hero = HeroC(id = heroId, factionId = None)
@@ -66,7 +67,8 @@ class DevelopProvincesQuestCommandChooserTest extends AnyFlatSpec with Matchers
id = actingFactionId,
factionHeadId = factionLeaderId,
name = "Test Faction",
leaderIds = Vector(factionLeaderId)
leaderIds = Vector(factionLeaderId),
focusProvinceId = focusProvinceId
)
val enemyFaction = FactionC(
id = enemyFactionId,
@@ -233,6 +235,25 @@ class DevelopProvincesQuestCommandChooserTest extends AnyFlatSpec with Matchers
}
}
it should "preserve the faction focus when changing province orders" in {
val result = DevelopProvincesQuestCommandChooser.chosenDeterministicCommand(
actingFactionId = actingFactionId,
gameState = createGameState(focusProvinceId = Some(province2Id)),
availableCommands = Vector(issueOrdersAvailable),
uhsWithQuests = Vector(
UnaffiliatedHeroWithQuest(pid = province1Id, heroId = heroId, quest = developQuest)
)
)
inside(result) {
case Some(command) =>
inside(command.selected) {
case selected: IssueOrdersSelected =>
selected.newFocusProvince shouldBe Some(province2Id)
}
}
}
it should "return None when IssueOrders is not available" in {
val result = DevelopProvincesQuestCommandChooser.chosenDeterministicCommand(
actingFactionId = actingFactionId,
@@ -37,6 +37,7 @@ import net.eagle0.eagle.model.state.quest.{
DismissSpecificVassalQuest,
ExecutePrisonerQuest,
ExilePrisonerQuest,
FocusProvinceQuest,
GiveToHeroesAcrossRealmQuest,
GiveToHeroesInProvinceQuest,
GrandArmyQuest,
@@ -140,6 +141,47 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
)
)
"availableQuests" should "include a focus quest for an owned border province" in {
val focusQuests = QuestCreationUtils
.availableQuests(
province = province,
allProvinces = allProvinces,
factions = factions,
battalions = Vector.empty,
heroes = Vector.empty,
functionalRandom = new JankyRandom(new Random(1L))
)
.newValue
.collect { case q: FocusProvinceQuest => q }
focusQuests should have size 1
inside(focusQuests.head) {
case quest: FocusProvinceQuest =>
quest.targetProvinceId shouldBe province.id
quest.componentCount should be >= 3
quest.componentCount should be <= 6
quest.componentsFulfilled shouldBe 0
}
}
it should "not include a focus quest when the faction has no border province" in {
val interiorProvince = province.copy(neighbors = Vector(Neighbor(immediate1.id, 1)))
val ownedNeighbor = immediate1.copy(rulingFactionId = Some(fid))
QuestCreationUtils
.availableQuests(
province = interiorProvince,
allProvinces = Vector(interiorProvince, ownedNeighbor),
factions = factions,
battalions = Vector.empty,
heroes = Vector.empty,
functionalRandom = new JankyRandom(new Random(1L))
)
.newValue
.collect { case q: FocusProvinceQuest => q }
.shouldBe(empty)
}
"availableQuests" should "include an agriculture improvement event if it is possible to raise enough" in
forAtLeast(
1,