mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 20:55:44 +00:00
Version changed available command sets (#8687)
* Version changed available command sets * Preserve active command across model refreshes
This commit is contained in:
+2
@@ -114,5 +114,7 @@ namespace eagle {
|
||||
// Note: Command tutorials are now opened explicitly instead of automatic popups.
|
||||
// See CommandPanelController.ShowCurrentCommandHelp() for the help-panel flow.
|
||||
}
|
||||
|
||||
public void UpdateModel(IGameModel model) { _model = model; }
|
||||
}
|
||||
}
|
||||
|
||||
+72
-45
@@ -436,10 +436,10 @@ namespace eagle {
|
||||
Model.AvailableCommandsByProvince.Count > 0);
|
||||
}
|
||||
|
||||
public void ProvinceWasSelected(ProvinceId? pid) {
|
||||
public void ProvinceWasSelected(ProvinceId? pid, bool selectRecommendedCommand = true) {
|
||||
if (Model == null) { return; }
|
||||
if (pid.HasValue) {
|
||||
SelectRecommendedCommand(pid.Value);
|
||||
if (selectRecommendedCommand) { SelectRecommendedCommand(pid.Value); }
|
||||
var currentProvince = Model.Provinces[pid.Value];
|
||||
provinceInfoPanelController.Province = currentProvince;
|
||||
freeHeroesTableController.CurrentProvince = currentProvince;
|
||||
@@ -449,7 +449,7 @@ namespace eagle {
|
||||
factionsTableControllerWide.CurrentProvince = currentProvince;
|
||||
heroesAndBattalionsPanelController.CurrentProvince = currentProvince;
|
||||
} else {
|
||||
SelectRecommendedCommand(null);
|
||||
if (selectRecommendedCommand) { SelectRecommendedCommand(null); }
|
||||
provinceInfoPanelController.Province = null;
|
||||
freeHeroesTableController.CurrentProvince = null;
|
||||
movingArmiesTableControllerNarrow.CurrentProvince = null;
|
||||
@@ -586,8 +586,7 @@ namespace eagle {
|
||||
|
||||
// Preserve the user's selection across non-strategic refreshes. A result-bearing
|
||||
// strategic update must still flow through even when its command token is unchanged:
|
||||
// automatic results can change visible state and command availability without
|
||||
// advancing the posted-command token.
|
||||
// automatic results can change visible state without changing available commands.
|
||||
if (hasExistingCommands && hasIncomingCommands && !commandTokenChanged &&
|
||||
!strategicUpdateChanged) {
|
||||
mapController.UpdateBattleEffects(_newModel);
|
||||
@@ -596,8 +595,16 @@ namespace eagle {
|
||||
}
|
||||
|
||||
var oldModel = Model;
|
||||
var activeCommandStillAvailable = CommandStillAvailable(
|
||||
_newModel,
|
||||
SelectedProvince,
|
||||
_commandPanelController.AvailableCommand);
|
||||
Model = _newModel;
|
||||
|
||||
if (availableCommandsChanged && !commandTokenChanged) {
|
||||
LogCommandTokenInvariantViolation(oldModel, _newModel);
|
||||
}
|
||||
|
||||
provinceInfoPanelController.Model = _newModel;
|
||||
freeHeroesTableController.Model = _newModel;
|
||||
movingArmiesTableControllerNarrow.Model = _newModel;
|
||||
@@ -610,6 +617,10 @@ namespace eagle {
|
||||
dominionPanelControllerWide.Model = _newModel;
|
||||
mapController.Model = _newModel;
|
||||
|
||||
if (activeCommandStillAvailable) {
|
||||
_commandPanelController.Selector.UpdateModel(Model);
|
||||
}
|
||||
|
||||
if (Model == null) {
|
||||
battleProgressPanel.gameObject.SetActive(false);
|
||||
return;
|
||||
@@ -627,49 +638,11 @@ namespace eagle {
|
||||
|
||||
SetMusic();
|
||||
|
||||
if ((oldModel == null || commandTokenChanged || availableCommandsChanged) &&
|
||||
if (!activeCommandStillAvailable &&
|
||||
(oldModel == null || commandTokenChanged || availableCommandsChanged) &&
|
||||
Model.AvailableCommandsByProvince.Count > 0) {
|
||||
if (oldModel != null && oldModel.AvailableCommandsByProvince.Count > 0 &&
|
||||
(commandTokenChanged || availableCommandsChanged)) {
|
||||
string str = "We got replacement commands, but we already had commands. " +
|
||||
oldModel.CommandToken + " " + _newModel.CommandToken;
|
||||
|
||||
var oldProvinces = oldModel.AvailableCommandsByProvince.Keys.ToHashSet();
|
||||
var newProvinces = _newModel.AvailableCommandsByProvince.Keys.ToHashSet();
|
||||
|
||||
if (!oldProvinces.SetEquals(newProvinces)) {
|
||||
var oldNotNew = oldProvinces.Except(newProvinces).ToList();
|
||||
if (oldNotNew.Any()) {
|
||||
str += $"\nWe used to have commands for provinces {string.Join(", ", oldNotNew)} but now we don't";
|
||||
}
|
||||
|
||||
var newNotOld = newProvinces.Except(oldProvinces).ToList();
|
||||
if (newNotOld.Any()) {
|
||||
str += $"\nWe now have commands for provinces {string.Join(", ", newNotOld)} where we didn't before";
|
||||
}
|
||||
}
|
||||
foreach (var kv in oldModel.AvailableCommandsByProvince) {
|
||||
if (_newModel.AvailableCommandsByProvince.ContainsKey(kv.Key)) {
|
||||
var oldCommands = kv.Value;
|
||||
var newCommands = _newModel.AvailableCommandsByProvince[kv.Key];
|
||||
if (oldCommands.Commands.Count != newCommands.Commands.Count) {
|
||||
str += $"\nWe had {oldCommands.Commands.Count} but now we have {newCommands.Commands.Count}";
|
||||
}
|
||||
|
||||
foreach (var cmd in oldCommands.Commands) {
|
||||
if (!newCommands.Commands.Contains(cmd)) {
|
||||
str += $"\nWe had {cmd} but now we don't";
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var cmd in newCommands.Commands) {
|
||||
if (!oldCommands.Commands.Contains(cmd)) {
|
||||
str += $"\nWe have {cmd}, we didn't before";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Debug.LogWarning(str);
|
||||
ClearActiveCommandUi();
|
||||
}
|
||||
|
||||
@@ -767,6 +740,60 @@ namespace eagle {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool
|
||||
CommandStillAvailable(IGameModel model, ProvinceId? provinceId, AvailableCommand command) {
|
||||
return model != null && provinceId is ProvinceId pid && command != null &&
|
||||
model.AvailableCommandsByProvince.TryGetValue(pid, out var commands) &&
|
||||
commands.Commands.Contains(command);
|
||||
}
|
||||
|
||||
private static void LogCommandTokenInvariantViolation(
|
||||
IGameModel oldModel,
|
||||
IGameModel newModel) {
|
||||
string str = "Available commands changed without command token advancement. Token " +
|
||||
oldModel.CommandToken;
|
||||
|
||||
var oldProvinces = oldModel.AvailableCommandsByProvince.Keys.ToHashSet();
|
||||
var newProvinces = newModel.AvailableCommandsByProvince.Keys.ToHashSet();
|
||||
|
||||
var oldNotNew = oldProvinces.Except(newProvinces).ToList();
|
||||
if (oldNotNew.Any()) {
|
||||
str += $"\nWe used to have commands for provinces {string.Join(", ", oldNotNew)} but now we don't";
|
||||
}
|
||||
|
||||
var newNotOld = newProvinces.Except(oldProvinces).ToList();
|
||||
if (newNotOld.Any()) {
|
||||
str += $"\nWe now have commands for provinces {string.Join(", ", newNotOld)} where we didn't before";
|
||||
}
|
||||
|
||||
foreach (var kv in oldModel.AvailableCommandsByProvince) {
|
||||
if (!newModel.AvailableCommandsByProvince.TryGetValue(
|
||||
kv.Key,
|
||||
out var newCommands)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var oldCommands = kv.Value;
|
||||
if (oldCommands.Commands.Count != newCommands.Commands.Count) {
|
||||
str += $"\nWe had {oldCommands.Commands.Count} but now we have {newCommands.Commands.Count}";
|
||||
}
|
||||
|
||||
foreach (var cmd in oldCommands.Commands) {
|
||||
if (!newCommands.Commands.Contains(cmd)) {
|
||||
str += $"\nWe had {cmd} but now we don't";
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var cmd in newCommands.Commands) {
|
||||
if (!oldCommands.Commands.Contains(cmd)) {
|
||||
str += $"\nWe have {cmd}, we didn't before";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.LogError(str);
|
||||
}
|
||||
|
||||
private void UpdateBattleProgressDisplay(IGameModel model) {
|
||||
if (model.ShardokBattles.Any()) {
|
||||
battleProgressPanel.gameObject.SetActive(true);
|
||||
|
||||
@@ -124,7 +124,8 @@ namespace eagle {
|
||||
|
||||
UpdateProvinceEffects(value);
|
||||
|
||||
eagleGameController.ProvinceWasSelected(_selectedProvinceId);
|
||||
eagleGameController
|
||||
.ProvinceWasSelected(_selectedProvinceId, selectRecommendedCommand: false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ namespace eagle0.Tests {
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ResultBearingUpdateReplacesCommandsWhenTokenIsUnchanged() {
|
||||
public void ResultBearingUpdateDefensivelyReplacesCommandsWhenTokenIsUnchanged() {
|
||||
const long gameId = 7124;
|
||||
const long token = 17;
|
||||
const int provinceId = 34;
|
||||
|
||||
+29
-5
@@ -1,11 +1,15 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import scala.collection.immutable.SortedMap
|
||||
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.library.actions.availability.AvailableCommandsFactory
|
||||
import net.eagle0.eagle.library.util.validations.Validator
|
||||
import net.eagle0.eagle.model.action_result.{ActionResultT, NotificationDetails, NotificationT}
|
||||
import net.eagle0.eagle.model.action_result.concrete.NotificationC
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.LlmRequestT
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.command.available.OneProvinceAvailableCommands
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
@@ -47,6 +51,8 @@ object ActionResultApplierImpl {
|
||||
class ActionResultApplierImpl(validator: Option[Validator]) extends ActionResultApplier {
|
||||
import ActionResultApplierImpl.{Validatable, ValidatableWithGameState, given}
|
||||
|
||||
private val availableCommandsFactory = new AvailableCommandsFactory()
|
||||
|
||||
// Generic single-argument validate
|
||||
def validate[T: Validatable](value: T): T =
|
||||
validator.fold(value)(v => summon[Validatable[T]].validate(v, value))
|
||||
@@ -108,8 +114,9 @@ class ActionResultApplierImpl(validator: Option[Validator]) extends ActionResult
|
||||
}
|
||||
}
|
||||
|
||||
// Apply all remaining entity changes using extension methods
|
||||
val finalState = stateAfterChangedBattalions
|
||||
// Apply all remaining entity changes using extension methods. Command tokens are
|
||||
// updated afterward so they version the derived command sets produced by this state.
|
||||
val stateBeforeCommandTokenUpdate = stateAfterChangedBattalions
|
||||
.applyProvinceActed(result.provinceIdActed)
|
||||
.applyLastCommand(result.provinceId, result.lastCommandTypeForActingProvince)
|
||||
.applyNewBattalions(result.newBattalions, result.provinceId)
|
||||
@@ -126,11 +133,20 @@ class ActionResultApplierImpl(validator: Option[Validator]) extends ActionResult
|
||||
.applyRemovedNotifications(result.removedNotifications)
|
||||
.applyNewSeed(result.newRandomSeed)
|
||||
.applyChronicleEntry(result.newChronicleEntry)
|
||||
.applyCommandCountUpdate(
|
||||
Option.when(result.lastCommandTypeForActingProvince.nonEmpty)(result.actingFactionId).flatten
|
||||
)
|
||||
.applyNewBattalionTypes(result.newBattalionTypes)
|
||||
|
||||
val factionsWithChangedCommands = stateBeforeCommandTokenUpdate.factions.keySet.filter { factionId =>
|
||||
availableCommands(startingState, factionId) != availableCommands(stateBeforeCommandTokenUpdate, factionId)
|
||||
}
|
||||
val factionThatPostedCommand =
|
||||
Option.when(result.lastCommandTypeForActingProvince.nonEmpty)(result.actingFactionId).flatten.toSet
|
||||
val factionsWithNewCommandTokens = factionsWithChangedCommands ++ factionThatPostedCommand
|
||||
|
||||
val finalState = factionsWithNewCommandTokens.toVector.sorted
|
||||
.foldLeft(stateBeforeCommandTokenUpdate) { (state, factionId) =>
|
||||
state.applyCommandCountUpdate(Some(factionId))
|
||||
}
|
||||
|
||||
val enhancedResult = withHeroReadyToJoinNotifications(result, startingState, finalState)
|
||||
|
||||
// Validate final game state
|
||||
@@ -142,6 +158,14 @@ class ActionResultApplierImpl(validator: Option[Validator]) extends ActionResult
|
||||
)
|
||||
}
|
||||
|
||||
private def availableCommands(
|
||||
gameState: GameState,
|
||||
factionId: FactionId
|
||||
): SortedMap[ProvinceId, OneProvinceAvailableCommands] =
|
||||
if gameState.factions.contains(factionId) then
|
||||
availableCommandsFactory.availablePlayerCommands(gameState, factionId)
|
||||
else SortedMap.empty
|
||||
|
||||
private def withHeroReadyToJoinNotifications(
|
||||
result: ActionResultT,
|
||||
startingState: GameState,
|
||||
|
||||
@@ -198,6 +198,7 @@ scala_library(
|
||||
deps = [
|
||||
":action_result_applier",
|
||||
":game_state_extensions",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/availability",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/validations:validator",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:notification_concrete",
|
||||
|
||||
+85
-4
@@ -27,7 +27,7 @@ import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.backstory_version.BackstoryVersion
|
||||
import net.eagle0.eagle.model.state.hero.concrete.HeroC
|
||||
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
|
||||
import net.eagle0.eagle.model.state.quest.AllianceQuest
|
||||
import net.eagle0.eagle.model.state.quest.{AllianceQuest, DefeatFactionQuest}
|
||||
import net.eagle0.eagle.model.state.run_status.RunStatus
|
||||
import net.eagle0.eagle.model.state.shardok_battle.{BattleType, ShardokBattle}
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.{RecruitmentInfo, UnaffiliatedHeroType}
|
||||
@@ -78,6 +78,36 @@ class ActionResultApplierImplTest extends AnyFlatSpec with Matchers with BeforeA
|
||||
)
|
||||
)
|
||||
|
||||
private def gameStateWithReturnCommand: GameState = {
|
||||
val factionId = 11
|
||||
val leaderId = 12
|
||||
val provinceId = 13
|
||||
|
||||
baseGameState.copy(
|
||||
provinces = Map(
|
||||
provinceId -> ProvinceC(
|
||||
id = provinceId,
|
||||
rulingFactionId = Some(factionId),
|
||||
rulingHeroId = Some(leaderId),
|
||||
rulingFactionHeroIds = Vector(leaderId),
|
||||
rulerIsVisitingTown = true
|
||||
)
|
||||
),
|
||||
heroes = Map(
|
||||
leaderId -> HeroC(id = leaderId, factionId = Some(factionId))
|
||||
),
|
||||
factions = Map(
|
||||
factionId -> FactionC(
|
||||
id = factionId,
|
||||
factionHeadId = leaderId,
|
||||
name = "Test Faction",
|
||||
leaderIds = Vector(leaderId)
|
||||
)
|
||||
),
|
||||
factionCommandCounts = Map(factionId -> 7)
|
||||
)
|
||||
}
|
||||
|
||||
// ============ Basic State Updates ============
|
||||
|
||||
"any result" should "increment the action result count" in {
|
||||
@@ -90,10 +120,11 @@ class ActionResultApplierImplTest extends AnyFlatSpec with Matchers with BeforeA
|
||||
}
|
||||
|
||||
it should "not advance command tokens for automatic results with an acting faction" in {
|
||||
val startingState = baseGameState.copy(factionCommandCounts = Map(11 -> 7))
|
||||
val startingState = gameStateWithReturnCommand
|
||||
val actionResult = ActionResultC(
|
||||
actionResultType = ActionResultType.PrisonerMoveTookEffect,
|
||||
actingFactionId = Some(11)
|
||||
actingFactionId = Some(11),
|
||||
newRandomSeed = Some(2L)
|
||||
)
|
||||
|
||||
val result = applier.applyActionResult(startingState, actionResult)
|
||||
@@ -101,8 +132,58 @@ class ActionResultApplierImplTest extends AnyFlatSpec with Matchers with BeforeA
|
||||
result.resultingState.factionCommandCounts shouldBe Map(11 -> 7)
|
||||
}
|
||||
|
||||
it should "advance command tokens when an automatic result changes available commands" in {
|
||||
val startingState = gameStateWithReturnCommand
|
||||
val actionResult = ActionResultC(
|
||||
actionResultType = ActionResultType.EndPlayerCommandsPhase,
|
||||
newRoundPhase = Some(RoundPhase.FoodConsumption)
|
||||
)
|
||||
|
||||
val result = applier.applyActionResult(startingState, actionResult)
|
||||
|
||||
result.resultingState.factionCommandCounts shouldBe Map(11 -> 8)
|
||||
}
|
||||
|
||||
it should "advance command tokens when automatic quest fulfillment replaces the decline command" in {
|
||||
val heroWithQuest = UnaffiliatedHeroC(
|
||||
heroId = 14,
|
||||
unaffiliatedHeroType = UnaffiliatedHeroType.Resident,
|
||||
recruitmentInfo = RecruitmentInfo.HasQuest(DefeatFactionQuest(targetFactionId = 99)),
|
||||
factionBiases = Map(11 -> 200.0)
|
||||
)
|
||||
val startingState = gameStateWithReturnCommand.copy(
|
||||
provinces = Map(
|
||||
13 -> ProvinceC(
|
||||
id = 13,
|
||||
rulingFactionId = Some(11),
|
||||
rulingHeroId = Some(12),
|
||||
rulingFactionHeroIds = Vector(12),
|
||||
rulerIsVisitingTown = true,
|
||||
unaffiliatedHeroes = Vector(heroWithQuest)
|
||||
)
|
||||
),
|
||||
heroes = gameStateWithReturnCommand.heroes.updated(14, HeroC(id = 14))
|
||||
)
|
||||
val actionResult = ActionResultC(
|
||||
actionResultType = ActionResultType.QuestFulfilled,
|
||||
actingFactionId = Some(11),
|
||||
changedProvinces = Vector(
|
||||
ChangedProvinceC(
|
||||
provinceId = 13,
|
||||
changedUnaffiliatedHeroes = Vector(
|
||||
heroWithQuest.copy(recruitmentInfo = RecruitmentInfo.WouldJoin)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = applier.applyActionResult(startingState, actionResult)
|
||||
|
||||
result.resultingState.factionCommandCounts shouldBe Map(11 -> 8)
|
||||
}
|
||||
|
||||
it should "advance command tokens for posted command results" in {
|
||||
val startingState = baseGameState.copy(factionCommandCounts = Map(11 -> 7))
|
||||
val startingState = gameStateWithReturnCommand
|
||||
val actionResult = ActionResultC(
|
||||
actionResultType = ActionResultType.OrdersIssued,
|
||||
actingFactionId = Some(11),
|
||||
|
||||
Reference in New Issue
Block a user