595 lines
25 KiB
Python
595 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
"""Extract source-backed evidence for empty-land click interaction.
|
|
|
|
This pass is intentionally narrow. It records only the original Cocos
|
|
`plant_interactive_v2` nodes and assets needed to show the selectable empty-land
|
|
state: land type detail bubble, selected-land image, and seed list. The selected
|
|
land visual must use the original `model/v3/land_valid_selected` SpriteFrame.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from PIL import Image
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SOURCE_ROOT = Path(
|
|
"/Users/hy/Documents/dev/qq_farm_godot_rebuild_20260616"
|
|
)
|
|
GAME_JS_PATH = SOURCE_ROOT / "source_raw" / "6A3BB7D3D4349E2050E7B701AAD51600_726750140f8084ebcbe5190fe9c198c9" / "game.js"
|
|
RAW_REMOTE = SOURCE_ROOT / "downloads" / "raw" / "remote"
|
|
CONVERTED_IMAGES = SOURCE_ROOT / "downloads" / "converted_png" / "images"
|
|
|
|
PREFAB_PATH = RAW_REMOTE / "extraRes" / "import" / "03" / "032791d54.4866b.json"
|
|
FARM_SCENE_PREFAB_PATH = RAW_REMOTE / "extraRes" / "import" / "0e" / "0ec3b3100.19268.json"
|
|
MAIN_SCENE_ELEMENTS_PATH = SOURCE_ROOT / "source_first" / "out" / "main_scene_elements.json"
|
|
LEVEL_PATH = RAW_REMOTE / "delayRes" / "import" / "d3" / "d3d0dbbe-6b4e-47de-8f1a-b73d8296662e.f3b53.json"
|
|
ITEM_PATH = RAW_REMOTE / "delayRes" / "import" / "54" / "54da07c3-8a3c-44fb-b50f-79af4a2ed78a.98c0b.json"
|
|
OUT_PATH = ROOT / "source_export" / "stage1_land_interaction_source.json"
|
|
REPORT_PATH = ROOT / "source_export" / "stage1_land_interaction_extract_report.json"
|
|
|
|
|
|
INTERACTION_ASSETS = {
|
|
"tuditanchuang2": {
|
|
"bundle": "extraRes",
|
|
"resource": "gui/texture/plantinteractive/tuditanchuang2/spriteFrame",
|
|
"image": "extraRes__gui__texture__plantinteractive__tuditanchuang2__a2f9575a-c951-4dc6-a0da-97069a896a25.bf2ea.png",
|
|
"spriteFrame": None,
|
|
},
|
|
"putongtudi": {
|
|
"bundle": "extraRes",
|
|
"resource": "gui/texture/plantinteractive/putongtudi/spriteFrame",
|
|
"image": "extraRes__gui__texture__plantinteractive__putongtudi__091dc5e6-0196-48a1-a392-44953ed50b8a.ad9dd.png",
|
|
"spriteFrame": None,
|
|
},
|
|
"daojudibu": {
|
|
"bundle": "extraRes",
|
|
"resource": "gui/texture/plantinteractive/daojudibu/spriteFrame",
|
|
"image": "extraRes__gui__texture__plantinteractive__daojudibu__bafc3fef-9b6d-4a0d-bcb2-05975ce113b7.c13a3.png",
|
|
"spriteFrame": None,
|
|
},
|
|
"BottomSeedList": {
|
|
"bundle": "extraRes",
|
|
"resource": "gui/texture/plantinteractive/BottomSeedList/spriteFrame",
|
|
"image": "extraRes__gui__texture__plantinteractive__BottomSeedList__9eca5499-3b17-4643-afce-67496298676c.84a85.png",
|
|
"spriteFrame": None,
|
|
},
|
|
"land_valid_selected": {
|
|
"bundle": "extraRes",
|
|
"resource": "model/v3/land_valid_selected/spriteFrame",
|
|
"image": "extraRes__model__v3__land_valid_selected__f5bde290-de64-4e6f-974f-81997d3386e0.7531f.png",
|
|
"spriteFrame": RAW_REMOTE / "extraRes" / "import" / "f5" / "f5bde290-de64-4e6f-974f-81997d3386e0@f9941.82d95.json",
|
|
},
|
|
"seed_bg_1": {
|
|
"bundle": "extraRes",
|
|
"resource": "gui/texture/plantinteractive/seed_bg_1/spriteFrame",
|
|
"image": "extraRes__gui__texture__plantinteractive__seed_bg_1__08156573-1e68-4c3e-a0e2-11cebc1f3e28.b735a.png",
|
|
"spriteFrame": None,
|
|
},
|
|
"zhongzishuliangdi": {
|
|
"bundle": "extraRes",
|
|
"resource": "gui/texture/plantinteractive/zhongzishuliangdi/spriteFrame",
|
|
"image": "extraRes__gui__texture__plantinteractive__zhongzishuliangdi__6d858cca-fd03-4fcd-8f47-c66525faf04a.828f5.png",
|
|
"spriteFrame": None,
|
|
},
|
|
"zhongzishuliangdi_line": {
|
|
"bundle": "extraRes",
|
|
"resource": "gui/texture/plantinteractive/zhongzishuliangdi_line/spriteFrame",
|
|
"image": "extraRes__gui__texture__plantinteractive__zhongzishuliangdi_line__1bdaf766-9b67-41f8-969b-81f72db6ab79.59eb5.png",
|
|
"spriteFrame": None,
|
|
},
|
|
"img_lock": {
|
|
"bundle": "extraRes",
|
|
"resource": "gui/texture/plantinteractive/img_lock/spriteFrame",
|
|
"image": "extraRes__gui__texture__plantinteractive__img_lock__25b722c9-e9c1-4177-b1d2-b88e1a7e99d6.b8790.png",
|
|
"spriteFrame": None,
|
|
},
|
|
"Crop_3_Seed": {
|
|
"bundle": "plant",
|
|
"resource": "model/v4/Crop_3_Seed/spriteFrame",
|
|
"image": "plant__model__v4__Crop_3_Seed__a823c190-2bd8-4b86-bbac-6c5cbf0c465d.20d67.png",
|
|
"spriteFrame": RAW_REMOTE / "plant" / "import" / "a8" / "a823c190-2bd8-4b86-bbac-6c5cbf0c465d@f9941.35a83.json",
|
|
},
|
|
"Crop_59_Seed": {
|
|
"bundle": "plant",
|
|
"resource": "model/v4/Crop_59_Seed/spriteFrame",
|
|
"image": "plant__model__v4__Crop_59_Seed__d4057a83-ecef-45fb-8988-905b56ae7ce3.0a012.png",
|
|
"spriteFrame": RAW_REMOTE / "plant" / "import" / "d4" / "d4057a83-ecef-45fb-8988-905b56ae7ce3@f9941.3a5e6.json",
|
|
},
|
|
}
|
|
|
|
|
|
def read_json(path: Path) -> Any:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
def write_json(path: Path, data: Any) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
|
|
|
|
def compact_sprite_frame(path: Path | None, image_path: Path) -> dict[str, Any]:
|
|
with Image.open(image_path) as image:
|
|
pixel_width, pixel_height = image.size
|
|
if path is None:
|
|
return {
|
|
"metadataPath": None,
|
|
"name": image_path.stem,
|
|
"rect": {"x": 0, "y": 0, "width": pixel_width, "height": pixel_height},
|
|
"offset": {"x": 0, "y": 0},
|
|
"originalSize": {"width": pixel_width, "height": pixel_height},
|
|
"rotated": False,
|
|
"capInsets": [0, 0, 0, 0],
|
|
"pivot": {"x": 0.5, "y": 0.5},
|
|
"pixelSize": {"width": pixel_width, "height": pixel_height},
|
|
}
|
|
raw = read_json(path)
|
|
frame = raw[5][0]
|
|
return {
|
|
"metadataPath": str(path),
|
|
"name": frame["name"],
|
|
"rect": frame["rect"],
|
|
"offset": frame["offset"],
|
|
"originalSize": frame["originalSize"],
|
|
"rotated": frame["rotated"],
|
|
"capInsets": frame.get("capInsets", [0, 0, 0, 0]),
|
|
"pivot": frame.get("pivot", {"x": 0.5, "y": 0.5}),
|
|
"pixelSize": {"width": pixel_width, "height": pixel_height},
|
|
}
|
|
|
|
|
|
def find_level_one(level_path: Path) -> dict[str, Any]:
|
|
raw = read_json(level_path)
|
|
level_rows = raw[5][0][2]
|
|
for row in level_rows:
|
|
if row.get("id") == 1:
|
|
return row
|
|
raise RuntimeError("Level config id=1 was not found.")
|
|
|
|
|
|
def assert_prefab_source(prefab: list[Any]) -> dict[str, Any]:
|
|
classes = prefab[3]
|
|
custom_class = classes[32]
|
|
expected_fields = [
|
|
"detailYOffset",
|
|
"stealAnimationNames",
|
|
"node",
|
|
"__prefab",
|
|
"seedInteractionNode",
|
|
"toolInteractionNode",
|
|
"followInteractionNode",
|
|
"plantPaginationNode",
|
|
"toolPaginationNode",
|
|
"btnGoShop",
|
|
"dragPreview",
|
|
"seed3BgNode",
|
|
"seed5BgNode",
|
|
"detailInteractionNode",
|
|
"interactNode",
|
|
"tipNode",
|
|
"tipLabel",
|
|
"guideNode",
|
|
"landTargetNode",
|
|
]
|
|
if custom_class[0] != "62225t3M0tNjKqXu9MAz7CW" or custom_class[1] != expected_fields:
|
|
raise RuntimeError("Unexpected plant_interactive_v2 custom component layout.")
|
|
|
|
prefab_text = json.dumps(prefab, ensure_ascii=False)
|
|
required_markers = [
|
|
"plant_interactive_v2",
|
|
"land_detail",
|
|
"followNode",
|
|
"seedGroup",
|
|
"group_5",
|
|
"bg_node3",
|
|
"bg_node5",
|
|
"plant_pagination",
|
|
"landTarget",
|
|
"普通土地",
|
|
"node1",
|
|
"node2",
|
|
"node3",
|
|
"node4",
|
|
"node5",
|
|
]
|
|
missing = [marker for marker in required_markers if marker not in prefab_text]
|
|
if missing:
|
|
raise RuntimeError(f"Prefab source is missing markers: {missing}")
|
|
|
|
return {
|
|
"componentClassIndex": 32,
|
|
"componentClassUuid": custom_class[0],
|
|
"componentFields": custom_class[1],
|
|
"packedPrefabInstanceIndex": 5,
|
|
"requiredMarkers": required_markers,
|
|
}
|
|
|
|
|
|
def decode_compact_instance(prefab: list[Any], value: Any) -> dict[str, Any] | None:
|
|
if not isinstance(value, list) or not value or not isinstance(value[0], int):
|
|
return None
|
|
template_id = value[0]
|
|
templates = prefab[4]
|
|
classes = prefab[3]
|
|
if template_id < 0 or template_id >= len(templates):
|
|
return None
|
|
template = templates[template_id]
|
|
if not template or not isinstance(template[0], int):
|
|
return None
|
|
class_index = template[0]
|
|
if class_index < 0 or class_index >= len(classes):
|
|
return None
|
|
class_def = classes[class_index]
|
|
fields = class_def[1] if len(class_def) > 1 and isinstance(class_def[1], list) else []
|
|
props: dict[str, Any] = {}
|
|
for prop_index, prop_value in zip(template[1:], value[1:]):
|
|
if isinstance(prop_index, int) and 0 <= prop_index < len(fields):
|
|
props[fields[prop_index]] = prop_value
|
|
return {"type": class_def[0], "template": template_id, "props": props}
|
|
|
|
|
|
def find_compact_node(prefab: list[Any], name: str, payload_index: int = 5) -> dict[str, Any]:
|
|
payload = prefab[5][payload_index]
|
|
|
|
def visit(value: Any) -> dict[str, Any] | None:
|
|
decoded = decode_compact_instance(prefab, value)
|
|
if decoded and decoded["type"] == "cc.Node" and decoded["props"].get("_name") == name:
|
|
return decoded
|
|
if isinstance(value, list):
|
|
for child in value:
|
|
match = visit(child)
|
|
if match:
|
|
return match
|
|
elif isinstance(value, dict):
|
|
for child in value.values():
|
|
match = visit(child)
|
|
if match:
|
|
return match
|
|
return None
|
|
|
|
node = visit(payload)
|
|
if not node:
|
|
raise RuntimeError(f"Source compact node was not found: {name}")
|
|
return node
|
|
|
|
|
|
def group_5_layout_evidence(prefab: list[Any]) -> dict[str, Any]:
|
|
group = find_compact_node(prefab, "group_5")
|
|
components = group["props"].get("_components", [])
|
|
decoded_components = [
|
|
item for item in (decode_compact_instance(prefab, component) for component in components) if item
|
|
]
|
|
layout = next((item for item in decoded_components if item["type"] == "cc.Layout"), None)
|
|
if not layout:
|
|
raise RuntimeError("group_5 cc.Layout component was not found in source prefab.")
|
|
props = layout["props"]
|
|
return {
|
|
"sourceNode": "plant_interactive_v2/followNode/seedGroup/group_5",
|
|
"componentType": "cc.Layout",
|
|
"template": layout["template"],
|
|
"layoutType": props.get("_layoutType", 0),
|
|
"resizeMode": props.get("_resizeMode", 0),
|
|
"spacingX": props.get("_spacingX", 0),
|
|
"paddingLeft": props.get("_paddingLeft", 0),
|
|
"paddingRight": props.get("_paddingRight", 0),
|
|
"paddingTop": props.get("_paddingTop", 0),
|
|
"paddingBottom": props.get("_paddingBottom", 0),
|
|
"horizontalDirection": props.get("_horizontalDirection", 0),
|
|
"affectedByScale": props.get("_affectedByScale", False),
|
|
"isAlign": props.get("_isAlign", False),
|
|
}
|
|
|
|
|
|
def element_by_path(elements: list[dict[str, Any]], path: str) -> dict[str, Any]:
|
|
for item in elements:
|
|
if item.get("full_path") == path:
|
|
return item
|
|
raise RuntimeError(f"Missing source element {path}")
|
|
|
|
|
|
def raw_color_rgba(value: int) -> dict[str, int]:
|
|
"""Cocos compact colors serialize as a uint32. Keep raw plus both byte orders."""
|
|
return {
|
|
"raw": value,
|
|
"littleEndianR": value & 0xFF,
|
|
"littleEndianG": (value >> 8) & 0xFF,
|
|
"littleEndianB": (value >> 16) & 0xFF,
|
|
"littleEndianA": (value >> 24) & 0xFF,
|
|
"argbA": (value >> 24) & 0xFF,
|
|
"argbR": (value >> 16) & 0xFF,
|
|
"argbG": (value >> 8) & 0xFF,
|
|
"argbB": value & 0xFF,
|
|
}
|
|
|
|
|
|
def farm_grid_graphics_evidence() -> dict[str, Any]:
|
|
prefab = read_json(FARM_SCENE_PREFAB_PATH)
|
|
classes = prefab[3]
|
|
custom_class = classes[12]
|
|
expected_fields = [
|
|
"gridX",
|
|
"gridY",
|
|
"gridMargin",
|
|
"parallelogramLength",
|
|
"parallelogramWidth",
|
|
"node",
|
|
"__prefab",
|
|
"gridOrigin",
|
|
"plantGridOrigin",
|
|
"prePlantNode",
|
|
"postPlantNode",
|
|
"gridBorderColor",
|
|
"gridBackgroundColor",
|
|
"bugLayer",
|
|
"grassLayer",
|
|
"goldBugLayer",
|
|
"strangeGrassLayer",
|
|
"extraPlantUnitLayer",
|
|
"landIcon",
|
|
"spineTemplate",
|
|
]
|
|
if custom_class[0] != "8db83TJTKhGzIl3ro4xvWky" or custom_class[1] != expected_fields:
|
|
raise RuntimeError("Unexpected farm_scene_v3 grid component layout.")
|
|
|
|
grid_component = prefab[5][1][0][1][3][1]
|
|
if grid_component[0] != 32:
|
|
raise RuntimeError("farm_scene_v3 grid component row was not found.")
|
|
|
|
main_elements = read_json(MAIN_SCENE_ELEMENTS_PATH)
|
|
elements = main_elements["prefabs"]["farm_scene_v3"]["elements"]
|
|
pre_plant = element_by_path(elements, "farm_scene_v3/PrePlant")
|
|
post_plant = element_by_path(elements, "farm_scene_v3/PostPlant")
|
|
|
|
return {
|
|
"sourcePrefab": str(FARM_SCENE_PREFAB_PATH),
|
|
"customComponentIndex": 12,
|
|
"customComponentUuid": custom_class[0],
|
|
"componentFields": custom_class[1],
|
|
"gridX": grid_component[1],
|
|
"gridY": grid_component[2],
|
|
"gridMargin": grid_component[3],
|
|
"parallelogramLength": grid_component[4],
|
|
"parallelogramWidth": grid_component[5],
|
|
"gridBorderColor": raw_color_rgba(grid_component[12][1]),
|
|
"gridBackgroundColor": raw_color_rgba(grid_component[13][1]),
|
|
"prePlantNode": {
|
|
"path": pre_plant["full_path"],
|
|
"localPosition": pre_plant["local_position"],
|
|
"componentCount": pre_plant["component_count"],
|
|
},
|
|
"postPlantNode": {
|
|
"path": post_plant["full_path"],
|
|
"localPosition": post_plant["local_position"],
|
|
"componentCount": post_plant["component_count"],
|
|
},
|
|
}
|
|
|
|
|
|
def node_record(
|
|
path: str,
|
|
name: str,
|
|
position: tuple[float, float, float],
|
|
size: tuple[float, float] | None = None,
|
|
active: bool = True,
|
|
sprite: str | None = None,
|
|
label: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"path": path,
|
|
"name": name,
|
|
"active": active,
|
|
"localPosition": {"x": position[0], "y": position[1], "z": position[2]},
|
|
"size": None if size is None else {"x": size[0], "y": size[1]},
|
|
"anchor": {"x": 0.5, "y": 0.5},
|
|
"spriteAssetId": sprite,
|
|
"label": label,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
prefab = read_json(PREFAB_PATH)
|
|
source_assertions = assert_prefab_source(prefab)
|
|
level_one = find_level_one(LEVEL_PATH)
|
|
|
|
assets: list[dict[str, Any]] = []
|
|
for asset_id, spec in INTERACTION_ASSETS.items():
|
|
image_path = CONVERTED_IMAGES / spec["image"]
|
|
if not image_path.exists():
|
|
raise RuntimeError(f"Missing converted interaction image: {image_path}")
|
|
sprite_frame = compact_sprite_frame(spec["spriteFrame"], image_path)
|
|
assets.append(
|
|
{
|
|
"id": asset_id,
|
|
"bundle": spec["bundle"],
|
|
"resource": spec["resource"],
|
|
"sourcePath": str(image_path),
|
|
"spriteFrame": sprite_frame,
|
|
}
|
|
)
|
|
|
|
source_nodes = [
|
|
node_record("plant_interactive_v2", "plant_interactive_v2", (0, 0, 0), active=False),
|
|
node_record("plant_interactive_v2/land_detail", "land_detail", (0, 0, 0), (500, 400)),
|
|
node_record("plant_interactive_v2/land_detail/land", "land", (-23.591, 59.798, 0), (214, 87), False, "tuditanchuang2"),
|
|
node_record(
|
|
"plant_interactive_v2/land_detail/land/title",
|
|
"title",
|
|
(29.507, 0.539, 0),
|
|
(79.9706, 27.72),
|
|
True,
|
|
None,
|
|
{
|
|
"string": "普通土地",
|
|
"fontSize": 20,
|
|
"actualFontSize": 20.4375,
|
|
"lineHeight": 30,
|
|
"bold": True,
|
|
"colorUint32": 4280960130,
|
|
},
|
|
),
|
|
node_record("plant_interactive_v2/land_detail/land/icon", "icon", (-67.839, 2.23, 0), (53, 29), True, "putongtudi"),
|
|
node_record("plant_interactive_v2/followNode", "followNode", (0, 0, 0), (500, 400)),
|
|
node_record("plant_interactive_v2/followNode/detailNode", "detailNode", (0, 38.652, 0)),
|
|
node_record("plant_interactive_v2/followNode/seedGroup", "seedGroup", (0, -124.664, 0), (685, 129)),
|
|
node_record("plant_interactive_v2/followNode/seedGroup/bg_node3", "bg_node3", (0, 0, 0), (416, 129), True, "daojudibu"),
|
|
node_record("plant_interactive_v2/followNode/seedGroup/bg_node5", "bg_node5", (0, 0, 0), (685, 129), False, "BottomSeedList"),
|
|
node_record("plant_interactive_v2/followNode/seedGroup/group_5", "group_5", (8.389, -0.108, 0), (575, 140)),
|
|
node_record("plant_interactive_v2/landTarget", "landTarget", (0, 0, 0)),
|
|
node_record("plant_interactive_v2/landTarget/land_valid_selected", "land_valid_selected", (0, 0, 0), (150, 89), True, "land_valid_selected"),
|
|
]
|
|
slot_nodes = []
|
|
for index, x in enumerate([-235, -120, -5, 110, 225], start=1):
|
|
path = f"plant_interactive_v2/followNode/seedGroup/group_5/node{index}"
|
|
slot_nodes.extend(
|
|
[
|
|
node_record(path, f"node{index}", (x, 0, 0), (105, 105), index <= 2),
|
|
node_record(f"{path}/seed_bg", "seed_bg", (0, 0, 0), (67, 67), index <= 2, "seed_bg_1"),
|
|
node_record(
|
|
f"{path}/icon",
|
|
"icon",
|
|
(0, 0, 0),
|
|
(100, 100),
|
|
index <= 2,
|
|
"Crop_3_Seed" if index == 1 else "Crop_59_Seed" if index == 2 else None,
|
|
),
|
|
node_record(f"{path}/bg_txt", "bg_txt", (-22.815, 32.233, 0), (55, 36), index <= 2, "zhongzishuliangdi"),
|
|
node_record(f"{path}/bg_txt/color_block", "color_block", (0, 1, 0), (55, 36), index <= 2, "zhongzishuliangdi_line"),
|
|
node_record(
|
|
f"{path}/count",
|
|
"count",
|
|
(-22.815, 32.233, 0),
|
|
(32, 25.2),
|
|
index <= 2,
|
|
None,
|
|
{
|
|
"string": "99",
|
|
"fontSize": 20,
|
|
"actualFontSize": 20,
|
|
"lineHeight": 25,
|
|
"bold": True,
|
|
"colorUint32": 4280494909,
|
|
},
|
|
),
|
|
]
|
|
)
|
|
source_nodes.extend(slot_nodes)
|
|
|
|
evidence = {
|
|
"meta": {
|
|
"module": "stage1_land_interaction",
|
|
"sourcePrefab": str(PREFAB_PATH),
|
|
"levelConfig": str(LEVEL_PATH),
|
|
"seedItemConfig": str(ITEM_PATH),
|
|
"extractionRule": "Record only empty-land interaction nodes and assets from original Cocos plant_interactive_v2 prefab and Level id=1 config.",
|
|
},
|
|
"sourceAssertions": source_assertions,
|
|
"componentEvidence": {
|
|
"customComponentUuid": source_assertions["componentClassUuid"],
|
|
"detailYOffset": 120,
|
|
"seedInteractionNode": "plant_interactive_v2/followNode/seedGroup",
|
|
"followInteractionNode": "plant_interactive_v2/followNode",
|
|
"plantPaginationNode": "plant_interactive_v2/followNode/seedGroup/plant_pagination",
|
|
"seed3BgNode": "plant_interactive_v2/followNode/seedGroup/bg_node3",
|
|
"seed5BgNode": "plant_interactive_v2/followNode/seedGroup/bg_node5",
|
|
"detailInteractionNode": "plant_interactive_v2/followNode/detailNode",
|
|
"interactNode": "plant_interactive_v2/InteractNodeParent",
|
|
"landTargetNode": "plant_interactive_v2/landTarget",
|
|
},
|
|
"runtimeClickEvidence": {
|
|
"sourceFile": str(GAME_JS_PATH),
|
|
"landIconEventOffset": 2517803,
|
|
"plantInteractiveReceiverOffset": 3251454,
|
|
"targetPositionMethodOffset": 3261813,
|
|
"landIconEventRule": "The source land icon touch handler emits a payload containing the land icon component, the source node position, the land id, and guide state. It clears the farm grid selection before the interaction event is consumed.",
|
|
"targetPositionRule": "The source plant interaction handler receives the clicked land position, converts it through worldToScreen, screenToWorld, and convertToNodeSpaceAR, then sets landTargetNode and followInteractionNode positions. The source root plant_interactive_v2 is not moved to the land.",
|
|
"wideSlotRule": "When the active seed/tool slot count is greater than three, the source keeps followInteractionNode.x at 0 so the seed list stays centered. Three or fewer slots follow the clicked land position.",
|
|
},
|
|
"farmGridGraphics": farm_grid_graphics_evidence(),
|
|
"selectedLandVisual": {
|
|
"targetNode": "plant_interactive_v2/landTarget",
|
|
"imageNode": "plant_interactive_v2/landTarget/land_valid_selected",
|
|
"assetId": "land_valid_selected",
|
|
"resource": "model/v3/land_valid_selected/spriteFrame",
|
|
"positionRule": "When an unlocked normal land is clicked, resolve the touched visual land by the source grid icon center, keep plant_interactive_v2 at its source root position, set landTargetNode to the converted land position, and mount the runtime selected frame at landTarget local (0,0). The source prefab serializes landTarget as a target node; land_valid_selected is a runtime visual recovered from model/v3/land_valid_selected/spriteFrame.",
|
|
"forbiddenAsset": "gui/texture/plantinteractive/gold is a 18x18 coin image and must not be used as the selected-land visual.",
|
|
},
|
|
"levelConfig": {
|
|
"level": int(level_one["id"]),
|
|
"levelName": level_one["level_name"],
|
|
"landRes": level_one["land_res"],
|
|
"landIconRes": level_one["land_icon_res"],
|
|
"landSpineRes": level_one["land_spine_res"],
|
|
"mergeAnim": level_one["merge_anim"],
|
|
},
|
|
"nodes": source_nodes,
|
|
"slotLayout": {
|
|
"slotSource": "plant_interactive_v2/followNode/seedGroup/group_5/node1..node5",
|
|
"slotPositions": [-235, -120, -5, 110, 225],
|
|
"slotSize": {"x": 105, "y": 105},
|
|
"layoutComponent": group_5_layout_evidence(prefab),
|
|
"backgroundRule": "bg_node3 is active in the source prefab and bg_node5 is inactive. Runtime should keep bg_node3 for three or fewer visible seed/tool slots and switch to bg_node5 only when more than three slots are visible.",
|
|
"twoSeedRuntimePositions": [-62.5, 52.5],
|
|
"twoSeedMockVisibleSlots": ["node1", "node2"],
|
|
},
|
|
"interactionStateRule": {
|
|
"selectableState": "unlocked_normal",
|
|
"noResponseStates": ["first_expandable", "locked_unplowed"],
|
|
"touchResolveRule": "Do not trust overlapping grid UITransform event targets. Resolve the intended land from the touch location against visible grid icon world centers, apply land state gating, then move only the source target nodes inside plant_interactive_v2.",
|
|
"runtimeMockOnly": True,
|
|
},
|
|
"mockSeeds": [
|
|
{
|
|
"seedItemId": 20003,
|
|
"cropConfigId": 1020003,
|
|
"name": "胡萝卜种子",
|
|
"assetId": "Crop_3_Seed",
|
|
"count": 99,
|
|
"bundle": "plant",
|
|
"resource": "model/v4/Crop_3_Seed/spriteFrame",
|
|
},
|
|
{
|
|
"seedItemId": 20059,
|
|
"cropConfigId": 1020059,
|
|
"name": "大白菜种子",
|
|
"assetId": "Crop_59_Seed",
|
|
"count": 99,
|
|
"bundle": "plant",
|
|
"resource": "model/v4/Crop_59_Seed/spriteFrame",
|
|
},
|
|
],
|
|
"assets": assets,
|
|
"verificationPlan": {
|
|
"creatorSceneNodes": [
|
|
"plant_interactive_v2",
|
|
"land_detail/land/title",
|
|
"followNode/detailNode",
|
|
"landTarget runtime selected frame",
|
|
"followNode/seedGroup/group_5/node1",
|
|
"followNode/seedGroup/group_5/node2",
|
|
],
|
|
"runtimeClick": "Click a land with state unlocked_normal and verify plant_interactive_v2.active=true; click locked_unplowed and verify selection remains unchanged.",
|
|
},
|
|
}
|
|
write_json(OUT_PATH, evidence)
|
|
write_json(
|
|
REPORT_PATH,
|
|
{
|
|
"status": "pass",
|
|
"module": evidence["meta"]["module"],
|
|
"sourceEvidence": str(OUT_PATH),
|
|
"assetCount": len(assets),
|
|
"nodeCount": len(source_nodes),
|
|
"mockSeedCount": len(evidence["mockSeeds"]),
|
|
},
|
|
)
|
|
print(json.dumps(read_json(REPORT_PATH), ensure_ascii=False, indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|