Add sensual location background assets
@@ -11,6 +11,7 @@
|
|||||||
- `data/character_location_presence.json`: 캐릭터별 출현 가능 장소, 시간대, 등장 가중치
|
- `data/character_location_presence.json`: 캐릭터별 출현 가능 장소, 시간대, 등장 가중치
|
||||||
- `data/progression.json`: 만남에서 연인 전환까지의 공통 진행 규칙
|
- `data/progression.json`: 만남에서 연인 전환까지의 공통 진행 규칙
|
||||||
- `data/character_assets.json`: 캐릭터 기본 이미지의 통과 상태와 파일 경로
|
- `data/character_assets.json`: 캐릭터 기본 이미지의 통과 상태와 파일 경로
|
||||||
|
- `data/location_assets.json`: 장소별 배경 CG의 통과 상태와 파일 경로
|
||||||
- `docs/game_structure.md`: 게임 루프와 시스템 설계 메모
|
- `docs/game_structure.md`: 게임 루프와 시스템 설계 메모
|
||||||
- `docs/art/facial_identity_matrix.md`: 캐릭터별 얼굴형, 눈매, 헤어, 표정 차별화 기준
|
- `docs/art/facial_identity_matrix.md`: 캐릭터별 얼굴형, 눈매, 헤어, 표정 차별화 기준
|
||||||
- `scripts/validate_content.py`: 캐릭터 나이와 장소 참조 무결성 검증
|
- `scripts/validate_content.py`: 캐릭터 나이와 장소 참조 무결성 검증
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
|
|
||||||
- 박민서 피트니스 데이트 101+ CG는 `assets/characters/date_cg/park_minseo_fitness_affection_100plus_v2.png`를 사용합니다.
|
- 박민서 피트니스 데이트 101+ CG는 `assets/characters/date_cg/park_minseo_fitness_affection_100plus_v2.png`를 사용합니다.
|
||||||
- 신규 후보 10장과 비교용 contact sheet는 `assets/characters/date_cg/candidates/park_minseo_100plus/`에 보관합니다.
|
- 신규 후보 10장과 비교용 contact sheet는 `assets/characters/date_cg/candidates/park_minseo_100plus/`에 보관합니다.
|
||||||
|
- 장소 배경 CG 22장은 `assets/locations/`에 보관하며, 비교용 contact sheet는 `assets/locations/location_backgrounds_contact_sheet.png`입니다.
|
||||||
|
|
||||||
## 검증
|
## 검증
|
||||||
|
|
||||||
|
|||||||
BIN
assets/locations/amusement_street_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
assets/locations/apartment_complex_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
assets/locations/art_studio_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
assets/locations/book_cafe_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
assets/locations/boutique_studio_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
assets/locations/cinema_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
assets/locations/cooking_studio_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
assets/locations/downtown_cafe_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
assets/locations/fashion_street_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
assets/locations/fitness_center_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
assets/locations/flower_shop_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
assets/locations/global_lounge_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
assets/locations/jazz_bar_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
assets/locations/lakeside_walk_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
assets/locations/location_backgrounds_contact_sheet.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
assets/locations/neighborhood_market_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
assets/locations/office_district_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
assets/locations/old_bookstore_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
assets/locations/police_box_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
assets/locations/riverside_park_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
assets/locations/rooftop_lounge_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
assets/locations/university_quarter_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
assets/locations/wine_bar_bg_v1.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
146
data/location_assets.json
Normal 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."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -57,10 +57,12 @@ def validate_asset_image(path: Path, min_width: int, min_height: int, label: str
|
|||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
characters_doc = load_json(DATA / "characters.json")
|
characters_doc = load_json(DATA / "characters.json")
|
||||||
|
locations_doc = load_json(DATA / "locations.json")
|
||||||
assets_doc = load_json(DATA / "character_assets.json")
|
assets_doc = load_json(DATA / "character_assets.json")
|
||||||
|
|
||||||
character_ids = [character["id"] for character in characters_doc["characters"]]
|
character_ids = [character["id"] for character in characters_doc["characters"]]
|
||||||
character_id_set = set(character_ids)
|
character_id_set = set(character_ids)
|
||||||
|
location_ids = {location["id"] for location in locations_doc["locations"]}
|
||||||
asset_entries = assets_doc["assets"]
|
asset_entries = assets_doc["assets"]
|
||||||
asset_ids = [entry["character_id"] for entry in asset_entries]
|
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}",
|
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(
|
print(
|
||||||
"Validated character asset manifest: "
|
"Validated character asset manifest: "
|
||||||
f"{accepted}/{len(asset_entries)} accepted, "
|
f"{accepted}/{len(asset_entries)} accepted, "
|
||||||
f"{revision_count} needs revision, "
|
f"{revision_count} needs revision, "
|
||||||
f"{date_cg_accepted} accepted date CG tiers, "
|
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."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||