Complete persistence boundaries and free-play supply with device verification
This commit is contained in:
@@ -16,10 +16,19 @@ namespace MagnetismHeroes.Core
|
||||
public BoardRules Board { get; private set; }
|
||||
public CameraState Camera { get; private set; } = DefaultCamera();
|
||||
public string LastSaveError { get; private set; }
|
||||
public event Action<string> SaveFinished;
|
||||
public bool HasFreePlay => _progress.latestFreePlay != null;
|
||||
public int[] CompletedStageIds => (int[])_progress.completedStageIds.Clone();
|
||||
public IReadOnlyList<StageDefinition> Definitions => Array.AsReadOnly(_definitions);
|
||||
public bool HasUnfinishedStage(int stageId) => _progress.puzzleStates.Any(state => state.stageId == stageId);
|
||||
public bool HasSeenTutorial(string id) => _progress.seenTutorialIds.Contains(id);
|
||||
|
||||
public bool MarkTutorialSeen(string id)
|
||||
{
|
||||
if ((id != "merge" && id != "split") || HasSeenTutorial(id)) return false;
|
||||
_progress.seenTutorialIds = _progress.seenTutorialIds.Concat(new[] { id }).ToArray();
|
||||
SaveCurrent(); return true;
|
||||
}
|
||||
|
||||
public GameSession(IEnumerable<StageDefinition> definitions, IProgressStore store)
|
||||
{
|
||||
@@ -67,6 +76,14 @@ namespace MagnetismHeroes.Core
|
||||
public RuleResult SplitAndMove(string groupId, IReadOnlyList<string> selectedIds, string newGroupId, Position3 delta) =>
|
||||
Commit(() => Board.TrySplitAndMove(groupId, selectedIds, newGroupId, delta));
|
||||
public RuleResult Count(string ballId) => Commit(() => Board.TryCount(ballId));
|
||||
public RuleResult ReturnGroup(string groupId) => Commit(() => Board.TryReturn(groupId));
|
||||
public RuleResult Spawn(Position3 position)
|
||||
{
|
||||
if (Board == null) return RuleResult.Reject(RejectionReason.InvalidGroup);
|
||||
int index = 1;
|
||||
while (Board.Balls.Any(ball => ball.id == $"free-b{index:D2}") || Board.Groups.Any(group => group.id == $"free-g{index:D2}")) index++;
|
||||
return Commit(() => Board.TrySpawn($"free-b{index:D2}", $"free-g{index:D2}", position));
|
||||
}
|
||||
|
||||
public void CommitCamera(CameraState camera)
|
||||
{
|
||||
@@ -104,6 +121,7 @@ namespace MagnetismHeroes.Core
|
||||
}
|
||||
var success = _store.TrySave(_progress, out var error);
|
||||
LastSaveError = error;
|
||||
SaveFinished?.Invoke(error);
|
||||
return success;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using MagnetismHeroes.Core;
|
||||
using MagnetismHeroes.UnityRuntime.Bootstrap;
|
||||
using MagnetismHeroes.UnityRuntime.StageAssets;
|
||||
using TMPro;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MagnetismHeroes.Editor
|
||||
{
|
||||
public static class PersistenceAssets
|
||||
{
|
||||
private const string Presentation = "Assets/MagnetismHeroes/UnityRuntime/Presentation/";
|
||||
[MenuItem("Magnetism Heroes/Build Persistence Assets")]
|
||||
public static void Build()
|
||||
{
|
||||
var font = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(Presentation + "Fonts/NotoSansCJKkr-FirstSlice.asset");
|
||||
const string glyphs = "2 하나 꺼내기 친구 반납 지금은 저장하지 못했어요. 다시 해볼게요. 빈 곳이 보이도록 화면을 움직여 볼까?";
|
||||
if (glyphs.Any(character => !font.HasCharacter(character)))
|
||||
{
|
||||
typeof(TMP_FontAsset).GetProperty("SourceFont_EditorRef", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.SetValue(font, AssetDatabase.LoadAssetAtPath<Font>(Presentation + "Fonts/NotoSansCJKkr-Regular.otf"));
|
||||
font.atlasPopulationMode = AtlasPopulationMode.Dynamic;
|
||||
try { if (!font.TryAddCharacters(glyphs, out var missing)) throw new InvalidOperationException("Missing persistence glyphs: " + missing); }
|
||||
finally { font.atlasPopulationMode = AtlasPopulationMode.Static; }
|
||||
EditorUtility.SetDirty(font);
|
||||
}
|
||||
const string stagePath = "Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage02.asset";
|
||||
var stage = AssetDatabase.LoadAssetAtPath<StageAsset>(stagePath);
|
||||
if (stage == null) { stage = ScriptableObject.CreateInstance<StageAsset>(); AssetDatabase.CreateAsset(stage, stagePath); }
|
||||
var stageFields = new SerializedObject(stage); stageFields.FindProperty("_stageId").intValue = 2;
|
||||
stageFields.FindProperty("_kind").enumValueIndex = (int)StageKind.Counting;
|
||||
var initial = stageFields.FindProperty("_initialGroups"); initial.arraySize = 2;
|
||||
initial.GetArrayElementAtIndex(0).intValue = initial.GetArrayElementAtIndex(1).intValue = 1;
|
||||
var target = stageFields.FindProperty("_targetGroups"); target.arraySize = 1; target.GetArrayElementAtIndex(0).intValue = 2;
|
||||
var positions = stageFields.FindProperty("_ballPositions"); positions.arraySize = 2;
|
||||
positions.GetArrayElementAtIndex(0).vector3Value = new Vector3(-1.2f, .8f, 0);
|
||||
positions.GetArrayElementAtIndex(1).vector3Value = new Vector3(1.2f, .8f, 0); stageFields.ApplyModifiedPropertiesWithoutUndo();
|
||||
var root = PrefabUtility.LoadPrefabContents(FirstSliceAssets.AppPrefabPath);
|
||||
try
|
||||
{
|
||||
var app = new SerializedObject(root.GetComponent<GameApp>());
|
||||
var stages = app.FindProperty("_stages"); var catalog = Enumerable.Range(0, stages.arraySize)
|
||||
.Select(index => (StageAsset)stages.GetArrayElementAtIndex(index).objectReferenceValue).Where(value => value.StageId != 2)
|
||||
.Concat(new[] { stage }).OrderBy(value => value.StageId).ToArray(); stages.arraySize = catalog.Length;
|
||||
for (int index = 0; index < catalog.Length; index++) stages.GetArrayElementAtIndex(index).objectReferenceValue = catalog[index];
|
||||
var safe = root.transform.Find("Interface/SafeArea"); var selection = safe.Find("StageSelection");
|
||||
var template = selection.Find("Stage01Button").gameObject;
|
||||
var stageButton = Button(selection, template, "Stage02Button", "2 친구 찾기", new Vector2(.5f, .36f), new Vector2(280, 72));
|
||||
app.FindProperty("_stageTwoButton").objectReferenceValue = stageButton;
|
||||
var play = safe.Find("Play"); var playTemplate = play.Find("HomeButton").gameObject;
|
||||
app.FindProperty("_spawnButton").objectReferenceValue = Button(play, playTemplate, "SpawnButton", "하나 꺼내기", new Vector2(.86f, .3f), new Vector2(240, 62));
|
||||
app.FindProperty("_returnButton").objectReferenceValue = Button(play, playTemplate, "ReturnButton", "친구 반납", new Vector2(.86f, .2f), new Vector2(240, 62));
|
||||
var notice = (TMP_Text)app.FindProperty("_saveNotice").objectReferenceValue;
|
||||
notice.transform.SetParent(safe, false); notice.transform.SetAsLastSibling();
|
||||
notice.rectTransform.anchorMin = notice.rectTransform.anchorMax = new Vector2(.5f, .07f);
|
||||
notice.rectTransform.anchoredPosition = Vector2.zero;
|
||||
app.ApplyModifiedPropertiesWithoutUndo(); PrefabUtility.SaveAsPrefabAsset(root, FirstSliceAssets.AppPrefabPath);
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(root); }
|
||||
AssetDatabase.SaveAssets(); Debug.Log("[Persistence] Stage02, locked selection, free supply/whole-friend return and shared save notice wired. Main unchanged.");
|
||||
}
|
||||
private static UnityEngine.UI.Button Button(Transform parent, GameObject template, string name, string label, Vector2 anchor, Vector2 size)
|
||||
{
|
||||
var existing = parent.Find(name); var button = existing == null ? UnityEngine.Object.Instantiate(template, parent) : existing.gameObject;
|
||||
button.name = name; var rect = (RectTransform)button.transform; rect.anchorMin = rect.anchorMax = anchor;
|
||||
rect.anchoredPosition = Vector2.zero; rect.sizeDelta = size;
|
||||
var text = button.GetComponentInChildren<TMP_Text>(); text.text = label; text.rectTransform.sizeDelta = size;
|
||||
return button.GetComponent<UnityEngine.UI.Button>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60c8eab85ee35ad40884c25ab21276c9
|
||||
@@ -26,9 +26,9 @@ namespace MagnetismHeroes.Tests
|
||||
{
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GameApp.prefab");
|
||||
var serialized = new SerializedObject(prefab.GetComponent<GameApp>()); var stages = serialized.FindProperty("_stages");
|
||||
Assert.That(stages.arraySize, Is.EqualTo(4)); var definitions = Enumerable.Range(0, stages.arraySize)
|
||||
Assert.That(stages.arraySize, Is.EqualTo(5)); var definitions = Enumerable.Range(0, stages.arraySize)
|
||||
.Select(index => ((StageAsset)stages.GetArrayElementAtIndex(index).objectReferenceValue).ToDefinition()).ToArray();
|
||||
Assert.That(definitions.Select(stage => stage.Id), Is.EqualTo(new[] { 1, 6, 10, 14 }));
|
||||
Assert.That(definitions.Select(stage => stage.Id), Is.EqualTo(new[] { 1, 2, 6, 10, 14 }));
|
||||
var session = new GameSession(definitions, new EmptyStore()); Assert.That(session.IsUnlocked(6), Is.False);
|
||||
Assert.That(session.StartStage(6), Is.False); Assert.That(session.IsUnlocked(10), Is.False); Assert.That(session.IsUnlocked(14), Is.False);
|
||||
Assert.That(serialized.FindProperty("_compositionButtons").arraySize, Is.EqualTo(3));
|
||||
|
||||
@@ -73,6 +73,7 @@ namespace MagnetismHeroes.Tests
|
||||
Assert.That(File.ReadAllText(_path), Is.EqualTo("broken-primary"));
|
||||
Assert.That(store.TrySave(new ProgressSave(), out var error), Is.True, error);
|
||||
Assert.That(Directory.GetFiles(_directory, "*.damaged-*").Select(File.ReadAllText), Does.Contain("broken-primary"));
|
||||
Assert.That(File.ReadAllText(_path + ".bak"), Is.EqualTo("broken-backup"));
|
||||
}
|
||||
[Test] public void InvalidGroupMembershipDoesNotReplaceAValidSave()
|
||||
{
|
||||
@@ -99,5 +100,23 @@ namespace MagnetismHeroes.Tests
|
||||
Assert.That(Store().TrySave(Puzzle(), out _), Is.True);
|
||||
Assert.That(File.ReadAllText(_path), Does.Not.Contain("IsValid"));
|
||||
}
|
||||
[Test] public void LockedTemporaryFileReportsFailureAndKeepsTheLastValidSave()
|
||||
{
|
||||
var store = Store(); Assert.That(store.TrySave(Puzzle(), out _), Is.True);
|
||||
Assert.That(store.TrySave(Puzzle(), out _), Is.True);
|
||||
var before = File.ReadAllBytes(_path);
|
||||
var backupBefore = File.ReadAllBytes(_path + ".bak");
|
||||
using (var locked = new FileStream(_path + ".tmp", FileMode.Create, FileAccess.ReadWrite, FileShare.None))
|
||||
{
|
||||
string error = null; bool saved = true;
|
||||
Assert.DoesNotThrow(() => saved = store.TrySave(Puzzle(), out error));
|
||||
Assert.That(saved, Is.False); Assert.That(error, Is.Not.Empty);
|
||||
Assert.That(File.ReadAllBytes(_path), Is.EqualTo(before));
|
||||
Assert.That(File.ReadAllBytes(_path + ".bak"), Is.EqualTo(backupBefore));
|
||||
}
|
||||
Assert.That(store.TryLoad(out var restored, out _), Is.True);
|
||||
Assert.That(restored.puzzleStates.Length, Is.EqualTo(1));
|
||||
Assert.That(store.TrySave(Puzzle(), out var recoveredError), Is.True, recoveredError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MagnetismHeroes.Core;
|
||||
using MagnetismHeroes.UnityRuntime.Persistence;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MagnetismHeroes.Tests
|
||||
{
|
||||
public sealed class PersistenceSessionTests
|
||||
{
|
||||
private string _directory, _path;
|
||||
private static readonly StageDefinition[] Definitions = {
|
||||
new StageDefinition(1, StageKind.Counting, new[] { 1 }, new[] { 1 }),
|
||||
new StageDefinition(2, StageKind.Counting, new[] { 1, 1 }, new[] { 2 }),
|
||||
new StageDefinition(6, StageKind.Composition, new[] { 1, 1 }, new[] { 2 }) };
|
||||
private sealed class RecordedStore : IProgressStore
|
||||
{
|
||||
private readonly LocalProgressStore _store;
|
||||
public int Writes;
|
||||
public RecordedStore(string path) { _store = new LocalProgressStore(path, Definitions); }
|
||||
public bool TryLoad(out ProgressSave save, out string error) => _store.TryLoad(out save, out error);
|
||||
public bool TrySave(ProgressSave save, out string error) { Writes++; return _store.TrySave(save, out error); }
|
||||
}
|
||||
[SetUp] public void SetUp()
|
||||
{ _directory = Path.Combine(Path.GetTempPath(), "MagnetismHeroes-persistence-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_directory); _path = Path.Combine(_directory, "progress.json"); }
|
||||
[TearDown] public void TearDown() => Directory.Delete(_directory, true);
|
||||
private ProgressSave Load()
|
||||
{ Assert.That(new LocalProgressStore(_path, Definitions).TryLoad(out var save, out var error), Is.True, error); return save; }
|
||||
private static CameraState Camera(float distance) => new CameraState {
|
||||
target = new Position3(1, .8f, -2), rotation = new Rotation4(0, .6f, 0, .8f), distance = distance };
|
||||
private static void CompleteOne(GameSession session)
|
||||
{ session.StartStage(1); session.MoveGroup(session.Board.Groups.Single().id, new Position3(.5f, .8f, 0)); session.Count(session.Board.Balls.Single().id); }
|
||||
|
||||
[Test] public void ActualFileAlternatesTwoUnfinishedBoardsAndLatestFreeWithoutLosingAnyField()
|
||||
{
|
||||
var store = new RecordedStore(_path); var session = new GameSession(Definitions, store); CompleteOne(session);
|
||||
session.Spawn(new Position3(-2, .8f, 0)); session.CommitCamera(Camera(9)); session.ShowHome();
|
||||
session.StartStage(2); var groups = session.Board.Groups;
|
||||
session.MoveAndMerge(groups[0].id, new Position3(.6f, 1.1f, -2), groups[1].id);
|
||||
session.Count(session.Board.Balls[0].id); session.CommitCamera(Camera(11)); session.ShowHome();
|
||||
var fixture = Load(); fixture.completedStageIds = new[] { 1, 2, 3, 4, 5 };
|
||||
Assert.That(new LocalProgressStore(_path, Definitions).TrySave(fixture, out var error), Is.True, error);
|
||||
session = new GameSession(Definitions, new RecordedStore(_path)); session.StartStage(6);
|
||||
session.MoveGroup(session.Board.Groups[0].id, new Position3(-1, .8f, 2)); session.CommitCamera(Camera(7)); session.ShowHome();
|
||||
var before = File.ReadAllBytes(_path); var saved = Load();
|
||||
Assert.That(saved.puzzleStates.Length, Is.EqualTo(2));
|
||||
Assert.That(saved.puzzleStates.Single(state => state.stageId == 2).countedBallIds.Length, Is.EqualTo(1));
|
||||
foreach (int id in new[] { 2, 6, 2, 6 })
|
||||
{
|
||||
session = new GameSession(Definitions, new RecordedStore(_path)); Assert.That(session.Screen, Is.EqualTo(SessionScreen.Home));
|
||||
Assert.That(session.StartStage(id), Is.True); var expected = saved.puzzleStates.Single(state => state.stageId == id);
|
||||
AssertBoard(session.Board, expected.balls, expected.groups); AssertCamera(session.Camera, expected.camera);
|
||||
Assert.That(session.Board.CollectedBallIds, Is.EqualTo(expected.collectedBallIds));
|
||||
Assert.That(session.Board.CountedBallIds, Is.EqualTo(expected.countedBallIds)); session.ShowHome();
|
||||
Assert.That(session.ContinueFreePlay(), Is.True); AssertBoard(session.Board, saved.latestFreePlay.balls, saved.latestFreePlay.groups);
|
||||
AssertCamera(session.Camera, saved.latestFreePlay.camera); session.ShowHome();
|
||||
Assert.That(File.ReadAllBytes(_path), Is.EqualTo(before), "Exact deterministic JSON bytes retain both unfinished plus global Free.");
|
||||
}
|
||||
}
|
||||
[Test] public void CompletionIsOneWriteReplacesOnlyLatestFreeAndReplayKeepsProgress()
|
||||
{
|
||||
var store = new RecordedStore(_path); var session = new GameSession(Definitions, store); CompleteOne(session);
|
||||
session.StartStage(2); session.MoveAndMerge(session.Board.Groups[0].id, new Position3(.5f, 0, 0), session.Board.Groups[1].id);
|
||||
session.Count(session.Board.Balls[0].id); int before = store.Writes;
|
||||
session.Count(session.Board.Balls[1].id); Assert.That(store.Writes, Is.EqualTo(before + 1));
|
||||
var completed = Load(); Assert.That(completed.completedStageIds, Is.EqualTo(new[] { 1, 2 }));
|
||||
Assert.That(completed.puzzleStates, Is.Empty); Assert.That(completed.latestFreePlay.sourceStageId, Is.EqualTo(2));
|
||||
session.StartStage(1); session.MoveGroup(session.Board.Groups[0].id, new Position3(.2f, 0, 0));
|
||||
var replay = Load(); Assert.That(replay.completedStageIds, Is.EqualTo(new[] { 1, 2 }));
|
||||
Assert.That(replay.puzzleStates.Single().stageId, Is.EqualTo(1)); Assert.That(replay.latestFreePlay.sourceStageId, Is.EqualTo(2));
|
||||
session.Count(session.Board.Balls.Single().id); Assert.That(Load().latestFreePlay.sourceStageId, Is.EqualTo(1));
|
||||
Assert.That(Load().completedStageIds, Is.EqualTo(new[] { 1, 2 }));
|
||||
}
|
||||
[Test] public void TutorialRecordsAreUniqueValidatedAndPersistAcrossFreeAndPuzzleTransitions()
|
||||
{
|
||||
var store = new RecordedStore(_path); var session = new GameSession(Definitions, store);
|
||||
Assert.That(session.MarkTutorialSeen("merge"), Is.True); int before = store.Writes;
|
||||
Assert.That(session.MarkTutorialSeen("merge"), Is.False); Assert.That(session.MarkTutorialSeen("unknown"), Is.False);
|
||||
Assert.That(store.Writes, Is.EqualTo(before)); CompleteOne(session); Assert.That(session.MarkTutorialSeen("split"), Is.True);
|
||||
session.StartStage(1); session.ShowHome(); session = new GameSession(Definitions, new RecordedStore(_path));
|
||||
Assert.That(session.HasSeenTutorial("merge"), Is.True); Assert.That(session.HasSeenTutorial("split"), Is.True);
|
||||
Assert.That(Load().seenTutorialIds, Is.EqualTo(new[] { "merge", "split" }));
|
||||
}
|
||||
private static void AssertBoard(BoardRules board, BallState[] balls, GroupState[] groups)
|
||||
{
|
||||
Assert.That(board.Balls.Select(ball => ball.id), Is.EqualTo(balls.Select(ball => ball.id)));
|
||||
Assert.That(board.Balls.Select(ball => ball.position), Is.EqualTo(balls.Select(ball => ball.position)));
|
||||
Assert.That(board.Groups.Select(group => group.id), Is.EqualTo(groups.Select(group => group.id)));
|
||||
for (int index = 0; index < groups.Length; index++) Assert.That(board.Groups[index].ballIds, Is.EqualTo(groups[index].ballIds));
|
||||
}
|
||||
private static void AssertCamera(CameraState actual, CameraState expected)
|
||||
{ Assert.That(actual.target, Is.EqualTo(expected.target)); Assert.That(actual.rotation, Is.EqualTo(expected.rotation)); Assert.That(actual.distance, Is.EqualTo(expected.distance)); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8666ab89be2e7ab4a96f90ee79122b2f
|
||||
@@ -20,8 +20,16 @@ namespace MagnetismHeroes.Tests
|
||||
private sealed class MemoryStore : IProgressStore
|
||||
{
|
||||
public ProgressSave Saved = new ProgressSave();
|
||||
public bool TryLoad(out ProgressSave save, out string error) { save = Saved; error = null; return true; }
|
||||
public bool TrySave(ProgressSave save, out string error) { Saved = save; error = null; return true; }
|
||||
public bool FailWrites;
|
||||
private static ProgressSave Copy(ProgressSave save)
|
||||
{ var copy = JsonUtility.FromJson<ProgressSave>(JsonUtility.ToJson(save)); if (save.latestFreePlay == null) copy.latestFreePlay = null; return copy; }
|
||||
public bool TryLoad(out ProgressSave save, out string error) { save = Copy(Saved); error = null; return true; }
|
||||
public bool TrySave(ProgressSave save, out string error)
|
||||
{
|
||||
error = FailWrites ? "Disk full during committed save" : null;
|
||||
if (FailWrites) return false;
|
||||
Saved = Copy(save); return true;
|
||||
}
|
||||
}
|
||||
private GameApp _app; private MemoryStore _store; private Touchscreen _touchscreen;
|
||||
private InputSettings _originalInputSettings, _testInputSettings;
|
||||
@@ -63,6 +71,75 @@ namespace MagnetismHeroes.Tests
|
||||
private void QueueTouch(int id, UnityEngine.InputSystem.TouchPhase phase, Vector2 position) =>
|
||||
InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = id, phase = phase, position = position });
|
||||
|
||||
[UnityTest] public IEnumerator FailedMoveCameraAndPauseSavesShowAVisibleNoticeAndRecoveryClearsIt()
|
||||
{
|
||||
_app.StartStage(1); yield return null;
|
||||
var before = JsonUtility.ToJson(_store.Saved); _store.FailWrites = true;
|
||||
_app.CommitMove(_app.Session.Board.Groups.Single().id, new Position3(.2f, 0, 0));
|
||||
var notice = _app.GetComponentsInChildren<TMP_Text>(true).Single(text => text.name == "SaveNotice");
|
||||
Assert.That(notice.text, Does.Contain("저장")); Assert.That(notice.gameObject.activeInHierarchy, Is.True);
|
||||
Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(before));
|
||||
_store.FailWrites = false; _app.Session.SaveCurrent(); Assert.That(notice.text, Is.Empty);
|
||||
_store.FailWrites = true; _app.Input.Begin(new Vector2(Screen.width - 30, Screen.height / 2), false);
|
||||
_app.Input.Move(new Vector2(Screen.width - 80, Screen.height / 2)); _app.Input.End(new Vector2(Screen.width - 80, Screen.height / 2));
|
||||
Assert.That(notice.text, Does.Contain("저장"));
|
||||
_store.FailWrites = false; _app.Session.SaveCurrent(); Assert.That(notice.text, Is.Empty);
|
||||
_store.FailWrites = true; _app.gameObject.SendMessage("OnApplicationPause", true);
|
||||
Assert.That(notice.text, Does.Contain("저장"));
|
||||
_store.FailWrites = false; _app.gameObject.SendMessage("OnApplicationPause", true);
|
||||
Assert.That(notice.text, Is.Empty);
|
||||
}
|
||||
|
||||
[UnityTest] public IEnumerator FreeSupplyButtonsCanReachTenRejectMoreReturnAWholeFriendAndRespawnEmptyBoard()
|
||||
{
|
||||
_app.StartStage(1); yield return null; Physics.SyncTransforms();
|
||||
var point = BallPoint(); Drag(point, point + new Vector2(60, 0)); yield return null; Physics.SyncTransforms(); Tap(BallPoint()); yield return null;
|
||||
var spawnObject = _app.transform.Find("Interface/SafeArea/Play/SpawnButton");
|
||||
Assert.That(spawnObject, Is.Not.Null, "Free play needs its real supply button.");
|
||||
var spawn = spawnObject.GetComponent<UnityEngine.UI.Button>();
|
||||
var giveBack = _app.transform.Find("Interface/SafeArea/Play/ReturnButton").GetComponent<UnityEngine.UI.Button>();
|
||||
for (int index = 0; index < 9; index++) { spawn.onClick.Invoke(); yield return null; }
|
||||
Assert.That(_app.Session.Board.BallCount, Is.EqualTo(10)); var full = JsonUtility.ToJson(_store.Saved);
|
||||
Assert.That(spawn.interactable, Is.False); spawn.onClick.Invoke(); Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(full));
|
||||
while (_app.Session.Board.BallCount > 0)
|
||||
{
|
||||
var ball = _app.Presenter.Balls.Values.First(); Vector2 location = _app.Presenter.Camera.WorldToScreenPoint(ball.transform.position);
|
||||
int before = _app.Session.Board.BallCount; Tap(location); giveBack.onClick.Invoke(); yield return null; Physics.SyncTransforms();
|
||||
Assert.That(_app.Session.Board.BallCount, Is.LessThan(before));
|
||||
}
|
||||
Assert.That(_app.Session.Board.Groups, Is.Empty); Assert.That(_store.Saved.completedStageIds, Is.EqualTo(new[] { 1 }));
|
||||
spawn.onClick.Invoke(); yield return null; Assert.That(_app.Session.Board.BallCount, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[UnityTest] public IEnumerator SuccessfulTwoCountKeepsItsFinalNumbersDuringFreeSplitCancelAndCommit()
|
||||
{
|
||||
_store.Saved.completedStageIds = new[] { 1 }; _app.Initialize(_store);
|
||||
foreach (bool cancel in new[] { true, false })
|
||||
{
|
||||
_app.StartStage(2); yield return null; Physics.SyncTransforms();
|
||||
var groups = _app.Session.Board.Groups;
|
||||
_app.CommitMove(groups[0].id, new Position3(.6f, 0, 0), groups[1].id);
|
||||
var ids = _app.Session.Board.Balls.Select(ball => ball.id).ToArray();
|
||||
foreach (string id in ids)
|
||||
{
|
||||
Vector2 point = _app.Presenter.Camera.WorldToScreenPoint(_app.Presenter.Balls[id].transform.position);
|
||||
Tap(point);
|
||||
}
|
||||
Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.FreePlay)); Assert.That(_app.Voice.PendingCount, Is.EqualTo(2));
|
||||
Assert.That(_app.GetComponent<AudioSource>().clip.name, Is.EqualTo("count_01"));
|
||||
Vector2 firstPoint = _app.Presenter.Camera.WorldToScreenPoint(_app.Presenter.Balls[ids[0]].transform.position); Tap(firstPoint);
|
||||
_app.transform.Find("Interface/SafeArea/Play/SplitButton").GetComponent<UnityEngine.UI.Button>().onClick.Invoke();
|
||||
if (cancel) _app.transform.Find("Interface/SafeArea/Play/CancelSplitButton").GetComponent<UnityEngine.UI.Button>().onClick.Invoke();
|
||||
else { Tap(firstPoint); Drag(firstPoint, firstPoint - new Vector2(120, 0)); }
|
||||
Assert.That(_app.Voice.PendingCount, Is.EqualTo(2), cancel ? "Cancel split must keep count FIFO." : "Committed split must keep count FIFO.");
|
||||
float deadline = Time.realtimeSinceStartup + 5;
|
||||
while (_app.Voice.CurrentClipId == "count_01" && Time.realtimeSinceStartup < deadline) yield return null;
|
||||
Assert.That(_app.Voice.CurrentClipId, Is.EqualTo("count_02")); Assert.That(_app.GetComponent<AudioSource>().isPlaying, Is.True);
|
||||
while (_app.Voice.PendingCount > 0 && Time.realtimeSinceStartup < deadline) yield return null;
|
||||
Assert.That(_app.Voice.PendingCount, Is.Zero);
|
||||
}
|
||||
}
|
||||
|
||||
[UnityTest] public IEnumerator SameUpdateShortTapRespondsBeforeCollectionAndCountsOnceAfterCollection()
|
||||
{
|
||||
_touchscreen = InputSystem.AddDevice<Touchscreen>(); _app.StartStage(1); yield return null; Physics.SyncTransforms();
|
||||
|
||||
@@ -25,6 +25,9 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap
|
||||
[SerializeField] private UnityEngine.UI.Button _playButton;
|
||||
[SerializeField] private UnityEngine.UI.Button _continueButton;
|
||||
[SerializeField] private UnityEngine.UI.Button _stageOneButton;
|
||||
[SerializeField] private UnityEngine.UI.Button _stageTwoButton;
|
||||
[SerializeField] private UnityEngine.UI.Button _spawnButton;
|
||||
[SerializeField] private UnityEngine.UI.Button _returnButton;
|
||||
[SerializeField] private UnityEngine.UI.Button _homeButton;
|
||||
[SerializeField] private UnityEngine.UI.Button _selectionHomeButton;
|
||||
[SerializeField] private UnityEngine.UI.Button[] _compositionButtons = Array.Empty<UnityEngine.UI.Button>();
|
||||
@@ -47,12 +50,15 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap
|
||||
_playButton.onClick.AddListener(ShowSelection);
|
||||
_continueButton.onClick.AddListener(ContinueFreePlay);
|
||||
_stageOneButton.onClick.AddListener(() => StartStage(1));
|
||||
if (_stageTwoButton != null) _stageTwoButton.onClick.AddListener(() => StartStage(2));
|
||||
if (_spawnButton != null) _spawnButton.onClick.AddListener(SpawnFriend);
|
||||
if (_returnButton != null) _returnButton.onClick.AddListener(ReturnSelectedFriend);
|
||||
_homeButton.onClick.AddListener(ShowHome);
|
||||
_selectionHomeButton.onClick.AddListener(ShowHome);
|
||||
for (int index = 0; index < _compositionButtons.Length; index++)
|
||||
{ int stageId = _stages[index + 1].StageId; _compositionButtons[index].onClick.AddListener(() => StartStage(stageId)); }
|
||||
{ int stageId = _stages.Where(stage => stage.ToDefinition().Kind == StageKind.Composition).ElementAt(index).StageId; _compositionButtons[index].onClick.AddListener(() => StartStage(stageId)); }
|
||||
if (_splitButton != null) _splitButton.onClick.AddListener(() => { _input.BeginSplit(); if (_input.IsSplitting) _voice.Guide("guide_split"); });
|
||||
if (_cancelSplitButton != null) _cancelSplitButton.onClick.AddListener(() => { _input.CancelSplit(); _voice.Clear(); });
|
||||
if (_cancelSplitButton != null) _cancelSplitButton.onClick.AddListener(() => { _input.CancelSplit(); _voice.CancelGuide(); });
|
||||
}
|
||||
private void Start()
|
||||
{
|
||||
@@ -62,11 +68,30 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap
|
||||
public void Initialize(IProgressStore store)
|
||||
{
|
||||
Session = new GameSession(_stages.Select(stage => stage.ToDefinition()), store);
|
||||
Session.SaveFinished += ShowSaveResult;
|
||||
_saveNotice.text = Session.LastSaveError == null ? "" : Session.LastSaveError.StartsWith("Recovered", StringComparison.Ordinal)
|
||||
? "이전 기록으로 이어서 시작할게요." : "기록을 읽지 못해 처음부터 시작할게요.";
|
||||
if (Session.LastSaveError != null) Debug.LogWarning("[ProgressLoad] " + Session.LastSaveError);
|
||||
Refresh(true);
|
||||
}
|
||||
private void ShowSaveResult(string error)
|
||||
{
|
||||
_saveNotice.text = error == null ? "" : "지금은 저장하지 못했어요. 다시 해볼게요.";
|
||||
if (error != null) Debug.LogWarning("[ProgressSave] " + error);
|
||||
}
|
||||
public void SpawnFriend()
|
||||
{
|
||||
if (Session.Screen != SessionScreen.FreePlay || Session.Board.BallCount >= 10) return;
|
||||
_input.CancelSplit();
|
||||
if (!_presenter.TryFindSpawnPosition(out var position)) { _instruction.text = "빈 곳이 보이도록 화면을 움직여 볼까?"; return; }
|
||||
if (Session.Spawn(position).Accepted) Refresh(false);
|
||||
}
|
||||
public void ReturnSelectedFriend()
|
||||
{
|
||||
if (Session.Screen != SessionScreen.FreePlay || _input.SelectedGroup == null) return;
|
||||
string groupId = _input.SelectedGroup; _input.CancelSplit();
|
||||
if (Session.ReturnGroup(groupId).Accepted) Refresh(false);
|
||||
}
|
||||
public void ShowSelection()
|
||||
{ _input.CancelSplit(); _voice.Clear(); Session.ShowSelection(); Refresh(true); }
|
||||
public void ShowHome()
|
||||
@@ -93,7 +118,7 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap
|
||||
string newId = groupId + "-split";
|
||||
while (Session.Board.Groups.Any(group => group.id == newId)) newId += "-split";
|
||||
var result = Session.SplitAndMove(groupId, selected, newId, delta);
|
||||
if (result.Accepted) { _voice.Clear(); Refresh(false); }
|
||||
if (result.Accepted) { _voice.CancelGuide(); Refresh(false); }
|
||||
return result.Accepted;
|
||||
}
|
||||
public void CountBall(string ballId)
|
||||
@@ -116,7 +141,10 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap
|
||||
bool playing = screen == SessionScreen.Puzzle || screen == SessionScreen.FreePlay;
|
||||
_playPanel.SetActive(playing); _world.SetActive(playing);
|
||||
_continueButton.gameObject.SetActive(Session.HasFreePlay);
|
||||
for (int index = 0; index < _compositionButtons.Length; index++) _compositionButtons[index].interactable = Session.IsUnlocked(_stages[index + 1].StageId);
|
||||
if (_stageTwoButton != null) _stageTwoButton.interactable = Session.IsUnlocked(2);
|
||||
for (int index = 0; index < _compositionButtons.Length; index++) _compositionButtons[index].interactable = Session.IsUnlocked(_stages.Where(stage => stage.ToDefinition().Kind == StageKind.Composition).ElementAt(index).StageId);
|
||||
if (_spawnButton != null) { _spawnButton.gameObject.SetActive(screen == SessionScreen.FreePlay); _spawnButton.interactable = Session.Board != null && Session.Board.BallCount < 10; }
|
||||
if (_returnButton != null) _returnButton.gameObject.SetActive(screen == SessionScreen.FreePlay);
|
||||
if (applyCamera) _orbit.Apply(Session.Camera);
|
||||
_presenter.Render(Session.Board);
|
||||
if (!playing) return;
|
||||
@@ -137,6 +165,7 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap
|
||||
{
|
||||
if (Session == null || _splitButton == null) return;
|
||||
var group = Session.Board?.Groups.FirstOrDefault(value => value.id == _input.SelectedGroup);
|
||||
if (_returnButton != null) _returnButton.interactable = Session.Screen == SessionScreen.FreePlay && group != null && !_input.IsSplitting;
|
||||
_splitButton.gameObject.SetActive(!_input.IsSplitting);
|
||||
_splitButton.interactable = group != null && group.ballIds.Length > 1;
|
||||
_cancelSplitButton.gameObject.SetActive(_input.IsSplitting);
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
|
||||
_splitting = true; _selected.Clear(); _app.RefreshSelection();
|
||||
}
|
||||
public void CancelSplit()
|
||||
{ Cancel(); if (_splitting) _app.Voice.Clear(); _splitting = false; _selected.Clear(); _selectedGroup = null; _presenter.ShowSelection(_selected); _app.RefreshSelection(); }
|
||||
{ Cancel(); if (_splitting) _app.Voice.CancelGuide(); _splitting = false; _selected.Clear(); _selectedGroup = null; _presenter.ShowSelection(_selected); _app.RefreshSelection(); }
|
||||
|
||||
public void Begin(Vector2 point, bool uiBlocked)
|
||||
{
|
||||
|
||||
@@ -96,6 +96,7 @@ namespace MagnetismHeroes.UnityRuntime.Persistence
|
||||
{
|
||||
if (!SaveValidator.TryValidate(save, _definitions, out error)) return false;
|
||||
string temporary = _path + ".tmp";
|
||||
bool createdTemporary = false;
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(_path)));
|
||||
@@ -106,7 +107,7 @@ namespace MagnetismHeroes.UnityRuntime.Persistence
|
||||
}
|
||||
using (var stream = new FileStream(temporary, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
using (var writer = new StreamWriter(stream))
|
||||
{ writer.Write(JsonConvert.SerializeObject(save, Formatting.Indented, SerializerSettings)); writer.Flush(); stream.Flush(true); }
|
||||
{ createdTemporary = true; writer.Write(JsonConvert.SerializeObject(save, Formatting.Indented, SerializerSettings)); writer.Flush(); stream.Flush(true); }
|
||||
if (File.Exists(_path))
|
||||
{
|
||||
// Never replace a valid recovered backup with the invalid primary.
|
||||
@@ -118,7 +119,7 @@ namespace MagnetismHeroes.UnityRuntime.Persistence
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException || exception is UnauthorizedAccessException)
|
||||
{ error = exception.Message; return false; }
|
||||
finally { if (File.Exists(temporary)) File.Delete(temporary); }
|
||||
finally { if (createdTemporary && File.Exists(temporary)) File.Delete(temporary); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,23 @@ namespace MagnetismHeroes.UnityRuntime.Presentation
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryFindSpawnPosition(out Position3 position)
|
||||
{
|
||||
Physics.SyncTransforms();
|
||||
// Search visible space at the same unit size, away from friends and background props.
|
||||
for (int row = 0; row < 5; row++)
|
||||
for (int column = 0; column < 7; column++)
|
||||
{
|
||||
var point = new Vector3((column - 3) * 1.2f, .8f + row * 1.2f, 0);
|
||||
var viewport = _camera.WorldToViewportPoint(point);
|
||||
if (viewport.z <= 0 || viewport.x < .08f || viewport.x > .92f || viewport.y < .3f || viewport.y > .78f) continue;
|
||||
if (_board.Balls.Any(ball => Vector3.Distance(ToVector(ball.position), point) < 1.1f)) continue;
|
||||
if (Physics.OverlapSphere(point, .55f, ~0, QueryTriggerInteraction.Ignore).Any(collider => !collider.transform.IsChildOf(transform))) continue;
|
||||
position = ToPosition(point); return true;
|
||||
}
|
||||
position = default; return false;
|
||||
}
|
||||
|
||||
private void PositionBadges()
|
||||
{
|
||||
if (_board == null) return;
|
||||
|
||||
+416
-81
File diff suppressed because one or more lines are too long
@@ -35,6 +35,8 @@ RectTransform:
|
||||
- {fileID: 947662399340529430}
|
||||
- {fileID: 8104365176756033704}
|
||||
- {fileID: 8364979175126307048}
|
||||
- {fileID: 2329883891830714780}
|
||||
- {fileID: 5090946158136759876}
|
||||
m_Father: {fileID: 3999613752604275916}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
@@ -289,6 +291,7 @@ RectTransform:
|
||||
- {fileID: 6801798374945474417}
|
||||
- {fileID: 4774970796337304695}
|
||||
- {fileID: 7868283166943063582}
|
||||
- {fileID: 2650272815419196348}
|
||||
m_Father: {fileID: 3999613752604275916}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
@@ -803,6 +806,401 @@ MonoBehaviour:
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!1 &1328309431493275319
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 5049519322727858860}
|
||||
- component: {fileID: 7493142574714255270}
|
||||
- component: {fileID: 8396089032746231139}
|
||||
m_Layer: 0
|
||||
m_Name: Label
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &5049519322727858860
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1328309431493275319}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 2329883891830714780}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 240, y: 62}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &7493142574714255270
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1328309431493275319}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &8396089032746231139
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1328309431493275319}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 0
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: "\uD558\uB098 \uAEBC\uB0B4\uAE30"
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: a5bc77ef7ce7c7e4d85ac578cfcd6646, type: 2}
|
||||
m_sharedMaterial: {fileID: 8207275579423446409, guid: a5bc77ef7ce7c7e4d85ac578cfcd6646, type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontColor: {r: 0.12, g: 0.2, b: 0.31, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_fontColorGradientPreset: {fileID: 0}
|
||||
m_spriteAsset: {fileID: 0}
|
||||
m_tintAllSprites: 0
|
||||
m_StyleSheet: {fileID: 0}
|
||||
m_TextStyleHashCode: 0
|
||||
m_overrideHtmlColors: 0
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 28
|
||||
m_fontSizeBase: 28
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 72
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 2
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 0
|
||||
m_ActiveFontFeatures: 6e72656bu
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
m_horizontalMapping: 0
|
||||
m_verticalMapping: 0
|
||||
m_uvLineOffset: 0
|
||||
m_geometrySortingOrder: 0
|
||||
m_IsTextObjectScaleStatic: 0
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!1 &1458915873150721834
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 2650272815419196348}
|
||||
- component: {fileID: 1467687443961635546}
|
||||
- component: {fileID: 6572129107923074595}
|
||||
- component: {fileID: 4131456494785851528}
|
||||
m_Layer: 0
|
||||
m_Name: Stage02Button
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &2650272815419196348
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1458915873150721834}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 5651594774981243508}
|
||||
m_Father: {fileID: 2361705476825752074}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.36}
|
||||
m_AnchorMax: {x: 0.5, y: 0.36}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 280, y: 72}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &1467687443961635546
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1458915873150721834}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &6572129107923074595
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1458915873150721834}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 0.8, b: 0.3, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 0}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!114 &4131456494785851528
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1458915873150721834}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button
|
||||
m_Navigation:
|
||||
m_Mode: 3
|
||||
m_WrapAround: 0
|
||||
m_SelectOnUp: {fileID: 0}
|
||||
m_SelectOnDown: {fileID: 0}
|
||||
m_SelectOnLeft: {fileID: 0}
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
|
||||
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
|
||||
m_ColorMultiplier: 1
|
||||
m_FadeDuration: 0.1
|
||||
m_SpriteState:
|
||||
m_HighlightedSprite: {fileID: 0}
|
||||
m_PressedSprite: {fileID: 0}
|
||||
m_SelectedSprite: {fileID: 0}
|
||||
m_DisabledSprite: {fileID: 0}
|
||||
m_AnimationTriggers:
|
||||
m_NormalTrigger: Normal
|
||||
m_HighlightedTrigger: Highlighted
|
||||
m_PressedTrigger: Pressed
|
||||
m_SelectedTrigger: Selected
|
||||
m_DisabledTrigger: Disabled
|
||||
m_Interactable: 1
|
||||
m_TargetGraphic: {fileID: 6572129107923074595}
|
||||
m_OnClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
--- !u!1 &1467268788090281726
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 5651594774981243508}
|
||||
- component: {fileID: 6040077154455259841}
|
||||
- component: {fileID: 3093407653909830820}
|
||||
m_Layer: 0
|
||||
m_Name: Label
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &5651594774981243508
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1467268788090281726}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 2650272815419196348}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 280, y: 72}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &6040077154455259841
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1467268788090281726}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &3093407653909830820
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1467268788090281726}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 0
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: "2 \uCE5C\uAD6C \uCC3E\uAE30"
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: a5bc77ef7ce7c7e4d85ac578cfcd6646, type: 2}
|
||||
m_sharedMaterial: {fileID: 8207275579423446409, guid: a5bc77ef7ce7c7e4d85ac578cfcd6646, type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontColor: {r: 0.12, g: 0.2, b: 0.31, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_fontColorGradientPreset: {fileID: 0}
|
||||
m_spriteAsset: {fileID: 0}
|
||||
m_tintAllSprites: 0
|
||||
m_StyleSheet: {fileID: 0}
|
||||
m_TextStyleHashCode: 0
|
||||
m_overrideHtmlColors: 0
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 34
|
||||
m_fontSizeBase: 34
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 72
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 2
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 0
|
||||
m_ActiveFontFeatures: 6e72656bu
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
m_horizontalMapping: 0
|
||||
m_verticalMapping: 0
|
||||
m_uvLineOffset: 0
|
||||
m_geometrySortingOrder: 0
|
||||
m_IsTextObjectScaleStatic: 0
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!1 &1734378736986617752
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -972,7 +1370,6 @@ RectTransform:
|
||||
- {fileID: 3194082258746179425}
|
||||
- {fileID: 4532829656532669174}
|
||||
- {fileID: 5317872834923726274}
|
||||
- {fileID: 983150840242648897}
|
||||
m_Father: {fileID: 3999613752604275916}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
@@ -1136,6 +1533,127 @@ Transform:
|
||||
- {fileID: 4847436933123184085}
|
||||
m_Father: {fileID: 6696913924934804788}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &3130945442339227461
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 5090946158136759876}
|
||||
- component: {fileID: 9219190355672231004}
|
||||
- component: {fileID: 665966907793119626}
|
||||
- component: {fileID: 4435257573578294946}
|
||||
m_Layer: 0
|
||||
m_Name: ReturnButton
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &5090946158136759876
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3130945442339227461}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 4450401086215199481}
|
||||
m_Father: {fileID: 2530971626885568150}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.86, y: 0.2}
|
||||
m_AnchorMax: {x: 0.86, y: 0.2}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 240, y: 62}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &9219190355672231004
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3130945442339227461}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &665966907793119626
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3130945442339227461}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 0.8, b: 0.3, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 0}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!114 &4435257573578294946
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3130945442339227461}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button
|
||||
m_Navigation:
|
||||
m_Mode: 3
|
||||
m_WrapAround: 0
|
||||
m_SelectOnUp: {fileID: 0}
|
||||
m_SelectOnDown: {fileID: 0}
|
||||
m_SelectOnLeft: {fileID: 0}
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
|
||||
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
|
||||
m_ColorMultiplier: 1
|
||||
m_FadeDuration: 0.1
|
||||
m_SpriteState:
|
||||
m_HighlightedSprite: {fileID: 0}
|
||||
m_PressedSprite: {fileID: 0}
|
||||
m_SelectedSprite: {fileID: 0}
|
||||
m_DisabledSprite: {fileID: 0}
|
||||
m_AnimationTriggers:
|
||||
m_NormalTrigger: Normal
|
||||
m_HighlightedTrigger: Highlighted
|
||||
m_PressedTrigger: Pressed
|
||||
m_SelectedTrigger: Selected
|
||||
m_DisabledTrigger: Disabled
|
||||
m_Interactable: 1
|
||||
m_TargetGraphic: {fileID: 665966907793119626}
|
||||
m_OnClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
--- !u!1 &3169550458985797308
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -2716,6 +3234,143 @@ MonoBehaviour:
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!1 &5066269170581119357
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 4450401086215199481}
|
||||
- component: {fileID: 6172297422653075404}
|
||||
- component: {fileID: 1778027915619506758}
|
||||
m_Layer: 0
|
||||
m_Name: Label
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &4450401086215199481
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5066269170581119357}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 5090946158136759876}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 240, y: 62}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &6172297422653075404
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5066269170581119357}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &1778027915619506758
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5066269170581119357}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 0
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: "\uCE5C\uAD6C \uBC18\uB0A9"
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: a5bc77ef7ce7c7e4d85ac578cfcd6646, type: 2}
|
||||
m_sharedMaterial: {fileID: 8207275579423446409, guid: a5bc77ef7ce7c7e4d85ac578cfcd6646, type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontColor: {r: 0.12, g: 0.2, b: 0.31, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_fontColorGradientPreset: {fileID: 0}
|
||||
m_spriteAsset: {fileID: 0}
|
||||
m_tintAllSprites: 0
|
||||
m_StyleSheet: {fileID: 0}
|
||||
m_TextStyleHashCode: 0
|
||||
m_overrideHtmlColors: 0
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 28
|
||||
m_fontSizeBase: 28
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 72
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 2
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 0
|
||||
m_ActiveFontFeatures: 6e72656bu
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
m_horizontalMapping: 0
|
||||
m_verticalMapping: 0
|
||||
m_uvLineOffset: 0
|
||||
m_geometrySortingOrder: 0
|
||||
m_IsTextObjectScaleStatic: 0
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!1 &5310382666504661114
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -3256,6 +3911,7 @@ RectTransform:
|
||||
- {fileID: 3237793279449092937}
|
||||
- {fileID: 2361705476825752074}
|
||||
- {fileID: 2530971626885568150}
|
||||
- {fileID: 983150840242648897}
|
||||
m_Father: {fileID: 4274269384354727046}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
@@ -3958,7 +4614,7 @@ RectTransform:
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 3237793279449092937}
|
||||
m_Father: {fileID: 3999613752604275916}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.07}
|
||||
m_AnchorMax: {x: 0.5, y: 0.07}
|
||||
@@ -4065,6 +4721,127 @@ MonoBehaviour:
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!1 &7061760651837010790
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 2329883891830714780}
|
||||
- component: {fileID: 5573057991149697080}
|
||||
- component: {fileID: 4086799282263640400}
|
||||
- component: {fileID: 3013192724825968427}
|
||||
m_Layer: 0
|
||||
m_Name: SpawnButton
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &2329883891830714780
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7061760651837010790}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 5049519322727858860}
|
||||
m_Father: {fileID: 2530971626885568150}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.86, y: 0.3}
|
||||
m_AnchorMax: {x: 0.86, y: 0.3}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 240, y: 62}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &5573057991149697080
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7061760651837010790}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &4086799282263640400
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7061760651837010790}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 0.8, b: 0.3, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 0}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!114 &3013192724825968427
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7061760651837010790}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button
|
||||
m_Navigation:
|
||||
m_Mode: 3
|
||||
m_WrapAround: 0
|
||||
m_SelectOnUp: {fileID: 0}
|
||||
m_SelectOnDown: {fileID: 0}
|
||||
m_SelectOnLeft: {fileID: 0}
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
|
||||
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
|
||||
m_ColorMultiplier: 1
|
||||
m_FadeDuration: 0.1
|
||||
m_SpriteState:
|
||||
m_HighlightedSprite: {fileID: 0}
|
||||
m_PressedSprite: {fileID: 0}
|
||||
m_SelectedSprite: {fileID: 0}
|
||||
m_DisabledSprite: {fileID: 0}
|
||||
m_AnimationTriggers:
|
||||
m_NormalTrigger: Normal
|
||||
m_HighlightedTrigger: Highlighted
|
||||
m_PressedTrigger: Pressed
|
||||
m_SelectedTrigger: Selected
|
||||
m_DisabledTrigger: Disabled
|
||||
m_Interactable: 1
|
||||
m_TargetGraphic: {fileID: 4086799282263640400}
|
||||
m_OnClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
--- !u!1 &7158186905691569963
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -4120,6 +4897,7 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier: MagnetismHeroes.UnityRuntime::MagnetismHeroes.UnityRuntime.Bootstrap.GameApp
|
||||
_stages:
|
||||
- {fileID: 11400000, guid: 1bd91d4fcb826e54e861dc2275fcbdee, type: 2}
|
||||
- {fileID: 11400000, guid: 74d68861704a8b541bb8ac1d37613620, type: 2}
|
||||
- {fileID: 11400000, guid: 6e1df3aeda3cea54b8441c528e7f503a, type: 2}
|
||||
- {fileID: 11400000, guid: 65683957e8f990649a840e8648fd470a, type: 2}
|
||||
- {fileID: 11400000, guid: a8fae6d8bb4a24246b5cfc1e89d7075f, type: 2}
|
||||
@@ -4134,6 +4912,9 @@ MonoBehaviour:
|
||||
_playButton: {fileID: 8869707931174415660}
|
||||
_continueButton: {fileID: 428650618540010171}
|
||||
_stageOneButton: {fileID: 227025040056778259}
|
||||
_stageTwoButton: {fileID: 4131456494785851528}
|
||||
_spawnButton: {fileID: 3013192724825968427}
|
||||
_returnButton: {fileID: 4435257573578294946}
|
||||
_homeButton: {fileID: 1767103786293493516}
|
||||
_selectionHomeButton: {fileID: 5673944335554824576}
|
||||
_compositionButtons:
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace MagnetismHeroes.UnityRuntime.Presentation
|
||||
{ _guide = Find(id); if (!_playingCount) { _source.Stop(); Advance(); } }
|
||||
public void Clear()
|
||||
{ _counts.Clear(); _guide = null; _playingCount = false; _source.Stop(); _source.clip = null; }
|
||||
public void CancelGuide()
|
||||
{ _guide = null; if (!_playingCount) { _source.Stop(); _source.clip = null; } }
|
||||
private AudioClip Find(string id) { foreach (var voice in _clips) if (voice.id == id) return voice.clip; return null; }
|
||||
private void Advance()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 25d91ab5f7c987840910f7a3683c8d3f, type: 3}
|
||||
m_Name: Stage02
|
||||
m_EditorClassIdentifier: MagnetismHeroes.UnityRuntime::MagnetismHeroes.UnityRuntime.StageAssets.StageAsset
|
||||
_stageId: 2
|
||||
_kind: 0
|
||||
_initialGroups: 0100000001000000i
|
||||
_targetGroups: 02000000i
|
||||
_ballPositions:
|
||||
- {x: -1.2, y: 0.8, z: 0}
|
||||
- {x: 1.2, y: 0.8, z: 0}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74d68861704a8b541bb8ac1d37613620
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# 합체·분리 대표 단계
|
||||
|
||||
실제 `Stage06/10/14.asset`은 각각 `[1,1] → [2]`, `[2] → [1,1]`, `[3,1] → [2,2]`다. GameApp 프리팹의 선택 버튼은 저장된 진행에 따른 기존 순차 해금을 사용한다. 새 저장에서는 6·10·14가 잠겨 있다. 중간 카탈로그에는 1·6·10·14만 있으며 전체 30단계는 후속 제작 범위다.
|
||||
실제 `Stage06/10/14.asset`은 각각 `[1,1] → [2]`, `[2] → [1,1]`, `[3,1] → [2,2]`다. GameApp 프리팹의 선택 버튼은 저장된 진행에 따른 기존 순차 해금을 사용한다. 새 저장에서는 6·10·14가 잠겨 있다. 현재 중간 카탈로그는 1·2·6·10·14이며 전체 30단계는 후속 제작 범위다.
|
||||
|
||||
친구를 끌어 다른 묶음에 가까이 놓으면 가장 가까운 두 구의 접촉 위치에 붙는다. 대상 구의 초록색 미리보기는 저장하지 않는다. 각 구의 크기와 ID는 유지하고, 각 묶음에 숫자·대표 얼굴·얇은 리본 망토 하나씩을 표시한다. 숫자는 카메라 위쪽으로 묶음 전체 구의 투영 경계보다 높게 놓는다.
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# 저장·진행·자유놀이
|
||||
|
||||
JSON v1은 앱 전용 `progress.json`에 확정 상태만 기록한다. 각 단계의 미완료 상태는 독립적으로 하나씩 보관하고, 최근 자유놀이는 전역 하나다. 완료 기록은 누적되며, 재도전 미완료와 같은 단계의 완료 기록이 함께 존재할 수 있다. 성공 시 완료 추가·해당 미완료 제거·최근 자유놀이 교체가 한 번의 저장 호출로 일어난다. 자동으로 다음 단계로 이동하지 않는다.
|
||||
|
||||
카탈로그는 현재 1·2·6·10·14다. 2번은 실제 `[1,1] → [2]` 계수 퍼즐이며 1번 완료 뒤 해금된다. 6·10·14는 정상 순차 잠금 규칙을 유지한다. 전체 30단계와 첫 시범은 후속 콘텐츠 범위다. `HasSeenTutorial` / `MarkTutorialSeen`는 `merge`·`split`만 한 번씩 저장하며, 이 단계에서 시범 자체를 추가하지 않는다.
|
||||
|
||||
성공한 구의 ID·위치·소속·카메라를 유지한 자유놀이에서 합체·분리·이동한다. **하나 꺼내기**는 현재 ID와 충돌하지 않는 구 하나를 기존과 같은 크기로 만든다. 화면 안에서 친구·배경과 겹치지 않는 공간을 검사하며 구 합계 10에서 추가를 거부한다. **친구 반납**은 탭한 친구의 모든 구를 반납한다. 일부만 반납하려면 먼저 분리한다. 빈 보드도 저장하고 다시 꺼낼 수 있다. 자유놀이 조작은 완료 기록을 취소하지 않는다.
|
||||
|
||||
확정 이동·합체·분리·계수·공급·반납, 카메라 조작 종료, 화면 이동, 일시정지에서 저장한다. 미확정 드래그와 선택·접촉 표시를 저장하지 않는다. 저장 실패는 현재 화면에 짧은 한국어 안내를 보여주고 실제 파일 오류를 개발 로그에 남긴다. 다음 저장 성공 시 안내를 비운다. 복원은 Home에서 시작하며 이전 계수나 성공 음성을 반복하지 않는다. 같은 보드의 자유놀이 조작 중에는 마지막 숫자 대기열을 보존하고 화면 이동·일시정지는 음성을 정리한다.
|
||||
|
||||
임시 파일을 디스크에 flush한 뒤 primary를 교체한다. 유효 primary를 backup으로 보존하며 불완전 `.tmp`는 읽지 않는다. 손상 primary는 유효 backup 전체로 복구한다. 다음 저장 전에 손상 원본을 `.damaged-*`로 복사하며, 손상 primary를 유효 backup 위에 덮어쓰지 않는다. 둘 다 손상되면 정상 초기 상태와 안내를 표시하고 원본 증거를 보존한다.
|
||||
|
||||
검증 명령:
|
||||
|
||||
```powershell
|
||||
pwsh -NoProfile -File tools/verify.ps1 -Phase EditMode -Milestone Persistence
|
||||
pwsh -NoProfile -File tools/verify.ps1 -Phase PlayMode -Milestone Persistence
|
||||
pwsh -NoProfile -File tools/verify.ps1 -Phase AndroidBuild -Milestone Persistence
|
||||
pwsh -NoProfile -File tools/verify.ps1 -Phase AndroidSmoke -Milestone Persistence
|
||||
```
|
||||
|
||||
`artifacts/verification/Persistence/`의 XML, 실제 명령과 종료 코드, APK SHA256, AVD serial, PNG, JSON 전후 쌍과 런타임 로그가 근거다. `red/`는 잠긴 임시 파일과 저장 안내·공급 UI 부재의 실제 실패 증거다. Android 검사는 실제 터치로 1/2 양수 부분 계수를 만들고 일시정지·force-stop·재시작한다. test-only 완료 진행 fixture는 대표 6 접근에만 사용하며 부분 계수 ID·위치를 주입하지 않는다. 실제 이동한 6번 미완료, 실제 완료한 1번 자유놀이와 번갈아 전체 필드를 비교한다. 공급 10개 제한·친구 전체 반납·빈 보드 재공급·쓰기 실패·backup 전체 복구·두 파일 손상을 검사한다. 에뮬레이터 검사는 실제 모바일 하드웨어 성능 판정이 아니다.
|
||||
|
||||
Stage02의 `count_02`는 이미 연결된 초안 음성을 사용하며 사람 청취 승인은 별도다. 승인된 `count_01`·`guide_collect`·`guide_count` WAV를 변경하지 않는다. SFX/BGM은 [요청표](audio-requests.md)를 유지하며 사용자 요청대로 나중에 추가한다.
|
||||
@@ -0,0 +1,182 @@
|
||||
param([Parameter(Mandatory)][string]$ApkPath,[Parameter(Mandatory)][string]$EvidenceDir,[string]$Serial='emulator-5554')
|
||||
$ErrorActionPreference='Stop'
|
||||
$EvidenceDir=(Resolve-Path -LiteralPath $EvidenceDir).Path
|
||||
$adb=Join-Path $env:LOCALAPPDATA 'Android/Sdk/platform-tools/adb.exe'
|
||||
$package='com.magnetismheroes.game'; $activity="$package/com.unity3d.player.UnityPlayerGameActivity"
|
||||
$remote="/sdcard/Android/data/$package/files/progress.json"
|
||||
$checks=[Collections.Generic.List[object]]::new(); $commands=[Collections.Generic.List[object]]::new()
|
||||
$result=[ordered]@{milestone='Persistence';sourceCommit=(& git rev-parse HEAD);sourceDirty=[bool](& git status --porcelain);serial=$Serial;apkSha256=(Get-FileHash -LiteralPath $ApkPath -Algorithm SHA256).Hash;status='failed'}
|
||||
$physicalInput=$false
|
||||
function Adb([string[]]$Arguments) {
|
||||
$output=& $adb -s $Serial @Arguments 2>&1; $code=$LASTEXITCODE
|
||||
$commands.Add(@{command=('adb -s '+$Serial+' '+($Arguments -join ' '));exitCode=$code;output=($output -join "`n")})
|
||||
if ($code -ne 0) { throw "ADB exit $code : $($Arguments -join ' ')" }; return ($output -join "`n").Trim()
|
||||
}
|
||||
function Check([string]$Id,[bool]$Passed,[string]$Actual) {
|
||||
$checks.Add(@{id=$Id;passed=$Passed;actual=$Actual}); if (!$Passed) { throw "Persistence check failed: $Id ($Actual)" }
|
||||
}
|
||||
function Step([string]$Name,[string[]]$Arguments=@(),[int]$Delay=500) {
|
||||
& (Join-Path $PSScriptRoot 'observe.ps1') -Step $Name -AdbArguments $Arguments -Serial $Serial -EvidenceDir $EvidenceDir -DelayMilliseconds $Delay
|
||||
if ($Arguments.Count -gt 1 -and $Arguments[1] -eq 'input') { $script:physicalInput=$false }
|
||||
}
|
||||
function Save([string]$Name) { Adb @('pull',$remote,(Join-Path $EvidenceDir "$Name.json")) | Out-Null; return Get-Content -LiteralPath (Join-Path $EvidenceDir "$Name.json") -Raw | ConvertFrom-Json }
|
||||
function Same($First,$Second) { return ($First | ConvertTo-Json -Depth 12 -Compress) -eq ($Second | ConvertTo-Json -Depth 12 -Compress) }
|
||||
function Restart([string]$Name) {
|
||||
Adb @('shell','input','keyevent','3') | Out-Null; Start-Sleep -Milliseconds 700
|
||||
Adb @('shell','am','force-stop',$package) | Out-Null
|
||||
Step "$Name-home" @('shell','am','start','-W','-n',$activity) 6500
|
||||
}
|
||||
function Touch([int[]]$Contacts) {
|
||||
if (!$script:physicalInput) {
|
||||
& (Join-Path $PSScriptRoot 'send-composition-touch.ps1') -Contacts @(30,400,999,100)
|
||||
Start-Sleep -Milliseconds 100
|
||||
& (Join-Path $PSScriptRoot 'send-composition-touch.ps1') -Contacts @(30,400,999,0)
|
||||
Start-Sleep -Milliseconds 100; $script:physicalInput=$true
|
||||
$commands.Add(@{command='EmulatorController.sendTouch warmup 30,400,999 down/up';exitCode=0})
|
||||
}
|
||||
& (Join-Path $PSScriptRoot 'send-composition-touch.ps1') -Contacts $Contacts
|
||||
$commands.Add(@{command=('EmulatorController.sendTouch contacts='+($Contacts -join ','));exitCode=0})
|
||||
}
|
||||
function BallPoint($Board,[string]$Id) {
|
||||
$ball=@($Board.balls | Where-Object id -eq $Id)[0]; $camera=$Board.camera
|
||||
$rotation=[Numerics.Quaternion]::new($camera.rotation.x,$camera.rotation.y,$camera.rotation.z,$camera.rotation.w)
|
||||
$origin=[Numerics.Vector3]::new($camera.target.x,$camera.target.y,$camera.target.z)+[Numerics.Vector3]::Transform([Numerics.Vector3]::new(0,0,-$camera.distance),$rotation)
|
||||
$local=[Numerics.Vector3]::Transform([Numerics.Vector3]::new($ball.position.x,$ball.position.y,$ball.position.z)-$origin,[Numerics.Quaternion]::Inverse($rotation))
|
||||
$scale=540/([Math]::Tan(23*[Math]::PI/180)*$local.Z)
|
||||
return @([int][Math]::Round(1200+$local.X*$scale),[int][Math]::Round(540-$local.Y*$scale))
|
||||
}
|
||||
function TapBall([string]$Name,$Board,[string]$Id) { $point=BallPoint $Board $Id; Step $Name @('shell','input','tap',"$($point[0])","$($point[1])") }
|
||||
function SelectStage([string]$Name,[int]$Id) {
|
||||
Step "$Name-selection" @('shell','input','tap','1200','616')
|
||||
$point=switch ($Id) { 1 {@(1200,562)} 2 {@(1200,691)} 6 {@(480,562)} 14 {@(1200,389)} default {throw 'Unsupported test stage'} }
|
||||
Step "$Name-stage" @('shell','input','tap',"$($point[0])","$($point[1])")
|
||||
}
|
||||
function RecordAudio([string]$Name,[int]$Seconds) {
|
||||
$arguments=@('-NoProfile','-File',(Join-Path $PSScriptRoot 'record-audio.ps1'),'-OutputPath',(Join-Path $EvidenceDir "$Name.wav"),'-Seconds',"$Seconds")
|
||||
$process=Start-Process pwsh -ArgumentList ($arguments | ForEach-Object {'"'+$_+'"'}) -WindowStyle Hidden -RedirectStandardOutput (Join-Path $EvidenceDir "$Name.stdout") -RedirectStandardError (Join-Path $EvidenceDir "$Name.stderr") -PassThru
|
||||
Start-Sleep -Seconds 2; return $process
|
||||
}
|
||||
try {
|
||||
Check 'clean-source' (!$result.sourceDirty) $result.sourceCommit
|
||||
$build=Get-Content -LiteralPath (Join-Path $EvidenceDir 'androidbuild-evidence.json') -Raw | ConvertFrom-Json
|
||||
Check 'build-provenance' ($build.status -eq 'passed' -and $build.exitCode -eq 0 -and !$build.sourceDirty -and !$build.sourceDirtyEnd -and $build.sourceCommit -eq $result.sourceCommit -and $build.sourceCommitEnd -eq $result.sourceCommit -and $build.apkSha256 -eq $result.apkSha256) $result.apkSha256
|
||||
Adb @('install','-r',$ApkPath) | Out-Null; Adb @('logcat','-c') | Out-Null; Adb @('shell','pm','clear',$package) | Out-Null
|
||||
$result.device=@{model=(Adb @('shell','getprop','ro.product.model'));api=(Adb @('shell','getprop','ro.build.version.sdk'));abi=(Adb @('shell','getprop','ro.product.cpu.abilist'));bridge=(Adb @('shell','getprop','ro.dalvik.vm.native.bridge'))}
|
||||
Step 'initial-home' @('shell','am','start','-W','-n',$activity) 6500
|
||||
Step 'initial-selection' @('shell','input','tap','1200','616'); Step 'initial-locked-two' @('shell','input','tap','1200','691')
|
||||
$locked=Save 'locked-two'; Check 'new-save-only-one-unlocked' ($locked.puzzleStates.Count -eq 0 -and $locked.completedStageIds.Count -eq 0) 'Stage02 tap creates no puzzle before Stage01 completion'
|
||||
Step 'one-start' @('shell','input','tap','1200','562'); Step 'one-collect' @('shell','input','swipe','1200','510','1280','510','650')
|
||||
$one=Save 'one-collected'; TapBall 'one-count-success' $one.puzzleStates[0] $one.puzzleStates[0].balls[0].id
|
||||
$free=Save 'one-free'; Check 'one-completion-atomic' ($free.completedStageIds.Count -eq 1 -and $free.completedStageIds[0] -eq 1 -and $free.puzzleStates.Count -eq 0 -and $free.latestFreePlay.sourceStageId -eq 1) 'Completed1, removed puzzle1, latestFree1 in one file'
|
||||
Step 'one-home' @('shell','input','tap','312','108'); SelectStage 'two' 2
|
||||
Step 'two-merge-collect' @('shell','input','swipe','1009','510','1291','510','650')
|
||||
$two=Save 'two-collected'; $board=@($two.puzzleStates | Where-Object stageId -eq 2)[0]
|
||||
Check 'two-real-collection' ($board.groups.Count -eq 1 -and $board.collectedBallIds.Count -eq 2 -and $board.countedBallIds.Count -eq 0) 'Actual confirmed drag merged both balls and collected both'
|
||||
TapBall 'two-count-one-of-two' $board $board.balls[0].id
|
||||
Step 'two-orbit' @('shell','input','swipe','2100','440','2180','480','650')
|
||||
Touch @(1700,400,501,100,1900,400,502,100); Start-Sleep -Milliseconds 150
|
||||
Touch @(1700,400,501,100,1920,400,502,100); Start-Sleep -Milliseconds 150
|
||||
Touch @(1700,400,501,0,1920,400,502,0); Step 'two-camera-confirmed'
|
||||
$partial=Save 'two-partial-before-pause'; $partialBoard=@($partial.puzzleStates | Where-Object stageId -eq 2)[0]
|
||||
Check 'positive-partial-count-real-tap' ($partialBoard.countedBallIds.Count -eq 1 -and $partialBoard.balls.Count -eq 2 -and $partialBoard.collectedBallIds.Count -eq 2 -and $partial.completedStageIds -notcontains 2) 'Actual tap saved exactly1/2 unfinished'
|
||||
Check 'nonidentity-camera-and-zoom' ([Math]::Abs($partialBoard.camera.rotation.w-1) -gt .001 -and [Math]::Abs($partialBoard.camera.distance-8) -gt .01) ($partialBoard.camera | ConvertTo-Json -Compress)
|
||||
Restart 'two-pause-force-stop'; $restoreAudio=RecordAudio 'audio-partial-restore' 8; SelectStage 'two-restored' 2
|
||||
$restored=Save 'two-partial-after-restart'; Check 'positive-partial-count-camera-restored' (Same $partialBoard @($restored.puzzleStates | Where-Object stageId -eq 2)[0]) 'IDs positions membership collected counted target rotation distance all match'
|
||||
Check 'restore-audio-recorder-finished' ($restoreAudio.WaitForExit(30000)) 'Actual emulator PCM capture finished'
|
||||
$restoreAudio.Refresh(); Check 'restore-audio-recorder-exit' ($restoreAudio.ExitCode -eq 0) "$($restoreAudio.ExitCode)"
|
||||
$wave=[IO.File]::ReadAllBytes((Join-Path $EvidenceDir 'audio-partial-restore.wav')); $sum=0.0; $samples=0
|
||||
for ($index=44; $index+1 -lt $wave.Length; $index+=2) { $sample=[BitConverter]::ToInt16($wave,$index); $sum+=[double]$sample*$sample; $samples++ }
|
||||
$rms=if ($samples -gt 0) {[Math]::Sqrt($sum/$samples)} else {[double]::PositiveInfinity}
|
||||
Check 'restore-no-historical-count-or-guide-audio' ($samples -gt 0 -and $rms -lt 32) "Actual restore PCM rms=$rms samples=$samples"
|
||||
Step 'two-home' @('shell','input','tap','312','108'); Adb @('shell','am','force-stop',$package) | Out-Null
|
||||
# Test-only completion fixture opens representative6; it retains the real counted puzzle and real Free1.
|
||||
$fixture=Save 'before-unlock-fixture'; $fixture.completedStageIds=@(1..5)
|
||||
$fixturePath=Join-Path $EvidenceDir 'fixture-completion-access-six.json'; $fixture | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $fixturePath -Encoding utf8
|
||||
Adb @('push',$fixturePath,$remote) | Out-Null; Step 'six-access-home' @('shell','am','start','-W','-n',$activity) 6500; SelectStage 'six' 6
|
||||
Step 'six-real-move-unfinished' @('shell','input','swipe','1009','510','930','560','650')
|
||||
$six=Save 'two-unfinished-before'; Check 'two-independent-unfinished' ($six.puzzleStates.Count -eq 2 -and (Same $partialBoard @($six.puzzleStates | Where-Object stageId -eq 2)[0]) -and @($six.puzzleStates | Where-Object stageId -eq 6)[0].groups.Count -eq 2) 'Stage02 partially counted and Stage06 actually moved coexist'
|
||||
Step 'six-home' @('shell','input','tap','312','108'); Step 'free-continue' @('shell','input','tap','1200','810')
|
||||
$freeBefore=Save 'free-before-move'; $point=BallPoint $freeBefore.latestFreePlay $freeBefore.latestFreePlay.balls[0].id
|
||||
Step 'free-actual-move' @('shell','input','swipe',"$($point[0])","$($point[1])","$($point[0]+45)","$($point[1]+20)",'650')
|
||||
$freeMoved=Save 'free-moved-before'; Check 'free-independent-of-two-puzzles' ($freeMoved.puzzleStates.Count -eq 2 -and (Same $six.puzzleStates $freeMoved.puzzleStates) -and !(Same $freeBefore.latestFreePlay.balls $freeMoved.latestFreePlay.balls)) 'Only latestFree board positions changed'
|
||||
Restart 'three-boards-restart'; Step 'free-restored' @('shell','input','tap','1200','810')
|
||||
Check 'latest-free-whole-restore' (Same $freeMoved (Save 'free-restored-after')) 'Two unfinished plus latestFree all restore'
|
||||
Step 'free-home' @('shell','input','tap','312','108'); SelectStage 'two-second-restore' 2
|
||||
Check 'two-second-independent-restore' (Same $partialBoard @((Save 'two-second-after').puzzleStates | Where-Object stageId -eq 2)[0]) 'Partial counting board remains unchanged through Free/six'
|
||||
Step 'two-second-home' @('shell','input','tap','312','108'); SelectStage 'six-second-restore' 6
|
||||
Check 'six-second-independent-restore' (Same @($six.puzzleStates | Where-Object stageId -eq 6)[0] @((Save 'six-second-after').puzzleStates | Where-Object stageId -eq 6)[0]) 'Moved composition board restores independently'
|
||||
Step 'six-second-home' @('shell','input','tap','312','108'); Step 'free-supply-continue' @('shell','input','tap','1200','810')
|
||||
for ($index=1; $index -le 9; $index++) { Step "free-spawn-$index" @('shell','input','tap','2064','756') }
|
||||
$full=Save 'free-ten'; Check 'actual-free-cap-ten' ($full.latestFreePlay.balls.Count -eq 10 -and @($full.latestFreePlay.balls.id | Sort-Object -Unique).Count -eq 10) 'Ten unique actual spawned balls'
|
||||
Step 'free-eleventh-refused' @('shell','input','tap','2064','756'); Check 'ten-add-refused-no-duplicate' (Same $full (Save 'free-cap-after')) 'Extra tap leaves exact saved file state'
|
||||
# Merge two actual singleton friends, then return the whole selected friend.
|
||||
$first=$full.latestFreePlay.balls[0].id; $second=$full.latestFreePlay.balls[1].id
|
||||
$start=BallPoint $full.latestFreePlay $first; $end=BallPoint $full.latestFreePlay $second
|
||||
Step 'free-merge-two' @('shell','input','swipe',"$($start[0])","$($start[1])","$($end[0]-70)","$($end[1])",'650')
|
||||
$merged=Save 'free-merged'; $whole=@($merged.latestFreePlay.groups | Where-Object {$_.ballIds.Count -gt 1})[0]
|
||||
Check 'actual-free-merge-for-whole-return' ($null -ne $whole) 'Real merge created a multi-ball friend'
|
||||
TapBall 'free-select-whole-friend' $merged.latestFreePlay $whole.ballIds[0]; Step 'free-whole-return' @('shell','input','tap','2064','864')
|
||||
$returned=Save 'free-whole-return-after'; Check 'whole-friend-return' ($returned.latestFreePlay.balls.Count -eq 10-$whole.ballIds.Count -and @($returned.latestFreePlay.balls | Where-Object {$whole.ballIds -contains $_.id}).Count -eq 0) 'Every selected friend member returned together'
|
||||
Step 'free-resupply-after-return' @('shell','input','tap','2064','756'); $supply=Save 'free-resupplied'
|
||||
Check 'resupply-after-return' ($supply.latestFreePlay.balls.Count -eq $returned.latestFreePlay.balls.Count+1) 'Capacity available again after return'
|
||||
while ($supply.latestFreePlay.balls.Count -gt 0) {
|
||||
TapBall "empty-select-$($supply.latestFreePlay.balls.Count)" $supply.latestFreePlay $supply.latestFreePlay.balls[0].id
|
||||
$beforeCount=$supply.latestFreePlay.balls.Count
|
||||
Step "empty-return-$beforeCount" @('shell','input','tap','2064','864'); $supply=Save "empty-after-$beforeCount"
|
||||
Check "empty-return-progress-$beforeCount" ($supply.latestFreePlay.balls.Count -lt $beforeCount) 'Selected friend actually returned'
|
||||
}
|
||||
Check 'empty-free-board-valid' ($supply.latestFreePlay.groups.Count -eq 0 -and $supply.completedStageIds -contains 1 -and $supply.puzzleStates.Count -eq 2) 'Empty board keeps completion and both unfinished'
|
||||
Step 'empty-respawn' @('shell','input','tap','2064','756'); $respawn=Save 'empty-respawn-after'; Check 'empty-respawn-one' ($respawn.latestFreePlay.balls.Count -eq 1) 'Supply works from empty board'
|
||||
# A directory at the temporary-file path causes a real FileStream failure, without touching valid primary/backup.
|
||||
$validBefore=Adb @('shell','sha256sum',$remote); Adb @('shell','mkdir',($remote+'.tmp')) | Out-Null
|
||||
$point=BallPoint $respawn.latestFreePlay $respawn.latestFreePlay.balls[0].id
|
||||
Step 'failed-write-visible-play-notice' @('shell','input','swipe',"$($point[0])","$($point[1])","$($point[0]+35)","$($point[1])",'650')
|
||||
Check 'failed-write-primary-bytes-preserved' ((Adb @('shell','sha256sum',$remote)) -eq $validBefore) 'Real write failure preserves primary SHA256'
|
||||
Step 'failed-camera-visible-notice' @('shell','input','swipe','2100','440','2170','475','650')
|
||||
Adb @('shell','input','keyevent','3') | Out-Null; Start-Sleep -Milliseconds 700
|
||||
Step 'failed-pause-resume-notice' @('shell','am','start','-W','-n',$activity) 1500
|
||||
Check 'failed-pause-primary-bytes-preserved' ((Adb @('shell','sha256sum',$remote)) -eq $validBefore) 'Camera and pause failures preserve same SHA256'
|
||||
Adb @('shell','rmdir',($remote+'.tmp')) | Out-Null
|
||||
Step 'save-recovered-notice-cleared' @('shell','input','tap','312','108'); $recovered=Save 'write-recovered'
|
||||
Check 'write-recovery-saves-current-whole-state' (!(Same $respawn.latestFreePlay $recovered.latestFreePlay) -and (Same $respawn.puzzleStates $recovered.puzzleStates)) 'Recovered write includes committed free move/camera, preserves both puzzles'
|
||||
Adb @('logcat','-d','-s','Unity','AndroidRuntime') | Set-Content -LiteralPath (Join-Path $EvidenceDir 'android-runtime.log') -Encoding utf8
|
||||
$runtime=Get-Content -LiteralPath (Join-Path $EvidenceDir 'android-runtime.log') -Raw
|
||||
Check 'actual-write-errors-logged' ($runtime -match '\[ProgressSave\].*(denied|directory|UnauthorizedAccess|Access)') 'Actual filesystem error in developer log'
|
||||
SelectStage 'two-final-count' 2; $secondCount=Save 'two-before-final-count'; $secondBoard=@($secondCount.puzzleStates | Where-Object stageId -eq 2)[0]
|
||||
$uncounted=@($secondBoard.balls | Where-Object {$secondBoard.countedBallIds -notcontains $_.id})[0]
|
||||
$countAudio=RecordAudio 'audio-stage-two-final-count' 8
|
||||
TapBall 'two-final-count-success' $secondBoard $uncounted.id
|
||||
$twoComplete=Save 'two-final-count-completed'; Check 'two-latest-free-replaces-one-preserves-six' ($twoComplete.latestFreePlay.sourceStageId -eq 2 -and $twoComplete.latestFreePlay.balls.Count -eq 2 -and $twoComplete.puzzleStates.Count -eq 1 -and $twoComplete.puzzleStates[0].stageId -eq 6 -and $twoComplete.completedStageIds -contains 1) 'Second actual count transitions to Free2 while retaining unfinished6 and completion1'
|
||||
TapBall 'two-free-select-during-final-number' $twoComplete.latestFreePlay $twoComplete.latestFreePlay.balls[0].id
|
||||
Step 'two-free-split-during-final-number' @('shell','input','tap','312','832'); Step 'two-free-cancel-during-final-number' @('shell','input','tap','312','832')
|
||||
Check 'count-two-audio-recorder-finished' ($countAudio.WaitForExit(30000)) 'Actual final count PCM capture finished'; $countAudio.Refresh()
|
||||
Check 'count-two-audio-recorder-exit' ($countAudio.ExitCode -eq 0) "$($countAudio.ExitCode)"
|
||||
$comparison=Join-Path $EvidenceDir 'audio-stage-two-count02-match.json'
|
||||
& pwsh -NoProfile -File (Join-Path $PSScriptRoot 'compare-audio.ps1') -ReferencePath 'Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_02.wav' -CapturedPath (Join-Path $EvidenceDir 'audio-stage-two-final-count.wav') -OutputPath $comparison
|
||||
$match=Get-Content -LiteralPath $comparison -Raw | ConvertFrom-Json; Check 'actual-count-two-plays-through-free-split-cancel' ($LASTEXITCODE -eq 0 -and $match.passed -and $match.normalizedCorrelation -ge .95) ($match | ConvertTo-Json -Compress)
|
||||
# Corrupt primary, restore complete valid backup, and preserve forensic bytes on next confirmed save.
|
||||
Adb @('shell','am','force-stop',$package) | Out-Null; Adb @('pull',($remote+'.bak'),(Join-Path $EvidenceDir 'valid-backup-before-damage.json')) | Out-Null
|
||||
$backup=Get-Content -LiteralPath (Join-Path $EvidenceDir 'valid-backup-before-damage.json') -Raw | ConvertFrom-Json
|
||||
$damagePath=Join-Path $EvidenceDir 'damaged-primary.txt'; [IO.File]::WriteAllText($damagePath,'damaged-primary-persistence')
|
||||
Adb @('push',$damagePath,$remote) | Out-Null; Step 'backup-recovery-home-notice' @('shell','am','start','-W','-n',$activity) 6500
|
||||
Check 'load-does-not-overwrite-damaged-primary' ((Adb @('shell','cat',$remote)) -eq 'damaged-primary-persistence') 'Damaged original remains before any new save'
|
||||
Step 'backup-restored-free' @('shell','input','tap','1200','810'); Check 'backup-whole-state-restored' (Same $backup (Save 'backup-restored-all')) 'Entire valid backup restored without partial primary load'
|
||||
$forensic=Adb @('shell','ls',($remote+'.damaged-*')); $result.forensicFiles=$forensic
|
||||
Check 'damaged-primary-forensic-preserved' ((Adb @('shell','cat',($remote+'.damaged-*'))) -match 'damaged-primary-persistence') $forensic
|
||||
Adb @('shell','am','force-stop',$package) | Out-Null
|
||||
[IO.File]::WriteAllText($damagePath,'damaged-both-primary'); Adb @('push',$damagePath,$remote) | Out-Null
|
||||
$badBackup=Join-Path $EvidenceDir 'damaged-backup.txt'; [IO.File]::WriteAllText($badBackup,'damaged-both-backup'); Adb @('push',$badBackup,($remote+'.bak')) | Out-Null
|
||||
Step 'both-damaged-initial-home-notice' @('shell','am','start','-W','-n',$activity) 6500
|
||||
Check 'both-damaged-originals-preserved-on-load' ((Adb @('shell','cat',$remote)) -eq 'damaged-both-primary' -and (Adb @('shell','cat',($remote+'.bak'))) -eq 'damaged-both-backup') 'Both original byte strings survive failed load'
|
||||
Step 'both-damaged-new-selection' @('shell','input','tap','1200','616'); $initial=Save 'both-damaged-new-initial'
|
||||
Check 'both-damage-normal-initial' ($initial.completedStageIds.Count -eq 0 -and $initial.puzzleStates.Count -eq 0 -and $null -eq $initial.latestFreePlay) 'No valid save starts clean initial state'
|
||||
Check 'both-damage-forensic-preserved' ((Adb @('shell','cat',($remote+'.damaged-*'))) -match 'damaged-both-primary' -and (Adb @('shell','cat',($remote+'.bak'))) -eq 'damaged-both-backup') 'Primary forensic and damaged backup retained'
|
||||
$result.sourceCommitEnd=(& git rev-parse HEAD); $result.sourceDirtyEnd=[bool](& git status --porcelain)
|
||||
Check 'clean-source-end' (!$result.sourceDirtyEnd -and $result.sourceCommitEnd -eq $result.sourceCommit) $result.sourceCommitEnd
|
||||
$result.status='passed'
|
||||
} catch { $result.error=$_.Exception.Message; Write-Host $result.error -ForegroundColor Red }
|
||||
finally {
|
||||
$result.checks=$checks.ToArray(); $result.commands=$commands.ToArray(); $result.finishedUtc=[DateTime]::UtcNow.ToString('o')
|
||||
$result | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $EvidenceDir 'android-persistence-evidence.json') -Encoding utf8
|
||||
}
|
||||
if ($result.status -ne 'passed') { exit 1 }; Write-Host "Persistence actual Android checks passed: $($checks.Count). PNG visual review remains explicit."; exit 0
|
||||
@@ -1,4 +1,8 @@
|
||||
param([Parameter(Mandatory)][string]$ApkPath, [string]$Milestone='FirstSlice', [Parameter(Mandatory)][string]$EvidenceDir, [string]$Serial='emulator-5554')
|
||||
if ($Milestone -eq 'Persistence') {
|
||||
& (Join-Path $PSScriptRoot 'exercise-persistence.ps1') -ApkPath $ApkPath -EvidenceDir $EvidenceDir -Serial $Serial
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
if ($Milestone -eq 'Composition') {
|
||||
& (Join-Path $PSScriptRoot 'exercise-composition.ps1') -ApkPath $ApkPath -EvidenceDir $EvidenceDir -Serial $Serial
|
||||
exit $LASTEXITCODE
|
||||
|
||||
Reference in New Issue
Block a user