cocos-farm/tools/verify_creator_project.py

691 lines
43 KiB
Python

#!/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())