Add anchor character location CGs

This commit is contained in:
2026-06-13 03:30:34 +09:00
parent 55f417c9e1
commit 7056ea8931
26 changed files with 265 additions and 1 deletions

View File

@@ -35,3 +35,9 @@
python3 scripts/validate_content.py python3 scripts/validate_content.py
python3 scripts/validate_assets.py python3 scripts/validate_assets.py
``` ```
## Character Location CG Assets
- Anchor character-location CG round 01 is stored under `assets/characters/location_cg/<character_id>/`.
- The round 01 comparison sheet is `assets/characters/location_cg/anchor_locations_round01_contact_sheet.png`.
- The manifest is `data/character_location_assets.json`; `scripts/validate_assets.py` checks character/location references, presence availability, PNG files, and minimum 1280x720 dimensions.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 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.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 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: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@@ -0,0 +1,174 @@
{
"version": "0.1.0",
"description": "Accepted anchor-location encounter CGs for the first character-location art pass.",
"minimum_dimensions": {
"width": 1280,
"height": 720
},
"source_presence_manifest": "data/character_location_presence.json",
"contact_sheet": "assets/characters/location_cg/anchor_locations_round01_contact_sheet.png",
"generation_rounds": [
{
"round_id": "anchor_locations_round01",
"description": "One mature glossy anime encounter CG for each character's anchor location.",
"status": "accepted",
"asset_count": 22
}
],
"assets": [
{
"character_id": "han_seoyun",
"location_id": "cooking_studio",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/han_seoyun/han_seoyun_cooking_studio_v1.png"
},
{
"character_id": "yu_harin",
"location_id": "old_bookstore",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/yu_harin/yu_harin_old_bookstore_v1.png"
},
{
"character_id": "kang_mirae",
"location_id": "office_district",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/kang_mirae/kang_mirae_office_district_v1.png"
},
{
"character_id": "jung_naeun",
"location_id": "downtown_cafe",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/jung_naeun/jung_naeun_downtown_cafe_v1.png"
},
{
"character_id": "baek_chaewon",
"location_id": "rooftop_lounge",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/baek_chaewon/baek_chaewon_rooftop_lounge_v1.png"
},
{
"character_id": "oh_sera",
"location_id": "flower_shop",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/oh_sera/oh_sera_flower_shop_v1.png"
},
{
"character_id": "moon_jia",
"location_id": "amusement_street",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/moon_jia/moon_jia_amusement_street_v1.png"
},
{
"character_id": "lim_sohee",
"location_id": "downtown_cafe",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/lim_sohee/lim_sohee_downtown_cafe_v1.png"
},
{
"character_id": "cha_yuri",
"location_id": "fashion_street",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/cha_yuri/cha_yuri_fashion_street_v1.png"
},
{
"character_id": "shin_yerim",
"location_id": "wine_bar",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/shin_yerim/shin_yerim_wine_bar_v1.png"
},
{
"character_id": "kim_doa",
"location_id": "university_quarter",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/kim_doa/kim_doa_university_quarter_v1.png"
},
{
"character_id": "park_minseo",
"location_id": "fitness_center",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/park_minseo/park_minseo_fitness_center_v1.png"
},
{
"character_id": "seo_irin",
"location_id": "art_studio",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/seo_irin/seo_irin_art_studio_v1.png"
},
{
"character_id": "yoon_gayoung",
"location_id": "apartment_complex",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/yoon_gayoung/yoon_gayoung_apartment_complex_v1.png"
},
{
"character_id": "choi_ria",
"location_id": "boutique_studio",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/choi_ria/choi_ria_boutique_studio_v1.png"
},
{
"character_id": "hong_sua",
"location_id": "jazz_bar",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/hong_sua/hong_sua_jazz_bar_v1.png"
},
{
"character_id": "lee_bom",
"location_id": "flower_shop",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/lee_bom/lee_bom_flower_shop_v1.png"
},
{
"character_id": "go_eunjae",
"location_id": "office_district",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/go_eunjae/go_eunjae_office_district_v1.png"
},
{
"character_id": "ryu_haena",
"location_id": "fashion_street",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/ryu_haena/ryu_haena_fashion_street_v1.png"
},
{
"character_id": "nam_juhee",
"location_id": "police_box",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/nam_juhee/nam_juhee_police_box_v1.png"
},
{
"character_id": "marina",
"location_id": "global_lounge",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/marina/marina_global_lounge_v1.png"
},
{
"character_id": "song_arin",
"location_id": "apartment_complex",
"round_id": "anchor_locations_round01",
"status": "accepted",
"path": "assets/characters/location_cg/song_arin/song_arin_apartment_complex_v1.png"
}
]
}

View File

@@ -210,6 +210,88 @@ def main() -> None:
f"{entry['location_id']} revision candidate", f"{entry['location_id']} revision candidate",
) )
character_location_asset_path = DATA / "character_location_assets.json"
character_location_accepted = 0
character_location_revision_count = 0
if character_location_asset_path.exists():
character_location_assets_doc = load_json(character_location_asset_path)
character_location_minimum = character_location_assets_doc[
"minimum_dimensions"
]
character_location_min_width = character_location_minimum["width"]
character_location_min_height = character_location_minimum["height"]
contact_sheet = ROOT / character_location_assets_doc["contact_sheet"]
if not contact_sheet.exists():
fail(f"character-location contact sheet is missing: {contact_sheet}")
png_dimensions(contact_sheet)
presence_doc = load_json(DATA / "character_location_presence.json")
allowed_pairs = {
(entry["character_id"], location["location_id"])
for entry in presence_doc["characters"]
for location in entry["locations"]
}
character_location_asset_entries = character_location_assets_doc["assets"]
seen_pairs = set()
for entry in character_location_asset_entries:
character_id = entry["character_id"]
location_id = entry["location_id"]
pair = (character_id, location_id)
if character_id not in character_id_set:
fail(
"character-location asset references missing character id: "
f"{character_id}"
)
if location_id not in location_ids:
fail(
"character-location asset references missing location id: "
f"{location_id}"
)
if pair not in allowed_pairs:
fail(
"character-location asset references unavailable location: "
f"{character_id}/{location_id}"
)
if pair in seen_pairs:
fail(
"character-location asset manifest contains duplicate pair: "
f"{character_id}/{location_id}"
)
seen_pairs.add(pair)
status = entry["status"]
if status not in {"accepted", "needs_revision", "pending", "rejected"}:
fail(
f"{character_id}/{location_id} has invalid "
f"character-location asset status {status}"
)
path_value = entry.get("path")
if status == "accepted":
character_location_accepted += 1
if not path_value:
fail(
f"{character_id}/{location_id} accepted asset lacks a path"
)
validate_asset_image(
ROOT / path_value,
character_location_min_width,
character_location_min_height,
f"{character_id}/{location_id}",
)
elif status == "needs_revision":
character_location_revision_count += 1
if path_value:
validate_asset_image(
ROOT / path_value,
character_location_min_width,
character_location_min_height,
f"{character_id}/{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, "
@@ -217,7 +299,9 @@ def main() -> None:
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_accepted} accepted location backgrounds, "
f"{location_revision_count} location backgrounds need revision." f"{location_revision_count} location backgrounds need revision, "
f"{character_location_accepted} accepted character-location CGs, "
f"{character_location_revision_count} character-location CGs need revision."
) )