#!/usr/bin/env python3 """Render a diagnostic Stage 1 canvas preview from source evidence.""" from __future__ import annotations import json from pathlib import Path from PIL import Image, ImageDraw, ImageFont ROOT = Path(__file__).resolve().parents[1] SOURCE_JSON = ROOT / "source_export" / "stage1_world_camera_source.json" LAND_SOURCE_JSON = ROOT / "source_export" / "stage1_visible_lands_source.json" TOP_HUD_SOURCE_JSON = ROOT / "source_export" / "stage1_top_hud_source.json" OUT_DIR = ROOT / "artifacts" / "canvas_preview" PREVIEW_PATH = OUT_DIR / "stage1_world_camera_source_preview.png" REPORT_PATH = OUT_DIR / "stage1_world_camera_source_preview_report.json" def load_json(path: Path) -> dict: return json.loads(path.read_text(encoding="utf-8")) def flatten_nodes(nodes: list[dict]) -> dict[str, dict]: return {item["path"]: item for item in nodes} def world_position(path: str, nodes: dict[str, dict]) -> tuple[float, float]: parts = path.split("/") current = [] x = 0.0 y = 0.0 for part in parts: current.append(part) node = nodes.get("/".join(current)) if node: x += float(node["localPosition"]["x"]) y += float(node["localPosition"]["y"]) return x, y def draw_source_sprite( output: Image.Image, source_path: str, world_x: float, world_y: float, node_width: float, node_height: float, anchor_x: float, anchor_y: float, camera: dict, ) -> dict: width, height = output.size with Image.open(source_path) as source_image: image = source_image.convert("RGBA").resize( (int(round(node_width)), int(round(node_height))), Image.Resampling.LANCZOS, ) screen_x = width / 2 + (world_x - float(camera["sourcePosition"]["x"])) - anchor_x * node_width screen_y = height / 2 - (world_y - float(camera["sourcePosition"]["y"])) - (1 - anchor_y) * node_height output.alpha_composite(image, (int(round(screen_x)), int(round(screen_y)))) return { "worldX": world_x, "worldY": world_y, "width": node_width, "height": node_height, "anchorX": anchor_x, "anchorY": anchor_y, "screenX": int(round(screen_x)), "screenY": int(round(screen_y)), } def uint32_to_rgba(value: int) -> tuple[int, int, int, int]: return (value & 255, (value >> 8) & 255, (value >> 16) & 255, (value >> 24) & 255) def load_font(font_size: float) -> ImageFont.ImageFont: for font_path in [ "/System/Library/Fonts/PingFang.ttc", "/System/Library/Fonts/Supplemental/Arial Unicode.ttf", "/System/Library/Fonts/Supplemental/Arial.ttf", ]: path = Path(font_path) if path.exists(): return ImageFont.truetype(str(path), int(round(font_size))) return ImageFont.load_default() 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 ui_rect(output_size: tuple[int, int], world_x: float, world_y: float, node_width: float, node_height: float, anchor_x: float, anchor_y: float) -> tuple[float, float, float, float]: width, height = output_size screen_x = width / 2 + world_x - anchor_x * node_width screen_y = height / 2 - world_y - (1 - anchor_y) * node_height return screen_x, screen_y, screen_x + node_width, screen_y + node_height def draw_ui_sprite( output: Image.Image, source_path: str, world_x: float, world_y: float, node_width: float, node_height: float, anchor_x: float, anchor_y: float, ) -> dict: with Image.open(source_path) as source_image: image = source_image.convert("RGBA").resize( (int(round(node_width)), int(round(node_height))), Image.Resampling.LANCZOS, ) left, top, _, _ = ui_rect(output.size, world_x, world_y, node_width, node_height, anchor_x, anchor_y) output.alpha_composite(image, (int(round(left)), int(round(top)))) return { "worldX": world_x, "worldY": world_y, "width": node_width, "height": node_height, "anchorX": anchor_x, "anchorY": anchor_y, "screenX": int(round(left)), "screenY": int(round(top)), } def draw_ui_label(output: Image.Image, source: dict, text: str) -> dict: label = source["label"] size = source["size"] anchor = source.get("anchor", {"x": 0.5, "y": 0.5}) world = source.get("worldPosition") if not world: raise ValueError(f"missing worldPosition for label {source['path']}") left, top, right, bottom = ui_rect( output.size, float(world["x"]), float(world["y"]), float(size["x"]), float(size["y"]), float(anchor["x"]), float(anchor["y"]), ) draw = ImageDraw.Draw(output) font = load_font(float(label["fontSize"])) bbox = draw.textbbox((0, 0), text, font=font) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] horizontal_align = int(label.get("horizontalAlign", 1)) if horizontal_align == 0: x = left elif horizontal_align == 2: x = right - text_width else: x = left + ((right - left) - text_width) / 2 y = top + ((bottom - top) - text_height) / 2 draw.text((int(round(x)), int(round(y))), text, font=font, fill=uint32_to_rgba(int(label["colorUint32"]))) return { "worldX": float(world["x"]), "worldY": float(world["y"]), "width": float(size["x"]), "height": float(size["y"]), "screenX": int(round(left)), "screenY": int(round(top)), "text": text, } def main() -> int: evidence = load_json(SOURCE_JSON) land_evidence = load_json(LAND_SOURCE_JSON) top_hud_evidence = load_json(TOP_HUD_SOURCE_JSON) width = int(evidence["canvas"]["width"]) height = int(evidence["canvas"]["height"]) camera = evidence["camera"] nodes = flatten_nodes(evidence["nodes"]) top_hud_nodes = flatten_nodes(top_hud_evidence["nodes"]) assets = {item["id"]: item for item in [*evidence["assets"], *land_evidence["assets"], *top_hud_evidence["assets"]]} output = Image.new("RGBA", (width, height), (0, 0, 0, 0)) rendered = [] for path in [ "root/scene/farm_scene_v3/Scene/BgLayer/defaultbg", "root/scene/farm_scene_v3/Scene/FgLayer/default", ]: node = nodes[path] asset = assets[node["spriteAssetId"]] world_x, world_y = world_position(path, nodes) node_width = float(node["size"]["x"]) node_height = float(node["size"]["y"]) result = draw_source_sprite( output, asset["sourcePath"], world_x, world_y, node_width, node_height, float(node["pivot"]["x"]), float(node["pivot"]["y"]), camera, ) rendered.append( { "path": path, "assetId": node["spriteAssetId"], **result, } ) for land in sorted(land_evidence["visibleLands"], key=lambda item: item["cell"]["sourceIndex"]): icon = land["icon"] asset_id = land["mockRender"]["assetId"] icon_asset_id = "land_locked" if asset_id == "land_extend" else asset_id asset = assets[icon_asset_id] result = draw_source_sprite( output, asset["sourcePath"], float(icon["worldPosition"]["x"]), float(icon["worldPosition"]["y"]), float(icon["size"]["x"]), float(icon["size"]["y"]), float(icon["anchor"]["x"]), float(icon["anchor"]["y"]), camera, ) rendered.append( { "path": icon["path"], "gridName": land["gridName"], "landId": land["landId"], "state": land["mockRender"]["state"], "assetId": icon_asset_id, "mockAssetId": asset_id, **result, } ) if asset_id == "land_extend": board_asset = assets["land_extend"] board_width = float(board_asset["pixelSize"]["width"]) board_height = float(board_asset["pixelSize"]["height"]) board_result = draw_source_sprite( output, board_asset["sourcePath"], float(icon["worldPosition"]["x"]), float(icon["worldPosition"]["y"]), board_width, board_height, float(board_asset["pivot"]["x"]), float(board_asset["pivot"]["y"]), camera, ) rendered.append( { "path": f"{icon['path']}/expand_board", "gridName": land["gridName"], "landId": land["landId"], "state": land["mockRender"]["state"], "assetId": "land_extend", **board_result, } ) for source in top_hud_evidence["nodes"]: if not source.get("spriteAssetId") or not source.get("size"): continue asset = assets[source["spriteAssetId"]] world = source.get("worldPosition") if world: world_x = float(world["x"]) world_y = float(world["y"]) else: world_x, world_y = world_position(source["path"], top_hud_nodes) anchor = source.get("anchor", {"x": 0.5, "y": 0.5}) result = draw_ui_sprite( output, asset["sourcePath"], world_x, world_y, float(source["size"]["x"]), float(source["size"]["y"]), float(anchor["x"]), float(anchor["y"]), ) rendered.append( { "path": source["path"], "assetId": source["spriteAssetId"], "uiLayer": True, **result, } ) for source in top_hud_evidence["nodes"]: if not source.get("label"): continue result = draw_ui_label(output, source, top_hud_mock_label(top_hud_evidence, source)) rendered.append( { "path": source["path"], "uiLayer": True, "label": True, **result, } ) OUT_DIR.mkdir(parents=True, exist_ok=True) output.save(PREVIEW_PATH) alpha = output.getchannel("A") non_transparent = sum(1 for value in alpha.getdata() if value) report = { "status": "pass", "previewPath": str(PREVIEW_PATH), "width": width, "height": height, "camera": { "sourcePosition": camera["sourcePosition"], "orthographicHeight": camera["orthographicHeight"], "defaultZoom": 1.0, }, "renderedNodeCount": len(rendered), "visibleLandCount": len(land_evidence["visibleLands"]), "topHudNodeCount": len(top_hud_evidence["nodes"]), "topHudMockUser": top_hud_evidence["mockUser"], "excludedStaticGridCount": land_evidence["excludedStaticGrid"]["visualLandCellCount"], "nonTransparentPixels": non_transparent, "renderedNodes": rendered, } 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 __name__ == "__main__": raise SystemExit(main())