Complete persistence boundaries and free-play supply with device verification

This commit is contained in:
NINI
2026-10-05 11:30:12 +09:00
parent a651e5ca7f
commit e84eb1a35e
21 changed files with 1786 additions and 95 deletions
@@ -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;
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: