demo
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5024fb2de5966154ca9fd4d6d17c27dc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "ReplacedPerson.EditMode.Tests",
|
||||
"rootNamespace": "ReplacedPerson.Tests",
|
||||
"references": ["ReplacedPerson.Core", "ReplacedPerson.Runtime", "ExampleGreedMod.Rules", "UnityEngine.TestRunner", "UnityEditor.TestRunner"],
|
||||
"includePlatforms": ["Editor"],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"autoReferenced": false,
|
||||
"defineConstraints": ["UNITY_INCLUDE_TESTS"],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 486ca7ff6bf7f444395635a675a3f4c0
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,362 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ExampleGreedMod;
|
||||
using NUnit.Framework;
|
||||
using ReplacedPerson.Core;
|
||||
|
||||
namespace ReplacedPerson.Tests
|
||||
{
|
||||
public sealed class ReplacedPersonCoreTests
|
||||
{
|
||||
[Test]
|
||||
public void Pcg32_IsDeterministic()
|
||||
{
|
||||
var a = new Pcg32(12345);
|
||||
var b = new Pcg32(12345);
|
||||
CollectionAssert.AreEqual(Enumerable.Range(0, 32).Select(_ => a.NextUInt()).ToArray(),
|
||||
Enumerable.Range(0, 32).Select(_ => b.NextUInt()).ToArray());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuiltInDeck_IsValid()
|
||||
{
|
||||
var content = ReplacedBuiltInContent.Create();
|
||||
var starter = ReplacedBuiltInContent.CreateStarterDeck();
|
||||
Assert.That(ReplacedDeckValidator.Validate(starter, content), Is.Empty);
|
||||
Assert.That(starter.NormalCards.Count(id => content.Cards[id].EffectType == ReplacedEffectType.Counter), Is.EqualTo(6));
|
||||
Assert.That(starter.NormalCards.Count(id => content.Cards[id].EffectType == ReplacedEffectType.Dodge), Is.EqualTo(2));
|
||||
Assert.That(starter.NormalCards.Count(id => content.Cards[id].EffectType == ReplacedEffectType.Attack), Is.EqualTo(4));
|
||||
Assert.That(content.Enemies.ContainsKey("greed.tutorial"));
|
||||
Assert.That(content.Enemies.ContainsKey("greed.full"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Match_StartsWithFourNormalAndOneEnd()
|
||||
{
|
||||
var engine = BuiltInEngine(1);
|
||||
Assert.That(engine.State.Round, Is.EqualTo(1));
|
||||
Assert.That(engine.State.Players[0].NormalHand, Has.Count.EqualTo(4));
|
||||
Assert.That(engine.State.Players[0].EndHand, Has.Count.EqualTo(1));
|
||||
Assert.That(engine.State.Phase, Is.EqualTo(ReplacedMatchPhase.AwaitingLead));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ResolvedSlots_RemainVisibleDuringTheNextRound()
|
||||
{
|
||||
var setup = FixedEngine(ReplacedEffectType.Attack, 4, ReplacedEffectType.Counter, 3, 31);
|
||||
ResolveOneCardRound(setup.engine, setup.cardOne, setup.cardTwo);
|
||||
Assert.That(setup.engine.State.Round, Is.EqualTo(2));
|
||||
Assert.That(setup.engine.State.LastResolution, Has.Count.EqualTo(1));
|
||||
Assert.That(setup.engine.State.LastResolution[0].Summary, Is.Not.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Command_RejectsWrongPlayerAndDeduplicatesAcceptedToken()
|
||||
{
|
||||
var engine = BuiltInEngine(2);
|
||||
var wrong = 1 - engine.State.LeadPlayer;
|
||||
var rejected = engine.Apply(Command(engine, wrong, Pass(), 1, "wrong"));
|
||||
Assert.That(rejected.ErrorCode, Is.EqualTo("turn.not-yours"));
|
||||
var lead = engine.State.LeadPlayer;
|
||||
var accepted = Command(engine, lead, Pass(), 1, "same");
|
||||
Assert.That(engine.Apply(accepted).Accepted, Is.True);
|
||||
var duplicate = engine.Apply(accepted);
|
||||
Assert.That(duplicate.Accepted, Is.True);
|
||||
Assert.That(duplicate.Duplicate, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AttackAgainstDodge_DealsDifferenceCappedAtEight()
|
||||
{
|
||||
var setup = FixedEngine(ReplacedEffectType.Attack, 20, ReplacedEffectType.Dodge, 1, 3);
|
||||
ResolveOneCardRound(setup.engine, setup.cardOne, setup.cardTwo);
|
||||
Assert.That(setup.engine.State.Players[1].Health, Is.EqualTo(22));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CounterAgainstAttack_CancelsAndReturnsDamageCappedAtSix()
|
||||
{
|
||||
var setup = FixedEngine(ReplacedEffectType.Counter, 20, ReplacedEffectType.Attack, 1, 4);
|
||||
ResolveOneCardRound(setup.engine, setup.cardOne, setup.cardTwo);
|
||||
Assert.That(setup.engine.State.Players[1].Health, Is.EqualTo(24));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DodgeAgainstAttack_GrantsExtraDraw()
|
||||
{
|
||||
var setup = FixedEngine(ReplacedEffectType.Dodge, 20, ReplacedEffectType.Attack, 1, 5);
|
||||
ResolveOneCardRound(setup.engine, setup.cardOne, setup.cardTwo);
|
||||
Assert.That(setup.engine.State.Players[0].NormalHand.Count, Is.EqualTo(6));
|
||||
Assert.That(setup.engine.State.Players[0].Health, Is.EqualTo(30));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DoubleAttackFusion_UsesOnePointFiveMultiplier()
|
||||
{
|
||||
var setup = FixedEngine(ReplacedEffectType.Attack, 2, ReplacedEffectType.Dodge, 1, 6);
|
||||
var state = setup.engine.State;
|
||||
state.Players[0].NormalHand.Clear();
|
||||
state.Players[0].NormalHand.AddRange(new[] { setup.cardOne, setup.cardOne, setup.cardOne, setup.cardOne });
|
||||
var first = new ReplacedTurnSubmission
|
||||
{
|
||||
NormalCardIds = new List<string> { setup.cardOne, setup.cardOne, setup.cardOne, setup.cardOne },
|
||||
EndCardIds = new List<string> { state.Players[0].EndHand[0] }
|
||||
};
|
||||
ResolveRound(setup.engine, first, Pass());
|
||||
Assert.That(setup.engine.State.Players[1].Health, Is.EqualTo(20));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Pass_SuppressesNextEndDrawAndPaysNoCost()
|
||||
{
|
||||
var engine = BuiltInEngine(7);
|
||||
var endsBefore = engine.State.Players[0].EndHand.Count;
|
||||
ResolveRound(engine, Pass(), Pass());
|
||||
Assert.That(engine.State.Players[0].Cost, Is.EqualTo(10));
|
||||
Assert.That(engine.State.Players[0].EndHand.Count, Is.EqualTo(endsBefore));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Cost_IsPaidAtLockAndRecoveredAtRoundEnd()
|
||||
{
|
||||
var engine = BuiltInEngine(8);
|
||||
var firstPlayer = engine.State.LeadPlayer;
|
||||
var player = engine.State.Players[firstPlayer];
|
||||
var affordable = player.NormalHand.First(id => engine.Content.Cards[id].Cost <= player.Cost);
|
||||
var ending = player.EndHand[0];
|
||||
var spent = engine.Content.Cards[affordable].Cost + engine.Content.EndCards[ending].Cost;
|
||||
var result = engine.Apply(Command(engine, firstPlayer, Submit(affordable, ending), 1, "lock"));
|
||||
Assert.That(result.Accepted);
|
||||
Assert.That(player.Cost, Is.EqualTo(10 - spent));
|
||||
var other = 1 - firstPlayer;
|
||||
engine.Apply(Command(engine, other, Pass(), 1, "other"));
|
||||
Assert.That(player.Cost, Is.EqualTo(Math.Min(10, 10 - spent + 1 + spent / 2)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SimultaneousLethal_EntersLastStand()
|
||||
{
|
||||
var setup = FixedEngine(ReplacedEffectType.Attack, 3, ReplacedEffectType.Attack, 1, 9);
|
||||
var engine = setup.engine;
|
||||
var strong = setup.cardOne;
|
||||
var weak = setup.cardTwo;
|
||||
engine.State.Players[0].Health = 1;
|
||||
engine.State.Players[1].Health = 1;
|
||||
engine.State.Players[0].NormalHand.Clear();
|
||||
engine.State.Players[1].NormalHand.Clear();
|
||||
engine.State.Players[0].NormalHand.AddRange(new[] { strong, weak });
|
||||
engine.State.Players[1].NormalHand.AddRange(new[] { weak, strong });
|
||||
var end0 = engine.State.Players[0].EndHand[0];
|
||||
var end1 = engine.State.Players[1].EndHand[0];
|
||||
ResolveRound(engine,
|
||||
new ReplacedTurnSubmission { NormalCardIds = new List<string> { strong, weak }, EndCardIds = new List<string> { end0 } },
|
||||
new ReplacedTurnSubmission { NormalCardIds = new List<string> { weak, strong }, EndCardIds = new List<string> { end1 } });
|
||||
Assert.That(engine.State.IsLastStand, Is.True);
|
||||
Assert.That(engine.State.LastStandUsed, Is.True);
|
||||
Assert.That(engine.State.Players[0].NormalHand, Has.Count.EqualTo(4));
|
||||
Assert.That(engine.State.Players[0].EndHand, Has.Count.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Replay_ProducesIdenticalStateHash()
|
||||
{
|
||||
var content = ReplacedBuiltInContent.Create();
|
||||
var deck = ReplacedBuiltInContent.CreateStarterDeck();
|
||||
var original = new ReplacedMatchEngine(content, deck, deck, 77);
|
||||
ResolveRound(original, Pass(), Pass());
|
||||
ResolveRound(original, Pass(), Pass());
|
||||
var replay = ReplacedReplayRunner.Replay(content, deck, deck, 77, original.Replay);
|
||||
Assert.That(replay.ComputeStateHash(), Is.EqualTo(original.ComputeStateHash()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StateHash_IncludesVisibleSlotResolution()
|
||||
{
|
||||
var engine = BuiltInEngine(91);
|
||||
var before = engine.ComputeStateHash();
|
||||
engine.State.LastResolution.Add(new ReplacedSlotResolution
|
||||
{
|
||||
SlotIndex = 0,
|
||||
PlayerOneType = ReplacedEffectType.Counter,
|
||||
PlayerTwoType = ReplacedEffectType.Attack,
|
||||
PlayerOneRoll = 5,
|
||||
PlayerTwoRoll = 2,
|
||||
PlayerOneValue = 17,
|
||||
PlayerTwoValue = 8,
|
||||
DamageToPlayerTwo = 6,
|
||||
Summary = "hash-visible-resolution"
|
||||
});
|
||||
Assert.That(engine.ComputeStateHash(), Is.Not.EqualTo(before));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Ai_ProducesLegalSubmissionWithoutMutatingVisibleState()
|
||||
{
|
||||
var engine = BuiltInEngine(12);
|
||||
var aiPlayer = engine.State.LeadPlayer;
|
||||
var before = engine.ComputeStateHash();
|
||||
var submission = new ReplacedAi(engine.Content).Choose(engine.State.Clone(), aiPlayer);
|
||||
Assert.That(engine.ComputeStateHash(), Is.EqualTo(before));
|
||||
Assert.That(engine.Apply(Command(engine, aiPlayer, submission, 1, "ai")).Accepted, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SaveMigration_AddsFirstOrnamentForLegacyProgress()
|
||||
{
|
||||
var save = new ReplacedPersonSaveData { Version = 1, ChapterProgress = 1, Ornaments = new List<string>() };
|
||||
ReplacedPersonSaveMigration.Migrate(save);
|
||||
Assert.That(save.Version, Is.EqualTo(3));
|
||||
Assert.That(save.Ornaments, Contains.Item("ornament.gold-eye"));
|
||||
Assert.That(save.ActiveDeck.NormalCards.Count(id => id == "counter.stance"), Is.EqualTo(2));
|
||||
Assert.That(save.CollectedCards.Count(id => id == "counter.stance"), Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ModHandshake_ReportsVersionAndFileHashDifferences()
|
||||
{
|
||||
var expected = Manifest("a", "1", "x.dll", "aa");
|
||||
var actual = Manifest("a", "2", "x.dll", "bb");
|
||||
var result = ReplacedModCompatibility.Compare(new[] { expected }, new[] { actual });
|
||||
Assert.That(result.Compatible, Is.False);
|
||||
Assert.That(result.Differences.Any(value => value.StartsWith("version:", StringComparison.Ordinal)));
|
||||
Assert.That(result.Differences.Any(value => value.StartsWith("hash:", StringComparison.Ordinal)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HotReload_DoesNotChangeCurrentMatchSnapshot()
|
||||
{
|
||||
var catalog = ReplacedBuiltInContent.Create();
|
||||
var hotReload = new ReplacedHotReloadCatalog(catalog);
|
||||
var registry = new ReplacedPersonRegistry(catalog);
|
||||
new ExampleGreedRules().Register(registry);
|
||||
var next = registry.CreateSnapshot();
|
||||
Assert.That(next.ComputeHash(), Is.Not.EqualTo(catalog.ComputeHash()));
|
||||
hotReload.Stage(next);
|
||||
Assert.That(hotReload.CurrentHash, Is.EqualTo(catalog.ComputeHash()));
|
||||
var promoted = hotReload.SnapshotForNewMatch();
|
||||
Assert.That(hotReload.Pending, Is.Null);
|
||||
Assert.That(promoted.Cards, Contains.Key("example.audit"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PlayerSnapshot_HidesOpponentCardsButPreservesCounts()
|
||||
{
|
||||
var room = StartedRoom();
|
||||
var expectedNormalCount = room.Match!.State.Players[1].NormalHand.Count;
|
||||
var snapshot = room.PlayerMatchSnapshot("a")!;
|
||||
Assert.That(snapshot.Players[1].NormalHand, Has.Count.EqualTo(expectedNormalCount));
|
||||
Assert.That(snapshot.Players[1].NormalHand.All(string.IsNullOrEmpty));
|
||||
Assert.That(snapshot.Players[0].NormalHand.All(value => !string.IsNullOrEmpty(value)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RoomAuthority_StartsOnlyAfterTwoValidReadyDecks()
|
||||
{
|
||||
var content = ReplacedBuiltInContent.Create();
|
||||
var room = new ReplacedRoomAuthority("test", content, Array.Empty<ReplacedPersonModManifest>(), 20);
|
||||
Assert.That(room.TryJoin(Join("a"), DateTime.UtcNow).Compatible);
|
||||
Assert.That(room.TryJoin(Join("b"), DateTime.UtcNow).Compatible);
|
||||
Assert.That(room.SetDeckAndReady("a", ReplacedBuiltInContent.CreateStarterDeck()), Is.Empty);
|
||||
Assert.That(room.Match, Is.Null);
|
||||
Assert.That(room.SetDeckAndReady("b", ReplacedBuiltInContent.CreateStarterDeck()), Is.Empty);
|
||||
Assert.That(room.Match, Is.Not.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TwoConsecutiveTimeouts_ForfeitMatch()
|
||||
{
|
||||
var content = ReplacedBuiltInContent.Create();
|
||||
var room = new ReplacedRoomAuthority("test", content, Array.Empty<ReplacedPersonModManifest>(), 21);
|
||||
room.TryJoin(Join("a"), DateTime.UtcNow); room.TryJoin(Join("b"), DateTime.UtcNow);
|
||||
room.SetDeckAndReady("a", ReplacedBuiltInContent.CreateStarterDeck()); room.SetDeckAndReady("b", ReplacedBuiltInContent.CreateStarterDeck());
|
||||
room.RecordTurnTimeout("a"); room.RecordTurnTimeout("a");
|
||||
Assert.That(room.Match!.State.Winner, Is.EqualTo(ReplacedWinner.PlayerTwo));
|
||||
}
|
||||
|
||||
private static ReplacedMatchEngine BuiltInEngine(ulong seed)
|
||||
{
|
||||
var deck = ReplacedBuiltInContent.CreateStarterDeck();
|
||||
return new ReplacedMatchEngine(ReplacedBuiltInContent.Create(), deck, deck, seed);
|
||||
}
|
||||
|
||||
private static ReplacedRoomAuthority StartedRoom()
|
||||
{
|
||||
var room = new ReplacedRoomAuthority("test", ReplacedBuiltInContent.Create(),
|
||||
Array.Empty<ReplacedPersonModManifest>(), 22);
|
||||
room.TryJoin(Join("a"), DateTime.UtcNow); room.TryJoin(Join("b"), DateTime.UtcNow);
|
||||
room.SetDeckAndReady("a", ReplacedBuiltInContent.CreateStarterDeck());
|
||||
room.SetDeckAndReady("b", ReplacedBuiltInContent.CreateStarterDeck());
|
||||
return room;
|
||||
}
|
||||
|
||||
private static (ReplacedMatchEngine engine, string cardOne, string cardTwo) FixedEngine(
|
||||
ReplacedEffectType firstType, int firstValue, ReplacedEffectType secondType, int secondValue, ulong seed)
|
||||
{
|
||||
var content = new ReplacedContentCatalog();
|
||||
var firstIds = AddSixCards(content, "one", firstType, firstValue);
|
||||
var secondIds = AddSixCards(content, "two", secondType, secondValue);
|
||||
var ends = new List<string>();
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
var id = "end." + i;
|
||||
content.EndCards.Add(id, new ReplacedEndCardDefinition { Id = id, Name = id, SlotBase = new int[3], SlotFactor = new int[3] });
|
||||
ends.Add(id);
|
||||
}
|
||||
var firstDeck = new ReplacedDeck { NormalCards = firstIds.SelectMany(id => new[] { id, id }).ToList(), EndCards = new List<string>(ends) };
|
||||
var secondDeck = new ReplacedDeck { NormalCards = secondIds.SelectMany(id => new[] { id, id }).ToList(), EndCards = new List<string>(ends) };
|
||||
var engine = new ReplacedMatchEngine(content, firstDeck, secondDeck, seed);
|
||||
return (engine, engine.State.Players[0].NormalHand[0], engine.State.Players[1].NormalHand[0]);
|
||||
}
|
||||
|
||||
private static List<string> AddSixCards(ReplacedContentCatalog content, string prefix, ReplacedEffectType type, int value)
|
||||
{
|
||||
var result = new List<string>();
|
||||
for (var i = 0; i < 6; i++)
|
||||
{
|
||||
var id = prefix + "." + i;
|
||||
content.Cards.Add(id, new ReplacedCardDefinition { Id = id, Name = id, EffectType = type, BaseValue = value, Factor = 0, Cost = 0 });
|
||||
result.Add(id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ResolveOneCardRound(ReplacedMatchEngine engine, string first, string second)
|
||||
{
|
||||
ResolveRound(engine, Submit(first, engine.State.Players[0].EndHand[0]), Submit(second, engine.State.Players[1].EndHand[0]));
|
||||
}
|
||||
|
||||
private static void ResolveRound(ReplacedMatchEngine engine, ReplacedTurnSubmission first, ReplacedTurnSubmission second)
|
||||
{
|
||||
var byPlayer = new[] { first, second };
|
||||
var lead = engine.State.LeadPlayer;
|
||||
Assert.That(engine.Apply(Command(engine, lead, byPlayer[lead], engine.State.LastAcceptedSequence[lead] + 1, Guid.NewGuid().ToString("N"))).Accepted);
|
||||
var response = 1 - lead;
|
||||
Assert.That(engine.Apply(Command(engine, response, byPlayer[response], engine.State.LastAcceptedSequence[response] + 1, Guid.NewGuid().ToString("N"))).Accepted);
|
||||
}
|
||||
|
||||
private static ReplacedMatchCommand Command(ReplacedMatchEngine engine, int player, ReplacedTurnSubmission submission, long sequence, string token) => new()
|
||||
{
|
||||
PlayerIndex = player, Submission = submission, Sequence = sequence, IdempotencyToken = token
|
||||
};
|
||||
|
||||
private static ReplacedTurnSubmission Submit(string card, string ending) => new()
|
||||
{
|
||||
NormalCardIds = new List<string> { card }, EndCardIds = new List<string> { ending }
|
||||
};
|
||||
|
||||
private static ReplacedTurnSubmission Pass() => new() { Pass = true };
|
||||
|
||||
private static ReplacedPersonModManifest Manifest(string id, string version, string path, string hash) => new()
|
||||
{
|
||||
Id = id, Version = version, Files = new List<ReplacedPersonModFile> { new() { Path = path, Sha256 = hash } }
|
||||
};
|
||||
|
||||
private static ReplacedJoinRequest Join(string id) => new()
|
||||
{
|
||||
RoomId = "test", PlayerId = id, DisplayName = id, Mods = new List<ReplacedPersonModManifest>()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66da5bf1a98e7aa42bb87d6685a4d862
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9797e4d566011044bb9e083d1c531488
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "ReplacedPerson.PlayMode.Tests",
|
||||
"rootNamespace": "ReplacedPerson.Tests",
|
||||
"references": ["ReplacedPerson.Core", "ReplacedPerson.Runtime", "UnityEngine.UI", "Unity.TextMeshPro"],
|
||||
"optionalUnityReferences": ["TestAssemblies"],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"autoReferenced": false,
|
||||
"defineConstraints": ["UNITY_INCLUDE_TESTS"],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22325762d0cf69e43bffc9022b9907df
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,267 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using ReplacedPerson.Runtime;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.TestTools;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace ReplacedPerson.Tests
|
||||
{
|
||||
public sealed class ReplacedPersonSceneTests
|
||||
{
|
||||
[UnityTest]
|
||||
public IEnumerator EntryScene_HasResponsiveCanvasAndAllScreens()
|
||||
{
|
||||
SceneManager.LoadScene("ReplacedPerson");
|
||||
yield return null;
|
||||
var root = GameObject.Find("ReplacedPersonDemo");
|
||||
Assert.That(root, Is.Not.Null);
|
||||
var camera = Camera.main;
|
||||
Assert.That(camera, Is.Not.Null);
|
||||
Assert.That(camera!.enabled, Is.True);
|
||||
Assert.That(camera.cullingMask, Is.Zero);
|
||||
var canvas = root.transform.Find("Canvas")!.GetComponent<Canvas>();
|
||||
var scaler = canvas.GetComponent<CanvasScaler>();
|
||||
Assert.That(canvas.renderMode, Is.EqualTo(RenderMode.ScreenSpaceOverlay));
|
||||
Assert.That(scaler.referenceResolution, Is.EqualTo(new Vector2(1920, 1080)));
|
||||
Assert.That(root.transform.Find("Canvas/SafeAreaRoot")!.GetComponent<ReplacedSafeArea>(), Is.Not.Null);
|
||||
Assert.That(root.GetComponentsInChildren<TMP_Text>(true), Has.Length.GreaterThan(20));
|
||||
Assert.That(root.GetComponentsInChildren<Button>(true), Has.Length.GreaterThan(12));
|
||||
Assert.That(root.transform.Find("Canvas/SafeAreaRoot/Screens/BattleScreen/LogPanel/CardDetailText"), Is.Not.Null);
|
||||
foreach (var audioName in new[] { "ui_click", "card_draw", "card_select", "lock", "dice_roll", "hit", "victory", "defeat", "error" })
|
||||
Assert.That(Resources.Load<AudioClip>("ReplacedPersonAudio/" + audioName), Is.Not.Null, audioName);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator GameService_CompletesARealRoundWithAi()
|
||||
{
|
||||
var service = new ReplacedPersonGameService();
|
||||
var match = service.StartAi("greed.tutorial", 123);
|
||||
while ((match.State.Phase == Core.ReplacedMatchPhase.AwaitingLead ? match.State.LeadPlayer : 1 - match.State.LeadPlayer) != 0)
|
||||
yield return null;
|
||||
var player = match.State.Players[0];
|
||||
var submission = new Core.ReplacedTurnSubmission
|
||||
{
|
||||
NormalCardIds = new System.Collections.Generic.List<string> { player.NormalHand[0] },
|
||||
EndCardIds = new System.Collections.Generic.List<string> { player.EndHand[0] },
|
||||
RerollSlot = 0
|
||||
};
|
||||
Assert.That(service.SubmitPlayer(submission).Accepted, Is.True);
|
||||
Assert.That(match.State.Round, Is.GreaterThanOrEqualTo(2));
|
||||
service.Dispose();
|
||||
yield return null;
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator DraggingCard_RestoresItsHandOrderAndLayoutPosition()
|
||||
{
|
||||
var canvasObject = new GameObject("DragTestCanvas", typeof(RectTransform), typeof(Canvas));
|
||||
canvasObject.GetComponent<Canvas>().renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
var hand = new GameObject("Hand", typeof(RectTransform), typeof(HorizontalLayoutGroup));
|
||||
hand.transform.SetParent(canvasObject.transform, false);
|
||||
var handRect = hand.GetComponent<RectTransform>();
|
||||
handRect.sizeDelta = new Vector2(480, 160);
|
||||
var layout = hand.GetComponent<HorizontalLayoutGroup>();
|
||||
layout.spacing = 8;
|
||||
layout.childControlWidth = false;
|
||||
layout.childControlHeight = false;
|
||||
layout.childForceExpandWidth = false;
|
||||
layout.childForceExpandHeight = false;
|
||||
|
||||
CreateDragCard(hand.transform, "CardA");
|
||||
var dragged = CreateDragCard(hand.transform, "CardB");
|
||||
CreateDragCard(hand.transform, "CardC");
|
||||
Canvas.ForceUpdateCanvases();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(handRect);
|
||||
|
||||
var originalIndex = dragged.transform.GetSiblingIndex();
|
||||
var originalPosition = dragged.GetComponent<RectTransform>().anchoredPosition;
|
||||
var clicked = false;
|
||||
dragged.GetComponent<Button>().onClick.AddListener(() => clicked = true);
|
||||
var eventSystem = EventSystem.current;
|
||||
var ownsEventSystem = eventSystem == null;
|
||||
if (eventSystem == null)
|
||||
eventSystem = new GameObject("DragTestEventSystem", typeof(EventSystem)).GetComponent<EventSystem>();
|
||||
var pointer = new PointerEventData(eventSystem) { position = new Vector2(540, 360), delta = new Vector2(80, 30) };
|
||||
var handler = dragged.GetComponent<ReplacedCardDragHandler>();
|
||||
|
||||
handler.OnBeginDrag(pointer);
|
||||
handler.OnDrag(pointer);
|
||||
Assert.That(dragged.transform.GetSiblingIndex(), Is.EqualTo(originalIndex));
|
||||
Assert.That(dragged.GetComponent<RectTransform>().anchoredPosition, Is.EqualTo(originalPosition));
|
||||
Assert.That(GameObject.Find("CardDragGhost"), Is.Not.Null);
|
||||
handler.OnEndDrag(pointer);
|
||||
|
||||
Assert.That(dragged.transform.GetSiblingIndex(), Is.EqualTo(originalIndex));
|
||||
Assert.That(dragged.GetComponent<RectTransform>().anchoredPosition, Is.EqualTo(originalPosition));
|
||||
Assert.That(clicked, Is.True);
|
||||
|
||||
Object.Destroy(canvasObject);
|
||||
if (ownsEventSystem) Object.Destroy(eventSystem.gameObject);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator DraggingConfiguredCard_ReportsDropWithoutInvokingClick()
|
||||
{
|
||||
var canvasObject = new GameObject("ConfiguredDragCanvas", typeof(RectTransform), typeof(Canvas));
|
||||
canvasObject.GetComponent<Canvas>().renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
var dragged = CreateDragCard(canvasObject.transform, "ConfiguredCard");
|
||||
var clicked = false;
|
||||
var dropped = false;
|
||||
var dropPosition = Vector2.zero;
|
||||
dragged.GetComponent<Button>().onClick.AddListener(() => clicked = true);
|
||||
dragged.GetComponent<ReplacedCardDragHandler>().Configure(position =>
|
||||
{
|
||||
dropped = true;
|
||||
dropPosition = position;
|
||||
});
|
||||
var eventSystem = EventSystem.current;
|
||||
var ownsEventSystem = eventSystem == null;
|
||||
if (eventSystem == null)
|
||||
eventSystem = new GameObject("ConfiguredDragEventSystem", typeof(EventSystem)).GetComponent<EventSystem>();
|
||||
var start = new PointerEventData(eventSystem) { position = new Vector2(100, 100) };
|
||||
var end = new PointerEventData(eventSystem) { position = new Vector2(240, 140), delta = new Vector2(140, 40) };
|
||||
var handler = dragged.GetComponent<ReplacedCardDragHandler>();
|
||||
|
||||
handler.OnBeginDrag(start);
|
||||
handler.OnDrag(end);
|
||||
handler.OnEndDrag(end);
|
||||
|
||||
Assert.That(dropped, Is.True);
|
||||
Assert.That(clicked, Is.False);
|
||||
Assert.That(dropPosition, Is.EqualTo(end.position));
|
||||
yield return null;
|
||||
Assert.That(GameObject.Find("CardDragGhost"), Is.Null);
|
||||
|
||||
Object.Destroy(canvasObject);
|
||||
if (ownsEventSystem) Object.Destroy(eventSystem.gameObject);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator BattleHand_DuplicateCopyCancelsAndDragChangesExecutionOrder()
|
||||
{
|
||||
SceneManager.LoadScene("ReplacedPerson");
|
||||
yield return null;
|
||||
var root = GameObject.Find("ReplacedPersonDemo");
|
||||
Assert.That(root, Is.Not.Null);
|
||||
var safe = root.transform.Find("Canvas/SafeAreaRoot")!;
|
||||
safe.Find("Screens/TitleScreen/Actions/ContinueButton")!.GetComponent<Button>().onClick.Invoke();
|
||||
safe.Find("Screens/ChapterScreen/Footer/StartBattleButton")!.GetComponent<Button>().onClick.Invoke();
|
||||
var phaseMotion = safe.Find("Screens/BattleScreen")!.GetComponent<ReplacedBattlePhaseMotion>();
|
||||
Assert.That(phaseMotion, Is.Not.Null);
|
||||
Assert.That(phaseMotion!.IsVisible, Is.True);
|
||||
Assert.That(phaseMotion.CurrentTitle, Is.EqualTo("先手骰"));
|
||||
|
||||
var match = ReplacedPersonGameService.Current!.Match!;
|
||||
var player = match.State.Players[0];
|
||||
player.NormalHand.Clear();
|
||||
player.NormalHand.AddRange(new[] { "counter.instant", "counter.instant", "counter.stance", "attack.kill", "dodge.flash" });
|
||||
player.EndHand.Clear();
|
||||
player.EndHand.Add("end.inherit");
|
||||
root.SendMessage("RefreshBattle", SendMessageOptions.DontRequireReceiver);
|
||||
yield return null;
|
||||
|
||||
var selection = safe.Find("Screens/BattleScreen/PlayerArea/SelectionText")!.GetComponent<TMP_Text>();
|
||||
Card(safe, 0).GetComponent<Button>().onClick.Invoke();
|
||||
yield return null;
|
||||
Assert.That(selection.text, Does.Contain("1. 一瞬千击"));
|
||||
Assert.That(Card(safe, 0).Find("Label")!.GetComponent<TMP_Text>().text, Does.StartWith("[1] 一瞬千击"));
|
||||
Assert.That(Card(safe, 1).Find("Label")!.GetComponent<TMP_Text>().text, Does.StartWith("一瞬千击"));
|
||||
|
||||
Card(safe, 0).GetComponent<Button>().onClick.Invoke();
|
||||
yield return null;
|
||||
Assert.That(selection.text, Does.Contain("普通牌执行:未选择"));
|
||||
|
||||
foreach (var handIndex in new[] { 0, 2, 3 })
|
||||
{
|
||||
Card(safe, handIndex).GetComponent<Button>().onClick.Invoke();
|
||||
yield return null;
|
||||
}
|
||||
Assert.That(selection.text, Does.Contain("1. 一瞬千击 → 2. 架势 → 3. 杀"));
|
||||
|
||||
var source = Card(safe, 3);
|
||||
var target = Card(safe, 0);
|
||||
var sourceRect = (RectTransform)source;
|
||||
var targetRect = (RectTransform)target;
|
||||
var originalPosition = sourceRect.anchoredPosition;
|
||||
var startPosition = RectTransformUtility.WorldToScreenPoint(null, sourceRect.TransformPoint(sourceRect.rect.center));
|
||||
var targetCenter = RectTransformUtility.WorldToScreenPoint(null, targetRect.TransformPoint(targetRect.rect.center));
|
||||
var endPosition = new Vector2(targetCenter.x - targetRect.rect.width * .25f, targetCenter.y);
|
||||
var begin = new PointerEventData(EventSystem.current) { position = startPosition };
|
||||
var end = new PointerEventData(EventSystem.current) { position = endPosition, delta = endPosition - startPosition };
|
||||
var handler = source.GetComponent<ReplacedCardDragHandler>();
|
||||
|
||||
handler.OnBeginDrag(begin);
|
||||
handler.OnDrag(end);
|
||||
handler.OnEndDrag(end);
|
||||
Assert.That(sourceRect.anchoredPosition, Is.EqualTo(originalPosition));
|
||||
yield return null;
|
||||
|
||||
Assert.That(selection.text, Does.Contain("1. 杀 → 2. 一瞬千击 → 3. 架势"));
|
||||
Assert.That(Card(safe, 3).Find("Label")!.GetComponent<TMP_Text>().text, Does.StartWith("[1] 杀"));
|
||||
Assert.That(Card(safe, 0).Find("Label")!.GetComponent<TMP_Text>().text, Does.StartWith("[2] 一瞬千击"));
|
||||
Assert.That(Card(safe, 2).Find("Label")!.GetComponent<TMP_Text>().text, Does.StartWith("[3] 架势"));
|
||||
Assert.That(GameObject.Find("CardDragGhost"), Is.Null);
|
||||
Assert.That(match.State.Players[0].NormalHand.Count(id => id == "counter.instant"), Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator SlotResolutionMotion_PresentsClashDiceAndFinalResult()
|
||||
{
|
||||
var slot = new GameObject("AnimatedSlot", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
||||
var textObject = new GameObject("Text", typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI));
|
||||
textObject.transform.SetParent(slot.transform, false);
|
||||
var textRect = textObject.GetComponent<RectTransform>();
|
||||
textRect.anchorMin = Vector2.zero;
|
||||
textRect.anchorMax = Vector2.one;
|
||||
var motion = slot.AddComponent<ReplacedSlotResolutionMotion>();
|
||||
motion.Play(new Core.ReplacedSlotResolution
|
||||
{
|
||||
SlotIndex = 0,
|
||||
PlayerOneType = Core.ReplacedEffectType.Counter,
|
||||
PlayerTwoType = Core.ReplacedEffectType.Attack,
|
||||
PlayerOneRoll = 5,
|
||||
PlayerTwoRoll = 2,
|
||||
PlayerOneValue = 17,
|
||||
PlayerTwoValue = 8,
|
||||
DamageToPlayerTwo = 6,
|
||||
Summary = "反击 6"
|
||||
}, 0);
|
||||
|
||||
yield return new WaitForSecondsRealtime(.72f);
|
||||
Assert.That(motion.IsPlaying, Is.True);
|
||||
Assert.That(slot.transform.Find("PlayerToken")!.gameObject.activeSelf, Is.True);
|
||||
Assert.That(textObject.GetComponent<TMP_Text>().text, Does.Contain("投掷中"));
|
||||
Assert.That(slot.transform.Find("PlayerToken/Label")!.GetComponent<TMP_Text>().text, Does.Contain("D6"));
|
||||
yield return new WaitForSecondsRealtime(1.9f);
|
||||
Assert.That(motion.IsPlaying, Is.False);
|
||||
Assert.That(textObject.GetComponent<TMP_Text>().text, Does.Contain("反击 6"));
|
||||
Assert.That(slot.transform.Find("PlayerToken")!.gameObject.activeSelf, Is.False);
|
||||
Object.Destroy(slot);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
private static GameObject CreateDragCard(Transform parent, string name)
|
||||
{
|
||||
var card = new GameObject(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button),
|
||||
typeof(LayoutElement), typeof(ReplacedCardDragHandler));
|
||||
card.transform.SetParent(parent, false);
|
||||
card.GetComponent<RectTransform>().sizeDelta = new Vector2(120, 140);
|
||||
card.GetComponent<LayoutElement>().preferredWidth = 120;
|
||||
return card;
|
||||
}
|
||||
|
||||
private static Transform Card(Transform safeArea, int handIndex)
|
||||
{
|
||||
return safeArea.Find("Screens/BattleScreen/PlayerArea/NormalHand/Normal_" + handIndex)!;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 777e057666d478748851b2c92e16e85d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user