Keep local improvement plans and execution history outside product source

This commit is contained in:
NINI
2026-10-10 01:45:07 +09:00
parent 9544f4bce5
commit 32690a510e
32 changed files with 2 additions and 6280 deletions
-96
View File
@@ -1,96 +0,0 @@
# MagnetismHeroes 개발 인계
2026-10-06 최신 사용자 승인: 효과음·배경음악 없이 첫 버전을 진행하고 T7을 마무리한 뒤 T8을 수행한다. 기존 음원 선택 대기는 해제됐다. 아래 효과음·음악 포함·선정·반복/믹싱 기준은 후속 범위로 이동하며 한국어 음성 44개·시각 연출·오프라인 및 저장/규칙 기준은 유지한다. 기획 초기의 환경 상태 설명은 실제 최신 검증 기록을 대신하지 않는다.
## 문서 역할과 우선순위
| 문서 | 역할 |
|---|---|
| `.dryforge/spec.md` | 게임 동작·범위·30개 스테이지·검증 기준의 기준 문서 |
| `.dryforge/plan.md` | 구현 작업 대상, 순서, 검증 방법을 제안하는 개발 계획 |
| `.dryforge/handoff.md` | 문서 사용법, 반드시 지킬 경계, 프로젝트 배경 |
동작이 충돌하면 기획서가 우선한다. 기획서의 사용자 의도를 바꾸려면 사용자와 먼저 조정한다. 계획의 파일 배치와 작업 순서는 같은 기획을 실현하는 범위에서 수정할 수 있다. 기존 코드가 생겨도 구현 방식의 참고이며 게임의 목적을 대체하지 않는다.
## 실행 개요
첫 개발 단계이며, 작업 폴더에 기존 게임 코드는 없다. 공통 프로젝트와 순수 C# 규칙을 준비한 뒤 1번 수 세기 퍼즐의 입력·화면·저장을 연결하고 Android에서 첫 실행을 확인한다. 이후 합체·분리 → 저장·자유 놀이 확장 → 30개 스테이지·도움 → 카툰·한국어 연출 → 최종 전체 검증으로 진행한다. 각 단계에서 플레이 가능한 이전 결과를 유지한다. 세부 작업과 의존 관계는 개발 계획을 따른다. 프로젝트 초기화와 단계별 실행 장면 연결은 실행 담당자가 맡고, 같은 장면을 여러 작업에서 동시에 수정하지 않는다. 첫 퍼즐부터 최소 자산의 포함·재생을 확인하고, 세 테마와 전체 배치를 연결한 뒤 최종 자산을 마감한다.
현재 문서는 기획 결과다. Unity의 Android Build Support, SDK·NDK·OpenJDK와 Android Studio 및 가상 기기 설치는 확인했다. 게임 코드, 프로젝트 패키지 설치, APK 제작, 가상 기기 실행은 아직 수행하지 않았다. 기존 Git 이력과 `.gitignore`는 이 문서 작성에서 변경하지 않는다. 사용자가 문서를 확인한 뒤 이 대화에서 `dryforge:go`를 호출하여 개발을 시작한다.
## 반드시 지킬 경계
1. 구 하나의 값은 1이고 합체·분리로 총량이 달라지지 않는다. 숫자 표시와 실제 개수가 일치해야 한다.
2. 모양이나 풀이 순서로 정답을 제한하지 않는다. 분리할 구들이 서로 떨어져 있어도 두 친구로 모은다.
3. 수 세기는 전부 찾고 직접 모은 뒤 각각 눌러 세야 한다. 이미 센 구를 중복으로 세지 않는다.
4. 기획서의 30개 스테이지와 순서를 그대로 구현한다. 더 적은 스테이지를 완성 결과로 제시하지 않는다.
5. 실패 점수·목숨·시간제한을 추가하지 않는다. 성공 후 같은 공간에서 자유롭게 놀게 한다.
6. 미완료 퍼즐은 스테이지별로 각각 저장하고, 최근 자유 놀이는 전체에서 하나만 저장한다. 완료 기록은 재플레이로 취소하지 않는다.
7. 한국어 소리와 자산을 포함한 오프라인 안드로이드 체험판이다. 서버·광고·결제·사용자 추적을 도입하지 않는다.
8. APK 생성과 Android 가상 기기 실행·진행·복원은 구분하여 검증한다. 가상 기기를 실행하지 못했으면 그 항목을 통과로 표시하지 않는다.
9. 실제 손가락 조작·실제 기기 가독성·성능은 기기 준비 후 검사한다. iOS 지원과 앱 스토어 배포는 이번 완료 범위가 아니다.
10. 기획을 바꾸지 않는 구현·튜닝은 개발자가 판단한다. 새로운 놀이 규칙·저장 범위·플랫폼 범위 변경은 사용자와 먼저 조정한다.
11. 순수 C# 규칙과 Unity 연결을 분리하고 URP·Input System·uGUI·TextMeshPro를 사용한다. 고정 ScriptableObject 자산과 JSON 플레이 저장을 혼용하지 않는다.
12. 첫 퍼즐부터 Android를 검증한다. 조기 검사나 에디터 검사만으로 최종 APK 검증을 통과 처리하지 않는다. 각 기능의 합격 사례와 실행 증거를 남긴다.
13. 합체 영웅은 개별 자석 구·대표 얼굴과 마스크·빛나는 연결부·자력 망토로 표현한다. 변신으로 구의 기본 크기나 아이가 만든 배치를 바꾸지 않으며, 분리 시 두 묶음으로 모으는 조정은 R04를 따른다. 장식이 구·숫자·선택·계수 표시와 입력을 가리지 않게 한다.
14. 배경 테마 3개를 제작한다. Unity·Blender를 함께 사용하고 실제 2D 자산에도 AI 이미지를 활용한다. 효과음·음악은 사용자 선택 절차를 따른다. 음성은 conda `qwen3-tts`의 로컬 Qwen3-TTS를 정비해 GPU·1.7B CustomVoice·한국어 Sohee로 제작한다. 임시·미선택 파일과 미검수 출력은 최종 완료가 아니다.
15. 숫자 음성은 계수 순서대로 들리고 같은 보드의 성공 후에도 마지막 숫자가 보존되어야 한다. 조작·성공 판정을 소리에 종속시키지 않는다. 화면 전환·일시 정지·복원 때 오래된 음성·연출이 남지 않게 한다.
## 별도로 남길 의도
실행에 필요한 사용자 의도는 기획서와 계획에 담았다. 대화를 다시 읽어야만 알 수 있는 추가 규칙은 없다. 처음의 20개 안은 합체·분리 개념을 충분히 반복하기 위해 30개로 확장되었으며, 초기의 모든 모바일 플랫폼 동시 제작은 첫 단계 안드로이드 제작으로 좁혀졌다. 실행 중 이전 안을 다시 적용하지 않는다.
## 프로젝트 배경
**실행 범위가 아닌 프로젝트 맥락이다.** 이 절은 개발 방향과 향후 프로젝트 안내 문서의 배경으로 사용한다. 구현할 항목과 완료 기준은 기획서에 따른다.
### 1. 정체성
5~7세가 숫자를 직접 만져 보는 체험형 3D 퍼즐 게임이다. 자석 구 하나를 1로 삼고, 친구들을 합체·분리하며 수량과 숫자를 연결한다. 모바일 가로 화면에서 손가락으로 다루는 장난감 같은 경험을 지향한다. Numberblocks의 수량을 드러내는 캐릭터 표현이 참고이며, 구 자석을 직접 조작하는 것이 이 게임의 중심이다.
캐릭터의 중심 문장은 ‘아이가 붙인 모양이 영웅의 몸이 되고, 연결되는 순간 대표 표정과 자력 망토가 완성된다’이다. 붙으면 하나의 영웅 팀, 나뉘면 함께 돕는 두 팀으로 표현한다. 변신을 보면서도 같은 구들이 보존된다는 사실을 따라갈 수 있게 한다. 특정 모양이나 더 큰 수를 상위 등급 영웅으로 취급하지 않는다.
첫 체험판은 한국어·안드로이드·오프라인·혼자 플레이로 한정한다. 광고와 결제는 없다. 30개 스테이지로 수집·계수·합체·분리를 반복하고, 성공 후 같은 공간에서 자유롭게 놀게 한다. 사용자에게 중요한 것은 실제 수의 의미와 조작의 자유이며, 빠른 정답이나 실패 점수는 목표가 아니다.
### 2. 놀이의 개념과 규칙
- **자석 구:** 값 1인 최소 단위다. 합체·분리·이동으로 소실되거나 복제되지 않는다. 자유 놀이에서만 꺼내거나 반납할 수 있다.
- **친구:** 하나 이상의 구로 된 묶음이다. 대표 얼굴 하나와 묶음 개수 표시를 가진다. 합체하면 한 친구가 되고, 분리하면 선택한 구와 남은 구가 각각 한 친구가 된다. 이전 묶음의 정체성보다 구성 구의 보존이 중요하다.
- **합체 영웅의 표현:** 기존 구 표면의 대표 얼굴·얇은 마스크, 접촉부의 빛, 묶음 뒤 리본형 망토로 구성한다. 안정 상태에서 얼굴·숫자·망토는 묶음마다 하나다. 합체 시 한 팀으로 통합되고 분리 시 각 팀에 생긴다. 짧은 포즈와 흔들림은 구의 정체성·기본 배치·계수 기록을 바꾸지 않으며 장식은 확정 상태에서 다시 만들 수 있다.
- **입체 놀이터:** 한 화면에서 회전·확대하며 둘러보는 작은 공간이다. 자석은 공중에 뜨고 모든 방향으로 붙일 수 있다. 탐험은 사물 뒤·위의 자석을 찾아 끌어오는 행동이며 보행 캐릭터나 넓은 월드는 없다.
- **배경 테마:** 서로 다른 세 장소의 외형을 작은 입체 세트와 주요 소품으로 구성한다. 소품은 공유할 수 있으며 테마와 배치는 고정 스테이지 정의에서 정한다. 테마가 학습 목표·정답 조건을 바꾸지 않고 성공 후 놀이와 복원에서도 유지된다.
- **학습 음성과 음원:** 화면의 계수·시범·도움에 대응하는 제작 파일이다. 숫자 발화는 순서를 지키며 입력·진행 상태를 소유하지 않는다. 효과음·음악의 최종 선택은 사용자가 한다. 사용자가 설치한 Qwen3-TTS 환경에서 실행 담당자가 대본을 파일로 생성·검수한다. 선택 전 임시 파일로 구현을 진행할 수 있지만 최종 품질·사용권 검수가 끝나야 완성으로 취급한다.
- **수 세기 퍼즐:** 목표만큼의 자석을 모두 모아 하나씩 눌러 센다. 단순히 목표 숫자를 표시하는 것만으로 끝나지 않는다. 구별 계수 기록은 배치가 바뀌거나 앱을 다시 열어도 유지된다.
- **구성 퍼즐:** 준비된 친구들을 목표 개수의 친구들로 바꾼다. 전체 묶음 구성만 같으면 모양과 풀이 순서는 자유다. 2와 2는 4 하나와 다르다.
- **나누기 선택:** 한 친구 안의 원하는 구들을 고른다. 서로 떨어진 구도 고를 수 있으며, 분리 후 양쪽을 각각 한 묶음으로 모은다. 0개나 전체 선택으로 빈 친구를 만들지 않는다.
- **도움:** 찾거나 셀 구 하나, 또는 목표에 접근할 첫 행동을 알려준다. 대신 실행하거나 사용 횟수로 불이익을 주지 않는다.
- **완료와 자유 놀이:** 성공하면 기록을 남기고 다음 번호를 열지만 화면을 강제로 바꾸지 않는다. 같은 친구들로 자유 놀이를 하며 최대 10개까지 꺼내거나 반납한다. 빈 놀이판도 다시 채울 수 있다.
- **퍼즐 저장:** 각 스테이지의 현재 미완료 상태를 따로 보존한다. 완료한 스테이지를 다시 풀다가 멈춘 상태와 과거 완료 기록은 함께 존재할 수 있다.
- **자유 놀이 저장:** 최근 하나만 보존한다. 퍼즐별 저장과 혼합하지 않는다. 새로운 자유 놀이로 넘어가면 이전 자유 놀이 저장을 교체한다.
- **학습 흐름:** 1~5 세기 5개 → 합치기 4개·분리하기 4개·혼합 2개 → 6~10 세기 5개 → 합치기 4개·분리하기 4개·혼합 2개. 스테이지는 순서대로 열리며 완료한 곳은 다시 플레이할 수 있다.
### 3. 기술 방향
- Unity 6를 사용한다. 작업 환경에서 확인한 설치 버전은 `6000.6.3f1`이며 기존 게임 코드는 없다. 다른 버전으로 옮겨야 한다면 도구 호환성 이유와 영향을 먼저 확인한다.
- 클라이언트 하나에서 입력을 순서대로 처리하고 수학 상태를 확정한다. 온라인 공유 상태와 서버 API가 없으므로 분산 동기화는 필요하지 않다.
- 수량·합체·분리·계수·목표 판정과 진행 제어는 순수 C#으로 작성한다. Unity 객체는 입력·3D 배치·표현·파일 저장의 연결을 담당한다. 규칙을 화면 없이 검사할 수 있게 하며 렌더링·물리 효과 때문에 구의 개수가 바뀌지 않게 한다.
- URP·Input System·uGUI·TextMeshPro를 사용한다. 프리팹과 Inspector에서 카툰 캐릭터·화면을 제작한다. 큰 UI 문서 프레임워크보다 현재 게임의 Unity 제작 흐름을 우선한다.
- 명시적 상태 머신, Presenter, 저장 인터페이스와 시작 지점의 직접 의존성 연결을 사용한다. 범용 DI·이벤트·MVVM 프레임워크는 도입하지 않는다. 상태 머신은 화면·조작의 유효 전이를 드러내고 Presenter는 수학 규칙을 소유하지 않는다.
- 스테이지의 고정 초기 구성·목표·배치는 ScriptableObject로 제작하고 런타임 순수 C# 정의로 변환한다. 플레이 중 배치와 완료 기록은 원본 자산과 분리한다.
- 저장은 기기 내 앱 전용 파일의 JSON 버전 1로 처리한다. 퍼즐별 배치·수집·계수·카메라와 최근 자유 놀이를 복원한다. 저장 인터페이스는 파일 없이 진행 제어를 검사하기 위한 경계이며 사용자 계정·클라우드·여러 사용자 프로필은 없다.
- 한국어 소리와 화면 자산을 앱에 포함한다. 실행 중 네트워크, 마이크, 연락처, 위치 정보가 필요한 기능을 만들지 않는다. 자산의 출처와 사용 범위를 기록한다.
- Unity에서 자석 구·표현·효과를, Blender에서 입체 배경 소품을 만든다. AI 이미지도 실제 얼굴·UI 그림·텍스처에 사용하며 한글·숫자 표시는 TMP에서 처리한다. 제작 원본과 FBX·PNG·WAV 등 Unity 입력 파일을 분리하고 사용 조건·생성/수정·검수 기록을 남긴다.
- 음원 요청 목록과 Qwen3-TTS 대본·생성 프로필·파일 규격을 실행 담당자가 준비한다. 효과음·음악의 최종 선정은 사용자가 한다. conda `qwen3-tts` 환경을 재사용하고 GPU·`Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice`·`Korean`·`Sohee`로 로컬 생성한다. 제작 환경과 원본/생성 이력은 Unity 코드 밖에 두고 채택한 WAV만 게임에 연결한다. 모델 다운로드·음성 생성은 개발 실행 단계에서 수행한다.
- Qwen3-TTS 패키지와 클래스 import는 확인했으나 현재 PyTorch는 CPU 구성이다. 기존 환경을 무조건 재설치하지 않고 GPU 실행에 필요한 변경만 수행한 뒤 모델 로딩·실제 추론·청각 품질을 각각 확인한다. 개발 도구가 준비되지 않아 임시 파일로 진행한 상태와 최종 음성 완료를 구분한다.
- 작은 퍼즐을 입력부터 저장까지 완성하며 확장한다. Unity 에디터의 EditMode·PlayMode 검사를 기능마다 수행하고, 첫 퍼즐의 Android 설치·실행·복원을 확인한 뒤 전체 Android 검증을 마지막에 반복한다. 실제 기기 검증은 이번 결과와 분리한다.
- 요구사항에서 기능별 합격 사례를 먼저 정하고 정상·무효·경계·복원을 검사한다. 테스트 실행 수와 실패·미실행 여부, 소스 상태·APK 식별 정보·기기·로그를 남긴다. 테스트가 없거나 검사하지 못한 결과를 성공으로 처리하지 않는다.
- Core와 Unity 실행 코드, Editor 도구와 테스트의 참조 경계를 지킨다. C# 이름 규칙과 프리팹·씬의 단독 작성 규칙은 계획에 기록한다. 조작 임계값·흡착 거리·연출 시간은 플레이 감각을 보고 조정하는 값이다. 실제 기기 성능 합격은 기기 준비 후의 범위다.
- 파일 배치·직렬화 라이브러리·그림/음원 편집 도구·미술 조정값은 위 동작을 보존하는 구현 선택이다. 제작용 AI 이미지는 허용하되 새 게임 서버·런타임 AI·클라우드 TTS·승인 없는 유료 자산 구매·배포는 도입하지 않는다.
### 4. 향후 범위
- 생활 속 문제를 돕는 영웅 세계관을 확장할 수 있다. 자석을 생활 장치에 연결하는 미션과 소풍 열차는 검토 아이디어이며, 장치 배치를 새로운 성공 조건으로 넣는 것은 별도 기획 대상이다. 이번 합체 영웅 외형 반영으로 R07의 30개 목표와 모양·위치에 무관한 판정을 바꾸지 않는다.
- 실제 안드로이드 스마트폰·태블릿에서 손가락 조작, 가독성, 성능 검증과 조정.
- 아이폰·아이패드용 빌드 및 Mac/Xcode·실제 기기를 통한 검증.
- 곱셈·나눗셈 등 수학 개념 확장. 이번 분리하기를 나눗셈 학습으로 간주하지 않는다.
- 영어, 광고·결제, 계정·동기화, 작품 여러 개 보관함, 자유 탐험 월드는 이번에 구현할 기능이 아니다. 향후 도입 여부도 별도 설계 대상이다.
-626
View File
@@ -1,626 +0,0 @@
# MagnetismHeroes 개발 계획
2026-10-06 승인된 범위 변경: 첫 버전은 효과음·배경음악을 제외한다. 아래 음원 요청표·음악 반복/믹싱·효과음 인수는 향후 참고이며 T7/T8의 필수 완료 기준에서 제외한다. 한국어 숫자·안내 음성 44개, 시각 연출, 오프라인 재생·순차 발화·마지막 숫자·화면 전환/일시 정지 정리 검증은 유지한다. 기존 null 음원 연결은 범위 제외 상태로 인정하며 임시 음원을 새로 채택하지 않는다. T7은 보존된 실제 검증과 앱 blob 동일성·새 범위의 독립 검토로 마감하고, T8은 Final 네 단계와 전체 진행·독립 복원을 새로 검증한다.
## 실행 방향과 현재 상태
한국어·오프라인·안드로이드 가로 화면의 30개 스테이지 체험판을 만든다. 수학 규칙은 순수 C#으로 구현하고 Unity는 입력·공간·표현·저장 연결을 담당한다. 작은 퍼즐 하나의 입력부터 저장까지 완성한 후 합체·분리와 전체 콘텐츠를 확장한다. 첫 퍼즐부터 Android를 검증하며, 마지막에는 완성된 APK로 전체 검증을 반복한다.
현재 저장소에는 게임 코드·Unity 프로젝트 설정·자동 검사 명령이 없다. 아래 경로·클래스 역할·명령은 **개발하면서 만들 대상**이다. 이 문서 작성에서 테스트·APK 제작·가상 기기 실행을 완료한 것으로 해석하지 않는다. 기존 게임 규칙과 R07의 30개 스테이지 표는 유지한다.
## 1. 아키텍처와 책임 경계
책임을 아래처럼 나누되 각 책임마다 별도 범용 프레임워크를 만들지 않는다.
| 책임 | 소유하는 내용 | 하지 않는 일 |
|---|---|---|
| Core의 수학 규칙 | 구 ID, 묶음 소속, 합체·분리, 계수 기록, 목표 다중집합 판정, 공급 한도 | GameObject·Transform·UI·파일 접근 |
| Core의 진행 제어 | 스테이지 시작·복원, 유효한 조작 실행, 정답·완료 전이, 자유 놀이, 저장 요청 | 입력 장치 읽기, 얼굴·애니메이션 구현 |
| Unity 입력·공간 연결 | 터치 해석, Raycast, 끌기 미리보기, 카메라, 흡착 후보, 확정 조작 전달 | 접촉 프레임을 근거로 구 복제·삭제·정답 처리 |
| Unity 화면·소리 표현 | Presenter, 캐릭터·숫자·선택 강조, 메뉴, 안내, 효과음 | 독립적인 수량·완료 상태 보유 |
| Unity 데이터·저장 연결 | ScriptableObject 정의 변환, JSON 직렬화·파일 교체·복구 | 실행 중 고정 자산 수정, UI 객체 직렬화 |
| 시작 지점 | 규칙·진행·입력·표현·저장 구현의 직접 연결과 수명 관리 | 여러 곳에서 찾는 전역 서비스 등록 |
컴파일 참조는 `UnityRuntime → Core` 방향이다. Core는 UnityRuntime과 UnityEngine을 참조하지 않는다. 저장 구현은 Core에서 선언한 저장 경계를 구현한다. 시작 지점이 그 구현을 진행 제어에 전달한다. Editor 코드만 자산 제작·검사·빌드 API에 접근하며 런타임에서 Editor 코드를 참조하지 않는다.
초기 실행 어셈블리는 Core와 UnityRuntime 두 개로 충분하다. Core 안에서 수학 규칙과 진행 제어를 폴더로 구분한다. Editor와 EditMode·PlayMode 테스트는 별도 어셈블리로 분리한다. 파일마다 인터페이스나 어셈블리를 만들지 않는다.
### 상태가 바뀌는 한 경로
```mermaid
flowchart LR
I["터치·버튼"] --> A["Unity 입력 해석"]
A --> C["진행 제어"]
C --> D["순수 C# 규칙 검증·상태 확정"]
D --> C
C --> P["Presenter·3D 표현"]
C --> S["저장 경계"]
S --> J["앱 전용 JSON 파일"]
```
예를 들어 3과 2를 붙이면 입력 연결이 두 묶음 ID를 전달하고, 규칙이 서로 다른 유효한 묶음인지 확인한 뒤 구 5개를 한 묶음으로 확정한다. 진행 제어는 목표 판정과 저장 요청을 처리하고 Presenter는 숫자 5와 얼굴 하나를 표시한다. 애니메이션 중간의 Transform이나 콜라이더 개수로 수량을 다시 판정하지 않는다.
끌기 중 위치·분리 선택·연출은 미리보기다. 저장과 정답 판정은 확정 상태를 기준으로 한다. 조작 취소나 핀치 전환은 R03의 마지막 확정 상태로 복구한다. 앱 일시 정지에서도 연출 중간값 대신 이 확정 상태를 저장한다. 저장 실패를 파일 저장 성공으로 보고하지 않으며 복구 경로는 R12를 따른다.
## 2. 적용할 디자인 패턴
| 방식 | 적용 지점과 목적 | 적용 범위 |
|---|---|---|
| 명시적 상태 머신 | 시작·선택·퍼즐·자유 놀이의 유효 전이와 일반/나누기 조작 구분 | 상태와 전이를 표·코드로 드러낸다. 상태마다 클래스를 만드는 것은 필수가 아니다. |
| Presenter | 상태를 숫자·얼굴·버튼·안내에 반영하고 UI 의도를 진행 제어로 전달 | UI에 수학 규칙을 넣지 않는다. 상태 변경을 구독한다면 해제도 같은 수명에서 처리한다. |
| 저장 인터페이스 | 진행 제어 검사에서는 메모리 저장 대역을, 실제 앱에서는 JSON 파일을 사용 | 파일 접근 경계 하나를 둔다. 범용 Repository 계층을 추가하지 않는다. |
| 직접 의존성 주입 | 시작 코드가 필요한 구현을 만들어 전달 | 외부 DI 컨테이너·전역 Singleton 서비스 탐색을 도입하지 않는다. |
| 데이터로 정의하는 스테이지 | 같은 규칙으로 30개 자산의 초기 구성·목표·배치를 실행 | 스테이지마다 별도 게임 로직을 작성하지 않는다. |
합체·분리·계수·공급·반납은 명시적인 조작 호출로 표현한다. 취소는 미리보기 취소이며 범용 실행 취소/재실행 시스템을 뜻하지 않는다. 호출 결과를 따라 상태와 화면을 갱신하므로 전역 이벤트 버스도 필요하지 않다. 구가 최대 10개인 이 프로젝트에서는 ECS나 대규모 객체 풀 도입보다 규칙과 화면의 일치를 우선한다.
## 3. Unity 제작 방식과 프로젝트 준비
프로젝트 초기화는 작업 번호 없이 실행 담당자가 수행한다.
- 저장소 루트에 설치된 Unity `6000.6.3f1`로 URP 프로젝트를 준비한다. Android Build Support와 Unity 제공 SDK·NDK·OpenJDK 설치는 확인되었으므로 설치를 반복하지 않는다. External Tools에서 실제 사용 경로를 확인한다.
- Input System·uGUI·TextMeshPro·Unity Test Framework의 에디터 호환 버전을 선택하고 `Packages/manifest.json`, 잠금 파일과 `ProjectSettings/`에 고정한다. 개발 도중 패키지나 에디터 버전을 임의로 올리지 않는다.
- `Assets/MagnetismHeroes/Core/`에 순수 데이터·수학 규칙·진행 제어·저장 경계를 둔다. `UnityRuntime/` 아래 `Interaction`, `Presentation`, `StageAssets`, `Persistence`, `Bootstrap`으로 Unity 책임을 구분한다.
- `Editor/`, `Tests/EditMode/`, `Tests/PlayMode/`, `Scenes/`를 별도로 둔다. 초기 어셈블리·공통 폴더 메타데이터·검증 진입점은 공통 준비에서 한 번 만든다. Core의 Unity 참조 금지는 어셈블리 설정으로 검증한다.
- 시작/선택 화면과 플레이 공간은 소수의 씬으로 관리한다. 친구·숫자·목표 패널·놀이터를 재사용 프리팹으로 만들며 30개 번호마다 씬을 복제하지 않는다.
- 코드는 외부 C# 편집기에서 작성하고 씬·프리팹·배치·색·조명·애니메이션 연결은 Unity 에디터에서 제작한다. Unity 자산을 수정할 때는 에디터가 정상적으로 읽는 생성·수정 경로를 사용하고 참조 누락을 확인한다.
- 실행 담당자가 단계마다 공통 씬·시작 연결·빌드 포함 목록을 갱신한다. 기능 작성자는 전용 코드·프리팹·자산을 제공하고 같은 공통 씬을 동시에 수정하지 않는다.
- Android Studio의 `Medium_Phone_API_37.0` AVD는 생성되어 있지만 실행 검증은 하지 않았다. 사용 가능한 ABI·그래픽 환경을 확인한다. Unity 빌드용 SDK와 Studio의 가상 기기용 SDK 경로를 혼동하지 않는다.
기본 그림은 단순 구와 임시 얼굴로 먼저 조작을 검증한다. 카툰 재질·세 테마 배경·표정·한국어 음성은 단계별로 확장하고 최종 파일로 교체한다. 아래 제작 절차와 음원·대본 요청을 첫 퍼즐부터 시작하여 전체 제작 단계까지 기다리지 않는다. 임시 자산으로 플레이가 된다는 사실과 R11·R14의 표현·소리 완성은 별개로 기록한다. 자산은 앱에 포함하며 런타임 다운로드를 요구하지 않는다.
### 합체 영웅의 제작 방향
**아이가 붙인 모양이 영웅의 몸이 되고, 연결되는 순간 대표 표정과 자력 망토가 완성된다.** R11의 외형과 변신 순서를 Presentation에서 구현한다. 자석 구, 대표 얼굴·얇은 마스크, 접촉부의 빛, 묶음 뒤의 리본형 자력 망토, 숫자 표시를 재사용 가능한 표현 요소로 준비한다. 묶음 소속과 배치에서 표시를 구성하며 숫자마다 고정된 몸체 프리팹으로 교체하지 않는다.
| 소개용 구성 | 자세·움직임의 예시 |
|---|---|
| 1인 친구 | 통통 움직이며 주변을 살피고 짧은 망토가 따라옴 |
| 2인 친구 | 붙은 두 구가 함께 기울며 균형을 잡음 |
| 3인 친구 | 삼각형은 안정적인 자세, 일렬 배치는 유연하게 움직이는 자세 |
| 5인 친구 | 둥근 묶음은 몸을 활짝 펴는 인상, 긴 묶음은 물결치는 움직임 |
위 형태는 소개·외형 검토용 예시다. 플레이 중 아이의 배치를 예시 형태로 바꾸지 않으며 6~10도 같은 표현 원칙으로 조합한다. 시안의 색과 마스크 윤곽은 고정하지 않고 개별 구·숫자·선택·계수 표시의 식별을 우선해 조정한다. 게임 안에서는 모든 구의 기본 크기를 같게 유지한다.
합체는 접근 미리보기 → 접촉 빛과 효과음 → 대표 얼굴·숫자·망토 통합 → 짧은 영웅 포즈로 표현한다. 분리는 선택 테두리·망토 흐름 → 끌어내기 확정 → 두 묶음 각각의 얼굴·숫자·망토 → 서로 바라보는 짧은 반응으로 표현한다. 선택만 한 상태, 0개·전체 선택, 취소에서 성공적인 분리 연출을 실행하지 않는다.
Presenter는 확정된 묶음 변화로 표현을 갱신한다. 흔들림·망토·접촉 빛은 수학 상태와 저장 좌표를 수정하지 않고 입력을 가로채지 않는다. 연속 조작·화면 전환·복원 시 지난 연출을 정리해 현재 묶음과 맞춘다. 기존 상태로 표현을 재구성하므로 외형만을 위한 저장 필드를 추가하지 않는다.
T2에서 기본 구·얼굴·숫자를 연결하고 T4에서 합체·분리 결과와 표현 전환 지점을 마련한다. T7에서 마스크·연결부·망토·표정·효과음을 완성하고 T8에서 최종 Android 화면과 반복 조작을 확인한다.
### 자산 제작·전달·검수
Unity에서 자석 구·표현 프리팹·재질·특수 효과를 만들고 Blender에서 입체 배경 소품을 제작한다. AI 이미지는 시안뿐 아니라 실제 얼굴·UI 그림·텍스처에도 사용한다. 효과음·음악의 선택은 사용자가 맡는다. 음성은 이미 설치한 conda `qwen3-tts` 환경의 Qwen3-TTS를 정비해 실행 담당자가 생성·검수한다. 필요한 자산을 요청하고 전달받거나 선택된 파일의 사용권·형식·품질을 확인해 게임에 연결한다. 사용자 선택을 대신해 무료 음원·직접 작곡·AI 음악으로 결정하지 않는다.
아래 수량은 **초기 제작 규모이며 조정 가능**하다. 같은 파일을 여러 용도에서 재사용할 수 있으나 필수 동작의 표현을 생략하지 않는다. 배경 테마 3개와 숫자 읽기 1~10은 기획서의 범위다.
| 자산 | 첫 퍼즐에서 준비 | 전체 버전 제작 목록 | 제작·연결 방식 |
|---|---|---|---|
| 자석 친구 | 구 1종, 대표 얼굴, 숫자 표시 | 공유 구 메시·재질, 얇은 마스크, 표정 6종(기본·관심·집중·웃음·격려·축하) | Unity 기본 구 또는 공유 메시, 얼굴 PNG/간단한 표면 메시, TMP 숫자; 10개 고정 몸체를 만들지 않음 |
| 배경 | 단순 입체 바닥·가림 소품으로 조작 검사 | 서로 다른 테마 3개, 공통 소품 4~6종과 테마별 주요 소품 3~4종 | Blender 모델 → FBX → URP 재질 → Unity 프리팹; 같은 테마 안에서도 배치를 바꿔 사용 |
| 화면 | 목표·나누기·도움·진행 버튼의 기본 표시 | 시작·스테이지 선택·플레이·자유 놀이에 필요한 아이콘과 패널 | AI 시안/이미지 정리 → 투명 PNG; uGUI 연결, 한글·숫자는 TMP |
| 글꼴 | 기본 한글·숫자 표시 | 실제 화면 문구·숫자·기호를 포함하는 글꼴과 TMP 폰트 자산 | 사용 조건이 확인된 한글 글꼴을 가져와 필요한 글리프를 구성; AI 그림으로 글자를 대체하지 않음 |
| 특수 효과 | 수집·계수 강조 | 흡착 후보, 연결 빛, 분리 선택/확정, 계수 표시, 도움 강조, 성공 효과와 자력 망토 | 공유 고리/리본 메시·재질, 작은 Particle System; 상태별 강조는 구별 |
| 효과음 | 아래 요청표의 첫 퍼즐 필수 음원 | 합체·분리·계수·격려·성공·화면·자유 놀이의 소리 | 사용자가 선택한 파일을 편집·음량 정리한 후 AudioClip으로 연결 |
| 한국어 음성 | ‘하나’와 수집/세기 안내 | 하나~열 10클립, 공통 안내, 합치기/나누기 시범, 상태별 도움 문구 | 대본 → conda Qwen3-TTS GPU·1.7B·Sohee → WAV → 발음·음량 검수 → AudioClip |
| 배경음악 | 임시 음악 또는 무음으로 기능 검사 | 놀이용 반복 음악 1곡과 메뉴용 편집본을 초기 요청 규모로 사용 | 사용자 선택 후 반복 구간·음량 편집; 3개 테마에 같은 곡을 사용할 수 있음 |
#### 캐릭터·배경·2D 제작 절차
1. R11의 외형을 기준으로 구, 얼굴·마스크, 연결 빛, 망토, 선택·계수 표시의 작은 스타일 시트를 만든다. 색·선 두께·광택·그림자 기준을 같은 시트에 기록한다. 기존 합체 영웅 그림은 참고 시안이며 그대로 3D 모델이 된 것으로 처리하지 않는다.
2. 구 하나와 2+3 합체, 5에서 떨어진 두 구를 선택한 분리를 먼저 Unity에서 표현한다. 기본·선택·계수 상태와 임의 입체 배열을 회전해 확인한 뒤 얼굴 그림·재질·소품을 확장한다.
3. AI 그림은 얼굴/마스크/아이콘/텍스처를 개별 요소로 생성한다. 공통 시안·스타일 지시를 재사용하고, 채택 결과의 여백·윤곽·투명 가장자리·색을 그림 편집 도구로 정리한다. 눈·입·마스크를 구 표면에 얹으며 숫자와 안내 문구를 이미지에 구워 넣지 않는다. 구 전체나 합체 몸체의 그림을 동적 3D 몸체로 대체하지 않는다.
4. Blender에서 둥근 모서리의 바닥·다리·받침·가림 소품을 공통 크기 기준으로 만든다. 불필요한 내부 면을 줄이고 피벗·스케일·회전·법선·UV를 정리한 뒤 선택한 모델을 FBX로 내보낸다. 모델 하나를 먼저 Unity에 넣어 방향·크기·재질·충돌 범위를 확인한 후 같은 방식으로 나머지를 제작한다.
5. Blender의 재질 노드가 Unity에 그대로 전달된다고 가정하지 않는다. Unity에서 공유 URP 재질과 제한된 색/텍스처 변형으로 장난감 질감을 구성한다. 단순 조명·그림자로 시작하고 후처리는 숫자와 개별 구가 읽히는 범위에서 조정한다.
6. 세 테마는 **정원 놀이터·구름 놀이터·별빛 공방**을 초기 미술 시안으로 제안한다. 이름·구체 소품·색은 조정할 수 있으며 테마 3개라는 범위는 유지한다. 정원은 잎/꽃/둥근 다리, 구름은 구름 받침/무지개 아치, 공방은 받침대/패널/별 모양 표지로 구별한다. 별·꽃 등 장식은 수량을 세는 자석과 외형·강조·입력에서 구별한다.
7. 배경은 카메라를 회전할 수 있는 입체 세트로 구성한다. 고정 카메라용 그림 한 장을 놀이터 전체로 사용하지 않는다. 테마는 스테이지 정의의 배치/장식 참조로 연결한다. 1~10/11~20/21~30에 각각 배정하는 것을 초기 배치값으로 두며, 배정 조정으로 R07의 순서·총량·목표를 바꾸지 않는다.
8. 모든 테마에서 자석은 장식 뒤·위에 놓을 수 있어도 회전하면 발견하고 집을 수 있어야 한다. 별도의 임무 장치·보행·새 성공 조건을 추가하지 않는다. 성공 후 자유 놀이와 이어하기에서도 해당 스테이지의 테마를 유지하며, 고정 정의와 `sourceStageId`에서 복원해 저장 필드를 늘리지 않는다.
Krita·Inkscape 같은 그림 편집기와 Audacity 같은 음원 편집기는 파일 정리용 도구 후보이며 필수 런타임 의존성이 아니다. 실행 시 사용 가능한 도구를 확인하고 필요한 무료 편집 도구만 준비한다. Blender·그림/음원 편집기 설치는 확인하지 않았다. Qwen3-TTS 패키지 설치와 클래스 import는 확인했지만 GPU 추론·음성 파일 생성은 아직 검증하지 않았다.
#### 특수 효과와 애니메이션
- 접촉 빛과 선택 강조는 공유 고리/윤곽 표현을 사용하되 형태·밝기·움직임을 다르게 한다. 이미 센 구의 표시는 지나가는 접촉 빛 뒤에도 유지한다.
- 자력 망토는 정지한 친구에게도 보이는 얇은 리본 메시로 만든다. 움직임에만 남는 TrailRenderer 하나로 망토 전체를 구현하지 않는다. 이동 꼬리는 보조로 사용할 수 있으며 망토는 안정 상태의 묶음마다 하나다.
- 성공은 짧은 작은 입자·표정·소리로 표현한다. 많은 입자나 큰 빛으로 구·목표를 덮지 않는다. 첫 버전에 전체 몸체 리깅·천 시뮬레이션·별도 대형 VFX 프레임워크를 요구하지 않는다.
- 떠오르기·기울기·망토 움직임은 표현용 하위 객체에서 실행한다. 기본 위치·구 ID·계수 기록은 바꾸지 않는다. 확정된 상태 변경과 후보/선택 미리보기를 구분해 재생하고 취소·후속 조작·복원 시 오래된 효과를 정리한다.
#### 효과음·음악 요청표와 사용자 선택
음원을 요청할 때 다음 표와 함께 용도 ID, 실행 화면/행동, 느낌, 권장 길이, 반복 여부, 필요한 단계, 파일 형식, 출처/사용권 정보를 전달한다. 아래 길이는 편집용 초기값이며 사용자 음원과 조작감에 맞춰 조정한다. 용도별 파일은 공유할 수 있고, 초기 UI는 임시 소리로 연결할 수 있으나 사용자 선택과 실제 재생 검수가 끝나야 최종 완성으로 처리한다.
| 용도 ID | 사용 시점·느낌 | 길이·반복 초기값 | 최초 필요 단계 |
|---|---|---|---|
| `sfx_pickup` | 친구를 집을 때, 가벼운 말랑함 | 0.1~0.3초, 1회 | T2 |
| `sfx_count` | 새 구를 셌을 때, 음성을 가리지 않는 짧은 톡 | 0.1~0.2초, 1회 | T2 |
| `sfx_ui` | 유효한 버튼 선택, 조용한 클릭 | 0.1~0.2초, 1회 | T2 |
| `sfx_success` | 퍼즐 성공, 부드러운 축하 | 1~2초, 1회 | T2 |
| `sfx_merge` | 합체 확정, 자석이 붙는 ‘착’ | 0.2~0.5초, 1회 | T4 |
| `sfx_split_select` | 분리 구 선택/해제, 작은 반응 | 0.1~0.2초, 1회 | T4 |
| `sfx_split` | 분리 확정, 연결이 풀리는 ‘톡’ | 0.2~0.5초, 1회 | T4 |
| `sfx_encourage` | 목표와 다른 구성/무효 조작, 부담 없는 반응 | 0.2~0.5초, 1회 | T4 |
| `sfx_spawn_return` | 자유 놀이 추가/반납 | 0.2~0.4초, 1회 | T5 |
| `sfx_hint` | 도움 강조, 주의를 부드럽게 끎 | 0.2~0.5초, 1회 | T6 |
| `music_play` | 퍼즐·자유 놀이, 가사와 긴장감 없는 반복 음악 | 60~120초, 반복 | T7 |
| `music_menu` | 시작/선택 화면, 같은 음악의 조용한 편집본 가능 | 30~60초, 반복 | T7 |
사용자가 파일을 제공하면 그 파일을 검수하고, 출처나 후보를 선택하면 허용된 파일을 확보하여 편집한다. 구매가 필요한 후보는 비용 조건을 먼저 제시하며 승인되지 않은 구매를 실행하지 않는다. 사용권을 확인하지 못한 후보나 사용자가 아직 선택하지 않은 임시 파일을 최종 자산으로 승격하지 않는다. 사용자 응답이 필요한 동안에는 해당 소리의 임시 연결과 다른 구현을 진행하되, 최종 음원 완료 항목은 미완료로 남긴다.
전달 규격의 초기값은 음성·효과음은 WAV/PCM 16bit·mono·44.1kHz, 음악은 WAV/PCM 16bit·stereo·44.1kHz다. 다른 형식의 허용된 파일을 받으면 원본을 보관하고 Unity 입력용 파일을 변환한다. TTS의 원래 출력 샘플레이트와 음악의 원래 채널 수는 보존 기록에 남기며, 샘플레이트·채널은 품질과 메모리를 확인해 조정할 수 있다. 숫자 클립의 이름은 대본표 ID와 일치시키고 잘리지 않은 한 발화를 담는다.
#### 로컬 TTS 대본과 출력 계약
로컬 Qwen3-TTS는 **제작 PC에서 음성 파일을 생성하는 도구**다. Unity 앱과 빌드에 TTS SDK·모델·서버를 추가하지 않는다. 생성된 WAV를 읽으면 되므로 Qwen의 Python API를 게임 코드에 결합하지 않는다. 환경·모델·파일·품질을 아래 절차에 따라 각각 확인하고 클라우드 TTS로 자동 전환하지 않는다.
| 음성 ID | 대본/생성 범위 | 연결 시점 |
|---|---|---|
| `count_01`~`count_10` | 하나, 둘, 셋, 넷, 다섯, 여섯, 일곱, 여덟, 아홉, 열 | 새 구를 센 순서의 개수; 숫자 기호를 TTS에 그대로 맡기지 않음 |
| `guide_collect` | ‘친구를 찾아 끌어와 볼까?’ | 수집 안내/도움 |
| `guide_count` | ‘모두 모았네! 하나씩 눌러 세어 보자.’ | 세기 안내 |
| `guide_merge` | ‘친구들을 가까이 붙여 볼까?’ | 합치기 시범/도움 |
| `guide_split` | ‘나누기를 누르고, 떼어 낼 친구를 골라 끌어내자.’ | 나누기 시범/도움 |
| `guide_find`, `guide_uncounted` | 찾을 자석/아직 세지 않은 구를 알려주는 짧은 문구 | R08의 현재 상태 도움 |
| `guide_split_N_K` | ‘네 개 친구에서 하나를 떼어 볼까?’처럼 실제 힌트에 쓰는 N·분리 개수 K별 문장 | 목표에 접근하는 분리 도움 |
대본은 초기 문안이며 같은 의미의 자연스러운 한국어로 다듬을 수 있다. N에 따라 달라지는 조사는 문장 단위로 생성·검수하고 숫자 클립을 기계적으로 이어 붙여 어색한 문장을 만들지 않는다. 분리 안내 ID의 N은 2~10, K는 1~N-1이며 실제 도움에 사용하는 쌍을 대본표에 기록한다. 위 예시 목록만으로 실제 힌트의 모든 분리 수를 하나로 고정하지 않는다. T6에서 실제 화면 안내·힌트 종류와 대본을 대조해 필요한 문장을 빠짐없이 요청한다.
실행 담당자가 음성 ID·화면 문구·발화 문구·사용 조건·필요 단계를 담은 대본표와 생성 프로필을 만들고 선택된 Qwen 환경에서 생성한다. 생성 도구명·버전·모델·목소리 식별자·설정과 사용 조건을 기록한다. WAV는 원본으로 보관하고 앞뒤 과도한 무음·잘림·잡음·발음 오류를 편집/재생성으로 수정한다. 모든 숫자와 실제 안내 문장을 듣고 확인하며 무음 파일·다른 숫자·누락을 통과시키지 않는다. 모델을 앱에 배포하지 않아도 결과물의 사용 가능 범위는 확인한다.
#### Qwen3-TTS 제작 환경과 생성 절차
제작 도구는 로컬 Qwen3-TTS이며 기존 conda 환경 `qwen3-tts`를 재사용한다. **GPU·1.7B CustomVoice·한국어 Sohee**로 확정한다. 고정 목소리를 재사용하면서 말투 지시를 적용하는 방향이다. VoiceDesign·음성 복제·온라인 API·대화형 서버를 제작의 기본 경로에 추가하지 않는다.
**2026-10-04 읽기 전용 확인 결과**는 아래와 같다. 패키지 설치 확인과 실제 음성 생성은 다르며 이 기획 보강에서는 환경 변경·모델 다운로드·추론·음성 파일 생성을 수행하지 않았다.
| 항목 | 확인 결과 | 개발 실행에서 확인할 사항 |
|---|---|---|
| conda 환경 | `qwen3-tts` 존재; 환경의 Python 직접 실행 가능 | conda 실행 경로 또는 해당 환경 Python을 찾아 사용; 시스템 Python과 혼용하지 않음 |
| Python·Qwen | Python 3.12.15, `qwen-tts` 0.1.1, `Qwen3TTSModel` import 성공 | 실제 제작에 사용한 버전 기록과 샘플 생성 |
| 주요 패키지 | torch 2.14.1, transformers 4.57.3, soundfile 0.14.0, accelerate 1.12.0 | GPU 정비 후 필요한 호환 버전과 실행 결과 고정 |
| 현재 PyTorch | `torch.version.cuda = null`, `torch.cuda.is_available() = false` | CUDA 지원 PyTorch 구성과 실제 GPU 연산 검증 |
| 하드웨어 | RTX 4080 SUPER, 약 16GB VRAM, 드라이버 591.86 | 모델 로딩·실제 추론의 메모리/시간 확인; 하드웨어 존재만으로 통과 처리하지 않음 |
| import 경고 | FlashAttention 미설치, SoX 실행 파일 미발견; 클래스 import는 성공 | 경고와 실패를 구분하고 선택한 생성 경로의 필요성을 샘플로 확인 |
| 모델·음성 | 가중치 확보·모델 로딩·GPU 추론·WAV 생성 미검증 | 각각 실행 증거를 남기며 설치 완료로 대신하지 않음 |
개발 실행에서는 기존 환경의 패키지 이름·버전과 GPU 상태를 먼저 기록한다. 공식 PyTorch의 Windows/Python/CUDA 설치 조합 중 Qwen과 호환되는 구성을 사용하여 필요한 PyTorch 변경만 수행한다. 환경 전체 삭제·재설치나 관련 없는 패키지 일괄 업그레이드는 하지 않는다. conda가 셸 PATH에 없으면 확인한 conda 실행 파일 또는 해당 환경 Python을 사용하며 전역 Python으로 대신 실행하지 않는다.
CUDA가 활성화되고 작은 텐서 연산이 성공한 뒤 모델을 로딩한다. FlashAttention 설치를 성공 조건으로 삼지 않는다. 현재 Windows 환경에서는 `attn_implementation="sdpa"`를 초기값으로 사용하고 같은 GPU·모델에서 호환성 문제를 확인한 경우 `eager`로 조정할 수 있다. `device_map="cuda:0"`, `dtype=torch.bfloat16`, 배치 크기 1을 초기값으로 두고 장치 지원·메모리·음질을 확인해 dtype/배치를 조정한다. SoX 경고만으로 선택한 12Hz CustomVoice 경로를 실패 처리하거나 무조건 추가 설치하지 않는다. 실제 생성이나 채택한 편집 경로가 필요로 할 때 확인해 준비한다.
| 생성 항목 | 값·규칙 |
|---|---|
| 모델 ID | `Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice` |
| 모델 revision | 첫 제작에서 공식 저장소의 커밋 해시를 기록하고 그 스냅샷을 고정; 이후 조용히 최신 버전으로 변경하지 않음 |
| 언어·목소리 | `language="Korean"`, `speaker="Sohee"`; 로드한 모델의 지원 목록과 대조 |
| 생성 함수 | `Qwen3TTSModel.from_pretrained()` → `generate_custom_voice()` |
| 초기 말투 지시 | ‘5~7세 아이에게 이야기하듯 따뜻하고 밝게, 천천히 또렷하게 읽어 주세요. 숫자는 정확하게 읽고 과장된 비명이나 웃음소리는 넣지 마세요.’ — 조정 가능 |
| 생성 방식 | 한 모델을 로드하여 선택한 대본들을 순서대로 생성; 처음에는 한 클립씩 검수 |
| 출력 | 함수가 반환한 파형과 실제 sample rate로 원본 WAV 저장; mono/PCM 16bit·44.1kHz의 Unity용 편집본은 별도로 준비 |
가중치와 필요한 tokenizer·processor 파일은 게임 저장소 밖의 로컬 모델 캐시에 둔다. 최초 확보는 개발 단계의 모델 다운로드이며 앱의 네트워크 기능이 아니다. 고정 revision의 필요한 파일을 완전히 확보한 뒤 로컬 스냅샷 경로로 모델을 로드하여 구성 요소의 버전이 섞이지 않도록 한다. 설치된 Qwen wrapper에서 모델과 processor에 revision 인자가 모두 전달된다고 가정하지 않는다. 캐시 경로는 실행 환경에서 찾고 기기별 절대 경로를 게임이나 공용 제작 프로필에 박아 넣지 않는다.
모델·목소리·언어·GPU 실행이 준비되지 않으면 오류 원인과 미검증 상태를 남긴다. 다른 제작 구성을 자동 선택하지 않는다. Unity 기능 개발은 기존 정책에 따라 임시 파일로 진행할 수 있지만 Qwen 제작·최종 음성 항목은 완료로 표시하지 않는다. CUDA 연산 확인만으로 실제 GPU 추론을 통과시키지 않는다.
**개발하면서 만들 파일과 명령**은 다음과 같다. 현재 파일이나 실행 결과가 존재한다는 뜻이 아니다.
| 제작 대상 | 역할 |
|---|---|
| `tools/audio/generate_voice.py` | 기존 환경에서 대본 선택 생성·일치 파일 재사용·파일 검사·생성 이력 기록; Unity 실행 코드와 분리 |
| `SourceAssets/Audio/Voice/voice-catalog.json` | `schemaVersion=1`, `clips` 배열. 각 항목은 고유 `id`, 비어 있지 않은 `text`(발화할 한국어 전체 문장), `usage`(화면/행동 설명), `stageIds`(1~30의 관련 번호), `required`(필수 여부)를 가짐 |
| `SourceAssets/Audio/Voice/qwen-profile.json` | 모델 ID/revision, 언어/목소리, 말투 지시, device/dtype/attention, seed와 실제 생성 설정; 환경별 캐시 절대 경로와 비밀 값은 포함하지 않음 |
| `SourceAssets/Audio/Voice/generated/<runId>/` | 생성 회차별 원본 WAV와 `manifest.json`; ID·대본·프로필·결과의 대응을 보존 |
| `SourceAssets/Audio/Voice/edited/` | 채택한 편집 후보·편집 내역; 생성 원본을 덮어쓰지 않음 |
| `Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/` | 청각/형식 검수가 끝나 게임에 연결할 ID별 WAV |
카탈로그에 `count_01`~`count_10`과 앞 대본표의 안내/실제 도움 문구를 기록한다. 같은 클립을 여러 스테이지에서 사용할 수 있다. 개별 회차에서는 `--ids`로 해당 단계에 필요한 ID만 생성하며, 전체 제작 완료 검사는 카탈로그 전체의 필수 항목을 대상으로 한다. 선택하지 않은 후속 단계의 음성이 첫 샘플 검사에서 없다는 사실과 전체 제작의 누락은 구분한다.
생성 프로필의 필드는 `schemaVersion=1`, `modelId`, `modelRevision`, `language`, `speaker`, `instruct`, `device`, `dtype`, `attention`, `seed`, `generationParameters`다. `seed`는 초기값 42로 두고 생성 설정과 함께 조정할 수 있다. `generationParameters`에는 실제 적용한 생성 인자와 모델의 기본 설정을 기록한다. 특정 seed가 음질을 보증하거나 장치/라이브러리가 달라도 동일한 WAV를 보장한다고 주장하지 않는다.
manifest는 `schemaVersion=1`, 고유 `runId`, 생성 시각, 실제 패키지·장치 정보, 고정 모델 revision과 카탈로그/프로필 해시, `clips` 결과를 가진다. 각 클립은 `id`, 대본 해시, 해당 생성 설정의 해시, `result`(`generated`·`reused`·`failed`), 원본 생성 회차/파일 참조를 기록한다. 성공 결과에는 WAV 해시·실제 sample rate·채널·샘플 수·길이, 실패에는 오류를 남긴다. 생성 성공 표시는 청각 검수 통과 표시가 아니다. 채택한 원본과 Unity 편집본·검수 결과는 자산 기록에서 연결한다.
파일 재사용은 ID만으로 판단하지 않는다. 같은 대본·모델 revision·목소리·말투·실제 생성 설정과 파일 해시가 일치하는 원본만 재사용하며 원래 생성 이력을 참조한다. 문구·말투·모델 설정이 바뀌면 바뀐 클립을 새 회차의 후보로 생성하고 기존 채택 파일을 조용히 덮어쓰지 않는다. 생성 실패·중단에서 빈 WAV를 정상 파일로 등록하지 않는다. 새 파일이 검수될 때까지 기존 채택본은 유지하되, 대본/프로필이 바뀌어 현재 조건과 맞지 않는 기존본을 최신 검수 통과로 처리하지 않는다.
다음은 구현할 진입점의 계약이다. 기본 샘플 → 부분 확장 → 전체 생성 → 파일 검사 순으로 사용하며 실제 실행 후에만 증거를 기록한다.
```powershell
conda run --no-capture-output -n qwen3-tts python tools/audio/generate_voice.py --catalog SourceAssets/Audio/Voice/voice-catalog.json --profile SourceAssets/Audio/Voice/qwen-profile.json --ids count_01,count_04,count_07,count_10,guide_collect
conda run --no-capture-output -n qwen3-tts python tools/audio/generate_voice.py --catalog SourceAssets/Audio/Voice/voice-catalog.json --profile SourceAssets/Audio/Voice/qwen-profile.json
$qwenVoiceManifest = '<생성 도구가 출력한 manifest.json 경로>'
conda run --no-capture-output -n qwen3-tts python tools/audio/generate_voice.py --catalog SourceAssets/Audio/Voice/voice-catalog.json --profile SourceAssets/Audio/Voice/qwen-profile.json --validate-only --manifest $qwenVoiceManifest
```
`--ids`를 생략하면 카탈로그 전체를 생성한다. `--validate-only`는 명시한 manifest와 현재 카탈로그/프로필에 대해 파일·이력 검사만 하고 모델을 로드하거나 다운로드하지 않는다. 형식 오류·선택한 필수 ID 누락·생성 실패·해시 불일치는 0이 아닌 종료 코드와 ID별 오류로 보고한다. 검사 범위가 부분 ID인지 전체 필수 목록인지 기록한다.
**검증 순서와 합격 기준**은 다음과 같다.
1. 환경·패키지/import → CUDA 및 실제 장치 연산 → 고정 모델 로딩 → 실제 GPU 생성의 결과를 각각 기록한다. 생성 시간·사용 장치·메모리·반환 sample rate와 로그를 남긴다. 설치 확인으로 이후 검사를 통과시키지 않는다.
2. ‘하나·넷·일곱·열’과 수집 안내를 첫 샘플로 생성하여 숫자 발음·짧은 말의 잘림·Sohee 목소리·친근한 말투를 듣고 확인한다. 문제가 있으면 같은 승인된 모델/목소리에서 대본·말투·생성 설정을 조정해 다시 검수한 뒤 전체 생성으로 확대한다.
3. 자동 파일 검사는 카탈로그 ID/본문/번호 유효성, 선택 범위의 필수 ID, WAV 읽기·양수 길이/sample rate·유효 채널·유한 파형, 파일/대본/프로필 해시와 생성 결과를 확인한다. 원본의 반환 sample rate를 그대로 기록하며 편집본만 전달 규격에 맞춘다. 무음·클리핑 후보는 음질 검수 대상으로 보고하고 잘못된 발음을 자동 통과로 판정하지 않는다.
4. 모든 숫자 10개와 실제 게임에서 사용하는 안내·도움 문장을 청각 검수한다. 다른 단어/숫자·발음 오류·문장 잘림·음색 불일치·과장된 소리가 남으면 재생성/편집한다. 이 검수 결과와 채택 파일 해시를 자산 기록에 남긴다.
5. 채택 원본을 보존한 채 앞뒤 무음·음량을 정리하고 Unity용 WAV를 만든다. 현재 카탈로그/프로필과 채택본의 대응, 파일 ID, mono/PCM 16bit·44.1kHz의 초기 전달 규격을 확인한다. 반환 sample rate를 표기만 바꾸거나 새로 샘플링했다고 원래 품질이 올라간 것으로 간주하지 않는다.
6. Unity에서 숫자 읽기·안내·순차 재생·마지막 계수·음악 혼합·전환 정리를 검사하고, 같은 최종 APK의 AVD 오프라인 재생으로 확인한다. 제작 PC에서 Qwen이 실행됐다는 사실과 앱에서 올바른 파일이 재생된다는 사실을 별도 증거로 남긴다.
공식 코드/선택 모델 revision의 라이선스와 모델 카드·목소리 출처를 확인해 기록한다. 모델이 Apache-2.0으로 표시되어 있다는 사실을 음성 결과물의 독점권이나 음질 보증으로 바꾸지 않는다. 제작 도구에 원본 음성 입력·복제를 요구하지 않으며 Qwen 패키지·모델 캐시는 APK에 넣지 않는다. 기능 개발의 전체 검증 진입점은 기존 `tools/verify.ps1` 계약을 유지하고, 음성 제작 도구 검사는 각 단계에서 파일 증거를 제공한다.
#### Unity 오디오 연결
- Mixer는 Master 아래 Voice·SFX·Music으로 나눈다. Presenter가 확정된 조작/상태에 대응하는 소리 ID를 전달하며 오디오 재생 완료가 수학 상태·성공 판정을 결정하지 않는다. 숫자 읽기·안내·UI 소리는 카메라 회전으로 작아지지 않는 2D 재생을 기본으로 한다.
- 빠른 계수 탭은 입력과 계수 표시를 즉시 확정하고 숫자 음성을 계수 순서대로 재생한다. 같은 구를 다시 누르면 새 계수 음성을 추가하지 않는다. 최대 10개 안에서 음성이 서로 겹치거나 중간 숫자가 누락되지 않게 한다.
- 성공 후 같은 보드에서 자유 놀이로 넘어갈 때 대기 중인 마지막 숫자 읽기는 끝까지 재생한다. 축하 소리와 안내가 이를 덮지 않도록 낮추거나 뒤로 배치한다. 성공 후 자유 놀이의 새 조작도 막지 않는다.
- 화면/스테이지 전환과 앱 일시 정지에는 이전 음성 큐·효과를 정리한다. 복원 시 지난 숫자·성공 안내를 다시 재생하지 않는다. 상태가 바뀐 뒤에는 이전 상태용 대기 안내를 취소하며 계수의 숫자 순서를 안내로 끊지 않는다.
- 음성 중에는 음악을 낮추고 효과음도 발음을 덮지 않게 섞는다. 오답에 큰 경고음이나 벌점 느낌을 넣지 않는다. 재생 우선순위·음량·짧은 반복 제한은 듣고 조정한다. 별도 설정 화면 추가는 이번 자산 계획에 포함하지 않는다.
- 원본은 WAV로 보관한다. 짧은 효과음·숫자 음성은 mono·Decompress On Load·Preload Audio Data를 초기값으로 사용하고 긴 음악은 Streaming을 초기값으로 둔다. 실제 길이·메모리·지연을 Profiler와 Android 재생으로 확인해 조정한다. 압축된 원본을 다시 무손실로 바꿨다고 품질이 복구된 것으로 간주하지 않는다.
#### 원본·출처·진행 기록
개발 단계에서 `SourceAssets/Models/`의 .blend, `SourceAssets/Images/`의 편집 원본, `SourceAssets/Audio/`의 원음·대본·편집 자료를 보관한다. AI 입력/채택 결과/정리 과정을 `SourceAssets/Generation/`에 기록한다. Unity가 가져오는 FBX·PNG·WAV 및 재질·프리팹은 `Assets/MagnetismHeroes/UnityRuntime/Presentation/` 아래 Models·Textures·Materials·UI·VFX·Audio·Prefabs에 둔다. 제작 원본은 빌드 포함 대상으로 참조하지 않는다. TTS 모델·가상 환경·도구 캐시는 게임 소스/앱에 넣지 않는다.
개발하면서 `docs/asset-register.md`에 실제 자산 목록과 출처를 만든다. 자산 ID는 고유하며 최소한 다음 열을 기록한다.
| 기록 | 내용 |
|---|---|
| 식별·연결 | 자산 ID, 유형, 원본/Unity 파일 경로, 사용 화면·스테이지·용도 ID |
| 출처·권리 | 제작자/제공자, 원본 URL 또는 자체 제작, 확보 날짜, 라이선스·조건 증거와 필요한 고지 |
| 생성·수정 | 도구/모델 버전, 대본/프롬프트·설정, 채택 파일, 편집 내역; 도구가 제공하는 재현 정보도 보관 |
| 선택·검수 | 사용자 선택이 필요한 음원의 선택 결과, 임시/최종 구분, import·화면·소리 검수 결과와 증거 경로 |
CC0 등 무료 자산도 실제 파일과 해당 출처의 조건을 기록한다. AI 결과에 일괄적으로 독점권이나 무조건적인 상업 이용 가능성을 부여하지 않는다. 필요한 사용 고지는 개발 단계의 `ThirdPartyNotices.txt` 등 배포 가능한 파일에 모은다. 임시 자산이 정상 재생되었다는 기록과 최종 파일의 권리·품질 검수 완료를 구분한다.
#### 단계별 자산 완성 기준
| 단계 | 자산 작업과 완료 판단 |
|---|---|
| T2·T3 | 첫 구·얼굴·숫자·기본 UI·입체 임시 배경과 ‘하나’ 음성 연결; Android에서 포함 파일·한글·오프라인 재생을 조기 확인. 임시 소품·음원은 최종 완료가 아님 |
| T4 | 대표 합체·분리의 접촉빛·선택·표정/망토 전환 시제품; 실조작에 연결해 구/표시 보존 확인, 필요한 효과음 요청 |
| T5·T6 | 자유 놀이·도움·시범용 음원/대본 요청; 3개 테마의 입체 세트와 30개 배치 연결, 모든 구의 발견·조작 가능성 확인 |
| T7 | 사용자 선택 음원과 Qwen3-TTS 생성 파일, 최종 얼굴·UI·모델·재질·망토·VFX를 통합; 임시 파일 교체와 전체 출처 기록·품질 검수 완료 |
| T8 | 최종 자산을 포함한 동일 APK로 세 테마·오프라인 소리·연속 조작·복원을 검사; 실제 기기 성능 판정은 후속으로 구분 |
자산 합격 기준은 다음과 같다. 픽셀 모양을 고정하는 자동 검사나 테스트 수 늘리기로 시각·청각 검수를 대신하지 않는다.
- **모델·재질:** FBX 방향/스케일/피벗이 맞고 분홍색 오류 재질·뒤집힌 면·필수 참조 누락이 없다. 3개 테마가 실제로 구별되며 모든 배치에서 회전해 자석을 발견·선택할 수 있다.
- **얼굴·UI·효과:** 투명 가장자리·한글 글리프·숫자 잘림을 화면 비율별로 확인한다. 1~10과 직선/평면/입체 배열의 구 경계·대표 얼굴/숫자/망토 개수, 선택·계수 가독성, 연속 조작/취소/복원 정리를 R11대로 확인한다.
- **오디오:** 요청 용도와 실제 파일/대본이 일치하고 모든 숫자·안내를 들어 발음·잘림·잡음·음량을 확인한다. 빠른 계수 입력의 순서·마지막 숫자·오래된 안내 취소를 검수한다. 반복 음악은 실제 게임에서 최소 3회 반복하며 경계의 끊김·클릭음을 확인한다.
- **원본·권리·빌드:** 최종 사용 파일에 출처/조건 기록이 있고 필요한 고지를 포함한다. 임시·미선택·누락 파일을 완료 처리하지 않는다. 앱에 TTS 모델/SDK/키/다운로드 경로가 없어도 오프라인 재생되며 실제 빌드 포함 파일을 검사한다.
- **모바일 비용:** 처음에는 공유 재질·텍스처와 적은 입자로 구성한다. 얼굴/UI 텍스처는 512~1024px을 초기값으로 두고 결과에 맞춰 조정한다. 구 10개·망토/연결 빛·음성·음악·가장 많은 소품의 상태에서 드로우콜·투명 오버드로·메모리·음성 로딩·프레임 비용을 기록한다. 이 조정값이나 AVD의 FPS를 실제 스마트폰 합격 기준으로 선언하지 않는다.
### 고정 정의와 플레이 상태
| 구분 | 제작·저장 방식 | 수명 |
|---|---|---|
| 고정 스테이지 | Inspector에서 편집하는 ScriptableObject; R07·R12 필드와 배치 | 제작 시 변경, 실행 시 읽기 전용 |
| 런타임 정의 | 고정 자산을 변환한 순수 C# 데이터 | 스테이지 실행에 사용 |
| 플레이 상태 | 구 ID·위치·묶음·수집·계수·카메라 | 조작을 확정할 때 변경 |
| 이어하기 | R12의 JSON 버전 1, 퍼즐별 슬롯과 최근 자유 놀이 하나 | 앱 재실행 후 복원 |
| 화면과 연출 | 상태에서 계산한 얼굴·숫자, 임시 선택·애니메이션 | 다시 생성 가능, 별도 저장하지 않음 |
Inspector 편집과 빌드 전 검사에서 총량·빈 묶음·잘못된 번호·누락된 배치 참조를 발견한다. 실제 정답은 Core 규칙으로 판정한다. 편집기 검사를 통과했다고 구를 실제로 발견·조작할 수 있다고 판정하지 않는다.
## 4. 기능별 기획에서 구현까지의 반복 절차
각 기능은 아래 항목을 계획의 해당 작업 또는 작업 변경 설명에 먼저 정리한다. 별도의 기획 시스템이나 대규모 문서 묶음을 만들지 않는다.
1. **학습·놀이 목적과 요구사항:** 어떤 아이의 행동을 가능하게 하는지와 R번호를 적는다.
2. **입력과 상태 변화:** 시작 상태, 아이의 조작, 확정 시점, 결과 상태, 화면·소리 반응을 적는다.
3. **불변 조건과 경계:** 수량·ID 보존, 무효 선택, 조작 충돌, 저장·재진입의 기대 결과를 적는다.
4. **합격 사례:** 준비 상태/행동/기대 결과를 작성하고 자동 검사와 수동 플레이 검사를 구분한다.
5. **최소 구현과 연결:** 규칙과 그 경계를 검사한 뒤 프리팹·UI를 연결한다. 한 기능의 정상 흐름이 플레이되도록 만든다.
6. **검증과 수정:** 해당 기능의 검사와 대표 플레이를 수행한다. 실패하면 원인을 수정하고 관련 검사를 반복한다. 단계가 끝나면 전체 회귀 검사를 수행한다.
수학 상태·저장·입력 충돌처럼 오류 비용이 큰 부분은 합격 사례를 자동 테스트로 먼저 구현한다. 색·장식·단순 문구마다 구현을 그대로 복제하는 테스트를 만들지 않는다. 버그 수정에는 재현 가능한 검사나 수동 재현 절차를 남긴다. 기능 이름만 있고 판정할 결과가 없으면 구현 준비가 끝난 것이 아니다.
### 대표 기능의 설계 카드
| 기능 / 근거 | 준비 상태와 행동 | 확정 결과 | 경계·검증 방법 |
|---|---|---|---|
| 모아 세기 / R05 | N개가 있는 놀이터에서 직접 끌어 모으고 각각 탭 | 전부 수집·한 묶음·전부 계수할 때만 성공 | 1번도 수집 필요; 미수집/중복 탭 거부. Core 검사 + 실제 화면 |
| 합체 / R02·R06 | 2와 3인 친구를 가까이 끌기 | 구 ID 5개를 보존한 친구 하나, 얼굴·숫자 5 | 이동 프레임마다 중복 합체하지 않음. 규칙 + 입력 연결 |
| 분리 / R04 | 5에서 나누기, 떨어진 구 2개 선택 후 끌기 | 선택 2와 나머지 3, 얼굴·숫자 각각 하나 | 0개·전체 선택은 불변; 선택만 해서는 분리 안 됨 |
| 입체 조작 / R03 | 친구/빈 공간/UI 위에서 터치를 시작 | 친구 이동/시점 회전/UI 조작을 구분 | 핀치·취소·영역 밖 놓기에서 구 소실 없음. PlayMode + Android |
| 성공과 자유 놀이 / R08·R09 | 목표 구성을 만들기 | 완료를 한 번 확정하고 같은 공간 자유 놀이 | 잘못된 구성은 계속 조작; 공급 11번째 거부, 0개에서 복구 |
| 이어하기 / R10·R12 | 두 퍼즐과 자유 놀이를 별도로 변경 후 재실행 | 세 상태와 ID·배치·계수·카메라 각각 복원 | 재플레이 완료 기록 유지, 손상·저장 중단 검사 |
| 도움 / R08 | 현재 수집·계수·구성 상태에서 도움 누르기 | 지금 가능한 대상/첫 행동 강조 | 자동 정답 실행 없음; 현재 상태에 맞는 안내 검사 |
단계별 기능 목록은 위 카드 형식을 사용하되 합격 사례는 기획서의 전체 경계를 포함한다. 예를 들어 합체 테스트 하나가 저장·분리·힌트까지 대신 검증하지 않는다.
## 5. 개발·테스트 환경의 역할
**주된 반복 작업은 Unity 에디터에서 수행한다. Android 검증은 첫 퍼즐, 중요한 연결 변경, 최종 완성 시점에 수행한다.**
| 환경 | 확인하는 내용 | 실행 시점 |
|---|---|---|
| EditMode | 순수 C# 규칙·상태 전이·스테이지 정의·저장 검증 | 규칙·데이터·저장 변경마다 관련 검사, 단계 종료 시 전체 |
| PlayMode | 입력과 규칙 연결, 프리팹·숫자·얼굴, 전환·복원, UI 입력 차단 | 기능 연결마다 관련 검사와 수동 플레이 |
| Game View / Device Simulator | 가로 화면, 스마트폰·태블릿 화면 비율, 안전 영역과 글자 | UI·카메라·배치 변경 시 |
| Android Studio AVD의 실제 APK | 빌드·설치·앱 실행·Android 파일 저장·재시작·오프라인 | 첫 퍼즐에서 조기 검사; 입력·저장·빌드 설정 변경 시 관련 재검사; 최종 전체 |
| 실제 Android 기기 | 실제 다중 터치·손맛·가독성·성능·발열·배터리 | 기기 준비 후의 후속 범위 |
Input System의 마우스 터치 시뮬레이션은 단일 포인터 개발을 돕는다. 핀치는 테스트 입력으로 두 접점을 주입하고 에뮬레이터의 지원 입력으로 연결도 검사한다. 마우스 한 개로 플레이했다는 이유로 실제 두 손가락 조작을 검증했다고 기록하지 않는다.
Unity Device Simulator는 화면·안전 영역 검사 도구다. Android Studio AVD는 APK를 설치하는 별도의 운영체제 환경이다. 둘을 같은 검증으로 처리하지 않는다. Device Simulator는 대상 그래픽 백엔드와 실제 기기 성능을 재현하지 않는다. [Unity Android 시뮬레이터 설명](https://docs.unity3d.com/6000.6/Documentation/Manual/android-device-simulator.html)
프로젝트를 열어 둔 에디터와 별도 배치 프로세스가 같은 프로젝트를 동시에 수정하지 않도록 검사 진입점에서 실행 방식을 정리한다. 정상 개발 반복은 열린 에디터의 Test Runner를 사용할 수 있고, 단계별 결과를 기록할 때는 같은 테스트 목록으로 검증 진입점을 사용한다.
## 6. 테스트 합격 기준과 증거
합격 기준은 **요구사항 → 실패할 수 있는 경계 → 검사 사례 → 실행 증거** 순서로 만든다. 테스트 개수나 코드 커버리지 수치만으로 제품 완료를 선언하지 않는다.
| 대상 | 필수 합격 기준 | 증거 |
|---|---|---|
| 규칙 | 1~10의 유효 합체·분리에서 구 ID 집합 보존; 구마다 소속 하나; 빈 묶음 없음 | EditMode 결과 |
| 분리 경계 | 크기 1~10의 모든 비어 있지 않은 진부분집합 선택 검사; 0개·전체·다른 묶음 선택 거부 | 매개변수화 테스트; 실제 분리 연결은 별도 |
| 정답·계수 | [2,2]와 [4] 구분; 중복 탭 불가; 수집·한 묶음·계수 조건 모두 필요 | Core·진행 검사 |
| 스테이지 | R07과 30개 정의 일치; 번호·총량 유효; 실제 규칙 호출로 모든 목표에 도달 가능 | 정의 비교 + 해답 경로 검사 |
| 입력·표현 | 같은 터치를 UI/이동/회전/계수에 중복 사용하지 않음; 취소·핀치 전환 복구; 표시 수량 일치 | PlayMode 결과 + 플레이 기록 |
| 합체 영웅 | 2+3→5와 5→2+3의 얼굴·숫자·망토 수 일치; 1~10 구 보존; 다른 모양의 5가 같은 판정; 선택·계수 표시 식별; 연속 조작·복원 후 잔상 정리 | PlayMode 상태 연결 검사 + 모바일 화면 비율에서 수동 외형 검사 |
| 저장 | 두 퍼즐+자유 놀이 독립 복원; 성공 전이 일관성; 재플레이 완료 기록 불변; 저장 중단·손상 복구 | 파일 검사 + Android 재실행 증거 |
| 자산 | 원본 정의 불변; Missing Script/참조 누락 없음; 한국어 글자·필수 음성 실제 포함·오프라인 재생 | 자산 검사 + 화면·소리 점검 |
| 자산 제작·인수 | 세 테마 실제 구별·모든 구 접근 가능; 최종 파일의 출처·사용 조건·검수 기록; 사용자 선택 음원·로컬 TTS 출력 연결; 순차 발화·마지막 숫자·음악 반복·전환 정리 | 자산 목록 + 에디터 검수 + Final APK 오프라인 재생 기록 |
| Android 조기 | 첫 퍼즐 설치·실행·미완료 위치/카메라/계수 복원·완료 후 자유 놀이 진입 | FirstSlice APK·기기·로그·화면 |
| Android 최종 | 대표 흐름과 저장·해금·오프라인을 완성 APK에서 재검사 | Final APK·로그·화면·체크 결과 |
전체 목표 도달 검사는 단순히 목표 배열을 정답 함수에 넣는 방식으로 대체하지 않는다. 초기 상태에서 실제 합체·분리 호출을 통해 도달하고, 수 세기는 수집·계수 호출을 모두 수행한다. 시각적인 발견 가능성과 조작 가능한 배치는 화면에서 별도 확인한다.
해당 단계의 필수 검사 실패 0개, 필수 검사 누락·미실행 0개를 합격 조건으로 삼는다. 전체 테스트 실행 수가 0이면 성공이 아니다. 정상 플레이에서 처리되지 않은 예외와 예상하지 못한 오류 로그가 없어야 한다. 손상 저장 같은 부정 사례의 의도된 실패는 테스트에서 기대 결과로 확인한다.
각 단계 기록은 요구사항/사례 ID, 실행 시각, 에디터·패키지 버전, 소스 커밋과 미커밋 변경 유무, 실행 명령, 실행·통과·실패·미실행 수, 로그 위치를 가진다. Android 기록에는 APK 해시, AVD/API/ABI/그래픽 설정, 설치·실행 여부, 동작별 결과와 화면 증거를 추가한다. 증거를 만든 후 소스나 자산이 달라지면 영향을 받는 검사와 최종 빌드를 다시 수행한다.
실제 기기가 없으므로 FPS·발열 합격을 이번 에뮬레이터 수치로 정의하지 않는다. 최대 10개 상태에서 Unity Profiler로 프레임·할당·렌더링·음성 비용을 관찰하고 명확한 불필요 비용을 줄인다. 실제 기기 성능 기준과 최종 판정은 후속 검증에서 정한다. 탭/끌기 임계값, 흡착 거리, 카메라 범위, 연출 시간은 조정 가능한 값으로 관리한다.
## 7. 검증 진입점의 계약
공통 준비에서 `tools/verify.ps1`을 만들고 아래처럼 프로젝트 루트에서 실행한다. 아래 명령은 현재 존재하는 명령이 아니다.
```powershell
pwsh -NoProfile -File tools/verify.ps1 -Phase EditMode -Milestone FirstSlice
pwsh -NoProfile -File tools/verify.ps1 -Phase PlayMode -Milestone FirstSlice
pwsh -NoProfile -File tools/verify.ps1 -Phase AndroidBuild -Milestone FirstSlice
pwsh -NoProfile -File tools/verify.ps1 -Phase AndroidSmoke -Milestone FirstSlice
pwsh -NoProfile -File tools/verify.ps1 -Phase EditMode -Milestone Final
pwsh -NoProfile -File tools/verify.ps1 -Phase PlayMode -Milestone Final
pwsh -NoProfile -File tools/verify.ps1 -Phase AndroidBuild -Milestone Final
pwsh -NoProfile -File tools/verify.ps1 -Phase AndroidSmoke -Milestone Final
```
`Phase`는 위 네 값, `Milestone`은 `FirstSlice`, `Composition`, `Persistence`, `Content`, `Final`이다. 생략한 Milestone은 Final이다. 초기 단계는 그때 구현한 기능의 사례만 대상으로 삼고, Final은 기획서 전체를 검사한다. 초기 결과를 최종 전체 결과로 재사용하지 않는다.
| 단계 | 남길 결과 |
|---|---|
| EditMode | `artifacts/verification/<Milestone>/editmode.xml`, 실행 로그, 검사 범위 |
| PlayMode | `artifacts/verification/<Milestone>/playmode.xml`, 실행 로그, 검사 범위 |
| AndroidBuild | `artifacts/android/<Milestone>/MagnetismHeroes.apk`, 빌드 로그·설정·해시 |
| AndroidSmoke | `artifacts/verification/<Milestone>/android-smoke.md`, 기기 설정·로그·화면 증거 |
마지막 APK는 `artifacts/android/MagnetismHeroes.apk`에도 제공하며 Final 검사 APK와 해시가 같아야 한다. 에디터·Android 도구는 설치 환경에서 찾고 특정 사용자 폴더를 소스에 고정하지 않는다. 프로세스 종료 코드뿐 아니라 검사 결과 파일·실행 수·실패·누락과 APK 존재도 확인한다.
AndroidSmoke는 APK 설치·앱 실행과 재현 사례의 증거를 모으는 진입점이다. 자동화할 수 없는 화면·소리 사례는 번호가 있는 수동 절차와 결과로 확인하며, 설치 성공만으로 이를 자동 통과시키지 않는다. 에뮬레이터 실패·미실행은 비정상 종료와 이유를 남긴다. ABI 불일치·가상화·그래픽 오류는 제품 규칙 실패와 구분하되 완료된 검사로 표시하지 않는다.
## 8. 단계별 구현 작업
### T1 — 순수 C# 규칙과 상태 계약
**목표:** Unity 없이 구·묶음·계수·정답·공급 규칙을 검증하고 이후 단계의 공통 데이터와 진행/저장 경계를 마련한다.
**근거:** R02, R04, R05, R06, R09, R12, R13.
**작업 대상:** `Assets/MagnetismHeroes/Core/`, `Tests/EditMode/Core/`.
**동작 계약:** 값은 구 개수에서 계산하고 구마다 묶음 하나를 보장한다. 합체·분리는 ID 집합을 보존한다. 무효 선택은 상태를 바꾸지 않는다. 목표 다중집합을 비교하고 수집·계수 ID는 이동·합체·분리로 초기화하지 않는다. 자유 놀이 공급·반납은 0~10 경계를 지키며 퍼즐 중에는 거부한다. R12의 정의·저장 데이터 필드와 제약을 보존한다.
**검증:** 모든 유효 분할, 다른 묶음 ID·중복 ID·빈 묶음·[2,2]/[4]·계수 경계·10개 한도를 검사한다. Core에 Unity 참조가 없고 EditMode가 통과해야 한다.
**공유 작성:** 공통 패키지·어셈블리·씬은 실행 담당자가 관리한다. 이 단계는 전용 Core 코드와 검사만 작성한다.
### T2 — 1번 수 세기의 첫 완성 흐름
**목표:** 1번을 시작해 직접 수집·계수하고 성공 후 같은 공간의 기본 자유 놀이로 들어가는 작동하는 흐름을 만든다. 성공 전 저장과 재시작 복원도 연결한다.
**근거:** R01, R02, R03, R05, R08, R09, R10, R11, R12, R13, R14.
**작업 대상:** Core의 진행 제어, `UnityRuntime/Interaction/`, `StageAssets/`의 1번 정의, `Presentation/`의 기본 화면·친구, `Persistence/`의 기본 저장, Qwen 생성 도구·초기 대본/프로필과 샘플 WAV, 대응 EditMode·PlayMode 검사.
**동작 계약:** 시작/선택에서 1번을 실행한다. 한 구도 직접 끌어 수집하고 한 번 눌러야 성공한다. 친구 이동·빈 공간 회전·핀치를 구분하고 UI 입력을 차단한다. 성공 전 구·카메라·계수 상태를 저장하며 성공 후 완료·퍼즐 제거·자유 놀이 생성은 같은 변경으로 처리한다. 숫자·얼굴과 같은 공간 전환을 최소 표현으로 연결한다.
**검증:** 1번 수집 생략 거부, 미완료 저장·복원, 중복 성공 거부, 카메라·조작 취소, 기본 자유 놀이 전환의 EditMode·PlayMode 및 실제 에디터 플레이를 통과한다.
**단계 범위:** 첫 저장부터 R12의 JSON 버전 1과 전체 필드 경계를 사용한다. 지원하는 플레이 경로는 먼저 1번이고 나머지는 T4~T6에서 확장한다. 임시 폰트·외관·소리와 제한된 콘텐츠를 최종 R11·30개 완성으로 표시하지 않는다.
구·기본 얼굴·숫자·UI·단순 입체 배경을 만들고, ‘하나’ 및 기본 안내 대본과 첫 효과음 요청을 준비한다. Qwen 환경의 GPU 지원을 정비하고 생성 도구·초기 카탈로그/프로필을 만들어 한국어 샘플과 필요한 첫 음성을 생성·검수한다. 중복 ID·잘못된 본문·누락/손상 WAV·대본/프로필 불일치의 파일 검사와 부분 재생성의 기존본 보존을 확인한다. 포함 파일의 한글·음성 재생을 연결하며, Qwen 출력이 준비되기 전의 임시 소리 검사와 최종 파일 검수를 구분한다.
**공유 작성:** 기능 코드는 전용 경로에 작성한다. 실행 담당자가 시작/플레이 씬·참조를 연결하고 FirstSlice 검사 범위를 기록한다.
### T3 — 첫 Android APK와 가상 기기 검사
**목표:** 첫 퍼즐을 실제 Android 앱으로 설치·실행하고 파일 저장·재시작을 확인해 플랫폼 문제를 일찍 발견한다.
**근거:** R01, R03, R05, R09, R10, R11, R12, R13, R14.
**작업 대상:** `Editor/`의 빌드 진입점, `tools/verify.ps1`의 Android 단계, FirstSlice 결과 경로, Android AVD.
**동작 계약:** AVD를 실행하고 adb 연결·ABI·그래픽 지원을 확인한다. APK의 대상 설정과 일치시켜 빌드·설치한다. 가로 화면·한국어 기본 표시·1번 수집/계수·같은 공간 자유 놀이를 확인한다. 성공 전 이동·카메라 변경을 저장하고 종료·재실행해 복원한다. 오프라인 실행을 확인한다.
**검증:** FirstSlice AndroidBuild·AndroidSmoke 실제 통과, APK·해시·로그·화면·절차별 결과를 남긴다. 빌드 성공만으로 실행 성공을 표시하지 않는다.
기본 모델·재질·글꼴과 준비된 음성/효과음이 APK에 포함되고 오프라인에서 재생되는지 확인한다. 이 단계에 사용한 임시/최종 자산과 아직 제공되지 않은 파일을 기록한다. 세 테마·최종 음악 전체의 검수는 T6~T8에서 수행한다.
1번은 한 번 세면 바로 성공하므로 성공 전 저장의 계수 목록은 비어 있다. FirstSlice에서는 이 빈 목록과 미완료 배치 복원을 검사한다. 일부 구만 센 미완료 상태의 복원은 T5의 저장 검사와 T6·T8의 N≥2 실제 스테이지 검사에서 확인한다.
**이유와 순서:** 첫 퍼즐의 Android 검사 후 확장하는 순서는 사용자와 정한 개발 방식이다. 환경 문제가 막으면 원인·미검증 항목을 보고하고 원인을 해결한다. 이 단계 실패를 숨겨 완료 처리하지 않는다.
**공유 작성:** 빌드 설정과 검사 진입점은 실행 담당자가 통합한다. 이 결과는 최종 검사를 대체하지 않는다.
### T4 — 합체·분리·혼합 퍼즐의 완성 흐름
**목표:** 6번·10번·14번을 대표로 합체·분리·혼합 조작을 기존 플레이 흐름에 추가한다.
**근거:** R02, R03, R04, R06, R07, R09, R11, R12, R13, R14.
**작업 대상:** Core 진행 제어 확장, Interaction과 기본 Presenter·프리팹, 대표 정의 자산, 대응 검사.
**동작 계약:** 가까운 친구에 붙이면 한 묶음이 된다. 나누기 → 구 선택 → 끌어내기로 선택/나머지를 각각 한 친구로 모은다. 떨어진 선택도 허용하며 분리 직후 의도하지 않은 재합체를 방지한다. 무효 선택은 상태 유지, 취소는 확정 상태 복구다. 14번은 행동 이력과 무관하게 최종 [2,2]로 판정한다. 회전해 공중 모든 방향을 편집할 수 있다.
**검증:** 6·10·14번 실제 규칙 해답과 화면 연결, 0개/전체/떨어진 선택, 핀치 전환·영역 밖 취소, 얼굴·숫자 일치를 검사한다. Composition EditMode·PlayMode와 변경된 입력의 Android 사례를 확인한다. 제작 중 대표 번호 직접 실행은 개발용 검사에만 사용하며 최종 순차 해금을 우회하지 않는다.
접촉 빛·선택·얼굴/망토 통합과 분리의 시제품을 실조작에 연결한다. 합체·분리 효과음을 요청하고 시범 대본을 Qwen 카탈로그에 추가해 생성·검수한다. 취소·연속 입력에서 상태보다 앞선 효과가 남지 않는지 확인한다. 최종 외형은 T7에서 마감한다.
**공유 작성:** 실행 담당자가 단계 씬을 갱신한다. 이전 1번 흐름과 FirstSlice 검사도 유지한다.
### T5 — 저장·진행·자유 놀이 전체 기능
**목표:** 첫 저장을 모든 미완료 슬롯과 최근 자유 놀이로 확장하고 10개 공급·반납과 진행의 수명을 완성한다.
**근거:** R08, R09, R10, R12, R13.
**작업 대상:** Core 진행/저장 경계, `UnityRuntime/Persistence/`, 관련 화면·검사.
**동작 계약:** 퍼즐별 하나씩 저장하고 자유 놀이는 전체 하나만 남긴다. 완료 기록은 재플레이로 취소되지 않는다. 공급·묶음 반납은 자유 놀이에서만 가능하며 0개 상태를 복구할 수 있다. 조작/카메라 확정·화면 이동·앱 일시 정지에서 저장한다. 성공 관련 상태는 일관되게 교체한다. 유효본 복구·손상 파일 보존·실패 기록을 제공한다.
**검증:** 두 미완료 퍼즐과 자유 놀이를 번갈아 저장·복원하고 ID·위치·소속·계수·카메라를 비교한다. 자동 저장 검사에는 N≥2에서 일부 구만 센 미완료 상태도 포함한다. 저장 중단·손상·자유 놀이 교체·재플레이·시범 기록을 검사한다. Persistence 단계의 Android 재시작·일시 정지 사례도 통과한다.
**공유 작성:** JSON 필드나 버전을 임의로 바꾸지 않는다. 같은 씬·저장 연결은 실행 담당자가 한 번 갱신한다.
### T6 — 30개 스테이지·도움·학습 안내
**목표:** R07 전체 정의와 실제 순차 해금·재플레이·도움·6/10번 시범을 완성한다.
**근거:** R05, R06, R07, R08, R09, R10, R11, R12, R13, R14.
**작업 대상:** StageAssets의 30개 자산·놀이터 배치, Core의 정의 검증·진행·힌트, 안내 화면, 검사.
**동작 계약:** 정의와 순서는 R07과 전부 일치한다. 수 세기는 정확히 N개를 제공하며 뒤·위에 있는 구도 찾고 잡을 수 있게 한다. 모든 퍼즐은 최종 조건으로 판정한다. 잠금/완료/이어하기를 표시한다. 시범은 첫 지정 시점에 한 번만 보이며 퍼즐을 대신 풀지 않는다. 도움은 현재 가능한 대상/첫 행동을 강조한다. 30번 후에는 존재하지 않는 다음 번호를 열지 않는다.
**검증:** 30개 자산의 번호·총량·목표·종류 검증, 실제 Core 호출로 전체 해답 경로, 새 저장에서 30번까지 해금, 다른 풀이·힌트 유효성·시범 재진입을 확인한다. 에디터에서 각 놀이터의 발견·조작 가능성을 플레이한다. Content 전체 EditMode·PlayMode와 변경된 진행 흐름의 Android 검사를 수행한다.
**공유 작성:** 번호마다 별도 씬·규칙을 만들지 않는다. 초기 배치와 장식은 자산·프리팹으로 관리한다.
Blender 소품과 공유 프리팹으로 세 테마의 입체 세트를 구성하여 고정 배치 참조에 연결한다. 테마별 발견·조작 가능성과 성공 후 동일 공간·복원된 테마를 확인한다. 실제 시범·현재 상태별 도움 문구를 Qwen 카탈로그에 빠짐없이 기록해 생성·검수하고, 최종 파일 교체에 필요한 전체 목록을 확정한다.
### T7 — 카툰 표현·한국어 음성·모바일 화면
**목표:** 검증된 기능을 아이가 읽고 만질 수 있는 최종 외관과 소리로 표현한다.
**근거:** R01, R02, R03, R04, R05, R06, R08, R09, R10, R11, R12, R13, R14.
**작업 대상:** Presentation의 캐릭터·UI·세 테마 놀이터·효과·한국어 음성·효과음·음악·글꼴, SourceAssets 제작 원본, 음원 요청/대본표·자산 출처와 사용 고지 기록, PlayMode 검사.
**동작 계약:** 묶음마다 얼굴·숫자 하나, 구별 선택/계수 강조, 부드러운 오답 반응, 성공 후 같은 공간을 제공한다. 가로 스마트폰·태블릿 비율과 안전 영역을 확인한다. 회전해도 숫자가 읽혀야 한다. 장식으로 구를 찾거나 잡을 수 없게 만들지 않는다. 필요한 한국어 숫자 읽기·안내·효과음을 허가된 실제 파일로 포함하고 오프라인 재생한다.
R11의 합체 영웅 외형과 변신 연출을 구현한다. 대표 얼굴과 얇은 마스크, 접촉부의 빛, 묶음마다 하나인 자력 망토로 합체·분리를 표현한다. 아이가 만든 기본 배치를 유지하며 각 구를 셀 수 있게 한다. 2+3의 합체와 5에서 떨어진 두 구를 고른 분리를 대표 연출로 제작하고, 1~10 및 임의 배열에도 적용한다. 연출의 취소·재진입·복원은 현재 확정 상태에 맞춰 처리한다.
**검증:** 얼굴·숫자·소속 일치, 시범·도움·오답·전환, 한글 누락·잘림·소리 누락, 입력 차단, 10개 한도·마지막 번호·복원 화면을 확인한다. 출처·사용 범위를 기록하며 임시 음성 또는 텍스트만으로 음성 완성을 선언하지 않는다. 전체 EditMode·PlayMode를 회귀 실행한다.
합체 전 두 얼굴·두 망토가 확정 후 각각 하나가 되고, 분리 후 두 묶음에 각각 하나씩 생기는지 검사한다. 선택만 한 상태·무효 분리·취소에서 묶음 구성과 얼굴·숫자·망토 개수가 바뀌지 않는지 확인한다. 연속 합체·분리와 연출 중 화면 이동·앱 복원 후 구 ID·계수·기본 배치가 유지되고 중복 얼굴·망토가 남지 않아야 한다. 직선·평면·입체 배열과 서로 다른 두 가지 5인 친구를 회전하며 구 경계·숫자·선택·계수의 가독성을 수동 확인한다. 이 검사를 장식의 픽셀 모양을 고정하는 자동 테스트로 대체하지 않는다.
**공유 작성:** 화면 작업에서 Core 규칙·저장 형식을 바꾸지 않는다. 실행 담당자가 최종 프리팹과 씬 연결을 갱신한다.
앞의 자산 제작 절차를 따라 AI 2D 그림·Blender FBX·URP 재질·망토/VFX와 Qwen3-TTS WAV의 최종 파일·출처 기록을 확인한다. 현재 카탈로그·고정 프로필의 전체 필수 음성에 대한 생성/재사용 이력과 파일 검사, 사용자 청취 승인 44개의 정확한 채택 해시를 유지한다. 필수 안내와 하나~열의 실제 재생, 빠른 탭 순차 발화와 성공 후 마지막 숫자, 화면 전환/일시 정지/복원 정리를 확인한다. 효과음·음악은 첫 버전에서 제외하여 반복·믹싱 청취 인수는 요구하지 않는다. 최종 사용 파일의 출처·조건·필요 고지가 누락되거나 필수 음성의 생성/청각 검수가 미완료이면 해당 항목을 통과로 표시하지 않는다.
### T8 — 최종 Android 전체 검증과 결과물
**목표:** 완성한 30개 체험판의 APK와 검증 증거를 제공한다.
**근거:** R01, R03, R07, R08, R09, R10, R11, R12, R13, R14.
**작업 대상:** 빌드/검증 진입점의 Final 범위, 최종 APK·자동 검사·Android 증거 경로.
**동작 계약:** 전체 EditMode·PlayMode를 실행하고 최종 자산을 포함한 APK를 빌드한다. AVD에서 설치·실행·수집·합체·분리·도움·시범·순차 해금·자유 놀이·오프라인 소리를 확인한다. 아래 대표 흐름과 두 미완료 퍼즐/자유 놀이 독립 복원을 재현한다. 첫 단계 증거와 최종 증거를 구분한다.
**검증:** Final 네 검사 단계, 실제 검사 수·실패/누락 여부, APK·해시·설정·로그·화면 기록을 확인한다. 결과를 통과/실패/미실행으로 구분하고 미실행이 있으면 전체 검증 완료를 선언하지 않는다. 실제 기기와 iOS·스토어 배포는 이번 범위가 아니다.
최종 파일·사용 고지가 포함된 APK에서 세 테마와 한국어 음성을 확인한다. 효과음·음악은 첫 버전에 포함하지 않는다. TTS 모델/서버/제작 도구 없이 오프라인 재생되며 빠른 계수·마지막 숫자·전환 정리가 유지되어야 한다. 에디터에서만 재생한 파일이나 다른 APK의 증거로 최종 앱의 음성 검사를 대체하지 않는다.
**공유 작성:** 최종 설정은 단독으로 갱신한다. 이때 기능을 고치면 관련 회귀 검사와 최종 APK/증거를 다시 만든다.
## 9. 단계별 장면 연결과 대표 플레이
장면 연결은 별도 작업 번호가 아닌 실행 담당자의 단계별 작업이다. T2 이후 각 단계의 프리팹·상태·입력·저장을 연결하고 검사한 뒤 다음 단계로 진행한다. 최종 T7까지 장면 연결을 미루지 않는다.
| 대표 흐름 | 실제 화면에서 확인할 결과 |
|---|---|
| 1번 | 직접 끌어 수집 후 한 번 눌러야 성공 |
| 6번 | 1과 1을 붙여 2; 최초 시범은 다음 진입에 자동 반복 안 됨 |
| 9번 | 2와 3을 합쳐 5; 구 다섯 알과 합체 후 얼굴·숫자·자력 망토 각각 하나 확인 |
| 10번 | 나누기·선택·끌기로 2를 1과 1로 분리; 선택만으로 성공 안 됨 |
| 13번 | 5에서 두 알을 선택해 2와 3으로 분리; 선택 중 강조와 확정 후 각 팀의 얼굴·숫자·망토 확인 |
| 14번 | 3과 1을 2와 2로; 다른 풀이 순서도 인정 |
| 20번 | 시점을 돌려 뒤쪽 구를 찾고 10개 수집·계수 전에는 완료 안 됨 |
| 30번 | 6과 4를 5와 5로 만들고 자유 놀이; 없는 다음 번호 표시 안 됨 |
| 저장 | 두 미완료 퍼즐과 최근 자유 놀이의 ID·배치·계수·카메라 독립 복원; N≥2에서 일부 구만 센 상태도 재실행 후 유지 |
| 자유 놀이 | 10개에서 추가 거부, 묶음 반납 후 추가, 빈 상태에서 다시 꺼내기 |
정상 순차 플레이 검사는 해금을 우회하지 않는다. 특정 경계의 자동 검사에서 상태를 준비하는 것과 실제 제품에 번호 우회 기능을 제공하는 것은 구분한다. 30개 자료·해답 검사는 위 대표 플레이만으로 대체하지 않는다.
## 10. 코드·자산 변경 규칙
- 네임스페이스는 `MagnetismHeroes` 아래 책임별로 나눈다. 타입·메서드·공개 속성은 PascalCase, 지역 변수는 camelCase, 비공개 필드는 _camelCase로 작성한다.
- Inspector 참조는 비공개 직렬화 필드로 드러내고 필수 참조 누락을 초기화·검사에서 발견한다. 고정 정의와 상태를 중복 소유하지 않는다.
- 데이터와 규칙의 변경 경로를 좁게 유지하고 정상적인 무효 조작은 거부 결과로 처리한다. 불가능한 상황을 위한 방어 계층을 여러 겹 만들지 않는다.
- `.meta`와 실제 소스·자산·프로젝트 설정은 함께 관리하고 생성 캐시·APK·검사 로그는 소스로 취급하지 않는다. 기존 Git 파일 변경과 커밋은 개발 실행 단계에서 담당한다.
- 기능 변경마다 영향을 받은 요구사항과 검증 결과를 남긴다. 관련 없는 리팩터링·프레임워크 추가·폴더 개편은 하지 않는다.
- 패키지 목록·어셈블리·씬·시작 연결·빌드 설정은 실행 담당자가 단독으로 갱신한다. 기능별 작업 대상과 겹치는 변경은 병합 전에 확인한다.
## 11. 요구사항 대응과 실행 그래프
| 요구사항 | 구현·검증 작업 |
|---|---|
| R01 | T2, T3, T7, T8 |
| R02 | T1, T2, T4, T7 |
| R03 | T2, T3, T4, T7, T8 |
| R04 | T1, T4, T7 |
| R05 | T1, T2, T3, T6, T7 |
| R06 | T1, T4, T6, T7 |
| R07 | T4, T6, T8 |
| R08 | T2, T5, T6, T7, T8 |
| R09 | T1, T2, T3, T4, T5, T6, T7, T8 |
| R10 | T2, T3, T5, T6, T7, T8 |
| R11 | T2, T3, T4, T6, T7, T8 |
| R12 | T1, T2, T3, T4, T5, T6, T7, T8 |
| R13 | T1, T2, T3, T4, T5, T6, T7, T8 |
| R14 | T2, T3, T4, T6, T7, T8 |
T2는 T1 규칙·데이터를 소비한다. T3는 T2의 실제 플레이·저장을 Android에서 검증한다. T4는 기존 플레이 경로를 확장하며 T3 뒤에 두는 것은 합의한 조기 플랫폼 검사 순서다. T5는 T4에서 확장한 묶음·조작 상태를 저장한다. T6는 T5의 진행·저장·자유 놀이 전체 수명을 사용한다. T7은 T6의 모든 목표·안내·진행 상태를 표현하고 T8은 최종 표현과 자산을 포함한 전체 앱을 검증한다.
현재는 같은 플레이 흐름을 단계마다 확장하므로 기능 단계를 순차 실행한다. 초기화와 단계별 공통 연결은 별도 그래프 작업으로 만들지 않는다. Qwen 음성 생성·부분 재생성은 T2·T4·T6·T7 내부의 제작 작업이며 후속 작업에는 검수된 파일과 기록을 전달한다. 작업 사이에 별도 일괄 재생성을 요구하는 단계는 없으므로 재생성 장벽은 두지 않는다. ScriptableObject의 일반 Inspector 편집·자산 검사도 코드 생성 장벽이 아니다.
```yaml
tasks:
- id: T1
depends: []
risk: RISKY
- id: T2
depends: [T1]
risk: RISKY
- id: T3
depends: [T2]
risk: RISKY
- id: T4
depends: [T3]
risk: RISKY
- id: T5
depends: [T4]
risk: RISKY
- id: T6
depends: [T5]
risk: RISKY
- id: T7
depends: [T6]
risk: RISKY
- id: T8
depends: [T7]
risk: RISKY
regen_barriers: []
```
## 참고한 공식 기술 문서
위 선택은 이 프로젝트의 규모와 합의한 Unity 제작 흐름에 맞춘 것이다. UI Toolkit도 현행 Unity에서 3D UI를 지원한다. uGUI 선택은 프리팹·MonoBehaviour 연결과 애니메이션 제작 흐름을 우선한 것이며 UI Toolkit의 기능 부재를 전제로 하지 않는다.
- [Unity URP 소개](https://docs.unity3d.com/6000.6/Documentation/Manual/urp/urp-introduction.html): 모바일을 포함한 렌더링 구성.
- [Unity UI 비교](https://docs.unity3d.com/6000.6/Documentation/Manual/UI-system-compare.html): uGUI·UI Toolkit 제작 방식과 지원 범위.
- [Input System 터치](https://docs.unity3d.com/Packages/com.unity.inputsystem@1.14/manual/Touch.html): EnhancedTouch와 포인터 기반 터치 시뮬레이션. 실제 패키지 버전은 초기화 시 에디터 호환 버전으로 고정한다.
- [Unity Test Framework 검사 구분](https://docs.unity3d.com/Packages/com.unity.test-framework@1.4/manual/edit-mode-vs-play-mode-tests.html): EditMode는 에디터에서, PlayMode는 에디터 또는 Player에서 실행 가능하다.
- [ScriptableObject](https://docs.unity3d.com/6000.6/Documentation/Manual/class-ScriptableObject.html), [어셈블리 구성](https://docs.unity3d.com/6000.6/Documentation/Manual/assembly-definition-files.html): 고정 자산 제작과 코드 참조 경계.
- [Unity 3D 파일 형식](https://docs.unity3d.com/6000.6/Documentation/Manual/3D-formats.html): 제작 원본을 별도로 두고 FBX 등 내보낸 파일을 가져오는 흐름.
- [AudioClip 가져오기](https://docs.unity3d.com/6000.6/Documentation/Manual/class-AudioClip.html), [Audio Mixer](https://docs.unity3d.com/6000.6/Documentation/Manual/AudioMixer.html): 클립별 메모리·재생 설정과 음성/효과음/음악 혼합.
- [TrailRenderer](https://docs.unity3d.com/6000.6/Documentation/Manual/class-TrailRenderer.html): 이동 흔적은 보조 표현으로 사용하고 정지 시 망토는 별도 리본 메시로 유지한다.
- [Qwen3-TTS 공식 코드·사용법](https://github.com/QwenLM/Qwen3-TTS), [선택한 1.7B CustomVoice 모델](https://huggingface.co/Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice): 한국어·Sohee·CustomVoice 생성과 모델 파일/라이선스 확인. 설치된 코드의 API와도 대조한다.
- [PyTorch 공식 설치 안내](https://pytorch.org/get-started/locally/): 개발 실행에서 기존 환경의 CPU 구성과 호환되는 Windows CUDA 구성을 확인한다. 특정 CUDA wheel 버전은 실제 호환성 검증 후 기록한다.
-277
View File
@@ -1,277 +0,0 @@
# MagnetismHeroes 첫 체험판 기획서
2026-10-06 사용자 결정: 첫 버전은 효과음·배경음악 없이 진행한다. 한국어 숫자·안내 음성 44개와 시각 연출은 유지한다. 아래 효과음·음악 제작·포함·반복·믹싱 기준은 첫 버전의 필수 인수 범위에서 제외하며, 무음 효과음·음악을 미완료로 취급하지 않는다. 이후 추가는 별도 요청으로 진행한다. 음성 순서·오프라인 재생·전환 정리 및 출처·고지 기준은 계속 적용한다.
이 문서는 구현할 게임의 동작과 완료 기준을 정의한다. 대상은 한국어를 사용하는 5~7세이며, 첫 제작 대상은 안드로이드 스마트폰·태블릿이다. 숫자를 이루는 자석을 직접 찾고, 세고, 붙이고, 떼면서 수량과 숫자 기호를 연결한다.
## R01. 제작 범위와 학습 목표
- Unity 6 기반의 가로 화면 3D 카툰 게임을 만든다. 설치된 에디터 버전은 `6000.6.3f1`이다.
- 첫 체험판에는 아래에 정의한 30개 스테이지, 순차 해금, 재플레이, 도움, 성공 후 자유 놀이, 기기 내 저장을 포함한다.
- 학습 범위는 1~10 수 세기, 합치기, 분리하기, 두 행동을 함께 쓰기다. 분리는 한 수를 두 수로 구성하는 경험이며, 이번에는 나눗셈·곱셈 교육으로 확장하지 않는다.
- 혼자 오프라인으로 플레이한다. 계정, 서버, 광고, 결제, 사용자 추적을 추가하지 않는다.
- 아이폰·아이패드 지원은 후속 범위다. 이번에 iOS 빌드나 검증을 완료했다고 주장하지 않는다. 앱 스토어 배포도 이번 작업에 포함하지 않는다.
- Numberblocks는 수량을 캐릭터와 연결하는 표현의 참고다. 게임의 캐릭터·배경·자석 표현은 이 프로젝트에 맞게 만든다.
## R02. 자석과 친구의 규칙
- 구 형태의 자석 한 개는 항상 1이다. 묶음의 값은 소속 구의 개수와 같다.
- 자석을 합쳐도 각각의 구가 보인다. 큰 구 하나로 대체하거나 구의 크기로 수량을 표현하지 않는다.
- 묶음마다 대표 얼굴 하나와 숫자 표시 하나가 있다. 서로 떨어진 1 두 개에는 각각 얼굴과 숫자 1이 있고, 붙이면 하나의 얼굴과 숫자 2를 가진 친구가 된다.
- 숫자는 묶음 위에 표시하며 카메라를 돌려도 읽을 수 있게 한다. 자석 배열로 숫자 글자의 모양을 만드는 기능은 포함하지 않는다.
- 같은 개수는 어떤 모양에서도 같은 수다. 직선·평면·입체 배열에 따라 정답 여부가 달라지지 않는다.
- 아이가 붙인 모양이 합체 영웅의 몸이 된다. 변신 연출 때문에 정해진 캐릭터 형태로 재배열하지 않는다. 분리 시 두 친구로 모이는 배치 조정은 R04를 따른다.
- 자석은 중력 없이 놓은 위치에 뜬다. 다른 묶음 가까이에 가져가 놓으면 접촉하는 위치에 붙고 하나의 묶음이 된다. 합체 직전에는 붙을 대상을 알아볼 수 있게 한다.
- 합체는 두 묶음의 구를 모두 보존한다. 분리는 기존 구를 두 묶음으로 나누며 구를 생성하거나 없애지 않는다. 한 구는 정확히 한 묶음에 속한다.
- 위치·모양 변경은 구의 정체성을 바꾸지 않는다. 이 원칙으로 중복 계수를 방지하고 저장 후 같은 놀이 상태를 복원한다.
## R03. 입체 공간과 터치 조작
| 상황 | 입력 | 결과 |
|---|---|---|
| 일반 상태 | 친구를 한 손가락으로 끌기 | 그 묶음 전체 이동 |
| 나누기 상태 | 구를 짧게 누르기 | 해당 구 선택 또는 선택 해제 |
| 나누기 상태 | 선택한 구를 끌어내기 | R04에 따라 분리 |
| 빈 공간 | 한 손가락으로 쓸기 | 놀이터를 보는 시점 회전 |
| 놀이터 | 두 손가락으로 오므리거나 벌리기 | 확대·축소 |
| 수 세기 상태 | 구를 짧게 누르기 | R05에 따라 한 번 세기 |
- 앞뒤 위치는 시점을 옆으로 돌린 후 끌어 옮기는 방식으로 조절한다. 평면에만 놓거나 정해진 방향으로만 붙도록 제한하지 않는다.
- 놀이터는 한 화면에서 둘러보는 작은 입체 공간이다. 플레이어 캐릭터를 걸어 다니게 하는 넓은 탐험 월드는 만들지 않는다.
- 회전하여 사물 뒤·위·아래를 확인할 수 있고, 각 수집 자석에는 발견하고 잡을 수 있는 시점이 존재해야 한다. 장식물 내부에 완전히 갇힌 자석을 배치하지 않는다.
- 구를 눌렀다가 끄는 동작은 탭과 구분한다. 끌기를 계수나 선택 탭으로 중복 처리하지 않는다.
- 화면 버튼 위에서 시작한 입력은 친구나 카메라를 움직이지 않는다. 두 손가락 조작을 친구 이동·분리로 동시에 처리하지 않는다.
- 화면을 벗어나 놓거나 조작이 취소되면 자석을 잃지 않도록 마지막 유효 위치를 유지한다. 카메라에는 놀이 대상이 복구 불가능하게 사라지지 않는 거리 경계를 둔다.
- 터치 판정 여유, 흡착 거리, 회전·확대 속도, 애니메이션 시간은 실제 조작으로 조정할 값이다. 수량·정답 규칙을 바꾸는 수단으로 사용하지 않는다.
## R04. 나누기
1. 아이가 친구를 선택하고 ‘나누기’를 누른다.
2. 같은 친구에 속한 구들을 눌러 선택한다. 선택된 구를 빛으로 구분하고 선택 개수를 표시한다.
3. 선택한 구를 끌어내면 선택한 쪽과 남은 쪽이 각각 하나의 친구로 자연스럽게 모인다.
- 서로 떨어진 구도 함께 선택할 수 있다. 선택 구끼리 물리적으로 붙어 있을 필요는 없다.
- 두 결과 묶음은 각각 숫자와 대표 얼굴을 가진다. 기존 모양은 분리 과정에서 일부 달라질 수 있다.
- N개 친구에서 유효한 선택 수는 1~N-1이다. 0개·전체 선택은 분리를 실행하지 않으며 구를 소실시키지 않는다. 1인 친구는 나눌 수 없다.
- 선택 중에는 한 묶음만 다룬다. 다른 묶음으로 대상을 바꾸거나 나누기를 취소하면 확정하지 않은 선택을 해제한다.
- 분리 후 선택 상태를 종료한다. 떨어진 두 친구는 아이가 다시 가까이 가져오기 전까지 즉시 재합체하지 않는다.
- 예: 5에서 양 끝의 두 개를 선택하면 결과는 항상 2와 3이다. 중간 연결이 끊겼다는 이유로 세 개 이상의 친구가 생기지 않는다.
## R05. 탐험하며 수 세기
- 해당 스테이지의 목표 N과 정확히 N개의 자석을 제공한다. 각 자석은 처음에는 분리된 1인 친구로 놀이터 곳곳에 있다.
- 아이는 시점을 돌려 발견한 자석을 직접 끌어와 다른 자석에 붙인다. 누르기만 해도 자동 수집되는 방식은 사용하지 않는다.
- 모든 자석을 하나의 N인 친구로 모은 뒤 하나씩 눌러 센다. 1 스테이지에서도 직접 자석을 끌어 모으는 행동과 한 번 세는 행동이 필요하다. 처음부터 놓여 있다는 이유만으로 수집 완료 또는 성공으로 처리하지 않는다.
- 직접 끌어 놓은 구와 그 조작으로 합쳐진 묶음의 구들을 수집한 것으로 기록한다. 카메라를 돌리거나 자석을 보기만 한 것은 수집이 아니다.
- 세기 입력은 모든 구를 수집하여 하나로 모은 뒤 활성화된다. 처음 누른 구에만 계수 표시를 남기고, 이번에 센 개수에 맞춰 ‘하나, 둘, …, 열’을 들려준다. 이미 센 구를 다시 눌러도 수가 늘지 않는다.
- 뒤쪽 구는 시점을 돌려 찾는다. 위치·모양 변경이나 합체·분리는 이미 센 구의 기록을 지우지 않는다. 분리하여 모으기 조건을 잃으면 다시 하나로 모은 후 세기를 계속한다.
- 성공 조건은 **전체 N개 수집 + N개의 단일 묶음 + N개 모두 계수 완료**다. 숫자 표시를 읽거나 묶음을 만들기만 해서는 성공하지 않는다.
- 완료 전에는 자석을 추가하거나 반납할 수 없다. 총 N개를 유지한다.
## R06. 합치기·분리하기 퍼즐
- 탐험 없이 표에 지정된 묶음들이 준비된 상태로 시작한다. 목표는 만들 친구들의 숫자로 보여준다.
- 놀이 공간에 있는 모든 묶음의 개수를 목표 구성과 비교한다. 순서와 위치는 무관하지만 묶음 수와 각 개수는 같아야 한다.
- 예: 목표가 2와 2이면 4 하나 또는 2·1·1은 오답이다. 2인 친구 두 개를 만들면 성공이다.
- ‘하나 옮기기’ 등 스테이지의 설명은 풀이를 돕는 표현이다. 최종 구성에 도달하면 모두 합친 뒤 다시 나누는 풀이도 인정한다. 특정 제스처 이력이나 최소 횟수를 성공 조건에 넣지 않는다.
- 조작을 확정한 후 정답을 판정한다. 합체·분리 애니메이션의 중간 모양으로 정답을 판정하지 않는다.
- 목표와 다르면 친구가 부담 없는 표정으로 반응하며 계속 조작할 수 있다. 시간제한, 목숨, 실패 점수, 오답에 따른 초기화는 없다.
- 완료 전 자석 공급·반납은 불가능하다. 모든 계산 퍼즐은 자석 총량을 보존한다.
## R07. 30개 스테이지
표의 `수집`은 처음에 흩어진 1인 친구들이다. `구성`은 준비된 묶음을 조작하는 퍼즐이다. 같은 숫자가 반복되면 서로 다른 묶음이다.
| 번호 | 종류 | 처음의 구 수 또는 묶음 | 목표 | 경험 |
|---:|---|---|---|---|
| 1 | 수집 | 1개 | 1개 모아 세기 | 하나 |
| 2 | 수집 | 2개 | 2개 모아 세기 | 둘 |
| 3 | 수집 | 3개 | 3개 모아 세기 | 셋 |
| 4 | 수집 | 4개 | 4개 모아 세기 | 넷 |
| 5 | 수집 | 5개 | 5개 모아 세기 | 다섯 |
| 6 | 구성 | 1, 1 | 2 | 처음 합치기 |
| 7 | 구성 | 2, 1 | 3 | 하나 더 붙이기 |
| 8 | 구성 | 3, 1 | 4 | 하나 더 붙이기 반복 |
| 9 | 구성 | 2, 3 | 5 | 두 묶음 합치기 |
| 10 | 구성 | 2 | 1, 1 | 처음 나누기 |
| 11 | 구성 | 3 | 2, 1 | 하나 떼기 |
| 12 | 구성 | 4 | 2, 2 | 두 개 함께 떼기 |
| 13 | 구성 | 5 | 3, 2 | 다른 크기로 나누기 |
| 14 | 구성 | 3, 1 | 2, 2 | 나누고 다시 합치기 |
| 15 | 구성 | 4, 1 | 3, 2 | 두 행동 함께 쓰기 |
| 16 | 수집 | 6개 | 6개 모아 세기 | 여섯 |
| 17 | 수집 | 7개 | 7개 모아 세기 | 일곱 |
| 18 | 수집 | 8개 | 8개 모아 세기 | 여덟 |
| 19 | 수집 | 9개 | 9개 모아 세기 | 아홉 |
| 20 | 수집 | 10개 | 10개 모아 세기 | 열 |
| 21 | 구성 | 5, 1 | 6 | 큰 수에 하나 붙이기 |
| 22 | 구성 | 3, 4 | 7 | 다른 두 묶음 합치기 |
| 23 | 구성 | 4, 4 | 8 | 같은 크기 합치기 |
| 24 | 구성 | 5, 5 | 10 | 열 만들기 |
| 25 | 구성 | 6 | 5, 1 | 큰 수에서 하나 떼기 |
| 26 | 구성 | 7 | 4, 3 | 여러 개 골라 떼기 |
| 27 | 구성 | 9 | 5, 4 | 아홉 나누기 |
| 28 | 구성 | 10 | 5, 5 | 열을 같은 크기로 |
| 29 | 구성 | 5, 3 | 4, 4 | 두 친구의 크기 바꾸기 |
| 30 | 구성 | 6, 4 | 5, 5 | 종합 놀이 |
앞의 구성 스테이지는 총량 5 이하, 뒤의 구성 스테이지는 총량 6~10이다. 뒤 구간에서도 1~5인 친구를 재료나 결과로 사용한다.
## R08. 진행·도움·학습 안내
- 새 저장에서는 1번만 열린다. 스테이지를 성공하면 완료 기록을 남기고 다음 번호를 연다. 완료한 스테이지는 다시 플레이할 수 있다.
- 재플레이에서 풀다가 나와도 이전 완료 기록과 뒤 스테이지의 해금은 유지한다.
- 성공하면 같은 공간과 자석 배치를 유지한 채 축하하고 자유 놀이로 전환한다. 자동으로 다음 스테이지에 이동하지 않는다.
- 자유 놀이에서 ‘다음’을 누르면 다음 스테이지의 저장 상태가 있을 경우 이어 하고, 없으면 처음 상태로 시작한다. 30번에는 존재하지 않는 31번으로 가는 버튼을 제공하지 않는다. 자유 놀이와 스테이지 선택은 계속 사용할 수 있다.
- 6번 첫 진입에서 합치기, 10번 첫 진입에서 ‘나누기 → 선택 → 끌어내기’를 짧게 시범으로 안내한다. 시범은 퍼즐의 실제 구를 조작하거나 완료 기록을 만들지 않는다. 이미 본 시범은 자동 반복하지 않는다.
- 도움 버튼은 현재 상태에 맞는 한 행동을 제안한다. 수집 중에는 아직 모으지 않은 자석 하나, 세는 중에는 아직 세지 않은 구 하나를 알려준다.
- 수집한 구들을 다시 분리하여 여러 친구가 된 경우에는 먼저 합칠 두 묶음을 알려준다. 수집 기록이 모두 있다는 이유로 세기 조건이 아직 안 되는 상태에서 계수 힌트만 제시하지 않는다.
- 구성 퍼즐의 도움은 현재 묶음에서 목표에 접근할 수 있는 첫 행동을 제안한다. 이전 배치에만 유효했던 고정 힌트를 반복하지 않는다. 예를 들어 적절한 묶음을 가리키며 ‘4에서 하나를 떼어 볼까?’라고 안내한다.
- 도움은 자석을 대신 움직이거나 정답을 자동 실행하지 않는다. 도움 사용 횟수로 보상을 깎거나 진행을 막지 않는다.
## R09. 성공 후 자유 놀이
- 스테이지를 성공한 자리에서 같은 친구들로 자유롭게 합체·분리·이동·입체 배열을 만들 수 있다.
- 자석을 하나씩 꺼낼 수 있고, 친구를 반납할 수 있다. 묶음에서 일부만 반납하려면 먼저 나누기로 분리한다.
- 화면에 존재하는 구의 합계는 최대 10이다. 10개이면 추가를 실행하지 않고 구를 중복 생성하지 않는다. 반납하면 그 수만큼 다시 꺼낼 수 있다.
- 전부 반납한 빈 공간도 허용한다. 이는 0을 가르치는 별도 스테이지가 아니며, 자석을 다시 꺼낼 수 있다.
- 자유 놀이에는 정답·실패가 없고 완료 기록을 취소하지 않는다.
- 최근 자유 놀이 상태 하나만 보존한다. 다른 스테이지에서 자유 놀이를 시작하면 이 저장은 새 놀이 상태로 교체된다. 스테이지별 미완료 퍼즐 저장과는 별개다.
## R10. 시작 화면과 저장·이어하기
- 시작 화면에서 스테이지 선택과, 저장이 있을 때 최근 자유 놀이 이어 하기를 제공한다.
- 스테이지별 미완료 퍼즐 상태를 각각 하나씩 저장한다. 다른 스테이지로 이동해도 이전 스테이지의 배치를 덮어쓰지 않는다.
- 저장이 있는 스테이지를 선택하면 해당 퍼즐을 이어 한다. 저장이 없는 완료 스테이지를 선택하면 처음 배치로 재플레이한다.
- 성공한 퍼즐의 미완료 저장은 제거하고, 완료 기록과 최근 자유 놀이 상태를 함께 갱신한다. 재플레이 중 미완료 저장과 과거 완료 기록은 동시에 존재할 수 있다.
- 복원 대상은 구의 위치, 묶음 소속, 수집·계수 여부, 바라보던 방향과 확대 정도다. 튜토리얼을 본 기록도 유지한다.
- 자석을 놓거나 합체·분리·계수·추가·반납을 확정했을 때, 카메라 조작을 마쳤을 때, 화면 전환·앱 일시 정지 시 저장한다. 손가락을 대고 있는 중간 위치, 미확정 분리 선택, 애니메이션 중간 프레임은 저장 대상이 아니다.
- 중단 시에는 마지막으로 확정한 조작 상태로 돌아온다. 앱이 중간에 종료되어도 반쪽 합체, 구의 복제·소실, 완료만 기록되고 자유 놀이가 비는 상태가 생기지 않도록 저장을 한 단위로 교체한다.
- 기기 내 앱 전용 공간에 저장한다. 기기 간 동기화·여러 사용자 프로필·계정 복구는 범위 밖이다.
## R11. 화면·캐릭터·한국어 소리
- 둥근 자석, 표정이 있는 대표 얼굴, 친근한 작은 입체 놀이터로 구성한다. 합체·분리 때 표정과 움직임으로 친구가 바뀌었음을 보여준다.
- 장식은 구·숫자·목표·선택 표시를 가리지 않는다. 탐험용 가림은 시점을 돌리면 해결할 수 있어야 한다.
- 숫자, 목표, 현재 선택 개수, 주요 버튼은 가로 스마트폰·태블릿 화면과 화면 가장자리 안전 영역에 맞춰 배치한다. 실제 기기의 가독성 합격은 후속 검증으로 구분한다.
- 한국어 화면, 하나~열 숫자 읽기와 짧은 안내 음성을 앱에 포함한다. 효과음·배경음악은 첫 버전에 포함하지 않는다. 음성은 네트워크 생성이나 다운로드가 없어도 재생되어야 한다.
- 소리만으로 조작·정답·힌트를 전달하지 않는다. 대응하는 숫자·강조·표정을 함께 보여준다.
- 원본 애니메이션의 캐릭터·음원을 제품 자산으로 가져오지 않는다. 사용할 자산의 출처와 사용 가능 범위를 기록한다.
### 합체 영웅의 외형
시각적 정체성은 **살아 있는 자석 구 + 빛나는 연결부 + 자력 망토**다. 붙으면 하나의 영웅 팀이 되고, 나뉘면 함께 돕는 두 팀이 된다는 느낌을 표정과 움직임으로 전달한다.
- 모든 구의 기본 크기는 같다. 매끈하고 둥근 장난감 질감으로 표현하며 각 구의 경계를 구분할 수 있어야 한다. 갑옷이나 몸체 장식으로 구를 덮거나 별도의 머리 구를 추가하지 않는다.
- 대표 얼굴은 기존 구 하나의 표면에 큰 눈·입과 얇은 영웅 마스크로 표현한다. 묶음 위 숫자와 함께 합체·분리 결과에 맞춰 갱신한다.
- 구가 접촉한 부분에는 부드러운 빛의 고리를 표현한다. 연결이 확정되면 붙은 자리에서 묶음 안으로 빛이 퍼져 자력으로 한 팀이 되었음을 보여준다.
- 자력 망토는 묶음 뒤에서 흐르는 얇은 빛의 리본이다. 안정된 상태에서 묶음마다 하나이며 이동·합체·분리에 반응한다. 얼굴·마스크·연결부·망토는 수량에 포함되지 않는 장식이며 독립적인 입력 대상이 아니다.
- 장식은 숫자와 구별 선택·계수 표시를 가리지 않는다. 입체 배치로 가려진 구는 카메라를 돌려 확인할 수 있어야 한다. 선택과 이미 센 구의 표시는 연결부의 장식 빛과 구분한다.
- 같은 수의 다른 배열은 동등한 수량 조건을 만족한다. 모양에 따른 자세·망토 움직임의 차이는 개성 표현이며 능력 등급이나 새로운 정답 조건이 아니다. 1~10 전체에 같은 외형 원칙을 적용한다.
- 시안의 색 조합·마스크 윤곽은 탐색안이다. 최종 색·빛의 강도·망토 길이·동작 시간은 모바일 가독성과 조작감을 보고 조정한다.
### 합체·분리의 변신 연출
| 단계 | 표현 | 동작과의 관계 |
|---|---|---|
| 합체 후보에 접근 | 두 친구가 서로를 바라보고 구와 망토 끝이 살짝 끌리는 모습 | 붙을 대상을 알리는 미리보기; 구 소속은 아직 바꾸지 않음 |
| 합체 확정 | 접촉부에 빛의 고리, 연결된 구를 따라 빛이 퍼짐 | 확정된 합체 결과를 표현 |
| 한 팀 완성 | 대표 얼굴·숫자·망토가 각각 하나가 되고 짧게 떠오르며 웃음 | 아이가 만든 배치를 유지하고 새 수량 표시 |
| 분리 선택 | 선택 구의 테두리와 선택 개수 표시, 망토 일부가 선택한 쪽으로 흐름 | 선택만으로 두 친구를 만들지 않음 |
| 분리 확정 | ‘톡’ 효과와 함께 연결이 풀리고 양쪽에 얼굴·숫자·망토가 각각 하나씩 생김 | R04에 따른 두 결과 묶음을 표현 |
| 두 팀 완성 | 두 친구가 짧게 서로를 바라보고 고개를 끄덕이는 모습 | 같은 구들이 나뉘어 함께 일한다는 인상 |
연출은 짧게 하고 반복 조작을 방해하는 긴 변신 장면을 넣지 않는다. 흔들림·살짝 떠오르기는 시각 표현이며 확정된 구 ID·수량·기본 배치·계수 기록을 바꾸지 않는다. 취소·다음 조작·화면 전환·앱 복원 시에는 현재 확정 상태에 맞춰 표현을 정리하고 이전 연출이 뒤늦게 얼굴·숫자를 덮어쓰지 않게 한다. 얼굴·연결부·망토는 복원 상태에서 다시 만들며 연출 중간 프레임은 저장하지 않는다.
## R12. 구성과 데이터 계약
외부 API는 없다. 게임 규칙, 터치·공간 조작, 화면·소리 표현, 저장을 작은 책임으로 나눈다. 단일 플레이 세션에서 조작을 순서대로 확정하므로 여러 사용자의 동시 쓰기 조정은 필요하지 않다. 물리 연출의 흔들림이나 렌더링 프레임이 수학 상태를 결정하지 않는다.
- 수량·구 소속·합체·분리·계수·목표 판정과 진행 제어는 Unity 객체에 의존하지 않는 순수 C#으로 구현한다. Unity 입력과 화면은 이 규칙을 호출하고 결과를 표현한다. 화면 객체의 직접 변경으로 수학 상태를 바꾸지 않는다.
- 렌더링은 URP, 입력은 Input System, 게임 UI는 uGUI와 TextMeshPro를 사용한다. 묶음의 숫자와 얼굴은 확정 상태에서 계산한다.
- 진행은 명시적인 상태 머신으로 제어하고, Presenter가 상태를 화면에 전달한다. 저장 인터페이스로 파일 접근을 분리하며 시작 코드에서 의존성을 직접 연결한다. 이 범위를 위해 범용 DI·이벤트·MVVM 프레임워크를 추가하지 않는다.
- 고정 스테이지는 ScriptableObject 자산으로 제작하고 실행 시 순수 C# 정의로 변환한다. 실행 중 원본 자산을 수정하지 않는다. 미완료 배치와 완료 기록은 별도의 로컬 JSON 버전 1에 저장한다.
- 개발은 작은 퍼즐의 입력·규칙·화면·저장을 함께 연결한 뒤 확장한다. 최초 수 세기 퍼즐부터 Android APK 설치·실행·이어하기를 검증하고, 전체 기능 완성 후 최종 Android 검증을 다시 수행한다.
### 스테이지 정의
| 필드 | 의미와 제약 |
|---|---|
| `id` | 1~30의 고유 정수 |
| `kind` | `counting` 또는 `composition` |
| `initialGroups` | 처음 묶음별 양의 정수 배열. 수집형은 N개의 1 |
| `targetGroups` | 목표 묶음별 양의 정수 배열. 수집형은 `[N]` |
| `layout` | 구와 장식의 초기 배치. 모든 구를 발견·조작할 수 있어야 함 |
초기 총량과 목표 총량은 같고 1~10이다. 표 R07이 정의의 기준이며 데이터 파일에서 임의로 숫자나 스테이지 수를 바꾸지 않는다. 목표 배열은 정렬하여 중복까지 포함해 비교한다.
### 저장 계약
저장 형식은 로컬 JSON이며 다음 의미와 필드 경계를 보존한다. 직렬화 라이브러리의 내부 선택으로 게임 동작이나 저장 계약을 변경하지 않는다.
| 필드 | 의미와 제약 |
|---|---|
| `schemaVersion` | 최초 버전 1 |
| `completedStageIds` | 중복 없는 완료 번호 목록. 정상 플레이에서 감소하지 않음 |
| `seenTutorialIds` | `merge`, `split` 중 이미 본 안내 목록 |
| `puzzleStates` | 스테이지별 미완료 상태 배열. 동일 번호 최대 하나, 총 최대 30개 |
| `latestFreePlay` | 없음 또는 최근 자유 놀이 상태 하나 |
퍼즐 상태는 `stageId`, `balls`, `groups`, `collectedBallIds`, `countedBallIds`, `camera`를 가진다. 자유 놀이 상태는 `sourceStageId`, `balls`, `groups`, `camera`를 가진다. `sourceStageId`는 완료한 스테이지여야 한다.
- `balls`: 상태 안에서 고유한 `id`와 3차원 `position`. 분리·합체·복원 후에도 구 ID는 유지한다. 자유 놀이에서 새로 꺼낸 구는 현재 상태와 충돌하지 않는 ID를 가진다.
- `groups`: 고유 `id`와 비어 있지 않은 `ballIds`. 모든 구는 하나의 묶음에 정확히 한 번 속하며, 존재하지 않는 구를 참조하지 않는다.
- `collectedBallIds`, `countedBallIds`: 존재하는 구 ID의 중복 없는 부분집합. 센 구는 수집한 구다. 구성 퍼즐에서는 두 목록이 비어 있다.
- `camera`: 유효한 시점 기준점 `target`, 회전 `rotation`, 양수 거리 `distance`. 값을 복원하여 같은 방향·확대 정도로 이어 한다.
- 얼굴·숫자는 묶음 소속에서 다시 계산한다. 중복 저장한 숫자 때문에 실제 개수와 표시가 달라지면 안 된다.
- 미완료 퍼즐에는 해당 스테이지 초기 총량과 동일한 구가 있어야 한다. 자유 놀이는 0~10개이며, 빈 상태이면 묶음도 비어 있다.
- 입력 선택과 진행 중 애니메이션은 저장하지 않는다. 수집·계수 단계는 복원된 상태에서 판정한다.
- 손상·불완전 저장은 구를 일부만 불러오지 않고 유효한 마지막 저장으로 복원한다. 유효본을 확인할 수 없으면 읽기 실패를 기록하고 정상 초기 상태로 진입하며, 손상 파일을 무조건 덮어써 원인을 없애지 않는다.
### 상태 전이
- 화면은 시작 화면, 스테이지 선택, 퍼즐, 자유 놀이로 구분한다.
- 퍼즐 안에서는 일반 조작과 나누기 선택을 구분한다. 수집형의 세기 가능 여부는 R05 조건으로 결정한다.
- 성공 전이는 한 번만 확정한다. 성공 시 완료 기록·미완료 저장 제거·자유 놀이 생성은 같은 상태 변경이다.
- 다시 실행하면 시작 화면으로 들어가 아이가 스테이지 또는 최근 자유 놀이를 선택한다. 저장을 불러온 것 자체로 성공 연출을 반복하지 않는다.
## R13. 필요한 검증과 결과물
현재 저장소에는 게임 코드와 기존 검증 명령이 없다. 개발 과정에서 자동 검증 진입점을 만들고 아래 증거를 남긴다. 이 기획 단계에서 게임 테스트나 빌드를 실행했다고 기록하지 않는다.
| 검증 | 합격 기준 |
|---|---|
| 스테이지 자료 | 고유 번호 1~30, R07과 전부 일치, 총량 보존, 목표 구성 유효 |
| 수학 규칙 | 합체·분리 후 구 ID 보존, 모든 유효 분할, 0개·전체 선택 거부, 목표 다중집합 판정 |
| 수 세기 | 1번의 수집 생략 불가, 미수집 상태 계수 불가, 중복 탭 불가, N개 수집·단일 묶음·계수 모두 필요 |
| 조작·진행 | 이동/탭/회전/핀치 충돌 없음, 순차 해금·재플레이·도움·성공 후 자유 놀이, 30번 이후 경계 |
| 자유 놀이 | 10개 제한, 반납 후 추가, 빈 놀이판 복구, 완료 기록 불변 |
| 저장 | 최소 두 스테이지의 서로 다른 미완료 상태와 자유 놀이를 함께 저장·복원; 구·묶음·계수·카메라 일치; 성공 상태 변경의 일관성 |
| 전체 과정 | 새 저장에서 30개를 모두 해결할 수 있고 마지막까지 해금됨. 자동화 가능한 규칙은 자동 검사하고 화면 연결은 플레이 검사 |
| 구조와 데이터 경계 | 순수 C# 규칙에서 Unity 객체 참조 없음; 화면·저장 결과와 규칙 상태 일치; 플레이 전후 고정 스테이지 자산 불변 |
| 합체 영웅 표현 | 1~10에서 구 개수·기본 크기·배치 유지; 안정 상태의 묶음마다 얼굴·숫자·망토 각각 하나; 2+3→5와 5→2+3 표현 일치; 선택·계수 가독성; 취소·연속 조작·복원 후 오래된 연출 잔존 없음 |
| 자산과 소리 | 서로 다른 배경 테마 3개; 최종 모델·2D·시각 효과·TTS 음성 실제 포함; 출처·사용 조건 기록; 빠른 계수 입력의 순차 발화·마지막 숫자 보존; 화면 전환 후 음성 정리 확인; 효과음·배경음악 제외 |
| 조기 Android 검사 | 첫 수 세기 퍼즐의 APK 설치·실행, 수집·계수, 성공 전 배치의 종료·재실행 복원. 전체 30개 완료 검증으로 대신 기록하지 않음 |
| Android 빌드 | 설치 가능한 APK 생성, 빌드 로그와 사용한 에디터·도구·대상 설정 기록 |
| Android 가상 기기 | APK 설치·실행, 가로 화면, 오프라인 동작, 수집/합체/분리, 스테이지 진행, 앱 종료·재실행 후 개별 퍼즐과 자유 놀이 복원 확인 |
- 결과물은 Unity 프로젝트, 안드로이드 APK, 자동 검사 결과, 가상 기기 검증 기록이다.
- 기능 구현 전에 해당 요구사항의 정상 동작·무효 동작·경계 조건을 합격 사례로 정한다. 필수 사례가 누락되거나 실패하면 그 기능을 완료로 표시하지 않는다. 필요한 검사 미실행은 통과와 구분하며, 실행한 소스 상태와 빌드에 검증 증거를 연결한다.
- 작업 환경의 Unity `6000.6.3f1`에는 Android Build Support와 Unity 제공 SDK·NDK·OpenJDK가 설치되어 있다. 설치 확인은 APK 빌드 성공과 별개이며, 빌드 결과로 개발 환경을 검증한다.
- 가상 기기 실행이 환경 문제로 불가능하면 실패 원인과 미검증 항목을 명시한다. APK가 생성되었다는 이유만으로 검증 전체를 완료 처리하지 않는다.
- 실제 손가락 조작, 스마트폰·태블릿별 가독성, 실제 기기의 성능·발열·배터리 확인은 기기 준비 후 수행한다. 가상 기기 결과로 이 항목들이 검증되었다고 주장하지 않는다.
## R14. 자산 제작 범위와 인수
- 자석 친구·얼굴·마스크, 배경, 화면 그림, 연결 빛·자력 망토 등 특수 효과와 한국어 음성을 제작·확보하여 앱에 포함한다. 효과음·배경음악은 첫 버전에서 제외한다. 기존 수량·배치·조작·성공 규칙은 유지한다.
- 자석 표현과 프리팹·효과는 Unity에서 만들고, 입체 배경 소품은 Blender로 제작한다. AI 이미지는 실제 얼굴·UI 그림·텍스처에도 활용하며 스타일·투명 가장자리·사용 조건을 검수한다. 동적으로 바뀌는 숫자와 한국어 문구는 실제 텍스트로 표시한다.
- 배경은 서로 다른 테마 3개로 구성한다. 공통 소품을 재사용할 수 있지만 주요 소품·색·배경 표현으로 테마를 구별한다. 같은 테마의 배치를 여러 스테이지에서 사용하며 30개의 별도 배경 씬을 요구하지 않는다. 테마 이름·소품·색·스테이지별 테마 배정은 미술 조정 대상이고 R07의 순서·목표는 고정이다.
- 테마는 고정 스테이지 정의에서 가져온다. 성공 후 같은 공간을 유지하고 퍼즐·자유 놀이를 복원할 때 원래 스테이지의 테마를 사용한다. 배경 변경이 새로운 성공 조건이 되지 않는다.
- 효과음·배경음악은 사용자의 2026-10-06 결정으로 첫 버전에서 제외한다. 후속 요청 전에는 제작·선정·구매를 진행하지 않는다. 기존 무음 연결은 정상 범위이며 반복·믹싱 청취 인수는 이번 필수 검사가 아니다.
- 한국어 음성은 로컬 Qwen3-TTS로 미리 파일을 제작한다. 기존 conda 환경 `qwen3-tts`를 재사용하고, GPU에서 `Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice`의 한국어 `Sohee` 목소리를 사용한다. 실행 담당자가 숫자·안내·실제 도움 문구의 대본과 생성 절차를 준비해 이 환경에서 파일을 생성·검수한다. 게임과 빌드는 Qwen3-TTS·모델·conda 환경에 의존하지 않으며 완성된 음성 파일만 사용한다. CPU·다른 모델/목소리·클라우드 TTS로 제작 방향을 자동 변경하지 않는다.
- 제작 환경의 패키지 설치, 모델 가중치 확보, 모델 로딩, GPU 추론, 파일 생성, 발음·품질 검수는 별도 확인 단계다. 현재 패키지 설치와 클래스 import는 확인했지만 PyTorch는 CPU 구성이다. GPU 실행과 모델·음성 생성은 검증하지 않았다. 개발 실행에서 필요한 GPU 환경을 정비하고 실제 한국어 샘플로 확인하며, 설치나 import만으로 제작 준비 완료를 선언하지 않는다.
- 음성은 고유 ID와 한국어 전체 대본, 고정 모델·목소리·언어 및 생성 설정을 연결하여 제작한다. 문구나 설정이 바뀌면 해당 음성을 새 후보로 생성하고 채택한 원본·Unity용 파일·생성 이력의 대응을 유지한다. 생성 성공과 청각 검수 통과를 구분하며 같은 seed를 기록했다고 동일 파일이 재현된다고 주장하지 않는다.
- 숫자 읽기는 하나~열의 발음과 계수 순서를 보존한다. 빠르게 눌러도 입력과 계수 표시는 즉시 처리하고 발화는 겹치지 않게 순서대로 재생한다. 중복 탭은 발화를 추가하지 않는다. 성공 후 같은 보드에서 자유 놀이로 바뀌어도 대기 중인 숫자 읽기를 끝내며, 축하 소리로 마지막 숫자를 가리지 않는다. 소리 때문에 조작이나 성공 판정을 지연하지 않는다.
- 화면·스테이지 전환과 앱 일시 정지에는 이전 음성 큐를 정리한다. 복원으로 지난 계수·성공 음성을 반복하지 않는다. 상태가 바뀌면 오래된 대기 안내를 취소하며 안내가 계수 순서를 끊지 않는다.
- 제작 원본과 Unity에 넣는 파일을 구별하고, 실제 최종 파일별 출처·사용 조건·생성 도구/모델·대본/프롬프트·수정·검수 기록을 남긴다. 필요한 사용 고지를 결과물에 포함한다. TTS 모델·개발 도구·제작 캐시는 앱에 포함하지 않는다.
- 임시·사용 조건 미확인 자산으로 최종 자산 완료를 선언하지 않는다. 필수 로컬 TTS 출력이 없거나 청각 검수가 끝나지 않았으면 해당 완료 항목은 미완료다. 효과음·배경음악 제외는 미완료가 아니다. 파일 존재만으로 발음·가독성을 통과 처리하지 않는다.
-24
View File
@@ -1,24 +0,0 @@
{
"status": "approved",
"recordedUtc": "2026-10-09T14:14:19.8102344Z",
"userStatement": "완성본을 최종 승인합니다",
"scope": "완성 제품과 변경된 제작·채택·운영·검증 문서",
"branch": "dryforge/hero-meadow",
"approvedHead": "0d992d9dcc94843a72489b454ca4e8137178bc56",
"testedProductSource": "3346eb559bdfba24332feb4d9be4b7104520233e",
"productFilesUnchanged": 732,
"apk": "artifacts/android/MagnetismHeroes.apk",
"apkSha256": "DA533F235A9869E5A68F4A244AF9A89E34A8E6FB9E7C7F2F57D830939507B829",
"gallery": "artifacts/verification/VisualUpgrade/FinalProduct/index.html",
"finalReview": "artifacts/verification/VisualUpgrade/FinalProduct/final-review.json",
"finalReviewSha256": "F11A355FF4316FADBA7D5532C481A5215F7CBF722F6C207D86D0781906A846EA",
"archive": ".dryforge/002",
"integration": "merged-local-dev",
"unityEditor": "정리 중 에디터 종료나 설정 변경 명령을 실행하지 않음",
"editorObservation": {
"observedUtc": "2026-10-09T14:15:23.2646118Z",
"pipelineInstances": 0,
"runningEditorDetected": false
},
"integrationEvidence": "artifacts/verification/VisualUpgrade/FinalProduct/dev-integration.json"
}
-186
View File
@@ -1,186 +0,0 @@
{
"documents": [
{
"source": "C:\\git\\MagnetismHeroes\\.dryforge\\handoff.md",
"archive": "C:\\git\\MagnetismHeroes\\.dryforge\\002\\handoff.md",
"sha256": "F29029B4AD620E8830408C5A4497A98193259DA709580F98A25343C360648982"
},
{
"source": "C:\\git\\MagnetismHeroes\\.dryforge\\spec.md",
"archive": "C:\\git\\MagnetismHeroes\\.dryforge\\002\\spec.md",
"sha256": "7F2B2E656B31AEF0124916A5EEA49E6DBF44A93C09C2009B1B59B7CD250FBE1F"
},
{
"source": "C:\\git\\MagnetismHeroes\\.dryforge\\plan.md",
"archive": "C:\\git\\MagnetismHeroes\\.dryforge\\002\\plan.md",
"sha256": "3074AB3A71C136530933ACA4ABAA52BBD001C52B97D5EB15D53AFC8125FE64EE"
}
],
"executionSupport": [
{
"original": "reference/cartoon-landscape.png",
"archive": ".dryforge/002/execution-support/reference/cartoon-landscape.png",
"sha256": "6D26FA1B3232FB0A78963A4D7BA140F610B69B073C99FDD0707E08DAB06DF442"
},
{
"original": "reviews/final-review.json",
"archive": ".dryforge/002/execution-support/reviews/final-review.json",
"sha256": "F11A355FF4316FADBA7D5532C481A5215F7CBF722F6C207D86D0781906A846EA"
},
{
"original": "reviews/horizon-fix-conformance.json",
"archive": ".dryforge/002/execution-support/reviews/horizon-fix-conformance.json",
"sha256": "5EBA704C16AB4E7319C10AAD8E337B8737E974DF99966FF4B9D05040F3AC3E22"
},
{
"original": "reviews/model-conformance-T1.json",
"archive": ".dryforge/002/execution-support/reviews/model-conformance-T1.json",
"sha256": "C134B8ACB71ED8A95F6B4A094B9D85670EED69AAA5FA97098EEC5C1B680B9AA0"
},
{
"original": "reviews/model-conformance-T2.json",
"archive": ".dryforge/002/execution-support/reviews/model-conformance-T2.json",
"sha256": "C65F5BB38C25EBFAFB09FD1032A8ECBD5994B19CD4D01C68BC9A08CA5BCD3BE2"
},
{
"original": "reviews/unity-conformance-T4.json",
"archive": ".dryforge/002/execution-support/reviews/unity-conformance-T4.json",
"sha256": "2C67D6BC681F614BB73B0BFA80A4968FD7CA259B9EDEFDDEED219E250AC8385F"
},
{
"original": "reviews/unity-conformance-T5.json",
"archive": ".dryforge/002/execution-support/reviews/unity-conformance-T5.json",
"sha256": "7CE20CD8554FD7BF99F8FDD939E2162E9F32815AE1381600431DAAE8174EB336"
},
{
"original": "audit-final-product.py",
"archive": ".dryforge/002/execution-support/audit-final-product.py",
"sha256": "870C2FBE370BA8C7AF67A3359FCF1D7F6DCA700673B1703AB8087BDF1C6C94AA"
},
{
"original": "execution.json",
"archive": ".dryforge/002/execution-support/execution.json",
"sha256": "E2A5CC7A7E19023C6A6C3ED01BA35DBD09795E5485B7402E63537D15E0A59224",
"previousSha256": "0317C3BB8866D341B355BF278EA1DB8D30A3E37B001A1D7E8532E1CB25B0886F",
"updateReason": "사용자 승인에 따른 로컬 dev 병합 완료 기록"
},
{
"original": "final-product-checks.ps1",
"archive": ".dryforge/002/execution-support/final-product-checks.ps1",
"sha256": "E46FD552A3E7C1B75FA7B0357D5B264FDABD2597E0A27B350DCC64EB566771AA"
},
{
"original": "preserve-before-horizon-fix.py",
"archive": ".dryforge/002/execution-support/preserve-before-horizon-fix.py",
"sha256": "920B812B0E4724FE38EEE8BE1D4F114825D4511DA66AB9E3728AA12A3952B9D4"
},
{
"original": "preserve-failed-smoke-and-prune-stale.py",
"archive": ".dryforge/002/execution-support/preserve-failed-smoke-and-prune-stale.py",
"sha256": "35E995C03C067A73ADD4748AC7D974C797DBC17627013CBC39A239E65A91AA77"
},
{
"original": "preserve-horizon-failed-smoke.py",
"archive": ".dryforge/002/execution-support/preserve-horizon-failed-smoke.py",
"sha256": "A7C077D9CE75F3CDA5C857CA14607DD15F080A3FF25F8326ED6C67600995FDDC"
},
{
"original": "preserve-previous-final.py",
"archive": ".dryforge/002/execution-support/preserve-previous-final.py",
"sha256": "017023C3852D7A687BE2707B7E5C28C1AA73A0662E44F5479402843DFB531EE3"
},
{
"original": "record-final-user-gate.py",
"archive": ".dryforge/002/execution-support/record-final-user-gate.py",
"sha256": "EB584202EDF13CCE5CB13A1A2DB67154B32E2022FD19F46B1903B104AF2874F2"
},
{
"original": "record-horizon-integration.py",
"archive": ".dryforge/002/execution-support/record-horizon-integration.py",
"sha256": "AB851721DC0C4FF2191799C8D338923BA13C595B7CF49DEB70ED3370318A5BE8"
},
{
"original": "record-model-approval.py",
"archive": ".dryforge/002/execution-support/record-model-approval.py",
"sha256": "2CCF692FE021EF1CBB6858676BEFA02E650DE318E6CEA9C3F5D091D2E15544FE"
},
{
"original": "record-new-completion.py",
"archive": ".dryforge/002/execution-support/record-new-completion.py",
"sha256": "A3261E88C43C986072637BF3F996014F0C3B92BD98DCB0360A3DCF5237C97AA2"
},
{
"original": "stage-visual-boundary.py",
"archive": ".dryforge/002/execution-support/stage-visual-boundary.py",
"sha256": "25AD7E9EC7D952C47E3875556A024DF54EA3B18F6090B6C95D17FA7779A4AAF8"
},
{
"original": "verify-model-integration.py",
"archive": ".dryforge/002/execution-support/verify-model-integration.py",
"sha256": "DCD39C0FA4B58E2B9BA3A82298B27D24D76D9D3191EB6F4D79A4A34D010E5196"
}
],
"worktrees": [
{
"path": "C:\\git\\MagnetismHeroes\\.dryforge\\worktrees\\T1",
"commit": "7d7efbe6b6a1a419ac998f37e3e019a75570a9e5",
"clean": true,
"ancestor": true
},
{
"path": "C:\\git\\MagnetismHeroes\\.dryforge\\worktrees\\T2",
"commit": "80be80566a309eab8cb333ea9774218c816efc30",
"clean": true,
"ancestor": true
},
{
"path": "C:\\git\\MagnetismHeroes\\.dryforge\\worktrees\\T4",
"commit": "ef03611628cb395d377cf56abd951bbf62cfa29c",
"clean": true,
"ancestor": true
},
{
"path": "C:\\git\\MagnetismHeroes\\.dryforge\\worktrees\\T6",
"commit": "620c73cbd24d936231bee5cb3469e5cff3424cd9",
"clean": true,
"ancestor": true
}
],
"taskBranches": [
{
"branch": "dryforge/hero-model",
"commit": "7d7efbe6b6a1a419ac998f37e3e019a75570a9e5",
"mergedIntoApprovedHead": true
},
{
"branch": "dryforge/meadow-model",
"commit": "80be80566a309eab8cb333ea9774218c816efc30",
"mergedIntoApprovedHead": true
},
{
"branch": "dryforge/unity-visual-upgrade",
"commit": "43b22d67886bc5715d3543fefda2dc24dead299f",
"mergedIntoApprovedHead": true
},
{
"branch": "dryforge/visual-verification",
"commit": "b4d88546a2e9c44e9b6babcc54acaf69a01c75b4",
"mergedIntoApprovedHead": true
},
{
"branch": "dryforge/final-delivery",
"commit": "a906be69a0c1211f01d5a3d1bc7fdcf5554efafb",
"mergedIntoApprovedHead": true
},
{
"branch": "dryforge/horizon-seam-fix",
"commit": "ef03611628cb395d377cf56abd951bbf62cfa29c",
"mergedIntoApprovedHead": true
},
{
"branch": "dryforge/horizon-final-delivery",
"commit": "620c73cbd24d936231bee5cb3469e5cff3424cd9",
"mergedIntoApprovedHead": true
}
]
}
-25
View File
@@ -1,25 +0,0 @@
{
"status": "completed",
"finishedUtc": "2026-10-09T14:14:24.8264555Z",
"archive": ".dryforge/002",
"documentsMovedAndHashVerified": 3,
"executionSupportFilesPreserved": 20,
"removedMergedWorktrees": 4,
"removedMergedTaskBranches": 7,
"productFileHashesUnchanged": 732,
"headUnchanged": "0d992d9dcc94843a72489b454ca4e8137178bc56",
"protectedDevUnchanged": "58410281e015c6600dc543c4e093fd95c036288c",
"remainingDryforgeItems": [
"001",
"002",
"status.json"
],
"integration": "merged-local-dev",
"unityEditorUnmodifiedByCleanup": true,
"editorObservation": {
"observedUtc": "2026-10-09T14:15:23.2646118Z",
"pipelineInstances": 0,
"runningEditorDetected": false
},
"integrationEvidence": "artifacts/verification/VisualUpgrade/FinalProduct/dev-integration.json"
}
@@ -1,58 +0,0 @@
import hashlib
import json
import subprocess
import xml.etree.ElementTree as ET
from pathlib import Path
root = Path('C:/git/MagnetismHeroes')
proof = root / 'artifacts/verification/VisualUpgrade/FinalProduct'
final = root / 'artifacts/verification/Final'
def read(path):
return json.loads(path.read_text(encoding='utf-8-sig'))
def sha(path):
return hashlib.sha256(path.read_bytes()).hexdigest().upper()
completion = read(proof / 'completion.json')
source = '3346eb559bdfba24332feb4d9be4b7104520233e'
assert completion['status'] == 'passed' and completion['sourceCommit'] == source
assert len(completion['phases']) == 4 and completion['productHashesUnchanged']
for phase in completion['phases']:
assert phase['exitCode'] == 0 and phase['status'] == 'passed'
assert phase['sourceCommit'] == source and phase['sourceDirty'] is False and phase['sourceDirtyEnd'] is False
raw = read(Path(phase['evidence']))
assert raw['status'] == 'passed' and raw['exitCode'] == 0 and raw['sourceCommit'].strip() == source
counts = {}
for mode, count in [('editmode', 103), ('playmode', 53)]:
cases = ET.parse(final / (mode + '.xml')).findall('.//test-case')
assert len(cases) == count and all(case.get('result') == 'Passed' for case in cases)
counts[mode] = count
apk_hash = sha(root / 'artifacts/android/Final/MagnetismHeroes.apk')
assert sha(root / 'artifacts/android/MagnetismHeroes.apk') == apk_hash
for path, count in [
('android-final-evidence.json', 226),
('Fresh30/android-final-progression-evidence.json', 151),
('FinalBoundary/android-final-boundary-evidence.json', 37),
('RapidBoundary/android-final-rapid-evidence.json', 39),
('ContentBoundary/android-content-evidence.json', 69),
]:
data = read(final / path)
assert data['status'] == 'passed' and data['sourceCommit'].strip() == source
assert data['apkSha256'].upper() == apk_hash and data['sourceDirty'] is False
assert len(data['checks']) == count and all(check['passed'] for check in data['checks'])
counts[path] = count
before = read(proof / 'product-before.json')
after = read(proof / 'product-after.json')
assert before == after and len(before) == 732
assert all(sha(root / path) == digest.upper() for path, digest in before.items())
assert subprocess.check_output(['git', 'status', '--porcelain'], cwd=root).strip() == b''
assert subprocess.check_output(['git', 'rev-parse', 'dev'], cwd=root).decode().strip() == '58410281e015c6600dc543c4e093fd95c036288c'
for failure in completion.get('failedAttempts', []):
assert failure['exitCode'] == 1 and failure['status'] == 'failed'
archived = read(Path(failure['evidence']))
assert archived['exitCode'] == 1 and archived['status'] == 'failed'
assert Path(failure['outerLog']).is_file()
report = {'status': 'PASS', 'testedProductSourceCommit': source,
'currentHead': subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=root).decode().strip(),
'counts': counts, 'apkSha256': apk_hash, 'productFileHashesUnchanged': len(before),
'failedAttemptsPreserved': len(completion.get('failedAttempts', [])), 'protectedDevUnchanged': True}
(proof / 'root-final-product-audit.json').write_text(json.dumps(report, indent=2) + '\n', encoding='utf-8')
print(json.dumps(report))
@@ -1,917 +0,0 @@
{
"feature_branch": "dryforge/hero-meadow",
"protected_base": "dev",
"initial_commit": "58410281e015c6600dc543c4e093fd95c036288c",
"started_utc": "2026-10-08T01:35:25.790795+09:00",
"protected_product_hashes": {
"Assets/InputSystem_Actions.inputactions": "1c6cbb519d65ecfe19a29231735b7f54741a20ac1a8ad4f46a18e1bfb28a3c39",
"Assets/InputSystem_Actions.inputactions.meta": "3b4e424a80f5b4fa76b6abd774884a24285eaa61f22ff9bb20e5ad505fd8b569",
"Assets/MagnetismHeroes.meta": "a4234b3a59055b04475ba74daad6190a023683099de4cb4fd08644651843dd9c",
"Assets/MagnetismHeroes/Core.meta": "de2805e10269b6d5230d5737d514ba7bd882aeed21c9709061a04d418758a09e",
"Assets/MagnetismHeroes/Core/AGENTS.md": "a80bb0bf851d469e6ed975e95a5849d6888d8f01a0991725b96a5cdcdc1bc715",
"Assets/MagnetismHeroes/Core/AGENTS.md.meta": "c40cb22815e8ed2264dabd920de5ba7a261cfa19f7bf1def141dbaf8eebbad36",
"Assets/MagnetismHeroes/Core/AssemblyInfo.cs": "7b8cc02b8a3798cddf7f944256f8ec7caf4ad5110df2203fa94c12b0159a9ae2",
"Assets/MagnetismHeroes/Core/AssemblyInfo.cs.meta": "1192dd1392f04fe3b208df16c762396e8d918e0f0d2bd26cba932ee8bde01aa3",
"Assets/MagnetismHeroes/Core/BoardHint.cs": "cc3dc3f021d8a8de5d99e3c4d2f8c4fdec5de75ebbafaf5e0777e64629c4de79",
"Assets/MagnetismHeroes/Core/BoardHint.cs.meta": "cfc017f9dc78f2993b29af89fe8b2ca81bf225613e6b9456abcc6757306f4762",
"Assets/MagnetismHeroes/Core/BoardRules.cs": "e1bba538b0dc9176ce451c5a940a6cde6c5004590a8e669949425d1162772920",
"Assets/MagnetismHeroes/Core/BoardRules.cs.meta": "1c790281e4d8ed75501199425d97d417a31c512eaaf68df1cc5d50c5483ab065",
"Assets/MagnetismHeroes/Core/GameData.cs": "854912cffe249a4cf23013a103a1750b15016ca3fcd9c6360f388d806e6a1942",
"Assets/MagnetismHeroes/Core/GameData.cs.meta": "4049c18bdf2bc1650039a7e761253b188488304840f5cbd1fb27109fd28d16ba",
"Assets/MagnetismHeroes/Core/GameSession.cs": "d5ac5b37f5e133614cb574a191929432f1802a9887ff08154b2280eb9f644c97",
"Assets/MagnetismHeroes/Core/GameSession.cs.meta": "967e33fafdd69d6df8afd63a79a76ee5a6b81c35c55a3481dee8a5aeccdf09fd",
"Assets/MagnetismHeroes/Core/MagnetismHeroes.Core.asmdef": "a3b77330b9bd624393eb31b5123c1b311f5ad168a9286afe24cb07b5051c5f3b",
"Assets/MagnetismHeroes/Core/MagnetismHeroes.Core.asmdef.meta": "59f47f86977a939fc944ae8235ef8c3ab9567edf17d0205477c614a2807fb5ac",
"Assets/MagnetismHeroes/Core/SaveValidator.cs": "339c27b556d2ec1f279da9cb211bcc46fc5e1cb99df4103618fa1633ecc04baa",
"Assets/MagnetismHeroes/Core/SaveValidator.cs.meta": "7bb6a48c26b24e8c4ee8b12a69906b22367b232f0fabf5f11663085c68be8bfb",
"Assets/MagnetismHeroes/Editor.meta": "392ee7b3635d4a43568a780cee7b982d3e21c9bc45533d9452c31448611fe1ff",
"Assets/MagnetismHeroes/Editor/AGENTS.md": "c448aa32d82c999d294634eb10fda77b2a570e0cdbe2ba9314991517f900fd60",
"Assets/MagnetismHeroes/Editor/AGENTS.md.meta": "6a1ced2b558b739144f509d5574f93c6b1db650dd1ec34f55803ce0db0ac1df8",
"Assets/MagnetismHeroes/Editor/AndroidBuildSanitizer.cs": "9946d40e8d6d8450a51155f9183d35bc789084f44f4a685b51774914cb055d49",
"Assets/MagnetismHeroes/Editor/AndroidBuildSanitizer.cs.meta": "30f7c19b85d5d3d554c82ec97c1495a7f59a19cf4acf9dfc45de901302eec2bc",
"Assets/MagnetismHeroes/Editor/CompositionAssets.cs": "5c03d7a80205b9c83b680ab4bfd41e016f3c53bd534ecd2a10cd0c4c91eb9562",
"Assets/MagnetismHeroes/Editor/CompositionAssets.cs.meta": "3e4f8eba6f5af6977d896574eb348aac946d6ee81405add081411f960a8df20f",
"Assets/MagnetismHeroes/Editor/ContentAssets.cs": "f79ec21a4868c7bbff8ebfc7777e7ed7fed574797658f730e2d70643ac8b2d43",
"Assets/MagnetismHeroes/Editor/ContentAssets.cs.meta": "21c02d17dc78f57386f8de2f244d1a53a145977fa3b658049838cdc7ad423332",
"Assets/MagnetismHeroes/Editor/FirstSliceAssets.cs": "1641225508a07451bd283682bc25c4f24ac21fdab18a873ceb5a02994eb2f819",
"Assets/MagnetismHeroes/Editor/FirstSliceAssets.cs.meta": "b989e9614c406e1313593508980e50e3ddd846d5a43cb01d51e29e2d6569f9ed",
"Assets/MagnetismHeroes/Editor/MagnetismHeroes.Editor.asmdef": "f2b62ac5f1186367e40021d12c878b6f057fc3ce7b4672adfcb2211189a32762",
"Assets/MagnetismHeroes/Editor/MagnetismHeroes.Editor.asmdef.meta": "c139a049cb5cbad7782d39566fcc2cf6f13a5822524640c5d4df3ef8b1c033e1",
"Assets/MagnetismHeroes/Editor/PackageInstaller.cs": "738a95b580c4bf70ae095dc52962b711661192fbdfb9c5206d9c51d8b3309d21",
"Assets/MagnetismHeroes/Editor/PackageInstaller.cs.meta": "a30063297a6235e8be9eebbf28f13e97ee3a7f68e40c9ba84483a93699f7a197",
"Assets/MagnetismHeroes/Editor/PersistenceAssets.cs": "a9e3d3e7d70b6d458904f92a82af2faac8fdc06d597a77c26b724d7367c128d5",
"Assets/MagnetismHeroes/Editor/PersistenceAssets.cs.meta": "00e7a48d1e203ae7e0c5c8337d797992ab5efb34ac17a470d638e9b934eaeb8c",
"Assets/MagnetismHeroes/Editor/PresentationAssets.cs": "a530b8fe72f26b242fd8482a83082b1bd99743b5538b713bfdeeb56b87f84a13",
"Assets/MagnetismHeroes/Editor/PresentationAssets.cs.meta": "46e6e43a82ab1141f43b30781673cd56a2f25b84efdbae27847e39e0bde85ed8",
"Assets/MagnetismHeroes/Editor/PresentationProof.cs": "1c350672efcd719302a92b91f8528cd0331ae483d37f4d7053491d603f798f0b",
"Assets/MagnetismHeroes/Editor/PresentationProof.cs.meta": "da0a39a723e2436d923b2312120a0e309b9f64c5948f69f1c9fbe5cb73cce089",
"Assets/MagnetismHeroes/Editor/ProjectBootstrap.cs": "0b8b95d8cb5c93bff3721fa8b34e97b2a8658df8ce75c9ceb75b30c1ad29384a",
"Assets/MagnetismHeroes/Editor/ProjectBootstrap.cs.meta": "079d1d993195e12ddd9c273345915818561459869397ba016416161efd69c81e",
"Assets/MagnetismHeroes/Scenes.meta": "b08de2ba7963250cc8201e9602085974164758f6cd7486d6702b191cdcfdb27c",
"Assets/MagnetismHeroes/Scenes/Main.unity": "26154649ce0c4cdd160d053227c1fd6f0190e9ae829f2be0ccb427f5deeb357a",
"Assets/MagnetismHeroes/Scenes/Main.unity.meta": "fb9e84bc5440c5d697e941526e912df3c08bb066b4078b9d027f0ad3b8412120",
"Assets/MagnetismHeroes/Tests.meta": "18ce57c5f66e3efa85146b4d53a0454630d77d2c9ab1a1ca67501541132eb7d4",
"Assets/MagnetismHeroes/Tests/EditMode.meta": "a101782b25f079b6daca580215b74d05651604f56daa24bbb62d11b635136869",
"Assets/MagnetismHeroes/Tests/EditMode/CompositionAssetTests.cs": "671d8d1a790d531eb7684b71de92d3503a6d7eba91d24701613cc55dfdb79aac",
"Assets/MagnetismHeroes/Tests/EditMode/CompositionAssetTests.cs.meta": "18afc2728da017b757b6bc8092e4eb1d5866bc0b1d614e702bfdd72c4b563738",
"Assets/MagnetismHeroes/Tests/EditMode/CompositionSessionTests.cs": "52eaa3b3edc993e0e916e96eaf2144b72f4dbed0738ff1f3cb8e16e3eecf0454",
"Assets/MagnetismHeroes/Tests/EditMode/CompositionSessionTests.cs.meta": "73c88128561e5328bafb35e579e86c265f5df73ea33d52ce4f0398a7631c0a11",
"Assets/MagnetismHeroes/Tests/EditMode/ContentAssetTests.cs": "d7ead26afbcaba631f58bcfbb0e84b9c58c79f61915c05df3caaf829afe13ec9",
"Assets/MagnetismHeroes/Tests/EditMode/ContentAssetTests.cs.meta": "c2e30ce735635744a27aedacf054129cdc017b736839314bc8f03b871d7f5c99",
"Assets/MagnetismHeroes/Tests/EditMode/ContentSessionTests.cs": "53b28af4e7ef50a80f35815948ff7e58e4eaed3581bf9b1ba4fda9b687f17afc",
"Assets/MagnetismHeroes/Tests/EditMode/ContentSessionTests.cs.meta": "fd12187acbe9ae1a53c0b4a8ce27755abd1f171d4a223a8e5fc16497f3bb8b9e",
"Assets/MagnetismHeroes/Tests/EditMode/ContentVoiceTests.cs": "0a98907804ae976dbac37bf1d2b2211905f17059871b490b870f79712f45591b",
"Assets/MagnetismHeroes/Tests/EditMode/ContentVoiceTests.cs.meta": "95cf38a75101caaedf3c7f7b59328460bbccbbc7bd82c96be5e34a816518c240",
"Assets/MagnetismHeroes/Tests/EditMode/Core.meta": "be46851d141c126a9706049aee91e375c76dd3747ca109271d881f2015e748ab",
"Assets/MagnetismHeroes/Tests/EditMode/Core/BoardRulesTests.cs": "0466ffa2f94a3ec59cddea4267ca7591f1a2f585343902e85666302fee548a17",
"Assets/MagnetismHeroes/Tests/EditMode/Core/BoardRulesTests.cs.meta": "04dcdd8030b52c7350d1249dcfb457259655049a6ef46e030f22960d80eac21f",
"Assets/MagnetismHeroes/Tests/EditMode/Core/SaveValidatorTests.cs": "141f1f53aa66aa9e7372828c2e8ee6f4237c6ec20356ca201164dc014f88e1e9",
"Assets/MagnetismHeroes/Tests/EditMode/Core/SaveValidatorTests.cs.meta": "1254a358e3bd9efeb86212abded98d50ee54a594cf61b1e07f01d1ff1f1ff505",
"Assets/MagnetismHeroes/Tests/EditMode/Core/StageLayoutTests.cs": "310ebf2b56eb4d6fdf1e23ef24b7714ef3a069f1ea6610f07a5d7e02ab7b1708",
"Assets/MagnetismHeroes/Tests/EditMode/Core/StageLayoutTests.cs.meta": "4c8ba2f93312e3661a919c843c889c89bd52d16133efa1c5924653bde25c921c",
"Assets/MagnetismHeroes/Tests/EditMode/FirstSliceAssetTests.cs": "7ebe9bb7eec107502c765c952ba066e89f6677674888579b840f2ec915a144c3",
"Assets/MagnetismHeroes/Tests/EditMode/FirstSliceAssetTests.cs.meta": "189dba50e2a632b9c9a823f13a8ab402b8f71860ec75b67fb337c7d3f3f2eb17",
"Assets/MagnetismHeroes/Tests/EditMode/GameSessionTests.cs": "eebeb7e6aaafc6f1cbb7cd1a263e5612b2a86db83a4b4fc03fbf71f9697ec4b0",
"Assets/MagnetismHeroes/Tests/EditMode/GameSessionTests.cs.meta": "4bfeea0e3c0ca73ed34013fec015adbb3826e402484dfd536c44c41c76c475be",
"Assets/MagnetismHeroes/Tests/EditMode/LocalProgressStoreTests.cs": "c21e95031974cc68ec624627176c2d30b75aba5ff724794b07737c9f1aeb7051",
"Assets/MagnetismHeroes/Tests/EditMode/LocalProgressStoreTests.cs.meta": "185b21bbc833a65793a8cd42f50fdd16c42f171f6150f82004c190d0fd053dd5",
"Assets/MagnetismHeroes/Tests/EditMode/MagnetismHeroes.EditModeTests.asmdef": "6ea5923da2fe43e9a968be26502c504e7730913b357944a40c061e131b78ee56",
"Assets/MagnetismHeroes/Tests/EditMode/MagnetismHeroes.EditModeTests.asmdef.meta": "0f437b8a74cc8240882f01b2b649ef8087e8c3ef66b4a4a9a8f412d0bf9dae50",
"Assets/MagnetismHeroes/Tests/EditMode/PersistenceSessionTests.cs": "f5445798e640239142161cd83b7c9b321d309b4f6ed8ab1433b740f8d7053ff5",
"Assets/MagnetismHeroes/Tests/EditMode/PersistenceSessionTests.cs.meta": "5913c492b66095714d8e0150192d0f7672bd923b79339ce96b56d8c260152549",
"Assets/MagnetismHeroes/Tests/EditMode/PresentationAssetTests.cs": "58efeba812f22c28b2dd721ca3fe94afcea220f4b87e2a9f58672ed393903ff4",
"Assets/MagnetismHeroes/Tests/EditMode/PresentationAssetTests.cs.meta": "0bd7661784fb24aae755f6b2722360b2fc1a15cec75cc2760f3f5f29f66090fe",
"Assets/MagnetismHeroes/Tests/PlayMode.meta": "d4fdb69ba2114665648c9465fb5377289690098ff5ad75d13c79c4af6a7bd669",
"Assets/MagnetismHeroes/Tests/PlayMode/CompositionPlayTests.cs": "8fb76df4bcbb777ebfa163b7d99ce755625cf3dbc12775a161732ff13a0a660d",
"Assets/MagnetismHeroes/Tests/PlayMode/CompositionPlayTests.cs.meta": "f24b1269ff391a00693dcdd856e1ea64901f74ec4b14cf44ff300f6d3986b91a",
"Assets/MagnetismHeroes/Tests/PlayMode/ContentPlayTests.cs": "55be21e779132f693d05cf28780d5c7d4b2d190cf0b5bdf5395923efaf0f849f",
"Assets/MagnetismHeroes/Tests/PlayMode/ContentPlayTests.cs.meta": "2f260827c964055294d2d2f155a67f4a0951ebe4a6fe470b1f13bb8e35bbe550",
"Assets/MagnetismHeroes/Tests/PlayMode/FirstPuzzlePlayTests.cs": "38d28d56d02e46ff190d347936119232cbde3e0c748c875cdb075cb44dcb0d1a",
"Assets/MagnetismHeroes/Tests/PlayMode/FirstPuzzlePlayTests.cs.meta": "722c0e3a05254eafb1206303cc0b412aff57d8f1ec26a76b30a0eeb46069d4af",
"Assets/MagnetismHeroes/Tests/PlayMode/MagnetismHeroes.PlayModeTests.asmdef": "06c4bfc9c973f9d1610560687fed27d3505ea0807a626744d5da5544733d8790",
"Assets/MagnetismHeroes/Tests/PlayMode/MagnetismHeroes.PlayModeTests.asmdef.meta": "9518a4247dfe92ac5b4a3fa1e262dcd4997a20ea636fde37d9a77af71587aa61",
"Assets/MagnetismHeroes/UnityRuntime.meta": "33a905e7ef9b11f270fbc5f086714d34edc7ba2bbfff4353f69fe7e295ea1a58",
"Assets/MagnetismHeroes/UnityRuntime/AGENTS.md": "0f6ffc45d7b1c894a791320e8bce1b80e9bac9aa225202176e4edab123b5db90",
"Assets/MagnetismHeroes/UnityRuntime/AGENTS.md.meta": "1670b8878b8c1c14f2ef35ad51a3888821bb382f67d3bd401bd3a8d5c52c8c49",
"Assets/MagnetismHeroes/UnityRuntime/AssemblyInfo.cs": "4b227cad2c5dcd6e90da3c32640259466a124890e51a2ae343023c758c7840a2",
"Assets/MagnetismHeroes/UnityRuntime/AssemblyInfo.cs.meta": "410c26c65fc3833181d8c0809416f38e4eebb691efac8c61da702eecc40d805b",
"Assets/MagnetismHeroes/UnityRuntime/Bootstrap.meta": "f50ddba1d8572fdf2c9f1d4ee3114da1d69bb8e70a4a80132c56b7d85e7bc7b9",
"Assets/MagnetismHeroes/UnityRuntime/Bootstrap/GameApp.cs": "54fff47f4c6bc9a7c3809027ce15f57e6f1f4a90f8095852149451619a4d07a5",
"Assets/MagnetismHeroes/UnityRuntime/Bootstrap/GameApp.cs.meta": "bd7997f83ee94e0669fcc3d18ef25e8bc290c5e1b03a1f9c66bd76fddb7253d2",
"Assets/MagnetismHeroes/UnityRuntime/Interaction.meta": "8c347d091439696bc3d3b2852c87696c9b410554cbc9a65abbd1648c3f49731f",
"Assets/MagnetismHeroes/UnityRuntime/Interaction/BoardInput.cs": "41823185afead383900aabe85a99b2fda73a4a70ccd768e55418876b4f98b0f9",
"Assets/MagnetismHeroes/UnityRuntime/Interaction/BoardInput.cs.meta": "1211f814ccc275b79080807db6b6014d703a08e49a8504035fd7e7cc4417c3a5",
"Assets/MagnetismHeroes/UnityRuntime/Interaction/OrbitCamera.cs": "78d71cab34243d4fe68a51ebddc41a2adab1b40b1e58c10bd94a2f16bd62f223",
"Assets/MagnetismHeroes/UnityRuntime/Interaction/OrbitCamera.cs.meta": "c0962976f8ab5219d74f02d0a8058b77358f18a44a6bca0ccbf9721ef727fcd0",
"Assets/MagnetismHeroes/UnityRuntime/MagnetismHeroes.UnityRuntime.asmdef": "8e618272389c248dc28d7e0261ea52d45387ac90bc46a7821576c9c1a17bca52",
"Assets/MagnetismHeroes/UnityRuntime/MagnetismHeroes.UnityRuntime.asmdef.meta": "78d46f2e1f511baada1b617c328ad0374e1880545ca27764f7dba8a52157ec24",
"Assets/MagnetismHeroes/UnityRuntime/Persistence.meta": "e08035f8440cffe9537c612b97c72b580bb27474d7de66cd81b013ae5784fd25",
"Assets/MagnetismHeroes/UnityRuntime/Persistence/LocalProgressStore.cs": "535278ccc141990f00342d6d32b12bff039e6e742fe494b82adbd39f74de46d0",
"Assets/MagnetismHeroes/UnityRuntime/Persistence/LocalProgressStore.cs.meta": "7ac5e2b29573063595f1e15bed404acdc04b1b0c45f90d75808b9746a1b7ff79",
"Assets/MagnetismHeroes/UnityRuntime/Presentation.meta": "093df3389c613b936dd393cf3625ff0f26bfd408d0f7047b37eeb94762b702f9",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio.meta": "d0a391ffadaef257630a38e23f5aaa923f616b600ea2655ff24c59ab88230344",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/GameAudio.mixer": "54ab67ac0e2ac14ad3cfdcb10ce1ff4ab08f921a0f31fd5bc9d4a651f5594247",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/GameAudio.mixer.meta": "bddf1fe71193cbade06c9e1e013f4ac127b95e1060df840ceff2f556984cadd1",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice.meta": "085d19326a2646680cac08c6a5f4cf7c94b84971955696375fe3ebc7dacc5361",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_01.wav": "13a882ce1620140d447635e14f2d4758045d5e348960c17386df1c720050c100",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_01.wav.meta": "485dd82a353c2fb267ddb8b13d92ee77344049ac04040df98fb4ffae3d3d3a1a",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_02.wav": "278dd0b6e96f2abca2237ff98eda453a011fa0b2e78d5575b7ddc9c9b3028b32",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_02.wav.meta": "39e0960145943bda38822ed7942ab58d4c22c24414bc571a214eb6b9c4e89c57",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_03.wav": "4133b0150aa08e26f1b93b11d29ecab1282bbff236b3c92f8416e781109188c1",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_03.wav.meta": "fba28c38f33a759b2224f5c4c52a9ac4c7aaed42339733b7752fd8d5fde03d40",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_04.wav": "c5c8c78ccd89011050d7317960a854130e3e445c8b92d23e9520eeb7490aa300",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_04.wav.meta": "8f12f33d293025d7738539b5de2350221e8c63aa3ea3bf7869af786406a2f216",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_05.wav": "700b9827ea827adeae0ebfc3b4af2b9125d7d3eddc699c63c0ce5c1ba5cbd04c",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_05.wav.meta": "718c3625f6cfa26459a58cfd39200b5bfbbbe26c5b6c28071320fba2e6a00db8",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_06.wav": "d4800a23a73d1315987d9129d260baa0680621d6502351137845a6ee351c6274",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_06.wav.meta": "deef2078506c24ccd884be8427bf9cb7e72391e430c7354dcfac5003fd2319b2",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_07.wav": "7d4ba785ccb28b8f2bf0d8f66053dc8adf1a19d332bedfab3bffd596208fe829",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_07.wav.meta": "394160eee1e91790b2608ec186693cc4b319fe39608cf5cb6eb7930c8112b625",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_08.wav": "fa8034948f6ac3e289e8e7e9fb447943760d4ed5ac577c8ab0c961c608fe49c2",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_08.wav.meta": "bda6f2d81d14dd0607c738ff5b36fc451bd0bd5f6c273f9d6db39d572c357d8a",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_09.wav": "4a921bc405ed5181e26365764ef94c2d311af45ed5508b27ffd7ac6d53d83802",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_09.wav.meta": "13cc0421fcfd58e85c6c0d32e7ce27f422d35d0c6ed778f672ab3c44fdcf36f4",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_10.wav": "b16ebccea20aedf93b884299edb0dd430f67e4deef069fb354b482f21b8e5fb8",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/count_10.wav.meta": "9e4047deae1118cc646c3f4bfe60766522cd63f187bb7b93a68707da384c709e",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/demo_composition.wav": "570452e9fa15ec545b126cd8bdba504a3db6f30b74a95405ee228015368761e7",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/demo_composition.wav.meta": "253482825f08dad867f1d0edab230bde3e27fcc7a3216535ea92c7eb4c4a5aec",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/demo_merge6.wav": "6300c5490442c3168a52611888568ce37fdaffa82b1b72780e5c9eb5ce81fa53",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/demo_merge6.wav.meta": "de3a9691371d5f3e2d27f22228651e2e6d1e8e0c8a50d9213fc56305fa8ff5f5",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/demo_split10.wav": "6bc2d42b24170fa0dda48ed8c5a24b147219e7237d9989432026cbf028e81712",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/demo_split10.wav.meta": "ae826ee7337d2dc3eb69159a6a693053a4daefb03d94434506927c94ea543c9b",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_collect.wav": "62e15c9e38c36f4094a6c7e9c5148510141290b5f41db5aad9dc1d8f1a158594",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_collect.wav.meta": "efed428f46d5019e3e55c6f5a8942f35b2bbd6762d0a34719bb52394f1a8e65f",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_count.wav": "debe29b89f3e2856f2bf4c5e01707a5f0a0657e5768c03be385ec88fa2328b40",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_count.wav.meta": "b5cc454fb99ec1d96b026d472cff2e11dcf461a50b4ee72ee5d9547424991774",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_find.wav": "14de802d534e95053b89c93f8ddd86510e1f68ac4bbf790f8b1f5274c2c34c41",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_find.wav.meta": "2f4ada332bdd1f4e0a1093c8a9b9bd6d9888de52851a251b3fbc9589ec405b9c",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_merge.wav": "522db037e0358b01b84dfc68f13e62073495a011a96d8ca407be28f36e98e16d",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_merge.wav.meta": "737f730ba365c64a03f00c72075ac3ff1f9c1508c38739c07bf3f4a8e6f43ec5",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split.wav": "72df6edc53a6e320e48b6bae18ba6bed43034028795021ff48d6f04bc4dc7649",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split.wav.meta": "6734167fefec385cb723dc63ce4af9a17231b163aeb468cb9191485771a00a12",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_10_1.wav": "045b2e2065ba0eb44fc5c49405227b7dc042a0a20e88b5aeae87bbea24d0a0c7",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_10_1.wav.meta": "e3cd6effc9df1ba883dcf97d15c94a4d10fdb5a676883adc9e3cff6fec08ab1e",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_10_2.wav": "f3d7912563c761ca8ebace355000ceb1522f6f31a2e15735c712c0d26d011ed6",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_10_2.wav.meta": "fdf14437e475f79953de36042796674e8d1dc7fef7f4a641c3e196b8904e9d93",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_10_3.wav": "dc7f9f9750379fae5b7e8067d467cf6704a6501c3f1a57f4b50a93191ac7140f",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_10_3.wav.meta": "2847f192b3ca561be8157d325de552df3ac40627eb52efb13f0a9672617dfb33",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_10_4.wav": "d1ad45d2432befcfc4d78c29c5c355bdd42eb02492c6ed668c3c0d8ce9c0822d",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_10_4.wav.meta": "2e433be17e683beb83da1edb21b729a23b37ee0ae3f450dd1d8a5ff18f5002d9",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_10_5.wav": "28680c94450411fc93022830c08c2922257df144b0c22f592915988f741f60ca",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_10_5.wav.meta": "aa6403aa649b2b7d43ccad35915411b5b659fb65ed1a95c6316801764399331e",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_2_1.wav": "305c4fe9bc09ac9785fad869420a37b0f15cdbe62e70d877b58774e15d16722b",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_2_1.wav.meta": "54515a0762846e96bb0edc360ecc5e3ddc570ef03742949242c9b0f7cc564691",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_3_1.wav": "ba409cd9f8d69111bb6a1a376cd56d98e1b88cd93f6e383662fcfb3430205ffc",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_3_1.wav.meta": "b7486818c8bbb7240f6847df04fa901d1411ba13277105be57c8f6cc752986c1",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_4_1.wav": "c6a33664da7a8d0ee722fae4c6e7ea3aabc002c6d914a1939f3273d7ce07be64",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_4_1.wav.meta": "e802449958d87aecfd9d1eb5cf4ee78f0fecb4d66584a0197160bf6fdb8cf149",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_4_2.wav": "520c0d11b828f1420c704e42e7b1790902b5fbec4726c369d6272827f36bfc03",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_4_2.wav.meta": "e709a090e093a3525c90bd5df3fca5c3bc0d2068ac3f75f1ee99f147959c459e",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_5_1.wav": "d5bcee96fc42217494e525fec51f20cd2f1188070e2e5aeb20b5fc50f58be80f",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_5_1.wav.meta": "cb15d69d846645dfb58c73f6a12201bef2bd0b0e9dbb92c0d933a9e07368f796",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_5_2.wav": "b946cda17c260eda8409d97ba13dee9f9b5fdf1f587ec623f047d0cf96a2e8b6",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_5_2.wav.meta": "23203e0d9fb94c44b39dd4c07ec378b03fc0ec62cb768a0200b1b396cc0da9d1",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_6_1.wav": "da9e39659a60eaf930f0ac0a7f41ebfc50508893d67ca0d1b7f6a0dfa0f773aa",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_6_1.wav.meta": "62157babc4c09a4f22df86c962ce4c71c0d62a485217467c7fc80ecd2c9218bf",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_6_2.wav": "6b634d5cf24b3d5eca2e9f6ab21519d31ef299b1e0204e9d767a106d9ec3ecf7",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_6_2.wav.meta": "e14203226cc3e4e4d641dd527a9f5719321bd1389c4b04375d55a216278c8911",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_6_3.wav": "9dfed6560f52343b57de209194a6342d41da444d1a3f2dcf5dabec7e93d23fa0",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_6_3.wav.meta": "5a551cfbbec6f74a44f78a1866db8d9f988eb414a649e681a43fede0b2d255ab",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_7_1.wav": "89f5600677849bb96c519bd2ef67776a0408df72c6d94d513263ac31193a69c1",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_7_1.wav.meta": "6b36d19507a1d9a2f6acd290e568109db7d25f3322a6a7375589bc794482262f",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_7_2.wav": "818a8fc291bce3fe7253fbd47b36607e097669ba2a03c6b0262a2647e25e4a16",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_7_2.wav.meta": "33c91f50bcef8b190464bc0bc08ba8b037cff46ebf81034cff6bf98f29c94ea0",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_7_3.wav": "07296ac45990a214efa6fe18c5924456304aa128c364abbe3e00d539d63f7363",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_7_3.wav.meta": "eb994c14db669fb0772a0b128b4e1db1f24802c2489b8d970dfb2b7c9857a9d4",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_8_1.wav": "eb1e903011bb3c82674c8b30f74683715dd988d22db2085db00fce4db697cfcc",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_8_1.wav.meta": "1d218fd1b9975e14c8c37682cb03d018c6f939f3805dd6b12c188f975b3e5313",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_8_2.wav": "c6e2211492e15196fb183e81845fab102f8c9cf1dcb82c29331d24403f9e68e3",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_8_2.wav.meta": "4b4072d9e54fa8aaf56fcfa975e57a7da0e8f4fc9eed88c99f572235152334b7",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_8_3.wav": "c7cf1eff30dde038774b40d895bb833d9dd9c640730ae679711aa48f453e8d90",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_8_3.wav.meta": "20661c608a6a6205176012ea499a7efc1429ede524a75e8c5fae96b23a94bac3",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_8_4.wav": "8c2254bf1d3d7fb773717f9cb006868f8978a91b2d926c49875c29d0af3a4db1",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_8_4.wav.meta": "555a78775fdeaec2aae7b722b823dd5d53c68cd282ee893503a35f6add5c22ca",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_9_1.wav": "c6a2a5a8b15795f40907629aafe639b18b28cdb4d1f574d32ad5324b8db35050",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_9_1.wav.meta": "4f456b9e08c0dded33ae30d9128617b02d9be592dc51110cde8561fc391d278d",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_9_2.wav": "80f290977d76cf097f1be04e3e8a33f5fc677a6213fa4865b876e25d5933e1e7",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_9_2.wav.meta": "bb37d2fd948344a96325c1925ad5e2c40b3785cb3c049d1899dd2238d432b349",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_9_3.wav": "c0a32bb1e7845d3b45c48b60ecab903670107ae0018cfd11e6a975f952feefd1",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_9_3.wav.meta": "fd11dd1afe2c673a8684b33df002aa647564f3b9bff5c711f87082a9fbcfe9bc",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_9_4.wav": "1d2bb033451a248b4a516da13a2e53dc89f4e5590a232ad978896845c8286168",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split_9_4.wav.meta": "e80d31a8edcbfb8b0d4eedd48cdad73af4cac8e868803bc4843a9e3a946b7545",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_uncounted.wav": "00eaa41c80725e45d62cc4f5a6b83ef2fce1a5810a8e74f8d60263e022d18575",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_uncounted.wav.meta": "8f41ae04e9b097b83fe7dc6033473640477585b002710d824df2501efc6f477e",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/BallView.cs": "4a577b14c39ef9fb47e8dbc64d22d13f6d3438522fe0b7eaf61b8676f5ec2837",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/BallView.cs.meta": "807faf0e796f33a9e8106ca18c33efa2f6a7841e65af55b1cc197ff3eede605a",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/BoardEnvironment.cs": "f2946d9b9ca66d9970c3f523d909170d96f06301cc1ab8b8ef2a060e6f299cc0",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/BoardEnvironment.cs.meta": "56746fd3f9461d3bb0377f3f3270a3d589469c1f78b1d7be088579a03e4c4480",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/BoardPresenter.cs": "6ebeefcd7c24bc4bd8147cddc430b4d86f8daf3296a8931ef1253880e25214a5",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/BoardPresenter.cs.meta": "f27b2aa532ccc921c68f336b700d51793d9b360a65cc887e506c03cf70f3ab1e",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/ContactPlacement.cs": "c891dfe3d14c3177e1916587b315b3d50636f7406086e35ccdbe66a761ff4b5a",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/ContactPlacement.cs.meta": "b00c57375fa2c06d781b808141942e97f61e321f7993ebe724a570e45eac1d99",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Fonts.meta": "7c2a6083ab8776b52f141988f3de71c075215c80181f133896da6fca99ba2308",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Fonts/NotoSansCJKkr-FirstSlice.asset": "bd66a789f4f0ede93cac08ebae2e620343239d69554df4126d554127538b8489",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Fonts/NotoSansCJKkr-FirstSlice.asset.meta": "2f79c0a2ff26ea8102b0bb62c9e135318c264c46067dcb9ee1913fa26fb5854e",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Fonts/NotoSansCJKkr-Regular.otf": "6bcb2a0703aa137e874fc2dffa85f6c21ba9a67fa329e81b8c801663af7e992a",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Fonts/NotoSansCJKkr-Regular.otf.meta": "b759c2f33ba8ca1b436ec49b17871ea3d5009a255ce8e68e3b9e7d302d321076",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/GameSound.cs": "bf868753d16839b282814d711ff1f38ad3a8e26985487bb5ca21fa3d54bd6f48",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/GameSound.cs.meta": "07a87b900105b362c304de5454bd609ce8dd1bacbacdf15ad041d287d21c73f3",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/GhostDemonstration.cs": "b79a1627f74aaf86d183a754c619b285104eba247ae810a588c052c94c4500eb",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/GhostDemonstration.cs.meta": "496e5ee6b79aebe8e3cfb4fb36ab7d3f191cd61748ca1db6ec5a6f5fb738b4d4",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/GroupBadge.cs": "892c06ec47633e442dc51f28e3f6882b6cbdebe55eac24a69ea8e1ae35c98f9d",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/GroupBadge.cs.meta": "a96898260fc7e70e6c406681f03a8766e7b222aa63f37eff8f691da1bf0f4d15",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials.meta": "1f41c8d430d0819835feaadcc7139d96cc9128492324c13b9768035151472e43",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/BlockArch.mat": "3a8da3d88895a82da83dc9a7b1f7cad650312de02591deb5571dd2b0f41200c8",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/BlockArch.mat.meta": "3c3a1e35d7e0eada07c99fa665fee5a18aa88257b0f92f9ab2ea3db5dd0fe4c1",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/CloudMoon.mat": "9b2e101df885e78b9450b72c4a3fe4cc3e3b0378baf86be76ba497e71eb6bc3e",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/CloudMoon.mat.meta": "c0963cc353a50ec1eb7ce1dd67f6b28ae3e596dc2d9bc45aa3181ea60ad589f8",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/CloudPuff.mat": "0a63dbe8eefaa8fcac38f300e0199b1747abdf11683d5c81106a3b3df6e8b4c5",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/CloudPuff.mat.meta": "6264e59a31c12fcf962075094a3c183189b64e69ac9df860d6839d7f6e7cb451",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/CloudRainbow.mat": "3e4ac5b656dfb84f11b4f22f9e6b3bc45763aebf767bc5c8dd2fbb6719d9f76f",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/CloudRainbow.mat.meta": "487f3abb4eb624ceab24eec7dcecdd46cdebfa973268eba9f0c4cf93587e68fa",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/CountGold.mat": "941df001007b3e747b3d7484e89e0d288aaf1fb41768430bb75e0dc188322798",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/CountGold.mat.meta": "bb03c4d8981b02696f6e83c90b9b778ee41035a4ae04838f8ded8440d7b44902",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/FaceInk.mat": "142a13b20f6746af2434864f308d0b41395535bdefc21288cfc14187084e8daa",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/FaceInk.mat.meta": "42b5473ae103f270fd0740b8c68c2907adb1643ebb9e5a08c1aa3aeae0bc57ce",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/GardenFlower.mat": "db4ad9607a853d755681b2f923ab58329bd1db21dbe0bbd3ad608f5faeefffd7",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/GardenFlower.mat.meta": "af14abbd00e1a68dc3d5b1c0830721d1dcf967e9ef534502d3d66a02f115a51c",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/GardenLeaf.mat": "514aa1ffc7a62f332c29fade163f77a47f3d63e5701cba5832c87e6895e77a3d",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/GardenLeaf.mat.meta": "6b4097e6c28136ab810cfd8541b63491426f83b270e5c7cbf919d46a7cce00d2",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/GardenMushroom.mat": "0d8099b05f77be9d926ad23b7050eca92bc30ae7d38d1dcc18e365ed7f4b480b",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/GardenMushroom.mat.meta": "8fda5b5e013751d3f566bcf66380c2a1b88ca2eaf7c9a142c0ec7718ffed1500",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/Island.mat": "20b0ef32184fbdd29ec920933d6e0c572ef9a3f54670b7df7c52a934f34de333",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/Island.mat.meta": "87bfec5e623fb07752bad467669559504b4099faca2a5456296d2ba2b5a96b15",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/LightRibbon.mat": "39dccc2ecda6829783e895770b989a51cc6864e2d98611d8f09beeede10ac1a5",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/LightRibbon.mat.meta": "380f05b6766fb3f421e293c0eb0f753c36a5b9c14429670329a8a48c2d073848",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/MagnetBlue.mat": "43649bf100681a19f4ba2e777ab3fbaf4159a35e229e9f40f2ee7451fb2321d2",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/MagnetBlue.mat.meta": "f07d502c79c004b9d04aa856048cbf03da6a0e7cfa4f53cc2fd04ce31bfead29",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/NumberOutline.mat": "f028a4c27574331f2d84a66bb8000bde992d357f93fba233f03f30d969aa2282",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/NumberOutline.mat.meta": "7e4e9454223f2c1f661056933d81922a5592df24da3675c37aefd834b222340c",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/Pebbles.mat": "92face663cc8caaba73010a59dcde838bfece1f45567633df7ffc7c29c415aa2",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/Pebbles.mat.meta": "d37151fdf4d2cc2dd2a507474980f6e97e1963875a7165b97ebee9bbd69b21ba",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/PlaygroundMint.mat": "900353f1c1756dec0238567ef22c6c067383df882fb5c4ca706556dcbe2da54d",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/PlaygroundMint.mat.meta": "a5b4cc539a11039bdfb22d0dccf076abfd1793581f308d6b093d556e84122105",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/ToyCoral.mat": "f9bcb12f8e69deff81050deee91ad413f10b86071b5b593ff0485c445d343749",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/ToyCoral.mat.meta": "e94d66896607f78b3585de171cab6321e3110ef0e7e192fa0dad3e071139e4b1",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/ToySteps.mat": "b648bddae1df2293f8bd90c23b6325c25f62dd51f23b5b239963cef33f290a73",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/ToySteps.mat.meta": "ab012a56bfb0024f6d8dbba0bb35bafb4b847f0219f2c90618a49b9c735930c8",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/WorkshopCrystals.mat": "42093594e5667838deed9c5317795137ea60aaf6055324f453a3c30db203ac9c",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/WorkshopCrystals.mat.meta": "589218049940045258786613242c836154dc9a560119b74c5ff5ee0a9d4a2fce",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/WorkshopGear.mat": "6b38dfddea22b4e48576891be9e362c9430097c2072cedca628b98d6ec30359c",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/WorkshopGear.mat.meta": "2cd5e2025f5806a3f0f5f57b35dcb36068bb1b087b0372e2e1cab37d8add47d5",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/WorkshopStar.mat": "c7d684bf7d3806a259ef144723ae4afed8af2c842f1d51fb2f77c73100a62e82",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/WorkshopStar.mat.meta": "c2d67cfd68f24ab9cbb2f459e5e1356211a5bb4992bb1d3657e81c2b39d97c49",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models.meta": "a059fd86f9d8ab894e2621ba1219855701cb81d1134f136539e73e095a9ece91",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/BlockArch.fbx": "b1ff050d9fbd670608aa10e1d08b73958ab2cea881e19f942a04ed8b7cbe861b",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/BlockArch.fbx.meta": "117bb55562d5594bd130d4aa86e9454f1ad279b37c29f8c9038c9eeadc02874a",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/CloudMoon.fbx": "4b459067f63c44f235209528af4a54d518fb4fbb853e717c9000dad48d7bc847",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/CloudMoon.fbx.meta": "5a0d323147d151b6fe0b5a750cb79533acf05e1cab91709f641e13b57f55b6d7",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/CloudPuff.fbx": "b30b350df8f659e22aac886fce6e93ec212fbab51bbadd6015d505524e344354",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/CloudPuff.fbx.meta": "3ddc039b14c9f67188963c5258639add67e8d55aa17dcf6eb543ab0a23be78f3",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/CloudRainbow.fbx": "0e83d9844040003a121b4a521386c4f7ff6ab78a18c29125d78ca374657e9884",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/CloudRainbow.fbx.meta": "df7d5057eaa0dcc03d880e12e7898dc80cd3f7d2e649f37f3772e7f34a98992e",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/GardenFlower.fbx": "34b20a629ae3c2b067b5c8cfdaa6d89b191ad9fe8f85d5eaea6cd28a240615d7",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/GardenFlower.fbx.meta": "152cfccee68c7f9ecd5eda550054dd82fa7ff97f6f908f496075bebd026a1292",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/GardenLeaf.fbx": "3de135bc7a5fef6866654061c38634d6bfcc4d7e5f5aaaa4019bbbefa400aea9",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/GardenLeaf.fbx.meta": "31236db8fde4f760247c36f6ce27e4c56b523781a9082ace5f9a70a2245d436e",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/GardenMushroom.fbx": "e6cd92801bc0c4995a0d6eead1efd30453fa69e6f6074398e6fe67e72058ce80",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/GardenMushroom.fbx.meta": "d7e699ad2357a726e1c3adb15a48b7e071642e2b6d2a0464e57b7a012956d30a",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Island.fbx": "425a81f9ae6ce69dbf73a38b4b929b19872f74777f7c6aa7d84ac7da5f2c465a",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Island.fbx.meta": "4c4c9204cfebe1f93ca84361ab9dbcdc491f0703c586032aeb1919210e7bda8e",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Pebbles.fbx": "14a28f6ac632c47c1a9483bdc77e8ef8184a7cac8265fff535e8671fe800f060",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Pebbles.fbx.meta": "a77b88ed65acb67d02760053642c4c0a973dd7578cfd89a3d0fbef2e25cca8b5",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/ToySteps.fbx": "98f0399b60cbbcde3c53ca38e5d97fb979416400c9882cb3c55113208459c292",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/ToySteps.fbx.meta": "76a12048830a4b38bba1a9286287a98abbfab2b3e33b32d9a2c0e0fc62caa79c",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/WorkshopCrystals.fbx": "f5fc07a401cc8e0d4ab5505697de5f6b8a97001cf757a513ecc50259627147b5",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/WorkshopCrystals.fbx.meta": "b786ed1d98e613fd7dedb3fefabb9f850e118ada35e27da82a0056b22dfb02be",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/WorkshopGear.fbx": "3310e609caf37c206f62f28c392ac2081a93356dd82857677bc3c512d96a7813",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/WorkshopGear.fbx.meta": "ba65bba9dcf915313bac98b68a4978aa735337f5d21cc5b5fb2c56d3c0a0cb37",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/WorkshopStar.fbx": "48c36cae1d4e4a35ce486757992c4f41f5cdb6a94e8a54d672f88c290692e618",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/WorkshopStar.fbx.meta": "428732c2cdfd985ced0ab3186ce8533468aacd7e38d93eb64b24f4eb3db8aeb6",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs.meta": "ca2788db895e69b7fabc76f2603950037ee0250e5db9e6d9279fca48a207846f",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GameApp.prefab": "d722503dcce59a467a1cd4db2286c1e95e84b5f6f614a03b586d0eec866d8394",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GameApp.prefab.meta": "248f9572123fca7ee45d9d56358e2e89a67c2a78b4c27cb74c4281170ace1db6",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GroupBadge.prefab": "ec66def535f920fab2ba641867756ee9519c016d9b7b16c136233d53062e192e",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GroupBadge.prefab.meta": "b76c78e7133454e8bf325948bcbc9f392c96c10fb4eb5a78b01eefac2109c8d7",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/MagnetFriend.prefab": "2de9d4f7cd65b4178cde35a32da6b6eeb3f81e3d53f8b520b09f4a6ebac9f098",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/MagnetFriend.prefab.meta": "5e31161dac1026b55ff8b8c40fec85f1acdf7f154efb573036cda67c7e99139b",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery.meta": "ed564f9e76e6d693e99b00d8b0f18e63864cb58e822a0480cb193039c594c9ee",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/BlockArch.prefab": "250f7470e2ba7e832e5fc3585345915a1ad1c9182ef8712230fbb7ac04a87a95",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/BlockArch.prefab.meta": "5572e39c13ba4cdf63760957a8240bca000b9e0ffee530abbc3c4851a7a32974",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/CloudMoon.prefab": "726139fa7dd50f3f99e5f9174583bc85cbacd5d5da654850870dad6f5ab5c832",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/CloudMoon.prefab.meta": "aec8ddd9916ca8d7c1bfaf7f6b5ca6a5372a34119770f7bf16835933ad8ec6df",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/CloudPuff.prefab": "62dea540dc831ede0224ed9d61dfb0a61966367d88423bef71882837ebc3582d",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/CloudPuff.prefab.meta": "dd91846026bf30763e8497ed0ec8c1b0caae1c49d666639e83420884278cb9da",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/CloudRainbow.prefab": "1f1b7ebc8b795380f98147247090926857c6bbb831778f9c0b969d8b01463b1c",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/CloudRainbow.prefab.meta": "34e9b08fa7d42e6c5c7a8150135000679b60a940b23f7756b8c937c7dfb5371f",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/GardenFlower.prefab": "e505ec53cc4056a112b22f16606cd025fd6aaa3b34f7d0f45f427214e9b2b938",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/GardenFlower.prefab.meta": "58ea83681053709ddbfdfab0bca2bb47f59fad4ff171b46d4b585f4eadd0835a",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/GardenLeaf.prefab": "827cde31118765c12f5f3941943d42a3a45cd06adbb931074b8cf5074b788f47",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/GardenLeaf.prefab.meta": "91ab44e0cd8cdd5114c98389412c05737016538ac1b7444b0ff1efe2e00b7e0d",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/GardenMushroom.prefab": "991ac63529f56cd751c39e1e2170452b43b157894cba79155f24f620f006acf5",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/GardenMushroom.prefab.meta": "5bfb10ca8abfd9799e579b608c343326482fd120a2a4553ae646f9f8dffe312b",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/Island.prefab": "20763c6440f318b731f2cf4c5a35e830ea547b2bb8346d670f86c4ee7e1d6e4c",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/Island.prefab.meta": "712a50247312e191576423d7d2ae808fbcd70b8d60ff1be330f4dee7e04bc2de",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/Pebbles.prefab": "3fe5b6fe8f2c233b3dbdbca4d9a9ea2d81e197ddda17cec7e4f5ca3dcccaf3da",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/Pebbles.prefab.meta": "08cd7af820b59000b3a85182e6c7dfc900098316a5109ca3f5b61a864f66ee73",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/ToySteps.prefab": "3bc8671ddf862163caee66cd766c3c4bd99b815ae93ca0feb479244b04223987",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/ToySteps.prefab.meta": "0d2b1099bb0f347d24958aec2fa8782b3fd51c48558246e1536db23c39557fdc",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/WorkshopCrystals.prefab": "60a4ad67b6f438a03615826d236145c92ce9e906b7eda7a301490d27bad5c845",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/WorkshopCrystals.prefab.meta": "eeac924def9abc8108451dc2953ea01733100041525981b5d77770d331c2e2d6",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/WorkshopGear.prefab": "fd681a1058097b933e84630685b52a0b9c2c3e49e3edc9d417167b02e5d2fe59",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/WorkshopGear.prefab.meta": "a169341b2bd3066f0e07e187c5f2a68758444a741421ecac7422a34cd897329f",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/WorkshopStar.prefab": "c82f9f5ac524e6ee7586a664cc929b0420ec55154f049bc5f9a03cbb162acc4c",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Scenery/WorkshopStar.prefab.meta": "d6469e653195714d64aab2e701e761710ae1a08c772ab17aa668b1846526542c",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/SafeAreaPanel.cs": "5726a0c8d7ebedc73106f84aa7f111bd42ec92d2132d7f82cbd9a67950b079d1",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/SafeAreaPanel.cs.meta": "1e5251eb0e7cc4ceee8bb9edf223a5ab6d368ad1a1e8d8f778223bc5aff26e12",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Sprites.meta": "98be287bf6ac2323f98b136c72effb20bf1ab79d00ef6f181cdc516aaa70c865",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Sprites/Face0.asset": "a351d09aef580f8bf3d73401008aa93c7fffb250ea010595824dff15766dfcf6",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Sprites/Face0.asset.meta": "f5613e301fff9c03223664e15a8db068a7f81f1b7642b992814265e8bef6ff36",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Sprites/Face1.asset": "a555b81eb573044b53edc7c57bbc2937ba1e7d789527bae02f97686cce6ff816",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Sprites/Face1.asset.meta": "6fadefb2d906d9f49a0fe756a94136787f8ffd033c006ad66c08e63c6288c568",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Sprites/Face2.asset": "6c8cfcc79bf7ab2781b39303ebd6e723fd0f96aec9f30378c7197bb73720d020",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Sprites/Face2.asset.meta": "ca2f2a3457ef05baeae23ca772e3304aa2c6a3e24eedb563c2bda398825560ec",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Sprites/Face3.asset": "9130d438849dc678504cc939f51c9ea91c5b1dff97b7f7f9248b2f617af4da26",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Sprites/Face3.asset.meta": "ca2b60c0c08de47ebf47018eca26be410fec9f91ecd973c6a80db58668039e9f",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Sprites/Face4.asset": "8325c280f8fd83b1f55102593dc772153a3953a70e818467894d7ac98c9f87c0",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Sprites/Face4.asset.meta": "7cf23edcc41e7415eba57c4aa9ead5990d7352f349377536c43b0172d4ecd693",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Sprites/Face5.asset": "a63967c62e7f1ddebe5cfa073b536992b1e02f2ff3aeacd515dc9cb230dca321",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Sprites/Face5.asset.meta": "fbe31b7a354bd949d46e160f600a021cbf13e79988e8aa55dfc2afc0a39f2046",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Textures.meta": "aa7b7000d0c56dc9f71e811146d1f1f8ccfc3f3f55d47a94eb7da9ec526686fb",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Textures/FaceExpressions.png": "0e3c115619c4ae68cf8859ccbc6d4bfb3a26a1a247b3ddb41327c46829540c98",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/Textures/FaceExpressions.png.meta": "1aadbba03b4a8d91e87c663a2e30ae981a57aa524d4ba653fbef34922f1b4f63",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/VoicePlayer.cs": "4920dd894f407dbe6d20c4eb6370d328ec7c30eece2fdd5e35d45a5e81fb74a3",
"Assets/MagnetismHeroes/UnityRuntime/Presentation/VoicePlayer.cs.meta": "03ae52a503737d680db99709bd712bd82a29f3d97c7cefddc8c1535e19cf64c3",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets.meta": "c148a837438ba800381dbbcc7916fd21c5e5ffef228a9588cd72013eed071ac6",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage01.asset": "9882a93e25a70b8eda53656a500c1200efc8ff1a51b5bc138f0406371545799d",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage01.asset.meta": "d191758be4296341ea14ef824080bffa7b6503c2707b83684c73573e57ff3576",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage02.asset": "737657dc2a8a73fb435c8e9e0f4ca0abe33c2bef939c5b57a7571d2df0f113ea",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage02.asset.meta": "0c4b446bddb0344d5f4100ce68123ee08d75f39f1e883535347b604a756ab3c6",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage03.asset": "cd32f32c67549cfde6428a3baefe3004e0729c2a7c4d93416f7ff1d60f58e622",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage03.asset.meta": "fbb526f0736e1e9a0863f50093a8b0323ae647bb6d10b81cfcfc2289eef1bfe7",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage04.asset": "e3e17be5881fdf25ea309ccc0ef122b0617c348b3c1a8f77f888c637da4b4cd0",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage04.asset.meta": "1bf89aa74b745936305791de5ffefdcfef3dee678e6a15506dd6a3284444db4e",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage05.asset": "7c0fb53d06c73a32efcac26063dfeccdb17eedf257e3eeebbaf93d802ccd5cc2",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage05.asset.meta": "dce6799d114e5f82ddeb662a632db10b145e177600a3475efd049d116d54e245",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage06.asset": "aac367d6c3f388f505aa3a25f2b295d30c7b67a0853875b77477c4f87fa9015d",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage06.asset.meta": "81b5cc34fa2e14288468f6d524ff5423f29cdf453af1a6860f8b92fafdfabeae",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage07.asset": "812f206b917e30b4e11950e959ebc1aee3e22f2e815b5126ef4629a2ff137aa4",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage07.asset.meta": "12ec2ae9d4b3cd4ebdc72f422da385208e994ab0542422c03f29b58eed6b8da4",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage08.asset": "8b64735052d78a785e8e76fb079ef2e68206643b86045dc7f986f0e246d94c12",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage08.asset.meta": "fa7787c13df551525c7a247b0913bd0edc3f9d302a8b1c61aeaa9b7ff4b215b4",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage09.asset": "400f3353afe5928772dd87a0d5361dd7cdd1074e64d5db2347a5c7622784cccd",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage09.asset.meta": "290ea5f8135079a754f9441b7db5a844f96a3880fa160dfdd029cabdfe6699d9",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage10.asset": "7746b8afc229340f13159f1b4a7c829d8f9eb2eccd74fcbde837841adbb95d2c",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage10.asset.meta": "667f87d6f986a344e3bdbb38806a9092b03313153d36512535451cc413183102",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage11.asset": "26f13a567794e20295e9318630b0489718ea054ded0316654791091df24e26f2",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage11.asset.meta": "d96ec6ce25ca5936703f3796d2d5fa946b0014a5b48a0b1d8f624cdc019ff486",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage12.asset": "eb65552aef9bfecef99be50768eddf2a527452f17466b118fc7192bd5f3518a5",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage12.asset.meta": "6b07067267abb134f1f2547ded3b5ecdad3a75a95e391aab4b08ef55983003ba",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage13.asset": "5a6d563e300476d591e0d872802ffec4f5a60bd1b8ab73ec63ae9370e8b463a9",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage13.asset.meta": "2b3abc72f2011fa59d9c4a0779ad6226dcc0120519b1e14ba3be4f8d7aa03093",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage14.asset": "ae9bf566f387858b7b2e557c61bd8f0ce8b61a5b36e8dd882bce7ca6469a0ba2",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage14.asset.meta": "18204258707d0bc2b083c4fa8e73fec77a603e7a50d804ec318f4c220eb11710",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage15.asset": "714cfe769f0d672db29d16586dd9a40643afd3d288c1e5917393cb07f0508135",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage15.asset.meta": "145360f4018881ca8724b752a8f2024005f64064bd4bb142dc57e07e038a2b1a",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage16.asset": "ca16d92aa28be84d4a08c40795cf1469407c51788a6ac4e195a810ea41f04571",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage16.asset.meta": "87a0aeb1f8f50b2dc2137e7ce8c45fab93e741cb66e97b10ad8833a40077d2d9",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage17.asset": "3fc87afbc987382799f0c62986edf42e6de55250e387557620d7b86db6d0c898",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage17.asset.meta": "f2cde45482d15f85beecde6e1ef37a17c4b32fd3d44d7fa09cdb48e298bc422d",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage18.asset": "4a779dbb012b8791202deb192ff21459395dc94a451edb226cc83ec05d52b3d8",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage18.asset.meta": "f8126d37d854fffda38b12d036218c78855ac2ea895460f879e9b0ddd7819da1",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage19.asset": "db69f150e92d3782dc4f34f998b07a5b4ed84257786469bd9b1f2900b8685335",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage19.asset.meta": "d2204076f08b08a6d8c173c7af67d2dfe142c2a048fd8fcb3cacbc17bea8ff8b",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage20.asset": "49c496c2a8c99b7670c12697d8c835f92c6dd46fc818c8c89902d668cc2dbc94",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage20.asset.meta": "eef5da97f2f010db711b0fbdd7e68864343d5cd41de8809ff4b0d61c3e2f45fe",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage21.asset": "8afd3c87e8080bd9baf3f2f85276b824f3a3d60c16ca22b7ace5575ee4ef5808",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage21.asset.meta": "397ce3a027feee2145e77951099bb9b920e440a619815036b693a43f80104bb4",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage22.asset": "d94bb8d2b1ab88a75223f71db4a62bc6bc8103c6d8079bfc8205181df16578a8",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage22.asset.meta": "5b7467670a320b3b916c034f167d5726f9ec1b2c6abe3f22250a7138907124ab",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage23.asset": "56bce6a82d4271b8919d733ee475760e43c3f8bf4035e2c1e79096c8dfb22823",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage23.asset.meta": "57761e15155453551024afb329aefd26f9743fbda9f8c4c0dfc30542aa4bbe58",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage24.asset": "98637095636b98dda4e53fe959b8787ab45e63ffa82a5379b0d52010edf6fc3d",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage24.asset.meta": "5d6a6e2aa5c779ec46656bcd4895ef4c6849b657e34ba666b1cfbcf260f44615",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage25.asset": "fbe23e52ef379f1d1009973d42da9c4616370b1a8fb3f2f5adcc5b2f66db6d33",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage25.asset.meta": "6614ddd6e162508445a38ae49e8b14ac6c9b686be3a44ea42f24cd2913554dc4",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage26.asset": "eb36ade8ef35fb89c490df4c75340763bc102e9cb15f98dda1a27e7e3234fc79",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage26.asset.meta": "29e80e3ae35acdd5ea865fa4f39fdcd2974c460d02084b327abe3332826a85ea",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage27.asset": "c14d49d11809e59c8e82d15d257efa9f79b951e2f231cca90147d58b5dcbbe9c",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage27.asset.meta": "025fc99567d27b618db5c7c292afac980db4ed14626a62cfa94f685e66e0839a",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage28.asset": "5c304cf2a5af710b072d9ad3a9ba83613e0a4149dc7d7e3245b2431419300700",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage28.asset.meta": "2bfca1e2842f0ecb24ee94966228058e23729b0709e7755e7c7752bb5eaaea89",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage29.asset": "0a2687bbd6ee2605dffadc50356fd56d9329f54d9713df6882f29d5999447f08",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage29.asset.meta": "3414f5ce22d691417e16217f7d8881f100b966489443a8ec2f48a08acb26f234",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage30.asset": "bd05de078e3625b955b147f12aea1b055d5a8513c98af06fc828e9e5a0bcb840",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage30.asset.meta": "7135b990186907960b62c20cb41c11a7ae881c12adace71995bf29d3b4d44529",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/StageAsset.cs": "83ee09cdf36e27fb293cd2a5ec4cf599096a67044f335028b5070d34763b2793",
"Assets/MagnetismHeroes/UnityRuntime/StageAssets/StageAsset.cs.meta": "38c224d68c81217a9a043ff683882e2593cc19fdc9dda6b67b37ff48a7e3cfa1",
"Assets/Settings.meta": "73c8e19843240273facb0af6bdbf430d719e397aded705629594060ce25ce7ad",
"Assets/Settings/DefaultVolumeProfile.asset": "bda0e0edfaac8afb004c90a2e0fe1b7797d1f2027f329cae5026f5b83727b14f",
"Assets/Settings/DefaultVolumeProfile.asset.meta": "efbcb026cee6b8e32fdf27018c06b0515296a6ccbb43969e792fea270310700a",
"Assets/Settings/Mobile_RPAsset.asset": "839fe528b015841bb60bba04d1c5830ef7b56eb2d78f61692db102de625460f3",
"Assets/Settings/Mobile_RPAsset.asset.meta": "53ea46647712c615fd1a3f2e0dddfa8559eeedc649e9a1dbf93ff913de28544c",
"Assets/Settings/Mobile_Renderer.asset": "aa93274c9521fa941170becec9e87c5fa36b8587eefd5eeb5c0eed0d1d4b1dbe",
"Assets/Settings/Mobile_Renderer.asset.meta": "a148ee2b4987b05b8ff7ae5f9bf84916184f77aefddc821dceb551f5a69f8179",
"Assets/Settings/PC_RPAsset.asset": "65b657b87062616fca959aecbb64b4b01dfd3b0051ac1119fb65be615539df96",
"Assets/Settings/PC_RPAsset.asset.meta": "bb469b9b6f318e9e9bed29c3b2e73e79371c04743c6f95dd6603941f1cd3e53e",
"Assets/Settings/PC_Renderer.asset": "3d0b01d8ded3327263b6690be128a3de58d17c514943ae42fe743a3e084a9c79",
"Assets/Settings/PC_Renderer.asset.meta": "b5df8ddb2708c6ed4c133a5ff9ee95a75cc20e4ee3ed6181366a61aedbaf31ec",
"Assets/Settings/SampleSceneProfile.asset": "a8677d4bac051d24caa11cd622e39f968155ee277bfce132274f1de225692fe9",
"Assets/Settings/SampleSceneProfile.asset.meta": "2e23abc214069f0adbf2b2207f1db54be7876629ec2a3681d0f4aa5b61622ef0",
"Assets/Settings/UniversalRenderPipelineGlobalSettings.asset": "831586bf4833c130db534749c7901260f2aa193ce1a2c3cccfe062a8cf6a38f8",
"Assets/Settings/UniversalRenderPipelineGlobalSettings.asset.meta": "f61661d8bc8dbe531daf09f6211ef978c7ba145c9207c8e76524396ae0722d7c",
"Assets/StreamingAssets.meta": "e7055ff841742399b7e3992ebdc41054bcab446ff7cc9c97d494b3d24e8eafc8",
"Assets/StreamingAssets/Notices.meta": "0ae917dd89ec866c890a0f49f7f587bc89c658a6362057736d317cbfebdbdc1f",
"Assets/StreamingAssets/Notices/NotoSansCJK-OFL.txt": "88f117575237307bdd86a17ef15e21790fc9a662fe4dfb103ca1ca077f0d9982",
"Assets/StreamingAssets/Notices/NotoSansCJK-OFL.txt.meta": "73549968dc8eaf85a156f3f82379ff0a210ed54e4ede31e56b930a1ed0fb628a",
"Assets/StreamingAssets/Notices/TTS-Apache-2.0.txt": "7137274bab133744763a3906ace2a13844c2b34635136fc6d4454c25bfd76eda",
"Assets/StreamingAssets/Notices/TTS-Apache-2.0.txt.meta": "c90234c623bb02c38471fc9f8088e9e9059d6fbf079f4a59d3424d6a0a0de448",
"Assets/StreamingAssets/Notices/TTS-model-card.txt": "2157466302fcaf8e4979d0505aa4b648ad32468aff98b3e964046db05e574f3c",
"Assets/StreamingAssets/Notices/TTS-model-card.txt.meta": "d6ed52dda74a6f3e2b1d0c343c7fc4997613401c6fe7d3aad6853aef0f62d65e",
"Assets/TextMesh Pro.meta": "01c98712be33191460d4eba7720f3b157d163deba149053aade1400d8197d8f9",
"Assets/TextMesh Pro/Fonts.meta": "8c596298d518c27d3f42a08e9849e679a66d62b8e79823a6d3b48525290ed647",
"Assets/TextMesh Pro/Fonts/LiberationSans - OFL.txt": "a130e89b7b2326b599c4b2bbd873eaae63feda407541a2899cb8acb306723de6",
"Assets/TextMesh Pro/Fonts/LiberationSans - OFL.txt.meta": "1b8062370a3bb2be74a46b0ae08688c87cd207cad602fcc77a750cafb106c9af",
"Assets/TextMesh Pro/Fonts/LiberationSans.ttf": "e5b0af421ea2bfbc1ac8d251d647268087ae82786234c57f757d1f0b90fa8b49",
"Assets/TextMesh Pro/Fonts/LiberationSans.ttf.meta": "58603424f5d1f468af3f0c031c288cc9eaa96cb3fb82a0429b16a204a18a5012",
"Assets/TextMesh Pro/Resources.meta": "4bc87da5a85b45fb3252482a603b56e3d34b2174973c527940b225e0b6d9f062",
"Assets/TextMesh Pro/Resources/Fonts & Materials.meta": "124f26d6401f350502d4ea38e1b27353dfbd9905ebf50a1b813cd695c2c4eefe",
"Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Drop Shadow.mat": "73f941d7702c761483bb184fad6e262648032969f0f310344155758b8fad764f",
"Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Drop Shadow.mat.meta": "fa0b8ba96378d1fed663d2206054a5e48139892e3d1c6a6ece97808a6869d70d",
"Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset": "6cd2faa131f346f601036104691f0223744c5806a51ceec454e6d4ff3bb6831d",
"Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset.meta": "0a7cce8a8d21c28bc719e5842c3fb8a3de8eac864a577160d7413d1df08d79d1",
"Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Outline.mat": "4a07f16fcfa697f984d78b7acc005985fe45a256b4bf8b7907faea08075b4961",
"Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Outline.mat.meta": "cb8f2abe533ef11a46ab112cf8dbefb44cb01fa4b7262060a3c480862824d1fb",
"Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset": "0c990817a90dbe8cc2c54e699e187fe4a27986811ba742050885c92c364626a1",
"Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset.meta": "eef709d8a9d8b4c781c532712a61aa28f3ef123b902f6b5a8903f0ea61845b50",
"Assets/TextMesh Pro/Resources/LineBreaking Following Characters.txt": "3a73e5ffe2510756dd3c2e982fdc1218b4be82c6e21abde88070d6860e1bc8e6",
"Assets/TextMesh Pro/Resources/LineBreaking Following Characters.txt.meta": "64ef1da1e73f25ba842200e26d43908685c4b9be66856d47103a26c1cae94fa9",
"Assets/TextMesh Pro/Resources/LineBreaking Leading Characters.txt": "62d3f4d5f64aaf885692d2d13a8313f6918e6e4c5aeb3990a43f614652bfd89c",
"Assets/TextMesh Pro/Resources/LineBreaking Leading Characters.txt.meta": "92a36fb59ae49c6974d4ef75b41ffe4c1612110c4b49ed7af5539dcb35e86e8f",
"Assets/TextMesh Pro/Resources/Style Sheets.meta": "9029980ffd3af688190a28cb7f0397a82efe9bd3a8861ed40dfc9018e76d5933",
"Assets/TextMesh Pro/Resources/Style Sheets/Default Style Sheet.asset": "49c813b42ff25580e3934554e3550e7bdb78bcfb2c3562bf1191f0d42f4da141",
"Assets/TextMesh Pro/Resources/Style Sheets/Default Style Sheet.asset.meta": "b652bfc488ebea2ac241cc440ad73c4a9af993c265c800fe0f7cc4917286d0b0",
"Assets/TextMesh Pro/Resources/TMP Settings.asset": "dafd957b416fdad34f611c30ee60d5f603497099f847b83fd688f883a56a3e72",
"Assets/TextMesh Pro/Resources/TMP Settings.asset.meta": "7e88f07fd05c1c4f829c55d8d7660d7ac572e906a5d7152857ef12a574d13375",
"Assets/TextMesh Pro/Shaders.meta": "6fc503950579b274e6745f9f18698f6b23d61ae9e3b8f208f023586c609d9d98",
"Assets/TextMesh Pro/Shaders/SDFFunctions.hlsl": "aa8b32660f8bfd9c1166d94539034437bb0332db6062122d074cd8645fbb98f4",
"Assets/TextMesh Pro/Shaders/SDFFunctions.hlsl.meta": "b35c24e7cc242687d24e1b99a0e921259defd305f2c73873173871b62f4c748f",
"Assets/TextMesh Pro/Shaders/TMP_Bitmap-Custom-Atlas.shader": "4fde41f9b01df2f31d54c5ddfa7703ca63ec89bba6615ea439c63e76f84f7b4b",
"Assets/TextMesh Pro/Shaders/TMP_Bitmap-Custom-Atlas.shader.meta": "41009354edccdb35431e595694e97cda8592094bebf607896161c769b66b3591",
"Assets/TextMesh Pro/Shaders/TMP_Bitmap-Mobile.shader": "d90e395a82303e7c048fe708f8cc76d2c4b304d723c942e8c1c85c30a28acdff",
"Assets/TextMesh Pro/Shaders/TMP_Bitmap-Mobile.shader.meta": "162e6113de4ff13e40bb6bc51f46542f5fc338c65facf395264db261e5d59847",
"Assets/TextMesh Pro/Shaders/TMP_Bitmap.shader": "1d88cbc0fd9f6d1189e6883b6a41433397a8d7fa2c6c267664af89f87687f050",
"Assets/TextMesh Pro/Shaders/TMP_Bitmap.shader.meta": "5278837021cc26fc254d06fdd87012bcc7d1e0521bfe068fbf24ef5481c9eff7",
"Assets/TextMesh Pro/Shaders/TMP_SDF Overlay.shader": "09602d3dd3834e7495fa233ee09a32c0af2e072db6f832b882c2b72014c56d6d",
"Assets/TextMesh Pro/Shaders/TMP_SDF Overlay.shader.meta": "1288285d5a6a95bb5405cc789d7f44139772e3ab9c8ec5fe9a919b3878055682",
"Assets/TextMesh Pro/Shaders/TMP_SDF SSD SpaceWarp.shader": "3819a806df61e6edd0835d26f4915c78726a7d563ff5b453e0af4c15302856ad",
"Assets/TextMesh Pro/Shaders/TMP_SDF SSD SpaceWarp.shader.meta": "7dbcf714e8b6525bc79d53b8393f58e6bd7919e4ecadf5ca70f484c85a9bc7a8",
"Assets/TextMesh Pro/Shaders/TMP_SDF SSD.shader": "02651d722125000a33ef3eedb32773038f878f6d8563b3e2b45af1e38859cdf4",
"Assets/TextMesh Pro/Shaders/TMP_SDF SSD.shader.meta": "12bfe855479eca270552d907ee15b8ce54f2276f15af3127bfc592706d4075a7",
"Assets/TextMesh Pro/Shaders/TMP_SDF SpaceWarp.shader": "f6156e7c6ed7146efdb49728a44474f3fad1ea1a848ada09858c7f761a182a86",
"Assets/TextMesh Pro/Shaders/TMP_SDF SpaceWarp.shader.meta": "ab64b44ebbacf053b1de937c83f78c593306988e3e9b77c84d54fff1ed86d4fb",
"Assets/TextMesh Pro/Shaders/TMP_SDF-HDRP LIT.shadergraph": "5d6bc7a7e8c31cf498e54c9828435815f18e044b898139eb9b491f08461f80a1",
"Assets/TextMesh Pro/Shaders/TMP_SDF-HDRP LIT.shadergraph.meta": "a8b46dd48416668b865839947e4f9079b3677e9da00befc5f7fc9fb7fd4b0306",
"Assets/TextMesh Pro/Shaders/TMP_SDF-HDRP UNLIT.shadergraph": "2506122540a83f3fbd7fecc073ca4eb6ed43a58a9d8b59caa135e66580b0c956",
"Assets/TextMesh Pro/Shaders/TMP_SDF-HDRP UNLIT.shadergraph.meta": "a1e19eef571b0f72337600d9758b51749b68a91ce3b25ede90a679c56e73dc7b",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile Masking.shader": "b3eae5be517f376299ed9152d78fbd6879e2c9a8f35a4b9a133b65bb9254ce18",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile Masking.shader.meta": "c4adca1c50f7380adf32fddcf1f37614d0c7829500e42c6247021a391f3d595c",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile Overlay.shader": "2ab40b8067e36d4b6f262bb3485203788b6b0e9c7521bae69cc47b2a05fb2b0a",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile Overlay.shader.meta": "2a3232392e8d038fd478b3f1704b66362b5e763afb50b3f16ac899c888de73ed",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile SSD SpaceWarp.shader": "79499fcf3da028943a71abd4bd4a2b71f9be5c29d88c9fceeab576373c45df5d",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile SSD SpaceWarp.shader.meta": "7392dad6b3a53b4847ee463883b44ed0f03865f582297f43c86a9b3006d299bd",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile SSD.shader": "c8de13a7886eddfc4f6994ea40b4042721f1238eee89a0ff4c29e32d33faefa2",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile SSD.shader.meta": "3e5f543ae95f82c5ed5738585d6dbf54008a98ae9a627d39b0dce9eb3ddb8374",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile SpaceWarp.shader": "17c4c1960194dae24402700c1a7d5f750843f320545d20a80829a0558648e6e4",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile SpaceWarp.shader.meta": "0cb8e3a86972bb1b099be459bd72c00631936ff733a27d7d98737339d6d3face",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile-2-Pass.shader": "e8cb1c858b51ef6e304e0d5b6b3197ea1ff8cfcc70c3ab89ee7d0d7bd499404f",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile-2-Pass.shader.meta": "80f392cea8fca4c1f5d50a334a40f87afc5cacbf1f2ff432a8992a601b39c007",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile.shader": "ea5bb3f5540de2fba59704d7f31382cf884595980e0b99c05a083873b9e26e3b",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile.shader.meta": "58eff2947dcabefbe4161ed7496b9be052221801758589fa4af452e09632b71c",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Surface-Mobile.shader": "0c2db2ea36e79e533e52ab847bc39bebab06285246955a4b2ff6ea546ee7a1b3",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Surface-Mobile.shader.meta": "507add334f0f385e78c2760b73b8cbb4d1bd60347d8bba1dcb38911302924b49",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Surface.shader": "f10ece0fce4433a45f601fd84b79b74ae82b275910be8d9aa7c09390aa58a668",
"Assets/TextMesh Pro/Shaders/TMP_SDF-Surface.shader.meta": "d849ca253d0f8e0124e5a192f921eef2ea411e78c1bf5bac71c28bb7fec59604",
"Assets/TextMesh Pro/Shaders/TMP_SDF-URP Lit.shadergraph": "d7816ac9c8e4c6bfb579b3101c1826c46f333279c96f69f5734160fc8f68e80a",
"Assets/TextMesh Pro/Shaders/TMP_SDF-URP Lit.shadergraph.meta": "3ae03e3081c0353a098954d842ddd5760dfffa17af6bc8963ce3b60801e6b249",
"Assets/TextMesh Pro/Shaders/TMP_SDF-URP Unlit.shadergraph": "73c1cc198de82cb5b7522618d29ffc47a307f0dfb5cb34ab9c86bf9a84d26e4d",
"Assets/TextMesh Pro/Shaders/TMP_SDF-URP Unlit.shadergraph.meta": "6bfd35c29cd78389fb93663f4bc1bb9b1402e0a0d35b67a86094671c72d796af",
"Assets/TextMesh Pro/Shaders/TMP_SDF.shader": "142c6e313f6731976cad18cfb5b283b3942cce97dd6345ac4fe5a7cf572e7fd5",
"Assets/TextMesh Pro/Shaders/TMP_SDF.shader.meta": "e77b8946de1f5eb76f2e1dd47ccaadc84ff67d30904b8f5bcb998203cdd61396",
"Assets/TextMesh Pro/Shaders/TMP_Sprite.shader": "b8606378647fc693029a0ad8ea7c3d162e1bfe1ff54f2f53f4ab158c9ddb6199",
"Assets/TextMesh Pro/Shaders/TMP_Sprite.shader.meta": "16f167e5dcf135fea9c31def18a5f639f9614bcf15d9f3a48986475858c9d7c0",
"Assets/TextMesh Pro/Shaders/TMPro.cginc": "7b5e42e957e80ddac2c42cd49d15dc48c50b97a0e263248be8a074d1b3c79543",
"Assets/TextMesh Pro/Shaders/TMPro.cginc.meta": "eb95df25958b25d82e30b5a6c543d368209b4a9af73105282537323f132c9cb8",
"Assets/TextMesh Pro/Shaders/TMPro_Mobile.cginc": "e3d42e5e7c32dad5b41ffb4f03d4e7d9382924aff35d470f46e31d291230385a",
"Assets/TextMesh Pro/Shaders/TMPro_Mobile.cginc.meta": "71adfbbc6aa5e2b27d3ab1317d6ef31b9cb4353dec581490d70de4eebe34db7c",
"Assets/TextMesh Pro/Shaders/TMPro_Properties.cginc": "18d4fa11c9b205459b6bc217b703c5fdea9875d7a022f6394a0dd18ac3cd0be4",
"Assets/TextMesh Pro/Shaders/TMPro_Properties.cginc.meta": "4a35e4e340b3fcf0d84abc3635db52dd4c34bead8e5cd547255ec39f09c9cbf5",
"Assets/TextMesh Pro/Shaders/TMPro_Surface.cginc": "3441f62bfb1f6f186118296f5cfd73e84f84776d0551b5195f18542e459b8f4b",
"Assets/TextMesh Pro/Shaders/TMPro_Surface.cginc.meta": "f155b9d0f2c80151af8c99b9c32835f178b71d150e208af7d1716648e4ca10d9",
"Packages/manifest.json": "2193aa859774807d3550546a26217199f1cae5d93b2754afb0d56b85e9d8b320",
"Packages/packages-lock.json": "f0fa7fd2aebf606379f17d8b5fb9f9580d413d3e561387b236c9c99068bed54b",
"ProjectSettings/AudioManager.asset": "aeedeb3243f07b61dbbe854aa08b5ef6f0b5274dd9852ad9f6129dde3e105a31",
"ProjectSettings/ClusterInputManager.asset": "40e65dd214bf254955d2c8829ca8ce2130fd3510485560ce012e9a1ddd93e623",
"ProjectSettings/DynamicsManager.asset": "bf017b547bb77ce3443dd5287ddf2516a74fd167f4f2ee5d798340694f51776d",
"ProjectSettings/EditorBuildSettings.asset": "5504d1c1ab64c6cd972eeba1c23d5310ebe50c01960c976b150ad2d9bb8c6cfc",
"ProjectSettings/EditorSettings.asset": "bbedcf93c32bca12eaba9ed61189ab13f064217e9f2984a66772719fa73c56b5",
"ProjectSettings/GraphicsSettings.asset": "2bbc2cce157a41d0ac0ad69d529c31f3d3abb0250ed4d6e7a84611c0b6a6a61f",
"ProjectSettings/InputManager.asset": "7a1bb836a1008d26d0860d143e9d9df7ca41731d30a72901b518063819c62df2",
"ProjectSettings/MemorySettings.asset": "1e8379a461b10ae09f3f696911088c16875d63e87ed5f2d95df9e1892703f9fd",
"ProjectSettings/MultiplayerManager.asset": "aba89fd1a1ddad182727ad82727e536d27372e90328a8196da2a4560398983dd",
"ProjectSettings/NavMeshAreas.asset": "80106cfa7e6181a1928b142c075f3e510d527441e3d79bc83590e4ac91e7cd66",
"ProjectSettings/PackageManagerSettings.asset": "f281d96e785df430d30ce0a515f4fe5c625ef44467125879f4b9f4cb087e1578",
"ProjectSettings/Physics2DSettings.asset": "d57a0fe55659c33879e8b70bc01c5ea9cdf082069dab649905c6c5138531e6eb",
"ProjectSettings/PhysicsCoreProjectSettings2D.asset": "bedc5339a832c60dc3f11b84abc4f4384dbfa20b0cd9ad6c444b907e751fc35e",
"ProjectSettings/PresetManager.asset": "29c125923fe6e29f90190d4fce1c82d660e7051546e2c9d02908d18c9cf2436c",
"ProjectSettings/ProjectAuditorSettings.asset": "9e68444b9bda981e2927a98df9edf69ea88362f0c6b7f3c81b39b9d515d2a416",
"ProjectSettings/ProjectSettings.asset": "17eb93d5d705ba05c86e4fe80e0d9e7678313266fdcaec03a41512efc255ea96",
"ProjectSettings/ProjectVersion.txt": "87289075c168cdd875e46a5fc308d15c686987848a70c7ce47faf40196ac0e20",
"ProjectSettings/QualitySettings.asset": "da30e325eae700ccd115f079d586886b460a35bc843c698110072e225720b738",
"ProjectSettings/SceneTemplateSettings.json": "bb9098b3bfcde78d93e264b96f7b77b5430e64e8c57a7aad7f5a5d9c3945e16a",
"ProjectSettings/ShaderGraphSettings.asset": "cdba1e5d65bcf6e3269d76ed3e60b6c21f98322002996642c9d74f1eb89f6b91",
"ProjectSettings/TagManager.asset": "23fc56b484aa3d7edf5914cd3591f7a823ccef5e3f3496f8a51f30388ead63dc",
"ProjectSettings/TimeManager.asset": "1a83e54adbbda7c9f4851103a0c6ab7f6448a3343d4ea5b7620452fa08416ecd",
"ProjectSettings/URPProjectSettings.asset": "596d5c8bf4f957b03684c949bebc252ae1918f25415a1147a00684c4b086efda",
"ProjectSettings/UnityConnectSettings.asset": "7f2112b8c6ea0885dcc72b312c9726ffcd909f2e89d1eead61889274a4e5d1c0",
"ProjectSettings/VFXManager.asset": "f066c6483919961332415ee64204f74fe52f38c02e82892eb02b7a5b2e381f0e",
"ProjectSettings/VersionControlSettings.asset": "e7bda45bf7a394745fee1316e6c71fa676004d591ad1f3aa247fd3e4ceb86071",
"ProjectSettings/XRSettings.asset": "edd2beb4a2e388312b4b7da241aa51070434005d82c59363f1237dd2e567585f"
},
"waves": [
[
"T1",
"T2"
],
[
"T3"
],
[
"T4"
],
[
"T5"
],
[
"T6"
]
],
"verification_plan": {
"modeling": "Actual Blender create/save/reopen/export/assert/render, Python syntax; capture commands and exit codes. Central Blender execution owner; independent script work in separate worktrees.",
"intermediate_integration": "Reopen and verify both merged asset packs plus confirm pre-existing product file hashes unchanged before human approval. Full game verification follows runtime wiring.",
"completion": [
"pwsh -NoProfile -File tools/verify.ps1 -Phase EditMode -Milestone Final",
"pwsh -NoProfile -File tools/verify.ps1 -Phase PlayMode -Milestone Final",
"pwsh -NoProfile -File tools/verify.ps1 -Phase AndroidBuild -Milestone Final",
"pwsh -NoProfile -File tools/verify.ps1 -Phase AndroidSmoke -Milestone Final"
],
"shared_resources": "Blender/Unity/ADB/recording commands serialized by orchestrator. No candidate adoption before T3 explicit user approval."
},
"tasks": {
"T1": "completed_model_candidate",
"T2": "completed_model_candidate",
"T3": "completed_explicit_user_approval",
"T4": "completed_merged_independently_reviewed",
"T5": "terrain_continuity_fixed_independently_reviewed",
"T6": "verified_delivered_independently_reviewed"
},
"model_integration": {
"status": "PASS",
"scope": "Model candidates only; user approval pending",
"checkedUtc": "2026-10-08T02:07:58.003206+09:00",
"featureCommit": "5185d9463163ee4c3ce456a6ec5414cc1448f10b",
"protectedBaseCommit": "58410281e015c6600dc543c4e093fd95c036288c",
"protectedProductFilesUnchanged": 533,
"candidateFilesAdded": 32,
"provenanceResourceHashesChecked": 26,
"commands": [
{
"command": "Blender --background --python-exit-code 1 --python tools/models/build_hero.py -- --verify-only",
"exitCode": 0,
"log": "Heroes/integrated-verify.log",
"assertion": "ASSERTIONS_OK and REOPEN_AND_FBX_REIMPORT_OK evaluated"
},
{
"command": "Blender --background --python-exit-code 1 --python tools/models/build_meadow.py -- --verify-only --review-dir artifacts/verification/VisualUpgrade/ModelReview/Meadow",
"exitCode": 0,
"log": "Meadow/integrated-verify.log",
"assertion": "MEADOW_VERIFY_PASS evaluated; 21 assets and 23 resources"
},
{
"command": "python .dryforge/verify-model-integration.py",
"exitCode": 0,
"assertion": "533 protected product hashes, 32 additions, candidate provenance hashes, clean tree and protected dev checked"
}
],
"userApproval": "PENDING",
"unityGameplayAndroid": "NOT_RUN; awaits explicit model approval"
},
"model_approval": {
"status": "APPROVED",
"userResponse": "승인합니다",
"confirmedUtc": "2026-10-08T23:05:56.585939+09:00",
"scope": "All five model groups presented in the immediately preceding review",
"groups": [
"body",
"knightEquipment",
"clearMeadow",
"flowerHills",
"stoneHills"
],
"reviewPacketSha256": "4787e5679d1fe940060a889ffee42020258587ffb19eff32696ea6fc2f48cc99",
"reviewPacket": "review-packet.json",
"approvedFiles": [
{
"kind": "BLENDER_SOURCE",
"relativePath": "../../../../SourceAssets/Models/Heroes/MagneticKnight.blend",
"repositoryRelativePath": "SourceAssets/Models/Heroes/MagneticKnight.blend",
"bytes": 1833499,
"sha256": "7ce1272dfe861aee986609cdb899f0edf8b8f2f0f17269c0eda472c8aa90ee3a"
},
{
"kind": "BLENDER_SOURCE",
"relativePath": "../../../../SourceAssets/Models/Meadow/MeadowCandidates.blend",
"repositoryRelativePath": "SourceAssets/Models/Meadow/MeadowCandidates.blend",
"bytes": 1350505,
"sha256": "2d95f1797c1a1042e32bb2588d0bde735b6e109a5f947b1f0720c7537bda6ea9"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Heroes/HeroBody.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Heroes/HeroBody.fbx",
"bytes": 60076,
"sha256": "56f3301176c612ebeab3fdaaba5c019674beeb0f727f97ed27dd67e32ff50a40"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Heroes/KnightEquipment.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Heroes/KnightEquipment.fbx",
"bytes": 222764,
"sha256": "a8059a020e79e080635e7326bb91576010cad10c60ee931d95536d47fe76e61c"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/DaisyColony.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/DaisyColony.fbx",
"bytes": 179196,
"sha256": "a646a875d99b7cbcef727723b45c6a1e5d6231255ef100689fe69db2e55af63c"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/DistantRidge.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/DistantRidge.fbx",
"bytes": 67788,
"sha256": "af68a4b08e0696cb0b56acf92bf0753d288ad6ba89fd4f19b183995f4ed6fd0c"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/FarHorizon.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/FarHorizon.fbx",
"bytes": 66924,
"sha256": "fcfb4a768ca33ab9ccd166f7e4b3a96cbc918d1a564f59144b0fab399cb7c179"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/FernCluster.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/FernCluster.fbx",
"bytes": 202508,
"sha256": "9153c91cb71a762c54bc0bc3c5e6fcfe2650185defaf60b5da75e25d78aa4911"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/GentleHill.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/GentleHill.fbx",
"bytes": 67420,
"sha256": "356d3baa0927264f7807e13f3acb717b0c040ae09b23b8feac67e717142d3a19"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/GrassTuftGold.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/GrassTuftGold.fbx",
"bytes": 72908,
"sha256": "0b2a3d68909b56ecdd3324503d3a6bc2b2f9dc8e1d173536dfe52ca96e49521f"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/GrassTuftMint.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/GrassTuftMint.fbx",
"bytes": 72700,
"sha256": "650e02dab0ae3a0f05d036e2d58846cb97a93b77864cbc2c704b4364c26d9962"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/LavenderLupineColony.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/LavenderLupineColony.fbx",
"bytes": 250284,
"sha256": "596266e001d7765634ff749deee33cbece1e5bb69ba693bf8c34da795f4f7bdb"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/LowLimestone.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/LowLimestone.fbx",
"bytes": 47228,
"sha256": "026abf6db9395751e7ad933455bacc96a06fe331a4bb777001ee29bf33f8417d"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/MeadowClearing.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/MeadowClearing.fbx",
"bytes": 68284,
"sha256": "78c307844de3ef5584aaa0bec3e57500f9115065dd643686473e52632d551a29"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/MintShrub.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/MintShrub.fbx",
"bytes": 96012,
"sha256": "f501d302845a6df4814076abf76bdd97f5ffd2f0527f2aff236e0b97e78e62da"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/MossStoneSteps.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/MossStoneSteps.fbx",
"bytes": 155068,
"sha256": "5a1bf51f80abde9a9a9e7a419ad30509ad62beac372244e0aba5681896a01739"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/OpenStoneArch.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/OpenStoneArch.fbx",
"bytes": 34636,
"sha256": "8835e7c96ca842ddece1c1f7c9d168a1eabf4f0555e5a025bf46637969dff243"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/PebbleScatter.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/PebbleScatter.fbx",
"bytes": 32060,
"sha256": "34748a549b40ebe8a53cb57d9638ccbf3e480e8b56453b686c378db8a069da8f"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/PinkCosmosColony.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/PinkCosmosColony.fbx",
"bytes": 153516,
"sha256": "6bfc69131ff2cde84f858aa50e3e11697679073d3cd1d0a39a15f4b02cc3afb6"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/RoundCanopyTree.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/RoundCanopyTree.fbx",
"bytes": 147772,
"sha256": "9a41dcb9cb40e080bf4be6418f68eb5893561d9454e54a79a402f936ceb59757"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/RoundLimestone.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/RoundLimestone.fbx",
"bytes": 48700,
"sha256": "eaff76573039ab66927749569e511d5031e90c9ceb7dd8d7cccd84ab7032ca36"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/SageCanopyTree.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/SageCanopyTree.fbx",
"bytes": 147772,
"sha256": "26665d557ef176a9fcce24107d19619b9848552538482eae4b35ebd9fbcd0aec"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/SageShrub.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/SageShrub.fbx",
"bytes": 96108,
"sha256": "fbfed3947cc3e05c82f5660bae9c864576839174c7f72c0cbf0a278d6847eefc"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/SlateBoulder.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/SlateBoulder.fbx",
"bytes": 49852,
"sha256": "48da619e712ba4ee21c55baea8d93ce656db8a1c30475f0d2cb6bfecfc3b0a84"
},
{
"kind": "FBX",
"relativePath": "../../../../Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/TallWindTree.fbx",
"repositoryRelativePath": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/TallWindTree.fbx",
"bytes": 116140,
"sha256": "378cdb0fc1d67a980d67d51a469148a6d720a03c9e650b35979bcf98ddf6bcad"
}
]
},
"T4_commit": "43b22d67886bc5715d3543fefda2dc24dead299f",
"T4_merge_commit": "2d4b4a88e3c72a501fc84f1c9f7933c73452160d",
"T5_commit": "b4d88546a2e9c44e9b6babcc54acaf69a01c75b4",
"T5_merge_commit": "b880cc196c02cac959bc5b195a6856048113903f",
"T5_concerns": [
"Shadow fixed and pixel-verified",
"Belowground broader underside explicitly accepted as unchanged physical-floor/camera limitation",
"Editor resource limitations retained",
"Capture method advisory corrected before final checks"
],
"completion": {
"productSourceCommit": "3346eb559bdfba24332feb4d9be4b7104520233e",
"record": "artifacts/verification/VisualUpgrade/FinalProduct/completion.json",
"recordSha256": "dd898f331a31fb49545e9c3ee2d1c5b52adca97d907f5cff1b0c21b1e6d429fd",
"fourPhasesExitZero": true,
"productFilesUnchanged": 732,
"editMode": 103,
"playMode": 53,
"androidGates": 226,
"apkSha256": "DA533F235A9869E5A68F4A244AF9A89E34A8E6FB9E7C7F2F57D830939507B829",
"failedAttemptsPreserved": 1
},
"T6_commit": "a906be69a0c1211f01d5a3d1bc7fdcf5554efafb",
"documentation_merge_commit": "da169df7c7c7215ce562f8f512d93fb4df2e39b7",
"post_documentation_product_hashes_unchanged": 732,
"final_followup": {
"reason": "Thin sky/dark gaps beneath far hills in actual orbit captures",
"branch": "dryforge/horizon-seam-fix",
"approvedScope": "Far terrain placement only; model binaries/materials/colliders/camera/rules preserved",
"previousProductEvidence": "artifacts/verification/VisualUpgrade/BeforeHorizonFix/manifest.json",
"fixCommit": "ef03611628cb395d377cf56abd951bbf62cfa29c",
"integratedProductCommit": "3346eb559bdfba24332feb4d9be4b7104520233e",
"targetedActualExitCodes": {
"EditMode": 0,
"PlayMode": 0
},
"targetedEvaluatedCases": {
"EditMode": 7,
"PlayMode": 10
},
"scope": "Only FarHorizon visual x/z radius35 to27; unchanged height, scale, geometry, materials and gameplay",
"geometryProof": "0 missing samples across central24m corridors in each of three worlds; actual same-camera images confirm reproduced gap removed",
"premergeReview": ".dryforge/reviews/horizon-fix-conformance.json",
"reviewSha256": "5eba704c16ab4e7319c10aad8e337b8737e974df99966ff4b9d05040f3ac3e22"
},
"previous_completion": {
"productSourceCommit": "83182e7f3e86926bf130817d401a331829401fac",
"record": "artifacts/verification/VisualUpgrade/FinalProduct/completion.json",
"recordSha256": "587b92a7f4f63986bfa32af647bc184745b5b100deeed2b0ca4b7818ac309414",
"fourPhasesExitZero": true,
"productFilesUnchanged": 732,
"editMode": 103,
"playMode": 52,
"androidGates": 226,
"apkSha256": "BEE8E80E03C4205EDEBF616A18940D58654E9115FDC91EE0A80F47DEA91559EA"
},
"horizon_documentation_commit": "620c73cbd24d936231bee5cb3469e5cff3424cd9",
"final_integrated_head": "0d992d9dcc94843a72489b454ca4e8137178bc56",
"final_review": {
"status": "approved",
"blockingFindings": 0,
"path": "artifacts/verification/VisualUpgrade/FinalProduct/final-review.json",
"sha256": "f11a355ff4316fadba7d5532c481a5215f7cbf722f6c207d86d0781906a846ea"
},
"final_user_gate": {
"status": "approved",
"recordedUtc": "2026-10-09T14:14:19.8102344Z",
"userStatement": "완성본을 최종 승인합니다",
"scope": "완성 제품과 변경된 제작·채택·운영·검증 문서",
"branch": "dryforge/hero-meadow",
"approvedHead": "0d992d9dcc94843a72489b454ca4e8137178bc56",
"testedProductSource": "3346eb559bdfba24332feb4d9be4b7104520233e",
"productFilesUnchanged": 732,
"apk": "artifacts/android/MagnetismHeroes.apk",
"apkSha256": "DA533F235A9869E5A68F4A244AF9A89E34A8E6FB9E7C7F2F57D830939507B829",
"gallery": "artifacts/verification/VisualUpgrade/FinalProduct/index.html",
"finalReview": "artifacts/verification/VisualUpgrade/FinalProduct/final-review.json",
"finalReviewSha256": "F11A355FF4316FADBA7D5532C481A5215F7CBF722F6C207D86D0781906A846EA",
"archive": ".dryforge/002",
"integration": "merged-local-dev",
"unityEditor": "정리 중 에디터 종료나 설정 변경 명령을 실행하지 않음",
"editorObservation": {
"observedUtc": "2026-10-09T14:15:23.2646118Z",
"pipelineInstances": 0,
"runningEditorDetected": false
},
"integrationEvidence": "artifacts/verification/VisualUpgrade/FinalProduct/dev-integration.json"
},
"status": "complete",
"integration": {
"status": "merged-local-dev",
"userStatement": "-dev에 병합",
"startedUtc": "2026-10-09T14:27:19.2336807Z",
"finishedUtc": "2026-10-09T14:27:20.7186100Z",
"targetBranch": "dev",
"sourceBranch": "dryforge/hero-meadow",
"approvedFeatureCommit": "0d992d9dcc94843a72489b454ca4e8137178bc56",
"previousDevCommit": "58410281e015c6600dc543c4e093fd95c036288c",
"fetchedOriginDevCommit": "58410281e015c6600dc543c4e093fd95c036288c",
"mergeCommit": "a54ae6fb230b0c87e1a2f08a1ad9e2cc2ceb0d73",
"mergeParents": [
"58410281e015c6600dc543c4e093fd95c036288c",
"0d992d9dcc94843a72489b454ca4e8137178bc56"
],
"mergeCommand": "git -c core.autocrlf=false merge --no-ff dryforge/hero-meadow -F artifacts/verification/VisualUpgrade/FinalProduct/dev-merge-message.txt",
"mergeExitCode": 0,
"approvedTreeEqualsMergedTree": true,
"testedProductFilesRawSha256Unchanged": 732,
"verification": "제품 파일과 전체 Git tree가 승인본과 동일하므로 기존 실제 검증 증거 유지; 새 검사를 실행한 것으로 기록하지 않음",
"testedProductSource": "3346eb559bdfba24332feb4d9be4b7104520233e",
"completionEvidence": "artifacts/verification/VisualUpgrade/FinalProduct/completion.json",
"currentBranch": "dev",
"workingTreeClean": true,
"mergedFeatureBranchRemoved": true,
"remotePushExecuted": false
}
}
@@ -1,81 +0,0 @@
param([switch]$ResumeAndroidSmoke)
$ErrorActionPreference = 'Stop'
$projectRoot = 'C:/git/MagnetismHeroes'
Set-Location -LiteralPath $projectRoot
$env:PATH = 'C:/Users/baram/AppData/Local/Unity/bin;' + $env:PATH
$evidence = Join-Path $projectRoot 'artifacts/verification/VisualUpgrade/FinalProduct'
New-Item -ItemType Directory -Path $evidence -Force | Out-Null
$sourceCommit = (& git rev-parse HEAD).Trim()
if ([bool](& git status --porcelain)) { throw 'Final source must be clean.' }
$productPaths = @(& git ls-files Assets Packages ProjectSettings tools)
$before = [ordered]@{}
foreach ($path in $productPaths) { $before[$path] = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash }
$beforePath = Join-Path $evidence 'product-before.json'
if ($ResumeAndroidSmoke) {
$originalBefore = Get-Content -LiteralPath $beforePath -Raw | ConvertFrom-Json -AsHashtable
if (@($productPaths | Where-Object { $originalBefore[$_] -ne $before[$_] }).Count -ne 0) { throw 'Product source differs from the original final-check source.' }
} else { $before | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $beforePath -Encoding utf8 }
$env:MAGNETISM_VISUAL_EVIDENCE = Join-Path $evidence 'EditorCapture'
$env:MAGNETISM_PROOF_COMMIT = $sourceCommit
$env:MAGNETISM_PROOF_DIRTY = 'false'
$record = [ordered]@{sourceCommit=$sourceCommit;startedUtc=[DateTime]::UtcNow.ToString('o');status='running';phases=@();productFiles=$productPaths.Count}
$recordPath = Join-Path $evidence 'completion.json'
$phases = @('EditMode','PlayMode','AndroidBuild','AndroidSmoke')
function Confirm-AndroidReadiness {
$sdkAdb = Join-Path $env:LOCALAPPDATA 'Android/Sdk/platform-tools/adb.exe'
$readiness = [ordered]@{startedUtc=[DateTime]::UtcNow.ToString('o');serial='emulator-5554';adb=$sdkAdb;commands=@();status='running'}
foreach ($arguments in @(@('kill-server'), @('start-server'), @('-s','emulator-5554','get-state'), @('-s','emulator-5554','shell','getprop','sys.boot_completed'))) {
$step = $readiness.commands.Count
$stdout = Join-Path $evidence "adb-readiness-$step.stdout.log"
$stderr = Join-Path $evidence "adb-readiness-$step.stderr.log"
$client = Start-Process -FilePath $sdkAdb -ArgumentList $arguments -WindowStyle Hidden -PassThru -RedirectStandardOutput $stdout -RedirectStandardError $stderr
if (-not $client.WaitForExit(30000)) { Stop-Process -Id $client.Id; throw "ADB readiness command timed out: $arguments" }
$client.Refresh()
$output = (Get-Content -LiteralPath $stdout -Raw -ErrorAction SilentlyContinue)
$readiness.commands += [ordered]@{arguments=$arguments;exitCode=$client.ExitCode;stdout=$stdout;stderr=$stderr;output=$output}
$readiness | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $evidence 'android-readiness.json') -Encoding utf8
if ($client.ExitCode -ne 0) { throw "ADB readiness command failed: $arguments" }
if ($step -eq 2 -and $output.Trim() -ne 'device') { throw 'Dedicated AVD is not online.' }
if ($step -eq 3 -and $output.Trim() -ne '1') { throw 'Dedicated AVD has not completed boot.' }
}
$readiness.status = 'passed'
$readiness.finishedUtc = [DateTime]::UtcNow.ToString('o')
$readiness | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $evidence 'android-readiness.json') -Encoding utf8
}
if ($ResumeAndroidSmoke) {
$previous = Get-Content -LiteralPath $recordPath -Raw | ConvertFrom-Json -AsHashtable
$passed = @($previous.phases | Where-Object { $_.phase -ne 'AndroidSmoke' })
if ($previous.sourceCommit -ne $sourceCommit -or $passed.Count -ne 3 -or @($passed | Where-Object { $_.exitCode -ne 0 -or $_.status -ne 'passed' }).Count -ne 0) { throw 'Cannot resume without three successful phases on this source.' }
$record.startedUtc = $previous.startedUtc
$record.resumedUtc = [DateTime]::UtcNow.ToString('o')
$record.phases = $passed
$record.failedAttempts = @($previous.phases | Where-Object { $_.phase -eq 'AndroidSmoke' })
$phases = @('AndroidSmoke')
}
try {
foreach ($phase in $phases) {
if ($phase -eq 'AndroidSmoke') { Confirm-AndroidReadiness }
$outerLog = Join-Path $evidence ($phase.ToLowerInvariant() + '-outer.log')
$started = [DateTime]::UtcNow.ToString('o')
Write-Output "Starting $phase at $started"
& pwsh -NoProfile -File tools/verify.ps1 -Phase $phase -Milestone Final *> $outerLog
$actualExit = $LASTEXITCODE
$phaseRecordPath = Join-Path $projectRoot ('artifacts/verification/Final/' + $phase.ToLowerInvariant() + '-evidence.json')
$phaseRecord = Get-Content -LiteralPath $phaseRecordPath -Raw | ConvertFrom-Json
$record.phases += [ordered]@{phase=$phase;command="pwsh -NoProfile -File tools/verify.ps1 -Phase $phase -Milestone Final";startedUtc=$started;finishedUtc=[DateTime]::UtcNow.ToString('o');exitCode=$actualExit;status=$phaseRecord.status;sourceCommit=$phaseRecord.sourceCommit;sourceDirty=$phaseRecord.sourceDirty;sourceDirtyEnd=$phaseRecord.sourceDirtyEnd;outerLog=$outerLog;evidence=$phaseRecordPath}
$record | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $recordPath -Encoding utf8
if ($actualExit -ne 0 -or $phaseRecord.status -ne 'passed' -or $phaseRecord.sourceCommit -ne $sourceCommit -or $phaseRecord.sourceDirty -or $phaseRecord.sourceDirtyEnd) { throw "$phase failed. See $outerLog" }
Write-Output "$phase exit $actualExit, status $($phaseRecord.status)"
}
$after = [ordered]@{}
foreach ($path in $productPaths) { $after[$path] = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash }
$after | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $evidence 'product-after.json') -Encoding utf8
$changed = @($productPaths | Where-Object { $before[$_] -ne $after[$_] })
$record.productHashesUnchanged = $changed.Count -eq 0
$record.changedProducts = $changed
if ($changed.Count -ne 0 -or [bool](& git status --porcelain) -or (& git rev-parse HEAD).Trim() -ne $sourceCommit) { throw 'Product source changed during final checks.' }
$record.status = 'passed'
} catch { $record.status='failed'; $record.error=$_.Exception.Message; Write-Output $record.error }
finally { $record.finishedUtc=[DateTime]::UtcNow.ToString('o'); $record | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $recordPath -Encoding utf8 }
if ($record.status -ne 'passed') { exit 1 }
Write-Output 'All four final phases passed on the same clean integrated product source.'
@@ -1,28 +0,0 @@
import datetime
import hashlib
import json
import pathlib
import shutil
root = pathlib.Path('C:/git/MagnetismHeroes').resolve()
backup = root / 'artifacts/verification/VisualUpgrade/BeforeHorizonFix'
assert backup.is_relative_to(root / 'artifacts/verification/VisualUpgrade')
backup.mkdir(exist_ok=True)
sources = [root / 'artifacts/verification/Final', root / 'artifacts/verification/VisualUpgrade/FinalProduct']
paths = [path for source in sources for path in source.rglob('*') if path.is_file()]
paths += [root / 'artifacts/android/Final/MagnetismHeroes.apk', root / 'artifacts/android/MagnetismHeroes.apk']
sha = lambda path: hashlib.sha256(path.read_bytes()).hexdigest()
records = []
for source in paths:
target = backup / source.relative_to(root)
target.parent.mkdir(parents=True, exist_ok=True)
expected = sha(source)
if target.exists():
assert sha(target) == expected, target
else:
shutil.copy2(source, target)
assert sha(target) == expected, target
records.append({'source': source.relative_to(root).as_posix(), 'copy': target.relative_to(backup).as_posix(), 'bytes': source.stat().st_size, 'sha256': expected})
result = {'status': 'PASS', 'preservedUtc': datetime.datetime.now(datetime.timezone.utc).isoformat(), 'productSourceCommit': '83182e7f3e86926bf130817d401a331829401fac', 'files': records}
(backup / 'manifest.json').write_text(json.dumps(result, indent=2) + '\n', encoding='utf-8')
print(json.dumps({'status': 'PASS', 'preservedFiles': len(records), 'preservedBytes': sum(item['bytes'] for item in records)}))
@@ -1,39 +0,0 @@
import datetime
import hashlib
import json
import pathlib
import shutil
root = pathlib.Path('C:/git/MagnetismHeroes').resolve()
final = root / 'artifacts/verification/Final'
visual = root / 'artifacts/verification/VisualUpgrade'
previous = visual / 'PreviousFinal'
failed = visual / 'FinalProduct/FailedAndroidAttempt1'
failed.mkdir(parents=True, exist_ok=True)
start = datetime.datetime.fromisoformat('2026-10-08T15:24:08+00:00').timestamp()
sha = lambda path: hashlib.sha256(path.read_bytes()).hexdigest()
copied = []
for path in final.rglob('*'):
if path.is_file() and path.stat().st_mtime >= start:
target = failed / 'Final' / path.relative_to(final)
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, target)
assert sha(path) == sha(target)
copied.append({'source': str(path.relative_to(root)), 'copy': str(target.relative_to(failed)), 'sha256': sha(target)})
for name in ['completion.json', 'androidsmoke-outer.log']:
path = visual / 'FinalProduct' / name
target = failed / name
shutil.copy2(path, target)
assert sha(path) == sha(target)
removed = []
old = json.loads((previous / 'manifest.json').read_text(encoding='utf-8'))
for entry in old['files']:
path = (root / entry['source']).resolve()
if path.is_relative_to(final.resolve()) and path.is_file() and path.stat().st_mtime < start:
backup = previous / entry['copy']
assert sha(path) == entry['sha256'] == sha(backup), path
removed.append({'source': entry['source'], 'sha256': entry['sha256'], 'preservedCopy': entry['copy']})
path.unlink()
record = {'status': 'PASS', 'failedAttemptPreserved': copied, 'stalePreviousFinalFilesRemovedAfterExactBackupProof': removed}
(failed / 'preservation-and-pruning.json').write_text(json.dumps(record, indent=2) + '\n', encoding='utf-8')
print(json.dumps({'status': 'PASS', 'failedAttemptFilesPreserved': len(copied), 'backedUpStaleFilesPruned': len(removed)}))
@@ -1,40 +0,0 @@
import hashlib
import json
import shutil
from pathlib import Path
root = Path('C:/git/MagnetismHeroes')
record_path = root / 'artifacts/verification/VisualUpgrade/FinalProduct/completion.json'
record = json.loads(record_path.read_text(encoding='utf-8-sig'))
assert record['sourceCommit'] == '3346eb559bdfba24332feb4d9be4b7104520233e'
failed = [phase for phase in record['phases'] if phase['phase'] == 'AndroidSmoke']
assert len(failed) == 1 and failed[0]['exitCode'] == 1 and failed[0]['status'] == 'failed'
destination = root / 'artifacts/verification/VisualUpgrade/HorizonFailedSmokeAttempt1'
files = list((root / 'artifacts/verification/Final/Fresh30').rglob('*'))
files += [root / relative for relative in [
'artifacts/verification/Final/androidsmoke-evidence.json',
'artifacts/verification/Final/androidsmoke.log',
'artifacts/verification/Final/android-final-evidence.json',
'artifacts/verification/VisualUpgrade/FinalProduct/completion.json',
'artifacts/verification/VisualUpgrade/FinalProduct/androidsmoke-outer.log',
'artifacts/verification/VisualUpgrade/FinalProduct/android-readiness.json',
]]
manifest = {'status': 'preserved-failed-attempt', 'sourceCommit': record['sourceCommit'], 'phaseExitCode': 1, 'files': []}
for source in sorted(set(path for path in files if path.is_file())):
relative = source.relative_to(root)
target = destination / relative
target.parent.mkdir(parents=True, exist_ok=True)
digest = hashlib.sha256(source.read_bytes()).hexdigest()
if target.exists():
assert hashlib.sha256(target.read_bytes()).hexdigest() == digest, str(target)
else:
shutil.copy2(source, target)
assert hashlib.sha256(target.read_bytes()).hexdigest() == digest, str(target)
manifest['files'].append({'path': relative.as_posix(), 'sha256': digest, 'bytes': source.stat().st_size})
(destination / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n', encoding='utf-8')
for field in ['evidence', 'outerLog']:
original = Path(failed[0][field])
failed[0][field + 'OriginalPath'] = str(original)
failed[0][field] = str(destination / original.relative_to(root))
record_path.write_text(json.dumps(record, indent=2) + '\n', encoding='utf-8')
print(json.dumps({'status': 'preserved', 'files': len(manifest['files']), 'phaseExitCode': 1}))
@@ -1,35 +0,0 @@
import hashlib
import json
import shutil
from pathlib import Path
from datetime import datetime, timezone
root = Path(__file__).resolve().parents[1]
target = root / 'artifacts/verification/VisualUpgrade/PreviousFinal'
assert not target.exists(), 'PreviousFinal already exists; do not overwrite evidence'
target.mkdir(parents=True)
records = []
for source_dir, name in [(root / 'artifacts/verification/Final', 'verification'),
(root / 'artifacts/android/Final', 'android')]:
for source in sorted(source_dir.rglob('*')):
if not source.is_file():
continue
dest = target / name / source.relative_to(source_dir)
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, dest)
actual = hashlib.sha256(dest.read_bytes()).hexdigest()
assert hashlib.sha256(source.read_bytes()).hexdigest() == actual
records.append({'source': source.relative_to(root).as_posix(),
'copy': dest.relative_to(target).as_posix(), 'bytes': dest.stat().st_size, 'sha256': actual})
standard = root / 'artifacts/android/MagnetismHeroes.apk'
if standard.is_file():
dest = target / 'android/standard-MagnetismHeroes.apk'
shutil.copy2(standard, dest)
actual = hashlib.sha256(dest.read_bytes()).hexdigest()
assert actual == hashlib.sha256(standard.read_bytes()).hexdigest()
records.append({'source': standard.relative_to(root).as_posix(), 'copy': dest.relative_to(target).as_posix(),
'bytes': dest.stat().st_size, 'sha256': actual})
assert len(records) > 2500
(target / 'manifest.json').write_text(json.dumps({'status': 'PREVIOUS_RUN_PRESERVED_NOT_CURRENT_VERIFICATION',
'preservedUtc': datetime.now(timezone.utc).isoformat(), 'files': records}, indent=2), encoding='utf-8')
print(f'PREVIOUS_FINAL_PRESERVED files={len(records)} bytes={sum(r["bytes"] for r in records)} sha256_all_matched')
@@ -1,23 +0,0 @@
import hashlib
import json
from pathlib import Path
root = Path('C:/git/MagnetismHeroes')
state_path = root / '.dryforge/execution.json'
state = json.loads(state_path.read_text(encoding='utf-8-sig'))
review_path = root / 'artifacts/verification/VisualUpgrade/FinalProduct/final-review.json'
review = json.loads(review_path.read_text(encoding='utf-8-sig'))
assert review['status'] == 'approved'
digest = hashlib.sha256(review_path.read_bytes()).hexdigest()
assert digest == 'f11a355ff4316fadba7d5532c481a5215f7cbf722f6c207d86d0781906a846ea'
state['tasks']['T6'] = 'verified_delivered_independently_reviewed'
state['horizon_documentation_commit'] = '620c73cbd24d936231bee5cb3469e5cff3424cd9'
state['final_integrated_head'] = '0d992d9dcc94843a72489b454ca4e8137178bc56'
state['post_documentation_product_hashes_unchanged'] = 732
state['final_review'] = {'status': 'approved', 'blockingFindings': 0,
'path': 'artifacts/verification/VisualUpgrade/FinalProduct/final-review.json', 'sha256': digest}
state['final_user_gate'] = {'status': 'pending', 'scope': '완성 제품과 변경된 제작·채택·검증 문서',
'gallery': 'artifacts/verification/VisualUpgrade/FinalProduct/index.html',
'apk': 'artifacts/android/MagnetismHeroes.apk'}
state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
print(json.dumps({'status': 'verified-and-reviewable', 'finalUserGate': 'pending', 'blockingFindings': 0}))
@@ -1,29 +0,0 @@
import hashlib
import json
from pathlib import Path
root = Path('C:/git/MagnetismHeroes')
path = root / '.dryforge/execution.json'
state = json.loads(path.read_text(encoding='utf-8-sig'))
state['tasks']['T5'] = 'terrain_continuity_fixed_independently_reviewed'
state['tasks']['T6'] = 'new_product_four_phase_verification_running'
state['previous_completion'] = state['completion']
state['completion'] = {
'status': 'running',
'productSourceCommit': '3346eb559bdfba24332feb4d9be4b7104520233e',
'record': 'artifacts/verification/VisualUpgrade/FinalProduct/completion.json',
}
state['final_followup'].update({
'fixCommit': 'ef03611628cb395d377cf56abd951bbf62cfa29c',
'integratedProductCommit': '3346eb559bdfba24332feb4d9be4b7104520233e',
'targetedActualExitCodes': {'EditMode': 0, 'PlayMode': 0},
'targetedEvaluatedCases': {'EditMode': 7, 'PlayMode': 10},
'scope': 'Only FarHorizon visual x/z radius35 to27; unchanged height, scale, geometry, materials and gameplay',
'geometryProof': '0 missing samples across central24m corridors in each of three worlds; actual same-camera images confirm reproduced gap removed',
'premergeReview': '.dryforge/reviews/horizon-fix-conformance.json',
})
review = root / '.dryforge/reviews/horizon-fix-conformance.json'
if review.exists():
state['final_followup']['reviewSha256'] = hashlib.sha256(review.read_bytes()).hexdigest()
path.write_text(json.dumps(state, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
print(json.dumps({'status': 'recorded', 'newProductVerification': 'running'}))
@@ -1,28 +0,0 @@
import hashlib
import json
from pathlib import Path
from datetime import datetime, timezone
root = Path(__file__).resolve().parents[1]
review = root / 'artifacts/verification/VisualUpgrade/ModelReview'
packet_path = review / 'review-packet.json'
packet = json.loads(packet_path.read_text(encoding='utf-8'))
approved = [r for r in packet['files'] if r['relativePath'].lower().endswith(('.blend', '.fbx'))]
assert len(approved) == 25, len(approved)
for record in approved:
path = (review / record['relativePath']).resolve()
assert path.stat().st_size == record['bytes']
assert hashlib.sha256(path.read_bytes()).hexdigest() == record['sha256'], str(path)
now = datetime.now(timezone.utc).isoformat()
approval = {'status': 'APPROVED', 'userResponse': '\uc2b9\uc778\ud569\ub2c8\ub2e4',
'confirmedUtc': now, 'scope': 'All five model groups presented in the immediately preceding review',
'groups': [g['id'] for g in packet['reviewGroups']],
'reviewPacketSha256': hashlib.sha256(packet_path.read_bytes()).hexdigest(),
'reviewPacket': 'review-packet.json', 'approvedFiles': approved}
(review / 'user-approval.json').write_text(json.dumps(approval, ensure_ascii=False, indent=2), encoding='utf-8')
state_path = root / '.dryforge/execution.json'
state = json.loads(state_path.read_text(encoding='utf-8'))
state['tasks']['T3'] = 'completed_explicit_user_approval'
state['model_approval'] = approval
state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding='utf-8')
print('EXPLICIT_USER_APPROVAL_RECORDED; 25 original/model hashes matched; 5 groups approved')
@@ -1,27 +0,0 @@
import hashlib
import json
from pathlib import Path
root = Path('C:/git/MagnetismHeroes')
state_path = root / '.dryforge/execution.json'
state = json.loads(state_path.read_text(encoding='utf-8-sig'))
proof = root / 'artifacts/verification/VisualUpgrade/FinalProduct'
record = json.loads((proof / 'completion.json').read_text(encoding='utf-8-sig'))
audit = json.loads((proof / 'root-final-product-audit.json').read_text(encoding='utf-8-sig'))
assert record['status'] == 'passed' and audit['status'] == 'PASS'
assert record['sourceCommit'] == audit['testedProductSourceCommit']
state['completion'] = {
'productSourceCommit': record['sourceCommit'],
'record': 'artifacts/verification/VisualUpgrade/FinalProduct/completion.json',
'recordSha256': hashlib.sha256((proof / 'completion.json').read_bytes()).hexdigest(),
'fourPhasesExitZero': True,
'productFilesUnchanged': audit['productFileHashesUnchanged'],
'editMode': 103, 'playMode': 53, 'androidGates': 226,
'apkSha256': audit['apkSha256'],
'failedAttemptsPreserved': audit['failedAttemptsPreserved'],
}
review_path = root / '.dryforge/reviews/horizon-fix-conformance.json'
state['final_followup']['reviewSha256'] = hashlib.sha256(review_path.read_bytes()).hexdigest()
state['tasks']['T6'] = 'new_four_phases_passed_documentation_refresh_running'
state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
print(json.dumps({'status': 'recorded', 'completion': 'passed', 'finalUserGate': 'pending'}))
Binary file not shown.

Before

Width:  |  Height:  |  Size: 964 KiB

@@ -1,329 +0,0 @@
{
"status": "approved",
"language": "ko",
"reviewedUtc": "2026-10-08T17:48:13.665069+00:00",
"summary": "독립 전체 네 관점 검토를 확정한다. 기존 전체 219파일 검토와 원경 32파일 delta 검토를 유지하고 새 제품 4단계 원문 및 최종 문서/채택 기록을 대조했다. 차단 0건이며 수용된 한계만 남아 있다.",
"scope": {
"mode": "읽기 전용 전체 검토 + 후속 delta/실제 원문 독립 대조",
"runtimeExecution": false,
"testsBuildUnityAdbBlenderRecordingStarted": false,
"onlyWrittenFile": ".dryforge/reviews/final-review.json",
"base": "58410281e015c6600dc543c4e093fd95c036288c",
"reviewedHead": "0d992d9dcc94843a72489b454ca4e8137178bc56",
"testedProductCommit": "3346eb559bdfba24332feb4d9be4b7104520233e",
"changedFilesFromProtectedDev": 219,
"latestDocumentationChangedFiles": 10,
"latestDocumentationPaths": [
"SourceAssets/Models/Heroes/active-adoption.json",
"SourceAssets/Models/Meadow/active-adoption.json",
"docs/asset-register.md",
"docs/content.md",
"docs/engineering-notes.md",
"docs/final-verification.md",
"docs/handoff.md",
"docs/operations.md",
"docs/tracking/decisions/0005-hybrid-assets.md",
"docs/tracking/status.md"
]
},
"repositoryChecks": {
"gitRootBefore": "C:/git/MagnetismHeroes",
"HEADBefore": "0d992d9dcc94843a72489b454ca4e8137178bc56",
"cleanBefore": true,
"HEADAfter": "0d992d9dcc94843a72489b454ca4e8137178bc56",
"cleanAfter": true,
"protectedDev": "58410281e015c6600dc543c4e093fd95c036288c",
"rootAndModuleInstructionsUnchanged": true,
"testedProductToCurrentAssetsPackagesProjectSettingsToolsDiff": "",
"beforeAfterRawProductHashCount": 732,
"beforeAfterDictionariesIdentical": true,
"currentRawProductHashesMatched": 732,
"acceptedGeneratedYamlWhitespaceCheckExit": 0,
"defaultCodeDocsWhitespaceCheckExit": 0
},
"rubric": [
{
"lens": "명세 준수",
"criteria": "R01~R10의 영웅 우선 장난감 외형·세 자연 테마·실제 지형 연결·승인 파일 동일성·수량/ID/배치/입력/저장 경계",
"result": "통과. 원래 전체 검토에서 승인 연결·대표 장비 lifecycle·30개 비시각 정의 보존을 확인했다. 유일한 원경 틈 차단 사항은 공통 radius27 배치로 수정되어 실제 A/B 및 최신 전체 화면에서 해결됐다. 전체 외곽/모든 각도 가시성을 보장하지 않는다."
},
{
"lens": "코드 품질",
"criteria": "기존 상태 소유와 취소·복원 경로 유지, scoped Unity API, 실제 메시/입력 검사 및 최소 변경",
"result": "통과. 원래 GroupBadge 3줄 변경은 기존 수명을 따르고 Core/input/persistence를 변경하지 않는다. 원경 helper는 y/scale/rotation/다른 props를 유지하며 generator와 같은 상수를 사용한다. 새 geometry 검사는 별도 PhysicsScene의 실제 sharedMesh hit이며 게임 collider/board 상태는 불변이다. 엄격한 55개 발견/pick/drag·아치 hole/side·95% label 검사를 유지한다."
},
{
"lens": "문서 내용·형식",
"criteria": "한국어 자기완결 설명, 현재/과거/미검증 범위 구분, 적절한 상세, 허위 통과·새 불필요 의존 금지",
"result": "통과. 최신 8개 docs는 source3346·103/53/226·새 APK 및 실패 시도/Smoke만 재실행을 실제 원문에 맞게 기술한다. ADR는 선택 이유 2문장으로 제한했다. 과거 83182 결과는 BeforeHorizonFix 이력으로 명시했다. 신규 cross-doc 링크 및 active 3문서 참조는 0이다."
},
{
"lens": "문서 완전성·소스 교차 확인",
"criteria": "원본/FBX→Unity GUID/재질→실제 증거/APK 해시, 실제 종료/소스/파일 동일성, 운영 API/필수 지침/최종 승인 구분",
"result": "통과. active-adoption 239개 SHA 기록(별도 승인 파일25개 포함 총264개 SHA 노드)·111 GUID/metaSHA가 실제 root 파일과 일치한다. gallery8Editor+3native 상대 링크 모두 유효하고 source/시각/카드수치가 일치한다. 문서 통합 이후 raw 제품732개 바이트가 동일하므로 문서만의 이유로 Unity 검사를 추가 요구하지 않는다. 제품 전체 사용자 최종 승인은 여전히 별도다."
}
],
"issues": [],
"advisory": [
{
"id": "기존-지면-아래-시점",
"location": "docs/tracking/findings.md:5",
"observation": "yaw210/pitch−45/distance15에서 새 넓은 지면 뒷면이 원래 Island보다 보드를 더 많이 가림을 실제 A/B로 확인했다. 여섯 first physical hit 동일성이 동일 시각 가림을 증명하지 않는다. 55구 탐색/직접 pick/drag 통과에는 음의 pitch 사례가 있다.",
"disposition": "명시적으로 수용된 이번 범위 제한. 기존 카메라/입력/배치 보존을 우선하고 별도 카메라 UX 결정으로 남긴 기록이 정확하다. 현재 변경의 추가 차단 항목으로 올리지 않는다."
},
{
"id": "실기기-및-얼굴-가시성-한계",
"location": "docs/tracking/status.md:11; FinalProduct/EditorCapture 캡처 metadata",
"observation": "실제 Stage21 기본 화면의 큰 묶음 얼굴 일부가 앞 구에 물리적으로 가려지며 숫자5와 다른 대표 얼굴은 보인다. Editor texture memory는 폰트/RT 포함이고 resident triangle 수는 꺼진 원래 floor도 포함한다. Camera.Render 시간은 Editor CPU wall time이다. AVD와 합성 viewport는 실제 모바일 GPU/FPS/열/배터리·아이 사용성·iOS를 증명하지 않는다.",
"disposition": "배치와 physical occlusion 보존에 따른 수용 제한·후속 검증 범위. 모든 각도/모든 얼굴 가시성 또는 실기기 성능 합격을 주장하지 않은 현재 문서를 유지한다."
},
{
"id": "원경-geometry-표본-범위",
"location": "docs/engineering-notes.md; DistantTerrainHasContinuousApronAcrossItsVisibleFootInsteadOfSkyGaps",
"observation": "0 누락 결과는 cardinal corridor lateral ±12m/방사 .25m/횡단2m 표본이며 actual foot 약±15.35m 전체 경계·픽셀·각도 증명이 아니다. FOV14는 비교용 임시 카메라이고 제품 FOV62는 유지한다.",
"disposition": "최종 문서의 표본 범위와 실제 close/full 시각 판단을 함께 근거로 사용한다. 현재 실제 관찰에서 중앙 틈은 해결되었고 양끝 평평한 림은 외곽 silhouette로 판정했다. 추가 차단 사항은 없다."
}
],
"resolvedIssues": [
{
"id": "원경-지형-연속성",
"previousStatus": "차단",
"resolution": "ef036 원경 배치 수정 및 3346 실제 전체 4단계 통과/현재 문서 반영",
"evidenceReview": ".dryforge/reviews/horizon-fix-conformance.json",
"reviewSha256": "5eba704c16ab4e7319c10aad8e337b8737e974df99966ff4b9d05040f3ac3e22",
"originalWholeReview": "artifacts/verification/VisualUpgrade/BeforeHorizonFix/final-review.json",
"originalReviewSha256": "2f4269c77224aea872289cf51f76a5f6cdb626ec7af1789a63f5fcfb91a0e5b0"
}
],
"capturedVerification": {
"phases": [
{
"phase": "EditMode",
"command": "pwsh -NoProfile -File tools/verify.ps1 -Phase EditMode -Milestone Final",
"outerExit": 0,
"rawExit": 0,
"sourceCommit": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceCommitEnd": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceDirty": false,
"sourceDirtyEnd": false,
"startedUtc": "2026-10-08T16:56:47.0526983Z",
"finishedUtc": "2026-10-08T16:57:10.801033Z",
"evidenceSha256": "c8e8b452df15fdf1a953d0b679af8f8f36b948668e75e4e6d7f76765b981dc14",
"outerLogSha256": "27b5747f628fe5968282f21fa7eb401c5280ad1dbf83967f45fdee5ac207abc2"
},
{
"phase": "PlayMode",
"command": "pwsh -NoProfile -File tools/verify.ps1 -Phase PlayMode -Milestone Final",
"outerExit": 0,
"rawExit": 0,
"sourceCommit": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceCommitEnd": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceDirty": false,
"sourceDirtyEnd": false,
"startedUtc": "2026-10-08T16:57:10.8088291Z",
"finishedUtc": "2026-10-08T16:57:54.4619847Z",
"evidenceSha256": "e279aa904d6852115df0204135caef908fdd3e74ae71a8ea8a5827b8c013a41b",
"outerLogSha256": "07316d712aa9e277134cb6a1a406b2718571b358da8a0a2186bd97e2c57f86ef"
},
{
"phase": "AndroidBuild",
"command": "pwsh -NoProfile -File tools/verify.ps1 -Phase AndroidBuild -Milestone Final",
"outerExit": 0,
"rawExit": 0,
"sourceCommit": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceCommitEnd": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceDirty": false,
"sourceDirtyEnd": false,
"startedUtc": "2026-10-08T16:57:54.4634842Z",
"finishedUtc": "2026-10-08T16:58:44.8633063Z",
"evidenceSha256": "ac136676b9052f84eeef31d40180e5cd6d91f8516b1e67f08d8134a5addf2dc5",
"outerLogSha256": "f3f115511da8d3e848d705f30789ee4a962ec0eb5709ebb7d7dfbd092e758ead"
},
{
"phase": "AndroidSmoke",
"command": "pwsh -NoProfile -File tools/verify.ps1 -Phase AndroidSmoke -Milestone Final",
"outerExit": 0,
"rawExit": 0,
"sourceCommit": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceCommitEnd": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceDirty": false,
"sourceDirtyEnd": false,
"startedUtc": "2026-10-08T17:04:22.9959710Z",
"finishedUtc": "2026-10-08T17:19:58.4677196Z",
"evidenceSha256": "0c1cbcd64170c4e3747aa40ea9de51e36dabf9621b13a09f46c974f51a399122",
"outerLogSha256": "50d4a2b0ee8dfe1891a02b236243aec79dec086cdad391f5d8b6bae66390fa1b"
}
],
"xmlActual": [
{
"name": "editmode",
"actualCaseCount": 103,
"actualPassed": 103,
"failed": 0,
"skipped": 0,
"inconclusive": 0,
"sha256": "5f5f187799de24fba7ec9db705306c38c86e69d944d61fc56d09d4413a146516"
},
{
"name": "playmode",
"actualCaseCount": 53,
"actualPassed": 53,
"failed": 0,
"skipped": 0,
"inconclusive": 0,
"sha256": "e8b3c90b13f4887126ef6a7d03711d3c8f48e678b6d5034fa9463ee95427e826"
}
],
"androidActual": [
{
"path": "android-final-evidence.json",
"count": 226,
"notPassed": 0,
"sourceCommit": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceCommitEnd": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceDirty": false,
"sourceDirtyEnd": false,
"apkSha256": "DA533F235A9869E5A68F4A244AF9A89E34A8E6FB9E7C7F2F57D830939507B829",
"sha256": "18ec499dd9d30cd0a00df16012e8e8021e0cbf77a97089611a3954ed5c00aa7b"
},
{
"path": "Fresh30/android-final-progression-evidence.json",
"count": 151,
"notPassed": 0,
"sourceCommit": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceCommitEnd": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceDirty": false,
"sourceDirtyEnd": false,
"apkSha256": "DA533F235A9869E5A68F4A244AF9A89E34A8E6FB9E7C7F2F57D830939507B829",
"sha256": "afe9adb99144f811761340586f27fdaff49d677c490f2bea8dad6a67afff066c"
},
{
"path": "FinalBoundary/android-final-boundary-evidence.json",
"count": 37,
"notPassed": 0,
"sourceCommit": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceCommitEnd": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceDirty": false,
"sourceDirtyEnd": false,
"apkSha256": "DA533F235A9869E5A68F4A244AF9A89E34A8E6FB9E7C7F2F57D830939507B829",
"sha256": "c3ea05c1b5d1041a3660af6063dfa21090e1d02857ac9e17d7cd72ade2bcf6eb"
},
{
"path": "RapidBoundary/android-final-rapid-evidence.json",
"count": 39,
"notPassed": 0,
"sourceCommit": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceCommitEnd": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceDirty": false,
"sourceDirtyEnd": false,
"apkSha256": "DA533F235A9869E5A68F4A244AF9A89E34A8E6FB9E7C7F2F57D830939507B829",
"sha256": "95a415c702a5f407a2fe86194ff22959a0584cb85328011ec83090b1ae5e42fe"
},
{
"path": "ContentBoundary/android-content-evidence.json",
"count": 69,
"notPassed": 0,
"sourceCommit": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceCommitEnd": "3346eb559bdfba24332feb4d9be4b7104520233e",
"sourceDirty": false,
"sourceDirtyEnd": false,
"apkSha256": "DA533F235A9869E5A68F4A244AF9A89E34A8E6FB9E7C7F2F57D830939507B829",
"sha256": "bf93139c8b749c10c2c562b20888320b586edeaebe2ba6f27ff5bf214755a9e7"
}
],
"childrenActualExitCodes": [
0,
0,
0,
0
],
"freshSequentialActualSuccessChecks": 30,
"freshSavePushCommands": 0,
"apkCopies": {
"paths": [
"artifacts/android/MagnetismHeroes.apk",
"artifacts/android/Final/MagnetismHeroes.apk",
"artifacts/verification/Final/Fresh30/installed-base.apk"
],
"bytes": 57215937,
"sha256": "DA533F235A9869E5A68F4A244AF9A89E34A8E6FB9E7C7F2F57D830939507B829",
"allThreeIndependentlyMatched": true
},
"voicePayload": {
"actualApkDecodedRawReportPassed44": true,
"actualRuntimeWavShaPcmFormatIndependentlyRecomputedMatched": 44,
"apkRedeocodeByReviewer": false,
"newRecordingOrListeningByReviewer": false
},
"noticesZipExactHashes": {
"NotoSansCJK-OFL.txt": "88f117575237307bdd86a17ef15e21790fc9a662fe4dfb103ca1ca077f0d9982",
"TTS-Apache-2.0.txt": "7137274bab133744763a3906ace2a13844c2b34635136fc6d4454c25bfd76eda",
"TTS-model-card.txt": "2157466302fcaf8e4979d0505aa4b648ad32468aff98b3e964046db05e574f3c"
},
"forbiddenSourceToolCacheApkEntries": 0,
"approvedModelFileHashesMatched": 25,
"materialExactBytesAndValuesMatched": 31,
"stageNonvisualDefinitionsPreserved": 30,
"metadataShaNodesMatched": 264,
"metadataShaRecordsExcludingApproved25": 239,
"guidAndMetaShaMatched": 111,
"latestEditorCaptureCount": 119,
"galleryEditorCards": 8,
"galleryActualAvdCards": 3,
"galleryDeadLinks": 0,
"galleryRemoteLinks": 0,
"latestImagesPersonallyViewed": {
"artifacts/verification/Final/Fresh30/stage-1-enter.png": "24a1fee6f57ed238976dde803184895fbe4900e3ed218fc3064c9743ef9c7e63",
"artifacts/verification/Final/Fresh30/stage-10-next.png": "a17e8540e959fcad55fcde48ccee3654999647e59f7e245839e3872108776fc9",
"artifacts/verification/Final/Fresh30/stage-20-next.png": "79cf0d0bef7671ebc92c2d1cf1f9b557633f7832425b6fc01809944ea8b0b3b3",
"artifacts/verification/VisualUpgrade/FinalProduct/EditorCapture/world11-ten-one-group-tablet-orbitzoom.png": "31e8a98b730295f894f448a2a45e71d246c723354dece8d047013fc55495ebef",
"artifacts/verification/VisualUpgrade/FinalProduct/EditorCapture/world21-single-resupplied-phone-orbitzoom.png": "91683df326dfe70f269b56d02cd2beedd93f6350fb0957995199b1a54c812dad"
},
"previousWholeReviewImagesKeptAt": "artifacts\\verification\\VisualUpgrade\\BeforeHorizonFix\\final-review.json",
"horizonImagesPersonallyViewed8KeptAt": ".dryforge\\reviews\\horizon-fix-conformance.json"
},
"failureAndHistoryPreservation": {
"latestFailedAttemptSource": "3346eb559bdfba24332feb4d9be4b7104520233e",
"latestFailedAttemptActualExit": 1,
"koreanObservation": "Fresh30 stage5의 adb shell wm size가 localhost5037 연결에서 약21초 뒤 timeout10060으로 실제 실패했다. 게임 판정/AVD 충돌로 원인을 단정하지 않는다.",
"latestFailedAttemptArchivedFiles": 1257,
"latestFailedAttemptArchiveHashesIndependentlyMatched": 1257,
"previous83182ArchivedFiles": 2311,
"previousArchiveHashesIndependentlyMatched": 2311,
"retry": "같은 source/APK/732제품 바이트에서 AndroidSmoke만 실제 재실행했다. 새 Edit/Play/Build의 선행 통과를 재사용했으며 세 단계를 재실행했다고 주장하지 않는다. 최초 실패를 통과로 바꾸지 않았다.",
"successfulSmokeStartedUtc": "2026-10-08T17:04:22.9959710Z",
"successfulSmokeFinishedUtc": "2026-10-08T17:19:58.4677196Z",
"olderPreviousFinal2517And113FailedAttemptPreservation": "이전 전체 독립 검토의 원문/해시 확인을 그대로 보존한다."
},
"latestDocumentsRead": [
"docs/asset-register.md",
"docs/content.md",
"docs/engineering-notes.md",
"docs/final-verification.md",
"docs/handoff.md",
"docs/operations.md",
"docs/tracking/decisions/0005-hybrid-assets.md",
"docs/tracking/status.md"
],
"harnessRequiredFilesPresent": true,
"reviewerInstructionsRead": [
"reviewer-prompt.md",
"harness-review.md",
"harness-format.md"
],
"currentDocsReadRule": "기존 전체 검토에서 root/모듈 AGENTS와 적용 문서를 직접 읽었고 이번 8개 docs 전후 diff와 운영/결정/인수인계 현재 문맥 및 최신 원문을 다시 대조했다. 역사/범위 밖 문서 참조를 광범위 정리할 신규 요구로 만들지 않는다.",
"evidenceSha256": {
"artifacts\\verification\\VisualUpgrade\\FinalProduct\\completion.json": "dd898f331a31fb49545e9c3ee2d1c5b52adca97d907f5cff1b0c21b1e6d429fd",
"artifacts\\verification\\VisualUpgrade\\FinalProduct\\product-before.json": "c8232551e6b814b7eaadc343e95929ece779102b09daad3817253c847643213f",
"artifacts\\verification\\VisualUpgrade\\FinalProduct\\product-after.json": "c8232551e6b814b7eaadc343e95929ece779102b09daad3817253c847643213f",
"artifacts\\verification\\VisualUpgrade\\FinalProduct\\horizon-documentation-result.json": "816b1725a45bd0206c517a2a2a6b71153a9150d519152547872b3d3ce66a3a1b",
"artifacts\\verification\\VisualUpgrade\\FinalProduct\\horizon-documentation-audit.json": "0cc133ea881ccc9a96172edb40ad12ba7ed25ce00eec0b366119018e8aa6203d",
"artifacts\\verification\\VisualUpgrade\\FinalProduct\\documentation-final-evidence-audit.json": "14e62264a22f360f9afda94e1c8660507cda3b27c30d6f9916607889c2819e08",
"artifacts\\verification\\VisualUpgrade\\FinalProduct\\root-final-product-audit.json": "7f1c84d9fa0f593fe72e15e9d3970c6b10fcb329e71adda291cd07e1802a5b49",
"artifacts\\verification\\VisualUpgrade\\FinalProduct\\index.html": "c959897dce94e24ea3d54198864bcc49dcdc58965aac739930920e26194d79cc",
"artifacts\\verification\\Final\\VoicePayload\\apk-voice-payload.json": "a3c1fc9e0ef0170b3fcb60170ae4ca5b9ec640b644fd1f2b00fea591bdc28650",
"SourceAssets\\Models\\Heroes\\active-adoption.json": "3447de26a676872a4f1527dea9c0c642c90cc690569323f3e19554c31ea99cb5",
"SourceAssets\\Models\\Meadow\\active-adoption.json": "8d77a7fce5a847af64bbca8cb54d0997ab728ff5ca8bf0a9b6949f1df233b6b4"
},
"nextGate": "검토 승인·차단0. 최종 제품 사용자 승인과 dev 통합/원격 push는 이 리뷰가 수행하거나 대체하지 않는다."
}
@@ -1,221 +0,0 @@
{
"status": "approved",
"reviewType": "원경 틈 수정 pre-merge 독립 검토",
"language": "ko",
"reviewedAtUtc": "2026-10-08T16:57:49.857929+00:00",
"scope": {
"baselineCommit": "da169df7c7c7215ce562f8f512d93fb4df2e39b7",
"reviewedCommit": "ef03611628cb395d377cf56abd951bbf62cfa29c",
"worktree": "C:\\git\\MagnetismHeroes\\.dryforge\\worktrees\\T4",
"changedFiles": 32,
"changedStageAssets": 30,
"changedFarHorizonPositionRows": 120,
"runtimeExecutionByReviewer": false,
"permittedWrite": "리뷰 JSON만",
"nextGate": "root 통합 후 새 제품의 전체 4단계 및 갱신 문서/해시 검토는 별도 후속"
},
"preservedReview": {
"path": "artifacts\\verification\\VisualUpgrade\\BeforeHorizonFix\\final-review.json",
"sha256": "2f4269c77224aea872289cf51f76a5f6cdb626ec7af1789a63f5fcfb91a0e5b0",
"meaning": "기존 전체 네 관점 검토의 결과와 실제 원문 증거를 유지하며 이번 판단은 원경 수정 delta에 적용한다."
},
"rubric": [
{
"lens": "요구·코드 품질",
"result": "통과",
"checks": [
"R07 실제 화면 중앙 원경 하단의 하늘/검은 틈 제거",
"R03/R04 승인된 구 배치·그룹·목표·순서와 수학 유지",
"R06 승인 원본/FBX 25개 및 31개 재질 정확 해시 유지",
"R09 기존 Stage의 FarHorizon x/z 위치만 갱신하는 최소 수정"
]
},
{
"lens": "내용 형식",
"result": "통과",
"checks": [
"commands/result/preservation JSON과 XML이 실패 시도·통과 범위·소스·환경을 구분",
"캡처 metadata에 14도 foot 비교 FOV 명시"
]
},
{
"lens": "완전성",
"result": "통과(이번 좁은 범위)",
"checks": [
"세 세계 actual geometry probe와 실제 화면",
"targeted Edit 7개 및 Play 10개 원문",
"새 전체 4단계를 이미 수행했다고 기록하지 않음"
]
},
{
"lens": "교차 참조·원문",
"result": "통과",
"checks": [
"CLI command/exit 기록과 Unity 원문 종료 코드 및 XML 대조",
"기존 실패 366·trial28 실패6을 녹색으로 바꾸지 않음",
"승인/재질/비시각부 보존 보고서를 실제 파일·git delta와 대조"
]
}
],
"issues": [],
"resolvedBlocker": {
"requirement": "R07 자연스러운 지형 연결",
"location": "Assets/MagnetismHeroes/Editor/VisualUpgradeAssets.cs / 30 StageAssets FarHorizon 배치",
"cause": "radius35에서 파란 원경 아래 apron과의 겹침 부족",
"fix": "공통 FarHorizonRadius27 및 scoped Unity Editor API 배치 갱신",
"visualEvidence": [
"horizon-world11-foot-legacy-radius35.png",
"horizon-world11-foot-current.png",
"world11-ten-one-group-tablet-orbitzoom.png",
"world21-single-resupplied-phone-orbitzoom.png"
],
"judgment": "동일 카메라 legacy/current close foot 및 원래 지적된 두 전체 화면에서 중앙 sky/검은 slit가 사라졌다. 전체 화면 양끝의 평평한 원경 림은 관찰 범위에서 녹색 apron과 맞닿는 원래 mound 외곽 실루엣이며, 하늘이 발밑으로 관통하는 잔여 floating gap 근거는 없다."
},
"codeReview": {
"helper": "ApplyHorizonContinuity는 기존 30 Stage의 FarHorizon 네 개를 기대하고 x/z 방향을 정규화하여 공통 반경으로 옮기며 y/scales/rotation/다른 props를 유지한다. 반복 실행 결과가 같고 기존 generator가 같은 상수를 사용한다. StageSignature로 비시각부 보존을 검사한다.",
"newTest": "실제 imported sharedMesh를 별도 LocalPhysics3D Scene의 MeshCollider로 탐색하므로 반경 상수 복사만으로 통과하는 검사가 아니다. 실제 게임 계층에는 collider를 추가하지 않는다. board JSON 및 actual collider array 불변을 종료 시 검사하고 camera/FOV/임시 legacy 배치를 복원한다.",
"serializedDiff": "32개 파일은 scoped helper·Play geometry/capture metadata·30 Stage이며 120개의 FarHorizon x/z 행 외 다른 Stage 필드 변경이 없다. Unity API 저장 형태를 유지한다."
},
"preservation": {
"approvedBinaryShaMatched": 25,
"materialExactBytesAndColorMatched": 31,
"stagesNonvisualPrefixMatched": 30,
"unchanged": "승인 geometry/FBX/import metadata/재질·초기 구/그룹/ID/수학/카메라/입력/collider/다른 장식 배치",
"independentMethod": "실제 파일 SHA 비교와 baseline→immutable commit 전체 32파일 diff 검토"
},
"actualVerification": {
"xmlChecks": [
{
"path": "red-geometry.xml",
"result": "Failed(Child)",
"total": "1",
"passed": "0",
"failed": "1",
"skipped": "0",
"inconclusive": "0",
"actualCaseCount": 1,
"actualPassedCaseCount": 0
},
{
"path": "trial28.xml",
"result": "Failed(Child)",
"total": "1",
"passed": "0",
"failed": "1",
"skipped": "0",
"inconclusive": "0",
"actualCaseCount": 1,
"actualPassedCaseCount": 0
},
{
"path": "editmode.xml",
"result": "Passed",
"total": "7",
"passed": "7",
"failed": "0",
"skipped": "0",
"inconclusive": "0",
"actualCaseCount": 7,
"actualPassedCaseCount": 7
},
{
"path": "playmode.xml",
"result": "Passed",
"total": "10",
"passed": "10",
"failed": "0",
"skipped": "0",
"inconclusive": "0",
"actualCaseCount": 10,
"actualPassedCaseCount": 10
}
],
"editUnityExit": 0,
"editCliExit": 0,
"playUnityExit": 0,
"playCliExit": 0,
"redUnityExit": 2,
"redCliExit": 8,
"redMissingSamples": 366,
"trial28UnityExit": 2,
"trial28CliExit": 8,
"trial28MissingSamples": 6,
"initialCompileAttempt": {
"log": "red.log",
"exit": 1,
"evaluatedGeometry": false
},
"geometryFinalMissingByStage": {
"1": 0,
"11": 0,
"21": 0
},
"commandsVerifiedFrom": "commands.json + Unity log COMMAND LINE ARGUMENTS + test completion exit lines + XML actual cases",
"captureInventory": 119,
"personallyViewedNewPngCount": 8,
"all119ViewedClaim": false
},
"advisories": [
{
"id": "geometry-sampling-bound",
"severity": "정보",
"finding": "새 probe는 cardinal corridor lateral ±12m, radial 0..실제 배치 거리, step .25m/2m이다. 실제 원경 foot 폭 약 ±15.35m의 전체 경계나 모든 각도를 망라하지 않는다.",
"disposition": "표본 0만으로 전체 외곽 해결을 주장하지 않고, 독립 실제 화면 판정을 함께 근거로 사용했다. 향후 더 넓은 카메라/원경 범위를 도입할 때 probe 범위를 함께 검토한다."
},
{
"id": "existing-below-ground-camera",
"severity": "정보",
"finding": "기존 자유 Orbit의 지하 각도에서 넓은 clearing의 underside가 더 큰 화면 영역을 가릴 수 있다.",
"disposition": "기존 전체 리뷰의 수용 한계를 유지한다. 이번 변경이 카메라/물리/구 배치를 바꾸지 않으며 모든 각도 가시성을 주장하지 않는다. 별도 UX 결정 범위이다."
},
{
"id": "existing-device-and-physical-occlusion",
"severity": "정보",
"finding": "합성 Editor CPU/메모리는 실제 단말 FPS/GPU/열/배터리 검증이 아니며, 일부 초기 그룹의 얼굴은 다른 구에 물리적으로 가려질 수 있다.",
"disposition": "원래 리뷰와 문서의 한계를 유지한다. 현재 초기 배열과 label/55 구 발견·선택·드래그 조건을 보존한다. 실제 단말/아동/iOS는 이번 검토 범위 밖이다."
}
],
"gitChecks": {
"rootBefore": {
"gitRoot": "C:/git/MagnetismHeroes",
"HEAD": "da169df7c7c7215ce562f8f512d93fb4df2e39b7",
"clean": true,
"protectedDev": "58410281e015c6600dc543c4e093fd95c036288c"
},
"rootAfter": {
"HEAD": "3346eb559bdfba24332feb4d9be4b7104520233e",
"clean": true,
"protectedDev": "58410281e015c6600dc543c4e093fd95c036288c"
},
"worktreeAfter": {
"HEAD": "ef03611628cb395d377cf56abd951bbf62cfa29c",
"clean": true
},
"rootHeadChangeExplanation": "pre-merge clear를 parent에게 먼저 전달한 뒤 root가 통합을 진행하여 종료 체크의 root HEAD가 변했다. 본 판정 대상은 immutable ef036 commit이며 root 통합 전체 단계 승인을 뜻하지 않는다."
},
"evidenceSha256": {
"commands.json": "30aa2370d563444fb5354fd7aad6e4c996baa2d2bbfd5c67499931a314deea5e",
"result.json": "d3b70ae81c29b15582fe77b0c9c6636432c62f68606d39faff1da8d48da2f7a4",
"preservation.json": "cbab404eef5524a053cfca09be50a579f6bae858cfab78dc3dfdc6e2501ddb34",
"red-geometry.xml": "73b8dbd327c7ddc0618f0c2a10befe12033ae8d0a0585eb66d6dcc37d87a6fe3",
"red-geometry.log": "171a631aa96287796c0c795a4d4ef6662efa65a92707e3894930af5731bc94aa",
"trial28.xml": "da2f290364723076609dc513df06fcb454a9a4d75ac575cb56bd0ac94fa37a5b",
"trial28.log": "e7c9148d4e16fa208b77d222eb6f1443dd5820ec9c54e2f63579f98d9c5c1d0d",
"editmode.xml": "bf3826d56e130eb11264035d563029d1d0277f0a5630f56cdb1ffc4100260a3a",
"editmode.log": "ee4b615dc1f07c731d0774d2b958611afa530173b570f70cb0074090567d7d03",
"playmode.xml": "95e3a935c26803c2af29d94b60c5b674b4ad1c61359bfbcadc9a8d196237ea03",
"playmode.log": "4fe5cc4e76e937ab7d3fef7ce67f0c9f5dabc481e52ec08c9311d7b192ffc14a",
"apply27.log": "c306375c2de4b24a88d59d26924005e37baec7157ca91f40f47e414488c00b09"
},
"personallyViewedImageSha256": {
"horizon-world11-foot-legacy-radius35.png": "d6d23085030e82f21f48dce40aab221d3657c3ce76d9d3606ae1b256676d1944",
"horizon-world11-foot-current.png": "181a9b24cbeb8a0971b4bb2aa0b467899cce40c3116556cb6fa6e848219ba4cb",
"world11-ten-one-group-tablet-orbitzoom.png": "1ecebdd4677377b490f5836a0ae8cabed6308df9d9bd43f12038a5da31b87720",
"world21-single-resupplied-phone-orbitzoom.png": "9096a13e0cf4ccd9bb8e4b982b6278fbe9f648423676583cc98880d9cfc297da",
"horizon-world1-tablet-default.png": "7f977eb18af84fdf5f2825bcc3b65e8a6733776893870950bdef8d62c8406931",
"horizon-world21-phone-orbitzoom.png": "7255763644f98a36a973b3b17752e79ac83d7633cf9969d8058106e415ee3997",
"horizon-world1-foot-current.png": "66cc35a90ac0ab0bde5a5f1fc52e380b994251149e42335ad9a792ed801386c4",
"horizon-world21-foot-current.png": "05dbbf98e8e1961222ed8579f52768134939a6601148002d9246a0761907d2f9"
},
"boundedConclusion": "원경 수정 delta를 승인하며 차단 0건이다. 원래 전체 검토 결과를 보존하고 새 통합 제품의 전체 네 단계·문서/메타데이터 변경 검토를 별도 후속으로 남긴다. 최종 사용자 승인이나 dev merge/push의 승인이 아니다."
}
@@ -1,77 +0,0 @@
{
"schemaVersion": 1,
"reviewer": "model_conformance independent spec reviewer",
"base": "58410281e015c6600dc543c4e093fd95c036288c",
"reviewDateUtc": "2026-10-07T17:03:27.724783+00:00",
"status": "pass",
"blockers": [],
"readOnlyScope": "Repository and worktrees read-only; Python bytes/SHA/FBX parsing and image viewing only; no Blender/Unity/commit/runtime adoption. Writes limited to .dryforge/reviews evidence.",
"userApproval": "PENDING; this review does not approve runtime adoption",
"runtimeVerification": "NOT_RUN; later approved Unity integration must verify actual interaction, selection, 30-stage mapping and Android",
"task": "T1",
"branch": "dryforge/hero-model",
"commit": "7d7efbe6b6a1a419ac998f37e3e019a75570a9e5",
"worktree": "C:\\git\\MagnetismHeroes\\.dryforge\\worktrees\\T1",
"changedFiles": 7,
"evidenceRoot": "artifacts/verification/VisualUpgrade/ModelReview/Heroes",
"artifacts": [
{
"path": "SourceAssets/Models/Heroes/MagneticKnight.blend",
"bytes": 1833499,
"sha256": "7ce1272dfe861aee986609cdb899f0edf8b8f2f0f17269c0eda472c8aa90ee3a"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Heroes/HeroBody.fbx",
"bytes": 60076,
"sha256": "56f3301176c612ebeab3fdaaba5c019674beeb0f727f97ed27dd67e32ff50a40"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Heroes/KnightEquipment.fbx",
"bytes": 222764,
"sha256": "a8059a020e79e080635e7326bb91576010cad10c60ee931d95536d47fe76e61c"
}
],
"hashes": "Source/2 FBX/face atlas/six sprite records independently matched actual bytes",
"preservation": "13 original FBX byte-identical; existing provenance Git blobs unchanged; only 7 added candidate/tool/doc files; no manual meta or runtime wiring",
"findings": [
{
"spec": "Body mesh/material improvement, radius .5m and root scale1",
"evidence": [
"tools/models/build_hero.py:238",
"tools/models/build_hero.py:251",
"SourceAssets/Models/Heroes/verification.json",
"artifacts/verification/VisualUpgrade/ModelReview/Heroes/body_standalone.png"
],
"observed": "New smooth body mesh/material authored. Actual FBX 1986 vertices with radii .499999984 to .500000019m; 1 body mesh and 22 equipment meshes, default scale1 and shared origin."
},
{
"spec": "Rounded open helmet, short blunt sword, round shield; existing 6 expressions, thin blue mask and cape coexist",
"evidence": [
"artifacts/verification/VisualUpgrade/ModelReview/Heroes/hero_three_quarter.png",
"artifacts/verification/VisualUpgrade/ModelReview/Heroes/knight_front.png",
"artifacts/verification/VisualUpgrade/ModelReview/Heroes/knight_side.png",
"artifacts/verification/VisualUpgrade/ModelReview/Heroes/knight_back.png",
"artifacts/verification/VisualUpgrade/ModelReview/Heroes/expression_0_Neutral.png",
"artifacts/verification/VisualUpgrade/ModelReview/Heroes/expression_5_Celebrating.png"
],
"observed": "All 11 actual PNGs viewed. No face/eye/mouth occlusion or visible broken silhouette preventing the later interaction promise. Intentional attachment overlap is not treated as a solid-intersection-zero requirement."
},
{
"spec": "Editable source, export/reopen/mesh/SHA checks, source-to-game-file provenance; preview-only atlas reuse",
"evidence": [
"SourceAssets/Models/Heroes/README.md",
"SourceAssets/Models/Heroes/provenance.json",
"SourceAssets/Models/Heroes/verification.json",
"tools/models/build_hero.py:138",
"tools/models/build_hero.py:212",
"artifacts/verification/VisualUpgrade/ModelReview/Heroes/build-final.log",
"artifacts/verification/VisualUpgrade/ModelReview/Heroes/verify.log"
],
"observed": "Actual Blender5.2.2LTS reopen/FBX-import logs, named body/equipment/review collections, m Z-up to -Z-forward/Y-up, no animation. Face/ribbon not included in FBX and preserve source atlas SHA."
}
],
"remaining": [
"Full hero plus three-landscape presentation and explicit user model approval are still required before any runtime adoption.",
"One representative hero per group, quantity/ID/collider boundaries and scene selection are later Unity integration checks."
]
}
@@ -1,602 +0,0 @@
{
"schemaVersion": 1,
"reviewer": "model_conformance independent spec reviewer",
"base": "58410281e015c6600dc543c4e093fd95c036288c",
"reviewDateUtc": "2026-10-07T17:03:27.724783+00:00",
"status": "pass",
"blockers": [],
"readOnlyScope": "Repository and worktrees read-only; Python bytes/SHA/FBX parsing and image viewing only; no Blender/Unity/commit/runtime adoption. Writes limited to .dryforge/reviews evidence.",
"userApproval": "PENDING; this review does not approve runtime adoption",
"runtimeVerification": "NOT_RUN; later approved Unity integration must verify actual interaction, selection, 30-stage mapping and Android",
"task": "T2",
"branch": "dryforge/meadow-model",
"commit": "80be80566a309eab8cb333ea9774218c816efc30",
"worktree": "C:\\git\\MagnetismHeroes\\.dryforge\\worktrees\\T2",
"changedFiles": 25,
"evidenceRoot": "artifacts/verification/VisualUpgrade/ModelReview/Meadow",
"artifacts": [
{
"path": "SourceAssets/Models/Meadow/MeadowCandidates.blend",
"bytes": 1350505,
"sha256": "2d95f1797c1a1042e32bb2588d0bde735b6e109a5f947b1f0720c7537bda6ea9"
},
{
"path": "tools/models/build_meadow.py",
"bytes": 43245,
"sha256": "f0e7717a5154bf85eeff68639f6f21ca1f79f6d69af8fa0125acf43415f32db1"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/DaisyColony.fbx",
"bytes": 179196,
"sha256": "a646a875d99b7cbcef727723b45c6a1e5d6231255ef100689fe69db2e55af63c"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/DistantRidge.fbx",
"bytes": 67788,
"sha256": "af68a4b08e0696cb0b56acf92bf0753d288ad6ba89fd4f19b183995f4ed6fd0c"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/FarHorizon.fbx",
"bytes": 66924,
"sha256": "fcfb4a768ca33ab9ccd166f7e4b3a96cbc918d1a564f59144b0fab399cb7c179"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/FernCluster.fbx",
"bytes": 202508,
"sha256": "9153c91cb71a762c54bc0bc3c5e6fcfe2650185defaf60b5da75e25d78aa4911"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/GentleHill.fbx",
"bytes": 67420,
"sha256": "356d3baa0927264f7807e13f3acb717b0c040ae09b23b8feac67e717142d3a19"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/GrassTuftGold.fbx",
"bytes": 72908,
"sha256": "0b2a3d68909b56ecdd3324503d3a6bc2b2f9dc8e1d173536dfe52ca96e49521f"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/GrassTuftMint.fbx",
"bytes": 72700,
"sha256": "650e02dab0ae3a0f05d036e2d58846cb97a93b77864cbc2c704b4364c26d9962"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/LavenderLupineColony.fbx",
"bytes": 250284,
"sha256": "596266e001d7765634ff749deee33cbece1e5bb69ba693bf8c34da795f4f7bdb"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/LowLimestone.fbx",
"bytes": 47228,
"sha256": "026abf6db9395751e7ad933455bacc96a06fe331a4bb777001ee29bf33f8417d"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/MeadowClearing.fbx",
"bytes": 68284,
"sha256": "78c307844de3ef5584aaa0bec3e57500f9115065dd643686473e52632d551a29"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/MintShrub.fbx",
"bytes": 96012,
"sha256": "f501d302845a6df4814076abf76bdd97f5ffd2f0527f2aff236e0b97e78e62da"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/MossStoneSteps.fbx",
"bytes": 155068,
"sha256": "5a1bf51f80abde9a9a9e7a419ad30509ad62beac372244e0aba5681896a01739"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/OpenStoneArch.fbx",
"bytes": 34636,
"sha256": "8835e7c96ca842ddece1c1f7c9d168a1eabf4f0555e5a025bf46637969dff243"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/PebbleScatter.fbx",
"bytes": 32060,
"sha256": "34748a549b40ebe8a53cb57d9638ccbf3e480e8b56453b686c378db8a069da8f"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/PinkCosmosColony.fbx",
"bytes": 153516,
"sha256": "6bfc69131ff2cde84f858aa50e3e11697679073d3cd1d0a39a15f4b02cc3afb6"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/RoundCanopyTree.fbx",
"bytes": 147772,
"sha256": "9a41dcb9cb40e080bf4be6418f68eb5893561d9454e54a79a402f936ceb59757"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/RoundLimestone.fbx",
"bytes": 48700,
"sha256": "eaff76573039ab66927749569e511d5031e90c9ceb7dd8d7cccd84ab7032ca36"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/SageCanopyTree.fbx",
"bytes": 147772,
"sha256": "26665d557ef176a9fcce24107d19619b9848552538482eae4b35ebd9fbcd0aec"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/SageShrub.fbx",
"bytes": 96108,
"sha256": "fbfed3947cc3e05c82f5660bae9c864576839174c7f72c0cbf0a278d6847eefc"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/SlateBoulder.fbx",
"bytes": 49852,
"sha256": "48da619e712ba4ee21c55baea8d93ce656db8a1c30475f0d2cb6bfecfc3b0a84"
},
{
"path": "Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/TallWindTree.fbx",
"bytes": 116140,
"sha256": "378cdb0fc1d67a980d67d51a469148a6d720a03c9e650b35979bcf98ddf6bcad"
}
],
"directFbxVerification": [
{
"asset": "DaisyColony",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 3225,
"triangles": 6210,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"Stem",
"LeafSage",
"PetalCream",
"PollenHoney"
],
"animationAbsent": true
},
{
"asset": "DistantRidge",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 1089,
"triangles": 2174,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"RidgeTeal",
"RidgeLight",
"SoilWarm"
],
"animationAbsent": true
},
{
"asset": "FarHorizon",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 1089,
"triangles": 2174,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"RidgeFar",
"SoilWarm"
],
"animationAbsent": true
},
{
"asset": "FernCluster",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 3906,
"triangles": 7504,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"LeafSage",
"Stem"
],
"animationAbsent": true
},
{
"asset": "GentleHill",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 1089,
"triangles": 2174,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"GroundMint",
"GroundSage",
"SoilWarm"
],
"animationAbsent": true
},
{
"asset": "GrassTuftGold",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 1026,
"triangles": 1976,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"GrassGold",
"GrassMint"
],
"animationAbsent": true
},
{
"asset": "GrassTuftMint",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 1026,
"triangles": 1976,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"GrassGold",
"GrassMint"
],
"animationAbsent": true
},
{
"asset": "LavenderLupineColony",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 4836,
"triangles": 9304,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"Stem",
"LeafSage",
"PetalLavender"
],
"animationAbsent": true
},
{
"asset": "LowLimestone",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 536,
"triangles": 1068,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"StoneCream",
"StoneLight",
"StoneShade"
],
"animationAbsent": true
},
{
"asset": "MeadowClearing",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 1089,
"triangles": 2174,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"GroundMint",
"GroundSage",
"SoilWarm"
],
"animationAbsent": true
},
{
"asset": "MintShrub",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 1524,
"triangles": 2968,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"BarkHoney",
"LeafMint",
"LeafLight",
"LeafShade"
],
"animationAbsent": true
},
{
"asset": "MossStoneSteps",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 3267,
"triangles": 6522,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"StoneCream",
"StoneLight",
"StoneShade"
],
"animationAbsent": true
},
{
"asset": "OpenStoneArch",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 396,
"triangles": 788,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"StoneCream",
"StoneLight",
"StoneShade"
],
"animationAbsent": true
},
{
"asset": "PebbleScatter",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 336,
"triangles": 656,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"StoneCream",
"StoneLight"
],
"animationAbsent": true
},
{
"asset": "PinkCosmosColony",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 2685,
"triangles": 5170,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"Stem",
"LeafSage",
"PetalPink",
"PollenHoney"
],
"animationAbsent": true
},
{
"asset": "RoundCanopyTree",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 2486,
"triangles": 4840,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"BarkHoney",
"LeafMint",
"LeafLight",
"LeafShade",
"BarkLight"
],
"animationAbsent": true
},
{
"asset": "RoundLimestone",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 563,
"triangles": 1122,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"StoneCream",
"StoneLight",
"StoneShade"
],
"animationAbsent": true
},
{
"asset": "SageCanopyTree",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 2486,
"triangles": 4840,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"BarkHoney",
"LeafSage",
"LeafLight",
"LeafShade",
"BarkLight"
],
"animationAbsent": true
},
{
"asset": "SageShrub",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 1524,
"triangles": 2968,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"BarkHoney",
"LeafSage",
"LeafLight",
"LeafShade"
],
"animationAbsent": true
},
{
"asset": "SlateBoulder",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 577,
"triangles": 1150,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"StoneBlue",
"StoneLight",
"StoneShade"
],
"animationAbsent": true
},
{
"asset": "TallWindTree",
"fbxVersion": 7400,
"meshCount": 1,
"vertices": 1936,
"triangles": 3804,
"floorPivot": true,
"localScaleOne": true,
"localOriginZero": true,
"nativeBoundsMatch": true,
"uvPresent": true,
"finiteNormals": true,
"materialSlots": [
"BarkHoney",
"PineMint",
"LeafLight",
"LeafShade"
],
"animationAbsent": true
}
],
"hashes": "All 23 resource byte counts/SHA and all10 actual PNG SHA with recorded source SHA independently matched",
"preservation": "PlaygroundProps.blend plus 13 original FBX byte-identical; diff25 added candidate/tool/doc files only; no manual meta/runtime wiring",
"findings": [
{
"spec": "R01 cyan clear daytime / pastel natural cute cartoon coherent with reference",
"evidence": [
"artifacts/verification/VisualUpgrade/ModelReview/Meadow/Preview_ClearMeadow.png",
"artifacts/verification/VisualUpgrade/ModelReview/Meadow/Preview_FlowerHills.png",
"artifacts/verification/VisualUpgrade/ModelReview/Meadow/Preview_StoneHills.png",
"tools/models/build_meadow.py:376"
],
"observed": "Three actual overviews viewed: cyan sky, rounded foliage/ground/horizon, pastel flowers and warm soft daylight. Hero and natural scenery use a coherent rounded cartoon direction; no copied reference character/weapon/UI geometry."
},
{
"spec": "R04 three landscapes; R05 editable detailed nature and unobscured playable center",
"evidence": [
"tools/models/build_meadow.py:217",
"tools/models/build_meadow.py:249",
"tools/models/build_meadow.py:456",
"artifacts/verification/VisualUpgrade/ModelReview/Meadow/Closeup_BranchesAndLeaves.png",
"artifacts/verification/VisualUpgrade/ModelReview/Meadow/Closeup_PetalsStemsLeaves.png",
"artifacts/verification/VisualUpgrade/ModelReview/Meadow/Closeup_OpenArchAndTerraces.png"
],
"observed": "All10 PNGs independently viewed. Meadow, denser pink/cream/lavender flower hills and rock-heavy hills are distinct. Tree branches/root/canopy/leaves, shrub, individual grass/fern blades, flower petals/stems/leaves, rock planes, rolling ground and open arch are present. Central low-density play area remains readable in candidate composition."
},
{
"spec": "R06 native blend +21FBX floor pivots/unit scales/UV/material slots/hash/license and original preservation",
"evidence": [
"SourceAssets/Models/Meadow/provenance.json",
"SourceAssets/Models/Meadow/README.md",
"tools/models/build_meadow.py:550",
"tools/models/build_meadow.py:582",
"artifacts/verification/VisualUpgrade/ModelReview/Meadow/source-verification.json",
"artifacts/verification/VisualUpgrade/ModelReview/Meadow/verify-only.log",
"artifacts/verification/VisualUpgrade/ModelReview/Meadow/run-evidence.json"
],
"observed": "Source reopened and 21 actual FBX roundtrips are recorded with exit0. Independent FBX byte parsing verified single mesh per file, exact vertex/triangle counts and native bounds, floor minZ0, default scale1/origin0, UV and finite normals, exact named material slots, axis rotation and no animation. All23 source/tool/export records matched SHA/bytes."
},
{
"spec": "R07 no unintentional dark patches/checker faceting/ground seams harming readability/coherence",
"evidence": [
"artifacts/verification/VisualUpgrade/ModelReview/Meadow/Preview_ClearMeadow.png",
"artifacts/verification/VisualUpgrade/ModelReview/Meadow/Preview_FlowerHills.png",
"artifacts/verification/VisualUpgrade/ModelReview/Meadow/Preview_StoneHills.png",
"artifacts/verification/VisualUpgrade/ModelReview/Meadow/Catalog_RockPlanes.png",
"artifacts/verification/VisualUpgrade/ModelReview/Meadow/Closeup_BranchesAndLeaves.png"
],
"observed": "No unintended black patch/checkerboard or open ground gap across assembled center. Broad intentional rock planes and ordinary foliage/ground shadows are not classified as violations."
},
{
"spec": "R10 candidate review output and no adoption before explicit user approval",
"evidence": [
"SourceAssets/Models/Meadow/README.md",
"SourceAssets/Models/Meadow/provenance.json",
"artifacts/verification/VisualUpgrade/ModelReview/Meadow/render-manifest.json"
],
"observed": "Three overview, four catalog and three detail renders, editable source and exact SHA are available. Metadata marks approval/import/gameplay/mobile PENDING or NOT_RUN. No runtime refs or meta added."
}
],
"remaining": [
"Explicit user approval of hero plus all three landscape candidates before any actual-game reference replacement/runtime adoption.",
"30-stage runtime mapping, actual sphere-position preservation, near-prop collider/arch selection, camera/zoom readability and Android performance require later approved integration."
]
}
@@ -1,375 +0,0 @@
{
"task": "T4",
"review": "Independent read-only pinned-commit spec conformance",
"verdict": "specpass",
"status": "PASS",
"reviewedUtc": "2026-10-08T14:49:34.161360+00:00",
"sourceCommit": "43b22d67886bc5715d3543fefda2dc24dead299f",
"baselineCommit": "7c786c5435042f3f4b5e9a4fb370a4350bb2309e",
"worktree": "C:/git/MagnetismHeroes/.dryforge/worktrees/T4",
"blockingFindings": [],
"checks": [
{
"requirements": [
"R10",
"R06"
],
"result": "pass",
"evidence": "All five groups explicitly approved. Review packet SHA verified. All 25 source/FBX SHA values independently match pinned Git blobs. Apply checks approval and hashes before mutation."
},
{
"requirements": [
"R02",
"R03"
],
"result": "pass",
"evidence": "Actual imported body diameter 1, root scale 1, sole SphereCollider radius .5, one equipment bundle, no equipment collider/Animator/rig. Existing six expressions/mask/ribbon/number and count/contact references preserved in raw prefab diffs. Only runtime edit: 3 GroupBadge lines following existing representative body centre and camera."
},
{
"requirements": [
"R03"
],
"result": "pass",
"evidence": "Actual Main test asserts equipment count, position and camera rotation across preview/cancel, merge/split, rotated representative changes and free-play reentry. Existing badge lifecycle deactivates/destroys previous badges; no Core/save/input changes."
},
{
"requirements": [
"R04"
],
"result": "pass",
"evidence": "All 30 nonvisual serialized StageAsset prefixes identical to baseline. New meadow decoration references everywhere; Garden 1-10/Cloud 11-20/Workshop 21-30 retained. Actual Garden free-play and reentry preserve scene theme; existing source-stage environment path unchanged for other themes."
},
{
"requirements": [
"R05"
],
"result": "pass within T4",
"evidence": "Arch/steps use imported nonconvex shared MeshCollider; no blocked BoxCollider arch. Small/distant vegetation has no collider. GameApp raw diff only disables floor renderer and changes camera background; floor physics preserved. No input ray bypass or ball relocation."
},
{
"requirements": [
"R06",
"R08"
],
"result": "pass",
"evidence": "Scoped Editor source uses Unity import/prefab/serialization APIs and excludes initial builders. All 23 FBX meta settings: scale1/fileScale/axisConversion/noAnimation. Imported roots preserved. All 31 material RGB values match portable provenance. All 268 pre-existing meta blobs, 13 legacy FBXs and 289 protected Core/audio/font/face/atlas/mixer/prefab-meta blobs identical to baseline. No runtime UnityEditor, new network/audio/platform/UI feature."
},
{
"requirements": [
"T4 verification"
],
"result": "pass",
"evidence": "Final asset tests 2/2 and actual Main PlayMode 2/2, all failed/skipped/inconclusive 0; raw logs independently report test exit0. Red XML evaluates the same asset tests and fails on missing body/natural decoration refs; green evaluates and passes. Ten final PNGs independently viewed; JSON pinned to clean commit, correct fixture origins and equipment counts."
},
{
"requirements": [
"Source integrity"
],
"result": "pass",
"evidence": "858 current source files independently match pre-final-play exact hashes; git status clean. Pinned Git content comparison:369 raw exact,488 exact CRLF conversions; GroupBadge mixed endings normalize exactly to pinned commit and exact worktree hash matches manifest. Post-final summary records clean same commit/858 hashes matched."
}
],
"tests": [
{
"file": "import-red.xml",
"result": "Failed(Child)",
"total": 2,
"passed": 0,
"failed": 2,
"skipped": 0,
"inconclusive": 0,
"startUtc": "2026-10-08 14:10:25Z",
"endUtc": "2026-10-08 14:10:25Z",
"cases": [
{
"name": "MagnetismHeroes.Tests.VisualUpgradeAssetTests.AllThirtyStagesConnectNaturalThemesWithOpenNearMeshCollidersAndColliderFreeDistantDecor",
"result": "Failed"
},
{
"name": "MagnetismHeroes.Tests.VisualUpgradeAssetTests.ApprovedBodyAndOneEquipmentAssemblyAreConnectedWithoutChangingSelectionSphere",
"result": "Failed"
}
]
},
{
"file": "import-green.xml",
"result": "Passed",
"total": 2,
"passed": 2,
"failed": 0,
"skipped": 0,
"inconclusive": 0,
"startUtc": "2026-10-08 14:16:25Z",
"endUtc": "2026-10-08 14:16:25Z",
"cases": [
{
"name": "MagnetismHeroes.Tests.VisualUpgradeAssetTests.AllThirtyStagesConnectNaturalThemesWithOpenNearMeshCollidersAndColliderFreeDistantDecor",
"result": "Passed"
},
{
"name": "MagnetismHeroes.Tests.VisualUpgradeAssetTests.ApprovedBodyAndOneEquipmentAssemblyAreConnectedWithoutChangingSelectionSphere",
"result": "Passed"
}
]
},
{
"file": "import-final.xml",
"result": "Passed",
"total": 2,
"passed": 2,
"failed": 0,
"skipped": 0,
"inconclusive": 0,
"startUtc": "2026-10-08 14:37:23Z",
"endUtc": "2026-10-08 14:37:23Z",
"cases": [
{
"name": "MagnetismHeroes.Tests.VisualUpgradeAssetTests.AllThirtyStagesConnectNaturalThemesWithOpenNearMeshCollidersAndColliderFreeDistantDecor",
"result": "Passed"
},
{
"name": "MagnetismHeroes.Tests.VisualUpgradeAssetTests.ApprovedBodyAndOneEquipmentAssemblyAreConnectedWithoutChangingSelectionSphere",
"result": "Passed"
}
]
},
{
"file": "play-final.xml",
"result": "Passed",
"total": 2,
"passed": 2,
"failed": 0,
"skipped": 0,
"inconclusive": 0,
"startUtc": "2026-10-08 14:38:35Z",
"endUtc": "2026-10-08 14:38:36Z",
"cases": [
{
"name": "MagnetismHeroes.Tests.VisualUpgradePlayTests.ActualMainShowsThreeThemesAndEquipmentFollowsPreviewCancelMergeSplitAndReentry",
"result": "Passed"
},
{
"name": "MagnetismHeroes.Tests.VisualUpgradePlayTests.TenSingleGroupsAndOneTenBallGroupRenderActualImportedResourceCounts",
"result": "Passed"
}
]
}
],
"visualObservations": [
"Three default natural themes render imported smooth sphere bodies and open helmets/blunt swords/round shields, readable faces/numbers and intact Korean UI; no visible missing-material pink or missing references.",
"Selection highlight visible; merged two-sphere group has one bundle and split/reentry two bundles. Synthetic ten-ball fixtures show10 versus1 bundles retaining10 balls. Attachment overlap/ordinary neighbouring-ball occlusion is not treated as a zero-intersection violation.",
"Rotated close view keeps gear/face alignment and continuous terrain. Extreme below-ground view obscures board; see T5 follow-up."
],
"nonblockingFindings": [
{
"id": "T5-QUALITY-SHADOWS",
"owner": "T5",
"requirements": [
"R01",
"R07"
],
"observation": "Default captures lack visible soft contact shadows; existing configuration unchanged. Full lighting/soft-shadow quality acceptance is not claimed by T4."
},
{
"id": "T5-EXTREME-CAMERA-OCCLUSION",
"owner": "T5",
"requirements": [
"R05",
"R07"
],
"evidence": "final/orbit210-pitch-45-distance15.png",
"observation": "Physical terrain underside obscures board at pitch-45/distance15. One view does not establish a regression. T5 must compare original floor/new ground, physical occlusion and allowed alternate views; this review requests no arbitrary camera restrictions or hit bypass."
}
],
"pendingChecks": [
"T5 full Edit/Play regression and legacy decoration-count/mesh-name expectation updates",
"T5 actual collider selection, arch hole, collection discovery and below-ground original/new A/B",
"T5 complete R07 quality/lifecycle/camera checklist and soft shadows",
"T6 full Final EditMode/PlayMode/AndroidBuild/AndroidSmoke, installed APK/source/audio hashes and adoption documentation"
],
"scope": "T4 application and initial contract checks only. T5/T6 outstanding work is not treated as a T4 blocker; no full-quality/APK acceptance claimed. Reviewer did not run Unity/ADB/Blender or modify products.",
"evidenceHashes": [
{
"path": "artifacts/verification/VisualUpgrade/ModelReview/user-approval.json",
"bytes": 9870,
"sha256": "b539d582aa77d032918bbb580d10d2374f65be4bdd6868432a84ed1d23a46bc5"
},
{
"path": "artifacts/verification/VisualUpgrade/ModelReview/review-packet.json",
"bytes": 35970,
"sha256": "4787e5679d1fe940060a889ffee42020258587ffb19eff32696ea6fc2f48cc99"
},
{
"path": "artifacts/verification/VisualUpgrade/UnityApply/source-import.json",
"bytes": 20062,
"sha256": "4a8788aff69cd30befe5c20d94e63542ee9ad4a102710bfd7c208a8beb14f9a6"
},
{
"path": "artifacts/verification/VisualUpgrade/UnityApply/source-before-final-play.json",
"bytes": 161828,
"sha256": "0318ab6bd9a5864e75ac51f096687a9011941558c06e6c4a69dc822f5e720076"
},
{
"path": "artifacts/verification/VisualUpgrade/UnityApply/source-after-final-play.json",
"bytes": 195,
"sha256": "1359db7d0cf3448f0e35230315ada0bc1c6b927181f8bf90cf4aef4e4a396089"
},
{
"path": "artifacts/verification/VisualUpgrade/UnityApply/t4-result.json",
"bytes": 22622,
"sha256": "cb8fd5cb61666ecbe1ce7616357b8a067f660859fb0db79d9ac7e987893cd88b"
},
{
"path": "artifacts/verification/VisualUpgrade/UnityApply/import-red.xml",
"bytes": 5944,
"sha256": "dea3629d727730b2ead7d949abf05f82312045e199f2a080860870159d3f8930"
},
{
"path": "artifacts/verification/VisualUpgrade/UnityApply/import-red-editor.log",
"bytes": 1634226,
"sha256": "4f0f89e645e81199a0c668b87959647aa753b65e7e06134232e3de3270021fa7"
},
{
"path": "artifacts/verification/VisualUpgrade/UnityApply/import-green.xml",
"bytes": 4316,
"sha256": "0f138bd95ad09aa3b4bd7c75a1cf580676be3494470508a37fc7e1edc9bb4798"
},
{
"path": "artifacts/verification/VisualUpgrade/UnityApply/import-green-editor.log",
"bytes": 50407,
"sha256": "29a3c8b40a16ae0c751e051936515f27983d2a8423bb7f73ad6ac2d02dc8d21c"
},
{
"path": "artifacts/verification/VisualUpgrade/UnityApply/import-final.xml",
"bytes": 4317,
"sha256": "945b57b4c5dc054e55aef3eb412463163d96e9bdb530074600d1e62eb69aac1d"
},
{
"path": "artifacts/verification/VisualUpgrade/UnityApply/import-final-editor.log",
"bytes": 51431,
"sha256": "1b04061bda7e4eea4b5440c009b028cf1c451a22fe16f584533bb1b15b231529"
},
{
"path": "artifacts/verification/VisualUpgrade/UnityApply/play-final.xml",
"bytes": 4398,
"sha256": "2b841c77377b9549d79170aebd8ec536ac74e8f8c55bd985d105f9d3f0236f98"
},
{
"path": "artifacts/verification/VisualUpgrade/UnityApply/play-final-editor.log",
"bytes": 40236,
"sha256": "7af95099cc70249ec0e3765d13e8e679e1650f92bc97d131eaaba818bea09905"
},
{
"path": "artifacts/verification/VisualUpgrade/UnityApply/apply-03-editor.log",
"bytes": 108743,
"sha256": "940ac55c31f44609ca2954582c08eb6c90a962a1f8792fae169854008d984323"
},
{
"path": "artifacts/verification/VisualUpgrade/UnityApply/generated-whitespace-check.log",
"bytes": 56623,
"sha256": "07ff4e1b1a8285808cab6b443557a3a5a8d0c2b379ccfe031df7013cf93cc9ef"
}
],
"captures": [
{
"name": "merged-single-group.json",
"bytes": 906,
"sha256": "4a13899bb20d15a50732b6f91e427782281c67e3406cd9f4b2716d5049ddf11d"
},
{
"name": "merged-single-group.png",
"bytes": 185826,
"sha256": "5196638c35963365696f875d1f86d084c1b2aab6abc022b1fde5fbed0bbda464"
},
{
"name": "orbit210-pitch-45-distance15.json",
"bytes": 1057,
"sha256": "9c2c38a5c2d5fe1f3dba92ba7d009f7366f6468ba73dc0d9f8e8990a368e1888"
},
{
"name": "orbit210-pitch-45-distance15.png",
"bytes": 169446,
"sha256": "438aef5dd8b0f71349ef4fc86c5b2015d53af7b46404de8f49273891866e8ca2"
},
{
"name": "orbit95-pitch45-distance6.json",
"bytes": 1054,
"sha256": "6004a3d38432ac30bf18c95099849d9050a295636a8f8453efbdf2f8d53ec17f"
},
{
"name": "orbit95-pitch45-distance6.png",
"bytes": 285897,
"sha256": "b9125308cd45e80f044e5e8c434e371605c67665d5708e47717848409adbacbb"
},
{
"name": "selected-two-groups.json",
"bytes": 912,
"sha256": "f4ee5e2088668a701cd222e05c2a9de729a4b3c1cc3e765642572abfd6c3cae4"
},
{
"name": "selected-two-groups.png",
"bytes": 188246,
"sha256": "e73cfb13d3a56fd89d44a7156c70c147a377133c52e603fe6b0a29fe73cedb42"
},
{
"name": "split-reentered-two-groups.json",
"bytes": 928,
"sha256": "26a4bd7cdec640e6c19031e3413bbd61c5eb33a58a705df7e88b0383d3af4412"
},
{
"name": "split-reentered-two-groups.png",
"bytes": 193022,
"sha256": "ab8bbad3929d731ede4648324aee860fa20918da133709f4daf34c987869e0e8"
},
{
"name": "stage01.json",
"bytes": 918,
"sha256": "0bc3025d16b0ee0aea1eab4a2ad5384ef0b31c3a53a05cecfbff0850fc461de0"
},
{
"name": "stage01.png",
"bytes": 178823,
"sha256": "73be5d42d42edc4777147c6767d91122f626d6eb7aa4bedd200500fef2728e82"
},
{
"name": "stage11.json",
"bytes": 920,
"sha256": "7822bd5b12e514c33bf06dc682d93f74e008566d5e91565dff7185e05b3534df"
},
{
"name": "stage11.png",
"bytes": 187397,
"sha256": "5800767403c97bbfdcf2f594f1a6da0463327115eb63f9fa406d22a8fc8d8186"
},
{
"name": "stage21.json",
"bytes": 923,
"sha256": "ab795ce5072b4e8b1df554f43824fa3ec6830ea54437885b5e0a856263d6904d"
},
{
"name": "stage21.png",
"bytes": 174475,
"sha256": "03b07031bbb6b477537a3b3b80574348d60f2e8915d1a37560bbf5c29c1a0188"
},
{
"name": "ten-one-group.json",
"bytes": 947,
"sha256": "9330c1913eac3cca1082a478cc67a390345eac13e08d0328acd42d8b550fa5f1"
},
{
"name": "ten-one-group.png",
"bytes": 401560,
"sha256": "ce3383d3d6e3700d41be4783fdabd34b36df2d36a2bee31b22964eb3acef3983"
},
{
"name": "ten-single-groups.json",
"bytes": 953,
"sha256": "83c9c9d9cf1b27d61fb514c129eff79ceeee668509306c0f36841529c0ca50f3"
},
{
"name": "ten-single-groups.png",
"bytes": 493264,
"sha256": "36fc04827b6cab409a668607126e95037c7a2581f6a10f25b2e340236c5a5ba5"
}
]
}
File diff suppressed because it is too large Load Diff
@@ -1,37 +0,0 @@
import hashlib
import json
import re
import subprocess
import sys
from pathlib import Path
root = Path(__file__).resolve().parents[1]
folder = root / 'artifacts/verification/VisualUpgrade'
path = folder / 'stage-nonvisual-baseline.json'
def stage_fields():
result = {}
for stage in range(1, 31):
name = f'Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage{stage:02}.asset'
text = (root / name).read_text(encoding='utf-8')
result[name] = {
field: re.search(r'(?m)^ ' + field + r':.*(?:\n(?: - .*| .*))*', text).group(0)
for field in ('_stageId', '_kind', '_initialGroups', '_targetGroups', '_ballPositions', '_theme')}
result[name]['guid'] = re.search(r'(?m)^guid: (.+)$', (root / (name + '.meta')).read_text()).group(1)
return result
if '--capture' in sys.argv:
assert not path.exists(), 'Baseline already captured'
path.write_text(json.dumps(stage_fields(), indent=2), encoding='utf-8')
print('CAPTURED_NONVISUAL_STAGE_FIELDS_AND_GUIDS count=30')
else:
baseline = json.loads(path.read_text(encoding='utf-8'))
actual = stage_fields()
for name, fields in baseline.items():
assert actual[name] == fields, name
approved = json.loads((folder / 'ModelReview/user-approval.json').read_text(encoding='utf-8'))
for record in approved['approvedFiles']:
assert hashlib.sha256((root / record['repositoryRelativePath']).read_bytes()).hexdigest() == record['sha256']
report = {'status': 'PASS', 'stageNonvisualFieldsAndGuidsUnchanged': 30,
'approvedModelHashesUnchanged': 25,
'sourceCommit': subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=root, text=True).strip()}
(folder / 'stage-and-model-boundary-verification.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report))
@@ -1,59 +0,0 @@
import hashlib
import json
import pathlib
import subprocess
from datetime import datetime, timezone
root = pathlib.Path(__file__).resolve().parents[1]
state_path = root / '.dryforge/execution.json'
state = json.loads(state_path.read_text(encoding='utf-8'))
def git(*args):
return subprocess.check_output(['git', *args], cwd=root, text=True).strip()
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
assert git('branch', '--show-current') == state['feature_branch']
assert git('rev-parse', state['protected_base']) == state['initial_commit']
for name, expected in state['protected_product_hashes'].items():
assert digest(root / name) == expected, name
diff = git('diff', '--name-status', state['initial_commit'] + '...HEAD').splitlines()
assert len(diff) == 32, len(diff)
for line in diff:
status, name = line.split('\t', 1)
assert status == 'A', line
assert name.startswith(('SourceAssets/Models/Heroes/', 'SourceAssets/Models/Meadow/',
'Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Heroes/',
'Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/Meadow/')) or name in (
'tools/models/build_hero.py', 'tools/models/build_meadow.py'), name
checked = 0
for group in ('Heroes', 'Meadow'):
provenance = json.loads((root / f'SourceAssets/Models/{group}/provenance.json').read_text())
for record in provenance['files']:
path = root / record['path']
assert path.stat().st_size == record['bytes'], record['path']
assert digest(path) == record['sha256'], record['path']
checked += 1
subprocess.run(['git', 'diff', '--check', state['initial_commit'] + '...HEAD'], cwd=root, check=True)
assert git('status', '--porcelain') == '', 'Unexpected tracked/untracked change'
evidence = {
'status': 'PASS', 'scope': 'Model candidates only; user approval pending',
'checkedUtc': datetime.now(timezone.utc).isoformat(), 'featureCommit': git('rev-parse', 'HEAD'),
'protectedBaseCommit': state['initial_commit'], 'protectedProductFilesUnchanged': len(state['protected_product_hashes']),
'candidateFilesAdded': len(diff), 'provenanceResourceHashesChecked': checked,
'commands': [
{'command': 'Blender --background --python-exit-code 1 --python tools/models/build_hero.py -- --verify-only',
'exitCode': 0, 'log': 'Heroes/integrated-verify.log',
'assertion': 'ASSERTIONS_OK and REOPEN_AND_FBX_REIMPORT_OK evaluated'},
{'command': 'Blender --background --python-exit-code 1 --python tools/models/build_meadow.py -- --verify-only --review-dir artifacts/verification/VisualUpgrade/ModelReview/Meadow',
'exitCode': 0, 'log': 'Meadow/integrated-verify.log',
'assertion': 'MEADOW_VERIFY_PASS evaluated; 21 assets and 23 resources'},
{'command': 'python .dryforge/verify-model-integration.py', 'exitCode': 0,
'assertion': '533 protected product hashes, 32 additions, candidate provenance hashes, clean tree and protected dev checked'}],
'userApproval': 'PENDING', 'unityGameplayAndroid': 'NOT_RUN; awaits explicit model approval'
}
review = root / 'artifacts/verification/VisualUpgrade/ModelReview'
(review / 'integration-verification.json').write_text(json.dumps(evidence, ensure_ascii=False, indent=2), encoding='utf-8')
state['tasks'].update(T1='completed_model_candidate', T2='completed_model_candidate', T3='awaiting_explicit_user_model_approval')
state['model_integration'] = evidence
state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps({k: evidence[k] for k in ('status', 'featureCommit', 'protectedProductFilesUnchanged', 'candidateFilesAdded', 'provenanceResourceHashesChecked', 'userApproval')}, ensure_ascii=False))
-45
View File
@@ -1,45 +0,0 @@
# 자석 영웅과 맑은 초원 세계 — 실행 인계
## 문서 역할과 위치
| 문서 | 역할 |
|---|---|
| `.dryforge/spec.md` | 제작할 모습·게임 동작·유지 경계·완성 및 검증 조건 |
| `.dryforge/plan.md` | 작업 대상·담당 경계·순서·작업별 검증과 실행 의존 관계 |
| `.dryforge/handoff.md` | 위 문서를 실행할 때 지킬 공통 원칙과 인계 조건 |
결과의 모습과 동작에 충돌이 있으면 spec을 따른다. plan의 방법과 세부 작업 분할은 spec을 만족하는 범위에서 조정할 수 있다. spec의 요구 변경은 사용자와 확인한다. 기존 코드와 제작 도구는 구현 방식의 참고이며 새 외형의 요구를 대신하지 않는다. 프로젝트 지침의 수학·저장·자산 보호 경계는 유지한다.
현재 `.dryforge/status.json`이 존재하는 기존 프로젝트 작업이다. 프로젝트 자체의 새 기획·기반 구축은 하지 않는다. 과거 `.dryforge/001/` 문서와 기존 프로젝트 문서는 이전 결정·검증 이력으로 보존한다. 이번 문서가 지정한 새 기사 외형과 세 자연 풍경은 이전 시각 디자인을 교체한다.
## 실행 개요
편집 가능한 Blender 영웅 원본과 자연 세계 원본을 각각 제작하고, 구 몸체·기사 장비·초원·꽃 언덕·바위 언덕의 완성 모델을 사용자에게 검토받는다. 모든 대상에 대한 명시적 승인 후 한 담당자가 실제 Unity 게임에 연결한다. 실제 장면에서 조작·시각 품질을 확인하고 보정한 뒤, 같은 최종 소스의 Android 앱을 빌드·설치·검증한다. 원본·게임 파일·출처·검수 증거를 함께 납품한다.
사용자가 채택한 방향은 묶음당 기사 한 명, 둥근 구 몸체와 기존 표정·작은 반응, 투구·짧은 칼·방패, 전체 30개 스테이지의 초원·꽃 언덕·바위 언덕 교체, 게임 적용과 Android 시험용 앱 검증까지다. 이 네 방향은 다시 선택받을 필요가 없다.
참고 이미지는 `.dryforge/reference/cartoon-landscape.png`다. 색·형태·소재·빛의 방향을 참고하며 원작 캐릭터나 총을 복제하지 않는다. 영웅 장비뿐 아니라 구 몸체의 메시·재질 자체를 개선해야 한다. 모든 핵심 의도는 spec과 plan에 수록했으며 별도의 숨은 요구는 없다.
## 반드시 지킬 조건
- 모델링 완료 후 실제 원본의 렌더·편집 가능한 파일 링크·검토 대상 해시를 제시하고 사용자 승인을 기다린다. 무응답·일부 승인·자동 검사·제작 문서 승인은 전체 모델 승인이 아니다. 승인 전 기존 게임의 시각 참조 교체·런타임 채택·후속 APK 검증을 진행하지 않는다. 수정 요청에는 변경본을 다시 검토받고 승인 기록을 갱신한다. 적용 중 승인된 외형을 눈에 띄게 바꾸는 경우에도 해당 변경본을 다시 검토받는다.
- 구 하나는 계속 1이고, 기본 크기·ID·총량·소속·아이 배열·수학 판정·기존 초기 위치를 보존한다. 기사 장비는 묶음당 한 벌이며 입력 대상이나 새 구가 아니다.
- 구의 얼굴 여섯 표정·숫자·계수·선택·접촉 표시가 실제 화면에서 보이고 조작 가능해야 한다. 분리·대표 변경·취소·재입장 때 장식이 남거나 중복되지 않는다.
- 30개 스테이지와 자유 놀이의 배경 전체를 같은 맑은 자연 세계로 교체한다. 내부 테마 값 `Garden`·`Cloud`·`Workshop`을 유지하면서 새 외형을 연결한다. 테마는 새로운 정답 조건이 아니다.
- 실제 가림과 아치 빈틈을 유지하고 구 발견·선택을 확인한다. 앞 소품 hit를 무시하는 입력 우회나 구 초기 위치 변경으로 검사를 통과시키지 않는다.
- 기존 진행과 JSON 저장 계약·복구·44개 승인 음성·한국어 UI·글꼴·오프라인을 보존한다. 새 음원·보행·전투·원격 전송·계정·광고·결제·플랫폼·게시를 추가하지 않는다.
- 초기 전체 builder로 완성 씬·프리팹·StageAsset·승인 자산을 다시 만들지 않는다. Unity 자산은 Unity의 편집·생성 경로로 필요한 대상만 변경한다. 모델 축·피벗·imported root 회전과 Unity 생성 `.meta`를 확인한다.
- 제작 원본과 과거 출처 이력을 보존한다. 새 원본·내보내기·런타임·검수의 경로·해시·사용 조건을 연결하고 제작 도구와 원본을 APK에 포함하지 않는다.
- Blender 렌더만으로 품질을 완료 판정하지 않는다. 실제 게임의 영웅·세 풍경, 여러 구 배열·시점·화면 비율에서 시각 검토표와 조작 증거를 남긴다. 비용은 모바일 사용을 고려해 조정하며 Editor·AVD 측정을 실기기 성능 합격으로 기록하지 않는다.
- 최종 제품 소스에서 spec의 네 단계 Final 검증을 실제 수행한다. 기존 증거를 보존하고 소스·제품 파일·APK·설치본 해시와 로그를 같은 실행으로 연결한다. 0개 검사·미평가·필수 사례 건너뜀은 통과가 아니다.
- 수량이나 이어하기 손상, 미승인 채택·요청하지 않은 외부 전송을 발견하면 관련 변경을 멈추고 증거·영향을 즉시 알린다. 다른 문제는 먼저 재현·수정하고 해결할 수 없는 실제 재현 문제만 findings에 남긴다.
## 실행 소유와 완료 인계
영웅·자연 모델은 서로 다른 파일을 소유한다. T3는 사용자의 모델 검토·승인 단계이며 자료 준비만으로 완료 처리하지 않는다. 공통 Unity 프리팹·씬·StageAsset·재질은 승인 후 T4의 한 작성자가 연결한다. 검사·Editor·Blender 저장·ADB·녹음은 지정한 실행 소유자가 관리하며 같은 환경을 동시에 조작하지 않는다. T6이 공통 자산 등록·운영·상태 문서를 최종 갱신한다.
현재 환경의 Unity·Blender·Android 절차와 실기기 후속 경계는 `docs/operations.md`, `docs/tracking/status.md`를 따른다. 현재 Android 검증은 전용 AVD에서 수행하며 실제 사용자 기록을 초기화하지 않는다. 실제 스마트폰·태블릿 FPS·발열·배터리와 아이 사용성, iOS·스토어 배포는 이번 검증 완료 주장에 포함하지 않는다.
최종 인계에는 Blender 원본, 채택된 게임용 파일 목록과 출처, 영웅과 세 풍경의 실제 게임 화면, 시각 검토표·조작/자원 관찰 결과, 네 단계 검사 결과, APK와 설치 해시, 미검증 범위를 포함한다.
이 문서 작성 단계의 산출물은 제작 명세와 계획이다. 제품 자산 생성·교체·빌드·Git 커밋은 아직 수행하지 않는다. 세 문서 검토·승인 후 같은 대화에서 `dryforge:go`를 호출하여 실행한다.
-27
View File
@@ -1,27 +0,0 @@
{
"status": "merged-local-dev",
"userStatement": "-dev에 병합",
"startedUtc": "2026-10-09T14:27:19.2336807Z",
"finishedUtc": "2026-10-09T14:27:20.7186100Z",
"targetBranch": "dev",
"sourceBranch": "dryforge/hero-meadow",
"approvedFeatureCommit": "0d992d9dcc94843a72489b454ca4e8137178bc56",
"previousDevCommit": "58410281e015c6600dc543c4e093fd95c036288c",
"fetchedOriginDevCommit": "58410281e015c6600dc543c4e093fd95c036288c",
"mergeCommit": "a54ae6fb230b0c87e1a2f08a1ad9e2cc2ceb0d73",
"mergeParents": [
"58410281e015c6600dc543c4e093fd95c036288c",
"0d992d9dcc94843a72489b454ca4e8137178bc56"
],
"mergeCommand": "git -c core.autocrlf=false merge --no-ff dryforge/hero-meadow -F artifacts/verification/VisualUpgrade/FinalProduct/dev-merge-message.txt",
"mergeExitCode": 0,
"approvedTreeEqualsMergedTree": true,
"testedProductFilesRawSha256Unchanged": 732,
"verification": "제품 파일과 전체 Git tree가 승인본과 동일하므로 기존 실제 검증 증거 유지; 새 검사를 실행한 것으로 기록하지 않음",
"testedProductSource": "3346eb559bdfba24332feb4d9be4b7104520233e",
"completionEvidence": "artifacts/verification/VisualUpgrade/FinalProduct/completion.json",
"currentBranch": "dev",
"workingTreeClean": true,
"mergedFeatureBranchRemoved": true,
"remotePushExecuted": false
}
-138
View File
@@ -1,138 +0,0 @@
# 자석 영웅과 자연 배경 제작 계획
## 진행 방식
영웅과 자연 소품의 원본을 각각 완성한 뒤, 사용자에게 구 몸체·기사 장비와 세 풍경을 검토받는다. 명시적인 승인 후 한 담당자가 실제 게임에 연결한다. 연결된 장면에서 조작과 외형을 함께 확인하고 필요한 수정 후 최종 Android 앱을 검증한다. 모델링용 원본과 런타임 자산, 검수 자료를 구분하여 보존한다.
새 프로젝트 초기화나 패키지 업그레이드는 필요 없다. 시작 전에 대상 모듈의 AGENTS.md와 `docs/standards.md`, `docs/engineering-notes.md`, `docs/architecture.md`, `docs/operations.md`를 읽고 기존 소스·GUID·자산 해시와 검증 증거를 확인한다. 초기 전체 builder는 실행하지 않는다.
## T1 — Blender 영웅 원본과 게임용 모델
**목표와 계약:** R01·R02·R03·R06·R07에 따라 구 몸체 자체의 메시·재질과 투구·짧은 칼·방패를 제작한다. 편집 가능한 실제 `.blend`와 Unity용 FBX, 필요한 텍스처를 제공한다. 구 하나의 크기와 중심을 유지하고 장비는 별도 수량이나 입력 대상이 없는 묶음별 부착물로 만든다. 현재 얼굴 여섯 표정·마스크·리본 망토와 공존할 공간을 확보한다.
**작업 대상:** `SourceAssets/Models/Heroes/`, `tools/models/`의 영웅 전용 제작·내보내기 도구, `Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/`의 영웅용 파일과 필요한 `Textures/` 파일. 해당 원본 영역에 별도 provenance를 두어 기존 `SourceAssets/Models/provenance.json`과 과거 자산 이력을 덮지 않는다.
**제작 판단의 근거:** 구 배열이 영웅의 몸이라는 기존 학습 표현을 유지하면서 기사 장비로 정체성을 강화한다. 모든 구를 개별 무장 캐릭터로 바꾸거나 팔·다리와 보행 애니메이션을 새로 만드는 방식은 채택된 모습과 다르다. 반사와 장식의 디테일은 작은 화면에서 먼저 읽히는 큰 형태를 위해 조정한다.
**검증:** Blender에서 원본을 실제로 열어 메시·재질·피벗·장비 구성을 확인하고 정면·옆면·뒷면 렌더와 내보내기 해시를 남긴다. FBX 내보내기 성공과 실제 존재·크기·메시 정보를 확인한다. T3의 사용자 검토에 제출하고 최종 Unity 적용 검수는 T4·T5에서 수행하며 원본 렌더만으로 게임 품질 합격을 주장하지 않는다.
**공유 파일 처리:** 프리팹·씬·StageAsset·공통 재질·자산 등록 문서는 수정하지 않는다. 영웅 원본·내보내기 범위만 소유한다. Blender 파일을 저장하고 내보내는 실행은 실행 담당자가 관리한다.
## T2 — Blender 자연 세계 원본과 게임용 소품
**목표와 계약:** R01·R04·R05·R06·R07에 따라 초원·꽃 언덕·바위 언덕에 필요한 지면·언덕·원경, 나무·수풀·풀·꽃·바위·자갈을 제작한다. 가까운 구조물의 가림·빈틈과 기존 구 초기 위치를 고려하여 기존 아치·단차의 자연 풍경용 대체 외형도 제공한다. 소품 변형과 배치 밀도는 실제 비용과 반복감을 보며 조정한다.
**작업 대상:** `SourceAssets/Models/Meadow/`, `tools/models/`의 자연 세계 전용 제작·내보내기 도구, `Assets/MagnetismHeroes/UnityRuntime/Presentation/Models/`의 자연 세계용 파일과 필요한 `Textures/` 파일, 별도 원본 provenance. 기존 `PlaygroundProps.blend`와 13개 FBX 원본 이력을 보존한다.
**제작 판단의 근거:** 요청한 밝은 언덕·평야를 전체 게임의 새 배경으로 만든다. 세 풍경은 서로 다른 세계가 아니라 식생·지형의 비중이 다른 같은 카툰 세계다. 수량을 헷갈리는 구형 캐릭터 소품이나 새 놀이 장치를 추가하지 않는다.
**검증:** 원본 파일의 편집 가능성과 메시·UV·노멀·다중 재질, 실제 바닥 피벗·축·내보내기 파일을 확인한다. 소품 목록 렌더와 세 풍경 구성 미리보기를 남긴다. 구멍을 가진 구조물의 메시가 실제 빈틈을 유지하는지 확인한다. T3의 사용자 검토에 제출하고 실제 구 발견·가림·입력은 T5에서 확인한다.
**공유 파일 처리:** T1과 다른 원본·텍스처·내보내기 파일명을 사용한다. 영웅 파일·프리팹·씬·StageAsset·공통 재질·자산 등록 문서는 수정하지 않는다. 원본 저장·내보내기 실행의 소유자는 한 명으로 유지한다.
## T3 — 완성 모델 제시와 사용자 검토·승인
**목표와 계약:** R10에 따라 T1·T2의 모델링 결과가 모두 준비되면 구 몸체·기사 장비·초원·꽃 언덕·바위 언덕을 사용자에게 제시하고 명시적인 승인을 받는다. 이 단계는 자료 생성만으로 끝나지 않으며 사용자 승인까지 완료되어야 T4를 시작할 수 있다.
**작업 대상:** `artifacts/verification/VisualUpgrade/ModelReview/`의 실제 Blender 렌더·검토 목록·파일 해시·승인 기록과 사용자 검토 대화. 구 몸체 단독 모습과 장비를 갖춘 영웅의 정면·옆면·뒷면, 세 풍경의 전체 장면과 주요 소품 상세 모습, 편집 가능한 원본 링크를 한 묶음으로 제공한다. 승인 기록은 검토 대상 원본·게임용 파일의 경로·SHA-256, 해당 자료에 대한 사용자 답변과 확인 시각을 연결한다.
**검토와 수정:** 사용자에게 귀여운 기사 모습·구 몸체·장비·세 풍경의 형태·색·재질·전체 조화를 검토받는다. 수정 요청이 있으면 대상만 보정하여 변경본과 갱신된 해시를 다시 제시한다. 이전 버전의 승인을 변경본에 재사용하지 않는다. 일부 승인이나 무응답 상태에서는 이 단계를 미완료로 유지하고 모든 대상의 승인까지 게임 적용을 기다린다.
**검증:** 구 몸체·기사 장비와 세 풍경 모두에 대해 최신 파일과 대응하는 명시적 사용자 승인이 있어야 완료다. 에이전트의 자체 평가·자동 검사·문서 승인으로 대체하지 않는다. 승인된 파일 목록을 T4에 인계한다. Blender 모델 검토와 이후 실제 게임의 시각·조작 검증은 각각 수행한다.
**공유 파일 처리:** 검토 대기 중 후보 원본·내보내기·검토 자료만 관리하고 기존 Main 씬·게임 프리팹·StageAsset 참조와 런타임 채택 상태를 유지한다. 후속 APK 검증은 시작하지 않는다. 이 승인 대기는 사용자가 요청한 모델 검토 단계이며 자동 진행으로 우회하지 않는다.
## T4 — Unity 장면과 30개 스테이지 적용
**목표와 계약:** R01~R06·R08·R10에 따라 T3에서 승인된 T1·T2의 자산을 실제 게임에 적용한다. 적용할 파일의 해시가 승인 기록과 일치하는지 확인한다. 대표 구에 영웅 장비 한 벌이 붙고 모든 구의 몸체가 개선된 모델·재질로 표시된다. 세 풍경이 1~10·11~20·21~30과 해당 자유 놀이에 연결된다. 장비 표시와 배경 연결에 필요한 표현 코드만 변경한다.
**작업 대상:** `Assets/MagnetismHeroes/Editor/`의 대상 한정 import·적용 경로, `UnityRuntime/Presentation/`의 재질·필요 텍스처·모델 import 설정·프리팹, `GroupBadge.cs`, `BallView.cs`, `BoardPresenter.cs`, 필요 시 `BoardEnvironment.cs`, `Bootstrap/GameApp.cs`, `StageAssets/Stage01.asset`~`Stage30.asset`, `Assets/MagnetismHeroes/Scenes/Main.unity`의 관련 표현 연결. 실제 변경이 필요한 파일만 수정한다.
**보존 계약:** 구 루트 크기·SphereCollider 반지름 0.5·ID·초기 위치·수학 목표를 유지한다. 영웅 장식에는 Collider를 붙이지 않는다. 묶음 얼굴·숫자·장비의 대표 위치와 수명을 맞추고 취소·합체·분리·재입장을 확정 상태에서 구성한다. 내부 테마 값 `Garden`·`Cloud`·`Workshop`과 기존 Stage·GameApp GUID를 유지한다. 승인 얼굴 여섯 표정·44개 음성·글꼴·UI·Mixer 참조를 보존한다.
**배경 계약:** 먼 지형·하늘과 가까운 식생까지 새 풍경으로 연결한다. 앞 소품의 실제 가림을 보존하되 구 발견과 입력을 막지 않도록 배치·Collider를 조정한다. 배경 가림을 무시하는 Raycast 우회로를 만들지 않는다. 아치 빈틈을 유지하고 저장된 구 배열을 새 배경에 맞추려고 재배치하지 않는다.
**검증:** Unity 실제 import와 초기화에서 누락 참조·셰이더·재질·좌표·피벗·축·GUID·콜라이더를 확인한다. 적용 전후 StageAsset의 비시각 필드와 음성·얼굴·글꼴 파일 해시를 대조한다. Main 씬에서 단일 구와 합체 묶음, 세 풍경과 자유 놀이를 실제 표시하고 초기 캡처를 남긴다. 변환 중 입력 간섭이나 구 수량 문제가 나오면 관련 변경을 멈추고 증거를 남긴다.
**공유 파일 처리:** 씬·프리팹·StageAsset·공통 재질·메타데이터는 이 단계의 한 작성자가 Unity API로 생성·수정·저장한다. `ProjectBootstrap.Prepare/ConnectApp`, `CompositionAssets.Build`, `PersistenceAssets.Build`, `ContentAssets.Build`, `PresentationAssets.Build`를 일반 준비 절차로 실행하지 않는다. 원본과 파일 생성은 T1·T2 결과를 사용한다.
## T5 — 시각 품질과 조작 회귀 검증·수정
**목표와 계약:** R03·R04·R05·R07·R08·R10에 따라 실제 연결된 프리팹과 장면에서 귀여운 기사와 세 풍경의 품질을 확인한다. 장식과 배경이 숫자·계수·선택·발견·분리·취소·저장 복원을 해치지 않아야 한다. 관찰한 결함은 최소 범위로 수정하고 영향을 받은 사례를 재검증한다. 승인된 형태·색·재질·풍경 구성을 눈에 띄게 바꾸는 수정은 해당 변경본을 사용자에게 다시 제시하고 승인 후 채택한다.
**작업 대상:** `Assets/MagnetismHeroes/Tests/EditMode/ContentAssetTests.cs`, `PresentationAssetTests.cs`, 필요 시 다른 영향받은 자산 검사, `Tests/PlayMode/ContentPlayTests.cs`, `CompositionPlayTests.cs`와 필요한 실제 프리팹 검사, `Editor/PresentationProof.cs` 또는 대상 한정 시각 증거 수집, `tools/android/`의 영향받은 배경·장비 검증, T1·T2·T4의 필요한 보정 파일, `artifacts/verification/VisualUpgrade/`.
**검사 갱신:** 기존 `Decorations.Count == 7`이나 옛 소품 이름·픽셀을 고정한 기대를 새 제작 목록에 맞춘다. 단순 존재 확인으로 낮추지 말고 각 풍경의 필수 자산 연결과 실제 구 발견·숫자 가독성을 계속 검사한다. 내부 테마 값이 유지되므로 이름 변경을 위한 제품 기능 수정은 하지 않는다. 기존 합체·분리·저장 검사를 없애거나 제품 판정을 완화하지 않는다.
**구체 사례:** 단일 구, 2+3 합체, 5의 떨어진 양 끝 두 구를 골라 2+3 분리, 10개 한 묶음과 10개 개별 구, 대표 변경, 취소·핀치·영역 이탈, 화면 이동·재입장·이어하기, 세 풍경의 0~10개 자유 놀이 공급·반납. 각 안정 묶음의 장비·얼굴·숫자가 하나인지와 모든 수집 구의 실제 Collider 선택 가능성을 확인한다.
**시각 자료:** 각 풍경의 대표 스테이지와 자유 놀이를 기본·회전·확대 시점, 스마트폰·태블릿 가로 비율에서 캡처한다. 검토표에는 실루엣·법선·UV·재질·관통·얼굴/숫자/계수 가독성·배경 경계·색과 빛의 조화에 대한 실제 관찰과 수정 결과를 적는다. 그림이 예쁜 것만으로 조작 가능성을 대신하지 않는다.
**모바일 비용:** 최대 구 상태의 메시 삼각형·텍스처 해상도/메모리·공유 재질·측정 가능한 렌더링 비용을 기록하고 과한 부분을 줄인다. 측정 도구와 환경, 측정 못한 항목을 명시한다. Editor 통계와 AVD를 실기기 성능 증거로 바꾸지 않는다.
**검증:** 영향받은 EditMode·PlayMode 사례가 실제로 평가되고 통과해야 한다. 색상이나 단순 재질 조정마다 구현을 복제하는 새 단위 검사를 작성하지 않는다. 시각 검토표와 원본/게임 화면 대조를 완료하고 발견한 수량·복원 결함이 없음을 확인한다. 최종 소스의 전체 네 단계와 APK는 T6에서 확정한다.
**공유 파일 처리:** Unity Editor·테스트 실행·캡처·ADB는 한 실행 담당자가 순서대로 관리한다. 보정으로 T1·T2·T4 파일이 바뀌면 원본·내보내기·Unity 연결·해시를 함께 갱신하고 R10의 재검토 경계를 따른다. 최종 APK 검증을 먼저 하고 제품 파일을 나중에 수정하지 않는다.
## T6 — 최종 Android 검증과 채택 이력·납품
**목표와 계약:** R06·R07·R08·R09에 따라 완성된 제품 소스의 네 단계 검증과 새 Android 설치본·시각 증거를 연결한다. 실제 제작 결과와 사용 조건을 문서에 반영하고 원본·게임 화면·시험용 APK를 납품한다.
**작업 대상:** 기존 `tools/verify.ps1`와 관련 Android 검증 경로의 실행, `artifacts/verification/Final/`, `artifacts/verification/VisualUpgrade/`, `artifacts/android/Final/MagnetismHeroes.apk`, `artifacts/android/MagnetismHeroes.apk`, `docs/asset-register.md`, `docs/tracking/decisions/0005-hybrid-assets.md`와 필요 시 관련 결정 기록, `docs/operations.md`, `docs/tracking/status.md`, 실제 재현 문제가 남을 때 `docs/tracking/findings.md`, 새 자산 provenance의 최종 검수 대응.
**실행:** 기존 Final 증거를 별도 사본과 해시로 보존한다. 최종 제품 변경이 반영되고 tracked 변경이 없는 검증 소스에서 아래 명령을 순서대로 실행한다. 소스 준비·커밋 등 Git 처리는 실행 단계의 절차를 따른다. 현재 문서 작성 단계에서는 커밋하지 않는다.
```powershell
pwsh -NoProfile -File tools/verify.ps1 -Phase EditMode -Milestone Final
pwsh -NoProfile -File tools/verify.ps1 -Phase PlayMode -Milestone Final
pwsh -NoProfile -File tools/verify.ps1 -Phase AndroidBuild -Milestone Final
pwsh -NoProfile -File tools/verify.ps1 -Phase AndroidSmoke -Milestone Final
```
Unity Editor와 배치 검사가 같은 프로젝트를 동시에 사용하지 않게 한다. 전용 API37 AVD의 상태와 boot 완료를 확인하고, 실제 이용자 데이터를 초기화하지 않는다. 정상 30개 순차 진행은 완료 기록 주입 없이 검증한다. 각 단계 실제 종료 코드와 XML/JSON 평가 결과, 소스·APK·설치 해시, 실행 시각, 로그와 Android 화면을 보존한다. 승인 44개 음성과 필요한 고지의 실제 APK 포함을 기존 절차로 확인한다.
**검증:** 네 단계 종료 0과 필수 사례 실패·건너뜀·미확정 0, 실제 APK 설치·진행·복원·오프라인 재생, 새 세 풍경의 Android 화면, 새 자산의 원본→게임 파일→검수 해시 대응을 확인한다. 새로운 제품 수정이 생기면 영향을 받은 검사와 최종 빌드를 갱신한다. 문서만 변경했을 때 제품 파일 동일성을 확인한다. 원본·캡처·검토표·APK 링크와 실기기 미검증 범위를 최종 보고한다. 실행 불가나 해결 불가를 완료로 표현하지 않는다.
**공유 파일 처리:** 이 단계가 공통 자산 등록·운영·상태 문서의 최종 작성자다. 이전 단계는 해당 문서 대신 자기 자산의 provenance와 검수 자료를 남긴다. 원격 push·스토어 게시·유료 구매는 실행하지 않는다.
## 요구사항과 작업의 대응
| 요구사항 | 구현·확인 작업 |
|---|---|
| R01 미술 방향 | T1, T2, T4, T5 |
| R02 구 몸체와 기사 | T1, T4, T5 |
| R03 합체·분리와 표현 수명 | T1, T4, T5, T6 |
| R04 세 풍경과 30개 스테이지 | T2, T4, T5, T6 |
| R05 자연 소품과 조작 | T2, T4, T5, T6 |
| R06 원본·게임 파일·출처 계약 | T1, T2, T4, T6 |
| R07 시각 품질 합격 | T1, T2, T5, T6 |
| R08 규칙·저장·음성·필수 검증 | T4, T5, T6 |
| R09 변경 경계와 완료 보고 | T6 |
| R10 완성 모델 사용자 검토·승인 | T3, T4, T5 |
## Execution Graph
T1·T2는 서로 다른 원본과 내보내기 파일을 제작하므로 독립이다. 파일 준비를 병행하더라도 Blender 저장·내보내기 실행은 한 소유자가 관리한다. T3는 두 제작 결과를 사용자에게 제시하며 모든 대상의 명시적 승인 전에는 완료되지 않는다. 실행 담당자는 승인 대기를 자동 검사나 작업 완료 보고로 대체하지 않고 T4~T6을 시작하지 않는다. T4는 승인된 자산을 실제 게임에 연결하고, T5는 연결된 게임을 검증·보정하며, T6은 보정된 최종 제품 소스와 증거를 확정한다. 전체 재생성 명령은 없고 각 제작의 내보내기·import는 해당 작업 안에서 처리한다.
```yaml
tasks:
- id: T1
depends: []
risk: RISKY
- id: T2
depends: []
risk: RISKY
- id: T3
depends: [T1, T2]
risk: RISKY
- id: T4
depends: [T3]
risk: RISKY
- id: T5
depends: [T4]
risk: RISKY
- id: T6
depends: [T5]
risk: RISKY
regen_barriers: []
```
-126
View File
@@ -1,126 +0,0 @@
# 자석 영웅과 맑은 초원 세계의 시각 품질 개선
## 목표와 완성 범위
5~7세용 MagnetismHeroes의 구 자석과 배경을 하나의 완성된 카툰 세계로 만든다. 기본 도형으로 만든 시제품 수준에서 벗어나, 부드러운 곡면·정돈된 재질·일관된 색과 빛·의도적인 배경 구성을 갖춘 상업용 게임에 어울리는 외형을 목표로 한다.
Blender에서 구 몸체와 기사 장비, 지형과 자연 소품의 실제 3D 원본을 제작한다. 게임용 모델과 재질을 Unity에 적용하고 기존 30개 스테이지와 자유 놀이에서 사용한다. 최종 결과는 편집 가능한 원본, 게임에 연결된 자산, 시각 검수 자료와 검증된 Android 시험용 APK다. 이 문서는 제작할 결과를 정하며, 작성 자체가 모델 제작이나 앱 검증 완료를 뜻하지 않는다.
## R01 — 미술 방향
참고 이미지는 `.dryforge/reference/cartoon-landscape.png`에 보존한다. 밝은 청록색 하늘, 부드러운 녹색과 파스텔색 식생, 둥근 바위와 나무, 따뜻한 햇빛, 부드러운 그림자, 귀여운 장난감 같은 재질을 기준으로 삼는다. 참고 이미지의 캐릭터·총·화면 구성을 복제하는 요청은 아니다. 현재 게임의 회전·확대 가능한 퍼즐 시점을 유지한다.
영웅이 먼저 보이고, 가까운 자연 소품과 먼 언덕이 그 뒤를 받치도록 구성한다. 녹색 평야·꽃·바위가 같은 세계에 속한다는 인상을 준다. 구 자석과 헷갈리는 얼굴 달린 식물이나 빛나는 구형 배경 장식을 만들지 않는다.
색조·재질의 거칠기·장식 크기·광원 강도·원경의 거리와 식생 밀도는 실제 화면을 보며 조정한다. 이 값은 조정 가능하지만, 기사 형태·맑은 낮·세 자연 풍경·구 수량의 명확성은 고정한다.
## R02 — 구 몸체와 장난감 기사
구 몸체 자체를 Blender 제작 메시와 정돈된 재질로 개선한다. 투구·칼·방패를 추가하는 것만으로 몸체 개선을 대체하지 않는다. 낮은 분할로 생기는 각진 외곽, 얼룩진 법선, 눈에 띄는 이음새와 과한 반사를 없애고, 작게 표시되어도 구 하나를 구분할 수 있게 한다.
친구 한 묶음마다 영웅 한 명을 표시한다. 대표 구에 현재 얼굴을 살린 둥근 투구, 짧고 끝이 둥근 장난감 칼, 둥근 방패를 붙인다. 구가 영웅의 몸이고 장비는 부속 장식이다. 기본 구의 시각 반지름과 기존 SphereCollider 반지름 0.5, 루트 배율 1을 유지한다.
기존 얼굴의 여섯 표정과 얇은 마스크, 리본형 자력 망토, 연결 빛을 기사 장비와 조화시킨다. 투구가 눈·입을 가리지 않고, 칼·방패가 숫자·계수 표시·선택 강조를 가리지 않아야 한다. 새 팔·다리, 걷기용 리깅과 전투 동작은 이번 범위에 없다. 기존 표정 전환·시선·작은 흔들림·축하 반응을 활용한다.
칼과 방패를 가진 실루엣은 정면과 실제 조작 시점에서 알아볼 수 있어야 한다. 장비의 돌출은 이웃 구를 세거나 선택하는 데 방해가 되지 않도록 조절한다. 장비에는 별도 구 ID, 입력 Collider, Raycast 대상이나 수량을 추가하지 않는다.
## R03 — 합체·분리와 표현의 수명
구 하나는 계속 수량 1이다. 모든 구 ID와 총량, 소속, 기본 크기, 아이가 만든 배열을 유지한다. 합체가 여러 구를 큰 구 하나나 정해진 인체로 대체해서는 안 된다. 묶음의 숫자는 확정 소속 구 개수와 일치한다.
안정 상태에서 묶음마다 얼굴·기사 장비 한 벌·숫자·망토가 각각 하나다. 합체하면 새 묶음에 한 벌만 남고, 분리하면 두 묶음에 각각 한 벌이 생긴다. 떨어져 있는 선택 구도 기존 규칙대로 두 친구로 나눈다. 대표 구가 바뀌면 장비와 얼굴이 함께 옮겨지고 지난 대표의 장식이 남지 않는다.
드래그 중 장식은 표시 중인 묶음을 따라간다. 취소·핀치·영역 이탈에서는 기존 확정 상태로 복원한다. 화면 전환·재입장·저장 복원에서는 확정 상태로 영웅을 재구성하며 지난 선택·축하·미리보기 상태를 이어 붙이지 않는다. 미리보기나 흔들림을 진행 데이터로 저장하지 않는다.
## R04 — 세 자연 풍경과 30개 스테이지
기존 세 배경의 외형을 모두 교체한다. 스테이지 수와 순서, 종류, 초기 묶음·구 위치, 목표·해금·도움·시범·완료 규칙은 유지한다.
| 스테이지 | 새 풍경 | 중심 구성 | 유지하는 내부 테마 값 |
|---|---|---|---|
| 1~10 | 맑은 초원 | 열린 잔디 평야, 둥근 나무와 수풀, 작은 꽃과 먼 언덕 | `Garden` |
| 11~20 | 꽃 언덕 | 완만한 언덕, 꽃 군락과 나무, 부드러운 색의 식생 | `Cloud` |
| 21~30 | 바위 언덕 | 둥근 바위와 낮은 암벽, 잔디·수풀, 먼 언덕 | `Workshop` |
내부 테마 값은 기존 연결 호환성을 위한 식별자다. 화면에서는 새 자연 풍경이 보이며 기존 구름 놀이터·별 작업실 소품은 새 배경으로 대체한다. 새 테마 식별자나 저장 형식은 추가하지 않는다.
놀이터 바닥만 바꾸지 않고, 카메라 회전·확대 범위에서 보이는 주변 지형·나무·하늘·원경까지 이어지는 완성된 장면을 구성한다. 커다란 탐험 월드나 이동 가능한 캐릭터 월드를 추가하지 않는다. 성공 후 자유 놀이는 해당 스테이지의 새 풍경을 유지하고 재입장에도 같은 풍경을 복원한다.
## R05 — 배경 소품과 조작 가능성
제작 묶음에는 플레이 지면과 완만한 언덕, 원경 지형, 둥근 나무, 수풀, 풀 군락, 꽃 군락, 바위·자갈이 포함된다. 나무의 줄기·가지·수관, 꽃의 꽃잎·줄기·잎, 바위의 큰 덩어리와 면 변화가 각 재질과 함께 읽히도록 제작한다. 모든 소품을 같은 재질로 덮거나 기본 도형을 단순 나열한 결과로 완료하지 않는다. 변형 수·세부 식생 수·배치 밀도는 반복감과 모바일 비용을 보며 조정한다.
기존 아치·단차 등 발견과 가림에 관계된 구성은 새 풍경에 어울리는 외형으로 교체하고 실제 발견 가능성을 유지한다. 기존 구 초기 위치를 미술 배치에 맞추려고 변경하지 않는다. 배경이 구의 이동과 접촉 후보 판정에 새 장애를 만들지 않게 한다.
장식용 먼 지형과 작은 식생은 불필요한 입력 Collider를 만들지 않는다. 실제 가림을 맡는 가까운 소품은 보이는 형상에 맞는 Collider를 사용한다. 아치 구멍은 막힌 BoxCollider로 대체하지 않으며, 앞 소품의 hit를 무시해 뒤 구를 선택하는 방식으로 문제를 숨기지 않는다. 각 수집 구는 지원 카메라 조작으로 찾아 실제 구 Collider를 선택하고 끌어올 수 있어야 한다.
## R06 — 원본·게임 자산·출처 계약
| 산출물 | 위치와 계약 |
|---|---|
| 영웅 원본 | `SourceAssets/Models/Heroes/`: 구 몸체와 투구·칼·방패를 편집할 수 있는 `.blend`, 필요한 제작 텍스처·출처 기록 |
| 자연 세계 원본 | `SourceAssets/Models/Meadow/`: 지형과 소품을 편집할 수 있는 `.blend`, 필요한 제작 텍스처·출처 기록 |
| 게임용 파일 | `Assets/MagnetismHeroes/UnityRuntime/Presentation/` 아래 모델·재질·필요한 텍스처·프리팹. 게임은 이 파일만 참조하고 제작 원본은 참조하지 않음 |
| 연결 결과 | 기존 `MagnetFriend.prefab`, `GroupBadge.prefab`, `GameApp.prefab`, Main 씬과 30개 StageAsset의 관련 시각 참조. 기존 GUID와 학습 데이터 유지 |
| 제작 도구 | `tools/models/` 아래 대상이 분리된 재현용 Blender 제작·내보내기 경로. 기존 원본과 13개 FBX를 통째로 덮는 초기 builder를 준비 작업으로 실행하지 않음 |
| 검수 자료 | `artifacts/verification/VisualUpgrade/`: 원본 렌더, Unity 실제 화면, Android 실제 화면, 조작·자원 관찰·해시 대응 자료 |
| 최종 APK | 기존 검증 절차의 `artifacts/android/Final/MagnetismHeroes.apk` 및 `artifacts/android/MagnetismHeroes.apk`, 같은 실행의 설치본 해시와 연결 |
모델은 자체 제작한다. 각 채택 파일은 원본→내보낸 게임 파일→Unity 연결→검수 증거를 추적할 수 있어야 한다. 기존 provenance 형식을 참고하여 도구와 실제 버전, 생성·내보내기 설정, 저장소 상대 경로, 바이트 수와 SHA-256, 출처·사용 조건·검수 범위를 남긴다. 필요 텍스처도 같은 대응을 갖는다. 구매나 외부 자산 채택을 임의로 추가하지 않는다.
Blender는 미터·Z-up, FBX는 -Z forward/Y-up·적용된 scale·애니메이션 없음을 따른다. 자연 소품은 바닥 피벗, 구는 중심 피벗, 장비는 대표 구에 부착 가능한 기준점을 갖는다. Unity import scale 1과 축 변환, imported root 회전을 실제 장면에서 확인한다. 재질·UV·노멀 등 Blender에서 보이는 속성이 Unity에서도 제대로 연결되어야 한다. Blender 전용 재질 노드가 FBX에 그대로 옮겨진다고 가정하지 않는다.
씬·프리팹·StageAsset·메타데이터는 Unity 편집·생성 경로로 변경한다. 변경 대상만 처리하고 Main 씬·GameApp·음성·글꼴·UI를 초기 builder로 다시 작성하지 않는다. 기존 원본과 출처 이력은 보존하고 새 제작본은 별도 경로에 둔다.
## R10 — 모델링 완료 후 사용자 검토와 승인
구 몸체·투구·칼·방패와 초원·꽃 언덕·바위 언덕의 모델링·재질 제작을 모두 마친 뒤, 실제 게임에 적용하기 전에 사용자에게 완성 모델을 검토받는다. 제작 명세 승인이나 자동 검사 통과는 완성 모델에 대한 사용자 승인을 대신하지 않는다.
검토 자료는 실제 제작한 Blender 원본에서 렌더한 구 몸체 단독 모습, 장비를 갖춘 영웅의 정면·옆면·뒷면, 세 풍경 각각의 전체 장면과 주요 자연 소품의 상세 모습을 포함한다. 편집 가능한 `.blend` 링크와 검토 대상 원본·게임용 내보내기 파일의 목록·SHA-256도 함께 제공한다. 참고 이미지나 아직 만들지 않은 모델의 예상 이미지로 검토 자료를 대신하지 않는다. 자료는 `artifacts/verification/VisualUpgrade/ModelReview/`에 보존한다.
검토 자료를 제시한 뒤 사용자의 명시적인 승인을 기다린다. 모든 검토 대상의 승인이 확인되기 전에는 Main 씬·게임 프리팹·StageAsset의 기존 시각 참조 교체, 새 자산의 런타임 채택과 후속 APK 검증을 진행하지 않는다. 후보 원본과 게임용 파일의 제작·내보내기는 가능하며, 승인 대기 중 기존 게임 연결은 보존한다. 답변이 없거나 일부 대상만 승인된 상태를 전체 승인으로 취급하지 않는다.
수정 요청이 있으면 해당 모델·재질·풍경 구성을 수정하고 변경본을 다시 제시한다. 검토한 파일과 해시, 사용자 승인 또는 수정 요청, 확인 시각을 연결하여 기록한다. 수정 전 파일에 대한 승인은 변경본으로 자동 승계하지 않는다. 모두 승인되면 게임 적용과 실제 화면·조작 검증을 이어간다.
승인 후 게임 적용 중 외형을 유지하는 축·피벗·import·Collider 연결 보정은 기존 계약 안에서 수행한다. 승인된 모델의 형태·색·재질·풍경 구성을 눈에 띄게 변경해야 하면 해당 변경본을 다시 검토받고 승인 후 채택한다. 이 모델 검토는 R07의 실제 게임 화면 품질 확인과 R08의 규칙·저장·Android 검증을 대체하지 않는다.
## R07 — 실제 화면에서의 품질 합격
‘상업 퀄리티’는 모델 파일 생성이나 Blender 렌더 한 장만으로 판정하지 않는다. 다음 항목을 실제 게임 캡처와 검토표로 확인하고 발견한 결함을 수정한다.
- 구·투구·칼·방패의 외곽이 부드럽고 의도한 형태로 읽힌다. 깨진 면·튀는 법선·눈에 띄는 UV 이음새·재질 누락·분홍 셰이더·투명 사각 배경이 없다.
- 얼굴·숫자·계수 표시·선택과 접촉 강조를 구별할 수 있다. 장식의 과한 돌출·관통·깜빡임·배경 색과의 혼동이 조작과 수량 이해를 방해하지 않는다.
- 나무·꽃·바위·지형은 서로 조화되면서 각 소재를 구별할 수 있다. 가까운 소품과 원경이 이어지고 카메라 이동에서 배경이 갑자기 끊기거나 지형 뒤 빈 공간이 드러나지 않는다.
- 세 풍경은 같은 세계의 다른 장소로 읽히며, 각 풍경의 특징이 실제 게임 화면에 보인다. 낮의 밝은 분위기와 부드러운 그림자가 영웅 가독성을 유지한다.
- 단일 구, 2+3 합체, 5에서 떨어진 양 끝 두 구를 고른 2+3 분리, 10개 모음과 10개 개별 구 상태, 빈 보드와 재공급에서 외형과 표시에 문제가 없다. 모든 구가 항상 한 시점에 보여야 하는 조건은 추가하지 않는다.
각 풍경의 대표 스테이지와 자유 놀이를 실제 기본 시점 및 허용된 회전·확대 상태에서 확인한다. Editor의 스마트폰·태블릿 가로 비율과 Android 시험 환경의 실제 화면을 구별해 기록한다. Blender 영웅의 정면·옆면·뒷면과 소품 목록 렌더도 남겨 편집 가능한 자산 품질을 검토할 수 있게 한다. 최종 자료는 사용자가 의도한 귀여움과 완성도를 평가할 수 있도록 전달한다.
모바일용 메시·공유 재질·텍스처·필요한 상세도 절감으로 비용을 관리한다. 영웅과 배경의 삼각형 수, 텍스처 해상도·메모리, 관찰 가능한 렌더링 비용을 최대 구 상태에서 기록하고 중복 메시·과한 재질·과밀 식생을 줄인다. 구체적인 제작 예산은 조정 가능하며 영웅 가독성을 비용 절감 때문에 훼손하지 않는다. Editor 통계나 AVD 결과를 실기기 FPS·발열·배터리 합격으로 표현하지 않는다. 실기기 검증은 `docs/tracking/status.md`의 후속 범위를 유지한다.
## R08 — 규칙·저장·음성 보존과 필수 검증
Core가 수학 상태를 소유한다. 완료 기록·퍼즐별 미완료 상태·최근 자유 놀이를 혼합하지 않는다. 저장 JSON 계약과 복구 정책, 승인된 44개 음성 바이트·참조, 한국어 UI·글꼴·오프라인 동작을 유지한다. 효과음·배경음악 추가는 제외한다. 런타임 네트워크·마이크·계정·광고·결제·새 성공 조건을 추가하지 않는다.
기존 테마·소품 수를 고정한 검사가 새 미술 계약과 충돌하면 해당 기대값만 실제 제작 목록에 맞춰 갱신한다. 구 발견·Collider 선택·아치 빈틈·숫자 가독성·수량 보존·저장 복원을 약화하거나 검사를 건너뛰어 통과시키지 않는다. 묶음별 장비 수명과 장식의 입력 비간섭은 실제 프리팹을 사용하는 의미 있는 검사로 확인한다.
완성 APK 판정은 통합된 변경 소스에서 다음 명령을 순서대로 실행해 각 실제 종료 0, 양수 검사 수, 필수 사례의 실패·건너뜀·미확정 0을 확인한다.
```powershell
pwsh -NoProfile -File tools/verify.ps1 -Phase EditMode -Milestone Final
pwsh -NoProfile -File tools/verify.ps1 -Phase PlayMode -Milestone Final
pwsh -NoProfile -File tools/verify.ps1 -Phase AndroidBuild -Milestone Final
pwsh -NoProfile -File tools/verify.ps1 -Phase AndroidSmoke -Milestone Final
```
현재 검증 환경은 Unity 6000.6.3f1, API37의 2400×1080 가로 AVD다. 최종 검사 대상 소스·제품 파일 해시·APK SHA·실제 설치본·실행 시각·로그를 같은 실행으로 연결한다. 기존 Final 증거는 재실행 전 별도 위치와 해시로 보존한다. Android 검사의 초기화는 전용 AVD 시험 앱에만 수행하며 실제 사용자 데이터를 지우지 않는다. 검사·Unity Editor·Blender 원본 저장·ADB·녹음의 실행 소유자를 각각 하나로 유지한다.
전체 30개 정상 진행과 미완료·자유 놀이 복원, 오프라인 음성, 0~10개 공급·반납, 취소·분리 경계는 기존 Final의 실제 사례로 재검증한다. 새 배경에서 수집 구 발견이 실패하면 시각 배치나 적절한 Collider를 수정하고 동일 사례로 다시 검사한다. 평가하지 못한 항목은 미검증과 원인을 남기며 완료로 기록하지 않는다.
## R09 — 변경 경계와 완료 보고
이번 작업은 영웅·자연 배경의 시각 품질과 필요한 연결·검증·제작 이력만 변경한다. Core 규칙·진행·저장 재설계, 플랫폼 추가, 광범위한 코드 정리, 새 UI 기능·새 음원·스토어 게시·유료 구매는 포함하지 않는다.
`docs/asset-register.md`, 관련 제작 결정, `docs/operations.md`, `docs/tracking/status.md`를 실제 채택 결과와 명령·검증 범위에 맞춰 갱신한다. 해결되지 않은 재현 문제는 원인과 영향·가능한 대응을 `docs/tracking/findings.md`에 남긴다. 단순히 실기기 검증이 아직 없다는 사실을 재현 결함으로 등록하지 않는다.
구 소실·복제, 잘못된 수량, 이어하기 손실·덮어쓰기, 미승인 자산 채택이나 요청하지 않은 외부 전송이 발견되면 관련 변경을 멈추고 증거와 영향을 즉시 알린다. 최종 보고에는 원본·게임용 자산·세 풍경 화면·APK·검증 결과·실기기 미검증 범위를 함께 제공한다.
-1
View File
@@ -1 +0,0 @@
{ "initialized": true }
+2
View File
@@ -14,3 +14,5 @@ artifacts/
*.slnx
*.user
__pycache__
.dryforge/