Add sensual location background assets

This commit is contained in:
2026-06-13 02:09:00 +09:00
parent c11da3530f
commit 55f417c9e1
26 changed files with 203 additions and 1 deletions

View File

@@ -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`입니다.
## 검증

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

146
data/location_assets.json Normal file
View File

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

View File

@@ -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."
)