화면 구성
01 게임
이 문서 안에서
플레이어가 보는 화면의 뼈대를 정합니다. 화면 안의 동작은 코어 루프와 각 시스템 문서가 다루고, 여기서는 틀만 다룹니다. ui/*.ui.json 문법은 UI 선언 시스템.
한 줄 요약 — 화면 전체가 DOM 탭 5개와 모달 오버레이이고, Phaser 는 왕국 광장 씬 하나와 작은 무대 3종(224×120 비네트 2종 + 240×160 가축 우리)에만 들어갑니다.
1. 월드 씬은 왕국 광장 하나뿐
src/game/main.ts에 Phaser import 없음. Phaser 가 들어가는 곳은 넷입니다.
| 무엇 | 크기·설정 |
|---|---|
| 왕국 광장 | 고정 월드(data/), zoom 2 고정, Scale., 카메라 드래그 |
| 주민 상세 작업 비네트 | 224×120, input: |
| 전사 전투 비네트 | 224×120, input: |
| 가축 우리 상세 | 240×160, Scale.FIT, input: |
전부 pixelArt:true, physics 없음, audio:{noAudio:true} 이고 동적 import 로 붙습니다. 청크가 실패해도 상세 화면과 왕국 허브는 그대로 동작합니다 — 광장 자리만 빕니다. 렌더러가 지연 청크로 남아 있는지는 tests/architecture.test.ts 가 지킵니다.
왕국 광장이 씬인 이유는 크기입니다. DOM display:grid 로 그리던 동안 광장의 크기는 기기에 남은 자리였고, 같은 왕국이 짧은 폰에서는 납작한 띠, 태블릿에서는 긴 밭이었습니다. 지금은 월드 픽셀로 authored 된 고정 크기를 고정 배율로 그리고 화면이 그 일부를 보여 줍니다 — 큰 화면은 더 큰 왕국이 아니라 왕국의 더 많은 부분을 봅니다. 스크롤이 가능해졌으므로 광장이 화면보다 커질 수 있고, 그래서 유료 확장이 가능해졌습니다. 폭은 8열 × 36 + 여백 8 × 2 = 304 월드 픽셀(zoom 2 에서 608 CSS 픽셀)로, 어떤 기기에서도 화면보다 넓습니다 — 그래서 카메라는 두 축 모두 실제로 움직입니다. 4열(=320 CSS 픽셀)이던 동안 가로 드래그는 어디서도 아무 일을 하지 않았습니다: 뷰포트보다 좁은 월드는 clampCameraScroll 이 가운데로 붙들어 손가락이 얼마를 움직여도 같은 x 를 돌려주기 때문입니다. 카메라가 처음 열리는 자리는 initialCameraScrollX/Y — 세로는 첫 칸 줄, 가로는 있는 칸들의 가운데입니다(칸 두 개짜리 새 왕국을 월드 중앙에 맞추면 폰 화면에서 두 칸이 왼쪽 밖으로 밀려납니다). 비네트 역할 11종(VIGNETTE_ROLES, 영입 가능한 직업 전부)은 아트와 LPC 캐릭터 참조.
광장 위쪽에는 왕궁이 늘 서 있습니다. 세이브의 건물 행이 아니라 항상 존재하는 하나이고, 짓지도 옮기지도 허물지도 못합니다 — 이 게임이 다시 세우고 있는 대상 자체이기 때문입니다. 자리는 palaceRect()(월드 폭 가운데, 첫 칸 줄에 밑변을 대고 64×80), 외형 5종은 PALACE_TIERS 이고 왕의 인장 0/8/14/20/26으로 영구 해금합니다. 왕국 레벨은 외형을 더 이상 자동 변경하지 않으며, 산 외형은 언제든 무료로 다시 고릅니다. 각 외형의 스프라이트(assets/sprites/palace/<id>.png, PALACE_TIERS.art 가 가리킴)를 씬이 텍스처로 바꿉니다 — AI 그림을 매지ента 크로마키로 투명화해 scripts/generate_palace_art.py 가 만든 64×80 픽셀아트입니다. 누르면 왕궁 시트가 열려 외형 전체와 가격을 보여 주고, 구매 확인을 거친 뒤 즉시 입힙니다. 푸터의 버튼은 왕실 꾸미기 시트로 들어갑니다(허브의 왕실 문과 같은 시트, 같은 레벨 게이트 chamberLevelReq()).
치장 테마는 이 세 가지 겉모습을 한 벌로 갈아입힙니다 — 광장의 하늘·풀·마당 색, 왕궁의 외형 5단계 그림, 왕실의 벽·바닥 마감입니다. 문은 허브 격자가 아니라 광장 위 한 줄의 왼쪽입니다(오른쪽은 광장 확장 버튼). 그 줄은 "광장 3/9칸"을 적던 자리였고, 그 숫자는 없앴습니다 — 빈 칸은 광장이 이미 그려서 보여 주고, 말로 적힌 숫자가 뭔가를 결정하는 곳은 확장 창 안뿐입니다. 버튼은 입고 있는 테마의 미리보기 타일을 달고 있어서, 라벨이 아니라 왕국의 현재 모습이 아이콘입니다. 격자에 남은 네 문은 전부 기다리는 일이고 각각 숫자 배지를 답니다 — 옷장은 기다리지 않으므로 배지가 켜질 수 없는 유일한 문이었습니다.
치장은 왕의 인장으로 삽니다(테마당 24, 확인 대화상자를 거쳐 시트 안에서 구매 — 2026-08-10까지는 IAP 상품 2종이었습니다). 사면 그 자리에서 입습니다: 방금 산 겉모습을 보러 한 번 더 누르게 하는 것은 시트가 할 일이 아닙니다. 산 것은 kingdom.themes(옷장)에, 입은 것은 kingdom.theme에 남고, 인장이 모자라면 부족분을 덮는 오퍼가 열립니다(인장소와 상품). 경계가 하나 있고 그것이 이 기능의 전부입니다: 테마는 왕궁 외형의 소유권이나 선택을 바꾸지 못합니다. 외형은 별도 선반에서 사고, 테마는 선택된 외형을 무엇으로 그리는지만 바꿉니다 — 그래서 테마마다 5장이 전부 있어야 합니다(한 장짜리 테마는 서로 다른 값으로 산 외형들을 같은 그림으로 만들어 버립니다). 왕실 마감은 레벨 사다리를 덮지 않고 늘립니다 — 골드로 산 마감은 테마를 갈아입어도 그대로 있고, 테마와 함께 온 마감만 테마를 벗을 때 사다리 기본값으로 돌아갑니다. 벽·바닥 칩도 누르는 즉시 바뀌지 않고 확인 대화상자에서 위치·이름·가격을 확인한 뒤 적용됩니다.
광장 아래쪽에는 가축 마당이 붙습니다. 칸 줄과 그 아래 여백(다음 줄이 될 자리)을 지나서 시작하는 띠이고, 우리 한 곳이 48×36 자리 하나를 씁니다(한 줄에 6곳, penRect() — 6×48 이 칸 8열의 8×36 과 정확히 같은 폭이라 마당은 칸 줄의 여백을 그대로 빌려 씁니다). 마당은 광장 칸과 똑같은 방식으로 그립니다: 지은 우리 수가 아니라 왕국이 열어 둔 자리 수만큼 땅과 울타리를 미리 깔고, 우리를 지으면 그 자리에 오두막이 들어섭니다(yardSlots). 빈 자리의 울타리는 흐리게, 우리가 선 자리는 진하게 그려서 — 빈 칸 윤곽선과 건물의 관계와 같습니다 — 아직 남은 자리가 어디인지 시트를 열지 않고도 보입니다. 자리 수는 왕국 레벨 사다리에 더해 유료 광장 확장 1단계마다 2곳씩 늘어납니다(penCapacity, PEN_SLOTS_PER_EXPANSION). 광장 칸과 우리 자리는 둘 다 언제나 짝수입니다 — 레벨 사다리도 두 칸씩, 유료 확장도 두 칸씩 올라가므로 한 줄 끝에 홀로 남는 자리가 생기지 않습니다(PLAZA_CELLS_BY_LEVEL, PEN_SLOTS_BY_LEVEL, tests/kingdom.test.ts). 마당은 칸과 똑같이 레벨 1부터 깔려 있습니다 — 가축 문이 레벨 2에 열리는 것은 문에 관한 사실이고 땅에 관한 사실이 아니기 때문입니다(yardSlots). 레벨로 마당을 가려 두었을 때는, 광장 확장 시트가 이미 "가축 마당도 2곳 넓어집니다"라며 자리를 세어(penCapacity) 인장을 받고는 그 땅을 그리지 않았습니다. 우리를 하나도 짓지 않았어도, 아직 아무 우리도 지을 수 없는 레벨이어도 마당은 이미 거기 있습니다. 자리마다 오두막 스프라이트와 울타리를 그리고, 산 마리 수만큼 동물이 그 안을 오갑니다 — 닭은 최대 3마리, 소는 1마리입니다(AnimalArt.yardMax: 48픽셀 자리에서 소 두 마리는 소 모양 얼룩 하나입니다). 기다리는 산물이 있으면 오두막 위에 개수 배지가 붙고 — 가득 차면 초록에서 호박색으로 바뀝니다. 우리는 누르면 열리고 꾹 누르면 들려서 다른 자리로 옮겨집니다 — 빈 자리에 놓으면 이동(movePen), 우리가 선 자리에 놓으면 자리 교환(swapPens)이고, 이는 건물의 moveBuilding·swapBuildings와 같은 한 쌍입니다. 마리 수와 저장 시각은 우리와 함께 따라갑니다(들고 다니는 것으로 산물을 거둘 수도, 잃을 수도 없습니다). 우리가 서 있는 자리는 LivestockPen.slot에 적히고, 이 필드가 없던 세이브는 배열 순서로 읽습니다(penPlacements) — 처음 만지는 동작이 전부를 기록합니다. 우리를 헐면 그 자리는 빈 자리로 남고 옆 우리는 움직이지 않으며, 다음에 짓는 우리가 그 빈 자리에 들어섭니다. 그냥 끌면 카메라가 움직입니다. 열리는 것은 ui/pen-detail.ui.json 대화상자이고, 그 안의 240×160 무대(scenes/pen-scene.ts)에서 세이브에 적힌 수 그대로의 동물이 돌아다닙니다. 배경과 오두막은 임시 그림이고 닭·소는 LPC 아트입니다(아트와 LPC 캐릭터). 양 우리의 양은 아직 없어서, 그 우리는 오두막과 배지만 그리고 안은 비워 둡니다 — 남의 동물을 대신 세우지 않습니다.
포인터 하나가 여러 일을 하고, 무엇인지는 누름 시점의 히트테스트와 누른 시간으로 정합니다.
| 누른 곳 | 탭 | 꾹 누르기(320ms) | 끌기 |
|---|---|---|---|
| 건물 | 상세 대화상자 | 들어서 운반 — 다른 칸에 놓으면 이동/교환 | 카메라 |
| 가축 우리 | 우리 무대 | 들어서 운반 — 빈 자리는 이동(movePen), 찬 자리는 교환(swapPens) | 카메라 |
| 주민 | 이름표 | — | 즉시 들어 올려 옮기기 |
| 왕궁 | 왕궁 오버레이 | — | 카메라 |
| 그 밖 | — | — | 카메라 |
건물과 우리에 시간 조건이 붙는 이유는 칸이 광장의 대부분을 덮기 때문입니다. "건물에서 시작한 끌기"는 왕국을 움직이겠다는 뜻이어야 하고, 두 뜻을 가르는 신호는 시간밖에 없습니다 — 아이폰 홈 화면이 하는 것과 같은 거래이고, 흔들림도 같은 답입니다: 꾹 누르기가 먹혔는지, 그리고 그것이 무엇에 대한 것인지(건물을 들면 건물들이, 우리를 들면 우리들이 흔들립니다)를 말 없이 알려 줍니다. 흔들림은 트윈이 아니라 update()에서 매 프레임 살아 있는 스프라이트를 다시 읽어 그립니다 — 화면이 1초마다 씬을 다시 싱크하므로 누름 시점에 잡아 둔 트윈 목록은 새로 생긴 스프라이트를 빠뜨리고 사라진 스프라이트를 붙들고 있게 됩니다.
주민만 꾹 누르기가 없습니다. 주민 아래에는 풀밭뿐이어서 끌기가 다른 뜻일 수 없고, 유일하게 가능한 일을 하려고 기다리게 하는 것은 기다림에 값을 매기는 일입니다. 내려놓은 자리는 세이브에 적지 않습니다 — 주민의 레인은 원래 id 해시(laneOf)에서 나오므로, 이 이동은 씬이 살아 있는 동안의 연출이고 다음 방문에는 해시가 정한 자리에서 다시 걷습니다. "내 사람들이 서 있는 자리" 필드는 세이브 마이그레이션과 클라우드 충돌을 불러오는데, 플레이어가 심심풀이로 옮기는 것에는 과한 값입니다.
카메라를 겨눌 때 Phaser 의 scrollX/scrollY 를 화면 왼쪽 위의 월드 좌표로 쓰면 안 됩니다. Phaser 는 카메라 중심을 기준으로 zoom 을 적용하므로 보이는 띠는 카메라 크기 / zoom 이고 그 중심이 scroll + 카메라 크기 / 2 입니다. 그래서 씬은 뷰 원점으로만 말하고 cameraScrollFor() / cameraOriginFor() 에서만 렌더러의 규약을 다룹니다. 이 둘을 혼동하던 동안 360px 캔버스가 월드 x 0 이 아니라 80 에서 열려 칸 절반이 화면 밖에 있었고 드래그 클램프도 엉뚱한 수를 붙들고 있었습니다(tests/plaza-world.test.ts).
2. 화면 상자 (game.html)
| 항목 | 값 |
|---|---|
lang 속성 | 없음 — 로케일 감지 간섭 방지 (다국어 텍스트) |
| 뷰포트 | width=device- |
| 기본 CSS | margin:0; padding:0; background:, #game- |
| 가로모드 차단 | @ → 한국어 하드코딩 안내 |
3. 층 구조
| z-index | 무엇 | 붙는 곳 |
|---|---|---|
| 10 | 탭 화면 6개 | #game- |
| 100 | 탭바 . / 세이브 실패 배너 | #game- |
| 150 | 모달 오버레이 . | document. |
| 200 | 토스트 . | document. |
| 400 | 제작·레벨업 연출 | document. |
| 450 | 골드 연출 . | document. |
| 9000 | 인트로 컷씬 . | #game- |
| 9999 | 튜토리얼 . / 가로모드 경고 | 컨테이너 / body |
4. 6개 탭
buildGame()이 6개 div를 한꺼번에 만들어 visibility로만 전환합니다. 탭을 누를 때마다 빌더 재실행.
| 탭 | 키 | DOM id | 배경색 | 정의 |
|---|---|---|---|---|
| 주민 | villagers | screen- | #1a1a2e | ui/ |
| 가방 | inventory | screen-inv | #2d1a2e | ui/ |
| 왕국 | kingdom | screen- | #1a232e | ui/ |
| 미션 | mission | screen- | #1a2e2e | ui/ |
| 인장소 | shop | screen- | #2e2a1a | ui/ |
| 설정 | settings | screen- | #2e1a1a | ui/ |
화면 위치: position:fixed; inset: var(--sat) 0 max(var(--mlk-bar-h), var(--kbi, 0px)) 0. 채팅 버튼은 이 화면들 위에 겹쳐 뜹니다(자리를 비워 주지 않습니다). 대신 버튼이 켜져 있는 동안 --mlk-fab-clear(42px, 꺼지면 0px)를 발행하므로, 코너에 자기 조작 버튼이 있는 화면은 자기 스타일시트에서 그만큼 비킵니다 — 주민 .v-footer 는 padding-right(그만큼 모두 수거 가 줄고 새 주민 영입 이 왼쪽으로 밀립니다), 인장소 .sh-restore 는 width: calc(100% - …). 버튼 발자국(46+10=56px)보다 작은 이유는 패널이 이미 화면 가장자리에서 들어와 있기 때문입니다 — 공용 프레임이 화면 패딩 8px + 테두리 3px + 패널 패딩 8px 를 쓰므로 패널 위로 겹치는 건 37px 뿐입니다. 여기에 간격 5px 을 더합니다(37+5=42) — 양쪽 다 두꺼운 픽셀 테두리(버튼 3px, 패널 버튼 2px)를 두르고 있어서 박스가 조금만 떨어져도 프레임이 갈라져 보이고, 화면 가장자리와 같은 10px 은 구멍처럼 보였습니다. 56px 를 다 넘기면 필요보다 20px 더 밀립니다. --sar 는 양쪽에 똑같이 붙어 상쇄되므로 식에 없습니다. 남는 대가는 스크롤되는 행의 오른쪽 끝 버튼(인장소 구매)이고, 스크롤을 조금 움직이면 닿습니다.
탭바 DOM 순서: 주민→가방→왕국→미션→인장소→설정 (코드 생성 순서와 다름, data-screen으로 매칭).
| 탭 전환 시 살아남는 상태 | 저장 위치 |
|---|---|
| 주민 역할 필터 | 모듈 전역 (villager-) |
| 가방 카테고리 필터 | 모듈 전역 (inventory-) |
| 미션 하위 탭 | dataset. |
| 스크롤 위치 | clearAndMount가 복원 |
채팅은 탭이 아닙니다. 우측 하단 플로팅 버튼(ui/chat-fab.ui.json)이 반투명 채팅창을 openGameOverlay로 띄웁니다. 컨트롤러는 모듈 전역 싱글턴(소켓·로그·IME 유지)이고 창이 닫혀 있어도 계속 수신하므로, 닫힌 동안 들어온 줄은 버튼의 미읽음 배지로 셉니다. soloMode 시 버튼 숨김 + 컨트롤러 파괴, applyConnectivity가 오프라인 시 자동 전환.
| 탭 | 핵심 | 상세 |
|---|---|---|
| 주민 | 카드 목록·드래그 정렬·모두 수거·영입 | 주민과 직업, 명부와 영입 |
| 가방 | 5열 격자·판매(수량 스테퍼) | 인벤토리와 판매 |
| 왕국 | 껍데기만 — 성 문장과 kingdom. 한 줄 | — |
| 미션 | 5 하위 탭(로그인·일일·주간·월간·업적) | 미션과 출석 |
| 인장소 | 한정 특가→패키지→재건 기금→시즌 패스→인장팩→영구 특권→골드 교환 | 인장소와 상품 |
| 설정 | 패치 노트→튜토리얼→BGM/SFX→언어→클라우드→싱글→UID→차단→피드백 | 소리와 에셋 파이프라인 |
| 채팅(창) | 로비, 뮤트·신고, 미읽음 배지 | 실시간 방과 채팅 |
탭바: ui/bottom-tab-bar.ui.json, 탭 6개, height:52px, border-top:3px solid #8b6914. 라벨은 한 줄 고정(white-space: nowrap, font-size: clamp(9px, 2.5vw, 11px), 영어는 clamp(8px, 2.3vw, 10px)) — 탭이 6개가 되면서 360px 폭에서 한 탭이 60px 밖에 안 되고, 그대로 두면 영어 Seal Office 와 일본어 ミッション 이 두 줄로 접혀 52px 상자를 넘겼습니다. 320px 영어 Seal Office 만 끝이 잘립니다(이전에는 같은 폭에서 두 줄로 접혔습니다). --mlk-bar-inset: clamp(6px, var(--sab, 0px), 10px), --mlk-bar-h = calc(55px + var(--mlk-bar-inset)).
채팅 버튼: ui/chat-fab.ui.json, 46×46, right: calc(10px + var(--sar)), bottom: calc(max(var(--mlk-bar-h), var(--kbi)) + 10px), z-index:120(탭바 100 < 120 < 오버레이 150). 화면과 같은 바닥 기준(max(탭바, 키보드))을 쓰므로 IME 가 올라오면 버튼도 같이 올라갑니다 — tests/tab-bar-inset.test.ts 가 지킵니다.
소프트 키보드
두 갈래이고, 한 갈래가 조용히 죽어 있었습니다.
| 환경 | 누가 올리나 |
|---|---|
| 브라우저·iOS | 키보드가 전체 높이 뷰포트를 덮고 visualViewport 만 줄어듭니다. - 가 그 높이를 CSS 로 옮깁니다 |
| Android 셸 | MainActivity 가 WebView 를 IME 만큼 마진합니다. 뷰포트 자체가 줄어드니 - 는 0 이 맞습니다 |
키보드가 올라온 동안에는 <html> 에 .kb-open 이 붙고 탭바가 접힙니다(display:none + --mlk-bar-h: 0px). 입력 중에는 이동할 곳이 없으니 탭바의 55px 을 메시지 로그에 줍니다. 판정은 포커스가 아니라 뷰포트입니다(isKeyboardOpen): 브라우저·iOS 는 --kbi > 0, Android 는 "키보드 없을 때 최대 높이보다 120px 이상 줄었는가". 포커스로 판정하면 Android 뒤로가기로 키보드를 닫았을 때 포커스가 입력창에 남아 탭바가 사라진 채로 남습니다.
targetSdk 35 는 창이 항상 edge-to-edge 라서 매니페스트의 adjustResize 는 혼자서는 아무 일도 하지 않습니다. 키보드는 WindowInsetsCompat.Type.ime() 로 오고 앱이 적용해야 합니다. insetWebViewFromSystemBars() 가 systemBars|displayCutout 만 요청하고 CONSUMED 를 반환하던 동안 IME inset 은 버려졌고, WebView 높이가 그대로라 visualViewport 도 줄지 않아 --kbi 가 0 에 머물렀습니다 — 채팅 입력창이 키보드 뒤에 남았습니다.
가로 회전 안내(@media (orientation: landscape) and (max-height: 500px))에 min-width: 500px 가 붙어 있는 이유도 여기입니다. 키보드가 올라온 세로 화면은 411×400 — 높이보다 넓어서 orientation: landscape 에 걸립니다.
5. 화면 프레임 규약
fillScreen(el, key): display:flex; overflow:hidden. 스크롤은 .mlk-scroll 전담. CSS 변수: --mlk-screen-pad-y:6px, --mlk-screen-pad-x:8px, --mlk-panel-pad:8px.
6. 오버레이 규약
| 항목 | 규칙 |
|---|---|
| 생성 | openGameOverlay(opts) → document., ., role="dialog" |
| 닫기 | close() / Escape / 백드롭 탭 |
| 효과음 | 열기 ui., 닫기 ui. |
| 스크림 | rgba(6,6,14,0.82) |
| 패딩 | 위 16px+, 아래 16px+, 좌우 8px+ |
singletonKey | 같은 키 이전 오버레이를 먼저 닫음 |
| Esc | 맨 위 한 장만 닫음 (스택) |
markBusy() | 400ms 중복 클릭 방지 |
| 오버레이 | singletonKey | 최대 폭 |
|---|---|---|
| 주민 상세 | villager- | 460px |
| 영입 | recruit | 500px |
| 일괄 수거 | collect-all | 420px |
| 제작 | craft | 480px |
| 제작 중 항목 | craft-job | 380px |
| 후보 상세 | recruit- | 380px |
| 확인 대화상자 | confirm- | 380px |
| 상품 설명 | shop- | 360px |
| 패치 노트 | patch- | 480px |
| 피드백 | feedback- | 420px |
| 차단 목록 | blocked- | 420px |
7. 토스트
| 값 | 수치 |
|---|---|
| 유지 | 2,600 ms |
| 페이드 | 300 ms |
| 동시 최대 | 3 (스택) |
| 위치 | calc(var(- |
| 최대 폭 | min(420px, 100%) |
| 톤 | info 금색 #e8c84a/#8b6914/#2a1a0d, warn 주황 #e8a84a/#8a4a4a/#2e1414 |
| 상호작용 | 없음 — 호스트와 본체 모두 pointer- |
토스트는 살아 있는 화면 위에 떠 있으므로 탭 입력을 가로채지 않습니다. 헤더 줄이나 탭바를 겹쳐도 탭은 뒤쪽 버튼/탭에 그대로 전달되고, 토스트는 자기 타이머로만 사라집니다(눌러서 닫기 없음).
쿠폰 동기화 완료가 인트로 중이면 gamePainted 후에 표시합니다.
8. 안전영역과 소프트 키보드
| 변수 | 뜻 |
|---|---|
-/-/-/- | 네 방향 안전영역 (env(), 안드로이드는 네이티브) |
- | 소프트 키보드 높이 |
- | clamp(6px, var(- |
- | calc(55px + var(- |
--kbi 사용: 탭 화면 inset, 인트로 이름 입력 모달, 피드백 오버레이.
9. 전체화면을 덮는 세 가지
| 표면 | z-index | 조건 |
|---|---|---|
| 인트로 컷씬 | 9000 | 첫 실행 (세계관과 인트로) |
| 튜토리얼 | 9999 | 첫 실행 1회 (tutorialSeen) · 설정에서 다시 보기 · ?tutorial=demo 강제 |
| 세이브 실패 배너 | 100 | 읽기 실패 시 상단 띠 (세이브와 마이그레이션) |
10. ui/*.ui.json 마운트
게임 코드가 import하는 20개:
| 파일 | 화면 |
|---|---|
bottom- | 탭바 |
villagers / inventory- / kingdom- / mission- / seal- / settings- | 탭 6종 |
chat-fab / chat- | 채팅 버튼 + 반투명 채팅창 |
villager- / recruit / recruit- / collect-all / craft / confirm- / shop- / patch- / feedback- / blocked- | 오버레이 10종 |
intro- / name- | 인트로 |
tutorial- | 튜토리얼 |
참조 없는 5개(hud, status-bars, daily-login, action-menu, dialog-sample)는 남겨둡니다. 5개 정의는 스토어 이미지 렌더러가 재사용합니다(마케팅 소재).
이어서 읽기
- 코어 루프 — 화면 위에서 반복되는 한 바퀴
- UI 선언 시스템 —
ui/*.ui.json문법과 테마 토큰 - 세계관과 인트로 — 첫 실행 컷씬
- 주민과 직업 — 주민 탭이 그리는 대상
- 실시간 방과 채팅 — 채팅 컨트롤러
- 구현 상태 — 어긋남과 미구현 항목