diff --git a/README.md b/README.md index ec7dc80..46f2bd0 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ - `data/character_location_presence.json`: 캐릭터별 출현 가능 장소, 시간대, 등장 가중치 - `data/progression.json`: 만남에서 연인 전환까지의 공통 진행 규칙 - `data/character_assets.json`: 캐릭터 기본 이미지의 통과 상태와 파일 경로 +- `data/location_assets.json`: 장소별 배경 CG의 통과 상태와 파일 경로 - `docs/game_structure.md`: 게임 루프와 시스템 설계 메모 - `docs/art/facial_identity_matrix.md`: 캐릭터별 얼굴형, 눈매, 헤어, 표정 차별화 기준 - `scripts/validate_content.py`: 캐릭터 나이와 장소 참조 무결성 검증 @@ -26,6 +27,7 @@ - 박민서 피트니스 데이트 101+ CG는 `assets/characters/date_cg/park_minseo_fitness_affection_100plus_v2.png`를 사용합니다. - 신규 후보 10장과 비교용 contact sheet는 `assets/characters/date_cg/candidates/park_minseo_100plus/`에 보관합니다. +- 장소 배경 CG 22장은 `assets/locations/`에 보관하며, 비교용 contact sheet는 `assets/locations/location_backgrounds_contact_sheet.png`입니다. ## 검증 diff --git a/assets/locations/amusement_street_bg_v1.png b/assets/locations/amusement_street_bg_v1.png new file mode 100644 index 0000000..d4b94a9 Binary files /dev/null and b/assets/locations/amusement_street_bg_v1.png differ diff --git a/assets/locations/apartment_complex_bg_v1.png b/assets/locations/apartment_complex_bg_v1.png new file mode 100644 index 0000000..27ee7c4 Binary files /dev/null and b/assets/locations/apartment_complex_bg_v1.png differ diff --git a/assets/locations/art_studio_bg_v1.png b/assets/locations/art_studio_bg_v1.png new file mode 100644 index 0000000..31baf75 Binary files /dev/null and b/assets/locations/art_studio_bg_v1.png differ diff --git a/assets/locations/book_cafe_bg_v1.png b/assets/locations/book_cafe_bg_v1.png new file mode 100644 index 0000000..492982d Binary files /dev/null and b/assets/locations/book_cafe_bg_v1.png differ diff --git a/assets/locations/boutique_studio_bg_v1.png b/assets/locations/boutique_studio_bg_v1.png new file mode 100644 index 0000000..01123ac Binary files /dev/null and b/assets/locations/boutique_studio_bg_v1.png differ diff --git a/assets/locations/cinema_bg_v1.png b/assets/locations/cinema_bg_v1.png new file mode 100644 index 0000000..de9af1a Binary files /dev/null and b/assets/locations/cinema_bg_v1.png differ diff --git a/assets/locations/cooking_studio_bg_v1.png b/assets/locations/cooking_studio_bg_v1.png new file mode 100644 index 0000000..0eefa89 Binary files /dev/null and b/assets/locations/cooking_studio_bg_v1.png differ diff --git a/assets/locations/downtown_cafe_bg_v1.png b/assets/locations/downtown_cafe_bg_v1.png new file mode 100644 index 0000000..7ea9193 Binary files /dev/null and b/assets/locations/downtown_cafe_bg_v1.png differ diff --git a/assets/locations/fashion_street_bg_v1.png b/assets/locations/fashion_street_bg_v1.png new file mode 100644 index 0000000..34be74a Binary files /dev/null and b/assets/locations/fashion_street_bg_v1.png differ diff --git a/assets/locations/fitness_center_bg_v1.png b/assets/locations/fitness_center_bg_v1.png new file mode 100644 index 0000000..c2961d0 Binary files /dev/null and b/assets/locations/fitness_center_bg_v1.png differ diff --git a/assets/locations/flower_shop_bg_v1.png b/assets/locations/flower_shop_bg_v1.png new file mode 100644 index 0000000..5242fc7 Binary files /dev/null and b/assets/locations/flower_shop_bg_v1.png differ diff --git a/assets/locations/global_lounge_bg_v1.png b/assets/locations/global_lounge_bg_v1.png new file mode 100644 index 0000000..2173cb8 Binary files /dev/null and b/assets/locations/global_lounge_bg_v1.png differ diff --git a/assets/locations/jazz_bar_bg_v1.png b/assets/locations/jazz_bar_bg_v1.png new file mode 100644 index 0000000..2d08967 Binary files /dev/null and b/assets/locations/jazz_bar_bg_v1.png differ diff --git a/assets/locations/lakeside_walk_bg_v1.png b/assets/locations/lakeside_walk_bg_v1.png new file mode 100644 index 0000000..88fddf9 Binary files /dev/null and b/assets/locations/lakeside_walk_bg_v1.png differ diff --git a/assets/locations/location_backgrounds_contact_sheet.png b/assets/locations/location_backgrounds_contact_sheet.png new file mode 100644 index 0000000..9687155 Binary files /dev/null and b/assets/locations/location_backgrounds_contact_sheet.png differ diff --git a/assets/locations/neighborhood_market_bg_v1.png b/assets/locations/neighborhood_market_bg_v1.png new file mode 100644 index 0000000..e8d1cb9 Binary files /dev/null and b/assets/locations/neighborhood_market_bg_v1.png differ diff --git a/assets/locations/office_district_bg_v1.png b/assets/locations/office_district_bg_v1.png new file mode 100644 index 0000000..43dfb5d Binary files /dev/null and b/assets/locations/office_district_bg_v1.png differ diff --git a/assets/locations/old_bookstore_bg_v1.png b/assets/locations/old_bookstore_bg_v1.png new file mode 100644 index 0000000..8d185d6 Binary files /dev/null and b/assets/locations/old_bookstore_bg_v1.png differ diff --git a/assets/locations/police_box_bg_v1.png b/assets/locations/police_box_bg_v1.png new file mode 100644 index 0000000..50a2555 Binary files /dev/null and b/assets/locations/police_box_bg_v1.png differ diff --git a/assets/locations/riverside_park_bg_v1.png b/assets/locations/riverside_park_bg_v1.png new file mode 100644 index 0000000..4518046 Binary files /dev/null and b/assets/locations/riverside_park_bg_v1.png differ diff --git a/assets/locations/rooftop_lounge_bg_v1.png b/assets/locations/rooftop_lounge_bg_v1.png new file mode 100644 index 0000000..6db60d3 Binary files /dev/null and b/assets/locations/rooftop_lounge_bg_v1.png differ diff --git a/assets/locations/university_quarter_bg_v1.png b/assets/locations/university_quarter_bg_v1.png new file mode 100644 index 0000000..495438f Binary files /dev/null and b/assets/locations/university_quarter_bg_v1.png differ diff --git a/assets/locations/wine_bar_bg_v1.png b/assets/locations/wine_bar_bg_v1.png new file mode 100644 index 0000000..0f09faa Binary files /dev/null and b/assets/locations/wine_bar_bg_v1.png differ diff --git a/data/location_assets.json b/data/location_assets.json new file mode 100644 index 0000000..c4a3031 --- /dev/null +++ b/data/location_assets.json @@ -0,0 +1,146 @@ +{ + "version": "0.1.0", + "style": { + "id": "sensual_romance_backgrounds", + "description": "Glossy adult romance visual-novel location backgrounds with mature lighting, saturated reflections, and no characters." + }, + "minimum_dimensions": { + "width": 1280, + "height": 720 + }, + "contact_sheet": "assets/locations/location_backgrounds_contact_sheet.png", + "assets": [ + { + "location_id": "downtown_cafe", + "status": "accepted", + "path": "assets/locations/downtown_cafe_bg_v1.png", + "notes": "Rainy downtown cafe interior with amber light and intimate two-person seating." + }, + { + "location_id": "book_cafe", + "status": "accepted", + "path": "assets/locations/book_cafe_bg_v1.png", + "notes": "Warm private reading lounge with velvet seating and late-afternoon light." + }, + { + "location_id": "university_quarter", + "status": "accepted", + "path": "assets/locations/university_quarter_bg_v1.png", + "notes": "Adult campus-adjacent nightlife street with neon, food stalls, and wet pavement." + }, + { + "location_id": "riverside_park", + "status": "accepted", + "path": "assets/locations/riverside_park_bg_v1.png", + "notes": "Blue-hour riverside path with skyline reflections and confession-route calm." + }, + { + "location_id": "cooking_studio", + "status": "accepted", + "path": "assets/locations/cooking_studio_bg_v1.png", + "notes": "Upscale cooking studio after class with warm counters, produce, and oven glow." + }, + { + "location_id": "neighborhood_market", + "status": "accepted", + "path": "assets/locations/neighborhood_market_bg_v1.png", + "notes": "Early-morning market lane with saturated produce, lamps, ice cases, and puddles." + }, + { + "location_id": "office_district", + "status": "accepted", + "path": "assets/locations/office_district_bg_v1.png", + "notes": "Rainy glass-tower office district with after-work tension and lobby glow." + }, + { + "location_id": "rooftop_lounge", + "status": "accepted", + "path": "assets/locations/rooftop_lounge_bg_v1.png", + "notes": "Luxury rooftop lounge with velvet seating, city lights, and red-gold highlights." + }, + { + "location_id": "wine_bar", + "status": "accepted", + "path": "assets/locations/wine_bar_bg_v1.png", + "notes": "Private wine bar with burgundy leather, bottle walls, and glass sparkle." + }, + { + "location_id": "jazz_bar", + "status": "accepted", + "path": "assets/locations/jazz_bar_bg_v1.png", + "notes": "After-hours jazz bar with empty stage, piano, spotlights, and velvet shadows." + }, + { + "location_id": "amusement_street", + "status": "accepted", + "path": "assets/locations/amusement_street_bg_v1.png", + "notes": "Neon amusement street with arcade glow, reflections, and adult date-night energy." + }, + { + "location_id": "fashion_street", + "status": "accepted", + "path": "assets/locations/fashion_street_bg_v1.png", + "notes": "Rainy fashion street with boutique windows, mannequins, and glossy pavement." + }, + { + "location_id": "boutique_studio", + "status": "accepted", + "path": "assets/locations/boutique_studio_bg_v1.png", + "notes": "High-end fashion studio with mirrors, vanity bulbs, satin curtains, and camera gear." + }, + { + "location_id": "fitness_center", + "status": "accepted", + "path": "assets/locations/fitness_center_bg_v1.png", + "notes": "Late-night premium gym with red rim lighting, mirrors, and glossy equipment." + }, + { + "location_id": "art_studio", + "status": "accepted", + "path": "assets/locations/art_studio_bg_v1.png", + "notes": "Rainy art-studio alley with warm doorway light, canvases, and emotional mood." + }, + { + "location_id": "old_bookstore", + "status": "accepted", + "path": "assets/locations/old_bookstore_bg_v1.png", + "notes": "Narrow old bookstore with amber lamps, aged paper, and hidden-corner intimacy." + }, + { + "location_id": "flower_shop", + "status": "accepted", + "path": "assets/locations/flower_shop_bg_v1.png", + "notes": "Closing-time flower shop with deep roses, warm sunlight, and lush floral color." + }, + { + "location_id": "cinema", + "status": "accepted", + "path": "assets/locations/cinema_bg_v1.png", + "notes": "Empty premium cinema with red panels, plush seats, and late-night date tension." + }, + { + "location_id": "police_box", + "status": "accepted", + "path": "assets/locations/police_box_bg_v1.png", + "notes": "Rainy police-box exterior with warm safety light and restrained late-night mood." + }, + { + "location_id": "global_lounge", + "status": "accepted", + "path": "assets/locations/global_lounge_bg_v1.png", + "notes": "International lounge with city-view windows, brass lamps, and cosmopolitan warmth." + }, + { + "location_id": "apartment_complex", + "status": "accepted", + "path": "assets/locations/apartment_complex_bg_v1.png", + "notes": "Modern apartment courtyard at night with rain reflections and private comfort." + }, + { + "location_id": "lakeside_walk", + "status": "accepted", + "path": "assets/locations/lakeside_walk_bg_v1.png", + "notes": "Quiet lakeside boardwalk with lanterns, mist, and mature confession mood." + } + ] +} diff --git a/scripts/validate_assets.py b/scripts/validate_assets.py index e628bc0..7546723 100644 --- a/scripts/validate_assets.py +++ b/scripts/validate_assets.py @@ -57,10 +57,12 @@ def validate_asset_image(path: Path, min_width: int, min_height: int, label: str def main() -> None: characters_doc = load_json(DATA / "characters.json") + locations_doc = load_json(DATA / "locations.json") assets_doc = load_json(DATA / "character_assets.json") character_ids = [character["id"] for character in characters_doc["characters"]] character_id_set = set(character_ids) + location_ids = {location["id"] for location in locations_doc["locations"]} asset_entries = assets_doc["assets"] asset_ids = [entry["character_id"] for entry in asset_entries] @@ -158,12 +160,64 @@ def main() -> None: f"{cg_set['set_id']} revision candidate {affection_min}", ) + location_asset_path = DATA / "location_assets.json" + location_accepted = 0 + location_revision_count = 0 + if location_asset_path.exists(): + location_assets_doc = load_json(location_asset_path) + location_minimum = location_assets_doc["minimum_dimensions"] + location_min_width = location_minimum["width"] + location_min_height = location_minimum["height"] + + contact_sheet = ROOT / location_assets_doc["contact_sheet"] + if not contact_sheet.exists(): + fail(f"location contact sheet is missing: {contact_sheet}") + png_dimensions(contact_sheet) + + location_asset_entries = location_assets_doc["assets"] + asset_location_ids = [entry["location_id"] for entry in location_asset_entries] + if set(asset_location_ids) != location_ids: + missing = sorted(location_ids.difference(asset_location_ids)) + extra = sorted(set(asset_location_ids).difference(location_ids)) + fail(f"location asset manifest mismatch; missing={missing}, extra={extra}") + + if len(asset_location_ids) != len(set(asset_location_ids)): + fail("location asset manifest contains duplicate location ids") + + for entry in location_asset_entries: + status = entry["status"] + if status not in {"accepted", "needs_revision", "pending", "rejected"}: + fail(f"{entry['location_id']} has invalid location asset status {status}") + + path_value = entry.get("path") + if status == "accepted": + location_accepted += 1 + if not path_value: + fail(f"{entry['location_id']} accepted location asset lacks a path") + validate_asset_image( + ROOT / path_value, + location_min_width, + location_min_height, + entry["location_id"], + ) + elif status == "needs_revision": + location_revision_count += 1 + if path_value: + validate_asset_image( + ROOT / path_value, + location_min_width, + location_min_height, + f"{entry['location_id']} revision candidate", + ) + print( "Validated character asset manifest: " f"{accepted}/{len(asset_entries)} accepted, " f"{revision_count} needs revision, " f"{date_cg_accepted} accepted date CG tiers, " - f"{date_cg_revision_count} date CG tiers need revision." + f"{date_cg_revision_count} date CG tiers need revision, " + f"{location_accepted} accepted location backgrounds, " + f"{location_revision_count} location backgrounds need revision." )