mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 20:55:44 +00:00
Clarify developed border tutorial dialogue (#8677)
This commit is contained in:
+42
@@ -189,6 +189,48 @@ namespace eagle0.Tests {
|
||||
textProvider.Clear();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ServerDialogueConversionResolvesFactionReplacement() {
|
||||
var textProvider = ClientTextProvider.Provider;
|
||||
textProvider.Clear();
|
||||
textProvider.HandleNewStreamingText("marek-name", "Old Marek", 0, true);
|
||||
textProvider.HandleNewStreamingText(
|
||||
"border-copy",
|
||||
"We've earned support on the border with {factionName}.",
|
||||
0,
|
||||
true);
|
||||
|
||||
var dialogue = new Net.Eagle0.Eagle.Views.TutorialDialogue {
|
||||
Id = "tutorial_attack_prepare_organize"
|
||||
};
|
||||
var step = new Net.Eagle0.Eagle.Views.TutorialDialogueStep {
|
||||
SpeakerHeroId = 17,
|
||||
DialogueTextId = "border-copy"
|
||||
};
|
||||
step.TextReplacements.Add(new Net.Eagle0.Eagle.Views.TutorialTextReplacement {
|
||||
Token = "{factionName}",
|
||||
FactionId = 5
|
||||
});
|
||||
dialogue.Steps.Add(step);
|
||||
var heroes = new Dictionary<int, EagleHeroView> {
|
||||
[17] = new EagleHeroView { Id = 17, NameTextId = "marek-name" }
|
||||
};
|
||||
|
||||
var converted = DialogueManager.TryCreateServerScript(
|
||||
dialogue,
|
||||
heroes,
|
||||
new Dictionary<int, EagleProvinceView>(),
|
||||
textProvider,
|
||||
out var script,
|
||||
factionId => factionId == 5 ? "The Border League" : null);
|
||||
|
||||
Assert.IsTrue(converted);
|
||||
Assert.AreEqual(
|
||||
"We've earned support on the border with The Border League.",
|
||||
script.steps[0].dialogueText);
|
||||
textProvider.Clear();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TacticalServerDialogueWaitsForBattleUi() {
|
||||
var gameObject = new GameObject("DialogueManager test");
|
||||
|
||||
+11
-2
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using eagle;
|
||||
@@ -140,7 +141,8 @@ namespace Eagle0.Tutorial {
|
||||
model.Heroes,
|
||||
model.Provinces,
|
||||
ClientTextProvider.Provider,
|
||||
out var script)) {
|
||||
out var script,
|
||||
model.FactionName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,7 +163,8 @@ namespace Eagle0.Tutorial {
|
||||
IDictionary<int, Net.Eagle0.Eagle.Views.HeroView> heroes,
|
||||
IDictionary<int, EagleProvinceView> provinces,
|
||||
ClientTextProvider textProvider,
|
||||
out DialogueScript script) {
|
||||
out DialogueScript script,
|
||||
Func<int, string> factionName = null) {
|
||||
script = null;
|
||||
if (dialogue == null || string.IsNullOrEmpty(dialogue.Id) ||
|
||||
dialogue.Steps.Count == 0 || heroes == null || provinces == null ||
|
||||
@@ -207,6 +210,12 @@ namespace Eagle0.Tutorial {
|
||||
}
|
||||
replacementText = replacementProvince.Name;
|
||||
break;
|
||||
case Net.Eagle0.Eagle.Views.TutorialTextReplacement.ValueOneofCase
|
||||
.FactionId:
|
||||
if (factionName == null) return false;
|
||||
replacementText = factionName(replacement.FactionId);
|
||||
if (string.IsNullOrEmpty(replacementText)) return false;
|
||||
break;
|
||||
default: return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ message TutorialTextReplacement {
|
||||
oneof value {
|
||||
int32 hero_id = 2;
|
||||
int32 province_id = 3;
|
||||
int32 faction_id = 4;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,9 +104,9 @@ tutorial_taxes_collected_dialogue_3 One warning before we march, Sadar: a provin
|
||||
tutorial_taxes_collected_instruction_3 Leave at least one faction hero in Onmaa. Marching every hero out will <b>abandon the province immediately</b>.
|
||||
tutorial_taxes_collected_dialogue_4 A province ruled by an ordinary faction hero remains ours, but acts as a vassal. You can directly issue commands there only while your Warlord or a sworn sibling is present.
|
||||
tutorial_taxes_collected_instruction_2 March your Warlord and most — <b>not all</b> — of your heroes to a neighboring province. Before submitting, verify that at least one faction hero remains in Onmaa.
|
||||
tutorial_new_province_development_dialogue We've secured {provinceName}, Sadar. Before we raise an army here, use Improve and Alms to build local support.
|
||||
tutorial_new_province_development_dialogue We've occupied {provinceName}, Sadar. Before we raise an army here, use Improve and Alms to build local support.
|
||||
tutorial_new_province_development_instruction Raise {provinceName} to <b>40 support</b> with <b>Improve</b> and <b>Alms</b> before organizing an army.
|
||||
tutorial_attack_prepare_organize_dialogue We've reached the border of another faction, Sadar. Before we invite a fight, we should make certain our own lands can withstand one. Our existing forces won't be enough — hire new soldiers and form two or three full battalions so each of our heroes can lead a force worth taking into battle.
|
||||
tutorial_attack_prepare_organize_dialogue We've earned popular support on the border with {factionName}, Sadar. Before we invite a fight, we should make certain our own lands can withstand one. Our existing forces won't be enough — hire new soldiers and form two or three full battalions so each of our heroes can lead a force worth taking into battle.
|
||||
tutorial_attack_prepare_organize_instruction Use <b>Organize Troops</b> to hire soldiers and raise at least <b>two or three full battalions</b> from the troop types currently available.
|
||||
tutorial_attack_prepare_train_dialogue Good. The ranks are formed; now make certain they know how to fight together. Have a <b>Champion</b> lead their drills if possible — someone like you, Sadar! A full battalion is only as useful as its training.
|
||||
tutorial_attack_prepare_train_instruction Use <b>Train</b> to improve your battalions' training before attacking.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 47.
|
@@ -83,12 +83,12 @@ object StrategicTutorialDialogueSelector {
|
||||
),
|
||||
newlyArrivedUndevelopedProvinceId(startingState, result)
|
||||
.map(provinceId => TutorialDialogueContent.developNewProvince(marekId, provinceId)),
|
||||
Option.when(
|
||||
!hasEmitted(startingState, TutorialDialogueContent.attackPrepareOrganizeDialogueId) &&
|
||||
becameEligible(startingState, after)(hasDevelopedNonKingFactionBorder)
|
||||
)(
|
||||
TutorialDialogueContent.prepareAttackOrganize(marekId)
|
||||
),
|
||||
developedNonKingBorderFactionId(after)
|
||||
.filter(_ =>
|
||||
!hasEmitted(startingState, TutorialDialogueContent.attackPrepareOrganizeDialogueId) &&
|
||||
developedNonKingBorderFactionId(startingState).isEmpty
|
||||
)
|
||||
.map(factionId => TutorialDialogueContent.prepareAttackOrganize(marekId, factionId)),
|
||||
Option.when(
|
||||
hasEmitted(startingState, TutorialDialogueContent.attackPrepareOrganizeDialogueId) &&
|
||||
!hasEmitted(startingState, TutorialDialogueContent.attackPrepareTrainDialogueId) &&
|
||||
@@ -207,10 +207,15 @@ object StrategicTutorialDialogueSelector {
|
||||
}
|
||||
}
|
||||
|
||||
private def hasDevelopedNonKingFactionBorder(state: GameState): Boolean =
|
||||
private def developedNonKingBorderFactionId(state: GameState): Option[FactionId] =
|
||||
state.provinces.valuesIterator
|
||||
.filter(_.rulingFactionId.contains(PlayerFactionId))
|
||||
.exists(province => isDevelopedNonKingFactionBorderProvince(state, province.id))
|
||||
.filter(_.support >= DevelopedSupport)
|
||||
.toVector
|
||||
.sortBy(_.id)
|
||||
.flatMap(_.neighbors.sortBy(_.provinceId))
|
||||
.flatMap(neighbor => state.provinces.get(neighbor.provinceId).flatMap(_.rulingFactionId))
|
||||
.find(factionId => factionId != PlayerFactionId && factionId != KingFactionId)
|
||||
|
||||
private def isDevelopedNonKingFactionBorderProvince(state: GameState, provinceId: Int): Boolean =
|
||||
state.provinces.get(provinceId).exists { province =>
|
||||
@@ -295,6 +300,7 @@ object StrategicTutorialDialogueSelector {
|
||||
dialogue = dialogue,
|
||||
heroIds = state.heroes.keySet,
|
||||
provinceIds = state.provinces.keySet,
|
||||
factionIds = state.factions.keySet,
|
||||
availableTextIds = TutorialDialogueContent.textIds.toSet
|
||||
)
|
||||
}
|
||||
|
||||
+4
@@ -59,6 +59,10 @@ object ActionResultViewConverter {
|
||||
provinceId
|
||||
) =>
|
||||
TutorialTextReplacementProto.Value.ProvinceId(provinceId)
|
||||
case net.eagle0.eagle.model.view.action_result.TutorialTextReplacementValue.FactionName(
|
||||
factionId
|
||||
) =>
|
||||
TutorialTextReplacementProto.Value.FactionId(factionId)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ sealed trait TutorialTextReplacementValue
|
||||
object TutorialTextReplacementValue {
|
||||
case class HeroName(heroId: Int) extends TutorialTextReplacementValue
|
||||
case class ProvinceName(provinceId: Int) extends TutorialTextReplacementValue
|
||||
case class FactionName(factionId: Int) extends TutorialTextReplacementValue
|
||||
}
|
||||
|
||||
case class TutorialTextReplacement(token: String, value: TutorialTextReplacementValue)
|
||||
@@ -32,7 +33,8 @@ object TutorialDialogueValidator {
|
||||
heroIds: Set[Int],
|
||||
provinceIds: Set[Int],
|
||||
availableTextIds: Set[String],
|
||||
unitIds: Option[Set[Int]] = None
|
||||
unitIds: Option[Set[Int]] = None,
|
||||
factionIds: Set[Int] = Set.empty
|
||||
): TutorialDialogue = {
|
||||
def isBlank(value: String): Boolean = Option(value).forall(_.trim.isEmpty)
|
||||
def invalid(detail: String): Nothing =
|
||||
@@ -66,6 +68,8 @@ object TutorialDialogueValidator {
|
||||
invalid(s"$label replacement references missing hero $heroId")
|
||||
case TutorialTextReplacementValue.ProvinceName(provinceId) if !provinceIds.contains(provinceId) =>
|
||||
invalid(s"$label replacement references missing province $provinceId")
|
||||
case TutorialTextReplacementValue.FactionName(factionId) if !factionIds.contains(factionId) =>
|
||||
invalid(s"$label replacement references missing faction $factionId")
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -289,14 +289,17 @@ object TutorialDialogueContent {
|
||||
)
|
||||
)
|
||||
|
||||
def prepareAttackOrganize(marekId: Int): TutorialDialogue = TutorialDialogue(
|
||||
def prepareAttackOrganize(marekId: Int, factionId: Int): TutorialDialogue = TutorialDialogue(
|
||||
id = attackPrepareOrganizeDialogueId,
|
||||
steps = Vector(
|
||||
step(
|
||||
marekId,
|
||||
"tutorial_attack_prepare_organize_dialogue",
|
||||
instructionTextId = Some("tutorial_attack_prepare_organize_instruction"),
|
||||
focus = Some(TutorialFocus.Button("OrganizeTroopsButton"))
|
||||
focus = Some(TutorialFocus.Button("OrganizeTroopsButton")),
|
||||
textReplacements = Vector(
|
||||
TutorialTextReplacement("{factionName}", TutorialTextReplacementValue.FactionName(factionId))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1105,6 +1105,12 @@ final class ClientState(
|
||||
.map(_.name)
|
||||
.filter(_.nonEmpty)
|
||||
.map(replacement.token -> _)
|
||||
case TutorialTextReplacement.Value.FactionId(factionId) =>
|
||||
latestState
|
||||
.flatMap(_.factions.get(factionId))
|
||||
.map(_.name)
|
||||
.filter(_.nonEmpty)
|
||||
.map(replacement.token -> _)
|
||||
case TutorialTextReplacement.Value.Empty => None
|
||||
}
|
||||
}
|
||||
|
||||
+23
-8
@@ -30,21 +30,27 @@ class StrategicTutorialDialogueSelectorTest extends AnyFlatSpec with Matchers {
|
||||
private val PlayerFactionId = TutorialDialogueContent.playerFactionId
|
||||
private val OnmaaId = 14
|
||||
|
||||
private val marek = HeroC(
|
||||
private val marek = HeroC(
|
||||
id = 10,
|
||||
factionId = Some(PlayerFactionId),
|
||||
nameTextId = LoadedHero.nameTextId("Old Marek the Learned")
|
||||
)
|
||||
private val ranil = HeroC(
|
||||
private val ranil = HeroC(
|
||||
id = 100,
|
||||
factionId = Some(PlayerFactionId),
|
||||
nameTextId = LoadedHero.nameTextId("John Ranil")
|
||||
)
|
||||
private val elena = HeroC(
|
||||
private val elena = HeroC(
|
||||
id = 101,
|
||||
factionId = Some(PlayerFactionId),
|
||||
nameTextId = LoadedHero.nameTextId("Elena Fyar")
|
||||
)
|
||||
private val borderFaction = FactionC(
|
||||
id = 5,
|
||||
factionHeadId = 200,
|
||||
name = "The Border League",
|
||||
leaderIds = Vector(200)
|
||||
)
|
||||
|
||||
private def state(
|
||||
currentRoundId: Int = 1,
|
||||
@@ -374,11 +380,18 @@ class StrategicTutorialDialogueSelectorTest extends AnyFlatSpec with Matchers {
|
||||
)
|
||||
val border = state(
|
||||
onmaa = borderProvince,
|
||||
additionalProvinces = Map(39 -> ProvinceC(id = 39, rulingFactionId = Some(5)))
|
||||
additionalProvinces = Map(39 -> ProvinceC(id = 39, rulingFactionId = Some(borderFaction.id))),
|
||||
factions = Map(borderFaction.id -> borderFaction)
|
||||
)
|
||||
|
||||
selectedIds(before, border, ActionResultType.ProvinceConquered) should contain(
|
||||
"tutorial_attack_prepare_organize"
|
||||
val organizeDialogue = selectedDialogues(before, border, ActionResultType.ProvinceConquered)
|
||||
.find(_.id == TutorialDialogueContent.attackPrepareOrganizeDialogueId)
|
||||
.getOrElse(fail("Organize guidance was not emitted"))
|
||||
organizeDialogue.steps.loneElement.textReplacements shouldBe Vector(
|
||||
TutorialTextReplacement(
|
||||
"{factionName}",
|
||||
TutorialTextReplacementValue.FactionName(borderFaction.id)
|
||||
)
|
||||
)
|
||||
selectedIds(
|
||||
border,
|
||||
@@ -459,11 +472,13 @@ class StrategicTutorialDialogueSelectorTest extends AnyFlatSpec with Matchers {
|
||||
)
|
||||
val undeveloped = state(
|
||||
onmaa = undevelopedProvince,
|
||||
additionalProvinces = Map(39 -> ProvinceC(id = 39, rulingFactionId = Some(5)))
|
||||
additionalProvinces = Map(39 -> ProvinceC(id = 39, rulingFactionId = Some(borderFaction.id))),
|
||||
factions = Map(borderFaction.id -> borderFaction)
|
||||
)
|
||||
val developed = state(
|
||||
onmaa = undevelopedProvince.copy(support = 40),
|
||||
additionalProvinces = Map(39 -> ProvinceC(id = 39, rulingFactionId = Some(5)))
|
||||
additionalProvinces = Map(39 -> ProvinceC(id = 39, rulingFactionId = Some(borderFaction.id))),
|
||||
factions = Map(borderFaction.id -> borderFaction)
|
||||
)
|
||||
val almostTrained = undeveloped.copy(
|
||||
battalions = Map(
|
||||
|
||||
@@ -29,9 +29,10 @@ class TutorialDialogueValidatorTest extends AnyFlatSpec with Matchers {
|
||||
heroIds: Set[Int] = Set(17),
|
||||
provinceIds: Set[Int] = Set(14),
|
||||
textIds: Set[String] = Set("dialogue-copy", "instruction-copy"),
|
||||
unitIds: Option[Set[Int]] = None
|
||||
unitIds: Option[Set[Int]] = None,
|
||||
factionIds: Set[Int] = Set.empty
|
||||
): TutorialDialogue =
|
||||
TutorialDialogueValidator.validate(dialogue, heroIds, provinceIds, textIds, unitIds)
|
||||
TutorialDialogueValidator.validate(dialogue, heroIds, provinceIds, textIds, unitIds, factionIds)
|
||||
|
||||
"TutorialDialogueValidator" should "accept resolvable dialogue" in {
|
||||
validate() shouldBe validDialogue
|
||||
@@ -96,4 +97,18 @@ class TutorialDialogueValidatorTest extends AnyFlatSpec with Matchers {
|
||||
error.getMessage should include("replacement references missing province 29")
|
||||
}
|
||||
|
||||
it should "reject a replacement that references a missing faction" in {
|
||||
val dialogue = validDialogue.copy(
|
||||
steps = validDialogue.steps.map(
|
||||
_.copy(
|
||||
textReplacements = Vector(
|
||||
TutorialTextReplacement("{factionName}", TutorialTextReplacementValue.FactionName(29))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val error = the[IllegalStateException] thrownBy validate(dialogue = dialogue)
|
||||
error.getMessage should include("replacement references missing faction 29")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
-5
@@ -210,19 +210,22 @@ class TutorialGameCreationTest extends AnyFlatSpec with Matchers {
|
||||
}
|
||||
|
||||
it should "provide valid server-owned content for every strategic tutorial dialogue" in {
|
||||
val gameState = engineAndResults.engine.currentState
|
||||
val marekId = gameState.heroes.values
|
||||
val gameState = engineAndResults.engine.currentState
|
||||
val marekId = gameState.heroes.values
|
||||
.find(_.nameTextId == LoadedHero.nameTextId("Old Marek the Learned"))
|
||||
.map(_.id)
|
||||
.getOrElse(fail("Old Marek is missing from the tutorial state"))
|
||||
val ranilId = gameState.heroes.values
|
||||
val ranilId = gameState.heroes.values
|
||||
.find(_.nameTextId == LoadedHero.nameTextId("John Ranil"))
|
||||
.map(_.id)
|
||||
.getOrElse(fail("John Ranil is missing from the tutorial state"))
|
||||
val elenaId = gameState.heroes.values
|
||||
val elenaId = gameState.heroes.values
|
||||
.find(_.nameTextId == LoadedHero.nameTextId("Elena Fyar"))
|
||||
.map(_.id)
|
||||
.getOrElse(fail("Elena Fyar is missing from the tutorial state"))
|
||||
val otherFactionId = gameState.factions.keys
|
||||
.find(factionId => factionId != TutorialDialogueContent.playerFactionId && factionId != 2)
|
||||
.getOrElse(fail("A non-player, non-King faction is missing from the tutorial state"))
|
||||
|
||||
val dialogues = Vector(
|
||||
TutorialDialogueContent.opening(marekId),
|
||||
@@ -241,7 +244,7 @@ class TutorialGameCreationTest extends AnyFlatSpec with Matchers {
|
||||
TutorialDialogueContent.readyToJoin(marekId, provinceId = 14),
|
||||
TutorialDialogueContent.inTown(marekId),
|
||||
TutorialDialogueContent.taxesCollected(marekId),
|
||||
TutorialDialogueContent.prepareAttackOrganize(marekId),
|
||||
TutorialDialogueContent.prepareAttackOrganize(marekId, otherFactionId),
|
||||
TutorialDialogueContent.prepareAttackTrain(marekId),
|
||||
TutorialDialogueContent.prepareAttackArm(marekId),
|
||||
TutorialDialogueContent.attackReady(marekId),
|
||||
@@ -280,6 +283,7 @@ class TutorialGameCreationTest extends AnyFlatSpec with Matchers {
|
||||
dialogue = dialogue,
|
||||
heroIds = gameState.heroes.keySet,
|
||||
provinceIds = gameState.provinces.keySet,
|
||||
factionIds = gameState.factions.keySet,
|
||||
availableTextIds = TutorialDialogueContent.textIds.toSet
|
||||
) shouldBe dialogue
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ import net.eagle0.eagle.common.diplomacy_offer_status.DiplomacyOfferStatus
|
||||
import net.eagle0.eagle.common.province_order_type.ProvinceOrderType
|
||||
import net.eagle0.eagle.common.tribute_amount.TributeAmount
|
||||
import net.eagle0.eagle.views.action_result_view.ActionResultView
|
||||
import net.eagle0.eagle.views.faction_view.FactionView
|
||||
import net.eagle0.eagle.views.game_state_view.{GameStateView, GameStateViewDiff}
|
||||
import net.eagle0.eagle.views.hero_view.HeroView
|
||||
import net.eagle0.eagle.views.province_view.{ProvinceView, ProvinceViewDiff}
|
||||
@@ -641,6 +642,67 @@ class ClientStateTest extends AnyFlatSpec with Matchers {
|
||||
output should include("Old Marek the Learned: A hero in Onmaa is ready to join.")
|
||||
}
|
||||
|
||||
it should "resolve tutorial faction replacements from the current game state" in {
|
||||
val state = new ClientState
|
||||
state.handle(
|
||||
UpdateStreamResponse(
|
||||
UpdateStreamResponse.ResponseDetails.GameUpdate(
|
||||
GameUpdate(
|
||||
gameId = 1L,
|
||||
startingState = Some(
|
||||
GameStateView(
|
||||
gameId = 1L,
|
||||
heroes = Map(17 -> HeroView(id = 17, nameTextId = "marek-name")),
|
||||
factions = Map(5 -> FactionView(id = 5, name = "The Border League"))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
state.handle(completedTextUpdate("marek-name", "Old Marek the Learned"))
|
||||
state.handle(completedTextUpdate("border-copy", "We've earned support on the border with {factionName}."))
|
||||
|
||||
val output = capturedOutput(
|
||||
state.handle(
|
||||
UpdateStreamResponse(
|
||||
UpdateStreamResponse.ResponseDetails.GameUpdate(
|
||||
GameUpdate(
|
||||
gameId = 1L,
|
||||
gameUpdateDetails = GameUpdate.GameUpdateDetails.ActionResultResponse(
|
||||
ActionResultResponse(
|
||||
actionResultViews = Vector(
|
||||
ActionResultView(
|
||||
tutorialDialogues = Vector(
|
||||
TutorialDialogue(
|
||||
id = "tutorial_attack_prepare_organize",
|
||||
steps = Vector(
|
||||
TutorialDialogueStep(
|
||||
speakerHeroId = 17,
|
||||
dialogueTextId = "border-copy",
|
||||
textReplacements = Vector(
|
||||
TutorialTextReplacement(
|
||||
token = "{factionName}",
|
||||
value = TutorialTextReplacement.Value.FactionId(5)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
output should include("Old Marek the Learned: We've earned support on the border with The Border League.")
|
||||
}
|
||||
|
||||
it should "apply Eagle game state diffs from action result updates" in {
|
||||
val state = new ClientState
|
||||
state.handle(
|
||||
|
||||
Reference in New Issue
Block a user