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/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/Scripts/Runtime/ExamGameController.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/ExamGameController.cs index d144e35..0cf4889 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/ExamGameController.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/ExamGameController.cs @@ -36,12 +36,12 @@ public sealed class ExamGameController : MonoBehaviour 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 static readonly string[] FirstFloorTutorialGoalOrder = { "puddle", "ember", "vane", "pillar", "vine" }; @@ -261,6 +261,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); @@ -397,12 +398,27 @@ private IEnumerable BaseIntentGoalsForCurrentFloor() return current == null ? Enumerable.Empty() : new[] { current }; } + /// + /// 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() { 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(); @@ -1037,6 +1053,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); @@ -1103,33 +1123,62 @@ 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, 476), Anchor.TopLeft); + reportText.color = MagicExamUiTheme.ParchmentInk; + reportText.verticalOverflow = VerticalWrapMode.Truncate; + MagicExamUiFactory.StyleParchmentText(reportText); 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); versionText = CreateText("Build Version", canvas.transform, BuildVersion, 11, FontStyle.Normal, new Vector2(-14, 10), new Vector2(300, 20), Anchor.BottomRight); @@ -1155,8 +1204,9 @@ private void BuildUi() 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 +1216,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 +1228,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 +1256,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 +1274,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 +1300,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() @@ -1641,11 +1686,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 +1700,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 +1708,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 +1735,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; @@ -1864,7 +1909,8 @@ private void LoadFloor(int index, bool saveCurrentQuestScore = true) resultText.text = ""; } - floorSkipButton.gameObject.SetActive(true); + floorSkipButton.gameObject.SetActive(Debug.isDebugBuild || Application.isEditor); + healthPanel.gameObject.SetActive(true); questScrollPanel.gameObject.SetActive(true); CloseCustomReferenceUi(); HideGoalProximityBubble(); @@ -2447,7 +2493,7 @@ 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++; } @@ -3644,11 +3690,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) @@ -5876,6 +5922,7 @@ private void ShowEndingReport() notePanel.gameObject.SetActive(false); resultPanel.gameObject.SetActive(false); floorSkipButton.gameObject.SetActive(false); + healthPanel.gameObject.SetActive(false); questScrollPanel.gameObject.SetActive(false); audioDirector?.PlaySfx(AudioCue.EndingReportOpened, 0.88f); mentor?.Say(MentorMood.Neutral, ""); @@ -5946,10 +5993,11 @@ 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.text = seal.Label; + AddTextOutline(text); var defaultFallbackAt = string.IsNullOrWhiteSpace(seal.customShapeId) ? seal.createdAt + DefaultSealFallbackDelaySeconds : float.PositiveInfinity; @@ -6277,14 +6325,19 @@ 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 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) + : 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 +6345,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.011f : 0.012f); 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 +6364,17 @@ 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, 22, FontStyle.Bold, textPosition, textSize, Anchor.Center); text.alignment = TextAnchor.MiddleCenter; - text.color = Color.Lerp(goal.color, Color.white, 0.45f); + text.color = Color.Lerp(goal.color, Color.white, 0.62f); text.horizontalOverflow = HorizontalWrapMode.Overflow; text.lineSpacing = 0.88f; text.raycastTarget = false; + AddTextOutline(text); if (visualRequirement) { CreateGoalRequirementIconRow(goal, canvasObject.transform, stageLabel); @@ -6329,6 +6383,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); @@ -6494,33 +6559,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 +6610,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--) @@ -8221,33 +8284,30 @@ public string BuildText(int totalAttempts, string outputDirectory, bool trueEndi var averageQuality = qualityScores.Count == 0 ? 0f : qualityScores.Average() * 100f; var endingName = trueEnding ? "진엔딩 (6/6 완전 복구)" : $"통과 엔딩 ({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" + - $"발견한 세계 반응: {discoveries.Count}개\n" + - $"평균 문양 안정도: {averageQuality:0}%\n" + - $"힌트 표시: {hintShownCount}회 / 최대 {AssistLevelLabel(maxAssistLevel)} / 힌트 후 성공 {assistedSuccessCount}회\n" + - $"퀘스트 체크: {questChecklistCompleted}/{questChecklistTotal}\n" + - $"{questChecklistSummary}\n" + + $"도달 상태 {endingName}\n" + + "────────────────────────────────────────\n" + + $"전체 시도 {totalAttempts}회 발견 {discoveries.Count}개 퀘스트 {questChecklistCompleted}/{questChecklistTotal}\n" + + $"가장 많이 사용한 base {favoriteBase} 가장 많이 사용한 overlay {favoriteOverlay}\n" + + $"평균 문양 안정도 {averageQuality:0}%\n" + + $"힌트 표시: {hintShownCount}회 / 최대 {AssistLevelLabel(maxAssistLevel)} / 힌트 후 성공 {assistedSuccessCount}회\n\n" + $"{BuildProfileSummary()}\n" + $"{excerptLine}\n" + - "보정 정책: profile은 성공/실패 판정을 뒤집지 않고 품질 설명과 다음 연습 방향에만 사용됩니다.\n\n" + + $"관찰 기록: {questChecklistSummary}\n\n" + + "보정 정책: 성공/실패 판정은 유지하고 품질 설명과 다음 연습 방향에만 사용합니다.\n" + BuildReflectionLine(favoriteBase, favoriteOverlay, discoveries.Count) + "\n" + - (trueEnding ? "이제 탑의 별자리에 당신의 이름이 있습니다." : "탑은 당신의 문양을 기억 속에 새겼습니다.") + "\n\n" + - "자기 평가\n" + - "1. 어떤 문양이 가장 내 손에 잘 맞았나요?\n" + - "2. 실패했을 때 다음에 고칠 점이 보였나요?\n" + - "3. base와 overlay 조합을 스스로 예측할 수 있었나요?\n" + - "4. 직접 마법을 시전한다는 느낌이 있었나요?\n\n" + - $"로그 저장 위치:\n{outputDirectory}"; + (trueEnding ? "탑의 별자리에 당신의 이름이 기록되었습니다." : "탑은 당신의 문양을 다음 시험까지 기억합니다.") + "\n\n" + + "자기 평가 손에 잘 맞은 문양 / 다음에 고칠 점 / 예측 가능한 조합\n" + + $"로그 저장 위치 {displayLogPath}"; } private static string BuildReflectionLine(string favoriteBase, string favoriteOverlay, int discoveryCount) diff --git a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/GameBootController.cs b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/GameBootController.cs index a666993..41d1ecc 100644 --- a/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/GameBootController.cs +++ b/unity/MagicExamHall/Assets/MagicExamHall/Scripts/Runtime/GameBootController.cs @@ -280,7 +280,7 @@ 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 void Initialize(ExamGameController gameController, Canvas targetCanvas, Font font) { @@ -420,72 +420,114 @@ 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(0f, 150f), MagicExamSettings.BgmVolume, value => MagicExamSettings.BgmVolume = value, value => $"{Mathf.RoundToInt(value * 100f)}%"); + sfxSlider = CreateSlider("SFX Slider", optionsBook, "SFX", new Vector2(0f, 88f), MagicExamSettings.SfxVolume, value => MagicExamSettings.SfxVolume = value, value => $"{Mathf.RoundToInt(value * 100f)}%"); + mouseSensitivitySlider = CreateSlider("Mouse Sensitivity Slider", optionsBook, "감도", new Vector2(0f, 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); + }, value => $"{Mathf.Lerp(0.55f, 1.75f, value):0.00}x"); + volumeSummaryText = CreateText("Volume Summary", optionsBook, "", 14, FontStyle.Normal, new Vector2(0f, -20f), new Vector2(720, 28), Anchor.Center, TextAnchor.MiddleCenter, MagicExamUiTheme.ParchmentMutedInk); + MagicExamUiFactory.StyleParchmentText(volumeSummaryText); + var optionsRule = CreateImage("Options Controls Rule", optionsBook, new Vector2(0f, -54f), new Vector2(800f, 2f), Anchor.Center, MagicExamUiTheme.BorderBrown); + optionsRule.raycastTarget = false; + CreateButton("Swap Mouse", optionsBook, "좌/우클릭", new Vector2(-265f, -98f), ToggleSwapMouse, MagicExamButtonStyle.Parchment); + CreateButton("Movement Preset", optionsBook, "이동 키", new Vector2(0f, -98f), CycleMovementPreset, MagicExamButtonStyle.Parchment); + CreateButton("Text Scale", optionsBook, "텍스트", new Vector2(265f, -98f), CycleTextScale, MagicExamButtonStyle.Parchment); + CreateButton("Color Assist", optionsBook, "색 보조", new Vector2(-265f, -160f), ToggleColorAssist, MagicExamButtonStyle.Parchment); + CreateButton("Observer Mode", optionsBook, "관찰 모드", new Vector2(0f, -160f), ToggleObserverMode, MagicExamButtonStyle.Parchment); + accessibilitySummaryText = CreateText("Accessibility Summary", optionsBook, "", 13, FontStyle.Normal, new Vector2(265f, -168f), new Vector2(230, 66), 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 +535,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)); @@ -639,7 +683,7 @@ private void ShowCodex() StateForTests = GameBootState.Codex; overlayRoot.gameObject.SetActive(true); overlayRoot.SetAsLastSibling(); - SetOverlayBackdrop(false); + SetOverlayBackdrop(true); HideAllPanels(); SetCodexPanelVisible(true); codexPanel.SetAsLastSibling(); @@ -663,7 +707,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); @@ -908,11 +952,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 +1019,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 +1069,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 +1092,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 +1189,74 @@ 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