commit d6cfaaeabb0f68ed6e6a4b1aef8940f0969ce6f8 Author: Wickedness Date: Thu Jun 11 22:34:07 2026 +0900 Add initial romance game design data diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1507c43 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +Thumbs.db +*.log +*.tmp +__pycache__/ +.pytest_cache/ +.venv/ +venv/ +dist/ +build/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..48f6478 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Date Game + +성인 로맨스 데이트 게임 프로토타입입니다. + +현재 단계의 목표는 엔진을 정하기 전에 게임의 핵심 데이터를 먼저 안정적으로 잡는 것입니다. 맵에서 장소를 클릭하고, 우연한 만남과 약속, 데이트, 연인 전환으로 이어지는 구조를 데이터 중심으로 설계합니다. + +## 현재 구성 + +- `data/characters.json`: 22명의 성인 여성 캐릭터와 각자의 루트 구조 +- `data/locations.json`: 맵 클릭 지점, 주요 시간대, 우연한 만남 태그 +- `data/progression.json`: 만남에서 연인 전환까지의 공통 진행 규칙 +- `docs/game_structure.md`: 게임 루프와 시스템 설계 메모 +- `scripts/validate_content.py`: 캐릭터 나이와 장소 참조 무결성 검증 + +## 콘텐츠 원칙 + +- 모든 공략 가능 캐릭터는 만 19세 이상 성인입니다. +- 성인 이벤트는 상호 동의, 관계 단계, 캐릭터 신뢰도 조건을 만족한 뒤 해금됩니다. +- 현재 데이터는 노골적인 장면 묘사 대신 캐릭터의 성향, 관계 흐름, 관능적 연출 방향을 정의합니다. + +## 검증 + +```bash +python3 scripts/validate_content.py +``` diff --git a/data/characters.json b/data/characters.json new file mode 100644 index 0000000..bdc8bb9 --- /dev/null +++ b/data/characters.json @@ -0,0 +1,1440 @@ +{ + "version": "0.1.0", + "content_rules": { + "all_romanceable_characters_are_adults": true, + "minimum_age": 19, + "adult_event_policy": "성인 캐릭터 간 상호 동의와 관계 단계 조건을 만족한 뒤 관능적 연출로 처리한다." + }, + "characters": [ + { + "id": "han_seoyun", + "display_name": "한서윤", + "age": 24, + "occupation": "쿠킹 클래스 강사", + "height_cm": 164, + "body_type": "균형 잡힌 부드러운 체형", + "archetype": ["현모양처", "다정함", "은근한 소유욕"], + "personality": { + "summary": "생활력이 강하고 상대를 챙기는 데 익숙하지만, 가까워질수록 독점욕이 살짝 드러난다.", + "traits": ["헌신적", "섬세함", "현실적", "질투를 숨김"], + "flaws": ["자기 마음을 뒤로 미룸", "약속 파기에 예민함"] + }, + "romance_style": "편안한 일상에서 시작해 깊고 안정적인 관계로 발전한다.", + "visual_direction": { + "fashion": ["앞치마", "오프숄더 니트", "부드러운 홈웨어"], + "adult_illustration_tone": "따뜻한 실내 조명, 관능적인 홈 데이트 분위기" + }, + "preferred_locations": ["cooking_studio", "neighborhood_market", "riverside_park"], + "likes": ["정성스러운 선물", "함께 장보기", "약속 시간 지키기"], + "dislikes": ["가벼운 작업 멘트", "음식 남기기", "갑작스러운 잠수"], + "first_encounter": { + "location": "cooking_studio", + "title": "태워버린 팬케이크", + "setup": "주인공이 체험 수업에서 실수하자 서윤이 웃으며 도와준다.", + "success_keys": ["솔직하게 도움 요청", "농담보다 감사 표현"] + }, + "date_events": [ + { + "stage": 1, + "title": "시장 골목의 장보기", + "location": "neighborhood_market", + "focus": "일상 취향과 생활감 확인", + "success_keys": ["무거운 짐 들어주기", "그녀가 고른 재료 기억하기"] + }, + { + "stage": 2, + "title": "둘만의 저녁 식탁", + "location": "cooking_studio", + "focus": "서로의 가족관과 연애관 공유", + "success_keys": ["진지한 미래 대화", "서윤의 부담을 덜어주기"] + }, + { + "stage": 3, + "title": "강변의 늦은 고백", + "location": "riverside_park", + "focus": "안정적인 연인 관계 확정", + "success_keys": ["독점욕을 비난하지 않기", "분명한 관계 의사 표현"] + } + ], + "lover_transition": { + "title": "따뜻한 식탁의 약속", + "requirements": ["date_stage_3_complete", "trust >= 50", "commitment >= 45"], + "result": "연인 단계와 홈 데이트 애프터 스토리 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "romantic_home", + "setup": "집 데이트 후 서로의 마음을 확인하는 밤", + "required_flags": ["lovers", "consent_confirmed"], + "presentation": "suggestive_cg_or_fade_to_black" + } + ] + }, + { + "id": "yu_harin", + "display_name": "유하린", + "age": 22, + "occupation": "도서관 계약직 사서", + "height_cm": 158, + "body_type": "아담하고 곡선적인 체형", + "archetype": ["요조숙녀", "부끄럼 많음", "순애"], + "personality": { + "summary": "말수는 적지만 좋아하는 책과 사람에게는 깊이 몰입한다.", + "traits": ["조심스러움", "상상력 풍부", "예의 바름", "천천히 가까워짐"], + "flaws": ["오해를 바로 풀지 못함", "갈등 회피"] + }, + "romance_style": "느리게 마음을 열고 작은 약속을 소중히 여긴다.", + "visual_direction": { + "fashion": ["롱스커트", "얇은 가디건", "단정한 원피스"], + "adult_illustration_tone": "수줍은 표정과 살짝 흐트러진 단정함" + }, + "preferred_locations": ["book_cafe", "old_bookstore", "riverside_park"], + "likes": ["조용한 대화", "책갈피", "비 오는 날 카페"], + "dislikes": ["큰 목소리", "성급한 스킨십", "약속 없는 방문"], + "first_encounter": { + "location": "old_bookstore", + "title": "같은 책을 잡은 손", + "setup": "절판된 소설을 동시에 집으며 어색한 미소를 나눈다.", + "success_keys": ["책을 양보하기", "작가 취향 물어보기"] + }, + "date_events": [ + { + "stage": 1, + "title": "북카페의 밑줄", + "location": "book_cafe", + "focus": "취향 공유와 편안함 형성", + "success_keys": ["그녀가 고른 문장 기억", "조용한 분위기 유지"] + }, + { + "stage": 2, + "title": "비 오는 강변", + "location": "riverside_park", + "focus": "수줍은 신뢰와 보호받는 감각", + "success_keys": ["우산 씌워주기", "무리하게 묻지 않기"] + }, + { + "stage": 3, + "title": "마지막 장의 고백", + "location": "book_cafe", + "focus": "서툰 진심을 받아들이기", + "success_keys": ["기다려주겠다고 말하기", "직접적인 고백"] + } + ], + "lover_transition": { + "title": "접힌 책갈피", + "requirements": ["date_stage_3_complete", "trust >= 55", "comfort >= 40"], + "result": "순애형 연인 루트 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "shy_romantic", + "setup": "오랜 망설임 끝에 서로의 속도를 확인하는 밤", + "required_flags": ["lovers", "consent_confirmed", "trust >= 60"], + "presentation": "soft_suggestive_cg" + } + ] + }, + { + "id": "kang_mirae", + "display_name": "강미래", + "age": 26, + "occupation": "브랜드 매니저", + "height_cm": 171, + "body_type": "슬림하고 탄탄한 체형", + "archetype": ["커리어우먼", "도도함", "주도권 싸움"], + "personality": { + "summary": "자기 기준이 확고하고 쉽게 흔들리지 않지만, 인정받고 싶은 마음이 크다.", + "traits": ["야심 있음", "분석적", "세련됨", "승부욕"], + "flaws": ["약한 모습을 싫어함", "감정 표현이 늦음"] + }, + "romance_style": "서로의 능력과 자존심을 인정하면서 뜨겁게 가까워진다.", + "visual_direction": { + "fashion": ["핏 좋은 수트", "슬릿 스커트", "깔끔한 이브닝 드레스"], + "adult_illustration_tone": "차가운 표정과 대비되는 강한 긴장감" + }, + "preferred_locations": ["office_district", "rooftop_lounge", "wine_bar"], + "likes": ["정확한 약속", "일에 대한 존중", "센스 있는 대화"], + "dislikes": ["무능한 허세", "즉흥적인 방해", "공개 망신"], + "first_encounter": { + "location": "office_district", + "title": "떨어진 기획서", + "setup": "바람에 날린 기획서를 함께 줍다가 그녀의 날카로운 평가를 듣는다.", + "success_keys": ["침착하게 받아치기", "일 이야기를 진지하게 듣기"] + }, + "date_events": [ + { + "stage": 1, + "title": "퇴근 후 한 잔", + "location": "wine_bar", + "focus": "능력과 취향 검증", + "success_keys": ["과시하지 않기", "그녀의 성과 인정"] + }, + { + "stage": 2, + "title": "루프탑의 협상", + "location": "rooftop_lounge", + "focus": "주도권과 감정의 균형", + "success_keys": ["밀리지 않되 존중하기", "솔직한 호감 표현"] + }, + { + "stage": 3, + "title": "새벽의 빈 사무실", + "location": "office_district", + "focus": "약한 모습을 공유하는 순간", + "success_keys": ["해결책보다 곁에 있기", "비밀 지키기"] + } + ], + "lover_transition": { + "title": "동등한 파트너", + "requirements": ["date_stage_3_complete", "trust >= 45", "tension >= 45"], + "result": "커리어 갈등 애프터 스토리 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "confident_tension", + "setup": "루프탑 데이트 후 서로의 주도권을 확인하는 사적인 시간", + "required_flags": ["lovers", "consent_confirmed"], + "presentation": "high_tension_suggestive_cg" + } + ] + }, + { + "id": "jung_naeun", + "display_name": "정나은", + "age": 21, + "occupation": "카페 아르바이트생", + "height_cm": 160, + "body_type": "부드러운 보통 체형", + "archetype": ["츤데레", "말보다 행동", "티격태격"], + "personality": { + "summary": "말은 차갑지만 챙길 건 다 챙기는 타입이다.", + "traits": ["솔직하지 못함", "정 많음", "반응이 빠름", "질투가 티남"], + "flaws": ["사과가 늦음", "놀림에 쉽게 발끈함"] + }, + "romance_style": "가벼운 말싸움에서 시작해 서로의 빈틈을 알아간다.", + "visual_direction": { + "fashion": ["크롭 가디건", "짧은 재킷", "카페 유니폼"], + "adult_illustration_tone": "새침한 표정과 부끄러움을 숨기는 포즈" + }, + "preferred_locations": ["downtown_cafe", "amusement_street", "cinema"], + "likes": ["가벼운 장난", "단 음식", "자기 편 들어주기"], + "dislikes": ["노골적인 칭찬", "비교", "대답 없는 메시지"], + "first_encounter": { + "location": "downtown_cafe", + "title": "잘못 나온 라떼", + "setup": "주문 실수로 말다툼이 시작되지만 그녀가 몰래 쿠키를 챙겨준다.", + "success_keys": ["과하게 따지지 않기", "쿠키를 눈치채고 고맙다고 하기"] + }, + "date_events": [ + { + "stage": 1, + "title": "인형 뽑기 승부", + "location": "amusement_street", + "focus": "장난과 승부욕", + "success_keys": ["져주지 않고 진심으로 하기", "성공 후 놀리지 않기"] + }, + { + "stage": 2, + "title": "옆자리의 공포 영화", + "location": "cinema", + "focus": "가까워지는 거리와 부끄러움", + "success_keys": ["놀란 걸 모른 척하기", "손을 잡기 전 확인하기"] + }, + { + "stage": 3, + "title": "닫힌 카페의 진심", + "location": "downtown_cafe", + "focus": "츤데레 방어막 해제", + "success_keys": ["먼저 사과하기", "분명하게 좋아한다고 말하기"] + } + ], + "lover_transition": { + "title": "투덜대는 고백", + "requirements": ["date_stage_3_complete", "affection >= 55", "trust >= 35"], + "result": "질투 이벤트와 카페 비밀 데이트 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "playful_blush", + "setup": "말다툼 뒤 화해하며 서로의 마음을 확인하는 밤", + "required_flags": ["lovers", "consent_confirmed"], + "presentation": "playful_suggestive_cg" + } + ] + }, + { + "id": "baek_chaewon", + "display_name": "백채원", + "age": 27, + "occupation": "이벤트 플래너", + "height_cm": 167, + "body_type": "글래머러스한 체형", + "archetype": ["사교적", "플러팅 능숙", "위험한 매력"], + "personality": { + "summary": "사람을 끌어당기는 법을 잘 알지만 진심을 들키는 건 두려워한다.", + "traits": ["화려함", "눈치 빠름", "농담 능숙", "외로움 숨김"], + "flaws": ["관계를 시험함", "진심을 장난처럼 포장함"] + }, + "romance_style": "가벼운 유혹에서 시작해 진심과 장난을 구분하는 루트다.", + "visual_direction": { + "fashion": ["깊은 브이넥 드레스", "화려한 액세서리", "파티룩"], + "adult_illustration_tone": "노출감 있는 의상과 자신감 있는 시선" + }, + "preferred_locations": ["rooftop_lounge", "fashion_street", "wine_bar"], + "likes": ["센스 있는 칭찬", "즉흥 데이트", "자신감"], + "dislikes": ["질투 폭발", "소유물 취급", "무심함"], + "first_encounter": { + "location": "rooftop_lounge", + "title": "잘못 건넨 초대장", + "setup": "행사장에서 초대장을 착각해 말을 걸고, 그녀가 흥미로운 장난을 건다.", + "success_keys": ["여유 있게 받아치기", "선을 넘지 않는 플러팅"] + }, + "date_events": [ + { + "stage": 1, + "title": "패션 거리의 시선", + "location": "fashion_street", + "focus": "관심과 질투의 경계", + "success_keys": ["그녀의 선택 존중", "타인의 시선에 흔들리지 않기"] + }, + { + "stage": 2, + "title": "와인잔 너머 진심", + "location": "wine_bar", + "focus": "장난 속 숨은 외로움", + "success_keys": ["농담 뒤 감정 읽기", "가볍게 단정하지 않기"] + }, + { + "stage": 3, + "title": "파티가 끝난 루프탑", + "location": "rooftop_lounge", + "focus": "진심 확인", + "success_keys": ["공개적으로 편 들어주기", "단둘이 진지하게 대화"] + } + ], + "lover_transition": { + "title": "장난이 아닌 고백", + "requirements": ["date_stage_3_complete", "tension >= 50", "trust >= 45"], + "result": "화려한 공개 연애와 질투 갈등 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "bold_romantic", + "setup": "파티 후 단둘이 남아 장난 아닌 마음을 확인한다.", + "required_flags": ["lovers", "consent_confirmed"], + "presentation": "sensual_cg_with_after_dialogue" + } + ] + }, + { + "id": "oh_sera", + "display_name": "오세라", + "age": 29, + "occupation": "플로리스트", + "height_cm": 169, + "body_type": "성숙하고 우아한 체형", + "archetype": ["미망인", "상처 치유", "성숙한 사랑"], + "personality": { + "summary": "조용하고 깊은 슬픔을 품었지만 다시 사랑할 용기를 찾고 있다.", + "traits": ["차분함", "배려 깊음", "성숙함", "조심스러움"], + "flaws": ["과거를 놓지 못함", "행복에 죄책감을 느낌"] + }, + "romance_style": "상처를 존중하고 천천히 새로운 관계를 허락받는 루트다.", + "visual_direction": { + "fashion": ["실크 블라우스", "차분한 원피스", "우아한 숄"], + "adult_illustration_tone": "성숙한 실루엣과 낮은 채도의 관능미" + }, + "preferred_locations": ["flower_shop", "lakeside_walk", "wine_bar"], + "likes": ["진중한 대화", "꽃말 기억", "기다려주는 태도"], + "dislikes": ["과거 캐묻기", "가벼운 위로", "불쌍히 여기는 시선"], + "first_encounter": { + "location": "flower_shop", + "title": "시들지 않는 꽃", + "setup": "주인공이 꽃을 고르지 못하자 세라가 조용히 꽃말을 설명해준다.", + "success_keys": ["꽃말 묻기", "개인사를 캐묻지 않기"] + }, + "date_events": [ + { + "stage": 1, + "title": "호숫가의 침묵", + "location": "lakeside_walk", + "focus": "말보다 함께 있는 시간", + "success_keys": ["침묵을 견디기", "성급한 위로 피하기"] + }, + { + "stage": 2, + "title": "닫힌 앨범", + "location": "flower_shop", + "focus": "과거와 현재의 경계", + "success_keys": ["선택권을 세라에게 주기", "기억을 존중하기"] + }, + { + "stage": 3, + "title": "와인바의 두 번째 시작", + "location": "wine_bar", + "focus": "다시 사랑할 용기", + "success_keys": ["죄책감 덜어주기", "서두르지 않는 고백"] + } + ], + "lover_transition": { + "title": "새 꽃병", + "requirements": ["date_stage_3_complete", "trust >= 65", "comfort >= 45"], + "result": "치유형 연인 루트와 조용한 애프터 스토리 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "mature_healing", + "setup": "오랜 망설임 끝에 새로운 사랑을 받아들이는 밤", + "required_flags": ["lovers", "consent_confirmed", "trust >= 70"], + "presentation": "elegant_suggestive_cg_and_afterglow_dialogue" + } + ] + }, + { + "id": "moon_jia", + "display_name": "문지아", + "age": 20, + "occupation": "이벤트 스태프", + "height_cm": 162, + "body_type": "발랄하고 건강한 체형", + "archetype": ["직진형", "에너지 넘침", "즉흥 데이트"], + "personality": { + "summary": "좋아하는 감정을 숨기지 않고 하루를 축제처럼 만든다.", + "traits": ["활발함", "낙천적", "직설적", "호기심 많음"], + "flaws": ["계획이 약함", "싫증이 빠름"] + }, + "romance_style": "우연한 놀이와 즉흥적인 약속이 빠르게 관계를 끌어올린다.", + "visual_direction": { + "fashion": ["핫팬츠", "컬러풀한 크롭 티", "축제 스태프 조끼"], + "adult_illustration_tone": "밝고 생동감 있는 노출 스타일" + }, + "preferred_locations": ["amusement_street", "university_quarter", "riverside_park"], + "likes": ["즉흥 약속", "길거리 음식", "사진 찍기"], + "dislikes": ["분위기 깨는 잔소리", "우유부단함", "긴 침묵"], + "first_encounter": { + "location": "amusement_street", + "title": "사라진 마스코트 머리띠", + "setup": "행사 준비 중 사라진 소품을 같이 찾으며 소란스러운 인연이 시작된다.", + "success_keys": ["같이 뛰어다니기", "실수를 웃어넘기기"] + }, + "date_events": [ + { + "stage": 1, + "title": "대학가 푸드트럭 투어", + "location": "university_quarter", + "focus": "함께 노는 호흡", + "success_keys": ["새 메뉴 도전", "사진 요청 받아주기"] + }, + { + "stage": 2, + "title": "놀이 거리의 벌칙 게임", + "location": "amusement_street", + "focus": "장난 속 호감 표현", + "success_keys": ["승부에 몰입", "벌칙을 유쾌하게 수락"] + }, + { + "stage": 3, + "title": "강변의 폭죽", + "location": "riverside_park", + "focus": "가벼움 뒤의 진심", + "success_keys": ["그녀의 불안을 알아채기", "확실한 미래 약속"] + } + ], + "lover_transition": { + "title": "축제 다음 날", + "requirements": ["date_stage_3_complete", "affection >= 60", "tension >= 35"], + "result": "즉흥 여행과 야간 축제 이벤트 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "playful_energy", + "setup": "축제 데이트 후 들뜬 감정을 서로 확인한다.", + "required_flags": ["lovers", "consent_confirmed"], + "presentation": "bright_suggestive_cg" + } + ] + }, + { + "id": "lim_sohee", + "display_name": "임소희", + "age": 25, + "occupation": "동네 병원 접수원", + "height_cm": 155, + "body_type": "작고 귀여운 체형", + "archetype": ["소심함", "보호본능", "불안형 애착"], + "personality": { + "summary": "조심스럽고 걱정이 많지만 한번 믿으면 매우 깊게 의지한다.", + "traits": ["세심함", "불안함", "정직함", "상대 반응을 잘 살핌"], + "flaws": ["버림받을까 두려워함", "확인 메시지가 많음"] + }, + "romance_style": "안심과 반복된 약속을 통해 안정적인 애착으로 바뀐다.", + "visual_direction": { + "fashion": ["파스텔 카디건", "짧은 원피스", "병원 접수 유니폼"], + "adult_illustration_tone": "보호받고 싶은 분위기와 조심스러운 관능미" + }, + "preferred_locations": ["downtown_cafe", "apartment_complex", "lakeside_walk"], + "likes": ["다정한 확인", "따뜻한 음료", "집 앞 배웅"], + "dislikes": ["읽씹", "농담 섞인 거절", "갑작스러운 일정 변경"], + "first_encounter": { + "location": "downtown_cafe", + "title": "흘린 약봉투", + "setup": "카페에서 떨어뜨린 약봉투를 주워주며 조심스러운 대화가 시작된다.", + "success_keys": ["개인정보 묻지 않기", "괜찮은지 부드럽게 확인"] + }, + "date_events": [ + { + "stage": 1, + "title": "익숙한 카페 자리", + "location": "downtown_cafe", + "focus": "안심할 수 있는 반복", + "success_keys": ["항상 같은 시간 도착", "작은 취향 기억"] + }, + { + "stage": 2, + "title": "집 앞의 긴 통화", + "location": "apartment_complex", + "focus": "불안의 원인 공유", + "success_keys": ["끝까지 들어주기", "불안을 비웃지 않기"] + }, + { + "stage": 3, + "title": "호숫가의 약속", + "location": "lakeside_walk", + "focus": "의존이 아닌 신뢰로 전환", + "success_keys": ["구체적인 약속", "스스로 선택하게 하기"] + } + ], + "lover_transition": { + "title": "안심해도 되는 사람", + "requirements": ["date_stage_3_complete", "trust >= 60", "comfort >= 55"], + "result": "안정 애착 애프터 스토리 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "gentle_reassurance", + "setup": "불안을 확인하고 서로의 속도에 맞추는 사적인 밤", + "required_flags": ["lovers", "consent_confirmed", "comfort >= 60"], + "presentation": "gentle_suggestive_cg" + } + ] + }, + { + "id": "cha_yuri", + "display_name": "차유리", + "age": 23, + "occupation": "프리랜서 모델", + "height_cm": 174, + "body_type": "장신의 모델형 체형", + "archetype": ["차가움", "독립적", "느린 해빙"], + "personality": { + "summary": "혼자 있는 데 익숙하고 타인의 평가를 믿지 않지만, 꾸준함에는 약하다.", + "traits": ["독립적", "냉정함", "관찰력", "자기관리"], + "flaws": ["방어적", "친절을 의심함"] + }, + "romance_style": "거리감을 존중하며 천천히 진짜 표정을 발견한다.", + "visual_direction": { + "fashion": ["모노톤 미니드레스", "롱부츠", "오버핏 코트"], + "adult_illustration_tone": "세련되고 차가운 노출, 긴 실루엣 강조" + }, + "preferred_locations": ["fashion_street", "boutique_studio", "rooftop_lounge"], + "likes": ["깔끔한 거리감", "사진 감각", "무심한 배려"], + "dislikes": ["외모 평가만 하는 말", "집착", "사적인 질문 공세"], + "first_encounter": { + "location": "fashion_street", + "title": "흔들린 사진", + "setup": "촬영 현장에서 실수로 프레임에 들어가고, 유리가 차갑게 한마디 한다.", + "success_keys": ["바로 사과", "촬영을 방해하지 않기"] + }, + "date_events": [ + { + "stage": 1, + "title": "부티크 피팅룸 앞", + "location": "boutique_studio", + "focus": "시선과 존중의 균형", + "success_keys": ["외모보다 노력 칭찬", "사적 공간 존중"] + }, + { + "stage": 2, + "title": "무표정한 루프탑", + "location": "rooftop_lounge", + "focus": "차가움 뒤의 피로", + "success_keys": ["대답 강요하지 않기", "따뜻한 음료 챙기기"] + }, + { + "stage": 3, + "title": "렌즈 밖의 얼굴", + "location": "fashion_street", + "focus": "꾸며진 이미지에서 벗어나기", + "success_keys": ["민낯의 감정 받아주기", "변하지 않는 태도"] + } + ], + "lover_transition": { + "title": "프레임 밖의 연인", + "requirements": ["date_stage_3_complete", "trust >= 55", "commitment >= 40"], + "result": "모델 촬영 갈등과 비공개 연애 이벤트 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "cool_private", + "setup": "화려한 촬영 후 꾸밈없는 모습으로 가까워지는 밤", + "required_flags": ["lovers", "consent_confirmed"], + "presentation": "stylish_suggestive_cg" + } + ] + }, + { + "id": "shin_yerim", + "display_name": "신예림", + "age": 28, + "occupation": "클래식 공연 기획자", + "height_cm": 166, + "body_type": "우아하고 균형 있는 체형", + "archetype": ["품위 있음", "고전적 취향", "은밀한 긴장"], + "personality": { + "summary": "격식과 예의를 중시하지만 사적인 순간에는 대담한 호기심을 보인다.", + "traits": ["우아함", "지적임", "절제", "은근한 장난기"], + "flaws": ["기준이 높음", "실수를 오래 기억함"] + }, + "romance_style": "격식 있는 데이트 속에 은근한 긴장감을 쌓는다.", + "visual_direction": { + "fashion": ["이브닝 드레스", "진주 귀걸이", "실크 블라우스"], + "adult_illustration_tone": "품위 있는 노출과 절제된 관능미" + }, + "preferred_locations": ["wine_bar", "jazz_bar", "old_bookstore"], + "likes": ["예의 있는 태도", "공연 감상", "와인 취향"], + "dislikes": ["천박한 농담", "무지한 척", "예약 실수"], + "first_encounter": { + "location": "wine_bar", + "title": "잘못 놓인 와인잔", + "setup": "예약석 착오로 합석하게 되고, 예림은 조용히 주인공의 태도를 살핀다.", + "success_keys": ["정중한 사과", "분위기 맞추기"] + }, + "date_events": [ + { + "stage": 1, + "title": "헌책방의 악보", + "location": "old_bookstore", + "focus": "지적인 취향 공유", + "success_keys": ["모르는 것을 솔직히 말하기", "그녀의 설명 경청"] + }, + { + "stage": 2, + "title": "재즈바의 낮은 조명", + "location": "jazz_bar", + "focus": "격식 뒤의 사적인 호기심", + "success_keys": ["가벼운 유혹을 품위 있게 받기", "분위기 읽기"] + }, + { + "stage": 3, + "title": "마지막 앙코르", + "location": "wine_bar", + "focus": "절제된 고백", + "success_keys": ["진지한 감상 공유", "사적인 초대 수락"] + } + ], + "lover_transition": { + "title": "사적인 앙코르", + "requirements": ["date_stage_3_complete", "trust >= 50", "tension >= 45"], + "result": "공연장 비하인드와 어른스러운 연인 루트 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "elegant_private", + "setup": "공연 후 사적인 공간에서 절제된 긴장이 풀린다.", + "required_flags": ["lovers", "consent_confirmed"], + "presentation": "elegant_suggestive_cg" + } + ] + }, + { + "id": "kim_doa", + "display_name": "김도아", + "age": 19, + "occupation": "대학교 1학년", + "height_cm": 159, + "body_type": "건강하고 풋풋한 체형", + "archetype": ["성인 초년생", "호기심 많음", "서툰 설렘"], + "personality": { + "summary": "성인이 된 뒤의 첫 연애를 조심스럽고도 들뜬 마음으로 경험한다.", + "traits": ["호기심", "솔직함", "배움이 빠름", "감정 표현이 큼"], + "flaws": ["경험 부족", "주변 말에 흔들림"] + }, + "romance_style": "서툴지만 분명한 동의와 배려를 중심으로 성장한다.", + "visual_direction": { + "fashion": ["대학 점퍼", "테니스 스커트", "캐주얼 크롭 티"], + "adult_illustration_tone": "밝고 성인다운 스타일, 과도한 유아화 금지" + }, + "preferred_locations": ["university_quarter", "downtown_cafe", "cinema"], + "likes": ["새로운 경험", "사진", "솔직한 칭찬"], + "dislikes": ["어리게 보는 태도", "무시하는 말투", "강요"], + "first_encounter": { + "location": "university_quarter", + "title": "길 잃은 신입생", + "setup": "축제 부스 위치를 찾던 도아가 주인공에게 길을 묻는다.", + "success_keys": ["동등한 성인으로 대하기", "친절하게 안내"] + }, + "date_events": [ + { + "stage": 1, + "title": "캠퍼스 디저트 지도", + "location": "university_quarter", + "focus": "새로운 장소를 함께 탐험", + "success_keys": ["추천 메뉴 기억", "결정권 나눠주기"] + }, + { + "stage": 2, + "title": "처음 고른 심야 영화", + "location": "cinema", + "focus": "서툰 긴장과 명확한 경계", + "success_keys": ["불편한지 확인", "장난으로 압박하지 않기"] + }, + { + "stage": 3, + "title": "카페 창가의 선언", + "location": "downtown_cafe", + "focus": "주변 시선에서 독립하기", + "success_keys": ["성인으로 존중", "관계를 분명히 하기"] + } + ], + "lover_transition": { + "title": "첫 연인의 이름", + "requirements": ["date_stage_3_complete", "trust >= 55", "comfort >= 45"], + "result": "캠퍼스 공개 연애와 성장형 루트 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "careful_first_love", + "setup": "서로의 속도와 경계를 확인한 뒤 가까워지는 밤", + "required_flags": ["lovers", "consent_confirmed", "trust >= 60"], + "presentation": "soft_suggestive_cg_with_choice_confirmation" + } + ] + }, + { + "id": "park_minseo", + "display_name": "박민서", + "age": 24, + "occupation": "퍼스널 트레이너", + "height_cm": 168, + "body_type": "운동으로 다져진 탄탄한 체형", + "archetype": ["헬스 트레이너", "승부욕", "활동형"], + "personality": { + "summary": "몸을 움직이며 감정을 푸는 타입으로, 말보다 행동의 진정성을 본다.", + "traits": ["건강함", "직설적", "의리", "경쟁심"], + "flaws": ["무리함", "약한 모습을 감춤"] + }, + "romance_style": "운동 파트너에서 서로의 회복을 챙기는 관계가 된다.", + "visual_direction": { + "fashion": ["스포츠 브라 탑", "레깅스", "집업 재킷"], + "adult_illustration_tone": "땀과 움직임이 강조되는 건강한 관능미" + }, + "preferred_locations": ["fitness_center", "riverside_park", "downtown_cafe"], + "likes": ["성실한 루틴", "건강식", "승부를 피하지 않는 태도"], + "dislikes": ["작심삼일", "몸매 품평", "핑계"], + "first_encounter": { + "location": "fitness_center", + "title": "잘못 든 덤벨", + "setup": "운동 자세를 틀리자 민서가 거침없이 교정해준다.", + "success_keys": ["자존심 세우지 않기", "다시 시도하기"] + }, + "date_events": [ + { + "stage": 1, + "title": "강변 러닝", + "location": "riverside_park", + "focus": "페이스 맞추기", + "success_keys": ["끝까지 완주", "과장된 칭찬 피하기"] + }, + { + "stage": 2, + "title": "스트레칭 룸의 침묵", + "location": "fitness_center", + "focus": "몸의 거리와 신뢰", + "success_keys": ["도움 전 확인", "부상 걱정하기"] + }, + { + "stage": 3, + "title": "운동 후 첫 치팅데이", + "location": "downtown_cafe", + "focus": "완벽주의 내려놓기", + "success_keys": ["쉼을 인정", "함께 흐트러지기"] + } + ], + "lover_transition": { + "title": "같은 페이스", + "requirements": ["date_stage_3_complete", "trust >= 45", "affection >= 55"], + "result": "커플 운동과 부상 회복 이벤트 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "athletic_tension", + "setup": "운동 후 서로를 챙기며 가까워지는 사적인 시간", + "required_flags": ["lovers", "consent_confirmed"], + "presentation": "sporty_suggestive_cg" + } + ] + }, + { + "id": "seo_irin", + "display_name": "서이린", + "age": 26, + "occupation": "일러스트레이터", + "height_cm": 161, + "body_type": "마르고 섬세한 체형", + "archetype": ["예술가", "감정 기복", "밤 산책"], + "personality": { + "summary": "감정이 작품에 그대로 드러나며, 이해받는 순간 깊게 의지한다.", + "traits": ["감성적", "창의적", "예민함", "몰입형"], + "flaws": ["기분 변화가 큼", "혼자 사라짐"] + }, + "romance_style": "작품과 감정을 함께 읽어주는 섬세한 관계가 필요하다.", + "visual_direction": { + "fashion": ["오버사이즈 셔츠", "페인트 묻은 쇼츠", "얇은 슬립 원피스"], + "adult_illustration_tone": "흐트러진 작업실과 몽환적인 관능미" + }, + "preferred_locations": ["art_studio", "riverside_park", "old_bookstore"], + "likes": ["밤 산책", "그림 감상", "말 없는 동행"], + "dislikes": ["작품 평가질", "감정 무시", "갑작스러운 방문"], + "first_encounter": { + "location": "art_studio", + "title": "벽화 앞의 얼룩", + "setup": "골목 벽화 작업 중 페인트가 튀며 어색한 사과가 오간다.", + "success_keys": ["옷보다 그림 걱정", "작품 의도 물어보기"] + }, + "date_events": [ + { + "stage": 1, + "title": "헌책방의 표지", + "location": "old_bookstore", + "focus": "영감과 취향 공유", + "success_keys": ["표지 그림 칭찬", "해석 강요하지 않기"] + }, + { + "stage": 2, + "title": "작업실의 새벽", + "location": "art_studio", + "focus": "몰입과 외로움", + "success_keys": ["잠깐 쉬게 하기", "작품보다 사람 챙기기"] + }, + { + "stage": 3, + "title": "강변의 스케치", + "location": "riverside_park", + "focus": "감정을 말로 꺼내기", + "success_keys": ["부정하지 않기", "함께 미래 상상"] + } + ], + "lover_transition": { + "title": "완성되지 않은 초상", + "requirements": ["date_stage_3_complete", "trust >= 55", "comfort >= 35"], + "result": "작업실 애프터 스토리와 전시회 루트 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "dreamy_artistic", + "setup": "새벽 작업실에서 서로의 감정을 확인한다.", + "required_flags": ["lovers", "consent_confirmed"], + "presentation": "dreamlike_suggestive_cg" + } + ] + }, + { + "id": "yoon_gayoung", + "display_name": "윤가영", + "age": 23, + "occupation": "편의점 점장 대리", + "height_cm": 163, + "body_type": "친근하고 자연스러운 체형", + "archetype": ["친구 같은 편안함", "현실적", "오랜 사이"], + "personality": { + "summary": "편한 농담과 현실적인 조언으로 곁에 있다가 어느 순간 특별해진다.", + "traits": ["털털함", "생활력", "의리", "눈치 빠름"], + "flaws": ["로맨스를 장난으로 넘김", "자기 매력을 낮게 봄"] + }, + "romance_style": "익숙한 편안함이 질투와 고백을 통해 연애 감정으로 바뀐다.", + "visual_direction": { + "fashion": ["편한 후드", "데님 쇼츠", "심플한 유니폼"], + "adult_illustration_tone": "친근함 속에서 갑자기 느껴지는 관능적 순간" + }, + "preferred_locations": ["downtown_cafe", "apartment_complex", "riverside_park"], + "likes": ["야식", "편한 농담", "작은 도움"], + "dislikes": ["친구 취급만 하기", "허세", "돈 낭비"], + "first_encounter": { + "location": "apartment_complex", + "title": "엘리베이터의 야식 봉투", + "setup": "같은 단지에서 야식 봉투를 들고 마주치며 자연스럽게 말을 튼다.", + "success_keys": ["편한 농담", "짐 들어주기"] + }, + "date_events": [ + { + "stage": 1, + "title": "단골 카페의 수다", + "location": "downtown_cafe", + "focus": "친구 같은 호흡", + "success_keys": ["가벼운 고민 기억", "잘난 척하지 않기"] + }, + { + "stage": 2, + "title": "강변의 질투", + "location": "riverside_park", + "focus": "친구 이상의 감정 자각", + "success_keys": ["질투를 놀리지 않기", "관계의 의미 묻기"] + }, + { + "stage": 3, + "title": "집 앞 5분", + "location": "apartment_complex", + "focus": "익숙한 사이를 넘기", + "success_keys": ["장난을 멈추고 고백", "불안한 마음 인정"] + } + ], + "lover_transition": { + "title": "친구 말고 연인", + "requirements": ["date_stage_3_complete", "comfort >= 60", "commitment >= 35"], + "result": "동네 연애와 질투 해소 이벤트 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "comfortable_to_sensual", + "setup": "익숙한 집 앞 대화가 처음으로 사적인 분위기로 바뀐다.", + "required_flags": ["lovers", "consent_confirmed"], + "presentation": "warm_suggestive_cg" + } + ] + }, + { + "id": "choi_ria", + "display_name": "최리아", + "age": 21, + "occupation": "라이프스타일 인플루언서", + "height_cm": 170, + "body_type": "화려하고 늘씬한 체형", + "archetype": ["인플루언서", "관심 욕구", "공개 연애 갈등"], + "personality": { + "summary": "사람들의 시선을 즐기지만 진짜 자신을 봐주는 사람을 찾는다.", + "traits": ["화려함", "사교성", "기획력", "인정 욕구"], + "flaws": ["반응에 예민함", "관계를 콘텐츠처럼 다룸"] + }, + "romance_style": "보여지는 관계와 사적인 진심 사이의 균형을 찾는다.", + "visual_direction": { + "fashion": ["타이트한 미니드레스", "촬영용 크롭 셋업", "화려한 수영복 스타일"], + "adult_illustration_tone": "카메라를 의식한 과감한 포즈와 화려한 노출" + }, + "preferred_locations": ["boutique_studio", "fashion_street", "rooftop_lounge"], + "likes": ["사진 잘 찍기", "센스 있는 장소", "공개적인 응원"], + "dislikes": ["무관심", "악플 방치", "통제하려는 태도"], + "first_encounter": { + "location": "boutique_studio", + "title": "라이브 방송 사고", + "setup": "촬영 중 장비가 넘어질 뻔한 순간 도와주며 방송에 잠깐 비친다.", + "success_keys": ["자연스럽게 수습", "카메라 앞에서 당황하지 않기"] + }, + "date_events": [ + { + "stage": 1, + "title": "패션 거리 브이로그", + "location": "fashion_street", + "focus": "공개 이미지와 실제 반응", + "success_keys": ["사진 구도 맞추기", "피곤함 눈치채기"] + }, + { + "stage": 2, + "title": "루프탑 비공개 계정", + "location": "rooftop_lounge", + "focus": "남에게 보이지 않는 얼굴", + "success_keys": ["핸드폰 내려놓기", "사적인 대화 집중"] + }, + { + "stage": 3, + "title": "댓글보다 가까운 사람", + "location": "boutique_studio", + "focus": "인정 욕구와 진짜 관계", + "success_keys": ["악플 대응보다 감정 챙기기", "관계 공개 여부 함께 결정"] + } + ], + "lover_transition": { + "title": "카메라 밖의 고백", + "requirements": ["date_stage_3_complete", "trust >= 45", "affection >= 55"], + "result": "공개/비공개 연애 선택 루트 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "glamorous_private", + "setup": "촬영이 끝난 뒤 카메라 없는 공간에서 진심을 확인한다.", + "required_flags": ["lovers", "consent_confirmed"], + "presentation": "glamorous_suggestive_cg" + } + ] + }, + { + "id": "hong_sua", + "display_name": "홍수아", + "age": 30, + "occupation": "재즈바 운영자", + "height_cm": 165, + "body_type": "성숙하고 풍만한 체형", + "archetype": ["어른의 여유", "밤의 만남", "농염함"], + "personality": { + "summary": "상대의 허세를 금방 알아차리며, 솔직하고 책임 있는 태도에 끌린다.", + "traits": ["여유", "통찰력", "농담", "현실 감각"], + "flaws": ["쉽게 믿지 않음", "외로움을 술기운 뒤에 숨김"] + }, + "romance_style": "어른스러운 거리감과 솔직한 욕망 사이를 오간다.", + "visual_direction": { + "fashion": ["슬립 드레스", "오픈 숄더 블라우스", "바 카운터 앞 앞치마"], + "adult_illustration_tone": "낮은 조명, 풍만한 실루엣, 성숙한 관능미" + }, + "preferred_locations": ["jazz_bar", "wine_bar", "lakeside_walk"], + "likes": ["솔직한 대화", "좋은 음악", "책임감"], + "dislikes": ["어른인 척하는 허세", "술 핑계", "무책임한 유혹"], + "first_encounter": { + "location": "jazz_bar", + "title": "마감 후 마지막 손님", + "setup": "마감 직전 들어온 주인공에게 수아가 딱 한 잔만 허락한다.", + "success_keys": ["마감 시간 존중", "취한 척하지 않기"] + }, + "date_events": [ + { + "stage": 1, + "title": "바 카운터의 신청곡", + "location": "jazz_bar", + "focus": "음악 취향과 어른의 농담", + "success_keys": ["취향 설명", "선 넘지 않는 유머"] + }, + { + "stage": 2, + "title": "와인바의 휴일", + "location": "wine_bar", + "focus": "운영자가 아닌 여자의 시간", + "success_keys": ["일 이야기에서 벗어나기", "그녀도 쉬게 하기"] + }, + { + "stage": 3, + "title": "호숫가의 새벽 공기", + "location": "lakeside_walk", + "focus": "가벼운 관계를 넘기", + "success_keys": ["책임 있는 고백", "나이 차이를 장난으로 쓰지 않기"] + } + ], + "lover_transition": { + "title": "마지막 잔 다음의 관계", + "requirements": ["date_stage_3_complete", "trust >= 55", "tension >= 50"], + "result": "재즈바 비밀 연애와 성숙한 애프터 스토리 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "mature_sensual", + "setup": "마감 후 조용한 바에서 서로의 진심과 욕망을 확인한다.", + "required_flags": ["lovers", "consent_confirmed"], + "presentation": "mature_suggestive_cg_and_afterglow_dialogue" + } + ] + }, + { + "id": "lee_bom", + "display_name": "이봄", + "age": 22, + "occupation": "꽃집 아르바이트생", + "height_cm": 157, + "body_type": "말랑하고 부드러운 인상", + "archetype": ["순진함", "고집 있음", "낮 데이트"], + "personality": { + "summary": "맑고 순한 분위기지만 중요한 순간에는 뜻을 굽히지 않는다.", + "traits": ["상냥함", "고집", "낙천성", "눈물이 많음"], + "flaws": ["현실 감각 부족", "거절을 어려워함"] + }, + "romance_style": "밝은 낮의 데이트에서 시작해 자기 의견을 배우는 성장형 루트다.", + "visual_direction": { + "fashion": ["플로럴 원피스", "얇은 카디건", "꽃집 앞치마"], + "adult_illustration_tone": "부드러운 색감과 자연스러운 노출" + }, + "preferred_locations": ["flower_shop", "riverside_park", "downtown_cafe"], + "likes": ["꽃 선물", "낮 산책", "다정한 말"], + "dislikes": ["거짓말", "강한 향수", "무례한 손님"], + "first_encounter": { + "location": "flower_shop", + "title": "잘못 묶은 꽃다발", + "setup": "봄이 만든 꽃다발이 주문과 달라 당황하고, 주인공이 상황을 도와준다.", + "success_keys": ["손님과 중재", "봄의 잘못만 탓하지 않기"] + }, + "date_events": [ + { + "stage": 1, + "title": "카페의 꽃 한 송이", + "location": "downtown_cafe", + "focus": "맑은 호감", + "success_keys": ["꽃말 맞히기", "부담 없는 칭찬"] + }, + { + "stage": 2, + "title": "강변 피크닉", + "location": "riverside_park", + "focus": "자기 의견 말하기", + "success_keys": ["선택권 주기", "싫다는 말을 존중"] + }, + { + "stage": 3, + "title": "닫힌 꽃집의 용기", + "location": "flower_shop", + "focus": "순진함에서 주체성으로", + "success_keys": ["봄의 결정 지지", "분명한 고백"] + } + ], + "lover_transition": { + "title": "계절이 바뀌는 꽃집", + "requirements": ["date_stage_3_complete", "affection >= 55", "trust >= 45"], + "result": "꽃집 운영 보조와 낮 데이트 애프터 스토리 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "soft_daylight", + "setup": "닫힌 꽃집에서 서로의 마음과 속도를 확인한다.", + "required_flags": ["lovers", "consent_confirmed"], + "presentation": "soft_suggestive_cg" + } + ] + }, + { + "id": "go_eunjae", + "display_name": "고은재", + "age": 27, + "occupation": "계약 전문 변호사", + "height_cm": 172, + "body_type": "시크하고 긴 실루엣", + "archetype": ["냉소적", "현실주의", "계약 같은 관계"], + "personality": { + "summary": "연애도 조건과 책임을 따지는 현실주의자지만, 진심을 만나면 기준이 흔들린다.", + "traits": ["논리적", "냉정함", "정확함", "책임감"], + "flaws": ["감정을 비용처럼 계산", "상처를 냉소로 숨김"] + }, + "romance_style": "조건을 따지는 관계에서 예외가 되어가는 루트다.", + "visual_direction": { + "fashion": ["슬림 수트", "셔츠 원피스", "검은 하이힐"], + "adult_illustration_tone": "차분한 권위감과 날카로운 관능미" + }, + "preferred_locations": ["office_district", "wine_bar", "book_cafe"], + "likes": ["명확한 약속", "논리적인 대화", "책임 있는 선택"], + "dislikes": ["감정적 협박", "불명확한 관계", "시간 낭비"], + "first_encounter": { + "location": "office_district", + "title": "분실된 계약서", + "setup": "잘못 전달된 서류를 돌려주며 은재의 차가운 검증이 시작된다.", + "success_keys": ["서류를 보지 않기", "정확히 돌려주기"] + }, + "date_events": [ + { + "stage": 1, + "title": "북카페의 조건", + "location": "book_cafe", + "focus": "연애관 토론", + "success_keys": ["말꼬리 잡지 않기", "자기 기준 설명"] + }, + { + "stage": 2, + "title": "와인바의 예외 조항", + "location": "wine_bar", + "focus": "논리로 설명되지 않는 끌림", + "success_keys": ["감정을 회피하지 않기", "은재의 기준 존중"] + }, + { + "stage": 3, + "title": "오피스 거리의 비", + "location": "office_district", + "focus": "계산 밖의 선택", + "success_keys": ["손해를 감수한 도움", "관계를 명확히 하기"] + } + ], + "lover_transition": { + "title": "서명 없는 약속", + "requirements": ["date_stage_3_complete", "trust >= 55", "commitment >= 50"], + "result": "현실 갈등과 책임형 연인 루트 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "controlled_tension", + "setup": "명확한 동의와 관계 확인 후 차가운 긴장이 풀린다.", + "required_flags": ["lovers", "consent_confirmed"], + "presentation": "minimalist_suggestive_cg" + } + ] + }, + { + "id": "ryu_haena", + "display_name": "류해나", + "age": 25, + "occupation": "네일 아티스트", + "height_cm": 160, + "body_type": "글래머러스하고 작은 체형", + "archetype": ["애교 많음", "질투심", "달콤하지만 피곤한 연애"], + "personality": { + "summary": "사랑받는 느낌을 좋아하고 애정 표현이 풍부하지만 불안하면 질투가 커진다.", + "traits": ["애교", "감각적", "섬세함", "표현력"], + "flaws": ["확인 욕구", "감정 기복"] + }, + "romance_style": "달콤한 애정 표현과 질투 조율이 핵심이다.", + "visual_direction": { + "fashion": ["타이트한 니트", "미니스커트", "화려한 네일 스타일"], + "adult_illustration_tone": "달콤하고 과감한 포즈, 가까운 거리감" + }, + "preferred_locations": ["fashion_street", "downtown_cafe", "rooftop_lounge"], + "likes": ["자주 하는 칭찬", "커플 아이템", "연락"], + "dislikes": ["다른 여자 언급", "느린 답장", "무뚝뚝함"], + "first_encounter": { + "location": "fashion_street", + "title": "번진 매니큐어", + "setup": "가게 앞에서 네일 샘플을 망친 해나를 도와주며 대화가 시작된다.", + "success_keys": ["손재주 칭찬", "장난스럽게 분위기 풀기"] + }, + "date_events": [ + { + "stage": 1, + "title": "카페의 커플 컬러", + "location": "downtown_cafe", + "focus": "애정 표현 취향", + "success_keys": ["커플 색 받아들이기", "부끄러워도 표현"] + }, + { + "stage": 2, + "title": "패션 거리의 질투", + "location": "fashion_street", + "focus": "불안과 소유욕", + "success_keys": ["무시하지 않기", "관계를 안심시켜주기"] + }, + { + "stage": 3, + "title": "루프탑의 진짜 부탁", + "location": "rooftop_lounge", + "focus": "확인보다 신뢰로", + "success_keys": ["경계 정하기", "애정 표현 유지"] + } + ], + "lover_transition": { + "title": "확인하지 않아도 되는 사랑", + "requirements": ["date_stage_3_complete", "affection >= 60", "trust >= 45"], + "result": "질투 조율과 커플 이벤트 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "sweet_possessive", + "setup": "질투를 풀고 서로의 애정 표현 방식을 맞춰가는 밤", + "required_flags": ["lovers", "consent_confirmed"], + "presentation": "sweet_suggestive_cg" + } + ] + }, + { + "id": "nam_juhee", + "display_name": "남주희", + "age": 28, + "occupation": "경찰관", + "height_cm": 176, + "body_type": "장신의 탄탄한 체형", + "archetype": ["책임감", "보호자 타입", "규칙과 욕망"], + "personality": { + "summary": "규칙과 책임을 중시하지만 사적인 감정을 통제하는 데 서툴다.", + "traits": ["정의감", "침착함", "체력", "보호 본능"], + "flaws": ["혼자 떠안음", "감정 표현 억제"] + }, + "romance_style": "사건과 도움 속에서 신뢰를 쌓고, 규칙 너머의 마음을 인정한다.", + "visual_direction": { + "fashion": ["경찰 제복", "민소매 트레이닝복", "심플한 셔츠"], + "adult_illustration_tone": "강인한 체형과 절제된 긴장감" + }, + "preferred_locations": ["police_box", "fitness_center", "riverside_park"], + "likes": ["책임 있는 행동", "안전 의식", "꾸준한 운동"], + "dislikes": ["무모함", "장난 신고", "약자 괴롭힘"], + "first_encounter": { + "location": "police_box", + "title": "잃어버린 지갑", + "setup": "지갑 분실 신고를 하며 주희의 냉정하지만 친절한 응대를 받는다.", + "success_keys": ["절차 존중", "고맙다는 말 잊지 않기"] + }, + "date_events": [ + { + "stage": 1, + "title": "강변 순찰길", + "location": "riverside_park", + "focus": "일과 사생활의 경계", + "success_keys": ["근무 방해하지 않기", "안전하게 동행"] + }, + { + "stage": 2, + "title": "피트니스 대련", + "location": "fitness_center", + "focus": "경쟁과 신뢰", + "success_keys": ["정면 승부", "무리한 접촉 피하기"] + }, + { + "stage": 3, + "title": "파출소 앞 우산", + "location": "police_box", + "focus": "규칙보다 중요한 마음", + "success_keys": ["책임을 존중", "사적인 감정 인정"] + } + ], + "lover_transition": { + "title": "근무 끝의 고백", + "requirements": ["date_stage_3_complete", "trust >= 60", "commitment >= 45"], + "result": "보호 갈등과 야간 순찰 애프터 스토리 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "restrained_passion", + "setup": "근무가 끝난 뒤 책임과 사적인 마음을 분리하고 가까워진다.", + "required_flags": ["lovers", "consent_confirmed"], + "presentation": "restrained_suggestive_cg" + } + ] + }, + { + "id": "marina", + "display_name": "마리나", + "age": 24, + "occupation": "외국계 회사 통역 코디네이터", + "height_cm": 169, + "body_type": "이국적이고 균형 잡힌 체형", + "archetype": ["자유분방", "문화 차이", "여행"], + "personality": { + "summary": "새로운 도시와 사람을 좋아하지만 정착과 관계의 무게는 조심스러워한다.", + "traits": ["개방적", "호기심", "독립적", "표현이 직접적"], + "flaws": ["갑자기 떠날 수 있음", "갈등을 가볍게 넘김"] + }, + "romance_style": "언어와 문화 차이를 넘어 서로의 생활 방식에 스며든다.", + "visual_direction": { + "fashion": ["홀터넥 드레스", "여행자 셔츠", "화려한 스카프"], + "adult_illustration_tone": "자유롭고 이국적인 색감, 과감한 여름 스타일" + }, + "preferred_locations": ["global_lounge", "riverside_park", "wine_bar"], + "likes": ["여행 이야기", "새 음식", "솔직한 감정 표현"], + "dislikes": ["편견", "문화 농담", "소유하려는 태도"], + "first_encounter": { + "location": "global_lounge", + "title": "통역이 필요한 메뉴판", + "setup": "메뉴판 번역을 도와주며 마리나가 장난스러운 감사를 건넨다.", + "success_keys": ["천천히 말하기", "문화 차이를 궁금해하기"] + }, + "date_events": [ + { + "stage": 1, + "title": "강변의 언어 교환", + "location": "riverside_park", + "focus": "서로의 표현 방식 배우기", + "success_keys": ["틀려도 시도", "웃음으로 넘기기"] + }, + { + "stage": 2, + "title": "와인바의 여행 지도", + "location": "wine_bar", + "focus": "자유와 관계의 균형", + "success_keys": ["떠남을 비난하지 않기", "같이 갈 미래 상상"] + }, + { + "stage": 3, + "title": "글로벌 라운지의 마지막 일정", + "location": "global_lounge", + "focus": "정착할 이유", + "success_keys": ["선택을 존중", "머물 이유가 되어주기"] + } + ], + "lover_transition": { + "title": "돌아올 곳", + "requirements": ["date_stage_3_complete", "trust >= 50", "commitment >= 40"], + "result": "여행 이벤트와 장거리 갈등 루트 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "free_spirited", + "setup": "여행을 앞둔 밤, 서로의 선택과 마음을 확인한다.", + "required_flags": ["lovers", "consent_confirmed"], + "presentation": "warm_travel_suggestive_cg" + } + ] + }, + { + "id": "song_arin", + "display_name": "송아린", + "age": 26, + "occupation": "인테리어 소품샵 운영자", + "height_cm": 156, + "body_type": "작지만 성숙한 분위기의 체형", + "archetype": ["이혼 경험", "현실적인 연애관", "다시 믿는 사랑"], + "personality": { + "summary": "관계의 실패를 겪은 뒤 쉽게 기대하지 않지만, 따뜻한 일상에 약하다.", + "traits": ["현실적", "부드러움", "신중함", "생활 감각"], + "flaws": ["방어적", "미래 약속을 두려워함"] + }, + "romance_style": "상처를 들추지 않고 현실적인 신뢰를 쌓는 루트다.", + "visual_direction": { + "fashion": ["니트 원피스", "린넨 셔츠", "차분한 홈웨어"], + "adult_illustration_tone": "작고 성숙한 실루엣, 차분한 실내 관능미" + }, + "preferred_locations": ["apartment_complex", "book_cafe", "lakeside_walk"], + "likes": ["실용적인 선물", "정리정돈", "차분한 약속"], + "dislikes": ["가벼운 결혼 농담", "과거 추궁", "허황된 말"], + "first_encounter": { + "location": "apartment_complex", + "title": "깨진 머그컵", + "setup": "이사 중 깨진 소품을 함께 치우며 현실적인 대화가 시작된다.", + "success_keys": ["조용히 돕기", "과거를 묻지 않기"] + }, + "date_events": [ + { + "stage": 1, + "title": "북카페의 새 컵", + "location": "book_cafe", + "focus": "취향과 생활감", + "success_keys": ["실용적인 선물", "과한 의미 부여 피하기"] + }, + { + "stage": 2, + "title": "호숫가의 현실적인 질문", + "location": "lakeside_walk", + "focus": "재연애에 대한 두려움", + "success_keys": ["장밋빛 약속 남발 금지", "지금 할 수 있는 행동 제시"] + }, + { + "stage": 3, + "title": "집 앞의 새 가구", + "location": "apartment_complex", + "focus": "다시 믿는 일", + "success_keys": ["함께 고르기", "도망가지 않겠다고 말하기"] + } + ], + "lover_transition": { + "title": "다시 놓인 두 개의 컵", + "requirements": ["date_stage_3_complete", "trust >= 60", "commitment >= 50"], + "result": "현실형 동거 준비 애프터 스토리 해금" + }, + "adult_event_hooks": [ + { + "stage": "after_lovers", + "tone": "quiet_second_chance", + "setup": "서로를 다시 믿어보기로 한 뒤 조용히 가까워진다.", + "required_flags": ["lovers", "consent_confirmed", "trust >= 65"], + "presentation": "quiet_suggestive_cg" + } + ] + } + ] +} diff --git a/data/locations.json b/data/locations.json new file mode 100644 index 0000000..eaac24d --- /dev/null +++ b/data/locations.json @@ -0,0 +1,159 @@ +{ + "version": "0.1.0", + "locations": [ + { + "id": "downtown_cafe", + "name": "다운타운 카페", + "mood": "밝고 일상적인 첫 만남", + "time_slots": ["morning", "afternoon", "evening"], + "encounter_tags": ["casual", "conversation", "rainy_day"] + }, + { + "id": "book_cafe", + "name": "북카페", + "mood": "조용한 대화와 취향 공유", + "time_slots": ["afternoon", "evening"], + "encounter_tags": ["quiet", "literary", "slow_burn"] + }, + { + "id": "university_quarter", + "name": "대학가", + "mood": "활기찬 우연과 풋풋한 설렘", + "time_slots": ["morning", "afternoon", "evening"], + "encounter_tags": ["student", "festival", "street_food"] + }, + { + "id": "riverside_park", + "name": "강변 공원", + "mood": "산책, 고백, 밤공기", + "time_slots": ["afternoon", "evening", "night"], + "encounter_tags": ["walk", "confession", "comfort"] + }, + { + "id": "cooking_studio", + "name": "쿠킹 스튜디오", + "mood": "생활감 있는 다정함", + "time_slots": ["afternoon", "evening"], + "encounter_tags": ["homey", "class", "care"] + }, + { + "id": "neighborhood_market", + "name": "동네 시장", + "mood": "현실적이고 따뜻한 일상", + "time_slots": ["morning", "afternoon"], + "encounter_tags": ["errand", "food", "local"] + }, + { + "id": "office_district", + "name": "오피스 거리", + "mood": "바쁜 하루 속 짧은 마주침", + "time_slots": ["morning", "evening", "night"], + "encounter_tags": ["career", "stress", "after_work"] + }, + { + "id": "rooftop_lounge", + "name": "루프탑 라운지", + "mood": "화려하고 긴장감 있는 야경", + "time_slots": ["evening", "night"], + "encounter_tags": ["night_view", "flirt", "tension"] + }, + { + "id": "wine_bar", + "name": "와인바", + "mood": "성숙한 대화와 은근한 거리감", + "time_slots": ["evening", "night"], + "encounter_tags": ["mature", "private", "slow_tension"] + }, + { + "id": "jazz_bar", + "name": "재즈바", + "mood": "어른스러운 여유와 낮은 조명", + "time_slots": ["evening", "night"], + "encounter_tags": ["music", "mature", "secret"] + }, + { + "id": "amusement_street", + "name": "놀이 거리", + "mood": "즉흥적이고 발랄한 데이트", + "time_slots": ["afternoon", "evening", "night"], + "encounter_tags": ["playful", "festival", "arcade"] + }, + { + "id": "fashion_street", + "name": "패션 거리", + "mood": "눈에 띄는 스타일과 시선", + "time_slots": ["afternoon", "evening"], + "encounter_tags": ["shopping", "style", "jealousy"] + }, + { + "id": "boutique_studio", + "name": "부티크 스튜디오", + "mood": "촬영, 의상, 화려한 이미지", + "time_slots": ["afternoon", "evening"], + "encounter_tags": ["photo", "fashion", "public_image"] + }, + { + "id": "fitness_center", + "name": "피트니스 센터", + "mood": "활동적이고 건강한 긴장감", + "time_slots": ["morning", "evening"], + "encounter_tags": ["training", "competition", "body_care"] + }, + { + "id": "art_studio", + "name": "작업실 골목", + "mood": "감정적이고 예술적인 밤", + "time_slots": ["afternoon", "evening", "night"], + "encounter_tags": ["art", "melancholy", "late_night"] + }, + { + "id": "old_bookstore", + "name": "헌책방", + "mood": "느린 시간과 숨은 취향", + "time_slots": ["afternoon", "evening"], + "encounter_tags": ["nostalgia", "quiet", "discovery"] + }, + { + "id": "flower_shop", + "name": "꽃집", + "mood": "부드럽고 맑은 낮의 호감", + "time_slots": ["morning", "afternoon"], + "encounter_tags": ["gentle", "gift", "daylight"] + }, + { + "id": "cinema", + "name": "영화관", + "mood": "어두운 객석과 가까운 거리", + "time_slots": ["afternoon", "evening", "night"], + "encounter_tags": ["date", "shared_emotion", "tension"] + }, + { + "id": "police_box", + "name": "파출소 앞", + "mood": "책임감과 보호 본능", + "time_slots": ["morning", "evening", "night"], + "encounter_tags": ["duty", "safety", "incident"] + }, + { + "id": "global_lounge", + "name": "글로벌 라운지", + "mood": "언어, 문화, 여행의 설렘", + "time_slots": ["afternoon", "evening", "night"], + "encounter_tags": ["foreign", "travel", "open_minded"] + }, + { + "id": "apartment_complex", + "name": "아파트 단지", + "mood": "사적인 일상과 조심스러운 거리", + "time_slots": ["morning", "evening", "night"], + "encounter_tags": ["neighbor", "private", "comfort"] + }, + { + "id": "lakeside_walk", + "name": "호숫가 산책로", + "mood": "회복, 고백, 조용한 위로", + "time_slots": ["afternoon", "evening", "night"], + "encounter_tags": ["healing", "confession", "mature"] + } + ] +} diff --git a/data/progression.json b/data/progression.json new file mode 100644 index 0000000..e64724f --- /dev/null +++ b/data/progression.json @@ -0,0 +1,79 @@ +{ + "version": "0.1.0", + "player": { + "age": 25, + "role": "male_protagonist" + }, + "relationship_stages": [ + { + "stage": 0, + "id": "stranger", + "name": "모르는 사이", + "requirements": [] + }, + { + "stage": 1, + "id": "acquaintance", + "name": "안면 있는 사이", + "requirements": ["first_encounter_complete"] + }, + { + "stage": 2, + "id": "contact", + "name": "연락 가능한 사이", + "requirements": ["contact_exchanged", "affection >= 15"] + }, + { + "stage": 3, + "id": "dating", + "name": "데이트 단계", + "requirements": ["trust >= 20", "date_invite_accepted"] + }, + { + "stage": 4, + "id": "intimate_trust", + "name": "깊은 신뢰", + "requirements": ["date_stage_2_complete", "trust >= 45", "tension >= 25"] + }, + { + "stage": 5, + "id": "lovers", + "name": "연인", + "requirements": ["date_stage_3_complete", "commitment >= 50", "confession_success"] + }, + { + "stage": 6, + "id": "after_story", + "name": "애프터 스토리", + "requirements": ["lovers", "route_clear"] + } + ], + "adult_event_requirements": { + "minimum_character_age": 19, + "required_flags": ["lovers", "consent_confirmed"], + "recommended_stats": ["trust >= 55", "commitment >= 45"], + "presentation_modes": ["suggestive_cg", "fade_to_black", "afterglow_dialogue"] + }, + "date_result_rules": { + "great": { + "affection": 12, + "trust": 8, + "tension": 6 + }, + "good": { + "affection": 8, + "trust": 5, + "tension": 3 + }, + "awkward": { + "affection": 2, + "trust": -2, + "tension": 1 + }, + "failed": { + "affection": -5, + "trust": -8, + "tension": -3 + } + } +} diff --git a/docs/game_structure.md b/docs/game_structure.md new file mode 100644 index 0000000..23a1a02 --- /dev/null +++ b/docs/game_structure.md @@ -0,0 +1,53 @@ +# 게임 구조 초안 + +## 핵심 루프 + +1. 플레이어는 도시 맵에서 장소를 선택한다. +2. 장소, 시간대, 요일, 캐릭터 상태에 따라 우연한 만남이 발생한다. +3. 첫 만남 이후 연락처, 약속, 선물, 대화 선택지를 통해 호감도와 신뢰도를 쌓는다. +4. 각 캐릭터별 데이트 이벤트 3단계를 완료하면 고백 또는 관계 전환 이벤트가 열린다. +5. 연인 전환 후 성인 로맨스 이벤트, 갈등 이벤트, 질투 이벤트, 동거/장기 관계 이벤트로 확장된다. + +## 주요 수치 + +- `affection`: 호감도. 데이트 성공, 선호 선물, 좋은 대화 선택지로 상승한다. +- `trust`: 신뢰도. 약속 지키기, 캐릭터의 고민 해결, 진심 어린 선택지로 상승한다. +- `tension`: 설렘과 긴장감. 플러팅, 밀당, 야간 데이트, 질투 상황에서 상승한다. +- `comfort`: 편안함. 일상 데이트, 대화, 반복 만남에서 상승한다. +- `commitment`: 관계 확신. 연인 전환과 장기 루트 진입에 필요하다. + +## 관계 단계 + +| 단계 | 이름 | 설명 | +| --- | --- | --- | +| 0 | Stranger | 아직 모르는 사이 | +| 1 | Acquaintance | 첫 만남 이후 인사를 나누는 사이 | +| 2 | Contact | 연락처를 교환하고 약속을 잡을 수 있는 사이 | +| 3 | Dating | 정식 데이트 이벤트가 열리는 사이 | +| 4 | Intimate Trust | 깊은 신뢰와 관능적 긴장감이 쌓인 사이 | +| 5 | Lovers | 연인 관계 | +| 6 | After Story | 연인 이후 장기 루트 | + +## 성인 이벤트 설계 원칙 + +성인 로맨스 이벤트는 캐릭터별 매력과 관계의 진행감을 강화하는 보상 구조로 사용한다. 모든 이벤트는 성인 캐릭터 간의 상호 동의와 관계 단계 조건을 요구한다. + +- 이벤트 진입 전 `consent_confirmed` 플래그가 필요하다. +- 강압, 취약 상태 악용, 나이 불명확 캐릭터는 금지한다. +- 장면 데이터는 `setup`, `tone`, `required_flags`, `presentation`으로 관리한다. +- 기본 연출은 관능적 CG, 암전, 후일담 대화, 관계 수치 변화로 처리한다. + +## 맵 설계 방향 + +장소는 단순 이동 메뉴가 아니라 캐릭터 성향과 루트가 드러나는 접점이다. + +- 낮 시간대: 카페, 대학가, 공원, 꽃집, 서점 중심의 밝은 만남 +- 저녁 시간대: 와인바, 루프탑, 재즈바, 영화관 중심의 긴장감 있는 데이트 +- 특수 장소: 피트니스 센터, 작업실, 경찰서 앞, 글로벌 라운지 등 캐릭터별 전용 이벤트 + +## 다음 구현 후보 + +- JSON 데이터를 읽는 간단한 프로토타입 UI +- 장소 클릭 후 만남 후보를 표시하는 맵 화면 +- 캐릭터 상세 화면과 호감도/신뢰도 상태 표시 +- 날짜, 시간대, 요일 기반 이벤트 스케줄러 diff --git a/scripts/validate_content.py b/scripts/validate_content.py new file mode 100644 index 0000000..72a6d22 --- /dev/null +++ b/scripts/validate_content.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Validate core romance data files.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +DATA = ROOT / "data" + + +def load_json(path: Path) -> dict: + with path.open("r", encoding="utf-8") as handle: + return json.load(handle) + + +def fail(message: str) -> None: + print(f"ERROR: {message}", file=sys.stderr) + raise SystemExit(1) + + +def main() -> None: + characters_doc = load_json(DATA / "characters.json") + locations_doc = load_json(DATA / "locations.json") + progression_doc = load_json(DATA / "progression.json") + + minimum_age = characters_doc["content_rules"]["minimum_age"] + locations = {location["id"] for location in locations_doc["locations"]} + character_ids: set[str] = set() + + for character in characters_doc["characters"]: + character_id = character["id"] + if character_id in character_ids: + fail(f"duplicate character id: {character_id}") + character_ids.add(character_id) + + if character["age"] < minimum_age: + fail( + f"{character['display_name']} is {character['age']}; " + f"minimum age is {minimum_age}" + ) + + for location_id in character["preferred_locations"]: + if location_id not in locations: + fail(f"{character_id} references missing preferred location {location_id}") + + first_location = character["first_encounter"]["location"] + if first_location not in locations: + fail(f"{character_id} references missing first encounter location {first_location}") + + event_stages = [] + for event in character["date_events"]: + event_stages.append(event["stage"]) + event_location = event["location"] + if event_location not in locations: + fail(f"{character_id} references missing date location {event_location}") + + if event_stages != [1, 2, 3]: + fail(f"{character_id} date event stages must be [1, 2, 3]") + + for hook in character["adult_event_hooks"]: + flags = set(hook["required_flags"]) + if "consent_confirmed" not in flags: + fail(f"{character_id} adult event hook lacks consent_confirmed") + if "lovers" not in flags: + fail(f"{character_id} adult event hook lacks lovers flag") + + adult_requirements = progression_doc["adult_event_requirements"] + if adult_requirements["minimum_character_age"] != minimum_age: + fail("progression minimum age does not match character content rules") + + print( + "Validated " + f"{len(character_ids)} characters, " + f"{len(locations)} locations, " + f"{len(progression_doc['relationship_stages'])} relationship stages." + ) + + +if __name__ == "__main__": + main()