Add initial romance game design data

This commit is contained in:
2026-06-11 22:34:07 +09:00
commit d6cfaaeabb
7 changed files with 1850 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
.DS_Store
Thumbs.db
*.log
*.tmp
__pycache__/
.pytest_cache/
.venv/
venv/
dist/
build/

25
README.md Normal file
View File

@@ -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
```

1440
data/characters.json Normal file

File diff suppressed because it is too large Load Diff

159
data/locations.json Normal file
View File

@@ -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"]
}
]
}

79
data/progression.json Normal file
View File

@@ -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
}
}
}

53
docs/game_structure.md Normal file
View File

@@ -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
- 장소 클릭 후 만남 후보를 표시하는 맵 화면
- 캐릭터 상세 화면과 호감도/신뢰도 상태 표시
- 날짜, 시간대, 요일 기반 이벤트 스케줄러

View File

@@ -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()