feat(content): author thirty stages, live hints, progression and Blender themes

This commit is contained in:
NINI
2026-10-05 13:05:27 +09:00
parent 60f8a55bb9
commit 64f4f19ae9
304 changed files with 20710 additions and 419 deletions
@@ -26,9 +26,9 @@ namespace MagnetismHeroes.Tests
{
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GameApp.prefab");
var serialized = new SerializedObject(prefab.GetComponent<GameApp>()); var stages = serialized.FindProperty("_stages");
Assert.That(stages.arraySize, Is.EqualTo(5)); var definitions = Enumerable.Range(0, stages.arraySize)
Assert.That(stages.arraySize, Is.EqualTo(30)); var definitions = Enumerable.Range(0, stages.arraySize)
.Select(index => ((StageAsset)stages.GetArrayElementAtIndex(index).objectReferenceValue).ToDefinition()).ToArray();
Assert.That(definitions.Select(stage => stage.Id), Is.EqualTo(new[] { 1, 2, 6, 10, 14 }));
Assert.That(definitions.Select(stage => stage.Id), Is.EqualTo(Enumerable.Range(1, 30)));
var session = new GameSession(definitions, new EmptyStore()); Assert.That(session.IsUnlocked(6), Is.False);
Assert.That(session.StartStage(6), Is.False); Assert.That(session.IsUnlocked(10), Is.False); Assert.That(session.IsUnlocked(14), Is.False);
Assert.That(serialized.FindProperty("_compositionButtons").arraySize, Is.EqualTo(3));
@@ -0,0 +1,41 @@
using System.Linq;
using MagnetismHeroes.Core;
using MagnetismHeroes.UnityRuntime.StageAssets;
using NUnit.Framework;
using UnityEditor;
namespace MagnetismHeroes.Tests
{
public sealed class ContentAssetTests
{
[Test] public void EveryFixedStageHasExactlyOneAsset()
{
var assets = AssetDatabase.FindAssets("t:StageAsset", new[] { "Assets/MagnetismHeroes/UnityRuntime/StageAssets" })
.Select(guid => AssetDatabase.LoadAssetAtPath<StageAsset>(AssetDatabase.GUIDToAssetPath(guid))).ToArray();
Assert.That(assets.Length, Is.EqualTo(30));
Assert.That(assets.Select(asset => asset.StageId).OrderBy(id => id), Is.EqualTo(Enumerable.Range(1, 30)));
}
[Test] public void FixedOrderMatchesEveryR07InitialTargetAndKind()
{
string[] expected = { "1>1", "1,1>2", "1,1,1>3", "1,1,1,1>4", "1,1,1,1,1>5",
"1,1>2", "2,1>3", "3,1>4", "2,3>5", "2>1,1", "3>2,1", "4>2,2", "5>3,2", "3,1>2,2", "4,1>3,2",
"1,1,1,1,1,1>6", "1,1,1,1,1,1,1>7", "1,1,1,1,1,1,1,1>8", "1,1,1,1,1,1,1,1,1>9", "1,1,1,1,1,1,1,1,1,1>10",
"5,1>6", "3,4>7", "4,4>8", "5,5>10", "6>5,1", "7>4,3", "9>5,4", "10>5,5", "5,3>4,4", "6,4>5,5" };
for (int id=1; id<=30; id++)
{
var asset=AssetDatabase.LoadAssetAtPath<StageAsset>($"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage{id:D2}.asset");
var definition=asset.ToDefinition();
Assert.That(string.Join(",",definition.InitialGroups)+">"+string.Join(",",definition.TargetGroups), Is.EqualTo(expected[id-1]), $"Stage {id}");
Assert.That(definition.Kind, Is.EqualTo(id<=5 || id>=16 && id<=20 ? StageKind.Counting : StageKind.Composition));
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);
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,2 @@
fileFormatVersion: 2
guid: ae9003f830dded844a056f200eb4b634
@@ -0,0 +1,111 @@
using System;
using System.Linq;
using MagnetismHeroes.Core;
using MagnetismHeroes.UnityRuntime.StageAssets;
using NUnit.Framework;
using UnityEditor;
namespace MagnetismHeroes.Tests
{
public sealed class ContentSessionTests
{
private sealed class Store : IProgressStore
{
public ProgressSave Saved = new ProgressSave();
public bool TryLoad(out ProgressSave save, out string error) { save = Saved; error = null; return true; }
public bool TrySave(ProgressSave save, out string error) { Saved = save; error = null; return true; }
}
private static StageDefinition[] Definitions() => Enumerable.Range(1,30).Select(id =>
AssetDatabase.LoadAssetAtPath<StageAsset>($"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage{id:D2}.asset").ToDefinition()).ToArray();
private static void Apply(GameSession session, BoardHint hint)
{
Assert.That(hint, Is.Not.Null);
RuleResult result;
switch (hint.Kind)
{
case HintKind.Collect: result = session.MoveGroup(hint.FirstGroupId, new Position3(.1f,0,0)); break;
case HintKind.Count: result = session.Count(hint.BallIds[0]); break;
case HintKind.Merge: result = session.MoveAndMerge(hint.FirstGroupId, new Position3(.1f,0,0), hint.SecondGroupId); break;
default: result = session.SplitAndMove(hint.FirstGroupId, hint.BallIds, "hint-split-" + Guid.NewGuid().ToString("N"), new Position3(1,0,0)); break;
}
Assert.That(result.Accepted, Is.True, result.Reason.ToString());
}
[Test] public void FreshSaveSolvesAllThirtyThroughActualCoreCallsUnlocksNextAndNeverThirtyOne()
{
var definitions = Definitions(); var store = new Store(); var session = new GameSession(definitions, store);
Assert.That(Enumerable.Range(1,30).Where(session.IsUnlocked), Is.EqualTo(new[] {1}));
for (int id = 1; id <= 30; id++)
{
if (id == 1) Assert.That(session.StartStage(id), Is.True);
else Assert.That(session.StartNextStage(), Is.True);
Assert.That(session.Board.StageId, Is.EqualTo(id));
for (int step = 0; session.Screen == SessionScreen.Puzzle && step < 40; step++)
{
var before = UnityEngine.JsonUtility.ToJson(store.Saved); var ids = session.Board.Balls.Select(ball => ball.id).ToArray();
var hint = session.Hint;
Assert.That(UnityEngine.JsonUtility.ToJson(store.Saved), Is.EqualTo(before), "Help is read-only.");
Apply(session, hint); Assert.That(session.Board.Balls.Select(ball => ball.id), Is.EqualTo(ids));
}
Assert.That(session.Screen, Is.EqualTo(SessionScreen.FreePlay), $"Stage {id}");
Assert.That(store.Saved.completedStageIds, Is.EqualTo(Enumerable.Range(1,id)));
Assert.That(session.HasNextStage, Is.EqualTo(id < 30));
Assert.That(store.Saved.latestFreePlay.sourceStageId, Is.EqualTo(id));
session = new GameSession(definitions, store); Assert.That(session.ContinueFreePlay(), Is.True);
}
Assert.That(session.IsUnlocked(31), Is.False); Assert.That(session.StartNextStage(), Is.False); Assert.That(session.StartStage(31), Is.False);
Assert.That(session.StartStage(6), Is.True); session.MoveGroup(session.Board.Groups[0].id, new Position3(.2f,0,0)); session.ShowHome();
Assert.That(store.Saved.completedStageIds, Is.EqualTo(Enumerable.Range(1,30))); Assert.That(session.IsUnlocked(30), Is.True);
Assert.That(session.StartStage(6), Is.True); Assert.That(session.Board.Balls[0].position.x, Is.EqualTo(definitions[5].BallPositions[0].x+.2f));
}
[Test] public void NextPrefersSavedPuzzleAndHelpRecoversFromAlternativeDisconnectedPartitions()
{
var definitions = Definitions(); var store = new Store { Saved = new ProgressSave { completedStageIds = Enumerable.Range(1,29).ToArray() } };
var session = new GameSession(definitions,store); Assert.That(session.StartStage(30), Is.True);
var group = session.Board.Groups[0]; session.SplitAndMove(group.id,new[] {group.ballIds[0],group.ballIds[2]},"alternative",new Position3(0,2,0));
session.CommitCamera(new CameraState { distance=12 }); session.ShowHome();
Assert.That(session.StartStage(29), Is.True);
for (int i=0; session.Screen==SessionScreen.Puzzle && i<30; i++) Apply(session,session.Hint);
Assert.That(session.StartNextStage(), Is.True); Assert.That(session.Camera.distance, Is.EqualTo(12));
Assert.That(session.Board.Groups.Any(value=>value.id=="alternative"), Is.True);
for (int i=0; session.Screen==SessionScreen.Puzzle && i<30; i++) Apply(session,session.Hint);
Assert.That(session.Screen, Is.EqualTo(SessionScreen.FreePlay));
Assert.That(session.Board.Groups.Select(value=>value.ballIds.Length).OrderBy(size=>size), Is.EqualTo(new[] {5,5}));
}
[Test] public void AllIntegerPartitionsHaveLiveHintsThatReachEachCompositionTarget()
{
foreach (var definition in Definitions().Where(stage=>stage.Kind==StageKind.Composition))
foreach (var sizes in Partitions(definition.Total,1))
{
var board = new BoardRules(new StageDefinition(definition.Id,StageKind.Composition,sizes,definition.TargetGroups.ToArray()));
for (int i=0; !board.IsSolved && i<20; i++)
{
var hint=BoardHint.Find(board,definition); Assert.That(hint, Is.Not.Null);
var result=hint.Kind==HintKind.Merge ? board.TryMerge(hint.FirstGroupId,hint.SecondGroupId)
: board.TrySplit(hint.FirstGroupId,hint.BallIds,"split-"+i);
Assert.That(result.Accepted, Is.True);
}
Assert.That(board.IsSolved, Is.True, $"{definition.Id}: {string.Join(",",sizes)}");
}
}
private static System.Collections.Generic.IEnumerable<int[]> Partitions(int total,int minimum)
{
if (total==0) { yield return Array.Empty<int>(); yield break; }
for (int first=minimum; first<=total; first++)
foreach (var rest in Partitions(total-first,first)) yield return new[] {first}.Concat(rest).ToArray();
}
[Test] public void CountingHelpRejoinsCollectedSplitBeforeFindingOtherFriendsThenCountsOnlyUncountedId()
{
var definition=new StageDefinition(4,StageKind.Counting,new[] {1,1,1,1},new[] {4}); var board=new BoardRules(definition);
board.TryMerge(board.Groups[0].id,board.Groups[1].id); var collected=board.Groups.First(group=>group.ballIds.Length==2);
board.TrySplit(collected.id,new[] {collected.ballIds[0]},"resplit");
var hint=BoardHint.Find(board,definition); Assert.That(hint.Kind, Is.EqualTo(HintKind.Merge));
Assert.That(hint.BallIds.All(board.CollectedBallIds.Contains), Is.True); board.TryMerge(hint.FirstGroupId,hint.SecondGroupId);
Assert.That(BoardHint.Find(board,definition).Kind, Is.EqualTo(HintKind.Collect));
foreach (var group in board.Groups) board.TryMoveGroup(group.id,new Position3(.1f,0,0));
while (board.Groups.Length>1) board.TryMerge(board.Groups[0].id,board.Groups[1].id);
board.TryCount(board.Balls[0].id); hint=BoardHint.Find(board,definition);
Assert.That(hint.Kind, Is.EqualTo(HintKind.Count)); Assert.That(board.CountedBallIds, Does.Not.Contain(hint.BallIds[0]));
Assert.That(board.TryCount(hint.BallIds[0]).Accepted, Is.True);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f65031cb242c4e746b894204a7e7c4a1
@@ -0,0 +1,39 @@
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using MagnetismHeroes.UnityRuntime.Presentation;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
namespace MagnetismHeroes.Tests
{
public sealed class ContentVoiceTests
{
[Test] public void EveryLiveHelpPairAndBothActualGhostScriptsHaveOfflinePcmClips()
{
var prefab=AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GameApp.prefab");
var clips=new SerializedObject(prefab.GetComponent<VoicePlayer>()).FindProperty("_clips");
var ids=Enumerable.Range(0,clips.arraySize).Select(i=>clips.GetArrayElementAtIndex(i).FindPropertyRelative("id").stringValue).ToArray();
Assert.That(ids.Length, Is.EqualTo(44)); Assert.That(ids.Distinct().Count(), Is.EqualTo(44));
foreach (string id in new[] {"guide_find","guide_uncounted","demo_merge6","demo_split10"}.Concat(
Enumerable.Range(2,9).SelectMany(size=>Enumerable.Range(1,size/2).Select(count=>$"guide_split_{size}_{count}"))))
{
Assert.That(ids, Does.Contain(id));
int index=System.Array.IndexOf(ids,id); var clip=(AudioClip)clips.GetArrayElementAtIndex(index).FindPropertyRelative("clip").objectReferenceValue;
Assert.That(clip, Is.Not.Null,id); Assert.That(clip.frequency, Is.EqualTo(44100)); Assert.That(clip.channels, Is.EqualTo(1)); Assert.That(clip.samples, Is.GreaterThan(0));
var importer=(AudioImporter)AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(clip));
Assert.That(importer.defaultSampleSettings.compressionFormat, Is.EqualTo(AudioCompressionFormat.PCM));
Assert.That(importer.defaultSampleSettings.loadType, Is.EqualTo(AudioClipLoadType.DecompressOnLoad));
}
Assert.That(ids.Count(id=>id.StartsWith("guide_split_")), Is.EqualTo(25));
}
[TestCase("count_01","13A882CE1620140D447635E14F2D4758045D5E348960C17386DF1C720050C100")]
[TestCase("guide_collect","62E15C9E38C36F4094A6C7E9C5148510141290B5F41DB5AAD9DC1D8F1A158594")]
[TestCase("guide_count","DEBE29B89F3E2856F2BF4C5E01707A5F0A0657E5768C03BE385EC88FA2328B40")]
public void UserApprovedVoiceBytesRemainExact(string id,string hash)
{
using (var sha=SHA256.Create()) Assert.That(System.BitConverter.ToString(sha.ComputeHash(File.ReadAllBytes($"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/{id}.wav"))).Replace("-",""), Is.EqualTo(hash));
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f2e9d6608639d744397aad7855886e4b