diff --git a/docs/CREDITS.md b/docs/CREDITS.md index 4def656..7d8a4f1 100644 --- a/docs/CREDITS.md +++ b/docs/CREDITS.md @@ -8,8 +8,11 @@ | 자산 | 작가 | 출처 | 라이선스 | 사용 위치 | | --- | --- | --- | --- | --- | -| 기본 32×32 도트 sprite pack | 프로젝트 내부 제작 | `Assets/MagicExamHall/Resources/Sprites` | 프로젝트 라이선스 | 시험장 배경, 플레이어, 설치물, 룬, 원소 문양 | +| 기본 32×32 도트 sprite pack | 프로젝트 내부 제작 | `Assets/MagicExamHall/Resources/Sprites` | 프로젝트 라이선스 | 시험장 배경, 설치물, 룬, 원소 문양 | +| Zelda-like tilesets and sprites (캐릭터 시트) | ArMM1998 | | CC0 | 플레이어 4방향 idle/walk/cast 프레임, 멘토 NPC 15종 (`scripts/import-character-pack.py`로 슬라이스·리컬러) | | Procedural fallback sprite | 프로젝트 내부 제작 | `PixelArtFactory.cs` | 프로젝트 라이선스 | PNG 누락 시 fallback | +| 고문서 UI fallback pack | 프로젝트 내부 제작 | `MagicExamUiTheme.cs` | 프로젝트 라이선스 | 타이틀 로고, 책/두루마리 패널, 버튼, 노트, 체크박스, 슬라이더 | +| `TitleLogo.png` | 프로젝트 내부 제작 | `Resources/Sprites/UI/TitleLogo.png` | 프로젝트 라이선스 | 타이틀 화면 마법탑 룬 문장 | ## 사운드 @@ -22,7 +25,8 @@ | 자산 | 작가 | 출처 | 라이선스 | 사용 위치 | | --- | --- | --- | --- | --- | -| Unity 기본 Arial | — | Unity 번들 | — | HUD, 룬 라벨, 노트 | +| Galmuri11 / Galmuri14 (한글 비트맵 폰트) | Lee Minseo (quiple) | | SIL Open Font License 1.1 | HUD, 룬 라벨, 노트, 멘토 대사 등 모든 UI 텍스트 (`Resources/Fonts/`) | +| Unity 기본 Arial | — | Unity 번들 | — | Galmuri 로드 실패 시 fallback | ## 자산 추가 시 작성 규칙 diff --git a/docs/FINAL_REPORT.md b/docs/FINAL_REPORT.md index a3ac030..7725fa8 100644 --- a/docs/FINAL_REPORT.md +++ b/docs/FINAL_REPORT.md @@ -128,14 +128,15 @@ stroke session(다획 입력 버퍼 0.8초) → 특징 추출 → family 판정 | family별 첫 시도 성공률 | base family별 첫 attempt 성공 비율 분포 | RQ3 | attempts.csv | | 실패 유형 분포 | invalid/incomplete/dependency/detached/no-seal | RQ2 | attempts.csv | | assist level 도달률 | 힌트 단계 1/2/3 사용 횟수 | RQ2 | attempts.csv | -| 피드백 이해도 | 실패 후 다음 행동을 말로 설명 가능한지 + 발화대로 다음 시도가 변했는지 로그 대조 | RQ2 | 인터뷰 + attempts.csv | +| 피드백 이해도 (자기보고) | 실패 후 다음 행동을 말로 설명 가능한지 | RQ2 | 인터뷰 | +| 피드백 이해도 (행동) | 발화한 교정 방향과 다음 시도의 실제 변화 일치율 | RQ2 | 인터뷰 + attempts.csv 대조 | | 막힘 지점 | 2분 이상 정체 또는 동일 목표 3회 이상 실패 | RQ1 | 관찰 | | 세션 내 학습 곡선 | 시도 순서 rolling 성공률의 기울기 (양수 = 플레이만으로 향상) | RQ1 | attempts.csv 분석 | | 힌트 전후 성공률 | 목표 단위 첫 힌트 노출 이전 vs 이후 성공률 비교 (escalator 효과의 준-인과 증거) | RQ2 | attempts.csv 분석 | | 설문 5점 척도 | 명확성·공정성·피드백 도움·조작감·몰입감 | 전체 | 설문 | | SUS | System Usability Scale 10문항, 표준 채점 0~100 (기준선 68) | 전체 | 설문 | -학습 곡선·힌트 전후·family 공정성 분석은 `scripts/playtest-attempts-analysis.py`가 자동 산출한다 (합성 데이터 자가 검증 포함). 단일 조건 관찰 연구의 한계는 §6.2에 명시하되, 힌트 전후 비교가 RQ2의 준-인과적 증거를 제공한다. +광범위한 실패 유형·혼동 행렬·막힘 지점·SUS 채점은 `scripts/analyze-playtest-attempts.py`로 산출한다. RQ1 학습 곡선, RQ2 힌트 전후, RQ3 family 공정성의 재현 가능한 집중 분석은 합성 데이터 self-test를 포함한 `scripts/playtest-attempts-analysis.py`로 교차 확인한다. 단일 조건 관찰 연구의 한계는 §6.2에 명시한다. ### 5.3 사전 수집: stroke 데이터 diff --git a/docs/GAME_DESIGN.md b/docs/GAME_DESIGN.md index c4f254a..38017f6 100644 --- a/docs/GAME_DESIGN.md +++ b/docs/GAME_DESIGN.md @@ -169,7 +169,7 @@ ### 시전 위치와 목표 판정 -주문은 문양의 중심이 가까운 목표 오브젝트에 영향을 준다. 즉 같은 `earth + steel_brace`도 3층 다리 영역 근처에서 그리면 다리 지지대로 해석되고, 4층 균열 근처에서 그리면 균열 안정화로 해석된다. 같은 주문이 위치에 따라 다르게 작동한다는 사실 자체가 게임의 한 가지 발견이다. +주문은 문양의 중심이 가까운 목표 오브젝트에 영향을 준다. 즉 같은 사각(rect) 커스텀 도형도 3층 깨진 구멍 근처에서 쓰면 길을 만드는 메움판으로 해석되고, 4층 표적 근처에서 쓰면 엄폐 방벽으로 해석된다. 같은 주문이 위치에 따라 다르게 작동한다는 사실 자체가 게임의 한 가지 발견이다. ## 5. 마법 문법 @@ -285,9 +285,9 @@ NPC가 등장한 발언은 동시에 노트에 자동 기록된다. 1회 실패 | 층 | 역할 | 가장 강하게 가르치는 것 | | --- | --- | --- | | 1층 발착층 | 입장과 첫 시전 | base 5종이 세계에 영향을 준다는 사실 | -| 2층 반응층 | overlay 발견 | 같은 base가 overlay에 따라 다르게 변한다 | -| 3층 흐름층 | 조합 응용 | 한 목표를 여러 방법으로 풀 수 있다 | -| 4층 균열층 | 실시간 위험 | 그리는 동안에도 세계는 움직인다 | +| 2층 반응층 | 커스텀 도형 발견 | 내가 등록한 모양이 인식 기준이 된다 (개인화) | +| 3층 건널목층 | 조합 응용 | 기본 문양 + 커스텀 도형으로 길을 직접 만든다 | +| 4층 타격층 | 조합 실험 | 조합이 표적 반응과 숫자로 즉시 보인다 | | 5층 성좌심 | 자유 조합 | 정답이 없는 큰 마법진을 자기 방식으로 푼다 | 층별 상세는 §9에서 본다. @@ -296,14 +296,16 @@ NPC가 등장한 발언은 동시에 노트에 자동 기록된다. 1회 실패 각 층은 다음 항목으로 정의한다: 들어올 때의 공간, **퍼즐 밀도**, 목표 상태, 가능한 해결 방향, 키 모먼트, 화면 배치, 진행 시간 목표, NPC가 등장하는 자리, 실패 회복 디자인. +> **2026-06-12 as-built 개정** — 커스텀 도형(two-track personalization) 시스템 도입으로 2~4층 디자인이 초안과 달라졌다. 이 절은 실제 구현(`ExamGameController.BuildFloorDefinitions`) 기준으로 개정되었다. 초안(2층 깨진 룬·overlay 복구, 4층 진행형 균열+번개 회로)은 git 이력에서 볼 수 있다. + ### 층별 퍼즐 밀도 요약 | 층 | 풀어야 할 인터랙션 | 통과 기준 | 해결 루트 최소 다양성 | | --- | --- | --- | --- | | 1층 | 인덱스 룬 5개 (base 5종에 1:1 대응) | 1개 이상 작동시키면 승강 룬이 차오름. 권장 3개 이상 | 5 base 각각 | -| 2층 | 깨진 룬 6개 (overlay 6종에 1:1 대응) | 4개 이상 복구 | overlay 6종 중 최소 4종 사용 | -| 3층 | 끊긴 다리 1개 | 어느 방법이든 한 경로로 통과 | 최소 4가지 해결 루트 (아래 목록) | -| 4층 | 균열 3개 + 번개 회로 1개 | 모두 안정화 | base+overlay 조합 최소 3가지 | +| 2층 | 커스텀 반응 표식 5개 (base 5종의 커스텀 도형에 1:1 대응) | 5개 안정화 | 도형 5종 (line/ellipse/arrow/rect/brace) 각각 | +| 3층 | 끊긴 구간 4개 (강물·깨진 구멍·낭떠러지·빈 공간) | 4구간 모두 개통 | 구간별 커스텀 스펠 4종 | +| 4층 | 훈련 표적 4개 | 4표적 모두 반응 | 효과 4종 (얼음·번개·정화·안정) | | 5층 | 슬롯 6개 (상태 요구) | **5개 만족 = 통과 엔딩 / 6개 만족 = 진엔딩** | 슬롯당 만족 방법 최소 2종 | 밀도는 "이 정도가 한 층에서 무리 없이 발견될 분량"의 잠정 값이다. 사용자 테스트에서 조정한다. @@ -354,129 +356,92 @@ ghost gesture는 1층에서만 1회 재생된다. 한 번 본 사람은 다음 ### 2층 반응층 -**공간** — 벽화가 가득한 둥근 방. 가운데에 6개의 깨진 룬이 원형으로 배치돼 있다. - -**목표 상태** — 6개 깨진 룬 중 4개 이상이 복구된다. +**공간** — 벽화가 가득한 둥근 방. 좌측에 커스텀 레퍼런스 책장이 있고, 방 안에 5개의 반응 표식이 배치돼 있다. -**가능한 해결 방향** — 깨진 룬은 각각 다른 overlay에 반응한다. 플레이어는 base를 먼저 그린 뒤 overlay를 그려야 룬이 복구된다. overlay는 처음부터 모두 그릴 수 있지만, 어떤 overlay가 어떤 룬과 맞는지는 시도로만 알 수 있다. +**목표 상태** — 커스텀 반응 표식 5개(불꽃 직선·물 보호막·바람 화살표·사각 방벽·생명 연결선)가 모두 안정화된다. -**키 모먼트** — overlay가 처음 base seal 위에 붙는 순간. 룬 가장자리에 작은 마크가 추가되며 라벨이 `불꽃 + 집중` 같은 형태로 변한다. 이 순간이 "base 위에 무언가를 더 올릴 수 있다"는 발견이다. +**가능한 해결 방향** — 좌측 책장에 가까이 가면 base별 커스텀 레퍼런스를 도형 슬롯으로 들여올 수 있다. 표식은 저장된 커스텀 도형의 gold capture를 기준으로 반응한다 — 즉 플레이어가 등록한 그 모양이 인식 기준이 된다. 각 표식은 요구 도형 토큰(line/ellipse/arrow/rect/brace)이 다르다. -**화면 배치** - -``` - [노트] [층 라벨] - - 깨진룬1 깨진룬2 - - 깨진룬6 [PLAYER] 깨진룬3 - - 깨진룬5 깨진룬4 - - [벽화 — overlay 형태 힌트] -``` - -벽화는 클릭하지 않아도 자연스럽게 시야에 들어오게 한다. 정답이 아니라 "이런 모양도 있다"는 단서. +**키 모먼트** — 자기가 등록한 커스텀 도형이 처음 표식을 깨우는 순간. "시스템이 내 모양을 기억한다"는 개인화의 발견이다. **진행 시간 목표** — 평균 8~12분. **환경이 먼저 가르치는 것** -- 첫 overlay 성공 시 룬 가장자리에 작은 마크가 추가되고 라벨이 `불꽃 + 집중` 형태로 변한다. NPC 등장 없음. -- `martial_axis` 의존성 부족 시 시도 자체가 잠긴 듯한 `cast_dependency_missing` SFX로 처리되고, 룬 라벨 위에 "축이 설 자리가 없음" 한 줄이 1초 표시. NPC 등장 없음. -- 벽화는 6 overlay의 실루엣 단서를 정적 배경으로 제공한다. +- 책장 접근 시 레퍼런스가 자연스럽게 보인다. 정답 강요가 아니라 "이런 모양을 등록할 수 있다"는 단서. +- 표식에서 먼 곳에 그린 커스텀 도형은 발동하지 않고 근접 안내 말풍선이 뜬다 (위치 의존 규칙 학습). +- 이전 seal이 활성인 상태에서도 다음 커스텀 base 목표를 이어서 진행할 수 있다. **NPC가 등장하는 자리** -- 같은 깨진 룬에 같은 base로 3회 실패 시 (escalator 2단계). family 체크리스트 한 번. -- 6개 룬 중 4개를 복구해 층 완료 시 한 줄 마무리. +- 같은 표식에 반복 실패 시 escalator 단계에 따라 등장. family 체크리스트 한 번. +- 5개 표식을 안정화해 층 완료 시 한 줄 마무리. 2층은 발견 위주 층이라 NPC 출연이 매우 드물다. 환경 자체가 단서다. -**실패 회복** — 같은 깨진 룬에 같은 base를 두 번 실패하면 그 룬이 5초간 살짝 진동한다. 비언어적 단서로 끝. 세 번째부터 escalator가 NPC로 넘어간다. - -### 3층 흐름층 +**실패 회복** — 표식 근처 재시도는 비용이 없고, 실패가 쌓이면 escalator가 단계적으로 개입한다. -**공간** — 끊어진 공중 다리. 플레이어와 다음 구역 사이에 빈 공간이 있다. +### 3층 건널목층 -**목표 상태** — 플레이어가 건너편으로 이동할 수 있는 경로가 만들어진다. +**공간** — 강물, 깨진 바닥 구멍, 낭떠러지, 빈 공간 네 구간이 길을 끊고 있다. 시작 구간 책장에서 3층 프리셋 도형을 가져올 수 있다. -**가능한 해결 방향** — 의도된 네 가지 + 발견 가능한 변주. +**목표 상태** — 네 구간의 길이 모두 열린다. -- `earth + steel_brace` — 빈 공간에 단단한 지지대가 솟아 다리가 됨. 가장 직관적인 해법. -- `wind + martial_axis` — 떠 있는 발판이 일직선으로 정렬되어 건널 수 있음. 발판이 보일 때만 가능. -- `life + soul_dot` — 한쪽 끝에서 덩굴이 자라 반대편에 닿음. 닿을 거리가 짧을 때만 충분. -- `water + ice_bar` — 임시 얼음 다리. 일정 시간 후 녹으니 그 안에 건너야 함. -- (선택적 변주) `earth` 단독으로도 짧은 발판은 가능하지만 길이가 부족함. 두 번 그어 연결해야 함. -- (선택적 변주) `electric_fork`로 끊긴 회로를 잇고 그 위를 건너는 변형도 가능 (성공 시 NPC가 "내가 고른 길이 나의 답이다" 발화). +**가능한 해결 방향** — 구간별로 의도된 커스텀 스펠이 하나씩 있다. 먼저 기본 문양으로 seal을 만들고, 그 빛나는 원 안쪽이나 가장자리에 요구 도형을 얹으면 발동한다. -같은 목표를 다른 방법으로 풀 수 있다는 것을 처음 명시적으로 알려주는 층이다. +- 강물 — `water` + 육각(hexagon) 결정: 강물이 얼어 안전한 얼음길이 됨. +- 깨진 구멍 — `earth` + 사각(rect) 메움판: 구멍이 메워져 지나갈 길이 생김. +- 낭떠러지 — `life` + 화살표/사각(arrow·rect): 덩굴 다리가 낭떠러지를 이음. +- 빈 공간 — `wind` + 사각(rect) 발판: 바람 발판이 마지막 빈 공간을 건너게 함. -**키 모먼트** — 플레이어가 만든 경로 위로 처음 한 발을 내딛는 순간. 카메라가 살짝 줌인된다. +base를 먼저 만들고 그 위에 도형을 얹는 2단 시전이 이 층의 핵심 학습이다. 표식에서 먼 곳에 그리면 발동하지 않는다 — 위치 의존 규칙을 여기서 체화한다. (초안의 "한 다리에 4가지 해법" 다양성은 5층의 자유 조합으로 옮겨졌다.) -**화면 배치** - -``` - [층 라벨] - - [건너편 — 다음 층 입구] - ────────────── - - ──────── - (빈 공간) - ────── - - [PLAYER] - ───────── -``` - -빈 공간은 의도적으로 시야 한가운데에 둔다. 플레이어 시선이 자연스럽게 "여기를 메워야 한다"로 향하게. +**키 모먼트** — 자기가 만든 길(얼음길·메움판·덩굴 다리) 위로 처음 한 발을 내딛는 순간. **진행 시간 목표** — 평균 6~10분. **환경이 먼저 가르치는 것** -- 빈 공간이 시야 한가운데 — "여기를 메우면 된다"는 비언어적 단서. -- 가까운 영역에 base/overlay 시전 시 그 효과가 빈 공간에서 한 번씩 미리 잠깐 비치는 시각 힌트 (예: `earth` 시전 시 작은 돌 부스러기가 빈 공간 가장자리에 잠깐 솟음). +- 끊긴 구간이 시야에 들어오는 배치 — "여기를 메우면 된다"는 비언어적 단서. +- 장애물 아트가 깊이 단서(수직 단면·원근)를 제공해 어디가 막혀 있는지 보인다. +- 키보드만으로도 발판 이동이 가능하다 (접근성 fallback). **NPC가 등장하는 자리** -- 층 완료 시 한 줄 마무리. 예: "한 자리에 닿는 길은 여럿이다." -- 두 번째 다른 방법으로 통과 시 (선택적): "내가 고른 길이 나의 답이다." (이 자리는 의도된 보너스라 NPC 등장에 가치가 있음) - -**실패 회복** — 만든 경로가 시간 경과로 약해지면 한 번 더 같은 주문을 덧그릴 수 있게 한다. 부분 진행은 유지된다. +- 층 완료 시 한 줄 마무리. +- 같은 구간 반복 실패 시 escalator 단계에 따라 등장. -### 4층 균열층 +**실패 회복** — 시간이 지나 약해지는 효과(얼음길 등)는 같은 시전을 덧그려 보수할 수 있다. 부분 진행은 유지된다. -**공간** — 불안정한 룬 회로가 깔린 좁은 공간. 번개 회로와 균열이 천천히 퍼지고, 발판이 떠 있다 사라진다. +### 4층 타격층 -**목표 상태** — 균열 3개가 모두 안정화되고, 번개 회로가 잠잠해진다. +**공간** — 훈련 표적 4개가 배치된 수련장. 표적은 어려운 말을 쓰지 않는다 — 필요한 조합을 만들면 바로 반응과 숫자로 보여 준다. -**가능한 해결 방향** — base+overlay 조합 최소 3가지가 의도된 답. +**목표 상태** — 네 표적이 모두 반응한다. -- `earth + steel_brace` — 균열 봉합. 가장 안정적이지만 시전 시간이 김. -- `water + ice_bar` 또는 `ice_bar` 단독 — 번개 회로 동결. 회로가 멈춤. -- `void_cut` — 균열 진행 차단. 즉발이지만 부분만 막음. -- `wind` 단독 — 떠 있는 발판을 안전 위치로 밀어 안전 지점 추가 (선택, 진행 보조). -- 균열 3개를 모두 같은 방법으로 풀어도 되고, 각각 다른 방법을 써도 됨. 다양성이 통과 조건은 아님. +**가능한 해결 방향** — 표적별 의도된 커스텀 스펠 효과. -**키 모먼트** — 그리는 중에 위험이 닿아 안전 지점으로 리셋되는 첫 순간. 게임이 처음으로 "실시간 압박"을 가르치는 순간이다. 화면 가장자리 비네트 + 짧은 효과음 + 즉시 안전 지점 이동. 죽음이나 체력 감소는 없다. +- 얼음 제압 — `water` 계열 얼음 결정: 표적의 움직임을 늦춤. +- 번개 타격 — `fire` 계열 번개 직선: 표적을 빠르게 때림. +- 정화 수막 — `water` + 둥근 수막: 표적의 오염 효과를 씻어 냄. +- 사각 방벽 — `earth` + 사각 방벽: 표적 앞에 엄폐물을 세움. -**화면 배치** — 다른 층과 달리 시야 안에 안전 지점(파란 룬)을 항상 1개 이상 보이게 둔다. 카메라가 안전 지점에서 너무 멀어지지 않게 약한 추적 제한을 둔다. +**키 모먼트** — 조합의 결과가 피해 숫자와 버프 표시로 즉시 보이는 첫 순간. 1~3층에서 배운 문법이 "전투적 반응"으로도 읽힌다는 발견이다. -**진행 시간 목표** — 평균 8~12분. +**진행 시간 목표** — 평균 6~10분. **환경이 먼저 가르치는 것** -- 첫 안전 지점 리셋 시 화면 가장자리 비네트 + `hazard_reset` SFX. 노트는 "탑은 떨어진 사람을 다시 돌려보낸다"를 자동 관찰문으로 기록. NPC 등장 없음. -- 균열 첫 안정화 시 균열이 또렷이 다물리는 시각 변화 + 짧은 `goal_satisfied`. 노트는 "벌어진 자리는 단단한 것이 덮는다"를 자동 기록. +- 피해 숫자와 버프 큐(지속 효과 시계 표시)가 엔티티에 붙어 효과의 세기·지속을 정량적으로 보여 준다. +- 속성 반응(점화·동결·전도 등)이 재질에 따라 다르게 일어나고, 첫 발생 시 관찰문이 codex에 기록된다. +- 위험 접촉 시 체력 하트가 절반씩 줄지만 죽음은 없다 — 위험의 무게만 가르친다. **NPC가 등장하는 자리** -- 같은 균열에 5회 이상 실패 시 (escalator 2~3단계). "그 균열은 단단한 것을 좋아한다" 같은 한 줄. 화면 가장자리 작은 윈도우로 등장해 그리는 손을 가리지 않음. +- 같은 표적에 반복 실패 시 escalator 단계에 따라 등장. - 층 완료 시 한 줄 마무리. -**실패 회복** — 무한 리셋이 안전 지점에서 일어나기 때문에 진행이 막히지 않는다. +**실패 회복** — 표적은 몇 번이고 다시 반응한다. 실패 누적은 escalator로만 이어지고 진행이 막히지 않는다. ### 5층 성좌심 @@ -539,7 +504,7 @@ ghost gesture는 1층에서만 1회 재생된다. 한 번 본 사람은 다음 - **시작** — 1층 입장에서 NPC가 짧게 환영한다. 시험이 무엇인지 거창하게 설명하지 않는다. "바닥은 너의 종이다." 정도의 한 줄. - **전개** — 1~3층 본문에서는 환경(룬 발광, 라벨, SFX, 노트 자동 기록)이 거의 모든 안내를 한다. NPC는 막혔을 때(2회 이상 실패)와 층 완료 시에만 짧게 등장한다. -- **위기** — 4층의 균열은 탑이 불안정하다는 사실을 보여준다. NPC가 화면 가장자리 작은 윈도우로 들어와 "탑의 입학 마법진이 흔들린다"고 한 줄을 남기고, 그 말이 노트에 기록된다. +- **위기** — 4층 수련장은 배운 문법이 위협 앞에서도 통하는지 시험한다. 표적의 반응과 체력 하트가 "탑의 시험이 진지해졌다"는 사실을 보여주고, 멘토(균열 감시자)의 발화가 노트에 기록된다. - **클라이맥스** — 5층 성좌심에서 5번째 슬롯이 만족되는 순간. 화면 전체가 밝아지고 BGM이 정점에 도달한다. 6번째까지 채우면 한 박 더 차오른다. - **마무리** — NPC가 다시 등장해 한 줄을 남긴다. "너의 방식이 마법진을 다시 일어서게 했다." 그 뒤 성찰형 리포트가 열린다. @@ -632,7 +597,7 @@ low quality로 발동은 되었지만 약한 경우는 별도 SFX를 두지 않 - **ambient_tower** — 1~3층용. 조용한 룸톤. 8~10분 루프. - **climax_seal** — 5층 시작부터. 한 단계 더 강한 톤. 마법진 1개 만족마다 음 추가. -4층은 BGM을 거의 끄고 환경음(번개 잡음, 균열 갈라짐) 위주로 둔다. 압박감을 음악이 아니라 침묵으로 만든다. +1~4층은 `ambient_tower`를 공유하고 5층에서 `climax_seal`로 전환한다. 4층의 압박감은 BGM 변화가 아니라 표적 반응음과 피해 표시의 타격감으로 만든다. ## 13. NPC: Exam Mentor @@ -710,7 +675,7 @@ NPC가 말하는 순간 그 대사가 그대로 마법 노트에 자동 기록 1. **첫 base 발동** — 빛 펄스 + 차임. 카메라 미세 흔들림 없음 (첫 인상은 부드럽게). 2. **첫 overlay 부착** — 작은 트레일이 base seal로 빨려 들어감 + 가벼운 차임. 3. **3층 첫 경로 통과** — 카메라 짧은 줌인 + 발걸음 사운드 강조. -4. **4층 첫 위험 리셋** — 가장자리 비네트 + 낮은 진동 + 짧은 화면 흐릿함. +4. **4층 첫 위험 접촉** — 체력 하트 절반 감소 + `hazard_reset` 계열 피드백. 위험의 무게를 처음 가르치는 순간. 5. **노트 자동 기록 새 줄** — 우상단 책 아이콘 0.4초 펄스 + 책장 넘김 사운드. NPC 없이도 일어나는 가장 자주 있는 보상. 6. **assist ghost trace 등장 (escalator 3단계)** — NPC가 짧게 등장해 "이런 모양이다" + 바닥에 흐릿한 모범선 1.5초 + 짧은 종소리. 셋이 동시에. 7. **5층 슬롯 1개 만족** — 슬롯 라벨이 또렷해지며 빛 + 상승 멜로디 1단. diff --git a/docs/PHASE5_6_CODE_INSPECTION.md b/docs/PHASE5_6_CODE_INSPECTION.md index 3983289..588e0c1 100644 --- a/docs/PHASE5_6_CODE_INSPECTION.md +++ b/docs/PHASE5_6_CODE_INSPECTION.md @@ -53,11 +53,11 @@ | # | 항목 | 판정 | 증거 | | --- | --- | --- | --- | -| 1 | 트리거 6종 | **부분** | 5분 침묵(300s) ✓ (`firstFloorLongSilenceShown`, 5343행). 실패 escalator ✓ (PlayMode `FailedBaseCastsEscalateMagicNoteHints`). **1층 8초 침묵 트리거는 코드에서 발견 못 함** | +| 1 | 트리거 6종 | **부분** | 5분 침묵(300s) ✓ (`firstFloorLongSilenceShown`, 5343행). 실패 escalator ✓ (PlayMode `FailedBaseCastsEscalateMagicNoteHints`). 1층 8초 침묵 ✓ — **정정(2026-06-12): `firstFloorGhostShown` 8초 트리거가 구현돼 있음** (ExamGameController 5331행, ghost gesture + 노트). 초기 검수의 grep 누락이었음 | | 2 | 의도적 미등장 | 빌드 검증 필요 | 코드 구조상 모순 없음, 전수 확인은 플레이 필요 | | 3 | 대사 풀 트리거당 3~5변주 ≈ 25줄 | **부분** | `MentorPresentationController`에 층별 멘토 프로필 5종(벽화 연구원·균열 감시자 등) 구현. 변주 수 전수 카운트는 미실시 | | 4 | 등장 연출 (페이드인+머리 위 룬+`npc_appear`, 4층 가장자리 윈도우) | **부분** | `npc_appear` SFX ✓, 멘토 프레젠테이션 ✓. 4층 가장자리 윈도우는 4층 디자인 자체가 변경돼 스펙 재정의 필요 | -| 5 | EditMode 테스트 (쿨다운·1회성·풀 무작위) | **미구현** | EditMode 테스트 5클래스 중 멘토 트리거 테스트 없음 (CustomShape/Gesture/PlayerMovement/StageDefinition/StrokeInput뿐) | +| 5 | EditMode 테스트 (쿨다운·1회성·풀 무작위) | **부분 (2026-06-12 갱신)** | `HintAssistanceTests` 추가 — escalator 단계 상승·캡·assisted 플래그·체크리스트·멘토 프로필을 EditMode로 검증. 단 "쿨다운·풀 무작위"는 **대사 풀/쿨다운 시스템 자체가 미구현**이라 테스트 대상이 없음 (스펙 §13 잔여 기능) | ## P6-2. 결정적 모먼트 검수 diff --git a/docs/RESEARCH_PROTOCOL.md b/docs/RESEARCH_PROTOCOL.md index 2a06e9c..64f98a6 100644 --- a/docs/RESEARCH_PROTOCOL.md +++ b/docs/RESEARCH_PROTOCOL.md @@ -37,30 +37,37 @@ | overlay별 실패 유형 | invalid, incomplete, dependency, detached, no seal | attempts.csv | | assist level 도달률 | assistLevel 1/2/3 사용 횟수 | attempts.csv | | 막힘 지점 | 2분 이상 진행 정체 또는 같은 목표 3회 이상 실패 | 관찰 | -| 피드백 이해도 | 실패 후 다음 행동을 말로 설명 가능한지 | 인터뷰 | -| 피드백 이해의 행동 검증 | 참가자가 말한 교정 방향대로 다음 시도가 실제로 변했는지 (발화 ↔ attempts.csv 대조 코딩) | 인터뷰 + attempts.csv | +| 피드백 이해도 (자기보고) | 실패 후 다음 행동을 말로 설명 가능한지 | 인터뷰 | +| 피드백 이해도 (행동) | 참가자가 말한 교정 방향대로 다음 시도가 실제로 변했는지 (일치/불일치 코딩) | 인터뷰 발화 + attempts.csv 대조 | +| 학습 곡선 | 세션 내 시도 순서에 따른 성공률 이동 평균 (전반부 vs 후반부) | attempts.csv | +| 힌트 효과 | 힌트 노출 전후 성공률 변화 (창 5회) — 관찰 경향 | attempts.csv | | 몰입감 | 직접 마법을 시전한다는 느낌 | 설문 | -| SUS 점수 | System Usability Scale 10문항 (0~100) | 설문 | +| SUS | 표준 사용성 점수 (68 이상 = 평균 이상) | SUS 설문 | -## 사후 설문 구성 +### 피드백 이해도(행동) 코딩 절차 -1. 커스텀 5문항 (5점 척도): 명확성, 공정성, 피드백 도움, 조작감, 몰입감 + 자유 의견 -2. SUS 10문항 (5점 척도, 표준 채점: 홀수 문항 점수-1, 짝수 문항 5-점수, 합계 ×2.5) +1. 실패 직후 진행자가 "다음에 무엇을 바꿔 볼 건가요?"를 묻고 발화를 기록한다 (10초 이내, 게임은 일시정지하지 않음). +2. 분석 시 해당 발화와 바로 다음 attempt의 변화(family 변경 / 위치 이동 / 도형 교정 / seal 먼저 생성 등)를 대조해 일치=1, 불일치=0으로 코딩한다. +3. 일치율을 세션·실패 유형별로 집계한다. 자기보고("이해했다")와 행동(실제 교정)의 이중 측정이 목적이다. -SUS 문항 (한국어 번안, "이 시스템" = 이 게임의 드로잉 입력): +### SUS 설문 (사후 설문에 추가, 10문항 5점 척도) -1. 이 게임을 자주 하고 싶다. -2. 이 게임의 조작이 불필요하게 복잡하다고 느꼈다. -3. 이 게임의 조작이 쉬웠다. -4. 이 게임을 하려면 누군가의 도움이 필요할 것 같다. -5. 이 게임의 여러 기능이 잘 어우러져 있었다. -6. 이 게임의 동작이 일관되지 않다고 느꼈다. -7. 대부분의 사람들이 이 게임의 조작을 빨리 익힐 것 같다. -8. 이 게임의 조작이 매우 번거로웠다. -9. 게임을 하는 동안 자신감을 느꼈다. -10. 이 게임을 시작하기 전에 알아야 할 것이 많았다. +표준 SUS 문항의 한국어 번안. "이 시스템" 대신 "이 게임의 드로잉 입력"으로 묻는다. -기준선: SUS 68점이 통상적 평균. 68 이상이면 평균 이상의 사용성으로 보고한다. +1. 이 게임의 드로잉 입력을 자주 사용하고 싶다. +2. 드로잉 입력이 불필요하게 복잡하다고 느꼈다. +3. 드로잉 입력이 사용하기 쉬웠다. +4. 드로잉 입력을 쓰려면 누군가의 도움이 필요할 것 같다. +5. 드로잉 입력의 여러 기능이 잘 통합되어 있었다. +6. 드로잉 입력의 동작이 일관되지 않다고 느꼈다. +7. 대부분의 사람들이 드로잉 입력을 빠르게 배울 수 있을 것이다. +8. 드로잉 입력이 다루기 매우 번거로웠다. +9. 드로잉 입력을 쓰는 동안 자신감이 있었다. +10. 드로잉 입력을 쓰기 전에 많은 것을 익혀야 했다. + +응답은 `outputs/playtest-*/sus.csv`(`participantId,q1..q10`)로 저장하고 `python scripts/analyze-playtest-attempts.py --sus <파일>`로 채점한다 (홀수 문항 응답-1, 짝수 문항 5-응답, 합계 ×2.5). + +사후 설문은 위 SUS 10문항과 명확성·공정성·피드백 도움·조작감·몰입감의 커스텀 5문항, 자유 의견으로 구성한다. SUS 68점을 통상적 평균 기준선으로 사용한다. ## 입력 버퍼 A/B @@ -76,14 +83,17 @@ SUS 문항 (한국어 번안, "이 시스템" = 이 게임의 드로잉 입력): 1. `attempts.csv`를 session 단위로 모은다. 2. 직접 식별 정보가 없는지 확인한다. -3. `python scripts/playtest-attempts-analysis.py <세션폴더...> --out outputs/playtest-1/analysis`를 실행한다. 산출: +3. `python scripts/analyze-playtest-attempts.py <세션폴더...> -o outputs/playtest-1/analysis`를 실행해 실패 유형, 혼동 행렬, assist 도달, 막힘 지점을 산출한다. +4. `python scripts/playtest-attempts-analysis.py <세션폴더...> --out outputs/playtest-1/rq-analysis`를 실행한다. 산출: - RQ1: 세션 내 학습 곡선(rolling 성공률 기울기), 층별 첫 성공까지 시도 수 - RQ2: 목표 단위 힌트 노출 전후 성공률 비교, assist level별 직후 성공률 - RQ3: family별 첫 시도 성공률 분포와 편차 -4. floor, phase, recognizedFamily, worldEffect, success, assistLevel을 pivot한다. -5. 상위 오인식/막힘 3개를 Phase 4/5 백로그로 옮긴다. -6. 입력 버퍼 D3 결정을 `docs/FINAL_COMPLETION_PLAN.md` 결정 로그에 기록한다. -7. SUS를 표준 채점해 세션별 점수와 평균을 기록한다. +5. `python scripts/playtest-attempts-analysis.py --self-test`로 집중 분석 로직을 합성 데이터에서 검증한다. +6. SUS를 `python scripts/analyze-playtest-attempts.py --sus outputs/playtest-1/sus.csv`로 채점한다. +7. 피드백 이해도(행동) 코딩을 수행하고 일치율을 기록한다. +8. floor, phase, recognizedFamily, worldEffect, success, assistLevel을 pivot한다. +9. 상위 오인식/막힘 3개를 Phase 4/5 백로그로 옮긴다. +10. 입력 버퍼 D3 결정을 `docs/FINAL_COMPLETION_PLAN.md` 결정 로그에 기록한다. ## 보고 산출물 diff --git a/docs/asset-licenses/fonts/Galmuri-LICENSE.txt b/docs/asset-licenses/fonts/Galmuri-LICENSE.txt new file mode 100644 index 0000000..ae34182 --- /dev/null +++ b/docs/asset-licenses/fonts/Galmuri-LICENSE.txt @@ -0,0 +1,93 @@ +Copyright (c) 2019–2025 Lee Minseo (quiple@quiple.dev) + +This font software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/docs/asset-licenses/fonts/README.md b/docs/asset-licenses/fonts/README.md new file mode 100644 index 0000000..f28d166 --- /dev/null +++ b/docs/asset-licenses/fonts/README.md @@ -0,0 +1,15 @@ +# Font Asset Notes + +추가일: 2026-06-13 + +## Galmuri (SIL Open Font License 1.1) + +| 파일 | 원본 | 작가 | 출처 | 라이선스 | +| --- | --- | --- | --- | --- | +| `Resources/Fonts/Galmuri11.ttf` | Galmuri11 v2.40.3 | Lee Minseo (quiple) | | SIL OFL 1.1 | +| `Resources/Fonts/Galmuri11-Bold.ttf` | Galmuri11 Bold | Lee Minseo (quiple) | 〃 | SIL OFL 1.1 | +| `Resources/Fonts/Galmuri14.ttf` | Galmuri14 | Lee Minseo (quiple) | 〃 | SIL OFL 1.1 | + +- 선정 사유: 게임 UI 전반의 한글 텍스트를 OS 폰트(Malgun Gothic) 의존 없이 모든 머신에서 동일하게 픽셀 단위로 또렷하게 렌더링하기 위함. 픽셀 아트 톤과도 맞는 비트맵 스타일. +- 로더: `ExamGameController.LoadGameFont`가 `Resources/Fonts/Galmuri11`을 우선 로드하고, 없으면 Malgun Gothic/Arial OS 폰트로 fallback. +- OFL 전문은 `Galmuri-LICENSE.txt` 참조. OFL은 폰트 번들·임베드를 허용하며, 폰트 파일 자체를 판매하지 않는 한 게임에 포함 가능. diff --git a/scripts/analyze-playtest-attempts.py b/scripts/analyze-playtest-attempts.py new file mode 100644 index 0000000..a22f754 --- /dev/null +++ b/scripts/analyze-playtest-attempts.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python3 +"""Playtest attempts.csv 분석 도구. + +RESEARCH_PROTOCOL.md의 측정 지표를 attempts.csv에서 자동 산출한다. +플레이테스트 후 세션 로그 폴더들을 지정하면 분석 요약(markdown + csv)을 만든다. + +산출 분석 (FINAL_REPORT.md §5.2 대응): + RQ1 학습 곡선 — 세션 내 시도 순서에 따른 성공률 이동 평균 + RQ2 힌트 전후 비교 — 힌트 노출 직전 vs 직후 성공률 변화 (준-인과 증거) + RQ3 family 공정성 — targetFamily별 첫 시도 성공률 + 혼동 행렬 + 공통 실패 유형 분포, assist level 도달률, 층별 막힘 지점, 첫 시전까지 시간 + +사용: + python scripts/analyze-playtest-attempts.py <세션폴더 또는 attempts.csv> [...] -o outputs/playtest-1/analysis + # 세션 폴더: Application.persistentDataPath/MagicExamHallLogs// + +SUS 채점 (선택): + python scripts/analyze-playtest-attempts.py --sus outputs/playtest-1/sus.csv + # sus.csv 형식: participantId,q1,...,q10 (각 1~5) + +의존성: 표준 라이브러리만 사용. +""" + +from __future__ import annotations + +import argparse +import csv +import statistics +import sys +from collections import Counter, defaultdict +from pathlib import Path + +TRUE_VALUES = {"true", "1", "yes"} +ROLLING_WINDOW = 10 # 학습 곡선 이동 평균 창 +HINT_WINDOW = 5 # 힌트 전후 비교 창 (시도 수) +STUCK_FAILS = 3 # 막힘 판정: 동일 목표 연속 실패 수 + + +def parse_bool(value: str) -> bool: + return value.strip().lower() in TRUE_VALUES + + +def load_attempts(path: Path) -> list[dict]: + """attempts.csv 한 개를 읽어 시도 dict 리스트로 반환.""" + rows: list[dict] = [] + with path.open(encoding="utf-8-sig", newline="") as f: + for row in csv.DictReader(f): + try: + rows.append({ + "sessionId": row.get("sessionId", ""), + "floorId": row.get("floorId", ""), + "targetFamily": row.get("targetFamily", ""), + "recognizedFamily": row.get("recognizedFamily", ""), + "phase": row.get("phase", ""), + "status": row.get("status", ""), + "targetObject": row.get("targetObject", ""), + "attemptIndex": int(float(row.get("attemptIndex") or 0)), + "elapsedMs": int(float(row.get("elapsedMs") or 0)), + "success": parse_bool(row.get("success", "")), + "hintShown": parse_bool(row.get("hintShown", "")), + "assistLevel": int(float(row.get("assistLevel") or 0)), + "assisted": parse_bool(row.get("assisted", "")), + }) + except (ValueError, TypeError): + continue # 손상 행은 건너뜀 + rows.sort(key=lambda r: r["attemptIndex"]) + return rows + + +def find_attempt_files(inputs: list[str]) -> list[Path]: + files: list[Path] = [] + for raw in inputs: + p = Path(raw) + if p.is_dir(): + files.extend(sorted(p.rglob("attempts.csv"))) + elif p.is_file(): + files.append(p) + else: + print(f"warning: not found, skipping: {p}", file=sys.stderr) + return files + + +def rate(success_count: int, total: int) -> float: + return success_count / total if total else 0.0 + + +# ---------------------------------------------------------------- RQ1 학습 곡선 + +def learning_curve(rows: list[dict]) -> list[tuple[int, float]]: + """(attemptIndex, 직전 ROLLING_WINDOW개 성공률) 시퀀스.""" + points = [] + window: list[bool] = [] + for r in rows: + window.append(r["success"]) + if len(window) > ROLLING_WINDOW: + window.pop(0) + points.append((r["attemptIndex"], rate(sum(window), len(window)))) + return points + + +def learning_summary(rows: list[dict]) -> dict: + """세션 전반부 vs 후반부 성공률 — 학습의 한 줄 요약.""" + if len(rows) < 4: + return {"firstHalf": None, "secondHalf": None, "delta": None} + mid = len(rows) // 2 + first = rate(sum(r["success"] for r in rows[:mid]), mid) + second = rate(sum(r["success"] for r in rows[mid:]), len(rows) - mid) + return {"firstHalf": first, "secondHalf": second, "delta": second - first} + + +# ---------------------------------------------------------------- RQ2 힌트 전후 + +def hint_effect(rows: list[dict]) -> dict: + """첫 힌트 노출(hintShown 또는 assistLevel>0) 전후 HINT_WINDOW개 시도의 성공률 비교. + + 또한 조건부 비교: 직전 시도에서 힌트를 본 경우 vs 못 본 경우의 성공률. + """ + first_hint = next((i for i, r in enumerate(rows) if r["hintShown"] or r["assistLevel"] > 0), None) + around = {"before": None, "after": None, "delta": None} + if first_hint is not None and first_hint >= 1: + before = rows[max(0, first_hint - HINT_WINDOW):first_hint] + after = rows[first_hint:first_hint + HINT_WINDOW] + if before and after: + b = rate(sum(r["success"] for r in before), len(before)) + a = rate(sum(r["success"] for r in after), len(after)) + around = {"before": b, "after": a, "delta": a - b} + + after_hint, after_hint_n = 0, 0 + after_nohint, after_nohint_n = 0, 0 + for prev, cur in zip(rows, rows[1:]): + if prev["hintShown"]: + after_hint += cur["success"] + after_hint_n += 1 + else: + after_nohint += cur["success"] + after_nohint_n += 1 + conditional = { + "pSuccessAfterHint": rate(after_hint, after_hint_n) if after_hint_n else None, + "nAfterHint": after_hint_n, + "pSuccessAfterNoHint": rate(after_nohint, after_nohint_n) if after_nohint_n else None, + "nAfterNoHint": after_nohint_n, + } + return {"aroundFirstHint": around, "conditional": conditional} + + +# ---------------------------------------------------------------- RQ3 공정성 + +def family_fairness(rows: list[dict]) -> dict: + """targetFamily별 첫 시도 성공률 + 혼동 행렬.""" + first_attempt: dict[tuple[str, str], dict] = {} + for r in rows: + if not r["targetFamily"]: + continue + key = (r["sessionId"], r["targetFamily"]) + if key not in first_attempt: + first_attempt[key] = r + + per_family: dict[str, list[bool]] = defaultdict(list) + for (_, family), r in first_attempt.items(): + per_family[family].append(r["success"]) + + confusion: Counter = Counter() + for r in rows: + if r["targetFamily"] and r["recognizedFamily"]: + confusion[(r["targetFamily"], r["recognizedFamily"])] += 1 + + return { + "firstAttemptSuccess": {f: rate(sum(v), len(v)) for f, v in sorted(per_family.items())}, + "firstAttemptN": {f: len(v) for f, v in sorted(per_family.items())}, + "confusion": confusion, + } + + +# ---------------------------------------------------------------- 공통 지표 + +def common_metrics(rows: list[dict]) -> dict: + status_dist = Counter(r["status"] for r in rows if r["status"]) + assist_reach = Counter() + for level in (1, 2, 3): + sessions = {r["sessionId"] for r in rows if r["assistLevel"] >= level} + assist_reach[level] = len(sessions) + + stuck: list[tuple[str, str, int]] = [] + streak: dict[tuple[str, str], int] = defaultdict(int) + for r in rows: + key = (r["floorId"], r["targetObject"] or r["targetFamily"]) + if r["success"]: + streak[key] = 0 + else: + streak[key] += 1 + if streak[key] == STUCK_FAILS: + stuck.append((key[0], key[1], r["attemptIndex"])) + + first_cast_ms = min((r["elapsedMs"] for r in rows), default=None) + per_floor_attempts = Counter(r["floorId"] for r in rows if r["floorId"]) + + return { + "totalAttempts": len(rows), + "overallSuccessRate": rate(sum(r["success"] for r in rows), len(rows)), + "statusDistribution": status_dist, + "assistReachSessions": dict(assist_reach), + "stuckPoints": stuck, + "firstCastMs": first_cast_ms, + "attemptsPerFloor": dict(per_floor_attempts), + } + + +# ---------------------------------------------------------------- SUS + +def score_sus(path: Path) -> list[tuple[str, float]]: + """SUS 표준 채점: 홀수 문항 (응답-1), 짝수 문항 (5-응답), 합계 x 2.5.""" + scores = [] + with path.open(encoding="utf-8-sig", newline="") as f: + for row in csv.DictReader(f): + pid = row.get("participantId", "?") + try: + total = 0 + for q in range(1, 11): + v = int(row[f"q{q}"]) + total += (v - 1) if q % 2 == 1 else (5 - v) + scores.append((pid, total * 2.5)) + except (KeyError, ValueError): + print(f"warning: invalid SUS row for {pid}", file=sys.stderr) + return scores + + +# ---------------------------------------------------------------- 출력 + +def fmt_pct(v) -> str: + return "-" if v is None else f"{v * 100:.1f}%" + + +def write_report(out_dir: Path, sessions: dict[str, list[dict]]) -> None: + out_dir.mkdir(parents=True, exist_ok=True) + all_rows = [r for rows in sessions.values() for r in rows] + lines = ["# Playtest attempts 분석", "", f"세션 {len(sessions)}개, 시도 {len(all_rows)}건", ""] + + lines += ["## RQ1 — 학습 곡선 (세션 전/후반 성공률)", "", + "| 세션 | 전반부 | 후반부 | 변화 |", "| --- | --- | --- | --- |"] + for sid, rows in sessions.items(): + s = learning_summary(rows) + lines.append(f"| {sid} | {fmt_pct(s['firstHalf'])} | {fmt_pct(s['secondHalf'])} | {fmt_pct(s['delta'])} |") + lines.append("") + + curve_path = out_dir / "learning-curve.csv" + with curve_path.open("w", encoding="utf-8", newline="") as f: + w = csv.writer(f) + w.writerow(["sessionId", "attemptIndex", f"rollingSuccessRate(window={ROLLING_WINDOW})"]) + for sid, rows in sessions.items(): + for idx, val in learning_curve(rows): + w.writerow([sid, idx, f"{val:.3f}"]) + lines += [f"이동 평균 곡선 원자료: `{curve_path.name}` (그래프용)", ""] + + lines += ["## RQ2 — 힌트 노출 전후 성공률", "", + "| 세션 | 첫 힌트 전 | 첫 힌트 후 | 변화 | P(성공\\|직전 힌트) | P(성공\\|직전 힌트 없음) |", + "| --- | --- | --- | --- | --- | --- |"] + for sid, rows in sessions.items(): + h = hint_effect(rows) + a, c = h["aroundFirstHint"], h["conditional"] + lines.append( + f"| {sid} | {fmt_pct(a['before'])} | {fmt_pct(a['after'])} | {fmt_pct(a['delta'])} " + f"| {fmt_pct(c['pSuccessAfterHint'])} (n={c['nAfterHint']}) " + f"| {fmt_pct(c['pSuccessAfterNoHint'])} (n={c['nAfterNoHint']}) |") + lines += ["", f"창 크기: 전후 {HINT_WINDOW}회. 관찰 연구이므로 인과가 아니라 경향으로 해석한다.", ""] + + fairness = family_fairness(all_rows) + lines += ["## RQ3 — family 공정성 (첫 시도 성공률)", "", + "| family | 첫 시도 성공률 | n(세션) |", "| --- | --- | --- |"] + for fam, sr in fairness["firstAttemptSuccess"].items(): + lines.append(f"| {fam} | {fmt_pct(sr)} | {fairness['firstAttemptN'][fam]} |") + lines.append("") + + conf_path = out_dir / "confusion-matrix.csv" + families = sorted({k[0] for k in fairness["confusion"]} | {k[1] for k in fairness["confusion"]}) + with conf_path.open("w", encoding="utf-8", newline="") as f: + w = csv.writer(f) + w.writerow(["target\\recognized"] + families) + for t in families: + w.writerow([t] + [fairness["confusion"].get((t, rec), 0) for rec in families]) + lines += [f"혼동 행렬: `{conf_path.name}`", ""] + + m = common_metrics(all_rows) + lines += ["## 공통 지표", "", + f"- 전체 시도 {m['totalAttempts']}건, 전체 성공률 {fmt_pct(m['overallSuccessRate'])}", + f"- 첫 시전까지 시간(최소 elapsedMs): {m['firstCastMs']} ms", + f"- assist level 도달 세션 수: {m['assistReachSessions']}", + f"- 층별 시도 수: {m['attemptsPerFloor']}", "", + "### 실패 유형 분포", "", "| status | 건수 |", "| --- | --- |"] + for status, n in m["statusDistribution"].most_common(): + lines.append(f"| {status} | {n} |") + lines += ["", f"### 막힘 지점 (동일 목표 연속 {STUCK_FAILS}회 실패)", ""] + if m["stuckPoints"]: + lines += ["| 층 | 목표 | 발생 시도 index |", "| --- | --- | --- |"] + lines += [f"| {f} | {g} | {i} |" for f, g, i in m["stuckPoints"]] + else: + lines.append("(없음)") + lines.append("") + + report_path = out_dir / "analysis.md" + report_path.write_text("\n".join(lines), encoding="utf-8") + print(f"written: {report_path}") + print(f"written: {curve_path}") + print(f"written: {conf_path}") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("inputs", nargs="*", help="세션 폴더 또는 attempts.csv 경로 (복수 가능)") + parser.add_argument("-o", "--out", default="outputs/playtest-analysis", help="분석 결과 출력 폴더") + parser.add_argument("--sus", help="SUS 응답 csv 채점 (participantId,q1..q10)") + args = parser.parse_args() + + if args.sus: + scores = score_sus(Path(args.sus)) + print("| participant | SUS |") + print("| --- | --- |") + for pid, s in scores: + print(f"| {pid} | {s:.1f} |") + if scores: + vals = [s for _, s in scores] + print(f"\n평균 {statistics.mean(vals):.1f} / 중앙값 {statistics.median(vals):.1f} (기준: 68 이상이면 평균 이상)") + if not args.inputs: + return 0 + + files = find_attempt_files(args.inputs) + if not files: + parser.error("attempts.csv를 찾지 못했습니다. 세션 폴더 또는 파일 경로를 지정하세요.") + + sessions: dict[str, list[dict]] = {} + for path in files: + rows = load_attempts(path) + if not rows: + print(f"warning: empty attempts file: {path}", file=sys.stderr) + continue + sessions[rows[0]["sessionId"] or path.parent.name] = rows + + if not sessions: + print("error: no usable attempt rows", file=sys.stderr) + return 1 + + write_report(Path(args.out), sessions) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/gen-title-art.py b/scripts/gen-title-art.py new file mode 100644 index 0000000..ffebddb --- /dev/null +++ b/scripts/gen-title-art.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +"""Magic Exam Hall 타이틀 화면용 픽셀아트 생성. + +- TitleTower.png : 떠 있는 마법탑 (돌탑 몸통 + 원뿔 지붕 + 빛나는 창 + 부유 암반). +- TitleSky.png : 밤하늘 그라데이션 + 별 + 달 배경 (1280x720). + +도형 나열이 아니라 음영/외곽선/디더링으로 입체감을 준 핸드드로잉 픽셀아트. +출력: Resources/Sprites/ (UI Image가 런타임에 Texture2D로 로드). +""" + +import math +import random +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFilter + +REPO = Path(__file__).resolve().parent.parent +OUT = REPO / "unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI" +PREVIEW = REPO / "outputs" + + +def lerp(a, b, t): + t = max(0.0, min(1.0, t)) + return tuple(int(a[i] + (b[i] - a[i]) * t) for i in range(len(a))) + + +# ---------------------------------------------------------------- tower +def make_tower(scale=4): + W, H = 150, 300 + img = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + d = ImageDraw.Draw(img) + + cx = W // 2 + # palette + stone = (108, 112, 126) + stone_hi = (140, 146, 162) + stone_lo = (74, 78, 92) + mortar = (56, 58, 70) + outline = (28, 26, 40) + roof = (74, 52, 110) + roof_hi = (104, 80, 150) + roof_lo = (50, 34, 78) + glow = (255, 214, 132) + glow_core = (255, 240, 200) + rock = (60, 50, 46) + rock_hi = (84, 70, 60) + rock_lo = (40, 32, 30) + + # ---- body (slightly tapered) ---- + body_top, body_bot = 96, 250 + top_half, bot_half = 36, 46 + + def half_at(y): + t = (y - body_top) / (body_bot - body_top) + return top_half + (bot_half - top_half) * t + + for y in range(body_top, body_bot): + hw = half_at(y) + # base shade with left-light / right-shadow gradient across the width + for x in range(int(cx - hw), int(cx + hw) + 1): + tx = max(0.0, (x - (cx - hw)) / (2 * hw)) + col = lerp(stone_hi, stone_lo, tx ** 1.1) + img.putpixel((x, y), col + (255,)) + + # stone courses (brick rows with offset joints) + course_h = 18 + row = 0 + for y in range(body_top + 2, body_bot, course_h): + hw = half_at(y) + d.line([(cx - hw, y), (cx + hw, y)], fill=mortar + (255,), width=1) + offset = (course_h // 2) if row % 2 else 0 + bx = int(cx - hw) + offset + while bx < cx + hw: + yy = min(y + course_h, body_bot) + d.line([(bx, y), (bx, yy)], fill=mortar + (200,), width=1) + bx += course_h + row += 1 + + # body outline + d.line([(cx - top_half, body_top), (cx - bot_half, body_bot)], fill=outline + (255,), width=2) + d.line([(cx + top_half, body_top), (cx + bot_half, body_bot)], fill=outline + (255,), width=2) + + # ---- glowing arched windows (3 tiers) ---- + def window(yc, w=11, h=18): + x0, x1 = cx - w // 2, cx + w // 2 + # soft halo + halo = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + hd = ImageDraw.Draw(halo) + hd.ellipse([x0 - 9, yc - 14, x1 + 9, yc + h + 8], fill=glow + (70,)) + halo = halo.filter(ImageFilter.GaussianBlur(4)) + img.alpha_composite(halo) + # frame + d.rectangle([x0 - 2, yc - 2, x1 + 2, yc + h + 2], fill=outline + (255,)) + # arch glow + d.rectangle([x0, yc + 4, x1, yc + h], fill=glow + (255,)) + d.pieslice([x0, yc - (x1 - x0) // 2, x1, yc + (x1 - x0)], 180, 360, fill=glow + (255,)) + d.rectangle([x0 + 2, yc + 7, x1 - 2, yc + h - 3], fill=glow_core + (255,)) + + window(120) + window(168) + window(212, w=13, h=20) + + # ---- battlement ring under the roof ---- + ring_y = body_top + d.rectangle([cx - top_half - 4, ring_y - 6, cx + top_half + 4, ring_y + 2], fill=stone + (255,)) + d.rectangle([cx - top_half - 4, ring_y - 6, cx + top_half + 4, ring_y - 6], fill=stone_hi + (255,)) + d.line([(cx - top_half - 4, ring_y + 2), (cx + top_half + 4, ring_y + 2)], fill=outline + (255,), width=2) + + # ---- conical roof ---- + roof_apex = 18 + roof_base_y = ring_y - 6 + roof_half = top_half + 8 + # shaded cone: draw vertical slices + for x in range(cx - roof_half, cx + roof_half + 1): + t = (x - (cx - roof_half)) / (2 * roof_half) + # height of cone surface at this x + edge_t = abs(x - cx) / roof_half + ytop = roof_apex + (roof_base_y - roof_apex) * edge_t + col = lerp(roof_hi, roof_lo, max(0.0, t) ** 1.2) + d.line([(x, ytop), (x, roof_base_y)], fill=col + (255,)) + # roof outline + d.line([(cx - roof_half, roof_base_y), (cx, roof_apex)], fill=outline + (255,), width=2) + d.line([(cx + roof_half, roof_base_y), (cx, roof_apex)], fill=outline + (255,), width=2) + d.line([(cx - roof_half, roof_base_y), (cx + roof_half, roof_base_y)], fill=outline + (255,), width=2) + # roof shingle hints + for ry in range(int(roof_apex) + 14, int(roof_base_y), 12): + et = (ry - roof_apex) / (roof_base_y - roof_apex) + hw = roof_half * et + d.line([(cx - hw + 2, ry), (cx + hw - 2, ry)], fill=roof_lo + (160,), width=1) + + # ---- spire + glowing orb ---- + d.line([(cx, roof_apex), (cx, roof_apex - 14)], fill=outline + (255,), width=2) + orb_y = roof_apex - 18 + halo = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + hd = ImageDraw.Draw(halo) + hd.ellipse([cx - 18, orb_y - 18, cx + 18, orb_y + 18], fill=glow + (120,)) + halo = halo.filter(ImageFilter.GaussianBlur(5)) + img.alpha_composite(halo) + d.ellipse([cx - 6, orb_y - 6, cx + 6, orb_y + 6], fill=glow + (255,)) + d.ellipse([cx - 3, orb_y - 4, cx + 2, orb_y + 1], fill=glow_core + (255,)) + + # ---- floating rock base ---- + rb_y = body_bot + pts = [(cx - bot_half - 2, rb_y - 2), (cx + bot_half + 2, rb_y - 2), + (cx + bot_half - 6, rb_y + 18), (cx + 14, rb_y + 30), + (cx - 6, rb_y + 40), (cx - 26, rb_y + 26), (cx - bot_half + 2, rb_y + 16)] + d.polygon(pts, fill=rock + (255,)) + # rock top light / bottom shadow + d.line([(cx - bot_half - 2, rb_y - 2), (cx + bot_half + 2, rb_y - 2)], fill=rock_hi + (255,), width=2) + for (px, py) in [(cx - 18, rb_y + 12), (cx + 10, rb_y + 16), (cx - 2, rb_y + 26)]: + d.line([(px, py), (px - 3, py + 8)], fill=rock_lo + (255,), width=1) + d.polygon(pts, outline=outline + (255,)) + # a couple of drifting pebbles below + for (px, py, s) in [(cx - 30, rb_y + 44, 3), (cx + 22, rb_y + 38, 2), (cx + 4, rb_y + 52, 2)]: + d.ellipse([px - s, py - s, px + s, py + s], fill=rock + (255,), outline=outline + (255,)) + + # base magical underglow + halo = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + hd = ImageDraw.Draw(halo) + hd.ellipse([cx - 40, rb_y + 8, cx + 40, rb_y + 48], fill=(120, 150, 255, 90)) + halo = halo.filter(ImageFilter.GaussianBlur(7)) + base = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + base.alpha_composite(halo) + base.alpha_composite(img) + img = base + + big = img.resize((W * scale, H * scale), Image.NEAREST) + return big + + +# ---------------------------------------------------------------- sky +def make_sky(): + W, H = 1280, 720 + img = Image.new("RGBA", (W, H), (0, 0, 0, 255)) + top = (18, 16, 38) + bot = (6, 6, 14) + for y in range(H): + t = y / H + img.paste(lerp(top, bot, t) + (255,), [0, y, W, y + 1]) + d = ImageDraw.Draw(img) + + rng = random.Random(7) + # stars + for _ in range(220): + x = rng.randint(0, W - 1) + y = rng.randint(0, int(H * 0.78)) + b = rng.randint(90, 230) + s = rng.choice([1, 1, 1, 2]) + d.ellipse([x, y, x + s, y + s], fill=(b, b, min(255, b + 30), 255)) + # a few brighter stars with glow + glow = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + gd = ImageDraw.Draw(glow) + for _ in range(14): + x = rng.randint(40, W - 40) + y = rng.randint(30, int(H * 0.6)) + gd.ellipse([x - 5, y - 5, x + 5, y + 5], fill=(200, 215, 255, 120)) + glow = glow.filter(ImageFilter.GaussianBlur(4)) + img.alpha_composite(glow) + + # moon upper-right with soft halo + mx, my, mr = 1050, 150, 70 + halo = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + hd = ImageDraw.Draw(halo) + hd.ellipse([mx - mr - 40, my - mr - 40, mx + mr + 40, my + mr + 40], fill=(180, 195, 240, 70)) + halo = halo.filter(ImageFilter.GaussianBlur(18)) + img.alpha_composite(halo) + d.ellipse([mx - mr, my - mr, mx + mr, my + mr], fill=(222, 228, 244, 255)) + d.ellipse([mx - mr + 18, my - mr + 10, mx + mr + 18, my + mr + 10], fill=(18, 16, 38, 255)) # crescent cut + # craters + for (cxp, cyp, cs) in [(mx - 20, my - 10, 8), (mx - 4, my + 18, 5), (mx - 30, my + 14, 4)]: + d.ellipse([cxp - cs, cyp - cs, cxp + cs, cyp + cs], fill=(200, 206, 224, 255)) + + # bottom vignette + vg = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + vd = ImageDraw.Draw(vg) + vd.rectangle([0, H - 160, W, H], fill=(0, 0, 0, 120)) + vg = vg.filter(ImageFilter.GaussianBlur(40)) + img.alpha_composite(vg) + return img + + +def main(): + OUT.mkdir(parents=True, exist_ok=True) + PREVIEW.mkdir(parents=True, exist_ok=True) + tower = make_tower() + sky = make_sky() + tower.save(OUT / "TitleTower.png") + sky.save(OUT / "TitleSky.png") + # previews on a dark bg + bg = Image.new("RGB", (tower.width + 80, tower.height + 80), (16, 16, 32)) + bg.paste(tower, (40, 40), tower) + bg.save(PREVIEW / "preview_tower.png") + sky.convert("RGB").resize((640, 360)).save(PREVIEW / "preview_sky.png") + print(f"tower {tower.size} -> {OUT/'TitleTower.png'}") + print(f"sky {sky.size} -> {OUT/'TitleSky.png'}") + + +if __name__ == "__main__": + main() diff --git a/scripts/import-character-pack.py b/scripts/import-character-pack.py new file mode 100644 index 0000000..76ccee0 --- /dev/null +++ b/scripts/import-character-pack.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""'Zelda-like tilesets and sprites' (ArMM1998, CC0) 캐릭터 시트를 +Magic Exam Hall의 드롭인 규약 프레임으로 슬라이스·리컬러한다. + +원본: https://opengameart.org/content/zelda-like-tilesets-and-sprites (gfx_3.zip) +시트 배치: character.png 16x32 셀, 행 0=down 1=right 2=up 3=left, + 열 0-3 = 걷기 4프레임, x=80~127 = 팔 들어올리기 3프레임(시전용). + +산출: + Resources/Sprites/Player/ — idle/walk 방향별 + cast 공용 (PlayerSpriteLibrary 규약, PPU32) + Resources/Sprites/ — 멘토 15종 (PixelSpriteKind 이름, 무드별 포즈 + 로브색, PPU16) + +플레이어는 PlayerSpriteLibrary가 PPU32로, 멘토는 PixelArtFactory가 PPU16으로 로드하므로 +같은 1x2 월드 크기를 내려면 플레이어만 2배 업스케일한다. + +사용: python scripts/import-character-pack.py +""" + +import sys +from pathlib import Path + +from PIL import Image + +REPO = Path(__file__).resolve().parent.parent +SPRITES = REPO / "unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites" +PLAYER_OUT = SPRITES / "Player" + +CELL_W, CELL_H = 16, 32 +PLAYER_SCALE = 2 # 16x32 -> 32x64, PPU32 기준 1x2 유닛 +MENTOR_SCALE = 1 # 16x32 그대로, PPU16 기준 1x2 유닛 +ROW_BY_FACING = {"down": 0, "right": 1, "up": 2, "left": 3} +CAST_CELLS_X = (80, 96, 112) # 팔 들어올리기 3프레임 (down 행) + +# 플레이어 로브(파랑 계열) — ExamGameController의 기존 fallback robe 색과 동일 계열 +PLAYER_ROBE = (78, 148, 235) + +# MentorProfile.ForFloor의 로브 색 (0-1 -> 0-255) +MENTOR_ROBES = { + "Mentor": (133, 82, 219), # 1층 발착층 조교 (보라) + "MentorScholar": (46, 158, 184), # 2층 벽화 연구원 (청록) + "MentorGuide": (107, 179, 92), # 3층 다리 안내원 (초록) + "MentorWatcher": (179, 61, 71), # 4층 균열 감시자 (적색) + "MentorArchivist": (64, 92, 199), # 5층 성좌 기록관 (남색) +} + + +def is_shirt(pixel) -> bool: + r, g, b, a = pixel + return a > 0 and r > g + 28 and r > b + 28 + + +def recolor(frame: Image.Image, robe) -> Image.Image: + """셔츠(붉은 계열)만 로브 색으로 바꾸고 명암은 원본 비율을 유지한다.""" + out = frame.copy() + px = out.load() + rr, rg, rb = robe + for y in range(out.height): + for x in range(out.width): + p = px[x, y] + if is_shirt(p): + lum = max(p[0], 1) / 200.0 # 원본 빨강 밝기 -> 음영 비율 + lum = min(lum, 1.25) + px[x, y] = ( + min(int(rr * lum), 255), + min(int(rg * lum), 255), + min(int(rb * lum), 255), + p[3], + ) + return out + + +def cell(sheet: Image.Image, x: int, row: int) -> Image.Image: + return sheet.crop((x, row * CELL_H, x + CELL_W, (row + 1) * CELL_H)) + + +def save(frame: Image.Image, path: Path, scale: int) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + frame.resize((frame.width * scale, frame.height * scale), Image.NEAREST).save(path) + + +def export_player(sheet: Image.Image) -> int: + count = 0 + for facing, row in ROW_BY_FACING.items(): + walk = [recolor(cell(sheet, i * CELL_W, row), PLAYER_ROBE) for i in range(4)] + for i, frame in enumerate(walk): + save(frame, PLAYER_OUT / f"walk_{facing}_{i}.png", PLAYER_SCALE) + count += 1 + for i, frame in enumerate((walk[0], walk[2])): + save(frame, PLAYER_OUT / f"idle_{facing}_{i}.png", PLAYER_SCALE) + count += 1 + + down = ROW_BY_FACING["down"] + cast = [recolor(cell(sheet, x, down), PLAYER_ROBE) for x in CAST_CELLS_X] + for i, frame in enumerate(cast): + save(frame, PLAYER_OUT / f"cast_charge_{i}.png", PLAYER_SCALE) + count += 1 + save(cast[2], PLAYER_OUT / "cast_release_0.png", PLAYER_SCALE) + save(recolor(cell(sheet, 0, down), PLAYER_ROBE), PLAYER_OUT / "cast_release_1.png", PLAYER_SCALE) + return count + 2 + + +def export_mentors(sheet: Image.Image) -> int: + """무드별 포즈: Neutral=정면 서기, Happy=팔 들어올리기, Frown=옆으로 돌아섬.""" + down, left = ROW_BY_FACING["down"], ROW_BY_FACING["left"] + poses = { + "Neutral": cell(sheet, 0, down), + "Happy": cell(sheet, CAST_CELLS_X[2], down), + "Frown": cell(sheet, 0, left), + } + count = 0 + for prefix, robe in MENTOR_ROBES.items(): + for mood, frame in poses.items(): + save(recolor(frame, robe), SPRITES / f"{prefix}{mood}.png", MENTOR_SCALE) + count += 1 + return count + + +def main() -> int: + if len(sys.argv) < 2: + print(__doc__) + return 1 + + sheet = Image.open(sys.argv[1]).convert("RGBA") + player = export_player(sheet) + mentors = export_mentors(sheet) + print(f"player frames: {player}, mentor overrides: {mentors}") + print(f"-> {PLAYER_OUT}") + print(f"-> {SPRITES} (mentors)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Editor/FloorScreenshotExporter.cs b/unity/MagicExamHall/Assets/MagicExamHall/Editor/FloorScreenshotExporter.cs index 3d1e67a..342e31d 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Editor/FloorScreenshotExporter.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Editor/FloorScreenshotExporter.cs @@ -26,6 +26,102 @@ public static void Export() Export(output); } + /// + /// Captures every boot/menu screen and each floor for a full UI review pass. + /// + [MenuItem("Magic Exam Hall/Export All UI Screens")] + public static void ExportAllScreens() + { + var output = GetArgument("-floorScreenshotOutput"); + if (string.IsNullOrWhiteSpace(output)) + { + output = Path.GetFullPath(Path.Combine(Application.dataPath, "..", "..", "..", "outputs", "unity-ui-screens")); + } + + ExportAllScreens(output); + } + + public static void ExportAllScreens(string output) + { + Directory.CreateDirectory(output); + EditorSceneManager.OpenScene(ScenePath); + + var controller = UnityEngine.Object.FindFirstObjectByType(); + if (controller == null) + { + throw new InvalidOperationException("ExamGameController was not found in the scene."); + } + + PrepareController(controller, startGame: false); + var boot = UnityEngine.Object.FindFirstObjectByType(); + if (boot == null) + { + throw new InvalidOperationException("GameBootController was not found in the scene."); + } + + var camera = controller.mainCamera != null ? controller.mainCamera : Camera.main; + var canvas = controller.canvas; + if (camera == null || canvas == null) + { + throw new InvalidOperationException("A camera and canvas are required for screenshots."); + } + + WithCaptureCanvas(camera, canvas, () => + { + Canvas.ForceUpdateCanvases(); + Capture(camera, output, "ui_01_title.png"); + + boot.ShowMainMenuForTests(); + Canvas.ForceUpdateCanvases(); + Capture(camera, output, "ui_02_menu.png"); + + InvokePrivate(boot, "ShowOptions", GameBootState.MainMenu); + Canvas.ForceUpdateCanvases(); + Capture(camera, output, "ui_03_options.png"); + + boot.StartNewGameForTests(); + controller.CloseFirstFloorLetterForTests(); + InvokePrivate(controller, "UpdateHud"); + HidePresentationOnlyControls(); + Canvas.ForceUpdateCanvases(); + Capture(camera, output, "ui_04_gameplay.png"); + + boot.ShowPauseForTests(); + Canvas.ForceUpdateCanvases(); + Capture(camera, output, "ui_05_pause.png"); + + boot.ResumeGameplayForTests(); + boot.ShowCodexForTests(); + Canvas.ForceUpdateCanvases(); + Capture(camera, output, "ui_06_codex.png"); + + boot.ResumeGameplayForTests(); + boot.ShowDiscoveryCodexForTests(); + Canvas.ForceUpdateCanvases(); + Capture(camera, output, "ui_07_codex_discovery.png"); + + boot.ResumeGameplayForTests(); + for (var floor = 0; floor < controller.FloorCount; floor++) + { + controller.LoadFloorForTests(floor); + DestroyStaleFloorRoots(floor + 1); + InvokePrivate(controller, "UpdateHud"); + HidePresentationOnlyControls(); + Canvas.ForceUpdateCanvases(); + Capture(camera, output, $"unity_floor_{floor + 1:00}.png"); + } + + controller.CompleteCurrentFloorForTests(); + controller.AdvanceFloorForTests(); + InvokePrivate(boot, "ShowEndingPrompt"); + HidePresentationOnlyControls(); + Canvas.ForceUpdateCanvases(); + Capture(camera, output, "ui_08_ending_report.png"); + }); + + Debug.Log($"All UI screenshots exported to {output}"); + } + public static void Export(string output) { Directory.CreateDirectory(output); @@ -37,7 +133,7 @@ public static void Export(string output) throw new InvalidOperationException("ExamGameController was not found in the scene."); } - PrepareController(controller); + PrepareController(controller, startGame: true); var camera = controller.mainCamera != null ? controller.mainCamera : Camera.main; if (camera == null) { @@ -58,7 +154,18 @@ public static void Export(string output) var originalPlane = canvas.planeDistance; camera.transform.position = new Vector3(0f, 0f, -10f); - PixelRenderSetup.ConfigureCamera(camera, ExamGameController.GameplayCameraOrthographicSize, camera.backgroundColor); + var pixelPerfect = PixelRenderSetup.ConfigureCamera(camera, ExamGameController.GameplayCameraOrthographicSize, camera.backgroundColor); + // The ScreenSpaceCamera HUD canvas is rendered THROUGH this camera, so + // the pixel-perfect 632x356 windowbox would downscale every HUD glyph and + // smear small text. Disable it during capture to render the world and HUD + // at the full 1280x720 target; the in-game HUD (ScreenSpaceOverlay) is + // already crisp, so this makes the screenshot faithful to what players see. + var pixelPerfectWasEnabled = pixelPerfect != null && pixelPerfect.enabled; + if (pixelPerfect != null) + { + pixelPerfect.enabled = false; + } + camera.orthographicSize = ExamGameController.GameplayCameraOrthographicSize; canvas.renderMode = RenderMode.ScreenSpaceCamera; canvas.worldCamera = camera; canvas.planeDistance = 1f; @@ -68,13 +175,19 @@ public static void Export(string output) for (var floor = 0; floor < controller.FloorCount; floor++) { controller.LoadFloorForTests(floor); + DestroyStaleFloorRoots(floor + 1); InvokePrivate(controller, "UpdateHud"); + HidePresentationOnlyControls(); Canvas.ForceUpdateCanvases(); Capture(camera, output, $"unity_floor_{floor + 1:00}.png"); } } finally { + if (pixelPerfect != null) + { + pixelPerfect.enabled = pixelPerfectWasEnabled; + } camera.transform.position = originalCameraPosition; camera.orthographic = originalOrthographic; camera.orthographicSize = originalOrthographicSize; @@ -86,12 +199,99 @@ public static void Export(string output) Debug.Log($"Floor screenshots exported to {output}"); } - private static void PrepareController(ExamGameController controller) + private static void WithCaptureCanvas(Camera camera, Canvas canvas, Action capture) + { + var originalCameraPosition = camera.transform.position; + var originalOrthographic = camera.orthographic; + var originalOrthographicSize = camera.orthographicSize; + var originalMode = canvas.renderMode; + var originalCamera = canvas.worldCamera; + var originalPlane = canvas.planeDistance; + var originalOverrideSorting = canvas.overrideSorting; + var originalSortingOrder = canvas.sortingOrder; + var childCanvases = canvas.GetComponentsInChildren(true); + var originalChildSortingOrders = new int[childCanvases.Length]; + for (var index = 0; index < childCanvases.Length; index++) + { + originalChildSortingOrders[index] = childCanvases[index].sortingOrder; + } + + camera.transform.position = new Vector3(0f, 0f, -10f); + PixelRenderSetup.ConfigureCamera(camera, ExamGameController.GameplayCameraOrthographicSize, camera.backgroundColor); + canvas.renderMode = RenderMode.ScreenSpaceCamera; + canvas.worldCamera = camera; + canvas.planeDistance = 1f; + canvas.overrideSorting = true; + canvas.sortingOrder = 10000; + var sortingOrderDelta = canvas.sortingOrder - originalSortingOrder; + for (var index = 0; index < childCanvases.Length; index++) + { + var childCanvas = childCanvases[index]; + if (childCanvas != canvas && childCanvas.overrideSorting) + { + childCanvas.sortingOrder = originalChildSortingOrders[index] + sortingOrderDelta; + } + } + + try + { + capture(); + } + finally + { + camera.transform.position = originalCameraPosition; + camera.orthographic = originalOrthographic; + camera.orthographicSize = originalOrthographicSize; + canvas.renderMode = originalMode; + canvas.worldCamera = originalCamera; + canvas.planeDistance = originalPlane; + canvas.overrideSorting = originalOverrideSorting; + canvas.sortingOrder = originalSortingOrder; + for (var index = 0; index < childCanvases.Length; index++) + { + childCanvases[index].sortingOrder = originalChildSortingOrders[index]; + } + } + } + + /// + /// Object.Destroy is deferred outside play mode, so remove floor roots left + /// behind by previously loaded floors before each screenshot. + /// + private static void DestroyStaleFloorRoots(int currentFloorNumber) + { + var expectedPrefix = $"Floor {currentFloorNumber} "; + foreach (var root in UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects()) + { + if (root.name.StartsWith("Floor ", StringComparison.Ordinal) && + !root.name.StartsWith(expectedPrefix, StringComparison.Ordinal)) + { + UnityEngine.Object.DestroyImmediate(root); + } + } + } + + private static void HidePresentationOnlyControls() + { + var floorSkipButton = GameObject.Find("Floor Test Skip Button"); + if (floorSkipButton != null) + { + floorSkipButton.SetActive(false); + } + } + + private static void PrepareController(ExamGameController controller, bool startGame) { if (controller.ActiveGoalCount == 0) { InvokePrivate(controller, "Awake"); } + + var boot = UnityEngine.Object.FindFirstObjectByType(); + if (startGame && boot != null && boot.StateForTests == GameBootState.Title) + { + boot.StartNewGameForTests(); + } } private static void Capture(Camera camera, string output, string fileName) diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts.meta new file mode 100644 index 0000000..1d0deed --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3cf0a426e5f811b4394fc018ff80a521 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri11-Bold.ttf b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri11-Bold.ttf new file mode 100644 index 0000000..fd8cdf0 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri11-Bold.ttf differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri11-Bold.ttf.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri11-Bold.ttf.meta new file mode 100644 index 0000000..a8ba7d0 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri11-Bold.ttf.meta @@ -0,0 +1,21 @@ +fileFormatVersion: 2 +guid: e61fc15efaf3058458b87aef2c92f7d2 +TrueTypeFontImporter: + externalObjects: {} + serializedVersion: 4 + fontSize: 16 + forceTextureCase: -2 + characterSpacing: 0 + characterPadding: 1 + includeFontData: 1 + fontNames: + - Galmuri11 + fallbackFontReferences: [] + customCharacters: + fontRenderingMode: 0 + ascentCalculationMode: 1 + useLegacyBoundsCalculation: 0 + shouldRoundAdvanceValue: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri11.ttf b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri11.ttf new file mode 100644 index 0000000..838b431 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri11.ttf differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri11.ttf.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri11.ttf.meta new file mode 100644 index 0000000..48212ec --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri11.ttf.meta @@ -0,0 +1,21 @@ +fileFormatVersion: 2 +guid: 2c3526f7878b0c049842460cf7878411 +TrueTypeFontImporter: + externalObjects: {} + serializedVersion: 4 + fontSize: 16 + forceTextureCase: -2 + characterSpacing: 0 + characterPadding: 1 + includeFontData: 1 + fontNames: + - Galmuri11 + fallbackFontReferences: [] + customCharacters: + fontRenderingMode: 0 + ascentCalculationMode: 1 + useLegacyBoundsCalculation: 0 + shouldRoundAdvanceValue: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri14.ttf b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri14.ttf new file mode 100644 index 0000000..676cae6 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri14.ttf differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri14.ttf.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri14.ttf.meta new file mode 100644 index 0000000..a37663c --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Fonts/Galmuri14.ttf.meta @@ -0,0 +1,21 @@ +fileFormatVersion: 2 +guid: b51fcd6d86af7bb4f852f023079bfb03 +TrueTypeFontImporter: + externalObjects: {} + serializedVersion: 4 + fontSize: 16 + forceTextureCase: -2 + characterSpacing: 0 + characterPadding: 1 + includeFontData: 1 + fontNames: + - Galmuri14 + fallbackFontReferences: [] + customCharacters: + fontRenderingMode: 0 + ascentCalculationMode: 1 + useLegacyBoundsCalculation: 0 + shouldRoundAdvanceValue: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech.meta new file mode 100644 index 0000000..4352811 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a951e86d681ddb8438497c8083bd2417 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorCorrect.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorCorrect.wav new file mode 100644 index 0000000..bd99e86 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorCorrect.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorCorrect.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorCorrect.wav.meta new file mode 100644 index 0000000..47af3e2 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorCorrect.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 1f46b6a52630abd4ab24c14a30aff573 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorFloorClear.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorFloorClear.wav new file mode 100644 index 0000000..020273e Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorFloorClear.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorFloorClear.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorFloorClear.wav.meta new file mode 100644 index 0000000..f4aad61 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorFloorClear.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: e71fbd507112afd43804f1f53d2688b1 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorFloorEnter.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorFloorEnter.wav new file mode 100644 index 0000000..976b183 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorFloorEnter.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorFloorEnter.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorFloorEnter.wav.meta new file mode 100644 index 0000000..1499f09 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorFloorEnter.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: bae03708263bcda44b1b6bc3cb4dea5a +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorNoteOpen.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorNoteOpen.wav new file mode 100644 index 0000000..84914cd Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorNoteOpen.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorNoteOpen.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorNoteOpen.wav.meta new file mode 100644 index 0000000..1f2dacd --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorNoteOpen.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 2f26fc2120ce4bf4b9e99338c3ef1b63 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorNotePage.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorNotePage.wav new file mode 100644 index 0000000..e268111 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorNotePage.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorNotePage.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorNotePage.wav.meta new file mode 100644 index 0000000..3660dff --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorNotePage.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: ec09c36e5d49e5b409e2533e2c3ca4b0 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorWrong.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorWrong.wav new file mode 100644 index 0000000..dd1ee74 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorWrong.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorWrong.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorWrong.wav.meta new file mode 100644 index 0000000..3ef8d9d --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSpeech/MentorWrong.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 9d9787a6b4feeab439a800b396d8f3e2 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites.meta new file mode 100644 index 0000000..61110d5 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a35328dcd70ef6743a7b5afd64468738 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_frown.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_frown.png new file mode 100644 index 0000000..77c69e2 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_frown.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_frown.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_frown.png.meta new file mode 100644 index 0000000..8a23178 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_frown.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: feb7441daa658c14db68a64a6332e658 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_happy.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_happy.png new file mode 100644 index 0000000..7ac299d Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_happy.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_happy.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_happy.png.meta new file mode 100644 index 0000000..33527ae --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_happy.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: eb01809a2d28e994a975c5ae9051145b +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_neutral.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_neutral.png new file mode 100644 index 0000000..f56a06e Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_neutral.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_neutral.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_neutral.png.meta new file mode 100644 index 0000000..e7ffe6a --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor1_TutorialMentor_neutral.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: af59a8314f67d2f45b86b6480e7d096c +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher.png new file mode 100644 index 0000000..821c140 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher.png.meta new file mode 100644 index 0000000..4e19c7b --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 39298291cb6a9f5408c7613c534e0f28 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_frown.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_frown.png new file mode 100644 index 0000000..f510e30 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_frown.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_frown.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_frown.png.meta new file mode 100644 index 0000000..421c33f --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_frown.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 2cc94073cc3658c4c90267b9fbc7c1fe +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_happy.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_happy.png new file mode 100644 index 0000000..1faf90e Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_happy.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_happy.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_happy.png.meta new file mode 100644 index 0000000..f64fe58 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_happy.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: e45ed27310f7fe349b231a577229f5ea +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_neutral.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_neutral.png new file mode 100644 index 0000000..5f043da Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_neutral.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_neutral.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_neutral.png.meta new file mode 100644 index 0000000..60e173e --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor2_MuralResearcher_neutral.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 5631cb6688a8f484db2e364bff6d43f1 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide.png new file mode 100644 index 0000000..c176ffa Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide.png.meta new file mode 100644 index 0000000..09fd2f2 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 87f17aa2f2dffa448a4c6eb2a625b620 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_frown.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_frown.png new file mode 100644 index 0000000..c9f5687 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_frown.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_frown.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_frown.png.meta new file mode 100644 index 0000000..b61c284 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_frown.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 5e034ed3dc41f6c4082e16fde87b437e +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_happy.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_happy.png new file mode 100644 index 0000000..e42f3ee Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_happy.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_happy.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_happy.png.meta new file mode 100644 index 0000000..14883b9 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_happy.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: c8606162735acfa4cb8d5bb463068880 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_neutral.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_neutral.png new file mode 100644 index 0000000..c176ffa Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_neutral.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_neutral.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_neutral.png.meta new file mode 100644 index 0000000..e53fdba --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor3_BridgeGuide_neutral.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 61e644d10719cdb4482eddbd4e6e5e7a +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher.png new file mode 100644 index 0000000..59580f5 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher.png.meta new file mode 100644 index 0000000..e70efff --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: dd677ec6a65b3204db78517aa3c24d86 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_frown.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_frown.png new file mode 100644 index 0000000..d74e36d Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_frown.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_frown.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_frown.png.meta new file mode 100644 index 0000000..284de5e --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_frown.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: c94c84038ca2858438e3679587b622e8 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_happy.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_happy.png new file mode 100644 index 0000000..2462883 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_happy.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_happy.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_happy.png.meta new file mode 100644 index 0000000..8dbef89 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_happy.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 68085406cfbbed74c8f31ea4cf767e86 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_neutral.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_neutral.png new file mode 100644 index 0000000..559db2e Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_neutral.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_neutral.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_neutral.png.meta new file mode 100644 index 0000000..bb16abd --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor4_RiftWatcher_neutral.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: f6d02cb5d86c38441aabed53337dda41 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist.png new file mode 100644 index 0000000..2e7cf83 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist.png.meta new file mode 100644 index 0000000..9cf63d7 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 97ef7fd194ad87b4aa37c25f6debd095 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_frown.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_frown.png new file mode 100644 index 0000000..9d787bb Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_frown.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_frown.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_frown.png.meta new file mode 100644 index 0000000..9558c96 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_frown.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 027405c8a9df22e4d9c4c740c35a8249 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_happy.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_happy.png new file mode 100644 index 0000000..0ccd618 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_happy.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_happy.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_happy.png.meta new file mode 100644 index 0000000..459269f --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_happy.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 00de6716bc21af54faf9dcd77bebdf59 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_neutral.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_neutral.png new file mode 100644 index 0000000..2e7cf83 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_neutral.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_neutral.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_neutral.png.meta new file mode 100644 index 0000000..3a9945e --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/MentorSprites/Floor5_StarArchivist_neutral.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 97a0edfbff8cdf94fb2ac54f05c027e7 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx.meta new file mode 100644 index 0000000..75a6df9 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 99636091a71d50240acac41eb368859f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02.meta new file mode 100644 index 0000000..b732b47 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2359bc2d3be219142b4aaca206dd40b7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardMultiplev1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardMultiplev1.wav new file mode 100644 index 0000000..758a17d Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardMultiplev1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardMultiplev1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardMultiplev1.wav.meta new file mode 100644 index 0000000..efa3d8b --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardMultiplev1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: b2c716dd8378e3044859f956feb8b4da +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardMultiplev2.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardMultiplev2.wav new file mode 100644 index 0000000..3cbe22c Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardMultiplev2.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardMultiplev2.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardMultiplev2.wav.meta new file mode 100644 index 0000000..5274966 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardMultiplev2.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 86dd2ca7382ec0043a682974af264e80 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev1.wav new file mode 100644 index 0000000..d9436b0 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev1.wav.meta new file mode 100644 index 0000000..a89fabe --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 5bb3a33ce1e21cc4b9f4c38cb4339b42 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev10.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev10.wav new file mode 100644 index 0000000..d7953fc Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev10.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev10.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev10.wav.meta new file mode 100644 index 0000000..03f46f9 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev10.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 33d6f5297c0e149498359588e3de599f +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev2.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev2.wav new file mode 100644 index 0000000..cfe52af Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev2.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev2.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev2.wav.meta new file mode 100644 index 0000000..bc357a4 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev2.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 4681359f49ba6fe41a960f7b5856eed7 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev3.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev3.wav new file mode 100644 index 0000000..2421817 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev3.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev3.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev3.wav.meta new file mode 100644 index 0000000..f2553c8 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev3.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 687080e779d62c8468439c810315928e +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev4.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev4.wav new file mode 100644 index 0000000..f200723 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev4.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev4.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev4.wav.meta new file mode 100644 index 0000000..46bfeb4 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev4.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 37c79dc88cdeb1d4fa021b6f0e022531 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev5.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev5.wav new file mode 100644 index 0000000..50407c3 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev5.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev5.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev5.wav.meta new file mode 100644 index 0000000..d97c0f0 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev5.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 94737a47398dcb741aa2b14f76b242cc +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev6.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev6.wav new file mode 100644 index 0000000..b6eb642 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev6.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev6.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev6.wav.meta new file mode 100644 index 0000000..70e284c --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev6.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 98eacbba03b17594a82c171953d59793 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev7.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev7.wav new file mode 100644 index 0000000..0f6172e Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev7.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev7.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev7.wav.meta new file mode 100644 index 0000000..a6910dc --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev7.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: af599ae8cc7e25145bf8bbe2781f63a8 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev8.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev8.wav new file mode 100644 index 0000000..aa57f91 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev8.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev8.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev8.wav.meta new file mode 100644 index 0000000..1e8cc96 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev8.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: d0a29a1ee3bcfa04e8a6e5b24ef2303a +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev9.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev9.wav new file mode 100644 index 0000000..2d4e974 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev9.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev9.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev9.wav.meta new file mode 100644 index 0000000..66af86b --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/BlackBoard02/SFX_BlackBoardSinglev9.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: b3ca4d2300a7a2548827ec96729ae36d +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01.meta new file mode 100644 index 0000000..9fb7d5b --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 31cc1ac4bd73e8f49aaabee749f78207 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev1.wav new file mode 100644 index 0000000..84914cd Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev1.wav.meta new file mode 100644 index 0000000..2297ca0 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 6d730eac56a240b4f835ad1d1b5cb337 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev2.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev2.wav new file mode 100644 index 0000000..bd99e86 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev2.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev2.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev2.wav.meta new file mode 100644 index 0000000..76409ca --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev2.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: d2fa5575601540547897f72ca01488d6 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev3.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev3.wav new file mode 100644 index 0000000..8ff8a06 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev3.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev3.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev3.wav.meta new file mode 100644 index 0000000..6061981 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev3.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: e667af8bc8fe9e844a5d97b160cd8743 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev4.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev4.wav new file mode 100644 index 0000000..4704185 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev4.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev4.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev4.wav.meta new file mode 100644 index 0000000..46dc62c --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastCompletev4.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: c94922db81d17c34bb4217da8d86f707 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev1.wav new file mode 100644 index 0000000..dd60921 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev1.wav.meta new file mode 100644 index 0000000..31849b7 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: ec9ef81dc00647c4db6e30f0b8fcb11f +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev2.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev2.wav new file mode 100644 index 0000000..8e243b6 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev2.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev2.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev2.wav.meta new file mode 100644 index 0000000..21910fa --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev2.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: f06b1f968b7084d4bb83b4ad1918f967 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev3.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev3.wav new file mode 100644 index 0000000..6d8ba79 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev3.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev3.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev3.wav.meta new file mode 100644 index 0000000..42e1fcf --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev3.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 75dde99eb8697cd4db31a3d1114024c5 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev4.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev4.wav new file mode 100644 index 0000000..bcfd976 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev4.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev4.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev4.wav.meta new file mode 100644 index 0000000..1192507 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev4.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 1d8007626a2a5b34aa1085ff5479d357 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev5.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev5.wav new file mode 100644 index 0000000..027f38a Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev5.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev5.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev5.wav.meta new file mode 100644 index 0000000..1de7521 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev5.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 8867fe86b9a17fd448d09f91823f05be +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev6.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev6.wav new file mode 100644 index 0000000..b3b64d8 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev6.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev6.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev6.wav.meta new file mode 100644 index 0000000..8a2cac5 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev6.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: fd548636eea26164bae473545a13f34a +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev7.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev7.wav new file mode 100644 index 0000000..622c657 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev7.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev7.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev7.wav.meta new file mode 100644 index 0000000..ec4ecb0 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Fast01/SFX_FastSinglev7.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 82e53c8a3408f2b42b348802c7c24bb9 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01.meta new file mode 100644 index 0000000..0bacb1a --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 66df079b6270e4b4bb4584564ab9ef54 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv1.wav new file mode 100644 index 0000000..19fc6f4 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv1.wav.meta new file mode 100644 index 0000000..0258802 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 45ea44762dd6b534b97207153076f152 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv2.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv2.wav new file mode 100644 index 0000000..4b86996 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv2.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv2.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv2.wav.meta new file mode 100644 index 0000000..51ee9da --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv2.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: cc966f77c1523a74598a6967d250c890 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv3.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv3.wav new file mode 100644 index 0000000..560231a Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv3.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv3.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv3.wav.meta new file mode 100644 index 0000000..8eb5b14 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/HydrographicPen01/SFX_HydrographicPenv3.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: f24f59dd830a66144953ab9f3de86a42 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01.meta new file mode 100644 index 0000000..32cd7fb --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 32ea64aae1fdb1e4bba738caba0fddff +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_PenAndPaperv1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_PenAndPaperv1.wav new file mode 100644 index 0000000..bdceb73 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_PenAndPaperv1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_PenAndPaperv1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_PenAndPaperv1.wav.meta new file mode 100644 index 0000000..8355a3b --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_PenAndPaperv1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 7ebe50c6bb97c94468bce2e791464479 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv1.wav new file mode 100644 index 0000000..bffeeef Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv1.wav.meta new file mode 100644 index 0000000..00c3d66 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: d604da2be36f552438eb0b7c6fa10abc +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv10.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv10.wav new file mode 100644 index 0000000..b34fbd7 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv10.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv10.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv10.wav.meta new file mode 100644 index 0000000..f5aab0e --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv10.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 570f730a66d5edd47841f5794b03997e +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv11.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv11.wav new file mode 100644 index 0000000..34349f9 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv11.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv11.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv11.wav.meta new file mode 100644 index 0000000..ee35391 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv11.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: ae4dbcfe87774d442a645185aa2e4018 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv12.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv12.wav new file mode 100644 index 0000000..697e11c Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv12.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv12.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv12.wav.meta new file mode 100644 index 0000000..1acfc88 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv12.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: aaa3ae2d1fb214344961bf681febd587 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv13.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv13.wav new file mode 100644 index 0000000..169ee99 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv13.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv13.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv13.wav.meta new file mode 100644 index 0000000..5bda722 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv13.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: a1d0e5a05055f4f4994e0db599b783e5 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv14.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv14.wav new file mode 100644 index 0000000..dc9d0f7 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv14.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv14.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv14.wav.meta new file mode 100644 index 0000000..29b422c --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv14.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: d85bf5d569523024faca879df1df82c7 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv15.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv15.wav new file mode 100644 index 0000000..00aae92 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv15.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv15.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv15.wav.meta new file mode 100644 index 0000000..09b41ee --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv15.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 92dc749a58173634db8bf1cc19f7f4ef +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv16.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv16.wav new file mode 100644 index 0000000..8f77bda Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv16.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv16.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv16.wav.meta new file mode 100644 index 0000000..8a665ea --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv16.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: b4f3c3e70123340498cf1f83b5ea08f8 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv17.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv17.wav new file mode 100644 index 0000000..55b9b76 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv17.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv17.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv17.wav.meta new file mode 100644 index 0000000..eb84a5d --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv17.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 43a7ec222143c404bbcbeca878756583 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv18.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv18.wav new file mode 100644 index 0000000..d463f3f Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv18.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv18.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv18.wav.meta new file mode 100644 index 0000000..a009713 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv18.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 695bec1330ff69943b14f234edd4bd59 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv2.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv2.wav new file mode 100644 index 0000000..1bc4267 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv2.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv2.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv2.wav.meta new file mode 100644 index 0000000..86141d2 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv2.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: cb89a3d680a4f9445b95286b0c526cf6 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv3.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv3.wav new file mode 100644 index 0000000..67c123d Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv3.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv3.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv3.wav.meta new file mode 100644 index 0000000..6746c62 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv3.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: c173021d764346547a4a1983e4c5a458 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv4.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv4.wav new file mode 100644 index 0000000..eeb4270 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv4.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv4.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv4.wav.meta new file mode 100644 index 0000000..157ae76 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv4.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: f43aec082bcd2da48a8199db7db2ef0b +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv5.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv5.wav new file mode 100644 index 0000000..cce069f Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv5.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv5.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv5.wav.meta new file mode 100644 index 0000000..c5ff927 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv5.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: a812e64187d6601468ce3702350613f7 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv6.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv6.wav new file mode 100644 index 0000000..aed5589 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv6.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv6.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv6.wav.meta new file mode 100644 index 0000000..adbe4ae --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv6.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 7d9ea242e5351e5438e611cf77928d9d +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv7.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv7.wav new file mode 100644 index 0000000..7f23ebf Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv7.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv7.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv7.wav.meta new file mode 100644 index 0000000..bdf8495 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv7.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: fc5658e2ad252f146ba45991e47355dc +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv8.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv8.wav new file mode 100644 index 0000000..7ff2211 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv8.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv8.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv8.wav.meta new file mode 100644 index 0000000..5cffb9a --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv8.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 8e3bba87642207f4c84db36bb7269e08 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv9.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv9.wav new file mode 100644 index 0000000..a4bc429 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv9.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv9.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv9.wav.meta new file mode 100644 index 0000000..b265940 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/PenAndPaper01/SFX_Penv9.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: d4c3df0dd44da334f8719ca9fb51c4ff +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01.meta new file mode 100644 index 0000000..11b7fec --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 45e99c7dfcc96064984318cc89c02e26 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev1.wav new file mode 100644 index 0000000..bb82a42 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev1.wav.meta new file mode 100644 index 0000000..25c06ba --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 0ab73bea6e47e5e4493a530ad60b9831 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev2.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev2.wav new file mode 100644 index 0000000..6e73b2a Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev2.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev2.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev2.wav.meta new file mode 100644 index 0000000..0aecb0f --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev2.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 141f759dea2519243bb79e93e79fb8cf +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev3.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev3.wav new file mode 100644 index 0000000..68609d5 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev3.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev3.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev3.wav.meta new file mode 100644 index 0000000..17f00d4 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev3.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 10426e3d0cb48ef4eaa2a7699eba8384 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev4.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev4.wav new file mode 100644 index 0000000..0b9d494 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev4.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev4.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev4.wav.meta new file mode 100644 index 0000000..0da91ed --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev4.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 7b57b2b55c48c0347ae7f2741b5ee87d +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev5.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev5.wav new file mode 100644 index 0000000..82698a5 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev5.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev5.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev5.wav.meta new file mode 100644 index 0000000..5c30bab --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroMultiplev5.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 7d00c3f6770894745bda305144fd3ed8 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev1.wav new file mode 100644 index 0000000..e268111 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev1.wav.meta new file mode 100644 index 0000000..049cc10 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: f4bbc1d5a914d9647b8b9bef5138ef7b +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev2.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev2.wav new file mode 100644 index 0000000..0d988e8 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev2.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev2.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev2.wav.meta new file mode 100644 index 0000000..d464e68 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev2.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 26503ebd70b5bf345ba0281d2f013d16 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev3.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev3.wav new file mode 100644 index 0000000..586a3ee Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev3.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev3.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev3.wav.meta new file mode 100644 index 0000000..f854937 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev3.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: e1b0009a88103ae47869f714454bfbaa +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev4.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev4.wav new file mode 100644 index 0000000..0082b44 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev4.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev4.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev4.wav.meta new file mode 100644 index 0000000..b139fc2 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev4.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 32873a24f3348a943b93b62c8e99822c +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev5.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev5.wav new file mode 100644 index 0000000..477790d Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev5.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev5.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev5.wav.meta new file mode 100644 index 0000000..127998a --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro01/SFX_RetroSinglev5.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 89e9acce6bf09c041857c3dda026f5b3 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02.meta new file mode 100644 index 0000000..a4570bd --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b2587e730bfc459459264b0b8d13cd63 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev1.wav new file mode 100644 index 0000000..47b420e Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev1.wav.meta new file mode 100644 index 0000000..5762b64 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 52f4ad3443ffb154e8425b89d70496d9 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev2.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev2.wav new file mode 100644 index 0000000..6905b14 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev2.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev2.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev2.wav.meta new file mode 100644 index 0000000..5a16c96 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev2.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: a90f0bf5e756237459447aa647b84389 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev3.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev3.wav new file mode 100644 index 0000000..6d0fde1 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev3.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev3.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev3.wav.meta new file mode 100644 index 0000000..41893a2 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev3.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 51979956b028e164aa21c95a685ef6a4 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev4.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev4.wav new file mode 100644 index 0000000..c4e84cb Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev4.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev4.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev4.wav.meta new file mode 100644 index 0000000..904ee70 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev4.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: bf0bb520dfe4a9c4987d8309a6ea6985 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev5.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev5.wav new file mode 100644 index 0000000..52365d0 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev5.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev5.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev5.wav.meta new file mode 100644 index 0000000..499c920 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroMultiplev5.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 26c8970f458b18e488e1e2b942c2a7e1 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev1.wav new file mode 100644 index 0000000..136ce4e Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev1.wav.meta new file mode 100644 index 0000000..dfbc18f --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 9e8fa38d792c21b4a961e76934626b70 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev2.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev2.wav new file mode 100644 index 0000000..900389c Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev2.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev2.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev2.wav.meta new file mode 100644 index 0000000..e14c775 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev2.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 9ab8718b63d2cb3449cf38ffc9ec647e +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev3.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev3.wav new file mode 100644 index 0000000..457cc69 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev3.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev3.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev3.wav.meta new file mode 100644 index 0000000..90d5358 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev3.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 187120554e5fe1d49bc0be0254c3aa52 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev4.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev4.wav new file mode 100644 index 0000000..e2334d7 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev4.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev4.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev4.wav.meta new file mode 100644 index 0000000..10aa170 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev4.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: f63be77be12ff40469796913e9204394 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev5.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev5.wav new file mode 100644 index 0000000..083330d Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev5.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev5.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev5.wav.meta new file mode 100644 index 0000000..0878e44 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro02/SFX_RetroSinglev5.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 9b47b925b60fbfb43bca5cab78b4234b +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03.meta new file mode 100644 index 0000000..d8df50d --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bcb6f51c0ac95b743abfdae6bcde03ae +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev1.wav new file mode 100644 index 0000000..020273e Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev1.wav.meta new file mode 100644 index 0000000..975df72 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: c52020482ed41d34a9271df5b703eabf +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev2.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev2.wav new file mode 100644 index 0000000..d97fbbc Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev2.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev2.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev2.wav.meta new file mode 100644 index 0000000..8118e04 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev2.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: da9c6543e8e7e65498d522ef0150543b +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev3.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev3.wav new file mode 100644 index 0000000..c8e68c0 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev3.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev3.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev3.wav.meta new file mode 100644 index 0000000..959bcc4 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev3.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 652032e937144304f95d592ffc28c02d +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev4.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev4.wav new file mode 100644 index 0000000..ef76c97 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev4.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev4.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev4.wav.meta new file mode 100644 index 0000000..3dc7506 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev4.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 5257fa4bad6378c4990c7fe0d692b15b +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev5.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev5.wav new file mode 100644 index 0000000..2cfed0b Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev5.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev5.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev5.wav.meta new file mode 100644 index 0000000..3c08959 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev5.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 2f187826e1aaaf9449427b954edb3929 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev6.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev6.wav new file mode 100644 index 0000000..ee4a417 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev6.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev6.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev6.wav.meta new file mode 100644 index 0000000..b246c17 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroMultiplev6.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 7e6b7eac452c4ba488fdbf1964079dee +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev1.wav new file mode 100644 index 0000000..7af6efe Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev1.wav.meta new file mode 100644 index 0000000..249b6fc --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 5a8e0df4686c90b4f82b2c573a037807 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev2.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev2.wav new file mode 100644 index 0000000..3d91cef Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev2.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev2.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev2.wav.meta new file mode 100644 index 0000000..24d50bd --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev2.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 2fc8b2a0a6ffb6d4fb98bad0320abaee +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev3.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev3.wav new file mode 100644 index 0000000..677c4be Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev3.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev3.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev3.wav.meta new file mode 100644 index 0000000..65d3f37 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev3.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 43a5226f48364b242b5ff96d127fdff9 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev4.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev4.wav new file mode 100644 index 0000000..c039dcc Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev4.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev4.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev4.wav.meta new file mode 100644 index 0000000..94849f5 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev4.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 4a8b4924a0fee214297a2ede4cc35429 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev5.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev5.wav new file mode 100644 index 0000000..7c60e92 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev5.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev5.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev5.wav.meta new file mode 100644 index 0000000..eb851cc --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev5.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 614cf3588245a1548aa9ad9929adf48d +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev6.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev6.wav new file mode 100644 index 0000000..039142a Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev6.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev6.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev6.wav.meta new file mode 100644 index 0000000..66b423f --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Retro03/SFX_RetroSinglev6.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 448068e06e765614083a67c5b6c70b73 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01.meta new file mode 100644 index 0000000..b8e83c2 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bc05dce0c039e2b47b78e66a7bc2bc7f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev1.wav new file mode 100644 index 0000000..529a205 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev1.wav.meta new file mode 100644 index 0000000..f24e3cb --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: f049edfdde625f246b7b2549f854e869 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev2.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev2.wav new file mode 100644 index 0000000..de48db4 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev2.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev2.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev2.wav.meta new file mode 100644 index 0000000..8b7a6db --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev2.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 68e05c159c5ea2148b8b6a49cfd30f38 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev3.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev3.wav new file mode 100644 index 0000000..b03c25b Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev3.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev3.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev3.wav.meta new file mode 100644 index 0000000..23dee73 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev3.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 78f988053621b9c4c9f0e7f398384b7a +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev4.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev4.wav new file mode 100644 index 0000000..1a3710b Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev4.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev4.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev4.wav.meta new file mode 100644 index 0000000..d98b85c --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev4.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 5336e14631f331940b9ff8206cc91f06 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev5.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev5.wav new file mode 100644 index 0000000..113e49a Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev5.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev5.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev5.wav.meta new file mode 100644 index 0000000..be4663e --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonMultiplev5.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: fe645d50b76e5cc4eb05ee2deca6f896 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev1.wav new file mode 100644 index 0000000..f8d1361 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev1.wav.meta new file mode 100644 index 0000000..c7c83b5 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: a0b76dab860a06849992d0a1c89a0d0b +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev10.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev10.wav new file mode 100644 index 0000000..1949c1d Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev10.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev10.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev10.wav.meta new file mode 100644 index 0000000..3023383 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev10.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 4e758e9116ee4c246b1bcc936257c28f +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev11.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev11.wav new file mode 100644 index 0000000..8f8f190 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev11.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev11.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev11.wav.meta new file mode 100644 index 0000000..49b745e --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev11.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: fdad17610d4d0ce45a02f6d08e743ab8 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev2.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev2.wav new file mode 100644 index 0000000..f41f660 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev2.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev2.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev2.wav.meta new file mode 100644 index 0000000..07ccc12 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev2.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 13ee17a2e1d3a3c4caf3e51141cc0ced +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev3.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev3.wav new file mode 100644 index 0000000..e928380 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev3.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev3.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev3.wav.meta new file mode 100644 index 0000000..9d72256 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev3.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 68b015aaa53b4e64d9e15e5284f29704 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev4.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev4.wav new file mode 100644 index 0000000..95b4a86 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev4.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev4.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev4.wav.meta new file mode 100644 index 0000000..087436e --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev4.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: c25cc90a5dcf7ab4383a1fc1f73d094b +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev5.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev5.wav new file mode 100644 index 0000000..bd198e1 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev5.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev5.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev5.wav.meta new file mode 100644 index 0000000..2d4ad66 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev5.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: de253fcd71c09d740b0513961c99a10b +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev6.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev6.wav new file mode 100644 index 0000000..59a2307 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev6.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev6.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev6.wav.meta new file mode 100644 index 0000000..90d159e --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev6.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: f571b49fb8696d249857beefbbaee212 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev7.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev7.wav new file mode 100644 index 0000000..dd1ee74 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev7.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev7.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev7.wav.meta new file mode 100644 index 0000000..6f7ae89 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev7.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 1acc9117f71ff7b4d9926be65a4e131f +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev8.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev8.wav new file mode 100644 index 0000000..f943e09 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev8.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev8.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev8.wav.meta new file mode 100644 index 0000000..56996e0 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev8.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 5ad1f268ce0b4724cb771a5c1aeea1ea +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev9.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev9.wav new file mode 100644 index 0000000..3627427 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev9.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev9.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev9.wav.meta new file mode 100644 index 0000000..c1c6511 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Skeleton01/SFX_SkeletonSinglev9.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 57c7f885c903bc747a598cfc05d65f9b +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01.meta new file mode 100644 index 0000000..5e9e098 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dfaa0453293babe4a86e4d6cd6d6cf2e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev1.wav new file mode 100644 index 0000000..976b183 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev1.wav.meta new file mode 100644 index 0000000..ea20b60 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 9340f51ddc502aa4e832f287e3d022d5 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev2.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev2.wav new file mode 100644 index 0000000..940a74b Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev2.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev2.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev2.wav.meta new file mode 100644 index 0000000..5a8cf5c --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev2.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: a3648eccd8ffc464cb7d0097183aeb47 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev3.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev3.wav new file mode 100644 index 0000000..e3f1b5b Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev3.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev3.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev3.wav.meta new file mode 100644 index 0000000..ff5caf6 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev3.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: b9b78035d4a2c2e408ee8cfb89a00a56 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev4.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev4.wav new file mode 100644 index 0000000..1def68f Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev4.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev4.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev4.wav.meta new file mode 100644 index 0000000..e964bda --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev4.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 76c6d430db7c3e546856d6b2b2fc8cb1 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev5.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev5.wav new file mode 100644 index 0000000..db21968 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev5.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev5.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev5.wav.meta new file mode 100644 index 0000000..b7187b0 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/TypeWriterMultiple01/SFX_TypeWriterMutiplev5.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: c5915050b4f56f74693ad5d8fc36d151 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Voices01.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Voices01.meta new file mode 100644 index 0000000..652f480 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Voices01.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 43325865354f6ab4ba1afbb9dbba4023 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Voices01/SFX_SingleLowONoReverbv1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Voices01/SFX_SingleLowONoReverbv1.wav new file mode 100644 index 0000000..3903176 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Voices01/SFX_SingleLowONoReverbv1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Voices01/SFX_SingleLowONoReverbv1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Voices01/SFX_SingleLowONoReverbv1.wav.meta new file mode 100644 index 0000000..afe8783 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Voices01/SFX_SingleLowONoReverbv1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: cf5b122c054b64a49997eb6f675222ec +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Voices01/SFX_SingleLowOReverbv1.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Voices01/SFX_SingleLowOReverbv1.wav new file mode 100644 index 0000000..4166d50 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Voices01/SFX_SingleLowOReverbv1.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Voices01/SFX_SingleLowOReverbv1.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Voices01/SFX_SingleLowOReverbv1.wav.meta new file mode 100644 index 0000000..8c23be2 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/Voices01/SFX_SingleLowOReverbv1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: d71736ca43e2a7744b2431f27f7a2eda +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_dependency_missing.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_dependency_missing.wav new file mode 100644 index 0000000..de48db4 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_dependency_missing.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_dependency_missing.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_dependency_missing.wav.meta new file mode 100644 index 0000000..c6167b0 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_dependency_missing.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: fab07d1b6375ee84091f4fbf39dd38ea +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_final_effect.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_final_effect.wav new file mode 100644 index 0000000..47b420e Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_final_effect.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_final_effect.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_final_effect.wav.meta new file mode 100644 index 0000000..bb6b278 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_final_effect.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: e86460e0f470c4047a32167f7d0570d2 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_incomplete.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_incomplete.wav new file mode 100644 index 0000000..027f38a Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_incomplete.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_incomplete.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_incomplete.wav.meta new file mode 100644 index 0000000..998238d --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_incomplete.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: b69b35ae53007bc44805c8b89e24137e +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_invalid.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_invalid.wav new file mode 100644 index 0000000..dd1ee74 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_invalid.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_invalid.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_invalid.wav.meta new file mode 100644 index 0000000..7299483 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_invalid.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 9e87acc902e90014c8e5f2d8c635dd91 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_overlay_success.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_overlay_success.wav new file mode 100644 index 0000000..ee4a417 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_overlay_success.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_overlay_success.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_overlay_success.wav.meta new file mode 100644 index 0000000..91f2e81 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/cast_overlay_success.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 96d542c98e2a79341af5ca9e885761b8 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_cleanse.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_cleanse.ogg new file mode 100644 index 0000000..82415d3 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_cleanse.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_cleanse.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_cleanse.ogg.meta new file mode 100644 index 0000000..fc73052 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_cleanse.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 5058fdd6912d0a14cbbcd08f6b122910 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_connection.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_connection.ogg new file mode 100644 index 0000000..c046493 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_connection.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_connection.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_connection.ogg.meta new file mode 100644 index 0000000..ffecce8 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_connection.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 0503a889082c7dc4ca64159d7c1cbf64 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_electric.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_electric.ogg new file mode 100644 index 0000000..68921cd Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_electric.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_electric.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_electric.ogg.meta new file mode 100644 index 0000000..161ae75 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_electric.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 69e32109731a76a4798b1a62ea65c3ee +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_flow.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_flow.ogg new file mode 100644 index 0000000..207e489 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_flow.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_flow.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_flow.ogg.meta new file mode 100644 index 0000000..c3339b7 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_flow.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 2e2fd36d4acfb1645a67ce289c8dccf4 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_focus.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_focus.ogg new file mode 100644 index 0000000..9f2c918 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_focus.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_focus.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_focus.ogg.meta new file mode 100644 index 0000000..f0f4c0d --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_focus.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 63d88d2b5130eba41b7a92bd5f4d052f +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_ice.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_ice.ogg new file mode 100644 index 0000000..dc8b03d Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_ice.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_ice.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_ice.ogg.meta new file mode 100644 index 0000000..d9ec7d5 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_ice.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: a27cf3cd41141344cbaeb77fe6ffc59a +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_living_bridge.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_living_bridge.ogg new file mode 100644 index 0000000..544aa55 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_living_bridge.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_living_bridge.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_living_bridge.ogg.meta new file mode 100644 index 0000000..3a34b08 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_living_bridge.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 6e54775db800532499e7dd91b750370d +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_stability.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_stability.ogg new file mode 100644 index 0000000..19765b7 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_stability.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_stability.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_stability.ogg.meta new file mode 100644 index 0000000..a257963 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_stability.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 0e3ebde527003ed44b9e37a11a428e8d +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_wind_platform.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_wind_platform.ogg new file mode 100644 index 0000000..31da11b Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_wind_platform.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_wind_platform.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_wind_platform.ogg.meta new file mode 100644 index 0000000..ab0b5b5 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/custom_wind_platform.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: f6f00c90ac9b1d2419bd2ea5126f8c19 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_earth.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_earth.ogg new file mode 100644 index 0000000..eb4dae2 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_earth.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_earth.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_earth.ogg.meta new file mode 100644 index 0000000..6879729 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_earth.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 03a9b93467fe13944a8a1c897017043a +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_fire.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_fire.ogg new file mode 100644 index 0000000..2180043 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_fire.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_fire.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_fire.ogg.meta new file mode 100644 index 0000000..1427012 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_fire.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 2d46bd0eb2ece4d43a7dbdcaf26b93ad +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_life.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_life.ogg new file mode 100644 index 0000000..fe02469 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_life.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_life.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_life.ogg.meta new file mode 100644 index 0000000..8b64145 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_life.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 9b5214b216a803b4da60aee34b010ad8 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_water.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_water.ogg new file mode 100644 index 0000000..dc10843 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_water.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_water.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_water.ogg.meta new file mode 100644 index 0000000..ee6ceb7 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_water.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 385f36a236a1e0d4cb652747afa01e6f +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_wind.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_wind.ogg new file mode 100644 index 0000000..2553556 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_wind.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_wind.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_wind.ogg.meta new file mode 100644 index 0000000..74939f3 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/element_wind.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 5b311857d0a4f3347aef310090e0640c +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/ending_report_opened.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/ending_report_opened.wav new file mode 100644 index 0000000..976b183 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/ending_report_opened.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/ending_report_opened.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/ending_report_opened.wav.meta new file mode 100644 index 0000000..9e0a92e --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/ending_report_opened.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 1bf0ca97d83ee2043bf57b952db7f32c +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_attack_buff.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_attack_buff.ogg new file mode 100644 index 0000000..8a02505 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_attack_buff.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_attack_buff.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_attack_buff.ogg.meta new file mode 100644 index 0000000..2858e4c --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_attack_buff.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 35b05ff16193cf04c9a0fa7b679233d9 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_attribute_laser.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_attribute_laser.ogg new file mode 100644 index 0000000..6084704 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_attribute_laser.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_attribute_laser.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_attribute_laser.ogg.meta new file mode 100644 index 0000000..8ef96ab --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_attribute_laser.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: d4da80e9755f35b42ae41ef7fdd1f03a +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_barrier.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_barrier.ogg new file mode 100644 index 0000000..82415d3 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_barrier.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_barrier.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_barrier.ogg.meta new file mode 100644 index 0000000..cc9ac25 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_barrier.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 53382007e02fdd94c86522fde584f9af +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_buff_dispel.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_buff_dispel.ogg new file mode 100644 index 0000000..831fc37 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_buff_dispel.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_buff_dispel.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_buff_dispel.ogg.meta new file mode 100644 index 0000000..24a3c6c --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_buff_dispel.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: d878e4fdfe784ce46ae396636ccfb7b8 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_curve_projectile.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_curve_projectile.ogg new file mode 100644 index 0000000..2bd91cc Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_curve_projectile.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_curve_projectile.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_curve_projectile.ogg.meta new file mode 100644 index 0000000..7a1d9a5 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_curve_projectile.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 2248c28689d088548823597493131da0 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_directional_projectile.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_directional_projectile.ogg new file mode 100644 index 0000000..09f866a Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_directional_projectile.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_directional_projectile.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_directional_projectile.ogg.meta new file mode 100644 index 0000000..dd8976e --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_directional_projectile.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 216dc2fdca3ea2649930dcd387092343 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_event_block.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_event_block.ogg new file mode 100644 index 0000000..9962ecd Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_event_block.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_event_block.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_event_block.ogg.meta new file mode 100644 index 0000000..62d1dc7 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_event_block.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: f447498692c4f88469c3096fcb860754 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_guard_buff.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_guard_buff.ogg new file mode 100644 index 0000000..af1af9b Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_guard_buff.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_guard_buff.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_guard_buff.ogg.meta new file mode 100644 index 0000000..a9f2326 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_guard_buff.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: a948038e187634d4db098488297bc4fc +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_magic_amplify.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_magic_amplify.ogg new file mode 100644 index 0000000..5560764 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_magic_amplify.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_magic_amplify.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_magic_amplify.ogg.meta new file mode 100644 index 0000000..1baa567 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_magic_amplify.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: bf31878fe474bf74ab56beec342e0c56 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_move_speed_buff.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_move_speed_buff.ogg new file mode 100644 index 0000000..03c6afb Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_move_speed_buff.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_move_speed_buff.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_move_speed_buff.ogg.meta new file mode 100644 index 0000000..94fe26e --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_move_speed_buff.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: fa576ff4c1c97bb40b606b92cca94eee +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_piercing_mark.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_piercing_mark.ogg new file mode 100644 index 0000000..2f93af8 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_piercing_mark.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_piercing_mark.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_piercing_mark.ogg.meta new file mode 100644 index 0000000..c2ec0e0 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_piercing_mark.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 6154a24c34ac9824aaf08e3e61e0e4fe +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_random_buff_dispel.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_random_buff_dispel.ogg new file mode 100644 index 0000000..3b16335 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_random_buff_dispel.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_random_buff_dispel.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_random_buff_dispel.ogg.meta new file mode 100644 index 0000000..36067ec --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_random_buff_dispel.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: a40adcc0e560a32469d04c75c8def396 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_slash_damage.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_slash_damage.ogg new file mode 100644 index 0000000..95af60b Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_slash_damage.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_slash_damage.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_slash_damage.ogg.meta new file mode 100644 index 0000000..d0ead4d --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_slash_damage.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 631ce93b4fe384f45bf74453a103af91 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_special_attack_boost.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_special_attack_boost.ogg new file mode 100644 index 0000000..57b8a2a Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_special_attack_boost.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_special_attack_boost.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_special_attack_boost.ogg.meta new file mode 100644 index 0000000..17366f8 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_special_attack_boost.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: bcad475fe7ef5494e9f9e8aa6bd43d3c +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_stun.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_stun.ogg new file mode 100644 index 0000000..e4731a8 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_stun.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_stun.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_stun.ogg.meta new file mode 100644 index 0000000..09838c4 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_stun.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 3d57bc29585fff645ae05220016011c0 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_trap.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_trap.ogg new file mode 100644 index 0000000..c7b3e17 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_trap.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_trap.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_trap.ogg.meta new file mode 100644 index 0000000..8856896 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_trap.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 4abcd71ddec9d4641a4aef2b9581f3b4 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_wall_entity.ogg b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_wall_entity.ogg new file mode 100644 index 0000000..415e5f7 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_wall_entity.ogg differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_wall_entity.ogg.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_wall_entity.ogg.meta new file mode 100644 index 0000000..c46d4f8 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/event_wall_entity.ogg.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 1c82dbe7ffcae244aa903ed472b24cbe +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/floor_complete.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/floor_complete.wav new file mode 100644 index 0000000..020273e Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/floor_complete.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/floor_complete.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/floor_complete.wav.meta new file mode 100644 index 0000000..d962f1e --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/floor_complete.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 829dda0ae98343747ac7aa29ba14c0fe +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/goal_satisfied.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/goal_satisfied.wav new file mode 100644 index 0000000..bd99e86 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/goal_satisfied.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/goal_satisfied.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/goal_satisfied.wav.meta new file mode 100644 index 0000000..a5b8aa9 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/goal_satisfied.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 3a0b9eac0ec4ea848af63cc146a504e6 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/hazard_reset.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/hazard_reset.wav new file mode 100644 index 0000000..1a3710b Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/hazard_reset.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/hazard_reset.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/hazard_reset.wav.meta new file mode 100644 index 0000000..b0afe39 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/hazard_reset.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 831a2f7c8529da5429fdc2255eca1acc +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/note_unlock.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/note_unlock.wav new file mode 100644 index 0000000..84914cd Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/note_unlock.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/note_unlock.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/note_unlock.wav.meta new file mode 100644 index 0000000..ca19634 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/note_unlock.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: fe1225d8718ab5442a4e0b31e7449e90 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/npc_appear.wav b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/npc_appear.wav new file mode 100644 index 0000000..976b183 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/npc_appear.wav differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/npc_appear.wav.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/npc_appear.wav.meta new file mode 100644 index 0000000..198012e --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sfx/npc_appear.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: a3d6e375f8ccd194aa39c78ffe21c04d +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistFrown.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistFrown.png new file mode 100644 index 0000000..6a8853d Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistFrown.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistFrown.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistFrown.png.meta new file mode 100644 index 0000000..10fa790 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistFrown.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 5a1a77fb1c2ebbd4d8feb519f1e699e1 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistHappy.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistHappy.png new file mode 100644 index 0000000..13c33a9 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistHappy.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistHappy.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistHappy.png.meta new file mode 100644 index 0000000..d69f47b --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistHappy.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: dd09b59ba6c930542a19f375ce4f9263 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistNeutral.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistNeutral.png new file mode 100644 index 0000000..874b73a Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistNeutral.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistNeutral.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistNeutral.png.meta new file mode 100644 index 0000000..a817eb5 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorArchivistNeutral.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 92c87f8be51834742b92336f6969b614 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorFrown.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorFrown.png new file mode 100644 index 0000000..62780ad Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorFrown.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorFrown.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorFrown.png.meta new file mode 100644 index 0000000..adf3eaa --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorFrown.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 36a26bfaa77781d469b72612a4b2bc75 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideFrown.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideFrown.png new file mode 100644 index 0000000..bd9ce02 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideFrown.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideFrown.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideFrown.png.meta new file mode 100644 index 0000000..bb45649 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideFrown.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 9082600971df1f244923ae67e06a79d8 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideHappy.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideHappy.png new file mode 100644 index 0000000..576e1ca Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideHappy.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideHappy.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideHappy.png.meta new file mode 100644 index 0000000..9b7448f --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideHappy.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: e80b4a9786c758243abd5819f011ce28 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideNeutral.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideNeutral.png new file mode 100644 index 0000000..0caeabd Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideNeutral.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideNeutral.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideNeutral.png.meta new file mode 100644 index 0000000..25309f1 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorGuideNeutral.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 83026e676a7918d4f9d16e58703cb51d +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorHappy.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorHappy.png new file mode 100644 index 0000000..0ac12bc Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorHappy.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorHappy.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorHappy.png.meta new file mode 100644 index 0000000..09ba1bd --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorHappy.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: e53049b4a28e89b49807a07806347ab4 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorNeutral.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorNeutral.png new file mode 100644 index 0000000..61e8128 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorNeutral.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorNeutral.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorNeutral.png.meta new file mode 100644 index 0000000..aa194f1 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorNeutral.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: e523ff11ab03e884e9adfd22a9518ff3 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarFrown.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarFrown.png new file mode 100644 index 0000000..cee7d7c Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarFrown.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarFrown.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarFrown.png.meta new file mode 100644 index 0000000..86e54df --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarFrown.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: fa0ab91a50235854a85ce3fafcdbd0d4 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarHappy.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarHappy.png new file mode 100644 index 0000000..be53d2d Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarHappy.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarHappy.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarHappy.png.meta new file mode 100644 index 0000000..3f3d682 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarHappy.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 243bb53baeec30743868e2f87ee883c8 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarNeutral.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarNeutral.png new file mode 100644 index 0000000..ee85cf1 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarNeutral.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarNeutral.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarNeutral.png.meta new file mode 100644 index 0000000..961fc4f --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorScholarNeutral.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 1c94daaa5ac277845b1c631047b057c7 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherFrown.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherFrown.png new file mode 100644 index 0000000..e9539df Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherFrown.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherFrown.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherFrown.png.meta new file mode 100644 index 0000000..c5aada0 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherFrown.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: f958b2ac688be864ebb501c2111a718d +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherHappy.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherHappy.png new file mode 100644 index 0000000..84e6641 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherHappy.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherHappy.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherHappy.png.meta new file mode 100644 index 0000000..3f34807 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherHappy.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 8af81fb9c0418a94b95a767d89d87034 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherNeutral.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherNeutral.png new file mode 100644 index 0000000..90d6360 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherNeutral.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherNeutral.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherNeutral.png.meta new file mode 100644 index 0000000..5fc6bb4 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/MentorWatcherNeutral.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 64650e64b9ed44840a6940eb2c81d6e8 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_charge_0.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_charge_0.png index 05d231c..56f8aa8 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_charge_0.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_charge_0.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_charge_1.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_charge_1.png index 49a3e2e..76b9085 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_charge_1.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_charge_1.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_charge_2.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_charge_2.png index 4cd63d0..4a6b2c3 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_charge_2.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_charge_2.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_release_0.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_release_0.png index ef754b5..4a6b2c3 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_release_0.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_release_0.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_release_1.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_release_1.png index 6fa66b0..a2bc134 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_release_1.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/cast_release_1.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_down_0.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_down_0.png index 7505572..a2bc134 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_down_0.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_down_0.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_down_1.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_down_1.png index 2b55b06..a2bc134 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_down_1.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_down_1.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_left_0.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_left_0.png index 9b8717c..56e9fa8 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_left_0.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_left_0.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_left_1.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_left_1.png index 7db6cae..56e9fa8 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_left_1.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_left_1.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_right_0.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_right_0.png index a122822..1a6b542 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_right_0.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_right_0.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_right_1.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_right_1.png index e1dadb0..1a6b542 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_right_1.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_right_1.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_up_0.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_up_0.png index 5926866..b8dd632 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_up_0.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_up_0.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_up_1.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_up_1.png index fd10679..b8dd632 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_up_1.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/idle_up_1.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_down_0.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_down_0.png index 7505572..a2bc134 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_down_0.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_down_0.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_down_1.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_down_1.png index e7b9ec5..9d6c337 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_down_1.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_down_1.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_down_2.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_down_2.png index 7505572..a2bc134 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_down_2.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_down_2.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_down_3.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_down_3.png index 33517eb..0fd73de 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_down_3.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_down_3.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_left_0.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_left_0.png index 9b8717c..56e9fa8 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_left_0.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_left_0.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_left_1.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_left_1.png index 634b249..a13bc60 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_left_1.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_left_1.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_left_2.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_left_2.png index 9b8717c..56e9fa8 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_left_2.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_left_2.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_left_3.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_left_3.png index 00e7379..dcca10b 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_left_3.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_left_3.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_right_0.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_right_0.png index a122822..1a6b542 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_right_0.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_right_0.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_right_1.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_right_1.png index ee03b77..e7eda12 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_right_1.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_right_1.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_right_2.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_right_2.png index a122822..1a6b542 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_right_2.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_right_2.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_right_3.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_right_3.png index 1acc18b..e22eea1 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_right_3.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_right_3.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_up_0.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_up_0.png index 5926866..b8dd632 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_up_0.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_up_0.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_up_1.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_up_1.png index c097db2..3138789 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_up_1.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_up_1.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_up_2.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_up_2.png index 5926866..b8dd632 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_up_2.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_up_2.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_up_3.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_up_3.png index ec14c12..970c082 100644 Binary files a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_up_3.png and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/Player/walk_up_3.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI.meta new file mode 100644 index 0000000..b2081dd --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0cc18eb64c544047814af826f15a53df +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/README.md b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/README.md new file mode 100644 index 0000000..18b1aa6 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/README.md @@ -0,0 +1,18 @@ +# Magic Exam UI Sprites + +Optional pixel-art overrides for `MagicExamUiSprites`. + +Place PNG files here with these names: + +- `TitleLogo.png` +- `BookPanel.png` +- `ScrollPanel.png` +- `DarkPanel.png` +- `ButtonPanel.png` +- `RuneCursor.png` +- `NoteIcon.png` +- `Checkbox.png` +- `SliderTrack.png` + +The runtime generates matching fallback sprites when an override is absent. Imported +panel sprites should use point filtering, no compression, and sliced borders. diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/README.md.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/README.md.meta new file mode 100644 index 0000000..1882c9f --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 98b16e0b18dc442a9db5d30a9e439807 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleLogo.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleLogo.png new file mode 100644 index 0000000..bd3b035 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleLogo.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleLogo.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleLogo.png.meta new file mode 100644 index 0000000..4a989e2 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleLogo.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 037117bcb2a43e341a2359a7c8708042 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleSky.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleSky.png new file mode 100644 index 0000000..4d96d5a Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleSky.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleSky.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleSky.png.meta new file mode 100644 index 0000000..1826650 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleSky.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 4f88783b47e80e942bc5d4f30d97302b +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleTower.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleTower.png new file mode 100644 index 0000000..cf36a00 Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleTower.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleTower.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleTower.png.meta new file mode 100644 index 0000000..6695d83 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Sprites/UI/TitleTower.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 2c4dbb2cf4ed9ab41898fefc2957d89c +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Ui.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Ui.meta new file mode 100644 index 0000000..c510e97 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Ui.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d31a6cac4478a1345b7238b06a4d5551 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Ui/MentorSpeechBubble.png b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Ui/MentorSpeechBubble.png new file mode 100644 index 0000000..2c0cbcf Binary files /dev/null and b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Ui/MentorSpeechBubble.png differ diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Resources/Ui/MentorSpeechBubble.png.meta b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Ui/MentorSpeechBubble.png.meta new file mode 100644 index 0000000..b0ecdd0 --- /dev/null +++ b/unity/MagicExamHall/Assets/MagicExamHall/Resources/Ui/MentorSpeechBubble.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: c441a8b369ccd584a84405e51166ac6a +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Core/ExamLogging.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Core/ExamLogging.cs index 97079d1..f78f009 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Core/ExamLogging.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Core/ExamLogging.cs @@ -102,6 +102,74 @@ public sealed class QuestChecklistLog public string items = ""; } + [Serializable] + public sealed class ActionEventLog + { + public string sessionId = ""; + public string eventId = ""; + public string utc = ""; + public int elapsedMs; + public int floorElapsedMs; + public string floorId = ""; + public string floorTitle = ""; + public string eventType = ""; + public string actor = ""; + public string phase = ""; + public string targetId = ""; + public string targetLabel = ""; + public float positionX; + public float positionY; + public string inputSessionId = ""; + public string strokeId = ""; + public int strokePointCount; + public string value = ""; + public string payloadJson = ""; + } + + [Serializable] + public sealed class CertificateLog + { + public string sessionId = ""; + public string issuedAtUtc = ""; + public string buildVersion = ""; + public string title = ""; + public string recipient = ""; + public string status = ""; + public string certificateNote = ""; + public string finalTasks = ""; + public int completedFinalGoals; + public int totalFinalGoals; + public int totalElapsedMs; + public int totalAttempts; + public int totalSuccess; + public float successRate; + public float questCompletionRate; + public string outputDirectory = ""; + } + + [Serializable] + public sealed class EnrollmentLog + { + public string sessionId = ""; + public string issuedAtUtc = ""; + public string buildVersion = ""; + public string status = ""; + public int currentFloor; + public string currentFloorTitle = ""; + public int completedGoals; + public int totalGoals; + public int globalCompletedGoals; + public int globalTotalGoals; + public int completedFinalGoals; + public int totalFinalGoals; + public string currentFinalTask = ""; + public int totalElapsedMs; + public int totalAttempts; + public string lastEventType = ""; + public string lastEventUtc = ""; + public string outputDirectory = ""; + } + [Serializable] public sealed class SessionResultContextLog { @@ -222,23 +290,33 @@ public sealed class ExamLogger public const string DisabledOutputDirectory = "log collection disabled"; public string OutputDirectory { get; } + public string CertificateOutputDirectory { get; } public bool IsCollectionEnabled { get; } public string SessionResultJsonPath => sessionResultJsonPath; public string SessionResultCsvPath => sessionResultCsvPath; public string FloorResultsCsvPath => floorResultsCsvPath; + public string ActionEventsJsonPath => actionEventsJsonPath; + public string ActionEventsCsvPath => actionEventsCsvPath; + public string CertificateCsvPath => certificateCsvPath; + public string EnrollmentCsvPath => enrollmentCsvPath; private readonly string attemptsJsonPath; private readonly string attemptsCsvPath; private readonly string surveyJsonPath; private readonly string surveyCsvPath; private readonly string questChecklistJsonPath; private readonly string questChecklistCsvPath; + private readonly string actionEventsJsonPath; + private readonly string actionEventsCsvPath; private readonly string sessionResultJsonPath; private readonly string sessionResultCsvPath; private readonly string floorResultsCsvPath; + private readonly string certificateCsvPath; + private readonly string enrollmentCsvPath; private readonly string globalSessionResultsCsvPath; private readonly List attemptHistory = new(); private readonly List surveyHistory = new(); private readonly List questChecklistHistory = new(); + private readonly List actionEventHistory = new(); private bool sessionResultAppended; public ExamLogger(string sessionId) @@ -246,44 +324,82 @@ public ExamLogger(string sessionId) { } - public ExamLogger(string sessionId, string outputRoot, bool? enableCollection = null) + public ExamLogger(string sessionId, string outputRoot, bool? enableCollection = null, string certificateOutputRoot = "") { sessionId = string.IsNullOrWhiteSpace(sessionId) ? "session" : sessionId; IsCollectionEnabled = enableCollection ?? !ShouldSuppressCollection(sessionId); if (!IsCollectionEnabled) { OutputDirectory = DisabledOutputDirectory; + CertificateOutputDirectory = DisabledOutputDirectory; attemptsJsonPath = ""; attemptsCsvPath = ""; surveyJsonPath = ""; surveyCsvPath = ""; questChecklistJsonPath = ""; questChecklistCsvPath = ""; + actionEventsJsonPath = ""; + actionEventsCsvPath = ""; sessionResultJsonPath = ""; sessionResultCsvPath = ""; floorResultsCsvPath = ""; + certificateCsvPath = ""; + enrollmentCsvPath = ""; globalSessionResultsCsvPath = ""; return; } - var root = string.IsNullOrWhiteSpace(outputRoot) + var usesDefaultOutputRoot = string.IsNullOrWhiteSpace(outputRoot); + var root = usesDefaultOutputRoot ? Path.Combine(Application.persistentDataPath, "MagicExamHallLogs") : outputRoot; OutputDirectory = Path.Combine(root, sessionId); Directory.CreateDirectory(OutputDirectory); + CertificateOutputDirectory = string.IsNullOrWhiteSpace(certificateOutputRoot) + ? usesDefaultOutputRoot ? ResolveExecutableDirectory() : OutputDirectory + : certificateOutputRoot; + Directory.CreateDirectory(CertificateOutputDirectory); attemptsJsonPath = Path.Combine(OutputDirectory, "attempts.jsonl"); attemptsCsvPath = Path.Combine(OutputDirectory, "attempts.csv"); surveyJsonPath = Path.Combine(OutputDirectory, "survey.jsonl"); surveyCsvPath = Path.Combine(OutputDirectory, "survey.csv"); questChecklistJsonPath = Path.Combine(OutputDirectory, "quest-checklist.jsonl"); questChecklistCsvPath = Path.Combine(OutputDirectory, "quest-checklist.csv"); + actionEventsJsonPath = Path.Combine(OutputDirectory, "action-events.jsonl"); + actionEventsCsvPath = Path.Combine(OutputDirectory, "action-events.csv"); sessionResultJsonPath = Path.Combine(OutputDirectory, "session-result.json"); sessionResultCsvPath = Path.Combine(OutputDirectory, "session-result.csv"); floorResultsCsvPath = Path.Combine(OutputDirectory, "floor-results.csv"); + certificateCsvPath = Path.Combine(CertificateOutputDirectory, "\uC218\uB8CC\uC99D.csv"); + enrollmentCsvPath = Path.Combine(CertificateOutputDirectory, "\uC7AC\uD559\uC99D\uC11C.csv"); globalSessionResultsCsvPath = Path.Combine(root, "session-results.csv"); EnsureAttemptHeader(); EnsureSurveyHeader(); EnsureQuestChecklistHeader(); + EnsureActionEventHeader(); + } + + private static string ResolveExecutableDirectory() + { + var dataPath = Application.dataPath; + if (!string.IsNullOrWhiteSpace(dataPath)) + { + var dataDirectory = new DirectoryInfo(dataPath); + if (dataDirectory.Exists && dataDirectory.Name.EndsWith("_Data", StringComparison.OrdinalIgnoreCase)) + { + return dataDirectory.Parent?.FullName ?? dataDirectory.FullName; + } + + if (Application.isEditor) + { + return dataDirectory.Parent?.FullName ?? dataDirectory.FullName; + } + } + + var baseDirectory = AppContext.BaseDirectory; + return string.IsNullOrWhiteSpace(baseDirectory) + ? Directory.GetCurrentDirectory() + : Path.GetFullPath(baseDirectory); } public void LogAttempt(AttemptLog log) @@ -400,6 +516,48 @@ public void LogQuestChecklist(QuestChecklistLog log) Csv(log.items)) + Environment.NewLine, Encoding.UTF8); } + public void LogActionEvent(ActionEventLog log) + { + if (!IsCollectionEnabled) + { + return; + } + + log ??= new ActionEventLog(); + if (string.IsNullOrWhiteSpace(log.eventId)) + { + log.eventId = Guid.NewGuid().ToString("N"); + } + + if (string.IsNullOrWhiteSpace(log.utc)) + { + log.utc = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture); + } + + actionEventHistory.Add(log); + File.AppendAllText(actionEventsJsonPath, JsonUtility.ToJson(log) + Environment.NewLine, Encoding.UTF8); + File.AppendAllText(actionEventsCsvPath, string.Join(",", + Csv(log.sessionId), + Csv(log.eventId), + Csv(log.utc), + log.elapsedMs, + log.floorElapsedMs, + Csv(log.floorId), + Csv(log.floorTitle), + Csv(log.eventType), + Csv(log.actor), + Csv(log.phase), + Csv(log.targetId), + Csv(log.targetLabel), + Float(log.positionX), + Float(log.positionY), + Csv(log.inputSessionId), + Csv(log.strokeId), + log.strokePointCount, + Csv(log.value), + Csv(log.payloadJson)) + Environment.NewLine, Encoding.UTF8); + } + public SessionResultLog WriteSessionResult(SessionResultContextLog context) { if (!IsCollectionEnabled) @@ -416,6 +574,58 @@ public SessionResultLog WriteSessionResult(SessionResultContextLog context) return result; } + public string WriteCertificateCsv(CertificateLog certificate) + { + if (!IsCollectionEnabled) + { + return ""; + } + + certificate ??= new CertificateLog(); + if (string.IsNullOrWhiteSpace(certificate.issuedAtUtc)) + { + certificate.issuedAtUtc = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture); + } + + if (string.IsNullOrWhiteSpace(certificate.outputDirectory) || + string.Equals(certificate.outputDirectory, OutputDirectory, StringComparison.Ordinal)) + { + certificate.outputDirectory = CertificateOutputDirectory; + } + + File.WriteAllText( + certificateCsvPath, + CertificateCsvHeader() + Environment.NewLine + CertificateCsvRow(certificate) + Environment.NewLine, + Encoding.UTF8); + return certificateCsvPath; + } + + public string WriteEnrollmentCsv(EnrollmentLog enrollment) + { + if (!IsCollectionEnabled) + { + return ""; + } + + enrollment ??= new EnrollmentLog(); + if (string.IsNullOrWhiteSpace(enrollment.issuedAtUtc)) + { + enrollment.issuedAtUtc = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture); + } + + if (string.IsNullOrWhiteSpace(enrollment.outputDirectory) || + string.Equals(enrollment.outputDirectory, OutputDirectory, StringComparison.Ordinal)) + { + enrollment.outputDirectory = CertificateOutputDirectory; + } + + File.WriteAllText( + enrollmentCsvPath, + EnrollmentCsvHeader() + Environment.NewLine + EnrollmentCsvRow(enrollment) + Environment.NewLine, + Encoding.UTF8); + return enrollmentCsvPath; + } + private void EnsureAttemptHeader() { if (!File.Exists(attemptsCsvPath)) @@ -443,6 +653,14 @@ private void EnsureQuestChecklistHeader() } } + private void EnsureActionEventHeader() + { + if (!File.Exists(actionEventsCsvPath)) + { + File.WriteAllText(actionEventsCsvPath, "sessionId,eventId,utc,elapsedMs,floorElapsedMs,floorId,floorTitle,eventType,actor,phase,targetId,targetLabel,positionX,positionY,inputSessionId,strokeId,strokePointCount,value,payloadJson" + Environment.NewLine, Encoding.UTF8); + } + } + private SessionResultLog BuildSessionResult(SessionResultContextLog context) { var floorCount = Math.Max(context.floorCount, HighestObservedFloorId()); @@ -504,7 +722,7 @@ private SessionResultLog BuildSessionResult(SessionResultContextLog context) averageTimeToFirstSuccessMs = averageTimeToFirstSuccess, gqmB1UnderstandingDeltaProxy = BuildUnderstandingDelta(allAttempts), floors = floors.ToArray(), - coverageNotes = "raw_strokes=false; test_sessions_excluded=true; attempt+quest+survey+ending_summary=true; cancel/backtrack require future movement instrumentation" + coverageNotes = "raw_strokes=true; action_events=true; test_sessions_excluded=true; attempt+quest+survey+ending_summary=true; movement logged as key/action state changes" }; result.gqmA1ShapeDifficultyScore = ShapeDifficultyScore( result.successRate, @@ -745,6 +963,60 @@ private static string FloorResultCsvRow(HciFloorResultLog floor) Csv(floor.coverageNotes)); } + private static string CertificateCsvHeader() + { + return "sessionId,issuedAtUtc,buildVersion,title,recipient,status,certificateNote,finalTasks,completedFinalGoals,totalFinalGoals,totalElapsedMs,totalAttempts,totalSuccess,successRate,questCompletionRate,outputDirectory"; + } + + private static string EnrollmentCsvHeader() + { + return "sessionId,issuedAtUtc,buildVersion,status,currentFloor,currentFloorTitle,completedGoals,totalGoals,globalCompletedGoals,globalTotalGoals,completedFinalGoals,totalFinalGoals,currentFinalTask,totalElapsedMs,totalAttempts,lastEventType,lastEventUtc,outputDirectory"; + } + + private static string CertificateCsvRow(CertificateLog certificate) + { + return string.Join(",", + Csv(certificate.sessionId), + Csv(certificate.issuedAtUtc), + Csv(certificate.buildVersion), + Csv(certificate.title), + Csv(certificate.recipient), + Csv(certificate.status), + Csv(certificate.certificateNote), + Csv(certificate.finalTasks), + certificate.completedFinalGoals, + certificate.totalFinalGoals, + certificate.totalElapsedMs, + certificate.totalAttempts, + certificate.totalSuccess, + Float(certificate.successRate), + Float(certificate.questCompletionRate), + Csv(certificate.outputDirectory)); + } + + private static string EnrollmentCsvRow(EnrollmentLog enrollment) + { + return string.Join(",", + Csv(enrollment.sessionId), + Csv(enrollment.issuedAtUtc), + Csv(enrollment.buildVersion), + Csv(enrollment.status), + enrollment.currentFloor, + Csv(enrollment.currentFloorTitle), + enrollment.completedGoals, + enrollment.totalGoals, + enrollment.globalCompletedGoals, + enrollment.globalTotalGoals, + enrollment.completedFinalGoals, + enrollment.totalFinalGoals, + Csv(enrollment.currentFinalTask), + enrollment.totalElapsedMs, + enrollment.totalAttempts, + Csv(enrollment.lastEventType), + Csv(enrollment.lastEventUtc), + Csv(enrollment.outputDirectory)); + } + private int HighestObservedFloorId() { var max = 0; diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Core/HintAssistance.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Core/HintAssistance.cs index ccdf4ac..755d2a9 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Core/HintAssistance.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Core/HintAssistance.cs @@ -59,7 +59,7 @@ public static IReadOnlyList ChecklistFor(SpellFamily family) return family switch { SpellFamily.Fire => new[] { "세 꼭짓점을 크게 찍기", "마지막 선을 처음 아래 꼭짓점으로 돌려 닫기", "삼각형을 기울이지 말고 세우기" }, - SpellFamily.Water => new[] { "한 획으로 둥글게 돌기", "끝점을 시작점 바로 옆에 놓기", "찌그러진 타원보다 고른 원 유지" }, + SpellFamily.Water => new[] { "한 획으로 둥글게 이어 돌기", "끝점을 시작점 바로 옆에 놓기", "네 모서리가 생기지 않게 부드러운 타원/물방울 만들기" }, SpellFamily.Wind => new[] { "위, 가운데, 아래 3획만 남기기", "세 선을 같은 기울기로 맞추기", "세 줄 사이 간격을 비슷하게 벌리기" }, SpellFamily.Earth => new[] { "윗변은 좁고 아랫변은 넓게 잡기", "네 모서리를 분명하게 꺾기", "마지막 선으로 사다리꼴을 닫기" }, SpellFamily.Life => new[] { "가운데 줄기를 먼저 세우기", "같은 분기점에서 좌우 가지 뻗기", "원처럼 닫지 말고 끝을 열어 두기" }, @@ -118,7 +118,7 @@ private static string ActionHintFor(SpellFamily family) return family switch { SpellFamily.Fire => "삼각형 꼭짓점 3개를 크게 잡고 마지막 점을 시작점 근처로 닫아 보세요.", - SpellFamily.Water => "한 획으로 둥글게 돌린 뒤 끝점을 시작점 가까이에 놓아 보세요.", + SpellFamily.Water => "한 획으로 부드러운 물방울이나 타원을 그리고 끝점을 시작점 가까이에 놓아 보세요.", SpellFamily.Wind => "위, 가운데, 아래에 짧은 평행선 3개를 같은 간격으로 따로 그려 보세요.", SpellFamily.Earth => "윗변이 좁고 아랫변이 넓은 사다리꼴을 닫힌 모양으로 그려 보세요.", SpellFamily.Life => "가운데 줄기에서 좌우 가지가 갈라지는 열린 Y 형태를 만들어 보세요.", @@ -131,7 +131,7 @@ private static string StrongHintFor(SpellFamily family) return family switch { SpellFamily.Fire => "불꽃은 닫힌 삼각형입니다. 아래 꼭짓점에서 시작해 위 양쪽 꼭짓점을 찍고 처음으로 돌아오세요.", - SpellFamily.Water => "물은 닫힌 원입니다. 한 번에 둥글게 돌리고 끝점을 시작점 바로 옆에 놓으세요.", + SpellFamily.Water => "물은 모서리 없는 닫힌 물방울입니다. 한 번에 둥글게 이어 그리고 끝점을 시작점 바로 옆에 놓으세요.", SpellFamily.Wind => "바람은 도형이 아니라 3획입니다. 위, 가운데, 아래에 같은 기울기의 짧은 선 3개만 남기세요.", SpellFamily.Earth => "땅은 닫힌 사다리꼴입니다. 네 모서리를 만들고 마지막 선으로 틈을 막으세요.", SpellFamily.Life => "생명은 열린 Y입니다. 줄기 하나를 세운 뒤 같은 분기점에서 왼쪽 가지와 오른쪽 가지를 뻗고 끝을 닫지 마세요.", diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Core/SpellRecognition.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Core/SpellRecognition.cs index 32e4623..7c31d35 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Core/SpellRecognition.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Core/SpellRecognition.cs @@ -129,13 +129,17 @@ public static class GestureRecognizer private const float WeakIntentMaximumScoreLift = 0.045f; private const float StrongIntentMinimumStrength = 0.25f; private const float StrongIntentMinimumSimilarity = 0.50f; + private const float NearGoalStrongIntentMinimumSimilarity = 0.46f; private const float StrongIntentMaximumScoreLift = 0.16f; + private const float NearGoalStrongIntentMaximumScoreLift = 0.22f; private const float StrongIntentAcceptThreshold = 0.66f; + private const float NearGoalStrongIntentAcceptThreshold = 0.64f; private const float StrongIntentHoldThreshold = 0.54f; - private const float WaterColdStartMinimumSimilarity = 0.58f; - private const float WaterColdStartAcceptThreshold = 0.70f; + private const float NearGoalStrongIntentHoldThreshold = 0.50f; + private const float WaterColdStartMinimumSimilarity = 0.54f; + private const float WaterColdStartAcceptThreshold = 0.66f; private const float WaterColdStartHoldThreshold = 0.56f; - private const float WaterColdStartClosureAcceptThreshold = 0.55f; + private const float WaterColdStartClosureAcceptThreshold = 0.48f; private const float WaterColdStartMarginThreshold = 0.035f; private const float DefaultClosureAcceptThreshold = 0.62f; private const float WaterClosureAcceptThreshold = 0.50f; @@ -144,8 +148,13 @@ public static class GestureRecognizer private const float DefaultClosureHintThreshold = 0.72f; private const float WaterClosureHintThreshold = 0.58f; private const float WaterClosureScoringFloor = 0.70f; - private const float WaterSingleStrokeLoopBonus = 0.035f; - private const float EarthSmoothLoopPenalty = 0.72f; + private const float WaterSingleStrokeLoopBonus = 0.055f; + private const float WaterAngularLoopPenalty = 0.66f; + private const float WaterBoxyLoopPenalty = 0.78f; + private const float WaterEarthLikeLoopPenalty = 0.30f; + private const float EarthAngularLoopBonus = 0.085f; + private const float EarthSmoothLoopPenalty = 0.46f; + private const float EarthRoundLoopPenalty = 0.58f; private static readonly IReadOnlyList Templates = BuildTemplates(); public static SpellResult Recognize(IReadOnlyList> rawStrokes, SpellFamily targetFamily) @@ -290,17 +299,19 @@ private static bool TryBuildIntentResult( return false; } - if (normalStatus == RecognitionStatus.Recognized) + if (normalStatus == RecognitionStatus.Recognized && + top.template.family == targetFamily) { return false; } - if (target.score < StrongIntentMinimumSimilarity) + var minimumSimilarity = StrongIntentMinimumSimilarityFor(targetFamily, intent); + if (target.score < minimumSimilarity) { return false; } - var lift = Mathf.Clamp01(intent.strength) * StrongIntentMaximumScoreLift; + var lift = Mathf.Clamp01(intent.strength) * StrongIntentMaximumScoreLiftFor(targetFamily, intent); var adjustedScore = Mathf.Clamp01(target.score + weakLift + lift); if (adjustedScore < top.score - 0.10f && target.score < 0.58f) { @@ -308,7 +319,7 @@ private static bool TryBuildIntentResult( } (SpellTemplate template, float score, float distance) intentTop = (target.template, adjustedScore, target.distance); - var status = ResolveIntentStatus(targetFamily, adjustedScore, target.score, quality, strokeCount, parallelism, windSpacing); + var status = ResolveIntentStatus(targetFamily, intent, adjustedScore, target.score, quality, strokeCount, parallelism, windSpacing); SpellFamily? recognized = status == RecognitionStatus.Recognized ? targetFamily : null; result = new SpellResult { @@ -336,6 +347,7 @@ private static bool TryBuildIntentResult( private static RecognitionStatus ResolveIntentStatus( SpellFamily targetFamily, + BaseRecognitionIntent intent, float adjustedScore, float rawSimilarity, QualityVector quality, @@ -359,9 +371,14 @@ private static RecognitionStatus ResolveIntentStatus( return RecognitionStatus.Incomplete; } - var acceptThreshold = targetFamily == SpellFamily.Water ? WaterColdStartAcceptThreshold : StrongIntentAcceptThreshold; - var holdThreshold = targetFamily == SpellFamily.Water ? WaterColdStartHoldThreshold : StrongIntentHoldThreshold; - var minimumSimilarity = targetFamily == SpellFamily.Water ? WaterColdStartMinimumSimilarity : StrongIntentMinimumSimilarity; + var nearGoalColdStart = IsGoalLocalColdStartIntent(intent); + var acceptThreshold = targetFamily == SpellFamily.Water + ? WaterColdStartAcceptThreshold + : nearGoalColdStart ? NearGoalStrongIntentAcceptThreshold : StrongIntentAcceptThreshold; + var holdThreshold = targetFamily == SpellFamily.Water + ? WaterColdStartHoldThreshold + : nearGoalColdStart ? NearGoalStrongIntentHoldThreshold : StrongIntentHoldThreshold; + var minimumSimilarity = StrongIntentMinimumSimilarityFor(targetFamily, intent); if (targetFamily == SpellFamily.Water && quality.closure < WaterColdStartClosureAcceptThreshold) { return RecognitionStatus.Incomplete; @@ -380,6 +397,33 @@ private static RecognitionStatus ResolveIntentStatus( return RecognitionStatus.Invalid; } + private static float StrongIntentMinimumSimilarityFor(SpellFamily targetFamily, BaseRecognitionIntent intent) + { + if (targetFamily == SpellFamily.Water) + { + return WaterColdStartMinimumSimilarity; + } + + return IsGoalLocalColdStartIntent(intent) ? NearGoalStrongIntentMinimumSimilarity : StrongIntentMinimumSimilarity; + } + + private static float StrongIntentMaximumScoreLiftFor(SpellFamily targetFamily, BaseRecognitionIntent intent) + { + if (targetFamily == SpellFamily.Water) + { + return StrongIntentMaximumScoreLift; + } + + return IsGoalLocalColdStartIntent(intent) ? NearGoalStrongIntentMaximumScoreLift : StrongIntentMaximumScoreLift; + } + + private static bool IsGoalLocalColdStartIntent(BaseRecognitionIntent intent) + { + return intent?.IsActive == true && + string.Equals(intent.source, "near_goal_symbol", StringComparison.Ordinal) && + !string.IsNullOrWhiteSpace(intent.goalId); + } + private static RecognitionStatus ApplyColdStartWaterGuard( SpellFamily targetFamily, BaseRecognitionIntent intent, @@ -494,11 +538,35 @@ private static (SpellTemplate template, float score, float distance) ScoreTempla var corners = ExpectedCornerScore(cornerCount, ExpectedCorners(template.family)); var circularity = EstimateCircularity(normalized.cloud); var parallel = EstimateParallelism(strokes); + var windSpacing = EstimateWindSpacingBalance(strokes); var fill = EstimateFillRatio(strokes); + var parallelThreeStroke = strokes.Count == 3 && parallel >= 0.62f && windSpacing >= 0.45f; var smoothSingleLoop = strokes.Count == 1 && quality.closure >= WaterIntentClosureAcceptThreshold && circularity >= 0.64f && cornerCount <= 1; + var roundSingleLoop = strokes.Count == 1 && + quality.closure >= WaterIntentClosureAcceptThreshold && + circularity >= 0.56f && + cornerCount <= 2; + var angularSingleLoop = strokes.Count == 1 && + quality.closure >= WaterIntentClosureAcceptThreshold && + cornerCount >= 3 && + circularity <= 0.58f; + var boxySingleLoop = strokes.Count == 1 && + quality.closure >= WaterIntentClosureAcceptThreshold && + cornerCount >= 3 && + fill >= 0.52f; + var earthLikeSingleLoop = LooksLikeEarthQuadrilateralLoop(strokes, quality, cornerCount, circularity, fill); + var softWaterLoop = strokes.Count == 1 && + quality.closure >= WaterColdStartClosureAcceptThreshold && + circularity >= 0.50f && + cornerCount <= 2 && + fill >= 0.30f && + fill <= 0.92f && + !earthLikeSingleLoop && + !boxySingleLoop && + !angularSingleLoop; var score = templateScore; switch (template.family) @@ -516,9 +584,28 @@ private static (SpellTemplate template, float score, float distance) ScoreTempla { score *= EarthSmoothLoopPenalty; } + else if (roundSingleLoop) + { + score *= EarthRoundLoopPenalty; + } + + if (angularSingleLoop || boxySingleLoop || earthLikeSingleLoop) + { + var earthLoopFit = Mathf.Max(Closeness(fill, 0.68f, 0.30f), Closeness(fill, 0.50f, 0.28f)); + score += EarthAngularLoopBonus * Mathf.Min(corners, earthLoopFit); + } break; case SpellFamily.Fire: score = templateScore * 0.42f + quality.closure * 0.24f + corners * 0.21f + Closeness(fill, 0.5f, 0.18f) * 0.07f + strokeScore * 0.06f; + if (parallelThreeStroke) + { + score *= 0.38f; + } + + if (smoothSingleLoop || roundSingleLoop) + { + score *= 0.72f; + } break; case SpellFamily.Water: score = templateScore * 0.44f + ClosureScoreFor(template.family, quality.closure) * 0.18f + circularity * 0.27f + quality.smoothness * 0.11f; @@ -526,6 +613,23 @@ private static (SpellTemplate template, float score, float distance) ScoreTempla { score += WaterSingleStrokeLoopBonus * Mathf.Min(circularity, quality.smoothness); } + else if (softWaterLoop) + { + score += WaterSingleStrokeLoopBonus * 0.72f * Mathf.Min(circularity, quality.smoothness); + } + + if (earthLikeSingleLoop) + { + score *= WaterEarthLikeLoopPenalty; + } + else if (angularSingleLoop) + { + score *= WaterAngularLoopPenalty * 0.78f; + } + else if (boxySingleLoop) + { + score *= WaterBoxyLoopPenalty * 0.82f; + } break; case SpellFamily.Life: score = templateScore * 0.36f + ExpectedEndpointScore(strokes) * 0.28f + openness * 0.18f + strokeScore * 0.10f + Closeness(fill, 0.16f, 0.20f) * 0.08f; @@ -650,7 +754,7 @@ private static string ClosureReasonFor(SpellFamily family) return family switch { SpellFamily.Fire => "불꽃 삼각형의 마지막 점이 시작점으로 돌아오지 않아 틈이 남았습니다.", - SpellFamily.Water => "물 원의 끝점이 시작점 옆에 닿지 않아 열린 곡선으로 읽혔습니다.", + SpellFamily.Water => "물방울의 끝점이 시작점 옆에 닿지 않아 열린 곡선으로 읽혔습니다.", SpellFamily.Earth => "땅 사다리꼴의 마지막 변이 닫히지 않아 바닥이 고정되지 않았습니다.", _ => "끝점이 시작점 가까이 닿지 않아 닫힌 문양으로 읽히지 않았습니다." }; @@ -661,7 +765,7 @@ private static string ClosureHintFor(SpellFamily family) return family switch { SpellFamily.Fire => "세 꼭짓점을 찍은 뒤 마지막 선을 처음 아래 꼭짓점으로 되돌려 삼각형을 닫으세요.", - SpellFamily.Water => "한 획으로 원을 돌리고 끝점을 시작점 바로 옆에 놓아 고리를 닫으세요.", + SpellFamily.Water => "한 획으로 둥근 물방울이나 타원을 돌리고 끝점을 시작점 바로 옆에 놓아 고리를 닫으세요.", SpellFamily.Earth => "윗변은 좁게, 아랫변은 넓게 만든 뒤 마지막 선으로 네 모서리를 닫으세요.", _ => "마지막 점을 시작점 근처로 가져와 닫힌 모양을 만들어 보세요." }; @@ -739,6 +843,26 @@ private static IReadOnlyList BuildTemplates() } }, new() + { + family = SpellFamily.Earth, + minStrokes = 1, + maxStrokes = 2, + strokes = new List> + { + Poly(new Vector2(-0.46f, -0.34f), new Vector2(0.46f, -0.34f), new Vector2(0.25f, 0.38f), new Vector2(-0.25f, 0.38f), new Vector2(-0.46f, -0.34f)) + } + }, + new() + { + family = SpellFamily.Earth, + minStrokes = 1, + maxStrokes = 2, + strokes = new List> + { + Poly(new Vector2(-0.42f, -0.34f), new Vector2(0.42f, -0.34f), new Vector2(0.42f, 0.34f), new Vector2(-0.42f, 0.34f), new Vector2(-0.42f, -0.34f)) + } + }, + new() { family = SpellFamily.Fire, minStrokes = 1, @@ -956,11 +1080,17 @@ private static int CountDominantCorners(List> strokes) return 0; } + var points = SimplifyStrokePoints(dominant); + if (points.Count < 3) + { + return 0; + } + var corners = 0; - for (var index = 1; index < dominant.Count - 1; index++) + for (var index = 1; index < points.Count - 1; index++) { - var a = (dominant[index].position - dominant[index - 1].position).normalized; - var b = (dominant[index + 1].position - dominant[index].position).normalized; + var a = (points[index] - points[index - 1]).normalized; + var b = (points[index + 1] - points[index]).normalized; if (Vector2.Angle(a, b) > 38f) { corners++; @@ -970,6 +1100,36 @@ private static int CountDominantCorners(List> strokes) return Mathf.Clamp(corners, 0, 6); } + private static List SimplifyStrokePoints(IReadOnlyList stroke) + { + var points = stroke.Select(sample => sample.position).ToList(); + if (points.Count < 2) + { + return points; + } + + var min = new Vector2(points.Min(point => point.x), points.Min(point => point.y)); + var max = new Vector2(points.Max(point => point.x), points.Max(point => point.y)); + var duplicateThreshold = Mathf.Max(Vector2.Distance(min, max) * 0.012f, 0.0005f); + var simplified = new List { points[0] }; + for (var index = 1; index < points.Count; index++) + { + if (Vector2.Distance(points[index], simplified[^1]) >= duplicateThreshold) + { + simplified.Add(points[index]); + } + } + + if (simplified.Count > 2 && + Vector2.Distance(simplified[0], simplified[^1]) < duplicateThreshold && + Vector2.Distance(points[0], points[^1]) >= duplicateThreshold) + { + simplified[^1] = points[^1]; + } + + return simplified; + } + private static float ExpectedCornerScore(int actual, int expected) { return Mathf.Clamp01(1f - Mathf.Abs(actual - expected) / Mathf.Max(expected, 1f)); @@ -998,7 +1158,7 @@ private static float EstimateFillRatio(List> strokes) var maxX = points.Max(point => point.x); var minY = points.Min(point => point.y); var maxY = points.Max(point => point.y); - var boxArea = Mathf.Max((maxX - minX) * (maxY - minY), 1f); + var boxArea = Mathf.Max((maxX - minX) * (maxY - minY), 0.0001f); var dominant = strokes.OrderByDescending(stroke => StrokePathLength(stroke)).First(); var area = Mathf.Abs(PolygonArea(dominant.Select(sample => sample.position).ToList())); return Mathf.Clamp01(area / boxArea); @@ -1017,6 +1177,34 @@ private static float EstimateCircularity(IReadOnlyList cloud) return Mathf.Clamp01(1f - Mathf.Sqrt(variance) / Mathf.Max(mean * 0.45f, 0.0001f)); } + private static bool LooksLikeEarthQuadrilateralLoop( + List> strokes, + QualityVector quality, + int cornerCount, + float circularity, + float fill) + { + if (strokes.Count != 1 || + quality.closure < WaterIntentClosureAcceptThreshold || + cornerCount < 3 || + cornerCount > 6 || + fill < 0.32f || + circularity > 0.86f) + { + return false; + } + + var dominant = strokes[0]; + var minX = dominant.Min(sample => sample.position.x); + var maxX = dominant.Max(sample => sample.position.x); + var minY = dominant.Min(sample => sample.position.y); + var maxY = dominant.Max(sample => sample.position.y); + var width = Mathf.Max(maxX - minX, 0.001f); + var height = Mathf.Max(maxY - minY, 0.001f); + var aspect = width / height; + return aspect >= 0.48f && aspect <= 2.35f; + } + private static float RangeScore(int value, int min, int max) { if (value >= min && value <= max) @@ -1150,7 +1338,7 @@ public static QualityVector Calculate(IReadOnlyList> var min = new Vector2(all.Min(sample => sample.position.x), all.Min(sample => sample.position.y)); var max = new Vector2(all.Max(sample => sample.position.x), all.Max(sample => sample.position.y)); - var diagonal = Mathf.Max(Vector2.Distance(min, max), 1f); + var diagonal = Mathf.Max(Vector2.Distance(min, max), 0.15f); var longest = strokes.OrderByDescending(PathLength).First(); var gap = Vector2.Distance(longest[0].position, longest[^1].position); var duration = Mathf.Max(all.Max(sample => sample.time) - all.Min(sample => sample.time), 0.001f); diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Core/SpellRuntime.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Core/SpellRuntime.cs index 24ca06a..0ad5cc5 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Core/SpellRuntime.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Core/SpellRuntime.cs @@ -66,12 +66,19 @@ public sealed class CompiledSeal public string customEventRole = ""; public Vector2 customEventOrigin; public Vector2 customEventDirection = Vector2.right; + public CustomSpellEffectKind customEffectKind = CustomSpellEffectKind.None; + public bool HasCustomEffectSeal => customEffectKind != CustomSpellEffectKind.None; public string Label { get { - var baseLabel = string.IsNullOrWhiteSpace(customShapeLabel) + var customEffectLabel = HasCustomEffectSeal + ? CustomSpellEffectCatalog.Korean(customEffectKind) + : ""; + var baseLabel = !string.IsNullOrWhiteSpace(customEffectLabel) + ? $"{customEffectLabel} 문양" + : string.IsNullOrWhiteSpace(customShapeLabel) ? SpellLabels.Korean(baseFamily) : $"{customShapeLabel} ({SpellLabels.Korean(baseFamily)})"; if (overlayStack.Count == 0) @@ -109,7 +116,8 @@ public static BaseRecognitionResult RecognizeBase( var candidates = Enum.GetValues(typeof(SpellFamily)) .Cast() .Select(family => GestureRecognizer.Recognize(strokes, family, intent)) - .OrderByDescending(result => result.intentStrongConsiderationApplied ? 3 : result.success ? 2 : result.status == RecognitionStatus.Recognized ? 1 : 0) + .OrderByDescending(RecognitionRank) + .ThenByDescending(result => ActiveIntentTargetRank(result, intent)) .ThenByDescending(result => result.confidence) .ToList(); var best = candidates.First(); @@ -125,6 +133,33 @@ public static BaseRecognitionResult RecognizeBase( }; } + private static int RecognitionRank(SpellResult result) + { + if (result.intentStrongConsiderationApplied) + { + return 3; + } + + if (result.success) + { + return 2; + } + + return result.status == RecognitionStatus.Recognized ? 1 : 0; + } + + private static int ActiveIntentTargetRank(SpellResult result, BaseRecognitionIntent intent) + { + if (intent?.IsActive != true || + result.targetFamily != intent.family || + intent.strength < 0.45f) + { + return 0; + } + + return result.success || result.status == RecognitionStatus.Recognized ? 1 : 0; + } + public static CompiledSeal CreateSeal(BaseRecognitionResult baseResult, float now, float durationSeconds = DefaultSealDurationSeconds) { return new CompiledSeal @@ -206,7 +241,7 @@ public static OverlayRecognitionResult Recognize( scaleRatio = features.scaleRatio, scaleHint = scaleHint, anchorZone = top.anchorZone, - feedbackReason = "축 장식은 먼저 절단 장식이 붙은 seal에서만 섭니다. 대각선 절단을 붙인 뒤 중심을 가르는 축을 그리세요." + feedbackReason = "축 장식은 먼저 절단 장식이 붙은 문양에서만 섭니다. 대각선 절단을 붙인 뒤 중심을 가르는 축을 그리세요." }; } @@ -252,7 +287,7 @@ public static OverlayRecognitionResult Recognize( scaleRatio = features.scaleRatio, scaleHint = scaleHint, anchorZone = top.anchorZone, - feedbackReason = $"{SpellLabels.Korean(top.op)} 장식이 seal에 붙었습니다." + feedbackReason = $"{SpellLabels.Korean(top.op)} 장식이 문양에 붙었습니다." }; } @@ -383,12 +418,12 @@ private static string BuildOverlayFailureReason(OverlayScore top, OverlayScaleHi { if (scaleHint == OverlayScaleHint.TooSmall) { - return $"{SpellLabels.Korean(top.op)} 장식처럼 보였지만 너무 작아 seal에 고정되지 않았습니다."; + return $"{SpellLabels.Korean(top.op)} 장식처럼 보였지만 너무 작아 문양에 고정되지 않았습니다."; } if (scaleHint == OverlayScaleHint.TooLarge) { - return $"{SpellLabels.Korean(top.op)} 장식처럼 보였지만 너무 커서 seal 안쪽 기준을 벗어났습니다."; + return $"{SpellLabels.Korean(top.op)} 장식처럼 보였지만 너무 커서 빛나는 원 안쪽 기준을 벗어났습니다."; } if (top.shapeConfidence >= 0.55f) @@ -397,8 +432,8 @@ private static string BuildOverlayFailureReason(OverlayScore top, OverlayScaleHi } return ambiguous - ? "장식 후보가 겹쳐 아직 seal에 붙이지 않았습니다. 한 번에 한 가지 장식만 더 단순하게 그려 보세요." - : "장식의 모양과 위치가 seal 기준과 충분히 맞지 않았습니다."; + ? "장식 후보가 겹쳐 아직 문양에 붙이지 않았습니다. 한 번에 한 가지 장식만 더 단순하게 그려 보세요." + : "장식의 모양과 위치가 문양 기준과 충분히 맞지 않았습니다."; } private static bool OverlaySpecificGate(OverlayOperator op, int strokeCount, OverlayFeatures features) @@ -427,7 +462,7 @@ private static string BuildOverlayGateReason(OverlayOperator op) { return op switch { - OverlayOperator.SoulDot => "집중 장식은 seal 중심에 작은 원 또는 점처럼 한 번에 닫아 그려야 합니다. 두 줄만 그은 모양은 집중으로 확정하지 않습니다.", + OverlayOperator.SoulDot => "집중 장식은 문양 중심에 작은 원 또는 점처럼 한 번에 닫아 그려야 합니다. 두 줄만 그은 모양은 집중으로 확정하지 않습니다.", OverlayOperator.IceBar => "얼음 장식은 한 획의 곧은 수평선으로 그려야 합니다.", OverlayOperator.VoidCut => "절단 장식은 한 획의 곧은 대각선으로 그려야 합니다.", OverlayOperator.SteelBrace => "보강 장식은 꺾인 ㄷ자 형태가 분명해야 합니다.", diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/CustomShapes/CustomShapeBookController.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/CustomShapes/CustomShapeBookController.cs index 800c9b2..06c4615 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/CustomShapes/CustomShapeBookController.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/CustomShapes/CustomShapeBookController.cs @@ -320,7 +320,7 @@ private void EnsurePage() modalRoot.SetAsLastSibling(); pagePanel = CreatePanel("Custom Shape Page", modalRoot, Vector2.zero, new Vector2(960f, 540f), UiAnchor.Center, new Color(0.032f, 0.04f, 0.058f, 0.995f)); AddSimpleBorder(pagePanel, new Color(0.25f, 0.34f, 0.48f, 0.82f), 2f); - CreateText("Custom Shape Title", pagePanel, "커스텀 도형", 24, FontStyle.Bold, new Vector2(24f, -18f), new Vector2(360f, 36f), UiAnchor.TopLeft); + CreateText("Custom Shape Title", pagePanel, "도형 책", 24, FontStyle.Bold, new Vector2(24f, -18f), new Vector2(360f, 36f), UiAnchor.TopLeft); CreateButton("Close Custom Shape Page", pagePanel, "닫기", 15, new Vector2(-22f, -18f), new Vector2(72f, 34f), UiAnchor.TopRight, ClosePage); wheelRoot = CreatePanel("Custom Shape Wheel Root", pagePanel, new Vector2(-136f, -4f), new Vector2(600f, 438f), UiAnchor.Center, new Color(0f, 0f, 0f, 0f)); diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/CustomShapes/CustomShapeEvents.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/CustomShapes/CustomShapeEvents.cs index 63425b5..357a388 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/CustomShapes/CustomShapeEvents.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/CustomShapes/CustomShapeEvents.cs @@ -50,7 +50,8 @@ public enum CustomSpellEffectKind Connection, Stability, LivingBridge, - WindPlatform + WindPlatform, + Steel } public readonly struct CustomSpellEffectDefinition @@ -88,14 +89,33 @@ SpellFamily.Fire when HasToken(spell, "line") || IsEvent(spell, CustomShapeEvent SpellFamily.Water when HasToken(spell, "ellipse") || IsEvent(spell, CustomShapeEventKind.Barrier) => For(CustomSpellEffectKind.Cleanse), SpellFamily.Fire when HasToken(spell, "star") || IsEvent(spell, CustomShapeEventKind.MagicAmplify) => For(CustomSpellEffectKind.Focus), SpellFamily.Wind when HasToken(spell, "wave") || IsEvent(spell, CustomShapeEventKind.MoveSpeedBuff) => For(CustomSpellEffectKind.Flow), - SpellFamily.Life when IsEvent(spell, CustomShapeEventKind.DirectionalProjectile) && HasToken(spell, "rect") => For(CustomSpellEffectKind.LivingBridge), + SpellFamily.Life when IsEvent(spell, CustomShapeEventKind.DirectionalProjectile) || HasToken(spell, "arrow") => For(CustomSpellEffectKind.LivingBridge), SpellFamily.Life when HasToken(spell, "brace") || IsEvent(spell, CustomShapeEventKind.AttackBuff) => For(CustomSpellEffectKind.Connection), + SpellFamily.Earth when HasToken(spell, "pentagon") || IsEvent(spell, CustomShapeEventKind.GuardBuff) => For(CustomSpellEffectKind.Steel), SpellFamily.Wind when HasToken(spell, "rect") || IsEvent(spell, CustomShapeEventKind.WallEntity) => For(CustomSpellEffectKind.WindPlatform), SpellFamily.Earth when HasToken(spell, "rect") || IsEvent(spell, CustomShapeEventKind.WallEntity) => For(CustomSpellEffectKind.Stability), _ => For(CustomSpellEffectKind.None) }; } + public static bool IsSingleStepTransform(SpellFamily baseFamily, SpellResult spell, CustomSpellEffectKind kind) + { + if (spell == null || !spell.isCustomShape) + { + return false; + } + + return baseFamily switch + { + SpellFamily.Fire => kind == CustomSpellEffectKind.Electric && HasToken(spell, "line"), + SpellFamily.Water => kind == CustomSpellEffectKind.Ice && HasToken(spell, "hexagon"), + SpellFamily.Earth => kind == CustomSpellEffectKind.Steel && HasToken(spell, "pentagon"), + SpellFamily.Wind => kind == CustomSpellEffectKind.Flow && HasToken(spell, "wave"), + SpellFamily.Life => kind == CustomSpellEffectKind.Connection && HasToken(spell, "brace"), + _ => false + }; + } + public static CustomSpellEffectDefinition For(CustomSpellEffectKind kind) { return kind switch @@ -106,8 +126,9 @@ public static CustomSpellEffectDefinition For(CustomSpellEffectKind kind) CustomSpellEffectKind.Focus => new(kind, "불꽃 초점", "불꽃 + 별 초점", "별 모양 초점이 다음 타격이 모일 지점을 밝혀 줍니다.", 24), CustomSpellEffectKind.Flow => new(kind, "바람 물결", "바람 + 물결", "바람이 물결 경로를 따라 이동 흐름을 만듭니다.", 20), CustomSpellEffectKind.Connection => new(kind, "생명 연결", "생명 + 연결선", "생명력이 떨어진 대상을 묶어 연결합니다.", 22), + CustomSpellEffectKind.Steel => new(kind, "강철 오각형", "땅 + 오각형", "땅 문양이 오각형 구조로 압축되어 강철 문양으로 굳습니다.", 26), CustomSpellEffectKind.Stability => new(kind, "구멍 메우기", "땅 + 메움판", "사각 암반판이 깨진 바닥 구멍을 메워 길을 안정시킵니다.", 16), - CustomSpellEffectKind.LivingBridge => new(kind, "덩굴 다리", "생명 + 화살표 + 사각판", "덩굴이 화살 방향으로 뻗어 낭떠러지를 잇습니다.", 0), + CustomSpellEffectKind.LivingBridge => new(kind, "덩굴 다리", "생명 + 화살표", "덩굴이 화살 방향으로 뻗어 낭떠러지를 잇습니다.", 0), CustomSpellEffectKind.WindPlatform => new(kind, "바람 발판", "바람 + 발판", "바람이 사각 발판을 띄워 건너갈 길을 만듭니다.", 0), _ => new(CustomSpellEffectKind.None, "", "", "", 0) }; @@ -251,21 +272,22 @@ public static class CustomShapeEventCatalog { private static readonly CustomShapeEventDefinition[] Definitions = { - new("line", "line_slash_damage", "절단 피해", CustomShapeEventRole.Effect, CustomShapeEventKind.SlashDamage, "이벤트: 절단 피해", usesDirection: true), + new("line", "line_slash_damage", "절단 피해", CustomShapeEventRole.Effect, CustomShapeEventKind.SlashDamage, "반응: 절단 피해", usesDirection: true), new("arrow", "arrow_operator", "방향 사출", CustomShapeEventRole.Operator, CustomShapeEventKind.DirectionalProjectile, "연산자: 끝점 방향 사출", usesDirection: true, emitsFromEndPoint: true, operatorOnlyKind: CustomShapeEventKind.AttributeLaser, operatorTargetKind: CustomShapeEventKind.DirectionalProjectile), - new("rect", "rect_wall_entity", "벽 생성", CustomShapeEventRole.Effect, CustomShapeEventKind.WallEntity, "이벤트: 벽 구조물", visualPersistence: CustomShapeEventPersistence.Permanent), - new("roundRect", "round_rect_guard_buff", "방어 버프", CustomShapeEventRole.Effect, CustomShapeEventKind.GuardBuff, "이벤트: 방어 버프"), - new("ellipse", "ellipse_barrier", "배리어", CustomShapeEventRole.Effect, CustomShapeEventKind.Barrier, "이벤트: 배리어"), - new("triangle", "triangle_trap", "함정", CustomShapeEventRole.Effect, CustomShapeEventKind.Trap, "이벤트: 함정 설치"), - new("diamond", "diamond_piercing_mark", "관통 표식", CustomShapeEventRole.Effect, CustomShapeEventKind.PiercingMark, "이벤트: 관통 표식"), - new("pentagon", "pentagon_guard_buff", "수호 버프", CustomShapeEventRole.Effect, CustomShapeEventKind.GuardBuff, "이벤트: 수호 버프"), - new("hexagon", "hexagon_stun", "스턴", CustomShapeEventRole.Effect, CustomShapeEventKind.Stun, "이벤트: 스턴"), - new("star", "star_magic_amplify", "마법 강화", CustomShapeEventRole.Effect, CustomShapeEventKind.MagicAmplify, "이벤트: 다음 마법 강화"), - new("arc", "arc_special_attack", "특공 상승", CustomShapeEventRole.Effect, CustomShapeEventKind.SpecialAttackBoost, "이벤트: 특공 상승"), - new("curve", "curve_projectile", "곡선 사출", CustomShapeEventRole.Effect, CustomShapeEventKind.CurveProjectile, "이벤트: 곡선 사출", usesDirection: true), - new("wave", "wave_move_speed", "이동속도", CustomShapeEventRole.Effect, CustomShapeEventKind.MoveSpeedBuff, "이벤트: 이동속도"), - new("brace", "brace_attack_buff", "공격력 버프", CustomShapeEventRole.Effect, CustomShapeEventKind.AttackBuff, "이벤트: 공격력 버프"), - new("cross", "cross_operator", "버프 삭제", CustomShapeEventRole.Operator, CustomShapeEventKind.BuffDispel, "연산자: 겹친 이벤트 차단", blocksOverlappedEvent: true, operatorOnlyKind: CustomShapeEventKind.RandomBuffDispel, operatorTargetKind: CustomShapeEventKind.EventBlock) + new("beamArrow", "beam_arrow_laser", "속성 레이저", CustomShapeEventRole.Effect, CustomShapeEventKind.AttributeLaser, "반응: 화살표 방향 속성 빛줄기", usesDirection: true, emitsFromEndPoint: true), + new("rect", "rect_wall_entity", "벽 생성", CustomShapeEventRole.Effect, CustomShapeEventKind.WallEntity, "반응: 벽 구조물", visualPersistence: CustomShapeEventPersistence.Permanent), + new("roundRect", "round_rect_guard_buff", "방어 버프", CustomShapeEventRole.Effect, CustomShapeEventKind.GuardBuff, "반응: 방어 버프"), + new("ellipse", "ellipse_barrier", "배리어", CustomShapeEventRole.Effect, CustomShapeEventKind.Barrier, "반응: 배리어"), + new("triangle", "triangle_trap", "함정", CustomShapeEventRole.Effect, CustomShapeEventKind.Trap, "반응: 함정 설치"), + new("diamond", "diamond_piercing_mark", "관통 표식", CustomShapeEventRole.Effect, CustomShapeEventKind.PiercingMark, "반응: 관통 표식"), + new("pentagon", "pentagon_guard_buff", "수호 버프", CustomShapeEventRole.Effect, CustomShapeEventKind.GuardBuff, "반응: 수호 버프"), + new("hexagon", "hexagon_stun", "스턴", CustomShapeEventRole.Effect, CustomShapeEventKind.Stun, "반응: 스턴"), + new("star", "star_magic_amplify", "마법 강화", CustomShapeEventRole.Effect, CustomShapeEventKind.MagicAmplify, "반응: 다음 마법 강화"), + new("arc", "arc_special_attack", "특공 상승", CustomShapeEventRole.Effect, CustomShapeEventKind.SpecialAttackBoost, "반응: 특공 상승"), + new("curve", "curve_projectile", "곡선 사출", CustomShapeEventRole.Effect, CustomShapeEventKind.CurveProjectile, "반응: 곡선 사출", usesDirection: true), + new("wave", "wave_move_speed", "이동속도", CustomShapeEventRole.Effect, CustomShapeEventKind.MoveSpeedBuff, "반응: 이동속도"), + new("brace", "brace_attack_buff", "공격력 버프", CustomShapeEventRole.Effect, CustomShapeEventKind.AttackBuff, "반응: 공격력 버프"), + new("cross", "cross_operator", "버프 삭제", CustomShapeEventRole.Operator, CustomShapeEventKind.BuffDispel, "연산자: 겹친 반응 차단", blocksOverlappedEvent: true, operatorOnlyKind: CustomShapeEventKind.RandomBuffDispel, operatorTargetKind: CustomShapeEventKind.EventBlock) }; public static IReadOnlyList All => Definitions; @@ -337,7 +359,9 @@ public static CustomShapeEventPayload BuildPayload( var targetDefinition = normalized.FirstOrDefault(definition => !definition.IsOperator); if (operatorDefinition == null || targetDefinition == null) { - return BuildPayload(normalized[0].token, strokes, operatorOnly: true); + var single = normalized[0]; + var operatorOnly = !string.Equals(single.token, "arrow", StringComparison.OrdinalIgnoreCase); + return BuildPayload(single.token, strokes, operatorOnly); } return ComposeWithOperator( @@ -366,7 +390,7 @@ public static CustomShapeEventPayload ComposeWithOperator( if (operatorDefinition.blocksOverlappedEvent) { target.eventId = $"{operatorDefinition.token}_blocks_{target.shapeToken}"; - target.displayName = "이벤트 차단"; + target.displayName = "반응 차단"; target.eventKind = CustomShapeEventKind.EventBlock; target.eventBlocked = true; target.blockedByToken = operatorDefinition.token; @@ -469,7 +493,7 @@ private static string DisplayNameFor(CustomShapeEventDefinition definition, Cust { CustomShapeEventKind.AttributeLaser => "속성 레이저", CustomShapeEventKind.RandomBuffDispel => "무작위 버프 삭제", - CustomShapeEventKind.EventBlock => "이벤트 차단", + CustomShapeEventKind.EventBlock => "반응 차단", CustomShapeEventKind.DirectionalProjectile => "방향 사출", _ => definition.displayName }; diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/CustomShapes/CustomShapeProfiles.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/CustomShapes/CustomShapeProfiles.cs index f263486..4b7950a 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/CustomShapes/CustomShapeProfiles.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/CustomShapes/CustomShapeProfiles.cs @@ -168,6 +168,7 @@ public sealed class CustomShapeProfileStore { "line", "arrow", + "beamArrow", "rect", "roundRect", "ellipse", @@ -315,7 +316,7 @@ public bool TrySaveSlot( var gold = CustomShapeCaptureRecord.FromStrokes(goldStrokes); if (gold.ToStrokeSamples().Count == 0) { - message = "gold capture가 필요합니다."; + message = "기준 그림이 필요합니다."; return false; } diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/CustomShapes/CustomShapeRecognition.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/CustomShapes/CustomShapeRecognition.cs index 97ef80f..42a9f4e 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/CustomShapes/CustomShapeRecognition.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/CustomShapes/CustomShapeRecognition.cs @@ -33,7 +33,8 @@ public static CustomShapeRecognitionResult Recognize( IReadOnlyList> strokes, BaseRecognitionResult baseResult, CustomShapeProfileStore store, - SpellFamily? preferredMappedFamily = null) + SpellFamily? preferredMappedFamily = null, + IReadOnlyList preferredShapeTokens = null) { if (store == null || baseResult?.spell == null) { @@ -52,15 +53,15 @@ public static CustomShapeRecognitionResult Recognize( var defaultSimilarity = Mathf.Clamp01(baseResult.spell.confidence); var candidates = store.Slots .Where(slot => slot.IsOccupied) - .Select(slot => ScoreSlot(slot, drawable, normalized.cloud, features, defaultFamily, defaultSimilarity, preferredMappedFamily)) + .Select(slot => ScoreSlot(slot, drawable, normalized.cloud, features, defaultFamily, defaultSimilarity, preferredShapeTokens)) .ToList(); - ApplyPostSealContrastPreference(candidates, preferredMappedFamily, defaultFamily, defaultSimilarity); + ApplyPostSealContrastPreference(candidates, defaultFamily, defaultSimilarity, preferredShapeTokens); var best = candidates .Where(result => result.customScore >= result.holdThreshold) .OrderByDescending(result => result.accepted ? 2 : result.held ? 1 : 0) - .ThenByDescending(result => preferredMappedFamily.HasValue && result.slot.mappedFamily == preferredMappedFamily.Value ? 1 : 0) + .ThenByDescending(result => PreferredShapeCoverage(result.slot, preferredShapeTokens)) .ThenByDescending(result => result.contrastMargin) .ThenByDescending(result => result.customScore) .FirstOrDefault(); @@ -86,9 +87,10 @@ public static bool ApplyToBaseResult( BaseRecognitionResult baseResult, IReadOnlyList> strokes, CustomShapeProfileStore store, - SpellFamily? preferredMappedFamily = null) + SpellFamily? preferredMappedFamily = null, + IReadOnlyList preferredShapeTokens = null) { - var result = Recognize(strokes, baseResult, store, preferredMappedFamily); + var result = Recognize(strokes, baseResult, store, preferredMappedFamily, preferredShapeTokens); if (result == null || baseResult?.spell == null) { return false; @@ -103,43 +105,45 @@ public static bool ApplyToBaseResult( return false; } - ApplyCustomMetadata(spell, result, strokes); + ApplyCustomMetadata(spell, result, strokes, preferredMappedFamily); spell.status = result.defaultConflict ? RecognitionStatus.Incomplete : RecognitionStatus.Ambiguous; spell.recognizedFamily = null; - spell.targetFamily = result.slot.mappedFamily; + spell.targetFamily = EffectiveMappedFamily(result, preferredMappedFamily); spell.success = false; spell.confidence = Mathf.Max(spell.confidence, result.customScore); spell.feedbackReason = result.defaultConflict - ? $"{result.slot.label} 도형은 보였지만 {SpellLabels.Korean(result.defaultFamily)} 기본 도형과 너무 가까워 보류했습니다." - : $"{result.slot.label} 커스텀 도형이 아직 안정 기준에 조금 부족합니다."; - spell.nextHint = "같은 슬롯의 gold capture와 비슷한 크기와 획 순서로 한 번 더 그려 보세요."; + ? $"{result.slot.label} 도형은 보였지만 {SpellLabels.Korean(result.defaultFamily)} 기본 문양과 너무 가까워 보류했습니다." + : $"{result.slot.label} 가져온 도형이 아직 안정 기준에 조금 부족합니다."; + spell.nextHint = "같은 슬롯의 기준 그림과 비슷한 크기와 획 순서로 한 번 더 그려 보세요."; return true; } - ApplyCustomMetadata(spell, result, strokes); + var effectiveFamily = EffectiveMappedFamily(result, preferredMappedFamily); + ApplyCustomMetadata(spell, result, strokes, preferredMappedFamily); spell.status = RecognitionStatus.Recognized; - spell.recognizedFamily = result.slot.mappedFamily; - spell.targetFamily = result.slot.mappedFamily; - spell.mappedFamily = result.slot.mappedFamily; + spell.recognizedFamily = effectiveFamily; + spell.targetFamily = effectiveFamily; + spell.mappedFamily = effectiveFamily; spell.success = true; spell.confidence = Mathf.Clamp01(Mathf.Max(spell.confidence, result.customScore)); spell.feedbackReason = - $"{result.slot.label} 커스텀 도형이 {SpellLabels.Korean(result.slot.mappedFamily)} 효과로 안정화되었습니다."; + $"{result.slot.label} 가져온 도형이 {SpellLabels.Korean(effectiveFamily)} 효과로 안정화되었습니다."; spell.nextHint = - $"기본 유사도 {result.defaultSimilarityScore:0.00}, custom {result.customScore:0.00}. 이후 입력은 이 슬롯 보정에 반영됩니다."; + $"기본 유사도 {result.defaultSimilarityScore:0.00}, 가져온 도형 점수 {result.customScore:0.00}. 이후 입력은 이 슬롯 보정에 반영됩니다."; return true; } private static void ApplyCustomMetadata( SpellResult spell, CustomShapeRecognitionResult result, - IReadOnlyList> strokes) + IReadOnlyList> strokes, + SpellFamily? preferredMappedFamily) { spell.isCustomShape = true; spell.customShapeId = result.slot.shapeId; spell.customShapeLabel = result.slot.label; spell.customShapeToken = result.slot.shapeToken; - spell.mappedFamily = result.slot.mappedFamily; + spell.mappedFamily = EffectiveMappedFamily(result, preferredMappedFamily); spell.customScore = Round(result.customScore); spell.defaultSimilarityScore = Round(result.defaultSimilarityScore); spell.personalization = result.summary; @@ -168,7 +172,7 @@ private static CustomShapeRecognitionResult ScoreSlot( ShapeFeatures features, SpellFamily defaultFamily, float defaultSimilarity, - SpellFamily? preferredMappedFamily) + IReadOnlyList preferredShapeTokens) { var goldScores = slot.goldCaptures .Select(capture => ScoreCapture(capture, cloud, features)) @@ -192,23 +196,23 @@ private static CustomShapeRecognitionResult ScoreSlot( var featureScore = ScoreFeatureSimilarity(features, AverageCaptureFeatures(slot)); var maturity = Mathf.Clamp01((slot.goldCaptures.Count + slot.autoCaptures.Count + slot.followCaptures.Count * 0.7f) / 6f); var customScore = Mathf.Clamp01(goldScore * 0.57f + featureScore * 0.21f + autoScore * 0.10f + followScore * 0.07f + maturity * 0.05f); - var preferredSlot = preferredMappedFamily.HasValue && slot.mappedFamily == preferredMappedFamily.Value; + customScore = ApplyPrimitiveShapeGuards(slot, features, customScore); + var preferredShapeCoverage = PreferredShapeCoverage(slot, preferredShapeTokens); var acceptThreshold = Mathf.Clamp( 0.73f - Mathf.Min(slot.autoCaptures.Count, 8) * 0.01f - Mathf.Min(slot.followCaptures.Count, 6) * 0.008f, 0.62f, 0.78f); - if (preferredSlot) + if (preferredShapeCoverage > 0f) { - acceptThreshold = Mathf.Clamp(acceptThreshold - 0.07f, 0.62f, 0.78f); + var shapeBias = Mathf.Lerp(0.035f, 0.075f, preferredShapeCoverage); + acceptThreshold = Mathf.Clamp(acceptThreshold - shapeBias, 0.56f, 0.78f); + customScore = Mathf.Clamp01(customScore + 0.04f * preferredShapeCoverage); } var holdThreshold = Mathf.Clamp(acceptThreshold - 0.13f, 0.48f, 0.68f); - var conflict = !preferredSlot && - defaultSimilarity >= 0.78f && - defaultFamily != slot.mappedFamily && - customScore < defaultSimilarity + 0.08f; + var conflict = false; var accepted = customScore >= acceptThreshold && !conflict; var held = !accepted && (customScore >= holdThreshold || conflict); @@ -246,13 +250,77 @@ private static CustomShapeRecognitionResult ScoreSlot( }; } + private static float ApplyPrimitiveShapeGuards(CustomShapeSlot slot, ShapeFeatures features, float score) + { + if (slot == null) + { + return score; + } + + if (string.Equals(slot.shapeToken, "line", StringComparison.OrdinalIgnoreCase) && + features.strokeCount > 1 && + SlotWasCapturedAsSingleStroke(slot)) + { + return Mathf.Min(score * 0.36f, 0.38f); + } + + return score; + } + + private static bool SlotMatchesPreferredShape(CustomShapeSlot slot, IReadOnlyList preferredShapeTokens) + { + return PreferredShapeCoverage(slot, preferredShapeTokens) > 0f; + } + + private static float PreferredShapeCoverage(CustomShapeSlot slot, IReadOnlyList preferredShapeTokens) + { + if (slot == null || preferredShapeTokens == null || preferredShapeTokens.Count == 0) + { + return 0f; + } + + var requestedTokens = preferredShapeTokens + .Where(token => !string.IsNullOrWhiteSpace(token)) + .Select(token => token.Trim().ToLowerInvariant()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + if (requestedTokens.Count == 0) + { + return 0f; + } + + var eventShapeTokens = slot.eventShapeTokens == null + ? Enumerable.Empty() + : slot.eventShapeTokens; + var availableTokens = eventShapeTokens + .Where(token => !string.IsNullOrWhiteSpace(token)) + .Select(token => token.Trim().ToLowerInvariant()) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + if (!string.IsNullOrWhiteSpace(slot.shapeToken)) + { + availableTokens.Add(slot.shapeToken.Trim().ToLowerInvariant()); + } + + var covered = requestedTokens.Count(token => availableTokens.Contains(token)); + return Mathf.Clamp01((float)covered / requestedTokens.Count); + } + + private static bool SlotWasCapturedAsSingleStroke(CustomShapeSlot slot) + { + return slot.AllCaptures() + .Select(capture => capture.ToStrokeSamples().Count) + .Where(count => count > 0) + .DefaultIfEmpty(1) + .Max() <= 1; + } + private static void ApplyPostSealContrastPreference( IReadOnlyList candidates, - SpellFamily? preferredMappedFamily, SpellFamily defaultFamily, - float defaultSimilarity) + float defaultSimilarity, + IReadOnlyList preferredShapeTokens) { - if (!preferredMappedFamily.HasValue || candidates.Count == 0) + if (candidates.Count == 0) { return; } @@ -270,19 +338,17 @@ private static void ApplyPostSealContrastPreference( if (contrastWinner && candidate.customScore >= 0.52f) { var followMaturity = Mathf.Clamp01(candidate.slot.followCaptures.Count / 3f); + var shapeCoverage = PreferredShapeCoverage(candidate.slot, preferredShapeTokens); var contrastBoost = Mathf.Clamp((Mathf.Max(margin, 0.12f) - 0.015f) * 0.45f, 0f, 0.045f); contrastBoost += followMaturity * 0.025f; - if (candidate.slot.mappedFamily == preferredMappedFamily.Value) - { - contrastBoost += 0.012f; - } + contrastBoost += shapeCoverage * 0.018f; candidate.contrastBoost = Round(contrastBoost); candidate.acceptThreshold = Round(Mathf.Clamp(candidate.acceptThreshold - contrastBoost, 0.56f, 0.78f)); candidate.holdThreshold = Round(Mathf.Clamp(candidate.acceptThreshold - 0.14f, 0.44f, 0.68f)); } - RefreshDecision(candidate, defaultFamily, defaultSimilarity, preferredMappedFamily, contrastWinner); + RefreshDecision(candidate, defaultFamily, defaultSimilarity, contrastWinner); } } @@ -290,22 +356,9 @@ private static void RefreshDecision( CustomShapeRecognitionResult result, SpellFamily defaultFamily, float defaultSimilarity, - SpellFamily? preferredMappedFamily, bool contrastWinner) { - var preferredSlot = preferredMappedFamily.HasValue && result.slot.mappedFamily == preferredMappedFamily.Value; - var conflict = !preferredSlot && - defaultSimilarity >= 0.78f && - defaultFamily != result.slot.mappedFamily && - result.customScore < defaultSimilarity + 0.08f; - if (preferredMappedFamily.HasValue && - contrastWinner && - result.contrastBoost > 0f && - result.customScore >= result.acceptThreshold && - result.contrastMargin >= 0.015f) - { - conflict = false; - } + var conflict = false; result.defaultConflict = conflict; result.accepted = result.customScore >= result.acceptThreshold && !conflict; @@ -330,6 +383,11 @@ private static void RefreshDecision( }; } + private static SpellFamily EffectiveMappedFamily(CustomShapeRecognitionResult result, SpellFamily? preferredMappedFamily) + { + return preferredMappedFamily ?? result.slot.mappedFamily; + } + private static float ScoreCapture(CustomShapeCaptureRecord capture, IReadOnlyList cloud, ShapeFeatures features) { var strokes = capture.ToStrokeSamples(); diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Input/WorldPointerInputSource.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Input/WorldPointerInputSource.cs index 5c68e4a..a8e8644 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Input/WorldPointerInputSource.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Input/WorldPointerInputSource.cs @@ -7,6 +7,8 @@ namespace MagicExamHall { public sealed class WorldPointerInputSource : MonoBehaviour { + private const float ForcedPointDuplicateDistance = 0.0025f; + public Camera mainCamera = null!; public int mouseButton = 1; public float minPointDistance = WorldDrawingController.DefaultMinPointDistance; @@ -22,6 +24,25 @@ public sealed class WorldPointerInputSource : MonoBehaviour public event Action StrokeCanceled = delegate { }; public bool IsDrawing => drawing; + public bool HasActiveWorldCenter => activePoints.Count > 0; + public Vector2 ActiveWorldCenter + { + get + { + if (activePoints.Count == 0) + { + return Vector2.zero; + } + + var sum = Vector2.zero; + foreach (var point in activePoints) + { + sum += point.Position; + } + + return sum / activePoints.Count; + } + } private void Awake() { @@ -76,7 +97,7 @@ private void BeginStroke(Vector2 screenPoint) private void CompleteStroke(Vector2 screenPoint) { - AddPoint(screenPoint); + AddPoint(screenPoint, force: true); drawing = false; if (activePoints.Count >= 2) @@ -96,9 +117,14 @@ private void AddPoint(Vector2 screenPoint, bool force = false) { var world = mainCamera.ScreenToWorldPoint(new Vector3(screenPoint.x, screenPoint.y, -mainCamera.transform.position.z)); var point = new StrokeInputPoint(new Vector2(world.x, world.y), Time.time, 1f, 0, InputCoordinateSpace.World); - if (!force && activePoints.Count > 0 && Vector2.Distance(activePoints[^1].Position, point.Position) < minPointDistance) + if (activePoints.Count > 0) { - return; + var distanceFromPrevious = Vector2.Distance(activePoints[^1].Position, point.Position); + if (distanceFromPrevious < ForcedPointDuplicateDistance || + (!force && distanceFromPrevious < minPointDistance)) + { + return; + } } activePoints.Add(point); diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Personalization/TutorialPersonalizationProfile.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Personalization/TutorialPersonalizationProfile.cs index b985417..51cab2b 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Personalization/TutorialPersonalizationProfile.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Personalization/TutorialPersonalizationProfile.cs @@ -215,11 +215,14 @@ public SpellResult ApplyBasePersonalization( result.personalization = summary; result.confidence = summary.adjustedConfidence; + var repeatedMismatch = summary.acceleratedByRepeatedCase && + (result.status == RecognitionStatus.Invalid || + result.status == RecognitionStatus.Incomplete || + result.status == RecognitionStatus.Ambiguous || + result.status == RecognitionStatus.Recognized && result.recognizedFamily != family); var canPromote = result.status == RecognitionStatus.Ambiguous || summary.extremeColdStartCorrection || - summary.acceleratedByRepeatedCase && - result.status == RecognitionStatus.Recognized && - result.recognizedFamily != family; + repeatedMismatch; if (canPromote && summary.decision == TutorialDynamicDecision.Accept) { result.status = RecognitionStatus.Recognized; @@ -640,7 +643,7 @@ public static MagicShapeFeatureVector DeriveShapeFeatures(IReadOnlyList stroke).Select(sample => sample.position).ToList(); var min = new Vector2(allPoints.Min(point => point.x), allPoints.Min(point => point.y)); var max = new Vector2(allPoints.Max(point => point.x), allPoints.Max(point => point.y)); - var diagonal = Mathf.Max(Vector2.Distance(min, max), 1f); + var diagonal = Mathf.Max(Vector2.Distance(min, max), 0.15f); var first = dominant[0].position; var last = dominant[^1].position; var closureGap = Vector2.Distance(first, last); @@ -652,8 +655,8 @@ public static MagicShapeFeatureVector DeriveShapeFeatures(IReadOnlyList stroke) return 0f; } - var simplified = RdpSimplify(points, 6f); - var area = Mathf.Abs(PolygonArea(simplified)); var min = new Vector2(points.Min(point => point.x), points.Min(point => point.y)); var max = new Vector2(points.Max(point => point.x), points.Max(point => point.y)); - var boxArea = Mathf.Max((max.x - min.x) * (max.y - min.y), 1f); + var diagonal = Mathf.Max(Vector2.Distance(min, max), 0.15f); + var simplified = RdpSimplify(points, Mathf.Max(diagonal * 0.035f, 0.008f)); + var area = Mathf.Abs(PolygonArea(simplified)); + var boxArea = Mathf.Max((max.x - min.x) * (max.y - min.y), 0.0001f); return Clamp(area / boxArea, 0f, 1f); } diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Presentation/WorldStrokeVisuals.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Presentation/WorldStrokeVisuals.cs index 0cd09fa..f709312 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Presentation/WorldStrokeVisuals.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Presentation/WorldStrokeVisuals.cs @@ -7,7 +7,7 @@ namespace MagicExamHall public sealed class WorldStrokeVisuals : MonoBehaviour { public const float DefaultStrokeVisualLifetimeSeconds = 4.2f; - public const float RecognizedStrokeFadeSeconds = 0.7f; + public const float RecognizedStrokeFadeSeconds = 0.32f; public const float InvalidStrokeFadeSeconds = 1.0f; public Color strokeColor = new(0.22f, 0.95f, 1f, 0.92f); @@ -70,7 +70,7 @@ public void ClearAll() public void MarkLastCompletedSessionRecognized(Color color) { - MarkPendingRecognition(StrokeVisualState.Recognized, new Color(color.r, color.g, color.b, 0.92f)); + MarkPendingRecognition(StrokeVisualState.Recognized, new Color(color.r, color.g, color.b, 0.38f)); } public void MarkLastCompletedSessionInvalid() @@ -212,10 +212,10 @@ public Color ColorFor(Color baseColor, float lifetimeSeconds) private Color RecognizedColor(Color baseColor) { - var tintT = Mathf.Clamp01(stateAge / 0.2f); - var fadeT = Mathf.Clamp01((stateAge - 0.2f) / 0.5f); + var tintT = Mathf.Clamp01(stateAge / 0.08f); + var fadeT = Mathf.Clamp01((stateAge - 0.08f) / Mathf.Max(WorldStrokeVisuals.RecognizedStrokeFadeSeconds - 0.08f, 0.001f)); var color = Color.Lerp(baseColor, targetColor, tintT); - color.a = Mathf.Lerp(0.92f, 0f, fadeT); + color.a = Mathf.Lerp(0.38f, 0f, fadeT); return color; } diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Recognition/SpellRecognitionHandoff.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Recognition/SpellRecognitionHandoff.cs index 2a937e4..878b83e 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Recognition/SpellRecognitionHandoff.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Recognition/SpellRecognitionHandoff.cs @@ -175,7 +175,7 @@ private string DefaultOverlayReason() return $"{SpellLabels.Korean(recognizedOperator.Value)} 양식으로 인식되었습니다."; } - return "양식 후보를 seal에 안정적으로 붙이지 못했습니다."; + return "장식 후보를 문양에 안정적으로 붙이지 못했습니다."; } } } diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Recognition/StrokeRecognitionService.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Recognition/StrokeRecognitionService.cs index bbd62e4..9b818ee 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Recognition/StrokeRecognitionService.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Recognition/StrokeRecognitionService.cs @@ -16,6 +16,7 @@ public sealed class RecognitionContext { public IReadOnlyList activeSeals = Array.Empty(); public BaseRecognitionIntent baseIntent; + public IReadOnlyList preferredCustomShapeTokens = Array.Empty(); public bool allowCustomShapes = true; public bool customShapesOnlyWhenSealActive; public bool hasCastCenter; @@ -70,17 +71,18 @@ public StrokeRecognitionResult Recognize(StrokeInputSession session, Recognition var center = context.hasCastCenter ? context.castCenter : session.GetWorldCenter(); var strokeCount = strokes.Count; var seals = context.activeSeals ?? Array.Empty(); - var hasActiveSeal = seals.Any(seal => context.now <= seal.expiresAt); var targetSeal = SpellCastingService.FindAttachableSeal(seals, center, context.now); var intentPreferredFamily = context.baseIntent?.IsActive == true ? context.baseIntent.family : (SpellFamily?)null; - if (context.customShapesOnlyWhenSealActive && hasActiveSeal) + var preferredCustomShapeTokens = context.preferredCustomShapeTokens ?? Array.Empty(); + if (context.customShapesOnlyWhenSealActive && targetSeal != null) { var customOnlyBase = RecognizeBaseCandidate( strokes, context.baseIntent, - targetSeal?.baseFamily ?? intentPreferredFamily, + null, + preferredCustomShapeTokens, context.allowCustomShapes, recordColdStartAttempt: false, now: context.now); @@ -107,7 +109,8 @@ public StrokeRecognitionResult Recognize(StrokeInputSession session, Recognition var baseResult = RecognizeBaseCandidate( strokes, context.baseIntent, - targetSeal.baseFamily, + null, + preferredCustomShapeTokens, context.allowCustomShapes, recordColdStartAttempt: false, now: context.now); @@ -142,7 +145,8 @@ public StrokeRecognitionResult Recognize(StrokeInputSession session, Recognition strokes, context.baseIntent, intentPreferredFamily, - context.allowCustomShapes, + preferredCustomShapeTokens, + context.allowCustomShapes && !context.customShapesOnlyWhenSealActive, recordColdStartAttempt: context.baseIntent?.IsActive == true, now: context.now); if (recognizedBase.spell.status == RecognitionStatus.Recognized && recognizedBase.spell.recognizedFamily.HasValue) @@ -189,6 +193,7 @@ private BaseRecognitionResult RecognizeBaseCandidate( List> strokes, BaseRecognitionIntent intent, SpellFamily? preferredCustomFamily = null, + IReadOnlyList preferredCustomShapeTokens = null, bool allowCustomShapes = true, bool recordColdStartAttempt = false, float now = 0f) @@ -196,9 +201,10 @@ private BaseRecognitionResult RecognizeBaseCandidate( var preparedIntent = PrepareBaseIntent(intent); var baseResult = SpellRuntime.RecognizeBase(strokes, preparedIntent); var personalizationTarget = preparedIntent?.IsActive == true ? preparedIntent.family : (SpellFamily?)null; + var goalLocalColdStart = IsGoalLocalColdStartIntent(preparedIntent); var rawColdStartSpell = recordColdStartAttempt && preparedIntent?.IsActive == true && - preparedIntent.tutorialCaptureCount < 3 + (preparedIntent.tutorialCaptureCount < 3 || goalLocalColdStart) ? CloneColdStartSpellResult(baseResult.spell) : null; PersonalizationStore.ApplyBasePersonalization(baseResult.spell, strokes, personalizationTarget); @@ -209,7 +215,7 @@ private BaseRecognitionResult RecognizeBaseCandidate( if (allowCustomShapes) { - CustomShapeRecognition.ApplyToBaseResult(baseResult, strokes, CustomShapeStore, preferredCustomFamily); + CustomShapeRecognition.ApplyToBaseResult(baseResult, strokes, CustomShapeStore, preferredCustomFamily, preferredCustomShapeTokens); } return baseResult; @@ -275,6 +281,7 @@ private BaseRecognitionIntent PrepareBaseIntent(BaseRecognitionIntent intent) } var targetCaptureCount = PersonalizationStore.CountBaseCaptures(intent.family); + var goalLocalColdStart = IsGoalLocalColdStartIntent(intent); return new BaseRecognitionIntent { family = intent.family, @@ -284,10 +291,30 @@ private BaseRecognitionIntent PrepareBaseIntent(BaseRecognitionIntent intent) radius = intent.radius, strength = intent.strength, tutorialCaptureCount = targetCaptureCount, - strongConsiderationEnabled = targetCaptureCount < StrongIntentColdStartCaptureLimit + strongConsiderationEnabled = targetCaptureCount < StrongIntentColdStartCaptureLimit || goalLocalColdStart }; } + private static bool IsGoalLocalColdStartIntent(BaseRecognitionIntent intent) + { + return intent?.IsActive == true && + string.Equals(intent.source, "near_goal_symbol", StringComparison.Ordinal) && + IsLaterFloorGoalId(intent.goalId); + } + + private static bool IsLaterFloorGoalId(string goalId) + { + if (string.IsNullOrWhiteSpace(goalId)) + { + return false; + } + + return goalId.StartsWith("custom_", StringComparison.Ordinal) || + goalId.StartsWith("beam_", StringComparison.Ordinal) || + goalId.StartsWith("final_", StringComparison.Ordinal) || + string.Equals(goalId, "earth_stairs", StringComparison.Ordinal); + } + private static void RejectNonCustomPostSealInput(BaseRecognitionResult baseResult) { if (baseResult?.spell == null) @@ -298,8 +325,8 @@ private static void RejectNonCustomPostSealInput(BaseRecognitionResult baseResul baseResult.spell.status = RecognitionStatus.Invalid; baseResult.spell.recognizedFamily = null; baseResult.spell.success = false; - baseResult.spell.feedbackReason = "활성 seal 이후의 추가 입력은 저장된 커스텀 도형만 사용합니다."; - baseResult.spell.nextHint = "커스텀 도형 책에 도형을 저장한 뒤, seal이 떠 있는 동안 그 도형을 그려 보세요."; + baseResult.spell.feedbackReason = "기본 문양이 빛나는 동안에는 원 안에 추가 도형만 이어 그릴 수 있습니다."; + baseResult.spell.nextHint = "새 기본 문양을 그리려면 빛나는 원 밖에서 다시 시작하세요. 원 안에서는 슬롯에 있는 추가 도형을 그리세요."; } public void RecordAcceptedResult(StrokeRecognitionResult result, float now) diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/AudioDirector.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/AudioDirector.cs index 2033ac1..182a294 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/AudioDirector.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/AudioDirector.cs @@ -32,26 +32,49 @@ public sealed class AudioDirector : MonoBehaviour private const int SampleRate = 44100; private readonly Dictionary sfxClips = new(); + private readonly Dictionary familyLayerClips = new(); + private readonly Dictionary customEffectLayerClips = new(); + private readonly Dictionary customShapeEventLayerClips = new(); private readonly Dictionary bgmClips = new(); private AudioSource sfxSource = null!; + private AudioSource familyLayerSource = null!; + private AudioSource customEffectLayerSource = null!; + private AudioSource customShapeEventLayerSource = null!; private AudioSource bgmSource = null!; private BgmCue currentBgm = BgmCue.None; public BgmCue CurrentBgmForTests => currentBgm; public int SfxClipCountForTests => sfxClips.Count; + public int FamilyLayerClipCountForTests => familyLayerClips.Count; + public int CustomEffectLayerClipCountForTests => customEffectLayerClips.Count; + public int CustomShapeEventLayerClipCountForTests => customShapeEventLayerClips.Count; public int BgmClipCountForTests => bgmClips.Count; + public bool HasFamilyLayerClipForTests(SpellFamily family) => familyLayerClips.ContainsKey(family); + public bool HasCustomEffectLayerClipForTests(CustomSpellEffectKind effect) => customEffectLayerClips.ContainsKey(effect); + public bool HasCustomShapeEventLayerClipForTests(CustomShapeEventKind eventKind) => customShapeEventLayerClips.ContainsKey(eventKind); public void Initialize() { - if (sfxSource != null && bgmSource != null) + if (sfxSource != null && + familyLayerSource != null && + customEffectLayerSource != null && + customShapeEventLayerSource != null && + bgmSource != null) { return; } sfxSource = gameObject.AddComponent(); - sfxSource.playOnAwake = false; - sfxSource.spatialBlend = 0f; - sfxSource.ignoreListenerPause = true; + ConfigureSfxSource(sfxSource); + + familyLayerSource = gameObject.AddComponent(); + ConfigureSfxSource(familyLayerSource); + + customEffectLayerSource = gameObject.AddComponent(); + ConfigureSfxSource(customEffectLayerSource); + + customShapeEventLayerSource = gameObject.AddComponent(); + ConfigureSfxSource(customShapeEventLayerSource); bgmSource = gameObject.AddComponent(); bgmSource.playOnAwake = false; @@ -102,16 +125,10 @@ public void PlayBgm(BgmCue cue) public void PlayBaseSuccess(SpellFamily family, QualityVector quality) { - var pitch = family switch - { - SpellFamily.Fire => 1.12f, - SpellFamily.Water => 0.94f, - SpellFamily.Wind => 1.06f, - SpellFamily.Earth => 0.88f, - SpellFamily.Life => 1.0f, - _ => 1f - }; - PlaySfx(AudioCue.CastBaseSuccess, quality.Average() < 0.58f ? 0.50f : 0.82f, pitch); + var pitch = FamilyPitch(family); + var averageQuality = quality.Average(); + PlaySfx(AudioCue.CastBaseSuccess, averageQuality < 0.58f ? 0.50f : 0.82f, pitch); + PlayFamilyLayer(family, averageQuality < 0.58f ? 0.28f : 0.44f, pitch); } public void PlayOverlaySuccess(OverlayOperator op) @@ -129,6 +146,30 @@ public void PlayOverlaySuccess(OverlayOperator op) PlaySfx(AudioCue.CastOverlaySuccess, 0.78f, pitch); } + public void PlayCustomSpellEffect(CustomSpellEffectKind effect, SpellFamily baseFamily) + { + if (effect == CustomSpellEffectKind.None) + { + return; + } + + var familyPitch = FamilyPitch(baseFamily); + var effectPitch = CustomEffectPitch(effect); + PlaySfx(AudioCue.CastOverlaySuccess, 0.52f, Mathf.Lerp(familyPitch, effectPitch, 0.35f)); + PlayFamilyLayer(baseFamily, 0.32f, familyPitch); + PlayCustomEffectLayer(effect, 0.64f, effectPitch); + } + + public void PlayCustomShapeEvent(CustomShapeEventKind eventKind) + { + if (eventKind == CustomShapeEventKind.None) + { + return; + } + + PlayCustomShapeEventLayer(eventKind, 0.46f, CustomShapeEventPitch(eventKind)); + } + public void PlaySfx(AudioCue cue, float volume = 0.82f, float pitch = 1f) { Initialize(); @@ -142,12 +183,58 @@ public void PlaySfx(AudioCue cue, float volume = 0.82f, float pitch = 1f) sfxSource.pitch = 1f; } + private void PlayFamilyLayer(SpellFamily family, float volume, float pitch) + { + Initialize(); + if (familyLayerClips.TryGetValue(family, out var clip)) + { + PlayLayerClip(familyLayerSource, clip, volume, pitch); + } + } + + private void PlayCustomEffectLayer(CustomSpellEffectKind effect, float volume, float pitch) + { + Initialize(); + if (customEffectLayerClips.TryGetValue(effect, out var clip)) + { + PlayLayerClip(customEffectLayerSource, clip, volume, pitch); + } + } + + private void PlayCustomShapeEventLayer(CustomShapeEventKind eventKind, float volume, float pitch) + { + Initialize(); + if (customShapeEventLayerClips.TryGetValue(eventKind, out var clip)) + { + PlayLayerClip(customShapeEventLayerSource, clip, volume, pitch); + } + } + + private static void PlayLayerClip(AudioSource source, AudioClip clip, float volume, float pitch) + { + source.pitch = Mathf.Clamp(pitch, 0.5f, 1.6f); + source.PlayOneShot(clip, Mathf.Clamp01(volume) * MagicExamSettings.SfxVolume); + source.pitch = 1f; + } + private void ApplyVolumes() { if (sfxSource != null) { sfxSource.volume = MagicExamSettings.SfxVolume; } + if (familyLayerSource != null) + { + familyLayerSource.volume = MagicExamSettings.SfxVolume; + } + if (customEffectLayerSource != null) + { + customEffectLayerSource.volume = MagicExamSettings.SfxVolume; + } + if (customShapeEventLayerSource != null) + { + customShapeEventLayerSource.volume = MagicExamSettings.SfxVolume; + } if (bgmSource != null) { bgmSource.volume = MagicExamSettings.BgmVolume * 0.55f; @@ -156,11 +243,34 @@ private void ApplyVolumes() private void BuildClips() { - if (sfxClips.Count > 0) + if (sfxClips.Count == 0) { - return; + BuildCoreSfxClips(); } + if (familyLayerClips.Count == 0) + { + BuildFamilyLayerClips(); + } + + if (customEffectLayerClips.Count == 0) + { + BuildCustomEffectLayerClips(); + } + + if (customShapeEventLayerClips.Count == 0) + { + BuildCustomShapeEventLayerClips(); + } + + if (bgmClips.Count == 0) + { + BuildBgmClips(); + } + } + + private void BuildCoreSfxClips() + { sfxClips[AudioCue.CastBaseSuccess] = ExternalSfx("cast_base_success") ?? Tone("cast_base_success", 0.40f, 523f, 784f, Wave.Sine, 0.12f); sfxClips[AudioCue.CastOverlaySuccess] = ExternalSfx("cast_overlay_success") ?? Tone("cast_overlay_success", 0.30f, 660f, 990f, Wave.Triangle, 0.10f); sfxClips[AudioCue.CastFinalEffect] = ExternalSfx("cast_final_effect") ?? Tone("cast_final_effect", 0.80f, 392f, 1175f, Wave.Sine, 0.18f); @@ -173,6 +283,54 @@ private void BuildClips() sfxClips[AudioCue.HazardReset] = ExternalSfx("hazard_reset") ?? Noise("hazard_reset", 0.50f, 0.18f); sfxClips[AudioCue.NpcAppear] = ExternalSfx("npc_appear") ?? Tone("npc_appear", 0.40f, 620f, 930f, Wave.Sine, 0.10f); sfxClips[AudioCue.EndingReportOpened] = ExternalSfx("ending_report_opened") ?? Tone("ending_report_opened", 0.70f, 294f, 880f, Wave.Sine, 0.16f); + } + + private void BuildFamilyLayerClips() + { + familyLayerClips[SpellFamily.Fire] = ExternalSfx("element_fire") ?? Tone("element_fire", 0.24f, 880f, 1320f, Wave.Triangle, 0.08f); + familyLayerClips[SpellFamily.Water] = ExternalSfx("element_water") ?? Noise("element_water", 0.28f, 0.08f); + familyLayerClips[SpellFamily.Wind] = ExternalSfx("element_wind") ?? Tone("element_wind", 0.32f, 720f, 1080f, Wave.Sine, 0.07f); + familyLayerClips[SpellFamily.Earth] = ExternalSfx("element_earth") ?? Tone("element_earth", 0.30f, 180f, 240f, Wave.Square, 0.06f); + familyLayerClips[SpellFamily.Life] = ExternalSfx("element_life") ?? Tone("element_life", 0.34f, 440f, 660f, Wave.Triangle, 0.07f); + } + + private void BuildCustomEffectLayerClips() + { + customEffectLayerClips[CustomSpellEffectKind.Ice] = ExternalSfx("custom_ice") ?? Tone("custom_ice", 0.28f, 988f, 1480f, Wave.Triangle, 0.08f); + customEffectLayerClips[CustomSpellEffectKind.Electric] = ExternalSfx("custom_electric") ?? Noise("custom_electric", 0.20f, 0.12f); + customEffectLayerClips[CustomSpellEffectKind.Cleanse] = ExternalSfx("custom_cleanse") ?? Tone("custom_cleanse", 0.38f, 523f, 880f, Wave.Sine, 0.07f); + customEffectLayerClips[CustomSpellEffectKind.Focus] = ExternalSfx("custom_focus") ?? Tone("custom_focus", 0.26f, 660f, 1320f, Wave.Sine, 0.09f); + customEffectLayerClips[CustomSpellEffectKind.Flow] = ExternalSfx("custom_flow") ?? Tone("custom_flow", 0.34f, 620f, 760f, Wave.Sine, 0.07f); + customEffectLayerClips[CustomSpellEffectKind.Connection] = ExternalSfx("custom_connection") ?? Tone("custom_connection", 0.32f, 392f, 784f, Wave.Triangle, 0.07f); + customEffectLayerClips[CustomSpellEffectKind.Steel] = ExternalSfx("custom_steel") ?? Tone("custom_steel", 0.30f, 246f, 370f, Wave.Square, 0.06f); + customEffectLayerClips[CustomSpellEffectKind.Stability] = ExternalSfx("custom_stability") ?? Tone("custom_stability", 0.30f, 196f, 294f, Wave.Square, 0.06f); + customEffectLayerClips[CustomSpellEffectKind.LivingBridge] = ExternalSfx("custom_living_bridge") ?? Tone("custom_living_bridge", 0.36f, 330f, 660f, Wave.Triangle, 0.07f); + customEffectLayerClips[CustomSpellEffectKind.WindPlatform] = ExternalSfx("custom_wind_platform") ?? Tone("custom_wind_platform", 0.30f, 740f, 1110f, Wave.Sine, 0.07f); + } + + private void BuildCustomShapeEventLayerClips() + { + customShapeEventLayerClips[CustomShapeEventKind.SlashDamage] = ExternalSfx("event_slash_damage") ?? Tone("event_slash_damage", 0.18f, 520f, 260f, Wave.Square, 0.08f); + customShapeEventLayerClips[CustomShapeEventKind.DirectionalProjectile] = ExternalSfx("event_directional_projectile") ?? Tone("event_directional_projectile", 0.24f, 620f, 980f, Wave.Triangle, 0.07f); + customShapeEventLayerClips[CustomShapeEventKind.WallEntity] = ExternalSfx("event_wall_entity") ?? Tone("event_wall_entity", 0.26f, 190f, 160f, Wave.Square, 0.07f); + customShapeEventLayerClips[CustomShapeEventKind.Barrier] = ExternalSfx("event_barrier") ?? Tone("event_barrier", 0.30f, 392f, 784f, Wave.Sine, 0.07f); + customShapeEventLayerClips[CustomShapeEventKind.Trap] = ExternalSfx("event_trap") ?? Noise("event_trap", 0.20f, 0.08f); + customShapeEventLayerClips[CustomShapeEventKind.Stun] = ExternalSfx("event_stun") ?? Tone("event_stun", 0.20f, 988f, 494f, Wave.Triangle, 0.08f); + customShapeEventLayerClips[CustomShapeEventKind.MagicAmplify] = ExternalSfx("event_magic_amplify") ?? Tone("event_magic_amplify", 0.26f, 660f, 1320f, Wave.Sine, 0.08f); + customShapeEventLayerClips[CustomShapeEventKind.AttackBuff] = ExternalSfx("event_attack_buff") ?? Tone("event_attack_buff", 0.24f, 440f, 880f, Wave.Triangle, 0.07f); + customShapeEventLayerClips[CustomShapeEventKind.MoveSpeedBuff] = ExternalSfx("event_move_speed_buff") ?? Tone("event_move_speed_buff", 0.24f, 740f, 980f, Wave.Sine, 0.07f); + customShapeEventLayerClips[CustomShapeEventKind.SpecialAttackBoost] = ExternalSfx("event_special_attack_boost") ?? Tone("event_special_attack_boost", 0.24f, 740f, 1480f, Wave.Triangle, 0.08f); + customShapeEventLayerClips[CustomShapeEventKind.BuffDispel] = ExternalSfx("event_buff_dispel") ?? Noise("event_buff_dispel", 0.22f, 0.08f); + customShapeEventLayerClips[CustomShapeEventKind.EventBlock] = ExternalSfx("event_event_block") ?? Tone("event_event_block", 0.22f, 294f, 147f, Wave.Square, 0.07f); + customShapeEventLayerClips[CustomShapeEventKind.AttributeLaser] = ExternalSfx("event_attribute_laser") ?? Tone("event_attribute_laser", 0.22f, 880f, 1760f, Wave.Sine, 0.08f); + customShapeEventLayerClips[CustomShapeEventKind.RandomBuffDispel] = ExternalSfx("event_random_buff_dispel") ?? Noise("event_random_buff_dispel", 0.24f, 0.08f); + customShapeEventLayerClips[CustomShapeEventKind.PiercingMark] = ExternalSfx("event_piercing_mark") ?? Tone("event_piercing_mark", 0.22f, 520f, 1040f, Wave.Triangle, 0.07f); + customShapeEventLayerClips[CustomShapeEventKind.GuardBuff] = ExternalSfx("event_guard_buff") ?? Tone("event_guard_buff", 0.26f, 330f, 495f, Wave.Triangle, 0.07f); + customShapeEventLayerClips[CustomShapeEventKind.CurveProjectile] = ExternalSfx("event_curve_projectile") ?? Tone("event_curve_projectile", 0.24f, 620f, 840f, Wave.Sine, 0.07f); + } + + private void BuildBgmClips() + { bgmClips[BgmCue.AmbientTower] = ExternalBgm("ambient_tower") ?? PadLoop( "ambient_tower", 32f, @@ -187,6 +345,69 @@ private void BuildClips() pulseCycles: 12); } + private static void ConfigureSfxSource(AudioSource source) + { + source.playOnAwake = false; + source.spatialBlend = 0f; + source.ignoreListenerPause = true; + } + + private static float FamilyPitch(SpellFamily family) + { + return family switch + { + SpellFamily.Fire => 1.12f, + SpellFamily.Water => 0.94f, + SpellFamily.Wind => 1.06f, + SpellFamily.Earth => 0.88f, + SpellFamily.Life => 1.0f, + _ => 1f + }; + } + + private static float CustomEffectPitch(CustomSpellEffectKind effect) + { + return effect switch + { + CustomSpellEffectKind.Ice => 1.12f, + CustomSpellEffectKind.Electric => 1.24f, + CustomSpellEffectKind.Cleanse => 0.98f, + CustomSpellEffectKind.Focus => 1.16f, + CustomSpellEffectKind.Flow => 1.04f, + CustomSpellEffectKind.Connection => 0.96f, + CustomSpellEffectKind.Steel => 0.90f, + CustomSpellEffectKind.Stability => 0.86f, + CustomSpellEffectKind.LivingBridge => 0.92f, + CustomSpellEffectKind.WindPlatform => 1.10f, + _ => 1f + }; + } + + private static float CustomShapeEventPitch(CustomShapeEventKind eventKind) + { + return eventKind switch + { + CustomShapeEventKind.SlashDamage => 1.10f, + CustomShapeEventKind.DirectionalProjectile => 1.08f, + CustomShapeEventKind.WallEntity => 0.86f, + CustomShapeEventKind.Barrier => 0.96f, + CustomShapeEventKind.Trap => 0.92f, + CustomShapeEventKind.Stun => 1.14f, + CustomShapeEventKind.MagicAmplify => 1.18f, + CustomShapeEventKind.AttackBuff => 1.06f, + CustomShapeEventKind.MoveSpeedBuff => 1.16f, + CustomShapeEventKind.SpecialAttackBoost => 1.20f, + CustomShapeEventKind.BuffDispel => 0.90f, + CustomShapeEventKind.EventBlock => 0.84f, + CustomShapeEventKind.AttributeLaser => 1.22f, + CustomShapeEventKind.RandomBuffDispel => 0.94f, + CustomShapeEventKind.PiercingMark => 1.02f, + CustomShapeEventKind.GuardBuff => 0.88f, + CustomShapeEventKind.CurveProjectile => 1.12f, + _ => 1f + }; + } + /// /// File-first lookup: drop a licensed clip at /// Resources/Sfx/<name> or Resources/Bgm/<name> diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/ElementalInteractionSystem.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/ElementalInteractionSystem.cs index b4d135a..6ffed2a 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/ElementalInteractionSystem.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/ElementalInteractionSystem.cs @@ -85,6 +85,8 @@ public ElementalInteractionContext( public bool IsElectric => customEffect == CustomSpellEffectKind.Electric || eventKind == CustomShapeEventKind.SlashDamage && family == SpellFamily.Fire; + public bool IsSteel => customEffect == CustomSpellEffectKind.Steel; + public bool IsWindForce => family == SpellFamily.Wind || customEffect == CustomSpellEffectKind.Flow || eventKind == CustomShapeEventKind.MoveSpeedBuff || @@ -259,6 +261,11 @@ private ElementalReactionKind ResolveReaction(ElementalInteractionContext contex return ElementalReactionKind.Stabilize; } + if (context.IsSteel && (HasMaterial(ElementalMaterial.Stone) || HasMaterial(ElementalMaterial.Soil) || HasMaterial(ElementalMaterial.Metal))) + { + return ElementalReactionKind.Stabilize; + } + return ElementalReactionKind.None; } @@ -429,6 +436,7 @@ public static ElementalMaterial InferMaterial(string name, PixelSpriteKind kind) { PixelSpriteKind.Player => ElementalMaterial.Creature | ElementalMaterial.Cloth, PixelSpriteKind.Target => ElementalMaterial.Creature | ElementalMaterial.Wood | ElementalMaterial.Metal, + PixelSpriteKind.Scarecrow => ElementalMaterial.Creature | ElementalMaterial.Wood | ElementalMaterial.Plant, PixelSpriteKind.FloorTile => ElementalMaterial.Stone | ElementalMaterial.Soil, PixelSpriteKind.WallTrim => ElementalMaterial.Stone, PixelSpriteKind.Rug => ElementalMaterial.Cloth, @@ -506,6 +514,7 @@ public static bool InferWindMovable(string name, PixelSpriteKind kind, bool tile var normalized = (name ?? "").ToLowerInvariant(); return kind == PixelSpriteKind.Target || + kind == PixelSpriteKind.Scarecrow || kind == PixelSpriteKind.Rubble || kind == PixelSpriteKind.Candle || normalized.Contains("loose") || @@ -536,6 +545,7 @@ public static float SpellRadiusFor(SpellFamily family, CustomSpellEffectKind cus CustomSpellEffectKind.Electric => 2.35f, CustomSpellEffectKind.LivingBridge => 2.75f, CustomSpellEffectKind.WindPlatform => 2.80f, + CustomSpellEffectKind.Steel => 2.45f, CustomSpellEffectKind.Stability => 2.40f, CustomSpellEffectKind.Connection => 2.50f, _ => family == SpellFamily.Wind ? 2.70f : 2.20f diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/ExamGameController.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/ExamGameController.cs index d144e35..715dc04 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/ExamGameController.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/ExamGameController.cs @@ -4,6 +4,7 @@ using System.Linq; using UnityEngine; using UnityEngine.EventSystems; +using UnityEngine.Rendering.Universal; using UnityEngine.UI; namespace MagicExamHall @@ -11,11 +12,11 @@ namespace MagicExamHall public sealed class ExamGameController : MonoBehaviour { public const float GameplayCameraOrthographicSize = PixelRenderSetup.ReferenceResolutionY / (2f * PixelRenderSetup.AssetsPixelsPerUnit); - public const int FinalFloorPassingGoalCount = 5; - public const float StandardFloorAdvanceDelaySeconds = 1.4f; + public const int FinalFloorPassingGoalCount = 3; + public const float StandardFloorAdvanceDelaySeconds = 1.8f; public const float StageFloorAdvanceDelaySeconds = 2.6f; public const float FinalFloorPassReportDelaySeconds = 4.8f; - public const float FinalFloorCompleteReportDelaySeconds = 1.9f; + public const float FinalFloorCompleteReportDelaySeconds = 1.8f; public const float DefaultSealFallbackDelaySeconds = 1.35f; public const string BuildVersion = "Magic Exam Hall 0.6.0-dev"; private const string FloorThreeStageResourcePath = "StageDefinitions/Floor3Crossing"; @@ -26,39 +27,78 @@ public sealed class ExamGameController : MonoBehaviour private const float GoalProximityBubbleSeconds = 3.2f; private const float StageGatePlayerCenterVerticalPadding = 1.05f; private const int CustomReferenceFloorNumber = 2; + private const string FloorFourScarecrowTargetId = "floor4_scarecrow"; + private const int FinalExamTaskCount = 3; + private const float FloorAdvanceSpeechReadGuardSeconds = 1.15f; + private const float FloorAdvanceMaxSpeechWaitSeconds = 5.5f; + private const float FloorTransitionFadeSeconds = 2.4f; + private const float FloorTransitionMinimumPlayerAlpha = 0.15f; + private const float FloorTransitionPulseIntervalSeconds = 0.48f; + private const float FinalFloorAssistGoalAlpha = 0f; + private const string FinalFrozenRiverTaskId = "final_frozen_river"; + private const string FinalBeamFireTaskId = "final_beam_fire"; + private const string FinalBeamWaterTaskId = "final_beam_water"; + private const float AttributeBeamMaxDistance = 12.5f; + private const float AttributeBeamHitPadding = 0.28f; + private const float AttributeBeamAutoAimRadius = 7.25f; + private const float AttributeBeamAutoAimMinimumDot = -0.85f; private const float CustomReferenceShelfRadius = 1.85f; private const float CustomReferenceGoalSuppressRadius = 1.65f; + private const int RepeatedGoalRecognitionAcceptThreshold = 3; + private const float RepeatedGoalRecognitionMinimumIntentStrength = 0.42f; private static readonly Vector2 CustomReferenceBubbleShelfOffset = new(0.92f, -1.12f); private static readonly Vector2 CustomReferencePanelPosition = new(18f, 20f); private static readonly Vector2 CustomReferencePanelSize = new(560f, 372f); + private static readonly IReadOnlyDictionary FinalTaskIdBySourceGoalId = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["puddle"] = "final_puddle", + ["ember"] = "final_ember", + ["custom_fire"] = "final_custom_fire", + ["custom_wind"] = "final_custom_wind", + ["frozen_river"] = FinalFrozenRiverTaskId, + ["living_bridge"] = "final_living_bridge", + ["beam_fire"] = FinalBeamFireTaskId, + ["beam_water"] = FinalBeamWaterTaskId + }; + private const float FirstFloorSequentialCurrentGoalAlpha = 1f; private const float FirstFloorSequentialFutureGoalAlpha = 0.36f; private const float FirstFloorSequentialCompletedGoalAlpha = 0.86f; + private const float SequentialGoalGuideArrowScale = 0.50f; + private const float SequentialGoalGuideRingScaleMultiplier = 1.38f; private const int MaxPlayerHealthHalfUnits = 6; private const float PlayerDamageInvulnerabilitySeconds = 1.05f; private const float PlayerDamageBlinkSeconds = 0.9f; - private const float QuestScrollWidth = 430f; - private const float QuestScrollExpandedHeight = 392f; - private const float QuestScrollCollapsedHeight = 88f; + private const float QuestScrollWidth = 374f; + private const float QuestScrollExpandedHeight = 350f; + private const float QuestScrollCollapsedHeight = 78f; private const float QuestScrollAnimationSeconds = 0.32f; - private const float QuestScrollBodyTopOffset = 76f; - private const float QuestScrollContentInset = 26f; + private const float QuestScrollBodyTopOffset = 70f; + private const float QuestScrollContentInset = 20f; private const float QuestScrollContentWidth = QuestScrollWidth - QuestScrollContentInset * 2f; private const float KeyboardMovementPulseSeconds = 0.18f; + private const float CameraZoomMin = 0.72f; + private const float CameraZoomMax = 1.55f; + private const float CameraZoomStep = 0.10f; + private const float CameraZoomWheelStep = 0.08f; private static readonly string[] FirstFloorTutorialGoalOrder = { "puddle", "ember", "vane", "pillar", "vine" }; + private static readonly string[] SecondFloorSequenceGoalOrder = { "custom_water", "custom_fire", "custom_wind", "custom_earth", "custom_life" }; private const float PlatformMoveSpeed = 5.4f; private const float PlatformMoveAcceleration = 42f; private const float PlatformJumpVelocity = 7.6f; + private const float PlatformGravityScale = 3.25f; + private const float StageShelfApproachHalfWidth = 1.1f; + private const float StageShelfVerticalSpeed = 3.8f; + private const float StageShelfApproachTargetYOffset = -0.35f; + private const float StageObstacleGoalRadiusMinimum = 2.95f; + private const float StageObstacleGoalRadiusPadding = 0.65f; + private const float StageLedgeStopPadding = 0.08f; + private const float StageLedgeReleaseNudge = 0.06f; + private const float StageLedgeStopInputThreshold = 0.25f; + private const float StageLedgeStopResetDistance = 0.55f; private const string FirstFloorLetterBody = - "수험생에게, 첫 번째 시험의 목표를 남깁니다.\n" + - "1. 눈앞의 다섯 표식을 차례로 살피세요.\n" + - "2. 표식 아래 이름과 가까운 위치를 확인하세요.\n" + - "3. 바닥에 직접 기본 문양을 천천히 그리세요.\n" + - "4. 불꽃은 뾰족한 삼각형으로 시작합니다.\n" + - "5. 물은 둥근 원형 흐름으로 안정시킵니다.\n" + - "6. 바람은 같은 방향의 세 줄로 읽힙니다.\n" + - "7. 땅은 단단한 사각형으로 고정됩니다.\n" + - "8. 생명은 부드럽게 갈라지는 줄기로 이어집니다.\n" + - "9. 다섯 표식이 밝아지면 다음 층으로 오르세요."; + "탑에 온 것을 환영한다.\n" + + "5층의 탑을 모두 오르면 그대는 어엿한 한 명의 마법사가 되어 있을걸세.\n" + + "행운을 비네."; private static readonly Vector2 WestBookcasePosition = new(-7.25f, 1.1f); private static readonly IReadOnlyList FloorTwoCustomShapeReferences = new List { @@ -72,15 +112,21 @@ public sealed class ExamGameController : MonoBehaviour { new(SpellFamily.Water, "얼음 결정", "hexagon", new[] { "hexagon" }, "강물을 얼음 타일로 굳혀 올라탈 수 있게 합니다."), new(SpellFamily.Earth, "구멍 메움판", "rect", new[] { "rect" }, "깨진 바닥 구멍을 암반 판으로 메웁니다."), - new(SpellFamily.Life, "덩굴 다리", "rect", new[] { "arrow", "rect" }, "화살 방향으로 덩굴 다리를 뻗어 낭떠러지를 잇습니다."), + new(SpellFamily.Life, "덩굴 다리", "arrow", new[] { "arrow" }, "화살 방향으로 덩굴 다리를 뻗어 낭떠러지를 잇습니다."), new(SpellFamily.Wind, "바람 발판", "rect", new[] { "rect" }, "바람으로 사각 발판을 띄워 빈 공간을 건넙니다.") }; private static readonly IReadOnlyList FloorFourCustomShapeReferences = new List { - new(SpellFamily.Water, "얼음 결정", "hexagon", new[] { "hexagon" }, "표적의 움직임을 늦추는 얼음 결정을 만듭니다."), - new(SpellFamily.Fire, "번개 직선", "line", new[] { "line" }, "직선 경로로 번개 타격을 뻗습니다."), - new(SpellFamily.Water, "정화 물막", "ellipse", new[] { "ellipse" }, "둥근 물막으로 오염을 씻어 냅니다."), - new(SpellFamily.Earth, "사각 방벽", "rect", new[] { "rect" }, "표적 앞에 암반 방벽을 세웁니다.") + new(SpellFamily.Fire, "불꽃 빛줄기 화살표", "arrow", new[] { "beamArrow" }, "불꽃 문양에 더하면 화살표 방향으로 빛줄기를 쏩니다."), + new(SpellFamily.Water, "물결 빛줄기 화살표", "arrow", new[] { "beamArrow" }, "물결 문양에 더하면 화살표 방향으로 빛줄기를 쏩니다."), + new(SpellFamily.Wind, "바람 빛줄기 화살표", "arrow", new[] { "beamArrow" }, "바람 문양에 더하면 화살표 방향으로 빛줄기를 쏩니다."), + new(SpellFamily.Earth, "대지 빛줄기 화살표", "arrow", new[] { "beamArrow" }, "대지 문양에 더하면 화살표 방향으로 빛줄기를 쏩니다."), + new(SpellFamily.Life, "생명 빛줄기 화살표", "arrow", new[] { "beamArrow" }, "생명 문양에 더하면 화살표 방향으로 빛줄기를 쏩니다.") + }; + private static readonly IReadOnlyList FloorFiveBeamShapeReferences = new List + { + new(SpellFamily.Fire, "최종 불꽃 빛줄기 화살표", "arrow", new[] { "beamArrow" }, "최종 시험용 불꽃 빛줄기 화살표입니다."), + new(SpellFamily.Water, "최종 물결 빛줄기 화살표", "arrow", new[] { "beamArrow" }, "최종 시험용 물결 빛줄기 화살표입니다.") }; [Header("Scene References")] @@ -98,6 +144,7 @@ public sealed class ExamGameController : MonoBehaviour private readonly List playerBlinkRenderers = new(); private readonly List spriteAccentAnimations = new(); private readonly List shelfGuideArrows = new(); + private readonly List sequentialGoalGuides = new(); private readonly List activeStageGates = new(); private readonly List stageEntityObjects = new(); private readonly List stageEffectObjects = new(); @@ -110,7 +157,10 @@ public sealed class ExamGameController : MonoBehaviour private readonly List activeHazards = new(); private readonly Dictionary baseFailureCounts = new(); private readonly Dictionary questChecklistSnapshots = new(); + private readonly Dictionary> finalTaskEnvironmentObjects = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet questImportedReferenceIdsThisFloor = new(); + private readonly HashSet mentorGrantedReferenceKeysThisFloor = new(StringComparer.OrdinalIgnoreCase); + private readonly HashSet encounteredFinalTaskIds = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet discoveredFamilies = new(); private readonly HashSet discoveredOverlays = new(); @@ -124,6 +174,7 @@ public sealed class ExamGameController : MonoBehaviour private EndingReport endingReport = null!; private SpellCastingService spellCasting = null!; private IStrokeRecognitionService recognitionService = null!; + private TutorialPersonalizationStore globalPersonalizationStore = new(); private CustomShapeProfileStore customShapeStore = null!; private CustomShapeBookController customShapeBook = null!; private FloorGoalSystem floorGoals = null!; @@ -140,9 +191,11 @@ public sealed class ExamGameController : MonoBehaviour private RectTransform reportPanel = null!; private RectTransform toastPanel = null!; private RectTransform firstFloorLetterOverlay = null!; + private RectTransform finalTaskBanner = null!; private RectTransform customReferenceBubble = null!; private RectTransform customReferencePanel = null!; private RectTransform goalProximityBubble = null!; + private RectTransform cameraZoomPanel = null!; private RectTransform questScrollPanel = null!; private RectTransform questScrollBodyRoot = null!; private RectTransform questScrollBottomRoll = null!; @@ -150,20 +203,27 @@ public sealed class ExamGameController : MonoBehaviour private Text customReferenceStatus = null!; private Text goalProximityBubbleText = null!; private Button floorSkipButton = null!; + private Button endingReportExitButton = null!; private Button questScrollToggleButton = null!; + private Button cameraZoomInButton = null!; + private Button cameraZoomOutButton = null!; + private Button cameraZoomResetButton = null!; private Text hudTitle = null!; private Text hudCopy = null!; private Text floorProgress = null!; private Text noteText = null!; private Text resultText = null!; private Text reportText = null!; + private Text endingReportExitButtonText = null!; private Text toastText = null!; private Text firstFloorLetterText = null!; + private Text finalTaskBannerText = null!; private Text questTitleText = null!; private Text questScoreText = null!; private Text questScrollToggleText = null!; private Text questStatusText = null!; private Text questProgressText = null!; + private Text cameraZoomText = null!; private Button firstFloorLetterCloseButton = null!; private Image toastBackground = null!; private Image toastAccent = null!; @@ -173,9 +233,14 @@ public sealed class ExamGameController : MonoBehaviour private WorldStateGoal goalProximityBubbleGoal = null!; private string sessionId = ""; private int trialCounter; + private string repeatedGoalRecognitionKey = ""; + private int repeatedGoalRecognitionCount; private int playerHealthHalfUnits = MaxPlayerHealthHalfUnits; private float floorStartedAt; private float pendingAdvanceAt = -1f; + private float floorTransitionStartedAt = -1f; + private float floorAdvanceSpeechDeadlineAt = -1f; + private float floorTransitionNextPulseAt = -1f; private float playerDamageInvulnerableUntil = -1f; private float playerBlinkUntil = -1f; private Vector2 velocity; @@ -200,16 +265,32 @@ public sealed class ExamGameController : MonoBehaviour private float fallbackUpPulseUntil = -1f; private float fallbackJumpPulseUntil = -1f; private float platformHorizontalVelocity; + private string stageLedgeStopGoalId = ""; + private int stageLedgeStopDirection; + private Vector2 stageLedgeStopPosition; + private int actionEventCounter; + private float cameraZoom = 1f; private float goalProximityBubbleUntil = -1f; private float lastGoalProximityGuideDistance; private string customReferenceLastStatus = ""; private string lastGoalProximityGuideGoalId = ""; + private string sequentialGoalGuideGoalId = ""; + private string lastEnrollmentEventType = "session_started"; + private string lastEnrollmentEventUtc = ""; private bool gameplayInputEnabled = true; + private FloorTransitionState floorTransitionState = FloorTransitionState.None; private float floorEnteredAt; private float sessionStartedAt; private int castsOnCurrentFloor; private bool firstFloorGhostShown; private bool firstFloorLongSilenceShown; + private readonly List currentFinalTasks = new(); + private FinalExamTaskDefinition currentFinalTask; + private int currentFinalTaskIndex; + private string forcedFinalTaskIdForTests = ""; + private bool finalCertificateQueued; + private bool finalCertificateAwaitingExit; + private bool finalTaskBannerSuppressedForTitle; public event Action ProgressCheckpointed = delegate { }; @@ -221,25 +302,38 @@ public sealed class ExamGameController : MonoBehaviour public int CompletedGoalCountForTests => activeGoals.Count(goal => goal.completed); public string CurrentFirstFloorTutorialGoalIdForTests => CurrentFirstFloorTutorialGoal()?.id ?? ""; public SpellFamily? CurrentFirstFloorTutorialFamilyForTests => CurrentFirstFloorTutorialGoal()?.requiredBase; + public string CurrentSecondFloorSequenceGoalIdForTests => CurrentSecondFloorSequenceGoal()?.id ?? ""; + public SpellFamily? CurrentSecondFloorSequenceFamilyForTests => CurrentSecondFloorSequenceGoal()?.requiredBase; public Vector2 PlayerPosition => player == null ? Vector2.zero : player.position; public Vector2 SafePositionForTests => safePosition; public bool HasEndingReport => reportPanel != null && reportPanel.gameObject.activeSelf; + public bool CanShowTitleReturnPrompt => HasEndingReport && !finalCertificateAwaitingExit; public bool IsDrawingPanelVisible => false; public bool IsResultPanelVisible => resultPanel != null && resultPanel.gameObject.activeSelf; public bool IsFirstFloorLetterVisibleForTests => firstFloorLetterOverlay != null && firstFloorLetterOverlay.gameObject.activeSelf; public Color FirstFloorLetterCloseButtonColorForTests => firstFloorLetterCloseButton?.targetGraphic is Graphic graphic ? graphic.color : Color.clear; + public bool IsFinalTaskBannerVisibleForTests => finalTaskBanner != null && finalTaskBanner.gameObject.activeInHierarchy; + public string FinalTaskBannerTextForTests => finalTaskBannerText == null ? "" : finalTaskBannerText.text; public int CurrentAssistLevel { get; private set; } public string LastHintText { get; private set; } = ""; public string LastMagicNoteText => magicNote?.Text ?? ""; public string LastResultPanelTextForTests => resultText == null ? "" : resultText.text; public string CurrentMentorNameForTests => mentor == null ? "" : mentor.CurrentMentorName; public string MentorSpeechTextForTests => mentor == null ? "" : mentor.SpeechText; + public int MentorSpeechPageCountForTests => mentor?.SpeechPageCount ?? 0; + public int MentorSpeechPageIndexForTests => mentor?.SpeechPageIndex ?? 0; + public bool IsMentorSpeechNextButtonVisibleForTests => mentor?.IsSpeechNextButtonVisible ?? false; + public Vector3 MentorWorldPositionForTests => mentor == null ? Vector3.zero : mentor.WorldPositionForTests; + public float MentorWorldScaleForTests => mentor?.WorldScaleForTests ?? 0f; + public PixelSpriteKind MentorProfileNeutralKindForTests => mentor?.ProfileNeutralKindForTests ?? PixelSpriteKind.Player; public MentorMood MentorMoodForTests => mentor == null ? MentorMood.Neutral : mentor.CurrentMood; public bool IsMentorVisibleForTests => mentor != null && mentor.IsVisible; + public string HudTitleForTests => hudTitle == null ? "" : hudTitle.text; public string HudCopyForTests => hudCopy == null ? "" : hudCopy.text; public string FirstFloorLetterTextForTests => firstFloorLetterText == null ? "" : firstFloorLetterText.text; public string FloorProgressForTests => floorProgress == null ? "" : floorProgress.text; public string EndingReportTextForTests => reportText == null ? "" : reportText.text; + public bool IsEndingReportExitButtonVisibleForTests => endingReportExitButton != null && endingReportExitButton.gameObject.activeInHierarchy; public int TrialCountForTests => trialCounter; public string VersionLabelForTests => versionText == null ? "" : versionText.text; public bool IsGameplayInputEnabledForTests => gameplayInputEnabled; @@ -253,6 +347,7 @@ public sealed class ExamGameController : MonoBehaviour public int ActiveStageGateCountForTests => activeStageGates.Count; public int ActiveStageInteractionCountForTests => activeStageGates.Count; public int ActiveStageEntityCountForTests => stageEntityObjects.Count; + public int ActiveStageEntityColliderCountForTests => stageEntityObjects.Count(body => body != null && body.activeInHierarchy && body.GetComponent() != null); public int ActiveStageEffectVisualCountForTests => stageEffectObjects.Count(effect => effect.IsActive); public int ActiveCustomShapeEventObjectCountForTests => customEventObjects.Count(effect => effect.IsActive); public int PermanentCustomShapeEventObjectCountForTests => customEventObjects.Count(effect => effect.IsActive && effect.IsPermanent); @@ -261,6 +356,7 @@ public sealed class ExamGameController : MonoBehaviour public int CurrentHealthHalfUnitsForTests => playerHealthHalfUnits; public int HealthHeartCountForTests => healthHearts.Count; public int LastHealthHeartStateForTests => healthHearts.Count == 0 ? -1 : healthHearts[^1].State; + public bool IsHealthBarVisibleForTests => healthPanel != null && healthPanel.gameObject.activeInHierarchy; public bool IsPlayerBlinkingForTests => Time.time < playerBlinkUntil; public Color PlayerBlinkTintForTests => playerBlinkRenderers.Count == 0 ? Color.clear : playerBlinkRenderers[0].color; public int ActiveBuffQueueCountForTests => buffQueues.Count(queue => queue.IsActive); @@ -276,16 +372,47 @@ public sealed class ExamGameController : MonoBehaviour public string LastCustomShapeEventKindForTests { get; private set; } = ""; public string LastCustomShapeEventLabelForTests { get; private set; } = ""; public Vector2 LastCustomShapeEventDirectionForTests { get; private set; } = Vector2.right; + public bool LastBeamHitForTests { get; private set; } + public string LastDamagedTargetNameForTests { get; private set; } = ""; + public string CurrentFinalTaskIdForTests => currentFinalTask?.id ?? ""; + public IReadOnlyList CurrentFinalTaskIdsForTests => currentFinalTasks.Select(task => task.id).ToArray(); + public int CurrentFinalTaskIndexForTests => currentFinalTaskIndex; + public int CurrentFinalTaskCountForTests => currentFinalTasks.Count; + public string CurrentFinalTaskPromptForTests => currentFinalTask?.prompt ?? ""; + public IReadOnlyList EncounteredFinalTaskIdsForTests => encounteredFinalTaskIds + .OrderBy(id => id, StringComparer.OrdinalIgnoreCase) + .ToArray(); + public int ActiveFinalTaskEnvironmentObjectCountForTests => finalTaskEnvironmentObjects.Values + .SelectMany(objects => objects) + .Where(body => body != null) + .Distinct() + .Count(body => body.activeInHierarchy); public int CustomShapeEventObjectCountForTests { get; private set; } public int VisibleGoalLabelCountForTests => activeGoals.Count(goal => goal.label != null && goal.label.gameObject.activeInHierarchy); public int VisibleGoalObjectCountForTests => activeGoals.Count(goal => IsGoalVisible(goal)); + public int TransparentFinalGoalObjectCountForTests => activeGoals.Count(IsGoalTransparentAssist); public int VisibleOverlayGuideCountForTests => seals.Count(seal => seal.HasAttachGuide); public int ActiveShelfGuideArrowCountForTests => shelfGuideArrows.Count(arrow => arrow.IsActive); + public int ActiveSequentialGoalGuideCountForTests => sequentialGoalGuides.Count(guide => guide.IsActive); + public string CurrentSequentialGoalGuideGoalIdForTests => sequentialGoalGuides.FirstOrDefault(guide => guide.IsActive)?.GoalId ?? sequentialGoalGuideGoalId; public bool IsFloorSkipButtonVisibleForTests => floorSkipButton != null && floorSkipButton.gameObject.activeInHierarchy; public string OutputDirectory => logger?.OutputDirectory ?? ""; + public string ActionEventsCsvPathForTests => logger?.ActionEventsCsvPath ?? ""; + public string CertificateCsvPathForTests => logger?.CertificateCsvPath ?? ""; + public string EnrollmentCsvPathForTests => logger?.EnrollmentCsvPath ?? ""; public bool IsLogCollectionEnabledForTests => logger?.IsCollectionEnabled ?? false; + public bool IsFloorTransitionActiveForTests => floorTransitionState != FloorTransitionState.None; + public string FloorTransitionStateForTests => floorTransitionState.ToString(); public float PendingAdvanceSecondsForTests => pendingAdvanceAt < 0f ? -1f : pendingAdvanceAt - Time.time; public float LastSealLifetimeSecondsForTests => seals.Count == 0 ? 0f : seals[^1].seal.expiresAt - seals[^1].seal.createdAt; + public CustomSpellEffectKind LastSealCustomEffectKindForTests => seals.Count == 0 ? CustomSpellEffectKind.None : seals[^1].seal.customEffectKind; + public string LastSealLabelForTests => seals.Count == 0 ? "" : seals[^1].seal.Label; + public string LastSealVisualLabelForTests => seals.Count == 0 ? "" : seals[^1].VisualLabelForTests; + public Color LastSealVisualColorForTests => seals.Count == 0 ? Color.clear : seals[^1].VisualColorForTests; + public Color LastSealAttachGuideColorForTests => seals.Count == 0 ? Color.clear : seals[^1].AttachGuideColorForTests; + public bool LastSealDefaultFallbackPendingForTests => seals.Count > 0 && seals[^1].IsDefaultFallbackPending(Time.time); + public SpellFamily? LastSealFamilyForTests => seals.Count == 0 ? null : seals[^1].seal.baseFamily; + public string CurrentInputPhaseLabelForTests => BuildInputPhaseLabel(); public IReadOnlyList LastOverlayStack => seals.Count == 0 ? Array.Empty() : seals[^1].seal.overlayStack; public int PersonalizationCaptureCountForTests => recognitionService?.PersonalizationStore.CaptureCount ?? 0; public int CustomShapeSlotCountForTests => customShapeBook?.SlotCount ?? 0; @@ -301,10 +428,39 @@ public sealed class ExamGameController : MonoBehaviour public string GoalProximityBubbleTextForTests => goalProximityBubbleText == null ? "" : goalProximityBubbleText.text; public string LastGoalProximityGuideGoalIdForTests => lastGoalProximityGuideGoalId; public float LastGoalProximityGuideDistanceForTests => lastGoalProximityGuideDistance; + public bool IsCameraZoomControlVisibleForTests => cameraZoomPanel != null && cameraZoomPanel.gameObject.activeInHierarchy; + public int CameraZoomPercentForTests => Mathf.RoundToInt(cameraZoom * 100f); + public float CameraOrthographicSizeForTests => mainCamera == null ? 0f : mainCamera.orthographicSize; + public string CameraZoomLabelForTests => cameraZoomText == null ? "" : cameraZoomText.text; public int CustomReferenceCountForTests => CurrentCustomShapeReferences().Count; public string CustomReferenceStatusForTests => customReferenceLastStatus; + public int MentorGrantedReferenceCountForTests => mentorGrantedReferenceKeysThisFloor.Count; public bool IsPlatformMotionActiveForTests => platformMotionActive; public Vector2 CurrentStageSafePositionForTests => safePosition; + public bool IsStageLedgeStopPrimedForTests => !string.IsNullOrWhiteSpace(stageLedgeStopGoalId); + public string StageLedgeStopGoalIdForTests => stageLedgeStopGoalId; + public float StageGoalRadiusForTests(string goalId) + { + return FindActiveGoal(goalId)?.radius ?? 0f; + } + + public bool HasStageEntityNearForTests(Vector2 position, float radius) + { + return stageEntityObjects.Any(body => + body != null && + body.activeInHierarchy && + Vector2.Distance(body.transform.position, position) <= radius); + } + + public bool HasStageEntityColliderNearForTests(Vector2 position, float radius) + { + return stageEntityObjects.Any(body => + body != null && + body.activeInHierarchy && + body.GetComponent() != null && + Vector2.Distance(body.transform.position, position) <= radius); + } + public Vector2 CustomReferenceShelfPositionForTests => CurrentCustomReferencePosition(); public Vector2 CustomReferenceInteractionPositionForTests => CurrentCustomReferenceInteractionPosition(); public bool IsQuestScrollVisibleForTests => questScrollPanel != null && questScrollPanel.gameObject.activeInHierarchy; @@ -336,9 +492,10 @@ public static Vector2 BuildMovementInputForTests( { return BuildMovementInput(horizontalAxis, verticalAxis, leftHeld, rightHeld, downHeld, upHeld); } - private bool IsFinalFloor => floorController.CurrentFloorIndex >= floorController.FloorCount - 1; + private bool IsFinalFloor => floorController != null && floorController.CurrentFloorIndex >= floorController.FloorCount - 1; private bool CustomShapesAvailableOnCurrentFloor => CurrentFloorNumber >= CustomReferenceFloorNumber; private bool IsFirstFloorTutorial => floorController?.Current.number == 1; + private bool IsSecondFloorSequence => floorController?.Current.number == CustomReferenceFloorNumber; private WorldStateGoal FindActiveGoal(string goalId) { @@ -375,26 +532,100 @@ private WorldStateGoal CurrentFirstFloorTutorialGoal() return FirstFloorTutorialGoalsInOrder().FirstOrDefault(goal => !goal.completed && goal.requiredBase.HasValue); } + private List SecondFloorSequenceGoalsInOrder() + { + var ordered = new List(SecondFloorSequenceGoalOrder.Length); + foreach (var goalId in SecondFloorSequenceGoalOrder) + { + var goal = FindActiveGoal(goalId); + if (goal != null) + { + ordered.Add(goal); + } + } + + foreach (var goal in activeGoals.Where(goal => goal.requiresCustomShape && !ordered.Contains(goal))) + { + ordered.Add(goal); + } + + return ordered; + } + + private WorldStateGoal CurrentSecondFloorSequenceGoal() + { + if (!IsSecondFloorSequence) + { + return null; + } + + return SecondFloorSequenceGoalsInOrder().FirstOrDefault(goal => !goal.completed && goal.requiredBase.HasValue); + } + + private WorldStateGoal CurrentSequentialGoalGuideGoal() + { + if (IsFirstFloorTutorial) + { + return CurrentFirstFloorTutorialGoal(); + } + + if (IsSecondFloorSequence) + { + return CurrentSecondFloorSequenceGoal(); + } + + return null; + } + private IReadOnlyList BaseResolutionGoalsForCurrentFloor() { - if (!IsFirstFloorTutorial) + if (IsFinalFloor && currentFinalTask != null) { - return activeGoals; + var currentGoal = CurrentFinalActiveGoal(); + return currentGoal == null ? Array.Empty() : new[] { currentGoal }; } var current = CurrentFirstFloorTutorialGoal(); - return current == null ? activeGoals : new[] { current }; + if (current != null) + { + return new[] { current }; + } + + var secondFloorCurrent = CurrentSecondFloorSequenceGoal(); + return secondFloorCurrent == null ? activeGoals : new[] { secondFloorCurrent }; } private IEnumerable BaseIntentGoalsForCurrentFloor() { - if (!IsFirstFloorTutorial) + if (IsFinalFloor && currentFinalTask != null) { - return activeGoals; + var currentGoal = CurrentFinalActiveGoal(); + return currentGoal == null ? Enumerable.Empty() : new[] { currentGoal }; } var current = CurrentFirstFloorTutorialGoal(); - return current == null ? Enumerable.Empty() : new[] { current }; + if (current != null) + { + return new[] { current }; + } + + var secondFloorCurrent = CurrentSecondFloorSequenceGoal(); + return secondFloorCurrent == null ? activeGoals : new[] { secondFloorCurrent }; + } + + /// + /// Bundled Galmuri (SIL OFL 1.1) keeps Korean text crisp and identical on + /// every machine; the OS font chain is only a fallback for stripped builds. + /// + private static Font LoadGameFont() + { + var bundled = Resources.Load("Fonts/Galmuri11"); + if (bundled != null) + { + return bundled; + } + + return Font.CreateDynamicFontFromOSFont(new[] { "Malgun Gothic", "Arial" }, 18); } private void Awake() @@ -402,13 +633,13 @@ private void Awake() sessionId = $"unity-{DateTime.UtcNow:yyyyMMddHHmmss}-{Guid.NewGuid().ToString("N")[..6]}"; logger = new ExamLogger(sessionId); sessionStartedAt = Time.time; - uiFont = Font.CreateDynamicFontFromOSFont(new[] { "Malgun Gothic", "Arial" }, 18); + uiFont = LoadGameFont(); floorController = new FloorController(); magicNote = new MagicNote(); endingReport = new EndingReport(); spellCasting = new SpellCastingService(); customShapeStore = CustomShapeProfileStore.LoadDefault(); - recognitionService = new HeuristicStrokeRecognitionService(null, customShapeStore); + recognitionService = new HeuristicStrokeRecognitionService(globalPersonalizationStore, customShapeStore); floorGoals = new FloorGoalSystem(); ResolveSceneReferences(); RefreshPlayerBlinkRenderers(); @@ -418,6 +649,9 @@ private void Awake() customShapeBook = new CustomShapeBookController(); customShapeBook.Initialize(canvas, mainCamera, player, uiFont, customShapeStore); ConfigureWorldDrawing(); + LogActionEvent("session_started", "system", "boot", payloadJson: BuildEventPayload( + Pair("buildVersion", BuildVersion), + Pair("sessionId", sessionId))); LoadFloor(0); ConfigureBoot(); } @@ -437,7 +671,9 @@ private void Update() TickCustomReferenceShelf(); TickGoalProximityBubble(); - if (IsFirstFloorLetterVisibleForTests || customShapeBook?.BlocksGameplayInput == true || IsCustomReferencePanelOpenForTests) + if (floorTransitionState != FloorTransitionState.None || + customShapeBook?.BlocksGameplayInput == true || + IsCustomReferencePanelOpenForTests) { ClearMovementInputFallback(); velocity = Vector2.zero; @@ -457,9 +693,13 @@ private void Update() TickSeals(); TickPulses(); TickShelfGuideArrows(); + TickSequentialGoalGuides(); TickWorldEffectObjects(); TickSpriteAccentAnimations(); RefreshFirstFloorTutorialGoalVisibility(); + RefreshSecondFloorSequenceGoalVisibility(); + RefreshSequentialGoalGuide(); + RefreshFinalExamGoalVisibility(); TickDefaultBarriers(); TickDamagePopups(); TickBuffQueues(); @@ -474,6 +714,9 @@ private void Update() magicNote.Tick(Time.deltaTime); mentor?.Tick(Time.time); TickToast(); + TickFinalTaskBanner(); + TickCameraZoomInput(); + RefreshWorldDrawingInputColor(); UpdateHud(); } @@ -497,6 +740,217 @@ private void TickGameplayCancelInput() } } + private void TickCameraZoomInput() + { + if (mainCamera == null) + { + return; + } + + if (Input.GetKeyDown(KeyCode.Equals) || Input.GetKeyDown(KeyCode.KeypadPlus)) + { + AdjustCameraZoom(CameraZoomStep); + } + + if (Input.GetKeyDown(KeyCode.Minus) || Input.GetKeyDown(KeyCode.KeypadMinus)) + { + AdjustCameraZoom(-CameraZoomStep); + } + + if (Input.GetKeyDown(KeyCode.Alpha0) || Input.GetKeyDown(KeyCode.Keypad0)) + { + SetCameraZoom(1f); + } + + var wheel = Input.mouseScrollDelta.y; + if (Mathf.Abs(wheel) <= 0.01f || + EventSystem.current != null && EventSystem.current.IsPointerOverGameObject()) + { + return; + } + + AdjustCameraZoom(wheel * CameraZoomWheelStep); + } + + private void AdjustCameraZoom(float delta) + { + SetCameraZoom(cameraZoom + delta); + } + + private void SetCameraZoom(float zoom) + { + var clamped = Mathf.Clamp(zoom, CameraZoomMin, CameraZoomMax); + if (Mathf.Approximately(cameraZoom, clamped)) + { + return; + } + + cameraZoom = clamped; + ApplyCameraZoom(); + LogActionEvent("camera_zoom_changed", "player", "ui", value: LogFloat(cameraZoom)); + } + + private void ApplyCameraZoom() + { + if (mainCamera != null) + { + var pixelPerfect = mainCamera.GetComponent(); + if (pixelPerfect != null) + { + pixelPerfect.assetsPPU = Mathf.Max(1, Mathf.RoundToInt(PixelRenderSetup.AssetsPixelsPerUnit * cameraZoom)); + var pixelPerfectSize = PixelRenderSetup.ReferenceResolutionY / (2f * pixelPerfect.assetsPPU); + pixelPerfect.CorrectCinemachineOrthoSize(pixelPerfectSize); + mainCamera.orthographicSize = pixelPerfectSize; + } + else + { + mainCamera.orthographicSize = GameplayCameraOrthographicSize / cameraZoom; + } + } + + RefreshCameraZoomUi(); + } + + private void RefreshCameraZoomUi() + { + if (cameraZoomText != null) + { + cameraZoomText.text = $"{Mathf.RoundToInt(cameraZoom * 100f)}%"; + } + + if (cameraZoomOutButton != null) + { + cameraZoomOutButton.interactable = cameraZoom > CameraZoomMin + 0.001f; + } + + if (cameraZoomInButton != null) + { + cameraZoomInButton.interactable = cameraZoom < CameraZoomMax - 0.001f; + } + + if (cameraZoomResetButton != null) + { + cameraZoomResetButton.interactable = !Mathf.Approximately(cameraZoom, 1f); + } + } + + private void LogActionEvent( + string eventType, + string actor = "system", + string phase = "", + string targetId = "", + string targetLabel = "", + Vector2? position = null, + string value = "", + string payloadJson = "", + string inputSessionId = "", + string strokeId = "", + int strokePointCount = 0) + { + if (logger == null) + { + return; + } + + var now = Time.time; + var currentFloor = floorController?.Current; + var eventPosition = position ?? (player == null ? Vector2.zero : (Vector2)player.position); + var utc = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture); + lastEnrollmentEventType = string.IsNullOrWhiteSpace(eventType) ? lastEnrollmentEventType : eventType; + lastEnrollmentEventUtc = utc; + logger.LogActionEvent(new ActionEventLog + { + sessionId = sessionId, + eventId = $"{sessionId}-{(++actionEventCounter).ToString("D6", CultureInfo.InvariantCulture)}", + utc = utc, + elapsedMs = sessionStartedAt <= 0f ? 0 : Mathf.Max(0, Mathf.RoundToInt((now - sessionStartedAt) * 1000f)), + floorElapsedMs = floorStartedAt <= 0f ? 0 : Mathf.Max(0, Mathf.RoundToInt((now - floorStartedAt) * 1000f)), + floorId = currentFloor == null ? "" : currentFloor.number.ToString(CultureInfo.InvariantCulture), + floorTitle = currentFloor?.title ?? "", + eventType = eventType ?? "", + actor = actor ?? "", + phase = phase ?? "", + targetId = targetId ?? "", + targetLabel = targetLabel ?? "", + positionX = eventPosition.x, + positionY = eventPosition.y, + inputSessionId = inputSessionId ?? "", + strokeId = strokeId ?? "", + strokePointCount = Mathf.Max(0, strokePointCount), + value = value ?? "", + payloadJson = TruncateForLog(payloadJson ?? "", 1000) + }); + } + + private void OnRawStrokeEvent(string eventType, StrokeInputStroke stroke) + { + if (stroke == null) + { + LogActionEvent(eventType, "player", "input"); + return; + } + + var points = stroke.Points; + var lastPoint = points.Count == 0 ? Vector2.zero : points[^1].Position; + var firstTime = points.Count == 0 ? 0f : points[0].TimeSeconds; + var lastTime = points.Count == 0 ? 0f : points[^1].TimeSeconds; + LogActionEvent( + eventType, + "player", + "input", + position: lastPoint, + payloadJson: BuildEventPayload( + Pair("firstPointTime", LogFloat(firstTime)), + Pair("lastPointTime", LogFloat(lastTime)), + Pair("durationMs", Mathf.Max(0, Mathf.RoundToInt((lastTime - firstTime) * 1000f)).ToString(CultureInfo.InvariantCulture))), + strokeId: stroke.Id, + strokePointCount: points.Count); + } + + private void OnRawStrokeStateEvent(string eventType) + { + LogActionEvent(eventType, "player", "input"); + } + + private static KeyValuePair Pair(string key, string value) + { + return new KeyValuePair(key ?? "", value ?? ""); + } + + private static string BuildEventPayload(params KeyValuePair[] values) + { + if (values == null || values.Length == 0) + { + return ""; + } + + return "{" + string.Join(",", values.Select(item => $"\"{JsonEscape(item.Key)}\":\"{JsonEscape(item.Value)}\"")) + "}"; + } + + private static string JsonEscape(string value) + { + return (value ?? "") + .Replace("\\", "\\\\", StringComparison.Ordinal) + .Replace("\"", "\\\"", StringComparison.Ordinal) + .Replace("\r", "\\r", StringComparison.Ordinal) + .Replace("\n", "\\n", StringComparison.Ordinal); + } + + private static string LogFloat(float value) + { + return value.ToString("0.###", CultureInfo.InvariantCulture); + } + + private static string TruncateForLog(string value, int maxLength) + { + if (string.IsNullOrEmpty(value) || value.Length <= maxLength) + { + return value ?? ""; + } + + return value[..Mathf.Max(0, maxLength)]; + } + public BaseRecognitionResult CastSyntheticBaseForTests(SpellFamily family, Vector2 worldCenter, bool movePlayerToReference = true) { if (movePlayerToReference) @@ -563,6 +1017,8 @@ public void CompleteCurrentGoalsForTests(int count) { var goals = IsFirstFloorTutorial ? FirstFloorTutorialGoalsInOrder() + : IsSecondFloorSequence + ? SecondFloorSequenceGoalsInOrder() : activeGoals; foreach (var goal in goals.Where(goal => !goal.completed).Take(count)) { @@ -588,11 +1044,75 @@ public void CloseFirstFloorLetterForTests() CloseFirstFloorLetter(); } + public bool AdvanceMentorSpeechForTests() + { + return mentor != null && mentor.AdvanceSpeechPage(); + } + + public void ClickEndingReportExitForTests() + { + FinishEndingReport(); + } + public void LoadFloorForTests(int index) { LoadFloor(index); } + public void ZoomCameraInForTests() + { + AdjustCameraZoom(CameraZoomStep); + } + + public void ZoomCameraOutForTests() + { + AdjustCameraZoom(-CameraZoomStep); + } + + public void ResetCameraZoomForTests() + { + SetCameraZoom(1f); + } + + public bool SelectFinalTaskForTests(string taskId) + { + if (string.IsNullOrWhiteSpace(taskId)) + { + forcedFinalTaskIdForTests = ""; + return false; + } + + var exists = FinalExamTaskPool().Any(task => + string.Equals(task.id, taskId.Trim(), StringComparison.OrdinalIgnoreCase)); + forcedFinalTaskIdForTests = exists ? taskId.Trim() : ""; + if (exists && IsFinalFloor) + { + LoadFloor(floorController.CurrentFloorIndex, saveCurrentQuestScore: false); + } + + return exists; + } + + public void MarkFinalTasksEncounteredForTests(params string[] taskIds) + { + var validIds = new HashSet( + FinalExamTaskPool().Select(task => task.id), + StringComparer.OrdinalIgnoreCase); + foreach (var taskId in taskIds ?? Array.Empty()) + { + var normalized = taskId?.Trim(); + if (!string.IsNullOrWhiteSpace(normalized) && validIds.Contains(normalized)) + { + encounteredFinalTaskIds.Add(normalized); + } + } + } + + public void ClearFinalTaskEncountersForTests() + { + encounteredFinalTaskIds.Clear(); + } + public void MovePlayerForTests(Vector2 worldPosition) { player.position = worldPosition; @@ -603,6 +1123,7 @@ public void MovePlayerForTests(Vector2 worldPosition) } platformHorizontalVelocity = 0f; + ResetStageLedgeStop(); } public void SetMovementInputFallbackForTests( @@ -643,6 +1164,13 @@ public float GoalVisualAlphaForTests(string goalId) return FindActiveGoal(goalId)?.renderer?.color.a ?? 0f; } + public bool IsFinalTaskEnvironmentVisibleForTests(string taskId) + { + return !string.IsNullOrWhiteSpace(taskId) && + finalTaskEnvironmentObjects.TryGetValue(taskId.Trim(), out var objects) && + objects.Any(body => body != null && body.activeInHierarchy); + } + public Vector3 SpriteAccentScaleForTests(string objectName) { return spriteAccentAnimations.FirstOrDefault(animation => animation.Name == objectName)?.CurrentScale ?? Vector3.zero; @@ -841,12 +1369,15 @@ public void OpenCustomReferencePanelForTests() public bool ImportCustomReferenceForTests(SpellFamily family, out int slotIndex, out string message) { - var references = CurrentCustomShapeReferences(); + var references = ImportLookupCustomShapeReferences(); var reference = references.FirstOrDefault(item => item.family == family); if (reference == null) { slotIndex = -1; - message = "reference not found"; + message = CurrentSecondFloorSequenceGoal() is { } currentSecondFloorGoal + ? $"지금은 {currentSecondFloorGoal.title} 도형부터 가져오세요." + : "reference not found"; + SetCustomReferenceStatus(message); return false; } @@ -855,7 +1386,7 @@ public bool ImportCustomReferenceForTests(SpellFamily family, out int slotIndex, public List> CustomReferenceStrokesForTests(SpellFamily family, Vector2 worldCenter) { - var references = CurrentCustomShapeReferences(); + var references = TestCustomShapeReferencesForCurrentFloor(); var reference = references.FirstOrDefault(item => item.family == family) ?? references.FirstOrDefault(); if (reference == null) { @@ -868,8 +1399,8 @@ public List> CustomReferenceStrokesForTests(SpellFamily famil public void UseCustomShapeStoreForTests(string storagePath) { customShapeStore = new CustomShapeProfileStore(storagePath); - var personalizationStore = recognitionService?.PersonalizationStore ?? new TutorialPersonalizationStore(); - recognitionService = new HeuristicStrokeRecognitionService(personalizationStore, customShapeStore); + globalPersonalizationStore = recognitionService?.PersonalizationStore ?? globalPersonalizationStore ?? new TutorialPersonalizationStore(); + recognitionService = new HeuristicStrokeRecognitionService(globalPersonalizationStore, customShapeStore); customShapeBook = new CustomShapeBookController(); customShapeBook.Initialize(canvas, mainCamera, player, uiFont, customShapeStore); if (!CustomShapesAvailableOnCurrentFloor) @@ -918,10 +1449,27 @@ public void StartPracticeMode() } public void LoadSavedProgress(int floorNumber, IEnumerable noteLines) + { + LoadSavedProgress(new GameProgressSnapshot + { + floorNumber = floorNumber, + noteLines = noteLines?.ToArray() ?? Array.Empty() + }); + } + + public void LoadSavedProgress(GameProgressSnapshot snapshot) { ResetRunState(); + var floorNumber = snapshot == null ? 1 : snapshot.floorNumber; + RestoreFinalTaskEncounters(snapshot?.encounteredFinalTaskIds); LoadFloor(Mathf.Clamp(floorNumber - 1, 0, FloorCount - 1)); - magicNote.Restore(noteLines ?? Array.Empty(), CurrentFloorNumber); + if (snapshot != null) + { + RestoreCompletedGoals(snapshot.completedGoalIds); + RestoreDiscoveries(snapshot); + } + + magicNote.Restore(snapshot?.noteLines ?? Array.Empty(), CurrentFloorNumber); if (noteText != null) { noteText.text = magicNote.Text; @@ -931,15 +1479,32 @@ public void LoadSavedProgress(int floorNumber, IEnumerable noteLines) public void PrepareForTitleScreen() { pendingAdvanceAt = -1f; + floorTransitionState = FloorTransitionState.None; + floorTransitionStartedAt = -1f; + floorAdvanceSpeechDeadlineAt = -1f; + floorTransitionNextPulseAt = -1f; + finalCertificateAwaitingExit = false; + finalTaskBannerSuppressedForTitle = true; if (reportPanel != null) { reportPanel.gameObject.SetActive(false); } + if (endingReportExitButton != null) + { + endingReportExitButton.gameObject.SetActive(false); + endingReportExitButton.interactable = true; + } if (notePanel != null) { notePanel.gameObject.SetActive(false); } HideFirstFloorLetter(); + if (finalTaskBanner != null) + { + finalTaskBanner.gameObject.SetActive(false); + } + + SetPlayerBlinkAlpha(1f); SetGameplayInputEnabled(false); } @@ -951,54 +1516,190 @@ public GameProgressSnapshot CreateProgressSnapshot(int resumeFloorNumber = 0) floorNumber = floorNumber, completedGoals = activeGoals.Count(goal => goal.completed), totalGoals = activeGoals.Count, + completedGoalIds = activeGoals + .Where(goal => goal.completed && !string.IsNullOrWhiteSpace(goal.id)) + .Select(goal => goal.id) + .ToArray(), noteLines = magicNote?.Lines.ToArray() ?? Array.Empty(), savedAtUtc = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture), discoveries = endingReport?.DiscoveryCount ?? 0, + discoveredFamilies = discoveredFamilies.Select(family => family.ToString()).ToArray(), + discoveredOverlays = discoveredOverlays.Select(op => op.ToString()).ToArray(), + discoveredReactions = discoveredReactions.Select(reaction => reaction.ToString()).ToArray(), + encounteredFinalTaskIds = encounteredFinalTaskIds + .OrderBy(id => id, StringComparer.OrdinalIgnoreCase) + .ToArray(), endingLabel = finalTrueEnding ? "진엔딩" : finalCompletionCelebrated ? "통과 엔딩" : "" }; } - private void ResetRunState() + private void RestoreCompletedGoals(IEnumerable completedGoalIds) { - sessionId = $"unity-{DateTime.UtcNow:yyyyMMddHHmmss}-{Guid.NewGuid().ToString("N")[..6]}"; - logger = new ExamLogger(sessionId); - endingReport = new EndingReport(); - sessionStartedAt = Time.time; - trialCounter = 0; - castsOnCurrentFloor = 0; - CurrentAssistLevel = 0; - LastHintText = ""; - pendingAdvanceAt = -1f; - finalCompletionCelebrated = false; - finalTrueEnding = false; - baseFailureCounts.Clear(); - discoveredFamilies.Clear(); - discoveredOverlays.Clear(); - discoveredReactions.Clear(); - practiceMode = false; - firstFloorLetterShownThisSession = false; - PlayerPrefs.SetInt("MagicExamHall.FirstFloorGhostSeen", 0); - magicNote.Clear(); - if (reportPanel != null) + var completedIds = new HashSet( + (completedGoalIds ?? Array.Empty()) + .Where(id => !string.IsNullOrWhiteSpace(id)) + .Select(id => id.Trim()), + StringComparer.OrdinalIgnoreCase); + if (completedIds.Count == 0) { - reportPanel.gameObject.SetActive(false); + return; } - if (toastPanel != null) + + foreach (var goal in activeGoals.Where(goal => completedIds.Contains(goal.id))) { - toastTtl = 0f; - toastPanel.gameObject.SetActive(false); + RestoreGoalCompletion(goal); } + + RefreshFirstFloorTutorialGoalVisibility(); + RefreshSecondFloorSequenceGoalVisibility(); + TickQuestChecklist(forceRefresh: true); + UpdateHud(); + UpdateResultPanelLayout(); } - private void PublishProgressCheckpoint(int resumeFloorNumber) + private void RestoreGoalCompletion(WorldStateGoal goal) { - if (practiceMode) + if (goal.completed) { return; } - ProgressCheckpointed(CreateProgressSnapshot(resumeFloorNumber)); - } + goal.completed = true; + if (ReferenceEquals(goalProximityBubbleGoal, goal)) + { + HideGoalProximityBubble(); + } + + if (goal.renderer != null) + { + goal.renderer.sprite = PixelArtFactory.CreateSprite($"{goal.title} Active", Color.white, goal.color, goal.kind); + goal.renderer.sharedMaterial = PixelMaterialProvider.SpriteMaterial; + } + + if (goal.body != null) + { + goal.body.transform.localScale *= 1.15f; + RegisterSpriteAccent(goal.body, SpriteAccentAnimationKind.RuneActive, 0.21f); + } + + if (goal.label != null) + { + goal.label.text = $"완료: {goal.title}"; + goal.label.color = Color.Lerp(goal.color, Color.white, 0.6f); + goal.label.fontStyle = FontStyle.Bold; + } + + ApplyGoalReaction(goal); + } + + private void RestoreDiscoveries(GameProgressSnapshot snapshot) + { + RestoreEnumSet(snapshot.discoveredFamilies, discoveredFamilies); + RestoreEnumSet(snapshot.discoveredOverlays, discoveredOverlays); + RestoreEnumSet(snapshot.discoveredReactions, discoveredReactions); + } + + private void RestoreFinalTaskEncounters(IEnumerable taskIds) + { + encounteredFinalTaskIds.Clear(); + var validIds = new HashSet( + FinalExamTaskPool().Select(task => task.id), + StringComparer.OrdinalIgnoreCase); + foreach (var taskId in taskIds ?? Array.Empty()) + { + var normalized = taskId?.Trim(); + if (!string.IsNullOrWhiteSpace(normalized) && validIds.Contains(normalized)) + { + encounteredFinalTaskIds.Add(normalized); + } + } + } + + private static void RestoreEnumSet(IEnumerable values, ISet target) + where TEnum : struct + { + if (values == null || target == null) + { + return; + } + + foreach (var value in values) + { + if (string.IsNullOrWhiteSpace(value)) + { + continue; + } + + if (Enum.TryParse(value.Trim(), ignoreCase: true, out TEnum parsed)) + { + target.Add(parsed); + } + } + } + + private void ResetRunState() + { + sessionId = $"unity-{DateTime.UtcNow:yyyyMMddHHmmss}-{Guid.NewGuid().ToString("N")[..6]}"; + logger = new ExamLogger(sessionId); + endingReport = new EndingReport(); + sessionStartedAt = Time.time; + actionEventCounter = 0; + trialCounter = 0; + castsOnCurrentFloor = 0; + CurrentAssistLevel = 0; + LastHintText = ""; + pendingAdvanceAt = -1f; + floorTransitionState = FloorTransitionState.None; + floorTransitionStartedAt = -1f; + floorAdvanceSpeechDeadlineAt = -1f; + floorTransitionNextPulseAt = -1f; + lastEnrollmentEventType = "session_started"; + lastEnrollmentEventUtc = ""; + finalCompletionCelebrated = false; + finalTrueEnding = false; + finalCertificateQueued = false; + finalCertificateAwaitingExit = false; + finalTaskBannerSuppressedForTitle = false; + encounteredFinalTaskIds.Clear(); + baseFailureCounts.Clear(); + ResetRepeatedGoalRecognition(); + discoveredFamilies.Clear(); + discoveredOverlays.Clear(); + discoveredReactions.Clear(); + globalPersonalizationStore = new TutorialPersonalizationStore(); + recognitionService = new HeuristicStrokeRecognitionService(globalPersonalizationStore, customShapeStore); + practiceMode = false; + firstFloorLetterShownThisSession = false; + PlayerPrefs.SetInt("MagicExamHall.FirstFloorGhostSeen", 0); + magicNote.Clear(); + if (reportPanel != null) + { + reportPanel.gameObject.SetActive(false); + } + if (endingReportExitButton != null) + { + endingReportExitButton.gameObject.SetActive(false); + endingReportExitButton.interactable = true; + } + if (toastPanel != null) + { + toastTtl = 0f; + toastPanel.gameObject.SetActive(false); + } + LogActionEvent("session_restarted", "system", "boot", payloadJson: BuildEventPayload( + Pair("buildVersion", BuildVersion), + Pair("sessionId", sessionId))); + } + + private void PublishProgressCheckpoint(int resumeFloorNumber) + { + if (practiceMode) + { + return; + } + + ProgressCheckpointed(CreateProgressSnapshot(resumeFloorNumber)); + } private void ResolveSceneReferences() { @@ -1037,6 +1738,10 @@ private void ResolveSceneReferences() } canvas.renderMode = RenderMode.ScreenSpaceOverlay; + // HUD must outrank every world-space canvas (goal labels 42, buffs 76) + // even when the canvas is temporarily switched to ScreenSpaceCamera + // (pause/screenshot capture paths). + canvas.sortingOrder = 100; var scaler = canvas.GetComponent() ?? canvas.gameObject.AddComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1280, 720); @@ -1069,6 +1774,8 @@ private void ConfigureWorldDrawing() worldDrawing = gameObject.GetComponent() ?? gameObject.AddComponent(); worldDrawing.mainCamera = mainCamera; worldDrawing.ApplyPlayableDefaults(); + worldDrawing.RawStrokeEvent += OnRawStrokeEvent; + worldDrawing.RawStrokeStateEvent += OnRawStrokeStateEvent; worldDrawing.StrokeSessionCompleted += OnStrokeSessionCompleted; worldDrawing.InputCancelled += OnDrawingCancelled; } @@ -1103,34 +1810,77 @@ private static void ConfigureMainCamera(Camera camera) private void BuildUi() { ClearChildren(canvas.transform); - hudPanel = CreatePanel("HUD", canvas.transform, new Vector2(20, -20), new Vector2(560, 132), Anchor.TopLeft, new Color(0.04f, 0.055f, 0.075f, 0.88f)); - hudTitle = CreateText("HUD Title", hudPanel, "Magic Exam Hall", 24, FontStyle.Bold, new Vector2(16, -12), new Vector2(520, 28), Anchor.TopLeft); - hudCopy = CreateText("HUD Copy", hudPanel, "", 15, FontStyle.Normal, new Vector2(16, -46), new Vector2(520, 60), Anchor.TopLeft); - floorProgress = CreateText("Floor Progress", hudPanel, "", 15, FontStyle.Bold, new Vector2(16, 12), new Vector2(520, 24), Anchor.BottomLeft); + hudPanel = CreatePanel("HUD", canvas.transform, new Vector2(20, -20), new Vector2(480, 110), Anchor.TopLeft, Color.white); + MagicExamUiFactory.ApplySprite(hudPanel.GetComponent(), MagicExamUiSpriteId.DarkPanel, sliced: true); + AddPanelBorder(hudPanel, MagicExamUiTheme.BorderGold, 1.8f); + MagicExamUiFactory.AddAccentRail(hudPanel, MagicExamUiTheme.RuneBlueDim, 5f); + hudTitle = CreateText("HUD Title", hudPanel, "Magic Exam Hall", 20, FontStyle.Bold, new Vector2(18, -10), new Vector2(444, 26), Anchor.TopLeft); + MagicExamUiFactory.StyleDarkText(hudTitle, emphasized: true); + hudCopy = CreateText("HUD Copy", hudPanel, "", 14, FontStyle.Normal, new Vector2(18, -40), new Vector2(444, 44), Anchor.TopLeft); + hudCopy.verticalOverflow = VerticalWrapMode.Truncate; + MagicExamUiFactory.StyleDarkText(hudCopy); + floorProgress = CreateText("Floor Progress", hudPanel, "", 14, FontStyle.Bold, new Vector2(18, 8), new Vector2(444, 22), Anchor.BottomLeft); + MagicExamUiFactory.StyleDarkText(floorProgress); hudPanel.gameObject.SetActive(false); BuildHealthUi(); - notePanel = CreatePanel("Magic Note", canvas.transform, new Vector2(20, 20), new Vector2(560, 112), Anchor.BottomLeft, new Color(0.04f, 0.055f, 0.075f, 0.84f)); - noteText = CreateText("Note Text", notePanel, "", 14, FontStyle.Normal, new Vector2(14, -12), new Vector2(530, 88), Anchor.TopLeft); - - resultPanel = CreatePanel("Spell Result", canvas.transform, new Vector2(-20, -20), new Vector2(430, 178), Anchor.TopRight, new Color(0.04f, 0.055f, 0.075f, 0.88f)); - resultText = CreateText("Result Text", resultPanel, "", 13, FontStyle.Normal, new Vector2(14, -12), new Vector2(402, 152), Anchor.TopLeft); + notePanel = CreatePanel("Magic Note", canvas.transform, new Vector2(20, 20), new Vector2(520, 88), Anchor.BottomLeft, Color.white); + MagicExamUiFactory.ApplySprite(notePanel.GetComponent(), MagicExamUiSpriteId.DarkPanel, sliced: true); + AddPanelBorder(notePanel, MagicExamUiTheme.RuneBlueDim, 1.6f); + MagicExamUiFactory.AddAccentRail(notePanel, MagicExamUiTheme.GoldSoft, 5f); + noteText = CreateText("Note Text", notePanel, "", 14, FontStyle.Normal, new Vector2(16, -10), new Vector2(488, 66), Anchor.TopLeft); + noteText.verticalOverflow = VerticalWrapMode.Truncate; + MagicExamUiFactory.StyleDarkText(noteText); + + resultPanel = CreatePanel("Spell Result", canvas.transform, new Vector2(-18, -18), new Vector2(374, 164), Anchor.TopRight, Color.white); + MagicExamUiFactory.ApplySprite(resultPanel.GetComponent(), MagicExamUiSpriteId.DarkPanel, sliced: true); + AddPanelBorder(resultPanel, MagicExamUiTheme.BorderGold, 1.8f); + resultText = CreateText("Result Text", resultPanel, "", 13, FontStyle.Normal, new Vector2(14, -12), new Vector2(346, 140), Anchor.TopLeft); + resultText.verticalOverflow = VerticalWrapMode.Truncate; + MagicExamUiFactory.StyleDarkText(resultText); UpdateResultPanelLayout(); resultPanel.gameObject.SetActive(false); BuildQuestScrollUi(); UpdateResultPanelLayout(); - reportPanel = CreatePanel("Ending Report", canvas.transform, Vector2.zero, new Vector2(760, 520), Anchor.Center, new Color(0.035f, 0.045f, 0.065f, 0.96f)); - reportText = CreateText("Report Text", reportPanel, "", 17, FontStyle.Normal, new Vector2(28, -28), new Vector2(704, 464), Anchor.TopLeft); + reportPanel = CreatePanel("Ending Report", canvas.transform, Vector2.zero, new Vector2(940, 620), Anchor.Center, Color.white); + MagicExamUiFactory.ApplySprite(reportPanel.GetComponent(), MagicExamUiSpriteId.BookPanel, sliced: true); + AddPanelBorder(reportPanel, MagicExamUiTheme.BorderBrown, 3f); + var reportHeading = CreateText("Report Heading", reportPanel, "수료 보고서", 28, FontStyle.Bold, new Vector2(35, -32), new Vector2(870, 42), Anchor.TopLeft); + reportHeading.color = MagicExamUiTheme.ParchmentInk; + MagicExamUiFactory.StyleParchmentText(reportHeading, emphasized: true); + var reportRule = CreateImage("Report Rule", reportPanel, new Vector2(35, -80), new Vector2(870, 2), Anchor.TopLeft, MagicExamUiTheme.BorderBrown); + reportRule.raycastTarget = false; + reportText = CreateText("Report Text", reportPanel, "", 15, FontStyle.Normal, new Vector2(35, -96), new Vector2(878, 442), Anchor.TopLeft); + reportText.color = MagicExamUiTheme.ParchmentInk; + reportText.verticalOverflow = VerticalWrapMode.Truncate; + MagicExamUiFactory.StyleParchmentText(reportText); + endingReportExitButton = CreateButton( + "Ending Report Exit Button", + reportPanel, + "종료", + 16, + FontStyle.Bold, + new Vector2(-35f, 34f), + new Vector2(132f, 42f), + Anchor.BottomRight, + new Color(0.70f, 0.18f, 0.12f, 0.96f), + FinishEndingReport); + endingReportExitButtonText = endingReportExitButton.GetComponentInChildren(); + endingReportExitButton.gameObject.SetActive(false); reportPanel.gameObject.SetActive(false); - toastPanel = CreatePanel("Action Toast", canvas.transform, new Vector2(-20, -20), new Vector2(500, 54), Anchor.TopRight, new Color(0.018f, 0.024f, 0.038f, 0.94f)); + toastPanel = CreatePanel("Action Toast", canvas.transform, new Vector2(-18, -18), new Vector2(420, 50), Anchor.TopRight, Color.white); + MagicExamUiFactory.ApplySprite(toastPanel.GetComponent(), MagicExamUiSpriteId.DarkPanel, sliced: true); toastBackground = toastPanel.GetComponent(); - toastAccent = CreateImage("Toast Accent", toastPanel, new Vector2(0f, 0f), new Vector2(6f, 54f), Anchor.TopLeft, new Color(1f, 0.82f, 0.38f, 1f)); + toastAccent = CreateImage("Toast Accent", toastPanel, new Vector2(0f, 0f), new Vector2(6f, 50f), Anchor.TopLeft, MagicExamUiTheme.Gold); toastAccent.raycastTarget = false; - toastText = CreateText("Toast Text", toastPanel, "", 16, FontStyle.Bold, new Vector2(18, -13), new Vector2(464, 28), Anchor.TopLeft); + toastText = CreateText("Toast Text", toastPanel, "", 15, FontStyle.Bold, new Vector2(18, -11), new Vector2(384, 28), Anchor.TopLeft); toastText.alignment = TextAnchor.MiddleLeft; + toastText.verticalOverflow = VerticalWrapMode.Truncate; + MagicExamUiFactory.StyleDarkText(toastText); toastPanel.gameObject.SetActive(false); + BuildFinalTaskBannerUi(); versionText = CreateText("Build Version", canvas.transform, BuildVersion, 11, FontStyle.Normal, new Vector2(-14, 10), new Vector2(300, 20), Anchor.BottomRight); versionText.alignment = TextAnchor.MiddleRight; @@ -1147,16 +1897,96 @@ private void BuildUi() Anchor.BottomRight, new Color(0.82f, 0.06f, 0.04f, 0.94f), SkipCurrentFloorForDebug); + BuildCameraZoomUi(); + floorSkipButton.transform.SetAsLastSibling(); BuildCustomReferenceUi(); BuildGoalProximityBubbleUi(); BuildFirstFloorLetterUi(); + floorSkipButton.transform.SetAsLastSibling(); + } + + private void BuildCameraZoomUi() + { + cameraZoomPanel = CreatePanel( + "Camera Zoom Panel", + canvas.transform, + new Vector2(-124f, 42f), + new Vector2(216f, 40f), + Anchor.BottomRight, + new Color(0.025f, 0.035f, 0.050f, 0.90f)); + AddPanelBorder(cameraZoomPanel, new Color(1f, 0.82f, 0.34f, 0.56f), 1.4f); + + var title = CreateText( + "Camera Zoom Title", + cameraZoomPanel, + "화면", + 12, + FontStyle.Bold, + new Vector2(8f, -7f), + new Vector2(40f, 26f), + Anchor.TopLeft); + title.alignment = TextAnchor.MiddleLeft; + title.color = new Color(1f, 0.91f, 0.64f, 1f); + title.raycastTarget = false; + + cameraZoomOutButton = CreateButton( + "Camera Zoom Out Button", + cameraZoomPanel, + "-", + 18, + FontStyle.Bold, + new Vector2(52f, -5f), + new Vector2(30f, 30f), + Anchor.TopLeft, + new Color(0.16f, 0.22f, 0.30f, 0.96f), + () => AdjustCameraZoom(-CameraZoomStep)); + + cameraZoomText = CreateText( + "Camera Zoom Percent", + cameraZoomPanel, + "", + 13, + FontStyle.Bold, + new Vector2(86f, -5f), + new Vector2(48f, 30f), + Anchor.TopLeft); + cameraZoomText.alignment = TextAnchor.MiddleCenter; + cameraZoomText.color = new Color(1f, 1f, 1f, 0.92f); + cameraZoomText.raycastTarget = false; + + cameraZoomInButton = CreateButton( + "Camera Zoom In Button", + cameraZoomPanel, + "+", + 18, + FontStyle.Bold, + new Vector2(138f, -5f), + new Vector2(30f, 30f), + Anchor.TopLeft, + new Color(0.20f, 0.30f, 0.24f, 0.96f), + () => AdjustCameraZoom(CameraZoomStep)); + + cameraZoomResetButton = CreateButton( + "Camera Zoom Reset Button", + cameraZoomPanel, + "1x", + 13, + FontStyle.Bold, + new Vector2(172f, -5f), + new Vector2(36f, 30f), + Anchor.TopLeft, + new Color(0.32f, 0.22f, 0.12f, 0.96f), + () => SetCameraZoom(1f)); + + RefreshCameraZoomUi(); } private void BuildHealthUi() { - healthPanel = CreatePanel("Health Bar", canvas.transform, new Vector2(20f, -20f), new Vector2(158f, 48f), Anchor.TopLeft, new Color(0.035f, 0.025f, 0.030f, 0.72f)); - AddPanelBorder(healthPanel, new Color(0.18f, 0.06f, 0.055f, 0.95f), 1.6f); + healthPanel = CreatePanel("Health Bar", canvas.transform, new Vector2(20f, -20f), new Vector2(158f, 48f), Anchor.TopLeft, Color.white); + MagicExamUiFactory.ApplySprite(healthPanel.GetComponent(), MagicExamUiSpriteId.DarkPanel, sliced: true); + AddPanelBorder(healthPanel, MagicExamUiTheme.Wax, 1.6f); healthHearts.Clear(); for (var index = 0; index < 3; index++) { @@ -1166,6 +1996,7 @@ private void BuildHealthUi() ApplyAnchor(rect, Anchor.TopLeft); rect.anchoredPosition = new Vector2(11f + index * 47f, -5f); rect.sizeDelta = new Vector2(38f, 36f); + heartObject.AddComponent(); var heart = heartObject.AddComponent(); heart.color = new Color(0.92f, 0.035f, 0.045f, 1f); heart.raycastTarget = false; @@ -1177,23 +2008,24 @@ private void BuildHealthUi() private void BuildQuestScrollUi() { - questScrollPanel = CreatePanel("Quest Scroll Panel", canvas.transform, new Vector2(-18f, -18f), new Vector2(QuestScrollWidth, QuestScrollExpandedHeight), Anchor.TopRight, new Color(0.88f, 0.69f, 0.42f, 0.995f)); + questScrollPanel = CreatePanel("Quest Scroll Panel", canvas.transform, new Vector2(-18f, -18f), new Vector2(QuestScrollWidth, QuestScrollExpandedHeight), Anchor.TopRight, Color.white); + MagicExamUiFactory.ApplySprite(questScrollPanel.GetComponent(), MagicExamUiSpriteId.ScrollPanel, sliced: true); questScrollPanel.gameObject.AddComponent(); - AddPanelBorder(questScrollPanel, new Color(0.38f, 0.20f, 0.07f, 0.98f), 3.2f); - CreateImage("Quest Scroll Readability Paper", questScrollPanel, new Vector2(16f, -52f), new Vector2(QuestScrollWidth - 32f, QuestScrollExpandedHeight - 82f), Anchor.TopLeft, new Color(0.92f, 0.74f, 0.46f, 0.88f)).raycastTarget = false; - CreateImage("Quest Scroll Top Roll", questScrollPanel, new Vector2(24f, -12f), new Vector2(382f, 30f), Anchor.TopLeft, new Color(0.96f, 0.78f, 0.46f, 0.995f)).raycastTarget = false; - var bottomRoll = CreateImage("Quest Scroll Bottom Roll", questScrollPanel, new Vector2(24f, 14f), new Vector2(382f, 30f), Anchor.BottomLeft, new Color(0.52f, 0.30f, 0.12f, 0.98f)); + AddPanelBorder(questScrollPanel, MagicExamUiTheme.BorderBrown, 3f); + CreateImage("Quest Scroll Readability Paper", questScrollPanel, new Vector2(12f, -48f), new Vector2(QuestScrollWidth - 24f, QuestScrollExpandedHeight - 76f), Anchor.TopLeft, new Color(0.98f, 0.82f, 0.53f, 0.40f)).raycastTarget = false; + CreateImage("Quest Scroll Top Roll", questScrollPanel, new Vector2(22f, -10f), new Vector2(QuestScrollWidth - 44f, 27f), Anchor.TopLeft, MagicExamUiTheme.ParchmentLight).raycastTarget = false; + var bottomRoll = CreateImage("Quest Scroll Bottom Roll", questScrollPanel, new Vector2(22f, 12f), new Vector2(QuestScrollWidth - 44f, 27f), Anchor.BottomLeft, new Color(0.52f, 0.30f, 0.12f, 0.98f)); bottomRoll.raycastTarget = false; questScrollBottomRoll = bottomRoll.rectTransform; - CreateImage("Quest Scroll Left Cap", questScrollPanel, new Vector2(10f, -12f), new Vector2(28f, 30f), Anchor.TopLeft, new Color(0.60f, 0.34f, 0.13f, 0.99f)).raycastTarget = false; - CreateImage("Quest Scroll Right Cap", questScrollPanel, new Vector2(-10f, -12f), new Vector2(28f, 30f), Anchor.TopRight, new Color(0.60f, 0.34f, 0.13f, 0.99f)).raycastTarget = false; + CreateImage("Quest Scroll Left Cap", questScrollPanel, new Vector2(9f, -10f), new Vector2(26f, 27f), Anchor.TopLeft, new Color(0.60f, 0.34f, 0.13f, 0.99f)).raycastTarget = false; + CreateImage("Quest Scroll Right Cap", questScrollPanel, new Vector2(-9f, -10f), new Vector2(26f, 27f), Anchor.TopRight, new Color(0.60f, 0.34f, 0.13f, 0.99f)).raycastTarget = false; - questTitleText = CreateText("Quest Scroll Title", questScrollPanel, "퀘스트", 21, FontStyle.Bold, new Vector2(26f, -42f), new Vector2(258f, 34f), Anchor.TopLeft); + questTitleText = CreateText("Quest Scroll Title", questScrollPanel, "퀘스트", 19, FontStyle.Bold, new Vector2(22f, -38f), new Vector2(218f, 30f), Anchor.TopLeft); ApplyQuestScrollReadableText(questTitleText, new Color(0.15f, 0.070f, 0.025f, 1f), emphasized: true); questTitleText.alignment = TextAnchor.MiddleLeft; questTitleText.raycastTarget = false; - questScoreText = CreateText("Quest Scroll Score", questScrollPanel, "", 14, FontStyle.Bold, new Vector2(-100f, -45f), new Vector2(74f, 28f), Anchor.TopRight); + questScoreText = CreateText("Quest Scroll Score", questScrollPanel, "", 13, FontStyle.Bold, new Vector2(-88f, -40f), new Vector2(64f, 26f), Anchor.TopRight); ApplyQuestScrollReadableText(questScoreText, new Color(0.17f, 0.075f, 0.025f, 1f), emphasized: false); questScoreText.alignment = TextAnchor.MiddleRight; questScoreText.raycastTarget = false; @@ -1204,8 +2036,8 @@ private void BuildQuestScrollUi() "접기", 14, FontStyle.Bold, - new Vector2(-24f, -45f), - new Vector2(62f, 28f), + new Vector2(-20f, -40f), + new Vector2(56f, 26f), Anchor.TopRight, new Color(0.52f, 0.26f, 0.095f, 0.96f), ToggleQuestScrollCollapsed); @@ -1222,13 +2054,13 @@ private void BuildQuestScrollUi() questScrollBodyGroup = questScrollBodyRoot.gameObject.AddComponent(); CreateImage("Quest Log Divider", questScrollBodyRoot, new Vector2(QuestScrollContentInset, -190f), new Vector2(QuestScrollContentWidth, 2.5f), Anchor.TopLeft, new Color(0.33f, 0.16f, 0.055f, 0.62f)).raycastTarget = false; - questStatusText = CreateText("Quest Status Text", questScrollBodyRoot, "", 13, FontStyle.Bold, new Vector2(QuestScrollContentInset, -200f), new Vector2(QuestScrollContentWidth, 54f), Anchor.TopLeft); + questStatusText = CreateText("Quest Status Text", questScrollBodyRoot, "", 12, FontStyle.Bold, new Vector2(QuestScrollContentInset, -200f), new Vector2(QuestScrollContentWidth, 48f), Anchor.TopLeft); ApplyQuestScrollReadableText(questStatusText, new Color(0.13f, 0.055f, 0.020f, 1f), emphasized: false); questStatusText.alignment = TextAnchor.UpperLeft; questStatusText.verticalOverflow = VerticalWrapMode.Truncate; questStatusText.raycastTarget = false; - questProgressText = CreateText("Quest Progress Text", questScrollBodyRoot, "", 12, FontStyle.Bold, new Vector2(QuestScrollContentInset, 17f), new Vector2(QuestScrollContentWidth, 24f), Anchor.BottomLeft); + questProgressText = CreateText("Quest Progress Text", questScrollBodyRoot, "", 12, FontStyle.Bold, new Vector2(QuestScrollContentInset, -252f), new Vector2(QuestScrollContentWidth, 26f), Anchor.TopLeft); ApplyQuestScrollReadableText(questProgressText, new Color(0.16f, 0.070f, 0.025f, 1f), emphasized: false); questProgressText.alignment = TextAnchor.MiddleLeft; questProgressText.horizontalOverflow = HorizontalWrapMode.Overflow; @@ -1248,15 +2080,8 @@ private static void ApplyQuestScrollReadableText(Text text, Color color, bool em return; } + MagicExamUiFactory.StyleParchmentText(text, emphasized); text.color = color; - text.lineSpacing = 1.06f; - - var shadow = text.gameObject.GetComponent() ?? text.gameObject.AddComponent(); - shadow.effectColor = emphasized - ? new Color(0.98f, 0.78f, 0.42f, 0.42f) - : new Color(0.98f, 0.78f, 0.42f, 0.30f); - shadow.effectDistance = emphasized ? new Vector2(1.2f, -1.2f) : new Vector2(0.8f, -0.8f); - shadow.useGraphicAlpha = true; } private void ToggleQuestScrollCollapsed() @@ -1332,53 +2157,94 @@ private void ApplyQuestScrollAnimationState() private void BuildFirstFloorLetterUi() { - var overlayImage = CreateImage("First Floor Letter Overlay", canvas.transform, Vector2.zero, Vector2.zero, Anchor.Center, new Color(0.004f, 0.004f, 0.006f, 0.82f)); - overlayImage.raycastTarget = true; - firstFloorLetterOverlay = overlayImage.rectTransform; - firstFloorLetterOverlay.anchorMin = Vector2.zero; - firstFloorLetterOverlay.anchorMax = Vector2.one; - firstFloorLetterOverlay.pivot = new Vector2(0.5f, 0.5f); - firstFloorLetterOverlay.offsetMin = Vector2.zero; - firstFloorLetterOverlay.offsetMax = Vector2.zero; - firstFloorLetterOverlay.anchoredPosition = Vector2.zero; - firstFloorLetterOverlay.sizeDelta = Vector2.zero; - - var parchment = CreatePanel("First Floor Letter Scroll", firstFloorLetterOverlay, Vector2.zero, new Vector2(760f, 540f), Anchor.Center, new Color(0.78f, 0.64f, 0.43f, 0.98f)); - AddPanelBorder(parchment, new Color(0.40f, 0.23f, 0.10f, 0.94f), 3f); - CreateImage("First Floor Letter Top Roll", parchment, new Vector2(32f, -22f), new Vector2(696f, 34f), Anchor.TopLeft, new Color(0.92f, 0.76f, 0.48f, 0.98f)); - CreateImage("First Floor Letter Bottom Roll", parchment, new Vector2(32f, 22f), new Vector2(696f, 34f), Anchor.BottomLeft, new Color(0.58f, 0.39f, 0.20f, 0.96f)); - CreateImage("First Floor Letter Wax Seal", parchment, new Vector2(-72f, 72f), new Vector2(54f, 54f), Anchor.BottomRight, new Color(0.56f, 0.05f, 0.04f, 0.96f)); - - var title = CreateText("First Floor Letter Title", parchment, "입학 안내 편지", 29, FontStyle.Bold, new Vector2(48f, -58f), new Vector2(560f, 40f), Anchor.TopLeft); + firstFloorLetterOverlay = CreatePanel( + "First Floor Letter Overlay", + canvas.transform, + new Vector2(-18f, -18f), + new Vector2(QuestScrollWidth, 248f), + Anchor.TopRight, + new Color(0.88f, 0.69f, 0.42f, 0.995f)); + firstFloorLetterOverlay.gameObject.AddComponent(); + AddPanelBorder(firstFloorLetterOverlay, new Color(0.38f, 0.20f, 0.07f, 0.98f), 3.2f); + CreateImage("First Floor Letter Readability Paper", firstFloorLetterOverlay, new Vector2(16f, -52f), new Vector2(QuestScrollWidth - 32f, 170f), Anchor.TopLeft, new Color(0.92f, 0.74f, 0.46f, 0.90f)).raycastTarget = false; + CreateImage("First Floor Letter Top Roll", firstFloorLetterOverlay, new Vector2(24f, -12f), new Vector2(382f, 30f), Anchor.TopLeft, new Color(0.96f, 0.78f, 0.46f, 0.995f)).raycastTarget = false; + CreateImage("First Floor Letter Bottom Roll", firstFloorLetterOverlay, new Vector2(24f, 14f), new Vector2(382f, 30f), Anchor.BottomLeft, new Color(0.52f, 0.30f, 0.12f, 0.98f)).raycastTarget = false; + CreateImage("First Floor Letter Left Cap", firstFloorLetterOverlay, new Vector2(10f, -12f), new Vector2(28f, 30f), Anchor.TopLeft, new Color(0.60f, 0.34f, 0.13f, 0.99f)).raycastTarget = false; + CreateImage("First Floor Letter Right Cap", firstFloorLetterOverlay, new Vector2(-10f, -12f), new Vector2(28f, 30f), Anchor.TopRight, new Color(0.60f, 0.34f, 0.13f, 0.99f)).raycastTarget = false; + CreateImage("First Floor Letter Wax Seal", firstFloorLetterOverlay, new Vector2(-38f, 38f), new Vector2(38f, 38f), Anchor.BottomRight, new Color(0.56f, 0.05f, 0.04f, 0.96f)).raycastTarget = false; + + var title = CreateText("First Floor Letter Title", firstFloorLetterOverlay, "탑 초대장", 21, FontStyle.Bold, new Vector2(26f, -42f), new Vector2(238f, 34f), Anchor.TopLeft); title.color = new Color(0.21f, 0.11f, 0.04f, 1f); title.alignment = TextAnchor.MiddleLeft; title.raycastTarget = false; - firstFloorLetterText = CreateText("First Floor Letter Text", parchment, FirstFloorLetterBody, 18, FontStyle.Normal, new Vector2(52f, -112f), new Vector2(650f, 332f), Anchor.TopLeft); + firstFloorLetterText = CreateText("First Floor Letter Text", firstFloorLetterOverlay, FirstFloorLetterBody, 15, FontStyle.Normal, new Vector2(26f, -82f), new Vector2(356f, 118f), Anchor.TopLeft); firstFloorLetterText.color = new Color(0.18f, 0.10f, 0.045f, 1f); - firstFloorLetterText.lineSpacing = 1.12f; + firstFloorLetterText.lineSpacing = 1.05f; firstFloorLetterText.raycastTarget = false; - var signature = CreateText("First Floor Letter Signature", parchment, "마법 시험관의 인장", 16, FontStyle.Italic, new Vector2(-244f, 58f), new Vector2(170f, 28f), Anchor.BottomRight); + var signature = CreateText("First Floor Letter Signature", firstFloorLetterOverlay, "고깔모자 시험관의 인장", 12, FontStyle.Italic, new Vector2(-196f, 28f), new Vector2(146f, 22f), Anchor.BottomRight); signature.color = new Color(0.28f, 0.12f, 0.05f, 0.88f); signature.alignment = TextAnchor.MiddleRight; signature.raycastTarget = false; firstFloorLetterCloseButton = CreateButton( "First Floor Letter Close Button", - parchment, - "X", - 20, + firstFloorLetterOverlay, + "접기", + 14, FontStyle.Bold, - new Vector2(-18f, -18f), - new Vector2(42f, 42f), + new Vector2(-18f, -45f), + new Vector2(76f, 30f), Anchor.TopRight, - new Color(0.78f, 0.03f, 0.02f, 0.98f), + new Color(0.58f, 0.30f, 0.10f, 0.98f), CloseFirstFloorLetter); firstFloorLetterOverlay.gameObject.SetActive(false); } + private void BuildFinalTaskBannerUi() + { + finalTaskBanner = CreatePanel( + "Final Task Banner", + canvas.transform, + new Vector2(0f, -54f), + new Vector2(680f, 54f), + Anchor.TopRight, + new Color(0.02f, 0.026f, 0.045f, 0.84f)); + finalTaskBanner.anchorMin = new Vector2(0.5f, 1f); + finalTaskBanner.anchorMax = new Vector2(0.5f, 1f); + finalTaskBanner.pivot = new Vector2(0.5f, 1f); + finalTaskBanner.anchoredPosition = new Vector2(0f, -54f); + AddPanelBorder(finalTaskBanner, new Color(1f, 0.82f, 0.34f, 0.64f), 1.6f); + + var accent = CreateImage( + "Final Task Banner Glow", + finalTaskBanner, + Vector2.zero, + new Vector2(680f, 4f), + Anchor.BottomLeft, + new Color(1f, 0.82f, 0.34f, 0.74f)); + accent.raycastTarget = false; + + finalTaskBannerText = CreateText( + "Final Task Banner Text", + finalTaskBanner, + "", + 17, + FontStyle.Bold, + Vector2.zero, + new Vector2(632f, 42f), + Anchor.Center); + finalTaskBannerText.alignment = TextAnchor.MiddleCenter; + finalTaskBannerText.color = new Color(1f, 0.92f, 0.55f, 1f); + finalTaskBannerText.raycastTarget = false; + var outline = finalTaskBannerText.gameObject.AddComponent(); + outline.effectColor = new Color(0.02f, 0.02f, 0.03f, 0.95f); + outline.effectDistance = new Vector2(1.4f, -1.4f); + finalTaskBanner.gameObject.SetActive(false); + } + private void ShowFirstFloorLetter() { if (firstFloorLetterOverlay == null) @@ -1422,7 +2288,7 @@ private void BuildCustomReferenceUi() var tail = CreateImage("Custom Reference Shelf Bubble Tail", customReferenceBubble, new Vector2(32f, -72f), new Vector2(28f, 28f), Anchor.TopLeft, new Color(0.035f, 0.055f, 0.07f, 0.95f)); tail.rectTransform.localRotation = Quaternion.Euler(0f, 0f, 45f); tail.raycastTarget = false; - var bubbleText = CreateText("Custom Reference Shelf Bubble Text", customReferenceBubble, "도형 레퍼런스\n책장에서 커스텀 도형을 가져올 수 있습니다.", 13, FontStyle.Bold, new Vector2(16f, -12f), new Vector2(164f, 66f), Anchor.TopLeft); + var bubbleText = CreateText("Custom Reference Shelf Bubble Text", customReferenceBubble, "도형함\n받은 도형을 확인하고 슬롯에 넣을 수 있습니다.", 13, FontStyle.Bold, new Vector2(16f, -12f), new Vector2(164f, 66f), Anchor.TopLeft); bubbleText.color = new Color(0.92f, 0.98f, 1f, 1f); bubbleText.raycastTarget = false; CreateButton( @@ -1446,7 +2312,7 @@ private void BuildCustomReferenceUi() Anchor.BottomLeft, new Color(0.025f, 0.035f, 0.055f, 0.98f)); AddPanelBorder(customReferencePanel, new Color(0.65f, 0.78f, 0.95f, 0.82f), 2f); - var title = CreateText("Custom Reference Panel Title", customReferencePanel, "커스텀 도형 레퍼런스", 23, FontStyle.Bold, new Vector2(24f, -18f), new Vector2(500f, 34f), Anchor.TopLeft); + var title = CreateText("Custom Reference Panel Title", customReferencePanel, "가져온 도형 예시", 23, FontStyle.Bold, new Vector2(24f, -18f), new Vector2(500f, 34f), Anchor.TopLeft); title.color = new Color(0.95f, 0.99f, 1f, 1f); title.fontSize = 21; title.rectTransform.anchoredPosition = new Vector2(22f, -16f); @@ -1476,51 +2342,59 @@ private void BuildCustomReferenceUi() customReferencePanel.gameObject.SetActive(false); } - private void CreateCustomReferenceCard(CustomShapeReferenceDefinition reference, int index) + private void CreateCustomReferenceCard(CustomShapeReferenceDefinition reference, int index, bool compact) { - var y = -58f - index * 56f; + var column = compact ? index % 2 : 0; + var row = compact ? index / 2 : index; + var cardWidth = compact ? 248f : 516f; + var cardHeight = compact ? 42f : 48f; + var x = 22f + column * 268f; + var y = -58f - row * (compact ? 49f : 56f); + var highlighted = IsCurrentFinalTaskReference(reference); var card = CreatePanel( - $"Custom Reference Card {reference.family}", + $"Custom Reference Card {reference.family} {reference.shapeToken} {index}", customReferencePanel, - new Vector2(22f, y), - new Vector2(516f, 48f), + new Vector2(x, y), + new Vector2(cardWidth, cardHeight), Anchor.TopLeft, - new Color(0.045f, 0.065f, 0.095f, 0.96f)); + highlighted ? new Color(0.075f, 0.092f, 0.12f, 0.98f) : new Color(0.045f, 0.065f, 0.095f, 0.96f)); customReferenceCards.Add(card.gameObject); - AddPanelBorder(card, new Color(1f, 1f, 1f, 0.12f), 1f); + AddPanelBorder(card, highlighted ? new Color(1f, 0.84f, 0.32f, 0.82f) : new Color(1f, 1f, 1f, 0.12f), highlighted ? 2f : 1f); var familyColor = FamilyColor(reference.family); - var swatch = CreateImage($"Custom Reference Swatch {reference.family}", card, new Vector2(10f, -8f), new Vector2(36f, 36f), Anchor.TopLeft, new Color(familyColor.r, familyColor.g, familyColor.b, 0.70f)); + var swatch = CreateImage($"Custom Reference Swatch {reference.family}", card, new Vector2(10f, -7f), new Vector2(compact ? 30f : 36f, compact ? 30f : 36f), Anchor.TopLeft, new Color(familyColor.r, familyColor.g, familyColor.b, 0.70f)); swatch.sprite = CustomShapeSpriteFactory.CreateShapeSprite(reference.shapeToken, 2); swatch.preserveAspect = true; var label = CreateText( $"Custom Reference Label {reference.family}", card, ReferenceShapeTitle(reference), - 15, + compact ? 12 : 15, FontStyle.Bold, - new Vector2(58f, -7f), - new Vector2(184f, 21f), + new Vector2(compact ? 46f : 58f, -6f), + new Vector2(compact ? 122f : 184f, compact ? 18f : 21f), Anchor.TopLeft); label.color = Color.Lerp(familyColor, Color.white, 0.55f); var summary = CreateText( $"Custom Reference Summary {reference.family}", card, ReferenceShapeSummary(reference), - 12, + compact ? 10 : 12, FontStyle.Normal, - new Vector2(58f, -29f), - new Vector2(322f, 17f), + new Vector2(compact ? 46f : 58f, compact ? -24f : -29f), + new Vector2(compact ? 124f : 322f, compact ? 14f : 17f), Anchor.TopLeft); summary.color = new Color(0.82f, 0.88f, 0.94f, 0.90f); var capturedReference = reference; CreateButton( - $"Import Custom Reference {reference.family}", + compact + ? $"Import Custom Reference {reference.family} {reference.shapeToken} {index}" + : $"Import Custom Reference {reference.family}", card, - "들여오기", - 13, + highlighted ? "문제 도형" : "들여오기", + compact ? 10 : 13, FontStyle.Bold, - new Vector2(404f, -7f), - new Vector2(92f, 34f), + new Vector2(compact ? 174f : 404f, -7f), + new Vector2(compact ? 60f : 92f, compact ? 30f : 34f), Anchor.TopLeft, new Color(0.10f, 0.24f, 0.38f, 0.98f), () => @@ -1531,7 +2405,9 @@ private void CreateCustomReferenceCard(CustomShapeReferenceDefinition reference, private static string ReferenceShapeTitle(CustomShapeReferenceDefinition reference) { - return ShapeTokenTitle(reference?.shapeToken); + return reference == null + ? ShapeTokenTitle("") + : $"{SpellLabels.Korean(reference.family)} {ShapeTokenTitle(reference.shapeToken)}"; } private static string ReferenceShapeSummary(CustomShapeReferenceDefinition reference) @@ -1539,6 +2415,48 @@ private static string ReferenceShapeSummary(CustomShapeReferenceDefinition refer return ShapeTokenSummary(reference?.shapeToken); } + private bool IsCurrentFinalTaskReference(CustomShapeReferenceDefinition reference) + { + if (!IsFinalFloor || currentFinalTask?.references == null) + { + return false; + } + + var key = ReferenceKey(reference); + return currentFinalTask.references.Any(item => string.Equals(ReferenceKey(item), key, StringComparison.OrdinalIgnoreCase)); + } + + private static bool SlotMatchesReference(CustomShapeSlot slot, CustomShapeReferenceDefinition reference) + { + return slot != null && + reference != null && + slot.IsOccupied && + slot.mappedFamily == reference.family && + string.Equals(slot.shapeToken, reference.shapeToken, StringComparison.OrdinalIgnoreCase) && + string.Equals(EventShapeKey(slot.eventShapeTokens), EventShapeKey(reference.eventShapeTokens), StringComparison.OrdinalIgnoreCase); + } + + private static string ReferenceKey(CustomShapeReferenceDefinition reference) + { + return reference == null + ? "" + : $"{reference.family}:{reference.shapeToken}:{EventShapeKey(reference.eventShapeTokens)}"; + } + + private static string SlotReferenceKey(CustomShapeSlot slot) + { + return slot == null + ? "" + : $"{slot.mappedFamily}:{slot.shapeToken}:{EventShapeKey(slot.eventShapeTokens)}"; + } + + private static string EventShapeKey(IEnumerable tokens) + { + return string.Join("+", (tokens ?? Array.Empty()) + .Where(token => !string.IsNullOrWhiteSpace(token)) + .Select(token => token.Trim().ToLowerInvariant())); + } + private static string ShapeTokenTitle(string token) { return NormalizeShapeToken(token) switch @@ -1548,6 +2466,7 @@ private static string ShapeTokenTitle(string token) "arrow" => "화살표", "rect" => "사각형", "brace" => "꺾쇠", + "pentagon" => "오각형", "hexagon" => "육각형", "star" => "별", "wave" => "물결", @@ -1564,6 +2483,7 @@ private static string ShapeTokenSummary(string token) "arrow" => "몸통 선을 긋고 끝에 화살촉을 붙입니다.", "rect" => "네 변을 이어 닫힌 사각형을 만듭니다.", "brace" => "양끝이 벌어진 꺾쇠 모양으로 휘어 그립니다.", + "pentagon" => "다섯 변을 이어 닫힌 강철형을 만듭니다.", "hexagon" => "여섯 변을 이어 닫힌 결정형을 만듭니다.", "star" => "뾰족한 꼭짓점을 반복해 별 모양을 만듭니다.", "wave" => "좌우로 흐르는 물결선을 한 획으로 그립니다.", @@ -1589,6 +2509,26 @@ private void BeginQuestChecklistForCurrentFloor() private IEnumerable BuildQuestChecklistDefinitions(FloorDefinition floor) { + if (floor.number == 4) + { + return new[] + { + QuestChecklistItemDefinition.ReferenceImports("floor4_arrow_import", "시험관이 넣어 준 화살표 도형 확인", 1), + QuestChecklistItemDefinition.GoalsAtLeast("floor4_three_beams", "속성 문양에 화살표를 더해 3회 명중", 3), + QuestChecklistItemDefinition.AllGoals("floor4_all_beams", "모든 속성 빛줄기를 허수아비에 명중") + }; + } + + if (floor.number == 5 && currentFinalTasks.Count > 0) + { + return currentFinalTasks + .Select((task, index) => QuestChecklistItemDefinition.Goal( + $"floor5_final_task_{index + 1}", + $"문제 {index + 1}: {task.goal.title}", + task.goal.id)) + .ToArray(); + } + return floor.number switch { 1 => new[] @@ -1599,14 +2539,14 @@ private IEnumerable BuildQuestChecklistDefinitions }, 2 => new[] { - QuestChecklistItemDefinition.ReferencePanel("floor2_shelf", "책장 프리셋 창 열기"), - QuestChecklistItemDefinition.ReferenceImports("floor2_import", "책장에서 도형 하나 가져오기", 1), - QuestChecklistItemDefinition.GoalsAtLeast("floor2_three", "커스텀 표식 세 개 깨우기", 3), - QuestChecklistItemDefinition.AllGoals("floor2_all", "다섯 커스텀 표식 모두 깨우기") + QuestChecklistItemDefinition.ReferencePanel("floor2_shelf", "도형함에서 지급 도형 확인"), + QuestChecklistItemDefinition.ReferenceImports("floor2_import", "시험관이 넣어 준 도형 확인", 1), + QuestChecklistItemDefinition.GoalsAtLeast("floor2_three", "슬롯 도형 표식 세 개 깨우기", 3), + QuestChecklistItemDefinition.AllGoals("floor2_all", "슬롯 도형 표식 모두 깨우기") }, 3 => new[] { - QuestChecklistItemDefinition.ReferenceImports("floor3_import", "3층 책장 도형 하나 가져오기", 1), + QuestChecklistItemDefinition.ReferenceImports("floor3_import", "시험관이 넣어 준 길 도형 확인", 1), QuestChecklistItemDefinition.Goal("floor3_river", "강물을 얼음길로 바꾸기", "frozen_river"), QuestChecklistItemDefinition.Goal("floor3_hole", "바닥 구멍 메우기", "earth_stairs"), QuestChecklistItemDefinition.Goal("floor3_cliff", "낭떠러지를 다리로 잇기", "living_bridge"), @@ -1614,9 +2554,9 @@ private IEnumerable BuildQuestChecklistDefinitions }, 4 => new[] { - QuestChecklistItemDefinition.ReferenceImports("floor4_import", "전투 도형 하나 가져오기", 1), - QuestChecklistItemDefinition.GoalsAtLeast("floor4_two", "훈련 표적 둘 반응시키기", 2), - QuestChecklistItemDefinition.AllGoals("floor4_all", "네 표적 모두 반응시키기") + QuestChecklistItemDefinition.ReferenceImports("floor4_import", "시험관이 넣어 준 화살표 도형 확인", 1), + QuestChecklistItemDefinition.GoalsAtLeast("floor4_two", "허수아비에게 빛줄기 두 번 명중", 2), + QuestChecklistItemDefinition.AllGoals("floor4_all", "모든 속성 빛줄기를 허수아비에 명중") }, _ => new[] { @@ -1641,11 +2581,11 @@ private void RebuildQuestChecklistRows() } questTitleText.text = $"층 {currentQuestChecklist.floorNumber} 퀘스트"; - var y = -10f; + var y = -8f; for (var index = 0; index < currentQuestChecklist.entries.Count; index++) { questChecklistViews.Add(CreateQuestChecklistRow(currentQuestChecklist.entries[index], index, y)); - y -= 38f; + y -= 34f; } } @@ -1655,7 +2595,7 @@ private QuestChecklistItemView CreateQuestChecklistRow(QuestChecklistEntry entry $"Quest Checklist Row {index + 1}", questScrollBodyRoot == null ? questScrollPanel : questScrollBodyRoot, new Vector2(QuestScrollContentInset, y), - new Vector2(QuestScrollContentWidth, 34f), + new Vector2(QuestScrollContentWidth, 31f), Anchor.TopLeft, new Color(0.82f, 0.60f, 0.34f, 0.32f)); row.GetComponent().raycastTarget = false; @@ -1663,8 +2603,8 @@ private QuestChecklistItemView CreateQuestChecklistRow(QuestChecklistEntry entry var box = CreateImage( $"Quest Checkbox {index + 1}", row, - new Vector2(5f, -5f), - new Vector2(24f, 24f), + new Vector2(4f, -4f), + new Vector2(22f, 22f), Anchor.TopLeft, new Color(0.96f, 0.82f, 0.54f, 0.72f)); box.raycastTarget = false; @@ -1690,8 +2630,8 @@ private QuestChecklistItemView CreateQuestChecklistRow(QuestChecklistEntry entry entry.definition.label, 15, FontStyle.Bold, - new Vector2(39f, -2f), - new Vector2(QuestScrollContentWidth - 46f, 30f), + new Vector2(34f, -1f), + new Vector2(QuestScrollContentWidth - 40f, 28f), Anchor.TopLeft); ApplyQuestScrollReadableText(label, new Color(0.12f, 0.050f, 0.016f, 1f), emphasized: false); label.alignment = TextAnchor.MiddleLeft; @@ -1789,6 +2729,16 @@ private void SaveCurrentQuestChecklistScore(string reason) elapsedMs = snapshot.elapsedMs, items = snapshot.items }); + LogActionEvent( + "quest_checklist_snapshot", + "system", + "quest", + snapshot.floorNumber.ToString(CultureInfo.InvariantCulture), + snapshot.floorTitle, + value: $"{snapshot.completedCount}/{snapshot.totalCount}", + payloadJson: BuildEventPayload( + Pair("reason", snapshot.reason), + Pair("items", snapshot.items))); } } @@ -1847,6 +2797,46 @@ private string BuildQuestChecklistSnapshotSummary() .Select(snapshot => $"{snapshot.floorNumber}층 {snapshot.completedCount}/{snapshot.totalCount} - {snapshot.reason}")); } + private void WriteEnrollmentSnapshot(string eventType) + { + if (logger == null || !logger.IsCollectionEnabled) + { + return; + } + + if (!string.IsNullOrWhiteSpace(eventType)) + { + lastEnrollmentEventType = eventType; + } + + lastEnrollmentEventUtc = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture); + var completedGoals = currentQuestChecklist?.CompletedCount ?? activeGoals.Count(goal => goal.completed); + var totalGoals = currentQuestChecklist?.TotalCount ?? activeGoals.Count; + var finalCompletedGoals = IsFinalFloor ? activeGoals.Count(goal => goal.completed) : 0; + var finalTotalGoals = IsFinalFloor ? CurrentFinalPassingGoalCount() : 0; + logger.WriteEnrollmentCsv(new EnrollmentLog + { + sessionId = sessionId, + issuedAtUtc = lastEnrollmentEventUtc, + buildVersion = BuildVersion, + status = finalTrueEnding ? "수료 완료" : HasEndingReport ? "시험 종료" : "재학 중", + currentFloor = CurrentFloorNumber, + currentFloorTitle = floorController?.Current?.title ?? "", + completedGoals = completedGoals, + totalGoals = totalGoals, + globalCompletedGoals = QuestChecklistGlobalCompleted(includeCurrent: true), + globalTotalGoals = QuestChecklistGlobalTotal(includeCurrent: true), + completedFinalGoals = finalCompletedGoals, + totalFinalGoals = finalTotalGoals, + currentFinalTask = currentFinalTask?.prompt ?? "", + totalElapsedMs = sessionStartedAt <= 0f ? 0 : Mathf.Max(0, Mathf.RoundToInt((Time.time - sessionStartedAt) * 1000f)), + totalAttempts = trialCounter, + lastEventType = lastEnrollmentEventType, + lastEventUtc = lastEnrollmentEventUtc, + outputDirectory = OutputDirectory + }); + } + private void LoadFloor(int index, bool saveCurrentQuestScore = true) { if (saveCurrentQuestScore) @@ -1855,9 +2845,23 @@ private void LoadFloor(int index, bool saveCurrentQuestScore = true) } pendingAdvanceAt = -1f; + floorTransitionState = FloorTransitionState.None; + floorTransitionStartedAt = -1f; + floorAdvanceSpeechDeadlineAt = -1f; + floorTransitionNextPulseAt = -1f; + SetPlayerBlinkAlpha(1f); finalCompletionCelebrated = false; finalTrueEnding = false; + finalCertificateQueued = false; + finalCertificateAwaitingExit = false; + finalTaskBannerSuppressedForTitle = false; + SetGameplayInputEnabled(true); reportPanel.gameObject.SetActive(false); + if (endingReportExitButton != null) + { + endingReportExitButton.gameObject.SetActive(false); + endingReportExitButton.interactable = true; + } resultPanel.gameObject.SetActive(false); if (resultText != null) { @@ -1865,13 +2869,18 @@ private void LoadFloor(int index, bool saveCurrentQuestScore = true) } floorSkipButton.gameObject.SetActive(true); + floorSkipButton.transform.SetAsLastSibling(); + healthPanel.gameObject.SetActive(true); questScrollPanel.gameObject.SetActive(true); CloseCustomReferenceUi(); HideGoalProximityBubble(); ClearFloorObjects(); floorController.Load(index); + finalTaskBannerSuppressedForTitle = false; + ConfigurePlanBFloorDefinitionsIfNeeded(); questReferencePanelOpenedThisFloor = false; questImportedReferenceIdsThisFloor.Clear(); + mentorGrantedReferenceKeysThisFloor.Clear(); activeStageDefinition = LoadStageDefinitionForFloor(floorController.Current.number); ConfigurePlatformMotion(activeStageDefinition != null); ApplyFloorTheme(floorController.Current); @@ -1880,11 +2889,23 @@ private void LoadFloor(int index, bool saveCurrentQuestScore = true) MovePlayerTo(safePosition); floorStartedAt = Time.time; floorEnteredAt = Time.time; + LogActionEvent( + "floor_entered", + "system", + "floor", + floorController.Current.number.ToString(CultureInfo.InvariantCulture), + floorController.Current.title, + safePosition, + payloadJson: BuildEventPayload( + Pair("objective", floorController.Current.objective), + Pair("goalCount", floorController.Current.goals.Count.ToString(CultureInfo.InvariantCulture)))); castsOnCurrentFloor = 0; firstFloorGhostShown = PlayerPrefs.GetInt("MagicExamHall.FirstFloorGhostSeen", 0) == 1; firstFloorLongSilenceShown = false; activeGoals.Clear(); activeGoals.AddRange(floorController.Current.goals.Select(goal => goal.Clone())); + RecordEncounteredFinalTasksForCurrentFloor(); + ConfigureFinalExamTaskIfNeeded(); ApplyStageGoalOverrides(); activeHazards.Clear(); activeHazards.AddRange(floorController.Current.hazards.Select(hazard => hazard.Clone())); @@ -1904,6 +2925,9 @@ private void LoadFloor(int index, bool saveCurrentQuestScore = true) { HideFirstFloorLetter(); } + + GrantMentorCustomReferencesForCurrentFloor(showSpeech: false); + WriteEnrollmentSnapshot("floor_entered"); } private static FloorStageDefinition LoadStageDefinitionForFloor(int floorNumber) @@ -1917,96 +2941,363 @@ private static FloorStageDefinition LoadStageDefinitionForFloor(int floorNumber) FloorStageDefinition.CreateFallbackFloorThree(); } - private void ApplyStageGoalOverrides() + private void ConfigurePlanBFloorDefinitionsIfNeeded() { - if (activeStageDefinition?.obstacles == null) + var floor = floorController?.Current; + if (floor == null) { return; } - foreach (var goal in activeGoals) + if (floor.number == 4) { - var obstacle = activeStageDefinition.FindObstacle(goal.id); - if (obstacle == null) - { - continue; - } + floor.title = "속성 빛줄기 교정층"; + floor.objective = "허수아비를 상대로 속성 문양을 만든 뒤 화살표 도형을 더해 빛줄기를 맞히세요."; + floor.entryNote = "힌트: 목표는 중앙 허수아비입니다. 속성 문양을 만든 뒤 빛나는 원 안에 화살표를 그려 허수아비를 직접 맞히세요."; + floor.completeNote = "허수아비가 모든 속성 빛줄기를 견뎌냈습니다. 최종 시험으로 올라갑니다."; + floor.goals.Clear(); + floor.goals.AddRange(BuildFloorFourBeamGoals()); + return; + } - goal.position = obstacle.goalPosition; - goal.radius = obstacle.goalRadius <= 0f ? goal.radius : obstacle.goalRadius; + if (floor.number == 5) + { + floor.title = "최종 시험장"; + floor.objective = "고깔모자 시험관이 내는 대표 문제 3개를 순서대로 듣고, 보이는 문제에 맞는 마법진을 완성하세요."; + floor.entryNote = "힌트: 최종 시험관이 1층부터 4층까지 배운 문제 중 3개를 무작위로 냅니다. 한 문제를 해결하면 다음 문제가 나타납니다."; + floor.completeNote = "최종 시험 세 문제를 모두 통과했습니다. 수료증을 발급합니다."; + floor.goals.Clear(); } } - private void ConfigurePlatformMotion(bool enabled) + private static IEnumerable BuildFloorFourBeamGoals() { - platformMotionActive = enabled; - EnsurePlayerPhysics(); - if (playerBody == null || playerCollider == null) + return new[] { - return; - } + WorldStateGoal.AttributeBeam("beam_fire", "불꽃 빛줄기", SpellFamily.Fire, new Vector2(-5.4f, 2.45f), new Color(1f, 0.31f, 0.18f), "불꽃 빛줄기가 허수아비를 맞히며 타격 판정을 남깁니다.").WithReaction(WorldReactionKind.CombatHit), + WorldStateGoal.AttributeBeam("beam_water", "물결 빛줄기", SpellFamily.Water, new Vector2(-2.7f, 3.05f), new Color(0.24f, 0.48f, 0.86f), "물결 빛줄기가 허수아비를 맞히며 타격 판정을 남깁니다.").WithReaction(WorldReactionKind.CombatHit), + WorldStateGoal.AttributeBeam("beam_wind", "바람 빛줄기", SpellFamily.Wind, new Vector2(0f, 3.25f), new Color(0.74f, 0.86f, 0.92f), "바람 빛줄기가 허수아비를 맞히며 타격 판정을 남깁니다.").WithReaction(WorldReactionKind.CombatHit), + WorldStateGoal.AttributeBeam("beam_earth", "대지 빛줄기", SpellFamily.Earth, new Vector2(2.7f, 3.05f), new Color(0.74f, 0.55f, 0.32f), "대지 빛줄기가 허수아비를 맞히며 타격 판정을 남깁니다.").WithReaction(WorldReactionKind.CombatHit), + WorldStateGoal.AttributeBeam("beam_life", "생명 빛줄기", SpellFamily.Life, new Vector2(5.4f, 2.45f), new Color(0.35f, 0.86f, 0.42f), "생명 빛줄기가 허수아비를 맞히며 타격 판정을 남깁니다.").WithReaction(WorldReactionKind.CombatHit) + }; + } - playerBody.simulated = enabled; - playerCollider.enabled = enabled; - playerBody.linearVelocity = Vector2.zero; - platformHorizontalVelocity = 0f; - velocity = Vector2.zero; - if (!enabled && mainCamera != null) + private static IReadOnlyList FinalExamTaskPool() + { + var fireLine = new CustomShapeReferenceDefinition(SpellFamily.Fire, "최종 불꽃 직선", "line", new[] { "line" }, "2층 대표 과제용 불꽃 직선입니다."); + var windArrow = new CustomShapeReferenceDefinition(SpellFamily.Wind, "최종 바람 화살표", "arrow", new[] { "arrow" }, "2층 대표 과제용 바람 화살표입니다."); + var iceHexagon = new CustomShapeReferenceDefinition(SpellFamily.Water, "최종 얼음 결정", "hexagon", new[] { "hexagon" }, "3층 대표 과제용 얼음 결정입니다."); + var lifeBridge = new CustomShapeReferenceDefinition(SpellFamily.Life, "최종 덩굴 다리", "arrow", new[] { "arrow" }, "3층 대표 과제용 생명 화살표입니다."); + var fireBeam = FloorFiveBeamShapeReferences.First(reference => reference.family == SpellFamily.Fire); + var waterBeam = FloorFiveBeamShapeReferences.First(reference => reference.family == SpellFamily.Water); + + return new[] { - mainCamera.transform.position = new Vector3(0f, 0f, -10f); - mainCamera.orthographicSize = GameplayCameraOrthographicSize; - } + new FinalExamTaskDefinition( + "final_puddle", + "1층 물웅덩이 문제입니다. 물 문양으로 표식을 정화하세요.", + 1, + WorldStateGoal.Base("final_puddle", "최종 물웅덩이", SpellFamily.Water, new Vector2(-1.8f, 2.7f), new Color(0.24f, 0.48f, 0.86f), "물 문양이 최종 시험 표식을 맑게 정화했습니다.")), + new FinalExamTaskDefinition( + "final_ember", + "1층 불씨 문제입니다. 불꽃 문양으로 표식을 점화하세요.", + 1, + WorldStateGoal.Base("final_ember", "최종 불씨", SpellFamily.Fire, new Vector2(-1.8f, 2.7f), new Color(1f, 0.31f, 0.18f), "불꽃 문양이 최종 시험 표식을 점화했습니다.")), + new FinalExamTaskDefinition( + "final_custom_fire", + "2층 불꽃 직선 문제입니다. 불꽃 문양 위에 직선을 더하세요.", + 2, + WorldStateGoal.CustomBase("final_custom_fire", "최종 불꽃 직선", SpellFamily.Fire, new Vector2(-1.8f, 2.7f), new Color(1f, 0.31f, 0.18f), "불꽃 직선 도형이 최종 시험에 맞게 반응했습니다.").WithRequirementShapes("line"), + fireLine), + new FinalExamTaskDefinition( + "final_custom_wind", + "2층 바람 화살표 문제입니다. 바람 문양 위에 화살표를 더하세요.", + 2, + WorldStateGoal.CustomBase("final_custom_wind", "최종 바람 화살표", SpellFamily.Wind, new Vector2(-1.8f, 2.7f), new Color(0.74f, 0.86f, 0.92f), "바람 화살표 도형이 최종 시험에 맞게 반응했습니다.").WithRequirementShapes("arrow"), + windArrow), + new FinalExamTaskDefinition( + "final_frozen_river", + "3층 강물 얼리기 문제입니다. 물 문양 위에 육각형 얼음 결정을 더하세요.", + 3, + WorldStateGoal.CustomSpell("final_frozen_river", "최종 강물 얼리기", SpellFamily.Water, CustomSpellEffectKind.Ice, new Vector2(-1.8f, 2.7f), new Color(0.48f, 0.84f, 1f), "얼음 결정이 최종 시험의 강물 표식을 얼렸습니다.").WithRequirementShapes("hexagon").WithReaction(WorldReactionKind.FreezeRiver), + iceHexagon), + new FinalExamTaskDefinition( + "final_living_bridge", + "3층 덩굴 다리 문제입니다. 생명 문양 위에 화살표를 더하세요.", + 3, + WorldStateGoal.CustomSpell("final_living_bridge", "최종 덩굴 다리", SpellFamily.Life, CustomSpellEffectKind.LivingBridge, new Vector2(-1.8f, 2.7f), new Color(0.35f, 0.86f, 0.42f), "덩굴 다리 반응이 최종 시험 표식을 이었습니다.").WithRequirementShapes("arrow").WithReaction(WorldReactionKind.LivingBridge), + lifeBridge), + new FinalExamTaskDefinition( + "final_beam_fire", + "4층 불꽃 빛줄기 문제입니다. 불꽃 문양에 화살표를 더해 허수아비에 맞히세요.", + 4, + WorldStateGoal.AttributeBeam("final_beam_fire", "최종 불꽃 빛줄기", SpellFamily.Fire, new Vector2(-3.6f, 2.5f), new Color(1f, 0.31f, 0.18f), "불꽃 빛줄기가 최종 시험 허수아비에 명중했습니다.").WithReaction(WorldReactionKind.CombatHit), + fireBeam), + new FinalExamTaskDefinition( + "final_beam_water", + "4층 물결 빛줄기 문제입니다. 물 문양에 화살표를 더해 허수아비에 맞히세요.", + 4, + WorldStateGoal.AttributeBeam("final_beam_water", "최종 물결 빛줄기", SpellFamily.Water, new Vector2(-3.6f, 2.5f), new Color(0.24f, 0.48f, 0.86f), "물결 빛줄기가 최종 시험 허수아비에 명중했습니다.").WithReaction(WorldReactionKind.CombatHit), + waterBeam) + }; } - private void SkipCurrentFloorForDebug() + private void ConfigureFinalExamTaskIfNeeded() { - if (HasEndingReport) + if (!IsFinalFloor) { + currentFinalTasks.Clear(); + currentFinalTask = null; + currentFinalTaskIndex = 0; return; } - pendingAdvanceAt = -1f; - resultPanel.gameObject.SetActive(false); - SaveCurrentQuestChecklistScore("skip"); - if (floorController.CurrentFloorIndex < floorController.FloorCount - 1) + currentFinalTasks.Clear(); + currentFinalTasks.AddRange(SelectFinalExamTasks()); + currentFinalTaskIndex = 0; + currentFinalTask = currentFinalTasks.FirstOrDefault(); + activeGoals.Clear(); + for (var index = 0; index < currentFinalTasks.Count; index++) { - LoadFloor(floorController.CurrentFloorIndex + 1, saveCurrentQuestScore: false); - return; + var clone = currentFinalTasks[index].goal.Clone(); + clone.position = FinalExamTaskPosition(index); + activeGoals.Add(clone); } - ShowEndingReport(); + EnsureFinalExamReferencesAvailable(); } - private void TickCustomReferenceShelf() + private void RecordEncounteredFinalTasksForCurrentFloor() { - if (customReferenceBubble == null || player == null || floorController == null) + if (IsFinalFloor) { return; } - var shelfPosition = CurrentCustomReferencePosition(); - var interactionPosition = CurrentCustomReferenceInteractionPosition(); - var hasReferences = CurrentCustomShapeReferences().Count > 0; - var closeToShelf = hasReferences && Vector2.Distance(player.position, interactionPosition) <= CustomReferenceShelfRadius; - var shouldShowBubble = closeToShelf && - !IsPlayerNearActiveGoalSymbol(CustomReferenceGoalSuppressRadius) && - !IsCustomReferencePanelOpenForTests && - customShapeBook?.BlocksGameplayInput != true && - !HasEndingReport; - customReferenceBubble.gameObject.SetActive(shouldShowBubble); - if (!shouldShowBubble) + foreach (var goal in activeGoals) { - return; + if (goal != null && FinalTaskIdBySourceGoalId.TryGetValue(goal.id, out var taskId)) + { + encounteredFinalTaskIds.Add(taskId); + } } + } - mentor?.HideSpeech(); - if (notePanel != null) + private IReadOnlyList SelectFinalExamTasks() + { + var allTasks = FinalExamTaskPool().ToList(); + var pool = encounteredFinalTaskIds.Count == 0 + ? allTasks.ToList() + : allTasks.Where(task => encounteredFinalTaskIds.Contains(task.id)).ToList(); + if (pool.Count == 0) { - notePanel.gameObject.SetActive(false); + return Array.Empty(); } - customReferenceBubble.SetAsLastSibling(); - customReferenceBubble.anchoredPosition = WorldToCanvasPosition(shelfPosition + CustomReferenceBubbleShelfOffset); + var selected = new List(); + if (!string.IsNullOrWhiteSpace(forcedFinalTaskIdForTests)) + { + var forced = pool.FirstOrDefault(task => + string.Equals(task.id, forcedFinalTaskIdForTests, StringComparison.OrdinalIgnoreCase)); + if (forced != null) + { + selected.Add(forced); + pool.Remove(forced); + } + } + + while (selected.Count < FinalExamTaskCount && pool.Count > 0) + { + var index = UnityEngine.Random.Range(0, pool.Count); + var task = pool[Mathf.Clamp(index, 0, pool.Count - 1)]; + selected.Add(task); + pool.Remove(task); + } + + return selected; + } + + private static Vector2 FinalExamTaskPosition(int index) + { + return new Vector2(0f, 2.22f); + } + + private void EnsureFinalExamReferencesAvailable() + { + if (currentFinalTasks.Count == 0) + { + return; + } + + foreach (var reference in currentFinalTasks.SelectMany(task => task.references)) + { + ImportCustomReference(reference, out _, out _); + } + + EnsureCurrentFinalTaskReferencesAvailable(forceReplaceIfFull: true); + } + + private void EnsureCurrentFinalTaskReferencesAvailable(bool forceReplaceIfFull) + { + if (!IsFinalFloor || currentFinalTask == null) + { + return; + } + + foreach (var reference in CurrentFinalTaskRecognitionReferences()) + { + ImportCustomReference(reference, out _, out _, forceReplaceIfFull); + } + } + + private void GrantMentorCustomReferencesForCurrentFloor(bool showSpeech) + { + if (!CustomShapesAvailableOnCurrentFloor || floorController?.Current == null) + { + return; + } + + var references = MentorReferencesForCurrentFloor().ToList(); + if (references.Count == 0) + { + return; + } + + var granted = new List(); + foreach (var reference in references) + { + var key = ReferenceKey(reference); + if (!mentorGrantedReferenceKeysThisFloor.Add(key)) + { + continue; + } + + if (ImportCustomReference(reference, out _, out _)) + { + granted.Add(reference); + } + } + + if (showSpeech && granted.Count > 0) + { + var label = string.Join(", ", granted.Take(3).Select(ReferenceShapeTitle)); + if (granted.Count > 3) + { + label += $" 외 {granted.Count - 3}개"; + } + + var speech = floorController.Current.number == 4 + ? $"시험관이 다가와 화살표 도형을 슬롯에 넣어 두었습니다.\n속성 문양 위에 이어 그려 허수아비를 맞히세요." + : $"시험관이 다가와 필요한 도형을 슬롯에 넣어 두었습니다.\n이번에는 {label}을 이어 그리세요."; + ShowMagicNote(speech, MentorMood.Neutral); + } + } + + private IEnumerable MentorReferencesForCurrentFloor() + { + return floorController?.Current.number switch + { + 2 => CurrentSecondFloorCustomShapeReferences(), + 3 => FloorThreeCustomShapeReferences, + 4 => FloorFourCustomShapeReferences, + _ => Array.Empty() + }; + } + + private void ApplyStageGoalOverrides() + { + if (activeStageDefinition?.obstacles == null) + { + return; + } + + foreach (var goal in activeGoals) + { + var obstacle = activeStageDefinition.FindObstacle(goal.id); + if (obstacle == null) + { + continue; + } + + goal.position = obstacle.goalPosition; + var configuredRadius = obstacle.goalRadius <= 0f ? goal.radius : obstacle.goalRadius; + goal.radius = Mathf.Max( + configuredRadius + StageObstacleGoalRadiusPadding, + StageObstacleGoalRadiusMinimum); + } + } + + private void ConfigurePlatformMotion(bool enabled) + { + platformMotionActive = enabled; + ResetStageLedgeStop(); + EnsurePlayerPhysics(); + if (playerBody == null || playerCollider == null) + { + return; + } + + playerBody.simulated = enabled; + playerCollider.enabled = enabled; + playerBody.linearVelocity = Vector2.zero; + platformHorizontalVelocity = 0f; + velocity = Vector2.zero; + if (!enabled && mainCamera != null) + { + mainCamera.transform.position = new Vector3(0f, 0f, -10f); + ApplyCameraZoom(); + } + } + + private void SkipCurrentFloorForDebug() + { + if (HasEndingReport) + { + return; + } + + pendingAdvanceAt = -1f; + resultPanel.gameObject.SetActive(false); + SaveCurrentQuestChecklistScore("skip"); + if (floorController.CurrentFloorIndex < floorController.FloorCount - 1) + { + LoadFloor(floorController.CurrentFloorIndex + 1, saveCurrentQuestScore: false); + return; + } + + ShowEndingReport(); + } + + private void TickCustomReferenceShelf() + { + if (customReferenceBubble == null || player == null || floorController == null) + { + return; + } + + var shelfPosition = CurrentCustomReferencePosition(); + var interactionPosition = CurrentCustomReferenceInteractionPosition(); + var hasReferences = CurrentCustomShapeReferences().Count > 0; + var closeToShelf = hasReferences && Vector2.Distance(player.position, interactionPosition) <= CustomReferenceShelfRadius; + var shouldShowBubble = closeToShelf && + !IsPlayerNearActiveGoalSymbol(CustomReferenceGoalSuppressRadius) && + !IsCustomReferencePanelOpenForTests && + customShapeBook?.BlocksGameplayInput != true && + !HasEndingReport; + customReferenceBubble.gameObject.SetActive(shouldShowBubble); + if (!shouldShowBubble) + { + return; + } + + mentor?.HideSpeech(); + if (notePanel != null) + { + notePanel.gameObject.SetActive(false); + } + + customReferenceBubble.SetAsLastSibling(); + customReferenceBubble.anchoredPosition = WorldToCanvasPosition(shelfPosition + CustomReferenceBubbleShelfOffset); } private bool IsPlayerNearActiveGoalSymbol(float radius) @@ -2123,20 +3414,132 @@ private Vector2 CurrentCustomReferenceInteractionPosition() return WestBookcasePosition; } - return new Vector2(activeStageDefinition.customReferencePosition.x, activeStageDefinition.playerStart.y + 0.08f); + return CurrentStageShelfApproachTargetPosition(); + } + + private Vector2 CurrentStageShelfApproachTargetPosition() + { + if (activeStageDefinition == null) + { + return WestBookcasePosition; + } + + var minY = CurrentStageShelfApproachMinY(); + var maxY = activeStageDefinition.stageMax.y - 0.5f; + return StageShelfApproachTargetPosition(activeStageDefinition, minY, maxY); + } + + private float CurrentStageShelfApproachMinY() + { + return activeStageDefinition == null ? WestBookcasePosition.y : StageShelfApproachMinY(activeStageDefinition); + } + + private static float StageShelfApproachMinY(FloorStageDefinition definition) + { + return definition.playerStart.y + 0.08f; + } + + private static Vector2 StageShelfApproachTargetPosition(FloorStageDefinition definition, float minY, float maxY) + { + return new Vector2( + definition.customReferencePosition.x, + Mathf.Clamp(definition.customReferencePosition.y + StageShelfApproachTargetYOffset, minY, maxY)); + } + + private bool IsStageShelfApproachZone(Vector2 position) + { + if (activeStageDefinition == null) + { + return false; + } + + var minY = CurrentStageShelfApproachMinY() - 0.35f; + var maxY = CurrentStageShelfApproachTargetPosition().y + 0.35f; + return Mathf.Abs(position.x - activeStageDefinition.customReferencePosition.x) <= StageShelfApproachHalfWidth && + position.y >= minY && + position.y <= maxY; } private IReadOnlyList CurrentCustomShapeReferences() { return floorController?.Current.number switch { - 2 when CustomShapesAvailableOnCurrentFloor => FloorTwoCustomShapeReferences, + 2 when CustomShapesAvailableOnCurrentFloor => CurrentSecondFloorCustomShapeReferences(), 3 => FloorThreeCustomShapeReferences, 4 => FloorFourCustomShapeReferences, + 5 => CumulativeCustomShapeReferences(), _ => Array.Empty() }; } + private IReadOnlyList CurrentSecondFloorCustomShapeReferences() + { + var current = CurrentSecondFloorSequenceGoal(); + if (current?.requiredBase.HasValue != true) + { + return Array.Empty(); + } + + var reference = FloorTwoCustomShapeReferences.FirstOrDefault(item => item.family == current.requiredBase.Value); + return reference == null ? Array.Empty() : new[] { reference }; + } + + private IReadOnlyList TestCustomShapeReferencesForCurrentFloor() + { + return floorController?.Current.number switch + { + 2 => FloorTwoCustomShapeReferences, + 3 => FloorThreeCustomShapeReferences, + 4 => FloorFourCustomShapeReferences, + 5 => CurrentFinalReferencesFirst(), + _ => CurrentCustomShapeReferences() + }; + } + + private IReadOnlyList ImportLookupCustomShapeReferences() + { + return floorController?.Current.number == 5 + ? CurrentFinalReferencesFirst() + : CurrentCustomShapeReferences(); + } + + private IReadOnlyList CurrentFinalReferencesFirst() + { + var ordered = new List(); + if (currentFinalTask?.references != null) + { + ordered.AddRange(currentFinalTask.references); + } + + ordered.AddRange(CumulativeCustomShapeReferences()); + return DistinctCustomShapeReferences(ordered); + } + + private static IReadOnlyList CumulativeCustomShapeReferences() + { + return DistinctCustomShapeReferences( + FloorTwoCustomShapeReferences + .Concat(FloorThreeCustomShapeReferences) + .Concat(FloorFourCustomShapeReferences)); + } + + private static IReadOnlyList DistinctCustomShapeReferences(IEnumerable references) + { + var result = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var reference in references ?? Array.Empty()) + { + if (reference == null || !seen.Add(ReferenceKey(reference))) + { + continue; + } + + result.Add(reference); + } + + return result; + } + private void OpenCustomReferencePanel() { if (customReferencePanel == null) @@ -2167,7 +3570,18 @@ private void OpenCustomReferencePanel() } var floorNumber = floorController?.Current.number ?? 0; - SetCustomReferenceStatus($"{floorNumber}층 책장의 프리셋만 표시됩니다. 필요한 도형을 빈 슬롯으로 들여오세요."); + if (floorNumber == 2 && CurrentSecondFloorSequenceGoal() is { } currentSecondFloorGoal) + { + SetCustomReferenceStatus($"2층 도형함: {currentSecondFloorGoal.title}에 쓸 도형은 시험관이 다가와 슬롯에 넣어 두었습니다."); + return; + } + + var status = floorNumber == 4 + ? "4층 도형함: 화살표 도형은 시험관이 다가와 넣어 두었습니다. 속성 문양 위에 이어 그려 허수아비를 맞히세요." + : floorNumber == 5 + ? "5층 도형함: 지금까지 배운 도형 전체입니다. 현재 문제에 필요한 카드를 우선 확인하세요." + : $"{floorNumber}층 도형함: 시험관이 다가와 넣어 둔 도형을 확인하고 필요한 슬롯으로 다시 들여올 수 있습니다."; + SetCustomReferenceStatus(status); } private void CloseCustomReferencePanel() @@ -2198,9 +3612,10 @@ private void RebuildCustomReferenceCards() customReferenceCards.Clear(); var references = CurrentCustomShapeReferences(); + var compact = references.Count > 6; for (var index = 0; index < references.Count; index++) { - CreateCustomReferenceCard(references[index], index); + CreateCustomReferenceCard(references[index], index, compact); } if (customReferenceStatus != null) @@ -2222,7 +3637,7 @@ private void EnsurePlayerPhysics() playerBody = player.gameObject.AddComponent(); } playerBody.bodyType = RigidbodyType2D.Dynamic; - playerBody.gravityScale = 3.25f; + playerBody.gravityScale = PlatformGravityScale; playerBody.freezeRotation = true; playerBody.interpolation = RigidbodyInterpolation2D.Interpolate; playerBody.collisionDetectionMode = CollisionDetectionMode2D.Continuous; @@ -2255,7 +3670,11 @@ private void CloseCustomShapeUiForFloorGate() CloseCustomReferenceUi(); } - private bool ImportCustomReference(CustomShapeReferenceDefinition reference, out int slotIndex, out string message) + private bool ImportCustomReference( + CustomShapeReferenceDefinition reference, + out int slotIndex, + out string message, + bool forceReplaceIfFull = false) { slotIndex = -1; if (!CustomShapesAvailableOnCurrentFloor) @@ -2272,7 +3691,15 @@ private bool ImportCustomReference(CustomShapeReferenceDefinition reference, out return false; } - var replacingExistingFamilySlot = false; + if (IsSecondFloorSequence && CurrentSecondFloorSequenceGoal() is { } currentGoal && + (!currentGoal.requiredBase.HasValue || currentGoal.requiredBase.Value != reference.family)) + { + message = $"지금은 {currentGoal.title} 도형부터 가져오세요."; + SetCustomReferenceStatus(message); + return false; + } + + var replacingExistingReferenceSlot = false; for (var index = 0; index < CustomShapeProfileStore.SlotCount; index++) { if (!customShapeStore.IsSlotOccupied(index)) @@ -2280,13 +3707,13 @@ private bool ImportCustomReference(CustomShapeReferenceDefinition reference, out continue; } - if (customShapeStore.GetSlot(index).mappedFamily != reference.family) + if (!SlotMatchesReference(customShapeStore.GetSlot(index), reference)) { continue; } slotIndex = index; - replacingExistingFamilySlot = true; + replacingExistingReferenceSlot = true; break; } @@ -2302,9 +3729,14 @@ private bool ImportCustomReference(CustomShapeReferenceDefinition reference, out } } + if (slotIndex < 0 && forceReplaceIfFull) + { + slotIndex = SelectCustomReferenceReplacementSlot(); + } + if (slotIndex < 0) { - message = "빈 커스텀 슬롯이 필요합니다."; + message = "빈 도형 슬롯이 필요합니다."; SetCustomReferenceStatus(message); return false; } @@ -2325,7 +3757,7 @@ private bool ImportCustomReference(CustomShapeReferenceDefinition reference, out customShapeBook?.RefreshFromStoreForExternalChange(); questImportedReferenceIdsThisFloor.Add($"{floorController.Current.number}:{reference.family}:{reference.shapeToken}"); TickQuestChecklist(forceRefresh: true); - message = replacingExistingFamilySlot + message = replacingExistingReferenceSlot ? $"{ReferenceShapeTitle(reference)} 도형으로 슬롯 {slotIndex + 1:00}을 갱신했습니다." : $"{ReferenceShapeTitle(reference)} 도형을 슬롯 {slotIndex + 1:00}에 가져왔습니다."; } @@ -2334,6 +3766,37 @@ private bool ImportCustomReference(CustomShapeReferenceDefinition reference, out return saved; } + private int SelectCustomReferenceReplacementSlot() + { + if (customShapeStore == null || CustomShapeProfileStore.SlotCount <= 0) + { + return -1; + } + + var protectedKeys = IsFinalFloor && currentFinalTask?.references != null + ? currentFinalTask.references + .Select(ReferenceKey) + .Where(key => !string.IsNullOrWhiteSpace(key)) + .ToHashSet(StringComparer.OrdinalIgnoreCase) + : new HashSet(StringComparer.OrdinalIgnoreCase); + + for (var index = 0; index < CustomShapeProfileStore.SlotCount; index++) + { + var slot = customShapeStore.GetSlot(index); + if (slot == null || !slot.IsOccupied) + { + return index; + } + + if (!protectedKeys.Contains(SlotReferenceKey(slot))) + { + return index; + } + } + + return 0; + } + private void SetCustomReferenceStatus(string message) { customReferenceLastStatus = message ?? ""; @@ -2416,10 +3879,19 @@ private void BuildFloorArt(FloorDefinition floor) CreateWorldSprite("Center Runner", new Vector2(0f, 0.12f), Vector3.one, floor.rugColor, floor.accentColor, PixelSpriteKind.Rug, -5, true, new Vector2(2.2f, 7.6f), floorRoot.transform); CreateWorldSprite("West Bookcase", WestBookcasePosition, Vector3.one * 1.15f, new Color(0.50f, 0.30f, 0.17f), floor.accentColor, PixelSpriteKind.Bookshelf, -1, false, Vector2.one, floorRoot.transform); CreateWorldSprite("East Bookcase", new Vector2(7.25f, 1.1f), Vector3.one * 1.15f, new Color(0.50f, 0.30f, 0.17f), floor.accentColor, PixelSpriteKind.Bookshelf, -1, false, Vector2.one, floorRoot.transform); - if (floor.number == CustomReferenceFloorNumber) + if (floor.number == 4) { - CreateBookcaseGuideArrow("West Bookcase Guide Arrow", WestBookcasePosition, floor.accentColor, emphasized: true, floorRoot.transform); - CreateBookcaseGuideArrow("East Bookcase Guide Arrow", new Vector2(7.25f, 1.1f), floor.accentColor, emphasized: false, floorRoot.transform); + CreateWorldSprite( + "Floor 4 Custom Shape Arrow Glyph", + WestBookcasePosition + new Vector2(0.06f, 0.82f), + Vector3.one * 0.34f, + Color.Lerp(floor.accentColor, Color.white, 0.48f), + Color.white, + PixelSpriteKind.ShapeArrow, + 8, + false, + Vector2.one, + floorRoot.transform); } var northwestCandle = CreateWorldSprite("Northwest Candle", new Vector2(-6.85f, 3.65f), Vector3.one * 0.85f, new Color(0.63f, 0.57f, 0.44f), new Color(1f, 0.56f, 0.15f), PixelSpriteKind.Candle, 2, false, Vector2.one, floorRoot.transform); var northeastCandle = CreateWorldSprite("Northeast Candle", new Vector2(6.85f, 3.65f), Vector3.one * 0.85f, new Color(0.63f, 0.57f, 0.44f), new Color(1f, 0.56f, 0.15f), PixelSpriteKind.Candle, 2, false, Vector2.one, floorRoot.transform); @@ -2434,7 +3906,11 @@ private void BuildFloorArt(FloorDefinition floor) } else if (floor.number == 4) { - BuildFloorFourCombatArt(floorRoot.transform); + BuildFloorFourScarecrowArt(floorRoot.transform); + } + else if (floor.number == 5) + { + BuildFloorFiveFinalTaskEnvironmentArt(floorRoot.transform); } var goalIndex = 0; @@ -2447,11 +3923,14 @@ private void BuildFloorArt(FloorDefinition floor) { body.transform.localScale *= 1.45f; } - goal.label = CreateGoalLabel(goal, floorRoot.transform); + goal.label = CreateGoalLabel(goal, floorRoot.transform, goalIndex); RegisterSpriteAccent(body, SpriteAccentAnimationKind.RuneIdle, goalIndex * 0.53f); goalIndex++; } RefreshFirstFloorTutorialGoalVisibility(); + RefreshSecondFloorSequenceGoalVisibility(); + RefreshSequentialGoalGuide(); + RefreshFinalExamGoalVisibility(); foreach (var hazard in activeHazards) { @@ -2523,12 +4002,89 @@ private void TickShelfGuideArrows() } } - private void TickWorldEffectObjects() + private void RefreshSequentialGoalGuide() { - TickWorldEffectObjects(stageEffectObjects); - TickWorldEffectObjects(customEventObjects); - } - + var current = CurrentSequentialGoalGuideGoal(); + if (current == null || current.completed || current.body == null) + { + ClearSequentialGoalGuides(); + return; + } + + if (string.Equals(sequentialGoalGuideGoalId, current.id, StringComparison.OrdinalIgnoreCase) && + sequentialGoalGuides.Any(guide => guide.IsActive)) + { + return; + } + + ClearSequentialGoalGuides(); + var parent = current.body.transform.parent; + var guideColor = Color.Lerp(current.color, Color.white, 0.42f); + guideColor.a = 1f; + var ringScale = Mathf.Max(0.78f, current.visualScale * SequentialGoalGuideRingScaleMultiplier); + var ring = CreateWorldSprite( + $"Next Symbol Guide Ring {current.id}", + current.position, + Vector3.one * ringScale, + new Color(guideColor.r, guideColor.g, guideColor.b, 0.88f), + Color.white, + PixelSpriteKind.Pulse, + 12, + false, + Vector2.one, + parent); + var arrowAnchor = current.position + new Vector2(0f, Mathf.Max(0.78f, current.visualScale * 0.68f)); + var arrow = CreateWorldSprite( + $"Next Symbol Guide Arrow {current.id}", + arrowAnchor, + Vector3.one * SequentialGoalGuideArrowScale, + new Color(1f, 0.82f, 0.22f, 1f), + guideColor, + PixelSpriteKind.GuideArrow, + 13, + false, + Vector2.one, + parent); + sequentialGoalGuides.Add(new SequentialGoalGuide(current.id, current.position, arrowAnchor, arrow, ring, SequentialGoalGuideArrowScale, ringScale)); + sequentialGoalGuideGoalId = current.id; + } + + private void TickSequentialGoalGuides() + { + for (var index = sequentialGoalGuides.Count - 1; index >= 0; index--) + { + var guide = sequentialGoalGuides[index]; + if (!guide.IsActive) + { + sequentialGoalGuides.RemoveAt(index); + if (sequentialGoalGuides.Count == 0) + { + sequentialGoalGuideGoalId = ""; + } + continue; + } + + guide.Tick(Time.time, Time.deltaTime); + } + } + + private void ClearSequentialGoalGuides() + { + foreach (var guide in sequentialGoalGuides) + { + guide.Destroy(); + } + + sequentialGoalGuides.Clear(); + sequentialGoalGuideGoalId = ""; + } + + private void TickWorldEffectObjects() + { + TickWorldEffectObjects(stageEffectObjects); + TickWorldEffectObjects(customEventObjects); + } + private void TickWorldEffectObjects(List effects) { for (var index = effects.Count - 1; index >= 0; index--) @@ -2591,23 +4147,145 @@ private void RefreshFirstFloorTutorialGoalVisibility() var orderedGoals = FirstFloorTutorialGoalsInOrder(); var current = orderedGoals.FirstOrDefault(goal => !goal.completed && goal.requiredBase.HasValue); - var hasCompletedAny = orderedGoals.Any(goal => goal.completed); foreach (var goal in orderedGoals) { var visible = goal.completed || ReferenceEquals(goal, current); var alpha = goal.completed ? FirstFloorSequentialCompletedGoalAlpha - : hasCompletedAny ? FirstFloorSequentialFutureGoalAlpha : 1f; + : ReferenceEquals(goal, current) ? FirstFloorSequentialCurrentGoalAlpha : FirstFloorSequentialFutureGoalAlpha; + SetGoalVisualState(goal, visible, alpha); + } + } + + private void RefreshSecondFloorSequenceGoalVisibility() + { + if (!IsSecondFloorSequence || activeGoals.Count == 0) + { + return; + } + + var orderedGoals = SecondFloorSequenceGoalsInOrder(); + var current = orderedGoals.FirstOrDefault(goal => !goal.completed && goal.requiredBase.HasValue); + foreach (var goal in orderedGoals) + { + var visible = goal.completed || ReferenceEquals(goal, current); + var alpha = goal.completed + ? FirstFloorSequentialCompletedGoalAlpha + : ReferenceEquals(goal, current) ? FirstFloorSequentialCurrentGoalAlpha : FirstFloorSequentialFutureGoalAlpha; SetGoalVisualState(goal, visible, alpha); } } + private void RefreshFinalExamGoalVisibility() + { + if (!IsFinalFloor) + { + return; + } + + RefreshFinalTaskEnvironmentVisibility(); + + if (currentFinalTasks.Count == 0 || activeGoals.Count == 0) + { + return; + } + + foreach (var goal in activeGoals) + { + SetFinalGoalAssistVisualState(goal); + } + } + + private void RefreshFinalTaskEnvironmentVisibility() + { + if (finalTaskEnvironmentObjects.Count == 0) + { + return; + } + + var activeTaskId = currentFinalTask?.id ?? ""; + var activeObjects = new HashSet(); + foreach (var entry in finalTaskEnvironmentObjects) + { + if (!string.Equals(entry.Key, activeTaskId, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + foreach (var body in entry.Value) + { + if (body != null) + { + activeObjects.Add(body); + } + } + } + + foreach (var body in finalTaskEnvironmentObjects.Values.SelectMany(objects => objects).Distinct()) + { + if (body != null) + { + body.SetActive(activeObjects.Contains(body)); + } + } + } + private static bool IsGoalVisible(WorldStateGoal goal) { return goal?.body != null && goal.body.activeInHierarchy && goal.renderer != null && - goal.renderer.enabled; + goal.renderer.enabled && + goal.renderer.color.a > 0.01f; + } + + private static bool IsGoalTransparentAssist(WorldStateGoal goal) + { + return goal?.body != null && + goal.body.activeInHierarchy && + goal.renderer != null && + goal.renderer.enabled && + goal.renderer.color.a <= 0.01f; + } + + private static void SetFinalGoalAssistVisualState(WorldStateGoal goal) + { + if (goal == null) + { + return; + } + + if (goal.body != null) + { + goal.body.SetActive(true); + } + + if (goal.renderer != null) + { + goal.renderer.enabled = true; + var color = goal.renderer.color; + color.a = FinalFloorAssistGoalAlpha; + goal.renderer.color = color; + } + + if (goal.label == null) + { + return; + } + + var labelRoot = goal.label.transform.parent == null + ? goal.label.gameObject + : goal.label.transform.parent.gameObject; + var group = labelRoot.GetComponent(); + if (group != null) + { + group.alpha = 0f; + } + + var labelColor = goal.label.color; + labelColor.a = 0f; + goal.label.color = labelColor; + labelRoot.SetActive(false); } private static void SetGoalVisualState(WorldStateGoal goal, bool visible, float alpha) @@ -2658,12 +4336,49 @@ private void CreateStageBackdrop(FloorStageDefinition definition, Transform pare CreateWorldSprite("Crossing South Trim", new Vector2(center.x, definition.stageMin.y + 0.22f), Vector3.one, new Color(0.16f, 0.14f, 0.13f), new Color(0.45f, 0.36f, 0.22f), PixelSpriteKind.WallTrim, -2, true, new Vector2(size.x, 0.42f), parent); CreateStageRouteBoundaryCues(definition, parent); CreateWorldSprite("Crossing Reference Bookcase", definition.customReferencePosition, Vector3.one * 1.15f, new Color(0.42f, 0.23f, 0.12f), floorController.Current.accentColor, PixelSpriteKind.Bookshelf, 2, false, Vector2.one, parent); - CreateBookcaseGuideArrow("Crossing Reference Bookcase Guide Arrow", definition.customReferencePosition, floorController.Current.accentColor, emphasized: true, parent); + CreateStageShelfApproachGuide(definition, parent); var crossingTorch = CreateWorldSprite("Crossing West Torch", definition.customReferencePosition + new Vector2(1.6f, 1.1f), Vector3.one * 0.78f, new Color(0.63f, 0.57f, 0.44f), new Color(1f, 0.56f, 0.15f), PixelSpriteKind.Candle, 4, false, Vector2.one, parent); CreateTorchLightSpread("Crossing West Torch", definition.customReferencePosition + new Vector2(1.6f, 1.1f), 3.45f, parent, 1.72f); RegisterSpriteAccent(crossingTorch, SpriteAccentAnimationKind.CandleFlicker, 1.72f); } + private void CreateStageShelfApproachGuide(FloorStageDefinition definition, Transform parent) + { + var minY = StageShelfApproachMinY(definition); + var maxY = definition.stageMax.y - 0.5f; + var target = StageShelfApproachTargetPosition(definition, minY, maxY); + var x = definition.customReferencePosition.x; + var height = Mathf.Max(0.5f, target.y - minY); + var center = new Vector2(x, (target.y + minY) * 0.5f); + var laneColor = new Color(0.38f, 0.86f, 1f, 0.74f); + var edgeColor = new Color(1f, 0.82f, 0.28f, 0.70f); + + var rail = CreateWorldSprite("Stage Shelf Vertical Guide Rail", center, Vector3.one, laneColor, Color.white, PixelSpriteKind.WindPlatformTile, 1, true, new Vector2(0.28f, height), parent); + RegisterSpriteAccent(rail, SpriteAccentAnimationKind.MistDrift, 0.38f); + + var leftBoundary = CreateWorldSprite("Stage Shelf Vertical Guide Left Boundary", center + new Vector2(-StageShelfApproachHalfWidth, 0f), Vector3.one, edgeColor, laneColor, PixelSpriteKind.WallTrim, 2, true, new Vector2(0.08f, height), parent); + var rightBoundary = CreateWorldSprite("Stage Shelf Vertical Guide Right Boundary", center + new Vector2(StageShelfApproachHalfWidth, 0f), Vector3.one, edgeColor, laneColor, PixelSpriteKind.WallTrim, 2, true, new Vector2(0.08f, height), parent); + RegisterSpriteAccent(leftBoundary, SpriteAccentAnimationKind.StageEffectGlow, 0.62f); + RegisterSpriteAccent(rightBoundary, SpriteAccentAnimationKind.StageEffectGlow, 1.12f); + + const int rungCount = 5; + for (var index = 0; index < rungCount; index++) + { + var t = rungCount == 1 ? 0.5f : index / (rungCount - 1f); + var y = Mathf.Lerp(minY + 0.24f, target.y - 0.16f, t); + var rung = CreateWorldSprite($"Stage Shelf Vertical Guide Rung {index + 1}", new Vector2(x, y), Vector3.one, edgeColor, Color.white, PixelSpriteKind.WallTrim, 3, true, new Vector2(StageShelfApproachHalfWidth * 1.36f, 0.07f), parent); + RegisterSpriteAccent(rung, SpriteAccentAnimationKind.StageEffectGlow, index * 0.27f); + } + + var upArrow = CreateWorldSprite("Stage Shelf Vertical Guide Up Arrow", target + new Vector2(0.52f, 0.34f), Vector3.one * 0.46f, edgeColor, laneColor, PixelSpriteKind.GuideArrow, 8, false, Vector2.one, parent); + upArrow.transform.rotation = Quaternion.Euler(0f, 0f, 45f); + RegisterSpriteAccent(upArrow, SpriteAccentAnimationKind.StageEffectGlow, 1.58f); + + var downArrow = CreateWorldSprite("Stage Shelf Vertical Guide Down Arrow", new Vector2(x + 0.52f, minY + 0.38f), Vector3.one * 0.42f, laneColor, edgeColor, PixelSpriteKind.GuideArrow, 8, false, Vector2.one, parent); + downArrow.transform.rotation = Quaternion.Euler(0f, 0f, 225f); + RegisterSpriteAccent(downArrow, SpriteAccentAnimationKind.StageEffectGlow, 2.06f); + } + private GameObject CreateTorchLightSpread(string name, Vector2 position, float radius, Transform parent, float phase) { var glow = CreateWorldSprite( @@ -2931,25 +4646,138 @@ private void CreateStageRim(string name, Vector2 center, Vector2 size, Color col CreateWorldSprite($"{name} East", center + new Vector2(size.x * 0.5f, 0f), Vector3.one, rimColor, glowColor, PixelSpriteKind.WallTrim, -1, true, new Vector2(0.10f, size.y), parent); } - private void BuildFloorFourCombatArt(Transform parent) + private void BuildFloorFourScarecrowArt(Transform parent) { + var target = CreateWorldSprite( + "Training Scarecrow", + Vector2.zero, + Vector3.one * 1.45f, + new Color(0.70f, 0.48f, 0.25f), + new Color(1f, 0.78f, 0.38f), + PixelSpriteKind.Scarecrow, + 2, + false, + Vector2.one, + parent); + RegisterSpriteAccent(target, SpriteAccentAnimationKind.RuneIdle, 0.37f); + foreach (var goal in activeGoals) { - var dummy = CreateWorldSprite( - $"Training Target {goal.id}", - goal.position + new Vector2(0f, -0.42f), - Vector3.one * 0.78f, - new Color(0.46f, 0.42f, 0.34f), - goal.color, - PixelSpriteKind.Target, - 1, - false, - Vector2.one, - parent); - var renderer = dummy.GetComponent(); - renderer.color = Color.Lerp(goal.color, Color.white, 0.34f); - goal.entityBody = dummy; + goal.entityBody = target; + } + } + + private void BuildFloorFiveFinalBeamArt(Transform parent) + { + var target = CreateWorldSprite( + "Final Exam Scarecrow", + Vector2.zero, + Vector3.one * 1.35f, + new Color(0.66f, 0.46f, 0.24f), + floorController.Current.accentColor, + PixelSpriteKind.Scarecrow, + 2, + false, + Vector2.one, + parent); + RegisterSpriteAccent(target, SpriteAccentAnimationKind.RuneIdle, 0.49f); + RegisterFinalTaskEnvironmentObject(FinalBeamFireTaskId, target); + RegisterFinalTaskEnvironmentObject(FinalBeamWaterTaskId, target); + + foreach (var goal in activeGoals.Where(goal => goal.RequiresBeamHit)) + { + goal.entityBody = target; + } + } + + private void BuildFloorFiveFinalTaskEnvironmentArt(Transform parent) + { + if (currentFinalTasks.Any(task => string.Equals(task.id, FinalFrozenRiverTaskId, StringComparison.OrdinalIgnoreCase))) + { + BuildFloorFiveFrozenRiverArt(parent); + } + + if (currentFinalTasks.Any(task => task.RequiresBeamTarget)) + { + BuildFloorFiveFinalBeamArt(parent); + } + } + + private void BuildFloorFiveFrozenRiverArt(Transform parent) + { + var current = CreateWorldSprite( + "Final Frozen River Current", + new Vector2(0f, 1.72f), + new Vector3(5.9f, 1.05f, 1f), + new Color(0.10f, 0.35f, 0.62f, 0.92f), + new Color(0.58f, 0.90f, 1f, 0.86f), + PixelSpriteKind.WaterHazard, + -2, + false, + Vector2.one, + parent); + RegisterFinalTaskEnvironmentObject(FinalFrozenRiverTaskId, current); + RegisterSpriteAccent(current, SpriteAccentAnimationKind.WaterFlow, 0.27f); + + var northBank = CreateWorldSprite( + "Final Frozen River North Bank", + new Vector2(0f, 2.34f), + new Vector3(6.2f, 0.18f, 1f), + new Color(0.22f, 0.30f, 0.30f, 0.94f), + new Color(0.52f, 0.82f, 0.92f, 0.82f), + PixelSpriteKind.WallTrim, + -1, + false, + Vector2.one, + parent); + RegisterFinalTaskEnvironmentObject(FinalFrozenRiverTaskId, northBank); + + var southBank = CreateWorldSprite( + "Final Frozen River South Bank", + new Vector2(0f, 1.10f), + new Vector3(6.2f, 0.18f, 1f), + new Color(0.18f, 0.25f, 0.27f, 0.94f), + new Color(0.52f, 0.82f, 0.92f, 0.82f), + PixelSpriteKind.WallTrim, + -1, + false, + Vector2.one, + parent); + RegisterFinalTaskEnvironmentObject(FinalFrozenRiverTaskId, southBank); + + var mist = CreateWorldSprite( + "Final Frozen River Mist", + new Vector2(0.4f, 1.76f), + new Vector3(2.7f, 0.45f, 1f), + new Color(0.64f, 0.92f, 1f, 0.44f), + Color.white, + PixelSpriteKind.Pulse, + 1, + false, + Vector2.one, + parent); + RegisterFinalTaskEnvironmentObject(FinalFrozenRiverTaskId, mist); + RegisterSpriteAccent(mist, SpriteAccentAnimationKind.MistDrift, 0.68f); + } + + private void RegisterFinalTaskEnvironmentObject(string taskId, GameObject body) + { + if (string.IsNullOrWhiteSpace(taskId) || body == null) + { + return; + } + + if (!finalTaskEnvironmentObjects.TryGetValue(taskId, out var objects)) + { + objects = new List(); + finalTaskEnvironmentObjects[taskId] = objects; + } + + if (!objects.Contains(body)) + { + objects.Add(body); } + body.SetActive(false); } private void OnSpellBuffered(List> strokes, Vector2 center, int strokeCount) @@ -2979,86 +4807,285 @@ private void OnDrawingCancelled() } resultPanel.gameObject.SetActive(false); - ShowMagicNote("입력을 취소했습니다. 우클릭 hold로 다시 그리세요.", MentorMood.Frown); + LogActionEvent("drawing_cancelled", "player", "input"); + ShowMagicNote("입력을 취소했습니다. 우클릭을 누르고 다시 그려 주세요.", MentorMood.Frown); } private void ShowMagicNote(string text, MentorMood mentorMood) { - magicNote.Show(text, InferNoteCategory(text), CurrentFloorNumber); - mentor?.Say(mentorMood, text); + var category = InferNoteCategory(text); + magicNote.Show(text, category, CurrentFloorNumber); + mentor?.Say(mentorMood, BuildMentorBubbleText(text)); if (!string.IsNullOrWhiteSpace(text)) { + LogActionEvent( + "mentor_note_shown", + "mentor", + "feedback", + value: mentorMood.ToString(), + payloadJson: BuildEventPayload( + Pair("category", category.ToString()), + Pair("text", TruncateForLog(text.Replace("\r", "", StringComparison.Ordinal), 600)))); audioDirector?.PlaySfx(AudioCue.NoteUnlock, 0.42f); audioDirector?.PlaySfx(AudioCue.NpcAppear, mentorMood == MentorMood.Neutral ? 0.18f : 0.28f); } } - private static MagicNoteCategory InferNoteCategory(string text) + private string BuildMentorBubbleText(string text) { if (string.IsNullOrWhiteSpace(text)) { - return MagicNoteCategory.Discovery; + return ""; } - if (text.StartsWith("노트:", StringComparison.Ordinal) || - text.Contains("다음 목표", StringComparison.Ordinal) || - text.Contains("목표:", StringComparison.Ordinal) || - text.Contains("완료", StringComparison.Ordinal) || - text.Contains("통과", StringComparison.Ordinal)) + var floor = floorController?.Current; + if (floor != null) { - return MagicNoteCategory.FloorNote; + if (ContainsText(text, floor.entryNote) && ContainsText(text, FloorEntryLore(floor.number))) + { + return FloorEntryBubbleLine(floor.number); + } + + if (ContainsText(text, floor.completeNote) || + (floor.number == FloorCount && (ContainsText(text, "최종 시험 통과") || ContainsText(text, "수료증") || ContainsText(text, "입학 시험 통과")))) + { + return FloorCompletionBubbleLine(floor.number); + } } - return MagicNoteCategory.Discovery; + return PolishMentorBubbleText(text); } - private void ShowToast(string message, Color accent, bool strong = false) + private string FloorEntryBubbleLine(int floorNumber) { - if (toastPanel == null || toastText == null) + if (floorNumber == 4) { - return; + return "허수아비를 직접 맞히세요.\n빛나는 원 안에 화살표를 더해 발사하세요."; } - toastText.text = message ?? ""; - toastText.color = Color.Lerp(accent, Color.white, 0.34f); - if (toastAccent != null) + if (floorNumber == FloorCount) { - toastAccent.color = accent; + return currentFinalTask == null + ? "마지막 시험은 세 문제입니다.\n위의 문제부터 차례대로 풀어 주세요." + : $"최종 문제 {currentFinalTaskIndex + 1}/{Mathf.Max(currentFinalTasks.Count, 1)}.\n{FinalTaskBubblePrompt(currentFinalTask.prompt)}"; } - if (toastBackground != null) + return floorNumber switch { - toastBackground.color = strong - ? Color.Lerp(new Color(0.018f, 0.024f, 0.038f, 0.96f), accent, 0.18f) - : new Color(0.018f, 0.024f, 0.038f, 0.94f); - } + 1 => "탑의 첫 문양이 흐릿합니다.\n우클릭을 누르고 선을 따라 그려 주세요.", + 2 => "벽화가 새 장식을 기다립니다.\n필요한 도형은 슬롯에 넣었습니다.", + 3 => "끊긴 길이 발밑을 시험합니다.\n빛나는 원 안에 도형을 이어 그려 주세요.", + 4 => "허수아비를 직접 맞히세요.\n빛나는 원 안에 화살표를 더해 발사하세요.", + 5 => "마지막 시험은 세 문제입니다.\n위의 문제부터 차례대로 풀어 주세요.", + _ => PolishMentorBubbleText(floorController?.Current?.entryNote ?? "") + }; + } - toastTtl = strong ? 2.35f : 1.65f; - toastPanel.gameObject.SetActive(!string.IsNullOrWhiteSpace(message)); + private static string FinalTaskBubblePrompt(string prompt) + { + var polished = PolishMentorBubbleText(prompt); + return ShortLine(polished.Replace("\n", " "), 54); } - private void TickToast() + private string FloorCompletionBubbleLine(int floorNumber) { - if (toastPanel == null || !toastPanel.gameObject.activeSelf) + if (floorNumber == FloorCount) { - return; + return "마지막 관문을 통과했습니다.\n수료증을 발급하겠습니다."; } - toastTtl -= Time.deltaTime; - if (toastTtl <= 0f) + if (floorNumber == 4) { - toastPanel.gameObject.SetActive(false); + return "허수아비가 모든 빛줄기를 받았습니다.\n마지막 시험관에게 올라가세요."; } + + return floorNumber switch + { + 1 => "첫 표식들이 모두 깨어났습니다.\n탑이 다음 문을 열 준비를 마쳤습니다.", + 2 => "벽화가 장식을 받아들였습니다.\n그 기억으로 위층의 길을 잇겠습니다.", + 3 => "끊긴 길이 다시 이어졌습니다.\n숨을 고르고 다음 층으로 올라가세요.", + 4 => "허수아비가 모든 빛줄기를 받았습니다.\n마지막 시험관에게 올라가세요.", + _ => "탑이 당신의 문양을 기억했습니다.\n숨을 고르고 다음 층으로 올라가세요." + }; } - private ProcessedSpell ProcessSpellGroup(List> strokes, Vector2 center, int strokeCount) + private static string PolishMentorBubbleText(string text) { - var session = StrokeInputSessionExtensions.FromStrokeSamples( - strokes, - $"legacy-{Guid.NewGuid():N}", - Time.time, - InputCoordinateSpace.World); - return ProcessStrokeSession(session); + var lines = text.Replace("\r", "") + .Split('\n') + .Select(PolishMentorBubbleLine) + .Where(line => !string.IsNullOrWhiteSpace(line)) + .ToArray(); + return string.Join("\n", lines); + } + + private static string PolishMentorBubbleLine(string line) + { + var result = (line ?? "").Trim(); + if (result.Length == 0) + { + return ""; + } + + result = StripPrefix(result, "노트:"); + result = RewriteNextPrefix(result); + result = result + .Replace("gold capture", "기준 그림") + .Replace("base seal", "기본 문양의 빛나는 원") + .Replace("base 문양", "기본 문양") + .Replace("base", "기본 문양") + .Replace("seal", "빛나는 원") + .Replace("overlay stack", "장식 반응") + .Replace("overlay", "장식") + .Replace("entity", "대상") + .Replace("우클릭 hold", "우클릭을 누르고") + .Replace("우클릭 누르고", "우클릭을 누르고") + .Replace("기초 속성 마법진", "빛나는 원") + .Replace("현재 거리", "지금 거리") + .Replace("목표 반경", "가까이 갈 거리") + .Replace("현재 보이는 표식부터 순서대로 수집합니다.", "지금 보이는 표식부터 차례대로 모아 주세요.") + .Replace("표식은 커스텀 도형으로만 반응합니다.", "표식은 가져온 도형으로만 반응합니다.") + .Replace("커스텀 도형", "가져온 도형") + .Replace("표식은 기본 문양만으로는 열리지 않습니다.", "표식은 기본 문양만으로는 열리지 않습니다.") + .Replace("도형을 얹으세요", "도형을 얹어 주세요") + .Replace("다시 그리세요", "다시 그려 주세요") + .Replace("그리세요", "그려 주세요") + .Replace("시도하세요", "시도해 주세요") + .Replace("완성하세요", "완성해 주세요") + .Replace("이동한 뒤", "움직인 뒤") + .Replace("너무 멉니다", "너무 멀리 있습니다") + .Replace("근처가 아닙니다", "근처가 아닙니다"); + return result; + } + + private static string StripPrefix(string text, string prefix) + { + return text.StartsWith(prefix, StringComparison.Ordinal) + ? text[prefix.Length..].TrimStart() + : text; + } + + private static string RewriteNextPrefix(string text) + { + if (!text.StartsWith("다음:", StringComparison.Ordinal)) + { + return text; + } + + var next = text["다음:".Length..].TrimStart(); + return string.IsNullOrWhiteSpace(next) ? "" : $"다음엔 {next}"; + } + + private static bool ContainsText(string text, string value) + { + return !string.IsNullOrWhiteSpace(text) && + !string.IsNullOrWhiteSpace(value) && + text.IndexOf(value, StringComparison.Ordinal) >= 0; + } + + private static MagicNoteCategory InferNoteCategory(string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return MagicNoteCategory.Discovery; + } + + if (text.StartsWith("노트:", StringComparison.Ordinal) || + text.Contains("다음 목표", StringComparison.Ordinal) || + text.Contains("목표:", StringComparison.Ordinal) || + text.Contains("완료", StringComparison.Ordinal) || + text.Contains("통과", StringComparison.Ordinal)) + { + return MagicNoteCategory.FloorNote; + } + + return MagicNoteCategory.Discovery; + } + + private void ShowToast(string message, Color accent, bool strong = false) + { + if (toastPanel == null || toastText == null) + { + return; + } + + toastText.text = message ?? ""; + toastText.color = Color.Lerp(accent, Color.white, 0.34f); + if (toastAccent != null) + { + toastAccent.color = accent; + } + + if (toastBackground != null) + { + toastBackground.color = strong + ? Color.Lerp(new Color(0.018f, 0.024f, 0.038f, 0.96f), accent, 0.18f) + : new Color(0.018f, 0.024f, 0.038f, 0.94f); + } + + toastTtl = strong ? 2.35f : 1.65f; + toastPanel.gameObject.SetActive(!string.IsNullOrWhiteSpace(message)); + if (!string.IsNullOrWhiteSpace(message)) + { + LogActionEvent( + "toast_shown", + "system", + "ui", + value: strong ? "strong" : "normal", + payloadJson: BuildEventPayload(Pair("message", TruncateForLog(message, 300)))); + } + } + + private void TickFinalTaskBanner() + { + if (finalTaskBanner == null || finalTaskBannerText == null) + { + return; + } + + var shouldShow = !finalTaskBannerSuppressedForTitle && + IsFinalFloor && + currentFinalTask != null && + !finalCompletionCelebrated && + !HasEndingReport; + finalTaskBanner.gameObject.SetActive(shouldShow); + if (!shouldShow) + { + return; + } + + var total = Mathf.Max(currentFinalTasks.Count, 1); + finalTaskBannerText.text = $"문제 {currentFinalTaskIndex + 1}/{total} · {ShortLine(currentFinalTask.prompt, 64)}"; + var glow = (Mathf.Sin(Time.time * 3.2f) + 1f) * 0.5f; + finalTaskBannerText.color = Color.Lerp( + new Color(1f, 0.82f, 0.34f, 0.86f), + new Color(1f, 0.97f, 0.68f, 1f), + glow); + finalTaskBanner.SetAsLastSibling(); + } + + private void TickToast() + { + if (toastPanel == null || !toastPanel.gameObject.activeSelf) + { + return; + } + + toastTtl -= Time.deltaTime; + if (toastTtl <= 0f) + { + toastPanel.gameObject.SetActive(false); + } + } + + private ProcessedSpell ProcessSpellGroup(List> strokes, Vector2 center, int strokeCount) + { + var session = StrokeInputSessionExtensions.FromStrokeSamples( + strokes, + $"legacy-{Guid.NewGuid():N}", + Time.time, + InputCoordinateSpace.World); + return ProcessStrokeSession(session); } private ProcessedSpell ProcessStrokeSession(StrokeInputSession session) @@ -3072,12 +5099,29 @@ private ProcessedSpell ProcessStrokeSession(StrokeInputSession session) MarkPostSealInputSeen(now); } - var castCenter = CurrentMagicCastOrigin(session.GetWorldCenter()); + var sessionCenter = session.GetWorldCenter(); + var castCenter = CurrentMagicCastOrigin(sessionCenter); + var sessionPointCount = session.Strokes.Sum(stroke => stroke.Points.Count); + LogActionEvent( + "spell_session_submitted", + "player", + "recognition", + position: castCenter, + inputSessionId: session.Id, + strokePointCount: sessionPointCount, + value: trialCounter.ToString(CultureInfo.InvariantCulture), + payloadJson: BuildEventPayload( + Pair("strokeCount", session.Strokes.Count.ToString(CultureInfo.InvariantCulture)), + Pair("startedAt", session.StartedAtSeconds.ToString("0.###", CultureInfo.InvariantCulture)), + Pair("endedAt", session.EndedAtSeconds.ToString("0.###", CultureInfo.InvariantCulture)), + Pair("hadActiveSeal", hadActiveSeal ? "true" : "false"))); var baseIntent = ResolveBaseIntent(castCenter); + var preferredCustomShapeTokens = PreferredCustomShapeTokensForRecognition(baseIntent); var recognition = recognitionService.Recognize(session, new RecognitionContext { activeSeals = seals.Select(view => view.seal).ToList(), baseIntent = baseIntent, + preferredCustomShapeTokens = preferredCustomShapeTokens, allowCustomShapes = CustomShapesAvailableOnCurrentFloor, customShapesOnlyWhenSealActive = true, hasCastCenter = true, @@ -3085,9 +5129,7 @@ private ProcessedSpell ProcessStrokeSession(StrokeInputSession session) now = now }); LastPersonalizationSummaryForTests = recognition.personalization ?? TutorialPersonalizationSummary.Empty; - if (hadActiveSeal && - !IsCustomShapeBaseGoalInput(recognition) && - TryApplyCustomShapeFollowup(recognition, out var customFollowup)) + if (TryApplyCustomShapeFollowup(recognition, out var customFollowup)) { if (recognition.baseResult?.spell?.success == true) { @@ -3099,7 +5141,10 @@ private ProcessedSpell ProcessStrokeSession(StrokeInputSession session) var outcome = spellCasting.ProcessRecognitionResult(recognition, now); var processed = ApplySpellOutcome(outcome); - if (outcome.kind == SpellCastOutcomeKind.BaseSucceeded || outcome.kind == SpellCastOutcomeKind.OverlaySucceeded) + if (outcome.kind == SpellCastOutcomeKind.BaseSucceeded || + outcome.kind == SpellCastOutcomeKind.OverlaySucceeded || + processed?.baseResult?.spell?.success == true || + processed?.overlayResult?.success == true) { recognitionService.RecordAcceptedResult(recognition, now); } @@ -3107,24 +5152,6 @@ private ProcessedSpell ProcessStrokeSession(StrokeInputSession session) return processed; } - private bool IsCustomShapeBaseGoalInput(StrokeRecognitionResult recognition) - { - if (recognition.kind != StrokeRecognitionKind.Base || - recognition.baseResult?.spell?.isCustomShape != true) - { - return false; - } - - var spell = recognition.baseResult.spell; - var family = spell.recognizedFamily ?? spell.mappedFamily ?? spell.targetFamily; - var castCenter = CurrentMagicCastOrigin(recognition.center); - return activeGoals.Any(goal => - !goal.completed && - goal.requiresCustomShape && - !goal.requiredCustomSpell.HasValue && - goal.MatchesBase(family, castCenter)); - } - private bool TryApplyCustomShapeFollowup(StrokeRecognitionResult recognition, out ProcessedSpell processed) { processed = null; @@ -3139,10 +5166,10 @@ private bool TryApplyCustomShapeFollowup(StrokeRecognitionResult recognition, ou if (sealView == null) { CurrentAssistLevel = 1; - LastHintText = "커스텀 도형은 먼저 만든 기본 문양의 빛나는 원 안에 얹어야 합니다."; + LastHintText = "가져온 도형은 먼저 만든 기본 문양의 빛나는 원 안에 이어 그려야 합니다."; endingReport.RecordHintShown(1); ShowMagicNote(LastHintText, MentorMood.Frown); - ShowBaseResultSummary(recognition.baseResult, "커스텀 부착 실패", LastHintText); + ShowBaseResultSummary(recognition.baseResult, "도형 이어그리기 실패", LastHintText); LogBaseAttempt(recognition.baseResult, null, "custom_followup_detached"); processed = new ProcessedSpell { baseResult = recognition.baseResult }; return true; @@ -3155,21 +5182,41 @@ private bool TryApplyCustomShapeFollowup(StrokeRecognitionResult recognition, ou private ProcessedSpell ApplyCustomShapeFollowup(SealView sealView, BaseRecognitionResult result, Vector2 center) { var seal = sealView.seal; + if (TryApplyAttributeBeamFollowup(sealView, result, center, out var beamProcessed)) + { + return beamProcessed; + } + + if (TryApplyTransformedSealCustomEvent(sealView, result, center, out var transformedEventProcessed)) + { + return transformedEventProcessed; + } + var customEffect = ResolveCustomSpellEffect(seal.baseFamily, result.spell); if (!customEffect.IsValid) { + if (TryApplyPlainCustomShapeGoal(sealView, result, center, out var plainCustomProcessed)) + { + return plainCustomProcessed; + } + CurrentAssistLevel = 1; LastHintText = - $"{SpellLabels.Korean(seal.baseFamily)} 문양 위에서 지금 도형 조합은 특별한 반응을 만들지 못했습니다.\n" + - "표식에 적힌 조합처럼 기본 문양을 먼저 만들고, 그 위에 맞는 커스텀 도형을 얹으세요."; + $"{SpellLabels.Korean(seal.baseFamily)} 기본 문양 위에서 지금 도형 조합은 특별한 반응을 만들지 못했습니다.\n" + + "표식에 적힌 조합처럼 기본 문양을 먼저 만들고, 빛나는 원 안에 맞는 도형을 이어 그리세요."; endingReport.RecordHintShown(1); ShowMagicNote(LastHintText, MentorMood.Frown); - ShowBaseResultSummary(result, "커스텀 반응 실패", LastHintText); + ShowBaseResultSummary(result, "도형 반응 실패", LastHintText); pulses.Add(new ParticlePulse(center, FamilyColor(seal.baseFamily), weak: true)); LogBaseAttempt(result, seal, "custom_effect_unmatched"); return new ProcessedSpell { baseResult = result }; } + if (TryTransformSealWithCustomEffect(sealView, result, center, customEffect, out var transformProcessed)) + { + return transformProcessed; + } + var goalEffect = ApplyCustomSpellToGoals(seal, customEffect, center, result.spell); var eventNote = ApplyCustomShapeEvent(result, seal, center); var elementalNote = ApplyElementalInteractions(seal.baseFamily, result.spell, center, result.spell.customEventDirection, customEffect.displayName); @@ -3192,6 +5239,7 @@ private ProcessedSpell ApplyCustomShapeFollowup(SealView sealView, BaseRecogniti CurrentAssistLevel = 0; LastHintText = ""; ShowMagicNote(note, MentorMood.Happy); + audioDirector?.PlayCustomSpellEffect(customEffect.kind, seal.baseFamily); ShowBaseResultSummary(result, $"{customEffect.displayName} 반응", note); pulses.Add(new ParticlePulse(center, FamilyColor(seal.baseFamily))); LogBaseAttempt(result, seal, $"{goalEffect.worldEffect}|{customEffect.kind}"); @@ -3200,6 +5248,450 @@ private ProcessedSpell ApplyCustomShapeFollowup(SealView sealView, BaseRecogniti return new ProcessedSpell { baseResult = result }; } + private bool TryApplyPlainCustomShapeGoal( + SealView sealView, + BaseRecognitionResult result, + Vector2 center, + out ProcessedSpell processed) + { + processed = null; + var seal = sealView?.seal; + if (seal == null || result?.spell?.isCustomShape != true) + { + return false; + } + + var resolution = floorGoals.ResolveBase( + ResolutionGoalsForCurrentFloor(), + seal.baseFamily, + center, + isCustomShape: true, + customEffect: CustomSpellEffectKind.None); + if (resolution.kind != GoalResolutionKind.Completed) + { + return false; + } + + GoalEffect goalEffect; + if (TryBuildEarlyTutorialSymbolDistanceEffect(resolution.goal, center, seal.baseFamily, out var blockedEffect)) + { + goalEffect = blockedEffect; + } + else + { + ActivateGoal(resolution.goal, "custom_shape", result.spell); + goalEffect = new GoalEffect(BuildGoalDiscoveryNote(resolution.goal), resolution.goal.id); + } + + var eventNote = ApplyCustomShapeEvent(result, seal, center); + var elementalNote = ApplyElementalInteractions( + seal.baseFamily, + result.spell, + center, + result.spell.customEventDirection, + result.spell.customShapeLabel); + var note = $"{SpellLabels.Korean(seal.baseFamily)} 문양에 {result.spell.customShapeLabel} 도형을 이어 그렸습니다."; + if (!string.IsNullOrWhiteSpace(goalEffect.note)) + { + note += $"\n{goalEffect.note}"; + } + + if (!string.IsNullOrWhiteSpace(eventNote)) + { + note += $"\n{eventNote}"; + } + + if (!string.IsNullOrWhiteSpace(elementalNote)) + { + note += $"\n{elementalNote}"; + } + + CurrentAssistLevel = 0; + LastHintText = ""; + ShowMagicNote(note, MentorMood.Happy); + ShowBaseResultSummary(result, "도형 반응", note); + pulses.Add(new ParticlePulse(center, FamilyColor(seal.baseFamily))); + LogBaseAttempt(result, seal, $"custom_shape_goal|{goalEffect.worldEffect}"); + EvaluateFloorCompletion(); + ConsumeSeal(sealView); + processed = new ProcessedSpell { baseResult = result }; + return true; + } + + private bool TryApplyTransformedSealCustomEvent( + SealView sealView, + BaseRecognitionResult result, + Vector2 center, + out ProcessedSpell processed) + { + processed = null; + var seal = sealView?.seal; + var spell = result?.spell; + if (seal == null || + !seal.HasCustomEffectSeal || + spell?.isCustomShape != true || + !TryParseCustomEventKind(spell, out var eventKind) || + eventKind == CustomShapeEventKind.None) + { + return false; + } + + var customEffect = CustomSpellEffectCatalog.For(seal.customEffectKind); + var eventLabel = string.IsNullOrWhiteSpace(spell.customShapeLabel) + ? eventKind.ToString() + : spell.customShapeLabel; + var eventNote = ApplyCustomShapeEvent(result, seal, center); + var elementalNote = ApplyElementalInteractions( + seal.baseFamily, + spell, + center, + spell.customEventDirection, + $"{customEffect.displayName} 문양", + seal.customEffectKind); + var note = $"{customEffect.displayName} 문양에 {eventLabel} 반응 도형을 덧그렸습니다."; + if (!string.IsNullOrWhiteSpace(eventNote)) + { + note += $"\n{eventNote}"; + } + + if (!string.IsNullOrWhiteSpace(elementalNote)) + { + note += $"\n{elementalNote}"; + } + + CurrentAssistLevel = 0; + LastHintText = ""; + ShowMagicNote(note, MentorMood.Happy); + ShowBaseResultSummary(result, $"{customEffect.displayName} 도형 반응", note); + pulses.Add(new ParticlePulse(center, FamilyColor(seal.baseFamily))); + LogBaseAttempt(result, seal, $"custom_effect_seal_event|{customEffect.kind}|{eventKind}"); + EvaluateFloorCompletion(); + ConsumeSeal(sealView); + processed = new ProcessedSpell { baseResult = result }; + return true; + } + + private bool TryTransformSealWithCustomEffect( + SealView sealView, + BaseRecognitionResult result, + Vector2 center, + CustomSpellEffectDefinition customEffect, + out ProcessedSpell processed) + { + processed = null; + var seal = sealView?.seal; + if (seal == null || + seal.HasCustomEffectSeal || + !CustomSpellEffectCatalog.IsSingleStepTransform(seal.baseFamily, result.spell, customEffect.kind)) + { + return false; + } + + TransformSealWithCustomEffect(sealView, result, customEffect); + var goalEffect = ApplyCustomSpellToGoals(seal, customEffect, center, result.spell); + var eventNote = ApplyCustomShapeEvent(result, seal, center); + var elementalNote = ApplyElementalInteractions( + seal.baseFamily, + result.spell, + center, + result.spell.customEventDirection, + customEffect.displayName, + customEffect.kind); + var note = + $"{SpellLabels.Korean(seal.baseFamily)} 문양이 {customEffect.displayName} 문양으로 변환되었습니다.\n" + + "추가 도형을 덧그리면 이 문양의 반응이 발동합니다.\n" + + customEffect.note; + if (!string.IsNullOrWhiteSpace(goalEffect.note)) + { + note += $"\n{goalEffect.note}"; + } + + if (!string.IsNullOrWhiteSpace(eventNote)) + { + note += $"\n{eventNote}"; + } + + if (!string.IsNullOrWhiteSpace(elementalNote)) + { + note += $"\n{elementalNote}"; + } + + CurrentAssistLevel = 0; + LastHintText = ""; + ShowMagicNote(note, MentorMood.Happy); + audioDirector?.PlayCustomSpellEffect(customEffect.kind, seal.baseFamily); + ShowBaseResultSummary(result, $"{customEffect.displayName} 문양", note); + pulses.Add(new ParticlePulse(center, FamilyColor(seal.baseFamily))); + LogBaseAttempt(result, seal, $"{goalEffect.worldEffect}|transform_seal|{customEffect.kind}"); + var completedWorldGoal = activeGoals.Any(goal => goal.completed && goal.id == goalEffect.worldEffect); + EvaluateFloorCompletion(); + if (completedWorldGoal) + { + ConsumeSeal(sealView); + } + + processed = new ProcessedSpell { baseResult = result }; + return true; + } + + private void TransformSealWithCustomEffect( + SealView sealView, + BaseRecognitionResult result, + CustomSpellEffectDefinition customEffect) + { + var seal = sealView.seal; + seal.customEffectKind = customEffect.kind; + seal.customShapeId = result.spell.customShapeId ?? ""; + seal.customShapeLabel = customEffect.displayName; + seal.customShapeToken = result.spell.customShapeToken ?? ""; + seal.customEventId = result.spell.customEventId ?? ""; + seal.customEventLabel = result.spell.customEventLabel ?? ""; + seal.customEventKind = result.spell.customEventKind ?? ""; + seal.customEventRole = result.spell.customEventRole ?? ""; + seal.customEventOrigin = result.spell.customEventOrigin; + seal.customEventDirection = result.spell.customEventDirection; + seal.createdAt = Time.time; + seal.expiresAt = Time.time + SpellRuntime.DefaultSealDurationSeconds; + sealView.ResetDefaultFallback(Time.time + DefaultSealFallbackDelaySeconds); + sealView.RefreshLabel(uiFont); + sealView.RefreshTint(SealTint(seal), SealSecondaryTint(seal), SealGuideTint(seal)); + } + + private bool TryApplyAttributeBeamFollowup( + SealView sealView, + BaseRecognitionResult result, + Vector2 center, + out ProcessedSpell processed) + { + processed = null; + var seal = sealView?.seal; + var spell = result?.spell; + if (seal == null || + spell == null || + !spell.isCustomShape || + !TryParseCustomEventKind(spell, out var eventKind) || + eventKind != CustomShapeEventKind.AttributeLaser) + { + return false; + } + + var direction = spell.customEventDirection.sqrMagnitude > 0.0001f + ? spell.customEventDirection.normalized + : Vector2.right; + var origin = seal.worldCenter; + var candidates = ResolutionGoalsForCurrentFloor() + .Where(goal => !goal.completed && goal.MatchesCustomEvent(seal.baseFamily, eventKind)) + .ToList(); + if (candidates.Count == 0) + { + return false; + } + + if (!TryResolveAttributeBeamHit(candidates, origin, direction, out var hitGoal, out var resolvedDirection)) + { + LastBeamHitForTests = false; + LastDamagedTargetNameForTests = ""; + ApplyCustomShapeEvent(result, seal, origin); + CurrentAssistLevel = 1; + LastHintText = "빛줄기가 허수아비를 빗나갔습니다. 문양은 유지되니 화살표 방향을 다시 맞춰 보세요."; + endingReport.RecordHintShown(1); + ShowMagicNote(LastHintText, MentorMood.Frown); + ShowBaseResultSummary(result, "빛줄기 빗나감", LastHintText); + pulses.Add(new ParticlePulse(origin + direction * 0.9f, FamilyColor(seal.baseFamily), weak: true)); + LogBaseAttempt(result, seal, "attribute_beam_missed"); + processed = new ProcessedSpell { baseResult = result }; + return true; + } + + direction = resolvedDirection; + var goalEffect = ActivateAttributeBeamGoal(hitGoal, result, seal, origin, direction); + var eventNote = ApplyCustomShapeEvent(result, seal, origin); + var elementalNote = ApplyElementalInteractions(seal.baseFamily, spell, origin, direction, "속성 빛줄기"); + var note = $"{SpellLabels.Korean(seal.baseFamily)} 문양에서 화살표 방향으로 빛줄기를 쐈습니다.\n{goalEffect.note}"; + if (!string.IsNullOrWhiteSpace(eventNote)) + { + note += $"\n{eventNote}"; + } + + if (!string.IsNullOrWhiteSpace(elementalNote)) + { + note += $"\n{elementalNote}"; + } + + CurrentAssistLevel = 0; + LastHintText = ""; + ShowMagicNote(note, MentorMood.Happy); + ShowBaseResultSummary(result, "속성 빛줄기 명중", note); + pulses.Add(new ParticlePulse(origin, FamilyColor(seal.baseFamily))); + LogBaseAttempt(result, seal, $"attribute_beam_hit|{hitGoal.id}"); + EvaluateFloorCompletion(); + ConsumeSeal(sealView); + processed = new ProcessedSpell { baseResult = result }; + return true; + } + + private bool TryResolveAttributeBeamHit( + IReadOnlyList candidates, + Vector2 origin, + Vector2 requestedDirection, + out WorldStateGoal hitGoal, + out Vector2 resolvedDirection) + { + resolvedDirection = requestedDirection.sqrMagnitude > 0.0001f + ? requestedDirection.normalized + : Vector2.right; + var initialDirection = resolvedDirection; + hitGoal = null; + foreach (var goal in candidates) + { + if (!BeamHitsGoalTarget(goal, origin, initialDirection, out _)) + { + continue; + } + + hitGoal = goal; + break; + } + + if (hitGoal != null) + { + return true; + } + + foreach (var goal in candidates) + { + if (!TryResolveAttributeBeamAutoAimDirection(goal, origin, resolvedDirection, out var autoDirection)) + { + continue; + } + + if (!BeamHitsGoalTarget(goal, origin, autoDirection, out _)) + { + continue; + } + + resolvedDirection = autoDirection; + hitGoal = goal; + return true; + } + + return false; + } + + private bool TryResolveAttributeBeamAutoAimDirection( + WorldStateGoal goal, + Vector2 origin, + Vector2 requestedDirection, + out Vector2 autoDirection) + { + autoDirection = requestedDirection.sqrMagnitude > 0.0001f + ? requestedDirection.normalized + : Vector2.right; + if (goal == null || !goal.RequiresBeamHit) + { + return false; + } + + var target = CombatTargetTransform(goal); + var targetCenter = target != null ? (Vector2)target.position : goal.position; + var toTarget = targetCenter - origin; + var distance = toTarget.magnitude; + if (distance < 0.12f || + distance > AttributeBeamAutoAimRadius || + distance > AttributeBeamMaxDistance) + { + return false; + } + + var targetDirection = toTarget / distance; + if (Mathf.Abs(toTarget.x) > 0.65f && + Mathf.Abs(toTarget.y) < 2.2f && + Mathf.Abs(autoDirection.x) > 0.35f && + Mathf.Sign(autoDirection.x) != Mathf.Sign(toTarget.x)) + { + return false; + } + + if (Vector2.Dot(autoDirection, targetDirection) < AttributeBeamAutoAimMinimumDot) + { + return false; + } + + autoDirection = targetDirection; + return true; + } + + private GoalEffect ActivateAttributeBeamGoal( + WorldStateGoal goal, + BaseRecognitionResult result, + CompiledSeal seal, + Vector2 origin, + Vector2 direction) + { + BeamHitsGoalTarget(goal, origin, direction, out var hitPoint); + ActivateGoal(goal, "attribute_beam", result.spell); + CreateAttributeBeamImpact(goal, origin, direction, hitPoint); + return new GoalEffect(BuildGoalDiscoveryNote(goal), goal.id); + } + + private bool BeamHitsGoalTarget(WorldStateGoal goal, Vector2 origin, Vector2 direction, out Vector2 hitPoint) + { + hitPoint = origin; + if (goal == null || !goal.RequiresBeamHit) + { + return false; + } + + direction = direction.sqrMagnitude > 0.0001f ? direction.normalized : Vector2.right; + var target = CombatTargetTransform(goal); + var targetCenter = target != null ? (Vector2)target.position : goal.position; + var toTarget = targetCenter - origin; + var distanceAlongRay = Vector2.Dot(toTarget, direction); + if (distanceAlongRay < 0.12f || distanceAlongRay > AttributeBeamMaxDistance) + { + return false; + } + + hitPoint = origin + direction * distanceAlongRay; + var hitRadius = goal.BeamHitRadius; + var renderer = target == null ? null : target.GetComponent(); + if (renderer != null) + { + hitRadius = Mathf.Max(hitRadius, Mathf.Max(renderer.bounds.extents.x, renderer.bounds.extents.y) + AttributeBeamHitPadding); + } + + return Vector2.Distance(hitPoint, targetCenter) <= hitRadius; + } + + private void CreateAttributeBeamImpact(WorldStateGoal goal, Vector2 origin, Vector2 direction, Vector2 hitPoint) + { + LastBeamHitForTests = true; + LastDamagedTargetNameForTests = CombatTargetTransform(goal)?.name ?? goal.title; + var color = goal.color; + var span = Mathf.Clamp(Vector2.Distance(origin, hitPoint), 0.7f, AttributeBeamMaxDistance); + var beam = CreateWorldSprite( + $"Attribute Beam {goal.id}", + origin + direction.normalized * (span * 0.5f), + new Vector3(0.19f, span, 1f), + WithAlpha(color, 0.88f), + Color.white, + PixelSpriteKind.Rug, + 31); + beam.transform.rotation = Quaternion.Euler(0f, 0f, Vector2.SignedAngle(Vector2.up, direction.normalized)); + RegisterCustomEventObject(beam, CustomShapeEventPersistence.Timed, 1.15f); + RegisterCustomEventObject(CreateWorldSprite( + $"Attribute Beam Impact {goal.id}", + hitPoint, + Vector3.one * 0.58f, + Color.Lerp(color, Color.white, 0.25f), + Color.white, + PixelSpriteKind.Pulse, + 33), + CustomShapeEventPersistence.Timed, + 1.15f); + pulses.Add(new ParticlePulse(hitPoint, color, scaleMultiplier: 1.2f, durationSeconds: 0.72f, sortingOrder: 34)); + } + private CustomSpellEffectDefinition ResolveCustomSpellEffect(SpellFamily baseFamily, SpellResult spell) { if (activeStageDefinition != null && @@ -3263,6 +5755,91 @@ private BaseRecognitionIntent ResolveBaseIntent(Vector2 center) }; } + private IReadOnlyList PreferredCustomShapeTokensForIntent(BaseRecognitionIntent intent) + { + if (intent?.IsActive != true || string.IsNullOrWhiteSpace(intent.goalId)) + { + return Array.Empty(); + } + + var goal = FindActiveGoal(intent.goalId); + return goal == null ? Array.Empty() : GoalRequirementShapeTokens(goal); + } + + private IReadOnlyList PreferredCustomShapeTokensForRecognition(BaseRecognitionIntent intent) + { + var tokens = new List(); + AddShapeTokens(tokens, PreferredCustomShapeTokensForIntent(intent)); + + if (IsFinalFloor && currentFinalTask != null) + { + var activeFinalGoal = CurrentFinalActiveGoal() ?? currentFinalTask.goal; + AddShapeTokens(tokens, GoalRequirementShapeTokens(activeFinalGoal)); + foreach (var reference in CurrentFinalTaskRecognitionReferences()) + { + AddShapeTokens(tokens, new[] { reference.shapeToken }); + AddShapeTokens(tokens, reference.eventShapeTokens); + } + } + + return tokens + .Where(token => !string.IsNullOrWhiteSpace(token)) + .Select(token => token.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + + private IEnumerable CurrentFinalTaskRecognitionReferences() + { + if (!IsFinalFloor || currentFinalTask == null) + { + return Array.Empty(); + } + + var references = new List(); + if (currentFinalTask.references != null) + { + references.AddRange(currentFinalTask.references); + } + + var activeFinalGoal = CurrentFinalActiveGoal() ?? currentFinalTask.goal; + if (activeFinalGoal?.requiredBase.HasValue == true) + { + references.AddRange(CustomShapeReferencesForFloor(currentFinalTask.sourceFloor) + .Where(reference => reference.family == activeFinalGoal.requiredBase.Value)); + } + + return DistinctCustomShapeReferences(references); + } + + private static IReadOnlyList CustomShapeReferencesForFloor(int floorNumber) + { + return floorNumber switch + { + 2 => FloorTwoCustomShapeReferences, + 3 => FloorThreeCustomShapeReferences, + 4 => FloorFourCustomShapeReferences, + 5 => CumulativeCustomShapeReferences(), + _ => Array.Empty() + }; + } + + private static void AddShapeTokens(List target, IEnumerable tokens) + { + if (target == null || tokens == null) + { + return; + } + + foreach (var token in tokens) + { + if (!string.IsNullOrWhiteSpace(token)) + { + target.Add(token.Trim()); + } + } + } + private ProcessedSpell ApplySubmittedSpellOutcome(SpellCastOutcome outcome) { trialCounter++; @@ -3292,9 +5869,92 @@ private ProcessedSpell ApplySpellOutcome(SpellCastOutcome outcome) }; } + private bool TryPromoteRepeatedGoalRecognition(BaseRecognitionResult result) + { + var spell = result?.spell; + var intent = result?.intent; + if (spell == null || + intent?.IsActive != true || + floorController.Current.number < 2 || + intent.strength < RepeatedGoalRecognitionMinimumIntentStrength) + { + return false; + } + + var goal = FindActiveGoal(intent.goalId); + if (goal == null || + goal.completed || + goal.requiredBase != intent.family) + { + return false; + } + + if (spell.status == RecognitionStatus.Recognized && spell.recognizedFamily == intent.family) + { + ResetRepeatedGoalRecognition(); + return false; + } + + var observed = spell.recognizedFamily?.ToString() ?? + spell.preIntentFamily?.ToString() ?? + spell.targetFamily.ToString(); + var key = $"{floorController.Current.number}|{goal.id}|{intent.family}|{observed}|{spell.status}"; + if (string.Equals(key, repeatedGoalRecognitionKey, StringComparison.Ordinal)) + { + repeatedGoalRecognitionCount++; + } + else + { + repeatedGoalRecognitionKey = key; + repeatedGoalRecognitionCount = 1; + } + + if (repeatedGoalRecognitionCount < RepeatedGoalRecognitionAcceptThreshold) + { + return false; + } + + PromoteBaseResultToIntent(result, intent.family); + ResetRepeatedGoalRecognition(); + return true; + } + + private static void PromoteBaseResultToIntent(BaseRecognitionResult result, SpellFamily family) + { + var spell = result.spell; + spell.status = RecognitionStatus.Recognized; + spell.recognizedFamily = family; + spell.targetFamily = family; + spell.mappedFamily = family; + spell.success = true; + spell.confidence = Mathf.Max(spell.confidence, 0.68f); + spell.feedbackReason = string.IsNullOrWhiteSpace(spell.feedbackReason) + ? "Repeated near-goal input accepted for the current target." + : spell.feedbackReason + " Repeated near-goal input accepted for the current target."; + } + + private void ResetRepeatedGoalRecognition() + { + repeatedGoalRecognitionKey = ""; + repeatedGoalRecognitionCount = 0; + } + private ProcessedSpell ApplyBaseFailure(SpellCastOutcome outcome) { var baseResult = outcome.baseResult; + if (TryPromoteRepeatedGoalRecognition(baseResult)) + { + var promotedSeal = SpellRuntime.CreateSeal(baseResult, Time.time); + return ApplyBaseSuccess(new SpellCastOutcome + { + kind = SpellCastOutcomeKind.BaseSucceeded, + baseResult = baseResult, + createdSeal = promotedSeal, + center = outcome.center, + strokeCount = outcome.strokeCount + }); + } + var feedbackFamily = baseResult.spell.recognizedFamily ?? baseResult.spell.targetFamily; var priorFailures = Mathf.Max(GetBaseFailureCount(feedbackFamily), MagicExamSettings.ObserverMode ? 1 : 0); var hintState = HintAssistance.ForAttempt(feedbackFamily, priorFailures, false, baseResult.spell); @@ -3306,7 +5966,7 @@ private ProcessedSpell ApplyBaseFailure(SpellCastOutcome outcome) audioDirector?.PlaySfx(baseResult.spell.status == RecognitionStatus.Incomplete ? AudioCue.CastIncomplete : AudioCue.CastInvalid); worldDrawing?.MarkLastBufferedStrokesInvalid(); ShowToast("문양 불안정 - 노트를 확인", new Color(0.92f, 0.72f, 0.34f)); - ShowBaseResultSummary(baseResult, "base 실패", resultSummary: hintState.body); + ShowBaseResultSummary(baseResult, "기본 문양 실패", resultSummary: hintState.body); pulses.Add(new ParticlePulse(outcome.center, new Color(0.75f, 0.75f, 0.82f), weak: true)); LogBaseAttempt(baseResult, null, "failed", hintState); return new ProcessedSpell { baseResult = baseResult }; @@ -3316,6 +5976,17 @@ private ProcessedSpell ApplyBaseSuccess(SpellCastOutcome outcome) { var baseResult = outcome.baseResult; var seal = outcome.createdSeal; + if (TryPromoteRepeatedGoalRecognition(baseResult)) + { + seal.baseFamily = baseResult.spell.recognizedFamily ?? baseResult.spell.targetFamily; + } + else if (baseResult.intent?.IsActive == true && + baseResult.intent.family == seal.baseFamily && + baseResult.spell.success) + { + ResetRepeatedGoalRecognition(); + } + var priorFailures = GetBaseFailureCount(seal.baseFamily); var successHintState = HintAssistance.ForAttempt(seal.baseFamily, priorFailures, true, baseResult.spell); baseFailureCounts[seal.baseFamily] = 0; @@ -3327,7 +5998,7 @@ private ProcessedSpell ApplyBaseSuccess(SpellCastOutcome outcome) discoveredFamilies.Add(seal.baseFamily); var effect = ApplyBaseToGoals(baseResult, seal.baseFamily, outcome.center); var customEventNote = ApplyCustomShapeEvent(baseResult, seal, outcome.center); - var elementalNote = ApplyElementalInteractions(seal.baseFamily, baseResult.spell, outcome.center, baseResult.spell.customEventDirection, "base"); + var elementalNote = ApplyElementalInteractions(seal.baseFamily, baseResult.spell, outcome.center, baseResult.spell.customEventDirection, "기본 문양"); var eventEffect = string.IsNullOrWhiteSpace(customEventNote) ? effect : new GoalEffect($"{effect.note}\n{customEventNote}", $"{effect.worldEffect}|{baseResult.spell.customEventId}"); @@ -3341,9 +6012,9 @@ private ProcessedSpell ApplyBaseSuccess(SpellCastOutcome outcome) var baseToastStrong = eventEffect.worldEffect != "base_off_target" && eventEffect.worldEffect != "seal_only"; var toastMessage = eventEffect.worldEffect == "base_off_target" ? $"{SpellLabels.Korean(seal.baseFamily)} 인식 - 목표 근처로 이동" - : baseToastStrong ? "목표 반응 적용" : $"{SpellLabels.Korean(seal.baseFamily)} seal 생성"; + : baseToastStrong ? "목표 반응 적용" : $"{SpellLabels.Korean(seal.baseFamily)} 문양 생성"; ShowToast(toastMessage, eventEffect.worldEffect == "base_off_target" ? new Color(0.92f, 0.72f, 0.34f) : FamilyColor(seal.baseFamily), baseToastStrong); - ShowBaseResultSummary(baseResult, "base 성공", resultSummary: eventEffect.note); + ShowBaseResultSummary(baseResult, "기본 문양 성공", resultSummary: eventEffect.note); pulses.Add(new ParticlePulse(outcome.center, FamilyColor(seal.baseFamily))); GlowPulse.Flash(outcome.center, FamilyColor(seal.baseFamily), 1.9f, 25); LogBaseAttempt(baseResult, seal, eventEffect.worldEffect, successHintState); @@ -3363,8 +6034,8 @@ private ProcessedSpell ApplyOverlayFailure(SpellCastOutcome outcome) ? AudioCue.CastDependencyMissing : result.status == RecognitionStatus.Incomplete ? AudioCue.CastIncomplete : AudioCue.CastInvalid); worldDrawing?.MarkLastBufferedStrokesInvalid(); - ShowToast("overlay 불안정 - seal 위치 확인", new Color(0.92f, 0.72f, 0.34f)); - ShowOverlayResultSummary(result, seal, "overlay 실패", LastHintText); + ShowToast("장식 불안정 - 문양 위치 확인", new Color(0.92f, 0.72f, 0.34f)); + ShowOverlayResultSummary(result, seal, "장식 실패", LastHintText); pulses.Add(new ParticlePulse(outcome.center, new Color(0.75f, 0.75f, 0.82f), weak: true)); LogOverlayAttempt(result, seal, outcome.center, outcome.strokeCount, "failed"); return new ProcessedSpell { overlayResult = result }; @@ -3376,13 +6047,13 @@ private ProcessedSpell ApplyOverlayDuplicate(SpellCastOutcome outcome) var seal = outcome.targetSeal; var op = outcome.overlayOperator!.Value; CurrentAssistLevel = 1; - LastHintText = "같은 장식 대신 아직 비어 있는 다른 장식을 seal 위에 그려 보세요."; + LastHintText = "같은 장식 대신 아직 비어 있는 다른 장식을 문양 위에 그려 보세요."; endingReport.RecordHintShown(1); - ShowMagicNote($"{SpellLabels.Korean(op)} 장식은 이미 이 seal에 붙어 있습니다.", MentorMood.Frown); + ShowMagicNote($"{SpellLabels.Korean(op)} 장식은 이미 이 문양에 붙어 있습니다.", MentorMood.Frown); audioDirector?.PlaySfx(AudioCue.CastInvalid); worldDrawing?.MarkLastBufferedStrokesInvalid(); - ShowToast("중복 overlay - 다른 장식 필요", OverlayColor(op)); - ShowOverlayResultSummary(result, seal, "overlay 중복", LastHintText); + ShowToast("중복 장식 - 다른 장식 필요", OverlayColor(op)); + ShowOverlayResultSummary(result, seal, "장식 중복", LastHintText); pulses.Add(new ParticlePulse(outcome.center, OverlayColor(op))); LogOverlayAttempt(result, seal, outcome.center, outcome.strokeCount, "duplicate_overlay"); return new ProcessedSpell { overlayResult = result }; @@ -3394,13 +6065,13 @@ private ProcessedSpell ApplyOverlayStackFull(SpellCastOutcome outcome) var seal = outcome.targetSeal; var op = outcome.overlayOperator!.Value; CurrentAssistLevel = 1; - LastHintText = "새 base seal을 만든 뒤 남은 장식을 붙여 보세요."; + LastHintText = "새 기본 문양을 만든 뒤 남은 장식을 붙여 보세요."; endingReport.RecordHintShown(1); - ShowMagicNote($"하나의 seal에는 overlay를 {SpellCastingService.MaxOverlayStack}개까지만 안정적으로 붙일 수 있습니다.", MentorMood.Frown); + ShowMagicNote($"하나의 문양에는 장식을 {SpellCastingService.MaxOverlayStack}개까지만 안정적으로 붙일 수 있습니다.", MentorMood.Frown); audioDirector?.PlaySfx(AudioCue.CastInvalid); worldDrawing?.MarkLastBufferedStrokesInvalid(); - ShowToast("overlay stack full - 새 base 필요", OverlayColor(op)); - ShowOverlayResultSummary(result, seal, "overlay 초과", LastHintText); + ShowToast("장식 가득 참 - 새 문양 필요", OverlayColor(op)); + ShowOverlayResultSummary(result, seal, "장식 초과", LastHintText); pulses.Add(new ParticlePulse(outcome.center, OverlayColor(op))); LogOverlayAttempt(result, seal, outcome.center, outcome.strokeCount, "overlay_stack_full"); return new ProcessedSpell { overlayResult = result }; @@ -3426,8 +6097,8 @@ private ProcessedSpell ApplyOverlaySuccess(SpellCastOutcome outcome) audioDirector?.PlayOverlaySuccess(op); worldDrawing?.MarkLastBufferedStrokesRecognized(OverlayColor(op)); var overlayToastStrong = effect.worldEffect != "overlay_stack"; - ShowToast(overlayToastStrong ? "목표 반응 적용" : $"{SpellLabels.Korean(op)} overlay 연결", OverlayColor(op), overlayToastStrong); - ShowOverlayResultSummary(result, seal, "overlay 성공", effect.note); + ShowToast(overlayToastStrong ? "목표 반응 적용" : $"{SpellLabels.Korean(op)} 장식 연결", OverlayColor(op), overlayToastStrong); + ShowOverlayResultSummary(result, seal, "장식 성공", effect.note); LogOverlayAttempt(result, seal, outcome.center, outcome.strokeCount, effect.worldEffect); pulses.Add(new ParticlePulse(outcome.center, OverlayColor(op))); EvaluateFloorCompletion(); @@ -3440,17 +6111,17 @@ private ProcessedSpell ApplyOverlayNoActiveSeal(SpellCastOutcome outcome) result.status = RecognitionStatus.Invalid; if (string.IsNullOrWhiteSpace(result.feedbackReason)) { - result.feedbackReason = "활성 base seal이 없습니다."; + result.feedbackReason = "활성 기본 문양이 없습니다."; } CurrentAssistLevel = 1; - LastHintText = "먼저 base 문양으로 seal을 만든 뒤, 그 원 안쪽이나 가장자리 바로 옆에 장식을 붙여 보세요."; + LastHintText = "먼저 기본 문양을 만든 뒤, 그 원 안쪽이나 가장자리 바로 옆에 장식을 붙여 보세요."; endingReport.RecordHintShown(1); ShowMagicNote($"노트: {result.feedbackReason}\n다음: {LastHintText}", MentorMood.Frown); audioDirector?.PlaySfx(AudioCue.CastDependencyMissing); worldDrawing?.MarkLastBufferedStrokesInvalid(); - ShowToast("base seal 먼저", new Color(0.92f, 0.72f, 0.34f), strong: true); - ShowOverlayNoSealResultSummary(result, "overlay 부착 실패", LastHintText); + ShowToast("기본 문양 먼저", new Color(0.92f, 0.72f, 0.34f), strong: true); + ShowOverlayNoSealResultSummary(result, "장식 부착 실패", LastHintText); pulses.Add(new ParticlePulse(outcome.center, new Color(0.75f, 0.75f, 0.82f), weak: true)); LogOverlayNoSealAttempt(result, outcome.center, outcome.strokeCount, "overlay_no_active_seal"); return new ProcessedSpell { overlayResult = result }; @@ -3468,8 +6139,8 @@ private ProcessedSpell ApplyDetachedOverlay(SpellCastOutcome outcome) ShowMagicNote(BuildDetachedOverlayFailureNote(result, seal), MentorMood.Frown); audioDirector?.PlaySfx(AudioCue.CastInvalid); worldDrawing?.MarkLastBufferedStrokesInvalid(); - ShowToast("seal 가까이 다시 그리기", new Color(0.92f, 0.72f, 0.34f)); - ShowOverlayResultSummary(result, seal, "overlay 거리 오류", LastHintText); + ShowToast("문양 가까이 다시 그리기", new Color(0.92f, 0.72f, 0.34f)); + ShowOverlayResultSummary(result, seal, "장식 거리 오류", LastHintText); pulses.Add(new ParticlePulse(outcome.center, new Color(0.75f, 0.75f, 0.82f), weak: true)); LogOverlayAttempt(result, seal, outcome.center, outcome.strokeCount, "detached_overlay"); return new ProcessedSpell { overlayResult = result }; @@ -3486,10 +6157,10 @@ private void ShowBaseResultSummary(BaseRecognitionResult result, string title, s ? $"{result.spell.customShapeLabel} ({family})" : family; var customLine = result.spell.isCustomShape - ? $"커스텀 {Percent(result.spell.customScore)} 기본 유사 {Percent(result.spell.defaultSimilarityScore)}\n" + ? $"가져온 도형 점수 {Percent(result.spell.customScore)} 기본 유사 {Percent(result.spell.defaultSimilarityScore)}\n" : ""; var eventLine = result.spell.isCustomShape && !string.IsNullOrWhiteSpace(result.spell.customEventLabel) - ? $"이벤트 {result.spell.customEventLabel} 역할 {result.spell.customEventRole}\n" + ? $"도형 반응 {result.spell.customEventLabel} 역할 {result.spell.customEventRole}\n" : ""; resultText.text = $"{title}: {label}\n" + @@ -3500,7 +6171,10 @@ private void ShowBaseResultSummary(BaseRecognitionResult result, string title, s $"해석: {ShortLine(QualityCoachLine(result.spell.quality), ResultLineLength(52, 42))}\n" + $"이유: {ShortLine(result.spell.feedbackReason, ResultLineLength(52, 42))}\n" + $"다음: {ShortLine(resultSummary, ResultLineLength(56, 46))}"; - ShowResultAsMentorSpeech(MoodFor(result.spell.status), BuildBaseResultSpeech(result, label, resultSummary)); + var forcedFailure = IsFailureSummary(title); + ShowResultAsMentorSpeech( + forcedFailure ? MentorMood.Frown : MoodFor(result.spell.status), + BuildBaseResultSpeech(result, label, resultSummary, forcedFailure)); } private void ShowOverlayResultSummary(OverlayRecognitionResult result, CompiledSeal seal, string title, string resultSummary) @@ -3510,7 +6184,7 @@ private void ShowOverlayResultSummary(OverlayRecognitionResult result, CompiledS var op = result.recognizedOperator.HasValue ? SpellLabels.Korean(result.recognizedOperator.Value) : "미확정"; resultText.text = $"{title}: {op}\n" + - $"대상 seal: {ShortLine(seal.Label, ResultLineLength(30, 24))}\n" + + $"대상 문양: {ShortLine(seal.Label, ResultLineLength(30, 24))}\n" + $"판정 {StatusLabel(result.status)} 점수 {Percent(result.score)} 모양 {Percent(result.shapeConfidence)}\n" + $"크기 {result.scaleRatio:0.00}x 위치 {AnchorLabel(result.anchorZone)}\n" + $"다음: {ShortLine(resultSummary, ResultLineLength(56, 46))}"; @@ -3523,7 +6197,7 @@ private void ShowOverlayNoSealResultSummary(OverlayRecognitionResult result, str var op = result.recognizedOperator.HasValue ? SpellLabels.Korean(result.recognizedOperator.Value) : "미확정"; resultText.text = $"{title}: {op}\n" + - $"대상 seal: 없음\n" + + $"대상 문양: 없음\n" + $"판정 {StatusLabel(result.status)} 점수 {Percent(result.score)} 모양 {Percent(result.shapeConfidence)}\n" + $"이유: {ShortLine(result.feedbackReason, ResultLineLength(52, 42))}\n" + $"다음: {ShortLine(resultSummary, ResultLineLength(56, 46))}"; @@ -3537,7 +6211,7 @@ private void ShowResultAsMentorSpeech(MentorMood mood, string speech) resultPanel.gameObject.SetActive(false); } - mentor?.Say(mood, speech); + mentor?.Say(mood, BuildMentorBubbleText(speech)); RefreshMagicNotePanelVisibility(); } @@ -3546,17 +6220,22 @@ private static MentorMood MoodFor(RecognitionStatus status) return status == RecognitionStatus.Recognized ? MentorMood.Happy : MentorMood.Frown; } - private string BuildBaseResultSpeech(BaseRecognitionResult result, string label, string resultSummary) + private string BuildBaseResultSpeech(BaseRecognitionResult result, string label, string resultSummary, bool forcedFailure) { + if (forcedFailure) + { + return $"{BaseFailureLead(result.spell, label)}\n{BaseActionLine(result.spell)}"; + } + if (result.spell.status == RecognitionStatus.Recognized) { - var extra = result.spell.isCustomShape ? " 커스텀 도형도 제대로 잡혔고." : ""; - return $"좋아, {label}은 됐어.{extra}\n{ConversationalNext(resultSummary)}"; + var extra = result.spell.isCustomShape ? " 가져온 도형도 함께 읽혔습니다." : ""; + return $"{label} 문양이 안정됐습니다.{extra}\n{ConversationalNext(resultSummary)}"; } if (result.spell.status == RecognitionStatus.Incomplete) { - return $"{label} 쪽으로 보이긴 해. 아직 마무리가 덜 됐어.\n{BaseActionLine(result.spell)}"; + return $"{BaseReadLine(result.spell, label)} 아직 마무리가 덜 되었습니다.\n{BaseActionLine(result.spell)}"; } if (result.spell.status == RecognitionStatus.Ambiguous) @@ -3564,7 +6243,64 @@ private string BuildBaseResultSpeech(BaseRecognitionResult result, string label, return $"{AmbiguousBaseLead(result.spell, label)}\n{BaseActionLine(result.spell)}"; } - return $"이번 건 {label}로 읽기엔 조금 어려워.\n{BaseActionLine(result.spell)}"; + return $"{BaseReadLine(result.spell, label)}\n{BaseActionLine(result.spell)}"; + } + + private static bool IsFailureSummary(string title) + { + return !string.IsNullOrWhiteSpace(title) && + (title.IndexOf("실패", StringComparison.Ordinal) >= 0 || + title.IndexOf("빗나감", StringComparison.Ordinal) >= 0); + } + + private static string BaseReadLine(SpellResult spell, string label) + { + if (spell == null) + { + return "이번 입력은 아직 기본 문양으로 읽기 어렵습니다."; + } + + if (spell.isCustomShape && !string.IsNullOrWhiteSpace(spell.customShapeLabel)) + { + return $"가져온 도형은 {spell.customShapeLabel}로 읽혔습니다."; + } + + if (spell.recognizedFamily.HasValue) + { + return $"이번 입력은 {SpellLabels.Korean(spell.recognizedFamily.Value)} 기본 문양으로 읽혔습니다."; + } + + if (spell.preIntentFamily.HasValue) + { + return $"이번 입력은 {SpellLabels.Korean(spell.preIntentFamily.Value)} 쪽으로 기울었습니다."; + } + + return !string.IsNullOrWhiteSpace(label) && label != "미확정" + ? $"이번 입력은 {label}로 읽기 어렵습니다." + : "이번 입력은 아직 기본 문양으로 읽기 어렵습니다."; + } + + private static string BaseFailureLead(SpellResult spell, string label) + { + if (spell == null) + { + return "이번 입력은 아직 기본 문양으로 읽기 어렵습니다."; + } + + var target = SpellLabels.Korean(spell.targetFamily); + if (spell.preIntentFamily.HasValue && spell.preIntentFamily.Value != spell.targetFamily) + { + var competitor = SpellLabels.Korean(spell.preIntentFamily.Value); + return $"목표는 {target} 문양입니다.\n{competitor} 특징도 함께 섞였습니다."; + } + + if (spell.recognizedFamily.HasValue && spell.recognizedFamily.Value != spell.targetFamily) + { + var recognized = SpellLabels.Korean(spell.recognizedFamily.Value); + return $"목표는 {target} 문양입니다.\n이번 입력은 {recognized} 쪽에 가깝습니다."; + } + + return BaseReadLine(spell, label); } private string BuildOverlayResultSpeech( @@ -3576,36 +6312,36 @@ private string BuildOverlayResultSpeech( var next = ConversationalNext(resultSummary); if (result.status == RecognitionStatus.Recognized) { - return $"좋아, {op} 장식 붙었어.\n지금 seal은 {ShortLine(seal.Label, 34)} 상태야.\n{next}"; + return $"{op} 장식이 붙었습니다.\n현재 문양은 {ShortLine(seal.Label, 34)} 상태입니다.\n{next}"; } if (result.scaleHint == OverlayScaleHint.TooLarge) { - return $"{op}은 지금 너무 크게 그렸어.\nseal 바로 옆에서 조금 작게 다시 해봐.\n{next}"; + return $"{op}이 너무 크게 들어갔습니다.\n문양 옆에 조금 작게 다시 그려 주세요.\n{next}"; } if (result.scaleHint == OverlayScaleHint.TooSmall) { - return $"{op}은 지금 너무 작게 들어갔어.\n조금 더 크게, seal 근처에 붙여 봐.\n{next}"; + return $"{op}이 너무 작게 들어갔습니다.\n문양 근처에 조금 더 크게 그려 주세요.\n{next}"; } - return $"{op} 장식은 아직 안 붙었어.\n위치랑 모양을 조금만 더 맞춰 보자.\n{next}"; + return $"{op} 장식이 아직 붙지 않았습니다.\n위치와 모양을 다시 맞춰 주세요.\n{next}"; } private string BuildOverlayNoSealSpeech(OverlayRecognitionResult result, string op, string resultSummary) { var next = ConversationalNext(resultSummary); - return $"{op}을 붙일 base seal이 근처에 없어.\n먼저 바닥에 base 문양을 만들고, 그 옆에 다시 그려 봐.\n{next}"; + return $"{op}을 붙일 기본 문양이 없습니다.\n먼저 기본 문양을 만들고 그 안에 그려 주세요.\n{next}"; } private static string ConversationalNext(string resultSummary) { if (string.IsNullOrWhiteSpace(resultSummary)) { - return "다음 동작은 바로 이어서 해도 돼."; + return "다음 동작을 이어 진행하세요."; } - return $"다음엔 {ShortLine(resultSummary, 44)}"; + return $"다음에는 {ShortLine(resultSummary, 44)}"; } private static string AmbiguousBaseLead(SpellResult spell, string label) @@ -3616,22 +6352,27 @@ private static string AmbiguousBaseLead(SpellResult spell, string label) : ""; if (!string.IsNullOrWhiteSpace(competitor)) { - return $"음, {target} 의도는 보여. 다만 {competitor} 쪽도 섞였어."; + return $"{target} 의도는 보입니다.\n{competitor} 특징도 함께 섞였습니다."; } - return $"음, {label}로 보이긴 하는데 아직 애매해."; + return $"{label}에 가깝지만 아직 애매합니다."; } private static string BaseActionLine(SpellResult spell) { + if (spell == null) + { + return "큰 실루엣을 먼저 맞추고 다시 그려 주세요."; + } + return spell.targetFamily switch { - SpellFamily.Water => "한 번에 둥글게 돌리고 끝점만 시작점 옆에 붙여 봐.", - SpellFamily.Fire => "세 꼭짓점을 크게 잡고 마지막 선을 처음 점으로 닫아 봐.", - SpellFamily.Wind => "위, 가운데, 아래 세 줄을 짧고 평행하게 그어 봐.", - SpellFamily.Earth => "윗변은 좁게, 아랫변은 넓게 잡고 네 변을 닫아 봐.", - SpellFamily.Life => "작게 감아 돌면서 끝을 시작점 근처로 데려와 봐.", - _ => "큰 실루엣을 먼저 맞추고 다시 그려 봐." + SpellFamily.Water => "끝점을 시작점 옆에 붙여 주세요.", + SpellFamily.Fire => "세 꼭짓점을 크게 잡고 마지막 선을 처음 점으로 닫아 주세요.", + SpellFamily.Wind => "위, 가운데, 아래 세 줄을 짧고 평행하게 그어 주세요.", + SpellFamily.Earth => "윗변은 좁게, 아랫변은 넓게 잡고 네 변을 닫아 주세요.", + SpellFamily.Life => "작게 감아 돌며 끝을 시작점 근처로 데려와 주세요.", + _ => "큰 실루엣을 먼저 맞추고 다시 그려 주세요." }; } @@ -3644,11 +6385,11 @@ private void UpdateResultPanelLayout() resultPanelCompact = Screen.width > 0 && Screen.width < ResultPanelCompactScreenWidth; var questHeight = questScrollPanel == null ? 246f : questScrollPanel.sizeDelta.y; - resultPanel.anchoredPosition = new Vector2(-20, -(questHeight + 40f)); - resultPanel.sizeDelta = resultPanelCompact ? new Vector2(360, 188) : new Vector2(430, 206); + resultPanel.anchoredPosition = new Vector2(-18, -(questHeight + 34f)); + resultPanel.sizeDelta = resultPanelCompact ? new Vector2(332, 168) : new Vector2(374, 180); resultText.fontSize = resultPanelCompact ? 12 : 13; resultText.rectTransform.anchoredPosition = new Vector2(14, -12); - resultText.rectTransform.sizeDelta = resultPanelCompact ? new Vector2(332, 162) : new Vector2(402, 180); + resultText.rectTransform.sizeDelta = resultPanelCompact ? new Vector2(304, 142) : new Vector2(346, 154); } private int ResultLineLength(int wideLength, int compactLength) @@ -3703,7 +6444,13 @@ private GoalEffect ApplyBaseToGoals(BaseRecognitionResult result, SpellFamily fa return BuildFirstFloorSequentialWaitEffect(family, currentFirstFloorGoal); } - var resolution = floorGoals.ResolveBase(BaseResolutionGoalsForCurrentFloor(), family, center, result?.spell?.isCustomShape == true); + var currentSecondFloorGoal = CurrentSecondFloorSequenceGoal(); + if (currentSecondFloorGoal?.requiredBase.HasValue == true && currentSecondFloorGoal.requiredBase.Value != family) + { + return BuildSecondFloorSequentialWaitEffect(family, currentSecondFloorGoal); + } + + var resolution = floorGoals.ResolveBase(BaseResolutionGoalsForCurrentFloor(), family, center, isCustomShape: false); if (resolution.kind == GoalResolutionKind.Completed) { if (TryBuildEarlyTutorialSymbolDistanceEffect(resolution.goal, center, family, out var blockedEffect)) @@ -3718,7 +6465,7 @@ private GoalEffect ApplyBaseToGoals(BaseRecognitionResult result, SpellFamily fa if (resolution.kind == GoalResolutionKind.CustomRequired) { return new GoalEffect( - $"{resolution.targetGoal.title} 표식은 커스텀 도형으로만 반응합니다.\n좌측 책장에서 레퍼런스 도형을 슬롯으로 들여온 뒤 같은 표식 근처에 다시 그리세요.", + $"{resolution.targetGoal.title} 표식은 슬롯 도형으로만 반응합니다.\n시험관이 넣어 준 도형을 기본 문양의 원 안에 이어 그리세요.", "custom_required"); } @@ -3726,7 +6473,7 @@ private GoalEffect ApplyBaseToGoals(BaseRecognitionResult result, SpellFamily fa { return new GoalEffect( $"{resolution.targetGoal.title} 표식은 기본 문양만으로는 열리지 않습니다.\n" + - $"{resolution.targetGoal.RequirementLabel} 조합이 되도록 기본 문양 위에 커스텀 도형을 얹으세요.", + $"{resolution.targetGoal.RequirementLabel} 조합이 되도록 기본 문양의 빛나는 원 안에 도형을 이어 그리세요.", "custom_effect_required"); } @@ -3740,7 +6487,7 @@ private GoalEffect ApplyBaseToGoals(BaseRecognitionResult result, SpellFamily fa return new GoalEffect(BuildBaseOffTargetGoalNote(family, resolution.targetGoal, resolution.distance, resolution.radius), resolution.worldEffect); } - return new GoalEffect($"{SpellLabels.Korean(family)} seal이 바닥에 잠깐 고정되었습니다.", "seal_only"); + return new GoalEffect($"{SpellLabels.Korean(family)} 문양이 바닥에 잠깐 고정되었습니다.", "seal_only"); } private GoalEffect BuildFirstFloorSequentialWaitEffect(SpellFamily submittedFamily, WorldStateGoal currentGoal) @@ -3748,17 +6495,26 @@ private GoalEffect BuildFirstFloorSequentialWaitEffect(SpellFamily submittedFami var note = $"1층 튜토리얼은 현재 보이는 표식부터 순서대로 수집합니다.\n" + $"지금은 {currentGoal.title}({currentGoal.RequirementLabel}) 근처에 {SpellLabels.Korean(currentGoal.requiredBase.Value)} 도형을 먼저 그려 주세요. " + - $"{SpellLabels.Korean(submittedFamily)} 도형은 seal로만 남겼습니다."; + $"{SpellLabels.Korean(submittedFamily)} 도형은 문양으로만 남겼습니다."; return new GoalEffect(note, "first_floor_sequence_wait"); } + private GoalEffect BuildSecondFloorSequentialWaitEffect(SpellFamily submittedFamily, WorldStateGoal currentGoal) + { + var note = + "2층은 보이는 목표부터 하나씩 진행합니다.\n" + + $"지금은 {currentGoal.title}에 맞는 {SpellLabels.Korean(currentGoal.requiredBase.Value)} 도형을 먼저 가져와 그려 주세요. " + + $"{SpellLabels.Korean(submittedFamily)} 입력은 아직 다음 차례가 아닙니다."; + return new GoalEffect(note, "second_floor_sequence_wait"); + } + private GoalEffect ApplyCustomSpellToGoals( CompiledSeal seal, CustomSpellEffectDefinition customEffect, Vector2 center, SpellResult spell) { - var resolution = floorGoals.ResolveBase(activeGoals, seal.baseFamily, center, true, customEffect.kind); + var resolution = floorGoals.ResolveBase(ResolutionGoalsForCurrentFloor(), seal.baseFamily, center, true, customEffect.kind); if (resolution.kind == GoalResolutionKind.Completed) { if (TryBuildEarlyTutorialSymbolDistanceEffect(resolution.goal, center, seal.baseFamily, out var blockedEffect)) @@ -3809,6 +6565,7 @@ private string ApplyCustomShapeEvent(BaseRecognitionResult result, CompiledSeal LastCustomShapeEventKindForTests = eventKind.ToString(); LastCustomShapeEventLabelForTests = spell.customEventLabel ?? ""; LastCustomShapeEventDirectionForTests = direction; + audioDirector?.PlayCustomShapeEvent(eventKind); switch (eventKind) { @@ -3863,7 +6620,7 @@ private string ApplyCustomShapeEvent(BaseRecognitionResult result, CompiledSeal return string.IsNullOrWhiteSpace(spell.customEventLabel) ? "" - : $"커스텀 이벤트: {spell.customEventLabel}"; + : $"도형 반응: {spell.customEventLabel}"; } private void CreateCustomShapeEventAccent( @@ -3983,13 +6740,14 @@ private string ApplyElementalInteractions( SpellResult spell, Vector2 center, Vector2 direction, - string sourceLabel) + string sourceLabel, + CustomSpellEffectKind customEffectOverride = CustomSpellEffectKind.None) { var eventKind = TryParseCustomEventKind(spell, out var parsedEvent) ? parsedEvent : CustomShapeEventKind.None; - var customEffect = CustomSpellEffectKind.None; - if (spell?.isCustomShape == true) + var customEffect = customEffectOverride; + if (customEffect == CustomSpellEffectKind.None && spell?.isCustomShape == true) { var resolved = ResolveCustomSpellEffect(family, spell); customEffect = resolved.IsValid ? resolved.kind : CustomSpellEffectKind.None; @@ -4125,14 +6883,14 @@ private string BuildBaseOffTargetGoalNote(SpellFamily family, WorldStateGoal tar private GoalEffect ApplyOverlayToGoals(CompiledSeal seal, OverlayOperator op, Vector2 center) { - var resolution = floorGoals.ResolveOverlay(activeGoals, seal, op, center); + var resolution = floorGoals.ResolveOverlay(ResolutionGoalsForCurrentFloor(), seal, op, center); if (resolution.kind == GoalResolutionKind.Completed) { ActivateGoal(resolution.goal, resolution.worldEffect); return new GoalEffect(BuildGoalDiscoveryNote(resolution.goal), resolution.goal.id); } - return new GoalEffect($"{seal.Label}: overlay stack이 빛났습니다.", "overlay_stack"); + return new GoalEffect($"{seal.Label}: 장식 반응이 빛났습니다.", "overlay_stack"); } private int GetBaseFailureCount(SpellFamily family) @@ -4151,26 +6909,26 @@ private static string BuildBaseFailureNote(SpellResult result, HintState hintSta private static string BuildBaseSuccessNote(CompiledSeal seal, GoalEffect effect, HintState hintState) { var assisted = hintState.assisted ? " 이전 힌트가 이번 시전에 도움이 되었습니다." : ""; - return $"{SpellLabels.Korean(seal.baseFamily)} seal 성공.{assisted}\n{effect.note}"; + return $"{SpellLabels.Korean(seal.baseFamily)} 문양 성공.{assisted}\n{effect.note}"; } private static string BuildOverlayFailureNote(OverlayRecognitionResult result, CompiledSeal seal) { return - "노트: 장식이 seal에 안정적으로 붙지 않았습니다.\n" + + "노트: 장식이 문양에 안정적으로 붙지 않았습니다.\n" + $"{result.feedbackReason}\n" + $"다음: {OverlayActionHint(result, seal)}"; } private static string BuildOverlaySuccessNote(CompiledSeal seal, OverlayOperator op, GoalEffect effect) { - return $"{SpellLabels.Korean(op)} 장식이 seal 가장자리에 붙었습니다.\n현재 seal: {seal.Label}\n{effect.note}"; + return $"{SpellLabels.Korean(op)} 장식이 문양 가장자리에 붙었습니다.\n현재 문양: {seal.Label}\n{effect.note}"; } private static string BuildDetachedOverlayFailureNote(OverlayRecognitionResult result, CompiledSeal seal) { return - "노트: 장식이 seal에 안정적으로 붙지 않았습니다.\n" + + "노트: 장식이 문양에 안정적으로 붙지 않았습니다.\n" + $"{result.feedbackReason}\n" + $"다음: {DetachedOverlayActionHint(seal)}"; } @@ -4179,17 +6937,17 @@ private static string OverlayActionHint(OverlayRecognitionResult result, Compile { if (result.recognizedOperator == OverlayOperator.MartialAxis && !seal.overlayStack.Contains(OverlayOperator.VoidCut)) { - return "먼저 같은 seal에 대각선 절단 장식을 붙인 뒤, 중심을 가르는 축을 다시 그리세요."; + return "먼저 같은 문양에 대각선 절단 장식을 붙인 뒤, 중심을 가르는 축을 다시 그리세요."; } if (result.scaleHint == OverlayScaleHint.TooSmall) { - return "장식이 너무 작습니다. seal 중심을 기준으로 조금 더 크게 그려 보세요."; + return "장식이 너무 작습니다. 문양 중심을 기준으로 조금 더 크게 그려 보세요."; } if (result.scaleHint == OverlayScaleHint.TooLarge) { - return "장식이 너무 큽니다. seal 안쪽에 들어오도록 작게 줄여 보세요."; + return "장식이 너무 큽니다. 문양 안쪽에 들어오도록 작게 줄여 보세요."; } return AnchorHint(result.anchorZone); @@ -4199,24 +6957,24 @@ private static string BuildDetachedOverlayReason(OverlayRecognitionResult result { var operatorName = result.recognizedOperator.HasValue ? SpellLabels.Korean(result.recognizedOperator.Value) : "장식"; var distance = Vector2.Distance(center, seal.worldCenter); - return $"{operatorName} 모양은 보였지만 seal에서 너무 멀어 붙지 않았습니다. 현재 거리 {distance:0.0}, seal 중심 가까이에서 다시 그리세요."; + return $"{operatorName} 모양은 보였지만 문양에서 너무 멀어 붙지 않았습니다. 현재 거리 {distance:0.0}, 문양 중심 가까이에서 다시 그리세요."; } private static string DetachedOverlayActionHint(CompiledSeal seal) { - return $"{SpellLabels.Korean(seal.baseFamily)} seal의 빛나는 원 안쪽이나 가장자리 바로 옆에 장식을 다시 그리세요."; + return $"{SpellLabels.Korean(seal.baseFamily)} 문양의 빛나는 원 안쪽이나 가장자리 바로 옆에 장식을 다시 그리세요."; } private static string AnchorHint(string anchorZone) { return anchorZone switch { - "upper_right" => "seal의 오른쪽 위 가장자리에서 짧고 또렷하게 다시 그려 보세요.", - "right" => "seal의 오른쪽 가장자리 옆에 붙이듯 다시 그려 보세요.", - "lower_right" => "seal의 오른쪽 아래 가장자리에서 다시 그려 보세요.", - "upper" => "seal의 위쪽 가장자리 가까이에서 다시 그려 보세요.", - "left" => "seal의 왼쪽 가장자리 가까이에서 다시 그려 보세요.", - _ => "seal 중심 가까이에 작게, 한 가지 장식만 다시 그려 보세요." + "upper_right" => "문양의 오른쪽 위 가장자리에서 짧고 또렷하게 다시 그려 보세요.", + "right" => "문양의 오른쪽 가장자리 옆에 붙이듯 다시 그려 보세요.", + "lower_right" => "문양의 오른쪽 아래 가장자리에서 다시 그려 보세요.", + "upper" => "문양의 위쪽 가장자리 가까이에서 다시 그려 보세요.", + "left" => "문양의 왼쪽 가장자리 가까이에서 다시 그려 보세요.", + _ => "문양 중심 가까이에 작게, 한 가지 장식만 다시 그려 보세요." }; } @@ -4246,13 +7004,52 @@ private void ActivateGoal(WorldStateGoal goal, string effect, SpellResult spell } ApplyGoalReaction(goal, spell); endingReport.RecordDiscovery(goal.id, effect); + LogActionEvent( + "goal_completed", + "system", + "goal", + goal.id, + goal.title, + goal.position, + effect, + BuildEventPayload( + Pair("requirement", goal.RequirementLabel), + Pair("reaction", goal.reactionKind.ToString()), + Pair("effect", effect ?? ""))); audioDirector?.PlaySfx(AudioCue.GoalSatisfied, 0.82f); pulses.Add(new ParticlePulse(goal.position, goal.color)); GlowPulse.Flash(goal.position, goal.color, 1.7f, 26); RefreshFirstFloorTutorialGoalVisibility(); + RefreshSecondFloorSequenceGoalVisibility(); + RefreshSequentialGoalGuide(); TickQuestChecklist(forceRefresh: true); + GrantMentorCustomReferencesForCurrentFloor(showSpeech: true); UpdateHud(); UpdateResultPanelLayout(); + WriteEnrollmentSnapshot("goal_completed"); + } + + private WorldStateGoal CurrentFinalActiveGoal() + { + if (!IsFinalFloor || currentFinalTask == null) + { + return null; + } + + return activeGoals.FirstOrDefault(goal => + string.Equals(goal.id, currentFinalTask.goal.id, StringComparison.OrdinalIgnoreCase)); + } + + private IReadOnlyList ResolutionGoalsForCurrentFloor() + { + if (IsFinalFloor && currentFinalTask != null) + { + var currentGoal = CurrentFinalActiveGoal(); + return currentGoal == null ? Array.Empty() : new[] { currentGoal }; + } + + var secondFloorCurrent = CurrentSecondFloorSequenceGoal(); + return secondFloorCurrent == null ? activeGoals : new[] { secondFloorCurrent }; } private void ApplyGoalReaction(WorldStateGoal goal, SpellResult spell = null) @@ -4304,6 +7101,7 @@ private bool TryCreateStageEnvironmentReaction(WorldStateGoal goal, SpellResult } var bodies = CreateStageEntityForObstacle(goal, obstacle, effect); + bodies = EnsureStageEntitySurface(goal, obstacle, effect, bodies); CreateStageEffectVisuals(goal, obstacle, effect, spell, bodies); if (obstacle.safePositionAfterSolved.sqrMagnitude > 0.001f) { @@ -4314,6 +7112,49 @@ private bool TryCreateStageEnvironmentReaction(WorldStateGoal goal, SpellResult return true; } + private IReadOnlyList EnsureStageEntitySurface( + WorldStateGoal goal, + StageObstacleDefinition obstacle, + StageEnvironmentEffect effect, + IReadOnlyList bodies) + { + var safeBodies = bodies?.Where(body => body != null).ToList() ?? new List(); + var entity = effect.entity ?? new StageEntityDefinition(); + var size = obstacle.solutionSize.sqrMagnitude <= 0.001f ? entity.size : obstacle.solutionSize; + if (safeBodies.Count == 0) + { + var fallback = CreateStageEntityBody( + $"Stage Entity {goal.id} Surface", + obstacle.solutionPosition + entity.offset, + size, + entity, + goal.color); + safeBodies.Add(fallback); + } + + foreach (var body in safeBodies) + { + if (body == null) + { + continue; + } + + if (entity.hasCollider && body.GetComponent() == null) + { + AddPlatformCollider(body, entity.createsSteps && entity.stepSize.sqrMagnitude > 0.001f ? entity.stepSize : size); + } + + if (goal.reactionKind == WorldReactionKind.EarthStairs && + body.TryGetComponent(out var renderer) && + renderer.sortingOrder < 1) + { + renderer.sortingOrder = 1; + } + } + + return safeBodies; + } + private IReadOnlyList CreateStageEntityForObstacle( WorldStateGoal goal, StageObstacleDefinition obstacle, @@ -4594,6 +7435,7 @@ private static Vector2 EffectWakeOffset(CustomSpellEffectKind kind) return kind switch { CustomSpellEffectKind.Ice => new Vector2(0f, 0.05f), + CustomSpellEffectKind.Steel => new Vector2(0f, -0.04f), CustomSpellEffectKind.Stability => new Vector2(0f, -0.02f), CustomSpellEffectKind.LivingBridge => new Vector2(0f, 0.08f), CustomSpellEffectKind.WindPlatform => new Vector2(0f, 0.16f), @@ -4606,6 +7448,7 @@ private static PixelSpriteKind EffectSurfaceSpriteKind(CustomSpellEffectKind kin return kind switch { CustomSpellEffectKind.Ice => PixelSpriteKind.IceBridge, + CustomSpellEffectKind.Steel => PixelSpriteKind.WallTrim, CustomSpellEffectKind.Stability => PixelSpriteKind.EarthStep, CustomSpellEffectKind.LivingBridge => PixelSpriteKind.VineBridge, CustomSpellEffectKind.WindPlatform => PixelSpriteKind.WindPlatformTile, @@ -4625,6 +7468,7 @@ private static PixelSpriteKind EventSignatureSpriteKind(CustomShapeEventKind eve _ => effectKind switch { CustomSpellEffectKind.Ice => PixelSpriteKind.WaterRune, + CustomSpellEffectKind.Steel => PixelSpriteKind.EarthRune, CustomSpellEffectKind.Stability => PixelSpriteKind.EarthRune, CustomSpellEffectKind.LivingBridge => PixelSpriteKind.LifeRune, CustomSpellEffectKind.WindPlatform => PixelSpriteKind.WindRune, @@ -4765,11 +7609,25 @@ private void CreateStageNode(WorldStateGoal goal, Vector2 position, PixelSpriteK private void CreateCombatHitReaction(WorldStateGoal goal) { + if (goal.RequiresBeamHit) + { + var beamTarget = CombatTargetTransform(goal); + LastDamagedTargetNameForTests = beamTarget == null ? goal.title : beamTarget.name; + ShowDamagePopup(CombatTargetTopPosition(goal, beamTarget), "-1/2", new Color(1f, 0.20f, 0.18f)); + if (beamTarget != null) + { + RegisterSpriteAccent(beamTarget.gameObject, SpriteAccentAnimationKind.RuneActive, 0.11f); + } + + pulses.Add(new ParticlePulse(goal.position, goal.color, scaleMultiplier: 1.05f, durationSeconds: 0.62f, sortingOrder: 24)); + return; + } + var impact = goal.requiredCustomSpell.HasValue ? CustomSpellEffectCatalog.For(goal.requiredCustomSpell.Value).impact : 20; var target = CombatTargetTransform(goal); - var label = impact > 0 ? $"-{impact}" : "효과"; + var label = impact > 0 ? $"+{impact} 안정" : "안정"; var damageColor = DamageColorFor(goal.requiredCustomSpell); ShowDamagePopup(CombatTargetTopPosition(goal, target), label, damageColor); AddCombatStatusBuff(goal, target); @@ -4828,41 +7686,93 @@ private void EvaluateFloorCompletion() } ShowMagicNote(BuildFloorCompletionNote(), MentorMood.Happy); + LogActionEvent( + "floor_completed", + "system", + "floor", + floorController.CurrentFloorNumber.ToString(CultureInfo.InvariantCulture), + floorController.Current.title, + value: activeGoals.Count(goal => goal.completed).ToString(CultureInfo.InvariantCulture), + payloadJson: BuildEventPayload( + Pair("goalCount", activeGoals.Count.ToString(CultureInfo.InvariantCulture)), + Pair("advanceDelay", LogFloat(CurrentFloorAdvanceDelaySeconds())))); audioDirector?.PlaySfx(AudioCue.FloorComplete, 0.92f); ShowToast($"{floorController.CurrentFloorNumber}층 완료 - 다음 층 개방", floorController.Current.accentColor, strong: true); - pendingAdvanceAt = Time.time + CurrentFloorAdvanceDelaySeconds(); + BeginFloorTransitionCountdown(CurrentFloorAdvanceDelaySeconds()); PublishProgressCheckpoint(Mathf.Min(floorController.CurrentFloorNumber + 1, FloorCount)); + WriteEnrollmentSnapshot("floor_completed"); return; } var completed = activeGoals.Count(goal => goal.completed); - if (completed < FinalFloorPassingGoalCount) + if (completed < CurrentFinalPassingGoalCount()) { + AdvanceFinalTaskAfterCurrentSolved(); return; } - var fullyCompleted = completed >= activeGoals.Count; - if (fullyCompleted && !finalTrueEnding) + if (pendingAdvanceAt > 0f) { - finalTrueEnding = true; - ShowMagicNote(BuildFloorCompletionNote(), MentorMood.Happy); - audioDirector?.PlaySfx(AudioCue.CastFinalEffect, 0.95f); - ShowToast("성좌심 완전 복구 - 보고서 준비", floorController.Current.accentColor, strong: true); - pendingAdvanceAt = Time.time + FinalFloorCompleteReportDelaySeconds; - PublishProgressCheckpoint(floorController.CurrentFloorNumber); return; } - if (pendingAdvanceAt > 0f) + finalTrueEnding = true; + finalCertificateQueued = true; + ShowMagicNote(BuildFloorCompletionNote(), MentorMood.Happy); + SetGameplayInputEnabled(false); + LogActionEvent( + "final_exam_completed", + "system", + "ending", + floorController.CurrentFloorNumber.ToString(CultureInfo.InvariantCulture), + floorController.Current.title, + value: $"{completed}/{CurrentFinalPassingGoalCount()}", + payloadJson: BuildEventPayload(Pair("certificateQueued", "true"))); + audioDirector?.PlaySfx(AudioCue.CastFinalEffect, 0.95f); + ShowToast("최종 시험 통과 - 수료증 발급", floorController.Current.accentColor, strong: true); + BeginFloorTransitionCountdown(FinalFloorCompleteReportDelaySeconds); + PublishProgressCheckpoint(floorController.CurrentFloorNumber); + WriteEnrollmentSnapshot("final_exam_completed"); + } + + private void AdvanceFinalTaskAfterCurrentSolved() + { + if (!IsFinalFloor || currentFinalTasks.Count == 0) { return; } - ShowMagicNote(BuildFloorCompletionNote(), MentorMood.Happy); - audioDirector?.PlaySfx(AudioCue.CastFinalEffect, 0.80f); - ShowToast("입학 시험 통과 - 보고서 준비", floorController.Current.accentColor, strong: true); - pendingAdvanceAt = Time.time + FinalFloorPassReportDelaySeconds; - PublishProgressCheckpoint(floorController.CurrentFloorNumber); + var nextIndex = activeGoals.FindIndex(goal => !goal.completed); + if (nextIndex < 0 || nextIndex >= currentFinalTasks.Count) + { + return; + } + + currentFinalTaskIndex = nextIndex; + currentFinalTask = currentFinalTasks[currentFinalTaskIndex]; + EnsureCurrentFinalTaskReferencesAvailable(forceReplaceIfFull: true); + RefreshFinalExamGoalVisibility(); + var nextNumber = currentFinalTaskIndex + 1; + LogActionEvent( + "final_task_advanced", + "system", + "final_exam", + currentFinalTask.id, + currentFinalTask.prompt, + value: $"{nextNumber}/{currentFinalTasks.Count}", + payloadJson: BuildEventPayload(Pair("sourceFloor", currentFinalTask.sourceFloor.ToString(CultureInfo.InvariantCulture)))); + ShowMagicNote($"좋습니다. 다음 문제입니다.\n문제 {nextNumber}/{currentFinalTasks.Count}: {currentFinalTask.prompt}", MentorMood.Neutral); + ShowToast($"다음 문제 {nextNumber}/{currentFinalTasks.Count}", floorController.Current.accentColor, strong: false); + TickQuestChecklist(forceRefresh: true); + UpdateHud(); + WriteEnrollmentSnapshot("final_task_advanced"); + } + + private int CurrentFinalPassingGoalCount() + { + return currentFinalTasks.Count <= 0 + ? FinalFloorPassingGoalCount + : Mathf.Min(FinalFloorPassingGoalCount, currentFinalTasks.Count); } private float CurrentFloorAdvanceDelaySeconds() @@ -4880,16 +7790,21 @@ private string BuildFloorCompletionNote() } CelebrateFinalCompletion(finalTrueEnding); + if (finalCertificateQueued) + { + return "최종 시험 통과.\n세 문제를 모두 확인했습니다. 수료증을 발급합니다."; + } + if (finalTrueEnding) { return - "성좌심 완전 복구.\n" + - "여섯 요구치가 하나의 마법진으로 닫혔고, 탑이 당신의 문양 언어를 완전히 기억합니다."; + "최종 시험 통과.\n" + + "세 문제를 모두 통과했고, 시험관이 수료증 발급을 준비합니다."; } return "입학 시험 통과.\n" + - "다섯 요구치가 마법진을 다시 일으켰습니다. 남은 조각까지 채우면 성좌심이 완전히 닫힙니다."; + "시험관이 요구한 문양을 확인했습니다."; } private void CelebrateFinalCompletion(bool trueEnding) @@ -4923,12 +7838,84 @@ private void CelebrateFinalCompletion(bool trueEnding) private void TickFloorAdvance() { - if (pendingAdvanceAt < 0f || Time.time < pendingAdvanceAt) + if (floorTransitionState == FloorTransitionState.None || pendingAdvanceAt < 0f) + { + return; + } + + if (floorTransitionState == FloorTransitionState.WaitingForSpeech) + { + if (Time.time < pendingAdvanceAt) + { + return; + } + + if (ShouldDeferFloorAdvanceForSpeech() && Time.time < floorAdvanceSpeechDeadlineAt) + { + pendingAdvanceAt = Time.time + 0.2f; + return; + } + + StartFloorTransitionFade(); + return; + } + + if (floorTransitionState != FloorTransitionState.Fading) + { + return; + } + + var progress = Mathf.Clamp01((Time.time - floorTransitionStartedAt) / FloorTransitionFadeSeconds); + var eased = Mathf.SmoothStep(0f, 1f, progress); + SetPlayerBlinkAlpha(Mathf.Lerp(1f, FloorTransitionMinimumPlayerAlpha, eased)); + if (Time.time >= floorTransitionNextPulseAt) + { + var playerPosition = player == null ? Vector2.zero : (Vector2)player.position; + pulses.Add(new ParticlePulse(playerPosition + Vector2.up * 0.25f, new Color(1f, 0.92f, 0.55f, 0.92f), scaleMultiplier: 0.92f + progress * 0.9f, durationSeconds: 0.72f, sortingOrder: 35)); + audioDirector?.PlaySfx(AudioCue.NoteUnlock, 0.22f + progress * 0.14f); + floorTransitionNextPulseAt = Time.time + FloorTransitionPulseIntervalSeconds; + } + + if (progress < 1f) { return; } + FinishFloorTransition(); + } + + private void BeginFloorTransitionCountdown(float delaySeconds) + { + floorTransitionState = FloorTransitionState.WaitingForSpeech; + pendingAdvanceAt = Time.time + Mathf.Max(0f, delaySeconds); + floorAdvanceSpeechDeadlineAt = pendingAdvanceAt + FloorAdvanceMaxSpeechWaitSeconds; + floorTransitionStartedAt = -1f; + floorTransitionNextPulseAt = -1f; + SetGameplayInputEnabled(false); + } + + private void StartFloorTransitionFade() + { + floorTransitionState = FloorTransitionState.Fading; + floorTransitionStartedAt = Time.time; + floorTransitionNextPulseAt = Time.time; + pendingAdvanceAt = Time.time + FloorTransitionFadeSeconds; + SetGameplayInputEnabled(false); + ClearMovementInputFallback(); + velocity = Vector2.zero; + platformHorizontalVelocity = 0f; + pulses.Add(new ParticlePulse(player == null ? Vector2.zero : (Vector2)player.position, new Color(0.62f, 0.86f, 1f, 0.9f), scaleMultiplier: 1.2f, durationSeconds: 1.0f, sortingOrder: 34)); + audioDirector?.PlaySfx(IsFinalFloor ? AudioCue.CastFinalEffect : AudioCue.FloorComplete, IsFinalFloor ? 0.70f : 0.52f); + } + + private void FinishFloorTransition() + { pendingAdvanceAt = -1f; + floorTransitionState = FloorTransitionState.None; + floorTransitionStartedAt = -1f; + floorAdvanceSpeechDeadlineAt = -1f; + floorTransitionNextPulseAt = -1f; + SetPlayerBlinkAlpha(1f); if (floorController.CurrentFloorIndex < floorController.FloorCount - 1) { LoadFloor(floorController.CurrentFloorIndex + 1); @@ -4938,6 +7925,21 @@ private void TickFloorAdvance() ShowEndingReport(); } + private bool ShouldDeferFloorAdvanceForSpeech() + { + if (mentor == null || !mentor.IsVisible) + { + return false; + } + + if (mentor.HasUnreadSpeechPages) + { + return true; + } + + return mentor.CurrentSpeechPageAgeSeconds < FloorAdvanceSpeechReadGuardSeconds; + } + private void TickPlayer() { if (HasEndingReport || !gameplayInputEnabled) @@ -4959,42 +7961,210 @@ private void TickPlayer() playerAnimator?.SetMotion(input, velocity); } - private void TickPlatformPlayer() + private void TickPlatformPlayer() + { + EnsurePlayerPhysics(); + if (playerBody == null || activeStageDefinition == null) + { + return; + } + + var inputX = ReadHorizontalMovementInput(); + var inputY = ReadVerticalMovementInput(); + platformHorizontalVelocity = Mathf.MoveTowards( + platformHorizontalVelocity, + inputX * PlatformMoveSpeed, + PlatformMoveAcceleration * Time.deltaTime); + + var position = playerBody.position; + var inShelfApproachZone = IsStageShelfApproachZone(position); + var bodyVelocity = playerBody.linearVelocity; + bodyVelocity.x = 0f; + playerBody.gravityScale = inShelfApproachZone ? 0f : PlatformGravityScale; + if (inShelfApproachZone) + { + bodyVelocity.y = 0f; + if (inputY > 0f) + { + fallbackJumpPulseUntil = -1f; + } + } + else + { + var jumpPressed = ReadJumpPressed(); + if (jumpPressed && IsPlatformGrounded()) + { + bodyVelocity.y = PlatformJumpVelocity; + LogActionEvent( + "player_jump", + "player", + "movement", + position: position, + value: LogFloat(PlatformJumpVelocity)); + } + } + + playerBody.linearVelocity = bodyVelocity; + var previousPosition = position; + if (Mathf.Abs(platformHorizontalVelocity) > 0.001f) + { + position.x += platformHorizontalVelocity * Time.deltaTime; + } + + var stoppedAtLedge = TryApplyStageLedgeStop(previousPosition, ref position, inputX, inShelfApproachZone); + if (inShelfApproachZone && Mathf.Abs(inputY) > 0.001f) + { + position.y += inputY * StageShelfVerticalSpeed * Time.deltaTime; + } + + if (inShelfApproachZone) + { + position.y = Mathf.Clamp(position.y, CurrentStageShelfApproachMinY(), CurrentStageShelfApproachTargetPosition().y); + } + + playerBody.position = position; + if (player != null) + { + player.position = position; + } + + if (stoppedAtLedge) + { + playerBody.linearVelocity = Vector2.zero; + } + + ClampPlatformPlayer(); + TickPlatformCamera(); + var animationInput = inShelfApproachZone ? new Vector2(inputX, inputY) : new Vector2(inputX, 0f); + var animationVelocityY = inShelfApproachZone ? inputY * StageShelfVerticalSpeed : playerBody.linearVelocity.y; + playerAnimator?.SetMotion(animationInput, new Vector2(platformHorizontalVelocity, animationVelocityY)); + } + + private bool TryApplyStageLedgeStop(Vector2 previousPosition, ref Vector2 nextPosition, float inputX, bool inShelfApproachZone) + { + RefreshStageLedgeStop(previousPosition); + if (activeStageDefinition == null || + activeStageGates.Count == 0 || + inShelfApproachZone || + Mathf.Abs(inputX) < StageLedgeStopInputThreshold) + { + return false; + } + + var direction = inputX > 0f ? 1 : -1; + if (stageLedgeStopDirection != 0 && stageLedgeStopDirection != direction) + { + ResetStageLedgeStop(); + } + + var gate = FindCrossedStageLedgeGate(previousPosition, nextPosition, direction, out var stopX); + if (gate == null) + { + return false; + } + + if (string.Equals(stageLedgeStopGoalId, gate.requiredGoalId, StringComparison.OrdinalIgnoreCase) && + stageLedgeStopDirection == direction) + { + nextPosition.x = direction > 0 + ? gate.LeftEdge + StageLedgeReleaseNudge + : gate.RightEdge - StageLedgeReleaseNudge; + LogActionEvent( + "stage_ledge_released", + "player", + "movement", + gate.requiredGoalId, + gate.requiredGoalId, + nextPosition, + direction > 0 ? "right" : "left"); + ResetStageLedgeStop(); + return false; + } + + stageLedgeStopGoalId = gate.requiredGoalId; + stageLedgeStopDirection = direction; + nextPosition.x = stopX; + stageLedgeStopPosition = nextPosition; + platformHorizontalVelocity = 0f; + LogActionEvent( + "stage_ledge_stopped", + "system", + "movement", + gate.requiredGoalId, + gate.requiredGoalId, + nextPosition, + direction > 0 ? "right" : "left"); + return true; + } + + private StageGate FindCrossedStageLedgeGate(Vector2 previousPosition, Vector2 nextPosition, int direction, out float stopX) + { + stopX = nextPosition.x; + var playerHalfWidth = CurrentPlatformPlayerHalfWidth(); + foreach (var gate in activeStageGates) + { + if (IsStageGateCompleted(gate) || !gate.IsVerticallyAligned(previousPosition, nextPosition)) + { + continue; + } + + var edgeX = direction > 0 ? gate.LeftEdge : gate.RightEdge; + var previousLeadingX = previousPosition.x + direction * playerHalfWidth; + var nextLeadingX = nextPosition.x + direction * playerHalfWidth; + var crossesEdge = direction > 0 + ? previousLeadingX < edgeX && nextLeadingX >= edgeX + : previousLeadingX > edgeX && nextLeadingX <= edgeX; + var alreadyPushingAtEdge = direction > 0 + ? previousLeadingX >= edgeX - StageLedgeStopPadding && previousPosition.x < edgeX + : previousLeadingX <= edgeX + StageLedgeStopPadding && previousPosition.x > edgeX; + if (!crossesEdge && !alreadyPushingAtEdge) + { + continue; + } + + stopX = edgeX - direction * (playerHalfWidth + StageLedgeStopPadding); + return gate; + } + + return null; + } + + private void RefreshStageLedgeStop(Vector2 position) { - EnsurePlayerPhysics(); - if (playerBody == null || activeStageDefinition == null) + if (string.IsNullOrWhiteSpace(stageLedgeStopGoalId)) { return; } - var inputX = ReadHorizontalMovementInput(); - platformHorizontalVelocity = Mathf.MoveTowards( - platformHorizontalVelocity, - inputX * PlatformMoveSpeed, - PlatformMoveAcceleration * Time.deltaTime); - - var bodyVelocity = playerBody.linearVelocity; - bodyVelocity.x = 0f; - if (ReadJumpPressed() && IsPlatformGrounded()) + var completed = FindActiveGoal(stageLedgeStopGoalId)?.completed == true; + var movedAwayHorizontally = Mathf.Abs(position.x - stageLedgeStopPosition.x) > StageLedgeStopResetDistance; + var movedAwayVertically = Mathf.Abs(position.y - stageLedgeStopPosition.y) > StageLedgeStopResetDistance * 2.5f; + if (completed || movedAwayHorizontally || movedAwayVertically) { - bodyVelocity.y = PlatformJumpVelocity; + ResetStageLedgeStop(); } + } - playerBody.linearVelocity = bodyVelocity; - if (Mathf.Abs(platformHorizontalVelocity) > 0.001f) + private void ResetStageLedgeStop() + { + stageLedgeStopGoalId = ""; + stageLedgeStopDirection = 0; + stageLedgeStopPosition = Vector2.zero; + } + + private bool IsStageGateCompleted(StageGate gate) + { + return gate != null && FindActiveGoal(gate.requiredGoalId)?.completed == true; + } + + private float CurrentPlatformPlayerHalfWidth() + { + if (playerCollider != null && playerCollider.size.x > 0.01f) { - var position = playerBody.position; - position.x += platformHorizontalVelocity * Time.deltaTime; - playerBody.position = position; - if (player != null) - { - player.position = position; - } + return playerCollider.size.x * 0.5f; } - ClampPlatformPlayer(); - TickPlatformCamera(); - playerAnimator?.SetMotion(new Vector2(inputX, 0f), new Vector2(platformHorizontalVelocity, playerBody.linearVelocity.y)); + return 0.28f; } private Vector2 ReadMovementInput() @@ -5016,6 +8186,14 @@ private float ReadHorizontalMovementInput() Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow) || IsFallbackActive(fallbackRightHeld, fallbackRightPulseUntil)); } + private float ReadVerticalMovementInput() + { + return ResolveAxisWithKeys( + Input.GetAxisRaw("Vertical"), + Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow) || IsFallbackActive(fallbackDownHeld, fallbackDownPulseUntil), + Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow) || IsFallbackActive(fallbackUpHeld, fallbackUpPulseUntil)); + } + private bool ReadJumpPressed() { var fallbackJumpPressed = IsFallbackActive(false, fallbackJumpPulseUntil); @@ -5038,21 +8216,26 @@ private void CaptureKeyboardFallback(KeyCode keyCode, bool pressed) case KeyCode.LeftArrow: case KeyCode.A: SetFallbackKey(ref fallbackLeftHeld, ref fallbackLeftPulseUntil, pressed); + LogMovementKeyEvent("left", keyCode, pressed); break; case KeyCode.RightArrow: case KeyCode.D: SetFallbackKey(ref fallbackRightHeld, ref fallbackRightPulseUntil, pressed); + LogMovementKeyEvent("right", keyCode, pressed); break; case KeyCode.DownArrow: case KeyCode.S: SetFallbackKey(ref fallbackDownHeld, ref fallbackDownPulseUntil, pressed); + LogMovementKeyEvent("down", keyCode, pressed); break; case KeyCode.UpArrow: case KeyCode.W: SetFallbackKey(ref fallbackUpHeld, ref fallbackUpPulseUntil, pressed); + LogMovementKeyEvent("up", keyCode, pressed); if (pressed) { fallbackJumpPulseUntil = Time.unscaledTime + KeyboardMovementPulseSeconds; + LogMovementKeyEvent("jump", keyCode, true); } break; @@ -5060,12 +8243,30 @@ private void CaptureKeyboardFallback(KeyCode keyCode, bool pressed) if (pressed) { fallbackJumpPulseUntil = Time.unscaledTime + KeyboardMovementPulseSeconds; + LogMovementKeyEvent("jump", keyCode, true); + } + else + { + LogMovementKeyEvent("jump", keyCode, false); } break; } } + private void LogMovementKeyEvent(string control, KeyCode keyCode, bool pressed) + { + LogActionEvent( + pressed ? "input_key_down" : "input_key_up", + "player", + "movement", + value: control, + payloadJson: BuildEventPayload( + Pair("key", keyCode.ToString()), + Pair("control", control), + Pair("pressed", pressed ? "true" : "false"))); + } + private void ClearMovementInputFallback() { fallbackLeftHeld = false; @@ -5149,7 +8350,16 @@ private void ClampPlatformPlayer() } position.x = Mathf.Clamp(position.x, activeStageDefinition.stageMin.x, activeStageDefinition.stageMax.x); + if (IsStageShelfApproachZone(position)) + { + position.y = Mathf.Clamp(position.y, CurrentStageShelfApproachMinY(), CurrentStageShelfApproachTargetPosition().y); + } + playerBody.position = position; + if (player != null) + { + player.position = position; + } } private void TickPlatformCamera() @@ -5166,6 +8376,12 @@ private void TickPlatformCamera() private void ResetPlayerToSafePosition(string note) { MovePlayerTo(safePosition); + LogActionEvent( + "player_reset_to_safe_position", + "system", + "stage", + position: safePosition, + payloadJson: BuildEventPayload(Pair("note", TruncateForLog(note ?? "", 500)))); if (!string.IsNullOrWhiteSpace(note)) { ShowMagicNote(note, MentorMood.Frown); @@ -5187,6 +8403,7 @@ private void MovePlayerTo(Vector2 worldPosition) velocity = Vector2.zero; platformHorizontalVelocity = 0f; + ResetStageLedgeStop(); } private void TickStageGates() @@ -5215,6 +8432,14 @@ private void TickStageGates() TakePlayerDamage("장애물 접촉", gate.center); MovePlayerTo(gate.resetPosition); + LogActionEvent( + "player_reset_to_safe_position", + "system", + "stage", + gate.requiredGoalId, + gate.requiredGoalId, + gate.resetPosition, + payloadJson: BuildEventPayload(Pair("reason", "stage_gate"))); ShowMagicNote(gate.lockedNote, MentorMood.Frown); pulses.Add(new ParticlePulse(gate.center, new Color(0.72f, 0.88f, 1f), weak: true)); return; @@ -5316,13 +8541,14 @@ private static string CombatStatusLabel(CustomSpellEffectKind effect) { return effect switch { - CustomSpellEffectKind.Ice => "감속", - CustomSpellEffectKind.Electric => "감전", + CustomSpellEffectKind.Ice => "냉각", + CustomSpellEffectKind.Electric => "회로", CustomSpellEffectKind.Cleanse => "정화", CustomSpellEffectKind.Focus => "표식", CustomSpellEffectKind.Flow => "흐름", CustomSpellEffectKind.Connection => "속박", - CustomSpellEffectKind.Stability => "방벽", + CustomSpellEffectKind.Steel => "강철", + CustomSpellEffectKind.Stability => "지지", _ => "상태" }; } @@ -5337,6 +8563,7 @@ private static CustomShapeEventKind CombatStatusEventKind(CustomSpellEffectKind CustomSpellEffectKind.Focus => CustomShapeEventKind.MagicAmplify, CustomSpellEffectKind.Flow => CustomShapeEventKind.MoveSpeedBuff, CustomSpellEffectKind.Connection => CustomShapeEventKind.AttackBuff, + CustomSpellEffectKind.Steel => CustomShapeEventKind.GuardBuff, CustomSpellEffectKind.Stability => CustomShapeEventKind.GuardBuff, _ => CustomShapeEventKind.PiercingMark }; @@ -5350,6 +8577,7 @@ private static float CombatStatusDuration(CustomSpellEffectKind effect) CustomSpellEffectKind.Electric => 4.4f, CustomSpellEffectKind.Cleanse => 3.6f, CustomSpellEffectKind.Focus => 6.0f, + CustomSpellEffectKind.Steel => 6.2f, CustomSpellEffectKind.Stability => 6.5f, _ => 5.0f }; @@ -5363,11 +8591,34 @@ private static Color CombatStatusColor(CustomSpellEffectKind effect, Color fallb CustomSpellEffectKind.Electric => new Color(1f, 0.88f, 0.18f), CustomSpellEffectKind.Cleanse => new Color(0.42f, 0.74f, 1f), CustomSpellEffectKind.Focus => new Color(1f, 0.58f, 0.18f), + CustomSpellEffectKind.Steel => new Color(0.72f, 0.72f, 0.66f), CustomSpellEffectKind.Stability => new Color(0.74f, 0.55f, 0.32f), _ => fallback }; } + private static Color SealTint(CompiledSeal seal) + { + if (seal != null && seal.HasCustomEffectSeal) + { + return CombatStatusColor(seal.customEffectKind, FamilyColor(seal.baseFamily)); + } + + return seal == null ? Color.white : FamilyColor(seal.baseFamily); + } + + private static Color SealSecondaryTint(CompiledSeal seal) + { + return Color.Lerp(SealTint(seal), Color.white, seal != null && seal.HasCustomEffectSeal ? 0.24f : 0.46f); + } + + private static Color SealGuideTint(CompiledSeal seal) + { + var tint = Color.Lerp(SealTint(seal), Color.white, seal != null && seal.HasCustomEffectSeal ? 0.12f : 0.26f); + tint.a = seal != null && seal.HasCustomEffectSeal ? 0.62f : 0.50f; + return tint; + } + private static Color DamageColorFor(CustomSpellEffectKind? effect) { return effect switch @@ -5375,6 +8626,7 @@ private static Color DamageColorFor(CustomSpellEffectKind? effect) CustomSpellEffectKind.Electric => new Color(1f, 0.80f, 0.18f), CustomSpellEffectKind.Ice => new Color(0.65f, 0.92f, 1f), CustomSpellEffectKind.Focus => new Color(1f, 0.46f, 0.18f), + CustomSpellEffectKind.Steel => new Color(0.82f, 0.82f, 0.76f), CustomSpellEffectKind.Stability => new Color(0.95f, 0.64f, 0.32f), _ => new Color(1f, 0.88f, 0.30f) }; @@ -5400,6 +8652,14 @@ private void TickHazards() TakePlayerDamage("위험 지대 접촉", hazard.position); player.position = safePosition; velocity = Vector2.zero; + LogActionEvent( + "player_reset_to_safe_position", + "system", + "stage", + hazard.title, + hazard.title, + safePosition, + payloadJson: BuildEventPayload(Pair("reason", "hazard"))); ShowMagicNote( "균열이 몸을 밀어냈습니다. 가까운 안전 지점에서 다시 시작합니다.\n" + "다음: 아직 완료하지 않은 고정 목표를 닿기 쉬운 안전 지점 앞에서 다시 겨냥하세요.", @@ -5431,7 +8691,7 @@ private void TickFirstFloorOnboarding() var goal = CurrentFirstFloorTutorialGoal() ?? activeGoals.FirstOrDefault(item => item.requiredBase.HasValue); if (goal != null && goal.requiredBase.HasValue) { - ShowMagicNote("우클릭 누르고\n흐릿한 선만 따라 그려 봐.", MentorMood.Neutral); + ShowMagicNote("우클릭을 누르고\n흐릿한 선만 따라 그려 주세요.", MentorMood.Neutral); PlayGhostGesture(goal.requiredBase.Value, goal.position); } } @@ -5439,7 +8699,7 @@ private void TickFirstFloorOnboarding() if (!firstFloorLongSilenceShown && elapsed >= 300f) { firstFloorLongSilenceShown = true; - ShowMagicNote("표식 바로 옆에서 시작해 봐.\n물은 둥글게, 바람은 세 줄이야.", MentorMood.Neutral); + ShowMagicNote("표식 바로 옆에서 시작해 주세요.\n물은 둥글게, 바람은 세 줄입니다.", MentorMood.Neutral); } } @@ -5504,6 +8764,7 @@ private void TickGhostTraces() private void TickSeals() { + var inputInProgress = worldDrawing != null && worldDrawing.HasBufferedInput; for (var index = seals.Count - 1; index >= 0; index--) { var seal = seals[index]; @@ -5516,7 +8777,12 @@ private void TickSeals() } seal.Tick(Time.time, remaining / Mathf.Max(seal.seal.expiresAt - seal.seal.createdAt, 0.1f)); - if (seal.TryTriggerDefaultFallback(Time.time)) + if (inputInProgress) + { + seal.MarkPostSealInputSeen(Time.time); + } + + if (seal.TryTriggerDefaultFallback(Time.time, inputInProgress)) { TriggerDefaultSealFallback(seal); ConsumeSeal(seal); @@ -5547,6 +8813,14 @@ private void TickHostileEntityContacts() TakePlayerDamage("적대적 entity 접촉", targetPosition); MovePlayerTo(safePosition); + LogActionEvent( + "player_reset_to_safe_position", + "system", + "stage", + goal.id, + goal.title, + safePosition, + payloadJson: BuildEventPayload(Pair("reason", "hostile_entity"))); ShowMagicNote("적대적 대상에 너무 가까이 닿았습니다. 거리를 두고 다시 시도하세요.", MentorMood.Frown); pulses.Add(new ParticlePulse(targetPosition, goal.color, weak: true, scaleMultiplier: 1.1f, durationSeconds: 0.8f, sortingOrder: 32)); return; @@ -5563,6 +8837,16 @@ private bool TakePlayerDamage(string reason, Vector2 sourcePosition) playerHealthHalfUnits = Mathf.Max(0, playerHealthHalfUnits - 1); playerDamageInvulnerableUntil = Time.time + PlayerDamageInvulnerabilitySeconds; playerBlinkUntil = Time.time + PlayerDamageBlinkSeconds; + LogActionEvent( + "player_damage", + "system", + "stage", + targetLabel: reason, + position: sourcePosition, + value: playerHealthHalfUnits.ToString(CultureInfo.InvariantCulture), + payloadJson: BuildEventPayload( + Pair("reason", reason ?? ""), + Pair("remainingHealthHalfUnits", playerHealthHalfUnits.ToString(CultureInfo.InvariantCulture)))); RefreshPlayerBlinkRenderers(); RefreshHealthUi(); ShowDamagePopup((Vector2)player.position + new Vector2(0f, 0.95f), "-1/2", new Color(1f, 0.20f, 0.18f)); @@ -5675,7 +8959,10 @@ private void TriggerDefaultSealFallback(SealView seal) var duration = Mathf.Max(1.2f, seal.seal.expiresAt - Time.time); defaultBarriers.Add(new CharacterBarrierView(player, seal.seal.sealId, color, duration)); pulses.Add(new ParticlePulse(player.position, color, weak: true, scaleMultiplier: 1.05f, durationSeconds: 0.8f, sortingOrder: 31)); - ShowMagicNote($"{SpellLabels.Korean(seal.seal.baseFamily)} 기초 속성 마법진을 그렸어. 보호막으로 굳었어.", MentorMood.Neutral); + var label = seal.seal.HasCustomEffectSeal + ? CustomSpellEffectCatalog.Korean(seal.seal.customEffectKind) + : SpellLabels.Korean(seal.seal.baseFamily); + ShowMagicNote($"{label} 문양이 잠시 빛난 뒤 기본 보호막이 되었습니다.", MentorMood.Neutral); } private void UpdateHud() @@ -5692,14 +8979,38 @@ private void UpdateHud() UpdateResultPanelLayout(); var floor = floorController.Current; + if (!finalCompletionCelebrated && IsFinalFloor && currentFinalTask != null) + { + var finalCompleted = activeGoals.Count(goal => goal.completed); + var finalQuestionNumber = currentFinalTaskIndex + 1; + var finalQuestionTotal = Mathf.Max(currentFinalTasks.Count, 1); + hudTitle.text = practiceMode ? $"연습실 - {floor.title}" : $"층 {floor.number}: {floor.title}"; + hudCopy.text = $"{floor.objective}\n시험관 질문 {finalQuestionNumber}/{finalQuestionTotal}: {currentFinalTask.prompt}"; + floorProgress.text = $"탑 진행 {floorController.CurrentFloorNumber}/{floorController.FloorCount} 최종 문제 {finalCompleted}/{activeGoals.Count} {currentFinalTask.goal.RequirementLabel} 입력 {BuildInputPhaseLabel()}"; + RefreshMagicNotePanelVisibility(); + noteText.text = magicNote.Text; + RefreshQuestLogText(); + return; + } + if (finalCompletionCelebrated && IsFinalFloor && pendingAdvanceAt > 0f) { var completedFinal = activeGoals.Count(goal => goal.completed); - hudTitle.text = finalTrueEnding ? "성좌심 완전 복구" : "입학 시험 통과"; + if (finalCertificateQueued) + { + hudTitle.text = "수료증 발급"; + hudCopy.text = "최종 시험관이 통과를 확인했습니다.\n곧 수료증 화면이 열립니다."; + floorProgress.text = $"탑 진행 {floorController.CurrentFloorNumber}/{floorController.FloorCount} 최종 문제 {completedFinal}/{activeGoals.Count}"; + RefreshMagicNotePanelVisibility(); + noteText.text = magicNote.Text; + RefreshQuestLogText(); + return; + } + hudTitle.text = finalTrueEnding ? "최종 시험 통과" : "입학 시험 통과"; hudCopy.text = finalTrueEnding - ? "여섯 요구치가 하나의 마법진으로 닫혔습니다.\n곧 완전 복구 보고서가 열립니다." - : "다섯 요구치로 입학 마법진이 다시 섰습니다.\n곧 통과 보고서가 열립니다."; - floorProgress.text = $"탑 진행 {floorController.CurrentFloorNumber}/{floorController.FloorCount} 목표 {completedFinal}/{activeGoals.Count} final seal"; + ? "최종 시험 세 문제가 모두 확인됐습니다.\n곧 수료 보고서가 열립니다." + : "입학 마법진이 다시 섰습니다.\n곧 통과 보고서가 열립니다."; + floorProgress.text = $"탑 진행 {floorController.CurrentFloorNumber}/{floorController.FloorCount} 최종 문제 {completedFinal}/{activeGoals.Count}"; RefreshMagicNotePanelVisibility(); noteText.text = magicNote.Text; RefreshQuestLogText(); @@ -5711,7 +9022,7 @@ private void UpdateHud() if (IsFinalFloor) { hudCopy.text = $"{floor.objective}\n남은 요구: {BuildRemainingFinalGoalSummary()}"; - floorProgress.text = $"탑 진행 {floorController.CurrentFloorNumber}/{floorController.FloorCount} 목표 {completed}/{activeGoals.Count} 다음 {BuildNextFinalGoalShortLabel()}"; + floorProgress.text = $"탑 진행 {floorController.CurrentFloorNumber}/{floorController.FloorCount} 목표 {completed}/{activeGoals.Count} 다음 {BuildNextFinalGoalShortLabel()} 입력 {BuildInputPhaseLabel()}"; } else { @@ -5721,23 +9032,59 @@ private void UpdateHud() } else if (floor.number == CustomReferenceFloorNumber) { - hudCopy.text = $"{floor.objective}\n좌측 책장 접근 -> 레퍼런스 보기 -> 들여오기 후 표식 근처에 커스텀 도형을 그리세요."; + hudCopy.text = $"{floor.objective}\n시험관이 현재 과제 도형을 슬롯에 넣어 줍니다. 표식 근처에서 기본 문양 뒤 이어 그리세요."; } else if (floor.number == 3) { - hudCopy.text = $"{floor.objective}\nA/D 또는 ←/→ 이동, Space 또는 ↑ 점프. 시작 책장에서 3층 프리셋을 가져온 뒤 강물/구멍/낭떠러지/빈 공간 표식 위에 도형을 얹으세요."; + hudCopy.text = $"{floor.objective}\nA/D 또는 ←/→ 이동, Space 점프. 시험관이 필요한 도형을 넣어 두었으니 표식 근처에서 순서대로 사용하세요."; + } + else if (floor.number == 4) + { + hudCopy.text = $"{floor.objective}\n속성 문양을 만든 뒤 원 안에 화살표를 이어 그려 허수아비를 직접 맞히세요."; } else { hudCopy.text = $"{floor.objective}\nWASD 또는 방향키 이동 / 우클릭 hold로 바닥에 직접 문양을 그리세요. Esc/Backspace 취소."; } - floorProgress.text = $"탑 진행 {floorController.CurrentFloorNumber}/{floorController.FloorCount} 목표 {completed}/{activeGoals.Count} seal {seals.Count}"; + floorProgress.text = $"탑 진행 {floorController.CurrentFloorNumber}/{floorController.FloorCount} 목표 {completed}/{activeGoals.Count} 문양 {seals.Count} 입력 {BuildInputPhaseLabel()}"; } RefreshMagicNotePanelVisibility(); noteText.text = magicNote.Text; RefreshQuestLogText(); } + private string BuildInputPhaseLabel() + { + if (worldDrawing != null && worldDrawing.HasActiveStrokeCenter) + { + return FindAttachableSeal(worldDrawing.ActiveStrokeCenter) != null + ? "추가 도형 입력 중" + : "새 기본 문양 입력 중"; + } + + if (HasActiveSeal(Time.time)) + { + return "추가 도형 차례"; + } + + return "새 기본 문양"; + } + + private void RefreshWorldDrawingInputColor() + { + if (worldDrawing == null) + { + return; + } + + var followupStroke = worldDrawing.HasActiveStrokeCenter && + FindAttachableSeal(worldDrawing.ActiveStrokeCenter) != null; + var color = followupStroke + ? new Color(1f, 0.82f, 0.32f, 0.92f) + : new Color(0.22f, 0.95f, 1f, 0.92f); + worldDrawing.SetStrokeColor(color); + } + private void RefreshMagicNotePanelVisibility() { if (notePanel == null) @@ -5767,17 +9114,27 @@ private string BuildFloorEntryNote(FloorDefinition floor) var lore = FloorEntryLore(floor.number); if (floor.number == 1) { - return $"{floor.entryNote}\n{lore}\n{BuildFirstFloorGoalHint()}"; + return $"{floor.entryNote}\n{lore}\n{BuildFirstFloorGoalHint()}\n다음 층부터 필요한 추가 도형은 시험관이 슬롯에 넣어 줍니다."; } if (floor.number == CustomReferenceFloorNumber) { - return $"{floor.entryNote}\n{lore}\n좌측 책장 근처에서 말풍선의 보기 버튼을 누르면 base별 커스텀 도형을 슬롯에 들여올 수 있습니다."; + return $"{floor.entryNote}\n{lore}\n시험관이 현재 과제에 필요한 도형을 슬롯에 넣어 줍니다. 기본 문양이 빛나면 원 안에 이어 그리세요."; } if (floor.number == 3) { - return $"{floor.entryNote}\n{lore}\n시작 구간의 책장에서 3층 프리셋 도형을 가져온 뒤 강물, 깨진 구멍, 낭떠러지, 빈 공간 표식 근처에서 기본 문양과 커스텀 도형을 순서대로 사용하세요."; + return $"{floor.entryNote}\n{lore}\n시험관이 3층 도형을 슬롯에 넣어 두었습니다. 강물, 구멍, 다리 표식 근처에서 기본 문양 뒤 이어 그리세요."; + } + + if (floor.number == 4) + { + return $"{floor.entryNote}\n{lore}\n허수아비를 직접 맞혀야 통과합니다. 속성 문양이 빛나면 원 안에 화살표를 이어 그리세요."; + } + + if (IsFinalFloor && currentFinalTask != null) + { + return $"{floor.entryNote}\n{lore}\n시험관 질문 {currentFinalTaskIndex + 1}/{Mathf.Max(currentFinalTasks.Count, 1)}: {currentFinalTask.prompt}"; } return IsFinalFloor @@ -5793,7 +9150,7 @@ private static string FloorEntryLore(int floorNumber) 2 => "벽화의 장식 문양은 먼저 지나간 입학생들의 서명이다.", 3 => "다리는 오래전에 끊겼고, 아무도 같은 방법으로 건너지 않았다.", 4 => "균열은 탑이 늙어가는 속도다.", - _ => "성좌심은 통과한 이름들을 별자리로 기억한다." + _ => "최종 시험관은 네가 배운 문양을 하나씩 확인한다." }; } @@ -5836,6 +9193,17 @@ private string BuildGoalDiscoveryNote(WorldStateGoal goal) return goal.discoveryNote; } + if (currentFinalTask != null) + { + var completed = activeGoals.Count(activeGoal => activeGoal.completed); + if (completed >= CurrentFinalPassingGoalCount()) + { + return $"{goal.discoveryNote}\n세 문제를 모두 통과했습니다. 수료증을 발급합니다."; + } + + return $"{goal.discoveryNote}\n이 문제는 확인했습니다. 다음 문제가 나타납니다."; + } + return $"{goal.discoveryNote}\n{BuildNextFinalGoalHint()}"; } @@ -5844,39 +9212,61 @@ private string BuildRemainingFinalGoalSummary() var remaining = activeGoals.Where(goal => !goal.completed).ToList(); if (remaining.Count == 0) { - return "모든 요구치 완료"; + return "최종 문제 모두 완료"; } - var shown = remaining.Take(2).Select(goal => $"{goal.title}({goal.RequirementLabel})"); + var shown = remaining.Take(2).Select(goal => $"문제 {activeGoals.IndexOf(goal) + 1}: {goal.title}({goal.RequirementLabel})"); var suffix = remaining.Count > 2 ? $" 외 {remaining.Count - 2}" : ""; return string.Join(" / ", shown) + suffix; } private string BuildNextFinalGoalHint() { - var next = activeGoals.FirstOrDefault(goal => !goal.completed); + var next = CurrentFinalActiveGoal() ?? activeGoals.FirstOrDefault(goal => !goal.completed); if (next == null) { - return "다음 목표: 모든 요구치가 채워졌습니다."; + return "다음 문제: 최종 문제를 모두 통과했습니다."; } - return $"다음 목표: {next.title} - {next.RequirementLabel}을 목표 표식 근처에서 완성하세요."; + return $"다음 문제 {activeGoals.IndexOf(next) + 1}/{Mathf.Max(activeGoals.Count, 1)}: {next.title} - {next.RequirementLabel}을 보이는 표식 근처에서 완성하세요."; } private string BuildNextFinalGoalShortLabel() { - var next = activeGoals.FirstOrDefault(goal => !goal.completed); - return next == null ? "완료" : $"{next.title}({next.RequirementLabel})"; + var next = CurrentFinalActiveGoal() ?? activeGoals.FirstOrDefault(goal => !goal.completed); + return next == null ? "완료" : $"문제 {activeGoals.IndexOf(next) + 1}: {next.title}({next.RequirementLabel})"; } private void ShowEndingReport() { + pendingAdvanceAt = -1f; + floorTransitionState = FloorTransitionState.None; + floorTransitionStartedAt = -1f; + floorAdvanceSpeechDeadlineAt = -1f; + floorTransitionNextPulseAt = -1f; + SetPlayerBlinkAlpha(1f); SaveCurrentQuestChecklistScore("ending"); reportPanel.gameObject.SetActive(true); + SetGameplayInputEnabled(false); + finalCertificateAwaitingExit = finalCertificateQueued; notePanel.gameObject.SetActive(false); resultPanel.gameObject.SetActive(false); floorSkipButton.gameObject.SetActive(false); + healthPanel.gameObject.SetActive(false); questScrollPanel.gameObject.SetActive(false); + if (finalTaskBanner != null) + { + finalTaskBanner.gameObject.SetActive(false); + } + if (endingReportExitButton != null) + { + endingReportExitButton.gameObject.SetActive(finalCertificateQueued); + endingReportExitButton.interactable = true; + if (endingReportExitButtonText != null) + { + endingReportExitButtonText.text = "종료"; + } + } audioDirector?.PlaySfx(AudioCue.EndingReportOpened, 0.88f); mentor?.Say(MentorMood.Neutral, ""); if (toastPanel != null) @@ -5885,8 +9275,17 @@ private void ShowEndingReport() toastPanel.gameObject.SetActive(false); } var completedFinalGoals = IsFinalFloor ? activeGoals.Count(goal => goal.completed) : activeGoals.Count; - hudTitle.text = finalTrueEnding ? "입학 시험 완전 통과" : "입학 시험 통과"; - hudCopy.text = finalTrueEnding ? "입학 마법진이 완전히 밝아졌습니다." : "입학 마법진이 다시 밝아졌습니다."; + hudTitle.text = finalTrueEnding ? "최종 시험 통과" : "입학 시험 통과"; + hudCopy.text = finalTrueEnding ? "최종 시험관이 세 문제 통과를 확인했습니다." : "입학 마법진이 다시 밝아졌습니다."; + if (finalCertificateQueued) + { + hudTitle.text = "수료증 발급 완료"; + hudCopy.text = "최종 시험관이 통과를 확인했고 수료증을 발급했습니다."; + } + + var taskSummary = currentFinalTasks.Count == 0 + ? currentFinalTask?.prompt ?? "최종 문제" + : string.Join("\n", currentFinalTasks.Select((task, index) => $"{index + 1}. {task.prompt}")); logger.LogSurvey(new SurveyLog { sessionId = sessionId, @@ -5899,7 +9298,7 @@ private void ShowEndingReport() completedTrials = endingReport.DiscoveryCount, totalAttempts = trialCounter }); - logger.WriteSessionResult(new SessionResultContextLog + var sessionResult = logger.WriteSessionResult(new SessionResultContextLog { sessionId = sessionId, buildVersion = BuildVersion, @@ -5912,6 +9311,20 @@ private void ShowEndingReport() totalFinalGoals = activeGoals.Count, discoveryCount = endingReport.DiscoveryCount }); + var certificateCsvPath = ""; + if (finalCertificateQueued) + { + certificateCsvPath = logger.WriteCertificateCsv(BuildCertificateLog(sessionResult, completedFinalGoals, activeGoals.Count, taskSummary)); + } + LogActionEvent( + "ending_report_shown", + "system", + finalCertificateQueued ? "certificate" : "ending", + value: finalCertificateQueued ? "certificate_ready" : "report_ready", + payloadJson: BuildEventPayload( + Pair("sessionResultCsv", logger?.SessionResultCsvPath ?? ""), + Pair("certificateCsv", certificateCsvPath))); + WriteEnrollmentSnapshot(finalCertificateQueued ? "certificate_ready" : "ending_report_shown"); reportText.text = endingReport.BuildText( trialCounter, OutputDirectory, @@ -5919,23 +9332,84 @@ private void ShowEndingReport() completedFinalGoals, activeGoals.Count, magicNote.DiscoveryExcerpts(3)); + if (finalCertificateQueued) + { + var downloadPath = string.IsNullOrWhiteSpace(certificateCsvPath) ? OutputDirectory : certificateCsvPath; + reportText.text = $"수료증\n최종 시험 통과를 확인했습니다.\n문제:\n{taskSummary}\n\nCSV 다운로드 파일: {downloadPath}\n\n{reportText.text}"; + } + } + + private CertificateLog BuildCertificateLog(SessionResultLog sessionResult, int completedFinalGoals, int totalFinalGoals, string taskSummary) + { + return new CertificateLog + { + sessionId = sessionId, + issuedAtUtc = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture), + buildVersion = BuildVersion, + title = "수료증", + recipient = "플레이어", + status = finalTrueEnding ? "최종 시험 통과" : "입학 시험 통과", + certificateNote = finalTrueEnding + ? "최종 시험 세 문제를 통과하여 수료증을 발급합니다." + : "입학 시험 통과 기록입니다.", + finalTasks = (taskSummary ?? "").Replace("\r", "", StringComparison.Ordinal).Replace("\n", " | ", StringComparison.Ordinal), + completedFinalGoals = completedFinalGoals, + totalFinalGoals = totalFinalGoals, + totalElapsedMs = sessionResult?.totalElapsedMs ?? Mathf.RoundToInt((Time.time - sessionStartedAt) * 1000f), + totalAttempts = sessionResult?.totalAttempts ?? trialCounter, + totalSuccess = sessionResult?.totalSuccess ?? activeGoals.Count(goal => goal.completed), + successRate = sessionResult?.successRate ?? 0f, + questCompletionRate = sessionResult?.questCompletionRate ?? 0f, + outputDirectory = OutputDirectory + }; + } + + private void FinishEndingReport() + { + if (!finalCertificateAwaitingExit && HasEndingReport) + { + return; + } + + finalCertificateAwaitingExit = false; + SetGameplayInputEnabled(false); + if (endingReportExitButton != null) + { + endingReportExitButton.interactable = false; + endingReportExitButton.gameObject.SetActive(false); + } + + if (hudTitle != null) + { + hudTitle.text = "수료증 확인 완료"; + } + + if (hudCopy != null) + { + hudCopy.text = "최종 시험을 마쳤습니다."; + } + + if (!Application.isEditor) + { + Application.Quit(); + } } private SealView CreateSealView(CompiledSeal seal) { - var root = CreateWorldSprite($"Seal {seal.sealId}", seal.worldCenter, Vector3.one * Mathf.Max(seal.worldScale * 1.08f, 0.9f), FamilyColor(seal.baseFamily), Color.white, PixelSpriteKind.RuneCircle, 18); + var root = CreateWorldSprite($"Seal {seal.sealId}", seal.worldCenter, Vector3.one * Mathf.Max(seal.worldScale * 1.08f, 0.9f), SealTint(seal), SealSecondaryTint(seal), PixelSpriteKind.RuneCircle, 18); var guide = CreateOverlayAttachGuide(seal, root.transform); - guide.SetActive(false); + guide.SetActive(true); var labelObject = new GameObject("Rune Label"); labelObject.transform.SetParent(root.transform, false); - labelObject.transform.localPosition = new Vector3(0f, 0.78f, 0f); + labelObject.transform.localPosition = new Vector3(0f, 0.96f, 0f); var canvasObject = new GameObject("Rune Label Canvas"); canvasObject.transform.SetParent(labelObject.transform, false); var worldCanvas = canvasObject.AddComponent(); worldCanvas.renderMode = RenderMode.WorldSpace; worldCanvas.sortingOrder = 40; var rect = canvasObject.GetComponent() ?? canvasObject.AddComponent(); - rect.sizeDelta = new Vector2(2.7f, 0.45f); + rect.sizeDelta = new Vector2(3.3f, 0.72f); canvasObject.transform.localScale = Vector3.one * 0.012f; var textObject = new GameObject("Text"); textObject.transform.SetParent(canvasObject.transform, false); @@ -5946,14 +9420,18 @@ private SealView CreateSealView(CompiledSeal seal) textRect.offsetMax = Vector2.zero; var text = textObject.AddComponent(); text.font = uiFont; - text.fontSize = 26; + text.fontSize = 22; text.alignment = TextAnchor.MiddleCenter; - text.color = Color.white; + text.color = Color.Lerp(SealTint(seal), Color.white, 0.35f); text.text = seal.Label; + AddTextOutline(text); var defaultFallbackAt = string.IsNullOrWhiteSpace(seal.customShapeId) ? seal.createdAt + DefaultSealFallbackDelaySeconds : float.PositiveInfinity; - return new SealView(root, seal, text, guide, SpellCastingService.AttachRadiusFor(seal), defaultFallbackAt); + var view = new SealView(root, seal, text, guide, SpellCastingService.AttachRadiusFor(seal), defaultFallbackAt); + view.RefreshLabel(uiFont); + view.RefreshTint(SealTint(seal), SealSecondaryTint(seal), SealGuideTint(seal)); + return view; } private GameObject CreateOverlayAttachGuide(CompiledSeal seal, Transform parent) @@ -5963,9 +9441,8 @@ private GameObject CreateOverlayAttachGuide(CompiledSeal seal, Transform parent) guide.transform.localPosition = Vector3.zero; guide.transform.localScale = Vector3.one * SpellCastingService.AttachRadiusFor(seal); var renderer = guide.AddComponent(); - var color = Color.Lerp(FamilyColor(seal.baseFamily), Color.white, 0.18f); - color.a = 0.34f; - renderer.sprite = PixelArtFactory.CreateSprite($"Attach Guide {seal.sealId}", color, Color.white, PixelSpriteKind.Pulse); + var color = SealGuideTint(seal); + renderer.sprite = PixelArtFactory.CreateSprite($"Attach Guide {seal.sealId}", color, SealSecondaryTint(seal), PixelSpriteKind.Pulse); renderer.sharedMaterial = PixelMaterialProvider.SpriteMaterial; renderer.color = color; renderer.sortingOrder = 17; @@ -6037,6 +9514,21 @@ private void LogBaseAttempt(BaseRecognitionResult result, CompiledSeal seal, str assistLevel = hintState?.AssistLevelNumber ?? (success ? 0 : 1), assisted = hintState?.assisted ?? false }); + LogActionEvent( + success ? "spell_attempt_success" : "spell_attempt_failure", + "player", + SpellPhase.Base.ToString(), + result.spell.intentGoalId ?? "", + worldEffect, + result.center, + result.spell.RecognizedFamilyText, + BuildEventPayload( + Pair("status", result.spell.status.ToString()), + Pair("confidence", LogFloat(result.spell.confidence)), + Pair("baseFamily", result.spell.RecognizedFamilyText), + Pair("worldEffect", worldEffect), + Pair("customShapeToken", result.spell.customShapeToken ?? "")), + strokePointCount: result.bufferStrokeCount); } private void LogOverlayNoSealAttempt(OverlayRecognitionResult result, Vector2 center, int strokeCount, string worldEffect) @@ -6072,6 +9564,19 @@ private void LogOverlayNoSealAttempt(OverlayRecognitionResult result, Vector2 ce assistLevel = CurrentAssistLevel, assisted = false }); + LogActionEvent( + "spell_attempt_failure", + "player", + SpellPhase.Overlay.ToString(), + targetLabel: worldEffect, + position: center, + value: result.OperatorText, + payloadJson: BuildEventPayload( + Pair("status", result.status.ToString()), + Pair("score", LogFloat(result.score)), + Pair("worldEffect", worldEffect), + Pair("reason", result.feedbackReason ?? "")), + strokePointCount: strokeCount); } private void LogOverlayAttempt(OverlayRecognitionResult result, CompiledSeal seal, Vector2 center, int strokeCount, string worldEffect) @@ -6107,6 +9612,22 @@ private void LogOverlayAttempt(OverlayRecognitionResult result, CompiledSeal sea assistLevel = result.success ? 0 : CurrentAssistLevel, assisted = false }); + LogActionEvent( + result.success ? "spell_attempt_success" : "spell_attempt_failure", + "player", + SpellPhase.Overlay.ToString(), + seal.sealId, + worldEffect, + center, + result.OperatorText, + BuildEventPayload( + Pair("status", result.status.ToString()), + Pair("score", LogFloat(result.score)), + Pair("baseFamily", SpellLabels.English(seal.baseFamily)), + Pair("overlayStack", string.Join(">", seal.overlayStack.Select(SpellLabels.English))), + Pair("worldEffect", worldEffect), + Pair("reason", result.feedbackReason ?? "")), + strokePointCount: strokeCount); } private void ClearFloorObjects() @@ -6145,10 +9666,12 @@ private void ClearFloorObjects() spriteAccentAnimations.Clear(); elementalEntities.Clear(); shelfGuideArrows.Clear(); + ClearSequentialGoalGuides(); activeStageGates.Clear(); stageEntityObjects.Clear(); stageEffectObjects.Clear(); customEventObjects.Clear(); + finalTaskEnvironmentObjects.Clear(); CustomShapeEventObjectCountForTests = 0; LastElementalReactionCountForTests = 0; LastElementalReactionSummaryForTests = ""; @@ -6157,6 +9680,9 @@ private void ClearFloorObjects() LastCustomShapeEventKindForTests = ""; LastCustomShapeEventLabelForTests = ""; LastCustomShapeEventDirectionForTests = Vector2.right; + LastBeamHitForTests = false; + LastDamagedTargetNameForTests = ""; + ResetRepeatedGoalRecognition(); foreach (var ghostTrace in ghostTraces) { if (ghostTrace.body != null) @@ -6277,14 +9803,29 @@ private static void ApplySpriteOverride(GameObject body, Sprite spriteOverride) } } - private Text CreateGoalLabel(WorldStateGoal goal, Transform parent) + private Text CreateGoalLabel(WorldStateGoal goal, Transform parent, int goalIndex) { var stageLabel = floorController?.Current.number == 3; - var visualRequirement = floorController != null && floorController.Current.number <= 3; - var labelSize = stageLabel ? new Vector2(210f, 66f) : new Vector2(220f, 88f); + var beamGoalLabel = floorController?.Current.number == 4 && + goal.requiredCustomEventKind == CustomShapeEventKind.AttributeLaser && + goal.RequiresBeamHit; + var visualRequirement = floorController != null && + (floorController.Current.number <= 3 || beamGoalLabel); + var labelSize = stageLabel + ? new Vector2(210f, 66f) + : beamGoalLabel + ? new Vector2(150f, 58f) + : new Vector2(220f, 88f); var canvasObject = new GameObject($"{goal.title} Goal Label"); canvasObject.transform.SetParent(parent, false); - canvasObject.transform.position = goal.position + (stageLabel ? new Vector2(0f, 0.78f) : new Vector2(0f, -0.86f)); + // Goals can sit 2.7 world units apart; alternating above/below keeps + // neighbouring labels from stacking on the same row. + var labelOffset = stageLabel + ? new Vector2(0f, 0.78f) + : beamGoalLabel + ? new Vector2(0f, -0.66f) + : goalIndex % 2 == 0 ? new Vector2(0f, -0.86f) : new Vector2(0f, 0.98f); + canvasObject.transform.position = goal.position + labelOffset; var worldCanvas = canvasObject.AddComponent(); worldCanvas.renderMode = RenderMode.WorldSpace; worldCanvas.overrideSorting = true; @@ -6292,7 +9833,7 @@ private Text CreateGoalLabel(WorldStateGoal goal, Transform parent) canvasObject.AddComponent(); var rect = canvasObject.GetComponent() ?? canvasObject.AddComponent(); rect.sizeDelta = labelSize; - canvasObject.transform.localScale = Vector3.one * (stageLabel ? 0.014f : 0.016f); + canvasObject.transform.localScale = Vector3.one * (stageLabel ? 0.014f : beamGoalLabel ? 0.014f : 0.016f); var backgroundAlpha = visualRequirement ? 0.16f : 0.86f; var background = CreateImage("Goal Label Background", canvasObject.transform, Vector2.zero, labelSize, Anchor.Center, new Color(0.02f, 0.025f, 0.04f, backgroundAlpha)); @@ -6311,16 +9852,20 @@ private Text CreateGoalLabel(WorldStateGoal goal, Transform parent) textPosition, new Vector2(textSize.x - 18f, stageLabel ? 30f : 34f), Anchor.Center, - new Color(0.006f, 0.010f, 0.018f, stageLabel ? 0.54f : 0.58f)); + new Color(0.006f, 0.010f, 0.018f, stageLabel ? 0.70f : 0.74f)); titleBacking.raycastTarget = false; } - var text = CreateText("Goal Label Text", canvasObject.transform, visualRequirement ? goal.title : goal.OpenLabel, stageLabel ? 22 : 24, FontStyle.Bold, textPosition, textSize, Anchor.Center); + var text = CreateText("Goal Label Text", canvasObject.transform, visualRequirement ? goal.title : goal.OpenLabel, beamGoalLabel ? 19 : stageLabel ? 22 : 24, FontStyle.Bold, textPosition, textSize, Anchor.Center); text.alignment = TextAnchor.MiddleCenter; - text.color = Color.Lerp(goal.color, Color.white, 0.45f); - text.horizontalOverflow = HorizontalWrapMode.Overflow; - text.lineSpacing = 0.88f; + text.color = Color.Lerp(goal.color, Color.white, beamGoalLabel ? 0.45f : 0.62f); + text.horizontalOverflow = beamGoalLabel ? HorizontalWrapMode.Wrap : HorizontalWrapMode.Overflow; + text.lineSpacing = beamGoalLabel ? 0.78f : 0.88f; + text.resizeTextForBestFit = beamGoalLabel; + text.resizeTextMinSize = 14; + text.resizeTextMaxSize = beamGoalLabel ? 19 : stageLabel ? 22 : 24; text.raycastTarget = false; + AddTextOutline(text); if (visualRequirement) { CreateGoalRequirementIconRow(goal, canvasObject.transform, stageLabel); @@ -6329,6 +9874,17 @@ private Text CreateGoalLabel(WorldStateGoal goal, Transform parent) return text; } + /// + /// World-space labels float over arbitrary scenery; a 1px dark outline keeps + /// them readable without forcing an opaque backing panel (GAME_DESIGN section 11). + /// + private static void AddTextOutline(Text text) + { + var outline = text.gameObject.AddComponent(); + outline.effectColor = new Color(0f, 0f, 0f, 0.85f); + outline.effectDistance = new Vector2(1f, -1f); + } + private void CreateGoalRequirementIconRow(WorldStateGoal goal, Transform parent, bool stageLabel) { var glyphs = BuildGoalRequirementGlyphs(goal); @@ -6379,6 +9935,11 @@ private void CreateGoalRequirementIconRow(WorldStateGoal goal, Transform parent, Anchor.Center, Color.white); image.sprite = PixelArtFactory.CreateSprite($"Goal Requirement {goal.id} {index + 1}", glyph.primary, glyph.secondary, glyph.kind); + if (!string.IsNullOrWhiteSpace(glyph.shapeToken)) + { + image.sprite = CustomShapeSpriteFactory.CreateShapeSprite(glyph.shapeToken, stageLabel ? 3 : 4); + image.color = glyph.primary; + } image.preserveAspect = true; image.raycastTarget = false; x += iconSize + plusWidth + gap * 2f; @@ -6402,7 +9963,10 @@ private static List BuildGoalRequirementGlyphs(WorldStateG if (goal.requiredBase.HasValue) { glyphs.Add(FamilyRequirementGlyph(goal.requiredBase.Value)); - if (goal.requiresCustomShape || goal.requiredCustomSpell.HasValue) + if (goal.requiresCustomShape || + goal.requiredCustomSpell.HasValue || + goal.requiredCustomEventKind.HasValue || + goal.requirementShapeTokens.Count > 0) { foreach (var token in GoalRequirementShapeTokens(goal)) { @@ -6441,8 +10005,9 @@ private static IReadOnlyList GoalRequirementShapeTokens(WorldStateGoal g CustomSpellEffectKind.Focus => new[] { "star" }, CustomSpellEffectKind.Flow => new[] { "wave" }, CustomSpellEffectKind.Connection => new[] { "brace" }, + CustomSpellEffectKind.Steel => new[] { "pentagon" }, CustomSpellEffectKind.Stability => new[] { "rect" }, - CustomSpellEffectKind.LivingBridge => new[] { "arrow", "rect" }, + CustomSpellEffectKind.LivingBridge => new[] { "arrow" }, CustomSpellEffectKind.WindPlatform => new[] { "rect" }, _ => Array.Empty() }; @@ -6474,7 +10039,7 @@ private static GoalRequirementGlyph ShapeRequirementGlyph(string token, Color fa { var primary = new Color(0.96f, 0.96f, 0.96f, 1f); var secondary = new Color(0.68f, 0.68f, 0.68f, 1f); - return new GoalRequirementGlyph(ShapeTokenSpriteKind(token), primary, secondary); + return new GoalRequirementGlyph(ShapeTokenSpriteKind(token), primary, secondary, token); } private static PixelSpriteKind ShapeTokenSpriteKind(string token) @@ -6485,6 +10050,7 @@ private static PixelSpriteKind ShapeTokenSpriteKind(string token) "arrow" => PixelSpriteKind.ShapeArrow, "rect" or "roundrect" => PixelSpriteKind.ShapeRect, "ellipse" => PixelSpriteKind.ShapeEllipse, + "pentagon" => PixelSpriteKind.ShapeHexagon, "hexagon" => PixelSpriteKind.ShapeHexagon, "brace" or "curve" or "arc" or "wave" => PixelSpriteKind.ShapeBrace, "cross" => PixelSpriteKind.ShapeCross, @@ -6494,33 +10060,12 @@ private static PixelSpriteKind ShapeTokenSpriteKind(string token) private Image CreateImage(string name, Transform parent, Vector2 anchoredPosition, Vector2 size, Anchor anchor, Color color) { - var body = new GameObject(name); - body.transform.SetParent(parent, false); - var rect = body.AddComponent(); - ApplyAnchor(rect, anchor); - rect.anchoredPosition = anchoredPosition; - rect.sizeDelta = size; - var image = body.AddComponent(); - image.color = color; - image.material = PixelMaterialProvider.UiMaterial; - return image; + return MagicExamUiFactory.CreateImage(name, parent, anchoredPosition, size, ToUiAnchor(anchor), color); } private static void AddPanelBorder(RectTransform target, Color color, float thickness) { - var body = new GameObject($"{target.name} Border"); - body.transform.SetParent(target, false); - var rect = body.AddComponent(); - rect.anchorMin = Vector2.zero; - rect.anchorMax = Vector2.one; - rect.pivot = new Vector2(0.5f, 0.5f); - rect.offsetMin = Vector2.zero; - rect.offsetMax = Vector2.zero; - body.AddComponent(); - var border = body.AddComponent(); - border.color = color; - border.thickness = thickness; - border.raycastTarget = false; + MagicExamUiFactory.AddPixelBorder(target, color, thickness); } private RectTransform CreatePanel(string name, Transform parent, Vector2 anchoredPosition, Vector2 size, Anchor anchor, Color color) @@ -6566,10 +10111,29 @@ private Button CreateButton( var text = CreateText($"{name} Text", image.transform, label, fontSize, style, Vector2.zero, size, Anchor.Center); text.alignment = TextAnchor.MiddleCenter; text.color = Color.white; + text.verticalOverflow = VerticalWrapMode.Truncate; text.raycastTarget = false; + var buttonStyle = color.r > 0.65f && color.g < 0.20f + ? MagicExamButtonStyle.Danger + : parent.name.Contains("Quest", StringComparison.Ordinal) || parent.name.Contains("Letter", StringComparison.Ordinal) + ? MagicExamButtonStyle.Parchment + : MagicExamButtonStyle.Secondary; + MagicExamUiFactory.StyleButton(button, buttonStyle); return button; } + private static MagicExamUiAnchor ToUiAnchor(Anchor anchor) + { + return anchor switch + { + Anchor.TopLeft => MagicExamUiAnchor.TopLeft, + Anchor.TopRight => MagicExamUiAnchor.TopRight, + Anchor.BottomLeft => MagicExamUiAnchor.BottomLeft, + Anchor.BottomRight => MagicExamUiAnchor.BottomRight, + _ => MagicExamUiAnchor.Center + }; + } + private static void ClearChildren(Transform parent) { for (var index = parent.childCount - 1; index >= 0; index--) @@ -6737,12 +10301,45 @@ private enum Anchor BottomRight } + private enum FloorTransitionState + { + None, + WaitingForSpeech, + Fading + } + private enum BuffOwnerKind { Player, Target } + private sealed class FinalExamTaskDefinition + { + public readonly string id; + public readonly string prompt; + public readonly int sourceFloor; + public readonly WorldStateGoal goal; + public readonly IReadOnlyList references; + + public FinalExamTaskDefinition( + string id, + string prompt, + int sourceFloor, + WorldStateGoal goal, + params CustomShapeReferenceDefinition[] references) + { + this.id = id; + this.prompt = prompt; + this.sourceFloor = sourceFloor; + this.goal = goal; + this.references = references?.Where(reference => reference != null).ToArray() ?? + Array.Empty(); + } + + public bool RequiresBeamTarget => goal?.RequiresBeamHit == true; + } + private sealed class CustomShapeReferenceDefinition { public readonly SpellFamily family; @@ -6930,12 +10527,14 @@ private readonly struct GoalRequirementGlyph public readonly PixelSpriteKind kind; public readonly Color primary; public readonly Color secondary; + public readonly string shapeToken; - public GoalRequirementGlyph(PixelSpriteKind kind, Color primary, Color secondary) + public GoalRequirementGlyph(PixelSpriteKind kind, Color primary, Color secondary, string shapeToken = null) { this.kind = kind; this.primary = primary; this.secondary = secondary; + this.shapeToken = shapeToken; } } @@ -7122,6 +10721,84 @@ public void Tick(float time, float deltaTime) } } + private sealed class SequentialGoalGuide + { + private readonly string goalId; + private readonly Vector2 goalAnchor; + private readonly Vector2 arrowAnchor; + private readonly GameObject arrow; + private readonly GameObject ring; + private readonly float arrowBaseScale; + private readonly float ringBaseScale; + private float verticalOffset; + private float verticalVelocity; + + public SequentialGoalGuide( + string goalId, + Vector2 goalAnchor, + Vector2 arrowAnchor, + GameObject arrow, + GameObject ring, + float arrowBaseScale, + float ringBaseScale) + { + this.goalId = goalId; + this.goalAnchor = goalAnchor; + this.arrowAnchor = arrowAnchor; + this.arrow = arrow; + this.ring = ring; + this.arrowBaseScale = arrowBaseScale; + this.ringBaseScale = ringBaseScale; + } + + public bool IsActive => arrow != null || ring != null; + public string GoalId => goalId; + + public void Tick(float time, float deltaTime) + { + var phase = Mathf.Abs(goalId.GetHashCode() % 997) * 0.013f; + var targetOffset = Mathf.Sin(time * 2.55f + phase) * 0.16f; + verticalVelocity += (targetOffset - verticalOffset) * 32f * deltaTime; + verticalVelocity *= Mathf.Exp(-5.2f * deltaTime); + verticalOffset += verticalVelocity * deltaTime; + + if (arrow != null) + { + arrow.transform.position = arrowAnchor + new Vector2(0f, verticalOffset); + arrow.transform.localScale = Vector3.one * arrowBaseScale * (1f + Mathf.Sin(time * 4.10f + phase) * 0.045f); + var renderer = arrow.GetComponent(); + if (renderer != null) + { + renderer.color = new Color(1f, 1f, 1f, 0.84f + Mathf.Sin(time * 3.2f + phase) * 0.12f); + } + } + + if (ring != null) + { + ring.transform.position = goalAnchor; + ring.transform.localScale = Vector3.one * ringBaseScale * (1f + Mathf.Sin(time * 3.0f + phase) * 0.055f); + var renderer = ring.GetComponent(); + if (renderer != null) + { + renderer.color = new Color(1f, 1f, 1f, 0.44f + Mathf.Sin(time * 2.35f + phase) * 0.14f); + } + } + } + + public void Destroy() + { + if (arrow != null) + { + UnityEngine.Object.Destroy(arrow); + } + + if (ring != null) + { + UnityEngine.Object.Destroy(ring); + } + } + } + private sealed class ParticlePulse { public readonly Vector2 position; @@ -7165,12 +10842,21 @@ public StageGate(string requiredGoalId, Vector2 center, Vector2 size, Vector2 re public readonly string lockedNote; public readonly GameObject body; + public float LeftEdge => center.x - halfSize.x; + public float RightEdge => center.x + halfSize.x; + public bool Contains(Vector2 position) { return Mathf.Abs(position.x - center.x) <= halfSize.x && Mathf.Abs(position.y - center.y) <= halfSize.y; } + public bool IsVerticallyAligned(Vector2 previousPosition, Vector2 nextPosition) + { + return Mathf.Abs(previousPosition.y - center.y) <= halfSize.y || + Mathf.Abs(nextPosition.y - center.y) <= halfSize.y; + } + public void Open() { if (opened || body == null) @@ -7585,11 +11271,14 @@ private sealed class SealView private readonly Text label; private readonly GameObject attachGuide; private readonly float attachRadius; - private readonly float defaultFallbackAt; + private float defaultFallbackAt; private readonly List overlayMarks = new(); private bool defaultFallbackTriggered; private bool postSealInputSeen; public bool HasAttachGuide => attachGuide != null && attachGuide.activeInHierarchy; + public string VisualLabelForTests => label == null ? "" : label.text; + public Color VisualColorForTests => root == null || root.GetComponent() == null ? Color.clear : root.GetComponent().color; + public Color AttachGuideColorForTests => attachGuide == null || attachGuide.GetComponent() == null ? Color.clear : attachGuide.GetComponent().color; public SealView(GameObject root, CompiledSeal seal, Text label, GameObject attachGuide, float attachRadius, float defaultFallbackAt) { @@ -7604,7 +11293,40 @@ public SealView(GameObject root, CompiledSeal seal, Text label, GameObject attac public void RefreshLabel(Font font) { label.font = font; - label.text = seal.Label; + label.fontSize = seal.HasCustomEffectSeal ? 20 : 18; + label.color = Color.Lerp(SealTint(seal), Color.white, 0.35f); + label.text = BuildLabelText(Time.time); + } + + private string BuildLabelText(float time) + { + var remaining = Mathf.Max(0f, seal.expiresAt - time); + if (seal.HasCustomEffectSeal) + { + return $"{seal.Label}\n추가 도형 차례\n남은 {remaining:0.0}초"; + } + + var fallback = IsDefaultFallbackPending(time) + ? $"보호막까지 {Mathf.Max(0f, defaultFallbackAt - time):0.0}초" + : $"남은 {remaining:0.0}초"; + return $"{SpellLabels.Korean(seal.baseFamily)} 기본 완료\n원 안에 이어 그리기\n{fallback}"; + } + + public void RefreshTint(Color tint, Color secondaryTint, Color guideTint) + { + var renderer = root == null ? null : root.GetComponent(); + if (renderer != null) + { + renderer.sprite = PixelArtFactory.CreateSprite($"Seal {seal.sealId} {seal.Label}", tint, secondaryTint, PixelSpriteKind.RuneCircle); + renderer.color = new Color(tint.r, tint.g, tint.b, renderer.color.a <= 0f ? 1f : renderer.color.a); + } + + var guideRenderer = attachGuide == null ? null : attachGuide.GetComponent(); + if (guideRenderer != null) + { + guideRenderer.sprite = PixelArtFactory.CreateSprite($"Attach Guide {seal.sealId} {seal.Label}", guideTint, secondaryTint, PixelSpriteKind.Pulse); + guideRenderer.color = guideTint; + } } public void AddOverlayMark(OverlayOperator op) @@ -7624,15 +11346,27 @@ public void AddOverlayMark(OverlayOperator op) public void MarkPostSealInputSeen(float time) { - if (time < defaultFallbackAt) + if (!defaultFallbackTriggered) { postSealInputSeen = true; } } - public bool TryTriggerDefaultFallback(float time) + public void ResetDefaultFallback(float triggerAt) + { + defaultFallbackAt = triggerAt; + defaultFallbackTriggered = false; + postSealInputSeen = false; + } + + public bool IsDefaultFallbackPending(float time) + { + return !defaultFallbackTriggered && !postSealInputSeen && time < defaultFallbackAt; + } + + public bool TryTriggerDefaultFallback(float time, bool inputInProgress) { - if (defaultFallbackTriggered || postSealInputSeen || time < defaultFallbackAt) + if (inputInProgress || defaultFallbackTriggered || postSealInputSeen || time < defaultFallbackAt) { return false; } @@ -7651,7 +11385,8 @@ public void Tick(float time, float normalizedLifetime) var renderer = root.GetComponent(); if (renderer != null) { - renderer.color = new Color(1f, 1f, 1f, Mathf.Clamp01(normalizedLifetime + 0.16f)); + var tint = SealTint(seal); + renderer.color = new Color(tint.r, tint.g, tint.b, Mathf.Clamp01(normalizedLifetime + 0.16f)); } if (attachGuide != null) { @@ -7660,9 +11395,15 @@ public void Tick(float time, float normalizedLifetime) var guideRenderer = attachGuide.GetComponent(); if (guideRenderer != null) { - guideRenderer.color = new Color(1f, 1f, 1f, Mathf.Clamp01(normalizedLifetime * 0.42f)); + var guideTint = SealGuideTint(seal); + guideRenderer.color = new Color(guideTint.r, guideTint.g, guideTint.b, Mathf.Clamp01(normalizedLifetime * guideTint.a)); } } + + if (label != null) + { + label.text = BuildLabelText(time); + } } } } @@ -8219,19 +11960,24 @@ public string BuildText(int totalAttempts, string outputDirectory, bool trueEndi var favoriteBase = baseUse.Count == 0 ? "없음" : SpellLabels.Korean(baseUse.OrderByDescending(item => item.Value).First().Key); var favoriteOverlay = overlayUse.Count == 0 ? "없음" : SpellLabels.Korean(overlayUse.OrderByDescending(item => item.Value).First().Key); var averageQuality = qualityScores.Count == 0 ? 0f : qualityScores.Average() * 100f; - var endingName = trueEnding ? "진엔딩 (6/6 완전 복구)" : $"통과 엔딩 ({completedFinalGoals}/{totalFinalGoals})"; - var header = trueEnding ? "입학 시험 완전 통과 - 성좌심 완전 복구 보고서" : "입학 시험 통과 - 성좌심 복구 보고서"; + var endingName = trueEnding ? $"수료 엔딩 ({completedFinalGoals}/{totalFinalGoals})" : $"통과 엔딩 ({completedFinalGoals}/{totalFinalGoals})"; + var header = trueEnding ? "최종 시험 통과 - 수료증 발급 보고서" : "입학 시험 통과 보고서"; + var rawLogPath = outputDirectory ?? ""; + var logMarker = rawLogPath.IndexOf("MagicExamHallLogs", StringComparison.Ordinal); + var displayLogPath = logMarker >= 0 + ? rawLogPath[logMarker..] + : rawLogPath.Length <= 72 ? rawLogPath : rawLogPath[..71] + "…"; var excerptLine = noteExcerpts == null || noteExcerpts.Count == 0 ? "대표 관찰문: 기록 없음" - : "대표 관찰문:\n- " + string.Join("\n- ", noteExcerpts); + : "대표 관찰문: " + string.Join(" / ", noteExcerpts.Take(2)); return $"{header}\n" + $"도달 상태: {endingName}\n\n" + "당신은 정답표를 따라간 것이 아니라, 탑이 알아들을 수 있는 문법을 끝까지 조립했습니다.\n\n" + "플레이 기록\n" + $"전체 시도: {totalAttempts}회\n" + - $"가장 많이 사용한 base: {favoriteBase}\n" + - $"가장 많이 사용한 overlay: {favoriteOverlay}\n" + + $"가장 많이 사용한 기본 문양: {favoriteBase}\n" + + $"가장 많이 사용한 장식: {favoriteOverlay}\n" + $"발견한 세계 반응: {discoveries.Count}개\n" + $"평균 문양 안정도: {averageQuality:0}%\n" + $"힌트 표시: {hintShownCount}회 / 최대 {AssistLevelLabel(maxAssistLevel)} / 힌트 후 성공 {assistedSuccessCount}회\n" + @@ -8239,15 +11985,16 @@ public string BuildText(int totalAttempts, string outputDirectory, bool trueEndi $"{questChecklistSummary}\n" + $"{BuildProfileSummary()}\n" + $"{excerptLine}\n" + - "보정 정책: profile은 성공/실패 판정을 뒤집지 않고 품질 설명과 다음 연습 방향에만 사용됩니다.\n\n" + + $"관찰 기록: {questChecklistSummary}\n\n" + + "보정 정책: 성공/실패 판정은 유지하고, 억지 설명과 다음 연습 방향에만 사용합니다.\n" + BuildReflectionLine(favoriteBase, favoriteOverlay, discoveries.Count) + "\n" + - (trueEnding ? "이제 탑의 별자리에 당신의 이름이 있습니다." : "탑은 당신의 문양을 기억 속에 새겼습니다.") + "\n\n" + + (trueEnding ? "최종 시험관이 수료증 발급을 확인했습니다." : "탑은 당신의 문양을 기억 속에 새겼습니다.") + "\n\n" + "자기 평가\n" + "1. 어떤 문양이 가장 내 손에 잘 맞았나요?\n" + "2. 실패했을 때 다음에 고칠 점이 보였나요?\n" + - "3. base와 overlay 조합을 스스로 예측할 수 있었나요?\n" + - "4. 직접 마법을 시전한다는 느낌이 있었나요?\n\n" + - $"로그 저장 위치:\n{outputDirectory}"; + "3. 기본 문양과 장식 조합을 스스로 예측할 수 있었나요?\n" + + "4. 직접 마법을 시전했다는 느낌이 있었나요?\n\n" + + $"로그 저장 위치 {displayLogPath}"; } private static string BuildReflectionLine(string favoriteBase, string favoriteOverlay, int discoveryCount) @@ -8259,22 +12006,22 @@ private static string BuildReflectionLine(string favoriteBase, string favoriteOv if (favoriteBase == "없음") { - return $"{favoriteOverlay} 장식을 중심으로 흐름을 조절했습니다. 이제 같은 장식을 다른 base와 묶으면 더 많은 해석이 열립니다."; + return $"{favoriteOverlay} 장식을 중심으로 흐름을 조절했습니다. 이제 같은 장식을 다른 기본 문양과 묶으면 더 많은 해석이 열립니다."; } if (favoriteOverlay == "없음") { - return $"{favoriteBase} base로 탑의 언어를 안정시켰습니다. overlay를 더하면 같은 문양도 다른 의도를 갖게 됩니다."; + return $"{favoriteBase} 기본 문양으로 탑의 언어를 안정시켰습니다. 장식을 더하면 같은 문양도 다른 의도를 갖게 됩니다."; } - return $"{favoriteBase} base와 {favoriteOverlay} 장식을 가장 자주 실험했습니다. 탑은 그 반복을 단순한 성공이 아니라 당신만의 문법으로 기록했습니다."; + return $"{favoriteBase} 기본 문양과 {favoriteOverlay} 장식을 가장 자주 실험했습니다. 탑은 그 반복을 단순한 성공이 아니라 당신만의 문법으로 기록했습니다."; } private string BuildProfileSummary() { if (successfulQualities.Count == 0) { - return "문양 습관: 아직 성공한 base 표본이 부족합니다."; + return "문양 습관: 아직 성공한 기본 문양 표본이 부족합니다."; } var metrics = BuildMetricScores(); @@ -8362,7 +12109,7 @@ public static List BuildAll() { number = 1, title = "발착층", - objective = "다섯 base 문양으로 시험장의 반응 오브젝트를 깨우세요.", + objective = "다섯 기본 문양으로 시험장의 반응 오브젝트를 깨우세요.", entryNote = "노트: 바닥에 직접 그린 선은 탑이 읽는 말이 된다.", completeNote = "승강 룬이 깨어났습니다. 탑이 다음 층을 열어 줍니다.", accentColor = new Color(0.96f, 0.68f, 0.28f), @@ -8380,9 +12127,9 @@ public static List BuildAll() { number = 2, title = "반응층", - objective = "좌측 책장에서 base별 커스텀 레퍼런스를 슬롯으로 들여온 뒤, 커스텀 도형으로 반응 표식을 깨우세요.", - entryNote = "노트: 이 층은 저장된 커스텀 도형의 gold capture를 기준으로 반응합니다. 좌측 책장에 가까이 가면 레퍼런스를 볼 수 있습니다.", - completeNote = "다섯 커스텀 반응 표식이 모두 안정화되었습니다.", + objective = "시험관이 슬롯에 넣어 준 도형을 기본 문양 안에 이어 그려 반응 표식을 깨우세요.", + entryNote = "노트: 이 층은 슬롯에 지급된 도형의 기준 그림을 따라 반응합니다. 도형함은 확인과 복습용으로 사용할 수 있습니다.", + completeNote = "슬롯 도형 반응 표식이 모두 안정화되었습니다.", accentColor = new Color(0.65f, 0.48f, 0.92f), rugColor = new Color(0.18f, 0.18f, 0.42f), goals = @@ -8398,7 +12145,7 @@ public static List BuildAll() { number = 3, title = "건널목층", - objective = "기본 문양 위에 커스텀 도형을 얹어 네 구간의 길을 직접 만드세요.", + objective = "기본 문양의 빛나는 원 안에 가져온 도형을 이어 그려 네 구간의 길을 직접 만드세요.", entryNote = "노트: 먼저 기본 문양을 만들고, 그 빛나는 원 안에 필요한 도형을 얹으면 길이 생긴다.", completeNote = "네 구간의 길이 모두 열렸습니다.", accentColor = new Color(0.48f, 0.8f, 0.92f), @@ -8407,44 +12154,44 @@ public static List BuildAll() { WorldStateGoal.CustomSpell("frozen_river", "강물 얼리기", SpellFamily.Water, CustomSpellEffectKind.Ice, new Vector2(-1.75f, -1.18f), new Color(0.48f, 0.84f, 1f), "얼음 결정이 강물을 얼려 안전한 얼음길로 만듭니다.").WithRequirementShapes("hexagon").WithReaction(WorldReactionKind.FreezeRiver), WorldStateGoal.CustomSpell("earth_stairs", "구멍 메우기", SpellFamily.Earth, CustomSpellEffectKind.Stability, new Vector2(1.75f, 0.42f), new Color(0.74f, 0.55f, 0.32f), "구멍 메움판이 깨진 바닥 구멍을 채워 지나갈 길을 만듭니다.").WithRequirementShapes("rect").WithReaction(WorldReactionKind.EarthStairs), - WorldStateGoal.CustomSpell("living_bridge", "덩굴 다리", SpellFamily.Life, CustomSpellEffectKind.LivingBridge, new Vector2(2.85f, -3.02f), new Color(0.35f, 0.86f, 0.42f), "덩굴 다리가 낭떠러지를 이어 줍니다.").WithRequirementShapes("arrow", "rect").WithReaction(WorldReactionKind.LivingBridge), + WorldStateGoal.CustomSpell("living_bridge", "덩굴 다리", SpellFamily.Life, CustomSpellEffectKind.LivingBridge, new Vector2(2.85f, -3.02f), new Color(0.35f, 0.86f, 0.42f), "덩굴 다리가 낭떠러지를 이어 줍니다.").WithRequirementShapes("arrow").WithReaction(WorldReactionKind.LivingBridge), WorldStateGoal.CustomSpell("wind_platform", "바람 발판", SpellFamily.Wind, CustomSpellEffectKind.WindPlatform, new Vector2(4.65f, 2.15f), new Color(0.74f, 0.86f, 0.92f), "바람 발판이 마지막 빈 공간을 건널 수 있게 합니다.").WithRequirementShapes("rect").WithReaction(WorldReactionKind.WindPlatform) } }, new() { number = 4, - title = "타격층", - objective = "훈련 표적에 기본 문양과 커스텀 도형 조합을 사용해 반응과 피해 표시를 확인하세요.", - entryNote = "노트: 표적은 어려운 말을 쓰지 않는다. 필요한 조합을 만들면 바로 반응과 숫자로 보여 준다.", - completeNote = "네 표적이 모두 반응했습니다.", + title = "균열층", + objective = "불안정한 균열 표식에 기본 문양과 가져온 도형 조합을 사용해 네 가지 안정화 반응을 확인하세요.", + entryNote = "노트: 이 층의 표식은 흔들리는 균열입니다. 필요한 조합을 만들면 균열의 반응이 바로 기록됩니다.", + completeNote = "네 균열 표식이 모두 안정화되었습니다.", accentColor = new Color(1f, 0.42f, 0.28f), rugColor = new Color(0.42f, 0.10f, 0.16f), goals = { - WorldStateGoal.CustomSpell("ice_training", "얼음 제압", SpellFamily.Water, CustomSpellEffectKind.Ice, new Vector2(-4.8f, 1.55f), new Color(0.48f, 0.84f, 1f), "얼음 결정이 표적의 움직임을 늦춥니다.").WithReaction(WorldReactionKind.CombatHit), - WorldStateGoal.CustomSpell("electric_training", "번개 타격", SpellFamily.Fire, CustomSpellEffectKind.Electric, new Vector2(-1.6f, 2.15f), new Color(1f, 0.9f, 0.22f), "번개 직선이 표적을 빠르게 때립니다.").WithReaction(WorldReactionKind.CombatHit), - WorldStateGoal.CustomSpell("cleanse_training", "정화 수막", SpellFamily.Water, CustomSpellEffectKind.Cleanse, new Vector2(1.6f, 2.15f), new Color(0.24f, 0.48f, 0.86f), "둥근 수막이 표적의 오염 효과를 씻어 냅니다.").WithReaction(WorldReactionKind.CombatHit), - WorldStateGoal.CustomSpell("stable_training", "사각 방벽", SpellFamily.Earth, CustomSpellEffectKind.Stability, new Vector2(4.8f, 1.55f), new Color(0.74f, 0.55f, 0.32f), "사각 방벽이 표적 앞에 엄폐물을 세웁니다.").WithReaction(WorldReactionKind.CombatHit) + WorldStateGoal.CustomSpell("ice_training", "냉각 균열", SpellFamily.Water, CustomSpellEffectKind.Ice, new Vector2(-4.8f, 1.55f), new Color(0.48f, 0.84f, 1f), "얼음 결정이 과열된 균열의 떨림을 늦춥니다.").WithReaction(WorldReactionKind.CombatHit), + WorldStateGoal.CustomSpell("electric_training", "번개 회로", SpellFamily.Fire, CustomSpellEffectKind.Electric, new Vector2(-1.6f, 2.15f), new Color(1f, 0.9f, 0.22f), "번개 직선이 끊어진 빛길을 다시 잇습니다.").WithReaction(WorldReactionKind.CombatHit), + WorldStateGoal.CustomSpell("cleanse_training", "정화 수막", SpellFamily.Water, CustomSpellEffectKind.Cleanse, new Vector2(1.6f, 2.15f), new Color(0.24f, 0.48f, 0.86f), "둥근 수막이 균열 주변의 오염을 씻어 냅니다.").WithReaction(WorldReactionKind.CombatHit), + WorldStateGoal.CustomSpell("stable_training", "사각 지지대", SpellFamily.Earth, CustomSpellEffectKind.Stability, new Vector2(4.8f, 1.55f), new Color(0.74f, 0.55f, 0.32f), "사각 방벽이 균열 가장자리를 받쳐 줍니다.").WithReaction(WorldReactionKind.CombatHit) } }, new() { number = 5, title = "성좌심", - objective = "대형 입학 마법진의 여섯 요구치를 어떤 조합으로든 채우세요.", - entryNote = "노트: 마지막 시험은 정답을 묻지 않는다. 탑이 요구하는 상태를 채워라.", - completeNote = "대형 마법진이 복구되었습니다. 입학 시험이 끝났습니다.", + objective = "성좌심의 여섯 빈 조각을 배운 문양 조합으로 채워 입학 마법진을 복구하세요.", + entryNote = "노트: 마지막 시험은 하나의 정답을 묻지 않습니다. 성좌심의 빈 조각을 읽고, 배운 문양으로 필요한 상태를 채우세요.", + completeNote = "성좌심 마법진이 복구되었습니다. 입학 시험이 끝났습니다.", accentColor = new Color(0.95f, 0.75f, 0.34f), rugColor = new Color(0.18f, 0.16f, 0.32f), goals = { - WorldStateGoal.Combo("stability", "안정", SpellFamily.Earth, OverlayOperator.SteelBrace, new Vector2(-4.8f, 2.6f), new Color(0.74f, 0.55f, 0.32f), "안정의 조각이 고정됩니다."), - WorldStateGoal.Base("cleanse", "정화", SpellFamily.Water, new Vector2(-1.6f, 3.0f), new Color(0.24f, 0.48f, 0.86f), "정화의 조각이 맑아집니다."), - WorldStateGoal.Combo("connection", "연결", SpellFamily.Life, OverlayOperator.SoulDot, new Vector2(1.6f, 3.0f), new Color(0.35f, 0.86f, 0.42f), "연결의 조각이 새싹처럼 이어집니다."), - WorldStateGoal.Overlay("cut", "절단", OverlayOperator.VoidCut, new Vector2(4.8f, 2.6f), new Color(0.58f, 0.42f, 0.92f), "절단의 조각이 오염을 분리합니다."), - WorldStateGoal.Overlay("focus", "집중", OverlayOperator.SoulDot, new Vector2(-2.2f, -2.5f), new Color(0.95f, 0.62f, 1f), "집중의 조각이 심장을 밝힙니다."), - WorldStateGoal.Base("flow", "흐름", SpellFamily.Wind, new Vector2(2.2f, -2.5f), new Color(0.74f, 0.86f, 0.92f), "흐름의 조각이 원을 다시 돌립니다.") + WorldStateGoal.Combo("stability", "안정", SpellFamily.Earth, OverlayOperator.SteelBrace, new Vector2(-4.8f, 2.6f), new Color(0.74f, 0.55f, 0.32f), "안정의 조각이 성좌심에 고정됩니다."), + WorldStateGoal.Base("cleanse", "정화", SpellFamily.Water, new Vector2(-1.6f, 3.0f), new Color(0.24f, 0.48f, 0.86f), "정화의 조각이 맑은 빛으로 돌아옵니다."), + WorldStateGoal.Combo("connection", "연결", SpellFamily.Life, OverlayOperator.SoulDot, new Vector2(1.6f, 3.0f), new Color(0.35f, 0.86f, 0.42f), "연결의 조각이 다른 조각들과 이어집니다."), + WorldStateGoal.Overlay("cut", "절단", OverlayOperator.VoidCut, new Vector2(4.8f, 2.6f), new Color(0.58f, 0.42f, 0.92f), "절단의 조각이 남은 오염을 분리합니다."), + WorldStateGoal.Overlay("focus", "집중", OverlayOperator.SoulDot, new Vector2(-2.2f, -2.5f), new Color(0.95f, 0.62f, 1f), "집중의 조각이 성좌심의 중심을 밝힙니다."), + WorldStateGoal.Base("flow", "흐름", SpellFamily.Wind, new Vector2(2.2f, -2.5f), new Color(0.74f, 0.86f, 0.92f), "흐름의 조각이 마법진을 다시 순환시킵니다.") } } }; @@ -8466,6 +12213,9 @@ public sealed class WorldStateGoal public SpellFamily? comboBase; public OverlayOperator? comboOverlay; public CustomSpellEffectKind? requiredCustomSpell; + public CustomShapeEventKind? requiredCustomEventKind; + public string requiredBeamHitTargetId = ""; + public float BeamHitRadius = 0.92f; public IReadOnlyList requirementShapeTokens = Array.Empty(); public string discoveryNote; public WorldReactionKind reactionKind; @@ -8513,6 +12263,25 @@ public static WorldStateGoal CustomSpell(string id, string title, SpellFamily fa return goal; } + public static WorldStateGoal AttributeBeam( + string id, + string title, + SpellFamily family, + Vector2 position, + Color color, + string note, + string beamTargetId = "floor4_scarecrow") + { + var goal = Base(id, title, family, position, color, note); + goal.requiresCustomShape = true; + goal.requiredCustomEventKind = CustomShapeEventKind.AttributeLaser; + goal.requiredBeamHitTargetId = beamTargetId; + goal.requirementShapeTokens = new[] { "arrow" }; + goal.visualScale = 0.82f; + goal.radius = 3.25f; + return goal; + } + private static PixelSpriteKind KindForFamily(SpellFamily family) { return family switch @@ -8558,13 +12327,23 @@ public string RequirementLabel if (requiredBase.HasValue) { + if (requiredCustomEventKind.HasValue) + { + if (requiredCustomEventKind == CustomShapeEventKind.AttributeLaser && RequiresBeamHit) + { + return "문양+화살표=명중"; + } + + return $"{SpellLabels.Korean(requiredBase.Value)} + 화살표 빛줄기"; + } + if (requiredCustomSpell.HasValue) { return CustomSpellEffectCatalog.RequirementLabel(requiredCustomSpell.Value); } return requiresCustomShape - ? $"커스텀 {SpellLabels.Korean(requiredBase.Value)}" + ? $"가져온 {SpellLabels.Korean(requiredBase.Value)} 도형" : SpellLabels.Korean(requiredBase.Value); } @@ -8599,6 +12378,16 @@ public bool MatchesBase(SpellFamily family, Vector2 center) return requiredBase == family && Vector2.Distance(center, position) <= radius; } + public bool MatchesCustomEvent(SpellFamily family, CustomShapeEventKind eventKind) + { + return requiredBase == family && + requiredCustomEventKind.HasValue && + requiredCustomEventKind.Value == eventKind; + } + + public bool RequiresBeamHit => requiredCustomEventKind == CustomShapeEventKind.AttributeLaser && + !string.IsNullOrWhiteSpace(requiredBeamHitTargetId); + public bool MatchesOverlay(CompiledSeal seal, OverlayOperator op, Vector2 center) { if (!CastTouchedGoalArea(seal, center)) @@ -8629,6 +12418,9 @@ public WorldStateGoal Clone() comboBase = comboBase, comboOverlay = comboOverlay, requiredCustomSpell = requiredCustomSpell, + requiredCustomEventKind = requiredCustomEventKind, + requiredBeamHitTargetId = requiredBeamHitTargetId, + BeamHitRadius = BeamHitRadius, requirementShapeTokens = requirementShapeTokens?.ToArray() ?? Array.Empty(), reactionKind = reactionKind, requiresCustomShape = requiresCustomShape, diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/FloorGoalSystem.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/FloorGoalSystem.cs index bdbd7f7..acfee87 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/FloorGoalSystem.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/FloorGoalSystem.cs @@ -37,6 +37,18 @@ public GoalResolution ResolveBase( { if (goal.MatchesBase(family, center)) { + if (goal.requiredCustomEventKind.HasValue) + { + return new GoalResolution + { + kind = GoalResolutionKind.CustomEffectRequired, + targetGoal = goal, + worldEffect = "custom_event_required", + distance = Vector2.Distance(center, goal.position), + radius = goal.radius + }; + } + if (goal.requiresCustomShape && !isCustomShape) { return new GoalResolution diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/GameBootController.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/GameBootController.cs index a666993..4d507f2 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/GameBootController.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/GameBootController.cs @@ -26,10 +26,15 @@ public sealed class GameProgressSnapshot public int floorNumber = 1; public int completedGoals; public int totalGoals; + public string[] completedGoalIds = Array.Empty(); public string[] noteLines = Array.Empty(); public string savedAtUtc = ""; public int slotIndex; public int discoveries; + public string[] discoveredFamilies = Array.Empty(); + public string[] discoveredOverlays = Array.Empty(); + public string[] discoveredReactions = Array.Empty(); + public string[] encounteredFinalTaskIds = Array.Empty(); public string endingLabel = ""; } @@ -280,7 +285,8 @@ public sealed class GameBootController : MonoBehaviour public string CodexPanelParentForTests => codexPanel != null && codexPanel.parent != null ? codexPanel.parent.name : ""; public Vector2 CodexPanelPositionForTests => codexPanel == null ? Vector2.zero : codexPanel.anchoredPosition; public bool CodexBackdropBlocksRaycastsForTests => overlayRootImage != null && overlayRootImage.raycastTarget; - public bool CodexPanelDrawsAboveBackdropForTests => codexPanel != null && overlayRoot != null && codexPanel.GetSiblingIndex() > overlayRoot.GetSiblingIndex(); + public bool CodexPanelDrawsAboveBackdropForTests => codexPanel != null && overlayRoot != null && codexPanel.IsChildOf(overlayRoot); + public bool IsEndingPromptVisibleForTests => endingPromptPanel != null && endingPromptPanel.gameObject.activeInHierarchy; public void Initialize(ExamGameController gameController, Canvas targetCanvas, Font font) { @@ -326,7 +332,11 @@ private void Update() if (StateForTests == GameBootState.Gameplay && controller.HasEndingReport) { - ShowEndingPrompt(); + if (controller.CanShowTitleReturnPrompt) + { + ShowEndingPrompt(); + } + return; } @@ -420,72 +430,112 @@ public void ShowMainMenuForTests() private void BuildUi() { - overlayRoot = CreatePanel("Boot Overlay", canvas.transform, Vector2.zero, new Vector2(1280, 720), Anchor.Stretch, new Color(0.012f, 0.015f, 0.023f, 0.94f)); + overlayRoot = CreatePanel("Boot Overlay", canvas.transform, Vector2.zero, new Vector2(1280, 720), Anchor.Stretch, MagicExamUiTheme.DimOverlay); overlayRootImage = overlayRoot.GetComponent(); + var overlayCanvas = overlayRoot.gameObject.AddComponent(); + overlayCanvas.overrideSorting = true; + overlayCanvas.sortingOrder = 200; + overlayRoot.gameObject.AddComponent(); overlayRoot.gameObject.SetActive(true); - titlePanel = CreatePanel("Title Screen", overlayRoot, Vector2.zero, new Vector2(1280, 720), Anchor.Stretch, new Color(0f, 0f, 0f, 0f)); - CreateText("Title", titlePanel, "Magic Exam Hall", 54, FontStyle.Bold, new Vector2(0f, 92f), new Vector2(920, 72), Anchor.Center, TextAnchor.MiddleCenter, new Color(1f, 0.86f, 0.48f)); - CreateText("Subtitle", titlePanel, "Magic Recognizer Playable", 21, FontStyle.Bold, new Vector2(0f, 32f), new Vector2(640, 36), Anchor.Center, TextAnchor.MiddleCenter, new Color(0.72f, 0.88f, 1f)); + titlePanel = CreatePanel("Title Screen", overlayRoot, Vector2.zero, new Vector2(1280, 720), Anchor.Stretch, MagicExamUiTheme.DeepTowerSolid); CreateTowerSilhouette(titlePanel); - CreateText("Title Flavor", titlePanel, "떠 있는 탑은 오늘도 입학생을 기다린다.", 15, FontStyle.Italic, new Vector2(0f, -168f), new Vector2(640, 30), Anchor.Center, TextAnchor.MiddleCenter, new Color(0.74f, 0.81f, 0.93f, 0.92f)); - CreateText("Any Key", titlePanel, "아무 키나 눌러 시작", 18, FontStyle.Bold, new Vector2(0f, -210f), new Vector2(420, 34), Anchor.Center, TextAnchor.MiddleCenter, new Color(0.92f, 0.95f, 1f)); - - menuPanel = CreatePanel("Main Menu", overlayRoot, Vector2.zero, new Vector2(1280, 720), Anchor.Stretch, new Color(0f, 0f, 0f, 0f)); - CreateText("Menu Title", menuPanel, "Magic Exam Hall", 42, FontStyle.Bold, new Vector2(-288f, 190f), new Vector2(520, 56), Anchor.Center, TextAnchor.MiddleLeft, new Color(1f, 0.86f, 0.48f)); - newGameButton = CreateButton("New Game", menuPanel, "새 게임", new Vector2(-310f, 92f), StartNewGame); - continueButton = CreateButton("Continue", menuPanel, "이어하기", new Vector2(-310f, 34f), ContinueGame); - CreateButton("Options", menuPanel, "옵션", new Vector2(-310f, -24f), () => ShowOptions(GameBootState.MainMenu)); - practiceButton = CreateButton("Practice", menuPanel, "연습장", new Vector2(-310f, -82f), StartPracticeMode); - CreateButton("Quit", menuPanel, "종료", new Vector2(-310f, -140f), Application.Quit); - CreateText("Save Slot Label", menuPanel, "저장 슬롯", 16, FontStyle.Bold, new Vector2(128f, 92f), new Vector2(520, 28), Anchor.Center, TextAnchor.MiddleLeft, new Color(1f, 0.86f, 0.48f)); + var titleLogoArt = CreateImage("Title Logo Art", titlePanel, new Vector2(-180f, 166f), new Vector2(300f, 112f), Anchor.Center, Color.white); + MagicExamUiFactory.ApplySprite(titleLogoArt, MagicExamUiSpriteId.TitleLogo, sliced: false); + titleLogoArt.raycastTarget = false; + var titleText = CreateText("Title", titlePanel, "MAGIC EXAM HALL", 48, FontStyle.Bold, new Vector2(-180f, 88f), new Vector2(650, 72), Anchor.Center, TextAnchor.MiddleCenter, MagicExamUiTheme.Gold); + MagicExamUiFactory.StyleDarkText(titleText, emphasized: true); + var subtitle = CreateText("Subtitle", titlePanel, "마법탑 입학 복구 시험", 20, FontStyle.Bold, new Vector2(-180f, 28f), new Vector2(520, 36), Anchor.Center, TextAnchor.MiddleCenter, MagicExamUiTheme.RuneBlue); + MagicExamUiFactory.StyleDarkText(subtitle); + var flavor = CreateText("Title Flavor", titlePanel, "떠 있는 탑은 오늘도 입학생의 문양을 기다린다.", 15, FontStyle.Italic, new Vector2(-180f, -122f), new Vector2(620, 30), Anchor.Center, TextAnchor.MiddleCenter, MagicExamUiTheme.TextOnDarkMuted); + MagicExamUiFactory.StyleDarkText(flavor); + var titlePrompt = MagicExamUiFactory.CreateFramedPanel( + "Title Prompt Scroll", + titlePanel, + new Vector2(-180f, -194f), + new Vector2(390f, 52f), + MagicExamUiAnchor.Center, + MagicExamUiSpriteId.ScrollPanel, + Color.white, + MagicExamUiTheme.BorderBrown, + 2f); + var promptText = CreateText("Any Key", titlePrompt, "아무 키나 눌러 입장", 17, FontStyle.Bold, Vector2.zero, new Vector2(350, 34), Anchor.Center, TextAnchor.MiddleCenter, MagicExamUiTheme.ParchmentInk); + MagicExamUiFactory.StyleParchmentText(promptText, emphasized: true); + + menuPanel = CreatePanel("Main Menu", overlayRoot, Vector2.zero, new Vector2(1280, 720), Anchor.Stretch, MagicExamUiTheme.DeepTowerSolid); + CreateTowerSilhouette(menuPanel); + var menuFrame = MagicExamUiFactory.CreateFramedPanel("Menu Commands", menuPanel, new Vector2(-388f, 0f), new Vector2(340f, 540f), MagicExamUiAnchor.Center, MagicExamUiSpriteId.DarkPanel, Color.white, MagicExamUiTheme.BorderGold, 2.4f); + MagicExamUiFactory.AddAccentRail(menuFrame, MagicExamUiTheme.Gold, 6f); + var slotFrame = MagicExamUiFactory.CreateFramedPanel("Save Ledger", menuPanel, new Vector2(180f, 0f), new Vector2(650f, 500f), MagicExamUiAnchor.Center, MagicExamUiSpriteId.BookPanel, Color.white, MagicExamUiTheme.BorderBrown, 3f); + var menuTitle = CreateText("Menu Title", menuFrame, "MAGIC EXAM HALL", 28, FontStyle.Bold, new Vector2(0f, 218f), new Vector2(280, 44), Anchor.Center, TextAnchor.MiddleCenter, MagicExamUiTheme.Gold); + MagicExamUiFactory.StyleDarkText(menuTitle, emphasized: true); + var menuSubtitle = CreateText("Menu Subtitle", menuFrame, "입학 복구 기록", 14, FontStyle.Bold, new Vector2(0f, 184f), new Vector2(250, 28), Anchor.Center, TextAnchor.MiddleCenter, MagicExamUiTheme.RuneBlue); + MagicExamUiFactory.StyleDarkText(menuSubtitle); + newGameButton = CreateButton("New Game", menuFrame, "새 게임", new Vector2(0f, 112f), StartNewGame, MagicExamButtonStyle.Primary); + continueButton = CreateButton("Continue", menuFrame, "이어하기", new Vector2(0f, 52f), ContinueGame); + CreateButton("Options", menuFrame, "옵션", new Vector2(0f, -8f), () => ShowOptions(GameBootState.MainMenu)); + practiceButton = CreateButton("Practice", menuFrame, "연습장", new Vector2(0f, -68f), StartPracticeMode); + CreateButton("Quit", menuFrame, "종료", new Vector2(0f, -128f), Application.Quit, MagicExamButtonStyle.Danger); + var saveLabel = CreateText("Save Slot Label", slotFrame, "입학 기록 보관함", 23, FontStyle.Bold, new Vector2(0f, 205f), new Vector2(550, 34), Anchor.Center, TextAnchor.MiddleLeft, MagicExamUiTheme.ParchmentInk); + MagicExamUiFactory.StyleParchmentText(saveLabel, emphasized: true); + var saveRule = CreateImage("Save Ledger Rule", slotFrame, new Vector2(0f, 172f), new Vector2(560f, 2f), Anchor.Center, MagicExamUiTheme.BorderBrown); + saveRule.raycastTarget = false; slotButtons = new Button[SaveSlotCount]; for (var index = 0; index < SaveSlotCount; index++) { var capturedIndex = index; - slotButtons[index] = CreateButton($"Save Slot {index + 1}", menuPanel, $"슬롯 {index + 1}", new Vector2(128f + index * 168f, 42f), () => SelectSaveSlot(capturedIndex)); + slotButtons[index] = CreateButton($"Save Slot {index + 1}", slotFrame, $"슬롯 {index + 1}", new Vector2(-190f + index * 190f, 118f), () => SelectSaveSlot(capturedIndex), MagicExamButtonStyle.Parchment); } - saveSummaryText = CreateText("Save Summary", menuPanel, "", 15, FontStyle.Normal, new Vector2(128f, -110f), new Vector2(520, 120), Anchor.Center, TextAnchor.UpperLeft, new Color(0.86f, 0.92f, 1f)); + saveSummaryText = CreateText("Save Summary", slotFrame, "", 16, FontStyle.Normal, new Vector2(0f, -58f), new Vector2(550, 230), Anchor.Center, TextAnchor.UpperLeft, MagicExamUiTheme.ParchmentInk); + saveSummaryText.verticalOverflow = VerticalWrapMode.Truncate; + MagicExamUiFactory.StyleParchmentText(saveSummaryText); optionsPanel = CreatePanel("Options Panel", overlayRoot, Vector2.zero, new Vector2(1280, 720), Anchor.Stretch, new Color(0f, 0f, 0f, 0f)); - CreateText("Options Title", optionsPanel, "옵션", 34, FontStyle.Bold, new Vector2(-285f, 150f), new Vector2(360, 46), Anchor.Center, TextAnchor.MiddleLeft, new Color(1f, 0.86f, 0.48f)); - bgmSlider = CreateSlider("BGM Slider", optionsPanel, "BGM", new Vector2(-120f, 82f), MagicExamSettings.BgmVolume, value => MagicExamSettings.BgmVolume = value); - sfxSlider = CreateSlider("SFX Slider", optionsPanel, "SFX", new Vector2(-120f, 24f), MagicExamSettings.SfxVolume, value => MagicExamSettings.SfxVolume = value); - mouseSensitivitySlider = CreateSlider("Mouse Sensitivity Slider", optionsPanel, "감도", new Vector2(-120f, -34f), NormalizeSensitivity(MagicExamSettings.MouseSensitivity), value => + var optionsBook = MagicExamUiFactory.CreateFramedPanel("Options Book", optionsPanel, Vector2.zero, new Vector2(940f, 580f), MagicExamUiAnchor.Center, MagicExamUiSpriteId.BookPanel, Color.white, MagicExamUiTheme.BorderBrown, 3f); + var optionsTitle = CreateText("Options Title", optionsBook, "시험 환경 설정", 30, FontStyle.Bold, new Vector2(0f, 242f), new Vector2(800, 46), Anchor.Center, TextAnchor.MiddleLeft, MagicExamUiTheme.ParchmentInk); + MagicExamUiFactory.StyleParchmentText(optionsTitle, emphasized: true); + bgmSlider = CreateSlider("BGM Slider", optionsBook, "BGM", new Vector2(-10f, 150f), MagicExamSettings.BgmVolume, value => MagicExamSettings.BgmVolume = value); + sfxSlider = CreateSlider("SFX Slider", optionsBook, "SFX", new Vector2(-10f, 88f), MagicExamSettings.SfxVolume, value => MagicExamSettings.SfxVolume = value); + mouseSensitivitySlider = CreateSlider("Mouse Sensitivity Slider", optionsBook, "감도", new Vector2(-10f, 26f), NormalizeSensitivity(MagicExamSettings.MouseSensitivity), value => { MagicExamSettings.MouseSensitivity = Mathf.Lerp(0.55f, 1.75f, value); UpdateOptionSummaries(); }); - volumeSummaryText = CreateText("Volume Summary", optionsPanel, "", 15, FontStyle.Normal, new Vector2(-120f, -82f), new Vector2(520, 30), Anchor.Center, TextAnchor.UpperLeft, new Color(0.86f, 0.92f, 1f)); - CreateButton("Swap Mouse", optionsPanel, "좌/우클릭", new Vector2(-310f, -126f), ToggleSwapMouse); - CreateButton("Movement Preset", optionsPanel, "이동 키", new Vector2(-62f, -126f), CycleMovementPreset); - CreateButton("Text Scale", optionsPanel, "텍스트", new Vector2(186f, -126f), CycleTextScale); - CreateButton("Color Assist", optionsPanel, "색 보조", new Vector2(-310f, -184f), ToggleColorAssist); - CreateButton("Observer Mode", optionsPanel, "관찰 모드", new Vector2(-62f, -184f), ToggleObserverMode); - accessibilitySummaryText = CreateText("Accessibility Summary", optionsPanel, "", 14, FontStyle.Normal, new Vector2(186f, -190f), new Vector2(300, 74), Anchor.Center, TextAnchor.UpperLeft, new Color(0.86f, 0.92f, 1f)); - optionsBackButton = CreateButton("Options Back", optionsPanel, "돌아가기", new Vector2(-310f, -260f), ReturnFromOptions); + volumeSummaryText = CreateText("Volume Summary", optionsBook, "", 15, FontStyle.Normal, new Vector2(-10f, -22f), new Vector2(650, 30), Anchor.Center, TextAnchor.UpperLeft, MagicExamUiTheme.ParchmentInk); + MagicExamUiFactory.StyleParchmentText(volumeSummaryText); + CreateButton("Swap Mouse", optionsBook, "좌/우클릭", new Vector2(-265f, -88f), ToggleSwapMouse, MagicExamButtonStyle.Parchment); + CreateButton("Movement Preset", optionsBook, "이동 키", new Vector2(0f, -88f), CycleMovementPreset, MagicExamButtonStyle.Parchment); + CreateButton("Text Scale", optionsBook, "텍스트", new Vector2(265f, -88f), CycleTextScale, MagicExamButtonStyle.Parchment); + CreateButton("Color Assist", optionsBook, "색 보조", new Vector2(-265f, -150f), ToggleColorAssist, MagicExamButtonStyle.Parchment); + CreateButton("Observer Mode", optionsBook, "관찰 모드", new Vector2(0f, -150f), ToggleObserverMode, MagicExamButtonStyle.Parchment); + accessibilitySummaryText = CreateText("Accessibility Summary", optionsBook, "", 14, FontStyle.Normal, new Vector2(265f, -166f), new Vector2(230, 72), Anchor.Center, TextAnchor.UpperLeft, MagicExamUiTheme.ParchmentInk); + accessibilitySummaryText.verticalOverflow = VerticalWrapMode.Truncate; + MagicExamUiFactory.StyleParchmentText(accessibilitySummaryText); + optionsBackButton = CreateButton("Options Back", optionsBook, "돌아가기", new Vector2(-330f, -235f), ReturnFromOptions, MagicExamButtonStyle.Primary); pausePanel = CreatePanel("Pause Panel", overlayRoot, Vector2.zero, new Vector2(1280, 720), Anchor.Stretch, new Color(0f, 0f, 0f, 0f)); - CreateText("Pause Title", pausePanel, "일시정지", 34, FontStyle.Bold, new Vector2(-286f, 120f), new Vector2(380, 46), Anchor.Center, TextAnchor.MiddleLeft, new Color(1f, 0.86f, 0.48f)); - resumeButton = CreateButton("Resume", pausePanel, "계속", new Vector2(-310f, 38f), ResumeGameplay); - CreateButton("Pause Options", pausePanel, "옵션", new Vector2(-310f, -20f), () => ShowOptions(GameBootState.Paused)); - CreateButton("Back To Title", pausePanel, "타이틀로", new Vector2(-310f, -78f), ShowTitleWithFade); - - codexPanel = CreatePanel("Codex Panel", canvas.transform, new Vector2(24f, -24f), new Vector2(520, 360), Anchor.TopLeft, new Color(0.82f, 0.64f, 0.40f, 0.98f)); + var pauseScroll = MagicExamUiFactory.CreateFramedPanel("Pause Scroll", pausePanel, Vector2.zero, new Vector2(430f, 390f), MagicExamUiAnchor.Center, MagicExamUiSpriteId.ScrollPanel, Color.white, MagicExamUiTheme.BorderBrown, 3f); + var pauseTitle = CreateText("Pause Title", pauseScroll, "시험 일시정지", 28, FontStyle.Bold, new Vector2(0f, 135f), new Vector2(360, 46), Anchor.Center, TextAnchor.MiddleCenter, MagicExamUiTheme.ParchmentInk); + MagicExamUiFactory.StyleParchmentText(pauseTitle, emphasized: true); + resumeButton = CreateButton("Resume", pauseScroll, "계속", new Vector2(0f, 55f), ResumeGameplay, MagicExamButtonStyle.Primary); + CreateButton("Pause Options", pauseScroll, "옵션", new Vector2(0f, -10f), () => ShowOptions(GameBootState.Paused), MagicExamButtonStyle.Parchment); + CreateButton("Back To Title", pauseScroll, "타이틀로", new Vector2(0f, -75f), ShowTitleWithFade, MagicExamButtonStyle.Danger); + + codexPanel = CreatePanel("Codex Panel", overlayRoot, Vector2.zero, new Vector2(1280, 720), Anchor.Stretch, new Color(0f, 0f, 0f, 0f)); codexPanelGroup = codexPanel.gameObject.AddComponent(); - CreateImage("Codex Header", codexPanel, Vector2.zero, new Vector2(520, 52), Anchor.TopLeft, new Color(0.58f, 0.32f, 0.12f, 0.98f)); - CreateImage("Codex Header Accent", codexPanel, Vector2.zero, new Vector2(8, 52), Anchor.TopLeft, new Color(1f, 0.82f, 0.38f, 0.92f)); - CreateText("Codex Title", codexPanel, "마법 노트", 24, FontStyle.Bold, new Vector2(18f, -10f), new Vector2(230, 32), Anchor.TopLeft, TextAnchor.MiddleLeft, new Color(1f, 0.86f, 0.48f)); - codexCloseButton = CreateButton("Codex Close", codexPanel, "닫기", new Vector2(-18f, -10f), new Vector2(86, 32), Anchor.TopRight, 15, ResumeGameplay); - CreateButton("Codex Dialogue Tab", codexPanel, "대사", new Vector2(18f, -66f), new Vector2(148, 34), Anchor.TopLeft, 14, () => SetCodexTab(MagicNoteCategory.Dialogue)); - CreateButton("Codex Floor Tab", codexPanel, "층노트", new Vector2(184f, -66f), new Vector2(148, 34), Anchor.TopLeft, 14, () => SetCodexTab(MagicNoteCategory.FloorNote)); - CreateButton("Codex Discovery Tab", codexPanel, "발견", new Vector2(350f, -66f), new Vector2(148, 34), Anchor.TopLeft, 14, () => SetCodexTab(MagicNoteCategory.Discovery)); - var codexViewport = CreatePanel("Codex Viewport", codexPanel, new Vector2(18f, -114f), new Vector2(484, 166), Anchor.TopLeft, new Color(0.94f, 0.80f, 0.54f, 0.96f)); + var codexBook = MagicExamUiFactory.CreateFramedPanel("Codex Book", codexPanel, Vector2.zero, new Vector2(1060f, 620f), MagicExamUiAnchor.Center, MagicExamUiSpriteId.BookPanel, Color.white, MagicExamUiTheme.BorderBrown, 3f); + var codexTitle = CreateText("Codex Title", codexBook, "마법 노트", 30, FontStyle.Bold, new Vector2(-250f, 255f), new Vector2(430, 44), Anchor.Center, TextAnchor.MiddleLeft, MagicExamUiTheme.ParchmentInk); + MagicExamUiFactory.StyleParchmentText(codexTitle, emphasized: true); + CreateButton("Codex Dialogue Tab", codexBook, "대사", new Vector2(-310f, 210f), () => SetCodexTab(MagicNoteCategory.Dialogue), MagicExamButtonStyle.Tab); + CreateButton("Codex Floor Tab", codexBook, "층노트", new Vector2(-55f, 210f), () => SetCodexTab(MagicNoteCategory.FloorNote), MagicExamButtonStyle.Tab); + CreateButton("Codex Discovery Tab", codexBook, "발견", new Vector2(200f, 210f), () => SetCodexTab(MagicNoteCategory.Discovery), MagicExamButtonStyle.Tab); + var codexViewport = CreatePanel("Codex Viewport", codexBook, new Vector2(0f, -10f), new Vector2(920f, 380f), Anchor.Center, new Color(0f, 0f, 0f, 0f)); codexViewport.gameObject.AddComponent(); - codexTextContent = CreatePanel("Codex Text Content", codexViewport, Vector2.zero, new Vector2(484, 166), Anchor.TopLeft, new Color(0f, 0f, 0f, 0f)); + codexTextContent = CreatePanel("Codex Text Content", codexViewport, Vector2.zero, new Vector2(920f, 380f), Anchor.TopLeft, new Color(0f, 0f, 0f, 0f)); codexTextContent.GetComponent().raycastTarget = false; - codexText = CreateText("Codex Text", codexTextContent, "", 14, FontStyle.Normal, new Vector2(12f, -10f), new Vector2(456, 146), Anchor.TopLeft, TextAnchor.UpperLeft, new Color(0.12f, 0.08f, 0.04f)); + codexText = CreateText("Codex Text", codexTextContent, "", 15, FontStyle.Normal, new Vector2(18f, -10f), new Vector2(884f, 360f), Anchor.TopLeft, TextAnchor.UpperLeft, MagicExamUiTheme.ParchmentInk); codexText.verticalOverflow = VerticalWrapMode.Overflow; + MagicExamUiFactory.StyleParchmentText(codexText); codexScrollRect = codexViewport.gameObject.AddComponent(); codexScrollRect.viewport = codexViewport; codexScrollRect.content = codexTextContent; @@ -493,10 +543,12 @@ private void BuildUi() codexScrollRect.vertical = true; codexScrollRect.movementType = ScrollRect.MovementType.Clamped; codexScrollRect.scrollSensitivity = 28f; - CreateButton("Codex Manual Save", codexPanel, "수동 저장", new Vector2(18f, -306f), new Vector2(224, 38), Anchor.TopLeft, 15, ManualSaveFromCodex); + CreateButton("Codex Manual Save", codexBook, "수동 저장", new Vector2(150f, -260f), ManualSaveFromCodex, MagicExamButtonStyle.Parchment); + codexCloseButton = CreateButton("Codex Close", codexBook, "닫기", new Vector2(400f, -260f), ResumeGameplay, MagicExamButtonStyle.Primary); - endingPromptPanel = CreatePanel("Ending Prompt", overlayRoot, new Vector2(0f, -292f), new Vector2(640, 64), Anchor.Center, new Color(0.025f, 0.032f, 0.047f, 0.94f)); - CreateText("Ending Prompt Text", endingPromptPanel, "Enter 또는 클릭으로 타이틀 복귀", 17, FontStyle.Bold, Vector2.zero, new Vector2(600, 38), Anchor.Center, TextAnchor.MiddleCenter, new Color(1f, 0.86f, 0.48f)); + endingPromptPanel = MagicExamUiFactory.CreateFramedPanel("Ending Prompt", overlayRoot, new Vector2(0f, -334f), new Vector2(520, 44), MagicExamUiAnchor.Center, MagicExamUiSpriteId.ScrollPanel, Color.white, MagicExamUiTheme.BorderBrown, 2f); + var endingPromptText = CreateText("Ending Prompt Text", endingPromptPanel, "Enter 또는 클릭으로 타이틀 복귀", 14, FontStyle.Bold, Vector2.zero, new Vector2(480, 30), Anchor.Center, TextAnchor.MiddleCenter, MagicExamUiTheme.ParchmentInk); + MagicExamUiFactory.StyleParchmentText(endingPromptText, emphasized: true); codexQuickButton = CreateQuickCodexButton(canvas.transform); codexQuickButton.gameObject.SetActive(false); fadeCurtain = CreateImage("Boot Fade Curtain", canvas.transform, Vector2.zero, new Vector2(1280, 720), Anchor.Stretch, new Color(0f, 0f, 0f, 0f)); @@ -521,7 +573,7 @@ private void ShowTitle() private void ShowMainMenu() { Time.timeScale = 1f; - controller.SetGameplayInputEnabled(false); + controller.PrepareForTitleScreen(); StateForTests = GameBootState.MainMenu; overlayRoot.gameObject.SetActive(true); SetOverlayBackdrop(true); @@ -567,7 +619,7 @@ private void ContinueGameImmediate() } Time.timeScale = 1f; - controller.LoadSavedProgress(snapshot.floorNumber, snapshot.noteLines); + controller.LoadSavedProgress(snapshot); EnterGameplay(); } @@ -639,7 +691,7 @@ private void ShowCodex() StateForTests = GameBootState.Codex; overlayRoot.gameObject.SetActive(true); overlayRoot.SetAsLastSibling(); - SetOverlayBackdrop(false); + SetOverlayBackdrop(true); HideAllPanels(); SetCodexPanelVisible(true); codexPanel.SetAsLastSibling(); @@ -663,7 +715,7 @@ private void ShowEndingPrompt() controller.SetGameplayInputEnabled(false); StateForTests = GameBootState.Ending; overlayRoot.gameObject.SetActive(true); - SetOverlayBackdrop(true); + SetOverlayBackdrop(false); HideAllPanels(); endingPromptPanel.gameObject.SetActive(true); SetQuickCodexVisible(false); @@ -680,7 +732,7 @@ private IEnumerator ContinueGameRoutine(GameProgressSnapshot snapshot) { yield return FadeTo(1f, 0.45f); Time.timeScale = 1f; - controller.LoadSavedProgress(snapshot.floorNumber, snapshot.noteLines); + controller.LoadSavedProgress(snapshot); EnterGameplay(); yield return FadeTo(0f, 0.65f); } @@ -774,7 +826,7 @@ private string BuildCodexText(MagicNoteCategory category) return category == MagicNoteCategory.Dialogue ? "아직 기록된 대사가 없습니다." : "아직 기록된 층 노트가 없습니다."; } - var recent = filtered.AsEnumerable().Reverse().Take(14).Reverse().Select(entry => entry.DisplayLine); + var recent = filtered.AsEnumerable().Reverse().Take(14).Select(entry => entry.DisplayLine); return string.Join("\n", recent); } @@ -788,11 +840,11 @@ private string BuildDiscoveryCodex(IReadOnlyList filtered) .Select(op => controller.DiscoveredOverlaysForTests.Contains(op) ? SpellLabels.Korean(op) : "???"); var notes = filtered.Count == 0 ? "아직 발견 기록이 없습니다." - : string.Join("\n", filtered.Reverse().Take(8).Reverse().Select(entry => entry.DisplayLine)); + : string.Join("\n", filtered.Reverse().Take(8).Select(entry => entry.DisplayLine)); return - "Base family\n" + + "기본 속성\n" + string.Join(" / ", familyRows) + "\n\n" + - "Overlay operator\n" + + "장식 도형\n" + string.Join(" / ", overlayRows) + "\n\n" + $"속성 반응 발견 {controller.DiscoveredReactionCountForTests}/10\n\n" + notes; @@ -855,6 +907,26 @@ private GameProgressSnapshot LoadProgress(int slotIndex) { snapshot.noteLines = Array.Empty(); } + if (snapshot.completedGoalIds == null) + { + snapshot.completedGoalIds = Array.Empty(); + } + if (snapshot.discoveredFamilies == null) + { + snapshot.discoveredFamilies = Array.Empty(); + } + if (snapshot.discoveredOverlays == null) + { + snapshot.discoveredOverlays = Array.Empty(); + } + if (snapshot.discoveredReactions == null) + { + snapshot.discoveredReactions = Array.Empty(); + } + if (snapshot.encounteredFinalTaskIds == null) + { + snapshot.encounteredFinalTaskIds = Array.Empty(); + } return snapshot; } catch (Exception exception) when (exception is IOException || exception is UnauthorizedAccessException || exception is ArgumentException) @@ -908,11 +980,11 @@ private void RefreshSlotButtons() var colors = button.colors; colors.normalColor = index == activeSaveSlotIndex - ? new Color(0.095f, 0.130f, 0.180f, 0.98f) - : new Color(0.045f, 0.058f, 0.085f, 0.96f); + ? new Color(0.96f, 0.76f, 0.42f, 1f) + : new Color(0.84f, 0.63f, 0.36f, 0.98f); colors.highlightedColor = index == activeSaveSlotIndex - ? new Color(0.120f, 0.170f, 0.230f, 1f) - : new Color(0.078f, 0.105f, 0.150f, 0.98f); + ? new Color(1f, 0.84f, 0.52f, 1f) + : new Color(0.94f, 0.72f, 0.42f, 1f); button.colors = colors; } } @@ -975,8 +1047,8 @@ private void TickCodexQuickButton() if (codexQuickImage != null && codexQuickButton.gameObject.activeSelf) { codexQuickImage.color = Time.unscaledTime < codexPulseUntil - ? new Color(0.18f, 0.28f, 0.38f, 0.98f) - : new Color(0.045f, 0.058f, 0.085f, 0.96f); + ? new Color(1f, 0.88f, 0.58f, 1f) + : Color.white; } } @@ -1025,7 +1097,7 @@ private void SetOverlayBackdrop(bool dimmed) overlayRootImage.color = dimmed ? new Color(0.012f, 0.015f, 0.023f, 0.94f) - : new Color(0.012f, 0.015f, 0.023f, 0.18f); + : Color.clear; overlayRootImage.raycastTarget = dimmed; } @@ -1048,10 +1120,11 @@ private void UpdateCodexTextLayout() } Canvas.ForceUpdateCanvases(); - var viewportHeight = codexScrollRect.viewport == null ? 166f : codexScrollRect.viewport.rect.height; + var viewportWidth = codexScrollRect.viewport == null ? 920f : codexScrollRect.viewport.rect.width; + var viewportHeight = codexScrollRect.viewport == null ? 380f : codexScrollRect.viewport.rect.height; var contentHeight = Mathf.Max(viewportHeight, Mathf.Ceil(codexText.preferredHeight) + 24f); - codexTextContent.sizeDelta = new Vector2(484f, contentHeight); - codexText.rectTransform.sizeDelta = new Vector2(456f, Mathf.Max(146f, contentHeight - 20f)); + codexTextContent.sizeDelta = new Vector2(viewportWidth, contentHeight); + codexText.rectTransform.sizeDelta = new Vector2(Mathf.Max(120f, viewportWidth - 36f), Mathf.Max(viewportHeight - 20f, contentHeight - 20f)); Canvas.ForceUpdateCanvases(); codexScrollRect.verticalNormalizedPosition = 1f; } @@ -1144,36 +1217,35 @@ private void ToggleObserverMode() UpdateOptionSummaries(); } - private Button CreateButton(string name, Transform parent, string label, Vector2 anchoredPosition, UnityEngine.Events.UnityAction action) - { - return CreateButton(name, parent, label, anchoredPosition, new Vector2(236, 44), Anchor.Center, 18, action); - } - - private Button CreateButton(string name, Transform parent, string label, Vector2 anchoredPosition, Vector2 size, Anchor anchor, int labelSize, UnityEngine.Events.UnityAction action) + private Button CreateButton( + string name, + Transform parent, + string label, + Vector2 anchoredPosition, + UnityEngine.Events.UnityAction action, + MagicExamButtonStyle style = MagicExamButtonStyle.Secondary) { - var body = CreatePanel(name, parent, anchoredPosition, size, anchor, new Color(0.045f, 0.058f, 0.085f, 0.96f)); + var body = CreatePanel(name, parent, anchoredPosition, new Vector2(236, 46), Anchor.Center, Color.white); var image = body.GetComponent(); var button = body.gameObject.AddComponent