1420 lines
57 KiB
Python
1420 lines
57 KiB
Python
#!/usr/bin/env python3
|
|
"""Capture the generated Cocos Creator web-mobile runtime through Chrome CDP.
|
|
|
|
The script validates the actual Cocos runtime scene before taking the screenshot.
|
|
It intentionally does not read reference screenshots or infer layout values.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import json
|
|
import shutil
|
|
import socket
|
|
import subprocess
|
|
import tempfile
|
|
import time
|
|
import urllib.request
|
|
from io import BytesIO
|
|
from pathlib import Path
|
|
|
|
from PIL import Image
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
TOP_HUD_SOURCE_JSON = ROOT / "source_export/stage1_top_hud_source.json"
|
|
DEFAULT_URL = "http://127.0.0.1:8787/index.html"
|
|
DEFAULT_OUT = ROOT / "artifacts/creator_capture/stage1_creator_capture.png"
|
|
DEFAULT_REPORT = ROOT / "artifacts/creator_capture/stage1_creator_capture_report.json"
|
|
EDGE = Path("/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge")
|
|
CHROME = Path("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome")
|
|
|
|
|
|
def browser_path() -> Path:
|
|
if CHROME.exists():
|
|
return CHROME
|
|
if EDGE.exists():
|
|
return EDGE
|
|
found = shutil.which("Google Chrome") or shutil.which("Microsoft Edge") or shutil.which("chrome")
|
|
if found:
|
|
return Path(found)
|
|
raise SystemExit("Could not find Chrome or Edge for headless Creator capture.")
|
|
|
|
|
|
def find_free_port() -> int:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
sock.bind(("127.0.0.1", 0))
|
|
return int(sock.getsockname()[1])
|
|
|
|
|
|
class CdpClient:
|
|
def __init__(self, ws_url: str) -> None:
|
|
try:
|
|
import websocket # type: ignore
|
|
except ImportError as exc: # pragma: no cover - depends on local env.
|
|
raise SystemExit(
|
|
"Missing websocket-client package; install or expose it before runtime capture."
|
|
) from exc
|
|
|
|
self.ws = websocket.create_connection(ws_url, timeout=30)
|
|
self.next_id = 1
|
|
self.events: list[dict] = []
|
|
|
|
def close(self) -> None:
|
|
self.ws.close()
|
|
|
|
def call(self, method: str, params: dict | None = None, timeout: float = 30.0) -> dict:
|
|
msg_id = self.next_id
|
|
self.next_id += 1
|
|
self.ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
|
|
deadline = time.time() + timeout
|
|
while time.time() < deadline:
|
|
raw = self.ws.recv()
|
|
data = json.loads(raw)
|
|
if data.get("id") == msg_id:
|
|
if "error" in data:
|
|
raise RuntimeError(f"CDP {method} failed: {data['error']}")
|
|
return data.get("result", {})
|
|
self.events.append(data)
|
|
raise TimeoutError(f"Timed out waiting for CDP response: {method}")
|
|
|
|
|
|
def wait_for_target_ws(port: int) -> str:
|
|
url = f"http://127.0.0.1:{port}/json/list"
|
|
deadline = time.time() + 20
|
|
last_error: Exception | None = None
|
|
while time.time() < deadline:
|
|
try:
|
|
with urllib.request.urlopen(url, timeout=1) as response:
|
|
targets = json.loads(response.read().decode("utf-8"))
|
|
for target in targets:
|
|
if target.get("type") == "page" and target.get("webSocketDebuggerUrl"):
|
|
return str(target["webSocketDebuggerUrl"])
|
|
except Exception as exc: # pragma: no cover - diagnostic fallback.
|
|
last_error = exc
|
|
time.sleep(0.2)
|
|
raise RuntimeError(f"CDP page target was not ready: {last_error}")
|
|
|
|
|
|
def extract_eval_value(result: dict) -> dict:
|
|
value = result.get("result", {}).get("value")
|
|
if not isinstance(value, dict):
|
|
raise RuntimeError(f"Unexpected runtime evaluation result: {result}")
|
|
return value
|
|
|
|
|
|
def canvas_png_expression() -> str:
|
|
return """
|
|
(async () => {
|
|
const canvas = document.getElementById('GameCanvas') || document.querySelector('canvas');
|
|
if (!canvas) {
|
|
return { ok: false, reason: 'missing_canvas' };
|
|
}
|
|
try {
|
|
if (window.cc) {
|
|
if (cc.game && typeof cc.game.resume === 'function') {
|
|
cc.game.resume();
|
|
}
|
|
if (cc.director && typeof cc.director.resume === 'function') {
|
|
cc.director.resume();
|
|
}
|
|
}
|
|
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
} catch (error) {
|
|
return { ok: false, reason: 'frame_wait_failed', error: String(error) };
|
|
}
|
|
try {
|
|
return {
|
|
ok: true,
|
|
width: canvas.width,
|
|
height: canvas.height,
|
|
clientWidth: canvas.clientWidth,
|
|
clientHeight: canvas.clientHeight,
|
|
dataUrl: canvas.toDataURL('image/png')
|
|
};
|
|
} catch (error) {
|
|
return { ok: false, reason: 'to_data_url_failed', error: String(error) };
|
|
}
|
|
})()
|
|
"""
|
|
|
|
|
|
def validate_rendered_pixels(image: Image.Image, label: str) -> dict:
|
|
sample = image.convert("RGB").resize((64, 64))
|
|
colors = sample.getcolors(maxcolors=4096)
|
|
color_count = None if colors is None else len(colors)
|
|
pixels = list(sample.getdata())
|
|
dominant_ratio = None
|
|
if colors:
|
|
dominant_ratio = max(count for count, _ in colors) / float(64 * 64)
|
|
mean_luma = sum((0.2126 * red + 0.7152 * green + 0.0722 * blue) for red, green, blue in pixels) / len(pixels)
|
|
if color_count is not None and color_count <= 12:
|
|
raise RuntimeError(
|
|
f"{label} has too few sampled colors for a rendered Cocos frame: "
|
|
f"colorCount={color_count}, dominantRatio={dominant_ratio:.4f}, meanLuma={mean_luma:.2f}"
|
|
)
|
|
if dominant_ratio is not None and dominant_ratio >= 0.85:
|
|
raise RuntimeError(
|
|
f"{label} is dominated by one sampled color: "
|
|
f"colorCount={color_count}, dominantRatio={dominant_ratio:.4f}, meanLuma={mean_luma:.2f}"
|
|
)
|
|
if mean_luma <= 8:
|
|
raise RuntimeError(
|
|
f"{label} is too dark for the recovered farm scene: "
|
|
f"colorCount={color_count}, dominantRatio={dominant_ratio}, meanLuma={mean_luma:.2f}"
|
|
)
|
|
return {"sampleColorCount": color_count, "dominantRatio": dominant_ratio, "meanLuma": mean_luma}
|
|
|
|
|
|
def validate_png_frame(data: bytes, expected_width: int, expected_height: int, label: str) -> dict:
|
|
with Image.open(BytesIO(data)) as image:
|
|
if image.size != (expected_width, expected_height):
|
|
raise RuntimeError(f"{label} size expected {(expected_width, expected_height)}, got {image.size}")
|
|
validation = validate_rendered_pixels(image, label)
|
|
return {"width": image.size[0], "height": image.size[1], **validation}
|
|
|
|
|
|
def normalize_page_png_frame(data: bytes, expected_width: int, expected_height: int, label: str) -> tuple[bytes, dict]:
|
|
with Image.open(BytesIO(data)) as image:
|
|
if image.size != (expected_width, expected_height):
|
|
raise RuntimeError(f"{label} size expected {(expected_width, expected_height)}, got {image.size}")
|
|
validation = validate_rendered_pixels(image, label)
|
|
return data, {"width": image.size[0], "height": image.size[1], **validation}
|
|
|
|
|
|
def decode_canvas_png(value: dict, expected_width: int, expected_height: int) -> tuple[bytes, dict]:
|
|
data_url = value.get("dataUrl")
|
|
if not isinstance(data_url, str) or not data_url.startswith("data:image/png;base64,"):
|
|
raise RuntimeError(f"Canvas capture did not return a PNG data URL: {value}")
|
|
data = base64.b64decode(data_url.split(",", 1)[1])
|
|
validation = validate_png_frame(data, expected_width, expected_height, "Canvas capture")
|
|
return data, validation
|
|
|
|
|
|
def capture_page_png(client: CdpClient, expected_width: int, expected_height: int, from_surface: bool) -> tuple[bytes, dict]:
|
|
screenshot = client.call(
|
|
"Page.captureScreenshot",
|
|
{
|
|
"format": "png",
|
|
"fromSurface": from_surface,
|
|
"clip": {"x": 0, "y": 0, "width": expected_width, "height": expected_height, "scale": 1},
|
|
},
|
|
timeout=60,
|
|
)
|
|
data = base64.b64decode(screenshot["data"])
|
|
return normalize_page_png_frame(
|
|
data,
|
|
expected_width,
|
|
expected_height,
|
|
f"Page.captureScreenshot(fromSurface={from_surface})",
|
|
)
|
|
|
|
|
|
def capture_current_frame(client: CdpClient, expected_width: int, expected_height: int) -> tuple[bytes, dict]:
|
|
try:
|
|
client.call("Page.bringToFront")
|
|
except Exception:
|
|
pass
|
|
canvas_capture = extract_eval_value(
|
|
client.call(
|
|
"Runtime.evaluate",
|
|
{"returnByValue": True, "awaitPromise": True, "expression": canvas_png_expression()},
|
|
timeout=60,
|
|
)
|
|
)
|
|
if canvas_capture.get("ok"):
|
|
try:
|
|
canvas_data, canvas_validation = decode_canvas_png(canvas_capture, expected_width, expected_height)
|
|
return canvas_data, {
|
|
"method": "canvas.toDataURL",
|
|
"canvas": {key: canvas_capture.get(key) for key in ("width", "height", "clientWidth", "clientHeight")},
|
|
"validation": canvas_validation,
|
|
}
|
|
except Exception as exc:
|
|
canvas_capture = {**canvas_capture, "decodeError": str(exc)}
|
|
|
|
page_attempts: list[dict] = []
|
|
for from_surface in (False, True):
|
|
try:
|
|
page_data, page_validation = capture_page_png(client, expected_width, expected_height, from_surface)
|
|
return page_data, {
|
|
"method": "Page.captureScreenshot",
|
|
"fromSurface": from_surface,
|
|
"validation": page_validation,
|
|
"canvasAttempt": {key: value for key, value in canvas_capture.items() if key != "dataUrl"},
|
|
"pageAttempts": page_attempts,
|
|
}
|
|
except Exception as exc:
|
|
page_attempts.append({"fromSurface": from_surface, "error": str(exc)})
|
|
|
|
raise RuntimeError(
|
|
"Could not capture a rendered Cocos frame: "
|
|
+ json.dumps(
|
|
{
|
|
"canvasAttempt": {key: value for key, value in canvas_capture.items() if key != "dataUrl"},
|
|
"pageAttempts": page_attempts,
|
|
},
|
|
ensure_ascii=False,
|
|
)
|
|
)
|
|
|
|
|
|
def dispatch_click(client: CdpClient, x: float, y: float) -> None:
|
|
touch_point = {"x": x, "y": y, "radiusX": 1, "radiusY": 1, "force": 1, "id": 1}
|
|
client.call("Input.dispatchTouchEvent", {"type": "touchStart", "touchPoints": [touch_point]})
|
|
client.call("Input.dispatchTouchEvent", {"type": "touchEnd", "touchPoints": []})
|
|
|
|
|
|
def js_string(value: str) -> str:
|
|
return json.dumps(value, ensure_ascii=False)
|
|
|
|
|
|
def load_json(path: Path) -> dict:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
def expected_top_hud_labels(top_hud_evidence: dict) -> dict[str, str]:
|
|
mock_user = top_hud_evidence["mockUser"]
|
|
return {
|
|
"txtName": str(mock_user["nickname"]),
|
|
"txtLevel": f" {mock_user['level']} ",
|
|
"txtExp": f"{mock_user['exp']}/{mock_user['expMax']}",
|
|
"txtGold": str(mock_user["goldDisplay"]),
|
|
"txtDiamond": str(mock_user["diamond"]),
|
|
}
|
|
|
|
|
|
def expected_top_hud_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 view_info_expression() -> str:
|
|
return """
|
|
function stage1ViewInfo() {
|
|
const view = window.cc && cc.view;
|
|
function size(value) {
|
|
return value ? { width: value.width, height: value.height } : null;
|
|
}
|
|
function rect(value) {
|
|
return value ? { x: value.x, y: value.y, width: value.width, height: value.height } : null;
|
|
}
|
|
let resolutionPolicy = null;
|
|
if (view && typeof view.getResolutionPolicy === 'function') {
|
|
const policy = view.getResolutionPolicy();
|
|
resolutionPolicy = policy ? {
|
|
name: policy.constructor && policy.constructor.name ? policy.constructor.name : null,
|
|
contentStrategy: policy._contentStrategy && policy._contentStrategy.constructor
|
|
? policy._contentStrategy.constructor.name
|
|
: null,
|
|
containerStrategy: policy._containerStrategy && policy._containerStrategy.constructor
|
|
? policy._containerStrategy.constructor.name
|
|
: null,
|
|
} : null;
|
|
}
|
|
return view ? {
|
|
designResolutionSize: typeof view.getDesignResolutionSize === 'function' ? size(view.getDesignResolutionSize()) : null,
|
|
visibleSize: typeof view.getVisibleSize === 'function' ? size(view.getVisibleSize()) : null,
|
|
frameSize: typeof view.getFrameSize === 'function' ? size(view.getFrameSize()) : null,
|
|
viewportRect: typeof view.getViewportRect === 'function' ? rect(view.getViewportRect()) : null,
|
|
scaleX: typeof view.getScaleX === 'function' ? view.getScaleX() : view._scaleX,
|
|
scaleY: typeof view.getScaleY === 'function' ? view.getScaleY() : view._scaleY,
|
|
devicePixelRatio: typeof view.getDevicePixelRatio === 'function' ? view.getDevicePixelRatio() : null,
|
|
resolutionPolicy,
|
|
} : null;
|
|
}
|
|
"""
|
|
|
|
|
|
def top_hud_state_expression() -> str:
|
|
return """
|
|
(() => {
|
|
const scene = window.cc && cc.director && cc.director.getScene && cc.director.getScene();
|
|
function find(root, name) {
|
|
if (!root) return null;
|
|
if (root.name === name) return root;
|
|
for (const child of root.children || []) {
|
|
const match = find(child, name);
|
|
if (match) return match;
|
|
}
|
|
return null;
|
|
}
|
|
function path(root, childPath) {
|
|
return childPath.split('/').reduce((current, part) => current ? current.getChildByName(part) : null, root);
|
|
}
|
|
const mainUi = find(scene, 'main_ui_v2');
|
|
const uiCameraNode = find(scene, 'uiCamera');
|
|
const uiCamera = uiCameraNode && uiCameraNode.getComponent(cc.Camera);
|
|
if (uiCamera && uiCamera.camera && uiCamera.camera.update) {
|
|
uiCamera.camera.update(true);
|
|
}
|
|
function label(childPath) {
|
|
const node = mainUi ? path(mainUi, childPath) : null;
|
|
const component = node && node.getComponent(cc.Label);
|
|
return component ? component.string : null;
|
|
}
|
|
function nodeInfo(childPath) {
|
|
const node = mainUi ? (childPath ? path(mainUi, childPath) : mainUi) : null;
|
|
if (!node) return null;
|
|
const ui = node.getComponent('cc.UITransform');
|
|
const sprite = node.getComponent(cc.Sprite);
|
|
const widget = node.getComponent(cc.Widget);
|
|
let bounds = null;
|
|
if (ui && uiCamera) {
|
|
const size = ui.contentSize;
|
|
const anchor = ui.anchorPoint;
|
|
const points = [
|
|
new cc.Vec3(-anchor.x * size.width, -anchor.y * size.height, 0),
|
|
new cc.Vec3((1 - anchor.x) * size.width, -anchor.y * size.height, 0),
|
|
new cc.Vec3(-anchor.x * size.width, (1 - anchor.y) * size.height, 0),
|
|
new cc.Vec3((1 - anchor.x) * size.width, (1 - anchor.y) * size.height, 0),
|
|
].map((point) => uiCamera.worldToScreen(ui.convertToWorldSpaceAR(point)));
|
|
const xs = points.map((point) => point.x);
|
|
const ys = points.map((point) => point.y);
|
|
bounds = {
|
|
screenCenter: { x: (Math.max(...xs) + Math.min(...xs)) / 2, y: (Math.max(...ys) + Math.min(...ys)) / 2 },
|
|
screenWidth: Math.max(...xs) - Math.min(...xs),
|
|
screenHeight: Math.max(...ys) - Math.min(...ys),
|
|
sourceSize: { width: size.width, height: size.height },
|
|
anchor: { x: anchor.x, y: anchor.y },
|
|
};
|
|
}
|
|
return {
|
|
name: node.name,
|
|
active: node.active,
|
|
activeInHierarchy: node.activeInHierarchy,
|
|
layer: node.layer,
|
|
localPosition: { x: node.position.x, y: node.position.y, z: node.position.z },
|
|
worldPosition: { x: node.worldPosition.x, y: node.worldPosition.y, z: node.worldPosition.z },
|
|
hasSprite: !!sprite,
|
|
uiTransform: ui ? {
|
|
width: ui.contentSize.width,
|
|
height: ui.contentSize.height,
|
|
anchor: { x: ui.anchorPoint.x, y: ui.anchorPoint.y },
|
|
} : null,
|
|
widget: widget ? {
|
|
alignFlags: widget.alignFlags,
|
|
alignMode: widget.alignMode,
|
|
left: widget.left,
|
|
right: widget.right,
|
|
top: widget.top,
|
|
bottom: widget.bottom,
|
|
} : null,
|
|
bounds,
|
|
};
|
|
}
|
|
const expNode = mainUi ? path(mainUi, 'HeadInfo/SubHeadInfo/expBar') : null;
|
|
const progress = expNode && expNode.getComponent('cc.ProgressBar');
|
|
const maskNode = mainUi ? path(mainUi, 'HeadInfo/SubHeadInfo/headMask') : null;
|
|
const mask = maskNode && maskNode.getComponent('cc.Mask');
|
|
return {
|
|
exists: !!mainUi,
|
|
active: !!(mainUi && mainUi.active),
|
|
layer: mainUi ? mainUi.layer : null,
|
|
labels: {
|
|
txtName: label('HeadInfo/SubHeadInfo/txtName'),
|
|
txtLevel: label('HeadInfo/SubHeadInfo/txtLevel'),
|
|
txtExp: label('HeadInfo/SubHeadInfo/txtExp'),
|
|
txtGold: label('Source/root/txtGold'),
|
|
txtDiamond: label('Source/root/txtDiamond'),
|
|
},
|
|
progress: progress ? { progress: progress.progress, totalLength: progress.totalLength } : null,
|
|
mask: mask ? { type: mask.type, segments: mask.segments } : null,
|
|
uiCamera: uiCamera ? {
|
|
nodeWorldPosition: {
|
|
x: uiCamera.node.worldPosition.x,
|
|
y: uiCamera.node.worldPosition.y,
|
|
z: uiCamera.node.worldPosition.z,
|
|
},
|
|
orthoHeight: uiCamera.orthoHeight,
|
|
internalOrthoHeight: uiCamera.camera ? uiCamera.camera.orthoHeight : null,
|
|
internalWindow: uiCamera.camera && uiCamera.camera.window ? {
|
|
width: uiCamera.camera.window.width,
|
|
height: uiCamera.camera.window.height,
|
|
} : null,
|
|
visibility: uiCamera.visibility,
|
|
} : null,
|
|
nodes: {
|
|
mainUi: nodeInfo(''),
|
|
headInfo: nodeInfo('HeadInfo'),
|
|
source: nodeInfo('Source'),
|
|
btnExit: nodeInfo('HeadInfo/btnExit'),
|
|
headDi: nodeInfo('HeadInfo/SubHeadInfo/head_di'),
|
|
expBar: nodeInfo('HeadInfo/SubHeadInfo/expBar'),
|
|
imgGold: nodeInfo('Source/root/img_gold'),
|
|
imgDiamond: nodeInfo('Source/root/img_diamond'),
|
|
}
|
|
};
|
|
})()
|
|
"""
|
|
|
|
|
|
def interaction_state_expression() -> str:
|
|
return """
|
|
(() => {
|
|
%s
|
|
const scene = window.cc && cc.director && cc.director.getScene && cc.director.getScene();
|
|
function find(root, name) {
|
|
if (!root) return null;
|
|
if (root.name === name) return root;
|
|
for (const child of root.children || []) {
|
|
const match = find(child, name);
|
|
if (match) return match;
|
|
}
|
|
return null;
|
|
}
|
|
function path(root, childPath) {
|
|
return childPath.split('/').reduce((current, part) => current ? current.getChildByName(part) : null, root);
|
|
}
|
|
const interaction = find(scene, 'plant_interactive_v2');
|
|
const cameraNode = find(scene, 'Camera');
|
|
const uiCameraNode = find(scene, 'uiCamera');
|
|
const camera = cameraNode && cameraNode.getComponent(cc.Camera);
|
|
const uiCamera = uiCameraNode && uiCameraNode.getComponent(cc.Camera);
|
|
const group = interaction ? path(interaction, 'followNode/seedGroup/group_5') : null;
|
|
function parentChain(node) {
|
|
const result = [];
|
|
let current = node;
|
|
while (current) {
|
|
result.push({ name: current.name, layer: current.layer });
|
|
current = current.parent;
|
|
}
|
|
return result;
|
|
}
|
|
function cameraInfo(node, component) {
|
|
if (!node || !component) return null;
|
|
return {
|
|
node: node.name,
|
|
layer: node.layer,
|
|
orthoHeight: component.orthoHeight,
|
|
near: component.near,
|
|
far: component.far,
|
|
visibility: component.visibility,
|
|
priority: component.priority,
|
|
clearFlags: component.clearFlags,
|
|
position: { x: node.position.x, y: node.position.y, z: node.position.z },
|
|
};
|
|
}
|
|
function cameraForNode(node) {
|
|
if (node && uiCamera && (uiCamera.visibility & node.layer)) return uiCamera;
|
|
if (node && camera && (camera.visibility & node.layer)) return camera;
|
|
return camera || uiCamera;
|
|
}
|
|
function projectedBounds(node) {
|
|
const projectCamera = cameraForNode(node);
|
|
if (!node || !projectCamera) return null;
|
|
const ui = node.getComponent('cc.UITransform');
|
|
if (!ui) return null;
|
|
const size = ui.contentSize;
|
|
const anchor = ui.anchorPoint;
|
|
const minX = -anchor.x * size.width;
|
|
const maxX = (1 - anchor.x) * size.width;
|
|
const minY = -anchor.y * size.height;
|
|
const maxY = (1 - anchor.y) * size.height;
|
|
const points = [
|
|
new cc.Vec3(minX, minY, 0),
|
|
new cc.Vec3(maxX, minY, 0),
|
|
new cc.Vec3(minX, maxY, 0),
|
|
new cc.Vec3(maxX, maxY, 0),
|
|
].map((point) => {
|
|
const world = ui.convertToWorldSpaceAR(point);
|
|
return projectCamera.worldToScreen(world);
|
|
});
|
|
const xs = points.map((point) => point.x);
|
|
const ys = points.map((point) => point.y);
|
|
return {
|
|
active: node.active,
|
|
activeInHierarchy: node.activeInHierarchy,
|
|
layer: node.layer,
|
|
localPosition: { x: node.position.x, y: node.position.y, z: node.position.z },
|
|
worldPosition: { x: node.worldPosition.x, y: node.worldPosition.y, z: node.worldPosition.z },
|
|
localScale: { x: node.scale.x, y: node.scale.y, z: node.scale.z },
|
|
worldScale: { x: node.worldScale.x, y: node.worldScale.y, z: node.worldScale.z },
|
|
camera: projectCamera === uiCamera ? 'uiCamera' : 'Camera',
|
|
sourceSize: { width: size.width, height: size.height },
|
|
screenWidth: Math.max(...xs) - Math.min(...xs),
|
|
screenHeight: Math.max(...ys) - Math.min(...ys),
|
|
screenCenter: {
|
|
x: (Math.max(...xs) + Math.min(...xs)) / 2,
|
|
y: (Math.max(...ys) + Math.min(...ys)) / 2,
|
|
},
|
|
};
|
|
}
|
|
const slots = [];
|
|
if (group) {
|
|
for (let index = 1; index <= 5; index += 1) {
|
|
const slot = group.getChildByName(`node${index}`);
|
|
const count = slot ? path(slot, 'count') : null;
|
|
const label = count && count.getComponent(cc.Label);
|
|
slots.push({
|
|
name: `node${index}`,
|
|
active: !!(slot && slot.active),
|
|
count: label ? label.string : null,
|
|
position: slot ? { x: slot.position.x, y: slot.position.y, z: slot.position.z } : null
|
|
});
|
|
}
|
|
}
|
|
const landBubble = interaction
|
|
? (path(interaction, 'followNode/detailNode/land') || path(interaction, 'land_detail/land'))
|
|
: null;
|
|
const seedBg3 = interaction ? path(interaction, 'followNode/seedGroup/bg_node3') : null;
|
|
const seedBg5 = interaction ? path(interaction, 'followNode/seedGroup/bg_node5') : null;
|
|
const selected = interaction ? path(interaction, 'landTarget/land_valid_selected') : null;
|
|
const followNode = interaction ? path(interaction, 'followNode') : null;
|
|
const landTarget = interaction ? path(interaction, 'landTarget') : null;
|
|
const detailNode = interaction ? path(interaction, 'followNode/detailNode') : null;
|
|
const selectedSprite = selected && selected.getComponent(cc.Sprite);
|
|
function nodePosition(node) {
|
|
return node ? {
|
|
active: node.active,
|
|
activeInHierarchy: node.activeInHierarchy,
|
|
localPosition: { x: node.position.x, y: node.position.y, z: node.position.z },
|
|
worldPosition: { x: node.worldPosition.x, y: node.worldPosition.y, z: node.worldPosition.z },
|
|
localScale: { x: node.scale.x, y: node.scale.y, z: node.scale.z },
|
|
worldScale: { x: node.worldScale.x, y: node.worldScale.y, z: node.worldScale.z }
|
|
} : null;
|
|
}
|
|
return {
|
|
interactionActive: !!(interaction && interaction.active),
|
|
interactionParentChain: interaction ? parentChain(interaction) : [],
|
|
interactionPosition: interaction ? { x: interaction.position.x, y: interaction.position.y, z: interaction.position.z } : null,
|
|
interactionLayer: interaction ? interaction.layer : null,
|
|
cameras: {
|
|
main: cameraInfo(cameraNode, camera),
|
|
ui: cameraInfo(uiCameraNode, uiCamera),
|
|
},
|
|
view: stage1ViewInfo(),
|
|
selectedLandVisual: selected ? {
|
|
active: selected.active,
|
|
position: { x: selected.position.x, y: selected.position.y, z: selected.position.z },
|
|
hasSprite: !!selectedSprite
|
|
} : null,
|
|
landBubbleActive: !!(landBubble && landBubble.active),
|
|
seedBackgrounds: {
|
|
bg_node3: seedBg3 ? { active: seedBg3.active, activeInHierarchy: seedBg3.activeInHierarchy } : null,
|
|
bg_node5: seedBg5 ? { active: seedBg5.active, activeInHierarchy: seedBg5.activeInHierarchy } : null,
|
|
},
|
|
targetNodes: {
|
|
interactionRoot: nodePosition(interaction),
|
|
followNode: nodePosition(followNode),
|
|
landTarget: nodePosition(landTarget),
|
|
detailNode: nodePosition(detailNode),
|
|
},
|
|
projectedBounds: {
|
|
landBubble: projectedBounds(landBubble),
|
|
seedGroup: projectedBounds(interaction ? path(interaction, 'followNode/seedGroup') : null),
|
|
seedBg3: projectedBounds(seedBg3),
|
|
seedBg5: projectedBounds(seedBg5),
|
|
selectedLandVisual: projectedBounds(selected)
|
|
},
|
|
seedSlots: slots
|
|
};
|
|
})()
|
|
""" % view_info_expression()
|
|
|
|
|
|
def decor_state_expression() -> str:
|
|
return """
|
|
(() => {
|
|
const scene = window.cc && cc.director && cc.director.getScene && cc.director.getScene();
|
|
function find(root, name) {
|
|
if (!root) return null;
|
|
if (root.name === name) return root;
|
|
for (const child of root.children || []) {
|
|
const match = find(child, name);
|
|
if (match) return match;
|
|
}
|
|
return null;
|
|
}
|
|
function path(root, childPath) {
|
|
return childPath.split('/').reduce((current, part) => current ? current.getChildByName(part) : null, root);
|
|
}
|
|
function parentChain(node) {
|
|
const result = [];
|
|
let current = node;
|
|
while (current) {
|
|
result.push({ name: current.name, layer: current.layer });
|
|
current = current.parent;
|
|
}
|
|
return result;
|
|
}
|
|
const cameraNode = find(scene, 'Camera');
|
|
const camera = cameraNode && cameraNode.getComponent(cc.Camera);
|
|
const root = find(scene, 'farm_scene_v3');
|
|
function skeletonInfo(node) {
|
|
if (!node) return null;
|
|
const skeleton = node.getComponent('sp.Skeleton');
|
|
const screen = camera ? camera.worldToScreen(node.worldPosition) : null;
|
|
let bounds = null;
|
|
if (skeleton) {
|
|
try {
|
|
if (typeof skeleton.getBoundingBox === 'function') {
|
|
const box = skeleton.getBoundingBox();
|
|
bounds = box ? { x: box.x, y: box.y, width: box.width, height: box.height } : null;
|
|
} else if (typeof skeleton.getRenderData === 'function') {
|
|
const renderData = skeleton.getRenderData();
|
|
bounds = renderData ? { hasRenderData: true } : null;
|
|
}
|
|
} catch (error) {
|
|
bounds = { error: String(error && error.message ? error.message : error) };
|
|
}
|
|
}
|
|
const protoNames = skeleton ? Object.getOwnPropertyNames(Object.getPrototypeOf(skeleton)).filter((name) => !name.startsWith('_')).slice(0, 80) : [];
|
|
let internalState = null;
|
|
if (skeleton) {
|
|
const drawList = skeleton.drawList || skeleton._drawList;
|
|
let vertexBounds = null;
|
|
const vBuffer = skeleton._vBuffer;
|
|
if (vBuffer && typeof skeleton._vLength === 'number' && skeleton._vLength > 0) {
|
|
const sample = [];
|
|
const xs = [];
|
|
const ys = [];
|
|
const stride = 8;
|
|
const limit = Math.min(skeleton._vLength, vBuffer.length || skeleton._vLength);
|
|
for (let index = 0; index + 1 < limit; index += stride) {
|
|
const x = Number(vBuffer[index]);
|
|
const y = Number(vBuffer[index + 1]);
|
|
if (Number.isFinite(x) && Number.isFinite(y)) {
|
|
xs.push(x);
|
|
ys.push(y);
|
|
if (sample.length < 8) sample.push({ x, y });
|
|
}
|
|
}
|
|
if (xs.length > 0) {
|
|
vertexBounds = {
|
|
stride,
|
|
vertexCount: xs.length,
|
|
sample,
|
|
minX: Math.min(...xs),
|
|
maxX: Math.max(...xs),
|
|
minY: Math.min(...ys),
|
|
maxY: Math.max(...ys),
|
|
};
|
|
}
|
|
}
|
|
function boneInfo(name) {
|
|
try {
|
|
const bone = skeleton.findBone(name);
|
|
if (!bone) return null;
|
|
const info = { found: true };
|
|
for (const key of ['x', 'y', 'worldX', 'worldY', 'rotation', 'worldRotationX', 'worldRotationY', 'scaleX', 'scaleY', 'a', 'b', 'c', 'd']) {
|
|
try {
|
|
const value = bone[key];
|
|
if (typeof value === 'number') info[key] = value;
|
|
} catch (_) {}
|
|
}
|
|
for (const method of ['getX', 'getY', 'getWorldX', 'getWorldY', 'getRotation', 'getScaleX', 'getScaleY']) {
|
|
try {
|
|
if (typeof bone[method] === 'function') info[method] = bone[method]();
|
|
} catch (_) {}
|
|
}
|
|
return info;
|
|
} catch (error) {
|
|
return { error: String(error && error.message ? error.message : error) };
|
|
}
|
|
}
|
|
function slotInfo(name) {
|
|
try {
|
|
const slot = skeleton.findSlot(name);
|
|
if (!slot) return null;
|
|
const attachment = slot.getAttachment ? slot.getAttachment() : slot.attachment;
|
|
const info = {
|
|
found: true,
|
|
attachmentName: attachment && attachment.getName ? attachment.getName() : attachment && attachment.name ? attachment.name : null,
|
|
attachmentType: attachment && attachment.constructor ? attachment.constructor.name : null,
|
|
};
|
|
const vertices = new Array(32).fill(0);
|
|
try {
|
|
if (attachment && typeof attachment.computeWorldVertices === 'function') {
|
|
attachment.computeWorldVertices(slot.bone, vertices, 0, 2);
|
|
const xs = [];
|
|
const ys = [];
|
|
for (let index = 0; index + 1 < vertices.length; index += 2) {
|
|
const x = Number(vertices[index]);
|
|
const y = Number(vertices[index + 1]);
|
|
if (Number.isFinite(x) && Number.isFinite(y) && (x !== 0 || y !== 0)) {
|
|
xs.push(x);
|
|
ys.push(y);
|
|
}
|
|
}
|
|
if (xs.length > 0) {
|
|
info.worldVertices = {
|
|
minX: Math.min(...xs),
|
|
maxX: Math.max(...xs),
|
|
minY: Math.min(...ys),
|
|
maxY: Math.max(...ys),
|
|
sample: vertices.slice(0, 12),
|
|
};
|
|
}
|
|
}
|
|
} catch (error) {
|
|
info.worldVerticesError = String(error && error.message ? error.message : error);
|
|
}
|
|
return info;
|
|
} catch (error) {
|
|
return { error: String(error && error.message ? error.message : error) };
|
|
}
|
|
}
|
|
internalState = {
|
|
ownKeys: Object.keys(skeleton).slice(0, 80),
|
|
animation: skeleton.animation,
|
|
skinName: skeleton._skinName || null,
|
|
animationName: skeleton._animationName || null,
|
|
hasRuntimeData: !!skeleton._runtimeData,
|
|
hasSkeletonObject: !!skeleton._skeleton,
|
|
hasInstance: !!skeleton._instance,
|
|
drawListLength: drawList && typeof drawList.length === 'number' ? drawList.length : null,
|
|
drawListArrayLength: drawList && drawList._array && typeof drawList._array.length === 'number' ? drawList._array.length : null,
|
|
vertexLength: skeleton._vLength || null,
|
|
indexLength: skeleton._iLength || null,
|
|
vertexBounds,
|
|
runtimeProtoNames: skeleton._skeleton ? Object.getOwnPropertyNames(Object.getPrototypeOf(skeleton._skeleton)).slice(0, 80) : [],
|
|
boneProbe: {
|
|
root: boneInfo('root'),
|
|
house: boneInfo('house'),
|
|
suofang: boneInfo('suofang'),
|
|
yanwu: boneInfo('yanwu')
|
|
},
|
|
slotProbe: {
|
|
xiaowu: slotInfo('xiaowu'),
|
|
gouwo2_new: slotInfo('gouwo2_new'),
|
|
gouwo3_new: slotInfo('gouwo3_new'),
|
|
goupen: slotInfo('goupen')
|
|
},
|
|
renderEntityEnabled: skeleton._renderEntity ? skeleton._renderEntity.enabled : null,
|
|
};
|
|
}
|
|
return {
|
|
active: node.active,
|
|
activeInHierarchy: node.activeInHierarchy,
|
|
layer: node.layer,
|
|
localPosition: { x: node.position.x, y: node.position.y, z: node.position.z },
|
|
worldPosition: { x: node.worldPosition.x, y: node.worldPosition.y, z: node.worldPosition.z },
|
|
screenPosition: screen ? { x: screen.x, y: screen.y, z: screen.z } : null,
|
|
parentChain: parentChain(node),
|
|
hasSkeleton: !!skeleton,
|
|
skeleton: skeleton ? {
|
|
enabled: skeleton.enabled,
|
|
defaultSkin: skeleton.defaultSkin,
|
|
defaultAnimation: skeleton.defaultAnimation,
|
|
loop: skeleton.loop,
|
|
hasSkeletonData: !!skeleton.skeletonData,
|
|
skeletonDataName: skeleton.skeletonData ? skeleton.skeletonData.name : null,
|
|
bounds,
|
|
protoNames,
|
|
internalState
|
|
} : null
|
|
};
|
|
}
|
|
const fgLayer = root ? path(root, 'Scene/FgLayer') : null;
|
|
return {
|
|
fgLayerChildOrder: fgLayer ? fgLayer.children.map((child) => child.name) : [],
|
|
farmSkin: skeletonInfo(root ? path(root, 'Scene/FgLayer/FarmSkin') : null),
|
|
layer2: skeletonInfo(root ? path(root, 'Scene/FgLayer/FarmSkin/Layer2') : null),
|
|
house: skeletonInfo(root ? path(root, 'Scene/FgLayer/FarmSkin/Layer2/house_201001') : null),
|
|
dogLayer: skeletonInfo(root ? path(root, 'Scene/FgLayer/DogLayer') : null),
|
|
dogHouse: skeletonInfo(root ? path(root, 'Scene/FgLayer/DogLayer/dogHouse') : null)
|
|
};
|
|
})()
|
|
"""
|
|
|
|
|
|
def click_position_expression(grid_name: str) -> str:
|
|
return f"""
|
|
(() => {{
|
|
const canvas = document.getElementById('GameCanvas') || document.querySelector('canvas');
|
|
const scene = window.cc && cc.director && cc.director.getScene && cc.director.getScene();
|
|
function find(root, name) {{
|
|
if (!root) return null;
|
|
if (root.name === name) return root;
|
|
for (const child of root.children || []) {{
|
|
const match = find(child, name);
|
|
if (match) return match;
|
|
}}
|
|
return null;
|
|
}}
|
|
const cameraNode = find(scene, 'Camera');
|
|
const camera = cameraNode && cameraNode.getComponent(cc.Camera);
|
|
const grid = find(scene, {js_string(grid_name)});
|
|
const icon = grid && grid.getChildByName('icon');
|
|
if (!canvas || !camera || !grid || !icon) {{
|
|
return {{ ok: false, gridName: {js_string(grid_name)}, reason: 'missing node or camera' }};
|
|
}}
|
|
const screen = camera.worldToScreen(icon.worldPosition);
|
|
const x = screen.x * (canvas.clientWidth / canvas.width);
|
|
const y = (canvas.height - screen.y) * (canvas.clientHeight / canvas.height);
|
|
return {{
|
|
ok: true,
|
|
gridName: {js_string(grid_name)},
|
|
screen: {{ x, y }},
|
|
world: {{ x: icon.worldPosition.x, y: icon.worldPosition.y, z: icon.worldPosition.z }},
|
|
gridPosition: {{ x: grid.position.x, y: grid.position.y, z: grid.position.z }},
|
|
iconPosition: {{ x: icon.position.x, y: icon.position.y, z: icon.position.z }}
|
|
}};
|
|
}})()
|
|
"""
|
|
|
|
|
|
def seed_click_position_expression(slot_name: str) -> str:
|
|
return f"""
|
|
(() => {{
|
|
const canvas = document.getElementById('GameCanvas') || document.querySelector('canvas');
|
|
const scene = window.cc && cc.director && cc.director.getScene && cc.director.getScene();
|
|
function find(root, name) {{
|
|
if (!root) return null;
|
|
if (root.name === name) return root;
|
|
for (const child of root.children || []) {{
|
|
const match = find(child, name);
|
|
if (match) return match;
|
|
}}
|
|
return null;
|
|
}}
|
|
function path(root, childPath) {{
|
|
return childPath.split('/').reduce((current, part) => current ? current.getChildByName(part) : null, root);
|
|
}}
|
|
function cameraFor(node) {{
|
|
const cameraNode = find(scene, 'Camera');
|
|
const uiCameraNode = find(scene, 'uiCamera');
|
|
const camera = cameraNode && cameraNode.getComponent(cc.Camera);
|
|
const uiCamera = uiCameraNode && uiCameraNode.getComponent(cc.Camera);
|
|
if (node && uiCamera && (uiCamera.visibility & node.layer)) return uiCamera;
|
|
if (node && camera && (camera.visibility & node.layer)) return camera;
|
|
return uiCamera || camera;
|
|
}}
|
|
const interaction = find(scene, 'plant_interactive_v2');
|
|
const slot = interaction ? path(interaction, 'followNode/seedGroup/group_5/' + {js_string(slot_name)}) : null;
|
|
const ui = slot && slot.getComponent('cc.UITransform');
|
|
const camera = slot && cameraFor(slot);
|
|
if (!canvas || !slot || !slot.activeInHierarchy || !ui || !camera) {{
|
|
return {{ ok: false, slotName: {js_string(slot_name)}, reason: 'missing active seed slot or camera' }};
|
|
}}
|
|
const center = camera.worldToScreen(slot.worldPosition);
|
|
const x = center.x * (canvas.clientWidth / canvas.width);
|
|
const y = (canvas.height - center.y) * (canvas.clientHeight / canvas.height);
|
|
return {{
|
|
ok: true,
|
|
slotName: {js_string(slot_name)},
|
|
screen: {{ x, y }},
|
|
world: {{ x: slot.worldPosition.x, y: slot.worldPosition.y, z: slot.worldPosition.z }},
|
|
sourceSize: {{ width: ui.contentSize.width, height: ui.contentSize.height }},
|
|
slotPosition: {{ x: slot.position.x, y: slot.position.y, z: slot.position.z }}
|
|
}};
|
|
}})()
|
|
"""
|
|
|
|
|
|
def crop_state_expression() -> str:
|
|
return """
|
|
(() => {
|
|
const scene = window.cc && cc.director && cc.director.getScene && cc.director.getScene();
|
|
function find(root, name) {
|
|
if (!root) return null;
|
|
if (root.name === name) return root;
|
|
for (const child of root.children || []) {
|
|
const match = find(child, name);
|
|
if (match) return match;
|
|
}
|
|
return null;
|
|
}
|
|
function path(root, childPath) {
|
|
return childPath.split('/').reduce((current, part) => current ? current.getChildByName(part) : null, root);
|
|
}
|
|
const gridOrigin = find(scene, 'GridOrigin');
|
|
const interaction = find(scene, 'plant_interactive_v2');
|
|
const landBubble = interaction ? (path(interaction, 'followNode/detailNode/land') || path(interaction, 'land_detail/land')) : null;
|
|
const cropInfo = interaction ? path(interaction, 'followNode/detailNode/mock_crop_info') : null;
|
|
const title = landBubble ? path(landBubble, 'title') : null;
|
|
const titleLabel = title && title.getComponent(cc.Label);
|
|
const cropInfoTitle = cropInfo ? path(cropInfo, 'title') : null;
|
|
const cropInfoTitleLabel = cropInfoTitle && cropInfoTitle.getComponent(cc.Label);
|
|
const cropInfoProgress = cropInfo ? path(cropInfo, 'progress_fill') : null;
|
|
const cropInfoProgressSprite = cropInfoProgress && cropInfoProgress.getComponent(cc.Sprite);
|
|
const cropInfoIcon = cropInfo ? path(cropInfo, 'icon') : null;
|
|
const cropInfoIconSprite = cropInfoIcon && cropInfoIcon.getComponent(cc.Sprite);
|
|
function nodeState(node) {
|
|
if (!node) return null;
|
|
const ui = node.getComponent('cc.UITransform');
|
|
return {
|
|
active: node.active,
|
|
activeInHierarchy: node.activeInHierarchy,
|
|
localPosition: { x: node.position.x, y: node.position.y, z: node.position.z },
|
|
worldPosition: { x: node.worldPosition.x, y: node.worldPosition.y, z: node.worldPosition.z },
|
|
sourceSize: ui ? { width: ui.contentSize.width, height: ui.contentSize.height } : null,
|
|
};
|
|
}
|
|
const group = interaction ? path(interaction, 'followNode/seedGroup/group_5') : null;
|
|
const crops = [];
|
|
if (gridOrigin) {
|
|
for (const grid of gridOrigin.children || []) {
|
|
const crop = grid.getChildByName('mock_crop');
|
|
if (!crop) continue;
|
|
const sprite = crop.getComponent(cc.Sprite);
|
|
const ui = crop.getComponent('cc.UITransform');
|
|
crops.push({
|
|
gridName: grid.name,
|
|
active: crop.active,
|
|
activeInHierarchy: crop.activeInHierarchy,
|
|
spriteFrameName: sprite && sprite.spriteFrame ? sprite.spriteFrame.name : null,
|
|
spriteEnabled: sprite ? sprite.enabled : null,
|
|
localPosition: { x: crop.position.x, y: crop.position.y, z: crop.position.z },
|
|
worldPosition: { x: crop.worldPosition.x, y: crop.worldPosition.y, z: crop.worldPosition.z },
|
|
sourceSize: ui ? { width: ui.contentSize.width, height: ui.contentSize.height } : null,
|
|
siblingIndex: grid.children.indexOf(crop),
|
|
gridChildren: grid.children.map((child) => child.name),
|
|
});
|
|
}
|
|
}
|
|
const seedSlots = [];
|
|
if (group) {
|
|
for (let index = 1; index <= 5; index += 1) {
|
|
const slot = group.getChildByName(`node${index}`);
|
|
const count = slot ? path(slot, 'count') : null;
|
|
const label = count && count.getComponent(cc.Label);
|
|
seedSlots.push({
|
|
name: `node${index}`,
|
|
active: !!(slot && slot.active),
|
|
activeInHierarchy: !!(slot && slot.activeInHierarchy),
|
|
count: label ? label.string : null,
|
|
});
|
|
}
|
|
}
|
|
return {
|
|
cropCount: crops.length,
|
|
crops,
|
|
interactionActive: !!(interaction && interaction.active),
|
|
seedGroupActive: !!(interaction && path(interaction, 'followNode/seedGroup') && path(interaction, 'followNode/seedGroup').active),
|
|
landBubbleTitle: titleLabel ? titleLabel.string : null,
|
|
cropInfo: {
|
|
node: nodeState(cropInfo),
|
|
title: cropInfoTitleLabel ? cropInfoTitleLabel.string : null,
|
|
progressFillRange: cropInfoProgressSprite ? cropInfoProgressSprite.fillRange : null,
|
|
iconSpriteFrameName: cropInfoIconSprite && cropInfoIconSprite.spriteFrame ? cropInfoIconSprite.spriteFrame.name : null,
|
|
stageDots: cropInfo ? cropInfo.children.filter((child) => /^stage_dot_/.test(child.name)).map((child) => ({
|
|
name: child.name,
|
|
active: child.active,
|
|
localPosition: { x: child.position.x, y: child.position.y, z: child.position.z },
|
|
})) : [],
|
|
},
|
|
seedSlots,
|
|
};
|
|
})()
|
|
"""
|
|
|
|
|
|
def hitbox_diagnostics_expression() -> str:
|
|
return """
|
|
(() => {
|
|
const canvas = document.getElementById('GameCanvas') || document.querySelector('canvas');
|
|
const scene = window.cc && cc.director && cc.director.getScene && cc.director.getScene();
|
|
function find(root, name) {
|
|
if (!root) return null;
|
|
if (root.name === name) return root;
|
|
for (const child of root.children || []) {
|
|
const match = find(child, name);
|
|
if (match) return match;
|
|
}
|
|
return null;
|
|
}
|
|
const cameraNode = find(scene, 'Camera');
|
|
const camera = cameraNode && cameraNode.getComponent(cc.Camera);
|
|
const gridOrigin = find(scene, 'GridOrigin');
|
|
function bounds(node) {
|
|
if (!node || !camera) return null;
|
|
const ui = node.getComponent('cc.UITransform');
|
|
if (!ui) return null;
|
|
const size = ui.contentSize;
|
|
const anchor = ui.anchorPoint;
|
|
const minX = -anchor.x * size.width;
|
|
const maxX = (1 - anchor.x) * size.width;
|
|
const minY = -anchor.y * size.height;
|
|
const maxY = (1 - anchor.y) * size.height;
|
|
const points = [
|
|
new cc.Vec3(minX, minY, 0),
|
|
new cc.Vec3(maxX, minY, 0),
|
|
new cc.Vec3(minX, maxY, 0),
|
|
new cc.Vec3(maxX, maxY, 0),
|
|
].map((point) => {
|
|
const world = ui.convertToWorldSpaceAR(point);
|
|
const screen = camera.worldToScreen(world);
|
|
return {
|
|
world: { x: world.x, y: world.y, z: world.z },
|
|
screen: { x: screen.x, y: screen.y },
|
|
input: canvas ? { x: screen.x * (canvas.clientWidth / canvas.width), y: (canvas.height - screen.y) * (canvas.clientHeight / canvas.height) } : null,
|
|
};
|
|
});
|
|
const xs = points.map((point) => point.world.x);
|
|
const ys = points.map((point) => point.world.y);
|
|
const inputXs = points.map((point) => point.input ? point.input.x : 0);
|
|
const inputYs = points.map((point) => point.input ? point.input.y : 0);
|
|
return {
|
|
sourceSize: { width: size.width, height: size.height },
|
|
anchor: { x: anchor.x, y: anchor.y },
|
|
localPosition: { x: node.position.x, y: node.position.y, z: node.position.z },
|
|
worldPosition: { x: node.worldPosition.x, y: node.worldPosition.y, z: node.worldPosition.z },
|
|
worldRect: { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) },
|
|
inputRect: canvas ? { minX: Math.min(...inputXs), maxX: Math.max(...inputXs), minY: Math.min(...inputYs), maxY: Math.max(...inputYs) } : null,
|
|
};
|
|
}
|
|
function containsWorld(rect, point) {
|
|
return !!rect && point.x >= rect.minX && point.x <= rect.maxX && point.y >= rect.minY && point.y <= rect.maxY;
|
|
}
|
|
const names = ['grid_0_5', 'grid_1_4', 'grid_2_4'];
|
|
const grids = names.map((name) => {
|
|
const grid = gridOrigin && gridOrigin.getChildByName(name);
|
|
const icon = grid && grid.getChildByName('icon');
|
|
return {
|
|
name,
|
|
siblingIndex: grid && gridOrigin ? gridOrigin.children.indexOf(grid) : -1,
|
|
gridBounds: bounds(grid),
|
|
iconBounds: bounds(icon),
|
|
};
|
|
});
|
|
const targetGrid = gridOrigin && gridOrigin.getChildByName('grid_0_5');
|
|
const targetIcon = targetGrid && targetGrid.getChildByName('icon');
|
|
const targetPoint = targetIcon ? { x: targetIcon.worldPosition.x, y: targetIcon.worldPosition.y, z: targetIcon.worldPosition.z } : null;
|
|
const parentHitCandidatesAtGrid05IconCenter = [];
|
|
if (gridOrigin && targetPoint) {
|
|
for (const grid of gridOrigin.children || []) {
|
|
const hitBounds = bounds(grid);
|
|
if (hitBounds && containsWorld(hitBounds.worldRect, targetPoint)) {
|
|
parentHitCandidatesAtGrid05IconCenter.push({
|
|
name: grid.name,
|
|
siblingIndex: gridOrigin.children.indexOf(grid),
|
|
worldRect: hitBounds.worldRect,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
targetPoint: targetPoint,
|
|
grids,
|
|
parentHitCandidatesAtGrid05IconCenter,
|
|
};
|
|
})()
|
|
"""
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--url", default=DEFAULT_URL)
|
|
parser.add_argument("--output", type=Path, default=DEFAULT_OUT)
|
|
parser.add_argument("--report", type=Path, default=DEFAULT_REPORT)
|
|
parser.add_argument("--width", type=int, default=720)
|
|
parser.add_argument("--height", type=int, default=1280)
|
|
parser.add_argument("--wait", type=float, default=12.0)
|
|
parser.add_argument("--click-grid", default="")
|
|
parser.add_argument("--locked-grid", default="")
|
|
parser.add_argument("--click-seed-slot", default="")
|
|
parser.add_argument("--after-seed-wait", type=float, default=0.8)
|
|
parser.add_argument("--crop-wait", type=float, action="append", default=[])
|
|
parser.add_argument("--blank-click", action="store_true", help="Click a fixed non-land sky point after the selectable click and record the hidden interaction state.")
|
|
parser.add_argument("--headed", action="store_true", help="Run a visible browser window for WebGL screenshot capture.")
|
|
args = parser.parse_args()
|
|
top_hud_evidence = load_json(TOP_HUD_SOURCE_JSON)
|
|
expected_labels = expected_top_hud_labels(top_hud_evidence)
|
|
expected_progress = expected_top_hud_progress(top_hud_evidence)
|
|
|
|
browser = browser_path()
|
|
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.report.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
port = find_free_port()
|
|
user_data = tempfile.TemporaryDirectory(prefix="farm-cdp-")
|
|
cmd = [
|
|
str(browser),
|
|
f"--remote-debugging-port={port}",
|
|
"--remote-allow-origins=*",
|
|
f"--user-data-dir={user_data.name}",
|
|
f"--window-size={args.width},{args.height}",
|
|
"--force-device-scale-factor=1",
|
|
"--no-first-run",
|
|
"--no-default-browser-check",
|
|
"--hide-scrollbars",
|
|
"--mute-audio",
|
|
"--enable-unsafe-swiftshader",
|
|
"--use-angle=swiftshader",
|
|
"--use-gl=angle",
|
|
"about:blank",
|
|
]
|
|
if not args.headed:
|
|
cmd.insert(1, "--headless=new")
|
|
|
|
proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, text=True)
|
|
client: CdpClient | None = None
|
|
try:
|
|
ws_url = wait_for_target_ws(port)
|
|
client = CdpClient(ws_url)
|
|
client.call("Page.enable")
|
|
client.call("Runtime.enable")
|
|
client.call("Log.enable")
|
|
client.call(
|
|
"Page.addScriptToEvaluateOnNewDocument",
|
|
{
|
|
"source": """
|
|
(() => {
|
|
const originalGetContext = HTMLCanvasElement.prototype.getContext;
|
|
HTMLCanvasElement.prototype.getContext = function patchedGetContext(type, attributes) {
|
|
if (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl') {
|
|
attributes = Object.assign({}, attributes || {}, { preserveDrawingBuffer: true });
|
|
}
|
|
return originalGetContext.call(this, type, attributes);
|
|
};
|
|
})();
|
|
"""
|
|
},
|
|
)
|
|
client.call(
|
|
"Emulation.setDeviceMetricsOverride",
|
|
{
|
|
"width": args.width,
|
|
"height": args.height,
|
|
"deviceScaleFactor": 1,
|
|
"mobile": True,
|
|
},
|
|
)
|
|
client.call("Emulation.setTouchEmulationEnabled", {"enabled": True, "maxTouchPoints": 1})
|
|
try:
|
|
client.call("Emulation.setVisibleSize", {"width": args.width, "height": args.height})
|
|
except Exception:
|
|
pass
|
|
client.call("Page.navigate", {"url": args.url})
|
|
time.sleep(args.wait)
|
|
|
|
state = extract_eval_value(
|
|
client.call(
|
|
"Runtime.evaluate",
|
|
{
|
|
"returnByValue": True,
|
|
"expression": """
|
|
(() => {
|
|
%s
|
|
const canvas = document.getElementById('GameCanvas') || document.querySelector('canvas');
|
|
const scene = window.cc && cc.director && cc.director.getScene && cc.director.getScene();
|
|
return {
|
|
readyState: document.readyState,
|
|
location: location.href,
|
|
viewport: { width: innerWidth, height: innerHeight, dpr: devicePixelRatio },
|
|
canvas: canvas ? { width: canvas.width, height: canvas.height, clientWidth: canvas.clientWidth, clientHeight: canvas.clientHeight } : null,
|
|
view: stage1ViewInfo(),
|
|
ccSettingsScreen: window._CCSettings ? window._CCSettings.screen : null,
|
|
hasCC: !!window.cc,
|
|
sceneName: scene ? scene.name : null,
|
|
childCount: scene ? scene.children.length : null
|
|
};
|
|
})()
|
|
""" % view_info_expression(),
|
|
},
|
|
)
|
|
)
|
|
|
|
if not state.get("hasCC") or state.get("sceneName") != "stage1_world_camera":
|
|
raise RuntimeError(f"Cocos runtime scene did not load correctly: {state}")
|
|
canvas = state.get("canvas") or {}
|
|
if int(canvas.get("width") or 0) != args.width or int(canvas.get("height") or 0) != args.height:
|
|
raise RuntimeError(f"Unexpected canvas size: {state}")
|
|
|
|
hitbox_diagnostics = extract_eval_value(
|
|
client.call(
|
|
"Runtime.evaluate",
|
|
{"returnByValue": True, "expression": hitbox_diagnostics_expression()},
|
|
)
|
|
)
|
|
decor_state = extract_eval_value(
|
|
client.call(
|
|
"Runtime.evaluate",
|
|
{"returnByValue": True, "expression": decor_state_expression()},
|
|
)
|
|
)
|
|
top_hud_state = extract_eval_value(
|
|
client.call(
|
|
"Runtime.evaluate",
|
|
{"returnByValue": True, "expression": top_hud_state_expression()},
|
|
)
|
|
)
|
|
if not top_hud_state.get("exists"):
|
|
raise RuntimeError(f"Top HUD did not exist in the runtime scene: {top_hud_state}")
|
|
for key, expected in expected_labels.items():
|
|
actual = (top_hud_state.get("labels") or {}).get(key)
|
|
if actual != expected:
|
|
raise RuntimeError(f"Top HUD label {key} expected {expected!r}, got {actual!r}: {top_hud_state}")
|
|
runtime_progress = top_hud_state.get("progress") or {}
|
|
if abs(float(runtime_progress.get("progress", -1)) - expected_progress) > 0.001:
|
|
raise RuntimeError(f"Top HUD progress expected {expected_progress}, got {runtime_progress}: {top_hud_state}")
|
|
|
|
client.call(
|
|
"Runtime.evaluate",
|
|
{
|
|
"returnByValue": True,
|
|
"expression": """
|
|
(() => {
|
|
if (window.cc && cc.profiler && cc.profiler.hideStats) {
|
|
cc.profiler.hideStats();
|
|
}
|
|
return true;
|
|
})()
|
|
""",
|
|
},
|
|
)
|
|
time.sleep(0.3)
|
|
|
|
interaction_checks: dict[str, dict] = {}
|
|
if args.locked_grid:
|
|
locked_position = extract_eval_value(
|
|
client.call(
|
|
"Runtime.evaluate",
|
|
{"returnByValue": True, "expression": click_position_expression(args.locked_grid)},
|
|
)
|
|
)
|
|
if not locked_position.get("ok"):
|
|
raise RuntimeError(f"Could not resolve locked grid click position: {locked_position}")
|
|
dispatch_click(client, locked_position["screen"]["x"], locked_position["screen"]["y"])
|
|
time.sleep(0.4)
|
|
interaction_checks["afterLockedGridClick"] = {
|
|
"clickPosition": locked_position,
|
|
"state": extract_eval_value(
|
|
client.call(
|
|
"Runtime.evaluate",
|
|
{"returnByValue": True, "expression": interaction_state_expression()},
|
|
)
|
|
),
|
|
}
|
|
if args.click_grid:
|
|
click_position = extract_eval_value(
|
|
client.call(
|
|
"Runtime.evaluate",
|
|
{"returnByValue": True, "expression": click_position_expression(args.click_grid)},
|
|
)
|
|
)
|
|
if not click_position.get("ok"):
|
|
raise RuntimeError(f"Could not resolve grid click position: {click_position}")
|
|
dispatch_click(client, click_position["screen"]["x"], click_position["screen"]["y"])
|
|
time.sleep(0.6)
|
|
interaction_checks["afterSelectableGridClick"] = {
|
|
"clickPosition": click_position,
|
|
"state": extract_eval_value(
|
|
client.call(
|
|
"Runtime.evaluate",
|
|
{"returnByValue": True, "expression": interaction_state_expression()},
|
|
)
|
|
),
|
|
}
|
|
if args.click_seed_slot:
|
|
seed_position = extract_eval_value(
|
|
client.call(
|
|
"Runtime.evaluate",
|
|
{"returnByValue": True, "expression": seed_click_position_expression(args.click_seed_slot)},
|
|
)
|
|
)
|
|
if not seed_position.get("ok"):
|
|
raise RuntimeError(f"Could not resolve seed click position: {seed_position}")
|
|
dispatch_click(client, seed_position["screen"]["x"], seed_position["screen"]["y"])
|
|
time.sleep(max(args.after_seed_wait, 0))
|
|
interaction_checks["afterSeedClick"] = {
|
|
"clickPosition": seed_position,
|
|
"interactionState": extract_eval_value(
|
|
client.call(
|
|
"Runtime.evaluate",
|
|
{"returnByValue": True, "expression": interaction_state_expression()},
|
|
)
|
|
),
|
|
"cropState": extract_eval_value(
|
|
client.call(
|
|
"Runtime.evaluate",
|
|
{"returnByValue": True, "expression": crop_state_expression()},
|
|
)
|
|
),
|
|
}
|
|
for wait_index, wait_seconds in enumerate(args.crop_wait, start=1):
|
|
time.sleep(max(wait_seconds, 0))
|
|
interaction_checks[f"afterCropWait{wait_index}"] = {
|
|
"waitSeconds": wait_seconds,
|
|
"cropState": extract_eval_value(
|
|
client.call(
|
|
"Runtime.evaluate",
|
|
{"returnByValue": True, "expression": crop_state_expression()},
|
|
)
|
|
),
|
|
}
|
|
if args.blank_click:
|
|
blank_position = {"screen": {"x": args.width * 0.5, "y": args.height * 0.18}}
|
|
dispatch_click(client, blank_position["screen"]["x"], blank_position["screen"]["y"])
|
|
time.sleep(0.4)
|
|
interaction_checks["afterBlankClick"] = {
|
|
"clickPosition": blank_position,
|
|
"state": extract_eval_value(
|
|
client.call(
|
|
"Runtime.evaluate",
|
|
{"returnByValue": True, "expression": interaction_state_expression()},
|
|
)
|
|
),
|
|
}
|
|
|
|
console = []
|
|
for event in client.events:
|
|
method = event.get("method")
|
|
if method in {"Runtime.consoleAPICalled", "Log.entryAdded"}:
|
|
console.append(event)
|
|
|
|
try:
|
|
screenshot_bytes, capture_info = capture_current_frame(client, args.width, args.height)
|
|
args.output.write_bytes(screenshot_bytes)
|
|
except Exception as exc:
|
|
report = {
|
|
"status": "needs_solution",
|
|
"url": args.url,
|
|
"output": str(args.output),
|
|
"browser": str(browser),
|
|
"viewport": {"width": args.width, "height": args.height},
|
|
"runtimeState": state,
|
|
"decorState": decor_state,
|
|
"topHudState": top_hud_state,
|
|
"hitboxDiagnostics": hitbox_diagnostics,
|
|
"interactionChecks": interaction_checks,
|
|
"captureError": str(exc),
|
|
"consoleEventCount": len(console),
|
|
"consoleEvents": console[-40:],
|
|
}
|
|
args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
raise
|
|
|
|
report = {
|
|
"status": "pass",
|
|
"url": args.url,
|
|
"output": str(args.output),
|
|
"browser": str(browser),
|
|
"viewport": {"width": args.width, "height": args.height},
|
|
"runtimeState": state,
|
|
"capture": capture_info,
|
|
"decorState": decor_state,
|
|
"topHudState": top_hud_state,
|
|
"hitboxDiagnostics": hitbox_diagnostics,
|
|
"interactionChecks": interaction_checks,
|
|
"consoleEventCount": len(console),
|
|
"consoleEvents": console[-20:],
|
|
}
|
|
args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
finally:
|
|
if client is not None:
|
|
client.close()
|
|
proc.terminate()
|
|
try:
|
|
proc.communicate(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
proc.kill()
|
|
try:
|
|
proc.communicate(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
pass
|
|
user_data.cleanup()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|