Pace post-battle tutorial guidance by month (#8682)

This commit is contained in:
2026-07-17 15:18:42 -07:00
committed by GitHub
parent 4dd30a9863
commit 2a660f05d7
5 changed files with 75 additions and 99 deletions
@@ -1,8 +1,6 @@
using System.Collections.Generic;
using System.Reflection;
using Eagle0.Tutorial;
using Net.Eagle0.Eagle.Common;
using Net.Eagle0.Eagle.Views;
using NUnit.Framework;
using Shardok;
using UnityEngine;
@@ -432,31 +430,6 @@ namespace eagle0.Tests {
} finally { Object.DestroyImmediate(gameObject); }
}
[Test]
public void AlmsDialogueWaitsForTheMonthAfterImproveDialogueWasPresented() {
var gameObject = new GameObject("DialogueManager test");
try {
var manager = gameObject.AddComponent<DialogueManager>();
var updater = new eagle.GameModelUpdater(987654321, null, null);
var model = GetPrivateField<eagle.IGameModel>(updater, "_currentModel");
SetCurrentDate(model, year: 12, month: 4);
manager.OnEagleModelUpdated(model);
InvokePrivate(manager, "RecordPresentedMonth", "post_battle_rebuild");
Assert.IsTrue(
InvokePrivate<bool>(manager, "ShouldWaitToPresent", "post_battle_alms"));
SetCurrentDate(model, year: 12, month: 5);
Assert.IsFalse(
InvokePrivate<bool>(manager, "ShouldWaitToPresent", "post_battle_alms"));
Assert.IsFalse(InvokePrivate<bool>(
manager,
"ShouldWaitToPresent",
"tutorial_attack_ready"));
} finally { Object.DestroyImmediate(gameObject); }
}
[Test]
public void FinalUiHighlightsPersistUntilPlayerActs() {
var managerObject = new GameObject("DialogueManager test");
@@ -537,14 +510,6 @@ namespace eagle0.Tests {
return (T)method.Invoke(target, null);
}
private static T InvokePrivate<T>(object target, string methodName, params object[] args) {
var method = target.GetType().GetMethod(
methodName,
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(method, $"Missing private method '{methodName}'");
return (T)method.Invoke(target, args);
}
private static void InvokePrivate(object target, string methodName, params object[] args) {
var method = target.GetType().GetMethod(
methodName,
@@ -578,14 +543,5 @@ namespace eagle0.Tests {
Assert.NotNull(property);
property.SetValue(controller, model);
}
private static void SetCurrentDate(eagle.IGameModel model, int year, int month) {
var property = model.GetType().GetProperty(
"GsView",
BindingFlags.Instance | BindingFlags.Public);
Assert.NotNull(property);
var gameState = (GameStateView)property.GetValue(model);
gameState.CurrentDate = new Date { Year = year, Month = month };
}
}
}
@@ -33,8 +33,6 @@ namespace Eagle0.Tutorial {
public Color unitHighlightSecondaryColor = new Color(1f, 0.85f, 0f, 0.2f);
private const string TutorialUnitOverlayIdPrefix = "tutorial_unit_highlight_";
private const string PostBattleRebuildDialogueId = "post_battle_rebuild";
private const string PostBattleAlmsDialogueId = "post_battle_alms";
private DialogueScript _activeScript;
private int _currentStepIndex;
@@ -53,7 +51,6 @@ namespace Eagle0.Tutorial {
private readonly HashSet<string> _receivedServerDialogueIds = new HashSet<string>();
private bool _persistingHighlight;
private long? _progressGameId;
private int? _postBattleRebuildPresentedMonth;
/// <summary>
/// Whether a dialogue is currently being displayed.
@@ -137,7 +134,6 @@ namespace Eagle0.Tutorial {
TryPlayPendingServerDialogue();
return;
}
if (ShouldWaitToPresent(dialogue.Id)) return;
var model = _eagleModel;
if (model == null || !TryCreateServerScript(
@@ -296,7 +292,6 @@ namespace Eagle0.Tutorial {
foreach (var id in TutorialDialogueProgressStore.LoadCompletedScriptIds(gameId)) {
_completedScriptIds.Add(id);
}
_postBattleRebuildPresentedMonth = null;
}
private void StartScript(DialogueScript script) {
@@ -308,7 +303,6 @@ namespace Eagle0.Tutorial {
_activeScript = script;
_currentStepIndex = 0;
RecordPresentedMonth(script.id);
// In combat, always anchor to top so the panel doesn't cover the hex grid.
// On the strategic map, always use the default position.
var position = _shardokController?.Model != null ? "top" : null;
@@ -385,9 +379,6 @@ namespace Eagle0.Tutorial {
_completedScriptIds.Remove(id);
_receivedServerDialogueIds.Remove(id);
}
if (ids.Contains(PostBattleRebuildDialogueId)) {
_postBattleRebuildPresentedMonth = null;
}
RemovePendingServerDialogues(ids, _pendingServerDialogues);
RemovePendingServerDialogues(ids, _pendingTacticalServerDialogues);
@@ -413,7 +404,6 @@ namespace Eagle0.Tutorial {
_receivedServerDialogueIds.Clear();
_pendingServerDialogues.Clear();
_pendingTacticalServerDialogues.Clear();
_postBattleRebuildPresentedMonth = null;
SaveProgress();
}
@@ -425,32 +415,6 @@ namespace Eagle0.Tutorial {
_receivedServerDialogueIds.Clear();
_pendingServerDialogues.Clear();
_pendingTacticalServerDialogues.Clear();
_postBattleRebuildPresentedMonth = null;
}
private bool ShouldWaitToPresent(string dialogueId) {
if (dialogueId != PostBattleAlmsDialogueId ||
!_postBattleRebuildPresentedMonth.HasValue) {
return false;
}
// Strategic dialogue can remain queued while a tactical battle is open. Use the
// month John was actually presented, rather than the server emission month, so Elena
// cannot appear directly behind him after returning to the strategic map.
var currentMonth = CurrentMonth();
return currentMonth.HasValue &&
currentMonth.Value <= _postBattleRebuildPresentedMonth.Value;
}
private void RecordPresentedMonth(string dialogueId) {
if (dialogueId == PostBattleRebuildDialogueId) {
_postBattleRebuildPresentedMonth = CurrentMonth();
}
}
private int? CurrentMonth() {
var date = _eagleModel?.CurrentDate;
return date == null ? null : date.Year * 12 + date.Month;
}
/// <summary>
@@ -40,10 +40,7 @@ object StrategicTutorialDialogueSelector {
TutorialDialogueContent.gameOver(marekId)
),
Option.when(tarnVanished)(TutorialDialogueContent.postBattleAftermath(marekId)),
Option.when(
(tarnVanished && !hasCapturedHeroes(after)) ||
(hasCapturedHeroes(startingState) && !hasCapturedHeroes(after))
)(
Option.when(shouldOfferPostBattleRebuild(startingState, after))(
TutorialDialogueContent.postBattleRebuild(
marekId = marekId,
ranilId = heroId(after, "John Ranil")
@@ -162,6 +159,13 @@ object StrategicTutorialDialogueSelector {
.get(OnmaaProvinceId)
.exists(province => province.rulingFactionId.contains(PlayerFactionId) && province.support < DevelopedSupport)
private def shouldOfferPostBattleRebuild(before: GameState, after: GameState): Boolean =
before.tutorialState
.flatMap(_.emittedRound(TutorialDialogueContent.postBattleAftermathDialogueId))
.contains(after.currentRoundId - 1) &&
!hasEmitted(before, TutorialDialogueContent.postBattleRebuildDialogueId) &&
!hasCapturedHeroes(after)
private def hasCapturedHeroes(state: GameState): Boolean =
state.provinces.values.exists(province =>
province.rulingFactionId.contains(PlayerFactionId) && province.capturedHeroes.nonEmpty
@@ -1,15 +1,16 @@
package net.eagle0.eagle.model.view.action_result
object TutorialDialogueContent {
val playerFactionId: Int = 3
val postBattleRebuildDialogueId: String = "post_battle_rebuild"
val postBattleAlmsDialogueId: String = "post_battle_alms"
val heroDepartedDialogueId: String = "tutorial_hero_departed"
val heroDepartedAgainDialogueId: String = "tutorial_hero_departed_again"
val attackPrepareOrganizeDialogueId = "tutorial_attack_prepare_organize"
val attackPrepareTrainDialogueId = "tutorial_attack_prepare_train"
val attackPrepareArmDialogueId = "tutorial_attack_prepare_arm"
val attackReadyDialogueId = "tutorial_attack_ready"
val playerFactionId: Int = 3
val postBattleAftermathDialogueId: String = "post_battle_aftermath"
val postBattleRebuildDialogueId: String = "post_battle_rebuild"
val postBattleAlmsDialogueId: String = "post_battle_alms"
val heroDepartedDialogueId: String = "tutorial_hero_departed"
val heroDepartedAgainDialogueId: String = "tutorial_hero_departed_again"
val attackPrepareOrganizeDialogueId = "tutorial_attack_prepare_organize"
val attackPrepareTrainDialogueId = "tutorial_attack_prepare_train"
val attackPrepareArmDialogueId = "tutorial_attack_prepare_arm"
val attackReadyDialogueId = "tutorial_attack_ready"
val openingFirstDialogueTextId: String = "tutorial_opening_dialogue_1"
val openingSecondDialogueTextId: String = "tutorial_opening_dialogue_2"
@@ -111,7 +112,7 @@ object TutorialDialogueContent {
)
def postBattleAftermath(marekId: Int): TutorialDialogue = TutorialDialogue(
id = "post_battle_aftermath",
id = postBattleAftermathDialogueId,
steps = Vector(
step(marekId, "post_battle_aftermath_dialogue_1"),
step(marekId, "post_battle_aftermath_dialogue_2")
@@ -3,7 +3,7 @@ package net.eagle0.eagle.library
import net.eagle0.eagle.library.actions.applier.ActionResultWithResultingState
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedHeroC}
import net.eagle0.eagle.model.action_result.types.ActionResultType
import net.eagle0.eagle.model.state.{BattalionTypeId, GameType, RoundPhase, TutorialPhase, TutorialState}
import net.eagle0.eagle.model.state.{BattalionTypeId, CapturedHero, GameType, RoundPhase, TutorialPhase, TutorialState}
import net.eagle0.eagle.model.state.battalion.concrete.BattalionC
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.faction.concrete.FactionC
@@ -144,10 +144,8 @@ class StrategicTutorialDialogueSelectorTest extends AnyFlatSpec with Matchers {
val gameState = state()
selectedIds(gameState, gameState, ActionResultType.GameStart) should contain("tutorial_opening")
selectedIds(gameState, gameState, ActionResultType.TutorialTarnDisappears) should contain allOf (
"post_battle_aftermath",
"post_battle_rebuild"
)
selectedIds(gameState, gameState, ActionResultType.TutorialTarnDisappears) should contain("post_battle_aftermath")
selectedIds(gameState, gameState, ActionResultType.TutorialTarnDisappears) should not contain "post_battle_rebuild"
selectedIds(gameState, gameState, ActionResultType.TutorialTarnDisappears) should not contain "post_battle_alms"
selectedIds(gameState, gameState, ActionResultType.TutorialFactionAppears) should contain(
"tutorial_eagle_appears"
@@ -183,6 +181,59 @@ class StrategicTutorialDialogueSelectorTest extends AnyFlatSpec with Matchers {
factionDialogue.steps(1).focus shouldBe Some(TutorialFocus.Button("InfoButton"))
}
it should "offer John's Improve guidance only in the month after Marek's captured hero guidance" in {
val mareksMonth = state(
currentRoundId = 4,
emittedTutorialDialogueRounds = Map(TutorialDialogueContent.postBattleAftermathDialogueId -> 4)
)
selectedIds(mareksMonth, mareksMonth, ActionResultType.HeroStatGained) should not contain "post_battle_rebuild"
val nextMonth = state(
currentRoundId = 5,
emittedTutorialDialogueRounds = Map(TutorialDialogueContent.postBattleAftermathDialogueId -> 4)
)
val dialogue = selectedDialogues(
mareksMonth,
nextMonth,
ActionResultType.NewRoundAction
).find(_.id == TutorialDialogueContent.postBattleRebuildDialogueId)
.getOrElse(fail("Improve guidance was not emitted"))
dialogue.steps(1).speakerHeroId shouldBe ranil.id
dialogue.steps(1).focus shouldBe Some(TutorialFocus.Button("ImproveProvinceButton"))
val stillHandlingCapturedHero = state(
currentRoundId = 5,
onmaa = ProvinceC(
id = OnmaaId,
rulingFactionId = Some(PlayerFactionId),
support = 20,
capturedHeroes = Vector(CapturedHero(heroId = 200, recruitmentAttempted = false, previousFactionId = 5))
),
emittedTutorialDialogueRounds = Map(TutorialDialogueContent.postBattleAftermathDialogueId -> 4)
)
selectedIds(mareksMonth, stillHandlingCapturedHero, ActionResultType.NewRoundAction) should not contain
"post_battle_rebuild"
selectedIds(stillHandlingCapturedHero, nextMonth, ActionResultType.CapturedHeroResolved) should contain(
"post_battle_rebuild"
)
val laterMonth = state(
currentRoundId = 6,
emittedTutorialDialogueRounds = Map(TutorialDialogueContent.postBattleAftermathDialogueId -> 4)
)
selectedIds(laterMonth, laterMonth, ActionResultType.HeroStatGained) should not contain "post_battle_rebuild"
val alreadyEmitted = state(
currentRoundId = 5,
emittedTutorialDialogueRounds = Map(
TutorialDialogueContent.postBattleAftermathDialogueId -> 4,
TutorialDialogueContent.postBattleRebuildDialogueId -> 5
)
)
selectedIds(alreadyEmitted, alreadyEmitted, ActionResultType.HeroStatGained) should not contain
"post_battle_rebuild"
}
it should "offer Elena's Alms guidance only in the month after John's Improve guidance" in {
val johnsMonth = state(
currentRoundId = 4,