Add initial romance game design data
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.log
|
||||
*.tmp
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
.venv/
|
||||
venv/
|
||||
dist/
|
||||
build/
|
||||
25
README.md
Normal file
25
README.md
Normal 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
1440
data/characters.json
Normal file
File diff suppressed because it is too large
Load Diff
159
data/locations.json
Normal file
159
data/locations.json
Normal 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
79
data/progression.json
Normal 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
53
docs/game_structure.md
Normal 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
|
||||
- 장소 클릭 후 만남 후보를 표시하는 맵 화면
|
||||
- 캐릭터 상세 화면과 호감도/신뢰도 상태 표시
|
||||
- 날짜, 시간대, 요일 기반 이벤트 스케줄러
|
||||
84
scripts/validate_content.py
Normal file
84
scripts/validate_content.py
Normal 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()
|
||||
Reference in New Issue
Block a user