Gate armament tutorial on available upgrades (#8679)

* Gate armament tutorial on available upgrades

* Delay attack-ready tutorial guidance
This commit is contained in:
2026-07-17 14:38:54 -07:00
committed by GitHub
parent 73b8087e07
commit 467d2e2c82
13 changed files with 252 additions and 50 deletions
@@ -193,6 +193,17 @@ namespace eagle {
newRow.Battalion = battalion;
SetBattalionRowSelections(newRow, battalion);
});
var registry = TutorialManager.Instance?.TargetRegistry;
if (registry != null) {
registry.RegisterTargets(
"BattalionArmamentValues",
Enumerable.Range(0, battalionsTable.RowCount)
.Select(index => battalionsTable
.ComponentAt<BattalionRowController>(index)
.armamentLabel
.GetComponent<RectTransform>()));
}
}
void Start() {
@@ -49,7 +49,44 @@ namespace eagle0.Tests {
Assert.AreEqual("fixed/old_marek.png", script.steps[0].speakerImagePath);
Assert.AreEqual("Hold the line.", script.steps[0].dialogueText);
Assert.AreEqual("Click Fight!", script.steps[0].instructionText);
Assert.AreEqual("FightButton", script.steps[0].highlightTarget);
CollectionAssert.AreEqual(new[] { "FightButton" }, script.steps[0].highlightTargets);
Assert.IsTrue(script.steps[0].persistHighlight);
textProvider.Clear();
}
[Test]
public void ServerDialogueConversionResolvesMultipleUiFocusTargets() {
var textProvider = ClientTextProvider.Provider;
textProvider.Clear();
textProvider.HandleNewStreamingText("marek-name", "Old Marek", 0, true);
textProvider.HandleNewStreamingText("arm-copy", "Arm the troops.", 0, true);
var uiElements = new Net.Eagle0.Eagle.Views.TutorialUiElements();
uiElements.ElementIds.Add("VisitTownButton");
uiElements.ElementIds.Add("BattalionArmamentValues");
uiElements.ElementIds.Add("InfrastructureField");
var dialogue = new Net.Eagle0.Eagle.Views.TutorialDialogue {
Id = "tutorial_attack_prepare_arm"
};
dialogue.Steps.Add(new Net.Eagle0.Eagle.Views.TutorialDialogueStep {
SpeakerHeroId = 17,
DialogueTextId = "arm-copy",
Focus = new Net.Eagle0.Eagle.Views.TutorialFocus { UiElements = uiElements }
});
var converted = DialogueManager.TryCreateServerScript(
dialogue,
new Dictionary<int, EagleHeroView> {
[17] = new EagleHeroView { Id = 17, NameTextId = "marek-name" }
},
new Dictionary<int, EagleProvinceView>(),
textProvider,
out var script);
Assert.IsTrue(converted);
CollectionAssert.AreEqual(
new[] { "VisitTownButton", "BattalionArmamentValues", "InfrastructureField" },
script.steps[0].highlightTargets);
Assert.IsTrue(script.steps[0].persistHighlight);
textProvider.Clear();
}
@@ -394,10 +431,12 @@ namespace eagle0.Tests {
}
[Test]
public void FinalButtonHighlightPersistsUntilPlayerActs() {
public void FinalUiHighlightsPersistUntilPlayerActs() {
var managerObject = new GameObject("DialogueManager test");
var panelObject = new GameObject("DialoguePanel test", typeof(RectTransform));
var targetObject = new GameObject("LoyaltyLabel test", typeof(RectTransform));
var secondTargetObject =
new GameObject("InfrastructureField test", typeof(RectTransform));
try {
var manager = managerObject.AddComponent<DialogueManager>();
var panel = panelObject.AddComponent<DialoguePanelController>();
@@ -412,26 +451,38 @@ namespace eagle0.Tests {
new DialogueScript {
id = "tutorial_loyalty_warning",
steps = new List<DialogueStep> { new DialogueStep {
highlightTarget = "LoyaltyLabel",
highlightTargets =
new List<string> { "LoyaltyLabel", "InfrastructureField" },
persistHighlight = true
} }
});
InvokePrivate(manager, "HighlightRect", targetObject.GetComponent<RectTransform>());
Assert.NotNull(GetPrivateField<GameObject>(manager, "_activeHighlight"));
InvokePrivate(
manager,
"HighlightRects",
new List<RectTransform> {
targetObject.GetComponent<RectTransform>(),
secondTargetObject.GetComponent<RectTransform>()
});
Assert.AreEqual(
2,
GetPrivateField<List<GameObject>>(manager, "_activeHighlights").Count);
manager.EndDialogueWithoutCompletion();
Assert.NotNull(GetPrivateField<GameObject>(manager, "_activeHighlight"));
Assert.AreEqual(
2,
GetPrivateField<List<GameObject>>(manager, "_activeHighlights").Count);
manager.OnPlayerAction();
Assert.IsNull(GetPrivateField<GameObject>(manager, "_activeHighlight"));
Assert.IsEmpty(GetPrivateField<List<GameObject>>(manager, "_activeHighlights"));
Assert.IsEmpty(GetPrivateField<List<Image>>(manager, "_highlightEdges"));
} finally {
Object.DestroyImmediate(managerObject);
Object.DestroyImmediate(panelObject);
Object.DestroyImmediate(targetObject);
Object.DestroyImmediate(secondTargetObject);
}
}
@@ -39,7 +39,7 @@ namespace Eagle0.Tutorial {
private MapController _mapController;
private IGameModel _eagleModel;
private ShardokGameController _shardokController;
private GameObject _activeHighlight;
private readonly List<GameObject> _activeHighlights = new List<GameObject>();
private readonly List<Image> _highlightEdges = new List<Image>();
private readonly List<int> _activeUnitHighlightCells = new List<int>();
private DialogueUnitHighlight _activeUnitHighlightCriteria;
@@ -242,7 +242,15 @@ namespace Eagle0.Tutorial {
step.highlightProvince = province.Name;
break;
case TutorialFocus.TargetOneofCase.ButtonId:
step.highlightTarget = source.Focus.ButtonId;
step.highlightTargets.Add(source.Focus.ButtonId);
step.persistHighlight = index == dialogue.Steps.Count - 1;
break;
case TutorialFocus.TargetOneofCase.UiElements:
if (source.Focus.UiElements.ElementIds.Count == 0 ||
source.Focus.UiElements.ElementIds.Any(string.IsNullOrEmpty)) {
return false;
}
step.highlightTargets.AddRange(source.Focus.UiElements.ElementIds);
step.persistHighlight = index == dialogue.Steps.Count - 1;
break;
case TutorialFocus.TargetOneofCase.UnitId:
@@ -325,13 +333,15 @@ namespace Eagle0.Tutorial {
ClearHighlight();
ClearUnitHighlight();
if (!string.IsNullOrEmpty(step.highlightTarget)) {
// Highlight a UI element
var target = TutorialManager.Instance?.TargetRegistry?.GetTargetOrFind(
step.highlightTarget);
if (target != null && target.gameObject.activeInHierarchy) {
HighlightRect(target.GetComponent<RectTransform>());
}
if (step.highlightTargets.Count > 0) {
var registry = TutorialManager.Instance?.TargetRegistry;
var targets =
registry == null
? Enumerable.Empty<RectTransform>()
: step.highlightTargets.SelectMany(registry.GetTargetsOrFind)
.Where(target => target.gameObject.activeInHierarchy)
.Select(target => target.GetComponent<RectTransform>());
HighlightRects(targets);
} else if (!string.IsNullOrEmpty(step.highlightProvince) && _mapController != null) {
// Highlight a province on the map
var proxy = _mapController.GetProvinceHighlightProxy(step.highlightProvince);
@@ -423,8 +433,7 @@ namespace Eagle0.Tutorial {
bool keepHighlight = false;
if (_activeScript != null && _activeScript.steps.Count > 0) {
var lastStep = _activeScript.steps[_activeScript.steps.Count - 1];
keepHighlight = lastStep.persistHighlight &&
!string.IsNullOrEmpty(lastStep.highlightTarget);
keepHighlight = lastStep.persistHighlight && lastStep.highlightTargets.Count > 0;
}
if (markCompleted && _activeScript != null && !string.IsNullOrEmpty(_activeScript.id)) {
@@ -482,6 +491,17 @@ namespace Eagle0.Tutorial {
ClearHighlight();
AddHighlightRect(target);
}
private void HighlightRects(IEnumerable<RectTransform> targets) {
ClearHighlight();
foreach (var target in targets.Where(target => target != null).Distinct()) {
AddHighlightRect(target);
}
}
private void AddHighlightRect(RectTransform target) {
var go = new GameObject("DialogueHighlight");
go.transform.SetParent(target, false);
@@ -531,7 +551,7 @@ namespace Eagle0.Tutorial {
new Vector2(-highlightThickness, 0),
Vector2.zero);
_activeHighlight = go;
_activeHighlights.Add(go);
}
private void CreateBorderEdge(
@@ -555,10 +575,10 @@ namespace Eagle0.Tutorial {
}
private void ClearHighlight() {
if (_activeHighlight != null) {
Destroy(_activeHighlight);
_activeHighlight = null;
foreach (var highlight in _activeHighlights) {
if (highlight != null) { Destroy(highlight); }
}
_activeHighlights.Clear();
_highlightEdges.Clear();
}
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
namespace Eagle0.Tutorial {
[Serializable]
@@ -7,7 +8,7 @@ namespace Eagle0.Tutorial {
public string speakerImagePath;
public string dialogueText;
public string instructionText;
public string highlightTarget;
public List<string> highlightTargets = new List<string>();
public string highlightProvince;
public DialogueUnitHighlight highlightUnit;
public bool persistHighlight;
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Eagle0.Tutorial {
@@ -43,6 +44,8 @@ namespace Eagle0.Tutorial {
// Dynamic targets registered at runtime (e.g., command buttons from prefabs)
private Dictionary<string, RectTransform> _dynamicTargets =
new Dictionary<string, RectTransform>();
private Dictionary<string, IReadOnlyList<RectTransform>> _dynamicTargetGroups =
new Dictionary<string, IReadOnlyList<RectTransform>>();
/// <summary>
/// Registers a target at runtime. Use for prefab-instantiated UI elements.
@@ -50,13 +53,22 @@ namespace Eagle0.Tutorial {
public void RegisterTarget(string targetId, RectTransform target) {
if (string.IsNullOrEmpty(targetId) || target == null) return;
_dynamicTargets[targetId] = target;
_dynamicTargetGroups.Remove(targetId);
}
public void RegisterTargets(string targetId, IEnumerable<RectTransform> targets) {
if (string.IsNullOrEmpty(targetId) || targets == null) return;
_dynamicTargets.Remove(targetId);
_dynamicTargetGroups[targetId] = targets.Where(target => target != null).ToList();
}
/// <summary>
/// Unregisters a dynamically registered target.
/// </summary>
public void UnregisterTarget(string targetId) {
if (!string.IsNullOrEmpty(targetId)) { _dynamicTargets.Remove(targetId); }
if (string.IsNullOrEmpty(targetId)) return;
_dynamicTargets.Remove(targetId);
_dynamicTargetGroups.Remove(targetId);
}
/// <summary>
@@ -105,5 +117,14 @@ namespace Eagle0.Tutorial {
return null;
}
public IReadOnlyList<Transform> GetTargetsOrFind(string targetId) {
if (_dynamicTargetGroups.TryGetValue(targetId, out var group)) {
return group.Cast<Transform>().ToList();
}
var target = GetTargetOrFind(targetId);
return target == null ? new List<Transform>() : new List<Transform> { target };
}
}
}
@@ -39,9 +39,14 @@ message TutorialFocus {
string button_id = 2;
TutorialHexTile hex_tile = 3;
int32 unit_id = 4;
TutorialUiElements ui_elements = 5;
}
}
message TutorialUiElements {
repeated string element_ids = 1;
}
message TutorialHexTile {
int32 row = 1;
int32 column = 2;
@@ -111,6 +111,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/util:game_state_view_differ",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/province",
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:game_state_view_filter",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/types",
@@ -2,6 +2,7 @@ package net.eagle0.eagle.library
import net.eagle0.eagle.library.actions.applier.ActionResultWithResultingState
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.model.action_result.types.ActionResultType
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.game_state.GameState
@@ -110,7 +111,8 @@ object StrategicTutorialDialogueSelector {
actionResult.provinceId.exists(provinceId =>
isDevelopedNonKingFactionBorderProvince(after, provinceId) &&
!hasTrainedAttackForce(startingState, provinceId) &&
hasTrainedAttackForce(after, provinceId)
hasTrainedAttackForce(after, provinceId) &&
hasUpgradeableArmament(after, provinceId)
)
)(
TutorialDialogueContent.prepareAttackArm(marekId)
@@ -122,6 +124,11 @@ object StrategicTutorialDialogueSelector {
TutorialDialogueContent.attackPrepareTrainDialogueId,
TutorialDialogueContent.attackPrepareArmDialogueId
) &&
wasEmittedBeforeRound(
startingState,
TutorialDialogueContent.attackPrepareArmDialogueId,
after.currentRoundId
) &&
!hasEmitted(startingState, TutorialDialogueContent.attackReadyDialogueId) &&
hasPreparedAttackForce(after)
)(
@@ -143,6 +150,9 @@ object StrategicTutorialDialogueSelector {
private def hasEmittedAll(state: GameState, dialogueIds: String*): Boolean =
dialogueIds.forall(hasEmitted(state, _))
private def wasEmittedBeforeRound(state: GameState, dialogueId: String, roundId: Int): Boolean =
state.tutorialState.flatMap(_.emittedRound(dialogueId)).exists(_ < roundId)
private def shouldOfferPostBattleAlms(before: GameState, after: GameState): Boolean =
before.tutorialState
.flatMap(_.emittedRound(TutorialDialogueContent.postBattleRebuildDialogueId))
@@ -243,6 +253,14 @@ object StrategicTutorialDialogueSelector {
.sum >= ReadyTroopCount
}
private def hasUpgradeableArmament(state: GameState, provinceId: Int): Boolean =
state.provinces.get(provinceId).exists { province =>
val effectiveInfrastructure = ProvinceUtils.effectiveInfrastructure(province)
province.battalionIds
.flatMap(state.battalions.get)
.exists(_.armament < effectiveInfrastructure)
}
private def hasPreparedAttackForce(state: GameState): Boolean =
state.provinces.valuesIterator
.filter(province => isDevelopedNonKingFactionBorderProvince(state, province.id))
@@ -10,7 +10,8 @@ import net.eagle0.eagle.views.tutorial_dialogue.{
TutorialDialogueStep as TutorialDialogueStepProto,
TutorialFocus as TutorialFocusProto,
TutorialHexTile as TutorialHexTileProto,
TutorialTextReplacement as TutorialTextReplacementProto
TutorialTextReplacement as TutorialTextReplacementProto,
TutorialUiElements as TutorialUiElementsProto
}
object ActionResultViewConverter {
@@ -38,15 +39,19 @@ object ActionResultViewConverter {
dialogueTextId = step.dialogueTextId,
instructionTextId = step.instructionTextId,
focus = step.focus.map {
case net.eagle0.eagle.model.view.action_result.TutorialFocus.Province(provinceId) =>
case net.eagle0.eagle.model.view.action_result.TutorialFocus.Province(provinceId) =>
TutorialFocusProto(target = TutorialFocusProto.Target.ProvinceId(provinceId))
case net.eagle0.eagle.model.view.action_result.TutorialFocus.Button(buttonId) =>
case net.eagle0.eagle.model.view.action_result.TutorialFocus.Button(buttonId) =>
TutorialFocusProto(target = TutorialFocusProto.Target.ButtonId(buttonId))
case net.eagle0.eagle.model.view.action_result.TutorialFocus.HexTile(row, column) =>
case net.eagle0.eagle.model.view.action_result.TutorialFocus.UiElements(elementIds) =>
TutorialFocusProto(
target = TutorialFocusProto.Target.UiElements(TutorialUiElementsProto(elementIds = elementIds))
)
case net.eagle0.eagle.model.view.action_result.TutorialFocus.HexTile(row, column) =>
TutorialFocusProto(
target = TutorialFocusProto.Target.HexTile(TutorialHexTileProto(row = row, column = column))
)
case net.eagle0.eagle.model.view.action_result.TutorialFocus.Unit(unitId) =>
case net.eagle0.eagle.model.view.action_result.TutorialFocus.Unit(unitId) =>
TutorialFocusProto(target = TutorialFocusProto.Target.UnitId(unitId))
},
textReplacements = step.textReplacements.map(replacement =>
@@ -2,10 +2,11 @@ package net.eagle0.eagle.model.view.action_result
sealed trait TutorialFocus
object TutorialFocus {
case class Province(provinceId: Int) extends TutorialFocus
case class Button(buttonId: String) extends TutorialFocus
case class HexTile(row: Int, column: Int) extends TutorialFocus
case class Unit(unitId: Int) extends TutorialFocus
case class Province(provinceId: Int) extends TutorialFocus
case class Button(buttonId: String) extends TutorialFocus
case class UiElements(elementIds: Vector[String]) extends TutorialFocus
case class HexTile(row: Int, column: Int) extends TutorialFocus
case class Unit(unitId: Int) extends TutorialFocus
}
sealed trait TutorialTextReplacementValue
@@ -74,11 +75,17 @@ object TutorialDialogueValidator {
}
}
step.focus.foreach {
case TutorialFocus.Province(provinceId) if !provinceIds.contains(provinceId) =>
case TutorialFocus.Province(provinceId) if !provinceIds.contains(provinceId) =>
invalid(s"$label references missing province $provinceId")
case TutorialFocus.Button(buttonId) if isBlank(buttonId) =>
case TutorialFocus.Button(buttonId) if isBlank(buttonId) =>
invalid(s"$label has an empty button id")
case TutorialFocus.Unit(unitId) =>
case TutorialFocus.UiElements(elementIds) if elementIds.isEmpty =>
invalid(s"$label has no UI element ids")
case TutorialFocus.UiElements(elementIds) if elementIds.exists(isBlank) =>
invalid(s"$label has an empty UI element id")
case TutorialFocus.UiElements(elementIds) if elementIds.distinct.size != elementIds.size =>
invalid(s"$label repeats a UI element id")
case TutorialFocus.Unit(unitId) =>
unitIds match {
case None =>
invalid(s"$label uses unit focus without tactical unit context")
@@ -86,9 +93,9 @@ object TutorialDialogueValidator {
invalid(s"$label references missing tactical unit $unitId")
case _ =>
}
case _: TutorialFocus.HexTile =>
case _: TutorialFocus.HexTile =>
invalid(s"$label uses hex focus without tactical map context")
case _ =>
case _ =>
}
}
@@ -323,7 +323,11 @@ object TutorialDialogueContent {
marekId,
"tutorial_attack_prepare_arm_dialogue",
instructionTextId = Some("tutorial_attack_prepare_arm_instruction"),
focus = Some(TutorialFocus.Button("VisitTownButton"))
focus = Some(
TutorialFocus.UiElements(
Vector("VisitTownButton", "BattalionArmamentValues", "InfrastructureField")
)
)
)
)
)
@@ -375,6 +375,8 @@ class StrategicTutorialDialogueSelectorTest extends AnyFlatSpec with Matchers {
id = OnmaaId,
rulingFactionId = Some(PlayerFactionId),
support = 40,
infrastructure = 50,
infrastructureDevastation = 10,
battalionIds = Vector(501, 502),
neighbors = Vector(Neighbor(provinceId = 39, startingPositionIndex = 0))
)
@@ -446,13 +448,24 @@ class StrategicTutorialDialogueSelectorTest extends AnyFlatSpec with Matchers {
actingFactionId = Some(PlayerFactionId),
provinceId = Some(OnmaaId)
) should not contain TutorialDialogueContent.attackPrepareArmDialogueId
selectedIds(
val armDialogues = selectedDialogues(
almostTrained,
trained,
ActionResultType.Train,
actingFactionId = Some(PlayerFactionId),
provinceId = Some(OnmaaId)
) should contain("tutorial_attack_prepare_arm")
)
armDialogues.map(_.id) should contain("tutorial_attack_prepare_arm")
armDialogues
.find(_.id == TutorialDialogueContent.attackPrepareArmDialogueId)
.getOrElse(fail("Armament guidance was not emitted"))
.steps
.loneElement
.focus shouldBe Some(
TutorialFocus.UiElements(
Vector("VisitTownButton", "BattalionArmamentValues", "InfrastructureField")
)
)
selectedIds(
trained,
trained,
@@ -460,6 +473,26 @@ class StrategicTutorialDialogueSelectorTest extends AnyFlatSpec with Matchers {
actingFactionId = Some(PlayerFactionId),
provinceId = Some(OnmaaId)
) should not contain "tutorial_attack_prepare_arm"
val fullyArmedAlmostTrained = almostTrained.copy(
battalions = Map(
501 -> BattalionC(501, BattalionTypeId.LightInfantry, size = 750, training = 50, armament = 40),
502 -> BattalionC(502, BattalionTypeId.Longbowmen, size = 750, training = 49, armament = 40)
)
)
val fullyArmedTrained = fullyArmedAlmostTrained.copy(
battalions = fullyArmedAlmostTrained.battalions.updated(
502,
fullyArmedAlmostTrained.battalions(502).withTraining(50)
)
)
selectedIds(
fullyArmedAlmostTrained,
fullyArmedTrained,
ActionResultType.Train,
actingFactionId = Some(PlayerFactionId),
provinceId = Some(OnmaaId)
) should not contain TutorialDialogueContent.attackPrepareArmDialogueId
}
it should "wait for 40 support before beginning attack preparation" in {
@@ -581,14 +614,14 @@ class StrategicTutorialDialogueSelectorTest extends AnyFlatSpec with Matchers {
}
it should "suggest attacking only after organizing, training, and arming a developed hostile-border force" in {
val borderProvince = ProvinceC(
val borderProvince = ProvinceC(
id = OnmaaId,
rulingFactionId = Some(PlayerFactionId),
support = 40,
battalionIds = Vector(501, 502),
neighbors = Vector(Neighbor(provinceId = 39, startingPositionIndex = 0))
)
val factions = Map(
val factions = Map(
PlayerFactionId -> FactionC(
id = PlayerFactionId,
factionHeadId = marek.id,
@@ -597,31 +630,32 @@ class StrategicTutorialDialogueSelectorTest extends AnyFlatSpec with Matchers {
),
5 -> FactionC(id = 5, factionHeadId = 500, name = "Kojarians", leaderIds = Vector(500))
)
val readyBattalions = Map(
val readyBattalions = Map(
501 -> BattalionC(501, BattalionTypeId.LightInfantry, size = 750, training = 50, armament = 30),
502 -> BattalionC(502, BattalionTypeId.Longbowmen, size = 750, training = 50, armament = 30)
)
val readyForce = state(
val readyForce = state(
onmaa = borderProvince,
additionalProvinces = Map(39 -> ProvinceC(id = 39, rulingFactionId = Some(5))),
battalions = readyBattalions,
factions = factions
)
val throughTrain = withEmittedDialogueIds(
val throughTrain = withEmittedDialogueIds(
readyForce,
TutorialDialogueContent.attackPrepareOrganizeDialogueId,
TutorialDialogueContent.attackPrepareTrainDialogueId
)
val throughArm = withEmittedDialogueIds(
val throughArmSameRound = withEmittedDialogueIds(
readyForce,
TutorialDialogueContent.attackPrepareOrganizeDialogueId,
TutorialDialogueContent.attackPrepareTrainDialogueId,
TutorialDialogueContent.attackPrepareArmDialogueId
)
val undeveloped = throughArm.copy(
val throughArm = throughArmSameRound.copy(currentRoundId = 2)
val undeveloped = throughArm.copy(
provinces = throughArm.provinces.updated(OnmaaId, borderProvince.copy(support = 39))
)
val alreadyReady = withEmittedDialogueIds(
val alreadyReady = withEmittedDialogueIds(
readyForce,
TutorialDialogueContent.attackPrepareOrganizeDialogueId,
TutorialDialogueContent.attackPrepareTrainDialogueId,
@@ -635,6 +669,13 @@ class StrategicTutorialDialogueSelectorTest extends AnyFlatSpec with Matchers {
TutorialDialogueContent.attackReadyDialogueId
selectedIds(undeveloped, undeveloped, ActionResultType.ArmTroops) should not contain
TutorialDialogueContent.attackReadyDialogueId
selectedIds(
throughArmSameRound,
throughArmSameRound,
ActionResultType.ArmTroops,
actingFactionId = Some(PlayerFactionId),
provinceId = Some(OnmaaId)
) should not contain TutorialDialogueContent.attackReadyDialogueId
selectedIds(
throughArm,
throughArm,
@@ -60,6 +60,23 @@ class TutorialDialogueValidatorTest extends AnyFlatSpec with Matchers {
validate(dialogue = tacticalDialogue, unitIds = Some(Set(27))) shouldBe tacticalDialogue
}
it should "accept multiple UI focus targets" in {
val uiDialogue = validDialogue.copy(
steps = validDialogue.steps.map(
_.copy(focus = Some(TutorialFocus.UiElements(Vector("ArmamentValues", "InfrastructureField"))))
)
)
validate(dialogue = uiDialogue) shouldBe uiDialogue
}
it should "reject an empty collection of UI focus targets" in {
val uiDialogue = validDialogue.copy(
steps = validDialogue.steps.map(_.copy(focus = Some(TutorialFocus.UiElements(Vector.empty))))
)
val error = the[IllegalStateException] thrownBy validate(dialogue = uiDialogue)
error.getMessage should include("no UI element ids")
}
it should "reject a tactical unit focus missing from the snapshot" in {
val tacticalDialogue = validDialogue.copy(
steps = validDialogue.steps.map(_.copy(focus = Some(TutorialFocus.Unit(27))))