Compare commits
66 Commits
58410281e0
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 23ddce925d | |||
| 2ea65df66f | |||
| 13822905d3 | |||
| b438ee266b | |||
| 70b7a5f553 | |||
| d5874240a5 | |||
| 71bf8fab57 | |||
| bb32fdae9d | |||
| d4128d713d | |||
| 3bfcac8a58 | |||
| 8bd6c426a8 | |||
| 2a59c4e8eb | |||
| 8728660288 | |||
| e7ae849f3d | |||
| 24f0347404 | |||
| db340b41ed | |||
| fa3da70923 | |||
| 2494459945 | |||
| e1a901ac15 | |||
| ff5d7e1b96 | |||
| 72638ab091 | |||
| 4184023769 | |||
| 4941dcab52 | |||
| 971a792bfd | |||
| f9ec2a8a51 | |||
| c9d81100fa | |||
| 53d5e8a2d5 | |||
| 891c7d0072 | |||
| 9850d2c539 | |||
| bc1ce03beb | |||
| 401fa6a1a6 | |||
| 14f89d1248 | |||
| 2382b4612e | |||
| 47dfb9ebed | |||
| 8601234e3c | |||
| 9ba70e2cd4 | |||
| ef547b050a | |||
| 4f39d621cd | |||
| 6e353a1281 | |||
| f31ebc6fd9 | |||
| 477bbcdda6 | |||
| a7439bd49b | |||
| 9a316cfd89 | |||
| 969bc92d46 | |||
| 32690a510e | |||
| 9544f4bce5 | |||
| a54ae6fb23 | |||
| 0d992d9dcc | |||
| 620c73cbd2 | |||
| 3346eb559b | |||
| ef03611628 | |||
| da169df7c7 | |||
| a906be69a0 | |||
| 83182e7f3e | |||
| b880cc196c | |||
| b4d88546a2 | |||
| 7e4bf1f5ea | |||
| 471a2b20c8 | |||
| 2d4b4a88e3 | |||
| 43b22d6788 | |||
| 7c786c5435 | |||
| 5185d94631 | |||
| 37010382fd | |||
| 80be80566a | |||
| 7d7efbe6b6 | |||
| 5ad1a237c8 |
+2
-1
@@ -1,4 +1,3 @@
|
||||
.dryforge/
|
||||
Library
|
||||
Temp
|
||||
Obj
|
||||
@@ -15,3 +14,5 @@ artifacts/
|
||||
*.slnx
|
||||
*.user
|
||||
__pycache__
|
||||
|
||||
.dryforge/
|
||||
|
||||
@@ -7,3 +7,5 @@
|
||||
합체·분리의 구 ID 집합 보존, 정확히 한 묶음 소속, 모든 비어 있지 않은 진부분집합 분리, 순서 없는 목표 다중집합을 검사한다. 계수는 수집·단일 묶음 조건과 중복 ID를 검사한다. 진행 수정에는 재플레이 완료 기록 유지, 두 미완료 슬롯과 한 자유 놀이, 성공의 단일 저장 갱신을 포함한다.
|
||||
|
||||
DTO lowerCamelCase는 파일 계약이다. 자유 놀이에 계수 필드를 추가하지 않는다. 저장 검증 수정은 누락·중복·비유한 위치·무효 quaternion·구 소속·잘못된 source와 unsupported 버전을 실제 사례로 확인한다. Tests/EditMode의 규칙·세션·저장 검사를 실행하며 결과를 화면 연결 검증으로 확대하지 않는다.
|
||||
|
||||
이어하기 준비는 Unity 판정 결과를 엔진 참조 없는 BoardRestore로 받는다. 기존 진행의 분리된 사본을 준비·검증하고 한 번 저장한 뒤에만 Board·Camera·Screen·진행을 교체한다. 위치 보정은 일반 이동의 수집 부작용을 호출하지 않으며 ID·묶음 순서·소속·수집·계수와 묶음 내 동일 이동을 검증한다. 유효 배치가 없는 경우의 초기화는 문제된 슬롯의 초기 정의만 허용한다. 자유 놀이 모드·source, 완료·해금·시범·다른 슬롯과 쓰기 실패의 기존 상태를 보존하는 정상·부정 대조를 포함한다.
|
||||
|
||||
@@ -72,6 +72,35 @@ namespace MagnetismHeroes.Core
|
||||
|
||||
public static BoardRules CreateFreePlay(FreePlayState state) => new BoardRules(state);
|
||||
|
||||
// Restore preparation changes positions without treating the correction as collection or play.
|
||||
public bool TryTranslateForRestore(IReadOnlyDictionary<string, Position3> translations, out BoardRules prepared)
|
||||
{
|
||||
prepared = null;
|
||||
if (translations == null || translations.Any(pair => !_groups.ContainsKey(pair.Key) || !pair.Value.IsValid)) return false;
|
||||
var balls = Balls;
|
||||
foreach (var group in _groups)
|
||||
{
|
||||
if (!translations.TryGetValue(group.Key, out var delta)) continue;
|
||||
foreach (var ball in balls.Where(ball => group.Value.Contains(ball.id)))
|
||||
{
|
||||
var point = ball.position;
|
||||
ball.position = new Position3(point.x + delta.x, point.y + delta.y, point.z + delta.z);
|
||||
if (!ball.position.IsValid) return false;
|
||||
}
|
||||
}
|
||||
if (Mode == BoardMode.FreePlay)
|
||||
{
|
||||
var state = ToFreePlayState(GameSession.DefaultCamera()); state.balls = balls;
|
||||
prepared = CreateFreePlay(state);
|
||||
}
|
||||
else
|
||||
{
|
||||
var state = ToPuzzleState(GameSession.DefaultCamera()); state.balls = balls;
|
||||
prepared = new BoardRules(_definition, state);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public RuleResult TryMoveGroup(string groupId, Position3 delta)
|
||||
{
|
||||
if (!ValidId(groupId) || !_groups.TryGetValue(groupId, out var members)) return RuleResult.Reject(RejectionReason.UnknownId);
|
||||
|
||||
@@ -6,6 +6,15 @@ namespace MagnetismHeroes.Core
|
||||
{
|
||||
public enum SessionScreen { Home, StageSelection, Puzzle, FreePlay }
|
||||
|
||||
public sealed class BoardRestore
|
||||
{
|
||||
public BoardRules Board { get; }
|
||||
public CameraState Camera { get; }
|
||||
public bool ResetToInitial { get; }
|
||||
public BoardRestore(BoardRules board, CameraState camera, bool resetToInitial = false)
|
||||
{ Board = board; Camera = camera; ResetToInitial = resetToInitial; }
|
||||
}
|
||||
|
||||
// Owns committed game state. A gesture preview never reaches this class.
|
||||
public sealed class GameSession
|
||||
{
|
||||
@@ -50,29 +59,106 @@ namespace MagnetismHeroes.Core
|
||||
public void ShowSelection() { SaveCurrent(); Screen = SessionScreen.StageSelection; Board = null; }
|
||||
public void ShowHome() { SaveCurrent(); Screen = SessionScreen.Home; Board = null; }
|
||||
|
||||
public bool StartStage(int stageId)
|
||||
public bool StartStage(int stageId, Func<BoardRules, CameraState, BoardRestore> prepare = null)
|
||||
{
|
||||
var definition = _definitions.FirstOrDefault(value => value.Id == stageId);
|
||||
if (definition == null || !IsUnlocked(stageId)) return false;
|
||||
SaveCurrent();
|
||||
var restored = _progress.puzzleStates.FirstOrDefault(state => state.stageId == stageId);
|
||||
Board = new BoardRules(definition, restored);
|
||||
Camera = restored == null ? DefaultCamera() : CopyCamera(restored.camera);
|
||||
Screen = SessionScreen.Puzzle;
|
||||
SaveCurrent();
|
||||
var next = CopyProgress(); SnapshotCurrent(next);
|
||||
var restored = next.puzzleStates.FirstOrDefault(state => state.stageId == stageId);
|
||||
return Enter(next, new BoardRules(definition, restored), restored == null ? DefaultCamera() : CopyCamera(restored.camera),
|
||||
SessionScreen.Puzzle, prepare);
|
||||
}
|
||||
|
||||
public bool ContinueFreePlay(Func<BoardRules, CameraState, BoardRestore> prepare = null)
|
||||
{
|
||||
if (_progress.latestFreePlay == null) return false;
|
||||
var next = CopyProgress(); SnapshotCurrent(next);
|
||||
return Enter(next, BoardRules.CreateFreePlay(next.latestFreePlay), CopyCamera(next.latestFreePlay.camera), SessionScreen.FreePlay, prepare);
|
||||
}
|
||||
|
||||
private bool Enter(ProgressSave next, BoardRules restored, CameraState camera, SessionScreen screen,
|
||||
Func<BoardRules, CameraState, BoardRestore> prepare)
|
||||
{
|
||||
var prepared = prepare == null ? new BoardRestore(restored, camera) : prepare(restored, camera);
|
||||
if (!ValidPreparation(restored, prepared)) return false;
|
||||
if (screen == SessionScreen.Puzzle)
|
||||
{
|
||||
var existing = next.puzzleStates.FirstOrDefault(state => state.stageId == restored.StageId);
|
||||
if (existing != null && !prepared.ResetToInitial)
|
||||
{ ApplyPreparedPositions(existing.balls, prepared.Board); existing.camera = CopyCamera(prepared.Camera); }
|
||||
else if (existing != null)
|
||||
next.puzzleStates = next.puzzleStates.Select(state => state.stageId == restored.StageId ? prepared.Board.ToPuzzleState(prepared.Camera) : state).ToArray();
|
||||
else next.puzzleStates = next.puzzleStates.Concat(new[] { prepared.Board.ToPuzzleState(prepared.Camera) }).OrderBy(state => state.stageId).ToArray();
|
||||
}
|
||||
else if (!prepared.ResetToInitial)
|
||||
{ ApplyPreparedPositions(next.latestFreePlay.balls, prepared.Board); next.latestFreePlay.camera = CopyCamera(prepared.Camera); }
|
||||
else next.latestFreePlay = prepared.Board.ToFreePlayState(prepared.Camera);
|
||||
if (!SaveValidator.TryValidateUpdate(_progress, next, _definitions, out _)) return false;
|
||||
var saved = _store.TrySave(next, out var error);
|
||||
LastSaveError = error; SaveFinished?.Invoke(error);
|
||||
if (!saved) return false;
|
||||
_progress = next; Board = prepared.Board; Camera = CopyCamera(prepared.Camera); Screen = screen;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ContinueFreePlay()
|
||||
private static void ApplyPreparedPositions(BallState[] balls, BoardRules prepared)
|
||||
{
|
||||
if (_progress.latestFreePlay == null) return false;
|
||||
SaveCurrent();
|
||||
Board = BoardRules.CreateFreePlay(_progress.latestFreePlay);
|
||||
Camera = CopyCamera(_progress.latestFreePlay.camera);
|
||||
Screen = SessionScreen.FreePlay;
|
||||
var positions = prepared.Balls.ToDictionary(ball => ball.id, ball => ball.position);
|
||||
foreach (var ball in balls) ball.position = positions[ball.id];
|
||||
}
|
||||
|
||||
private bool ValidPreparation(BoardRules restored, BoardRestore prepared)
|
||||
{
|
||||
if (prepared?.Board == null || !SaveValidator.ValidCamera(prepared.Camera)
|
||||
|| prepared.Board.StageId != restored.StageId || prepared.Board.Mode != restored.Mode) return false;
|
||||
var expected = restored;
|
||||
if (prepared.ResetToInitial)
|
||||
{
|
||||
expected = new BoardRules(_definitions.First(stage => stage.Id == restored.StageId));
|
||||
if (restored.Mode == BoardMode.FreePlay) expected = BoardRules.CreateFreePlay(expected.ToFreePlayState(DefaultCamera()));
|
||||
}
|
||||
if (!expected.CollectedBallIds.SequenceEqual(prepared.Board.CollectedBallIds)
|
||||
|| !expected.CountedBallIds.SequenceEqual(prepared.Board.CountedBallIds)) return false;
|
||||
var sourceGroups = expected.Groups; var groups = prepared.Board.Groups;
|
||||
if (sourceGroups.Length != groups.Length) return false;
|
||||
for (int index = 0; index < groups.Length; index++)
|
||||
if (sourceGroups[index].id != groups[index].id || !sourceGroups[index].ballIds.SequenceEqual(groups[index].ballIds)) return false;
|
||||
var source = expected.Balls.ToDictionary(ball => ball.id, ball => ball.position);
|
||||
var target = prepared.Board.Balls.ToDictionary(ball => ball.id, ball => ball.position);
|
||||
if (source.Count != target.Count || source.Keys.Except(target.Keys).Any()) return false;
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var first = group.ballIds[0]; var before = source[first]; var after = target[first];
|
||||
var delta = new Position3(after.x - before.x, after.y - before.y, after.z - before.z);
|
||||
if (prepared.ResetToInitial && (delta.x != 0 || delta.y != 0 || delta.z != 0)) return false;
|
||||
foreach (var id in group.ballIds)
|
||||
if (Math.Abs(target[id].x - source[id].x - delta.x) > .0001f
|
||||
|| Math.Abs(target[id].y - source[id].y - delta.y) > .0001f
|
||||
|| Math.Abs(target[id].z - source[id].z - delta.z) > .0001f) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private ProgressSave CopyProgress() => new ProgressSave {
|
||||
schemaVersion = _progress.schemaVersion, completedStageIds = (int[])_progress.completedStageIds.Clone(),
|
||||
seenTutorialIds = (string[])_progress.seenTutorialIds.Clone(),
|
||||
puzzleStates = _progress.puzzleStates.Select(state => new PuzzleState { stageId = state.stageId,
|
||||
balls = CopyBalls(state.balls), groups = CopyGroups(state.groups), camera = CopyCamera(state.camera),
|
||||
collectedBallIds = (string[])state.collectedBallIds.Clone(), countedBallIds = (string[])state.countedBallIds.Clone() }).ToArray(),
|
||||
latestFreePlay = _progress.latestFreePlay == null ? null : new FreePlayState { sourceStageId = _progress.latestFreePlay.sourceStageId,
|
||||
balls = CopyBalls(_progress.latestFreePlay.balls), groups = CopyGroups(_progress.latestFreePlay.groups), camera = CopyCamera(_progress.latestFreePlay.camera) } };
|
||||
private static BallState[] CopyBalls(BallState[] balls) => balls.Select(ball => new BallState { id = ball.id, position = ball.position }).ToArray();
|
||||
private static GroupState[] CopyGroups(GroupState[] groups) => groups.Select(group => new GroupState { id = group.id, ballIds = (string[])group.ballIds.Clone() }).ToArray();
|
||||
|
||||
private void SnapshotCurrent(ProgressSave progress)
|
||||
{
|
||||
if (Board == null) return;
|
||||
if (Screen == SessionScreen.Puzzle)
|
||||
progress.puzzleStates = progress.puzzleStates.Where(state => state.stageId != Board.StageId)
|
||||
.Concat(new[] { Board.ToPuzzleState(Camera) }).OrderBy(state => state.stageId).ToArray();
|
||||
else if (Screen == SessionScreen.FreePlay) progress.latestFreePlay = Board.ToFreePlayState(Camera);
|
||||
}
|
||||
|
||||
public RuleResult MoveGroup(string groupId, Position3 delta) => Commit(() => Board.TryMoveGroup(groupId, delta));
|
||||
public RuleResult Merge(string first, string second) => Commit(() => Board.TryMerge(first, second));
|
||||
public RuleResult MoveAndMerge(string groupId, Position3 delta, string targetGroupId) => Commit(() => Board.TryMoveAndMerge(groupId, delta, targetGroupId));
|
||||
@@ -115,13 +201,7 @@ namespace MagnetismHeroes.Core
|
||||
|
||||
public bool SaveCurrent()
|
||||
{
|
||||
if (Board != null)
|
||||
{
|
||||
if (Screen == SessionScreen.Puzzle)
|
||||
_progress.puzzleStates = _progress.puzzleStates.Where(state => state.stageId != Board.StageId)
|
||||
.Concat(new[] { Board.ToPuzzleState(Camera) }).OrderBy(state => state.stageId).ToArray();
|
||||
else if (Screen == SessionScreen.FreePlay) _progress.latestFreePlay = Board.ToFreePlayState(Camera);
|
||||
}
|
||||
SnapshotCurrent(_progress);
|
||||
var success = _store.TrySave(_progress, out var error);
|
||||
LastSaveError = error;
|
||||
SaveFinished?.Invoke(error);
|
||||
|
||||
@@ -6,4 +6,6 @@
|
||||
|
||||
씬·프리팹·ScriptableObject·메타데이터는 Editor API로 만들고 Undo/dirty/save 및 필수 참조를 처리한다. URP의 임시 생성 설정 변화는 원본과 정확히 일치하는 알려진 변환인지 확인한 경우만 정리한다. 모델 root 회전·아치 구멍의 MeshCollider·TMP static glyph와 승인 WAV GUID를 보존한다.
|
||||
|
||||
Android 빌드는 Main만 포함하고 ARM64 IL2CPP release APK를 만든다. 출력 경로는 `MAGNETISM_APK_PATH`를 사용한다. 개발 모델·캐시·QA 녹음 도구를 포함하지 않는다. 빌드 수정은 실제 AndroidBuild와 APK inspection, 표현·연결 변경은 Edit/Play와 실제 화면으로 검증한다. 파일 생성 성공을 설치·재생 성공으로 기록하지 않는다.
|
||||
Android 빌드는 Main만 포함하고 ARM64 IL2CPP release APK를 만든다. 출력 경로는 `MAGNETISM_APK_PATH`를 사용한다. 개발 모델 가중치·제작 캐시·QA 녹음 도구를 포함하지 않는다. 승인 메시에서 파생한 읽기 전용 복원 형상은 필요한 런타임 판정 자산이므로 이 제외 대상과 구분한다. 빌드 수정은 실제 AndroidBuild와 APK inspection, 표현·연결 변경은 Edit/Play와 실제 화면으로 검증한다. 파일 생성 성공을 설치·재생 성공으로 기록하지 않는다.
|
||||
|
||||
풍경 변경은 PlaygroundSceneryAssets의 대상 한정 경로로 지면·충돌 역할·30개 고정 배치만 갱신한다. 실제 import root를 보존하고 메시 바닥·접근 가능한 표면으로 접지한다. PlaygroundRestoreGeometryAssets는 승인 메시의 읽기 전용 정점·삼각형과 원본·meta 해시를 추출하며 기존 캐시의 자동 재생성을 거부한다. 초기 구·목표·기존 GUID와 승인 파일 보존, 모든 초기 구의 실제 첫 hit 선택·끌기, 두 가로 비율의 화면을 검증한다.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using MagnetismHeroes.UnityRuntime.Bootstrap;
|
||||
using Unity.Collections;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MagnetismHeroes.Editor
|
||||
{
|
||||
public static class PlaygroundRestoreGeometryAssets
|
||||
{
|
||||
public const string CachePath = "Assets/MagnetismHeroes/UnityRuntime/Bootstrap/PlaygroundRestoreGeometry.asset";
|
||||
public static void BindGameApp()
|
||||
{
|
||||
var cache = AssetDatabase.LoadAssetAtPath<PlaygroundRestoreGeometry>(CachePath);
|
||||
if (cache == null) throw new InvalidOperationException("Restore geometry must exist before binding the game prefab.");
|
||||
const string path = "Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GameApp.prefab";
|
||||
var prefab = PrefabUtility.LoadPrefabContents(path);
|
||||
try
|
||||
{
|
||||
var app = prefab.GetComponent<GameApp>();
|
||||
var data = new SerializedObject(app);
|
||||
var reference = data.FindProperty("_restoreGeometry");
|
||||
if (reference.objectReferenceValue == cache) return;
|
||||
Undo.RecordObject(app, "Bind playground restore geometry");
|
||||
reference.objectReferenceValue = cache; data.ApplyModifiedPropertiesWithoutUndo();
|
||||
EditorUtility.SetDirty(app); PrefabUtility.SaveAsPrefabAsset(prefab, path);
|
||||
Debug.Log("Bound the existing read-only restore geometry to the game prefab.");
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(prefab); }
|
||||
}
|
||||
public static void CreateCache()
|
||||
{
|
||||
if (AssetDatabase.LoadMainAssetAtPath(CachePath) != null) throw new InvalidOperationException("Restore geometry already exists; verify before replacing it.");
|
||||
var meshes = AssetDatabase.FindAssets("t:GameObject", new[] { "Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs" })
|
||||
.Select(AssetDatabase.GUIDToAssetPath).Select(AssetDatabase.LoadAssetAtPath<GameObject>)
|
||||
.SelectMany(prefab => prefab.GetComponentsInChildren<MeshCollider>(true)).Select(collider => collider.sharedMesh)
|
||||
.Where(mesh => mesh != null).Distinct().OrderBy(mesh => AssetDatabase.GetAssetPath(mesh)).ThenBy(mesh => mesh.name).ToArray();
|
||||
var entries = meshes.Select(mesh => {
|
||||
Vector3[] vertices; int[] triangles;
|
||||
using (var readOnly = MeshUtility.AcquireReadOnlyMeshData(mesh))
|
||||
{
|
||||
var data = readOnly[0]; using var buffer = new NativeArray<Vector3>(data.vertexCount, Allocator.Temp);
|
||||
data.GetVertices(buffer); vertices = buffer.ToArray();
|
||||
var indices = data.indexFormat == UnityEngine.Rendering.IndexFormat.UInt16
|
||||
? data.GetIndexData<ushort>().ToArray().Select(index => (int)index).ToArray()
|
||||
: data.GetIndexData<uint>().ToArray().Select(index => (int)index).ToArray();
|
||||
triangles = Enumerable.Range(0, data.subMeshCount).SelectMany(submesh => {
|
||||
var descriptor = data.GetSubMesh(submesh);
|
||||
return Enumerable.Range(descriptor.indexStart, descriptor.indexCount).Select(index => indices[index] + descriptor.baseVertex);
|
||||
}).ToArray();
|
||||
}
|
||||
string path = AssetDatabase.GetAssetPath(mesh);
|
||||
return new PlaygroundRestoreGeometry.Entry { mesh = mesh, vertices = vertices, triangles = triangles, bounds = mesh.bounds,
|
||||
geometrySha256 = PlaygroundRestoreGeometry.Hash(vertices, triangles), sourceSha256 = FileHash(path),
|
||||
sourceMetaSha256 = FileHash(path + ".meta"), sourceGuid = AssetDatabase.AssetPathToGUID(path) };
|
||||
}).ToArray();
|
||||
if (entries.Length == 0) throw new InvalidOperationException("No authored collider meshes found.");
|
||||
var cache = ScriptableObject.CreateInstance<PlaygroundRestoreGeometry>(); cache.SetEntries(entries);
|
||||
AssetDatabase.CreateAsset(cache, CachePath); EditorUtility.SetDirty(cache); AssetDatabase.SaveAssets();
|
||||
Debug.Log("Created read-only restore geometry for " + entries.Length + " unchanged collider meshes.");
|
||||
}
|
||||
private static string FileHash(string path)
|
||||
{ using var sha = SHA256.Create(); return BitConverter.ToString(sha.ComputeHash(File.ReadAllBytes(path))).Replace("-", "").ToLowerInvariant(); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 64260de0ba3968c43ba2b2f17991ebae
|
||||
@@ -0,0 +1,215 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MagnetismHeroes.UnityRuntime.StageAssets;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace MagnetismHeroes.Editor
|
||||
{
|
||||
// Targeted authoring only. Reuses approved model/material bytes and all existing GUIDs.
|
||||
public static class PlaygroundSceneryAssets
|
||||
{
|
||||
private const string Root = "Assets/MagnetismHeroes/UnityRuntime/Presentation/";
|
||||
private static readonly HashSet<string> Terrain = new HashSet<string> { "MeadowClearing", "GentleHill", "DistantRidge", "FarHorizon" };
|
||||
private static readonly HashSet<string> Solid = new HashSet<string> { "MeadowClearing", "GentleHill", "RoundCanopyTree", "SageCanopyTree", "TallWindTree", "RoundLimestone", "LowLimestone", "SlateBoulder", "OpenStoneArch", "MossStoneSteps" };
|
||||
|
||||
public static void Apply()
|
||||
{
|
||||
var paths = AssetDatabase.FindAssets("t:Prefab", new[] { Root + "Prefabs/Meadow" }).Select(AssetDatabase.GUIDToAssetPath).ToArray();
|
||||
foreach (string path in paths)
|
||||
{
|
||||
var prefab = PrefabUtility.LoadPrefabContents(path);
|
||||
try
|
||||
{
|
||||
if (Solid.Contains(prefab.name))
|
||||
foreach (var filter in prefab.GetComponentsInChildren<MeshFilter>(true))
|
||||
{
|
||||
var collider = filter.GetComponent<MeshCollider>();
|
||||
if (collider == null) collider = filter.gameObject.AddComponent<MeshCollider>();
|
||||
collider.sharedMesh = filter.sharedMesh; collider.convex = false;
|
||||
}
|
||||
if (Solid.Contains(prefab.name)) PrefabUtility.SaveAsPrefabAsset(prefab, path);
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(prefab); }
|
||||
}
|
||||
var props = paths.ToDictionary(path => Path.GetFileNameWithoutExtension(path), AssetDatabase.LoadAssetAtPath<GameObject>);
|
||||
var signatures = new List<string>();
|
||||
for (int id = 1; id <= 30; id++)
|
||||
{
|
||||
var stage = AssetDatabase.LoadAssetAtPath<StageAsset>($"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage{id:D2}.asset");
|
||||
var data = new SerializedObject(stage);
|
||||
string before = GameplaySignature(data);
|
||||
var placements = Compose(stage, props);
|
||||
Ground(placements);
|
||||
var decorations = data.FindProperty("_decorations"); decorations.arraySize = placements.Count;
|
||||
for (int i = 0; i < placements.Count; i++)
|
||||
{
|
||||
var item = decorations.GetArrayElementAtIndex(i); var placement = placements[i];
|
||||
item.FindPropertyRelative("prefab").objectReferenceValue = placement.prefab;
|
||||
item.FindPropertyRelative("position").vector3Value = placement.position;
|
||||
item.FindPropertyRelative("eulerAngles").vector3Value = placement.eulerAngles;
|
||||
item.FindPropertyRelative("scale").vector3Value = placement.scale;
|
||||
}
|
||||
// Compact aerial play volume; whole-sphere padding is applied by the movement layer.
|
||||
// The lowest visible mesh surface sits at approximately -.457 here.
|
||||
data.FindProperty("_playgroundBounds").boundsValue = new Bounds(new Vector3(0, 3.025f, 0), new Vector3(17, 6.95f, 13));
|
||||
data.ApplyModifiedPropertiesWithoutUndo();
|
||||
if (GameplaySignature(new SerializedObject(stage)) != before) throw new InvalidOperationException("Gameplay fields changed: " + id);
|
||||
signatures.Add(id + ":" + before); EditorUtility.SetDirty(stage);
|
||||
}
|
||||
var app = PrefabUtility.LoadPrefabContents(Root + "Prefabs/GameApp.prefab");
|
||||
try
|
||||
{
|
||||
var floor = app.transform.Find("Playground/PlaygroundIsland");
|
||||
foreach (var collider in floor.GetComponents<Collider>()) Object.DestroyImmediate(collider);
|
||||
PrefabUtility.SaveAsPrefabAsset(app, Root + "Prefabs/GameApp.prefab");
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(app); }
|
||||
AssetDatabase.SaveAssets();
|
||||
string evidence = Environment.GetEnvironmentVariable("MAGNETISM_PLAYGROUND_EVIDENCE");
|
||||
if (!string.IsNullOrEmpty(evidence)) { Directory.CreateDirectory(evidence); File.WriteAllLines(Path.Combine(evidence, "unchanged-gameplay-signatures.txt"), signatures); }
|
||||
Debug.Log("[PlaygroundScenery] 30 fixed layouts grounded on imported meshes. Approved artwork and original gameplay fields preserved.");
|
||||
}
|
||||
|
||||
private static string GameplaySignature(SerializedObject data)
|
||||
{
|
||||
string Array(string field)
|
||||
{
|
||||
var array = data.FindProperty(field);
|
||||
return string.Join("|", Enumerable.Range(0, array.arraySize).Select(i => {
|
||||
var item = array.GetArrayElementAtIndex(i);
|
||||
return item.propertyType == SerializedPropertyType.Vector3 ? JsonUtility.ToJson(item.vector3Value) : item.intValue.ToString(); }));
|
||||
}
|
||||
return data.FindProperty("_stageId").intValue + ";" + data.FindProperty("_kind").intValue + ";" + data.FindProperty("_theme").stringValue + ";" + Array("_initialGroups") + ";" + Array("_targetGroups") + ";" + Array("_ballPositions");
|
||||
}
|
||||
|
||||
private static List<StageDecoration> Compose(StageAsset stage, Dictionary<string, GameObject> props)
|
||||
{
|
||||
var result = new List<StageDecoration>();
|
||||
int group = (stage.StageId - 1) / 5, variant = (stage.StageId - 1) % 5;
|
||||
void Place(string name, float x, float z, float scale = 1, float yaw = 0) => result.Add(new StageDecoration { prefab = props[name], position = new Vector3(x, 0, z), scale = Vector3.one * scale, eulerAngles = new Vector3(0, yaw, 0) });
|
||||
result.Add(new StageDecoration { prefab = props["MeadowClearing"], position = new Vector3(0, -.485f, 0), scale = new Vector3(5, .2f, 5) });
|
||||
// Preserve the original discoveries, with their openings genuinely selectable.
|
||||
Place("OpenStoneArch", -4, 1.6f); Place("MossStoneSteps", 4, 1.7f);
|
||||
for (int side = 0; side < 4; side++)
|
||||
{
|
||||
float yaw = side * 90; var direction = Quaternion.Euler(0, yaw, 0) * Vector3.forward;
|
||||
Place("FarHorizon", direction.x * 27, direction.z * 27, 1, yaw);
|
||||
result[result.Count - 1].position += Vector3.down * .46f;
|
||||
Place("DistantRidge", direction.x * 22, direction.z * 22, 1, yaw);
|
||||
result[result.Count - 1].position += Vector3.down * .46f;
|
||||
}
|
||||
int hills = group == 2 ? 5 : group == 5 ? 4 : 2;
|
||||
for (int i = 0; i < hills; i++)
|
||||
{
|
||||
float angle = (35 + i * 360f / hills + variant * 6) * Mathf.Deg2Rad;
|
||||
// Keep sloping hills beyond the full root/stone/plant footprints.
|
||||
// The approved props have level bases; a pivot-only height cannot seat them on a steep slope.
|
||||
float radius = group == 2 ? 17.5f : 19;
|
||||
Place("GentleHill", Mathf.Sin(angle) * radius, Mathf.Cos(angle) * radius, group == 2 ? .8f + i % 2 * .15f : 1.3f, i * 63 + variant * 9);
|
||||
}
|
||||
// Dense woodland, broad open flower meadows, and low stone silhouettes differ in composition.
|
||||
int trees = group == 1 ? 9 : group == 3 ? 11 : group == 0 ? 2 : group == 2 ? 3 : 1;
|
||||
for (int i = 0; i < trees; i++)
|
||||
{
|
||||
// Frame the board from behind and the sides; a full ring puts large crowns beside the default camera.
|
||||
float angle = (trees == 1 ? 35 + variant * 8 : -75 + i * 150f / (trees - 1) + (variant - 2) * 4) * Mathf.Deg2Rad;
|
||||
float radius = group == 1 || group == 3 ? 8.6f + i % 3 * .7f : 10.5f;
|
||||
string tree = group == 1 ? new[] { "RoundCanopyTree", "SageCanopyTree", "RoundCanopyTree" }[(i + variant) % 3] : group == 3 ? (i % 2 == 0 ? "TallWindTree" : "SageCanopyTree") : "RoundCanopyTree";
|
||||
Place(tree, Mathf.Sin(angle) * radius, Mathf.Cos(angle) * radius * .72f, .85f + (i + variant) % 3 * .13f, i * 47 + variant * 21);
|
||||
}
|
||||
int stones = group == 4 ? 10 : group == 5 ? 7 : 2;
|
||||
for (int i = 0; i < stones; i++)
|
||||
{
|
||||
float angle = (50 + i * 360f / stones + variant * 11) * Mathf.Deg2Rad;
|
||||
string stone = new[] { "RoundLimestone", "SlateBoulder", "LowLimestone" }[(i + variant) % 3];
|
||||
float radius = 7.7f + i % 2 * 1.2f;
|
||||
float z = Mathf.Cos(angle) * radius * .73f;
|
||||
float stoneScale = (.75f + (i + variant) % 3 * .17f) * (z < -2 ? .6f : 1);
|
||||
Place(stone, Mathf.Sin(angle) * radius, z, stoneScale, i * 39 + variant * 13);
|
||||
Place("PebbleScatter", Mathf.Sin(angle) * (radius - .7f), Mathf.Cos(angle) * radius * .73f, .8f, i * 63);
|
||||
}
|
||||
if (group == 5)
|
||||
{
|
||||
Place("OpenStoneArch", 7.2f, -3.5f + variant * .35f, .85f, 60 + variant * 8);
|
||||
Place("MossStoneSteps", -7, -2.8f - variant * .2f, .8f, -35 + variant * 12);
|
||||
}
|
||||
int vegetation = group == 3 ? 22 : group == 0 || group == 2 ? 18 : group == 1 ? 12 : 9;
|
||||
for (int i = 0; i < vegetation; i++)
|
||||
{
|
||||
float angle = (i * 137.5f + variant * 19) * Mathf.Deg2Rad;
|
||||
float radius = 5.4f + i % 4 * .8f;
|
||||
string name = group == 0 ? new[] { "DaisyColony", "GrassTuftMint", "MintShrub" }[(i + variant) % 3]
|
||||
: group == 1 ? new[] { "MintShrub", "SageShrub", "GrassTuftMint" }[(i + variant) % 3]
|
||||
: group == 2 ? new[] { "PinkCosmosColony", "LavenderLupineColony", "GrassTuftGold" }[(i + variant) % 3]
|
||||
: group == 3 ? new[] { "FernCluster", "FernCluster", "GrassTuftMint", "SageShrub" }[(i + variant) % 4]
|
||||
: new[] { "GrassTuftGold", "FernCluster", "GrassTuftMint" }[(i + variant) % 3];
|
||||
Place(name, Mathf.Sin(angle) * radius, Mathf.Cos(angle) * radius * .77f, .6f + (i + variant) % 3 * .14f, i * 71 + variant * 29);
|
||||
}
|
||||
// A low continuous natural outline marks the compact playable rectangle.
|
||||
// The broad clearing outside it remains a connected background apron.
|
||||
int edgeIndex = 0;
|
||||
void Edge(float x, float z)
|
||||
{
|
||||
string name = group >= 4 ? "PebbleScatter" : group == 0 && edgeIndex % 10 == 0 ? "DaisyColony"
|
||||
: group == 2 && edgeIndex % 10 == 0 ? "LavenderLupineColony"
|
||||
: (group == 1 || group == 3) && edgeIndex % 4 == 0 ? "GrassTuftGold" : "PebbleScatter";
|
||||
Place(name, x, z, .75f + edgeIndex % 2 * .1f, edgeIndex * 47 + variant * 9); edgeIndex++;
|
||||
}
|
||||
for (int i = 0; i <= 13; i++) { float x = -8.5f + 17f * i / 13; Edge(x, -6.5f); Edge(x, 6.5f); }
|
||||
for (int i = 1; i < 10; i++) { float z = -6.5f + 13f * i / 10; Edge(-8.5f, z); Edge(8.5f, z); }
|
||||
return result;
|
||||
}
|
||||
|
||||
private static GameObject Instance(StageDecoration placement)
|
||||
{
|
||||
var item = (GameObject)PrefabUtility.InstantiatePrefab(placement.prefab);
|
||||
item.hideFlags = HideFlags.HideAndDontSave;
|
||||
item.transform.SetPositionAndRotation(placement.position, Quaternion.Euler(placement.eulerAngles));
|
||||
item.transform.localScale = placement.scale; return item;
|
||||
}
|
||||
private static float Surface(IEnumerable<Collider> ground, float x, float z)
|
||||
{
|
||||
float height = float.NegativeInfinity; var ray = new Ray(new Vector3(x, 30, z), Vector3.down);
|
||||
foreach (var collider in ground)
|
||||
if (collider.Raycast(ray, out var hit, 60)) height = Mathf.Max(height, hit.point.y);
|
||||
if (float.IsNegativeInfinity(height)) throw new InvalidOperationException($"No real terrain at {x},{z}");
|
||||
return height;
|
||||
}
|
||||
private static float Bottom(GameObject item) => item.GetComponentsInChildren<MeshRenderer>().Min(renderer => renderer.bounds.min.y);
|
||||
private static void Ground(List<StageDecoration> placements)
|
||||
{
|
||||
var instances = new List<GameObject>(); var ground = new List<Collider>();
|
||||
try
|
||||
{
|
||||
var clearing = Instance(placements[0]); instances.Add(clearing); ground.AddRange(clearing.GetComponentsInChildren<Collider>());
|
||||
Physics.SyncTransforms();
|
||||
// Peripheral terrain joins the clearing at its own actual surface height, with a small overlap.
|
||||
foreach (var placement in placements.Where(item => item.prefab.name == "GentleHill"))
|
||||
{
|
||||
var hill = Instance(placement); instances.Add(hill);
|
||||
placement.position.y += Surface(ground, placement.position.x, placement.position.z) - Bottom(hill) - .015f;
|
||||
hill.transform.position = placement.position; ground.AddRange(hill.GetComponentsInChildren<Collider>()); Physics.SyncTransforms();
|
||||
}
|
||||
foreach (var placement in placements.Where(item => !Terrain.Contains(item.prefab.name)))
|
||||
{
|
||||
var prop = Instance(placement); instances.Add(prop);
|
||||
placement.position.y += Surface(ground, placement.position.x, placement.position.z) - Bottom(prop);
|
||||
// Rounded approved rocks otherwise touch on a tiny pole and read as floating.
|
||||
// Seat the visible lower cap slightly in the actual terrain, without changing the mesh.
|
||||
if (placement.prefab.name == "RoundLimestone" || placement.prefab.name == "SlateBoulder" || placement.prefab.name == "LowLimestone")
|
||||
placement.position.y -= .10f * placement.scale.y;
|
||||
// These approved bent-trunk models have a lower trunk pole than their
|
||||
// five root flare end caps. Seat those actual root undersides, too.
|
||||
if (placement.prefab.name == "RoundCanopyTree" || placement.prefab.name == "SageCanopyTree")
|
||||
placement.position.y -= .095f * placement.scale.y;
|
||||
prop.transform.position = placement.position;
|
||||
}
|
||||
}
|
||||
finally { foreach (var item in instances) Object.DestroyImmediate(item); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e1bb7dc6a9bb57b46ba65160734d277e
|
||||
@@ -0,0 +1,293 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using MagnetismHeroes.UnityRuntime.Presentation;
|
||||
using MagnetismHeroes.UnityRuntime.StageAssets;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace MagnetismHeroes.Editor
|
||||
{
|
||||
// Deliberately scoped: never invokes the initial project/content builders.
|
||||
public static class VisualUpgradeAssets
|
||||
{
|
||||
private const string Root = "Assets/MagnetismHeroes/UnityRuntime/Presentation/";
|
||||
private const float FarHorizonRadius = 27;
|
||||
[Serializable] private sealed class ApprovedFile { public string repositoryRelativePath, sha256; }
|
||||
[Serializable] private sealed class Approval { public string status; public ApprovedFile[] approvedFiles; }
|
||||
[Serializable] private sealed class PortableMaterial
|
||||
{
|
||||
public string name;
|
||||
public float[] baseColorSrgb, authoredSRGB;
|
||||
public float roughness, metallic;
|
||||
}
|
||||
[Serializable] private sealed class Provenance { public PortableMaterial[] portableMaterials, materials; }
|
||||
[Serializable] private sealed class ImportRecord
|
||||
{
|
||||
public string asset, sha256;
|
||||
public Vector3 rootEuler;
|
||||
public int meshes, triangles;
|
||||
}
|
||||
[Serializable] private sealed class ApplyRecord
|
||||
{
|
||||
public string utc, status;
|
||||
public int approvedFiles, stages;
|
||||
public ImportRecord[] imports;
|
||||
public string[] unchangedNonvisualStageSignatures;
|
||||
}
|
||||
|
||||
public static void ApplyDaylightShadows()
|
||||
{
|
||||
var app = PrefabUtility.LoadPrefabContents(FirstSliceAssets.AppPrefabPath);
|
||||
try
|
||||
{
|
||||
var sun = app.transform.Find("Sun").GetComponent<Light>();
|
||||
sun.shadows = LightShadows.Soft; sun.shadowStrength = .55f;
|
||||
sun.color = new Color(1, .97f, .91f); sun.intensity = 1.3f;
|
||||
PrefabUtility.SaveAsPrefabAsset(app, FirstSliceAssets.AppPrefabPath);
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(app); }
|
||||
var pipeline = new SerializedObject(AssetDatabase.LoadMainAssetAtPath("Assets/Settings/Mobile_RPAsset.asset"));
|
||||
pipeline.FindProperty("m_SoftShadowsSupported").boolValue = true;
|
||||
pipeline.FindProperty("m_SoftShadowQuality").intValue = 1;
|
||||
pipeline.FindProperty("m_ShadowDistance").floatValue = 30;
|
||||
pipeline.ApplyModifiedPropertiesWithoutUndo(); AssetDatabase.SaveAssets();
|
||||
Debug.Log("[VisualUpgrade] Existing daylight light and mobile URP: soft .55-strength shadows, low soft-shadow quality, one 1024 cascade, 30m distance; model/material/stage bytes untouched.");
|
||||
}
|
||||
|
||||
public static void ApplyHorizonContinuity()
|
||||
{
|
||||
foreach (int id in Enumerable.Range(1, 30))
|
||||
{
|
||||
var stage = AssetDatabase.LoadAssetAtPath<StageAsset>($"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage{id:D2}.asset");
|
||||
string before = StageSignature(stage);
|
||||
if (stage.Decorations.Count(item => item.prefab.name == "FarHorizon") != 4)
|
||||
throw new InvalidOperationException("Expected four existing horizon placements in stage " + id);
|
||||
Undo.RecordObject(stage, "Connect distant horizon to meadow apron");
|
||||
var data = new SerializedObject(stage); var placements = data.FindProperty("_decorations");
|
||||
for (int index = 0; index < placements.arraySize; index++)
|
||||
{
|
||||
var placement = placements.GetArrayElementAtIndex(index);
|
||||
if (placement.FindPropertyRelative("prefab").objectReferenceValue.name != "FarHorizon") continue;
|
||||
var position = placement.FindPropertyRelative("position"); var point = position.vector3Value;
|
||||
var direction = new Vector3(point.x, 0, point.z).normalized;
|
||||
position.vector3Value = new Vector3(direction.x * FarHorizonRadius, point.y, direction.z * FarHorizonRadius);
|
||||
}
|
||||
data.ApplyModifiedPropertiesWithoutUndo();
|
||||
if (StageSignature(stage) != before) throw new InvalidOperationException("Nonvisual stage state changed: " + id);
|
||||
}
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log("[VisualUpgrade] Only 120 FarHorizon positions moved inward to overlap the existing apron/ridge; stage nonvisual fields unchanged.");
|
||||
}
|
||||
|
||||
public static void Apply()
|
||||
{
|
||||
string approvalPath = Environment.GetEnvironmentVariable("MAGNETISM_VISUAL_APPROVAL");
|
||||
if (string.IsNullOrEmpty(approvalPath)) throw new InvalidOperationException("Exact reviewed approval record is required.");
|
||||
var approval = JsonUtility.FromJson<Approval>(File.ReadAllText(approvalPath));
|
||||
if (approval.status != "APPROVED" || approval.approvedFiles.Length != 25)
|
||||
throw new InvalidOperationException("All 25 reviewed files must be approved before adoption.");
|
||||
foreach (var file in approval.approvedFiles)
|
||||
if (Hash(file.repositoryRelativePath) != file.sha256)
|
||||
throw new InvalidOperationException("Approved bytes changed: " + file.repositoryRelativePath);
|
||||
|
||||
var stages = Enumerable.Range(1, 30).Select(id => AssetDatabase.LoadAssetAtPath<StageAsset>(
|
||||
$"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage{id:D2}.asset")).ToArray();
|
||||
var signatures = stages.Select(StageSignature).ToArray();
|
||||
Directory.CreateDirectory(Root + "Materials/VisualUpgrade");
|
||||
Directory.CreateDirectory(Root + "Prefabs/Meadow");
|
||||
AssetDatabase.Refresh();
|
||||
var materials = new Dictionary<string, Material>();
|
||||
foreach (string path in new[] { "SourceAssets/Models/Heroes/provenance.json", "SourceAssets/Models/Meadow/provenance.json" })
|
||||
{
|
||||
var provenance = JsonUtility.FromJson<Provenance>(File.ReadAllText(path));
|
||||
foreach (var spec in provenance.portableMaterials ?? provenance.materials)
|
||||
{
|
||||
string materialPath = Root + "Materials/VisualUpgrade/" + spec.name + ".mat";
|
||||
var material = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
|
||||
if (material == null)
|
||||
{
|
||||
material = new Material(Shader.Find("Universal Render Pipeline/Lit"));
|
||||
AssetDatabase.CreateAsset(material, materialPath);
|
||||
}
|
||||
// Unity's material Color is sRGB; Lit converts it for the linear render pipeline.
|
||||
// The Blender linearBaseColor is deliberately not assigned as another sRGB color.
|
||||
var color = spec.baseColorSrgb ?? spec.authoredSRGB;
|
||||
material.SetColor("_BaseColor", new Color(color[0], color[1], color[2], 1));
|
||||
material.SetFloat("_Smoothness", 1 - spec.roughness);
|
||||
material.SetFloat("_Metallic", spec.metallic);
|
||||
material.enableInstancing = true;
|
||||
EditorUtility.SetDirty(material); materials.Add(spec.name, material);
|
||||
}
|
||||
}
|
||||
var imports = new List<ImportRecord>();
|
||||
foreach (var file in approval.approvedFiles.Where(item => item.repositoryRelativePath.EndsWith(".fbx", StringComparison.Ordinal)))
|
||||
{
|
||||
var importer = (ModelImporter)AssetImporter.GetAtPath(file.repositoryRelativePath);
|
||||
importer.globalScale = 1; importer.useFileScale = true; importer.bakeAxisConversion = true;
|
||||
importer.importAnimation = false; importer.importCameras = false; importer.importLights = false;
|
||||
importer.SaveAndReimport();
|
||||
foreach (var source in AssetDatabase.LoadAllAssetsAtPath(file.repositoryRelativePath).OfType<Material>())
|
||||
{
|
||||
if (!materials.TryGetValue(source.name, out var material))
|
||||
throw new InvalidOperationException("Material has no portable provenance: " + source.name);
|
||||
importer.AddRemap(new AssetImporter.SourceAssetIdentifier(typeof(Material), source.name), material);
|
||||
}
|
||||
importer.SaveAndReimport();
|
||||
var model = AssetDatabase.LoadAssetAtPath<GameObject>(file.repositoryRelativePath);
|
||||
var meshes = model.GetComponentsInChildren<MeshFilter>(true);
|
||||
imports.Add(new ImportRecord { asset = file.repositoryRelativePath, sha256 = file.sha256,
|
||||
rootEuler = model.transform.localEulerAngles, meshes = meshes.Length,
|
||||
triangles = meshes.Sum(filter => filter.sharedMesh.triangles.Length / 3) });
|
||||
}
|
||||
ApplyBody(); ApplyEquipment();
|
||||
var props = approval.approvedFiles.Where(item => item.repositoryRelativePath.Contains("/Models/Meadow/") && item.repositoryRelativePath.EndsWith(".fbx", StringComparison.Ordinal))
|
||||
.ToDictionary(item => Path.GetFileNameWithoutExtension(item.repositoryRelativePath), item => Scenery(item.repositoryRelativePath));
|
||||
foreach (var stage in stages) ApplyStage(stage, props);
|
||||
for (int index = 0; index < stages.Length; index++)
|
||||
if (StageSignature(stages[index]) != signatures[index]) throw new InvalidOperationException("Nonvisual stage state changed: " + (index + 1));
|
||||
var app = PrefabUtility.LoadPrefabContents(FirstSliceAssets.AppPrefabPath);
|
||||
try
|
||||
{
|
||||
// Preserve the old floor's exact input/drag collision boundary.
|
||||
app.transform.Find("Playground/PlaygroundIsland").GetComponent<MeshRenderer>().enabled = false;
|
||||
app.transform.Find("GameCamera").GetComponent<Camera>().backgroundColor = new Color(.58f, .83f, .88f);
|
||||
PrefabUtility.SaveAsPrefabAsset(app, FirstSliceAssets.AppPrefabPath);
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(app); }
|
||||
AssetDatabase.SaveAssets();
|
||||
string evidence = Environment.GetEnvironmentVariable("MAGNETISM_VISUAL_EVIDENCE");
|
||||
Directory.CreateDirectory(evidence);
|
||||
File.WriteAllText(Path.Combine(evidence, "source-import.json"), JsonUtility.ToJson(new ApplyRecord {
|
||||
utc = DateTime.UtcNow.ToString("o"), status = "APPLIED_EXACT_APPROVED_BYTES_NONVISUAL_STAGE_FIELDS_UNCHANGED",
|
||||
approvedFiles = approval.approvedFiles.Length, stages = stages.Length, imports = imports.ToArray(),
|
||||
unchangedNonvisualStageSignatures = signatures }, true));
|
||||
Debug.Log("[VisualUpgrade] Exact approved models imported, original gameplay stage fields preserved, one equipment assembly per badge connected.");
|
||||
}
|
||||
|
||||
private static void ApplyBody()
|
||||
{
|
||||
string path = Root + "Prefabs/MagnetFriend.prefab";
|
||||
var ball = PrefabUtility.LoadPrefabContents(path);
|
||||
try
|
||||
{
|
||||
var old = ball.transform.Find("ApprovedHeroBody");
|
||||
if (old != null) Object.DestroyImmediate(old.gameObject);
|
||||
var body = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(Root + "Models/Heroes/HeroBody.fbx"));
|
||||
body.name = "ApprovedHeroBody"; body.transform.SetParent(ball.transform, false);
|
||||
var renderer = body.GetComponentInChildren<MeshRenderer>();
|
||||
var data = new SerializedObject(ball.GetComponent<BallView>());
|
||||
data.FindProperty("_body").objectReferenceValue = renderer; data.ApplyModifiedPropertiesWithoutUndo();
|
||||
Object.DestroyImmediate(ball.GetComponent<MeshRenderer>());
|
||||
Object.DestroyImmediate(ball.GetComponent<MeshFilter>());
|
||||
PrefabUtility.SaveAsPrefabAsset(ball, path);
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(ball); }
|
||||
}
|
||||
private static void ApplyEquipment()
|
||||
{
|
||||
string path = Root + "Prefabs/GroupBadge.prefab";
|
||||
var badge = PrefabUtility.LoadPrefabContents(path);
|
||||
try
|
||||
{
|
||||
var old = badge.transform.Find("KnightEquipment");
|
||||
if (old != null) Object.DestroyImmediate(old.gameObject);
|
||||
var equipment = new GameObject("KnightEquipment"); equipment.transform.SetParent(badge.transform, false);
|
||||
var alignment = new GameObject("ViewAlignment"); alignment.transform.SetParent(equipment.transform, false);
|
||||
var model = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(Root + "Models/Heroes/KnightEquipment.fbx"));
|
||||
model.transform.SetParent(alignment.transform, false);
|
||||
// Keep the imported FBX root untouched. Orient its front to the sprite's -Z front.
|
||||
var shield = model.GetComponentsInChildren<MeshRenderer>().Single(item => item.name == "Shield_TealFace");
|
||||
float front = equipment.transform.InverseTransformPoint(shield.bounds.center).z;
|
||||
if (Mathf.Abs(front) < .01f) throw new InvalidOperationException("Cannot identify imported equipment front.");
|
||||
if (front > 0) alignment.transform.localRotation = Quaternion.Euler(0, 180, 0);
|
||||
var data = new SerializedObject(badge.GetComponent<GroupBadge>());
|
||||
data.FindProperty("_equipment").objectReferenceValue = equipment.transform; data.ApplyModifiedPropertiesWithoutUndo();
|
||||
Debug.Log($"[VisualUpgrade] equipment imported front={front}, view alignment={alignment.transform.localEulerAngles}, imported root={model.transform.localEulerAngles}");
|
||||
PrefabUtility.SaveAsPrefabAsset(badge, path);
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(badge); }
|
||||
}
|
||||
private static GameObject Scenery(string source)
|
||||
{
|
||||
string name = Path.GetFileNameWithoutExtension(source);
|
||||
var prop = new GameObject(name);
|
||||
try
|
||||
{
|
||||
var model = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(source));
|
||||
model.transform.SetParent(prop.transform, false);
|
||||
if (name == "OpenStoneArch" || name == "MossStoneSteps")
|
||||
foreach (var filter in model.GetComponentsInChildren<MeshFilter>())
|
||||
filter.gameObject.AddComponent<MeshCollider>().sharedMesh = filter.sharedMesh;
|
||||
return PrefabUtility.SaveAsPrefabAsset(prop, Root + "Prefabs/Meadow/" + name + ".prefab");
|
||||
}
|
||||
finally { Object.DestroyImmediate(prop); }
|
||||
}
|
||||
private static void ApplyStage(StageAsset stage, Dictionary<string, GameObject> props)
|
||||
{
|
||||
var placements = new List<StageDecoration>();
|
||||
void Place(string name, float x, float y, float z, float scale = 1, float yaw = 0) => placements.Add(new StageDecoration {
|
||||
prefab = props[name], position = new Vector3(x, y, z), scale = Vector3.one * scale, eulerAngles = new Vector3(0, yaw, 0) });
|
||||
// A broad, nearly level visual meadow meets the original floor at y=-.45.
|
||||
// Keep its approved mesh/material bytes and the existing playable collider.
|
||||
placements.Add(new StageDecoration { prefab = props["MeadowClearing"],
|
||||
position = new Vector3(0, -.485f, 0), scale = new Vector3(5, .2f, 5) });
|
||||
Place("OpenStoneArch", -4, 0, 1.6f); Place("MossStoneSteps", 4, 0, 1.7f);
|
||||
Place("PebbleScatter", 3.8f, 0, -2.6f);
|
||||
for (int side = 0; side < 4; side++)
|
||||
{
|
||||
float yaw = side * 90; var direction = Quaternion.Euler(0, yaw, 0) * Vector3.forward;
|
||||
Place("FarHorizon", direction.x * FarHorizonRadius, -.485f, direction.z * FarHorizonRadius, 1, yaw);
|
||||
Place("DistantRidge", direction.x * 22, -.485f, direction.z * 22, 1, yaw);
|
||||
var corner = Quaternion.Euler(0, yaw + 45, 0) * Vector3.forward * 19;
|
||||
Place("GentleHill", corner.x, -.485f, corner.z, 1.8f, yaw);
|
||||
}
|
||||
string tree = stage.StageId <= 10 ? "RoundCanopyTree" : stage.StageId <= 20 ? "SageCanopyTree" : "TallWindTree";
|
||||
Place(tree, -6.6f, -.45f, 4.8f); Place(tree, 6.6f, -.45f, 5.8f);
|
||||
Place(tree, -7.5f, -.45f, -6); Place(tree, 7.5f, -.45f, -6);
|
||||
Place(stage.StageId <= 10 ? "MintShrub" : "SageShrub", -6, -.45f, -2.4f, .75f);
|
||||
if (stage.StageId <= 10)
|
||||
{
|
||||
Place("DaisyColony", -4.1f, 0, -1.8f, .7f); Place("FernCluster", 4.7f, 0, 3.3f);
|
||||
Place("GrassTuftMint", -3.4f, 0, 1.4f); Place("GrassTuftGold", 3.7f, 0, 3);
|
||||
}
|
||||
else if (stage.StageId <= 20)
|
||||
{
|
||||
Place("PinkCosmosColony", -4.1f, 0, -1.8f, .7f); Place("LavenderLupineColony", 4.7f, 0, 3.3f, .8f);
|
||||
Place("DaisyColony", -5.6f, 0, 3.8f); Place("GrassTuftGold", 3.7f, 0, 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
Place("SlateBoulder", -6.2f, -.45f, 5.4f); Place("RoundLimestone", 5.6f, -.45f, 4.2f);
|
||||
Place("LowLimestone", -4.8f, 0, -2.7f); Place("FernCluster", 3.7f, 0, 3);
|
||||
}
|
||||
var data = new SerializedObject(stage); var decorations = data.FindProperty("_decorations");
|
||||
decorations.arraySize = placements.Count;
|
||||
for (int index = 0; index < placements.Count; index++)
|
||||
{
|
||||
var target = decorations.GetArrayElementAtIndex(index); var placement = placements[index];
|
||||
target.FindPropertyRelative("prefab").objectReferenceValue = placement.prefab;
|
||||
target.FindPropertyRelative("position").vector3Value = placement.position;
|
||||
target.FindPropertyRelative("eulerAngles").vector3Value = placement.eulerAngles;
|
||||
target.FindPropertyRelative("scale").vector3Value = placement.scale;
|
||||
}
|
||||
data.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
private static string StageSignature(StageAsset stage)
|
||||
{
|
||||
var data = new SerializedObject(stage);
|
||||
string Array(string field) { var value = data.FindProperty(field); return string.Join("|", Enumerable.Range(0, value.arraySize).Select(index => {
|
||||
var item = value.GetArrayElementAtIndex(index);
|
||||
return item.propertyType == SerializedPropertyType.Vector3 ? JsonUtility.ToJson(item.vector3Value) : item.intValue.ToString(); })); }
|
||||
return stage.StageId + ";" + stage.Theme + ";" + data.FindProperty("_kind").intValue + ";" + Array("_initialGroups") + ";" + Array("_targetGroups") + ";" + Array("_ballPositions");
|
||||
}
|
||||
private static string Hash(string path)
|
||||
{
|
||||
using (var algorithm = SHA256.Create()) using (var file = File.OpenRead(path))
|
||||
return BitConverter.ToString(algorithm.ComputeHash(file)).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 40789cbe0397f8745bff78d14a544113
|
||||
@@ -30,8 +30,14 @@ namespace MagnetismHeroes.Tests
|
||||
Assert.That(definition.Total, Is.EqualTo(definition.TargetGroups.Sum())); Assert.That(definition.Total, Is.InRange(1,10));
|
||||
Assert.That(definition.BallPositions.Count, Is.EqualTo(definition.Total));
|
||||
Assert.That(asset.Theme, Is.EqualTo(id<=10 ? "Garden" : id<=20 ? "Cloud" : "Workshop"));
|
||||
Assert.That(asset.Decorations.Count, Is.EqualTo(7));
|
||||
foreach (var placement in asset.Decorations) Assert.That(placement.prefab, Is.Not.Null);
|
||||
foreach (string name in new[] { "MeadowClearing", "OpenStoneArch", "MossStoneSteps", "PebbleScatter", "GentleHill" })
|
||||
Assert.That(asset.Decorations.Any(placement => placement.prefab.name == name), Is.True, $"Stage {id} shared discovery and terrain: {name}");
|
||||
var primaryDecorations = new[] { "DaisyColony", "RoundCanopyTree", "PinkCosmosColony", "FernCluster", "RoundLimestone", "OpenStoneArch" };
|
||||
Assert.That(asset.Decorations.Any(placement => placement.prefab.name == primaryDecorations[(id - 1) / 5]), Is.True, $"Stage {id} five-stage natural composition");
|
||||
foreach (string name in new[] { "FarHorizon", "DistantRidge" })
|
||||
Assert.That(asset.Decorations.Where(placement => placement.prefab.name == name).Select(placement => placement.eulerAngles.y),
|
||||
Is.EquivalentTo(new[] { 0f, 90f, 180f, 270f }), $"Stage {id} {name} surrounds all four directions");
|
||||
string before=EditorJsonUtility.ToJson(asset); var board=new BoardRules(definition);
|
||||
board.TryMoveGroup(board.Groups[0].id,new Position3(1,0,0));
|
||||
Assert.That(EditorJsonUtility.ToJson(asset), Is.EqualTo(before), "Runtime rules never mutate their source asset.");
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MagnetismHeroes.Core;
|
||||
using MagnetismHeroes.UnityRuntime.Persistence;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MagnetismHeroes.Tests
|
||||
{
|
||||
public sealed class PlaygroundResumeSessionTests
|
||||
{
|
||||
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 Store : IProgressStore
|
||||
{
|
||||
public ProgressSave Save;
|
||||
public int Writes;
|
||||
public bool TryLoad(out ProgressSave save, out string error) { save = Save; error = null; return true; }
|
||||
public bool TrySave(ProgressSave save, out string error) { Save = save; Writes++; error = null; return true; }
|
||||
}
|
||||
private static PuzzleState Legacy()
|
||||
{
|
||||
return new PuzzleState { stageId = 2,
|
||||
balls = new[] { new BallState { id = "legacy-a", position = new Position3(30, 2, 0) }, new BallState { id = "legacy-b", position = new Position3(31, 2, 0) } },
|
||||
groups = new[] { new GroupState { id = "legacy-group", ballIds = new[] { "legacy-a", "legacy-b" } } },
|
||||
collectedBallIds = new[] { "legacy-a" }, countedBallIds = new[] { "legacy-a" }, camera = GameSession.DefaultCamera() };
|
||||
}
|
||||
private static ProgressSave Fixture() => new ProgressSave {
|
||||
completedStageIds = new[] { 1, 2, 3, 4, 5, 6 }, seenTutorialIds = new[] { "merge", "split" },
|
||||
puzzleStates = new[] { new BoardRules(Definitions[0]).ToPuzzleState(GameSession.DefaultCamera()), Legacy(), new BoardRules(Definitions[2]).ToPuzzleState(GameSession.DefaultCamera()) },
|
||||
latestFreePlay = new BoardRules(Definitions[2]).ToFreePlayState(GameSession.DefaultCamera()) };
|
||||
private static BoardRestore Repair(BoardRules board, CameraState camera)
|
||||
{
|
||||
Assert.That(board.TryTranslateForRestore(new Dictionary<string, Position3> { ["legacy-group"] = new Position3(-23, 0, 0) }, out var prepared), Is.True);
|
||||
return new BoardRestore(prepared, camera);
|
||||
}
|
||||
[Test] public void RestoreTranslationRetainsIdentityMembershipPartialCollectionAndCountWithoutSuccess()
|
||||
{
|
||||
var original = new BoardRules(Definitions[1], Legacy());
|
||||
var prepared = Repair(original, GameSession.DefaultCamera()).Board;
|
||||
Assert.That(original.Balls[0].position.x, Is.EqualTo(30));
|
||||
Assert.That(prepared.Balls.Select(ball => ball.id), Is.EqualTo(original.Balls.Select(ball => ball.id)));
|
||||
Assert.That(prepared.Groups.Single().ballIds, Is.EqualTo(original.Groups.Single().ballIds));
|
||||
Assert.That(prepared.CollectedBallIds, Is.EqualTo(original.CollectedBallIds));
|
||||
Assert.That(prepared.CountedBallIds, Is.EqualTo(original.CountedBallIds));
|
||||
Assert.That(prepared.IsSolved, Is.False);
|
||||
Assert.That(prepared.Balls[1].position.x - prepared.Balls[0].position.x, Is.EqualTo(1));
|
||||
var ordinary = new BoardRules(Definitions[1], Legacy()); ordinary.TryMoveGroup("legacy-group", new Position3(-23, 0, 0));
|
||||
Assert.That(ordinary.CollectedBallIds.Length, Is.EqualTo(2), "Negative control: ordinary play movement would corrupt partial collection.");
|
||||
}
|
||||
[Test] public void InvalidTranslationRejectsWholeReplacementAndLeavesSourceUntouched()
|
||||
{
|
||||
var board = new BoardRules(Definitions[1], Legacy());
|
||||
Assert.That(board.TryTranslateForRestore(new Dictionary<string, Position3> { ["legacy-group"] = new Position3(float.NaN, 0, 0) }, out var prepared), Is.False);
|
||||
Assert.That(prepared, Is.Null); Assert.That(board.Balls[0].position.x, Is.EqualTo(30));
|
||||
Assert.That(board.CollectedBallIds, Is.EqualTo(new[] { "legacy-a" }));
|
||||
}
|
||||
[Test] public void OneValidatedWriteReplacesOnlyTargetWhileKeepingOtherPuzzlesFreeProgressAndTutorials()
|
||||
{
|
||||
var fixture = Fixture(); var otherOne = fixture.puzzleStates[0]; var otherSix = fixture.puzzleStates[2]; var free = fixture.latestFreePlay;
|
||||
var store = new Store { Save = fixture }; var session = new GameSession(Definitions, store);
|
||||
Assert.That(session.StartStage(2, Repair), Is.True);
|
||||
Assert.That(store.Writes, Is.EqualTo(1)); Assert.That(session.Screen, Is.EqualTo(SessionScreen.Puzzle));
|
||||
Assert.That(store.Save.completedStageIds, Is.EqualTo(fixture.completedStageIds)); Assert.That(store.Save.seenTutorialIds, Is.EqualTo(fixture.seenTutorialIds));
|
||||
AssertPuzzle(store.Save.puzzleStates[0], otherOne); AssertPuzzle(store.Save.puzzleStates[2], otherSix);
|
||||
Assert.That(store.Save.latestFreePlay.balls.Select(ball => ball.position), Is.EqualTo(free.balls.Select(ball => ball.position)));
|
||||
Assert.That(store.Save.latestFreePlay.sourceStageId, Is.EqualTo(free.sourceStageId));
|
||||
Assert.That(store.Save.puzzleStates[1].balls[0].position.x, Is.EqualTo(7));
|
||||
Assert.That(fixture.puzzleStates[1].balls[0].position.x, Is.EqualTo(30), "Preparing and saving must not mutate a loaded store snapshot.");
|
||||
}
|
||||
[Test] public void InconclusivePreparationHasNoWriteAndKeepsCurrentBoardCameraAndEverySlot()
|
||||
{
|
||||
var fixture = Fixture(); var store = new Store { Save = fixture }; var session = new GameSession(Definitions, store);
|
||||
session.StartStage(6); var current = session.Board; var camera = session.Camera; int before = store.Writes;
|
||||
Assert.That(session.StartStage(2, (board, view) => null), Is.False);
|
||||
Assert.That(store.Writes, Is.EqualTo(before)); Assert.That(session.Board, Is.SameAs(current)); Assert.That(session.Camera, Is.SameAs(camera));
|
||||
Assert.That(store.Save.puzzleStates.Single(state => state.stageId == 2).balls[0].position.x, Is.EqualTo(30));
|
||||
}
|
||||
[TestCase(false)] [TestCase(true)] public void ValidUnsortedDtosKeepTheirRawIdentityMembershipAndMarkerOrder(bool repair)
|
||||
{
|
||||
var fixture = Fixture(); fixture.puzzleStates = fixture.puzzleStates.Reverse().ToArray();
|
||||
var original = fixture.puzzleStates.Single(state => state.stageId == 2);
|
||||
original.balls = original.balls.Reverse().ToArray(); original.groups[0].ballIds = original.groups[0].ballIds.Reverse().ToArray();
|
||||
original.collectedBallIds = new[] { "legacy-b", "legacy-a" };
|
||||
var store = new Store { Save = fixture }; var session = new GameSession(Definitions, store);
|
||||
Assert.That(session.StartStage(2, repair ? Repair : null), Is.True);
|
||||
Assert.That(store.Writes, Is.EqualTo(1)); Assert.That(store.Save.puzzleStates.Select(state => state.stageId), Is.EqualTo(new[] { 6, 2, 1 }));
|
||||
var actual = store.Save.puzzleStates.Single(state => state.stageId == 2);
|
||||
Assert.That(actual.balls.Select(ball => ball.id), Is.EqualTo(original.balls.Select(ball => ball.id)));
|
||||
Assert.That(actual.groups[0].ballIds, Is.EqualTo(original.groups[0].ballIds));
|
||||
Assert.That(actual.collectedBallIds, Is.EqualTo(original.collectedBallIds)); Assert.That(actual.countedBallIds, Is.EqualTo(original.countedBallIds));
|
||||
Assert.That(actual.balls[0].position.x, Is.EqualTo(repair ? 8 : 31)); Assert.That(original.balls[0].position.x, Is.EqualTo(31));
|
||||
AssertPuzzle(store.Save.puzzleStates[0], fixture.puzzleStates[0]); AssertPuzzle(store.Save.puzzleStates[2], fixture.puzzleStates[2]);
|
||||
}
|
||||
[Test] public void ReplacementRejectsChangedIdsMembershipAndNonRigidShapeWithoutAnyWrite()
|
||||
{
|
||||
foreach (string change in new[] { "ids", "shape", "collection" })
|
||||
{
|
||||
var store = new Store { Save = Fixture() }; var session = new GameSession(Definitions, store);
|
||||
Assert.That(session.StartStage(2, (board, camera) => {
|
||||
var state = board.ToPuzzleState(camera);
|
||||
if (change == "ids") { state.balls[1].id = "new-id"; state.groups[0].ballIds[1] = "new-id"; }
|
||||
if (change == "shape") state.balls[1].position.x += 1;
|
||||
if (change == "collection") state.collectedBallIds = new[] { "legacy-a", "legacy-b" };
|
||||
return new BoardRestore(new BoardRules(Definitions[1], state), camera);
|
||||
}), Is.False);
|
||||
Assert.That(store.Writes, Is.Zero); Assert.That(session.Board, Is.Null);
|
||||
}
|
||||
}
|
||||
[Test] public void ExplicitInitialResetOnlyChangesTheAffectedPuzzleAndNeverRemovesPastCompletion()
|
||||
{
|
||||
var fixture = Fixture(); var store = new Store { Save = fixture }; var session = new GameSession(Definitions, store);
|
||||
Assert.That(session.StartStage(2, (board, camera) => new BoardRestore(new BoardRules(Definitions[1]), GameSession.DefaultCamera(), true)), Is.True);
|
||||
Assert.That(store.Writes, Is.EqualTo(1)); Assert.That(session.Board.CollectedBallIds, Is.Empty); Assert.That(session.Board.CountedBallIds, Is.Empty);
|
||||
Assert.That(session.Board.Balls.Select(ball => ball.id), Is.EqualTo(new[] { "s02-b01", "s02-b02" }));
|
||||
Assert.That(session.CompletedStageIds, Is.EqualTo(fixture.completedStageIds));
|
||||
AssertPuzzle(store.Save.puzzleStates[0], fixture.puzzleStates[0]); AssertPuzzle(store.Save.puzzleStates[2], fixture.puzzleStates[2]);
|
||||
Assert.That(store.Save.latestFreePlay.sourceStageId, Is.EqualTo(6));
|
||||
}
|
||||
[Test] public void ActualStoreWriteFailureKeepsPrimaryValidBackupCurrentBoardAndNoIntermediateTargetWrite()
|
||||
{
|
||||
string directory = Path.Combine(Path.GetTempPath(), "MagnetismHeroes-restore-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(directory);
|
||||
string path = Path.Combine(directory, "progress.json");
|
||||
try
|
||||
{
|
||||
var store = new LocalProgressStore(path, Definitions); Assert.That(store.TrySave(Fixture(), out _), Is.True); Assert.That(store.TrySave(Fixture(), out _), Is.True);
|
||||
var session = new GameSession(Definitions, store); session.StartStage(6);
|
||||
byte[] primary = File.ReadAllBytes(path), backup = File.ReadAllBytes(path + ".bak"); var board = session.Board; var camera = session.Camera;
|
||||
using (new FileStream(path + ".tmp", FileMode.Create, FileAccess.ReadWrite, FileShare.None))
|
||||
Assert.That(session.StartStage(2, Repair), Is.False);
|
||||
Assert.That(session.LastSaveError, Is.Not.Null); Assert.That(session.Board, Is.SameAs(board)); Assert.That(session.Camera, Is.SameAs(camera));
|
||||
Assert.That(File.ReadAllBytes(path), Is.EqualTo(primary)); Assert.That(File.ReadAllBytes(path + ".bak"), Is.EqualTo(backup));
|
||||
Assert.That(store.TryLoad(out var loaded, out var error), Is.True, error);
|
||||
Assert.That(loaded.puzzleStates.Single(state => state.stageId == 2).balls[0].position.x, Is.EqualTo(30));
|
||||
}
|
||||
finally { Directory.Delete(directory, true); }
|
||||
}
|
||||
private static void AssertPuzzle(PuzzleState actual, PuzzleState expected)
|
||||
{
|
||||
Assert.That(actual.stageId, Is.EqualTo(expected.stageId)); Assert.That(actual.balls.Select(ball => ball.id), Is.EqualTo(expected.balls.Select(ball => ball.id)));
|
||||
Assert.That(actual.balls.Select(ball => ball.position), Is.EqualTo(expected.balls.Select(ball => ball.position)));
|
||||
Assert.That(actual.groups.Select(group => group.id), Is.EqualTo(expected.groups.Select(group => group.id)));
|
||||
for (int index = 0; index < actual.groups.Length; index++) Assert.That(actual.groups[index].ballIds, Is.EqualTo(expected.groups[index].ballIds));
|
||||
Assert.That(actual.collectedBallIds, Is.EqualTo(expected.collectedBallIds)); Assert.That(actual.countedBallIds, Is.EqualTo(expected.countedBallIds));
|
||||
Assert.That(actual.camera.target, Is.EqualTo(expected.camera.target)); Assert.That(actual.camera.rotation, Is.EqualTo(expected.camera.rotation)); Assert.That(actual.camera.distance, Is.EqualTo(expected.camera.distance));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e1b9640c8d119ad488ff61dadfd6d2ce
|
||||
@@ -0,0 +1,294 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Object = UnityEngine.Object;
|
||||
using MagnetismHeroes.UnityRuntime.Presentation;
|
||||
using MagnetismHeroes.UnityRuntime.StageAssets;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MagnetismHeroes.Tests
|
||||
{
|
||||
public sealed class VisualUpgradeAssetTests
|
||||
{
|
||||
private const string Root = "Assets/MagnetismHeroes/UnityRuntime/Presentation/";
|
||||
[Test] public void MobileDaylightSupportsSoftGroundShadowsWithoutReplacingThePlayableFloor()
|
||||
{
|
||||
var app = AssetDatabase.LoadAssetAtPath<GameObject>(Root + "Prefabs/GameApp.prefab");
|
||||
var sun = app.transform.Find("Sun").GetComponent<Light>();
|
||||
Assert.That(sun.shadows, Is.EqualTo(LightShadows.Soft));
|
||||
Assert.That(sun.shadowStrength, Is.InRange(.3f, .7f));
|
||||
Assert.That(sun.intensity, Is.InRange(1.1f, 1.5f));
|
||||
var pipeline = new SerializedObject(AssetDatabase.LoadMainAssetAtPath("Assets/Settings/Mobile_RPAsset.asset"));
|
||||
Assert.That(pipeline.FindProperty("m_MainLightShadowsSupported").boolValue, Is.True);
|
||||
Assert.That(pipeline.FindProperty("m_SoftShadowsSupported").boolValue, Is.True);
|
||||
Assert.That(pipeline.FindProperty("m_MainLightShadowmapResolution").intValue, Is.EqualTo(1024));
|
||||
Assert.That(pipeline.FindProperty("m_ShadowCascadeCount").intValue, Is.EqualTo(1));
|
||||
Assert.That(pipeline.FindProperty("m_ShadowDistance").floatValue, Is.InRange(20f, 35f));
|
||||
var floor = app.transform.Find("Playground/PlaygroundIsland");
|
||||
Assert.That(floor.GetComponents<Collider>(), Is.Empty, "The hidden legacy floor must not block above/below the visible meadow.");
|
||||
Assert.That(floor.GetComponent<MeshRenderer>().enabled, Is.False);
|
||||
}
|
||||
[Test] public void ApprovedBodyAndOneEquipmentAssemblyAreConnectedWithoutChangingSelectionSphere()
|
||||
{
|
||||
var ball = AssetDatabase.LoadAssetAtPath<GameObject>(Root + "Prefabs/MagnetFriend.prefab");
|
||||
var body = ball.transform.Find("ApprovedHeroBody");
|
||||
Assert.That(body, Is.Not.Null, "Approved FBX body must replace the primitive rendering.");
|
||||
Assert.That(ball.transform.localScale, Is.EqualTo(Vector3.one));
|
||||
Assert.That(ball.GetComponentsInChildren<Collider>(true).Length, Is.EqualTo(1));
|
||||
Assert.That(ball.GetComponent<SphereCollider>().radius, Is.EqualTo(.5f));
|
||||
var renderer = body.GetComponentInChildren<MeshRenderer>();
|
||||
Assert.That(renderer.sharedMaterial.name, Is.EqualTo("Hero_CyanSatin"));
|
||||
Assert.That(renderer.sharedMaterial.color.r, Is.EqualTo(.17f).Within(.0001f));
|
||||
Assert.That(renderer.sharedMaterial.color.g, Is.EqualTo(.72f).Within(.0001f));
|
||||
Assert.That(renderer.sharedMaterial.color.b, Is.EqualTo(.83f).Within(.0001f));
|
||||
var mesh = body.GetComponentInChildren<MeshFilter>().sharedMesh;
|
||||
Assert.That(mesh.bounds.size.x, Is.EqualTo(1).Within(.002f));
|
||||
var badge = AssetDatabase.LoadAssetAtPath<GameObject>(Root + "Prefabs/GroupBadge.prefab");
|
||||
Assert.That(badge.transform.Cast<Transform>().Count(child => child.name == "KnightEquipment"), Is.EqualTo(1));
|
||||
Assert.That(badge.GetComponentsInChildren<Collider>(true), Is.Empty);
|
||||
Assert.That(badge.GetComponentsInChildren<Animator>(true), Is.Empty);
|
||||
Assert.That(badge.GetComponentsInChildren<SkinnedMeshRenderer>(true), Is.Empty);
|
||||
Assert.That(badge.GetComponentsInChildren<SpriteRenderer>(true).Length, Is.EqualTo(1));
|
||||
foreach (var material in badge.transform.Find("KnightEquipment").GetComponentsInChildren<MeshRenderer>(true).SelectMany(item => item.sharedMaterials))
|
||||
Assert.That(AssetDatabase.GetAssetPath(material), Does.StartWith(Root + "Materials/VisualUpgrade/"));
|
||||
foreach (string name in new[] { "HeroBody", "KnightEquipment" })
|
||||
{
|
||||
var importer = (ModelImporter)AssetImporter.GetAtPath(Root + "Models/Heroes/" + name + ".fbx");
|
||||
Assert.That(importer.globalScale, Is.EqualTo(1));
|
||||
Assert.That(importer.bakeAxisConversion, Is.True); Assert.That(importer.importAnimation, Is.False);
|
||||
}
|
||||
}
|
||||
[Test] public void AllThirtyStagesConnectNaturalThemesWithOpenNearMeshCollidersAndColliderFreeDistantDecor()
|
||||
{
|
||||
foreach (int id in Enumerable.Range(1, 30))
|
||||
{
|
||||
var stage = AssetDatabase.LoadAssetAtPath<StageAsset>($"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage{id:D2}.asset");
|
||||
Assert.That(stage.Theme, Is.EqualTo(id <= 10 ? "Garden" : id <= 20 ? "Cloud" : "Workshop"));
|
||||
Assert.That(stage.Decorations.Any(item => item.prefab.name == "MeadowClearing"), Is.True);
|
||||
var arch = stage.Decorations.First(item => item.prefab.name == "OpenStoneArch").prefab;
|
||||
Assert.That(arch.GetComponentsInChildren<BoxCollider>(true), Is.Empty);
|
||||
Assert.That(arch.GetComponentsInChildren<MeshCollider>(true).All(item => !item.convex && item.sharedMesh != null), Is.True);
|
||||
Assert.That(arch.GetComponentsInChildren<MeshCollider>(true), Is.Not.Empty);
|
||||
foreach (var decoration in stage.Decorations)
|
||||
{
|
||||
string name = decoration.prefab.name;
|
||||
bool solid = name == "MeadowClearing" || name == "GentleHill" || name.Contains("Tree") || name.Contains("Limestone") || name == "SlateBoulder" || name == "OpenStoneArch" || name == "MossStoneSteps";
|
||||
var colliders = decoration.prefab.GetComponentsInChildren<Collider>(true);
|
||||
if (solid) Assert.That(colliders, Is.Not.Empty, name + " needs a shape-matched collider; runtime disables inaccessible instances, while authoring uses terrain for surface probes.");
|
||||
else Assert.That(colliders, Is.Empty, name + " is small vegetation or unreachable scenery.");
|
||||
foreach (var collider in colliders)
|
||||
{
|
||||
Assert.That(collider, Is.TypeOf<MeshCollider>());
|
||||
var meshCollider = (MeshCollider)collider;
|
||||
Assert.That(meshCollider.sharedMesh, Is.SameAs(collider.GetComponent<MeshFilter>().sharedMesh));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static StageAsset Stage(int id) => AssetDatabase.LoadAssetAtPath<StageAsset>($"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage{id:D2}.asset");
|
||||
private static GameObject[] Place(StageAsset stage) => stage.Decorations.Select(placement => {
|
||||
var item = Object.Instantiate(placement.prefab);
|
||||
item.transform.SetPositionAndRotation(placement.position, Quaternion.Euler(placement.eulerAngles));
|
||||
item.transform.localScale = placement.scale; return item;
|
||||
}).ToArray();
|
||||
private static bool Terrain(string name) => name == "MeadowClearing" || name == "GentleHill" || name == "DistantRidge" || name == "FarHorizon";
|
||||
private static float Surface(IEnumerable<Collider> colliders, float x, float z)
|
||||
{
|
||||
float top = float.NegativeInfinity;
|
||||
foreach (var collider in colliders)
|
||||
if (collider.Raycast(new Ray(new Vector3(x, 30, z), Vector3.down), out var hit, 60)) top = Mathf.Max(top, hit.point.y);
|
||||
return top;
|
||||
}
|
||||
[Test] public void AllPlacedRootsTouchTheActualImportedSurfaceIncludingHillSlopes()
|
||||
{
|
||||
for (int id = 1; id <= 30; id++)
|
||||
{
|
||||
var stage = Stage(id); var items = Place(stage);
|
||||
try
|
||||
{
|
||||
Physics.SyncTransforms();
|
||||
var terrain = items.Where((item, i) => stage.Decorations[i].prefab.name == "MeadowClearing" || stage.Decorations[i].prefab.name == "GentleHill")
|
||||
.SelectMany(item => item.GetComponentsInChildren<Collider>()).ToArray();
|
||||
for (int i = 0; i < items.Length; i++)
|
||||
{
|
||||
var placement = stage.Decorations[i]; if (Terrain(placement.prefab.name)) continue;
|
||||
float surface = Surface(terrain, placement.position.x, placement.position.z);
|
||||
float bottom = items[i].GetComponentsInChildren<MeshRenderer>().Min(renderer => renderer.bounds.min.y);
|
||||
Assert.That(float.IsNegativeInfinity(surface), Is.False, $"Stage {id}: {placement.prefab.name} has no supporting surface.");
|
||||
bool rock = placement.prefab.name == "RoundLimestone" || placement.prefab.name == "SlateBoulder" || placement.prefab.name == "LowLimestone";
|
||||
bool rootTree = placement.prefab.name == "RoundCanopyTree" || placement.prefab.name == "SageCanopyTree";
|
||||
Assert.That(bottom - surface, Is.InRange(rock || rootTree ? -.15f : -.03f, .03f), $"Stage {id}: {placement.prefab.name} floating or deeply buried.");
|
||||
if (rock)
|
||||
{
|
||||
var stone = items[i].GetComponentInChildren<MeshCollider>();
|
||||
// Check the stone itself, not nearby pebbles or its overall Bounds.
|
||||
Assert.That(stone.Raycast(new Ray(new Vector3(placement.position.x, surface + .02f, placement.position.z - 3), Vector3.forward), out _, 6), Is.True, "The main stone lower cap must meet the terrain.");
|
||||
float top = items[i].GetComponentsInChildren<MeshRenderer>().Max(renderer => renderer.bounds.max.y);
|
||||
Assert.That(top - surface, Is.GreaterThan((top - bottom) * .7f), "Only a shallow lower cap may be seated.");
|
||||
}
|
||||
}
|
||||
// Every reachable horizontal point has a real surface; no holes at the boundary corners.
|
||||
for (float x = stage.PlaygroundBounds.min.x; x <= stage.PlaygroundBounds.max.x; x += .5f)
|
||||
for (float z = stage.PlaygroundBounds.min.z; z <= stage.PlaygroundBounds.max.z; z += .5f)
|
||||
Assert.That(float.IsNegativeInfinity(Surface(terrain, x, z)), Is.False, $"Stage {id}: terrain gap {x},{z}.");
|
||||
}
|
||||
finally { foreach (var item in items) Object.DestroyImmediate(item); }
|
||||
}
|
||||
}
|
||||
private static IEnumerable<int> StageIds => Enumerable.Range(1, 30);
|
||||
[TestCaseSource(nameof(StageIds))] public void EveryAuthoredRootFootprintMeetsTerrainWithoutSlopeGapsOrDeepBurial(int id)
|
||||
{
|
||||
var stage = Stage(id); var items = Place(stage);
|
||||
try
|
||||
{
|
||||
Physics.SyncTransforms();
|
||||
var terrain = items.Where((item, i) => stage.Decorations[i].prefab.name == "MeadowClearing" || stage.Decorations[i].prefab.name == "GentleHill")
|
||||
.SelectMany(item => item.GetComponentsInChildren<Collider>()).ToArray();
|
||||
for (int i = 0; i < items.Length; i++)
|
||||
{
|
||||
var placement = stage.Decorations[i]; if (Terrain(placement.prefab.name)) continue;
|
||||
var vertices = items[i].GetComponentsInChildren<MeshFilter>().SelectMany(filter => filter.sharedMesh.vertices.Select(filter.transform.TransformPoint)).ToArray();
|
||||
float bottom = vertices.Min(point => point.y);
|
||||
bool rock = placement.prefab.name == "RoundLimestone" || placement.prefab.name == "SlateBoulder" || placement.prefab.name == "LowLimestone";
|
||||
// Use the actual imported lower roots/stems/cap vertices, not the center
|
||||
// or a Renderer AABB. A root naturally has some height above its authored
|
||||
// base; require the supporting ground under its full footprint to agree
|
||||
// with that base instead of tolerating slope-induced air or burial.
|
||||
foreach (var point in vertices.Where(point => point.y - bottom <= .12f * placement.scale.y))
|
||||
{
|
||||
float surface = Surface(terrain, point.x, point.z);
|
||||
float baseGap = (point.y - surface) - (point.y - bottom);
|
||||
bool rootTree = placement.prefab.name == "RoundCanopyTree" || placement.prefab.name == "SageCanopyTree";
|
||||
Assert.That(baseGap, Is.InRange(rock || rootTree ? -.16f : -.035f, .035f),
|
||||
$"Stage {id}: {placement.prefab.name} root footprint {point} floats or penetrates a slope (baseGap={baseGap}).");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally { foreach (var item in items) Object.DestroyImmediate(item); }
|
||||
}
|
||||
[TestCaseSource(nameof(StageIds))] public void EachOfTheFiveTreeRootFlaresActuallyTouchesItsSupportingGround(int id)
|
||||
{
|
||||
var stage = Stage(id); var items = Place(stage);
|
||||
try
|
||||
{
|
||||
Physics.SyncTransforms();
|
||||
var terrain = items.Where((item, i) => stage.Decorations[i].prefab.name == "MeadowClearing" || stage.Decorations[i].prefab.name == "GentleHill")
|
||||
.SelectMany(item => item.GetComponentsInChildren<Collider>()).ToArray();
|
||||
for (int i = 0; i < items.Length; i++)
|
||||
{
|
||||
var placement = stage.Decorations[i];
|
||||
if (placement.prefab.name != "RoundCanopyTree" && placement.prefab.name != "SageCanopyTree") continue;
|
||||
var vertices = items[i].GetComponentsInChildren<MeshFilter>().SelectMany(filter => filter.sharedMesh.vertices.Select(filter.transform.TransformPoint)).ToArray();
|
||||
float bottom = vertices.Min(point => point.y);
|
||||
// The authored five flares extend beyond the central trunk. Inspect their
|
||||
// actual lower end caps in all five directions, independently of the pivot.
|
||||
var feet = vertices.Where(point => point.y - bottom < .18f * placement.scale.y
|
||||
&& new Vector2(point.x - placement.position.x, point.z - placement.position.z).magnitude > .45f * placement.scale.x).ToArray();
|
||||
Assert.That(feet, Is.Not.Empty);
|
||||
var firstRoot = items[i].transform.InverseTransformPoint(feet[0]);
|
||||
float phase = Mathf.Atan2(firstRoot.z, firstRoot.x) * Mathf.Rad2Deg;
|
||||
for (int direction = 0; direction < 5; direction++)
|
||||
{
|
||||
var cap = feet.Where(point => {
|
||||
var local = items[i].transform.InverseTransformPoint(point);
|
||||
float angle = Mathf.Repeat(Mathf.Atan2(local.z, local.x) * Mathf.Rad2Deg - phase, 360);
|
||||
return Mathf.RoundToInt(angle / 72) % 5 == direction;
|
||||
}).ToArray();
|
||||
Assert.That(cap, Is.Not.Empty, "The approved root flare cap must be identifiable.");
|
||||
float gap = cap.Min(point => point.y - Surface(terrain, point.x, point.z));
|
||||
Assert.That(gap, Is.InRange(-.025f, .025f), $"Stage {id}: {placement.prefab.name} root {direction} end cap is floating or deeply buried (gap={gap}).");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally { foreach (var item in items) Object.DestroyImmediate(item); }
|
||||
}
|
||||
[Test] public void EveryOriginalSphereFitsAndCanBeSelectedFromAnAboveGroundView()
|
||||
{
|
||||
var spherePrefab = AssetDatabase.LoadAssetAtPath<GameObject>(Root + "Prefabs/MagnetFriend.prefab");
|
||||
for (int id = 1; id <= 30; id++)
|
||||
{
|
||||
var stage = Stage(id); var items = Place(stage); var balls = new List<GameObject>();
|
||||
try
|
||||
{
|
||||
var positions = new SerializedObject(stage).FindProperty("_ballPositions");
|
||||
for (int i = 0; i < positions.arraySize; i++)
|
||||
{
|
||||
var sphere = Object.Instantiate(spherePrefab); sphere.transform.position = positions.GetArrayElementAtIndex(i).vector3Value; balls.Add(sphere);
|
||||
}
|
||||
Physics.SyncTransforms();
|
||||
var obstacles = items.SelectMany(item => item.GetComponentsInChildren<Collider>()).ToArray();
|
||||
var allColliders = obstacles.Concat(balls.Select(ball => ball.GetComponent<SphereCollider>())).ToArray();
|
||||
foreach (var ball in balls)
|
||||
{
|
||||
var collider = ball.GetComponent<SphereCollider>(); var point = ball.transform.position;
|
||||
var bounds = stage.PlaygroundBounds;
|
||||
Assert.That(bounds.Contains(point - Vector3.one * collider.radius) && bounds.Contains(point + Vector3.one * collider.radius), Is.True, $"Stage {id}: original sphere outside playground.");
|
||||
foreach (var obstacle in obstacles)
|
||||
Assert.That(Physics.ComputePenetration(collider, point, Quaternion.identity, obstacle, obstacle.transform.position, obstacle.transform.rotation, out _, out float depth) && depth > .005f, Is.False, $"Stage {id}: sphere at {point} buried in {obstacle.name}.");
|
||||
bool selectable = false;
|
||||
foreach (float pitch in new[] { 20f, 45f, 70f })
|
||||
for (int yaw = 0; yaw < 360 && !selectable; yaw += 30)
|
||||
{
|
||||
var rotation = Quaternion.Euler(pitch, yaw, 0);
|
||||
var origin = new Vector3(0, 1.25f, 0) + rotation * new Vector3(0, 0, -12);
|
||||
var local = Quaternion.Inverse(rotation) * (point - origin);
|
||||
if (local.z <= 0 || Mathf.Abs(local.x / local.z) > Mathf.Tan(31 * Mathf.Deg2Rad) * 16f / 9 || Mathf.Abs(local.y / local.z) > Mathf.Tan(31 * Mathf.Deg2Rad)) continue;
|
||||
var ray = new Ray(origin, (point - origin).normalized);
|
||||
Collider first = null; float nearest = float.PositiveInfinity;
|
||||
foreach (var candidate in allColliders)
|
||||
if (candidate.Raycast(ray, out var hit, 30) && hit.distance < nearest) { nearest = hit.distance; first = candidate; }
|
||||
selectable |= first == collider;
|
||||
}
|
||||
Assert.That(selectable, Is.True, $"Stage {id}: original sphere {point} has no selectable view.");
|
||||
}
|
||||
}
|
||||
finally { foreach (var ball in balls) Object.DestroyImmediate(ball); foreach (var item in items) Object.DestroyImmediate(item); }
|
||||
}
|
||||
}
|
||||
[Test] public void ArchHoleIsOpenWhileItsStonePillarStillBlocksARay()
|
||||
{
|
||||
var arch = Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>(Root + "Prefabs/Meadow/OpenStoneArch.prefab"));
|
||||
try
|
||||
{
|
||||
Physics.SyncTransforms(); var colliders = arch.GetComponentsInChildren<Collider>();
|
||||
bool Hit(float x) => colliders.Any(collider => collider.Raycast(new Ray(new Vector3(x, .8f, -3), Vector3.forward), out _, 6));
|
||||
Assert.That(Hit(0), Is.False, "The visible arch opening must stay open.");
|
||||
Assert.That(Hit(1.18f), Is.True, "The visible stone pillar must remain solid.");
|
||||
}
|
||||
finally { Object.DestroyImmediate(arch); }
|
||||
}
|
||||
[Test] public void AVisiblePassableNaturalOutlineMarksEveryPlaygroundSide()
|
||||
{
|
||||
for (int id = 1; id <= 30; id++)
|
||||
{
|
||||
var stage = Stage(id); var bounds = stage.PlaygroundBounds;
|
||||
var passable = stage.Decorations.Where(item => item.prefab.GetComponentsInChildren<Collider>().Length == 0 && !Terrain(item.prefab.name)).ToArray();
|
||||
foreach (float x in new[] { bounds.min.x, bounds.max.x })
|
||||
Assert.That(passable.Count(item => Mathf.Abs(item.position.x - x) < .05f && item.position.z >= bounds.min.z && item.position.z <= bounds.max.z), Is.GreaterThan(5), "The side edge must be visually marked without obstructing small vegetation.");
|
||||
foreach (float z in new[] { bounds.min.z, bounds.max.z })
|
||||
Assert.That(passable.Count(item => Mathf.Abs(item.position.z - z) < .05f && item.position.x >= bounds.min.x && item.position.x <= bounds.max.x), Is.GreaterThan(5), "The front/back edge must be visually marked.");
|
||||
}
|
||||
}
|
||||
[Test] public void SixStageGroupsHaveDifferentNaturalCompositionsAndFiveFixedVariations()
|
||||
{
|
||||
var primary = new[] { "DaisyColony", "RoundCanopyTree", "PinkCosmosColony", "FernCluster", "RoundLimestone", "OpenStoneArch" };
|
||||
var groupCompositions = new HashSet<string>();
|
||||
for (int group = 0; group < 6; group++)
|
||||
{
|
||||
var layouts = new HashSet<string>();
|
||||
for (int variant = 0; variant < 5; variant++)
|
||||
{
|
||||
var stage = Stage(group * 5 + variant + 1);
|
||||
Assert.That(stage.Decorations.Any(placement => placement.prefab.name == primary[group]), Is.True);
|
||||
layouts.Add(string.Join(";", stage.Decorations.Select(placement => placement.prefab.name + JsonUtility.ToJson(placement.position) + JsonUtility.ToJson(placement.scale) + JsonUtility.ToJson(placement.eulerAngles))));
|
||||
if (variant == 0) groupCompositions.Add(string.Join(";", stage.Decorations.GroupBy(placement => placement.prefab.name).OrderBy(items => items.Key).Select(items => items.Key + ":" + items.Count())));
|
||||
}
|
||||
Assert.That(layouts.Count, Is.EqualTo(5), "Fixed stages must not repeat exactly the same layout.");
|
||||
}
|
||||
Assert.That(groupCompositions.Count, Is.EqualTo(6), "Changing only colors or positions must not disguise identical compositions.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61d4021e6d4aace41964197d9377009b
|
||||
@@ -33,7 +33,7 @@ namespace MagnetismHeroes.Tests
|
||||
foreach (var listener in Object.FindObjectsByType<AudioListener>(FindObjectsSortMode.None)) listener.enabled = false;
|
||||
foreach (var camera in Object.FindObjectsByType<Camera>(FindObjectsSortMode.None)) camera.enabled = false;
|
||||
_app = Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GameApp.prefab")).GetComponent<GameApp>();
|
||||
_store = new SpyStore(); _app.Initialize(_store); _touchscreen = InputSystem.AddDevice<Touchscreen>(); yield return null;
|
||||
_store = new SpyStore(); _app.Initialize(_store); _app.Presenter.BindEnvironment(_app.Environment); _touchscreen = InputSystem.AddDevice<Touchscreen>(); yield return null;
|
||||
}
|
||||
[UnityTearDown] public IEnumerator TearDown()
|
||||
{
|
||||
@@ -183,15 +183,42 @@ namespace MagnetismHeroes.Tests
|
||||
yield return new WaitForSecondsRealtime(.4f);
|
||||
Assert.That(_app.Presenter.Balls.Values.All(ball => !ball.transform.Find("ContactLight").GetComponent<LineRenderer>().enabled), Is.True);
|
||||
}
|
||||
private void OrbitUntilOriginalBallsAreFirstHits(params string[] ids)
|
||||
{
|
||||
var start = new Vector2(Screen.width * .1f, Screen.height * .8f);
|
||||
if (_app.Presenter.Camera.transform.forward.y > -.1f)
|
||||
{ _app.Input.Begin(start, false); _app.Input.Move(start - new Vector2(0, 200)); _app.Input.End(start - new Vector2(0, 200)); }
|
||||
for (int pitch = 0; pitch < 2; pitch++)
|
||||
{
|
||||
for (int yaw = 0; yaw < 12; yaw++)
|
||||
{
|
||||
Physics.SyncTransforms();
|
||||
if (Mathf.Abs(Vector3.Dot(_app.Presenter.Balls[ids[0]].transform.position - _app.Presenter.Balls[ids[1]].transform.position, _app.Presenter.Camera.transform.forward)) < .02f && ids.All(id =>
|
||||
{
|
||||
var point = Point(id);
|
||||
return point.x > 0 && point.x < Screen.width && point.y > 0 && point.y < Screen.height
|
||||
&& Physics.Raycast(_app.Presenter.Camera.ScreenPointToRay(point), out var hit, 100)
|
||||
&& hit.collider == _app.Presenter.Balls[id].GetComponent<SphereCollider>();
|
||||
})) return;
|
||||
_app.Input.Begin(start, false); _app.Input.Move(start + new Vector2(120, 0)); _app.Input.End(start + new Vector2(120, 0));
|
||||
}
|
||||
if (pitch == 0)
|
||||
{ _app.Input.Begin(start, false); _app.Input.Move(start - new Vector2(0, 40)); _app.Input.End(start - new Vector2(0, 40)); }
|
||||
}
|
||||
Assert.Fail("The original requested IDs must be reachable by an above-ground empty-space orbit.");
|
||||
}
|
||||
[UnityTest] public IEnumerator FourteenDisconnectedSelectionThenMergeIsAcceptedByFinalNumbers()
|
||||
{
|
||||
_app.StartStage(14); yield return null; Physics.SyncTransforms(); var group = _app.Session.Board.Groups[0];
|
||||
OrbitUntilOriginalBallsAreFirstHits(group.ballIds[0], group.ballIds[2]);
|
||||
Tap(group.ballIds[0]); _app.Input.BeginSplit(); Tap(group.ballIds[0]); Tap(group.ballIds[2]);
|
||||
Assert.That(_app.Input.SelectedCount, Is.EqualTo(2)); var point = Point(group.ballIds[0]);
|
||||
var endpoint = point + new Vector2(0, 65); _app.Input.Begin(point, false); _app.Input.Move(endpoint); _app.Input.End(endpoint); yield return null;
|
||||
Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.Puzzle));
|
||||
Assert.That(_app.Session.Board.Groups.Select(value => value.ballIds.Length).OrderBy(value => value), Is.EqualTo(new[] { 1, 1, 2 }));
|
||||
var singles = _app.Session.Board.Groups.Where(value => value.ballIds.Length == 1).ToArray(); var writes = _store.Writes;
|
||||
Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True);
|
||||
var singles = _app.Session.Board.Groups.Where(value => value.ballIds.Length == 1).ToArray();
|
||||
OrbitUntilOriginalBallsAreFirstHits(singles.Select(value => value.ballIds[0]).ToArray()); var writes = _store.Writes;
|
||||
point = Point(singles[0].ballIds[0]); endpoint = Point(singles[1].ballIds[0]) - new Vector2(35, 0);
|
||||
_app.Input.Begin(point, false); _app.Input.Move(endpoint); _app.Input.End(endpoint); yield return null;
|
||||
Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.FreePlay)); Assert.That(_store.Writes, Is.EqualTo(writes + 1));
|
||||
|
||||
@@ -272,9 +272,13 @@ namespace MagnetismHeroes.Tests
|
||||
[UnityTest] public IEnumerator MeshArchReallyHasAnOpenGapAndHelpHighlightsOnlyCurrentIdsWithoutExecuting()
|
||||
{
|
||||
_app.StartStage(14); yield return null; Physics.SyncTransforms();
|
||||
var arch=_app.Environment.GetComponentsInChildren<MeshCollider>().First(collider=>collider.sharedMesh.name.Contains("BlockArch"));
|
||||
var local=new Ray(new Vector3(0,1,-3),Vector3.forward);
|
||||
Assert.That(arch.Raycast(new Ray(arch.transform.parent.TransformPoint(local.origin),Vector3.forward),out _,6), Is.False,"No box collider can close the Blender arch gap.");
|
||||
var arch=_app.Environment.GetComponentsInChildren<Transform>().Single(item=>item.name=="OpenStoneArch(Clone)");
|
||||
var colliders=arch.GetComponentsInChildren<MeshCollider>();
|
||||
Assert.That(colliders, Is.Not.Empty);
|
||||
Assert.That(arch.GetComponentsInChildren<BoxCollider>(), Is.Empty);
|
||||
Ray ArchRay(float x) => new Ray(arch.TransformPoint(new Vector3(x,1,-3)),arch.TransformDirection(Vector3.forward));
|
||||
Assert.That(colliders.Any(collider=>collider.Raycast(ArchRay(0),out _,6)), Is.False,"Actual approved arch gap remains open.");
|
||||
Assert.That(colliders.Any(collider=>collider.Raycast(ArchRay(1.1f),out _,6)), Is.True,"Actual approved arch side blocks input rays.");
|
||||
var before=JsonUtility.ToJson(_store.Saved); var hint=_app.Session.Hint; _app.ShowHelp();
|
||||
Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(before));
|
||||
Assert.That(_app.Presenter.Balls.Values.Where(ball=>ball.Hinted).Select(ball=>ball.BallId), Is.EquivalentTo(hint.BallIds));
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using MagnetismHeroes.Core;
|
||||
using MagnetismHeroes.UnityRuntime.Bootstrap;
|
||||
using MagnetismHeroes.UnityRuntime.Presentation;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
namespace MagnetismHeroes.Tests
|
||||
{
|
||||
public sealed class DragCameraPlayTests
|
||||
{
|
||||
private sealed class MemoryStore : IProgressStore
|
||||
{
|
||||
public ProgressSave Save = new ProgressSave { completedStageIds = Enumerable.Range(1, 30).ToArray(), seenTutorialIds = new[] { "merge", "split" } };
|
||||
public int Writes;
|
||||
public bool TryLoad(out ProgressSave save, out string error) { save = Save; error = null; return true; }
|
||||
public bool TrySave(ProgressSave save, out string error) { Save = save; Writes++; error = null; return true; }
|
||||
}
|
||||
private GameApp _app;
|
||||
private MemoryStore _store;
|
||||
private readonly System.Collections.Generic.List<Collider> _disabledScenery = new System.Collections.Generic.List<Collider>();
|
||||
[UnitySetUp] public IEnumerator Setup()
|
||||
{
|
||||
foreach (var listener in Object.FindObjectsByType<AudioListener>(FindObjectsSortMode.None)) listener.enabled = false;
|
||||
foreach (var camera in Object.FindObjectsByType<Camera>(FindObjectsSortMode.None)) camera.enabled = false;
|
||||
_app = Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GameApp.prefab")).GetComponent<GameApp>();
|
||||
_store = new MemoryStore(); _app.Initialize(_store); _app.StartStage(6); _app.Voice.Clear();
|
||||
_disabledScenery.Clear();
|
||||
foreach (var collider in _app.Environment.GetComponentsInChildren<Collider>())
|
||||
if (!collider.isTrigger && !collider.name.Contains("Clearing") && collider.enabled) { _disabledScenery.Add(collider); collider.enabled = false; }
|
||||
yield return null;
|
||||
}
|
||||
[UnityTearDown] public IEnumerator Teardown() { Object.Destroy(_app.gameObject); yield return null; }
|
||||
private void Inject(params Vector3[] positions)
|
||||
{
|
||||
var rotation = Quaternion.Euler(12, 0, 0);
|
||||
var camera = new CameraState { target = new Position3(0, 2, 0), distance = 8, rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w) };
|
||||
var balls = positions.Select((point, index) => new BallState { id = "camera-" + index, position = BoardPresenter.ToPosition(point) }).ToArray();
|
||||
_store.Save.latestFreePlay = new FreePlayState { sourceStageId = 6, balls = balls,
|
||||
groups = balls.Select((ball, index) => new GroupState { id = "group-" + index, ballIds = new[] { ball.id } }).ToArray(), camera = camera };
|
||||
_app.Initialize(_store); _app.ContinueFreePlay(); _app.Voice.Clear(); Physics.SyncTransforms();
|
||||
}
|
||||
private Vector2 Point(string id) => _app.Presenter.Camera.WorldToScreenPoint(_app.Presenter.Balls[id].transform.position);
|
||||
[UnityTest] public IEnumerator ActualDragMovesCameraWhileStationaryFingerPreservesPreviewAndCommittedBoard()
|
||||
{
|
||||
Inject(new Vector3(-1, 2, 0), new Vector3(.5f, 2, 2));
|
||||
var camera = _app.Presenter.Camera; var initialRotation = camera.transform.rotation;
|
||||
var saved = JsonUtility.ToJson(_store.Save); int writes = _store.Writes;
|
||||
var board = JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Session.Camera));
|
||||
var manual = JsonUtility.ToJson(_app.Orbit.Snapshot());
|
||||
var start = Point("camera-0"); var pointer = start + new Vector2(22, 0);
|
||||
_app.Input.Begin(start, false); _app.Input.Move(pointer);
|
||||
var preview = _app.Presenter.Balls["camera-0"].transform.position;
|
||||
float until = Time.realtimeSinceStartup + .7f; float sampleAt = 0;
|
||||
var frames = new System.Collections.Generic.List<string> { "elapsed,cameraX,cameraY,cameraZ,rotationX,rotationY,rotationZ,rotationW,previewX,previewY,previewZ,saveWrites" };
|
||||
while (Time.realtimeSinceStartup < until)
|
||||
{
|
||||
yield return null; _app.Input.Move(pointer);
|
||||
if (Time.realtimeSinceStartup < sampleAt) continue;
|
||||
sampleAt = Time.realtimeSinceStartup + .02f;
|
||||
var pose = camera.transform; var position = _app.Presenter.Balls["camera-0"].transform.position;
|
||||
frames.Add($"{Time.realtimeSinceStartup},{pose.position.x},{pose.position.y},{pose.position.z},{pose.rotation.x},{pose.rotation.y},{pose.rotation.z},{pose.rotation.w},{position.x},{position.y},{position.z},{_store.Writes}");
|
||||
}
|
||||
WriteRecords("camera-motion-stationary-finger.csv", frames);
|
||||
Assert.That(Quaternion.Angle(initialRotation, camera.transform.rotation), Is.GreaterThan(3), "A real friend drag must smoothly choose a view that reveals depth.");
|
||||
Assert.That(_app.Presenter.Balls["camera-0"].transform.position, Is.EqualTo(preview));
|
||||
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Session.Camera)), Is.EqualTo(board));
|
||||
Assert.That(JsonUtility.ToJson(_app.Orbit.Snapshot()), Is.EqualTo(manual));
|
||||
Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(saved)); Assert.That(_store.Writes, Is.EqualTo(writes));
|
||||
_app.Input.Cancel();
|
||||
yield return new WaitForSecondsRealtime(1.5f);
|
||||
Assert.That(Quaternion.Angle(initialRotation, camera.transform.rotation), Is.LessThan(.1f));
|
||||
Assert.That(_app.Presenter.Balls["camera-0"].transform.position, Is.EqualTo(BoardPresenter.ToVector(_app.Session.Board.Balls[0].position)));
|
||||
}
|
||||
[UnityTest] public IEnumerator DeepFriendsCanActuallyMeetAndMergeThroughTheRebasedInputPlane()
|
||||
{
|
||||
Inject(new Vector3(-1.5f, 2, -1), new Vector3(.5f, 2, 2));
|
||||
var initial = _app.Session.Board.Balls.Select(ball => ball.id).ToArray();
|
||||
var start = Point("camera-0"); var pointer = start + new Vector2(20, 0);
|
||||
_app.Input.Begin(start, false); _app.Input.Move(pointer);
|
||||
yield return new WaitForSecondsRealtime(.6f);
|
||||
var camera = _app.Presenter.Camera;
|
||||
var records = new System.Collections.Generic.List<string> { "frame,worldX,worldY,worldZ,pointerX,pointerY,cameraX,cameraY,cameraZ,distance,candidate" };
|
||||
float until = Time.realtimeSinceStartup + 4; int frame = 0;
|
||||
while (_app.Presenter.MergeCandidate == null && Time.realtimeSinceStartup < until)
|
||||
{
|
||||
var source = _app.Presenter.Balls["camera-0"].transform.position;
|
||||
var target = _app.Presenter.Balls["camera-1"].transform.position;
|
||||
var screenDelta = (Vector2)(camera.WorldToScreenPoint(target) - camera.WorldToScreenPoint(source));
|
||||
pointer += Vector2.ClampMagnitude(screenDelta, 3);
|
||||
_app.Input.Move(pointer);
|
||||
float distance = Vector3.Distance(_app.Presenter.Balls["camera-0"].transform.position, target);
|
||||
if (frame++ % 20 == 0) records.Add($"{frame},{source.x},{source.y},{source.z},{pointer.x},{pointer.y},{camera.transform.position.x},{camera.transform.position.y},{camera.transform.position.z},{distance},{_app.Presenter.MergeCandidate}");
|
||||
yield return null;
|
||||
}
|
||||
WriteRecords("depth-drag.csv", records);
|
||||
Assert.That(_app.Presenter.MergeCandidate, Is.EqualTo("group-1"), "Screen overlap is insufficient: the actual safe 3D spheres must reach contact.");
|
||||
var preview = _app.Presenter.Balls.ToDictionary(pair => pair.Key, pair => pair.Value.transform.position);
|
||||
Assert.That(Vector3.Distance(preview["camera-0"], preview["camera-1"]), Is.EqualTo(1).Within(.001));
|
||||
Assert.That(_app.Session.Board.Groups.Length, Is.EqualTo(2));
|
||||
_app.Input.End(pointer); yield return null;
|
||||
Assert.That(_app.Session.Board.Groups.Single().ballIds, Is.EquivalentTo(initial));
|
||||
foreach (var ball in _app.Session.Board.Balls) Assert.That(BoardPresenter.ToVector(ball.position), Is.EqualTo(preview[ball.id]));
|
||||
}
|
||||
private static void WriteRecords(string name, System.Collections.Generic.IEnumerable<string> records)
|
||||
{
|
||||
string directory = System.Environment.GetEnvironmentVariable("MAGNETISM_DRAG_CAMERA_EVIDENCE");
|
||||
if (string.IsNullOrEmpty(directory)) return;
|
||||
System.IO.Directory.CreateDirectory(directory); System.IO.File.WriteAllLines(System.IO.Path.Combine(directory, name), records);
|
||||
}
|
||||
[UnityTest] public IEnumerator CandidateHysteresisAvoidsFrameByFrameSwitchingAndFarFriendsAreNotTracked()
|
||||
{
|
||||
Inject(new Vector3(-1, 2, 0), new Vector3(1, 2, 1), new Vector3(1.05f, 2, -1));
|
||||
var pointer = Point("camera-0"); _app.Input.Begin(pointer, false); pointer += new Vector2(22, 0); _app.Input.Move(pointer);
|
||||
yield return new WaitForSecondsRealtime(.1f);
|
||||
string candidate = _app.Orbit.TrackedFriend; Assert.That(candidate, Is.Not.Null);
|
||||
for (int index = 0; index < 20; index++)
|
||||
{
|
||||
pointer += new Vector2(index % 2 == 0 ? 1 : -1, 0); _app.Input.Move(pointer); yield return null;
|
||||
Assert.That(_app.Orbit.TrackedFriend, Is.EqualTo(candidate));
|
||||
}
|
||||
_app.Input.Cancel();
|
||||
Inject(new Vector3(-4, 2, 0), new Vector3(4, 2, 0));
|
||||
pointer = Point("camera-0"); _app.Input.Begin(pointer, false); _app.Input.Move(pointer + new Vector2(22, 0));
|
||||
yield return new WaitForSecondsRealtime(.2f);
|
||||
Assert.That(_app.Orbit.TrackedFriend, Is.Null);
|
||||
}
|
||||
[UnityTest] public IEnumerator TapThresholdUiAndSplitSelectionDoNotStartCameraAssistance()
|
||||
{
|
||||
Inject(new Vector3(-1, 2, 0), new Vector3(1, 2, 0));
|
||||
var camera = _app.Presenter.Camera; var position = camera.transform.position; var rotation = camera.transform.rotation;
|
||||
var point = Point("camera-0");
|
||||
_app.Input.Begin(point, false); _app.Input.Move(point + new Vector2(10, 0)); yield return new WaitForSecondsRealtime(.1f); _app.Input.End(point);
|
||||
Assert.That(_app.Orbit.IsAssisting, Is.False);
|
||||
_app.Input.Begin(point, true); _app.Input.Move(point + new Vector2(100, 0)); yield return new WaitForSecondsRealtime(.1f); _app.Input.End(point);
|
||||
Assert.That(camera.transform.position, Is.EqualTo(position)); Assert.That(camera.transform.rotation, Is.EqualTo(rotation));
|
||||
_store.Save.latestFreePlay.groups = new[] { new GroupState { id = "group-0", ballIds = new[] { "camera-0", "camera-1" } } };
|
||||
_app.Initialize(_store); _app.ContinueFreePlay(); _app.Voice.Clear(); Physics.SyncTransforms();
|
||||
point = Point("camera-0"); _app.Input.Begin(point, false); _app.Input.End(point); _app.Input.BeginSplit();
|
||||
_app.Input.Begin(point, false); _app.Input.End(point); yield return new WaitForSecondsRealtime(.1f);
|
||||
Assert.That(_app.Input.SelectedIds, Is.EquivalentTo(new[] { "camera-0" })); Assert.That(_app.Orbit.IsAssisting, Is.False);
|
||||
Assert.That(camera.transform.position, Is.EqualTo(position)); Assert.That(camera.transform.rotation, Is.EqualTo(rotation));
|
||||
}
|
||||
[UnityTest] public IEnumerator SplitDragAssistsAndReturnsWithPreviewExactlyMatchingCommittedBothHalves()
|
||||
{
|
||||
Inject(new Vector3(-1, 2, 0), new Vector3(0, 2, 0), new Vector3(1, 2, 0));
|
||||
_store.Save.latestFreePlay.groups = new[] { new GroupState { id = "group-0", ballIds = new[] { "camera-0", "camera-1", "camera-2" } } };
|
||||
_app.Initialize(_store); _app.ContinueFreePlay(); _app.Voice.Clear(); Physics.SyncTransforms();
|
||||
var camera = _app.Presenter.Camera; var original = camera.transform.rotation; var manual = JsonUtility.ToJson(_app.Orbit.Snapshot());
|
||||
var point = Point("camera-0"); _app.Input.Begin(point, false); _app.Input.End(point); _app.Input.BeginSplit();
|
||||
_app.Input.Begin(point, false); _app.Input.End(point); _app.Input.Begin(point, false); _app.Input.Move(point + new Vector2(-55, 25));
|
||||
yield return new WaitForSecondsRealtime(.4f);
|
||||
Assert.That(_app.Orbit.IsAssisting, Is.True); Assert.That(Quaternion.Angle(original, camera.transform.rotation), Is.GreaterThan(2));
|
||||
Assert.That(_app.Session.Board.Groups.Length, Is.EqualTo(1));
|
||||
var preview = _app.Presenter.Balls.ToDictionary(pair => pair.Key, pair => pair.Value.transform.position);
|
||||
Assert.That(JsonUtility.ToJson(_app.Orbit.Snapshot()), Is.EqualTo(manual));
|
||||
_app.Input.End(point + new Vector2(-55, 25)); yield return null;
|
||||
Assert.That(_app.Session.Board.Groups.Length, Is.EqualTo(2));
|
||||
Assert.That(_app.Session.Board.Balls.Select(ball => ball.id), Is.EquivalentTo(preview.Keys));
|
||||
foreach (var ball in _app.Session.Board.Balls) Assert.That(BoardPresenter.ToVector(ball.position), Is.EqualTo(preview[ball.id]));
|
||||
Assert.That(JsonUtility.ToJson(_store.Save.latestFreePlay.camera), Is.EqualTo(manual));
|
||||
yield return new WaitForSecondsRealtime(1.5f); Assert.That(Quaternion.Angle(original, camera.transform.rotation), Is.LessThan(.1));
|
||||
}
|
||||
[UnityTest] public IEnumerator CancelPinchOutsidePauseAndNavigationRestorePreviewCameraAndSavedState()
|
||||
{
|
||||
foreach (var mode in new[] { "cancel", "pinch", "outside", "pause", "navigation" })
|
||||
{
|
||||
Inject(new Vector3(-1, 2, 0), new Vector3(1, 2, 1));
|
||||
var camera = _app.Presenter.Camera; var original = camera.transform.rotation;
|
||||
var saved = JsonUtility.ToJson(_store.Save); var manual = JsonUtility.ToJson(_app.Orbit.Snapshot());
|
||||
var point = Point("camera-0"); _app.Input.Begin(point, false); _app.Input.Move(point + new Vector2(24, 0));
|
||||
yield return new WaitForSecondsRealtime(.25f);
|
||||
Assert.That(Quaternion.Angle(original, camera.transform.rotation), Is.GreaterThan(2), mode);
|
||||
switch (mode)
|
||||
{
|
||||
case "cancel": _app.Input.Cancel(); break;
|
||||
case "pinch": _app.Input.BeginPinch(100, false); _app.Input.Pinch(125); _app.Input.End(point, true); break;
|
||||
case "outside": _app.Input.Move(new Vector2(-1, point.y)); break;
|
||||
case "pause": _app.Input.SendMessage("OnApplicationPause", true); break;
|
||||
case "navigation": _app.ShowHome(); _app.ContinueFreePlay(); break;
|
||||
}
|
||||
yield return new WaitForSecondsRealtime(1.5f);
|
||||
Assert.That(_app.Orbit.IsAssisting, Is.False, mode); Assert.That(Quaternion.Angle(original, camera.transform.rotation), Is.LessThan(.1), mode);
|
||||
Assert.That(JsonUtility.ToJson(_app.Orbit.Snapshot()), Is.EqualTo(manual), mode);
|
||||
Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(saved), mode);
|
||||
foreach (var ball in _app.Session.Board.Balls) Assert.That(_app.Presenter.Balls[ball.id].transform.position, Is.EqualTo(BoardPresenter.ToVector(ball.position)), mode);
|
||||
}
|
||||
}
|
||||
[UnityTest] public IEnumerator RegrabDuringReturnDoesNotSnapCameraOrChangeFirstHitSelection()
|
||||
{
|
||||
Inject(new Vector3(-1, 2, 0), new Vector3(1, 2, 1));
|
||||
var point = Point("camera-0"); _app.Input.Begin(point, false); _app.Input.Move(point + new Vector2(25, 0));
|
||||
yield return new WaitForSecondsRealtime(.4f); _app.Input.Cancel(); yield return null;
|
||||
var camera = _app.Presenter.Camera; var position = camera.transform.position; var rotation = camera.transform.rotation;
|
||||
point = Point("camera-0"); Physics.SyncTransforms();
|
||||
Assert.That(Physics.Raycast(camera.ScreenPointToRay(point), out var hit, 100), Is.True);
|
||||
Assert.That(hit.collider.GetComponent<BallView>().BallId, Is.EqualTo("camera-0"));
|
||||
_app.Input.Begin(point, false);
|
||||
Assert.That(camera.transform.position, Is.EqualTo(position)); Assert.That(camera.transform.rotation, Is.EqualTo(rotation));
|
||||
_app.Input.End(point); Assert.That(_app.Input.SelectedGroup, Is.EqualTo("group-0"));
|
||||
}
|
||||
[UnityTest] public IEnumerator ValidManualRolledPoseRoundTripsAndInsidePropPoseIsCorrected()
|
||||
{
|
||||
Inject(new Vector3(0, 2, 0));
|
||||
var rotation = Quaternion.Euler(25, 135, 45);
|
||||
var state = new CameraState { target = new Position3(0, 2, 0), distance = 7, rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w) };
|
||||
_app.Orbit.Apply(state);
|
||||
Assert.That(Quaternion.Angle(rotation, _app.Presenter.Camera.transform.rotation), Is.LessThan(.001f));
|
||||
_app.Session.CommitCamera(_app.Orbit.Snapshot());
|
||||
var snapshot = JsonUtility.ToJson(_app.Orbit.Snapshot()); _app.ShowHome(); _app.ContinueFreePlay();
|
||||
Assert.That(JsonUtility.ToJson(_app.Orbit.Snapshot()), Is.EqualTo(snapshot));
|
||||
var obstacle = new GameObject("camera-test-solid"); obstacle.transform.SetParent(_app.Environment.transform); obstacle.transform.position = _app.Presenter.Camera.transform.position;
|
||||
obstacle.AddComponent<BoxCollider>().size = Vector3.one * 2; Physics.SyncTransforms();
|
||||
var balls = _app.Session.Board.Balls.Select(ball => (ball.id, ball.position)).ToArray();
|
||||
_app.Orbit.Apply(state); yield return null;
|
||||
Assert.That(obstacle.GetComponent<Collider>().bounds.Contains(_app.Presenter.Camera.transform.position), Is.False);
|
||||
Assert.That(_app.Session.Board.Balls.Select(ball => (ball.id, ball.position)), Is.EqualTo(balls));
|
||||
Object.Destroy(obstacle);
|
||||
}
|
||||
[UnityTest] public IEnumerator PinchTakesVisiblePoseWithoutSnapAndSavesOnlyAfterActualManualZoom()
|
||||
{
|
||||
Inject(new Vector3(-1, 2, 0), new Vector3(1, 2, 1));
|
||||
var point = Point("camera-0"); _app.Input.Begin(point, false); _app.Input.Move(point + new Vector2(25, 0));
|
||||
yield return new WaitForSecondsRealtime(.4f);
|
||||
var camera = _app.Presenter.Camera; var position = camera.transform.position; var rotation = camera.transform.rotation;
|
||||
var saved = JsonUtility.ToJson(_store.Save); int writes = _store.Writes;
|
||||
_app.Input.BeginPinch(100, false);
|
||||
Assert.That(camera.transform.position, Is.EqualTo(position)); Assert.That(camera.transform.rotation, Is.EqualTo(rotation));
|
||||
Assert.That(_app.Orbit.IsAssisting, Is.False); Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(saved));
|
||||
_app.Input.Pinch(110);
|
||||
Assert.That(Quaternion.Angle(camera.transform.rotation, rotation), Is.LessThan(.001f));
|
||||
Assert.That(Vector3.Distance(camera.transform.position, position), Is.LessThan(1));
|
||||
var manual = JsonUtility.ToJson(_app.Orbit.Snapshot()); Assert.That(_store.Writes, Is.EqualTo(writes));
|
||||
_app.Input.End(point); Assert.That(_store.Writes, Is.EqualTo(writes + 1));
|
||||
Assert.That(JsonUtility.ToJson(_store.Save.latestFreePlay.camera), Is.EqualTo(manual));
|
||||
_app.ShowHome(); _app.ContinueFreePlay(); Assert.That(JsonUtility.ToJson(_app.Orbit.Snapshot()), Is.EqualTo(manual));
|
||||
}
|
||||
[UnityTest] public IEnumerator NearbyFriendBehindAThinPropIsNotAnInteractionCameraCandidate()
|
||||
{
|
||||
Inject(new Vector3(-1, 2, 0), new Vector3(1.5f, 2, 1));
|
||||
var wall = new GameObject("camera-test-blocked-approach"); wall.transform.SetParent(_app.Environment.transform);
|
||||
wall.transform.position = new Vector3(.3f, 2, .5f); wall.AddComponent<BoxCollider>().size = new Vector3(.02f, 5, 5); Physics.SyncTransforms();
|
||||
var point = Point("camera-0"); _app.Input.Begin(point, false); _app.Input.Move(point + new Vector2(22, 0));
|
||||
yield return new WaitForSecondsRealtime(.2f);
|
||||
Assert.That(_app.Orbit.IsAssisting, Is.True); Assert.That(_app.Orbit.TrackedFriend, Is.Null); Assert.That(_app.Presenter.MergeCandidate, Is.Null);
|
||||
_app.Input.Cancel(); Object.Destroy(wall);
|
||||
}
|
||||
[UnityTest] public IEnumerator EntireRequestedPitchOrbitBlockedByLargePropStillFindsAnExteriorCamera()
|
||||
{
|
||||
Inject(new Vector3(0, 2, 0));
|
||||
var state = _app.Orbit.Snapshot(); var box = new GameObject("camera-test-large-terrain"); box.transform.SetParent(_app.Environment.transform);
|
||||
box.transform.position = new Vector3(0, 10, -7); box.AddComponent<BoxCollider>().size = new Vector3(12, 25, 12); Physics.SyncTransforms();
|
||||
_app.Orbit.Apply(state); yield return null;
|
||||
var camera = _app.Presenter.Camera;
|
||||
Assert.That(box.GetComponent<Collider>().bounds.Contains(camera.transform.position), Is.False);
|
||||
Assert.That(camera.transform.position.y, Is.GreaterThan(_app.Environment.PlaygroundBounds.min.y));
|
||||
Assert.That(_app.Orbit.Snapshot().distance, Is.InRange(3, 15));
|
||||
Assert.That(_app.Session.Board.Balls.Single().id, Is.EqualTo("camera-0"));
|
||||
Assert.That(_app.Session.Board.Balls.Single().position, Is.EqualTo(new Position3(0, 2, 0)));
|
||||
Object.Destroy(box);
|
||||
}
|
||||
[UnityTest] public IEnumerator PhoneAndTabletAssistanceKeepsGrabAndNearbyFriendInFrameWithVisibleNumbers()
|
||||
{
|
||||
foreach (var size in new[] { new Vector2Int(2400, 1080), new Vector2Int(1600, 1200) })
|
||||
{
|
||||
Inject(new Vector3(-1, 2, 0), new Vector3(1, 2, 2));
|
||||
var camera = _app.Presenter.Camera; camera.aspect = (float)size.x / size.y;
|
||||
var point = Point("camera-0"); Assert.That(Physics.Raycast(camera.ScreenPointToRay(point), out var hit, 100), Is.True);
|
||||
var offset = hit.point - _app.Presenter.Balls["camera-0"].transform.position;
|
||||
_app.Input.Begin(point, false); var pointer = point + new Vector2(25, 0); _app.Input.Move(pointer);
|
||||
var preview = _app.Presenter.Balls["camera-0"].transform.position;
|
||||
float until = Time.realtimeSinceStartup + .6f;
|
||||
while (Time.realtimeSinceStartup < until)
|
||||
{
|
||||
yield return null; _app.Input.Move(pointer);
|
||||
Assert.That(_app.Presenter.Balls["camera-0"].transform.position, Is.EqualTo(preview));
|
||||
Assert.That(Vector2.Distance(camera.WorldToScreenPoint(preview + offset), pointer), Is.LessThan(.1f), "The grabbed surface remains under the stationary finger while camera moves.");
|
||||
}
|
||||
foreach (var ball in _app.Presenter.Balls.Values)
|
||||
{
|
||||
var viewport = camera.WorldToViewportPoint(ball.transform.position);
|
||||
Assert.That(viewport.z, Is.GreaterThan(camera.nearClipPlane));
|
||||
Assert.That(viewport.x, Is.InRange(.12f, .88f)); Assert.That(viewport.y, Is.InRange(.12f, .88f));
|
||||
}
|
||||
_app.Presenter.SendMessage("LateUpdate");
|
||||
foreach (var badge in _app.Presenter.GetComponentsInChildren<GroupBadge>())
|
||||
{
|
||||
var number = badge.GetComponentInChildren<TMPro.TextMeshPro>(); number.ForceMeshUpdate();
|
||||
Assert.That(number.text, Is.EqualTo("1")); Assert.That(number.textInfo.characterInfo[0].isVisible, Is.True);
|
||||
var character = number.textInfo.characterInfo[0];
|
||||
foreach (var vertex in new[] { character.bottomLeft, character.topLeft, character.topRight, character.bottomRight })
|
||||
{
|
||||
var view = camera.WorldToViewportPoint(number.transform.TransformPoint(vertex));
|
||||
Assert.That(view.z, Is.GreaterThan(camera.nearClipPlane)); Assert.That(view.x, Is.InRange(0, 1)); Assert.That(view.y, Is.InRange(0, 1));
|
||||
}
|
||||
}
|
||||
Capture(size, "drag-assisted-" + size.x + "x" + size.y);
|
||||
_app.Input.Cancel();
|
||||
}
|
||||
}
|
||||
private void Capture(Vector2Int size, string name)
|
||||
{
|
||||
string directory = System.Environment.GetEnvironmentVariable("MAGNETISM_DRAG_CAMERA_EVIDENCE");
|
||||
if (string.IsNullOrEmpty(directory)) return;
|
||||
var camera = _app.Presenter.Camera; var canvas = _app.GetComponentInChildren<Canvas>();
|
||||
var renderMode = canvas.renderMode; var canvasCamera = canvas.worldCamera; float planeDistance = canvas.planeDistance;
|
||||
var target = new RenderTexture(size.x, size.y, 24); var oldTarget = camera.targetTexture; var active = RenderTexture.active;
|
||||
var pixels = new Texture2D(size.x, size.y, TextureFormat.RGB24, false);
|
||||
try
|
||||
{
|
||||
canvas.renderMode = RenderMode.ScreenSpaceCamera; canvas.worldCamera = camera; canvas.planeDistance = .5f;
|
||||
camera.targetTexture = target; camera.Render(); RenderTexture.active = target;
|
||||
pixels.ReadPixels(new Rect(0, 0, size.x, size.y), 0, 0); pixels.Apply();
|
||||
System.IO.File.WriteAllBytes(System.IO.Path.Combine(directory, name + ".png"), pixels.EncodeToPNG());
|
||||
WriteRecords(name + ".txt", new[] { "Actual GameApp prefab Camera.Render with simulated viewport; Editor result, not physical phone/tablet input or performance.",
|
||||
"camera=" + camera.transform.position + ";rotation=" + camera.transform.rotation + ";tracked=" + _app.Orbit.TrackedFriend + ";manual=" + JsonUtility.ToJson(_app.Orbit.Snapshot()) });
|
||||
}
|
||||
finally
|
||||
{
|
||||
camera.targetTexture = oldTarget; RenderTexture.active = active; target.Release(); Object.Destroy(target); Object.Destroy(pixels);
|
||||
canvas.renderMode = renderMode; canvas.worldCamera = canvasCamera; canvas.planeDistance = planeDistance;
|
||||
}
|
||||
}
|
||||
[UnityTest] public IEnumerator ThirtyOriginalStageBoardsRemainDiscoverableFromValidAboveGroundManualViews()
|
||||
{
|
||||
foreach (var collider in _disabledScenery) if (collider != null) collider.enabled = true;
|
||||
Physics.SyncTransforms();
|
||||
var discoveries = new System.Collections.Generic.List<string>();
|
||||
var dragProof = new System.Collections.Generic.List<string>();
|
||||
int totalDragged = 0;
|
||||
for (int stage = 1; stage <= 30; stage++)
|
||||
{
|
||||
_app.StartStage(stage); _app.Voice.Clear(); yield return null;
|
||||
var before = _app.Session.Board.Balls.Select(ball => (ball.id, ball.position)).ToArray();
|
||||
var visible = new System.Collections.Generic.HashSet<string>(); var original = _app.Orbit.Snapshot();
|
||||
var dragged = new System.Collections.Generic.HashSet<string>();
|
||||
var positions = _app.Session.Board.Balls.ToDictionary(ball => ball.id, ball => BoardPresenter.ToVector(ball.position));
|
||||
var boardSnapshot = JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Session.Camera));
|
||||
var saved = JsonUtility.ToJson(_store.Save); int writes = _store.Writes;
|
||||
WriteRecords($"initial-stage-{stage}-board-before.json", new[] { boardSnapshot });
|
||||
WriteRecords($"initial-stage-{stage}-save-before.json", new[] { saved });
|
||||
var grabField = _app.Input.GetType().GetField("_ballId", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||
foreach (float pitch in new[] { 15f, 35f, 60f, 80f })
|
||||
for (float yaw = 0; yaw < 360; yaw += 15)
|
||||
{
|
||||
var rotation = Quaternion.Euler(pitch, yaw, 0);
|
||||
_app.Orbit.Apply(new CameraState { target = original.target, distance = 12, rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w) });
|
||||
var camera = _app.Presenter.Camera; Physics.SyncTransforms(); _app.Presenter.SendMessage("LateUpdate");
|
||||
var badges = _app.Presenter.GetComponentsInChildren<GroupBadge>();
|
||||
Assert.That(camera.transform.position.y, Is.GreaterThan(_app.Environment.PlaygroundBounds.min.y));
|
||||
foreach (var ball in _app.Session.Board.Balls)
|
||||
{
|
||||
var world = BoardPresenter.ToVector(ball.position); var view = camera.WorldToViewportPoint(world);
|
||||
if (view.z <= camera.nearClipPlane || view.x < .04f || view.x > .96f || view.y < .25f || view.y > .84f) continue;
|
||||
if (!Physics.Raycast(camera.ScreenPointToRay(camera.WorldToScreenPoint(world)), out var hit, 100) || hit.collider.GetComponent<BallView>()?.BallId != ball.id) continue;
|
||||
var group = _app.Session.Board.Groups.First(item => item.ballIds.Contains(ball.id));
|
||||
int groupIndex = System.Array.FindIndex(_app.Session.Board.Groups, item => item.id == group.id);
|
||||
var number = badges[groupIndex].GetComponentInChildren<TMPro.TextMeshPro>(); number.ForceMeshUpdate();
|
||||
Assert.That(number.text, Is.EqualTo(group.ballIds.Length.ToString()), "The actual group number must match membership in every explored original stage.");
|
||||
bool readable = true;
|
||||
foreach (var character in number.textInfo.characterInfo.Take(number.textInfo.characterCount))
|
||||
{
|
||||
readable &= character.isVisible;
|
||||
foreach (var vertex in new[] { character.bottomLeft, character.topLeft, character.topRight, character.bottomRight })
|
||||
{
|
||||
var glyphPoint = camera.WorldToViewportPoint(number.transform.TransformPoint(vertex));
|
||||
readable &= glyphPoint.z > camera.nearClipPlane && glyphPoint.x > 0 && glyphPoint.x < 1 && glyphPoint.y > .2f && glyphPoint.y < .9f;
|
||||
}
|
||||
}
|
||||
var glyphCenter = number.transform.position; var glyphRay = new Ray(camera.transform.position, (glyphCenter - camera.transform.position).normalized);
|
||||
readable &= !Physics.Raycast(glyphRay, Vector3.Distance(camera.transform.position, glyphCenter) - .01f);
|
||||
if (!readable) continue;
|
||||
if (visible.Add(ball.id)) discoveries.Add($"stage={stage};id={ball.id};position={world};camera={camera.transform.position};pitch={pitch};yaw={yaw};visibleGlyph={number.text};glyph={number.transform.position}");
|
||||
if (dragged.Contains(ball.id) || view.x < .221f || view.x > .771f || view.y < .306f || view.y > .741f) continue;
|
||||
foreach (var direction in new[] { Vector2.up, Vector2.down, Vector2.left, Vector2.right })
|
||||
{
|
||||
var manual = _app.Orbit.Snapshot(); var manualJson = JsonUtility.ToJson(manual);
|
||||
var point = (Vector2)camera.WorldToScreenPoint(world); var end = point + direction * 40;
|
||||
var endView = new Vector2(end.x / Screen.width, end.y / Screen.height);
|
||||
if (endView.x < .221f || endView.x > .771f || endView.y < .306f || endView.y > .741f) continue;
|
||||
_app.Input.Begin(point, false); Assert.That(grabField.GetValue(_app.Input), Is.EqualTo(ball.id), "Begin must grab this exact initial sphere ID.");
|
||||
_app.Input.Move(end);
|
||||
var preview = _app.Presenter.Balls[ball.id].transform.position;
|
||||
float pixels = Vector2.Distance(camera.WorldToScreenPoint(preview), point); var delta = _app.Presenter.DropDelta;
|
||||
bool moved = pixels >= 25 && Vector3.Distance(preview, world) >= .05f && _app.Orbit.IsAssisting;
|
||||
Assert.That(_app.Presenter.CanDropGroup(group.id), Is.True, "Every whole-sphere preview remains safe against the actual ground and scenery.");
|
||||
Assert.That(_app.Presenter.Balls.Keys, Is.EquivalentTo(positions.Keys));
|
||||
foreach (var member in _app.Session.Board.Balls)
|
||||
Assert.That(_app.Presenter.Balls[member.id].transform.position, Is.EqualTo(positions[member.id] + (group.ballIds.Contains(member.id) ? delta : Vector3.zero)), "Only the grabbed whole group translates, preserving its shape.");
|
||||
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Session.Camera)), Is.EqualTo(boardSnapshot));
|
||||
Assert.That(JsonUtility.ToJson(_app.Orbit.Snapshot()), Is.EqualTo(manualJson));
|
||||
Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(saved)); Assert.That(_store.Writes, Is.EqualTo(writes));
|
||||
var attempt = $"stage={stage};id={ball.id};grabbed={grabField.GetValue(_app.Input)};group={group.id};members={string.Join(",", group.ballIds)};initial={world};preview={preview};delta={delta};pointerStart={point};pointerEnd={end};pointerStartViewport={view};pointerEndViewport={endView};previewWorldDistance={Vector3.Distance(preview, world):R};previewPixels={pixels:R};passed={moved};manual={manualJson};coreUnchanged=True;saveWrites={writes};sceneryEnabled={_app.Environment.GetComponentsInChildren<Collider>().Count(item => item.enabled && item.gameObject.activeInHierarchy && !item.isTrigger)}";
|
||||
_app.Input.Cancel();
|
||||
foreach (var member in _app.Session.Board.Balls) Assert.That(_app.Presenter.Balls[member.id].transform.position, Is.EqualTo(positions[member.id]), "Cancel restores every initial sphere exactly.");
|
||||
Assert.That(_app.Presenter.DropDelta, Is.EqualTo(Vector3.zero)); Assert.That(_app.Presenter.MergeCandidate, Is.Null);
|
||||
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Session.Camera)), Is.EqualTo(boardSnapshot));
|
||||
Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(saved)); Assert.That(_store.Writes, Is.EqualTo(writes));
|
||||
Assert.That(JsonUtility.ToJson(_app.Orbit.Snapshot()), Is.EqualTo(manualJson));
|
||||
// The camera has not advanced a frame between Move and Cancel; verify the live return settles without a fixed sleep.
|
||||
float deadline = Time.realtimeSinceStartup + 2;
|
||||
while (_app.Orbit.IsReturning && Time.realtimeSinceStartup < deadline) yield return null;
|
||||
Assert.That(_app.Orbit.IsReturning, Is.False);
|
||||
Assert.That(Vector3.Distance(camera.transform.position, BoardPresenter.ToVector(manual.target) + Vector3.up * .65f
|
||||
- new Quaternion(manual.rotation.x, manual.rotation.y, manual.rotation.z, manual.rotation.w) * Vector3.forward * manual.distance), Is.LessThan(.0011f));
|
||||
Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(saved)); Assert.That(_store.Writes, Is.EqualTo(writes));
|
||||
dragProof.Add(attempt + ";cancelRestored=True;manualRestored=True;idsShapeMembershipPreserved=True;saveUnchanged=True");
|
||||
WriteRecords("initial-173-drag-attempts.txt", dragProof);
|
||||
if (moved) { dragged.Add(ball.id); totalDragged++; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Assert.That(visible, Is.EquivalentTo(_app.Session.Board.Balls.Select(ball => ball.id)), "Actual sphere first-hit discovery stage " + stage);
|
||||
WriteRecords($"initial-stage-{stage}-board-after.json", new[] { JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Session.Camera)) });
|
||||
Assert.That(dragged, Is.EquivalentTo(_app.Session.Board.Balls.Select(ball => ball.id)), "Every original sphere ID must actually move at least 25 screen pixels through Input, then cancel without committing stage " + stage);
|
||||
Assert.That(_app.Session.Board.Balls.Select(ball => (ball.id, ball.position)), Is.EqualTo(before));
|
||||
}
|
||||
Assert.That(totalDragged, Is.EqualTo(173));
|
||||
WriteRecords("thirty-stage-first-hit-discovery.txt", discoveries);
|
||||
WriteRecords("initial-173-drag-attempts.txt", dragProof);
|
||||
}
|
||||
[UnityTest] public IEnumerator ObstructedReturnRetracesTheActualSafeAssistPathWithoutCameraSnap()
|
||||
{
|
||||
Inject(new Vector3(-1.5f, 2, -1), new Vector3(.5f, 2, 2));
|
||||
var camera = _app.Presenter.Camera; var manualPosition = camera.transform.position; var manualRotation = camera.transform.rotation;
|
||||
var point = Point("camera-0"); _app.Input.Begin(point, false); _app.Input.Move(point + new Vector2(20, 0));
|
||||
yield return new WaitForSecondsRealtime(.7f); var assisted = camera.transform.position;
|
||||
_app.Input.Cancel(); yield return new WaitForSecondsRealtime(1.5f);
|
||||
var obstacle = new GameObject("camera-test-return-shortcut-obstacle"); obstacle.transform.SetParent(_app.Environment.transform);
|
||||
obstacle.transform.position = Vector3.Lerp(manualPosition, assisted, .5f); obstacle.AddComponent<BoxCollider>().size = Vector3.one * .15f; Physics.SyncTransforms();
|
||||
point = Point("camera-0"); _app.Input.Begin(point, false); var pointer = point + new Vector2(20, 0); _app.Input.Move(pointer);
|
||||
yield return new WaitForSecondsRealtime(.8f);
|
||||
Assert.That(Vector3.Distance(camera.transform.position, assisted), Is.LessThan(.1f), "The original curved assistance path must remain traversable with a blocked straight return chord.");
|
||||
Assert.That(Physics.SphereCastAll(camera.transform.position, .35f, (manualPosition - camera.transform.position).normalized, Vector3.Distance(camera.transform.position, manualPosition)).Any(hit => hit.collider == obstacle.GetComponent<Collider>()), Is.True);
|
||||
var saved = JsonUtility.ToJson(_store.Save); _app.Input.Cancel(); float until = Time.realtimeSinceStartup + 4;
|
||||
var records = new System.Collections.Generic.List<string>();
|
||||
while (_app.Orbit.IsReturning && Time.realtimeSinceStartup < until)
|
||||
{
|
||||
var previous = camera.transform.position; yield return null;
|
||||
records.Add("previous=" + previous + ";position=" + camera.transform.position + ";rotation=" + camera.transform.rotation);
|
||||
bool crossed = Physics.SphereCastAll(previous, .35f, (camera.transform.position - previous).normalized, Vector3.Distance(previous, camera.transform.position)).Any(hit => hit.collider == obstacle.GetComponent<Collider>());
|
||||
WriteRecords("obstructed-return-frames.txt", records);
|
||||
Assert.That(crossed, Is.False);
|
||||
Assert.That(Vector3.Distance(previous, manualPosition) > .1f && Vector3.Distance(camera.transform.position, manualPosition) < .001f, Is.False, "A blocked return cannot instantly snap to the baseline camera.");
|
||||
|
||||
}
|
||||
WriteRecords("safe-obstructed-return.txt", records);
|
||||
Assert.That(_app.Orbit.IsReturning, Is.False); Assert.That(Vector3.Distance(camera.transform.position, manualPosition), Is.LessThan(.002f));
|
||||
Assert.That(Quaternion.Angle(camera.transform.rotation, manualRotation), Is.LessThan(.02)); Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(saved));
|
||||
Object.Destroy(obstacle);
|
||||
}
|
||||
[UnityTest] public IEnumerator UndergroundSavedViewIsCorrectedWithoutChangingBallIdsOrPositions()
|
||||
{
|
||||
Inject(new Vector3(0, 2, 0));
|
||||
var board = JsonUtility.ToJson(_app.Session.Board.Balls);
|
||||
var rotation = Quaternion.Euler(-90, 0, 0);
|
||||
_app.Orbit.Apply(new CameraState { target = new Position3(0, 2, 0), distance = 8, rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w) });
|
||||
yield return null;
|
||||
Assert.That(_app.Presenter.Camera.transform.position.y, Is.GreaterThan(0));
|
||||
Assert.That(JsonUtility.ToJson(_app.Session.Board.Balls), Is.EqualTo(board));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fed67558eff50b4a85be0f7cf14aa35
|
||||
@@ -31,9 +31,15 @@ namespace MagnetismHeroes.Tests
|
||||
Saved = Copy(save); return true;
|
||||
}
|
||||
}
|
||||
private GameApp _app; private MemoryStore _store; private Touchscreen _touchscreen;
|
||||
private GameApp _app; private MemoryStore _store; private Touchscreen _touchscreen, _otherTouchscreen;
|
||||
private InputSettings _originalInputSettings, _testInputSettings;
|
||||
private HideFlags _originalSettingsFlags;
|
||||
private readonly System.Collections.Generic.List<string> _touchTrace = new System.Collections.Generic.List<string>();
|
||||
private void TraceTouch(UnityEngine.InputSystem.EnhancedTouch.Finger finger)
|
||||
{
|
||||
var touch = finger.lastTouch;
|
||||
_touchTrace.Add($"{Time.frameCount},{finger.screen.deviceId},{Touchscreen.current?.deviceId},{touch.touchId},{touch.phase},{touch.screenPosition.x:R},{touch.screenPosition.y:R}");
|
||||
}
|
||||
[UnitySetUp] public IEnumerator SetUp()
|
||||
{
|
||||
// Batch tests have no focused Game View; route synthetic pointers into the actual player loop.
|
||||
@@ -44,15 +50,31 @@ namespace MagnetismHeroes.Tests
|
||||
_testInputSettings.editorInputBehaviorInPlayMode = InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView;
|
||||
_testInputSettings.backgroundBehavior = InputSettings.BackgroundBehavior.IgnoreFocus;
|
||||
InputSystem.settings = _testInputSettings;
|
||||
UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport.Enable();
|
||||
_touchTrace.Clear(); _touchTrace.Add("frame,sourceDevice,currentDevice,touchId,phase,pointerX,pointerY");
|
||||
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerDown += TraceTouch;
|
||||
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerMove += TraceTouch;
|
||||
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerUp += TraceTouch;
|
||||
foreach (var listener in Object.FindObjectsByType<AudioListener>(FindObjectsSortMode.None)) listener.enabled = false;
|
||||
foreach (var camera in Object.FindObjectsByType<Camera>(FindObjectsSortMode.None)) camera.enabled = false;
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GameApp.prefab");
|
||||
Assert.That(prefab, Is.Not.Null); _app = Object.Instantiate(prefab).GetComponent<GameApp>();
|
||||
_store = new MemoryStore(); _app.Initialize(_store); yield return null;
|
||||
_store = new MemoryStore(); _app.Initialize(_store); _app.Presenter.BindEnvironment(_app.Environment); _app.Orbit.BindEnvironment(_app.Environment); yield return null;
|
||||
}
|
||||
[UnityTearDown] public IEnumerator TearDown()
|
||||
{
|
||||
if (_touchscreen != null) InputSystem.RemoveDevice(_touchscreen); Object.Destroy(_app.gameObject);
|
||||
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerDown -= TraceTouch;
|
||||
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerMove -= TraceTouch;
|
||||
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerUp -= TraceTouch;
|
||||
UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport.Disable();
|
||||
var evidence = System.Environment.GetEnvironmentVariable("MAGNETISM_TOUCH_EVIDENCE");
|
||||
if (!string.IsNullOrEmpty(evidence) && _touchTrace.Count > 1)
|
||||
{
|
||||
System.IO.Directory.CreateDirectory(evidence);
|
||||
System.IO.File.WriteAllLines(System.IO.Path.Combine(evidence, TestContext.CurrentContext.Test.Name + ".csv"), _touchTrace);
|
||||
}
|
||||
if (_touchscreen != null) InputSystem.RemoveDevice(_touchscreen);
|
||||
if (_otherTouchscreen != null) InputSystem.RemoveDevice(_otherTouchscreen); Object.Destroy(_app.gameObject);
|
||||
InputSystem.settings = _originalInputSettings; _originalInputSettings.hideFlags = _originalSettingsFlags;
|
||||
Object.Destroy(_testInputSettings); yield return null;
|
||||
}
|
||||
@@ -71,6 +93,91 @@ 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 FirstAndSecondDragAfterAnotherTouchscreenBothUseTheActualPointerDelta()
|
||||
{
|
||||
_touchscreen = InputSystem.AddDevice<Touchscreen>(); _otherTouchscreen = InputSystem.AddDevice<Touchscreen>();
|
||||
_app.StartStage(1); yield return null; Physics.SyncTransforms();
|
||||
InputSystem.QueueStateEvent(_otherTouchscreen, new TouchState { touchId = 201, phase = UnityEngine.InputSystem.TouchPhase.Began, position = new Vector2(5, 5) });
|
||||
InputSystem.QueueStateEvent(_otherTouchscreen, new TouchState { touchId = 201, phase = UnityEngine.InputSystem.TouchPhase.Ended, position = new Vector2(5, 5) });
|
||||
yield return null; yield return null;
|
||||
Assert.That(Touchscreen.current, Is.SameAs(_otherTouchscreen));
|
||||
var camera = JsonUtility.ToJson(_app.Session.Camera); var id = _app.Session.Board.Balls.Single().id;
|
||||
for (int attempt = 0; attempt < 2; attempt++)
|
||||
{
|
||||
Physics.SyncTransforms(); var start = BallPoint(); var end = start + new Vector2(-40, 0);
|
||||
var before = BoardPresenter.ToVector(_app.Session.Board.Balls.Single().position);
|
||||
var ray = _app.Presenter.Camera.ScreenPointToRay(start); Physics.Raycast(ray, out var hit, 100);
|
||||
var plane = new Plane(_app.Presenter.Camera.transform.forward, hit.point);
|
||||
Assert.That(plane.Raycast(ray, out var from), Is.True);
|
||||
var endRay = _app.Presenter.Camera.ScreenPointToRay(end); Assert.That(plane.Raycast(endRay, out var to), Is.True);
|
||||
var expected = before + endRay.GetPoint(to) - ray.GetPoint(from);
|
||||
QueueTouch(202 + attempt, UnityEngine.InputSystem.TouchPhase.Began, start);
|
||||
QueueTouch(202 + attempt, UnityEngine.InputSystem.TouchPhase.Moved, end);
|
||||
QueueTouch(202 + attempt, UnityEngine.InputSystem.TouchPhase.Ended, end);
|
||||
yield return null; yield return null;
|
||||
Assert.That(_app.Session.Board.Balls.Single().id, Is.EqualTo(id));
|
||||
Assert.That(Vector3.Distance(BoardPresenter.ToVector(_app.Session.Board.Balls.Single().position), expected), Is.LessThan(.001f), "Both the first and second real queued drag must retain Began and the pointer direction.");
|
||||
Assert.That(JsonUtility.ToJson(_app.Session.Camera), Is.EqualTo(camera)); Assert.That(_app.Session.Board.CountedCount, Is.Zero);
|
||||
yield return new WaitForSecondsRealtime(1.5f);
|
||||
}
|
||||
}
|
||||
|
||||
[UnityTest] public IEnumerator SwitchedTouchscreenBoundaryDragAndHeldReleaseCommitTheSameLastSafePosition()
|
||||
{
|
||||
_touchscreen = InputSystem.AddDevice<Touchscreen>(); _otherTouchscreen = InputSystem.AddDevice<Touchscreen>();
|
||||
_store.Saved.completedStageIds = new[] { 1 };
|
||||
for (int attempt = 0; attempt < 2; attempt++)
|
||||
{
|
||||
_store.Saved.latestFreePlay = new FreePlayState { sourceStageId = 1,
|
||||
balls = new[] { new BallState { id = "native-drag-0", position = new Position3(-6.8f, 4, 0) } },
|
||||
groups = new[] { new GroupState { id = "native-drag-group", ballIds = new[] { "native-drag-0" } } },
|
||||
camera = new CameraState { target = new Position3(-6.8f, 3.35f, 0), rotation = new Rotation4(0, 0, 0, 1), distance = 14 } };
|
||||
_app.Initialize(_store); _app.ContinueFreePlay(); yield return null; Physics.SyncTransforms();
|
||||
InputSystem.QueueStateEvent(_otherTouchscreen, new TouchState { touchId = 1, phase = UnityEngine.InputSystem.TouchPhase.Began, position = new Vector2(5, 5) });
|
||||
InputSystem.QueueStateEvent(_otherTouchscreen, new TouchState { touchId = 1, phase = UnityEngine.InputSystem.TouchPhase.Ended, position = new Vector2(5, 5) });
|
||||
yield return null; yield return null;
|
||||
var saved = JsonUtility.ToJson(_store.Saved); var camera = JsonUtility.ToJson(_app.Session.Camera);
|
||||
var start = BallPoint(); var end = start - new Vector2(Screen.width * 193f / 2400f, 0);
|
||||
QueueTouch(1, UnityEngine.InputSystem.TouchPhase.Began, start);
|
||||
QueueTouch(1, UnityEngine.InputSystem.TouchPhase.Moved, end);
|
||||
yield return null; yield return null;
|
||||
var preview = _app.Presenter.Balls.Single().Value.transform.position;
|
||||
Assert.That(preview.x, Is.EqualTo(-8).Within(.004f)); Assert.That(preview.y, Is.EqualTo(4).Within(.004f)); Assert.That(preview.z, Is.EqualTo(0).Within(.004f));
|
||||
yield return new WaitForSecondsRealtime(.7f);
|
||||
Assert.That(_app.Presenter.Balls.Single().Value.transform.position, Is.EqualTo(preview));
|
||||
Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved), "Camera assistance and a held pointer cannot save a preview.");
|
||||
QueueTouch(1, UnityEngine.InputSystem.TouchPhase.Ended, end); yield return null; yield return null;
|
||||
Assert.That(_app.Session.Board.Balls.Single().id, Is.EqualTo("native-drag-0"));
|
||||
Assert.That(_app.Session.Board.Groups.Single().ballIds, Is.EqualTo(new[] { "native-drag-0" }));
|
||||
Assert.That(Vector3.Distance(BoardPresenter.ToVector(_app.Session.Board.Balls.Single().position), preview), Is.LessThan(.001f));
|
||||
Assert.That(JsonUtility.ToJson(_app.Session.Camera), Is.EqualTo(camera));
|
||||
Assert.That(_store.Saved.completedStageIds, Is.EqualTo(new[] { 1 })); Assert.That(_store.Saved.puzzleStates, Is.Empty);
|
||||
yield return new WaitForSecondsRealtime(1.5f);
|
||||
}
|
||||
}
|
||||
|
||||
[UnityTest] public IEnumerator AnotherTouchscreenWithTheSameIdCannotMovePinchOrEndTheHeldFriend()
|
||||
{
|
||||
_touchscreen = InputSystem.AddDevice<Touchscreen>(); _otherTouchscreen = InputSystem.AddDevice<Touchscreen>();
|
||||
_app.StartStage(1); yield return null; Physics.SyncTransforms();
|
||||
var start = BallPoint(); var saved = JsonUtility.ToJson(_store.Saved); var camera = JsonUtility.ToJson(_app.Session.Camera);
|
||||
QueueTouch(1, UnityEngine.InputSystem.TouchPhase.Began, start); yield return null; yield return null;
|
||||
QueueTouch(1, UnityEngine.InputSystem.TouchPhase.Moved, start + new Vector2(-40, 0)); yield return null; yield return null;
|
||||
var preview = _app.Presenter.Balls.Values.Single().transform.position;
|
||||
foreach (var phase in new[] { UnityEngine.InputSystem.TouchPhase.Began, UnityEngine.InputSystem.TouchPhase.Moved, UnityEngine.InputSystem.TouchPhase.Ended })
|
||||
{
|
||||
InputSystem.QueueStateEvent(_otherTouchscreen, new TouchState { touchId = 1, phase = phase, position = start + new Vector2(100, 30) });
|
||||
InputSystem.QueueStateEvent(_otherTouchscreen, new TouchState { touchId = 2, phase = phase, position = start + new Vector2(200, 30) });
|
||||
yield return null; yield return null;
|
||||
Assert.That(_app.Presenter.Balls.Values.Single().transform.position, Is.EqualTo(preview));
|
||||
Assert.That(_app.Orbit.IsAssisting, Is.True); Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved));
|
||||
}
|
||||
QueueTouch(1, UnityEngine.InputSystem.TouchPhase.Canceled, start + new Vector2(-40, 0)); yield return null; yield return null;
|
||||
Assert.That(_app.Session.Board.CollectedBallIds, Is.Empty); Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved));
|
||||
Assert.That(JsonUtility.ToJson(_app.Session.Camera), Is.EqualTo(camera));
|
||||
Assert.That(_app.Presenter.Balls.Values.Single().transform.position, Is.EqualTo(BoardPresenter.ToVector(_app.Session.Board.Balls.Single().position)));
|
||||
}
|
||||
|
||||
[UnityTest] public IEnumerator FailedMoveCameraAndPauseSavesShowAVisibleNoticeAndRecoveryClearsIt()
|
||||
{
|
||||
_app.StartStage(1); yield return null;
|
||||
@@ -219,7 +326,7 @@ namespace MagnetismHeroes.Tests
|
||||
}
|
||||
|
||||
[UnityTest] public IEnumerator GroupNumberFrontViewStaysAboveAndVisible() => CheckGroupNumber(Quaternion.identity);
|
||||
[UnityTest] public IEnumerator GroupNumberBelowViewStaysAboveAndVisible() => CheckGroupNumber(Quaternion.Euler(-90, 0, 0));
|
||||
[UnityTest] public IEnumerator GroupNumberCorrectedUndergroundViewStaysAboveAndVisible() => CheckGroupNumber(Quaternion.Euler(-90, 0, 0));
|
||||
[UnityTest] public IEnumerator GroupNumberRolledViewStaysAboveAndVisible() => CheckGroupNumber(Quaternion.Euler(0, 0, 90));
|
||||
[UnityTest] public IEnumerator GroupNumberPreviewFollowsTheFriendWithoutChangingCommittedState() => CheckGroupNumber(Quaternion.Euler(-90, 0, 0), true);
|
||||
|
||||
@@ -233,6 +340,7 @@ namespace MagnetismHeroes.Tests
|
||||
var committedCamera = JsonUtility.ToJson(_app.Session.Camera);
|
||||
_app.Orbit.Apply(new CameraState { target = new Position3(4, 0.6f, 0), distance = 8,
|
||||
rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w) });
|
||||
Assert.That(_app.Presenter.Camera.transform.position.y, Is.GreaterThan(-.45f), "Saved underground views must be corrected above the clearing.");
|
||||
if (preview) _app.Presenter.PreviewGroup(_app.Session.Board.Groups.Single().id, new Vector3(0.8f, 0, 0.3f));
|
||||
yield return null; Physics.SyncTransforms();
|
||||
var camera = _app.Presenter.Camera; var badge = _app.Presenter.GetComponentInChildren<GroupBadge>();
|
||||
@@ -288,7 +396,7 @@ namespace MagnetismHeroes.Tests
|
||||
Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved));
|
||||
}
|
||||
|
||||
[UnityTest] public IEnumerator BackgroundInteriorDropIsRejectedAndSurfaceContactRemainsPlayable()
|
||||
[UnityTest] public IEnumerator BackgroundInteriorDragStopsAndCommitsLastSafeSurfacePosition()
|
||||
{
|
||||
_app.StartStage(1); yield return null; DragTo(new Vector3(1.8f, 0.8f, 0)); yield return null;
|
||||
Assert.That(_app.Session.Board.CollectedBallIds.Length, Is.EqualTo(1));
|
||||
@@ -298,9 +406,12 @@ namespace MagnetismHeroes.Tests
|
||||
Assert.That(initial.y, Is.EqualTo(0.65f).Within(0.001)); Assert.That(initial.z, Is.EqualTo(1.1f).Within(0.001)); BallPoint();
|
||||
var saved = JsonUtility.ToJson(_store.Saved); var camera = _app.Orbit.Snapshot();
|
||||
DragTo(new Vector3(1.8f, -0.4f, 1.1f)); yield return null;
|
||||
Assert.That(_app.Session.Board.Balls.Single().position, Is.EqualTo(initial), "The scene interior must not become a committed drop.");
|
||||
Assert.That(_app.Presenter.Balls.Values.Single().transform.position, Is.EqualTo(new Vector3(initial.x, initial.y, initial.z)));
|
||||
Assert.That(_app.Orbit.Snapshot().rotation, Is.EqualTo(camera.rotation)); Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved));
|
||||
var stopped = _app.Session.Board.Balls.Single().position;
|
||||
Assert.That(stopped.y, Is.LessThan(initial.y));
|
||||
Assert.That(stopped.y, Is.GreaterThanOrEqualTo(_app.Environment.PlaygroundBounds.min.y + .499f));
|
||||
Assert.That(_app.Presenter.IsSafePosition(stopped), Is.True);
|
||||
Assert.That(_app.Presenter.Balls.Values.Single().transform.position, Is.EqualTo(BoardPresenter.ToVector(stopped)));
|
||||
Assert.That(_app.Orbit.Snapshot().rotation, Is.EqualTo(camera.rotation)); Assert.That(JsonUtility.ToJson(_store.Saved), Is.Not.EqualTo(saved));
|
||||
Physics.SyncTransforms(); BallPoint();
|
||||
}
|
||||
|
||||
@@ -445,13 +556,19 @@ namespace MagnetismHeroes.Tests
|
||||
InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 41, phase = UnityEngine.InputSystem.TouchPhase.Ended, position = new Vector2(-1, point.y) }); yield return null; yield return null;
|
||||
Assert.That(_app.Session.Board.Balls.Single().position, Is.EqualTo(initial)); Assert.That(_app.Session.Board.CollectedBallIds, Is.Empty);
|
||||
}
|
||||
[UnityTest] public IEnumerator FlatFloorColliderMatchesTheVisibleMeshAndKeepsTheFriendReachable()
|
||||
[UnityTest] public IEnumerator VisibleClearingColliderMatchesItsMeshAndHiddenLegacyFloorDoesNotBlock()
|
||||
{
|
||||
_app.StartStage(1); yield return null; Physics.SyncTransforms();
|
||||
var floor = _app.transform.Find("Playground/PlaygroundIsland");
|
||||
Assert.That(floor.GetComponent<Collider>(), Is.TypeOf<MeshCollider>());
|
||||
Assert.That(floor.GetComponent<Collider>().bounds.max.y, Is.EqualTo(floor.GetComponent<Renderer>().bounds.max.y).Within(0.01));
|
||||
Assert.That(floor.GetComponent<Collider>().bounds.max.y, Is.LessThan(0)); BallPoint();
|
||||
Assert.That(floor.GetComponent<Collider>() == null, Is.True, "The hidden legacy floor must have no collider.");
|
||||
var clearing = _app.Environment.GetComponentsInChildren<MeshCollider>().Single(collider => collider.name == "MeadowClearing");
|
||||
Assert.That(clearing.enabled && !clearing.isTrigger, Is.True);
|
||||
Assert.That(clearing.sharedMesh, Is.SameAs(clearing.GetComponent<MeshFilter>().sharedMesh));
|
||||
var ray = new Ray(new Vector3(0, .1f, 0), Vector3.down);
|
||||
Assert.That(clearing.Raycast(ray, out var surface, 2), Is.True);
|
||||
Assert.That(Physics.Raycast(ray, out var actual, 2), Is.True);
|
||||
Assert.That(actual.collider, Is.SameAs(clearing)); Assert.That(actual.point, Is.EqualTo(surface.point));
|
||||
Assert.That(surface.point.y, Is.LessThan(0)); BallPoint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,587 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MagnetismHeroes.Core;
|
||||
using MagnetismHeroes.UnityRuntime.Bootstrap;
|
||||
using MagnetismHeroes.UnityRuntime.Presentation;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using TMPro;
|
||||
using Unity.Collections;
|
||||
|
||||
namespace MagnetismHeroes.Tests
|
||||
{
|
||||
public sealed class PlaygroundResumePlayTests
|
||||
{
|
||||
private sealed class Store : IProgressStore
|
||||
{
|
||||
public ProgressSave Save;
|
||||
public int Writes;
|
||||
public bool FailWrites;
|
||||
public bool TryLoad(out ProgressSave save, out string error) { save = Clone(Save); error = null; return true; }
|
||||
public bool TrySave(ProgressSave save, out string error) { Writes++; error = FailWrites ? "test storage unavailable" : null; if (FailWrites) return false; Save = Clone(save); return true; }
|
||||
}
|
||||
private GameApp _app;
|
||||
private Store _store;
|
||||
private readonly List<Object> _fixtures = new List<Object>();
|
||||
private static ProgressSave Clone(ProgressSave value)
|
||||
{
|
||||
var copy = JsonUtility.FromJson<ProgressSave>(JsonUtility.ToJson(value));
|
||||
// JsonUtility recreates a null nested serializable class; the store contract keeps null.
|
||||
if (value.latestFreePlay == null) copy.latestFreePlay = null;
|
||||
return copy;
|
||||
}
|
||||
private static BallState Ball(string id, float x) => new BallState { id = id, position = new Position3(x, 5, 0) };
|
||||
private static GroupState Group(string id, params string[] balls) => new GroupState { id = id, ballIds = balls };
|
||||
[UnitySetUp] public IEnumerator Setup()
|
||||
{
|
||||
foreach (var listener in Object.FindObjectsByType<AudioListener>(FindObjectsSortMode.None)) listener.enabled = false;
|
||||
_app = Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GameApp.prefab")).GetComponent<GameApp>();
|
||||
_store = new Store { Save = new ProgressSave { completedStageIds = Enumerable.Range(1, 30).ToArray(), seenTutorialIds = new[] { "merge", "split" } } };
|
||||
_app.Initialize(_store);
|
||||
yield return null;
|
||||
}
|
||||
[UnityTearDown] public IEnumerator Teardown() { foreach (var fixture in _fixtures) Object.Destroy(fixture); _fixtures.Clear(); Object.Destroy(_app.gameObject); yield return null; }
|
||||
private void Reload() { _app.Initialize(_store); }
|
||||
private TMP_Text Text(string field) => (TMP_Text)new SerializedObject(_app).FindProperty(field).objectReferenceValue;
|
||||
private BoxCollider Box(Vector3 center, Vector3 size)
|
||||
{
|
||||
var obstacle = new GameObject("test-only-restore-obstacle"); _fixtures.Add(obstacle); obstacle.transform.position = center;
|
||||
var box = obstacle.AddComponent<BoxCollider>(); box.size = size; Physics.SyncTransforms(); return box;
|
||||
}
|
||||
private void Free(Vector3 point)
|
||||
{
|
||||
_store.Save.latestFreePlay = new FreePlayState { sourceStageId = 6,
|
||||
balls = new[] { new BallState { id = "legacy-free", position = BoardPresenter.ToPosition(point) } },
|
||||
groups = new[] { Group("legacy-free-group", "legacy-free") }, camera = GameSession.DefaultCamera() };
|
||||
Reload();
|
||||
}
|
||||
private void MeshDiagnostic(MeshCollider collider, Vector3 source, Vector3 candidate)
|
||||
{
|
||||
var cache = AssetDatabase.LoadAssetAtPath<PlaygroundRestoreGeometry>("Assets/MagnetismHeroes/UnityRuntime/Bootstrap/PlaygroundRestoreGeometry.asset");
|
||||
Assert.That(cache.TryGet(collider.sharedMesh, out var vertices, out var triangles), Is.True);
|
||||
using (var dataArray = MeshUtility.AcquireReadOnlyMeshData(collider.sharedMesh))
|
||||
{
|
||||
var data = dataArray[0]; using var buffer = new NativeArray<Vector3>(data.vertexCount, Allocator.Temp); data.GetVertices(buffer);
|
||||
Assert.That(vertices.SequenceEqual(buffer.ToArray()), Is.True, "Cache local vertices must equal actual imported collider MeshData.");
|
||||
var indices = data.indexFormat == UnityEngine.Rendering.IndexFormat.UInt16 ? data.GetIndexData<ushort>().ToArray().Select(index => (int)index).ToArray() : data.GetIndexData<uint>().ToArray().Select(index => (int)index).ToArray();
|
||||
var actual = Enumerable.Range(0, data.subMeshCount).SelectMany(submesh => { var descriptor = data.GetSubMesh(submesh); return Enumerable.Range(descriptor.indexStart, descriptor.indexCount).Select(index => indices[index] + descriptor.baseVertex); }).ToArray();
|
||||
Assert.That(triangles.SequenceEqual(actual), Is.True, "Cache triangle indices must equal actual imported collider MeshData.");
|
||||
}
|
||||
foreach (var point in new[] { source, candidate })
|
||||
{
|
||||
float closest = float.PositiveInfinity; Vector3 surface = Vector3.zero; int closestTriangle = -1;
|
||||
for (int index = 0; index < triangles.Length; index += 3)
|
||||
{
|
||||
var a = collider.transform.TransformPoint(vertices[triangles[index]]); var b = collider.transform.TransformPoint(vertices[triangles[index+1]]); var c = collider.transform.TransformPoint(vertices[triangles[index+2]]);
|
||||
var normal = Vector3.Cross(b-a,c-a); if (normal.sqrMagnitude < .000000001f) continue;
|
||||
var projected = point - normal * (Vector3.Dot(point-a,normal)/normal.sqrMagnitude);
|
||||
var candidates = new List<Vector3>();
|
||||
if (Vector3.Dot(Vector3.Cross(b-a,projected-a),normal)>=0 && Vector3.Dot(Vector3.Cross(c-b,projected-b),normal)>=0 && Vector3.Dot(Vector3.Cross(a-c,projected-c),normal)>=0) candidates.Add(projected);
|
||||
foreach (var edge in new[] { (a,b), (b,c), (c,a) }) candidates.Add(edge.Item1 + (edge.Item2-edge.Item1) * Mathf.Clamp01(Vector3.Dot(point-edge.Item1,edge.Item2-edge.Item1)/(edge.Item2-edge.Item1).sqrMagnitude));
|
||||
foreach (var nearest in candidates) if (Vector3.Distance(point,nearest)<closest) { closest=Vector3.Distance(point,nearest);surface=nearest;closestTriangle=index/3; }
|
||||
}
|
||||
bool original = Physics.queriesHitBackfaces;
|
||||
try
|
||||
{
|
||||
var crossings = _app.Environment.GetType().GetMethod("SurfaceCrossings", System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic);
|
||||
foreach (var direction in new[] {new Vector3(1,.371f,.529f).normalized,new Vector3(.217f,1,.613f).normalized,new Vector3(.419f,.293f,1).normalized})
|
||||
{
|
||||
Physics.queriesHitBackfaces=false; int entries=(int)crossings.Invoke(_app.Environment,new object[]{collider,point,direction});
|
||||
Physics.queriesHitBackfaces=true; int total=(int)crossings.Invoke(_app.Environment,new object[]{collider,point,direction});
|
||||
Debug.Log($"[ProgressRestoreMeshDiagnostic] point={point:R}, direction={direction:R}, nativeEntries={entries}, nativeCrossings={total}");
|
||||
}
|
||||
}
|
||||
finally {Physics.queriesHitBackfaces=original;}
|
||||
Debug.Log($"[ProgressRestoreMeshDiagnostic] mesh={collider.sharedMesh.name}, point={point:R}, closestSurface={surface:R}, distance={closest:R}, triangle={closestTriangle}, position={collider.transform.position:R}, rotation={collider.transform.rotation:R}, scale={collider.transform.lossyScale:R}, vertices={vertices.Length}, triangles={triangles.Length/3}");
|
||||
}
|
||||
}
|
||||
private delegate void MeshDistance(Vector3 point, out Vector3 nearest, out float distance, out bool inside);
|
||||
[UnityTest] public IEnumerator ApprovedTreeDistanceMatchesFrozenReferenceAndNativeAndSyntheticEntriesRemainStable()
|
||||
{
|
||||
_store.Save.puzzleStates=new[]{new BoardRules(_app.Session.Definitions.First(stage=>stage.Id==2)).ToPuzzleState(GameSession.DefaultCamera()),new BoardRules(_app.Session.Definitions.First(stage=>stage.Id==9)).ToPuzzleState(GameSession.DefaultCamera())};
|
||||
Reload();_app.StartStage(6);yield return null;
|
||||
var cache=(PlaygroundRestoreGeometry)new SerializedObject(_app).FindProperty("_restoreGeometry").objectReferenceValue;
|
||||
var meshType=typeof(PlaygroundResume).GetNestedType("RestoreMesh",System.Reflection.BindingFlags.NonPublic);
|
||||
// The same authored stage 6 tree (loop index 5) now frames the board from behind.
|
||||
var nativePoint=new Vector3(1.6815286f,1.6242015f,7.1185765f);
|
||||
var native=_app.Environment.GetComponentsInChildren<MeshCollider>().Single(mesh=>mesh.enabled&&mesh.sharedMesh.name=="RoundCanopyTree"
|
||||
&&Mathf.Abs(Mathf.DeltaAngle(mesh.transform.parent.localEulerAngles.y,235))<.001f
|
||||
&&Vector3.Distance(mesh.transform.parent.localScale,Vector3.one*1.11f)<.00001f);
|
||||
Assert.That(Vector3.Distance(native.transform.parent.localPosition,new Vector3(1.8652403f,-.56261694f,7.073643f)),Is.LessThan(.001f));
|
||||
Assert.That(Vector3.Distance(native.bounds.center,nativePoint),Is.LessThan(.001f));
|
||||
foreach(string fixture in new[]{"native-stage6","synthetic-tree"})
|
||||
{
|
||||
var collider=native;
|
||||
if(fixture=="synthetic-tree")
|
||||
{
|
||||
foreach(var item in _app.Environment.GetComponentsInChildren<Collider>()) if(!item.isTrigger&&!item.name.Contains("Clearing")) item.enabled=false;
|
||||
var obstacle=Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Meadow/RoundCanopyTree.prefab"),_app.Environment.transform);_fixtures.Add(obstacle);
|
||||
obstacle.transform.localScale=Vector3.one*2;collider=obstacle.GetComponentInChildren<MeshCollider>();Physics.SyncTransforms();
|
||||
obstacle.transform.position+=new Vector3(0,3,0)-collider.bounds.center;Physics.SyncTransforms();
|
||||
}
|
||||
Assert.That(collider.sharedMesh.isReadable,Is.False);Assert.That(cache.TryGet(collider.sharedMesh,out var local,out var triangles),Is.True);
|
||||
Assert.That(local.Length,Is.EqualTo(4187));Assert.That(triangles.Length/3,Is.EqualTo(4840));
|
||||
var world=local.Select(collider.transform.TransformPoint).ToArray();
|
||||
var geometry=System.Activator.CreateInstance(meshType,new object[]{collider,local,triangles});Assert.That(meshType.GetProperty("IsClosed").GetValue(geometry),Is.True);
|
||||
var distance=(MeshDistance)meshType.GetMethod("Distance").CreateDelegate(typeof(MeshDistance),geometry);
|
||||
var samples=new List<Vector3>{collider.bounds.center,collider.bounds.min-Vector3.one,collider.bounds.max+Vector3.one};
|
||||
for(int index=0;index<24;index++)
|
||||
{
|
||||
int triangle=(index*(triangles.Length/3)/24)*3;var a=world[triangles[triangle]];var b=world[triangles[triangle+1]];var c=world[triangles[triangle+2]];
|
||||
var point=(a+b+c)/3;var normal=Vector3.Cross(b-a,c-a).normalized;
|
||||
samples.Add(point);samples.Add(point+normal*.002f);samples.Add(point-normal*.002f);
|
||||
}
|
||||
// Keep the original 75 samples, then cover vertices/edges on both sides of numerical surface boundaries.
|
||||
for(int index=0;index<24;index++)
|
||||
{
|
||||
int triangle=(index*(triangles.Length/3)/24)*3;var a=world[triangles[triangle]];var b=world[triangles[triangle+1]];var c=world[triangles[triangle+2]];
|
||||
var normal=Vector3.Cross(b-a,c-a).normalized;var edge=(a+b)*.5f;
|
||||
foreach(var point in new[]{a,edge}) {samples.Add(point);samples.Add(point+normal*.000001f);samples.Add(point-normal*.000001f);}
|
||||
}
|
||||
foreach(var axis in new[]{Vector3.right,Vector3.up,Vector3.forward}) {samples.Add(collider.bounds.center+axis*1000);samples.Add(collider.bounds.center-axis*1000);}
|
||||
int insideSamples=0,outsideSamples=0;
|
||||
foreach(var point in samples)
|
||||
{
|
||||
ReferenceMeshDistance(world,triangles,point,out var expectedNearest,out float expectedDistance,out bool expectedInside,out double winding);
|
||||
distance(point,out var actualNearest,out float actualDistance,out bool actualInside);
|
||||
Assert.That(actualNearest.x,Is.EqualTo(expectedNearest.x));Assert.That(actualNearest.y,Is.EqualTo(expectedNearest.y));Assert.That(actualNearest.z,Is.EqualTo(expectedNearest.z));
|
||||
Assert.That(actualDistance,Is.EqualTo(expectedDistance));Assert.That(actualInside,Is.EqualTo(expectedInside));Assert.That(actualInside,Is.EqualTo(System.Math.Abs(winding)>System.Math.PI*2));
|
||||
if(expectedInside) insideSamples++;else outsideSamples++;
|
||||
}
|
||||
Assert.That(insideSamples,Is.GreaterThan(0));Assert.That(outsideSamples,Is.GreaterThan(0));
|
||||
AssetDatabase.TryGetGUIDAndLocalFileIdentifier(collider.sharedMesh,out string guid,out long fileId);
|
||||
Debug.Log($"[ResumeMagnitudeReference] fixture={fixture}, meshGuid={guid}, fileId={fileId}, geometryHash={PlaygroundRestoreGeometry.Hash(local,triangles)}, vertices={local.Length}, triangles={triangles.Length/3}, position={collider.transform.position:R}, rotation={collider.transform.rotation:R}, scale={collider.transform.lossyScale:R}, samples={samples.Count}, inside={insideSamples}, outside={outsideSamples}");
|
||||
var timingPoints=samples.Take(12).ToArray();
|
||||
for(int warmup=0;warmup<2;warmup++) foreach(var point in timingPoints) {ReferenceMeshDistance(world,triangles,point,out _,out _,out _,out _);distance(point,out _,out _,out _);}
|
||||
for(int batch=0;batch<5;batch++)
|
||||
{
|
||||
var timer=System.Diagnostics.Stopwatch.StartNew();foreach(var point in timingPoints) ReferenceMeshDistance(world,triangles,point,out _,out _,out _,out _);timer.Stop();double referenceMilliseconds=timer.Elapsed.TotalMilliseconds;
|
||||
timer.Restart();foreach(var point in timingPoints) ReferenceMeshDistance(world,triangles,point,out _,out _,out _,out _,false);timer.Stop();double nearestMilliseconds=timer.Elapsed.TotalMilliseconds;
|
||||
timer.Restart();foreach(var point in timingPoints) distance(point,out _,out _,out _);timer.Stop();
|
||||
Debug.Log($"[ResumeMagnitudeQueryTiming] fixture={fixture}, batch={batch}, queries={timingPoints.Length}, referenceMs={referenceMilliseconds:R}, nearestOnlyMs={nearestMilliseconds:R}, runtimeMs={timer.Elapsed.TotalMilliseconds:R}");
|
||||
}
|
||||
string puzzles=string.Join("|",_store.Save.puzzleStates.Select(state=>JsonUtility.ToJson(state)));var completed=(int[])_store.Save.completedStageIds.Clone();var tutorials=(string[])_store.Save.seenTutorialIds.Clone();
|
||||
string expected=null;
|
||||
for(int iteration=0;iteration<4;iteration++)
|
||||
{
|
||||
_store.Save.latestFreePlay=new FreePlayState{sourceStageId=6,balls=new[]{new BallState{id="native-tree-interior",position=BoardPresenter.ToPosition(collider.bounds.center)}},groups=new[]{Group("native-group","native-tree-interior")},camera=GameSession.DefaultCamera()};
|
||||
Reload();int writes=_store.Writes;var timer=System.Diagnostics.Stopwatch.StartNew();_app.ContinueFreePlay();timer.Stop();yield return null;
|
||||
Assert.That(_app.Session.Screen,Is.EqualTo(SessionScreen.FreePlay));Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board),Is.True);Assert.That(_store.Writes,Is.EqualTo(writes+1));
|
||||
Assert.That(_app.Session.Board.Balls.Select(ball=>ball.id),Is.EqualTo(new[]{"native-tree-interior"}));Assert.That(_app.Session.Board.Groups.Single().id,Is.EqualTo("native-group"));
|
||||
Assert.That(string.Join("|",_store.Save.puzzleStates.Select(state=>JsonUtility.ToJson(state))),Is.EqualTo(puzzles));Assert.That(_store.Save.completedStageIds,Is.EqualTo(completed));Assert.That(_store.Save.seenTutorialIds,Is.EqualTo(tutorials));
|
||||
string result=JsonUtility.ToJson(_store.Save.latestFreePlay);if(expected==null) expected=result;else Assert.That(result,Is.EqualTo(expected));
|
||||
Debug.Log($"[ResumeMagnitudeEntryTiming] fixture={fixture}, warmup={iteration==0}, sample={iteration-1}, utc={System.DateTime.UtcNow:O}, entryMs={timer.Elapsed.TotalMilliseconds:R}, result={result}");
|
||||
}
|
||||
_app.ShowHome();string stable=JsonUtility.ToJson(_store.Save.latestFreePlay);_app.ContinueFreePlay();yield return null;Assert.That(JsonUtility.ToJson(_store.Save.latestFreePlay),Is.EqualTo(stable));Assert.That(_app.Instruction,Is.Not.EqualTo("친구를 놀이터로 다시 보냈어요."));
|
||||
}
|
||||
}
|
||||
[UnityTest] public IEnumerator ClosedComponentsAndUnsupportedTopologyMatchTheFrozenWindingAtEdgesOverlapsAndRemotePoints()
|
||||
{
|
||||
var primitive=GameObject.CreatePrimitive(PrimitiveType.Cube);_fixtures.Add(primitive);
|
||||
var cube=primitive.GetComponent<MeshFilter>().sharedMesh;var cubeVertices=cube.vertices;var cubeTriangles=cube.triangles;primitive.SetActive(false);
|
||||
var meshType=typeof(PlaygroundResume).GetNestedType("RestoreMesh",System.Reflection.BindingFlags.NonPublic);
|
||||
foreach(string configuration in new[]{"overlap","opposite","vertex-touch","open","collapsed","thin"})
|
||||
{
|
||||
var vertices=new List<Vector3>();var triangles=new List<int>();
|
||||
foreach(var center in new[]{Vector3.zero,configuration=="vertex-touch"?Vector3.one:new Vector3(.3f,.2f,.1f),new Vector3(7,3,-4)})
|
||||
{
|
||||
int offset=vertices.Count;vertices.AddRange(cubeVertices.Select(point=>point+center));var indices=cubeTriangles;
|
||||
if(configuration=="opposite"&&offset==cubeVertices.Length) indices=cubeTriangles.Reverse().ToArray();
|
||||
triangles.AddRange(indices.Select(index=>offset+index));
|
||||
}
|
||||
if(configuration=="open") triangles.RemoveRange(0,3);
|
||||
var mesh=new Mesh();mesh.vertices=vertices.ToArray();mesh.triangles=triangles.ToArray();mesh.RecalculateBounds();_fixtures.Add(mesh);
|
||||
var obstacle=new GameObject("test-only-component-distance");_fixtures.Add(obstacle);obstacle.transform.SetPositionAndRotation(new Vector3(3,-2,5),Quaternion.Euler(23,37,11));obstacle.transform.localScale=new Vector3(1.7f,configuration=="collapsed"?0:configuration=="thin"?.0001f:.65f,-1.1f);
|
||||
var collider=obstacle.AddComponent<MeshCollider>();collider.sharedMesh=mesh;var world=vertices.Select(collider.transform.TransformPoint).ToArray();
|
||||
var geometry=System.Activator.CreateInstance(meshType,new object[]{collider,vertices.ToArray(),triangles.ToArray()});bool closed=(bool)meshType.GetProperty("IsClosed").GetValue(geometry);
|
||||
Assert.That(closed,Is.EqualTo(configuration!="open"));var distance=(MeshDistance)meshType.GetMethod("Distance").CreateDelegate(typeof(MeshDistance),geometry);
|
||||
var points=new List<Vector3>{obstacle.transform.TransformPoint(new Vector3(.15f,.1f,.05f)),obstacle.transform.TransformPoint(new Vector3(7,3,-4)),Vector3.one*1000,-Vector3.one*1000};
|
||||
for(int index=0;index<triangles.Count;index+=3)
|
||||
{
|
||||
var a=world[triangles[index]];var b=world[triangles[index+1]];var c=world[triangles[index+2]];var normal=Vector3.Cross(b-a,c-a).normalized;
|
||||
foreach(var point in new[]{a,(a+b)*.5f,(a+b+c)/3}) {points.Add(point);points.Add(point+normal*.000001f);points.Add(point-normal*.000001f);}
|
||||
}
|
||||
// Component faces just beyond the winding bound, including singular and non-finite query controls.
|
||||
for(int offset=0;offset<world.Length;offset+=cubeVertices.Length)
|
||||
{
|
||||
var part=world.Skip(offset).Take(cubeVertices.Length).ToArray();var min=part.Aggregate(Vector3.Min);var max=part.Aggregate(Vector3.Max);
|
||||
for(int axis=0;axis<3;axis++) foreach(float face in new[]{min[axis],max[axis]})
|
||||
{
|
||||
var point=(min+max)*.5f;point[axis]=face;var delta=Vector3.zero;delta[axis]=.0002f;
|
||||
points.Add(point);points.Add(point+delta);points.Add(point-delta);
|
||||
}
|
||||
}
|
||||
points.Add(new Vector3(float.NaN,0,0));points.Add(new Vector3(float.PositiveInfinity,0,0));points.Add(new Vector3(float.NegativeInfinity,0,0));
|
||||
foreach(var point in points)
|
||||
{
|
||||
ReferenceMeshDistance(world,triangles.ToArray(),point,out var expectedNearest,out float expectedDistance,out bool expectedInside,out _,closed);
|
||||
distance(point,out var actualNearest,out float actualDistance,out bool actualInside);
|
||||
Assert.That(actualNearest,Is.EqualTo(expectedNearest));Assert.That(actualDistance,Is.EqualTo(expectedDistance));Assert.That(actualInside,Is.EqualTo(expectedInside),configuration+" "+point.ToString("R"));
|
||||
}
|
||||
Debug.Log($"[ResumeDistancePathology] configuration={configuration}, closed={closed}, samples={points.Count}, exact=True");
|
||||
obstacle.SetActive(false);
|
||||
}
|
||||
yield return null;
|
||||
}
|
||||
private static void ReferenceMeshDistance(Vector3[] vertices, int[] triangles, Vector3 point, out Vector3 nearest, out float distance, out bool inside, out double winding, bool closed = true)
|
||||
{
|
||||
nearest = Vector3.zero; float squared = float.PositiveInfinity; winding = 0;
|
||||
for (int index = 0; index < triangles.Length; index += 3)
|
||||
{
|
||||
var a = vertices[triangles[index]]; var b = vertices[triangles[index + 1]]; var c = vertices[triangles[index + 2]];
|
||||
var candidate = ReferenceClosestTriangle(point, a, b, c); float candidateSquared = (candidate - point).sqrMagnitude;
|
||||
if (candidateSquared < squared) { squared = candidateSquared; nearest = candidate; }
|
||||
if (!closed) continue;
|
||||
a -= point; b -= point; c -= point;
|
||||
double numerator = Vector3.Dot(a, Vector3.Cross(b, c));
|
||||
double denominator = (double)a.magnitude * b.magnitude * c.magnitude + (double)Vector3.Dot(a, b) * c.magnitude
|
||||
+ (double)Vector3.Dot(b, c) * a.magnitude + (double)Vector3.Dot(c, a) * b.magnitude;
|
||||
winding += 2 * System.Math.Atan2(numerator, denominator);
|
||||
}
|
||||
distance = Mathf.Sqrt(squared); inside = closed && System.Math.Abs(winding) > System.Math.PI * 2;
|
||||
}
|
||||
private static Vector3 ReferenceClosestTriangle(Vector3 point, Vector3 a, Vector3 b, Vector3 c)
|
||||
{
|
||||
var ab = b - a; var ac = c - a; var ap = point - a; float d1 = Vector3.Dot(ab, ap), d2 = Vector3.Dot(ac, ap);
|
||||
if (d1 <= 0 && d2 <= 0) return a;
|
||||
var bp = point - b; float d3 = Vector3.Dot(ab, bp), d4 = Vector3.Dot(ac, bp);
|
||||
if (d3 >= 0 && d4 <= d3) return b;
|
||||
float vc = d1 * d4 - d3 * d2; if (vc <= 0 && d1 >= 0 && d3 <= 0) return a + ab * (d1 / (d1 - d3));
|
||||
var cp = point - c; float d5 = Vector3.Dot(ab, cp), d6 = Vector3.Dot(ac, cp);
|
||||
if (d6 >= 0 && d5 <= d6) return c;
|
||||
float vb = d5 * d2 - d1 * d6; if (vb <= 0 && d2 >= 0 && d6 <= 0) return a + ac * (d2 / (d2 - d6));
|
||||
float va = d3 * d6 - d5 * d4; if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) return b + (c - b) * ((d4 - d3) / (d4 - d3 + d5 - d6));
|
||||
float inverse = 1 / (va + vb + vc); return a + ab * (vb * inverse) + ac * (vc * inverse);
|
||||
}
|
||||
[UnityTest] public IEnumerator ActualGardenSplitFiveResumeRepairsWithoutChangingThreeAndTwoMembershipOrOtherSlots()
|
||||
{
|
||||
_store.Save.puzzleStates=new[]{new BoardRules(_app.Session.Definitions.First(stage=>stage.Id==2)).ToPuzzleState(GameSession.DefaultCamera()),new BoardRules(_app.Session.Definitions.First(stage=>stage.Id==6)).ToPuzzleState(GameSession.DefaultCamera())};
|
||||
Reload();_app.StartStage(9);
|
||||
var camera = new CameraState {target=new Position3(0,.6f,0),distance=11,rotation=new Rotation4(-.022557568f,.96225017f,.08418597f,.2578342f)};
|
||||
var saved = new FreePlayState {sourceStageId=9,camera=camera,
|
||||
balls=new[] {
|
||||
new BallState {id="s09-b01",position=new Position3(-1.0351909f,.8f,.60371524f)},
|
||||
new BallState {id="s09-b02",position=new Position3(-.03519098f,.8f,.60371524f)},
|
||||
new BallState {id="s09-b03",position=new Position3(-3.9800465f,.8f,1.649168f)},
|
||||
new BallState {id="s09-b04",position=new Position3(.964809f,.8f,.60371524f)},
|
||||
new BallState {id="s09-b05",position=new Position3(-2.9800465f,.8f,1.649168f)}},
|
||||
groups=new[] {new GroupState {id="s09-g01",ballIds=new[] {"s09-b01","s09-b02","s09-b04"}},new GroupState {id="s09-g01-split",ballIds=new[] {"s09-b03","s09-b05"}}}};
|
||||
var rawRotation=new Quaternion(camera.rotation.x,camera.rotation.y,camera.rotation.z,camera.rotation.w);
|
||||
var rawEye=BoardPresenter.ToVector(camera.target)+Vector3.up*.65f-rawRotation*Vector3.forward*camera.distance;
|
||||
Assert.That(rawEye.y,Is.LessThan(_app.Environment.PlaygroundBounds.min.y),"The exact old Content camera is underground and must be corrected separately from the friends.");
|
||||
var source=saved.balls.ToDictionary(ball=>ball.id,ball=>BoardPresenter.ToVector(ball.position));
|
||||
var fixedPoints=saved.groups[0].ballIds.Select(id=>source[id]).ToArray();
|
||||
Assert.That(fixedPoints.All(point=>_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point))),Is.True);
|
||||
var movedPoints=saved.groups[1].ballIds.Select(id=>source[id]).ToArray();
|
||||
Assert.That(movedPoints.All(point=>_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point))),Is.False);
|
||||
Assert.That(movedPoints.All(point=>_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point+Vector3.forward))&&fixedPoints.All(other=>Vector3.Distance(point+Vector3.forward,other)>=.999f)),Is.True,"A valid one-unit escape proves this board is repairable.");
|
||||
float sampledUpperBound=1;
|
||||
for(int index=0;index<64;index++)
|
||||
{
|
||||
float y=1-2*(index+.5f)/64,angle=index*2.39996323f;
|
||||
var direction=new Vector3(Mathf.Sqrt(1-y*y)*Mathf.Cos(angle),y,Mathf.Sqrt(1-y*y)*Mathf.Sin(angle));
|
||||
for(float radius=.01f;radius<sampledUpperBound;radius+=.01f)
|
||||
if(movedPoints.All(point=>_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point+direction*radius))&&fixedPoints.All(other=>Vector3.Distance(point+direction*radius,other)>=.999f))) {sampledUpperBound=radius;break;}
|
||||
}
|
||||
_store.Save.latestFreePlay=saved; string puzzles=string.Join("|",_store.Save.puzzleStates.Select(state=>JsonUtility.ToJson(state))); var completed=(int[])_store.Save.completedStageIds.Clone(); var tutorials=(string[])_store.Save.seenTutorialIds.Clone();
|
||||
Reload();int before=_store.Writes;_app.ContinueFreePlay();yield return null;
|
||||
Assert.That(_app.Session.Board,Is.Not.Null,"The exact existing Content split-five record must enter after certified repair.");
|
||||
Assert.That(_app.Session.Screen,Is.EqualTo(SessionScreen.FreePlay));Assert.That(_app.Session.Board.StageId,Is.EqualTo(9));
|
||||
Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board),Is.True);Assert.That(_store.Writes,Is.EqualTo(before+1));
|
||||
foreach(var group in saved.groups)
|
||||
{
|
||||
var result=_app.Session.Board.Groups.Single(value=>value.id==group.id);Assert.That(result.ballIds,Is.EquivalentTo(group.ballIds));
|
||||
var target=_app.Session.Board.Balls.ToDictionary(ball=>ball.id,ball=>BoardPresenter.ToVector(ball.position));
|
||||
foreach(string id in group.ballIds) Assert.That(Vector3.Distance(target[id]-target[group.ballIds[0]],source[id]-source[group.ballIds[0]]),Is.LessThan(.0001f));
|
||||
}
|
||||
var restored=_app.Session.Board.Balls.ToDictionary(ball=>ball.id,ball=>BoardPresenter.ToVector(ball.position));
|
||||
Assert.That(restored.Keys,Is.EquivalentTo(source.Keys));Assert.That(_app.Session.Board.Groups.Length,Is.EqualTo(2));
|
||||
foreach(string id in saved.groups[0].ballIds) Assert.That(restored[id],Is.EqualTo(source[id]),"Already safe friends stay exactly placed.");
|
||||
float distance=Vector3.Distance(restored["s09-b03"],source["s09-b03"]);
|
||||
Assert.That(distance,Is.LessThanOrEqualTo(sampledUpperBound+.011f));
|
||||
foreach(string id in saved.groups[1].ballIds) foreach(string other in saved.groups[0].ballIds) Assert.That(Vector3.Distance(restored[id],restored[other]),Is.GreaterThanOrEqualTo(.999f));
|
||||
Assert.That(string.Join("|",_store.Save.puzzleStates.Select(state=>JsonUtility.ToJson(state))),Is.EqualTo(puzzles));Assert.That(_store.Save.completedStageIds,Is.EqualTo(completed));Assert.That(_store.Save.seenTutorialIds,Is.EqualTo(tutorials));
|
||||
Assert.That(_app.Orbit.Camera.transform.position.y,Is.GreaterThanOrEqualTo(_app.Environment.PlaygroundBounds.min.y+.35f-.001f));
|
||||
Assert.That(JsonUtility.ToJson(_store.Save.latestFreePlay.camera),Is.EqualTo(JsonUtility.ToJson(_app.Orbit.Snapshot())));
|
||||
Debug.Log($"[GardenRestoreActual] utc={System.DateTime.UtcNow:O}, distance={distance:R}, independentRadialUpperBound={sampledUpperBound:R}");
|
||||
_app.ShowHome();string corrected=JsonUtility.ToJson(_store.Save.latestFreePlay);_app.ContinueFreePlay();yield return null;
|
||||
Assert.That(JsonUtility.ToJson(_store.Save.latestFreePlay),Is.EqualTo(corrected));Assert.That(_app.Instruction,Is.Not.EqualTo("친구를 놀이터로 다시 보냈어요."));
|
||||
}
|
||||
[UnityTest] public IEnumerator CountingResumeRepairsTheWholeGroupWithoutCollectingOrCompletingAndWritesOnce()
|
||||
{
|
||||
var puzzle = new PuzzleState { stageId = 2, balls = new[] { Ball("old-a", 30), Ball("old-b", 31) },
|
||||
groups = new[] { Group("old-group", "old-a", "old-b") }, collectedBallIds = new[] { "old-a" },
|
||||
countedBallIds = new[] { "old-a" }, camera = GameSession.DefaultCamera() };
|
||||
_store.Save.puzzleStates = new[] { puzzle }; Reload();
|
||||
var progress = (int[])_store.Save.completedStageIds.Clone(); int before = _store.Writes;
|
||||
_app.StartStage(2); yield return null;
|
||||
Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True, "Actual GameApp must preflight legacy positions before displaying/saving them.");
|
||||
Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.Puzzle));
|
||||
Assert.That(_app.Session.Board.Balls.Select(ball => ball.id), Is.EqualTo(new[] { "old-a", "old-b" }));
|
||||
Assert.That(_app.Session.Board.Groups.Single().id, Is.EqualTo("old-group"));
|
||||
Assert.That(_app.Session.Board.CountedBallIds, Is.EqualTo(puzzle.countedBallIds));
|
||||
Assert.That(_app.Session.Board.CollectedBallIds, Is.EqualTo(puzzle.collectedBallIds));
|
||||
var positions = _app.Session.Board.Balls.Select(ball => BoardPresenter.ToVector(ball.position)).ToArray();
|
||||
Assert.That(positions[1] - positions[0], Is.EqualTo(Vector3.right));
|
||||
Assert.That(positions[1].x, Is.EqualTo(_app.Environment.PlaygroundBounds.max.x - .5f).Within(.002f));
|
||||
Assert.That(_store.Writes, Is.EqualTo(before + 1));
|
||||
Assert.That(_store.Save.completedStageIds, Is.EqualTo(progress));
|
||||
Assert.That(_app.Instruction, Is.EqualTo("친구를 놀이터로 다시 보냈어요."));
|
||||
foreach (char character in _app.Instruction) Assert.That(Text("_instruction").font.HasCharacter(character), Is.True, "Approved static atlas must contain every restore character.");
|
||||
_app.ShowHome(); _app.StartStage(2); yield return null;
|
||||
Assert.That(_app.Instruction, Is.Not.EqualTo("친구를 놀이터로 다시 보냈어요."));
|
||||
}
|
||||
[UnityTest] public IEnumerator BoxInteriorMovesToTheNearestActualSafeSurfaceRatherThanAFullBoundaryEscape()
|
||||
{
|
||||
_app.StartStage(6); Box(new Vector3(0, 5, 0), new Vector3(2, 2, 2)); Free(new Vector3(0, 5, 0));
|
||||
_app.ContinueFreePlay(); yield return null;
|
||||
var restored = BoardPresenter.ToVector(_app.Session.Board.Balls.Single().position);
|
||||
Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True);
|
||||
Assert.That(Vector3.Distance(restored, new Vector3(0, 5, 0)), Is.InRange(1.498f, 1.501f));
|
||||
Assert.That(_app.Session.Board.Balls.Single().id, Is.EqualTo("legacy-free"));
|
||||
}
|
||||
[UnityTest] public IEnumerator NonReadableAsymmetricClosedMeshUsesCachedTrianglesAndFindsItsNearestSafeFace()
|
||||
{
|
||||
_app.StartStage(6);
|
||||
foreach (var collider in _app.Environment.GetComponentsInChildren<Collider>()) if (!collider.isTrigger && !collider.name.Contains("Clearing")) collider.enabled = false;
|
||||
var obstacle = new GameObject("test-only-asymmetric-closed-mesh"); _fixtures.Add(obstacle);
|
||||
obstacle.transform.SetParent(_app.Environment.transform, true);
|
||||
obstacle.transform.SetPositionAndRotation(new Vector3(0, 3, 0), Quaternion.Euler(0, 31, 0));
|
||||
var vertices = new[] { new Vector3(-1,-2,-3),new Vector3(1,-2,-3),new Vector3(1,2,-3),new Vector3(-1,2,-3),
|
||||
new Vector3(-1,-2,3),new Vector3(1,-2,3),new Vector3(1,2,3),new Vector3(-1,2,3) };
|
||||
var triangles = new[] { 0,2,1,0,3,2,4,5,6,4,6,7,0,1,5,0,5,4,3,7,6,3,6,2,0,4,7,0,7,3,1,2,6,1,6,5 };
|
||||
var mesh = new Mesh { vertices = vertices, triangles = triangles }; mesh.RecalculateBounds(); _fixtures.Add(mesh);
|
||||
var colliderMesh = obstacle.AddComponent<MeshCollider>(); colliderMesh.sharedMesh = mesh;
|
||||
var original = AssetDatabase.LoadAssetAtPath<PlaygroundRestoreGeometry>("Assets/MagnetismHeroes/UnityRuntime/Bootstrap/PlaygroundRestoreGeometry.asset");
|
||||
var cache = Object.Instantiate(original); _fixtures.Add(cache);
|
||||
var entries = new List<PlaygroundRestoreGeometry.Entry>();
|
||||
foreach (var actual in _app.Environment.GetComponentsInChildren<MeshCollider>())
|
||||
if (original.TryGet(actual.sharedMesh, out var sourceVertices, out var sourceTriangles)) entries.Add(new PlaygroundRestoreGeometry.Entry {
|
||||
mesh = actual.sharedMesh, vertices = sourceVertices, triangles = sourceTriangles, bounds = actual.sharedMesh.bounds,
|
||||
geometrySha256 = PlaygroundRestoreGeometry.Hash(sourceVertices, sourceTriangles) });
|
||||
entries.Add(new PlaygroundRestoreGeometry.Entry { mesh = mesh, vertices = vertices, triangles = triangles, bounds = mesh.bounds,
|
||||
geometrySha256 = PlaygroundRestoreGeometry.Hash(vertices, triangles) }); cache.SetEntries(entries.ToArray());
|
||||
mesh.UploadMeshData(true); Assert.That(mesh.isReadable, Is.False);
|
||||
_app.BindRestoreGeometry(cache); Physics.SyncTransforms(); Free(obstacle.transform.position);
|
||||
_app.ContinueFreePlay(); yield return null;
|
||||
Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.FreePlay)); Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True);
|
||||
var delta = BoardPresenter.ToVector(_app.Session.Board.Balls.Single().position) - obstacle.transform.position;
|
||||
Assert.That(delta.magnitude, Is.InRange(1.498f, 1.501f));
|
||||
Assert.That(Mathf.Abs(Vector3.Dot(delta.normalized, obstacle.transform.right)), Is.GreaterThan(.999f));
|
||||
Assert.That(_app.Session.Board.Balls.Single().id, Is.EqualTo("legacy-free"));
|
||||
}
|
||||
[UnityTest] public IEnumerator NearestCorrectionAccountsForAnotherObstacleAcrossAnOtherwiseSafeEscapeInterval()
|
||||
{
|
||||
_app.StartStage(6);
|
||||
Box(new Vector3(0,5,0),new Vector3(2,2,2)); Box(new Vector3(-1.7f,5,0),new Vector3(.2f,2,.4f));
|
||||
var point = new Vector3(-.8f,5,0); Free(point); _app.ContinueFreePlay(); yield return null;
|
||||
Assert.That(_app.Session.Screen,Is.EqualTo(SessionScreen.FreePlay)); Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board),Is.True);
|
||||
float expected = Mathf.Sqrt(.699f*.699f + Mathf.Pow(.2f+Mathf.Sqrt(.499f*.499f-.101f*.101f),2));
|
||||
float distance = Vector3.Distance(point,BoardPresenter.ToVector(_app.Session.Board.Balls.Single().position));
|
||||
Assert.That(distance,Is.InRange(expected-.003f,expected+.011f));
|
||||
Assert.That(_app.Session.Board.Balls.Single().id,Is.EqualTo("legacy-free"));
|
||||
}
|
||||
[UnityTest] public IEnumerator ExhaustiveTerrainCellExclusionCanResetWithoutMistakingACappedSearchForProof()
|
||||
{
|
||||
var state = new PuzzleState { stageId=2, balls=new[] {new BallState{id="covered-a",position=new Position3(-8,.05f,-6)},new BallState{id="covered-b",position=new Position3(8,6,6)}},
|
||||
groups=new[]{Group("covered-group","covered-a","covered-b")},camera=GameSession.DefaultCamera()};
|
||||
_store.Save.puzzleStates=new[]{state};
|
||||
// Neither solid contains the entire center domain. Their actual sphere-clearance excludes every cell.
|
||||
Box(new Vector3(-8.075f,.05f,-6),new Vector3(.15f,.3f,.3f)); Box(new Vector3(-7.925f,.05f,-6),new Vector3(.15f,.3f,.3f));
|
||||
Reload();int before=_store.Writes;_app.StartStage(2);yield return null;
|
||||
Assert.That(_app.Session.Board.Balls.Select(ball=>ball.id),Is.EqualTo(new[]{"s02-b01","s02-b02"}));
|
||||
Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board),Is.True);Assert.That(_store.Writes,Is.EqualTo(before+1));
|
||||
Assert.That(_app.Instruction,Is.EqualTo("친구가 놀이터에 들어가지 못해서 처음부터 다시 시작해요."));
|
||||
}
|
||||
[UnityTest] public IEnumerator UnsupportedSolidSearchFailureKeepsTheRecordWithoutResettingOrWriting()
|
||||
{
|
||||
_app.StartStage(6);
|
||||
foreach (var collider in _app.Environment.GetComponentsInChildren<Collider>()) if (!collider.isTrigger) collider.enabled = false;
|
||||
var obstacle = new GameObject("test-only-unsupported-restore-solid"); _fixtures.Add(obstacle);
|
||||
obstacle.transform.position = new Vector3(0,3,0); var capsule = obstacle.AddComponent<CapsuleCollider>(); capsule.radius = 50; capsule.height = 100;
|
||||
Free(new Vector3(0,3,0)); string saved = JsonUtility.ToJson(_store.Save); int before = _store.Writes;
|
||||
_app.ContinueFreePlay(); yield return null;
|
||||
Assert.That(_app.Session.Screen,Is.EqualTo(SessionScreen.Home)); Assert.That(_app.Session.Board,Is.Null);
|
||||
Assert.That(_store.Writes,Is.EqualTo(before)); Assert.That(JsonUtility.ToJson(_store.Save),Is.EqualTo(saved));
|
||||
Assert.That(_app.Instruction,Is.Not.EqualTo("친구가 놀이터에 들어가지 못해서 처음부터 다시 시작해요."));
|
||||
}
|
||||
[UnityTest] public IEnumerator CorruptGeometryChecksumKeepsTheExistingRecordInsteadOfTreatingSearchFailureAsImpossible()
|
||||
{
|
||||
_app.StartStage(6); var cache = Object.Instantiate(AssetDatabase.LoadAssetAtPath<PlaygroundRestoreGeometry>("Assets/MagnetismHeroes/UnityRuntime/Bootstrap/PlaygroundRestoreGeometry.asset")); _fixtures.Add(cache);
|
||||
var serialized = new SerializedObject(cache); var entries = serialized.FindProperty("_entries");
|
||||
for (int index = 0; index < entries.arraySize; index++) entries.GetArrayElementAtIndex(index).FindPropertyRelative("geometrySha256").stringValue = "corrupt";
|
||||
serialized.ApplyModifiedPropertiesWithoutUndo(); _app.BindRestoreGeometry(cache);
|
||||
Free(new Vector3(30, 5, 0)); string saved = JsonUtility.ToJson(_store.Save); int before = _store.Writes;
|
||||
_app.ContinueFreePlay(); yield return null;
|
||||
Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.Home)); Assert.That(_app.Session.Board, Is.Null);
|
||||
Assert.That(_store.Writes, Is.EqualTo(before)); Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(saved));
|
||||
Assert.That(_app.Instruction, Is.Not.EqualTo("친구가 놀이터에 들어가지 못해서 처음부터 다시 시작해요."));
|
||||
}
|
||||
[UnityTest] public IEnumerator ApprovedUnreadableRockAndOverlappingTreeDeepInteriorsResumeThroughTheRuntimeCache()
|
||||
{
|
||||
_app.StartStage(6);
|
||||
foreach (var collider in _app.Environment.GetComponentsInChildren<Collider>()) if (!collider.isTrigger && !collider.name.Contains("Clearing")) collider.enabled = false;
|
||||
foreach (string name in new[] { "SlateBoulder", "RoundCanopyTree" })
|
||||
{
|
||||
var obstacle = Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Meadow/" + name + ".prefab"), _app.Environment.transform);
|
||||
_fixtures.Add(obstacle); obstacle.transform.localScale = Vector3.one * (name == "SlateBoulder" ? 3 : 2);
|
||||
var collider = obstacle.GetComponentInChildren<MeshCollider>(); Physics.SyncTransforms();
|
||||
obstacle.transform.position += new Vector3(0, 3, 0) - collider.bounds.center; Physics.SyncTransforms();
|
||||
Assert.That(collider.sharedMesh.isReadable, Is.False, "Actual approved imports stay non-readable.");
|
||||
Vector3 point = collider.bounds.center; bool found = !_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point));
|
||||
for (float y = Mathf.Max(.6f, collider.bounds.min.y + .5f); !found && y < Mathf.Min(5.8f, collider.bounds.max.y - .5f); y += .1f)
|
||||
{ point = new Vector3(collider.bounds.center.x, y, collider.bounds.center.z); found = !_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point)); }
|
||||
Assert.That(found, Is.True, "Boundary injection must start inside the actual approved solid.");
|
||||
Free(point); int before = _store.Writes;
|
||||
var elapsed = System.Diagnostics.Stopwatch.StartNew(); _app.ContinueFreePlay(); elapsed.Stop(); yield return null;
|
||||
Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.FreePlay), name + " must repair rather than merely retain the unsafe legacy save.");
|
||||
Assert.That(_app.Session.Board.Balls.Single().id, Is.EqualTo("legacy-free")); Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True);
|
||||
Assert.That(_store.Writes, Is.EqualTo(before + 1));
|
||||
float distance = Vector3.Distance(point, BoardPresenter.ToVector(_app.Session.Board.Balls.Single().position));
|
||||
float sampledUpperBound = float.PositiveInfinity;
|
||||
Vector3 sampledPoint = point;
|
||||
// Independent actual-physics rays sample every radial interval; no binary assumption about solid gaps.
|
||||
for (int index = 0; index < 96; index++)
|
||||
{
|
||||
float y = 1 - 2 * (index + .5f) / 96; float angle = index * 2.39996323f;
|
||||
var direction = new Vector3(Mathf.Sqrt(1-y*y)*Mathf.Cos(angle), y, Mathf.Sqrt(1-y*y)*Mathf.Sin(angle));
|
||||
for (float radius = .02f; radius <= Mathf.Min(4, sampledUpperBound); radius += .02f)
|
||||
if (_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point + direction * radius))) { sampledUpperBound = radius; sampledPoint = point + direction * radius; break; }
|
||||
}
|
||||
var sphere = _app.Presenter.Balls["legacy-free"].GetComponent<SphereCollider>();
|
||||
bool penetration = Physics.ComputePenetration(sphere, sampledPoint, Quaternion.identity, collider, collider.transform.position, collider.transform.rotation, out _, out float depth);
|
||||
bool overlap = Physics.OverlapSphere(sampledPoint, .5f, ~0, QueryTriggerInteraction.Ignore).Contains(collider);
|
||||
bool ray = collider.Raycast(new Ray(sampledPoint, (point - sampledPoint).normalized), out var hit, 6);
|
||||
Debug.Log($"[ProgressRestoreActual] approved={name}, utc={System.DateTime.UtcNow:O}, ms={elapsed.ElapsedMilliseconds}, source={point:R}, restoredDistance={distance:R}, sampledSafe={sampledPoint:R}, radialUpperBound={sampledUpperBound:R}, overlap={overlap}, penetration={penetration}, depth={depth:R}, inwardRay={ray}, surfaceDistance={hit.distance:R}");
|
||||
MeshDiagnostic(collider,point,sampledPoint);
|
||||
Assert.That(distance, Is.LessThanOrEqualTo(sampledUpperBound + .011f));
|
||||
Debug.Log($"[ProgressRestoreActual] approved={name}, utc={System.DateTime.UtcNow:O}, ms={elapsed.ElapsedMilliseconds}, distance={distance:R}, independentRadialUpperBound={sampledUpperBound:R}");
|
||||
obstacle.SetActive(false);
|
||||
}
|
||||
}
|
||||
[UnityTest] public IEnumerator ClosedBoxCoversEveryRigidCandidateButInitialPuzzleFitsAndOnlyAffectedSlotResets()
|
||||
{
|
||||
var state = new PuzzleState { stageId = 2,
|
||||
balls = new[] { new BallState { id = "blocked-a", position = new Position3(-8, .05f, -6) }, new BallState { id = "blocked-b", position = new Position3(8, 6, 6) } },
|
||||
groups = new[] { Group("blocked-group", "blocked-a", "blocked-b") },
|
||||
collectedBallIds = new[] { "blocked-a" }, countedBallIds = new[] { "blocked-a" }, camera = new CameraState { distance = 12 } };
|
||||
_store.Save.puzzleStates = new[] { new BoardRules(_app.Session.Definitions.First(stage => stage.Id == 1)).ToPuzzleState(GameSession.DefaultCamera()), state,
|
||||
new BoardRules(_app.Session.Definitions.First(stage => stage.Id == 6)).ToPuzzleState(GameSession.DefaultCamera()) };
|
||||
_store.Save.latestFreePlay = new BoardRules(_app.Session.Definitions.First(stage => stage.Id == 6)).ToFreePlayState(GameSession.DefaultCamera());
|
||||
string otherOne = JsonUtility.ToJson(_store.Save.puzzleStates[0]), otherSix = JsonUtility.ToJson(_store.Save.puzzleStates[2]), free = JsonUtility.ToJson(_store.Save.latestFreePlay);
|
||||
Box(new Vector3(-8, .05f, -6), Vector3.one * .3f); Reload(); int before = _store.Writes;
|
||||
_app.StartStage(2); yield return null;
|
||||
Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True);
|
||||
Assert.That(_app.Session.Board.Balls.Select(ball => ball.id), Is.EqualTo(new[] { "s02-b01", "s02-b02" }));
|
||||
Assert.That(_app.Session.Board.CollectedBallIds, Is.Empty); Assert.That(_app.Session.Board.CountedBallIds, Is.Empty);
|
||||
Assert.That(_store.Writes, Is.EqualTo(before + 1));
|
||||
Assert.That(JsonUtility.ToJson(_store.Save.puzzleStates[0]), Is.EqualTo(otherOne)); Assert.That(JsonUtility.ToJson(_store.Save.puzzleStates[2]), Is.EqualTo(otherSix));
|
||||
Assert.That(JsonUtility.ToJson(_store.Save.latestFreePlay), Is.EqualTo(free));
|
||||
Assert.That(_app.Session.Camera.distance, Is.EqualTo(8));
|
||||
}
|
||||
[UnityTest] public IEnumerator MissingCacheCannotResetAnImpossibleBoardAndFailedWriteRestoresPreviousStageAndCamera()
|
||||
{
|
||||
_store.Save.puzzleStates = new[] { new PuzzleState { stageId = 2, balls = new[] { Ball("wide-a", -30), Ball("wide-b", 30) },
|
||||
groups = new[] { Group("wide-group", "wide-a", "wide-b") }, camera = GameSession.DefaultCamera() } };
|
||||
Reload(); _app.StartStage(6);
|
||||
var manual = new CameraState { target = new Position3(0, .6f, 0), rotation = new Rotation4(0, 0, 0, 1), distance = 11 };
|
||||
_app.Session.CommitCamera(manual); _app.Orbit.Apply(manual);
|
||||
var current = _app.Session.Board; string theme = _app.Environment.Theme, saved = JsonUtility.ToJson(_store.Save); int before = _store.Writes;
|
||||
_app.BindRestoreGeometry(null); _app.StartStage(2); yield return null;
|
||||
Assert.That(_app.Session.Board, Is.SameAs(current)); Assert.That(_app.Environment.Theme, Is.EqualTo(theme));
|
||||
Assert.That(_store.Writes, Is.EqualTo(before)); Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(saved));
|
||||
Assert.That(JsonUtility.ToJson(_app.Orbit.Snapshot()), Is.EqualTo(JsonUtility.ToJson(manual)));
|
||||
_app.BindRestoreGeometry(AssetDatabase.LoadAssetAtPath<PlaygroundRestoreGeometry>("Assets/MagnetismHeroes/UnityRuntime/Bootstrap/PlaygroundRestoreGeometry.asset"));
|
||||
_store.FailWrites = true; _app.StartStage(2); yield return null;
|
||||
Assert.That(_app.Session.Board, Is.SameAs(current)); Assert.That(_app.Environment.Theme, Is.EqualTo(theme));
|
||||
Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(saved)); Assert.That(_store.Writes, Is.EqualTo(before + 1));
|
||||
Assert.That(JsonUtility.ToJson(_app.Orbit.Snapshot()), Is.EqualTo(JsonUtility.ToJson(manual)));
|
||||
Assert.That(Text("_saveNotice").text, Is.EqualTo("지금은 저장하지 못했어요. 다시 해볼게요."));
|
||||
Assert.That(_app.Instruction, Is.Not.EqualTo("친구가 놀이터에 들어가지 못해서 처음부터 다시 시작해요."));
|
||||
}
|
||||
[UnityTest] public IEnumerator RepairedUnseenTutorialDoesNotChangeTutorialRecordAddAWriteOrOverwriteTheNotice()
|
||||
{
|
||||
_store.Save.completedStageIds = Enumerable.Range(1, 5).ToArray(); _store.Save.seenTutorialIds = new string[0];
|
||||
_store.Save.puzzleStates = new[] { new PuzzleState { stageId = 6, balls = new[] { Ball("demo-a", 30), Ball("demo-b", 33) },
|
||||
groups = new[] { Group("demo-first", "demo-a"), Group("demo-second", "demo-b") }, camera = GameSession.DefaultCamera() } };
|
||||
Reload(); int before = _store.Writes; _app.StartStage(6); yield return null;
|
||||
Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True);
|
||||
Assert.That(_store.Writes, Is.EqualTo(before + 1)); Assert.That(_store.Save.seenTutorialIds, Is.Empty);
|
||||
Assert.That(_app.Instruction, Is.EqualTo("친구를 놀이터로 다시 보냈어요."));
|
||||
}
|
||||
[UnityTest] public IEnumerator TargetSceneryReplacesInactivePreviousStageCollidersBeforeRestorePreflight()
|
||||
{
|
||||
_app.StartStage(6); _app.ShowHome();
|
||||
var stale = Box(new Vector3(0, 3, 0), new Vector3(40, 40, 40)); stale.transform.SetParent(_app.Environment.transform, true);
|
||||
_store.Save.puzzleStates = new[] { new PuzzleState { stageId = 2, balls = new[] { Ball("target-a", 30), Ball("target-b", 31) },
|
||||
groups = new[] { Group("target-group", "target-a", "target-b") }, camera = GameSession.DefaultCamera() } };
|
||||
Reload(); _app.StartStage(2); yield return null;
|
||||
Assert.That(_app.Session.Board.StageId, Is.EqualTo(2)); Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True);
|
||||
Assert.That(_app.Session.Board.Balls.Select(ball => ball.id), Is.EqualTo(new[] { "target-a", "target-b" }));
|
||||
Assert.That(_app.Environment.gameObject.activeInHierarchy, Is.True);
|
||||
}
|
||||
[UnityTest] public IEnumerator SafeResumeKeepsExactPuzzleFreeProgressAndManualCamera()
|
||||
{
|
||||
var definition = _app.Session.Definitions.First(stage => stage.Id == 2);
|
||||
var camera = new CameraState { target = new Position3(0, .6f, 0), rotation = new Rotation4(0, 0, 0, 1), distance = 11 };
|
||||
_store.Save.puzzleStates = new[] { new BoardRules(definition).ToPuzzleState(camera) };
|
||||
_store.Save.latestFreePlay = new BoardRules(_app.Session.Definitions.First(stage => stage.Id == 6)).ToFreePlayState(GameSession.DefaultCamera());
|
||||
string expected = JsonUtility.ToJson(_store.Save); Reload();
|
||||
_app.StartStage(2); yield return null;
|
||||
Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True);
|
||||
Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(expected));
|
||||
Assert.That(JsonUtility.ToJson(_app.Session.Camera), Is.EqualTo(JsonUtility.ToJson(camera)));
|
||||
Assert.That(_app.Instruction, Is.Not.EqualTo("친구를 놀이터로 다시 보냈어요."));
|
||||
}
|
||||
[UnityTest] public IEnumerator RepairableFreeKeepsSourceShapeAndAllOtherSlotsWithNoAutomaticMerge()
|
||||
{
|
||||
_store.Save.puzzleStates = new[] { new BoardRules(_app.Session.Definitions.First(stage => stage.Id == 2)).ToPuzzleState(GameSession.DefaultCamera()),
|
||||
new BoardRules(_app.Session.Definitions.First(stage => stage.Id == 6)).ToPuzzleState(GameSession.DefaultCamera()) };
|
||||
_store.Save.latestFreePlay = new FreePlayState { sourceStageId = 6,
|
||||
balls = new[] { Ball("free-a", 30), Ball("free-b", 31), Ball("free-c", 34) },
|
||||
groups = new[] { Group("free-pair", "free-a", "free-b"), Group("free-single", "free-c") }, camera = GameSession.DefaultCamera() };
|
||||
string firstPuzzle = JsonUtility.ToJson(_store.Save.puzzleStates[0]), secondPuzzle = JsonUtility.ToJson(_store.Save.puzzleStates[1]);
|
||||
Reload(); int before = _store.Writes; _app.ContinueFreePlay(); yield return null;
|
||||
Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True);
|
||||
Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.FreePlay));
|
||||
Assert.That(_app.Session.Board.StageId, Is.EqualTo(6));
|
||||
Assert.That(_app.Session.Board.Groups.Select(group => group.id), Is.EqualTo(new[] { "free-pair", "free-single" }));
|
||||
var balls = _app.Session.Board.Balls.ToDictionary(ball => ball.id, ball => BoardPresenter.ToVector(ball.position));
|
||||
Assert.That(balls["free-b"] - balls["free-a"], Is.EqualTo(Vector3.right));
|
||||
foreach (string pair in new[] { "free-a", "free-b" }) Assert.That(Vector3.Distance(balls[pair], balls["free-c"]), Is.GreaterThanOrEqualTo(.999f));
|
||||
Assert.That(JsonUtility.ToJson(_store.Save.puzzleStates[0]), Is.EqualTo(firstPuzzle));
|
||||
Assert.That(JsonUtility.ToJson(_store.Save.puzzleStates[1]), Is.EqualTo(secondPuzzle));
|
||||
Assert.That(_store.Writes, Is.EqualTo(before + 1));
|
||||
}
|
||||
[UnityTest] public IEnumerator ImpossibleFreeResumeResetsOnlyLatestFreeToSourceInitialAndKeepsPuzzleSlotsAndProgress()
|
||||
{
|
||||
var definition = _app.Session.Definitions.First(stage => stage.Id == 6);
|
||||
var initial = new BoardRules(definition);
|
||||
_store.Save.puzzleStates = new[] { new BoardRules(_app.Session.Definitions.First(stage => stage.Id == 2)).ToPuzzleState(GameSession.DefaultCamera()), initial.ToPuzzleState(GameSession.DefaultCamera()) };
|
||||
_store.Save.latestFreePlay = new FreePlayState { sourceStageId = 6, balls = new[] { Ball("wide-a", -30), Ball("wide-b", 30) },
|
||||
groups = new[] { Group("wide-group", "wide-a", "wide-b") }, camera = new CameraState { distance = 12, target = new Position3(1, 1, 1) } };
|
||||
var puzzles = JsonUtility.ToJson(_store.Save); Reload(); int before = _store.Writes;
|
||||
_app.ContinueFreePlay(); yield return null;
|
||||
Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True);
|
||||
Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.FreePlay));
|
||||
Assert.That(_app.Session.Board.Balls.Select(ball => ball.id), Is.EqualTo(initial.Balls.Select(ball => ball.id)));
|
||||
Assert.That(_app.Session.Board.Balls.Select(ball => ball.position), Is.EqualTo(initial.Balls.Select(ball => ball.position)));
|
||||
Assert.That(_store.Save.puzzleStates.Length, Is.EqualTo(2));
|
||||
var expected = JsonUtility.FromJson<ProgressSave>(puzzles);
|
||||
Assert.That(JsonUtility.ToJson(_store.Save.puzzleStates[0]), Is.EqualTo(JsonUtility.ToJson(expected.puzzleStates[0])));
|
||||
Assert.That(JsonUtility.ToJson(_store.Save.puzzleStates[1]), Is.EqualTo(JsonUtility.ToJson(expected.puzzleStates[1])));
|
||||
Assert.That(_store.Save.completedStageIds, Is.EqualTo(expected.completedStageIds));
|
||||
Assert.That(_store.Save.seenTutorialIds, Is.EqualTo(expected.seenTutorialIds));
|
||||
Assert.That(_store.Writes, Is.EqualTo(before + 1));
|
||||
Assert.That(_app.Session.Camera.distance, Is.EqualTo(GameSession.DefaultCamera().distance));
|
||||
Assert.That(_app.Instruction, Is.EqualTo("친구가 놀이터에 들어가지 못해서 처음부터 다시 시작해요."));
|
||||
foreach (char character in _app.Instruction) Assert.That(Text("_instruction").font.HasCharacter(character), Is.True);
|
||||
Text("_instruction").ForceMeshUpdate(); Assert.That(Text("_instruction").textBounds.size.x, Is.LessThanOrEqualTo(Text("_instruction").rectTransform.rect.width + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 51217ffa475e6664fbe43e44d14c421f
|
||||
@@ -0,0 +1,387 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MagnetismHeroes.Core;
|
||||
using MagnetismHeroes.UnityRuntime.Bootstrap;
|
||||
using MagnetismHeroes.UnityRuntime.Presentation;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using Unity.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
namespace MagnetismHeroes.Tests
|
||||
{
|
||||
public sealed class PlaygroundSafetyPlayTests
|
||||
{
|
||||
private sealed class MemoryStore : IProgressStore
|
||||
{
|
||||
public ProgressSave Save = new ProgressSave { completedStageIds = Enumerable.Range(1, 30).ToArray(), seenTutorialIds = new[] { "merge", "split" } };
|
||||
public int Writes;
|
||||
public bool TryLoad(out ProgressSave save, out string error) { save = Save; error = null; return true; }
|
||||
public bool TrySave(ProgressSave save, out string error) { Save = save; Writes++; error = null; return true; }
|
||||
}
|
||||
private GameApp _app;
|
||||
private MemoryStore _store;
|
||||
private readonly List<GameObject> _obstacles = new List<GameObject>();
|
||||
[UnitySetUp] public IEnumerator Setup()
|
||||
{
|
||||
foreach (var listener in Object.FindObjectsByType<AudioListener>(FindObjectsSortMode.None)) listener.enabled = false;
|
||||
foreach (var camera in Object.FindObjectsByType<Camera>(FindObjectsSortMode.None)) camera.enabled = false;
|
||||
_app = Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GameApp.prefab")).GetComponent<GameApp>();
|
||||
_store = new MemoryStore(); _app.Initialize(_store); _app.StartStage(6); _app.Voice.Clear();
|
||||
// Keep the real clearing; isolate movement cases from the unrelated authored props.
|
||||
foreach (var collider in _app.Environment.GetComponentsInChildren<Collider>())
|
||||
if (!collider.isTrigger && !collider.name.Contains("Clearing")) collider.enabled = false;
|
||||
yield return null;
|
||||
}
|
||||
[UnityTearDown] public IEnumerator Teardown()
|
||||
{
|
||||
foreach (var obstacle in _obstacles) Object.Destroy(obstacle);
|
||||
_obstacles.Clear(); Object.Destroy(_app.gameObject); yield return null;
|
||||
}
|
||||
private void Inject(Vector3[] positions, params int[] sizes)
|
||||
{
|
||||
// Boundary state injection, not normal stage progression: free-play balls/groups/camera only.
|
||||
var balls = positions.Select((point, index) => new BallState { id = "safety-" + index, position = BoardPresenter.ToPosition(point) }).ToArray();
|
||||
int offset = 0;
|
||||
var groups = sizes.Select((size, index) => { var ids = balls.Skip(offset).Take(size).Select(ball => ball.id).ToArray(); offset += size; return new GroupState { id = "group-" + index, ballIds = ids }; }).ToArray();
|
||||
_store.Save.latestFreePlay = new FreePlayState { sourceStageId = 6, balls = balls, groups = groups, camera = _app.Session.Camera };
|
||||
_app.Initialize(_store); _app.ContinueFreePlay(); _app.Voice.Clear(); Physics.SyncTransforms();
|
||||
}
|
||||
private GameObject Wall(Vector3 center, Vector3 size, Quaternion rotation)
|
||||
{
|
||||
var wall = new GameObject("test-only-thin-obstacle"); wall.transform.SetPositionAndRotation(center, rotation);
|
||||
wall.AddComponent<BoxCollider>().size = size; _obstacles.Add(wall); Physics.SyncTransforms(); return wall;
|
||||
}
|
||||
private Vector2 Point(string id) => _app.Presenter.Camera.WorldToScreenPoint(_app.Presenter.Balls[id].transform.position);
|
||||
private void AssertUnchanged(string snapshot) => Assert.That(JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Session.Camera)), Is.EqualTo(snapshot));
|
||||
|
||||
[UnityTest] public IEnumerator FastThinWallDragStopsThenReversesImmediatelyAndCommitsLastPreview()
|
||||
{
|
||||
Inject(new[] { new Vector3(0, 2, 0) }, 1);
|
||||
var camera = _app.Presenter.Camera; var initial = _app.Presenter.Balls["safety-0"].transform.position;
|
||||
Wall(initial + camera.transform.right * .8f, new Vector3(.02f, 12, 12), camera.transform.rotation);
|
||||
var start = Point("safety-0"); var destination = start + new Vector2(150, 0);
|
||||
_app.Input.Begin(start, false); _app.Input.Move(destination);
|
||||
var stopped = _app.Presenter.Balls["safety-0"].transform.position;
|
||||
float movement = Vector3.Dot(stopped - initial, camera.transform.right);
|
||||
Assert.That(movement, Is.InRange(.1f, .291f), "A fast pointer move must sweep the sphere, not just test its endpoint.");
|
||||
_app.Input.Move(destination - new Vector2(12, 0));
|
||||
var reversed = _app.Presenter.Balls["safety-0"].transform.position;
|
||||
Assert.That(Vector3.Dot(reversed - stopped, camera.transform.right), Is.LessThan(-.01f), "Reverse input must act at the stopped point without consuming the blocked delta.");
|
||||
_app.Input.End(destination - new Vector2(12, 0)); yield return null;
|
||||
Assert.That(BoardPresenter.ToVector(_app.Session.Board.Balls.Single().position), Is.EqualTo(reversed));
|
||||
Assert.That(_app.Session.Board.Balls.Single().id, Is.EqualTo("safety-0"));
|
||||
}
|
||||
[UnityTest] public IEnumerator FastTenSphereTranslationStopsForTheLeadingMemberAndCommitsOneUnchangedShape()
|
||||
{
|
||||
var positions = Enumerable.Range(0, 10).Select(index => new Vector3(index % 5 - 2, 2 + index / 5, 0)).ToArray();
|
||||
Inject(positions, 10); Wall(new Vector3(3, 2, 0), new Vector3(.02f, 12, 12), Quaternion.identity);
|
||||
_app.Presenter.PreviewGroup("group-0", Vector3.right * 20);
|
||||
var delta = _app.Presenter.DropDelta;
|
||||
Assert.That(delta.x, Is.InRange(.48f, .491f));
|
||||
var preview = _app.Presenter.Balls.ToDictionary(pair => pair.Key, pair => pair.Value.transform.position);
|
||||
_app.CommitMove("group-0", BoardPresenter.ToPosition(delta)); yield return null;
|
||||
Assert.That(_app.Session.Board.BallCount, Is.EqualTo(10));
|
||||
Assert.That(_app.Session.Board.Groups.Single().ballIds, Is.EquivalentTo(preview.Keys));
|
||||
foreach (var ball in _app.Session.Board.Balls)
|
||||
{
|
||||
Assert.That(BoardPresenter.ToVector(ball.position), Is.EqualTo(preview[ball.id]));
|
||||
int index = int.Parse(ball.id.Substring(7)); Assert.That(preview[ball.id], Is.EqualTo(positions[index] + delta));
|
||||
}
|
||||
}
|
||||
[UnityTest] public IEnumerator SingleAndTenSpheresStopAtAllSixFiniteBoundsWithoutChangingShape()
|
||||
{
|
||||
foreach (int count in new[] { 1, 10 })
|
||||
foreach (var direction in new[] { Vector3.left, Vector3.right, Vector3.forward, Vector3.back, Vector3.up, Vector3.down })
|
||||
{
|
||||
var positions = Enumerable.Range(0, count).Select(index => new Vector3(index % 5 - 2, 2 + index / 5, 0)).ToArray();
|
||||
Inject(positions, count); _app.Presenter.PreviewGroup("group-0", direction * 40);
|
||||
var delta = _app.Presenter.DropDelta;
|
||||
Assert.That(delta.magnitude, Is.GreaterThan(.1f)); Assert.That(delta.magnitude, Is.LessThan(40));
|
||||
var bounds = _app.Environment.PlaygroundBounds;
|
||||
foreach (var ball in _app.Presenter.Balls.Values)
|
||||
{
|
||||
var point = ball.transform.position;
|
||||
Assert.That(point.x - .5f, Is.GreaterThanOrEqualTo(bounds.min.x - .002f));
|
||||
Assert.That(point.x + .5f, Is.LessThanOrEqualTo(bounds.max.x + .002f));
|
||||
Assert.That(point.y - .5f, Is.GreaterThanOrEqualTo(bounds.min.y - .002f));
|
||||
Assert.That(point.y + .5f, Is.LessThanOrEqualTo(bounds.max.y + .002f));
|
||||
Assert.That(point.z - .5f, Is.GreaterThanOrEqualTo(bounds.min.z - .002f));
|
||||
Assert.That(point.z + .5f, Is.LessThanOrEqualTo(bounds.max.z + .002f));
|
||||
var index = int.Parse(ball.BallId.Substring(7));
|
||||
Assert.That(Vector3.Distance(point, positions[index] + delta), Is.LessThan(.0001f));
|
||||
}
|
||||
Assert.That(_app.Presenter.CanDropGroup("group-0"), Is.True);
|
||||
_app.Presenter.CancelPreview(); yield return null;
|
||||
}
|
||||
}
|
||||
[UnityTest] public IEnumerator ActualGroundStopsTheFullRadiusAndSmallFoliageAllowsMovement()
|
||||
{
|
||||
Inject(new[] { new Vector3(0, 2, 0) }, 1);
|
||||
Assert.That(Physics.Raycast(new Vector3(0, 2, 0), Vector3.down, out var ground, 10), Is.True);
|
||||
_app.Presenter.PreviewGroup("group-0", Vector3.down * 20);
|
||||
Assert.That(_app.Presenter.Balls["safety-0"].transform.position.y, Is.GreaterThanOrEqualTo(ground.point.y + .499f));
|
||||
Assert.That(_app.Presenter.DropDelta.y, Is.LessThan(-.1f)); _app.Presenter.CancelPreview();
|
||||
var foliage = Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Meadow/GrassTuftMint.prefab"));
|
||||
foliage.transform.position = new Vector3(1, 2, 0); _obstacles.Add(foliage);
|
||||
Assert.That(foliage.GetComponentsInChildren<Collider>(), Is.Empty);
|
||||
_app.Presenter.PreviewGroup("group-0", Vector3.right * 2);
|
||||
Assert.That(_app.Presenter.DropDelta, Is.EqualTo(Vector3.right * 2)); yield return null;
|
||||
}
|
||||
[UnityTest] public IEnumerator ContactCorrectionCannotCrossThinObstacleOrOfferFalseDepthMerge()
|
||||
{
|
||||
Inject(new[] { new Vector3(-1, 2, 0), new Vector3(1, 2, 0) }, 1, 1);
|
||||
Wall(new Vector3(0, 2, 0), new Vector3(.02f, 10, 10), Quaternion.identity);
|
||||
_app.Presenter.PreviewGroup("group-0", Vector3.right * 2);
|
||||
Assert.That(_app.Presenter.Balls["safety-0"].transform.position.x, Is.LessThanOrEqualTo(-.509f));
|
||||
Assert.That(_app.Presenter.MergeCandidate, Is.Null);
|
||||
_app.Presenter.CancelPreview(); _obstacles[0].SetActive(false);
|
||||
Inject(new[] { new Vector3(0, 2, 0), new Vector3(0, 2, 2) }, 1, 1);
|
||||
_app.Presenter.PreviewGroup("group-0", Vector3.right * .1f);
|
||||
Assert.That(_app.Presenter.MergeCandidate, Is.Null, "Projected overlap is not three-dimensional contact."); yield return null;
|
||||
}
|
||||
[UnityTest] public IEnumerator SafePointerMoveRejectsContactSnapIntoAThinWall()
|
||||
{
|
||||
Inject(new[] { new Vector3(-1.2f, 2, 0), new Vector3(0, 2, 0) }, 1, 1);
|
||||
Wall(new Vector3(-.6f, 2, 0), new Vector3(.02f, 10, 10), Quaternion.identity);
|
||||
_app.Presenter.PreviewGroup("group-0", Vector3.right * .01f);
|
||||
Assert.That(_app.Presenter.DropDelta.x, Is.EqualTo(.01f).Within(.0001f));
|
||||
Assert.That(_app.Presenter.MergeCandidate, Is.Null, "The contact snap must sweep and reject wall penetration even when the pointer path is safe.");
|
||||
Assert.That(_app.Presenter.Balls.Values.All(ball => !ball.ContactCandidate), Is.True); yield return null;
|
||||
}
|
||||
private void SelectSplitBall(string id)
|
||||
{
|
||||
var point = Point(id); _app.Input.Begin(point, false); _app.Input.End(point);
|
||||
_app.Input.BeginSplit(); _app.Input.Begin(point, false); _app.Input.End(point);
|
||||
Assert.That(_app.Input.SelectedIds, Does.Contain(id));
|
||||
}
|
||||
[UnityTest] public IEnumerator SplitFirstClearanceJumpCannotTeleportThroughAThinWall()
|
||||
{
|
||||
var positions = Enumerable.Range(0, 10).Select(index => new Vector3(index % 5 - 2, 2 + index / 5, 0)).ToArray();
|
||||
Inject(positions, 10); Wall(new Vector3(-4.9f, 2, 0), new Vector3(.02f, 12, 12), Quaternion.identity);
|
||||
Assert.That(_app.Session.Board.PreviewSplit("group-0", new[] { "safety-0" }, new Position3(-.5f, 0, 0)).All(ball => _app.Presenter.IsSafePosition(ball.position)), Is.True, "The Core final arrays are safe; only their drag path crosses the wall.");
|
||||
SelectSplitBall("safety-0");
|
||||
var snapshot = JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Session.Camera)); var writes = _store.Writes;
|
||||
var point = Point("safety-0"); _app.Input.Begin(point, false); _app.Input.Move(point - new Vector2(30, 0));
|
||||
Assert.That(_app.Presenter.DropDelta, Is.EqualTo(Vector3.zero));
|
||||
Assert.That(_app.Presenter.CanDropGroup("group-0"), Is.False);
|
||||
_app.Input.End(point - new Vector2(30, 0)); yield return null;
|
||||
AssertUnchanged(snapshot); Assert.That(_store.Writes, Is.EqualTo(writes));
|
||||
foreach (var ball in _app.Session.Board.Balls) Assert.That(_app.Presenter.Balls[ball.id].transform.position, Is.EqualTo(BoardPresenter.ToVector(ball.position)));
|
||||
}
|
||||
[UnityTest] public IEnumerator SplitBlockedPathKeepsExactValidDeltaThenReversesAndCommitsItsPreview()
|
||||
{
|
||||
Inject(new[] { new Vector3(-3, 2, 0), new Vector3(-2, 2, 0), new Vector3(0, 2, 0) }, 3);
|
||||
Wall(new Vector3(1.2f, 2, 0), new Vector3(.02f, 12, 12), Quaternion.identity); SelectSplitBall("safety-2");
|
||||
var point = Point("safety-2"); _app.Input.Begin(point, false); _app.Input.Move(point + new Vector2(20, 0));
|
||||
Assert.That(_app.Presenter.CanDropGroup("group-0"), Is.True);
|
||||
var validDelta = _app.Presenter.DropDelta; var validPoint = _app.Presenter.Balls["safety-2"].transform.position;
|
||||
_app.Input.Move(point + new Vector2(140, 0));
|
||||
var stopped = _app.Presenter.Balls["safety-2"].transform.position;
|
||||
Assert.That(stopped.x, Is.GreaterThan(validPoint.x + .2f), "Continue the safe part of the split drag up to the obstacle.");
|
||||
Assert.That(stopped.x, Is.EqualTo(.689f).Within(.003f), "The whole unit sphere must stop just before the thin wall.");
|
||||
Assert.That(_app.Presenter.DropDelta.x, Is.GreaterThan(validDelta.x + .2f));
|
||||
var corePreview = _app.Session.Board.PreviewSplit("group-0", new[] { "safety-2" }, BoardPresenter.ToPosition(_app.Presenter.DropDelta));
|
||||
foreach (var ball in corePreview) Assert.That(_app.Presenter.Balls[ball.id].transform.position, Is.EqualTo(BoardPresenter.ToVector(ball.position)));
|
||||
_app.Input.Move(point + new Vector2(128, 0));
|
||||
var reverse = _app.Presenter.Balls["safety-2"].transform.position;
|
||||
Assert.That(reverse.x, Is.LessThan(stopped.x - .01f), "Reverse must act immediately at the last safe split delta.");
|
||||
var preview = _app.Presenter.Balls.ToDictionary(pair => pair.Key, pair => pair.Value.transform.position);
|
||||
_app.Input.End(point + new Vector2(128, 0)); yield return null;
|
||||
Assert.That(_app.Session.Board.Groups.Length, Is.EqualTo(2));
|
||||
foreach (var ball in _app.Session.Board.Balls) Assert.That(BoardPresenter.ToVector(ball.position), Is.EqualTo(preview[ball.id]));
|
||||
}
|
||||
[UnityTest] public IEnumerator SplitRejectsTheSelectedHalfBeyondEveryBoundary()
|
||||
{
|
||||
foreach (var direction in new[] { Vector3.left, Vector3.right, Vector3.forward, Vector3.back, Vector3.up, Vector3.down })
|
||||
{
|
||||
Inject(new[] { new Vector3(-1, 2, 0), new Vector3(0, 2, 0), new Vector3(1, 2, 0) }, 3);
|
||||
var snapshot = JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Session.Camera));
|
||||
Assert.That(_app.Presenter.PreviewSplit("group-0", new[] { "safety-0" }, direction * 40), Is.False);
|
||||
Assert.That(_app.Presenter.CanDropGroup("group-0"), Is.False);
|
||||
foreach (var ball in _app.Session.Board.Balls) Assert.That(_app.Presenter.Balls[ball.id].transform.position, Is.EqualTo(BoardPresenter.ToVector(ball.position)));
|
||||
AssertUnchanged(snapshot); yield return null;
|
||||
}
|
||||
}
|
||||
[UnityTest] public IEnumerator ApprovedClosedRockDeepInteriorIsRejectedAndProbeDiagnosticsAreRecorded()
|
||||
{
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Meadow/SlateBoulder.prefab");
|
||||
var rock = Object.Instantiate(prefab, _app.Environment.transform); _obstacles.Add(rock);
|
||||
// Approved runtime mesh, synthetic enlarged pose to put the entire unit sphere deep inside.
|
||||
rock.transform.localScale = Vector3.one * 3;
|
||||
var collider = rock.GetComponentInChildren<MeshCollider>();
|
||||
Physics.SyncTransforms(); rock.transform.position += new Vector3(0, 3, 0) - collider.bounds.center; Physics.SyncTransforms();
|
||||
var point = collider.bounds.center;
|
||||
var source = _app.Presenter.Balls.Values.First().GetComponent<SphereCollider>();
|
||||
bool penetration = Physics.ComputePenetration(source, point, Quaternion.identity, collider, collider.transform.position, collider.transform.rotation, out _, out float depth);
|
||||
bool overlap = Physics.OverlapSphere(point, .5f, ~0, QueryTriggerInteraction.Ignore).Contains(collider);
|
||||
Debug.Log($"Deep approved mesh {collider.name}: center={point:R} bounds={collider.bounds} overlap={overlap} penetration={penetration} depth={depth:R} safe={_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point))}");
|
||||
var wall = Wall(new Vector3(0, 2, 0), Vector3.one, Quaternion.identity).GetComponent<BoxCollider>();
|
||||
var diagnostic = new GameObject("test-only-probe-diagnostic"); _obstacles.Add(diagnostic);
|
||||
var sphere = diagnostic.AddComponent<SphereCollider>(); sphere.radius = .5f; sphere.enabled = false;
|
||||
bool disabledResult = Physics.ComputePenetration(sphere, wall.bounds.center, Quaternion.identity, wall, wall.transform.position, wall.transform.rotation, out _, out float disabledDepth);
|
||||
sphere.enabled = true; sphere.isTrigger = true; diagnostic.layer = 2; Physics.SyncTransforms();
|
||||
bool triggerResult = Physics.ComputePenetration(sphere, wall.bounds.center, Quaternion.identity, wall, wall.transform.position, wall.transform.rotation, out _, out float triggerDepth);
|
||||
Debug.Log($"Probe direct comparison: disabled={disabledResult} depth={disabledDepth:R}; liveTrigger={triggerResult} depth={triggerDepth:R}");
|
||||
bool backfaces = Physics.queriesHitBackfaces;
|
||||
Assert.That(_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point)), Is.False, "Old-save safety must reject the deep interior of an approved closed rock mesh.");
|
||||
Assert.That(Physics.queriesHitBackfaces, Is.EqualTo(backfaces));
|
||||
Physics.queriesHitBackfaces = !backfaces;
|
||||
try
|
||||
{
|
||||
Assert.That(_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point)), Is.False);
|
||||
Assert.That(Physics.queriesHitBackfaces, Is.EqualTo(!backfaces));
|
||||
}
|
||||
finally { Physics.queriesHitBackfaces = backfaces; }
|
||||
yield return null;
|
||||
}
|
||||
[UnityTest] public IEnumerator ApprovedArchHoleAcceptsFullSphereAndOverlappingTreeSolidsRejectInterior()
|
||||
{
|
||||
var arch = Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Meadow/OpenStoneArch.prefab"), _app.Environment.transform);
|
||||
_obstacles.Add(arch); arch.transform.localScale = Vector3.one * 2;
|
||||
var shell = arch.GetComponentInChildren<MeshCollider>(); Physics.SyncTransforms();
|
||||
arch.transform.position += new Vector3(0, 3, 0) - shell.bounds.center; Physics.SyncTransforms();
|
||||
bool foundHole = false;
|
||||
for (float y = Mathf.Max(.6f, shell.bounds.min.y + .55f); y < shell.bounds.max.y - .55f; y += .2f)
|
||||
{
|
||||
var point = new Vector3(shell.bounds.center.x, y, shell.bounds.center.z);
|
||||
var start = point - Vector3.forward * (shell.bounds.extents.z + 1);
|
||||
if (Physics.SphereCastAll(start, .5f, Vector3.forward, shell.bounds.size.z + 2, ~0, QueryTriggerInteraction.Ignore).Any(hit => hit.collider == shell)) continue;
|
||||
Assert.That(_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point)), Is.True, "The actual visible arch opening must accept a whole sphere.");
|
||||
foundHole = true; Debug.Log("Approved arch unit-sphere hole point=" + point); break;
|
||||
}
|
||||
Assert.That(foundHole, Is.True); arch.SetActive(false);
|
||||
var tree = Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Meadow/RoundCanopyTree.prefab"), _app.Environment.transform);
|
||||
_obstacles.Add(tree); tree.transform.localScale = Vector3.one * 2;
|
||||
var mesh = tree.GetComponentInChildren<MeshCollider>(); Physics.SyncTransforms();
|
||||
tree.transform.position += new Vector3(0, 3, 0) - mesh.bounds.center; Physics.SyncTransforms();
|
||||
// The approved tree includes intersecting closed crown volumes. Search its central volume for two enclosing pieces.
|
||||
Vector3[] vertices; int[] triangles;
|
||||
using (var readOnly = MeshUtility.AcquireReadOnlyMeshData(mesh.sharedMesh))
|
||||
{
|
||||
var data = readOnly[0]; var buffer = new NativeArray<Vector3>(data.vertexCount, Allocator.Temp);
|
||||
data.GetVertices(buffer); vertices = buffer.ToArray(); buffer.Dispose();
|
||||
triangles = data.indexFormat == UnityEngine.Rendering.IndexFormat.UInt16
|
||||
? data.GetIndexData<ushort>().ToArray().Select(index => (int)index).ToArray()
|
||||
: data.GetIndexData<uint>().ToArray().Select(index => (int)index).ToArray();
|
||||
}
|
||||
bool foundOverlap = false;
|
||||
for (float y = Mathf.Max(.6f, mesh.bounds.min.y + .5f); y < Mathf.Min(5.8f, mesh.bounds.max.y - .5f); y += .2f)
|
||||
{
|
||||
var point = new Vector3(mesh.bounds.center.x, y, mesh.bounds.center.z);
|
||||
int exits = 0, entries = 0; var direction = new Vector3(1, .371f, .529f).normalized;
|
||||
for (int index = 0; index < triangles.Length; index += 3)
|
||||
{
|
||||
var a = mesh.transform.TransformPoint(vertices[triangles[index]]);
|
||||
var b = mesh.transform.TransformPoint(vertices[triangles[index + 1]]);
|
||||
var c = mesh.transform.TransformPoint(vertices[triangles[index + 2]]);
|
||||
var edge1 = b - a; var edge2 = c - a; var cross = Vector3.Cross(direction, edge2);
|
||||
float determinant = Vector3.Dot(edge1, cross);
|
||||
if (Mathf.Abs(determinant) < .000001f) continue;
|
||||
var offset = point - a; float u = Vector3.Dot(offset, cross) / determinant;
|
||||
var q = Vector3.Cross(offset, edge1); float v = Vector3.Dot(direction, q) / determinant;
|
||||
float t = Vector3.Dot(edge2, q) / determinant;
|
||||
if (u < 0 || v < 0 || u + v > 1 || t <= .001f) continue;
|
||||
if (Vector3.Dot(Vector3.Cross(edge1, edge2), direction) > 0) exits++; else entries++;
|
||||
}
|
||||
if (exits - entries < 2) continue;
|
||||
foundOverlap = true; bool backfaces = Physics.queriesHitBackfaces;
|
||||
Assert.That(_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point)), Is.False, "Overlapping approved closed tree parts are solid even with an even crossing count.");
|
||||
Assert.That(Physics.queriesHitBackfaces, Is.EqualTo(backfaces));
|
||||
Debug.Log($"Approved tree overlapping solids point={point} entries={entries} exits={exits}"); break;
|
||||
}
|
||||
Assert.That(foundOverlap, Is.True, "The approved tree fixture must actually include two overlapping closed volumes."); yield return null;
|
||||
}
|
||||
[UnityTest] public IEnumerator EveryOriginalStageBoardIsSafeAgainstActualSceneryAndFullSphereBounds()
|
||||
{
|
||||
for (int stage = 1; stage <= 30; stage++)
|
||||
{
|
||||
_app.StartStage(stage); _app.Voice.Clear(); yield return null;
|
||||
Physics.SyncTransforms();
|
||||
Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True, "Original authored board stage " + stage);
|
||||
foreach (var ball in _app.Session.Board.Balls)
|
||||
Assert.That(_app.Presenter.IsSafePosition(ball.position), Is.True, "Stage " + stage + " sphere " + ball.id + " at " + BoardPresenter.ToVector(ball.position));
|
||||
}
|
||||
}
|
||||
[UnityTest] public IEnumerator SafeTwoPlusThreeContactAndCommitPreserveAllIdsAndBothShapes()
|
||||
{
|
||||
var positions = new[] { new Vector3(-3, 2, 0), new Vector3(-2, 2, 0), new Vector3(1, 2, 0), new Vector3(2, 2, 0), new Vector3(3, 2, 0) };
|
||||
Inject(positions, 2, 3); _app.Presenter.PreviewGroup("group-0", Vector3.right * 2.2f);
|
||||
Assert.That(_app.Presenter.MergeCandidate, Is.EqualTo("group-1"));
|
||||
var preview = _app.Presenter.Balls.ToDictionary(pair => pair.Key, pair => pair.Value.transform.position);
|
||||
_app.CommitMove("group-0", BoardPresenter.ToPosition(_app.Presenter.DropDelta), _app.Presenter.MergeCandidate); yield return null;
|
||||
Assert.That(_app.Session.Board.Groups.Single().ballIds.Length, Is.EqualTo(5));
|
||||
foreach (var ball in _app.Session.Board.Balls) Assert.That(BoardPresenter.ToVector(ball.position), Is.EqualTo(preview[ball.id]));
|
||||
Assert.That(Vector3.Distance(preview["safety-0"], preview["safety-1"]), Is.EqualTo(1).Within(.001f));
|
||||
}
|
||||
[UnityTest] public IEnumerator SplitRejectsUnsafeUnselectedHalfAndRestoresTheCommittedBoard()
|
||||
{
|
||||
Inject(new[] { new Vector3(-2, 2, 0), new Vector3(0, 2, 0), new Vector3(2, 2, 0) }, 3);
|
||||
// Rest's compact arrangement moves from (-2, 2) to (-.5, .5); block just that final half.
|
||||
Wall(new Vector3(-.5f, 2, 0), new Vector3(.05f, .05f, .05f), Quaternion.identity);
|
||||
var snapshot = JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Session.Camera));
|
||||
Assert.That(_app.Presenter.PreviewSplit("group-0", new[] { "safety-1" }, Vector3.up * 2), Is.False);
|
||||
Assert.That(_app.Presenter.CanDropGroup("group-0"), Is.False);
|
||||
foreach (var ball in _app.Session.Board.Balls) Assert.That(_app.Presenter.Balls[ball.id].transform.position, Is.EqualTo(BoardPresenter.ToVector(ball.position)));
|
||||
AssertUnchanged(snapshot); yield return null;
|
||||
}
|
||||
[UnityTest] public IEnumerator SafeSplitBothHalvesMatchCommittedCoreArrangement()
|
||||
{
|
||||
Inject(new[] { new Vector3(-1, 2, 0), new Vector3(0, 2, 0), new Vector3(1, 2, 0) }, 3);
|
||||
var delta = Vector3.up * 2;
|
||||
Assert.That(_app.Presenter.PreviewSplit("group-0", new[] { "safety-0" }, delta), Is.True);
|
||||
var preview = _app.Presenter.Balls.ToDictionary(pair => pair.Key, pair => pair.Value.transform.position);
|
||||
Assert.That(_app.Presenter.CanDropGroup("group-0"), Is.True);
|
||||
Assert.That(_app.CommitSplit("group-0", new[] { "safety-0" }, BoardPresenter.ToPosition(delta)), Is.True); yield return null;
|
||||
Assert.That(_app.Session.Board.Groups.Length, Is.EqualTo(2));
|
||||
foreach (var ball in _app.Session.Board.Balls) Assert.That(BoardPresenter.ToVector(ball.position), Is.EqualTo(preview[ball.id]));
|
||||
}
|
||||
[UnityTest] public IEnumerator SupplyUsesFiniteSafeSpaceAndNoSpaceLeavesIdsAndSaveUntouched()
|
||||
{
|
||||
Inject(new[] { new Vector3(0, 2, 0) }, 1);
|
||||
_app.SpawnFriend(); Assert.That(_app.Session.Board.BallCount, Is.EqualTo(2));
|
||||
var spawned = _app.Session.Board.Balls.Single(ball => ball.id != "safety-0"); var point = BoardPresenter.ToVector(spawned.position);
|
||||
var bounds = _app.Environment.PlaygroundBounds;
|
||||
Assert.That(bounds.Contains(point + Vector3.up * .5f) && bounds.Contains(point - Vector3.up * .5f), Is.True);
|
||||
Wall(bounds.center, bounds.size * 2, Quaternion.identity);
|
||||
var snapshot = JsonUtility.ToJson(_store.Save); var writes = _store.Writes;
|
||||
_app.SpawnFriend(); Assert.That(_app.Session.Board.BallCount, Is.EqualTo(2));
|
||||
Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(snapshot)); Assert.That(_store.Writes, Is.EqualTo(writes));
|
||||
Assert.That(_app.Instruction, Does.Contain("빈 곳")); yield return null;
|
||||
}
|
||||
[UnityTest] public IEnumerator CancelUIOutsidePinchPauseAndNavigationRestoreIdsCountsAndPositions()
|
||||
{
|
||||
foreach (var mode in new[] { "cancel", "ui", "outside", "pinch", "pause", "navigation" })
|
||||
{
|
||||
Inject(new[] { new Vector3(0, 2, 0) }, 1);
|
||||
var snapshot = JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Session.Camera));
|
||||
var point = Point("safety-0"); _app.Input.Begin(point, mode == "ui"); _app.Input.Move(point + new Vector2(60, 0));
|
||||
switch (mode)
|
||||
{
|
||||
case "cancel": _app.Input.End(point, true); break;
|
||||
case "ui": _app.Input.End(point); break;
|
||||
case "outside": _app.Input.End(new Vector2(-1, point.y)); break;
|
||||
case "pinch": _app.Input.BeginPinch(100, false); _app.Input.Cancel(); break;
|
||||
case "pause": _app.Input.SendMessage("OnApplicationPause", true); break;
|
||||
case "navigation": _app.ShowHome(); _app.ContinueFreePlay(); break;
|
||||
}
|
||||
AssertUnchanged(snapshot);
|
||||
foreach (var ball in _app.Session.Board.Balls) Assert.That(_app.Presenter.Balls[ball.id].transform.position, Is.EqualTo(BoardPresenter.ToVector(ball.position)));
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
[UnityTest] public IEnumerator FrontPropBlocksSelectingTheSphereBehindIt()
|
||||
{
|
||||
Inject(new[] { new Vector3(0, 2, 0) }, 1);
|
||||
var point = Point("safety-0"); var ray = _app.Presenter.Camera.ScreenPointToRay(point);
|
||||
Wall(ray.GetPoint(Vector3.Distance(ray.origin, new Vector3(0, 2, 0)) - 1), new Vector3(.5f, .5f, .5f), Quaternion.identity);
|
||||
var snapshot = JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Session.Camera));
|
||||
_app.Input.Begin(point, false); _app.Input.End(point);
|
||||
Assert.That(_app.Input.SelectedGroup, Is.Null); AssertUnchanged(snapshot); yield return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8bc02646e4e7a3b478da6c1a9aa2023d
|
||||
@@ -0,0 +1,242 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MagnetismHeroes.Core;
|
||||
using MagnetismHeroes.UnityRuntime.Bootstrap;
|
||||
using MagnetismHeroes.UnityRuntime.Presentation;
|
||||
using NUnit.Framework;
|
||||
using TMPro;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Profiling;
|
||||
using UnityEngine.TestTools;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace MagnetismHeroes.Tests
|
||||
{
|
||||
public sealed class PlaygroundSceneryPlayTests
|
||||
{
|
||||
private sealed class Store : IProgressStore
|
||||
{
|
||||
public bool TryLoad(out ProgressSave save, out string error)
|
||||
{ save = new ProgressSave { completedStageIds = Enumerable.Range(1, 30).ToArray(), seenTutorialIds = new[] { "merge", "split" } }; error = null; return true; }
|
||||
public bool TrySave(ProgressSave save, out string error) { error = null; return true; }
|
||||
}
|
||||
private GameApp _app;
|
||||
[UnitySetUp] public IEnumerator SetUp()
|
||||
{
|
||||
foreach (var listener in Object.FindObjectsByType<AudioListener>()) listener.enabled = false;
|
||||
foreach (var camera in Object.FindObjectsByType<Camera>()) camera.enabled = false;
|
||||
_app = Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GameApp.prefab")).GetComponent<GameApp>();
|
||||
_app.Initialize(new Store()); yield return null;
|
||||
}
|
||||
[UnityTearDown] public IEnumerator TearDown() { Object.Destroy(_app.gameObject); yield return null; }
|
||||
[Serializable] private sealed class CaptureRecord
|
||||
{
|
||||
public string utc, sourceCommit, boardOrigin, method, theme;
|
||||
public bool sourceDirty;
|
||||
public int stage, balls, groups, meshInstances, triangles, sharedMaterials, width = 2400, height = 1080;
|
||||
public long textureMemory, allocatedMemory;
|
||||
public float editorCameraRenderMilliseconds, fieldOfView;
|
||||
public Vector3 cameraPosition; public Quaternion cameraRotation; public Bounds playgroundBounds;
|
||||
}
|
||||
private void View(float yaw, float pitch, float distance)
|
||||
{
|
||||
var rotation = Quaternion.Euler(pitch, yaw, 0);
|
||||
_app.Orbit.Apply(new CameraState { target = new Position3(0, .6f, 0), distance = distance,
|
||||
rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w) });
|
||||
}
|
||||
private void Capture(string name, string origin)
|
||||
{
|
||||
string folder = Environment.GetEnvironmentVariable("MAGNETISM_PLAYGROUND_EVIDENCE") ?? Path.GetFullPath("artifacts/verification/PlaygroundInteraction/T1");
|
||||
Directory.CreateDirectory(folder); var camera = _app.Presenter.Camera; var canvas = _app.GetComponentInChildren<Canvas>();
|
||||
var previousTexture = camera.targetTexture; var previousActive = RenderTexture.active; var previousMode = canvas.renderMode;
|
||||
var previousCamera = canvas.worldCamera; float previousPlane = canvas.planeDistance, previousAspect = camera.aspect;
|
||||
var texture = RenderTexture.GetTemporary(2400, 1080, 24); var image = new Texture2D(2400, 1080, TextureFormat.RGB24, false);
|
||||
try
|
||||
{
|
||||
camera.targetTexture = texture; camera.aspect = 2400f / 1080; canvas.renderMode = RenderMode.ScreenSpaceCamera; canvas.worldCamera = camera; canvas.planeDistance = .5f;
|
||||
Canvas.ForceUpdateCanvases(); camera.Render(); camera.Render();
|
||||
var timer = System.Diagnostics.Stopwatch.StartNew(); camera.Render(); timer.Stop(); RenderTexture.active = texture;
|
||||
image.ReadPixels(new Rect(0, 0, 2400, 1080), 0, 0); image.Apply(); File.WriteAllBytes(Path.Combine(folder, name + ".png"), image.EncodeToPNG());
|
||||
var meshes = _app.GetComponentsInChildren<MeshFilter>();
|
||||
var record = new CaptureRecord { utc = DateTime.UtcNow.ToString("o"), sourceCommit = Environment.GetEnvironmentVariable("MAGNETISM_PROOF_COMMIT"),
|
||||
boardOrigin = origin, method = "Actual GameApp prefab in PlayMode Camera.Render to 2400x1080 RT with temporary ScreenSpaceCamera canvas. Mesh index counts include active hidden legacy floor; not submitted GPU triangles. Texture memory is all Editor-loaded textures; Camera.Render is warmed Editor CPU wall time, not Android FPS.",
|
||||
sourceDirty = true, stage = _app.Session.Board.StageId, theme = _app.Environment.Theme, balls = _app.Presenter.Balls.Count, groups = _app.GetComponentsInChildren<GroupBadge>().Length,
|
||||
meshInstances = meshes.Length, triangles = meshes.Sum(mesh => Enumerable.Range(0, mesh.sharedMesh.subMeshCount).Sum(index => (int)mesh.sharedMesh.GetIndexCount(index)) / 3),
|
||||
sharedMaterials = _app.GetComponentsInChildren<MeshRenderer>().SelectMany(renderer => renderer.sharedMaterials).Distinct().Count(),
|
||||
textureMemory = Resources.FindObjectsOfTypeAll<Texture>().Sum(item => Profiler.GetRuntimeMemorySizeLong(item)), allocatedMemory = Profiler.GetTotalAllocatedMemoryLong(),
|
||||
editorCameraRenderMilliseconds = (float)timer.Elapsed.TotalMilliseconds, fieldOfView = camera.fieldOfView, cameraPosition = camera.transform.position, cameraRotation = camera.transform.rotation,
|
||||
playgroundBounds = _app.Environment.PlaygroundBounds };
|
||||
File.WriteAllText(Path.Combine(folder, name + ".json"), JsonUtility.ToJson(record, true));
|
||||
}
|
||||
finally
|
||||
{
|
||||
camera.targetTexture = previousTexture; camera.aspect = previousAspect; RenderTexture.active = previousActive; canvas.renderMode = previousMode; canvas.worldCamera = previousCamera; canvas.planeDistance = previousPlane;
|
||||
Object.Destroy(image); RenderTexture.ReleaseTemporary(texture); Canvas.ForceUpdateCanvases();
|
||||
}
|
||||
}
|
||||
[UnityTest] public IEnumerator SixLandscapesRenderAndAllThirtyOriginalBoardsRemainSelectable()
|
||||
{
|
||||
for (int id = 1; id <= 30; id++)
|
||||
{
|
||||
_app.StartStage(id); _app.Voice.Clear(); yield return null; yield return null; Physics.SyncTransforms();
|
||||
Assert.That(_app.Environment.PlaygroundBounds.size.sqrMagnitude, Is.GreaterThan(0));
|
||||
foreach (var ball in _app.Presenter.Balls.Values)
|
||||
{
|
||||
bool found = false;
|
||||
foreach (float pitch in new[] { 20f, 45f, 70f })
|
||||
for (int yaw = 0; yaw < 360 && !found; yaw += 30)
|
||||
{
|
||||
View(yaw, pitch, 12);
|
||||
var point = _app.Presenter.Camera.WorldToViewportPoint(ball.transform.position);
|
||||
if (point.z <= 0 || point.x < .05f || point.x > .95f || point.y < .05f || point.y > .95f) continue;
|
||||
var ray = _app.Presenter.Camera.ViewportPointToRay(point);
|
||||
found = Physics.Raycast(ray, out var hit, 100) && hit.collider == ball.GetComponent<SphereCollider>();
|
||||
}
|
||||
Assert.That(found, Is.True, $"Stage {id} original sphere {ball.BallId} cannot be reached by a supported above-ground view.");
|
||||
}
|
||||
if ((id - 1) % 5 == 0)
|
||||
{
|
||||
View(0, 0, 8); yield return null; Capture($"stage{id:D2}-default", "Actual original puzzle board; seeded completion IDs used only to unlock stage access.");
|
||||
View(30, 25, 12); yield return null; Capture($"stage{id:D2}-overview", "Actual original puzzle board; seeded completion IDs used only to unlock stage access.");
|
||||
View(150, 12, 15); yield return null; Capture($"stage{id:D2}-horizon", "Actual original puzzle board; seeded completion IDs used only to unlock stage access.");
|
||||
View(270, 45, 6); yield return null; Capture($"stage{id:D2}-near-ground", "Actual original puzzle board; seeded completion IDs used only to unlock stage access.");
|
||||
}
|
||||
}
|
||||
_app.StartStage(1); _app.Voice.Clear(); yield return null; yield return null;
|
||||
var balls = Enumerable.Range(0, 10).Select(i => new BallState { id = "capture-" + i, position = new Position3(i % 5 - 2, .8f + i / 5 * 1.8f, 0) }).ToArray();
|
||||
var board = BoardRules.CreateFreePlay(new FreePlayState { sourceStageId = 1, balls = balls, groups = balls.Select(ball => new GroupState { id = ball.id, ballIds = new[] { ball.id } }).ToArray() });
|
||||
_app.Presenter.Render(board); View(0, 0, 8); yield return null; yield return null;
|
||||
Capture("world1-ten-single-groups-phone-default", "Validated synthetic Core FreePlay fixture of ten single groups; underlying stage 1 UI remains puzzle. Used for like-for-like Editor resource observation only.");
|
||||
Assert.That(_app.Presenter.Balls.Count, Is.EqualTo(10));
|
||||
}
|
||||
[UnityTest] public IEnumerator AllThirtyRuntimeEnvironmentsOnlyBlockWithReachableScenery()
|
||||
{
|
||||
var errors = new List<string>(); int distant = 0, reachable = 0, selected = 0;
|
||||
for (int id = 1; id <= 30; id++)
|
||||
{
|
||||
_app.StartStage(id); _app.Voice.Clear(); yield return null; yield return null; Physics.SyncTransforms();
|
||||
var playground = _app.Environment.PlaygroundBounds;
|
||||
foreach (var collider in _app.Environment.GetComponentsInChildren<MeshCollider>())
|
||||
{
|
||||
var surface = collider.GetComponent<MeshRenderer>().bounds;
|
||||
bool outside = collider.name != "MeadowClearing" && !playground.Intersects(surface);
|
||||
if (outside)
|
||||
{
|
||||
distant++;
|
||||
if (collider.enabled) errors.Add($"Stage {id} unreachable {collider.name} still blocks at {surface}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
reachable++;
|
||||
Assert.That(collider.enabled && !collider.isTrigger, Is.True, $"Stage {id} reachable {collider.name} must block.");
|
||||
}
|
||||
bool hit = false;
|
||||
foreach (var direction in new[] { Vector3.up, Vector3.down, Vector3.left, Vector3.right, Vector3.forward, Vector3.back })
|
||||
{
|
||||
float distance = surface.extents.magnitude + 1;
|
||||
hit |= Physics.RaycastAll(surface.center + direction * distance, -direction, distance * 2)
|
||||
.Any(item => item.collider == collider);
|
||||
}
|
||||
if (outside && hit) errors.Add($"Stage {id} background {collider.name} still receives a Physics ray.");
|
||||
if (!outside) Assert.That(hit, Is.True, $"Stage {id} reachable {collider.name} no longer blocks a Physics ray.");
|
||||
}
|
||||
Assert.That(Physics.Raycast(new Vector3(0, .1f, 0), Vector3.down, out var ground, 2), Is.True);
|
||||
Assert.That(ground.collider.GetComponentInParent<BoardEnvironment>(), Is.SameAs(_app.Environment), "Visible play ground must still block.");
|
||||
Debug.Log($"Stage {id} ground ray y={ground.point.y:R}, aerial bounds min y={playground.min.y:R}, difference={ground.point.y - playground.min.y:R}");
|
||||
foreach (var ball in _app.Presenter.Balls.Values)
|
||||
{
|
||||
bool found = false;
|
||||
foreach (float pitch in new[] { 20f, 45f, 70f })
|
||||
for (int yaw = 0; yaw < 360 && !found; yaw += 30)
|
||||
{
|
||||
View(yaw, pitch, 12); var point = _app.Presenter.Camera.WorldToViewportPoint(ball.transform.position);
|
||||
if (point.z <= 0 || point.x < .05f || point.x > .95f || point.y < .05f || point.y > .95f) continue;
|
||||
var ray = _app.Presenter.Camera.ViewportPointToRay(point);
|
||||
found = Physics.Raycast(ray, out var first, 100) && first.collider == ball.GetComponent<SphereCollider>();
|
||||
}
|
||||
Assert.That(found, Is.True, $"Stage {id} sphere {ball.BallId} no longer selectable by first hit."); selected++;
|
||||
}
|
||||
}
|
||||
Debug.Log($"Runtime scenery roles: 30 stages, {distant} distant colliders, {reachable} reachable colliders, {selected} selectable spheres.");
|
||||
Assert.That(distant, Is.GreaterThan(0)); Assert.That(reachable, Is.GreaterThan(0));
|
||||
Assert.That(errors, Is.Empty, string.Join("\n", errors));
|
||||
}
|
||||
[UnityTest] public IEnumerator InitialTreesLeaveOriginalSpheresNumbersAndTopHudClearOnPhoneAndTablet()
|
||||
{
|
||||
var errors = new List<string>(); int spheres = 0, numbers = 0, hudSamples = 0;
|
||||
var camera = _app.Presenter.Camera; float previousAspect = camera.aspect;
|
||||
bool previousBackfaces = Physics.queriesHitBackfaces;
|
||||
try
|
||||
{
|
||||
for (int id = 1; id <= 30; id++)
|
||||
{
|
||||
_app.StartStage(id); _app.Voice.Clear(); yield return null; yield return null;
|
||||
foreach (float aspect in new[] { 2400f / 1080, 1600f / 1000 })
|
||||
{
|
||||
camera.aspect = aspect; _app.Orbit.Apply(GameSession.DefaultCamera()); yield return null; yield return null;
|
||||
var trees = _app.Environment.GetComponentsInChildren<MeshCollider>()
|
||||
.Where(item => item.name == "RoundCanopyTree" || item.name == "SageCanopyTree" || item.name == "TallWindTree").ToArray();
|
||||
Assert.That(trees, Is.Not.Empty, $"Stage {id} must query its actual authored tree meshes.");
|
||||
var enabled = trees.Select(item => item.enabled).ToArray();
|
||||
try
|
||||
{
|
||||
// Distant tree colliders can be disabled for movement while their meshes still render.
|
||||
// Enable only these actual meshes for this synchronous visual query, then restore them.
|
||||
foreach (var tree in trees) tree.enabled = true;
|
||||
Physics.queriesHitBackfaces = true; Physics.SyncTransforms();
|
||||
void Clear(Ray ray, float distance, string target)
|
||||
{
|
||||
foreach (var tree in trees)
|
||||
if (tree.Raycast(ray, out var hit, distance))
|
||||
errors.Add($"Stage {id}, aspect {aspect:R}, {target} blocked by {tree.name} at {hit.distance:R}m.");
|
||||
}
|
||||
foreach (var ball in _app.Presenter.Balls.Values)
|
||||
{
|
||||
foreach (var offset in new[] { Vector3.zero, camera.transform.right * .3f, camera.transform.right * -.3f,
|
||||
camera.transform.up * .3f, camera.transform.up * -.3f })
|
||||
{
|
||||
var point = ball.transform.position + offset;
|
||||
var direction = point - camera.transform.position;
|
||||
Clear(new Ray(camera.transform.position, direction), direction.magnitude - .4f, $"sphere {ball.BallId}, offset {offset}");
|
||||
}
|
||||
spheres++;
|
||||
}
|
||||
foreach (var badge in _app.GetComponentsInChildren<GroupBadge>())
|
||||
{
|
||||
var number = badge.GetComponentInChildren<TMP_Text>();
|
||||
foreach (var offset in new[] { Vector3.zero, camera.transform.right * .15f, camera.transform.right * -.15f })
|
||||
{
|
||||
var direction = number.transform.position + offset - camera.transform.position;
|
||||
Clear(new Ray(camera.transform.position, direction), direction.magnitude, $"number {number.text}, offset {offset}");
|
||||
}
|
||||
numbers++;
|
||||
}
|
||||
// Sample the visible title and counter bands behind the overlay, not opaque buttons.
|
||||
// This covers their central/side columns at two heights; Native captures verify full text.
|
||||
foreach (float x in new[] { .42f, .5f, .58f, .84f, .9f, .96f })
|
||||
foreach (float y in new[] { .90f, .94f })
|
||||
{
|
||||
Clear(camera.ViewportPointToRay(new Vector3(x, y, 0)), 100, $"top HUD ({x},{y})"); hudSamples++;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
for (int index = 0; index < trees.Length; index++) trees[index].enabled = enabled[index];
|
||||
Physics.queriesHitBackfaces = previousBackfaces; Physics.SyncTransforms();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally { camera.aspect = previousAspect; Physics.queriesHitBackfaces = previousBackfaces; }
|
||||
Debug.Log($"Initial tree clearance: 30 stages x 2 aspects, {spheres} original spheres, {numbers} numbers, {hudSamples} top HUD samples; {errors.Count} blocked rays.");
|
||||
Assert.That(spheres, Is.EqualTo(346)); Assert.That(numbers, Is.GreaterThan(0)); Assert.That(hudSamples, Is.EqualTo(720));
|
||||
Assert.That(errors, Is.Empty, string.Join("\n", errors));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dde7f051095b2d744956b3291bc6b3bf
|
||||
@@ -0,0 +1,681 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MagnetismHeroes.Core;
|
||||
using MagnetismHeroes.UnityRuntime.Bootstrap;
|
||||
using MagnetismHeroes.UnityRuntime.Presentation;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Profiling;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.TestTools;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace MagnetismHeroes.Tests
|
||||
{
|
||||
public sealed class VisualUpgradePlayTests
|
||||
{
|
||||
private sealed class Store : IProgressStore
|
||||
{
|
||||
public ProgressSave Saved = new ProgressSave { completedStageIds = Enumerable.Range(1, 30).ToArray(), seenTutorialIds = new[] { "merge", "split" } };
|
||||
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; }
|
||||
}
|
||||
[Serializable] private sealed class CaptureRecord
|
||||
{
|
||||
public string name, sourceCommit, sourceDirty, boardOrigin, theme, utc, method;
|
||||
public int width, height, stage, balls, groups, equipmentAssemblies, meshInstances, triangles, sharedMaterials;
|
||||
public long textureMemory, allocatedMemory;
|
||||
public float editorCameraRenderMilliseconds, fieldOfView;
|
||||
public Vector3 cameraPosition;
|
||||
public Quaternion cameraRotation;
|
||||
}
|
||||
private GameApp _app;
|
||||
[Serializable] private sealed class NativeFixtures
|
||||
{
|
||||
public string sourceCommit, sourceDirty, utc, origin;
|
||||
public NativeStage[] stages;
|
||||
public NativeRestore[] restores;
|
||||
public NativeDrag[] drags;
|
||||
}
|
||||
[Serializable] private sealed class NativeStage
|
||||
{
|
||||
public int stage;
|
||||
public string theme;
|
||||
public Vector3 min, max;
|
||||
public PuzzleState initial;
|
||||
}
|
||||
[Serializable] private sealed class NativeRestore
|
||||
{
|
||||
public string name, slot, origin;
|
||||
public int stage;
|
||||
public PuzzleState puzzleSource, puzzleExpected;
|
||||
public FreePlayState freeSource, freeExpected;
|
||||
public float editorEntryMilliseconds;
|
||||
}
|
||||
[Serializable] private sealed class NativeDrag
|
||||
{
|
||||
public string name, ballId, kind = "boundary";
|
||||
public FreePlayState source, expected;
|
||||
public Vector3 requested, grabOffset, pointerDelta;
|
||||
public string firstHitId;
|
||||
public Vector2 start, end;
|
||||
}
|
||||
private Store _store;
|
||||
[Serializable] private sealed class MatchedBenchmark
|
||||
{
|
||||
public string sourceCommit, sourceDirty, utc, method, loadOrder;
|
||||
public int stage, balls, groups, activeProps, width = 2400, height = 1080, warmups = 8, sampleCount = 30;
|
||||
public Vector3 cameraPosition;
|
||||
public Quaternion cameraRotation;
|
||||
public float fieldOfView;
|
||||
public double[] renderCpuMilliseconds;
|
||||
public long allocatedMemory, textureMemory;
|
||||
}
|
||||
[Serializable] private sealed class BenchmarkSceneryScan { public int stage, activeProps, renderers, residentTriangles; }
|
||||
[Serializable] private sealed class BenchmarkSceneryReport { public string sourceCommit, utc; public BenchmarkSceneryScan[] stages; public int[] maximumPropStages; }
|
||||
|
||||
[UnityTest] public IEnumerator MatchedPlaygroundResourceBenchmark()
|
||||
{
|
||||
string directory = Environment.GetEnvironmentVariable("MAGNETISM_BENCHMARK_EVIDENCE") ?? Path.GetFullPath("artifacts/verification/PlaygroundInteraction/EditorBenchmark");
|
||||
Directory.CreateDirectory(directory);
|
||||
var scans = new System.Collections.Generic.List<BenchmarkSceneryScan>();
|
||||
foreach (int stage in Enumerable.Range(1, 30))
|
||||
{
|
||||
_app.StartStage(stage); _app.Voice.Clear(); yield return null; yield return null;
|
||||
var filters = _app.Environment.GetComponentsInChildren<MeshFilter>();
|
||||
scans.Add(new BenchmarkSceneryScan { stage = stage,
|
||||
activeProps = _app.Environment.transform.Cast<Transform>().Count(child => child.gameObject.activeInHierarchy && child.name != "PlacementSafetySphere"),
|
||||
renderers = _app.Environment.GetComponentsInChildren<Renderer>().Count(value => value.enabled),
|
||||
residentTriangles = filters.Sum(value => Enumerable.Range(0, value.sharedMesh.subMeshCount).Sum(index => (int)value.sharedMesh.GetIndexCount(index)) / 3) });
|
||||
}
|
||||
var maximumPropStages = scans.Where(value => value.activeProps == scans.Max(item => item.activeProps)).Select(value => value.stage).ToArray();
|
||||
File.WriteAllText(Path.Combine(directory, "actual-thirty-scenery-scan.json"), JsonUtility.ToJson(new BenchmarkSceneryReport { sourceCommit = Environment.GetEnvironmentVariable("MAGNETISM_PROOF_COMMIT"), utc = DateTime.UtcNow.ToString("O"), stages = scans.ToArray(), maximumPropStages = maximumPropStages }, true));
|
||||
Assert.That(maximumPropStages.Intersect(new[] { 1, 16, 21 }), Is.Not.Empty, "Fixed matched stages must include an actually observed maximum-prop scene.");
|
||||
var camera = _app.Presenter.Camera; var canvas = _app.GetComponentInChildren<Canvas>();
|
||||
var target = RenderTexture.GetTemporary(2400, 1080, 24);
|
||||
var previousTarget = camera.targetTexture; var previousMode = canvas.renderMode; var previousCamera = canvas.worldCamera; float previousPlane = canvas.planeDistance, previousAspect = camera.aspect;
|
||||
try
|
||||
{
|
||||
camera.targetTexture = target; camera.aspect = 2400f / 1080; canvas.renderMode = RenderMode.ScreenSpaceCamera; canvas.worldCamera = camera; canvas.planeDistance = .5f;
|
||||
foreach (int stage in new[] { 1, 16, 21 }) foreach (bool singles in new[] { true, false })
|
||||
{
|
||||
_app.StartStage(stage); _app.Voice.Clear(); yield return null; yield return null;
|
||||
var balls = Enumerable.Range(0, 10).Select(index => new BallState { id = "benchmark-" + index, position = new Position3((index % 5 - 2) * 1.6f, .8f + index / 5 * 1.8f, 0) }).ToArray();
|
||||
var groups = singles ? balls.Select(ball => new GroupState { id = ball.id, ballIds = new[] { ball.id } }).ToArray() : new[] { new GroupState { id = "benchmark-ten", ballIds = balls.Select(ball => ball.id).ToArray() } };
|
||||
_app.Presenter.Render(BoardRules.CreateFreePlay(new FreePlayState { sourceStageId = stage, balls = balls, groups = groups }));
|
||||
var rotation = Quaternion.Euler(25, 30, 0); _app.Orbit.Apply(new CameraState { target = new Position3(0, .6f, 0), distance = 12, rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w) });
|
||||
yield return null; yield return null; Canvas.ForceUpdateCanvases();
|
||||
for (int index = 0; index < 8; index++) { camera.Render(); yield return null; }
|
||||
var samples = new double[30];
|
||||
for (int index = 0; index < samples.Length; index++) { var watch = System.Diagnostics.Stopwatch.StartNew(); camera.Render(); watch.Stop(); samples[index] = watch.Elapsed.TotalMilliseconds; yield return null; }
|
||||
Assert.That(samples.All(value => value > 0), Is.True); Assert.That(_app.Presenter.Balls.Count, Is.EqualTo(10));
|
||||
File.WriteAllText(Path.Combine(directory, "stage" + stage + (singles ? "-ten-singles" : "-ten-group") + ".json"), JsonUtility.ToJson(new MatchedBenchmark {
|
||||
sourceCommit = Environment.GetEnvironmentVariable("MAGNETISM_PROOF_COMMIT"), sourceDirty = Environment.GetEnvironmentVariable("MAGNETISM_PROOF_DIRTY"), utc = DateTime.UtcNow.ToString("O"),
|
||||
method = "Standalone matched PlayMode filter; actual Main scene. 8 warmups and 30 Camera.Render CPU wall samples separated by frames; allocated/texture memory includes Editor. These values are not Android frame timing, GPU time or real-device performance.",
|
||||
loadOrder = "Main, scan original stages1..30, stage1 singles/group, stage16 singles/group, stage21 singles/group; actual-thirty-scenery-scan.json proves maximum-prop coverage",
|
||||
stage = stage, balls = balls.Length, groups = groups.Length, activeProps = _app.Environment.transform.Cast<Transform>().Count(child => child.gameObject.activeInHierarchy && child.name != "PlacementSafetySphere"),
|
||||
cameraPosition = camera.transform.position, cameraRotation = camera.transform.rotation, fieldOfView = camera.fieldOfView, renderCpuMilliseconds = samples,
|
||||
allocatedMemory = Profiler.GetTotalAllocatedMemoryLong(), textureMemory = Resources.FindObjectsOfTypeAll<Texture>().Sum(item => Profiler.GetRuntimeMemorySizeLong(item)) }, true));
|
||||
}
|
||||
}
|
||||
finally { camera.targetTexture = previousTarget; camera.aspect = previousAspect; canvas.renderMode = previousMode; canvas.worldCamera = previousCamera; canvas.planeDistance = previousPlane; RenderTexture.ReleaseTemporary(target); }
|
||||
}
|
||||
private string _evidence;
|
||||
[UnitySetUp] public IEnumerator SetUp()
|
||||
{
|
||||
EditorSceneManager.LoadSceneInPlayMode("Assets/MagnetismHeroes/Scenes/Main.unity", new LoadSceneParameters(LoadSceneMode.Single));
|
||||
yield return null;
|
||||
_app = Object.FindFirstObjectByType<GameApp>();
|
||||
Assert.That(_app, Is.Not.Null, "Actual Main scene must initialize its app.");
|
||||
_store = new Store(); _app.Initialize(_store);
|
||||
_app.Presenter.BindEnvironment(_app.Environment); _app.Orbit.BindEnvironment(_app.Environment);
|
||||
_evidence = Environment.GetEnvironmentVariable("MAGNETISM_VISUAL_EVIDENCE") ?? Path.GetFullPath("artifacts/verification/VisualUpgrade/editor-play");
|
||||
Directory.CreateDirectory(_evidence);
|
||||
yield return null;
|
||||
}
|
||||
[UnityTearDown] public IEnumerator TearDown() { Object.Destroy(_app.gameObject); yield return null; }
|
||||
|
||||
private Vector2 NativePoint(Vector3 world)
|
||||
{
|
||||
var viewport = _app.Orbit.Camera.WorldToViewportPoint(world);
|
||||
return new Vector2((float)Math.Round(viewport.x * 2400), (float)Math.Round((1 - viewport.y) * 1080));
|
||||
}
|
||||
private Vector2 PlayerPoint(Vector2 native) => _app.Orbit.Camera.ViewportToScreenPoint(new Vector3(native.x / 2400, 1 - native.y / 1080));
|
||||
private NativeDrag ExerciseNativeDrag(string name, string kind, FreePlayState source, Vector3 requested)
|
||||
{
|
||||
var id = source.balls[0].id; var origin = BoardPresenter.ToVector(source.balls[0].position);
|
||||
Assert.That(JsonUtility.ToJson(_app.Session.Camera), Is.EqualTo(JsonUtility.ToJson(source.camera)), "Exported camera must be the actual confirmed manual pose.");
|
||||
Assert.That(JsonUtility.ToJson(_app.Orbit.Snapshot()), Is.EqualTo(JsonUtility.ToJson(source.camera)));
|
||||
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Session.Camera)), Is.EqualTo(JsonUtility.ToJson(source)), "Entry must preserve every fixture ID, position and group.");
|
||||
var start = NativePoint(origin); Physics.SyncTransforms();
|
||||
var ray = _app.Orbit.Camera.ScreenPointToRay(PlayerPoint(start));
|
||||
var nativeRay = _app.Orbit.Camera.ViewportPointToRay(new Vector3(start.x / 2400, 1 - start.y / 1080));
|
||||
Assert.That(Vector3.Distance(ray.origin, nativeRay.origin), Is.LessThan(.00001f));
|
||||
Assert.That(Vector3.Distance(ray.direction, nativeRay.direction), Is.LessThan(.00001f), "Editor pixelRect must preserve the same native viewport ray.");
|
||||
Assert.That(Physics.Raycast(ray, out var hit, 100), Is.True);
|
||||
Assert.That(hit.collider.TryGetComponent<BallView>(out var ball) ? ball.BallId : hit.collider.name, Is.EqualTo(id), name + " actual first hit must select the requested sphere.");
|
||||
var grabOffset = hit.point - origin;
|
||||
var end = NativePoint(origin + requested + grabOffset);
|
||||
foreach (var point in new[] { start, end })
|
||||
Assert.That(point.x >= 530 && point.x <= 1850 && point.y >= 280 && point.y <= 750, Is.True, name + " integer native pointer must stay in the phone world UI-safe region.");
|
||||
var plane = new Plane(_app.Orbit.Camera.transform.forward, hit.point);
|
||||
var endRay = _app.Orbit.Camera.ScreenPointToRay(PlayerPoint(end));
|
||||
Assert.That(plane.Raycast(ray, out var from), Is.True); Assert.That(plane.Raycast(endRay, out var to), Is.True);
|
||||
var pointerDelta = endRay.GetPoint(to) - ray.GetPoint(from);
|
||||
var saved = JsonUtility.ToJson(_store.Saved);
|
||||
_app.Input.Begin(PlayerPoint(start), false); _app.Input.Move(PlayerPoint(end));
|
||||
Assert.That(_app.Orbit.IsAssisting, Is.True, "Actual BoardInput must acquire and move this friend.");
|
||||
Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved), "Preview and assistance cannot save intermediate state.");
|
||||
var actual = _app.Presenter.DropDelta; var merge = _app.Presenter.MergeCandidate;
|
||||
if (kind == "boundary") Assert.That(actual.magnitude, Is.InRange(1.19f, 1.21f), "Actual pointer must stop at the .5 sphere margin.");
|
||||
if (kind == "foliage-pass")
|
||||
{
|
||||
Assert.That(Vector3.Distance(actual, pointerDelta), Is.LessThan(.001f), "The entire actual integer-pointer movement through foliage must pass without clipping.");
|
||||
Assert.That(actual.x, Is.GreaterThan(3.9f));
|
||||
}
|
||||
if (kind == "thin-obstacle") { Assert.That(actual.x, Is.GreaterThan(.1f).And.LessThan(requested.x - 1)); Assert.That(merge, Is.Null); }
|
||||
if (kind == "two-plus-three") Assert.That(merge, Is.EqualTo("native-contact-target"));
|
||||
var preview = _app.Presenter.Balls.ToDictionary(pair => pair.Key, pair => pair.Value.transform.position);
|
||||
var members = source.groups.Single(group => group.ballIds.Contains(id)).ballIds;
|
||||
foreach (var value in source.balls)
|
||||
Assert.That(Vector3.Distance(preview[value.id], BoardPresenter.ToVector(value.position) + (members.Contains(value.id) ? actual : Vector3.zero)), Is.LessThan(.001f), "Only the whole grabbed group may translate; all relative offsets and other friends are preserved.");
|
||||
_app.Input.End(PlayerPoint(end));
|
||||
Assert.That(_app.Session.Board.Balls.Select(value => value.id), Is.EquivalentTo(source.balls.Select(value => value.id)));
|
||||
foreach (var value in _app.Session.Board.Balls) Assert.That(Vector3.Distance(BoardPresenter.ToVector(value.position), preview[value.id]), Is.LessThan(.001f), "Drop must equal the actual input preview for every ID.");
|
||||
Assert.That(JsonUtility.ToJson(_app.Session.Camera), Is.EqualTo(JsonUtility.ToJson(source.camera)));
|
||||
if (kind != "two-plus-three") Assert.That(_app.Session.Board.Groups.Select(group => JsonUtility.ToJson(group)), Is.EqualTo(source.groups.Select(group => JsonUtility.ToJson(group))));
|
||||
else Assert.That(_app.Session.Board.Groups.Single().ballIds, Is.EquivalentTo(source.balls.Select(value => value.id)));
|
||||
return new NativeDrag { name = name, kind = kind, ballId = id, source = source, requested = requested, start = start, end = end,
|
||||
grabOffset = grabOffset, pointerDelta = pointerDelta, firstHitId = ball.BallId, expected = _app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot()) };
|
||||
}
|
||||
|
||||
[UnityTest] public IEnumerator ExportActualMainPlaygroundNativeBoundaryFixtures()
|
||||
{
|
||||
var stages = new System.Collections.Generic.List<NativeStage>();
|
||||
var restores = new System.Collections.Generic.List<NativeRestore>();
|
||||
var drags = new System.Collections.Generic.List<NativeDrag>();
|
||||
foreach (int stage in Enumerable.Range(1, 30))
|
||||
{
|
||||
_app.StartStage(stage); _app.Voice.Clear(); yield return null;
|
||||
stages.Add(new NativeStage { stage = stage, theme = _app.Environment.Theme,
|
||||
min = _app.Environment.PlaygroundBounds.min, max = _app.Environment.PlaygroundBounds.max,
|
||||
initial = new BoardRules(_app.Session.Definitions.Single(value => value.Id == stage)).ToPuzzleState(GameSession.DefaultCamera()) });
|
||||
if (new[] {1, 2, 6, 7, 11, 12, 16, 17, 21, 22, 26, 27}.Contains(stage))
|
||||
CaptureViews("playground-stage" + stage, "Actual Main initial board; labelled unlocked-stage fixture, not normal progression");
|
||||
}
|
||||
foreach (var entry in new[] { (name: "safe-free", stage: 1, point: new Vector3(0, 4, 0)),
|
||||
(name: "boundary-free", stage: 1, point: new Vector3(30, 4, 0)),
|
||||
(name: "tree-interior", stage: 6, point: Vector3.zero), (name: "rock-interior", stage: 21, point: Vector3.zero) })
|
||||
{
|
||||
_app.StartStage(entry.stage); yield return null; Physics.SyncTransforms();
|
||||
Vector3 point = entry.point;
|
||||
if (entry.point == Vector3.zero)
|
||||
{
|
||||
string asset = entry.name.StartsWith("tree") ? "RoundCanopyTree" : "SlateBoulder";
|
||||
var meshes = _app.Environment.GetComponentsInChildren<MeshCollider>().Where(value => value.enabled && value.transform.GetComponentsInParent<Transform>().Any(parent => parent.name.Contains(asset))).ToArray();
|
||||
Assert.That(meshes, Is.Not.Empty, "Native interior fixture must use an actual scenery collider.");
|
||||
var collider = meshes.OrderByDescending(value => value.bounds.size.sqrMagnitude).First();
|
||||
Assert.That(collider.sharedMesh.isReadable, Is.False);
|
||||
point = collider.bounds.center;
|
||||
Assert.That(_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point)), Is.False, "Actual approved mesh interior starts unsafe.");
|
||||
}
|
||||
var source = new FreePlayState { sourceStageId = entry.stage, camera = GameSession.DefaultCamera(),
|
||||
balls = new[] { new BallState { id = "native-" + entry.name, position = BoardPresenter.ToPosition(point) } },
|
||||
groups = new[] { new GroupState { id = "native-group", ballIds = new[] { "native-" + entry.name } } } };
|
||||
_store.Saved.latestFreePlay = source; _app.Initialize(_store);
|
||||
var watch = System.Diagnostics.Stopwatch.StartNew(); _app.ContinueFreePlay(); watch.Stop(); yield return null;
|
||||
Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.FreePlay)); Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True);
|
||||
restores.Add(new NativeRestore { name = entry.name, stage = entry.stage, slot = "free", freeSource = source,
|
||||
freeExpected = _app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot()), editorEntryMilliseconds = (float)watch.Elapsed.TotalMilliseconds,
|
||||
origin = "Actual unchanged Main scenery; injected position only; original imports remain unreadable; expected result observed through GameApp entry" });
|
||||
}
|
||||
var legacy = new FreePlayState { sourceStageId = 9,
|
||||
camera = new CameraState { target = new Position3(0, .6f, 0), distance = 11, rotation = new Rotation4(-.022557568f, .96225017f, .08418597f, .2578342f) },
|
||||
balls = new[] { new BallState { id = "s09-b01", position = new Position3(-1.0351909f, .8f, .60371524f) },
|
||||
new BallState { id = "s09-b02", position = new Position3(-.03519098f, .8f, .60371524f) },
|
||||
new BallState { id = "s09-b03", position = new Position3(-3.9800465f, .8f, 1.649168f) },
|
||||
new BallState { id = "s09-b04", position = new Position3(.964809f, .8f, .60371524f) },
|
||||
new BallState { id = "s09-b05", position = new Position3(-2.9800465f, .8f, 1.649168f) } },
|
||||
groups = new[] { new GroupState { id = "s09-g01", ballIds = new[] { "s09-b01", "s09-b02", "s09-b04" } }, new GroupState { id = "s09-g01-split", ballIds = new[] { "s09-b03", "s09-b05" } } } };
|
||||
var wide = new FreePlayState { sourceStageId = 6, camera = GameSession.DefaultCamera(),
|
||||
balls = new[] { new BallState { id = "wide-a", position = new Position3(-30, 4, 0) }, new BallState { id = "wide-b", position = new Position3(30, 4, 0) } },
|
||||
groups = new[] { new GroupState { id = "wide-group", ballIds = new[] { "wide-a", "wide-b" } } } };
|
||||
var multi = JsonUtility.FromJson<FreePlayState>(JsonUtility.ToJson(wide));
|
||||
multi.sourceStageId = 1; multi.groups = new[] { new GroupState { id = "left", ballIds = new[] { "wide-a" } }, new GroupState { id = "right", ballIds = new[] { "wide-b" } } };
|
||||
foreach (var entry in new[] { (name: "legacy-nine", source: legacy), (name: "impossible-free", source: wide), (name: "multiple-repairs", source: multi) })
|
||||
{
|
||||
_store.Saved.latestFreePlay = entry.source; _app.Initialize(_store);
|
||||
var watch = System.Diagnostics.Stopwatch.StartNew(); _app.ContinueFreePlay(); watch.Stop(); yield return null;
|
||||
Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.FreePlay)); Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True);
|
||||
restores.Add(new NativeRestore { name = entry.name, stage = entry.source.sourceStageId, slot = "free", freeSource = entry.source,
|
||||
freeExpected = _app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot()), editorEntryMilliseconds = (float)watch.Elapsed.TotalMilliseconds,
|
||||
origin = entry.name == "legacy-nine" ? "Exact original Content stage9 3+2 record and underground camera; original failure retained in T4 integration-fix" : "Separate injected boundary record; impossible variant exceeds rigid domain, multiple variant has two independent repairable friends" });
|
||||
}
|
||||
var puzzle = new PuzzleState { stageId = 2, camera = GameSession.DefaultCamera(),
|
||||
balls = new[] { new BallState { id = "native-count-a", position = new Position3(30, 4, 0) }, new BallState { id = "native-count-b", position = new Position3(31, 4, 0) } },
|
||||
groups = new[] { new GroupState { id = "native-count-group", ballIds = new[] { "native-count-a", "native-count-b" } } },
|
||||
collectedBallIds = new[] { "native-count-a" }, countedBallIds = new[] { "native-count-a" } };
|
||||
foreach (bool impossible in new[] { false, true })
|
||||
{
|
||||
if (impossible) { puzzle = JsonUtility.FromJson<PuzzleState>(JsonUtility.ToJson(puzzle)); puzzle.balls[0].position.x = -30; puzzle.balls[1].position.x = 30; }
|
||||
_store.Saved.puzzleStates = new[] { puzzle }; _app.Initialize(_store); _app.StartStage(2); yield return null;
|
||||
Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.Puzzle)); Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True);
|
||||
restores.Add(new NativeRestore { name = impossible ? "impossible-puzzle" : "counting-position-only", stage = 2, slot = "puzzle", puzzleSource = puzzle,
|
||||
puzzleExpected = _app.Session.Board.ToPuzzleState(_app.Orbit.Snapshot()), origin = "Separate injected two-ball Counting record; preserved partial collect/count; impossible variant changes x span to 60" });
|
||||
}
|
||||
_store.Saved.puzzleStates = Array.Empty<PuzzleState>();
|
||||
foreach (int count in new[] { 1, 10 }) foreach (int axis in Enumerable.Range(0, 3)) foreach (int sign in new[] { -1, 1 })
|
||||
{
|
||||
_app.StartStage(1); yield return null;
|
||||
var bounds = _app.Environment.PlaygroundBounds;
|
||||
var points = Enumerable.Range(0, count).Select(index => new Vector3((index % 5 - (count == 1 ? 0 : 2)), 4, index / 5)).ToArray();
|
||||
var lower = points.Aggregate(Vector3.Min); var upper = points.Aggregate(Vector3.Max);
|
||||
float offset = sign < 0 ? bounds.min[axis] + .5f + 1.2f - lower[axis] : bounds.max[axis] - .5f - 1.2f - upper[axis];
|
||||
var translation = Vector3.zero; translation[axis] = offset;
|
||||
points = points.Select(point => point + translation).ToArray();
|
||||
Assert.That(points.All(point => _app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point))), Is.True);
|
||||
var rotation = Quaternion.Euler(0, axis == 2 ? 90 : 0, 0);
|
||||
var focus = points[0] - Vector3.up * .65f;
|
||||
var camera = new CameraState { target = BoardPresenter.ToPosition(focus), distance = 14, rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w) };
|
||||
var source = new FreePlayState { sourceStageId = 1, camera = camera,
|
||||
balls = points.Select((point, index) => new BallState { id = "native-drag-" + index, position = BoardPresenter.ToPosition(point) }).ToArray(),
|
||||
groups = new[] { new GroupState { id = "native-drag-group", ballIds = Enumerable.Range(0, count).Select(index => "native-drag-" + index).ToArray() } } };
|
||||
_store.Saved.latestFreePlay = source; _app.Initialize(_store); _app.ContinueFreePlay(); yield return null;
|
||||
_app.Orbit.Camera.aspect = 2400f / 1080;
|
||||
var requested = Vector3.zero; requested[axis] = sign * 3;
|
||||
drags.Add(ExerciseNativeDrag("boundary-" + count + "-" + axis + "-" + sign, "boundary", source, requested));
|
||||
yield return null;
|
||||
}
|
||||
_app.StartStage(1); yield return null; Physics.SyncTransforms();
|
||||
var arch = _app.Environment.GetComponentsInChildren<MeshCollider>().First(value => value.enabled && value.GetComponentsInParent<Transform>().Any(parent => parent.name.Contains("OpenStoneArch")));
|
||||
var foliage = _app.Environment.transform.Cast<Transform>().Where(value => value.gameObject.activeInHierarchy && value.name.Contains("GrassTuftMint") && value.GetComponentsInChildren<Collider>().Length == 0).ToArray();
|
||||
Assert.That(foliage, Is.Not.Empty);
|
||||
foreach (string kind in new[] { "thin-obstacle", "foliage-pass", "two-plus-three" })
|
||||
{
|
||||
var sourcePoint = new Vector3(-3, 3, 0); var requested = Vector3.right * 4;
|
||||
if (kind == "thin-obstacle") { sourcePoint = new Vector3(arch.bounds.min.x - 1.5f, 1.5f, arch.bounds.center.z); requested = Vector3.right * (arch.bounds.size.x + 3); }
|
||||
if (kind == "foliage-pass")
|
||||
{
|
||||
bool found = false;
|
||||
foreach (var plant in foliage)
|
||||
{
|
||||
var center = plant.GetComponentInChildren<Renderer>().bounds.center; sourcePoint = new Vector3(center.x - 2, .8f, center.z);
|
||||
if (!_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(sourcePoint))) continue;
|
||||
if (Vector3.Distance(_app.Environment.ConstrainTranslation(new[] { sourcePoint }, requested, _app.Presenter.transform), requested) > .001f) continue;
|
||||
found = true; break;
|
||||
}
|
||||
Assert.That(found, Is.True, "Actual small foliage path must cross visible plant and remain unobstructed.");
|
||||
}
|
||||
var balls = new System.Collections.Generic.List<BallState> { new BallState { id = "native-contact-a", position = BoardPresenter.ToPosition(sourcePoint) } };
|
||||
var groups = new System.Collections.Generic.List<GroupState> { new GroupState { id = "native-contact-source", ballIds = new[] { "native-contact-a" } } };
|
||||
if (kind == "thin-obstacle")
|
||||
{
|
||||
balls.Add(new BallState { id = "native-contact-b", position = BoardPresenter.ToPosition(sourcePoint + requested) });
|
||||
groups.Add(new GroupState { id = "native-contact-target", ballIds = new[] { "native-contact-b" } });
|
||||
}
|
||||
if (kind == "two-plus-three")
|
||||
{
|
||||
balls = Enumerable.Range(0, 5).Select(index => new BallState { id = "native-contact-" + index, position = new Position3(index - 3, 3, 0) }).ToList();
|
||||
groups = new System.Collections.Generic.List<GroupState> { new GroupState { id = "native-contact-source", ballIds = balls.Take(2).Select(ball => ball.id).ToArray() }, new GroupState { id = "native-contact-target", ballIds = balls.Skip(2).Select(ball => ball.id).ToArray() } };
|
||||
// Source/target begin separated rather than already touching.
|
||||
foreach (var ball in balls.Skip(2)) ball.position.x += 1;
|
||||
}
|
||||
var camera = new CameraState { target = BoardPresenter.ToPosition(sourcePoint - Vector3.up * .65f), distance = 14 };
|
||||
var source = new FreePlayState { sourceStageId = 1, camera = camera, balls = balls.ToArray(), groups = groups.ToArray() };
|
||||
_store.Saved.latestFreePlay = source; _app.Initialize(_store); _app.ContinueFreePlay(); yield return null;
|
||||
Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True); _app.Orbit.Camera.aspect = 2400f / 1080;
|
||||
if (kind == "thin-obstacle")
|
||||
{
|
||||
bool visible = false;
|
||||
foreach (float pitch in new[] { 0f, 15f, 30f, 45f, 60f })
|
||||
{
|
||||
var rotation = Quaternion.Euler(pitch, 0, 0);
|
||||
camera.rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w); _app.Orbit.Apply(camera);
|
||||
Physics.SyncTransforms(); var ray = _app.Orbit.Camera.ScreenPointToRay(PlayerPoint(NativePoint(sourcePoint)));
|
||||
if (!Physics.Raycast(ray, out var hit, 100) || !hit.collider.TryGetComponent<BallView>(out var ball) || ball.BallId != source.balls[0].id) continue;
|
||||
Assert.That(Quaternion.Angle(_app.Orbit.Camera.transform.rotation, rotation), Is.LessThan(.001f), "Chosen pitch must already be a valid manual pose.");
|
||||
source.camera = _app.Orbit.Snapshot(); _app.Session.CommitCamera(source.camera); visible = true; break;
|
||||
}
|
||||
Assert.That(visible, Is.True, "The same thin-arch source must have an actually selectable supported pitch.");
|
||||
}
|
||||
Capture(kind + "-native-pointer-source", "Actual Editor-selected first hit and preserved source; integer native pointer oracle, not Android pass", 2400, 1080);
|
||||
drags.Add(ExerciseNativeDrag(kind, kind, source, requested)); yield return null;
|
||||
}
|
||||
var directory = Environment.GetEnvironmentVariable("MAGNETISM_NATIVE_FIXTURES") ?? Path.GetFullPath("artifacts/verification/PlaygroundInteraction/NativeFixtures");
|
||||
Directory.CreateDirectory(directory);
|
||||
File.WriteAllText(Path.Combine(directory, "native-fixtures.json"), JsonUtility.ToJson(new NativeFixtures {
|
||||
sourceCommit = Environment.GetEnvironmentVariable("MAGNETISM_PROOF_COMMIT"), sourceDirty = Environment.GetEnvironmentVariable("MAGNETISM_PROOF_DIRTY"),
|
||||
utc = DateTime.UtcNow.ToString("O"), origin = "Actual Main scene, unchanged geometry, separately labelled boundary fixtures. Editor oracle is not Android pass.",
|
||||
stages = stages.ToArray(), restores = restores.ToArray(), drags = drags.ToArray() }, true));
|
||||
Assert.That(stages.Count, Is.EqualTo(30)); Assert.That(restores.Count, Is.EqualTo(9)); Assert.That(drags.Count, Is.EqualTo(15));
|
||||
}
|
||||
|
||||
private void AssertEquipmentFollowsRepresentatives(BoardRules board)
|
||||
{
|
||||
_app.Presenter.SendMessage("LateUpdate");
|
||||
var badges = _app.Presenter.GetComponentsInChildren<GroupBadge>();
|
||||
Assert.That(badges.Length, Is.EqualTo(board.Groups.Length));
|
||||
Assert.That(_app.Presenter.Balls.Keys, Is.EquivalentTo(board.Balls.Select(ball => ball.id)));
|
||||
Assert.That(board.Groups.SelectMany(group => group.ballIds), Is.EquivalentTo(board.Balls.Select(ball => ball.id)));
|
||||
Assert.That(_app.Presenter.GetComponentsInChildren<SpriteRenderer>().Length, Is.EqualTo(board.Groups.Length));
|
||||
Assert.That(_app.Presenter.GetComponentsInChildren<TMPro.TextMeshPro>().Select(label => label.text),
|
||||
Is.EqualTo(board.Groups.Select(group => group.ballIds.Length.ToString())));
|
||||
for (int index = 0; index < badges.Length; index++)
|
||||
{
|
||||
var gear = badges[index].transform.Find("KnightEquipment");
|
||||
Assert.That(gear, Is.Not.Null);
|
||||
var representative = board.Groups[index].ballIds.Select(id => _app.Presenter.Balls[id].transform.position)
|
||||
.OrderBy(point => Vector3.Dot(point - _app.Presenter.Camera.transform.position, _app.Presenter.Camera.transform.forward)).First();
|
||||
Assert.That(Vector3.Distance(gear.position, representative), Is.LessThan(.0001f));
|
||||
Assert.That(Quaternion.Angle(gear.rotation, _app.Presenter.Camera.transform.rotation), Is.LessThan(.001f));
|
||||
Assert.That(gear.GetComponentsInChildren<Collider>(), Is.Empty);
|
||||
Assert.That(badges[index].transform.Find("RibbonCape"), Is.Not.Null);
|
||||
foreach (string id in board.Groups[index].ballIds)
|
||||
Assert.That(_app.Presenter.Balls[id].GroupId, Is.EqualTo(board.Groups[index].id));
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureViews(string name, string origin)
|
||||
{
|
||||
var saved = _app.Orbit.Snapshot();
|
||||
Capture(name + "-phone-default", origin, 2400, 1080);
|
||||
Capture(name + "-tablet-default", origin, 1600, 1000);
|
||||
var rotation = Quaternion.Euler(25, 95, 0);
|
||||
_app.Orbit.Apply(new CameraState { target = saved.target, distance = 9,
|
||||
rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w) });
|
||||
Capture(name + "-phone-orbitzoom", origin, 2400, 1080);
|
||||
Capture(name + "-tablet-orbitzoom", origin, 1600, 1000);
|
||||
_app.Orbit.Apply(saved);
|
||||
}
|
||||
|
||||
[UnityTest] public IEnumerator DistantTerrainHasContinuousApronAcrossItsVisibleFootInsteadOfSkyGaps()
|
||||
{
|
||||
var report = new System.Collections.Generic.List<string>();
|
||||
foreach (int stage in new[] { 1, 11, 21 })
|
||||
{
|
||||
_app.StartStage(stage); _app.Voice.Clear(); yield return null;
|
||||
CaptureViews("horizon-world" + stage, "Actual Main board, unchanged default/orbitzoom cameras; terrain continuity regression");
|
||||
var horizon = _app.Environment.GetComponentsInChildren<Transform>().Where(item => item.name == "FarHorizon(Clone)").ToArray();
|
||||
var originalPositions = horizon.Select(item => item.position).ToArray();
|
||||
var savedCamera = _app.Orbit.Snapshot(); var closeRotation = Quaternion.Euler(25, 95, 0);
|
||||
_app.Orbit.Apply(new CameraState { target = savedCamera.target, distance = 9,
|
||||
rotation = new Rotation4(closeRotation.x, closeRotation.y, closeRotation.z, closeRotation.w) });
|
||||
var camera = _app.Presenter.Camera; float fieldOfView = camera.fieldOfView;
|
||||
try
|
||||
{
|
||||
// Real camera renders of the reproduced (+X, z12) foot gap; no screenshot cropping/editing.
|
||||
camera.fieldOfView = 14; camera.transform.LookAt(new Vector3(28, -.45f, 12));
|
||||
Capture("horizon-world" + stage + "-foot-current", "Actual Main terrain foot; temporary narrow camera FOV only", 2400, 1080);
|
||||
foreach (var far in horizon)
|
||||
{
|
||||
var direction = new Vector3(far.position.x, 0, far.position.z).normalized;
|
||||
far.position = new Vector3(direction.x * 35, far.position.y, direction.z * 35);
|
||||
}
|
||||
Capture("horizon-world" + stage + "-foot-legacy-radius35", "Same actual Main board, materials and close camera; temporary previous radius35 placement control", 2400, 1080);
|
||||
}
|
||||
finally
|
||||
{
|
||||
for (int index = 0; index < horizon.Length; index++) horizon[index].position = originalPositions[index];
|
||||
camera.fieldOfView = fieldOfView; _app.Orbit.Apply(savedCamera);
|
||||
}
|
||||
string board = JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Orbit.Snapshot()));
|
||||
var collisionBefore = _app.GetComponentsInChildren<Collider>();
|
||||
var probeScene = SceneManager.CreateScene("TerrainGeometryProbe", new CreateSceneParameters(LocalPhysicsMode.Physics3D));
|
||||
var probes = new System.Collections.Generic.List<MeshCollider>();
|
||||
try
|
||||
{
|
||||
var terrain = _app.Environment.GetComponentsInChildren<Transform>().Where(item =>
|
||||
item.name == "MeadowClearing(Clone)" || item.name == "GentleHill(Clone)" ||
|
||||
item.name == "DistantRidge(Clone)" || item.name == "FarHorizon(Clone)").ToArray();
|
||||
foreach (var surface in terrain.SelectMany(item => item.GetComponentsInChildren<MeshFilter>()))
|
||||
{
|
||||
var probe = new GameObject("GeometryOnly-" + surface.name);
|
||||
SceneManager.MoveGameObjectToScene(probe, probeScene);
|
||||
probe.transform.SetPositionAndRotation(surface.transform.position, surface.transform.rotation);
|
||||
probe.transform.localScale = surface.transform.lossyScale;
|
||||
probes.Add(probe.AddComponent<MeshCollider>()); probes.Last().sharedMesh = surface.sharedMesh;
|
||||
report.Add($"stage={stage}; terrain={surface.name}; actualRendererBounds={surface.GetComponent<Renderer>().bounds}; meshBounds={surface.sharedMesh.bounds}; rootPosition={surface.transform.position}");
|
||||
}
|
||||
probeScene.GetPhysicsScene().Simulate(.001f);
|
||||
int missing = 0;
|
||||
foreach (var far in terrain.Where(item => item.name == "FarHorizon(Clone)"))
|
||||
{
|
||||
var direction = new Vector3(far.position.x, 0, far.position.z).normalized;
|
||||
var right = Vector3.Cross(Vector3.up, direction);
|
||||
float distance = new Vector2(far.position.x, far.position.z).magnitude;
|
||||
// Traverse the apron to the far mound crown across its central 24m foot.
|
||||
// A positive gap is real missing geometry, independent of lighting/material colors.
|
||||
for (float lateral = -12; lateral <= 12; lateral += 2)
|
||||
for (float radial = 0; radial <= distance; radial += .25f)
|
||||
{
|
||||
var point = direction * radial + right * lateral;
|
||||
var ray = new Ray(point + Vector3.up * 20, Vector3.down);
|
||||
if (probes.Any(probe => probe.Raycast(ray, out _, 40))) continue;
|
||||
missing++;
|
||||
if (missing <= 24) report.Add($"stage={stage}; unsupportedApron={point}; farCrown={far.position}");
|
||||
}
|
||||
}
|
||||
report.Add($"stage={stage}; missingApronSamples={missing}; probe=separate local PhysicsScene using actual shared mesh geometry, no product collider edits");
|
||||
File.WriteAllLines(Path.Combine(_evidence, "horizon-geometry.txt"), report);
|
||||
Assert.That(missing, Is.Zero, "Far terrain foot must join the actual clearing/hills/ridge without open geometry gaps.");
|
||||
}
|
||||
finally { SceneManager.UnloadSceneAsync(probeScene); }
|
||||
yield return null;
|
||||
Assert.That(_app.GetComponentsInChildren<Collider>(), Is.EqualTo(collisionBefore));
|
||||
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Orbit.Snapshot())), Is.EqualTo(board));
|
||||
}
|
||||
}
|
||||
|
||||
[UnityTest] public IEnumerator FiveBallMergeDisconnectedSplitAndZeroResupplyKeepActualIdsAcrossAllWorlds()
|
||||
{
|
||||
foreach (int stage in new[] { 9, 11, 21 })
|
||||
{
|
||||
// Boundary fixture: a completed-stage store with a validated five-ball free board,
|
||||
// not evidence of normal puzzle progression. All operations below use the real app/session.
|
||||
var balls = Enumerable.Range(0, 5).Select(index => new BallState { id = "five-" + index,
|
||||
position = new Position3(index - 2, .8f, 0) }).ToArray();
|
||||
_store.Saved.latestFreePlay = new FreePlayState { sourceStageId = stage,
|
||||
camera = _app.Orbit.Snapshot(), balls = balls,
|
||||
groups = new[] { new GroupState { id = "two", ballIds = balls.Take(2).Select(ball => ball.id).ToArray() },
|
||||
new GroupState { id = "three", ballIds = balls.Skip(2).Select(ball => ball.id).ToArray() } } };
|
||||
_app.Initialize(_store); _app.ContinueFreePlay(); _app.Voice.Clear(); yield return null;
|
||||
var ids = _app.Session.Board.Balls.Select(ball => ball.id).ToArray();
|
||||
AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
||||
CaptureViews("world" + stage + "-two-plus-three", "Validated five-ball 2+3 free-play store fixture; actual Main app/session");
|
||||
var before = JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot()));
|
||||
_app.Presenter.PreviewGroup("two", new Vector3(.3f, .2f, 0));
|
||||
AssertEquipmentFollowsRepresentatives(_app.Session.Board); _app.Presenter.CancelPreview();
|
||||
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot())), Is.EqualTo(before));
|
||||
_app.CommitMove("two", new Position3(.1f, 0, 0), "three"); yield return null;
|
||||
Assert.That(_app.Session.Board.Groups.Single().ballIds.Length, Is.EqualTo(5));
|
||||
Assert.That(_app.Session.Board.Balls.Select(ball => ball.id), Is.EquivalentTo(ids));
|
||||
AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
||||
CaptureViews("world" + stage + "-merged-five", "Actual app/session 2+3 merge from labeled boundary fixture");
|
||||
var selected = new[] { ids[0], ids[4] };
|
||||
var points = selected.Select(id => BoardPresenter.ToVector(_app.Session.Board.Balls.Single(ball => ball.id == id).position)).ToArray();
|
||||
Assert.That(Vector3.Distance(points[0], points[1]), Is.GreaterThan(2f), "Selected endpoints are disconnected.");
|
||||
var merged = _app.Session.Board.Groups.Single();
|
||||
before = JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot()));
|
||||
Assert.That(_app.Presenter.PreviewSplit(merged.id, selected, new Vector3(0, 1.5f, 0)), Is.True);
|
||||
AssertEquipmentFollowsRepresentatives(_app.Session.Board); _app.Presenter.CancelPreview();
|
||||
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot())), Is.EqualTo(before));
|
||||
Assert.That(_app.CommitSplit(merged.id, selected, new Position3(0, 1.5f, 0)), Is.True); yield return null;
|
||||
Assert.That(_app.Session.Board.Groups.Select(group => group.ballIds.Length), Is.EquivalentTo(new[] { 2, 3 }));
|
||||
Assert.That(_app.Session.Board.Groups.Single(group => group.ballIds.Length == 2).ballIds, Is.EquivalentTo(selected));
|
||||
Assert.That(_app.Session.Board.Balls.Select(ball => ball.id), Is.EquivalentTo(ids));
|
||||
AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
||||
CaptureViews("world" + stage + "-disconnected-split-two-three", "Actual app/session split of disconnected five-group endpoints");
|
||||
before = JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot()));
|
||||
_app.ShowHome(); yield return null;
|
||||
Assert.That(_app.Presenter.GetComponentsInChildren<GroupBadge>(), Is.Empty);
|
||||
_app.ContinueFreePlay(); yield return null;
|
||||
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot())), Is.EqualTo(before));
|
||||
AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
||||
foreach (var group in _app.Session.Board.Groups) Assert.That(_app.Session.ReturnGroup(group.id).Accepted, Is.True);
|
||||
_app.ContinueFreePlay(); yield return null;
|
||||
Assert.That(_app.Session.Board.BallCount, Is.Zero); AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
||||
CaptureViews("world" + stage + "-zero", "Actual app/session return of both groups; empty free play");
|
||||
_app.SpawnFriend(); yield return null;
|
||||
Assert.That(_app.Session.Board.BallCount, Is.EqualTo(1)); AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
||||
CaptureViews("world" + stage + "-single-resupplied", "Actual app/session resupply after empty board");
|
||||
}
|
||||
}
|
||||
|
||||
[UnityTest] public IEnumerator UndergroundViewIsCorrectedAboveGroundWithoutChangingActualBoard()
|
||||
{
|
||||
_app.StartStage(21); _app.Voice.Clear(); yield return null;
|
||||
var balls = _app.Session.Board.Balls.Select(ball => (ball.id, ball.position)).ToArray();
|
||||
var groups = _app.Session.Board.Groups.Select(group => (group.id, ids: group.ballIds.ToArray())).ToArray();
|
||||
var rotation = Quaternion.Euler(-45, 210, 0);
|
||||
_app.Orbit.Apply(new CameraState { target = _app.Orbit.Snapshot().target, distance = 15,
|
||||
rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w) });
|
||||
yield return null; Physics.SyncTransforms();
|
||||
Assert.That(_app.Presenter.Camera.transform.position.y, Is.GreaterThan(_app.Environment.PlaygroundBounds.min.y));
|
||||
Assert.That(_app.Session.Board.Balls.Select(ball => (ball.id, ball.position)), Is.EqualTo(balls));
|
||||
Assert.That(_app.Session.Board.Groups.Select(group => group.id), Is.EqualTo(groups.Select(group => group.id)));
|
||||
for (int index = 0; index < groups.Length; index++) Assert.That(_app.Session.Board.Groups[index].ballIds, Is.EqualTo(groups[index].ids));
|
||||
Capture("corrected-underground-view", "Actual stage21; legacy below-ground saved view corrected above clearing; board IDs, groups and positions unchanged");
|
||||
}
|
||||
|
||||
[UnityTest] public IEnumerator AllWorldsProduceActualSoftShadowPixelsWithUnchangedBoardAndMaterials()
|
||||
{
|
||||
var sun = _app.transform.Find("Sun").GetComponent<Light>();
|
||||
_app.ShowHome(); yield return null;
|
||||
Capture("home-cyan-readiness", "Actual Main home UI for Android sky readiness predicate", 2400, 1080);
|
||||
foreach (int stage in new[] { 1, 11, 21 })
|
||||
{
|
||||
_app.StartStage(stage); _app.Voice.Clear(); yield return null;
|
||||
string before = JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Orbit.Snapshot()));
|
||||
Capture("world" + stage + "-soft-shadow", "Actual Main daylight with soft shadows");
|
||||
try { sun.shadows = LightShadows.None; Capture("world" + stage + "-shadow-off", "Same board/camera/materials; light shadow toggle comparison only"); }
|
||||
finally { sun.shadows = LightShadows.Soft; }
|
||||
var on = new Texture2D(2, 2); var off = new Texture2D(2, 2);
|
||||
try
|
||||
{
|
||||
on.LoadImage(File.ReadAllBytes(Path.Combine(_evidence, "world" + stage + "-soft-shadow.png")));
|
||||
off.LoadImage(File.ReadAllBytes(Path.Combine(_evidence, "world" + stage + "-shadow-off.png")));
|
||||
var a = on.GetPixels32(); var b = off.GetPixels32(); int darker = 0;
|
||||
// The central lower ground excludes the board, badge labels, scenery crowns and HUD.
|
||||
for (int y = 140; y < 300; y++) for (int x = 350; x < 1000; x++)
|
||||
{
|
||||
int index = y * on.width + x;
|
||||
if (a[index].r + a[index].g + a[index].b + 12 < b[index].r + b[index].g + b[index].b) darker++;
|
||||
}
|
||||
File.WriteAllText(Path.Combine(_evidence, "world" + stage + "-shadow-pixels.txt"), $"same-board/camera/materials; shadow-on darker ground ROI pixels={darker}; groundROI=350,140,650,160; viewport=1280x720; Editor RT only");
|
||||
Assert.That(darker, Is.GreaterThan(100), "Actual rendered soft shadows must darken a meaningful ground area.");
|
||||
}
|
||||
finally { Object.Destroy(on); Object.Destroy(off); }
|
||||
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Orbit.Snapshot())), Is.EqualTo(before));
|
||||
}
|
||||
}
|
||||
[UnityTest] public IEnumerator ActualMainShowsThreeThemesAndEquipmentFollowsPreviewCancelMergeSplitAndReentry()
|
||||
{
|
||||
foreach (int stage in new[] { 1, 11, 21 })
|
||||
{
|
||||
_app.StartStage(stage); _app.Voice.Clear(); yield return null;
|
||||
AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
||||
Capture("stage" + stage.ToString("D2"), "Actual Main GameApp.Session.Board; unlocked-stage fixture store");
|
||||
}
|
||||
var normalView = _app.Orbit.Snapshot();
|
||||
foreach (var view in new[] { (name: "orbit95-pitch45-distance6", yaw: 95f, pitch: 45f, distance: 6f),
|
||||
(name: "orbit210-pitch25-distance15", yaw: 210f, pitch: 25f, distance: 15f) })
|
||||
{
|
||||
var rotation = Quaternion.Euler(view.pitch, view.yaw, 0);
|
||||
_app.Orbit.Apply(new CameraState { target = normalView.target, distance = view.distance,
|
||||
rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w) });
|
||||
yield return null; AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
||||
Capture(view.name, "Actual Main session; camera-only orbit/pitch/distance exploration fixture");
|
||||
}
|
||||
_app.Orbit.Apply(normalView);
|
||||
_app.StartStage(6); _app.Voice.Clear(); yield return null;
|
||||
var original = _app.Session.Board.Balls.Select(ball => ball.position).ToArray();
|
||||
var groups = _app.Session.Board.Groups;
|
||||
_app.Presenter.PreviewGroup(groups[0].id, new Vector3(.3f, .2f, 0));
|
||||
AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
||||
_app.Presenter.CancelPreview();
|
||||
AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
||||
for (int index = 0; index < original.Length; index++)
|
||||
Assert.That(_app.Presenter.Balls[_app.Session.Board.Balls[index].id].transform.position, Is.EqualTo(BoardPresenter.ToVector(original[index])));
|
||||
_app.Presenter.ShowSelection(new[] { groups[0].ballIds[0] }); yield return null;
|
||||
Capture("selected-two-groups", "Actual Main session, visual selection only");
|
||||
_app.CommitMove(groups[0].id, new Position3(.1f, 0, 0), groups[1].id); yield return null;
|
||||
Assert.That(_app.Session.Board.Groups.Length, Is.EqualTo(1));
|
||||
AssertEquipmentFollowsRepresentatives(_app.Session.Board); Capture("merged-single-group", "Actual Main session merge -> free play");
|
||||
var merged = _app.Session.Board.Groups[0];
|
||||
Assert.That(_app.CommitSplit(merged.id, new[] { merged.ballIds[0] }, new Position3(1.5f, 0, 0)), Is.True); yield return null;
|
||||
Assert.That(_app.Session.Board.Groups.Length, Is.EqualTo(2)); AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
||||
_app.ShowHome(); _app.ContinueFreePlay(); yield return null;
|
||||
Assert.That(_app.Session.Board.BallCount, Is.EqualTo(2));
|
||||
Assert.That(_app.Environment.Theme, Is.EqualTo("Garden")); AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
||||
Capture("split-reentered-two-groups", "Actual Main session free play split and home/reentry");
|
||||
}
|
||||
[UnityTest] public IEnumerator TenSingleGroupsAndOneTenBallGroupRenderActualImportedResourceCounts()
|
||||
{
|
||||
var balls = Enumerable.Range(0, 10).Select(index => new BallState { id = "render-" + index,
|
||||
position = new Position3((index % 5 - 2) * 1.6f, .8f + index / 5 * 1.8f, 0) }).ToArray();
|
||||
foreach (int stage in new[] { 1, 11, 21 })
|
||||
{
|
||||
_app.StartStage(stage); _app.Voice.Clear(); yield return null;
|
||||
foreach (bool singles in new[] { true, false })
|
||||
{
|
||||
var groups = singles ? balls.Select(ball => new GroupState { id = ball.id, ballIds = new[] { ball.id } }).ToArray()
|
||||
: new[] { new GroupState { id = "ten", ballIds = balls.Select(ball => ball.id).ToArray() } };
|
||||
var board = BoardRules.CreateFreePlay(new FreePlayState { sourceStageId = stage, balls = balls, groups = groups });
|
||||
_app.Presenter.Render(board); yield return null;
|
||||
AssertEquipmentFollowsRepresentatives(board);
|
||||
CaptureViews("world" + stage + (singles ? "-ten-single-groups" : "-ten-one-group"), "Validated synthetic Core FreePlay render fixture; session UI remains underlying stage " + stage);
|
||||
}
|
||||
}
|
||||
}
|
||||
private void Capture(string name, string origin, int width = 1280, int height = 720)
|
||||
{
|
||||
var camera = _app.Presenter.Camera; var canvas = _app.GetComponentInChildren<Canvas>();
|
||||
var oldTarget = camera.targetTexture; var oldActive = RenderTexture.active; var oldMode = canvas.renderMode;
|
||||
var oldCamera = canvas.worldCamera; float oldPlane = canvas.planeDistance, oldAspect = camera.aspect;
|
||||
var target = new RenderTexture(width, height, 24); var image = new Texture2D(width, height, TextureFormat.RGB24, false);
|
||||
try
|
||||
{
|
||||
camera.targetTexture = target; camera.aspect = (float)width / height;
|
||||
canvas.renderMode = RenderMode.ScreenSpaceCamera; canvas.worldCamera = camera; canvas.planeDistance = .5f;
|
||||
foreach (var ball in _app.Presenter.Balls.Values) ball.SendMessage("Update");
|
||||
if (_app.Presenter.gameObject.activeInHierarchy) _app.Presenter.SendMessage("LateUpdate");
|
||||
Canvas.ForceUpdateCanvases();
|
||||
var timer = System.Diagnostics.Stopwatch.StartNew(); camera.Render(); timer.Stop();
|
||||
RenderTexture.active = target; image.ReadPixels(new Rect(0, 0, width, height), 0, 0); image.Apply();
|
||||
File.WriteAllBytes(Path.Combine(_evidence, name + ".png"), image.EncodeToPNG());
|
||||
var meshes = _app.GetComponentsInChildren<MeshFilter>();
|
||||
var badges = _app.Presenter.GetComponentsInChildren<GroupBadge>();
|
||||
var record = new CaptureRecord { name = name, sourceCommit = Environment.GetEnvironmentVariable("MAGNETISM_PROOF_COMMIT"),
|
||||
sourceDirty = Environment.GetEnvironmentVariable("MAGNETISM_PROOF_DIRTY"), utc = DateTime.UtcNow.ToString("o"),
|
||||
boardOrigin = origin, theme = _app.Environment.Theme, stage = _app.Session.Board?.StageId ?? 0,
|
||||
width = width, height = height, balls = _app.Presenter.Balls.Count, groups = badges.Length,
|
||||
equipmentAssemblies = badges.Count(item => item.transform.Find("KnightEquipment") != null), meshInstances = meshes.Length,
|
||||
triangles = meshes.Sum(item => Enumerable.Range(0, item.sharedMesh.subMeshCount).Sum(submesh => (int)item.sharedMesh.GetIndexCount(submesh) / 3)),
|
||||
sharedMaterials = _app.GetComponentsInChildren<Renderer>().SelectMany(item => item.sharedMaterials).Where(item => item != null).Distinct().Count(),
|
||||
editorCameraRenderMilliseconds = (float)timer.Elapsed.TotalMilliseconds,
|
||||
fieldOfView = camera.fieldOfView,
|
||||
textureMemory = Resources.FindObjectsOfTypeAll<Texture>().Sum(item => Profiler.GetRuntimeMemorySizeLong(item)),
|
||||
allocatedMemory = Profiler.GetTotalAllocatedMemoryLong(), cameraPosition = camera.transform.position, cameraRotation = camera.transform.rotation,
|
||||
method = "Actual Main scene PlayMode Camera.Render to RT, ScreenSpaceCamera canvas; simulated viewport, not Android capture. Triangles are resident index counts for active mesh instances, including the retained floor with its Renderer disabled; they are not submitted GPU triangles. Camera.Render elapsed is Editor CPU wall time, not GPU time or device FPS. Texture memory includes all Editor-loaded textures." };
|
||||
File.WriteAllText(Path.Combine(_evidence, name + ".json"), JsonUtility.ToJson(record, true));
|
||||
if (name == "world21-ten-single-groups-phone-default")
|
||||
File.WriteAllLines(Path.Combine(_evidence, "editor-loaded-textures.txt"), Resources.FindObjectsOfTypeAll<Texture>()
|
||||
.Select(item => $"{AssetDatabase.GetAssetPath(item)};name={item.name};dimensions={item.width}x{item.height};format={(item is Texture2D texture ? texture.format.ToString() : item.GetType().Name)};runtimeBytes={Profiler.GetRuntimeMemorySizeLong(item)}"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
camera.targetTexture = oldTarget; camera.aspect = oldAspect; RenderTexture.active = oldActive;
|
||||
canvas.renderMode = oldMode; canvas.worldCamera = oldCamera; canvas.planeDistance = oldPlane;
|
||||
Object.DestroyImmediate(image); target.Release(); Object.DestroyImmediate(target);
|
||||
if (_app.Presenter.gameObject.activeInHierarchy) _app.Presenter.SendMessage("LateUpdate");
|
||||
Canvas.ForceUpdateCanvases();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ef592ccad6c188458f0ab2ec0f9e291
|
||||
@@ -4,8 +4,10 @@
|
||||
|
||||
GameApp이 저장 구현을 GameSession에 전달하고 BoardInput의 확정 의도를 처리한다. 끌기·분리 선택은 미리보기이며 취소·핀치·영역 이탈에서 확정 상태로 복구한다. UI hit를 세계 Raycast보다 먼저 처리하고 소품의 앞쪽 hit를 무시해 뒤 구를 선택하지 않는다.
|
||||
|
||||
Presenter와 GroupBadge는 확정 소속에서 구·숫자·대표 얼굴·망토를 만든다. 장식에 별도 입력 콜라이더나 수량을 추가하지 않는다. 자석 크기·합체한 아이 배열은 유지하고 분리 재배치만 Core의 확정 결과를 따른다. 화면 전환·재진입에서 이전 미리보기·표정·음성이 남지 않아야 한다.
|
||||
Presenter와 GroupBadge는 확정 소속에서 구·숫자·대표 얼굴·망토를 만든다. 얼굴·장비·작은 식생에 별도 자석 입력 콜라이더나 수량을 추가하지 않는다. 지면·큰 자연물은 실제 이동·선택을 막는 환경 Collider이며 앞쪽 hit를 우회하지 않는다. 자석 크기·합체한 아이 배열은 유지하고 분리 재배치만 Core의 확정 결과를 따른다. 화면 전환·재진입에서 이전 미리보기·표정·음성이 남지 않아야 한다.
|
||||
|
||||
VoicePlayer는 승인된 ID의 계수 큐를 순차 재생한다. 같은 보드의 성공 전이에서 큐를 지우지 않으며 화면 이동·pause에서는 지운다. 현재 무음인 SFX/Music 참조를 누락 음성으로 혼동하지 않는다. LocalProgressStore는 전체 JSON을 검증하고 유효 백업을 손상 primary로 교체하지 않는다.
|
||||
|
||||
입력·화면·저장 변경은 Tests/PlayMode에서 실제 프리팹과 충돌·취소·복원을 확인한다. 터치와 오프라인 음성·Android 파일 수명은 같은 APK의 AVD에서 재확인한다. StageAsset 편집이 규칙 목표를 바꾸지 않는지, 전체 한글 glyph와 스마트폰·태블릿 비율에서 잘림이 없는지도 확인한다.
|
||||
입력·화면·저장 변경은 Tests/PlayMode에서 실제 프리팹과 충돌·취소·복원을 확인한다. 터치와 오프라인 음성·Android 파일 수명은 같은 APK의 AVD에서 재확인한다. StageAsset 편집이 규칙 목표를 바꾸지 않는지, 실제 사용하는 한국어 glyph와 스마트폰·태블릿 비율에서 잘림이 없는지도 확인한다.
|
||||
|
||||
묶음의 경로·접촉 보정·분리 양쪽·공급은 같은 환경 안전 판정을 사용하며 원래 초기 구를 옮겨 지형 문제를 숨기지 않는다. 끌기 보조는 현재 렌더 시점과 잡은 표면에 맞추고 정지한 손가락에서 이동량이 0인지 검사한다. 임시 보조·복귀값을 저장하지 않고 놓기·취소·핀치·pause·전환에서 수동 시점 소유를 정리한다. 복원은 위치만 준비하고 검색 오류·한도 소진을 초기화 사유로 취급하지 않는다.
|
||||
|
||||
@@ -43,6 +43,8 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap
|
||||
[SerializeField] private UnityEngine.UI.Button _nextButton;
|
||||
[SerializeField] private BoardEnvironment _environment;
|
||||
[SerializeField] private GhostDemonstration _demonstration;
|
||||
[SerializeField] private PlaygroundRestoreGeometry _restoreGeometry;
|
||||
public void BindRestoreGeometry(PlaygroundRestoreGeometry geometry) => _restoreGeometry = geometry;
|
||||
public GameSession Session { get; private set; }
|
||||
public BoardPresenter Presenter => _presenter;
|
||||
public BoardInput Input => _input;
|
||||
@@ -84,6 +86,8 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap
|
||||
}
|
||||
public void Initialize(IProgressStore store)
|
||||
{
|
||||
_presenter.BindEnvironment(_environment);
|
||||
_orbit.BindEnvironment(_environment);
|
||||
Session = new GameSession(_stages.Select(stage => stage.ToDefinition()), store);
|
||||
Session.SaveFinished += ShowSaveResult;
|
||||
_saveNotice.text = Session.LastSaveError == null ? "" : Session.LastSaveError.StartsWith("Recovered", StringComparison.Ordinal)
|
||||
@@ -116,8 +120,10 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap
|
||||
public void StartStage(int id)
|
||||
{
|
||||
_input.CancelSplit(); _voice.Clear(); _demonstration?.Hide(); bool restored = Session.HasUnfinishedStage(id);
|
||||
if (!Session.StartStage(id)) return;
|
||||
string restoreNotice = null;
|
||||
if (!Session.StartStage(id, (board, camera) => PrepareRestore(board, camera, out restoreNotice))) { Refresh(true); return; }
|
||||
Refresh(true);
|
||||
if (restoreNotice != null) { _instruction.text = restoreNotice; return; }
|
||||
string tutorial = id == 6 ? "merge" : id == 10 ? "split" : null;
|
||||
if (tutorial != null && !Session.HasSeenTutorial(tutorial) && _demonstration != null)
|
||||
{ Session.MarkTutorialSeen(tutorial); _demonstration.Show(id == 10); _voice.Guide(id == 6 ? "demo_merge6" : "demo_split10"); return; }
|
||||
@@ -126,7 +132,38 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap
|
||||
else if (!restored) _voice.Guide(id == 10 ? "guide_split" : "guide_merge");
|
||||
}
|
||||
public void ContinueFreePlay()
|
||||
{ _input.CancelSplit(); _voice.Clear(); _demonstration?.Hide(); if (Session.ContinueFreePlay()) Refresh(true); }
|
||||
{
|
||||
_input.CancelSplit(); _voice.Clear(); _demonstration?.Hide(); string restoreNotice = null;
|
||||
bool started = Session.ContinueFreePlay((board, camera) => PrepareRestore(board, camera, out restoreNotice));
|
||||
Refresh(true);
|
||||
if (started && restoreNotice != null) _instruction.text = restoreNotice;
|
||||
}
|
||||
private BoardRestore PrepareRestore(BoardRules board, CameraState camera, out string notice)
|
||||
{
|
||||
notice = null;
|
||||
try
|
||||
{
|
||||
// Activate the target scenery before any physics query; a hidden/previous stage cannot validate a restore.
|
||||
_world.SetActive(true);
|
||||
var stage = _stages.First(value => value.StageId == board.StageId);
|
||||
_environment.Apply(stage); Physics.SyncTransforms();
|
||||
var prepared = PlaygroundResume.Prepare(board, Session.Definitions.First(value => value.Id == board.StageId),
|
||||
camera, _presenter, _environment, _restoreGeometry, out notice);
|
||||
if (prepared == null)
|
||||
{
|
||||
_saveNotice.text = "지금은 시작하지 못했어요. 다시 해볼게요.";
|
||||
return null;
|
||||
}
|
||||
_orbit.Apply(prepared.Camera);
|
||||
return new BoardRestore(prepared.Board, _orbit.Snapshot(), prepared.ResetToInitial);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogWarning("[ProgressRestore] " + exception.Message);
|
||||
_saveNotice.text = "지금은 시작하지 못했어요. 다시 해볼게요.";
|
||||
notice = null; return null;
|
||||
}
|
||||
}
|
||||
public void StartNextStage()
|
||||
{ if (Session.HasNextStage) StartStage(Session.Board.StageId + 1); }
|
||||
public void ShowHelp()
|
||||
@@ -181,6 +218,7 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap
|
||||
_selectionPanel.SetActive(screen == SessionScreen.StageSelection);
|
||||
bool playing = screen == SessionScreen.Puzzle || screen == SessionScreen.FreePlay;
|
||||
_playPanel.SetActive(playing); _world.SetActive(playing);
|
||||
if (playing) _environment?.Apply(_stages.First(stage => stage.StageId == Session.Board.StageId));
|
||||
_continueButton.gameObject.SetActive(Session.HasFreePlay);
|
||||
if (_stageTwoButton != null) _stageTwoButton.interactable = Session.IsUnlocked(2);
|
||||
for (int index = 0; index < _stageButtons.Length; index++)
|
||||
@@ -198,7 +236,6 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap
|
||||
if (applyCamera) _orbit.Apply(Session.Camera);
|
||||
_presenter.Render(Session.Board);
|
||||
if (!playing) { _sound?.Clear(); _sound?.Theme("Menu"); return; }
|
||||
_environment?.Apply(_stages.First(stage => stage.StageId == Session.Board.StageId));
|
||||
_sound?.Theme(_stages.First(stage => stage.StageId == Session.Board.StageId).Theme);
|
||||
_stageTitle.text = screen == SessionScreen.FreePlay ? "자유 놀이터" : $"숫자 {Session.Board.BallCount} 친구 찾기";
|
||||
_counter.text = screen == SessionScreen.FreePlay ? $"친구 {Session.Board.BallCount}" : $"센 친구 {Session.Board.CountedCount} / {Session.Board.BallCount}";
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bbbd7c6c248fe414da209364fa007ca1
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MagnetismHeroes.UnityRuntime.Bootstrap
|
||||
{
|
||||
// Read-only derived collision geometry; the approved imported meshes remain non-readable and unchanged.
|
||||
public sealed class PlaygroundRestoreGeometry : ScriptableObject
|
||||
{
|
||||
[Serializable] public sealed class Entry
|
||||
{
|
||||
public Mesh mesh;
|
||||
public Vector3[] vertices;
|
||||
public int[] triangles;
|
||||
public Bounds bounds;
|
||||
public string geometrySha256, sourceSha256, sourceMetaSha256, sourceGuid;
|
||||
}
|
||||
[SerializeField] private Entry[] _entries = Array.Empty<Entry>();
|
||||
public bool TryGet(Mesh mesh, out Vector3[] vertices, out int[] triangles)
|
||||
{
|
||||
vertices = null; triangles = null;
|
||||
var entry = _entries.FirstOrDefault(value => value != null && value.mesh == mesh);
|
||||
if (entry == null || entry.vertices == null || entry.triangles == null || entry.vertices.Length != mesh.vertexCount
|
||||
|| entry.triangles.Length == 0 || entry.triangles.Length % 3 != 0 || entry.bounds != mesh.bounds
|
||||
|| entry.vertices.Any(point => !float.IsFinite(point.x) || !float.IsFinite(point.y) || !float.IsFinite(point.z))
|
||||
|| entry.triangles.Any(index => index < 0 || index >= entry.vertices.Length)
|
||||
|| entry.geometrySha256 != Hash(entry.vertices, entry.triangles)) return false;
|
||||
ulong indices = 0; for (int index = 0; index < mesh.subMeshCount; index++) indices += mesh.GetIndexCount(index);
|
||||
if (indices != (ulong)entry.triangles.Length) return false;
|
||||
vertices = (Vector3[])entry.vertices.Clone(); triangles = (int[])entry.triangles.Clone(); return true;
|
||||
}
|
||||
public static string Hash(Vector3[] vertices, int[] triangles)
|
||||
{
|
||||
using var bytes = new MemoryStream();
|
||||
using (var writer = new BinaryWriter(bytes, System.Text.Encoding.UTF8, true))
|
||||
{
|
||||
writer.Write(vertices.Length);
|
||||
foreach (var point in vertices) { writer.Write(point.x); writer.Write(point.y); writer.Write(point.z); }
|
||||
writer.Write(triangles.Length); foreach (int index in triangles) writer.Write(index);
|
||||
}
|
||||
using var sha = SHA256.Create();
|
||||
return BitConverter.ToString(sha.ComputeHash(bytes.ToArray())).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
#if UNITY_EDITOR
|
||||
public void SetEntries(Entry[] entries) => _entries = entries;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15dba9ff95d33b74db1572556cc79b2b
|
||||
@@ -0,0 +1,418 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MagnetismHeroes.Core;
|
||||
using MagnetismHeroes.UnityRuntime.Presentation;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MagnetismHeroes.UnityRuntime.Bootstrap
|
||||
{
|
||||
public static class PlaygroundResume
|
||||
{
|
||||
private const float Radius = .5f;
|
||||
private const float Margin = Radius - .001f;
|
||||
private const int SearchLimit = 8192;
|
||||
private const float NearestTolerance = .01f;
|
||||
|
||||
public static BoardRestore Prepare(BoardRules board, StageDefinition definition, CameraState camera,
|
||||
BoardPresenter presenter, BoardEnvironment environment, PlaygroundRestoreGeometry geometry, out string notice)
|
||||
{
|
||||
notice = null;
|
||||
Physics.SyncTransforms();
|
||||
var balls = board.Balls.ToDictionary(ball => ball.id, ball => BoardPresenter.ToVector(ball.position));
|
||||
var groups = board.Groups;
|
||||
if (balls.Values.All(point => environment.IsSafeSphere(point, presenter.transform)) && SeparateGroups(groups, balls)) return new BoardRestore(board, camera);
|
||||
if (geometry == null) { Debug.LogWarning("[ProgressRestore] Missing restore geometry; existing record retained."); return null; }
|
||||
var bounds = environment.PlaygroundBounds;
|
||||
var obstacles = Physics.OverlapBox(bounds.center, bounds.extents + Vector3.one * Radius, Quaternion.identity, ~0, QueryTriggerInteraction.Ignore)
|
||||
.Where(collider => collider.enabled && collider.gameObject.activeInHierarchy && !collider.isTrigger
|
||||
&& !collider.transform.IsChildOf(presenter.transform)).ToArray();
|
||||
var meshes = new Dictionary<MeshCollider, RestoreMesh>();
|
||||
foreach (var mesh in obstacles.OfType<MeshCollider>())
|
||||
{
|
||||
if (!geometry.TryGet(mesh.sharedMesh, out var vertices, out var triangles))
|
||||
{ Debug.LogWarning("[ProgressRestore] Invalid restore geometry; existing record retained."); return null; }
|
||||
meshes.Add(mesh, new RestoreMesh(mesh, vertices, triangles));
|
||||
}
|
||||
var domains = new Dictionary<string, Bounds>();
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var points = group.ballIds.Select(id => balls[id]).ToArray();
|
||||
var min = points.Aggregate(Vector3.Min); var max = points.Aggregate(Vector3.Max);
|
||||
var lower = bounds.min + Vector3.one * Margin - min;
|
||||
var upper = bounds.max - Vector3.one * Margin - max;
|
||||
if (lower.x > upper.x || lower.y > upper.y || lower.z > upper.z)
|
||||
return Reset(board, definition, presenter, environment, out notice);
|
||||
var domain = new Bounds((lower + upper) * .5f, upper - lower); domains.Add(group.id, domain);
|
||||
// This proves obstruction for every rigid translation, rather than inferring it from failed samples.
|
||||
if (points.Any(point => obstacles.OfType<BoxCollider>().Any(box => CoversCenterDomain(box, point, domain))))
|
||||
return Reset(board, definition, presenter, environment, out notice);
|
||||
}
|
||||
var translations = new Dictionary<string, Position3>();
|
||||
var occupied = new List<Vector3>();
|
||||
var pending = new List<GroupState>();
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var points = group.ballIds.Select(id => balls[id]).ToArray();
|
||||
if (points.All(point => environment.IsSafeSphere(point, presenter.transform)) && Clear(points, occupied)) occupied.AddRange(points);
|
||||
else pending.Add(group);
|
||||
}
|
||||
var probeObject = new GameObject("RestorePlacementProbe");
|
||||
var probe = probeObject.AddComponent<SphereCollider>(); probe.radius = Radius; probe.isTrigger = true; probeObject.layer = 2;
|
||||
try
|
||||
{
|
||||
foreach (var group in pending.OrderByDescending(group => group.ballIds.Length).ThenBy(group => group.id, StringComparer.Ordinal))
|
||||
{
|
||||
var points = group.ballIds.Select(id => balls[id]).ToArray();
|
||||
if (!TryNearestTranslation(points, domains[group.id], occupied, obstacles, meshes, probe, presenter, environment, out var delta, out bool noPlacement))
|
||||
{
|
||||
if (noPlacement) return Reset(board, definition, presenter, environment, out notice);
|
||||
Debug.LogWarning("[ProgressRestore] Placement search inconclusive; existing record retained.");
|
||||
return null;
|
||||
}
|
||||
translations.Add(group.id, BoardPresenter.ToPosition(delta));
|
||||
occupied.AddRange(points.Select(point => point + delta));
|
||||
}
|
||||
}
|
||||
finally { probeObject.SetActive(false); UnityEngine.Object.Destroy(probeObject); }
|
||||
if (!board.TryTranslateForRestore(translations, out var prepared) || !prepared.Balls.All(ball => environment.IsSafeSphere(BoardPresenter.ToVector(ball.position), presenter.transform))
|
||||
|| !SeparateGroups(prepared.Groups, prepared.Balls.ToDictionary(ball => ball.id, ball => BoardPresenter.ToVector(ball.position)))) return null;
|
||||
notice = "친구를 놀이터로 다시 보냈어요.";
|
||||
return new BoardRestore(prepared, camera);
|
||||
}
|
||||
|
||||
private static BoardRestore Reset(BoardRules board, StageDefinition definition, BoardPresenter presenter, BoardEnvironment environment, out string notice)
|
||||
{
|
||||
notice = null;
|
||||
var initial = new BoardRules(definition);
|
||||
if (board.Mode == BoardMode.FreePlay) initial = BoardRules.CreateFreePlay(initial.ToFreePlayState(GameSession.DefaultCamera()));
|
||||
if (!initial.Balls.All(ball => environment.IsSafeSphere(BoardPresenter.ToVector(ball.position), presenter.transform)))
|
||||
{
|
||||
Debug.LogWarning("[ProgressRestore] Authored initial layout is unsafe; existing record retained.");
|
||||
return null;
|
||||
}
|
||||
notice = "친구가 놀이터에 들어가지 못해서 처음부터 다시 시작해요.";
|
||||
return new BoardRestore(initial, GameSession.DefaultCamera(), true);
|
||||
}
|
||||
|
||||
private static bool CoversCenterDomain(BoxCollider box, Vector3 point, Bounds translations)
|
||||
{
|
||||
var center = box.transform.TransformPoint(box.center); var inverse = Quaternion.Inverse(box.transform.rotation);
|
||||
var scale = box.transform.lossyScale; var half = Vector3.Scale(box.size * .5f, new Vector3(Mathf.Abs(scale.x), Mathf.Abs(scale.y), Mathf.Abs(scale.z)));
|
||||
var min = -half; var max = half;
|
||||
for (int corner = 0; corner < 8; corner++)
|
||||
{
|
||||
var world = point + new Vector3((corner & 1) == 0 ? translations.min.x : translations.max.x,
|
||||
(corner & 2) == 0 ? translations.min.y : translations.max.y,
|
||||
(corner & 4) == 0 ? translations.min.z : translations.max.z);
|
||||
var local = inverse * (world - center);
|
||||
if (local.x < min.x || local.y < min.y || local.z < min.z || local.x > max.x || local.y > max.y || local.z > max.z) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryNearestTranslation(Vector3[] points, Bounds domain, List<Vector3> occupied,
|
||||
Collider[] obstacles, Dictionary<MeshCollider, RestoreMesh> meshes, SphereCollider probe, BoardPresenter presenter, BoardEnvironment environment,
|
||||
out Vector3 result, out bool noPlacement)
|
||||
{
|
||||
result = Vector3.zero;
|
||||
noPlacement = false;
|
||||
var elapsed = System.Diagnostics.Stopwatch.StartNew();
|
||||
var candidates = new List<Vector3>(); var visited = new HashSet<Vector3>();
|
||||
Vector3 Clamp(Vector3 delta) => new Vector3(Mathf.Clamp(delta.x, domain.min.x, domain.max.x),
|
||||
Mathf.Clamp(delta.y, domain.min.y, domain.max.y), Mathf.Clamp(delta.z, domain.min.z, domain.max.z));
|
||||
void Add(Vector3 delta)
|
||||
{
|
||||
delta = Clamp(delta);
|
||||
if (visited.Add(delta)) candidates.Add(delta);
|
||||
}
|
||||
var closestBound = Clamp(Vector3.zero); Add(closestBound);
|
||||
bool Safe(Vector3 translation) => points.All(point => environment.IsSafeSphere(point + translation, presenter.transform))
|
||||
&& Clear(points.Select(point => point + translation), occupied);
|
||||
if (Safe(closestBound)) { result = closestBound; Debug.Log("[ProgressRestore] Exact bounds projection, ms=" + elapsed.ElapsedMilliseconds); return true; }
|
||||
// Boundary faces/corners provide escape candidates even from deep inside a closed triangle mesh.
|
||||
for (int x = -1; x <= 1; x++) for (int y = -1; y <= 1; y++) for (int z = -1; z <= 1; z++)
|
||||
Add(new Vector3(x < 0 ? domain.min.x : x > 0 ? domain.max.x : closestBound.x,
|
||||
y < 0 ? domain.min.y : y > 0 ? domain.max.y : closestBound.y,
|
||||
z < 0 ? domain.min.z : z > 0 ? domain.max.z : closestBound.z));
|
||||
Add(domain.center);
|
||||
var best = Vector3.zero; bool found = false;
|
||||
// Establish a safe upper bound before short, unsafe surface candidates can consume the escape budget.
|
||||
// The domain center also avoids relying only on float-sensitive exact boundary faces.
|
||||
foreach (var candidate in candidates)
|
||||
if (Safe(candidate) && (!found || candidate.sqrMagnitude < best.sqrMagnitude)) { found = true; best = candidate; }
|
||||
// Escape candidates seed an upper bound. Their lengths never prune another direction or imply impossibility.
|
||||
for (int examined = 0; candidates.Count > 0 && examined < 128; examined++)
|
||||
{
|
||||
int index = 0;
|
||||
for (int i = 1; i < candidates.Count; i++) if (candidates[i].sqrMagnitude < candidates[index].sqrMagnitude) index = i;
|
||||
var delta = candidates[index]; candidates.RemoveAt(index);
|
||||
var translated = points.Select(point => point + delta).ToArray();
|
||||
if (Safe(delta))
|
||||
{
|
||||
if (!found || delta.sqrMagnitude < best.sqrMagnitude) { found = true; best = delta; }
|
||||
continue;
|
||||
}
|
||||
foreach (var point in translated)
|
||||
{
|
||||
foreach (var other in occupied)
|
||||
{
|
||||
var away = point - other; float distance = away.magnitude;
|
||||
if (distance >= .999f) continue;
|
||||
if (distance > .000001f) Add(delta + away / distance * (1 - distance));
|
||||
else foreach (var axis in new[] { Vector3.right, Vector3.up, Vector3.forward }) { Add(delta + axis); Add(delta - axis); }
|
||||
}
|
||||
if (environment.IsSafeSphere(point, presenter.transform)) continue;
|
||||
foreach (var obstacle in obstacles)
|
||||
{
|
||||
if (obstacle.bounds.SqrDistance(point) > Radius * Radius) continue;
|
||||
if (Clearance(obstacle, point, meshes, out _, out var surfaceEscape)) Add(delta + surfaceEscape);
|
||||
if (Physics.ComputePenetration(probe, point, Quaternion.identity, obstacle, obstacle.transform.position,
|
||||
obstacle.transform.rotation, out var direction, out float depth)) Add(delta + direction * Mathf.Max(0, depth - .0009f));
|
||||
var min = obstacle.bounds.min - Vector3.one * Radius;
|
||||
var max = obstacle.bounds.max + Vector3.one * Radius;
|
||||
for (int axis = 0; axis < 3; axis++)
|
||||
{
|
||||
var escape = delta; escape[axis] += min[axis] - point[axis]; Add(escape);
|
||||
escape = delta; escape[axis] += max[axis] - point[axis]; Add(escape);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// A cell's closest point to zero is an exact lower bound on every translation it contains.
|
||||
// Signed distance is 1-Lipschitz: penetration deeper than its circumradius excludes the entire cell.
|
||||
var cells = new List<(Bounds bounds, float lower)>();
|
||||
var penetrationByTranslation = new Dictionary<Vector3, float>();
|
||||
float PenetrationAt(Vector3 translation)
|
||||
{
|
||||
if (!penetrationByTranslation.TryGetValue(translation, out float value))
|
||||
{ value = Penetration(points, translation, occupied, obstacles, meshes); penetrationByTranslation.Add(translation, value); }
|
||||
return value;
|
||||
}
|
||||
void AddCell(Bounds cell)
|
||||
{
|
||||
var centerPenetration = PenetrationAt(cell.center);
|
||||
if (centerPenetration > cell.extents.magnitude + .000001f) return;
|
||||
var closest = cell.ClosestPoint(Vector3.zero);
|
||||
float penetration = PenetrationAt(closest);
|
||||
float lower = Mathf.Sqrt(closest.sqrMagnitude + penetration * penetration);
|
||||
if (found && lower >= best.magnitude - NearestTolerance) return;
|
||||
cells.Add((cell, lower));
|
||||
}
|
||||
AddCell(domain);
|
||||
int examinedCells = 0;
|
||||
for (; cells.Count > 0 && examinedCells < SearchLimit; examinedCells++)
|
||||
{
|
||||
int index = 0;
|
||||
for (int i = 1; i < cells.Count; i++) if (cells[i].lower < cells[index].lower) index = i;
|
||||
var cell = cells[index]; cells.RemoveAt(index);
|
||||
if (found && cell.lower >= best.magnitude - NearestTolerance) continue;
|
||||
// A cell projection may remain on an unsafe surface while its interior already contains safe placements.
|
||||
// Interior samples only improve the safe upper bound; all exclusion/lower-bound certificates remain unchanged.
|
||||
var center = cell.bounds.center;
|
||||
if ((!found || center.sqrMagnitude < best.sqrMagnitude) && Safe(center)) { found = true; best = center; }
|
||||
var closest = cell.bounds.ClosestPoint(Vector3.zero);
|
||||
if (Safe(closest))
|
||||
{
|
||||
if (!found || closest.sqrMagnitude < best.sqrMagnitude) { found = true; best = closest; }
|
||||
continue;
|
||||
}
|
||||
var size = cell.bounds.size; int axis = size.x >= size.y && size.x >= size.z ? 0 : size.y >= size.z ? 1 : 2;
|
||||
// Float exhaustion is an inconclusive query, never evidence of missing space.
|
||||
if (size[axis] < .0000001f) return false;
|
||||
size[axis] *= .5f; var offset = Vector3.zero; offset[axis] = size[axis] * .5f;
|
||||
AddCell(new Bounds(cell.bounds.center - offset, size)); AddCell(new Bounds(cell.bounds.center + offset, size));
|
||||
}
|
||||
result = best;
|
||||
bool certified = found && !cells.Any(cell => cell.lower < best.magnitude - NearestTolerance);
|
||||
// With no fixed friends, exhausting certified unsafe cells proves terrain-only impossibility.
|
||||
// A limit/error or exclusions caused by other friends cannot establish it.
|
||||
noPlacement = !found && cells.Count == 0 && occupied.Count == 0;
|
||||
Debug.Log($"[ProgressRestore] Nearest certificate={certified}, tolerance={NearestTolerance:R}, cells={examinedCells}, remaining={cells.Count}, distance={best.magnitude:R}, ms={elapsed.ElapsedMilliseconds}");
|
||||
return certified;
|
||||
}
|
||||
|
||||
private static float Penetration(Vector3[] points, Vector3 translation, List<Vector3> occupied,
|
||||
Collider[] obstacles, Dictionary<MeshCollider, RestoreMesh> meshes)
|
||||
{
|
||||
float penetration = 0;
|
||||
foreach (var source in points)
|
||||
{
|
||||
var point = source + translation;
|
||||
foreach (var other in occupied) penetration = Mathf.Max(penetration, .999f - Vector3.Distance(point, other));
|
||||
foreach (var obstacle in obstacles)
|
||||
if (obstacle.bounds.SqrDistance(point) <= Radius * Radius && Clearance(obstacle, point, meshes, out float clearance, out _))
|
||||
penetration = Mathf.Max(penetration, -clearance);
|
||||
}
|
||||
return penetration;
|
||||
}
|
||||
|
||||
private static bool Clearance(Collider collider, Vector3 point, Dictionary<MeshCollider, RestoreMesh> meshes,
|
||||
out float clearance, out Vector3 escape)
|
||||
{
|
||||
clearance = 0; escape = Vector3.zero;
|
||||
if (collider is BoxCollider box)
|
||||
{
|
||||
var center = box.transform.TransformPoint(box.center); var rotation = box.transform.rotation;
|
||||
var local = Quaternion.Inverse(rotation) * (point - center); var scale = box.transform.lossyScale;
|
||||
var half = Vector3.Scale(box.size * .5f, new Vector3(Mathf.Abs(scale.x), Mathf.Abs(scale.y), Mathf.Abs(scale.z)));
|
||||
var min = -half; var max = half;
|
||||
var nearest = new Vector3(Mathf.Clamp(local.x, min.x, max.x), Mathf.Clamp(local.y, min.y, max.y), Mathf.Clamp(local.z, min.z, max.z));
|
||||
var surface = center + rotation * nearest; var away = point - surface;
|
||||
if (away.sqrMagnitude > .000000001f) { clearance = away.magnitude - Margin; escape = away.normalized * Mathf.Max(0, -clearance); return true; }
|
||||
float distance = float.PositiveInfinity; Vector3 direction = Vector3.zero;
|
||||
for (int axis = 0; axis < 3; axis++) foreach (float edge in new[] { min[axis], max[axis] })
|
||||
{
|
||||
nearest = local; nearest[axis] = edge; away = center + rotation * nearest - point;
|
||||
if (away.magnitude < distance) { distance = away.magnitude; direction = away.normalized; }
|
||||
}
|
||||
clearance = -distance - Margin; escape = direction * -clearance;
|
||||
return true;
|
||||
}
|
||||
if (collider is SphereCollider sphere)
|
||||
{
|
||||
var scale = sphere.transform.lossyScale; var away = point - sphere.transform.TransformPoint(sphere.center);
|
||||
float radius = sphere.radius * Mathf.Max(Mathf.Abs(scale.x), Mathf.Abs(scale.y), Mathf.Abs(scale.z));
|
||||
clearance = away.magnitude - radius - Margin;
|
||||
escape = (away.sqrMagnitude > .000000001f ? away.normalized : Vector3.right) * Mathf.Max(0, -clearance);
|
||||
return true;
|
||||
}
|
||||
if (collider is MeshCollider mesh && mesh.sharedMesh != null)
|
||||
{
|
||||
if (!meshes.TryGetValue(mesh, out var geometry)) return false;
|
||||
if (!geometry.IsClosed) return false;
|
||||
geometry.Distance(point, out var nearest, out float distance, out bool inside);
|
||||
clearance = (inside ? -distance : distance) - Margin;
|
||||
var direction = inside ? nearest - point : point - nearest;
|
||||
escape = direction.normalized * Mathf.Max(0, -clearance);
|
||||
return true;
|
||||
}
|
||||
// Unsupported geometry supplies no exclusion certificate; actual safety still evaluates it.
|
||||
return false;
|
||||
}
|
||||
|
||||
private sealed class RestoreMesh
|
||||
{
|
||||
private readonly Vector3[] _vertices;
|
||||
private readonly int[] _triangles;
|
||||
private readonly float[] _magnitudes;
|
||||
private readonly int[] _triangleComponents;
|
||||
private readonly Vector3[] _componentMin, _componentMax;
|
||||
private readonly bool[] _windingComponents;
|
||||
private readonly bool _closed;
|
||||
public bool IsClosed => _closed;
|
||||
public RestoreMesh(MeshCollider collider, Vector3[] vertices, int[] triangles)
|
||||
{
|
||||
_triangles = triangles;
|
||||
_vertices = vertices.Select(collider.transform.TransformPoint).ToArray();
|
||||
_magnitudes = new float[_vertices.Length];
|
||||
var edges = new Dictionary<(Vector3 first, Vector3 second), (int count, int orientation)>();
|
||||
for (int index = 0; index < _triangles.Length; index += 3)
|
||||
for (int edge = 0; edge < 3; edge++)
|
||||
{
|
||||
var first = vertices[_triangles[index + edge]]; var second = vertices[_triangles[index + (edge + 1) % 3]];
|
||||
bool forward = Compare(first, second) < 0; var key = forward ? (first, second) : (second, first);
|
||||
edges.TryGetValue(key, out var value); edges[key] = (value.count + 1, value.orientation + (forward ? 1 : -1));
|
||||
}
|
||||
_closed = edges.Count > 0 && edges.Values.All(value => value.count == 2 && value.orientation == 0);
|
||||
if (!_closed)
|
||||
{
|
||||
_triangleComponents = Array.Empty<int>(); _componentMin = _componentMax = Array.Empty<Vector3>();
|
||||
_windingComponents = Array.Empty<bool>(); return;
|
||||
}
|
||||
// Every welded component inherits the checked closed, oriented edges. Outside its bounds its winding is zero.
|
||||
var parents = new int[vertices.Length]; var welded = new Dictionary<Vector3, int>();
|
||||
for (int index = 0; index < vertices.Length; index++)
|
||||
{
|
||||
if (!welded.TryGetValue(vertices[index], out int first)) { first = index; welded.Add(vertices[index], first); }
|
||||
parents[index] = first;
|
||||
}
|
||||
int Root(int index)
|
||||
{
|
||||
while (parents[index] != index) { parents[index] = parents[parents[index]]; index = parents[index]; }
|
||||
return index;
|
||||
}
|
||||
for (int index = 0; index < triangles.Length; index += 3)
|
||||
{ int first = Root(triangles[index]), second = Root(triangles[index + 1]), third = Root(triangles[index + 2]); parents[second] = first; parents[third] = first; }
|
||||
_triangleComponents = new int[triangles.Length / 3];
|
||||
var components = new Dictionary<int, int>(); var minimum = new List<Vector3>(); var maximum = new List<Vector3>();
|
||||
for (int index = 0; index < triangles.Length; index += 3)
|
||||
{
|
||||
int root = Root(triangles[index]);
|
||||
if (!components.TryGetValue(root, out int component))
|
||||
{ component = components.Count; components.Add(root, component); minimum.Add(_vertices[triangles[index]]); maximum.Add(_vertices[triangles[index]]); }
|
||||
_triangleComponents[index / 3] = component;
|
||||
for (int vertex = 0; vertex < 3; vertex++)
|
||||
{ var point = _vertices[triangles[index + vertex]]; minimum[component] = Vector3.Min(minimum[component], point); maximum[component] = Vector3.Max(maximum[component], point); }
|
||||
}
|
||||
_componentMin = minimum.ToArray(); _componentMax = maximum.ToArray(); _windingComponents = new bool[components.Count];
|
||||
}
|
||||
private static int Compare(Vector3 first, Vector3 second)
|
||||
{ int value = first.x.CompareTo(second.x); if (value != 0) return value; value = first.y.CompareTo(second.y); return value != 0 ? value : first.z.CompareTo(second.z); }
|
||||
public void Distance(Vector3 point, out Vector3 nearest, out float distance, out bool inside)
|
||||
{
|
||||
nearest = Vector3.zero; float squared = float.PositiveInfinity; double winding = 0;
|
||||
bool canCull = _closed, skipped = false;
|
||||
// Each query keeps the original float magnitude and triangle summation order, reusing shared vertices.
|
||||
if (_closed) for (int index = 0; index < _vertices.Length; index++)
|
||||
{ _magnitudes[index] = (_vertices[index] - point).magnitude; canCull &= float.IsFinite(_magnitudes[index]) && _magnitudes[index] > 0; }
|
||||
for (int index = 0; index < _componentMin.Length; index++)
|
||||
{
|
||||
// Winding is zero outside a closed component, independently of the sphere clearance radius.
|
||||
var min = _componentMin[index] - Vector3.one * .0001f; var max = _componentMax[index] + Vector3.one * .0001f;
|
||||
_windingComponents[index] = point.x >= min.x && point.y >= min.y && point.z >= min.z && point.x <= max.x && point.y <= max.y && point.z <= max.z;
|
||||
}
|
||||
for (int index = 0; index < _triangles.Length; index += 3)
|
||||
{
|
||||
var a = _vertices[_triangles[index]]; var b = _vertices[_triangles[index + 1]]; var c = _vertices[_triangles[index + 2]];
|
||||
var candidate = ClosestTriangle(point, a, b, c); float candidateSquared = (candidate - point).sqrMagnitude;
|
||||
if (candidateSquared < squared) { squared = candidateSquared; nearest = candidate; }
|
||||
if (!_closed) continue;
|
||||
if (!canCull || _windingComponents[_triangleComponents[index / 3]]) winding += SolidAngle(index, point);
|
||||
else skipped = true;
|
||||
}
|
||||
distance = Mathf.Sqrt(squared);
|
||||
// Surface singularities and the numerical inside threshold retain the original full summation.
|
||||
if (skipped && (distance <= .0001f || Math.Abs(Math.Abs(winding) - Math.PI * 2) <= .001))
|
||||
{ winding = 0; for (int index = 0; index < _triangles.Length; index += 3) winding += SolidAngle(index, point); }
|
||||
inside = _closed && Math.Abs(winding) > Math.PI * 2;
|
||||
}
|
||||
private double SolidAngle(int index, Vector3 point)
|
||||
{
|
||||
var a = _vertices[_triangles[index]] - point; var b = _vertices[_triangles[index + 1]] - point; var c = _vertices[_triangles[index + 2]] - point;
|
||||
double numerator = Vector3.Dot(a, Vector3.Cross(b, c));
|
||||
float aMagnitude = _magnitudes[_triangles[index]], bMagnitude = _magnitudes[_triangles[index + 1]], cMagnitude = _magnitudes[_triangles[index + 2]];
|
||||
double denominator = (double)aMagnitude * bMagnitude * cMagnitude + (double)Vector3.Dot(a, b) * cMagnitude
|
||||
+ (double)Vector3.Dot(b, c) * aMagnitude + (double)Vector3.Dot(c, a) * bMagnitude;
|
||||
return 2 * Math.Atan2(numerator, denominator);
|
||||
}
|
||||
private static Vector3 ClosestTriangle(Vector3 point, Vector3 a, Vector3 b, Vector3 c)
|
||||
{
|
||||
var ab = b - a; var ac = c - a; var ap = point - a; float d1 = Vector3.Dot(ab, ap), d2 = Vector3.Dot(ac, ap);
|
||||
if (d1 <= 0 && d2 <= 0) return a;
|
||||
var bp = point - b; float d3 = Vector3.Dot(ab, bp), d4 = Vector3.Dot(ac, bp);
|
||||
if (d3 >= 0 && d4 <= d3) return b;
|
||||
float vc = d1 * d4 - d3 * d2; if (vc <= 0 && d1 >= 0 && d3 <= 0) return a + ab * (d1 / (d1 - d3));
|
||||
var cp = point - c; float d5 = Vector3.Dot(ab, cp), d6 = Vector3.Dot(ac, cp);
|
||||
if (d6 >= 0 && d5 <= d6) return c;
|
||||
float vb = d5 * d2 - d1 * d6; if (vb <= 0 && d2 >= 0 && d6 <= 0) return a + ac * (d2 / (d2 - d6));
|
||||
float va = d3 * d6 - d5 * d4; if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) return b + (c - b) * ((d4 - d3) / (d4 - d3 + d5 - d6));
|
||||
float inverse = 1 / (va + vb + vc); return a + ab * (vb * inverse) + ac * (vc * inverse);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Clear(IEnumerable<Vector3> positions, IEnumerable<Vector3> occupied)
|
||||
=> positions.All(point => occupied.All(other => (point - other).sqrMagnitude >= .999f * .999f));
|
||||
private static bool SeparateGroups(GroupState[] groups, Dictionary<string, Vector3> balls)
|
||||
{
|
||||
var occupied = new List<Vector3>();
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var points = group.ballIds.Select(id => balls[id]).ToArray();
|
||||
if (!Clear(points, occupied)) return false;
|
||||
occupied.AddRange(points);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32953c8bdbfc3f542967d944ce912740
|
||||
@@ -28,19 +28,20 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
|
||||
public int SelectedCount => _selected.Count;
|
||||
public string SelectedGroup => _selectedGroup;
|
||||
public IReadOnlyCollection<string> SelectedIds => _selected;
|
||||
private Plane _dragPlane;
|
||||
private Vector3 _grabPoint, _previewDelta;
|
||||
private Vector3 _grabOffset, _previewDelta;
|
||||
private float _pinchDistance;
|
||||
private int _fingerId = -1;
|
||||
private bool _waitForAllTouchesReleased;
|
||||
private int _pinchFirstId = -1, _pinchSecondId = -1;
|
||||
private Touchscreen _touchscreen;
|
||||
private readonly struct TouchContact
|
||||
{
|
||||
public readonly Touchscreen Screen;
|
||||
public readonly int Id;
|
||||
public readonly UnityEngine.InputSystem.TouchPhase Phase;
|
||||
public readonly Vector2 Position;
|
||||
public TouchContact(int id, UnityEngine.InputSystem.TouchPhase phase, Vector2 position)
|
||||
{ Id = id; Phase = phase; Position = position; }
|
||||
public TouchContact(Touchscreen screen, int id, UnityEngine.InputSystem.TouchPhase phase, Vector2 position)
|
||||
{ Screen = screen; Id = id; Phase = phase; Position = position; }
|
||||
public bool InProgress => Phase == UnityEngine.InputSystem.TouchPhase.Began || Phase == UnityEngine.InputSystem.TouchPhase.Moved || Phase == UnityEngine.InputSystem.TouchPhase.Stationary;
|
||||
}
|
||||
private readonly Queue<TouchContact> _touchEvents = new Queue<TouchContact>();
|
||||
@@ -72,8 +73,8 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
|
||||
_app.Sound?.Pickup();
|
||||
if (_splitting && _selectedGroup != _groupId) { _selected.Clear(); _selectedGroup = _groupId; _app.RefreshSelection(); }
|
||||
_splitDrag = _splitting && _selected.Contains(_ballId);
|
||||
_dragPlane = new Plane(_orbit.Camera.transform.forward, hit.point);
|
||||
_grabPoint = hit.point;
|
||||
_grabOffset = hit.point - ball.transform.position;
|
||||
_orbit.SuspendReturn();
|
||||
}
|
||||
else _gesture = Gesture.Orbit;
|
||||
}
|
||||
@@ -81,16 +82,30 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
|
||||
public void Move(Vector2 point)
|
||||
{
|
||||
if (_gesture == Gesture.None || _gesture == Gesture.Blocked || _gesture == Gesture.Pinch) return;
|
||||
if (!IsInside(point)) { if (_splitting) CancelSplit(); else Cancel(); return; }
|
||||
bool wasDragged = _dragged;
|
||||
_dragged |= (point - _start).sqrMagnitude >= _dragThreshold * _dragThreshold;
|
||||
if (_dragged && _gesture == Gesture.Friend)
|
||||
if (_dragged && _gesture == Gesture.Friend && (!_splitting || _splitDrag))
|
||||
{
|
||||
var ray = _orbit.Camera.ScreenPointToRay(point);
|
||||
if (_dragPlane.Raycast(ray, out var distance))
|
||||
if (!wasDragged)
|
||||
{
|
||||
_previewDelta = ray.GetPoint(distance) - _grabPoint;
|
||||
if (_splitting) { if (_splitDrag) _presenter.PreviewSplit(_groupId, _selected.ToArray(), _previewDelta); }
|
||||
else _presenter.PreviewGroup(_groupId, _previewDelta);
|
||||
var members = _splitting ? _selected.ToArray() : _app.Session.Board.Groups.First(group => group.id == _groupId).ballIds;
|
||||
_orbit.BeginDrag(_presenter, _ballId, members, _grabOffset, point);
|
||||
}
|
||||
// Rebase both pointer endpoints in the CURRENT camera's plane. Camera motion alone has zero delta.
|
||||
var anchor = _presenter.Balls[_ballId].transform.position + _grabOffset;
|
||||
var plane = new Plane(_orbit.Camera.transform.forward, anchor);
|
||||
var previousPoint = wasDragged ? _last : _start;
|
||||
var previousRay = _orbit.Camera.ScreenPointToRay(previousPoint);
|
||||
var ray = _orbit.Camera.ScreenPointToRay(point);
|
||||
if (point != previousPoint && plane.Raycast(previousRay, out var previousDistance) && plane.Raycast(ray, out var distance))
|
||||
{
|
||||
var step = ray.GetPoint(distance) - previousRay.GetPoint(previousDistance);
|
||||
if (_splitting) _presenter.PreviewSplit(_groupId, _selected.ToArray(), _previewDelta + step);
|
||||
else _presenter.PreviewGroup(_groupId, _presenter.DropDelta + step);
|
||||
_previewDelta = _presenter.DropDelta;
|
||||
}
|
||||
_orbit.DragPointer(point);
|
||||
}
|
||||
else if (_dragged && _gesture == Gesture.Orbit) _orbit.Orbit(point - _last);
|
||||
_last = point;
|
||||
@@ -103,10 +118,15 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
|
||||
{
|
||||
if (_dragged)
|
||||
{
|
||||
if (!_presenter.CanDropGroup(_groupId)) { if (_splitting) CancelSplit(); else Cancel(); return; }
|
||||
if (!_presenter.CanDropGroup(_groupId))
|
||||
{
|
||||
if (_splitting && _selected.Count == _app.Session.Board.Groups.First(group => group.id == _groupId).ballIds.Length) Cancel();
|
||||
else if (_splitting) CancelSplit(); else Cancel();
|
||||
return;
|
||||
}
|
||||
if (_splitting)
|
||||
{
|
||||
if (_splitDrag && _app.CommitSplit(_groupId, _selected.ToArray(), BoardPresenter.ToPosition(_previewDelta)))
|
||||
if (_splitDrag && _app.CommitSplit(_groupId, _selected.ToArray(), BoardPresenter.ToPosition(_presenter.DropDelta)))
|
||||
{ _splitting = false; _selected.Clear(); _selectedGroup = null; }
|
||||
else _presenter.CancelPreview();
|
||||
_app.RefreshSelection();
|
||||
@@ -122,13 +142,15 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
|
||||
else { _selectedGroup = _groupId; _app.CountBall(_ballId); _app.RefreshSelection(); }
|
||||
}
|
||||
else if (_gesture == Gesture.Orbit && _dragged) _app.Session.CommitCamera(_orbit.Snapshot());
|
||||
else if (_gesture == Gesture.Pinch) _app.Session.CommitCamera(_orbit.Snapshot());
|
||||
else if (_gesture == Gesture.Pinch) { _orbit.EndManualControl(); _app.Session.CommitCamera(_orbit.Snapshot()); }
|
||||
_orbit.EndDrag();
|
||||
_gesture = Gesture.None; _dragged = false; _previewDelta = Vector3.zero; _fingerId = -1;
|
||||
}
|
||||
|
||||
public void BeginPinch(float distance, bool uiBlocked)
|
||||
{
|
||||
if (_splitting) CancelSplit(); else Cancel();
|
||||
if (!uiBlocked && AcceptInput) _orbit.TakeManualControl();
|
||||
_gesture = uiBlocked || !AcceptInput ? Gesture.Blocked : Gesture.Pinch;
|
||||
_pinchDistance = distance;
|
||||
}
|
||||
@@ -140,6 +162,7 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
|
||||
public void Cancel()
|
||||
{
|
||||
if (_gesture == Gesture.Friend) _presenter.CancelPreview();
|
||||
_orbit.EndDrag();
|
||||
if ((_gesture == Gesture.Orbit || _gesture == Gesture.Pinch) && _app.Session != null) _orbit.Apply(_app.Session.Camera);
|
||||
_gesture = Gesture.None; _dragged = false; _previewDelta = Vector3.zero; _fingerId = -1;
|
||||
_waitForAllTouchesReleased = false; _pinchFirstId = _pinchSecondId = -1;
|
||||
@@ -165,10 +188,11 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
|
||||
|
||||
private void RecordTouch(Finger finger)
|
||||
{
|
||||
if (!AcceptInput || finger.screen != Touchscreen.current) return;
|
||||
if (!AcceptInput) return;
|
||||
// Copy each official history callback before later events overwrite the native touch slot.
|
||||
// Input System makes this device current AFTER its EnhancedTouch callbacks.
|
||||
var touch = finger.lastTouch;
|
||||
_touchEvents.Enqueue(new TouchContact(touch.touchId, touch.phase, touch.screenPosition));
|
||||
_touchEvents.Enqueue(new TouchContact(finger.screen, touch.touchId, touch.phase, touch.screenPosition));
|
||||
}
|
||||
|
||||
private void ProcessTouches()
|
||||
@@ -231,11 +255,19 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!AcceptInput) { _touchEvents.Clear(); _touches.Clear(); return; }
|
||||
if (!AcceptInput) { _touchEvents.Clear(); _touches.Clear(); _touchscreen = null; return; }
|
||||
bool hadTouches = _touchEvents.Count > 0, suppressRestOfFrame = false;
|
||||
while (_touchEvents.Count > 0)
|
||||
{
|
||||
var touch = _touchEvents.Dequeue(); _touches[touch.Id] = touch;
|
||||
var touch = _touchEvents.Dequeue();
|
||||
if (_touchscreen == null)
|
||||
{
|
||||
if (touch.Phase != UnityEngine.InputSystem.TouchPhase.Began) continue;
|
||||
_touchscreen = touch.Screen;
|
||||
}
|
||||
// Touch IDs are local to a device; another device cannot replace a held finger.
|
||||
if (touch.Screen != _touchscreen) continue;
|
||||
_touches[touch.Id] = touch;
|
||||
if (!suppressRestOfFrame)
|
||||
{
|
||||
bool ownsGesture = touch.Id == _fingerId || (_gesture == Gesture.Pinch && (touch.Id == _pinchFirstId || touch.Id == _pinchSecondId));
|
||||
@@ -244,6 +276,7 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
|
||||
ProcessTouches(); suppressRestOfFrame = ownsGesture && canceled;
|
||||
}
|
||||
if (!touch.InProgress) _touches.Remove(touch.Id);
|
||||
if (_touches.Count == 0) _touchscreen = null;
|
||||
}
|
||||
if (suppressRestOfFrame) _waitForAllTouchesReleased = _touches.Count > 0;
|
||||
if (hadTouches || _touches.Count > 0) return;
|
||||
@@ -254,7 +287,7 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
|
||||
if (mouse.leftButton.isPressed) Move(position);
|
||||
if (mouse.leftButton.wasReleasedThisFrame) End(position);
|
||||
if (mouse.scroll.ReadValue().y != 0 && !IsUI(position))
|
||||
{ _orbit.Zoom(Mathf.Exp(mouse.scroll.ReadValue().y * 0.001f)); _app.Session.CommitCamera(_orbit.Snapshot()); }
|
||||
{ if (_splitting) CancelSplit(); else Cancel(); _orbit.Zoom(Mathf.Exp(mouse.scroll.ReadValue().y * 0.001f)); _app.Session.CommitCamera(_orbit.Snapshot()); }
|
||||
}
|
||||
private void OnEnable()
|
||||
{
|
||||
@@ -263,13 +296,13 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
|
||||
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerMove += RecordTouch;
|
||||
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerUp += RecordTouch;
|
||||
}
|
||||
private void OnApplicationPause(bool paused) { if (paused) { Cancel(); _touchEvents.Clear(); _touches.Clear(); } }
|
||||
private void OnApplicationPause(bool paused) { if (paused) { Cancel(); _touchEvents.Clear(); _touches.Clear(); _touchscreen = null; } }
|
||||
private void OnDisable()
|
||||
{
|
||||
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerDown -= RecordTouch;
|
||||
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerMove -= RecordTouch;
|
||||
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerUp -= RecordTouch;
|
||||
EnhancedTouchSupport.Disable(); Cancel(); _touchEvents.Clear(); _touches.Clear();
|
||||
EnhancedTouchSupport.Disable(); Cancel(); _touchEvents.Clear(); _touches.Clear(); _touchscreen = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MagnetismHeroes.Core;
|
||||
using MagnetismHeroes.UnityRuntime.Presentation;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MagnetismHeroes.UnityRuntime.Interaction
|
||||
@@ -9,28 +12,268 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
|
||||
[SerializeField] private float _minDistance = 3;
|
||||
[SerializeField] private float _maxDistance = 15;
|
||||
[SerializeField] private float _degreesPerPixel = 0.25f;
|
||||
[SerializeField] private float _minElevation = -89;
|
||||
[SerializeField] private float _maxElevation = 89;
|
||||
[SerializeField] private float _collisionMargin = .35f;
|
||||
[SerializeField] private float _assistSpeed = 6;
|
||||
[SerializeField] private float _assistAngle = 90;
|
||||
[SerializeField] private float _assistElevation = 30;
|
||||
[SerializeField] private float _nearbyDistance = 5;
|
||||
[SerializeField] private float _candidateHysteresis = .4f;
|
||||
[SerializeField] private float _screenMargin = .12f;
|
||||
private Vector3 _target;
|
||||
private Quaternion _rotation;
|
||||
private float _distance;
|
||||
private BoardEnvironment _environment;
|
||||
private BoardPresenter _dragPresenter;
|
||||
private string _grabBall, _candidate;
|
||||
private string[] _movingIds;
|
||||
private Vector3 _grabOffset;
|
||||
private Vector2 _pointer;
|
||||
private bool _returning, _heldReturn, _manualPending;
|
||||
private float _assistYaw;
|
||||
private readonly List<Pose> _assistPath = new List<Pose>();
|
||||
public Camera Camera => _camera;
|
||||
public string TrackedFriend => _candidate;
|
||||
public bool IsAssisting => _dragPresenter != null;
|
||||
public bool IsReturning => _returning;
|
||||
|
||||
public void BindEnvironment(BoardEnvironment environment) => _environment = environment;
|
||||
public void Apply(CameraState state)
|
||||
{
|
||||
_dragPresenter = null; _candidate = null; _returning = false; _heldReturn = false; _manualPending = false;
|
||||
_assistPath.Clear();
|
||||
_target = new Vector3(state.target.x, state.target.y, state.target.z);
|
||||
_rotation = new Quaternion(state.rotation.x, state.rotation.y, state.rotation.z, state.rotation.w);
|
||||
_distance = Mathf.Clamp(state.distance, _minDistance, _maxDistance); UpdateCamera();
|
||||
}
|
||||
public void Orbit(Vector2 delta)
|
||||
{
|
||||
if (delta == Vector2.zero) return;
|
||||
TakeManualControl(); AdoptVisiblePose();
|
||||
_rotation = Quaternion.AngleAxis(delta.x * _degreesPerPixel, Vector3.up) * _rotation
|
||||
* Quaternion.AngleAxis(-delta.y * _degreesPerPixel, Vector3.right);
|
||||
UpdateCamera();
|
||||
}
|
||||
public void Zoom(float ratio)
|
||||
{ if (ratio > 0 && float.IsFinite(ratio)) { _distance = Mathf.Clamp(_distance / ratio, _minDistance, _maxDistance); UpdateCamera(); } }
|
||||
{
|
||||
if (ratio > 0 && float.IsFinite(ratio) && ratio != 1)
|
||||
{ TakeManualControl(); AdoptVisiblePose(); _distance = Mathf.Clamp(_distance / ratio, _minDistance, _maxDistance); UpdateCamera(); }
|
||||
}
|
||||
// Only the confirmed manual pose is eligible for persistence, including during assistance/return.
|
||||
public CameraState Snapshot() => new CameraState { target = new Position3(_target.x, _target.y, _target.z),
|
||||
rotation = new Rotation4(_rotation.x, _rotation.y, _rotation.z, _rotation.w), distance = _distance };
|
||||
|
||||
public void BeginDrag(BoardPresenter presenter, string ballId, IReadOnlyCollection<string> movingIds, Vector3 grabOffset, Vector2 pointer)
|
||||
{
|
||||
// Interrupt a return at the visible pose. Begin never snaps before the first-hit grab.
|
||||
if (!_returning && !_heldReturn) _assistPath.Clear();
|
||||
if (_assistPath.Count == 0) _assistPath.Add(new Pose(_camera.transform.position, _camera.transform.rotation));
|
||||
_returning = false; _heldReturn = false; _dragPresenter = presenter; _grabBall = ballId;
|
||||
_movingIds = movingIds.ToArray(); _grabOffset = grabOffset; _pointer = pointer; _candidate = null;
|
||||
}
|
||||
public void DragPointer(Vector2 pointer) => _pointer = pointer;
|
||||
public void SuspendReturn() { _heldReturn = _returning; _returning = false; }
|
||||
public void EndDrag()
|
||||
{
|
||||
if (_dragPresenter == null && !_heldReturn) return;
|
||||
_heldReturn = false;
|
||||
_dragPresenter = null; _candidate = null; _returning = true;
|
||||
}
|
||||
public void TakeManualControl()
|
||||
{
|
||||
// Pinch/wheel/empty-space orbit takes ownership at the visible pose without persisting an auto pose.
|
||||
_manualPending |= _dragPresenter != null || _returning || _heldReturn;
|
||||
_dragPresenter = null; _candidate = null; _returning = false; _heldReturn = false; _assistPath.Clear();
|
||||
}
|
||||
private void AdoptVisiblePose()
|
||||
{
|
||||
if (!_manualPending) return;
|
||||
_rotation = _camera.transform.rotation;
|
||||
_target = _camera.transform.position + _rotation * Vector3.forward * _distance - Vector3.up * .65f;
|
||||
_manualPending = false;
|
||||
}
|
||||
public void EndManualControl()
|
||||
{
|
||||
if (!_manualPending) return;
|
||||
_manualPending = false; _returning = true;
|
||||
}
|
||||
private Vector3 Pivot => _target + Vector3.up * .65f;
|
||||
private float Ground => _environment != null && _environment.PlaygroundBounds.size != Vector3.zero ? _environment.PlaygroundBounds.min.y : -.45f;
|
||||
private Vector3 Position(Quaternion rotation, float distance) => Pivot - rotation * Vector3.forward * distance;
|
||||
private Quaternion SafeRotation(Quaternion rotation, float distance)
|
||||
{
|
||||
float elevation = Mathf.Asin(Mathf.Clamp((rotation * Vector3.back).y, -1, 1)) * Mathf.Rad2Deg;
|
||||
// Reapplying an already valid stored pose must preserve its exact quaternion, including at the ground limit.
|
||||
if (elevation >= _minElevation && elevation <= _maxElevation && IsSafeCamera(Position(rotation, distance))) return rotation;
|
||||
float groundElevation = Mathf.Asin(Mathf.Clamp((Ground + _collisionMargin - Pivot.y) / distance, -1, 1)) * Mathf.Rad2Deg;
|
||||
float corrected = Mathf.Clamp(elevation, Mathf.Max(_minElevation, groundElevation), _maxElevation);
|
||||
var pitchAxis = Vector3.Cross(rotation * Vector3.back, Vector3.up).normalized;
|
||||
if (pitchAxis.sqrMagnitude < .01f) pitchAxis = Vector3.ProjectOnPlane(rotation * Vector3.right, Vector3.up).normalized;
|
||||
rotation = Quaternion.AngleAxis(corrected - elevation, pitchAxis) * rotation;
|
||||
// Lift along the same orbit only as far as needed to clear large scenery; preserve valid roll/yaw.
|
||||
for (float lift = 0; lift <= _maxElevation - corrected; lift += 2)
|
||||
{
|
||||
var candidate = Quaternion.AngleAxis(lift, pitchAxis) * rotation;
|
||||
if (IsSafeCamera(Position(candidate, distance))) return candidate;
|
||||
}
|
||||
// A large prop can fill the entire requested orbit. Search nearby distances/yaws, validating each pose.
|
||||
for (float distanceOffset = 1; distanceOffset <= _maxDistance - _minDistance; distanceOffset++)
|
||||
foreach (float direction in new[] { 1f, -1f })
|
||||
{
|
||||
float alternativeDistance = Mathf.Clamp(distance + distanceOffset * direction, _minDistance, _maxDistance);
|
||||
for (float yaw = 0; yaw <= 180; yaw += 15)
|
||||
foreach (float side in new[] { 1f, -1f })
|
||||
for (float pitch = Mathf.Max(corrected, 0); pitch <= _maxElevation; pitch += 5)
|
||||
{
|
||||
var candidate = Quaternion.AngleAxis(yaw * side, Vector3.up)
|
||||
* Quaternion.AngleAxis(pitch - corrected, pitchAxis) * rotation;
|
||||
if (!IsSafeCamera(Position(candidate, alternativeDistance))) continue;
|
||||
_distance = alternativeDistance; return candidate;
|
||||
}
|
||||
}
|
||||
// No exterior orbit exists: retain a previously visible safe pose, with no board mutation.
|
||||
if (IsSafeCamera(_camera.transform.position))
|
||||
{
|
||||
_target = _camera.transform.position + _camera.transform.forward * _distance - Vector3.up * .65f;
|
||||
return _camera.transform.rotation;
|
||||
}
|
||||
// The board's aerial bounds can themselves be completely enclosed. An exterior viewpoint remains valid.
|
||||
var bounds = _environment.PlaygroundBounds;
|
||||
var exterior = bounds.max + Vector3.one * (_maxDistance + _collisionMargin);
|
||||
foreach (var collider in _environment.GetComponentsInChildren<Collider>())
|
||||
if (collider.enabled && collider.gameObject.activeInHierarchy && !collider.isTrigger)
|
||||
exterior = Vector3.Max(exterior, collider.bounds.max + Vector3.one * (_maxDistance + _collisionMargin));
|
||||
var exteriorRotation = Quaternion.LookRotation(bounds.center - exterior);
|
||||
_target = exterior + exteriorRotation * Vector3.forward * _distance - Vector3.up * .65f;
|
||||
return exteriorRotation;
|
||||
}
|
||||
private bool IsSafeCamera(Vector3 point)
|
||||
{
|
||||
if (point.y < Ground + _collisionMargin - .001f) return false;
|
||||
if (_environment == null) return true;
|
||||
var overlapping = Physics.OverlapSphere(point, _collisionMargin, ~0, QueryTriggerInteraction.Ignore);
|
||||
foreach (var collider in _environment.GetComponentsInChildren<Collider>())
|
||||
{
|
||||
if (!collider.enabled || !collider.gameObject.activeInHierarchy || collider.isTrigger) continue;
|
||||
if (overlapping.Contains(collider)) return false;
|
||||
if (collider is MeshCollider mesh && !mesh.convex && mesh.bounds.Contains(point) && InsideMesh(mesh, point)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private bool IsSafeCameraPath(Vector3 from, Vector3 to)
|
||||
{
|
||||
if (_environment == null || (to - from).sqrMagnitude < .000001f) return true;
|
||||
foreach (var hit in Physics.SphereCastAll(from, _collisionMargin, (to - from).normalized, Vector3.Distance(from, to), ~0, QueryTriggerInteraction.Ignore))
|
||||
if (hit.collider.enabled && hit.collider.transform.IsChildOf(_environment.transform)) return false;
|
||||
return true;
|
||||
}
|
||||
private static bool InsideMesh(MeshCollider mesh, Vector3 point)
|
||||
{
|
||||
bool backfaces = Physics.queriesHitBackfaces; int inside = 0;
|
||||
try
|
||||
{
|
||||
foreach (var direction in new[] { new Vector3(1, .371f, .529f).normalized, new Vector3(.217f, 1, .613f).normalized, new Vector3(.419f, .293f, 1).normalized })
|
||||
{
|
||||
Physics.queriesHitBackfaces = false; int entries = Crossings(mesh, point, direction);
|
||||
Physics.queriesHitBackfaces = true;
|
||||
if (Crossings(mesh, point, direction) > entries * 2) inside++;
|
||||
}
|
||||
}
|
||||
finally { Physics.queriesHitBackfaces = backfaces; }
|
||||
return inside >= 2;
|
||||
}
|
||||
private static int Crossings(MeshCollider mesh, Vector3 point, Vector3 direction)
|
||||
{
|
||||
int count = 0; float remaining = mesh.bounds.size.magnitude + 1;
|
||||
while (remaining > 0 && mesh.Raycast(new Ray(point, direction), out var hit, remaining))
|
||||
{ count++; float step = hit.distance + .001f; point += direction * step; remaining -= step; }
|
||||
return count;
|
||||
}
|
||||
private void UpdateCamera()
|
||||
{ _camera.transform.SetPositionAndRotation(_target + Vector3.up * .65f + _rotation * new Vector3(0, 0, -_distance), _rotation); }
|
||||
{
|
||||
_rotation = SafeRotation(_rotation, _distance);
|
||||
_camera.transform.SetPositionAndRotation(Position(_rotation, _distance), _rotation);
|
||||
}
|
||||
private void RecordPath(Vector3 position, Quaternion rotation)
|
||||
{
|
||||
var last = _assistPath[_assistPath.Count - 1];
|
||||
if (Vector3.Distance(last.position, position) < .02f && Quaternion.Angle(last.rotation, rotation) < .2f) return;
|
||||
var pose = new Pose(position, rotation);
|
||||
// Collinear/clear stretches occupy one segment, even during a long drag or stationary camera easing.
|
||||
if (_assistPath.Count > 1 && IsSafeCameraPath(_assistPath[_assistPath.Count - 2].position, position)) _assistPath[_assistPath.Count - 1] = pose;
|
||||
else _assistPath.Add(pose);
|
||||
}
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (_dragPresenter != null)
|
||||
{
|
||||
if (!_dragPresenter.Balls.TryGetValue(_grabBall, out var grabbed)) { EndDrag(); return; }
|
||||
var moving = new Bounds(grabbed.transform.position, Vector3.one);
|
||||
foreach (var id in _movingIds) moving.Encapsulate(new Bounds(_dragPresenter.Balls[id].transform.position, Vector3.one));
|
||||
string previousCandidate = _candidate;
|
||||
bool nearby = _dragPresenter.TryGetNearbyGroup(_movingIds, _candidate, _nearbyDistance, _candidateHysteresis, out _candidate, out var friend);
|
||||
var framing = moving; if (nearby) framing.Encapsulate(friend);
|
||||
var rotation = _rotation;
|
||||
if (nearby)
|
||||
{
|
||||
var separation = friend.center - moving.center;
|
||||
// A stable oblique direction exposes front/back overlap without changing the drag's actual geometry.
|
||||
var flatSeparation = Vector3.ProjectOnPlane(separation, Vector3.up);
|
||||
var flatRight = Vector3.ProjectOnPlane(_rotation * Vector3.right, Vector3.up);
|
||||
float yaw = Vector3.SignedAngle(flatRight, flatSeparation, Vector3.up);
|
||||
// Facing either direction along the same screen plane reveals the pair; keep the nearer rotation.
|
||||
if (yaw > 90) yaw -= 180; else if (yaw < -90) yaw += 180;
|
||||
yaw = Mathf.Clamp(yaw, -_assistAngle, _assistAngle);
|
||||
_assistYaw = _candidate != previousCandidate ? yaw : Mathf.LerpAngle(_assistYaw, yaw, 1 - Mathf.Exp(-_assistSpeed * Time.unscaledDeltaTime));
|
||||
rotation = Quaternion.AngleAxis(_assistYaw, Vector3.up) * _rotation;
|
||||
}
|
||||
float elevation = Mathf.Asin(Mathf.Clamp((rotation * Vector3.back).y, -1, 1)) * Mathf.Rad2Deg;
|
||||
rotation = Quaternion.AngleAxis(Mathf.Max(0, _assistElevation - elevation), Vector3.Cross(rotation * Vector3.back, Vector3.up).normalized) * rotation;
|
||||
float distance = Mathf.Max(_distance, framing.extents.magnitude / Mathf.Tan(_camera.fieldOfView * Mathf.Deg2Rad * .5f) / (1 - _screenMargin * 2));
|
||||
distance = Mathf.Clamp(distance, _minDistance, _maxDistance);
|
||||
var desiredPosition = framing.center - rotation * Vector3.forward * distance;
|
||||
float blend = 1 - Mathf.Exp(-_assistSpeed * Time.unscaledDeltaTime);
|
||||
rotation = Quaternion.Slerp(_camera.transform.rotation, rotation, blend);
|
||||
var position = Vector3.Lerp(_camera.transform.position, desiredPosition, blend);
|
||||
// Pan the visible camera so the actual grabbed surface point remains under this pointer.
|
||||
var anchor = grabbed.transform.position + _grabOffset;
|
||||
var oldPosition = _camera.transform.position; var oldRotation = _camera.transform.rotation;
|
||||
_camera.transform.SetPositionAndRotation(position, rotation);
|
||||
var ray = _camera.ScreenPointToRay(_pointer);
|
||||
var plane = new Plane(rotation * Vector3.forward, anchor);
|
||||
if (plane.Raycast(ray, out var depth)) position += anchor - ray.GetPoint(depth);
|
||||
if (IsSafeCamera(position) && IsSafeCameraPath(oldPosition, position))
|
||||
{ _camera.transform.SetPositionAndRotation(position, rotation); RecordPath(position, rotation); }
|
||||
else _camera.transform.SetPositionAndRotation(oldPosition, oldRotation);
|
||||
}
|
||||
else if (_returning)
|
||||
{
|
||||
var desired = Position(_rotation, _distance); var desiredRotation = _rotation;
|
||||
if (!IsSafeCameraPath(_camera.transform.position, desired))
|
||||
{
|
||||
// Replay only the path this camera actually traversed; shorten it with verified safe shortcuts.
|
||||
for (int index = 0; index < _assistPath.Count; index++)
|
||||
{
|
||||
var pose = _assistPath[index];
|
||||
if (!IsSafeCameraPath(_camera.transform.position, pose.position)) continue;
|
||||
desired = pose.position; desiredRotation = pose.rotation;
|
||||
if (Vector3.Distance(_camera.transform.position, desired) < .02f)
|
||||
{
|
||||
// Reach the tested waypoint before discarding it; a near-corner shortcut can still be blocked.
|
||||
_camera.transform.SetPositionAndRotation(desired, desiredRotation);
|
||||
_assistPath.RemoveRange(index, _assistPath.Count - index);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
float blend = 1 - Mathf.Exp(-_assistSpeed * Time.unscaledDeltaTime);
|
||||
var position = Vector3.Lerp(_camera.transform.position, desired, blend);
|
||||
var rotation = Quaternion.Slerp(_camera.transform.rotation, desiredRotation, blend);
|
||||
if (IsSafeCamera(position) && IsSafeCameraPath(_camera.transform.position, position)) _camera.transform.SetPositionAndRotation(position, rotation);
|
||||
if (Vector3.Distance(_camera.transform.position, Position(_rotation, _distance)) < .001f && Quaternion.Angle(_camera.transform.rotation, _rotation) < .01f)
|
||||
{ _returning = false; _assistPath.Clear(); UpdateCamera(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using MagnetismHeroes.UnityRuntime.StageAssets;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -6,11 +7,16 @@ namespace MagnetismHeroes.UnityRuntime.Presentation
|
||||
public sealed class BoardEnvironment : MonoBehaviour
|
||||
{
|
||||
private int _stageId;
|
||||
[SerializeField] private float _collisionTolerance = .001f;
|
||||
private const float BallRadius = .5f;
|
||||
private SphereCollider _safetySphere;
|
||||
public string Theme { get; private set; }
|
||||
public Bounds PlaygroundBounds { get; private set; }
|
||||
public void Apply(StageAsset stage)
|
||||
{
|
||||
if (_stageId == stage.StageId) return;
|
||||
foreach (Transform child in transform) { child.gameObject.SetActive(false); Destroy(child.gameObject); }
|
||||
_safetySphere = null;
|
||||
foreach (var placement in stage.Decorations)
|
||||
{
|
||||
if (placement.prefab.name == "Island" && transform.parent.Find("PlaygroundIsland") != null) continue;
|
||||
@@ -20,6 +26,112 @@ namespace MagnetismHeroes.UnityRuntime.Presentation
|
||||
prop.transform.localScale = placement.scale;
|
||||
}
|
||||
_stageId = stage.StageId; Theme = stage.Theme;
|
||||
var bounds = stage.PlaygroundBounds;
|
||||
var worldBounds = new Bounds(transform.TransformPoint(bounds.min), Vector3.zero);
|
||||
for (int corner = 0; corner < 8; corner++)
|
||||
worldBounds.Encapsulate(transform.TransformPoint(new Vector3(
|
||||
(corner & 1) == 0 ? bounds.min.x : bounds.max.x,
|
||||
(corner & 2) == 0 ? bounds.min.y : bounds.max.y,
|
||||
(corner & 4) == 0 ? bounds.min.z : bounds.max.z)));
|
||||
PlaygroundBounds = worldBounds;
|
||||
Physics.SyncTransforms();
|
||||
foreach (Transform prop in transform)
|
||||
{
|
||||
// The clearing supports the aerial volume from just below its lower boundary.
|
||||
if (!prop.gameObject.activeSelf || prop.name == "MeadowClearing(Clone)") continue;
|
||||
foreach (var collider in prop.GetComponentsInChildren<Collider>())
|
||||
if (!PlaygroundBounds.Intersects(collider.bounds)) collider.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsObstacle(Collider collider, Transform friendsRoot) => collider.enabled && collider.gameObject.activeInHierarchy
|
||||
&& !collider.isTrigger && !collider.transform.IsChildOf(friendsRoot) && collider != _safetySphere;
|
||||
|
||||
public bool IsSafeSphere(Vector3 point, Transform friendsRoot)
|
||||
{
|
||||
var margin = Vector3.one * (BallRadius - _collisionTolerance);
|
||||
if (!PlaygroundBounds.Contains(point - margin) || !PlaygroundBounds.Contains(point + margin)) return false;
|
||||
if (_safetySphere == null)
|
||||
{
|
||||
var probe = new GameObject("PlacementSafetySphere"); probe.transform.SetParent(transform, false);
|
||||
// A trigger on Ignore Raycast supplies a live sphere shape for ComputePenetration.
|
||||
// All safety queries ignore triggers and it cannot become a pointer target.
|
||||
_safetySphere = probe.AddComponent<SphereCollider>(); _safetySphere.radius = BallRadius; _safetySphere.isTrigger = true; probe.layer = 2;
|
||||
probe.transform.localScale = new Vector3(1 / transform.lossyScale.x, 1 / transform.lossyScale.y, 1 / transform.lossyScale.z);
|
||||
}
|
||||
foreach (var collider in Physics.OverlapSphere(point, BallRadius, ~0, QueryTriggerInteraction.Ignore))
|
||||
if (IsObstacle(collider, friendsRoot) && Physics.ComputePenetration(_safetySphere, point, Quaternion.identity,
|
||||
collider, collider.transform.position, collider.transform.rotation, out _, out float depth) && depth > _collisionTolerance)
|
||||
return false;
|
||||
foreach (var mesh in GetComponentsInChildren<MeshCollider>())
|
||||
if (IsObstacle(mesh, friendsRoot) && !mesh.convex && mesh.bounds.Contains(point) && IsInsideSolidMesh(mesh, point)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsInsideSolidMesh(MeshCollider mesh, Vector3 point)
|
||||
{
|
||||
// Triangle meshes do not report a sphere wholly inside a closed surface.
|
||||
// Count oriented crossings: two overlapping closed pieces remain solid, while an arch hole stays empty.
|
||||
bool originalBackfaces = Physics.queriesHitBackfaces; int insideDirections = 0;
|
||||
try
|
||||
{
|
||||
foreach (var direction in new[] { new Vector3(1, .371f, .529f).normalized, new Vector3(.217f, 1, .613f).normalized, new Vector3(.419f, .293f, 1).normalized })
|
||||
{
|
||||
Physics.queriesHitBackfaces = false;
|
||||
int entries = SurfaceCrossings(mesh, point, direction);
|
||||
Physics.queriesHitBackfaces = true;
|
||||
int crossings = SurfaceCrossings(mesh, point, direction);
|
||||
if (crossings > entries * 2) insideDirections++;
|
||||
}
|
||||
}
|
||||
finally { Physics.queriesHitBackfaces = originalBackfaces; }
|
||||
return insideDirections >= 2;
|
||||
}
|
||||
|
||||
private int SurfaceCrossings(MeshCollider mesh, Vector3 point, Vector3 direction)
|
||||
{
|
||||
float remaining = mesh.bounds.size.magnitude + 1; int crossings = 0;
|
||||
float step = Mathf.Max(.0001f, _collisionTolerance);
|
||||
while (remaining > 0 && mesh.Raycast(new Ray(point, direction), out var hit, remaining))
|
||||
{
|
||||
crossings++; float advance = hit.distance + step;
|
||||
point += direction * advance; remaining -= advance;
|
||||
}
|
||||
return crossings;
|
||||
}
|
||||
|
||||
public Vector3 ConstrainTranslation(IReadOnlyList<Vector3> positions, Vector3 requested, Transform friendsRoot)
|
||||
{
|
||||
Physics.SyncTransforms();
|
||||
float length = requested.magnitude;
|
||||
if (length < .000001f) return Vector3.zero;
|
||||
var direction = requested / length; float distance = length;
|
||||
var min = PlaygroundBounds.min + Vector3.one * BallRadius;
|
||||
var max = PlaygroundBounds.max - Vector3.one * BallRadius;
|
||||
foreach (var point in positions)
|
||||
{
|
||||
for (int axis = 0; axis < 3; axis++)
|
||||
if (Mathf.Abs(direction[axis]) > .000001f)
|
||||
{
|
||||
float edge = direction[axis] > 0 ? max[axis] : min[axis];
|
||||
distance = Mathf.Min(distance, Mathf.Max(0, (edge - point[axis]) / direction[axis]));
|
||||
}
|
||||
foreach (var hit in Physics.SphereCastAll(point, BallRadius, direction, length, ~0, QueryTriggerInteraction.Ignore))
|
||||
if (IsObstacle(hit.collider, friendsRoot)) distance = Mathf.Min(distance, Mathf.Max(0, hit.distance - _collisionTolerance));
|
||||
}
|
||||
var translation = direction * distance;
|
||||
// Numerical contact error must never turn a safe preview into an interior drop.
|
||||
bool safe = true;
|
||||
foreach (var point in positions) safe &= IsSafeSphere(point + translation, friendsRoot);
|
||||
if (safe) return translation;
|
||||
float low = 0, high = distance;
|
||||
for (int iteration = 0; iteration < 20; iteration++)
|
||||
{
|
||||
float middle = (low + high) * .5f; safe = true;
|
||||
foreach (var point in positions) safe &= IsSafeSphere(point + direction * middle, friendsRoot);
|
||||
if (safe) low = middle; else high = middle;
|
||||
}
|
||||
return direction * low;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,50 @@ namespace MagnetismHeroes.UnityRuntime.Presentation
|
||||
private readonly Dictionary<string, BallView> _balls = new Dictionary<string, BallView>();
|
||||
private readonly List<GroupBadge> _badges = new List<GroupBadge>();
|
||||
private BoardRules _board;
|
||||
private BoardEnvironment _environment;
|
||||
private string _previewGroup;
|
||||
private bool _previewValid = true;
|
||||
private bool _splitPreview;
|
||||
public void BindEnvironment(BoardEnvironment environment) => _environment = environment;
|
||||
public bool IsSafePosition(Position3 position) => _environment != null && _environment.IsSafeSphere(ToVector(position), transform);
|
||||
public bool IsSafeBoard(BoardRules board)
|
||||
{ Physics.SyncTransforms(); return board != null && board.Balls.All(ball => IsSafePosition(ball.position)); }
|
||||
public Camera Camera => _camera;
|
||||
public IReadOnlyDictionary<string, BallView> Balls => _balls;
|
||||
public string MergeCandidate { get; private set; }
|
||||
public Vector3 DropDelta { get; private set; }
|
||||
|
||||
public bool TryGetNearbyGroup(IReadOnlyCollection<string> movingIds, string previous, float range, float hysteresis, out string candidate, out Bounds bounds)
|
||||
{
|
||||
candidate = null; bounds = default;
|
||||
if (_environment == null || movingIds.Count == 0) return false;
|
||||
Physics.SyncTransforms();
|
||||
var moving = movingIds.Select(id => _balls[id].transform.position).ToArray();
|
||||
float nearest = range, previousDistance = float.PositiveInfinity;
|
||||
foreach (var group in _balls.Values.Where(ball => !movingIds.Contains(ball.BallId)).GroupBy(ball => ball.GroupId))
|
||||
{
|
||||
float distance = float.PositiveInfinity; Vector3 source = default, target = default;
|
||||
foreach (var point in moving)
|
||||
foreach (var other in group)
|
||||
{
|
||||
float separation = Vector3.Distance(point, other.transform.position);
|
||||
if (separation < distance) { distance = separation; source = point; target = other.transform.position; }
|
||||
}
|
||||
if (distance > range) continue;
|
||||
var approach = (target - source).normalized * Mathf.Max(0, distance - 1);
|
||||
if ((_environment.ConstrainTranslation(moving, approach, transform) - approach).sqrMagnitude > .000001f) continue;
|
||||
if (group.Key == previous) previousDistance = distance;
|
||||
if (distance < nearest) { nearest = distance; candidate = group.Key; }
|
||||
}
|
||||
if (previousDistance <= range && previousDistance <= nearest + hysteresis) candidate = previous;
|
||||
if (candidate == null) return false;
|
||||
string selectedCandidate = candidate;
|
||||
var members = _balls.Values.Where(ball => ball.GroupId == selectedCandidate && !movingIds.Contains(ball.BallId)).ToArray();
|
||||
bounds = new Bounds(members[0].transform.position, Vector3.one);
|
||||
foreach (var member in members) bounds.Encapsulate(new Bounds(member.transform.position, Vector3.one));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Render(BoardRules board)
|
||||
{
|
||||
bool transformed = board != null && _balls.Count == board.BallCount && _badges.Count != board.Groups.Length
|
||||
@@ -45,7 +84,7 @@ namespace MagnetismHeroes.UnityRuntime.Presentation
|
||||
foreach (var view in _balls.Values) { view.gameObject.SetActive(false); Destroy(view.gameObject); }
|
||||
foreach (var badge in _badges) { badge.gameObject.SetActive(false); Destroy(badge.gameObject); }
|
||||
_balls.Clear(); _badges.Clear();
|
||||
MergeCandidate = null; DropDelta = Vector3.zero;
|
||||
MergeCandidate = null; DropDelta = Vector3.zero; _previewGroup = null; _previewValid = true; _splitPreview = false;
|
||||
if (board == null) return;
|
||||
foreach (var group in board.Groups)
|
||||
{
|
||||
@@ -74,51 +113,104 @@ namespace MagnetismHeroes.UnityRuntime.Presentation
|
||||
|
||||
public void PreviewGroup(string groupId, Vector3 delta)
|
||||
{
|
||||
MergeCandidate = null; DropDelta = delta;
|
||||
if (_previewGroup != groupId || _splitPreview) CancelPreview();
|
||||
var previousDelta = DropDelta;
|
||||
_previewGroup = groupId; _splitPreview = false; MergeCandidate = null;
|
||||
var members = _board.Groups.First(group => group.id == groupId).ballIds;
|
||||
var positions = members.Select(id => ToVector(_board.Balls.First(ball => ball.id == id).position) + previousDelta).ToArray();
|
||||
DropDelta = previousDelta + (_environment == null ? Vector3.zero : _environment.ConstrainTranslation(positions, delta - previousDelta, transform));
|
||||
foreach (var state in _board.Balls)
|
||||
_balls[state.id].transform.position = ToVector(state.position) + (_balls[state.id].GroupId == groupId ? delta : Vector3.zero);
|
||||
_balls[state.id].transform.position = ToVector(state.position) + (members.Contains(state.id) ? DropDelta : Vector3.zero);
|
||||
foreach (var ball in _balls.Values) ball.ContactCandidate = false;
|
||||
var moving = members.Select(id => _balls[id]).ToArray();
|
||||
var stationary = _balls.Values.Where(ball => ball.GroupId != groupId).ToArray();
|
||||
float closest = 1.35f; BallView source = null, target = null;
|
||||
foreach (var moving in _balls.Values.Where(ball => ball.GroupId == groupId))
|
||||
foreach (var other in _balls.Values.Where(ball => ball.GroupId != groupId))
|
||||
foreach (var ball in moving)
|
||||
foreach (var other in stationary)
|
||||
{
|
||||
float distance = Vector3.Distance(moving.transform.position, other.transform.position);
|
||||
if (distance < closest) { closest = distance; source = moving; target = other; }
|
||||
float distance = Vector3.Distance(ball.transform.position, other.transform.position);
|
||||
if (distance < closest) { closest = distance; source = ball; target = other; }
|
||||
}
|
||||
if (target != null)
|
||||
if (target != null && _environment != null)
|
||||
{
|
||||
MergeCandidate = target.GroupId;
|
||||
var direction = source.transform.position - target.transform.position;
|
||||
if (direction.sqrMagnitude < 0.0001f) direction = -_camera.transform.right;
|
||||
DropDelta += direction.normalized * (1 - closest);
|
||||
foreach (var ball in _balls.Values.Where(ball => ball.GroupId == groupId)) ball.transform.position += DropDelta - delta;
|
||||
var moving = _balls.Values.Where(ball => ball.GroupId == groupId).ToArray();
|
||||
var fixedBalls = _balls.Values.Where(ball => ball.GroupId != groupId).ToArray();
|
||||
var correction = direction.normalized * ContactPlacement.Clearance(moving.Select(ball => ball.transform.position).ToArray(), fixedBalls.Select(ball => ball.transform.position).ToArray(), direction.normalized);
|
||||
DropDelta += correction;
|
||||
foreach (var ball in moving) ball.transform.position += correction;
|
||||
closest = float.PositiveInfinity;
|
||||
foreach (var ball in moving)
|
||||
foreach (var other in fixedBalls)
|
||||
if (direction.sqrMagnitude < .0001f) direction = -_camera.transform.right;
|
||||
direction.Normalize();
|
||||
var correction = direction * (1 - closest);
|
||||
var adjusted = moving.Select(ball => ball.transform.position + correction).ToArray();
|
||||
correction += direction * ContactPlacement.Clearance(adjusted, stationary.Select(ball => ball.transform.position).ToArray(), direction);
|
||||
var safeCorrection = _environment.ConstrainTranslation(moving.Select(ball => ball.transform.position).ToArray(), correction, transform);
|
||||
if ((safeCorrection - correction).sqrMagnitude < .00000001f)
|
||||
{
|
||||
// Only offer an actual unit-sphere contact after the entire correction path is safe.
|
||||
closest = float.PositiveInfinity;
|
||||
foreach (var ball in moving)
|
||||
foreach (var other in stationary)
|
||||
{
|
||||
float distance = Vector3.Distance(ball.transform.position + correction, other.transform.position);
|
||||
if (distance < closest) { closest = distance; source = ball; target = other; }
|
||||
}
|
||||
if (closest >= .999f && closest <= 1.001f)
|
||||
{
|
||||
float distance = Vector3.Distance(ball.transform.position, other.transform.position);
|
||||
if (distance < closest) { closest = distance; source = ball; target = other; }
|
||||
DropDelta += correction;
|
||||
foreach (var ball in moving) ball.transform.position = ToVector(_board.Balls.First(state => state.id == ball.BallId).position) + DropDelta;
|
||||
MergeCandidate = target.GroupId;
|
||||
source.ShowContact(target.transform.position); target.ShowContact(source.transform.position);
|
||||
}
|
||||
MergeCandidate = target.GroupId;
|
||||
source.ShowContact(target.transform.position); target.ShowContact(source.transform.position);
|
||||
}
|
||||
}
|
||||
_previewValid = moving.All(ball => _environment != null && _environment.IsSafeSphere(ball.transform.position, transform));
|
||||
PositionBadges();
|
||||
}
|
||||
|
||||
public bool PreviewSplit(string groupId, IReadOnlyList<string> selected, Vector3 delta)
|
||||
{
|
||||
CancelPreview();
|
||||
if (_previewGroup != groupId || !_splitPreview) CancelPreview();
|
||||
bool hadValidPreview = _previewValid && DropDelta != Vector3.zero;
|
||||
_previewGroup = groupId; _splitPreview = true;
|
||||
var positions = _board.PreviewSplit(groupId, selected, ToPosition(delta));
|
||||
if (positions == null) return false;
|
||||
Physics.SyncTransforms();
|
||||
if (positions == null || _environment == null || positions.Any(ball => !_environment.IsSafeSphere(ToVector(ball.position), transform)))
|
||||
{
|
||||
CancelPreview(); _previewGroup = groupId; _splitPreview = true; _previewValid = false;
|
||||
return false;
|
||||
}
|
||||
if (!CanSweepSplit(positions, selected))
|
||||
{
|
||||
if (!hadValidPreview) { _previewValid = false; return false; }
|
||||
// Search Core input deltas, so the last safe arrays and the committed delta stay identical.
|
||||
var previousDelta = DropDelta;
|
||||
positions = _board.PreviewSplit(groupId, selected, ToPosition(previousDelta));
|
||||
float safe = 0, blocked = 1;
|
||||
for (int iteration = 0; iteration < 20; iteration++)
|
||||
{
|
||||
float fraction = (safe + blocked) * .5f;
|
||||
var candidateDelta = Vector3.Lerp(previousDelta, delta, fraction);
|
||||
var candidate = _board.PreviewSplit(groupId, selected, ToPosition(candidateDelta));
|
||||
if (candidate != null && candidate.All(ball => _environment.IsSafeSphere(ToVector(ball.position), transform))
|
||||
&& CanSweepSplit(candidate, selected))
|
||||
{ safe = fraction; positions = candidate; }
|
||||
else blocked = fraction;
|
||||
}
|
||||
delta = Vector3.Lerp(previousDelta, delta, safe);
|
||||
}
|
||||
foreach (var ball in positions) _balls[ball.id].transform.position = ToVector(ball.position);
|
||||
DropDelta = delta; _previewValid = true;
|
||||
PositionBadges(); return true;
|
||||
}
|
||||
|
||||
private bool CanSweepSplit(IReadOnlyList<BallState> positions, IReadOnlyList<string> selected)
|
||||
{
|
||||
foreach (var ball in positions.Where(ball => selected.Contains(ball.id)))
|
||||
{
|
||||
var start = _balls[ball.id].transform.position;
|
||||
var movement = ToVector(ball.position) - start;
|
||||
if ((_environment.ConstrainTranslation(new[] { start }, movement, transform) - movement).sqrMagnitude > .00000001f)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ShowSelection(IReadOnlyCollection<string> selected)
|
||||
{ foreach (var ball in _balls.Values) ball.Selected = selected.Contains(ball.BallId); }
|
||||
public void ShowHint(IReadOnlyCollection<string> ids)
|
||||
@@ -127,7 +219,7 @@ namespace MagnetismHeroes.UnityRuntime.Presentation
|
||||
public void CancelPreview()
|
||||
{
|
||||
if (_board == null) return;
|
||||
MergeCandidate = null; DropDelta = Vector3.zero;
|
||||
MergeCandidate = null; DropDelta = Vector3.zero; _previewGroup = null; _previewValid = true; _splitPreview = false;
|
||||
foreach (var state in _board.Balls) { _balls[state.id].transform.position = ToVector(state.position); _balls[state.id].ContactCandidate = false; _balls[state.id].ClearPulse(); }
|
||||
foreach (var badge in _badges) badge.ClearReaction();
|
||||
PositionBadges();
|
||||
@@ -136,21 +228,9 @@ namespace MagnetismHeroes.UnityRuntime.Presentation
|
||||
public bool CanDropGroup(string groupId)
|
||||
{
|
||||
Physics.SyncTransforms();
|
||||
foreach (var ball in _balls.Values.Where(ball => ball.GroupId == groupId))
|
||||
{
|
||||
var sphere = ball.GetComponent<SphereCollider>();
|
||||
var scale = sphere.transform.lossyScale;
|
||||
float radius = sphere.radius * Mathf.Max(Mathf.Abs(scale.x), Mathf.Abs(scale.y), Mathf.Abs(scale.z));
|
||||
foreach (var blocker in Physics.OverlapSphere(sphere.transform.TransformPoint(sphere.center), radius, ~0, QueryTriggerInteraction.Ignore))
|
||||
{
|
||||
// Board friends and their face/count decorations are not background obstacles.
|
||||
if (blocker.transform.IsChildOf(transform) || blocker.attachedRigidbody != null) continue;
|
||||
if (Physics.ComputePenetration(sphere, sphere.transform.position, sphere.transform.rotation,
|
||||
blocker, blocker.transform.position, blocker.transform.rotation, out _, out float depth) && depth > 0.001f)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return _previewValid && _environment != null && _balls.Values
|
||||
.Where(ball => _splitPreview || ball.GroupId == groupId)
|
||||
.All(ball => _environment.IsSafeSphere(ball.transform.position, transform));
|
||||
}
|
||||
|
||||
public bool TryFindSpawnPosition(out Position3 position)
|
||||
@@ -164,7 +244,7 @@ namespace MagnetismHeroes.UnityRuntime.Presentation
|
||||
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;
|
||||
if (_environment == null || !_environment.IsSafeSphere(point, transform)) continue;
|
||||
position = ToPosition(point); return true;
|
||||
}
|
||||
position = default; return false;
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace MagnetismHeroes.UnityRuntime.Presentation
|
||||
[SerializeField] private TMP_Text _number;
|
||||
[SerializeField] private Transform _face;
|
||||
[SerializeField] private Transform _cape;
|
||||
[SerializeField] private Transform _equipment;
|
||||
[SerializeField] private SpriteRenderer _expression;
|
||||
[SerializeField] private Sprite[] _expressions;
|
||||
private float _celebrateUntil;
|
||||
@@ -28,6 +29,8 @@ namespace MagnetismHeroes.UnityRuntime.Presentation
|
||||
_number.transform.rotation = camera.transform.rotation;
|
||||
_face.SetPositionAndRotation(_facePosition + camera.transform.up * (Mathf.Sin(remaining / .45f * Mathf.PI) * .025f),
|
||||
camera.transform.rotation * Quaternion.Euler(0, 0, _look * 6 + Mathf.Sin(remaining * 18) * 3));
|
||||
if (_equipment != null)
|
||||
_equipment.SetPositionAndRotation(_facePosition + camera.transform.forward * .49f, camera.transform.rotation);
|
||||
if (_expression != null && _expressions != null && _expressions.Length == 6)
|
||||
_expression.sprite = _expressions[remaining > 0 ? 5 : _mood];
|
||||
if (_cape != null)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e1e30263fddb289489f17f322202fe4e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: BarkHoney
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.52, g: 0.36, b: 0.21, a: 1}
|
||||
- _Color: {r: 0.52, g: 0.35999998, b: 0.20999995, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &4600306967038393692
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a8685925f90bd94c8811c6d573026ef
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-4806790676016323265
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: BarkLight
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.68, g: 0.49, b: 0.3, a: 1}
|
||||
- _Color: {r: 0.68, g: 0.49, b: 0.29999998, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ae1f946849b74f44a457fd96fea460b
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-3840683563196816297
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Blade_SatinSilver
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0.2
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.6
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.85, g: 0.92, b: 0.91, a: 1}
|
||||
- _Color: {r: 0.85, g: 0.92, b: 0.91, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59b7934cb6c96624abd461dad0a6a8f2
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Ceramic_WarmCream
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.57
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.97, g: 0.92, b: 0.78, a: 1}
|
||||
- _Color: {r: 0.96999997, g: 0.92, b: 0.78, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &2294763419762427514
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dad2eea71471abc488393ffb4dc0cc6f
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Enamel_DeepTeal
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.58000004
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.055, g: 0.4, b: 0.44, a: 1}
|
||||
- _Color: {r: 0.054999977, g: 0.39999998, b: 0.43999997, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &1910819220224396764
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 794c64e2850c3f541981a51bb4314d5e
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: GrassGold
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.69, g: 0.8, b: 0.46, a: 1}
|
||||
- _Color: {r: 0.69, g: 0.8, b: 0.45999992, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &1601601172693520391
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a196ef6961e0f34a94a5eba1a792661
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: GrassMint
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.49, g: 0.73, b: 0.46, a: 1}
|
||||
- _Color: {r: 0.49, g: 0.73, b: 0.45999992, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &2777168729409749414
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5cc2b5aea2dcbb34b8ac884c9cbbaad2
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: GroundMint
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.53, g: 0.74, b: 0.5, a: 1}
|
||||
- _Color: {r: 0.53, g: 0.74, b: 0.5, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &1500194591545923706
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 65fe78c059069dc4eb024634ede61511
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-3265832774704495893
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: GroundSage
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.59, g: 0.77, b: 0.49, a: 1}
|
||||
- _Color: {r: 0.59, g: 0.77, b: 0.49, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f9535162b5c71d40a029dd752f48ebd
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Helmet_SilverTint
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0.18
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.61
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.72, g: 0.85, b: 0.86, a: 1}
|
||||
- _Color: {r: 0.72, g: 0.85, b: 0.86, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &1399086876211211452
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6bc3d158d578da846bebebd6363bd204
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Hero_CyanSatin
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.52
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.17, g: 0.72, b: 0.83, a: 1}
|
||||
- _Color: {r: 0.16999996, g: 0.72, b: 0.83, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &8258434983827264131
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21b713eed8aa73146a39c8501b952ef9
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: LeafLight
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.62, g: 0.77, b: 0.46, a: 1}
|
||||
- _Color: {r: 0.62, g: 0.77, b: 0.45999992, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &7376113409471861641
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de846ce1925aa7a44a5dc79b86b8e3ef
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-2887732570966398048
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: LeafMint
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.45, g: 0.67, b: 0.44, a: 1}
|
||||
- _Color: {r: 0.45, g: 0.67, b: 0.43999997, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 492110ad2d3f787439e3de73af77f9b8
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: LeafSage
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.53, g: 0.69, b: 0.41, a: 1}
|
||||
- _Color: {r: 0.53, g: 0.69, b: 0.40999997, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &2164614564685061923
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8bbd8a0045d5b97478feb0fa1950401d
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: LeafShade
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.31, g: 0.56, b: 0.45, a: 1}
|
||||
- _Color: {r: 0.30999997, g: 0.56, b: 0.45, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &2285944471229101495
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: efe8750e774b2034d8fafb4603fd64f7
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-1345738869956853151
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: PathSand
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.76, g: 0.77, b: 0.57, a: 1}
|
||||
- _Color: {r: 0.76, g: 0.77, b: 0.56999993, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8393e09817915d84faa4e1d17b59a7e1
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: PetalCream
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.98, g: 0.88, b: 0.59, a: 1}
|
||||
- _Color: {r: 0.97999996, g: 0.88, b: 0.59, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &1852049430049663096
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 355f2da97deea5e4b861f7aa6b202e3f
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: PetalLavender
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.54, g: 0.42, b: 0.77, a: 1}
|
||||
- _Color: {r: 0.53999996, g: 0.41999996, b: 0.77, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &757417965304048539
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4b7ac151d98566449e34f0e578f38a3
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-4876930336145741697
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: PetalPink
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.9, g: 0.43, b: 0.56, a: 1}
|
||||
- _Color: {r: 0.9, g: 0.42999998, b: 0.56, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ebf4ac2789f6a644813d9ef2500063a
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: PineMint
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.38, g: 0.66, b: 0.55, a: 1}
|
||||
- _Color: {r: 0.37999994, g: 0.65999997, b: 0.54999995, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &2296403098765609282
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b794879c7dfca147b3369766232f531
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-7618314845755370783
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: PollenHoney
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.94, g: 0.58, b: 0.13, a: 1}
|
||||
- _Color: {r: 0.94, g: 0.58, b: 0.12999997, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b397483557461184c8654d3e02a5dcf3
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: RidgeFar
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.66, g: 0.82, b: 0.83, a: 1}
|
||||
- _Color: {r: 0.65999997, g: 0.82, b: 0.83, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &977487373055539988
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 667b96e0a9115bb47a0dbc4892f17efc
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-6326109607119454012
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: RidgeLight
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.61, g: 0.78, b: 0.74, a: 1}
|
||||
- _Color: {r: 0.61, g: 0.78, b: 0.74, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a03ee10ba3e8a744ab476785053f9169
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-1156016394531227513
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: RidgeTeal
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.54, g: 0.73, b: 0.72, a: 1}
|
||||
- _Color: {r: 0.53999996, g: 0.73, b: 0.72, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bef17d23e751d784d82b6c35d978f57f
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-4444051176984281619
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: SoilWarm
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.52, g: 0.42, b: 0.29, a: 1}
|
||||
- _Color: {r: 0.52, g: 0.41999996, b: 0.28999996, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c728602787baf4e45b66a0f864d18bda
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-5936126992530721773
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Stem
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.32999998
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.32, g: 0.52, b: 0.27, a: 1}
|
||||
- _Color: {r: 0.31999996, g: 0.52, b: 0.26999995, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1ff2daca9c05adc408ea2d50f5c5cbc5
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-2359564263495287107
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: StoneBlue
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.22000003
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.58, g: 0.7, b: 0.71, a: 1}
|
||||
- _Color: {r: 0.58, g: 0.7, b: 0.71, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16d401d6a1f43e349ab3d903c4b96c28
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: StoneCream
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.22000003
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.59, g: 0.64, b: 0.57, a: 1}
|
||||
- _Color: {r: 0.59, g: 0.6399999, b: 0.56999993, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &634282108172900272
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ef609db6d51de24cbfc1a9e2f0028ad
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: StoneLight
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _ScreenSpaceReflections: 1
|
||||
- _ScreenSpaceReflectionsContributeTransparent: 1
|
||||
- _Smoothness: 0.22000003
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.75, g: 0.76, b: 0.65, a: 1}
|
||||
- _Color: {r: 0.75, g: 0.76, b: 0.65, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &9085574280362469963
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user