extends SceneTree const SeedBagViewScript := preload("res://scripts/ui/farm/seed_bag_view.gd") const PlantTipViewScript := preload("res://scripts/ui/farm/plant_tip_view.gd") const LandGridViewScript := preload("res://scripts/ui/farm/land_grid_view.gd") const LandActionMenuScript := preload("res://scripts/ui/farm/land_action_menu.gd") const FarmInteractionCoordinatorScript := preload("res://scripts/ui/farm/farm_interaction_coordinator.gd") const MainMenuControllerScript := preload("res://scripts/ui/menus/main_menu_controller.gd") const TextureRegionCatalogScript := preload("res://scripts/resources/texture_region_catalog.gd") const AtlasTextureCacheScript := preload("res://scripts/resources/atlas_texture_cache.gd") const TextureProviderScript := preload("res://scripts/ui/common/texture_provider.gd") const UiBuilderScript := preload("res://scripts/ui/common/ui_builder.gd") const UiMotionScript := preload("res://scripts/ui/common/ui_motion.gd") const I18nScript := preload("res://scripts/core/i18n.gd") const FriendPanelScript := preload("res://scripts/ui/panels/friend_panel.gd") const BuildingPanelScript := preload("res://scripts/ui/panels/building_panel.gd") const MarketPanelScript := preload("res://scripts/ui/panels/market_panel.gd") const PetPanelScript := preload("res://scripts/ui/panels/pet_panel.gd") const GameConfigCatalogScript := preload("res://scripts/core/game_config_catalog.gd") const GameScene := preload("res://scenes/ui/game_scene.tscn") const BagBgTexture := preload("res://assets/egret/common/bag_bg_png.tres") const BagLineTexture := preload("res://assets/egret/common/bag_line_png.tres") const PlantIconTexture := preload("res://assets/egret/plant/1031.png") const TipBgTexture := preload("res://assets/egret/common/log_itembg_png.tres") const PhaseClockTexture := preload("res://assets/egret/common/m_phase_clock_png.tres") const PhaseBarTexture := preload("res://assets/egret/common/m_phase_pro_bar_png.tres") const PhaseBarBgTexture := preload("res://assets/egret/common/m_phase_pro_bg_png.tres") const MainMenuHomeTexture := preload("res://assets/egret/menu2/menu2_m_home_png.tres") const MainMenuShopTexture := preload("res://assets/egret/menu2/menu2_m_shop_png.tres") const MainMenuWarehouseTexture := preload("res://assets/egret/menu2/menu2_m_warehouse_png.tres") const MainMenuFriendTexture := preload("res://assets/egret/menu2/menu2_m_rank_png.tres") const CommonAtlasPath := "res://assets/egret/common/common.png" const FriendAtlasPath := "res://assets/egret/assets/game/friend/friend.png" const RankAtlasPath := "res://assets/egret/assets/game/rank/rank.png" const BattlePetAtlasPath := "res://assets/egret/assets/game/battlePet/battle_pet.png" class FakeBattlePetApi: extends RefCounted var fetch_count := 0 func fetch_list(_token: String) -> Dictionary: fetch_count += 1 return { "ok": true, "body": { "data": { "list": [ { "id": 7, "pet_config_id": 11001, "is_enter_war": 1, "feed": 10, "level": 1, "blood": 100, "attack": 10, "anti": 5, "crit": 1, "dodge": 1, "hp": 100, "max_hp": 100, }, ], }, }, } var _failures: Array[String] = [] var _land_node: Control var _land_nodes: Dictionary = {} var _main_menu_action := "" var _seed_bag_press: Dictionary = {} var _land_action_menu_action := "" var _land_action_menu_dismissed := false var _persistent_land_data: Dictionary = {} func _init() -> void: _run.call_deferred() func _run() -> void: await process_frame await _verify_seed_bag() _verify_plant_tip() await _verify_persistent_plant_tip() _verify_friend_steal_overlay_removed() await _verify_land_action_menu_direct_hit() _verify_server_status_text() await _verify_main_menu_direct_hit() _verify_top_menu_single_row() _verify_pet_list_item_background_region() await _verify_ui_motion_patrol_loop() _verify_land_extend_gold_icon_constraints() _verify_welfare_removed() _verify_market_replaces_world_entry() _verify_market_panel_uses_fruit_rows() _verify_pet_health_defaults() _verify_battle_pet_shop_filters() _verify_battle_pet_patrol_rules() _verify_battle_pet_panel_hit_passthrough() await _verify_battle_pet_replaces_guard_display_data() _verify_friend_rank_row_alignment() await _verify_friend_visit_mode() if not _failures.is_empty(): for failure in _failures: push_error(failure) quit(1) return print("UI regression verification passed.") quit() func _verify_seed_bag() -> void: var owner := Control.new() owner.name = "SeedBagOwner" owner.size = Vector2(640.0, 960.0) get_root().add_child(owner) var seed_bag = SeedBagViewScript.new() seed_bag.setup({ "owner": owner, "style_label": Callable(self, "_style_label"), "get_item_icon_texture": Callable(self, "_get_item_icon_texture"), "seed_icon_id": Callable(self, "_seed_icon_id"), "textures": { "bg": BagBgTexture, "line": BagLineTexture, }, "on_item_pressed": Callable(self, "_record_seed_bag_press"), }) seed_bag.open("seed", [{"item_id": 101001, "icon_id": 1031, "num": 1}], []) await create_timer(0.25).timeout var panel := seed_bag.panel() _expect(panel != null, "seed bag panel should be created") if panel != null: _expect(_near(panel.position.x, 526.0), "seed bag x should match BagPanelSkin x=526") _expect(_near(panel.position.x + panel.size.x, 640.0), "seed bag right edge should be 640") _expect(panel.mouse_filter == Control.MOUSE_FILTER_STOP, "seed bag panel should block inside clicks from outside dismiss") var scroll := panel.get_node_or_null("SeedScroll") as ScrollContainer _expect(scroll != null, "seed bag scroll should exist") if scroll != null: _expect(scroll.horizontal_scroll_mode == ScrollContainer.SCROLL_MODE_DISABLED, "seed bag horizontal scrollbar should be disabled") _expect(scroll.vertical_scroll_mode == ScrollContainer.SCROLL_MODE_SHOW_NEVER, "seed bag vertical scrollbar should be hidden") var seed_hit := panel.get_node_or_null("SeedScroll/SeedList/BagItem101001/SeedHit") as Button _expect(seed_hit != null, "seed bag item hit should exist") if seed_hit != null: _seed_bag_press = {} seed_hit.pressed.emit() await process_frame _expect(int(_seed_bag_press.get("item_id", 0)) == 101001, "seed bag item hit should dispatch item id") _expect(str(_seed_bag_press.get("mode", "")) == "seed", "seed bag item hit should dispatch seed mode") var dismiss := owner.get_node_or_null("SeedBagDismissHit") as Button _expect(dismiss != null, "seed bag outside dismiss hit should exist") if dismiss != null and panel != null: _expect(dismiss.visible, "seed bag outside dismiss should be visible while bag is open") _expect(not dismiss.disabled, "seed bag outside dismiss should be enabled while bag is open") _expect(dismiss.z_index < panel.z_index, "seed bag outside dismiss should stay below bag panel") dismiss.pressed.emit() await process_frame _expect(not panel.visible, "pressing outside dismiss should close seed bag") _expect(not dismiss.visible and dismiss.disabled, "outside dismiss should disable after seed bag closes") owner.queue_free() func _verify_server_status_text() -> void: var scene := GameScene.instantiate() as Control var text := str(scene.call("_response_status_text", {"game_status": 1009}, "播种失败")) _expect(text == I18nScript.t("当前等级不足"), "server status 1009 should use original H5 level limit text") scene.queue_free() func _verify_plant_tip() -> void: var owner := Control.new() owner.name = "PlantTipOwner" owner.size = Vector2(640.0, 960.0) get_root().add_child(owner) _land_node = Control.new() _land_node.position = Vector2(220.0, 420.0) _land_node.size = Vector2(172.0, 102.0) _land_nodes = {0: _land_node} owner.add_child(_land_node) var plant_tip = PlantTipViewScript.new() plant_tip.setup({ "owner": owner, "game_config": GameConfigCatalogScript.new(), "get_land_node": Callable(self, "_get_land_node"), "style_label": Callable(self, "_style_label"), "format_duration": Callable(self, "_format_duration"), "phase_names": ["发芽期", "生长期", "成熟期", "成熟期"], "textures": { "bg": TipBgTexture, "clock": PhaseClockTexture, "bar": PhaseBarTexture, "bar_bg": PhaseBarBgTexture, }, }) plant_tip.show(0, { "crop_id": 1, "crop_phase": 0, "crop_start_time": int(Time.get_unix_time_from_system()) - 1, }) var tip := plant_tip.node() _expect(tip != null, "plant tip should be created") if tip != null: var bg := tip.get_node_or_null("Bg") as NinePatchRect var bar_bg := tip.get_node_or_null("BarBg") as NinePatchRect var bar_fill := tip.get_node_or_null("BarFill") as NinePatchRect _expect(bg != null, "plant tip bg should use NinePatchRect") _expect(bar_bg != null, "plant tip bar bg should use NinePatchRect") _expect(bar_fill != null, "plant tip bar fill should use NinePatchRect") if bg != null: _expect(bg.patch_margin_left == 24 and bg.patch_margin_top == 20 and bg.patch_margin_right == 25 and bg.patch_margin_bottom == 26, "plant tip bg margins should convert PlantCdTipSkin scale9Grid") _expect(_near(tip.size.x, 128.0), "plant tip width should be reduced for compact crop bubble") if bar_bg != null: _expect(bar_bg.patch_margin_left == 8 and bar_bg.patch_margin_top == 5 and bar_bg.patch_margin_right == 7 and bar_bg.patch_margin_bottom == 5, "plant tip bar bg margins should match CDBarSkin") if bar_fill != null: _expect(bar_fill.patch_margin_left == 8 and bar_fill.patch_margin_top == 5 and bar_fill.patch_margin_right == 7 and bar_fill.patch_margin_bottom == 6, "plant tip bar fill margins should match CDBarSkin") var count_label := tip.get_node_or_null("LabelCount") as Label _expect(count_label != null and count_label.text.find("/") >= 0, "plant tip should render current/total count line") plant_tip.show(0, { "crop_id": 1, "crop_phase": 3, "crop_start_time": int(Time.get_unix_time_from_system()) - 30, }) var time_label := tip.get_node_or_null("LabelTime") as Label _expect(time_label != null and not time_label.visible, "mature plant tip should hide time label") _expect(bar_bg != null and not bar_bg.visible, "mature plant tip should hide progress bar background") _expect(bar_fill != null and not bar_fill.visible, "mature plant tip should hide progress bar fill") _expect(count_label != null and count_label.text.begins_with("count:"), "mature plant tip should use localized count field") _expect(count_label != null and count_label.text == "count:30/30", "naturally mature plant tip should fallback to configured output count") plant_tip.show(0, { "crop_id": 1, "crop_phase": 4, "has_gather": 1, }) _expect(not plant_tip.is_visible(), "plant tip should be logically hidden after crop has been harvested") await create_timer(0.2).timeout _expect(not tip.visible, "plant tip should fade out after crop has been harvested") owner.queue_free() func _verify_persistent_plant_tip() -> void: var owner := Control.new() owner.name = "PersistentPlantTipOwner" owner.size = Vector2(640.0, 960.0) get_root().add_child(owner) _land_node = Control.new() _land_node.name = "Land0" _land_node.position = Vector2(220.0, 420.0) _land_node.size = Vector2(172.0, 102.0) _land_nodes = {0: _land_node} owner.add_child(_land_node) var second_land_node := Control.new() second_land_node.name = "Land1" second_land_node.position = Vector2(410.0, 500.0) second_land_node.size = Vector2(172.0, 102.0) _land_nodes[1] = second_land_node owner.add_child(second_land_node) var plant_tip = PlantTipViewScript.new() plant_tip.setup({ "owner": owner, "get_land_node": Callable(self, "_get_land_node"), "style_label": Callable(self, "_style_label"), "format_duration": Callable(self, "_format_duration"), "phase_names": ["发芽期", "生长期", "成熟期", "成熟期"], "textures": { "bg": TipBgTexture, "clock": PhaseClockTexture, "bar": PhaseBarTexture, "bar_bg": PhaseBarBgTexture, }, }) var land_menu = LandActionMenuScript.new() land_menu.setup({ "owner": owner, "get_land_node": Callable(self, "_get_land_node"), "on_action": Callable(self, "_record_land_action_menu_action"), "on_dismiss": Callable(self, "_record_land_action_menu_dismiss"), "button_size": Vector2(92.0, 88.0), }) var coordinator = FarmInteractionCoordinatorScript.new() coordinator.setup({ "owner": owner, "land_action_menu": land_menu, "plant_tip_view": plant_tip, "constants": { "land_count": 2, "plant_tip_breathe_seconds": 0.1, }, "callbacks": { "get_land_node": Callable(self, "_get_land_node"), "land_data": Callable(self, "_persistent_land_data_for"), "land_actions": Callable(self, "_persistent_land_actions"), "is_operation_in_progress": Callable(self, "_false"), "is_visiting_friend": Callable(self, "_false"), }, }) var growing_land := { "position": 0, "crop_id": 1, "crop_phase": 1, "crop_start_time": int(Time.get_unix_time_from_system()) - 1, "has_gather": 0, } var second_growing_land := growing_land.duplicate(true) second_growing_land["position"] = 1 _persistent_land_data = growing_land coordinator.tick({0: growing_land, 1: second_growing_land}) await process_frame _expect(plant_tip.is_visible(), "persistent plant tip should show above growing crop") _expect(owner.get_node_or_null("PlantCdTip0") != null and owner.get_node("PlantCdTip0").visible, "persistent plant tip should show for first crop") _expect(owner.get_node_or_null("PlantCdTip1") != null and owner.get_node("PlantCdTip1").visible, "persistent plant tip should show for second crop") await create_timer(0.25).timeout coordinator.tick({0: growing_land, 1: second_growing_land}) _expect(not plant_tip.is_visible(), "persistent plant tip should hide during breathe cycle") coordinator.on_land_pressed(0) await create_timer(0.25).timeout _expect(land_menu.is_visible(), "land action menu should open while plant tip breathe cycle is hidden") land_menu.hide() await create_timer(0.25).timeout coordinator.tick({0: growing_land, 1: second_growing_land}) _expect(plant_tip.is_visible(), "persistent plant tip should show again during breathe cycle") land_menu.show(0, ["uproot", "manure"]) await process_frame coordinator.tick({0: growing_land, 1: second_growing_land}) _expect(not plant_tip.is_visible(), "persistent plant tip should hide while land action menu is visible") land_menu.hide() coordinator.tick({0: growing_land, 1: second_growing_land}) await process_frame _expect(plant_tip.is_visible(), "persistent plant tip should restore after land action menu closes") var tip_node := plant_tip.node() var old_tip_position := tip_node.position if tip_node != null else Vector2.ZERO _land_node.position += Vector2(42.0, 58.0) coordinator.tick({0: growing_land, 1: second_growing_land}) await process_frame var moved_tip_position := tip_node.position if tip_node != null else Vector2.ZERO _expect(moved_tip_position.distance_to(old_tip_position) > 40.0, "persistent plant tip should follow moved land position") var harvested_land := growing_land.duplicate(true) harvested_land["has_gather"] = 1 harvested_land["crop_phase"] = 4 coordinator.tick({0: harvested_land}) _expect(not plant_tip.is_visible(), "persistent plant tip should hide when no crop remains visible") var empty_land := growing_land.duplicate(true) empty_land["crop_id"] = 0 empty_land["has_gather"] = 0 coordinator.tick({0: empty_land}) _expect(not plant_tip.is_visible(), "persistent plant tip should hide for empty land") owner.queue_free() func _verify_friend_steal_overlay_removed() -> void: var owner := Control.new() owner.name = "FriendStealOverlayOwner" owner.size = Vector2(640.0, 960.0) get_root().add_child(owner) var game_view := Control.new() game_view.name = "GameView" game_view.set_anchors_preset(Control.PRESET_FULL_RECT) owner.add_child(game_view) _land_node = TextureRect.new() _land_node.name = "Land0" _land_node.position = Vector2(240.0, 420.0) _land_node.size = Vector2(172.0, 102.0) game_view.add_child(_land_node) var image := Image.create(8, 8, false, Image.FORMAT_RGBA8) image.fill(Color.WHITE) var texture := ImageTexture.create_from_image(image) var land_grid = LandGridViewScript.new() land_grid.setup({ "owner": owner, "game_view": game_view, "get_land_node": Callable(self, "_get_land_node"), "land_count": 1, "textures": { "level_0": texture, "level_1": texture, "level_2": texture, "level_3": texture, "locked": texture, }, }) land_grid.render({0: { "position": 0, "level": 0, "crop_id": 1, "crop_phase": 3, "is_ripe": 1, "crop_num": 30, "crop_min_num": 15, "has_gather": 0, "interaction_type_list": [0], }}) _expect(game_view.get_node_or_null("StealIconLayer") == null, "friend visit should not render the extra steal overlay icon") owner.queue_free() func _verify_land_action_menu_direct_hit() -> void: var owner := Control.new() owner.name = "LandActionMenuOwner" owner.size = Vector2(640.0, 960.0) get_root().add_child(owner) _land_node = Control.new() _land_node.name = "Land0" _land_node.position = Vector2(340.0, 420.0) _land_node.size = Vector2(172.0, 102.0) owner.add_child(_land_node) var menu = LandActionMenuScript.new() menu.setup({ "owner": owner, "get_land_node": Callable(self, "_get_land_node"), "on_action": Callable(self, "_record_land_action_menu_action"), "on_dismiss": Callable(self, "_record_land_action_menu_dismiss"), "button_size": Vector2(92.0, 88.0), }) menu.show(0, ["uproot", "manure"]) await create_timer(0.25).timeout var layer := menu.layer() _expect(layer != null and layer.visible, "land action menu should be visible") if layer != null: var manure := layer.get_node_or_null("MenuManure") as TextureButton _expect(manure != null, "land action menu manure button should exist") if manure != null: _land_action_menu_action = "" _expect(menu.handle_direct_hit(manure.get_global_rect().get_center()), "land action menu direct hit should consume button click") _expect(_land_action_menu_action == "manure", "land action menu direct hit should dispatch manure before scene hits") menu.show(0, ["steal"]) await create_timer(0.25).timeout if layer != null: var steal := layer.get_node_or_null("MenuSteal") as TextureButton _expect(steal != null, "land action menu steal button should exist") if steal != null: _land_action_menu_action = "" _expect(menu.handle_direct_hit(steal.get_global_rect().get_center()), "land action menu direct hit should consume steal click") _expect(_land_action_menu_action == "steal", "land action menu direct hit should dispatch steal action") menu.show(0, ["uproot"]) await create_timer(0.25).timeout _land_action_menu_dismissed = false _expect(menu.handle_direct_hit(Vector2(20.0, 20.0)), "land action menu outside click should be consumed") _expect(_land_action_menu_dismissed, "land action menu outside click should dismiss menu") owner.queue_free() func _verify_welfare_removed() -> void: var scene := GameScene.instantiate() as Control _expect(scene.get_node_or_null("LeftMiddleMenu/Fuli") == null, "welfare menu button should be removed") _expect(scene.get_node_or_null("LeftMiddleMenu/Notice") != null, "notice menu should remain") _expect(scene.get_node_or_null("LeftMiddleMenu/Sign") != null, "sign menu should remain") _expect(scene.get_node_or_null("LeftMiddleMenu/RewardGift") != null, "online gift menu should remain") scene.queue_free() func _verify_market_replaces_world_entry() -> void: var scene := GameScene.instantiate() as Control var market_art := scene.get_node_or_null("LeftMenu/MarketButtonArt") as TextureRect _expect(market_art != null, "market menu button should replace world entry") if market_art != null: _expect(_near(market_art.size.x, 118.0) and _near(market_art.size.y, 118.0), "market menu icon should have fixed 118x118 visual size") _expect(scene.get_node_or_null("LeftMenu/WorldButtonArt") == null, "world menu button should be removed") scene.queue_free() func _verify_market_panel_uses_fruit_rows() -> void: var owner := Control.new() owner.name = "MarketPanelOwner" owner.size = Vector2(640.0, 960.0) get_root().add_child(owner) var atlas_cache = AtlasTextureCacheScript.new() var texture_provider = TextureProviderScript.new() texture_provider.setup(atlas_cache, {"common": CommonAtlasPath}) var ui_builder = UiBuilderScript.new() ui_builder.setup({ "owner": owner, "panel_screen_size": Vector2(640.0, 960.0), "texture_provider": texture_provider, "close_modal_callback": Callable(self, "_noop"), "get_item_icon_id": Callable(self, "_seed_icon_id"), }) var panel = MarketPanelScript.new() panel.setup({ "owner": owner, "ui_builder": ui_builder, "texture_provider": texture_provider, "game_config": GameConfigCatalogScript.new(), "callbacks": { "token": Callable(self, "_token"), "player_data": Callable(self, "_market_player_data"), "warehouse_items": Callable(self, "_market_warehouse_items"), "owned_amount": Callable(self, "_market_owned_amount"), "render_player": Callable(self, "_noop"), "refresh_store_house": Callable(self, "_noop_async"), "close_modal_panel": Callable(self, "_noop"), "open_modal_panel": Callable(self, "_test_open_modal"), "is_operation_in_progress": Callable(self, "_false"), "set_operation_in_progress": Callable(self, "_noop"), "show_toast": Callable(self, "_noop"), "response_data": Callable(self, "_response_data_test"), "response_status_text": Callable(self, "_response_status_text_test"), "decrease_player_currency": Callable(self, "_noop"), "decrease_warehouse_item": Callable(self, "_noop"), }, }) var rows: Array = panel.call("_normalize_trade_rows", [ {"id": 1, "itemid": 102001, "itemname": "金桔", "nowOpen": 6, "HighestPrice": 7, "LowestPrice": 3, "successNum": 2}, {"id": 15, "itemid": 200350, "itemname": "猪肉", "nowOpen": 20}, ]) _expect(rows is Array and rows.size() == 1, "market panel should only list crop fruit rows") if rows is Array and rows.size() > 0: _expect(int(rows[0].get("item_id", 0)) == 102001, "market fruit row should keep fruit item id") _expect(int(rows[0].get("price", 0)) == 6, "market fruit row should use realtime nowOpen price") owner.queue_free() func _verify_pet_health_defaults() -> void: var panel = PetPanelScript.new() var default_pet = panel.call("_normalize_pet_row", {"id": 1, "blood": 360}) _expect(default_pet is Dictionary and int(default_pet.get("hp", 0)) == 100, "pet hp should default to 100") _expect(default_pet is Dictionary and int(default_pet.get("max_hp", 0)) == 100, "pet max hp should default to 100") _expect(str(panel.call("_pet_health_text", default_pet)) == "100/100", "pet health text should default to 100/100") var custom_pet = panel.call("_normalize_pet_row", {"id": 2, "hp": 35, "max_hp": 80}) _expect(custom_pet is Dictionary and str(panel.call("_pet_health_text", custom_pet)) == "35/80", "pet health text should use explicit hp and max_hp") func _verify_battle_pet_shop_filters() -> void: var panel = PetPanelScript.new() panel.setup({ "game_config": GameConfigCatalogScript.new(), "dog_food_item_ids": [103005, 103006, 103007], }) var items: Array = panel.call("_pet_shop_items") var item_ids := {} for shop_item in items: if not shop_item is Dictionary: continue var item_config: Dictionary = shop_item.get("item", {}) item_ids[int(shop_item.get("item_id", item_config.get("id", 0)))] = true _expect(item_ids.has(201017), "battle pet shop should include battle pet eggs") _expect(item_ids.has(201021), "battle pet shop should include higher battle pet eggs") _expect(item_ids.has(103005), "battle pet shop should include pet food") _expect(not item_ids.has(106002), "battle pet shop should exclude ornamental guard pet items") func _verify_battle_pet_patrol_rules() -> void: var panel = PetPanelScript.new() _expect(bool(panel.call("_pet_can_patrol", {"is_enter_war": 1, "feed": 1})), "battle pet should patrol when it is in war and has feed time") _expect(not bool(panel.call("_pet_can_patrol", {"is_enter_war": 1, "feed": 0})), "battle pet should not patrol without feed time") _expect(not bool(panel.call("_pet_can_patrol", {"is_enter_war": 0, "feed": 10})), "battle pet should not patrol when not in war") func _verify_battle_pet_panel_hit_passthrough() -> void: var owner := Control.new() owner.name = "BattlePetPanelHitOwner" owner.size = Vector2(640.0, 960.0) get_root().add_child(owner) var atlas_cache = AtlasTextureCacheScript.new() var texture_provider = TextureProviderScript.new() texture_provider.setup(atlas_cache, { "common": CommonAtlasPath, "battle_pet": BattlePetAtlasPath, }) var ui_builder = UiBuilderScript.new() ui_builder.setup({ "owner": owner, "panel_screen_size": Vector2(640.0, 960.0), "texture_provider": texture_provider, "close_modal_callback": Callable(self, "_noop"), "get_item_icon_texture": Callable(self, "_get_item_icon_texture"), }) var panel = PetPanelScript.new() panel.setup({ "owner": owner, "ui_builder": ui_builder, "texture_provider": texture_provider, "game_config": GameConfigCatalogScript.new(), "pet_api": FakeBattlePetApi.new(), "callbacks": { "token": Callable(self, "_token"), "open_modal_panel": Callable(self, "_test_open_modal"), "close_modal": Callable(self, "_noop"), "refresh_store_house": Callable(self, "_noop_async"), "response_data": Callable(self, "_response_data_test"), "style_label": Callable(self, "_style_label"), }, }) await panel.open() var opened_panel := get_root().get_node_or_null("PetPanel") as Control var content := opened_panel.get_node_or_null("PetList") as Control if opened_panel != null else null _expect(content != null and content.mouse_filter == Control.MOUSE_FILTER_IGNORE, "battle pet content layer should not block tab or leave clicks") if opened_panel != null: opened_panel.queue_free() owner.queue_free() func _verify_battle_pet_replaces_guard_display_data() -> void: var owner := Control.new() owner.name = "BattlePetDisplayOwner" owner.size = Vector2(640.0, 960.0) get_root().add_child(owner) var display := Control.new() display.name = "BattlePetDisplay" owner.add_child(display) var atlas_cache = AtlasTextureCacheScript.new() var texture_provider = TextureProviderScript.new() texture_provider.setup(atlas_cache, { "common": CommonAtlasPath, "battle_pet": BattlePetAtlasPath, }) var ui_builder = UiBuilderScript.new() ui_builder.setup({ "owner": owner, "texture_provider": texture_provider, "get_item_icon_texture": Callable(self, "_get_item_icon_texture"), }) var pet_api := FakeBattlePetApi.new() var panel = PetPanelScript.new() panel.setup({ "owner": owner, "ui_builder": ui_builder, "texture_provider": texture_provider, "game_config": GameConfigCatalogScript.new(), "pet_api": pet_api, "callbacks": { "token": Callable(self, "_token"), "player_data": Callable(self, "_battle_pet_guard_player_data"), "response_data": Callable(self, "_response_data_test"), "style_label": Callable(self, "_style_label"), }, }) panel.set_display(display) await panel.refresh_display() _expect(pet_api.fetch_count == 1, "battle pet display should fetch pet/get-list even when guard pet item is active") _expect(display.visible, "battle pet display should stay visible after loading battle pet rows") _expect(display.get_node_or_null("BattlePetMovieClip") != null, "battle pet display should render battle pet clip") _expect(display.get_node_or_null("GuardDogMovieClip") == null, "battle pet display should not render guard pet clip") owner.queue_free() func _verify_friend_rank_row_alignment() -> void: var owner := Control.new() owner.name = "FriendPanelOwner" owner.size = Vector2(640.0, 960.0) get_root().add_child(owner) var atlas_cache = AtlasTextureCacheScript.new() var texture_provider = TextureProviderScript.new() texture_provider.setup(atlas_cache, { "common": CommonAtlasPath, "friend": FriendAtlasPath, "rank": RankAtlasPath, }) var ui_builder = UiBuilderScript.new() ui_builder.setup({ "owner": owner, "panel_screen_size": Vector2(640.0, 960.0), "texture_provider": texture_provider, "close_modal_callback": Callable(self, "_noop"), }) var panel = FriendPanelScript.new() panel.setup({ "ui_builder": ui_builder, "texture_provider": texture_provider, "page_size": 7, }) var root: Control = panel.create(0) _expect(root.get_node_or_null("FriendAdd") == null, "friend add icon should be removed") _expect(root.get_node_or_null("FriendReq") == null, "friend request icon should be removed") var row: Control = panel.call("_create_row", { "ranking": 1, "nickname": "Jessica", "level": 60, "gold": 4294970000, "user_id": 256, }) var value := row.get_node_or_null("Value") as Label var home := row.get_node_or_null("Home") as Control var hand := row.get_node_or_null("Hand") as Control _expect(value != null and _near(value.position.x, 200.0), "friend rank value column should match FriendRankItemSkin horizontalCenter=31.5") _expect(home != null and _near(home.position.x, 308.0), "friend rank home button should match FriendRankItemSkin horizontalCenter=114.5") _expect(hand == null, "friend rank hand button column should be removed") _expect(_near(row.size.x, 450.0), "friend rank row should fill the list after removing hand button") row.queue_free() root.queue_free() owner.queue_free() func _verify_friend_visit_mode() -> void: var scene := GameScene.instantiate() as Control scene.setup({ "id": 600, "user_id": 600, "nickname": "1231", "level": 14, "gold": 100, "gem": 1, "score": 1998, "exp": 0, "farm_level": 6, "bg_image_id": 104004, "landList": [{ "position": 0, "level": 0, "crop_id": 0 }], }) get_root().add_child(scene) await process_frame var friend_land := { "position": 0, "level": 0, "crop_id": 1, "crop_start_time": int(Time.get_unix_time_from_system()) - 30, "crop_gather_time": int(Time.get_unix_time_from_system()) - 1, "is_ripe": 1, "crop_num": 30, "crop_min_num": 15, "has_dry": 0, "has_bug": 0, "has_grass": 0, "has_gather": 0, "crop_phase": 3, "interaction_type_list": [0], } scene.call("_enter_friend_farm", { "id": 256, "user_id": 256, "nickname": "aaaa", "level": 60, "gold": 200, "gem": 2, "score": 60, "exp": 0, "farm_level": 3, "bg_image_id": 104004, "use_dog_item_id": 0, "landList": [friend_land], }) await process_frame _expect(scene.get_node("BackHome").visible, "friend visit should show back home button") _expect(not scene.get_node("MainMenu").visible, "friend visit should hide home menu") _expect(not scene.get_node("LeftMenu").visible, "friend visit should hide market menu") _expect(not scene.get_node("LeftMiddleMenu").visible, "friend visit should hide left middle menu") _expect(not scene.get_node("TopLayer/TopButtonGroup").visible, "friend visit should hide top menu buttons") _expect(not scene.get_node("TopLayer/TopHide").visible, "friend visit should hide top toggle") var add_land := scene.get_node_or_null("GameView/AddLand") as Control if add_land == null: add_land = scene.get_node_or_null("FarmSceneRoot/GameView/AddLand") as Control _expect(add_land != null and not add_land.visible, "friend visit should hide add-land indicator") var scene_hit_layer := scene.get_node_or_null("SceneHitLayer") as Control var back_hit: Button = null var notice_hit: Button = null if scene_hit_layer != null: back_hit = scene_hit_layer.get_node_or_null("BackHomeHit") as Button notice_hit = scene_hit_layer.get_node_or_null("NoticeHit") as Button _expect(back_hit != null and back_hit.visible and not back_hit.disabled, "friend visit should enable back home hit") _expect(notice_hit != null and not notice_hit.visible and notice_hit.disabled, "friend visit should disable own menu hits") var farm_hit_layer := scene.get_node_or_null("FarmSceneRoot/FarmHitLayer") as Control _expect(farm_hit_layer != null and not farm_hit_layer.visible, "friend visit should disable building farm hits") var actions = scene.call("_get_land_actions", friend_land) var action_array := actions as Array _expect(action_array != null and action_array.has("steal"), "friend ripe land should expose steal action") var protected_land := friend_land.duplicate(true) protected_land["crop_num"] = 15 var protected_actions = scene.call("_get_land_actions", protected_land) as Array _expect(protected_actions != null and not protected_actions.has("steal"), "friend land at 50 percent protected output should not expose steal action") scene.call("_back_to_own_farm") await process_frame _expect(not scene.get_node("BackHome").visible, "back to own farm should hide back home button") _expect(scene.get_node("MainMenu").visible, "back to own farm should restore home menu") _expect(scene.get_node("LeftMenu").visible, "back to own farm should restore market menu") _expect(scene.get_node("LeftMiddleMenu").visible, "back to own farm should restore left middle menu") scene.queue_free() func _verify_top_menu_single_row() -> void: var scene := GameScene.instantiate() as Control var back_bar := scene.get_node_or_null("TopLayer/TopButtonGroup/TopPanelBack") as TextureRect var front_bar := scene.get_node_or_null("TopLayer/TopButtonGroup/TopPanelFront") as TextureRect var lottery := scene.get_node_or_null("TopLayer/TopButtonGroup/TopLottery") as TextureRect var gift := scene.get_node_or_null("TopLayer/TopButtonGroup/TopPayGift") as TextureRect var landup := scene.get_node_or_null("TopLayer/TopButtonGroup/TopLandup") as TextureRect _expect(front_bar != null and front_bar.visible, "top menu front bar should remain visible") _expect(back_bar != null and not back_bar.visible, "top menu back bar should be hidden for single row layout") _expect(lottery != null, "top menu lottery item should exist") if lottery != null and gift != null and landup != null: _expect(_near(lottery.position.y, 101.0), "top menu lottery should be moved to the first row") _expect(lottery.position.y <= gift.position.y + 1.0, "top menu lottery should align with pack row") _expect(lottery.position.y <= landup.position.y + 1.0, "top menu lottery should align with land up row") scene.queue_free() func _verify_pet_list_item_background_region() -> void: var region := TextureRegionCatalogScript.region_for(TextureRegionCatalogScript.COMMON_ATLAS_PATH, "shop_itembg") _expect(_near(region.position.x, 262.0), "pet list item shop_itembg x should match common.json") _expect(_near(region.position.y, 303.0), "pet list item shop_itembg y should match common.json") _expect(_near(region.size.x, 179.0), "pet list item shop_itembg width should match common.json") _expect(_near(region.size.y, 110.0), "pet list item shop_itembg height should match common.json") func _verify_ui_motion_patrol_loop() -> void: var owner := Control.new() owner.name = "PatrolMotionOwner" get_root().add_child(owner) var target := Control.new() target.name = "PatrolTarget" owner.add_child(target) var callbacks: Array[String] = [] var tween := UiMotionScript.play_patrol_loop(owner, target, Vector2(1.0, 2.0), Vector2(3.0, 4.0), { "forward_duration": 0.01, "back_duration": 0.01, "idle_delay": 0.01, "before_forward": func() -> void: callbacks.append("forward"), "before_back": func() -> void: callbacks.append("back"), "before_idle_tail": func() -> void: callbacks.append("tail"), "before_idle": func() -> void: callbacks.append("idle"), "meta_name": "patrol_tween", }) _expect(tween != null, "patrol motion should create a tween") _expect(target.has_meta("patrol_tween"), "patrol motion should store tween on target") await create_timer(0.08).timeout _expect(callbacks.has("forward"), "patrol motion should call forward callback") _expect(callbacks.has("back"), "patrol motion should call back callback") _expect(callbacks.has("tail"), "patrol motion should call idle-tail callback") _expect(callbacks.has("idle"), "patrol motion should call idle callback") owner.queue_free() func _verify_land_extend_gold_icon_constraints() -> void: var owner := Control.new() owner.name = "LandExtendOwner" owner.size = Vector2(640.0, 960.0) get_root().add_child(owner) var atlas_cache = AtlasTextureCacheScript.new() var texture_provider = TextureProviderScript.new() texture_provider.setup(atlas_cache, { "common": CommonAtlasPath, "landup": TextureRegionCatalogScript.LANDUP_ATLAS_PATH, }) var ui_builder = UiBuilderScript.new() ui_builder.setup({ "owner": owner, "panel_screen_size": Vector2(640.0, 960.0), "texture_provider": texture_provider, "close_modal_callback": Callable(self, "_noop"), }) var panel_module = BuildingPanelScript.new() panel_module.setup({ "ui_builder": ui_builder, "texture_provider": texture_provider, "game_config": GameConfigCatalogScript.new(), "panel_screen_size": Vector2(640.0, 960.0), "land_count": 20, "callbacks": { "player_data": Callable(self, "_land_extend_player_data"), "lands_by_position": Callable(self, "_land_extend_lands_by_position"), "close_modal_panel": Callable(self, "_noop"), "set_texture_button_enabled": Callable(self, "_set_texture_button_enabled"), "is_operation_in_progress": Callable(self, "_false"), }, }) var panel: Control = panel_module.create_land_extend() _expect(panel != null, "land extend panel should be created") if panel != null: var gold_icon := panel.get_node_or_null("GoldIcon") as TextureRect _expect(gold_icon != null, "land extend gold icon should exist") if gold_icon != null: _expect(_near(gold_icon.size.x, 35.0) and _near(gold_icon.size.y, 35.0), "land extend gold icon should stay 35x35, got %s" % gold_icon.size) _expect(gold_icon.stretch_mode == TextureRect.STRETCH_KEEP_ASPECT_CENTERED, "land extend gold icon should constrain replacement gold bean") panel.queue_free() owner.queue_free() func _verify_main_menu_direct_hit() -> void: var owner := Control.new() owner.name = "MainMenuOwner" owner.size = Vector2(640.0, 960.0) get_root().add_child(owner) var menu := Control.new() menu.name = "MainMenu" menu.position = Vector2(451.0, 771.0) menu.size = Vector2(189.0, 189.0) owner.add_child(menu) var home_art := TextureRect.new() home_art.name = "HomeButtonArt" home_art.position = Vector2(24.0, 34.0) home_art.size = Vector2(155.0, 145.0) menu.add_child(home_art) _main_menu_action = "" var controller = MainMenuControllerScript.new() controller.setup( owner, menu, home_art, MainMenuHomeTexture, { "shop": MainMenuShopTexture, "warehouse": MainMenuWarehouseTexture, "friend": MainMenuFriendTexture, }, Callable(self, "_style_menu_title"), Callable(), Callable(self, "_record_main_menu_action") ) var home_point := home_art.get_global_rect().get_center() _expect(controller.handle_direct_hit(home_point), "home menu direct hit should consume home button") _expect(controller.is_open(), "home menu direct hit should open menu") await create_timer(0.3).timeout var shop_button := menu.get_node_or_null("MainMenuShop") as TextureButton _expect(shop_button != null, "home menu shop item should exist") if shop_button != null: _expect(controller.handle_direct_hit(shop_button.get_global_rect().get_center()), "home menu direct hit should consume menu item") _expect(_main_menu_action == "shop", "home menu direct hit should dispatch menu item action") await create_timer(0.4).timeout _main_menu_action = "" _expect(controller.handle_direct_hit(home_point), "home menu direct hit should reopen home menu") _expect(controller.is_open(), "home menu should be open before outside hit") _expect(controller.handle_direct_hit(Vector2(280.0, 500.0)), "open home menu should consume outside hit") _expect(_main_menu_action.is_empty(), "outside hit should only close home menu, not dispatch action") owner.queue_free() func _style_label(_label: Label, _size: int, _color: Color, _stroke := 0, _stroke_color := Color.TRANSPARENT) -> void: pass func _style_menu_title(_label: Label, _size: int) -> void: pass func _record_main_menu_action(action: String) -> void: _main_menu_action = action func _record_seed_bag_press(item: Dictionary, mode: String) -> void: _seed_bag_press = item.duplicate(true) _seed_bag_press["mode"] = mode func _record_land_action_menu_action(action: String) -> void: _land_action_menu_action = action func _record_land_action_menu_dismiss() -> void: _land_action_menu_dismissed = true func _get_item_icon_texture(_icon_id: int) -> Texture2D: return PlantIconTexture func _seed_icon_id(_item_id: int) -> int: return 1031 func _get_land_node(position: int) -> Control: if _land_nodes.has(position): var candidate = _land_nodes.get(position) if is_instance_valid(candidate): return candidate as Control _land_nodes.erase(position) return _land_node func _persistent_land_data_for(position: int) -> Dictionary: if position == 1: var data := _persistent_land_data.duplicate(true) data["position"] = 1 return data if position != 0: return {} return _persistent_land_data func _persistent_land_actions(_land_data: Dictionary) -> Array[String]: return ["uproot", "manure"] func _format_duration(seconds: int) -> String: return "%s秒" % seconds func _land_extend_player_data() -> Dictionary: return { "level": 17, "gold": 4294940, } func _land_extend_lands_by_position() -> Dictionary: var lands := {} for index in range(17): lands[index] = { "position": index, "level": 0, "crop_id": 0, } return lands func _market_player_data() -> Dictionary: return {"gold": 1000} func _battle_pet_guard_player_data() -> Dictionary: return { "use_dog_item_id": 106001, "dog_hunger_end_time": int(Time.get_unix_time_from_system()) + 3600, } func _market_warehouse_items() -> Array[Dictionary]: return [{"item_id": 102001, "num": 12}] func _market_owned_amount(item_id: int) -> int: return 12 if item_id == 102001 else 0 func _token() -> String: return "token" func _test_open_modal(panel: Control) -> void: if panel != null: get_root().add_child(panel) func _noop_async() -> void: pass func _response_data_test(result: Dictionary, fallback: Variant = {}) -> Variant: var body = result.get("body", {}) if body is Dictionary and body.has("data"): return body.get("data") return fallback func _response_status_text_test(_result: Dictionary, fallback := "操作失败") -> String: return fallback func _set_texture_button_enabled(button_holder: Control, enabled: bool, normal_texture: Texture2D = null, disabled_texture: Texture2D = null) -> void: if button_holder == null: return var art := button_holder.get_node_or_null("Art") as TextureRect if art != null: art.texture = normal_texture if enabled or disabled_texture == null else disabled_texture var hit := button_holder.get_node_or_null("Hit") as Button if hit != null: hit.disabled = not enabled func _false() -> bool: return false func _noop(_value: Variant = null) -> void: pass func _expect(condition: bool, message: String) -> void: if not condition: _failures.append(message) func _near(value: float, expected: float) -> bool: return absf(value - expected) <= 0.5