#!/usr/bin/env python3 """Verify the generated Cocos Creator 3.8 Stage 1 project assets.""" from __future__ import annotations import json from pathlib import Path from PIL import Image ROOT = Path(__file__).resolve().parents[1] PROJECT = ROOT / "CocosFarm" SOURCE_JSON = ROOT / "source_export" / "stage1_world_camera_source.json" LAND_SOURCE_JSON = ROOT / "source_export" / "stage1_visible_lands_source.json" INTERACTION_SOURCE_JSON = ROOT / "source_export" / "stage1_land_interaction_source.json" DECOR_SOURCE_JSON = ROOT / "source_export" / "stage1_scene_decor_source.json" TOP_HUD_SOURCE_JSON = ROOT / "source_export" / "stage1_top_hud_source.json" SCENE_PATH = PROJECT / "assets" / "scenes" / "stage1_world_camera.scene" SPINE_DIR = PROJECT / "assets" / "spine" / "stage1" REPORT_DIR = ROOT / "artifacts" / "creator_capture" REPORT_PATH = REPORT_DIR / "stage1_cocos_verify.json" EXPECTED_SCENE_UUID = "02bb8b9c-d1f7-40d3-b9d3-3568d015c285" EXPECTED_WORLD_LAYER = 1073741824 EXPECTED_UI_LAYER = 33554432 EXPECTED_MAIN_WORLD_LAYER_CLASS_ID = "4be13LrEwxZU7TcuozIMIug" EXPECTED_LAND_INTERACTION_CLASS_ID = "b32f9UsoC5TmImNNdBv0Q9D" EXPECTED_TOP_HUD_CLASS_ID = "60555IUx2NSTYh5t00b23aD" EXPECTED_FIXED_UI_LAYER_CLASS_ID = "840fawjq1VdtIEfAgdKNAUh" def load_json(path: Path) -> dict | list: return json.loads(path.read_text(encoding="utf-8")) def fail(message: str) -> dict: return {"status": "needs_solution", "error": message} def by_name(scene: list[dict]) -> dict[str, dict]: return {item.get("_name"): item for item in scene if isinstance(item, dict) and item.get("__type__") == "cc.Node"} def node_indices_by_name(scene: list[dict]) -> dict[str, list[int]]: result: dict[str, list[int]] = {} for index, item in enumerate(scene): if isinstance(item, dict) and item.get("__type__") == "cc.Node": result.setdefault(item.get("_name"), []).append(index) return result def components(scene: list[dict], component_type: str) -> list[dict]: return [item for item in scene if isinstance(item, dict) and item.get("__type__") == component_type] def assert_equal(errors: list[str], label: str, actual: object, expected: object) -> None: if actual != expected: errors.append(f"{label}: expected {expected!r}, got {actual!r}") def assert_close(errors: list[str], label: str, actual: float, expected: float, tolerance: float = 0.001) -> None: if abs(float(actual) - float(expected)) > tolerance: errors.append(f"{label}: expected {expected!r}, got {actual!r}") def component_for(scene: list[dict], node_index: int, component_type: str) -> dict | None: node = scene[node_index] for component_ref in node.get("_components", []): component = scene[component_ref["__id__"]] if isinstance(component, dict) and component.get("__type__") == component_type: return component return None def child_by_name(scene: list[dict], parent_index: int, child_name: str) -> int | None: for child_ref in scene[parent_index].get("_children", []): child_index = child_ref["__id__"] if scene[child_index].get("_name") == child_name: return child_index return None def child_by_path(scene: list[dict], parent_index: int, child_path: str) -> int | None: current = parent_index for part in child_path.split("/"): next_index = child_by_name(scene, current, part) if next_index is None: return None current = next_index return current def source_by_path(evidence: dict, path: str) -> dict: for item in evidence["nodes"]: if item["path"] == path: return item raise KeyError(path) def top_hud_mock_label(top_hud_evidence: dict, source: dict) -> str: mock_user = top_hud_evidence["mockUser"] mock_field = source["label"].get("mockField") if mock_field == "nickname": return str(mock_user["nickname"]) if mock_field == "level": return f" {mock_user['level']} " if mock_field == "experience": return f"{mock_user['exp']}/{mock_user['expMax']}" if mock_field == "goldDisplay": return str(mock_user["goldDisplay"]) if mock_field == "diamond": return str(mock_user["diamond"]) return str(source["label"]["string"]) def top_hud_mock_progress(top_hud_evidence: dict) -> float: mock_user = top_hud_evidence["mockUser"] exp_max = float(mock_user["expMax"]) if exp_max <= 0: return 0.0 return max(0.0, min(1.0, float(mock_user["exp"]) / exp_max)) def main() -> int: evidence = load_json(SOURCE_JSON) land_evidence = load_json(LAND_SOURCE_JSON) interaction_evidence = load_json(INTERACTION_SOURCE_JSON) decor_evidence = load_json(DECOR_SOURCE_JSON) top_hud_evidence = load_json(TOP_HUD_SOURCE_JSON) scene = load_json(SCENE_PATH) errors: list[str] = [] scene_asset = scene[0] scene_root = scene[1] nodes = by_name(scene) node_indices = node_indices_by_name(scene) assert_equal(errors, "scene uuid", scene_asset["scene"]["__id__"], 1) assert_equal(errors, "scene root uuid", scene_root["_id"], EXPECTED_SCENE_UUID) for name in ["root", "scene", "Camera", "ui", "uiCamera", "farm_scene_v3", "Scene", "BgLayer", "defaultbg", "FgLayer", "default", "fg", "FarmSkin", "Layer1", "Layer2", "Layer3", "Layer4", "Layer5", "Layer6", "Layer7", "house_201001", "Trees", "DogLayer", "dogHouse", "DynamicDogLayer", "Board", "Tower", "Storage", "PrePlant", "PostPlant", "Scaled", "Rotate", "GridOrigin"]: if not node_indices.get(name): errors.append(f"missing node {name}") for name in ["root", "scene", "farm_scene_v3", "Scene", "BgLayer", "defaultbg", "FgLayer", "default", "fg", "FarmSkin", "Layer1", "Layer2", "Layer3", "Layer4", "Layer5", "Layer6", "Layer7", "house_201001", "Trees", "DogLayer", "dogHouse", "DynamicDogLayer", "Board", "Tower", "Storage", "PrePlant", "PostPlant", "Scaled", "Rotate", "GridOrigin"]: indexes = node_indices.get(name, []) if indexes and not any(scene[index]["_layer"] == EXPECTED_WORLD_LAYER for index in indexes): errors.append(f"{name} layer: expected at least one node on {EXPECTED_WORLD_LAYER}, got {[scene[index]['_layer'] for index in indexes]}") for name in ["ui", "uiCamera"]: indexes = node_indices.get(name, []) if indexes and not any(scene[index]["_layer"] == EXPECTED_UI_LAYER for index in indexes): errors.append(f"{name} layer: expected at least one node on {EXPECTED_UI_LAYER}, got {[scene[index]['_layer'] for index in indexes]}") ui_camera_component_scene_index = None cameras = components(scene, "cc.Camera") if len(cameras) != 2: errors.append(f"camera component count: expected 2, got {len(cameras)}") else: scene_layer_index = child_by_path(scene, 2, "scene") ui_layer_index = child_by_path(scene, 2, "ui") camera_index = child_by_path(scene, 2, "scene/Camera") ui_camera_index = child_by_path(scene, 2, "ui/uiCamera") camera = component_for(scene, camera_index, "cc.Camera") if camera_index is not None else None ui_camera = component_for(scene, ui_camera_index, "cc.Camera") if ui_camera_index is not None else None main_camera_component_scene_index = None if camera_index is not None: for component_ref in scene[camera_index].get("_components", []): component_index = component_ref["__id__"] if scene[component_index].get("__type__") == "cc.Camera": main_camera_component_scene_index = component_index break if ui_camera_index is not None: for component_ref in scene[ui_camera_index].get("_components", []): component_index = component_ref["__id__"] if scene[component_index].get("__type__") == "cc.Camera": ui_camera_component_scene_index = component_index break source_camera = evidence["camera"] source_ui_camera = evidence["uiCamera"] if not camera: errors.append("missing source main camera component") else: assert_equal(errors, "camera orthoHeight", camera["_orthoHeight"], source_camera["orthographicHeight"]) assert_equal(errors, "camera near", camera["_near"], source_camera["near"]) assert_equal(errors, "camera far", camera["_far"], source_camera["far"]) assert_equal(errors, "camera visibility", camera["_visibility"], source_camera["visibility"]) world_camera_runtime = component_for(scene, scene_layer_index, EXPECTED_MAIN_WORLD_LAYER_CLASS_ID) if scene_layer_index is not None else None if not world_camera_runtime: errors.append("root/scene missing Stage1MainWorldLayer component") else: assert_equal( errors, "Stage1MainWorldLayer mainCamera", world_camera_runtime.get("mainCamera", {}).get("__id__"), main_camera_component_scene_index, ) if not ui_camera: errors.append("missing source uiCamera component") else: assert_equal(errors, "uiCamera orthoHeight", ui_camera["_orthoHeight"], source_ui_camera["orthographicHeight"]) assert_equal(errors, "uiCamera near", ui_camera["_near"], source_ui_camera["near"]) assert_equal(errors, "uiCamera far", ui_camera["_far"], source_ui_camera["far"]) assert_equal(errors, "uiCamera visibility", ui_camera["_visibility"], source_ui_camera["visibility"]) assert_equal(errors, "uiCamera priority", ui_camera["_priority"], source_ui_camera["priority"]) assert_equal(errors, "uiCamera clearFlags", ui_camera["_clearFlags"], source_ui_camera["clearFlags"]) if not (ui_layer_index is not None and component_for(scene, ui_layer_index, EXPECTED_FIXED_UI_LAYER_CLASS_ID)): errors.append("root/ui missing Stage1FixedUiLayer component") sprite_components = components(scene, "cc.Sprite") expected_interaction_sprites = sum(1 for item in interaction_evidence["nodes"] if item.get("spriteAssetId")) expected_top_hud_sprites = sum(1 for item in top_hud_evidence["nodes"] if item.get("spriteAssetId")) expected_extend_boards = sum(1 for item in land_evidence["visibleLands"] if item["mockRender"]["assetId"] == "land_extend") assert_equal(errors, "sprite component count", len(sprite_components), 2 + expected_extend_boards + expected_interaction_sprites + expected_top_hud_sprites) expected_top_hud_graphics = sum(1 for item in top_hud_evidence["nodes"] if item.get("mask", {}).get("fillColorUint32") is not None) assert_equal(errors, "cc.Graphics component count", len(components(scene, "cc.Graphics")), expected_top_hud_graphics) skeleton_components = components(scene, "sp.Skeleton") assert_equal(errors, "spine skeleton component count", len(skeleton_components), len(land_evidence["visibleLands"]) + len(decor_evidence["decorSpineAssets"])) skeleton_data = components(scene, "sp.SkeletonData") assert_equal(errors, "inline spine skeleton data count", len(skeleton_data), 0) spine_json = SPINE_DIR / "scene_land.json" spine_atlas = SPINE_DIR / "scene_land.atlas" spine_png = SPINE_DIR / "scene_land.png" spine_json_meta = spine_json.with_suffix(spine_json.suffix + ".meta") spine_atlas_meta = spine_atlas.with_suffix(spine_atlas.suffix + ".meta") spine_png_meta = spine_png.with_suffix(spine_png.suffix + ".meta") spine_data_uuid = "" spine_skeleton_json = {} for path in [spine_json, spine_atlas, spine_png, spine_json_meta, spine_atlas_meta, spine_png_meta]: if not path.exists(): errors.append(f"missing spine asset {path}") if spine_json.exists(): spine_skeleton_json = load_json(spine_json) for animation in ["land_valid1", "land_valid2", "land_valid3", "land_valid4", "land_locked"]: if animation not in spine_skeleton_json.get("animations", {}): errors.append(f"scene_land missing animation {animation}") if spine_json_meta.exists() and spine_atlas_meta.exists(): json_meta = load_json(spine_json_meta) atlas_meta = load_json(spine_atlas_meta) assert_equal(errors, "scene_land json importer", json_meta.get("importer"), "spine-data") assert_equal(errors, "scene_land atlas importer", atlas_meta.get("importer"), "*") assert_equal(errors, "scene_land json atlasUuid", json_meta.get("userData", {}).get("atlasUuid"), atlas_meta.get("uuid")) spine_data_uuid = json_meta.get("uuid", "") if spine_png.exists(): with Image.open(spine_png) as image: assert_equal(errors, "scene_land spine atlas size", image.size, (1024, 512)) decor_spine_uuids: dict[str, str] = {} for asset in decor_evidence["decorSpineAssets"]: asset_id = asset["id"] skin_cfg_id = str(asset["skinCfgId"]) asset_dir = SPINE_DIR / asset_id skel_path = asset_dir / f"{skin_cfg_id}.skel" atlas_path = asset_dir / f"{skin_cfg_id}.atlas" png_path = asset_dir / asset["atlasImageName"] skel_meta_path = skel_path.with_suffix(skel_path.suffix + ".meta") atlas_meta_path = atlas_path.with_suffix(atlas_path.suffix + ".meta") png_meta_path = png_path.with_suffix(png_path.suffix + ".meta") for path in [skel_path, atlas_path, png_path, skel_meta_path, atlas_meta_path, png_meta_path]: if not path.exists(): errors.append(f"missing decor spine asset {path}") if skel_meta_path.exists() and atlas_meta_path.exists(): skel_meta = load_json(skel_meta_path) atlas_meta = load_json(atlas_meta_path) assert_equal(errors, f"{asset_id} skeleton importer", skel_meta.get("importer"), "spine-data") assert_equal(errors, f"{asset_id} atlas importer", atlas_meta.get("importer"), "*") assert_equal(errors, f"{asset_id} atlasUuid", skel_meta.get("userData", {}).get("atlasUuid"), atlas_meta.get("uuid")) decor_spine_uuids[asset_id] = skel_meta.get("uuid", "") if png_path.exists(): with Image.open(png_path) as image: assert_equal(errors, f"{asset_id} atlas image size", image.size, (asset["atlasImageInfo"]["width"], asset["atlasImageInfo"]["height"])) expected_textures = { item["id"]: PROJECT / "assets" / "textures" / "stage1" / f"{item['id']}.png" for item in [*evidence["assets"], *land_evidence["assets"], *interaction_evidence["assets"], *top_hud_evidence["assets"], {"id": "scene_land_atlas"}] } image_sizes = {} for asset_id, path in expected_textures.items(): if not path.exists(): errors.append(f"missing texture {path}") continue with Image.open(path) as image: image_sizes[asset_id] = image.size if not path.with_suffix(path.suffix + ".meta").exists(): errors.append(f"missing texture meta {path}.meta") expected_grid_names = [item["gridName"] for item in land_evidence["visibleLands"]] actual_grid_names = [name for name in expected_grid_names if name in node_indices] assert_equal(errors, "visible grid node count", len(actual_grid_names), 24) sorted_lands = sorted(land_evidence["visibleLands"], key=lambda item: item["cell"]["sourceIndex"]) for land in land_evidence["visibleLands"]: grid_name = land["gridName"] indexes = node_indices.get(grid_name, []) if len(indexes) != 1: errors.append(f"{grid_name} node count: expected 1, got {len(indexes)}") continue grid_index = indexes[0] grid_node = scene[grid_index] cell = land["cell"] assert_close(errors, f"{grid_name} local x", grid_node["_lpos"]["x"], cell["localPosition"]["x"]) assert_close(errors, f"{grid_name} local y", grid_node["_lpos"]["y"], cell["localPosition"]["y"]) grid_transform = component_for(scene, grid_index, "cc.UITransform") if not grid_transform: errors.append(f"{grid_name} missing UITransform") else: assert_close(errors, f"{grid_name} width", grid_transform["_contentSize"]["width"], cell["size"]["x"]) assert_close(errors, f"{grid_name} height", grid_transform["_contentSize"]["height"], cell["size"]["y"]) assert_close(errors, f"{grid_name} anchor x", grid_transform["_anchorPoint"]["x"], cell["anchor"]["x"]) assert_close(errors, f"{grid_name} anchor y", grid_transform["_anchorPoint"]["y"], cell["anchor"]["y"]) children = grid_node.get("_children", []) expected_child_count = 2 if land["mockRender"]["assetId"] == "land_extend" else 1 if len(children) != expected_child_count: errors.append(f"{grid_name} child count: expected {expected_child_count}, got {len(children)}") continue icon_child_ref = next((item for item in children if scene[item["__id__"]].get("_name") == "icon"), None) if not icon_child_ref: errors.append(f"{grid_name} missing icon child") continue icon_node = scene[icon_child_ref["__id__"]] icon = land["icon"] assert_equal(errors, f"{grid_name} child name", icon_node["_name"], "icon") assert_close(errors, f"{grid_name}/icon local x", icon_node["_lpos"]["x"], icon["localPosition"]["x"]) assert_close(errors, f"{grid_name}/icon local y", icon_node["_lpos"]["y"], icon["localPosition"]["y"]) icon_transform = component_for(scene, icon_child_ref["__id__"], "cc.UITransform") icon_skeleton = component_for(scene, icon_child_ref["__id__"], "sp.Skeleton") if not icon_transform: errors.append(f"{grid_name}/icon missing UITransform") else: assert_close(errors, f"{grid_name}/icon width", icon_transform["_contentSize"]["width"], icon["size"]["x"]) assert_close(errors, f"{grid_name}/icon height", icon_transform["_contentSize"]["height"], icon["size"]["y"]) assert_close(errors, f"{grid_name}/icon anchor x", icon_transform["_anchorPoint"]["x"], icon["anchor"]["x"]) assert_close(errors, f"{grid_name}/icon anchor y", icon_transform["_anchorPoint"]["y"], icon["anchor"]["y"]) if not icon_skeleton: errors.append(f"{grid_name}/icon missing sp.Skeleton") else: source_order = sorted_lands.index(land) source_component = land_evidence["iconSkeletonComponents"][source_order] expected_animation = "land_locked" if land["mockRender"]["assetId"] == "land_extend" else land["mockRender"]["assetId"] assert_equal(errors, f"{grid_name}/icon defaultSkin", icon_skeleton["defaultSkin"], source_component["defaultSkin"]) assert_equal(errors, f"{grid_name}/icon defaultAnimation", icon_skeleton["defaultAnimation"], expected_animation) assert_equal(errors, f"{grid_name}/icon preCacheMode", icon_skeleton["_preCacheMode"], source_component["preCacheMode"]) assert_equal(errors, f"{grid_name}/icon cacheMode", icon_skeleton["_cacheMode"], source_component["cacheMode"]) assert_equal(errors, f"{grid_name}/icon enableBatch", icon_skeleton["_enableBatch"], source_component["enableBatch"]) if spine_data_uuid: assert_equal(errors, f"{grid_name}/icon skeletonData uuid", icon_skeleton.get("_skeletonData", {}).get("__uuid__"), spine_data_uuid) if land["mockRender"]["assetId"] == "land_extend": board_child_ref = next((item for item in children if scene[item["__id__"]].get("_name") == "expand_board"), None) if not board_child_ref: errors.append(f"{grid_name} missing expand_board child") else: child_names = [scene[item["__id__"]].get("_name") for item in children] if child_names.index("expand_board") > child_names.index("icon"): errors.append(f"{grid_name}/expand_board order: expected before icon, got {child_names}") board_transform = component_for(scene, board_child_ref["__id__"], "cc.UITransform") board_sprite = component_for(scene, board_child_ref["__id__"], "cc.Sprite") if not board_transform: errors.append(f"{grid_name}/expand_board missing UITransform") else: assert_close(errors, f"{grid_name}/expand_board width", board_transform["_contentSize"]["width"], 106) assert_close(errors, f"{grid_name}/expand_board height", board_transform["_contentSize"]["height"], 106) if not board_sprite: errors.append(f"{grid_name}/expand_board missing cc.Sprite") fg_layer_index = child_by_path(scene, 2, "scene/farm_scene_v3/Scene/FgLayer") if fg_layer_index is None: errors.append("missing FgLayer for decor ordering") else: fg_child_names = [scene[item["__id__"]].get("_name") for item in scene[fg_layer_index].get("_children", [])] expected_fg_child_names = [item["name"] for item in decor_evidence["sourceParentNodes"]["fgLayerChildrenOrder"]] assert_equal(errors, "FgLayer recovered child order", fg_child_names, expected_fg_child_names) farm_skin_index = child_by_path(scene, 2, "scene/farm_scene_v3/Scene/FgLayer/FarmSkin") if farm_skin_index is None: errors.append("missing FarmSkin source parent") else: farm_skin_children = [scene[item["__id__"]].get("_name") for item in scene[farm_skin_index].get("_children", [])] assert_equal(errors, "FarmSkin layer order", farm_skin_children, [item["name"] for item in decor_evidence["sourceParentNodes"]["farmSkinLayers"]]) decor_nodes = {item["path"]: item for item in decor_evidence["generatedNodes"]} for decor_path in [ "farm_scene_v3/Scene/FgLayer/FarmSkin/Layer2/house_201001", "farm_scene_v3/Scene/FgLayer/DogLayer/dogHouse", ]: source = decor_nodes[decor_path] scene_path = f"scene/{decor_path}" node_index = child_by_path(scene, 2, scene_path) if node_index is None: errors.append(f"missing decor node {decor_path}") continue node_data = scene[node_index] assert_close(errors, f"{decor_path} local x", node_data["_lpos"]["x"], source["localPosition"]["x"]) assert_close(errors, f"{decor_path} local y", node_data["_lpos"]["y"], source["localPosition"]["y"]) skeleton = component_for(scene, node_index, "sp.Skeleton") if not skeleton: errors.append(f"{decor_path} missing sp.Skeleton") else: asset = next(item for item in decor_evidence["decorSpineAssets"] if item["id"] == source["spineAssetId"]) assert_equal(errors, f"{decor_path} defaultSkin", skeleton["defaultSkin"], asset["defaultSkin"]) assert_equal(errors, f"{decor_path} defaultAnimation", skeleton["defaultAnimation"], asset["defaultAnimation"]) expected_uuid = decor_spine_uuids.get(source["spineAssetId"]) if expected_uuid: assert_equal(errors, f"{decor_path} skeletonData uuid", skeleton.get("_skeletonData", {}).get("__uuid__"), expected_uuid) interaction_components = components(scene, EXPECTED_LAND_INTERACTION_CLASS_ID) assert_equal(errors, "Stage1LandInteraction component count", len(interaction_components), 1) world_camera_components = components(scene, EXPECTED_MAIN_WORLD_LAYER_CLASS_ID) assert_equal(errors, "Stage1MainWorldLayer component count", len(world_camera_components), 1) top_hud_components = components(scene, EXPECTED_TOP_HUD_CLASS_ID) assert_equal(errors, "Stage1TopHud component count", len(top_hud_components), 1) ui_camera_viewport_components = components(scene, EXPECTED_FIXED_UI_LAYER_CLASS_ID) assert_equal(errors, "Stage1FixedUiLayer component count", len(ui_camera_viewport_components), 1) canvas_components = components(scene, "cc.Canvas") assert_equal(errors, "Canvas component count", len(canvas_components), 1) if canvas_components and len(cameras) == 2: camera_ref = canvas_components[0].get("_cameraComponent", {}) assert_equal( errors, "Canvas source camera binding", camera_ref.get("__id__"), ui_camera_component_scene_index, ) grid_graphics = interaction_evidence.get("farmGridGraphics", {}) farm_scene_indexes = node_indices.get("farm_scene_v3", []) if farm_scene_indexes: farm_scene_index = farm_scene_indexes[0] for key, node_name in [("prePlantNode", "PrePlant"), ("postPlantNode", "PostPlant")]: source = grid_graphics.get(key) node_index = child_by_name(scene, farm_scene_index, node_name) if source and node_index is None: errors.append(f"farm_scene_v3 missing {node_name}") continue if source and node_index is not None: node_data = scene[node_index] assert_close(errors, f"{node_name} local x", node_data["_lpos"]["x"], source["localPosition"]["x"]) assert_close(errors, f"{node_name} local y", node_data["_lpos"]["y"], source["localPosition"]["y"]) interaction_index = None ui_indexes = node_indices.get("ui", []) if not ui_indexes: errors.append("missing ui for interaction lookup") else: top_hud_index = child_by_name(scene, ui_indexes[0], "main_ui_v2") if top_hud_index is None: errors.append("ui missing main_ui_v2 child") else: assert_equal(errors, "main_ui_v2 layer", scene[top_hud_index]["_layer"], EXPECTED_UI_LAYER) for source in top_hud_evidence["nodes"]: scene_path = f"ui/{source['path']}" node_index = child_by_path(scene, 2, scene_path) if node_index is None: errors.append(f"missing top HUD node {source['path']}") continue node_data = scene[node_index] assert_equal(errors, f"{source['path']} layer", node_data["_layer"], EXPECTED_UI_LAYER) assert_equal(errors, f"{source['path']} active", node_data["_active"], bool(source.get("active", True))) assert_close(errors, f"{source['path']} local x", node_data["_lpos"]["x"], source["localPosition"]["x"]) assert_close(errors, f"{source['path']} local y", node_data["_lpos"]["y"], source["localPosition"]["y"]) if source.get("size"): transform = component_for(scene, node_index, "cc.UITransform") anchor = source.get("anchor", {"x": 0.5, "y": 0.5}) if not transform: errors.append(f"{source['path']} missing UITransform") else: assert_close(errors, f"{source['path']} width", transform["_contentSize"]["width"], source["size"]["x"]) assert_close(errors, f"{source['path']} height", transform["_contentSize"]["height"], source["size"]["y"]) assert_close(errors, f"{source['path']} anchor x", transform["_anchorPoint"]["x"], anchor["x"]) assert_close(errors, f"{source['path']} anchor y", transform["_anchorPoint"]["y"], anchor["y"]) if source.get("widget"): widget = component_for(scene, node_index, "cc.Widget") widget_source = source["widget"] if not widget: errors.append(f"{source['path']} missing source cc.Widget") else: assert_equal(errors, f"{source['path']} widget alignFlags", widget["_alignFlags"], widget_source["alignFlags"]) assert_equal(errors, f"{source['path']} widget alignMode", widget["_alignMode"], widget_source["alignMode"]) assert_close(errors, f"{source['path']} widget originalWidth", widget["_originalWidth"], widget_source.get("originalWidth") or 0) assert_close(errors, f"{source['path']} widget originalHeight", widget["_originalHeight"], widget_source.get("originalHeight") or 0) if source.get("spriteAssetId") and not component_for(scene, node_index, "cc.Sprite"): errors.append(f"{source['path']} missing cc.Sprite") if source.get("label"): label = component_for(scene, node_index, "cc.Label") expected_label = top_hud_mock_label(top_hud_evidence, source) if not label: errors.append(f"{source['path']} missing cc.Label") else: assert_equal(errors, f"{source['path']} mock label", label["_string"], expected_label) assert_equal(errors, f"{source['path']} horizontalAlign", label["_horizontalAlign"], source["label"]["horizontalAlign"]) assert_equal(errors, f"{source['path']} overflow", label["_overflow"], source["label"]["overflow"]) assert_equal(errors, f"{source['path']} enableWrapText", label["_enableWrapText"], source["label"]["enableWrapText"]) assert_close(errors, f"{source['path']} fontSize", label["_fontSize"], source["label"]["fontSize"]) if source.get("mask"): mask = component_for(scene, node_index, "cc.Mask") graphics = component_for(scene, node_index, "cc.Graphics") if not mask: errors.append(f"{source['path']} missing cc.Mask") else: assert_equal(errors, f"{source['path']} mask type", mask["_type"], source["mask"]["type"]) assert_equal(errors, f"{source['path']} mask segments", mask["_segments"], source["mask"]["segments"]) if not graphics: errors.append(f"{source['path']} missing cc.Graphics for source mask fill") if source.get("progressBar"): progress_bar = component_for(scene, node_index, "cc.ProgressBar") if not progress_bar: errors.append(f"{source['path']} missing cc.ProgressBar") else: assert_close(errors, f"{source['path']} totalLength", progress_bar["_totalLength"], source["progressBar"]["totalLength"]) assert_close(errors, f"{source['path']} mock progress", progress_bar["_progress"], top_hud_mock_progress(top_hud_evidence)) interaction_index = child_by_name(scene, ui_indexes[0], "plant_interactive_v2") if interaction_index is None: errors.append("ui missing plant_interactive_v2 child") else: assert_equal(errors, "plant_interactive_v2 active", scene[interaction_index]["_active"], False) assert_equal(errors, "plant_interactive_v2 layer", scene[interaction_index]["_layer"], EXPECTED_UI_LAYER) land_target_index = child_by_path(scene, interaction_index, "landTarget") if land_target_index is None: errors.append("plant_interactive_v2 missing landTarget") elif child_by_name(scene, land_target_index, "gold") is not None: errors.append("landTarget must not contain gold; source gold asset is a coin, not the selected-land frame") else: selected_index = child_by_name(scene, land_target_index, "land_valid_selected") if selected_index is None: errors.append("landTarget missing source land_valid_selected selected-land image") elif not component_for(scene, selected_index, "cc.Sprite"): errors.append("landTarget/land_valid_selected missing cc.Sprite") elif component_for(scene, selected_index, "cc.Graphics"): errors.append("landTarget/land_valid_selected must use the source SpriteFrame, not cc.Graphics") group_5_index = child_by_path(scene, interaction_index, "followNode/seedGroup/group_5") if group_5_index is None: errors.append("plant_interactive_v2 missing followNode/seedGroup/group_5") else: layout = component_for(scene, group_5_index, "cc.Layout") layout_source = interaction_evidence["slotLayout"]["layoutComponent"] if not layout: errors.append("group_5 missing source cc.Layout") else: assert_equal(errors, "group_5 layoutType", layout["_layoutType"], layout_source["layoutType"]) assert_equal(errors, "group_5 resizeMode", layout["_resizeMode"], layout_source["resizeMode"]) assert_close(errors, "group_5 spacingX", layout["_spacingX"], layout_source["spacingX"]) assert_close(errors, "group_5 paddingRight", layout["_paddingRight"], layout_source["paddingRight"]) for source in interaction_evidence["nodes"]: if source["path"] == "plant_interactive_v2": continue relative_path = source["path"].replace("plant_interactive_v2/", "", 1) node_index = child_by_path(scene, interaction_index, relative_path) if node_index is None: errors.append(f"missing interaction node {source['path']}") continue node_data = scene[node_index] assert_equal(errors, f"{source['path']} layer", node_data["_layer"], EXPECTED_UI_LAYER) assert_close(errors, f"{source['path']} local x", node_data["_lpos"]["x"], source["localPosition"]["x"]) assert_close(errors, f"{source['path']} local y", node_data["_lpos"]["y"], source["localPosition"]["y"]) if source.get("size"): transform = component_for(scene, node_index, "cc.UITransform") if not transform: errors.append(f"{source['path']} missing UITransform") else: assert_close(errors, f"{source['path']} width", transform["_contentSize"]["width"], source["size"]["x"]) assert_close(errors, f"{source['path']} height", transform["_contentSize"]["height"], source["size"]["y"]) if source.get("spriteAssetId") and not component_for(scene, node_index, "cc.Sprite"): errors.append(f"{source['path']} missing cc.Sprite") if source.get("label"): label = component_for(scene, node_index, "cc.Label") if not label: errors.append(f"{source['path']} missing cc.Label") else: assert_equal(errors, f"{source['path']} label", label["_string"], source["label"]["string"]) for script_path in [ PROJECT / "assets" / "scripts" / "stage1" / "Stage1MainWorldLayer.ts", PROJECT / "assets" / "scripts" / "stage1" / "Stage1LandInteraction.ts", PROJECT / "assets" / "scripts" / "stage1" / "Stage1TopHud.ts", PROJECT / "assets" / "scripts" / "stage1" / "Stage1FixedUiLayer.ts", PROJECT / "assets" / "scripts" / "mock" / "Stage1SeedMock.ts", PROJECT / "assets" / "scripts" / "mock" / "Stage1UserMock.ts", PROJECT / "assets" / "scripts" / "core" / "SourceLandInteraction.ts", PROJECT / "assets" / "scripts" / "core" / "SourceTopHud.ts", ]: if not script_path.exists(): errors.append(f"missing interaction script {script_path}") world_camera_script_path = PROJECT / "assets" / "scripts" / "stage1" / "Stage1MainWorldLayer.ts" if world_camera_script_path.exists(): script_text = world_camera_script_path.read_text(encoding="utf-8") for required in ["sourceCameraInitialOrthoHeight", "this.mainCamera.orthoHeight", "camera.update(true)"]: if required not in script_text: errors.append(f"Stage1MainWorldLayer missing source-backed camera rule {required}") source_space_path = PROJECT / "assets" / "scripts" / "core" / "SourceSpace.ts" if source_space_path.exists(): source_space = source_space_path.read_text(encoding="utf-8") if "return SOURCE_CAMERA_MIN_ZOOM;" not in source_space: errors.append("SourceSpace sourceCameraInitialZoom must use the source camera controller minZoom") if "return SOURCE_CAMERA_ORTHO_HEIGHT / sourceCameraInitialZoom();" not in source_space: errors.append("SourceSpace sourceCameraInitialOrthoHeight must apply the source camera controller zoom to serialized _orthoHeight") else: errors.append(f"missing generated source space script {source_space_path}") interaction_script_path = PROJECT / "assets" / "scripts" / "stage1" / "Stage1LandInteraction.ts" if interaction_script_path.exists(): script_text = interaction_script_path.read_text(encoding="utf-8") if "Graphics" in script_text or "source_selected_land_border" in script_text: errors.append("Stage1LandInteraction must not draw the selected-land visual; use land_valid_selected") if "this.interactionRoot.setPosition(targetPosition)" in script_text: errors.append("Stage1LandInteraction must not move plant_interactive_v2 to the land target") for required in [ "this.interactionRoot.setPosition(0, 0, 0)", "applySourceTargetPositions(targetPosition)", "followPosition.x = 0", ]: if required not in script_text: errors.append(f"Stage1LandInteraction missing source-backed target positioning rule {required}") for required in ["screenToWorld", "hideInteraction", "activeSlotCount > 3", "mountLandDetailBubble"]: if required not in script_text: errors.append(f"Stage1LandInteraction missing {required}") top_hud_script_path = PROJECT / "assets" / "scripts" / "stage1" / "Stage1TopHud.ts" if top_hud_script_path.exists(): script_text = top_hud_script_path.read_text(encoding="utf-8") for required in ["STAGE1_USER_MOCK.nickname", "STAGE1_USER_MOCK.goldDisplay", "stage1ExpProgress", "HeadInfo/SubHeadInfo/txtExp", "Source/root/txtDiamond"]: if required not in script_text: errors.append(f"Stage1TopHud missing mock binding {required}") ui_camera_viewport_script_path = PROJECT / "assets" / "scripts" / "stage1" / "Stage1FixedUiLayer.ts" if ui_camera_viewport_script_path.exists(): script_text = ui_camera_viewport_script_path.read_text(encoding="utf-8") for required in ["SOURCE_DESIGN_HEIGHT / 2", "SOURCE_DESIGN_WIDTH", "setContentSize", "design-resolution-changed", "camera.update(true)"]: if required not in script_text: errors.append(f"Stage1FixedUiLayer missing source-backed fixed UI rule {required}") project_settings_path = PROJECT / "settings" / "v2" / "packages" / "project.json" if project_settings_path.exists(): project_settings = load_json(project_settings_path) design_resolution = project_settings.get("general", {}).get("designResolution", {}) assert_equal(errors, "project designResolution width", design_resolution.get("width"), int(evidence["canvas"]["width"])) assert_equal(errors, "project designResolution height", design_resolution.get("height"), int(evidence["canvas"]["height"])) assert_equal(errors, "project designResolution policy", design_resolution.get("policy"), 2) else: errors.append(f"missing project settings {project_settings_path}") build_settings_path = PROJECT / "build" / "web-mobile" / "src" / "settings.json" if build_settings_path.exists(): build_settings = load_json(build_settings_path) build_screen = build_settings.get("screen", {}) build_resolution = build_screen.get("designResolution", {}) build_splash = build_settings.get("splashScreen", {}) assert_equal(errors, "build exactFitScreen", build_screen.get("exactFitScreen"), True) assert_equal(errors, "build designResolution width", build_resolution.get("width"), int(evidence["canvas"]["width"])) assert_equal(errors, "build designResolution height", build_resolution.get("height"), int(evidence["canvas"]["height"])) assert_equal(errors, "build designResolution policy", build_resolution.get("policy"), 2) assert_equal(errors, "build splash totalTime", build_splash.get("totalTime"), 0) assert_equal(errors, "build splash logo", build_splash.get("logo", {}).get("type"), "none") assert_equal(errors, "build splash background", build_splash.get("background", {}).get("type"), "color") assert_equal(errors, "build splash autoFit", build_splash.get("autoFit"), True) builder_profile_path = PROJECT / "profiles" / "v2" / "packages" / "builder.json" if builder_profile_path.exists(): builder_profile = load_json(builder_profile_path) splash = builder_profile.get("splashScreen", {}) assert_equal(errors, "builder useSplashScreen", builder_profile.get("useSplashScreen"), True) assert_equal(errors, "builder splash totalTime", splash.get("totalTime"), 0) assert_equal(errors, "builder splash logo", splash.get("logo", {}).get("type"), "none") assert_equal(errors, "builder splash background", splash.get("background", {}).get("type"), "color") assert_equal(errors, "builder splash autoFit", splash.get("autoFit"), True) else: errors.append(f"missing builder profile {builder_profile_path}") report = { "status": "pass" if not errors else "needs_solution", "project": str(PROJECT), "scene": str(SCENE_PATH), "sceneUuid": EXPECTED_SCENE_UUID, "designResolution": evidence["canvas"], "camera": evidence["camera"], "uiCamera": evidence["uiCamera"], "visibleLandCount": len(land_evidence["visibleLands"]), "interactionNodeCount": len(interaction_evidence["nodes"]), "topHudNodeCount": len(top_hud_evidence["nodes"]), "decorNodeCount": len(decor_evidence["generatedNodes"]), "decorSpineAssetCount": len(decor_evidence["decorSpineAssets"]), "mockSeedCount": len(interaction_evidence["mockSeeds"]), "mockUser": top_hud_evidence["mockUser"], "topHudSolutionDetails": top_hud_evidence.get("solutionDetails", []), "excludedStaticGridCount": land_evidence["excludedStaticGrid"]["visualLandCellCount"], "imageSizes": image_sizes, "spineAsset": { "json": str(spine_json), "atlas": str(spine_atlas), "png": str(spine_png), "skeletonDataUuid": spine_data_uuid, "animationCount": len(spine_skeleton_json.get("animations", {})) if isinstance(spine_skeleton_json, dict) else 0, }, "decorSpineAssets": decor_spine_uuids, "errors": errors, } REPORT_DIR.mkdir(parents=True, exist_ok=True) REPORT_PATH.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps(report, ensure_ascii=False, indent=2)) return 0 if not errors else 1 if __name__ == "__main__": raise SystemExit(main())