extends Control # GameScene 是当前 Godot 农场主场景的装配层。 # # 经过模块化拆分后,它仍保留以下职责: # - 调用 Bootstrap/RuntimeAssembler 完成服务、状态、规则、资源和 UI 模块装配。 # - 连接场景节点和独立模块,例如 HUD、菜单、土地网格、面板路由。 # - 维护少量跨模块共享状态,例如库存、当前选中土地。 # - 通过 GameSceneContext 桥接独立模块,避免主场景继续维护大 callback 字典。 # # 不应再把新功能的大块 UI/业务写回本文件;新功能应新增独立模块并在 Bootstrap/RuntimeAssembler 中装配。 # 依赖按层分组: # 1. core/state/rules/mappers/services 是非 UI 逻辑; # 2. ui/common/ui/farm/ui/panels 是可视化和交互模块; # 3. assets 常量是旧 Egret 资源路径兼容层。 const NumberFormatterScript := preload("res://scripts/core/number_formatter.gd") const I18nScript := preload("res://scripts/core/i18n.gd") const CostRulesScript := preload("res://scripts/domain/rules/cost_rules.gd") const GameSceneBootstrapScript := preload("res://scripts/ui/game_scene_bootstrap.gd") const GameSceneRuntimeAssemblerScript := preload("res://scripts/ui/game_scene_runtime_assembler.gd") const NewbieSeedGuideScript := preload("res://scripts/ui/newbie/newbie_seed_guide.gd") const TitleFont := preload("res://assets/fonts/google/LilitaOne-Regular.ttf") const ButtonFont := preload("res://assets/fonts/google/LilitaOne-Regular.ttf") const BodyFont := preload("res://assets/fonts/google/LilitaOne-Regular.ttf") const LogItemBgTexture := preload("res://assets/egret/common/log_itembg_png.tres") const CommonAtlasPath := "res://assets/egret/common/common.png" const CommonJsonAtlasPath := "res://assets/egret/assets/common.png" const FriendAtlasPath := "res://assets/egret/assets/game/friend/friend.png" const RankAtlasPath := "res://assets/egret/assets/game/rank/rank.png" const BuildingAtlasPath := "res://assets/egret/assets/game/building/building.png" const LandupAtlasPath := "res://assets/egret/assets/game/landup/landup.png" const SignAtlasPath := "res://assets/egret/assets/game/sign/sign.png" const RoleInfoAtlasPath := "res://assets/egret/assets/game/roleinfo/roleinfo.png" const CreateRoleAtlasPath := "res://assets/egret/assets/game/createrole/createrole.png" const Common2AtlasPath := "res://assets/egret/assets/common2.png" const LotteryAtlasPath := "res://assets/egret/assets/game/lottery/lottery.png" const GiftAtlasPath := "res://assets/egret/assets/game/gift/onLineGift.png" const MofaHechengAtlasPath := "res://assets/egret/assets/game/mofahecheng/mofahecheng.png" const BattlePetAtlasPath := "res://assets/egret/assets/game/battlePet/battle_pet.png" const Menu2AtlasPath := "res://assets/egret/assets/game/menu2/menu3.png" const PayComAtlasPath := "res://assets/egret/assets/game/pay/pay_com.png" const PayBgPath := "res://assets/egret/assets/game/pay/pay_bg.png" const LAND_COUNT := 12 const LAND_TEXTURE_SIZE := Vector2(172.0, 102.0) const MENU_BUTTON_SIZE := Vector2(92.0, 88.0) const BAG_MODE_SEED := "seed" const BAG_MODE_FERTILIZER := "fertilizer" const ACTION_PLANT := "plant" const ACTION_GAIN := "gain" const ACTION_UPROOT := "uproot" const ACTION_MANURE := "manure" const ACTION_WATER := "water" const ACTION_DIE_BUG := "die_bug" const ACTION_DIE_GRASS := "die_grass" const ACTION_STEAL := "steal" const ACTION_PUT_BUG := "put_bug" const ACTION_PUT_GRASS := "put_grass" const DISEASE_TYPE_BUG := 1 const DISEASE_TYPE_GRASS := 2 const DISEASE_TYPE_WATER := 0 const PHASE_NAMES := ["种子期", "发芽期", "生长期", "成熟期", "枯萎"] const PANEL_SCREEN_SIZE := Vector2(640.0, 960.0) const SCENE_ZOOM_MIN := 1.0 / 1.5 const SCENE_ZOOM_MAX := 1.3 const WARE_CAPACITY := 32 const FRIEND_PAGE_SIZE := 7 const ITEM_TYPE_SEED := 1 const ITEM_TYPE_FRUIT := 2 const ITEM_TYPE_PROPS := 3 const ITEM_ID_GEM := 105001 const ITEM_ID_GOLD := 105002 const ITEM_ID_STONE := 201000 const ITEM_ID_STEEL := 201001 const REMOVED_BOX_ITEM_TYPE := 400 const SERVER_STATUS_TEXT := { 1008: "农田上已经有农作物", 1009: "当前等级不足", 1011: "你没有这个物品", 1012: "物品数量不够", 1014: "物品类型错误", 1015: "农田上没有种植种子", 1016: "尚未成熟", 1017: "你已经收割过该农作物", 1018: "农田不需要清理", 1019: "农作物已经成熟", 1020: "操作错误", 1044: "施肥已经到达上限", 1045: "施肥时间错误", 1047: "土地等级不够,无法种植该植物", 1073: "Not enough stamina", 1074: "You did not participate in the pet duel and cannot steal.", } const REMOVED_BOX_ITEM_IDS := [201012, 201013, 201014, 201015, 201016] const ITEM_ID_BUG_KILLER := 108001 const ITEM_ID_GRASS_KILLER := 109001 const MAX_FARM_LEVEL := 12 const DOG_FOOD_ITEM_IDS := [103005, 103006, 103007] const PLANT_PHASE_REFRESH_SECONDS := 1.0 const STAMINA_KEYS := [ "stamina", "stamina_max", "stamina_next_recover_time", "stamina_recover_amount", "stamina_recover_interval", "stamina_cost_steal", ] # 场景节点引用全部集中在 @onready 区域,避免业务方法里散落 get_node 路径。 # 这些节点来自 scenes/ui/game_scene.tscn,修改场景结构时优先同步这里。 @onready var label_name: Label = %LabelName @onready var label_level: Label = %LabelLevel @onready var label_gold: Label = %LabelGold @onready var label_diamond: Label = %LabelDiamond @onready var label_score: Label = %LabelScore @onready var label_exp: Label = %LabelExp @onready var label_stamina: Label = %LabelStamina @onready var exp_fill: NinePatchRect = %ExpFill @onready var stamina_fill: NinePatchRect = %StaminaFill @onready var avatar: TextureRect = %Avatar @onready var game_view: Control = $GameView @onready var top_layer: Control = $TopLayer @onready var top_button_group: Control = $TopLayer/TopButtonGroup @onready var top_hide: TextureRect = $TopLayer/TopHide @onready var main_menu: Control = $MainMenu @onready var home_button_art: TextureRect = $MainMenu/HomeButtonArt @onready var left_menu: Control = $LeftMenu @onready var left_middle_menu: Control = $LeftMiddleMenu @onready var market_button_art: TextureRect = $LeftMenu/MarketButtonArt @onready var exchange_gold_art: TextureRect = $ExchangeGold @onready var ground_art: TextureRect = $Ground @onready var fence_back_art: TextureRect = $FenceBack @onready var fence_front_art: TextureRect = $FenceFront @onready var house_art: TextureRect = $House @onready var dog_house_art: TextureRect = $DogHouse @onready var add_land_art: TextureRect = $GameView/AddLand @onready var back_home_art: TextureRect = $BackHome var _player_data: Dictionary = {} # 架构层对象:负责接口、配置、状态、规则、资源缓存。 # 这些对象只在 _setup_architecture_modules() 创建一次,其他模块通过引用复用。 var _api_client: ApiClient var _game_config var _atlas_cache: AtlasTextureCache var _player_state: PlayerState var _farm_state: FarmState var _inventory_state: InventoryState var _player_inventory_facade var _crop_phase_rules: CropPhaseRules var _land_action_rules: LandActionRules var _farm_mapper: FarmMapper var _inventory_mapper: InventoryMapper var _farm_api: FarmApi var _inventory_api: InventoryApi var _building_api var _shop_api var _warehouse_api var _friend_api var _activity_api var _pet_api var _pay_api var _texture_provider var _ui_builder var _context var _game_scene_bootstrap var _runtime_assembler var _panel_router var _scene_hit_router # 农场运行时状态。 # _lands_by_position 是渲染和交互的土地快照;真正归一化逻辑在 FarmState。 var _lands_by_position: Dictionary = {} var _seed_items: Array[Dictionary] = [] var _fertilizer_items: Array[Dictionary] = [] var _operation_in_progress := false var _visiting_friend := false var _visited_friend_id := 0 var _visited_friend_data: Dictionary = {} # 主场景临时 UI 节点。 # 这些是土地菜单、背包、提示、弹窗层等可复用节点,不是业务面板本体。 var _bag_panel: Control var _long_press_timer: Timer var _plant_phase_timer: Timer var _last_land_visual_signature := "" var _farm_scene_root: Control var _modal_layer: Control var _warehouse_items: Array[Dictionary] = [] var _farm_viewport_controller var _modal_controller var _main_menu_controller var _top_bar_view var _newbie_seed_guide var _farm_controller var _friend_visit_controller var _land_grid_view var _land_action_menu var _seed_bag_view var _farm_interaction_coordinator var _add_land_hit: Button var _pet_layer: Control var _pet_display: Control var _pet_battle_vs_overlay func setup(player_data: Dictionary) -> void: # Bootstrap 自动登录成功后把后端玩家数据注入主场景。 _player_data = player_data.duplicate(true) func _ready() -> void: # 启动顺序很重要: # 先装配服务/状态,再装配 UI 模块,最后渲染场景并发起异步库存/宠物刷新。 _setup_architecture_modules() _setup_runtime_views() _setup_newbie_seed_guide() _collect_land_data() _render_scene_skin() _render_house_art() _render_player() _render_lands() _apply_visit_mode_visuals() _refresh_store_house() if _panel_router != null: _panel_router.refresh_pet_display() _play_scene_enter_animation() func _input(event: InputEvent) -> void: # 弹窗打开时屏蔽主场景缩放和底层点击,避免弹窗按钮和土地同时响应。 if _modal_layer != null and _modal_layer.visible: return var mouse_event := event as InputEventMouseButton if mouse_event != null and mouse_event.button_index == MOUSE_BUTTON_LEFT and mouse_event.pressed and _handle_main_menu_direct_hit(mouse_event.position): get_viewport().set_input_as_handled() return var touch_event := event as InputEventScreenTouch if touch_event != null and touch_event.pressed and _handle_main_menu_direct_hit(touch_event.position): get_viewport().set_input_as_handled() return if mouse_event != null and mouse_event.button_index == MOUSE_BUTTON_LEFT and mouse_event.pressed and _handle_land_action_menu_direct_hit(mouse_event.position): get_viewport().set_input_as_handled() return if touch_event != null and touch_event.pressed and _handle_land_action_menu_direct_hit(touch_event.position): get_viewport().set_input_as_handled() return if _handle_farm_viewport_input(event): get_viewport().set_input_as_handled() return if mouse_event != null and mouse_event.button_index == MOUSE_BUTTON_LEFT and mouse_event.pressed and _handle_direct_scene_hit(mouse_event.position): get_viewport().set_input_as_handled() return if mouse_event != null and mouse_event.button_index == MOUSE_BUTTON_LEFT and _handle_direct_land_input(mouse_event.position, mouse_event.pressed): get_viewport().set_input_as_handled() return if touch_event != null and touch_event.pressed and _handle_direct_scene_hit(touch_event.position): get_viewport().set_input_as_handled() return if touch_event != null and _handle_direct_land_input(touch_event.position, touch_event.pressed): get_viewport().set_input_as_handled() func _handle_direct_scene_hit(position: Vector2) -> bool: if _scene_hit_router == null: return false return _scene_hit_router.handle_direct_scene_hit(position) func _handle_main_menu_direct_hit(position: Vector2) -> bool: if _main_menu_controller == null: return false if not _main_menu_controller.has_method("handle_direct_hit"): return false return _main_menu_controller.handle_direct_hit(position) func _handle_land_action_menu_direct_hit(position: Vector2) -> bool: if _land_action_menu == null: return false if not _land_action_menu.has_method("handle_direct_hit"): return false return _land_action_menu.handle_direct_hit(position) func _handle_direct_land_input(position: Vector2, pressed: bool) -> bool: if _farm_interaction_coordinator == null: return false if not _farm_interaction_coordinator.has_method("handle_direct_land_pointer"): return false return _farm_interaction_coordinator.handle_direct_land_pointer(position, pressed) func _handle_farm_viewport_input(event: InputEvent) -> bool: if _farm_viewport_controller == null: return false return _farm_viewport_controller.handle_input(event) func _setup_architecture_modules() -> void: _game_scene_bootstrap = GameSceneBootstrapScript.new() _apply_bootstrap_modules(_game_scene_bootstrap.setup({ "owner": self, "player_data": _player_data, "panel_screen_size": PANEL_SCREEN_SIZE, "font": BodyFont, "fonts": { "title": TitleFont, "button": ButtonFont, "body": BodyFont, }, "atlas_paths": { "common": CommonAtlasPath, "common_json": CommonJsonAtlasPath, "friend": FriendAtlasPath, "rank": RankAtlasPath, "building": BuildingAtlasPath, "landup": LandupAtlasPath, "sign": SignAtlasPath, "roleinfo": RoleInfoAtlasPath, "createrole": CreateRoleAtlasPath, "common2": Common2AtlasPath, "gift": GiftAtlasPath, "lottery": LotteryAtlasPath, "mofahecheng": MofaHechengAtlasPath, "battle_pet": BattlePetAtlasPath, "menu2": Menu2AtlasPath, "pay_com": PayComAtlasPath, }, "paths": { "pay_bg": PayBgPath, }, "textures": { "log_item_bg": LogItemBgTexture, }, "constants": { "land_count": LAND_COUNT, "ware_capacity": WARE_CAPACITY, "friend_page_size": FRIEND_PAGE_SIZE, "common_atlas_path": CommonAtlasPath, "friend_atlas_path": FriendAtlasPath, "landup_atlas_path": LandupAtlasPath, "item_type_seed": ITEM_TYPE_SEED, "item_type_fruit": ITEM_TYPE_FRUIT, "item_type_props": ITEM_TYPE_PROPS, "item_id_gem": ITEM_ID_GEM, "item_id_gold": ITEM_ID_GOLD, "item_id_stone": ITEM_ID_STONE, "item_id_steel": ITEM_ID_STEEL, "item_id_bug_killer": ITEM_ID_BUG_KILLER, "item_id_grass_killer": ITEM_ID_GRASS_KILLER, "disease_type_bug": DISEASE_TYPE_BUG, "disease_type_grass": DISEASE_TYPE_GRASS, "disease_type_water": DISEASE_TYPE_WATER, "max_farm_level": MAX_FARM_LEVEL, "dog_food_item_ids": DOG_FOOD_ITEM_IDS, }, })) func _apply_bootstrap_modules(modules: Dictionary) -> void: _game_config = modules.get("game_config") _api_client = modules.get("api_client") _atlas_cache = modules.get("atlas_cache") _texture_provider = modules.get("texture_provider") _ui_builder = modules.get("ui_builder") _context = modules.get("context") _panel_router = modules.get("panel_router") _player_state = modules.get("player_state") if _player_state != null and _player_state.has_method("player_data"): _player_data = _player_state.player_data() _farm_state = modules.get("farm_state") _inventory_state = modules.get("inventory_state") _player_inventory_facade = modules.get("player_inventory_facade") _crop_phase_rules = modules.get("crop_phase_rules") _land_action_rules = modules.get("land_action_rules") _farm_mapper = modules.get("farm_mapper") _inventory_mapper = modules.get("inventory_mapper") _farm_api = modules.get("farm_api") _inventory_api = modules.get("inventory_api") _building_api = modules.get("building_api") _shop_api = modules.get("shop_api") _warehouse_api = modules.get("warehouse_api") _friend_api = modules.get("friend_api") _activity_api = modules.get("activity_api") _pet_api = modules.get("pet_api") _pay_api = modules.get("pay_api") _farm_controller = modules.get("farm_controller") _friend_visit_controller = modules.get("friend_visit_controller") func _setup_runtime_views() -> void: _runtime_assembler = GameSceneRuntimeAssemblerScript.new() _apply_runtime_modules(_runtime_assembler.setup({ "owner": self, "texture_provider": _texture_provider, "nodes": { "label_name": label_name, "label_level": label_level, "label_gold": label_gold, "label_diamond": label_diamond, "label_score": label_score, "label_exp": label_exp, "label_stamina": label_stamina, "exp_fill": exp_fill, "stamina_fill": stamina_fill, "avatar": avatar, "game_view": game_view, "top_layer": top_layer, "top_button_group": top_button_group, "top_hide": top_hide, "main_menu": main_menu, "left_middle_menu": left_middle_menu, "home_button_art": home_button_art, "left_menu": left_menu, "market_button_art": market_button_art, "exchange_gold_art": exchange_gold_art, "house_art": house_art, "dog_house_art": dog_house_art, "add_land_art": add_land_art, "back_home_art": back_home_art, }, "constants": { "panel_screen_size": PANEL_SCREEN_SIZE, "scene_zoom_min": SCENE_ZOOM_MIN, "scene_zoom_max": SCENE_ZOOM_MAX, "land_count": LAND_COUNT, "land_texture_size": LAND_TEXTURE_SIZE, "menu_button_size": MENU_BUTTON_SIZE, "bag_mode_seed": BAG_MODE_SEED, "bag_mode_fertilizer": BAG_MODE_FERTILIZER, "action_plant": ACTION_PLANT, "action_gain": ACTION_GAIN, "action_uproot": ACTION_UPROOT, "action_manure": ACTION_MANURE, "action_water": ACTION_WATER, "action_die_bug": ACTION_DIE_BUG, "action_die_grass": ACTION_DIE_GRASS, "action_steal": ACTION_STEAL, "action_put_bug": ACTION_PUT_BUG, "action_put_grass": ACTION_PUT_GRASS, "disease_type_bug": DISEASE_TYPE_BUG, "disease_type_grass": DISEASE_TYPE_GRASS, "disease_type_water": DISEASE_TYPE_WATER, "phase_names": PHASE_NAMES, "plant_phase_refresh_seconds": PLANT_PHASE_REFRESH_SECONDS, "menu2_atlas_path": Menu2AtlasPath, "game_config": _game_config, "crop_phase_rules": _crop_phase_rules, }, "callbacks": { "style_label": Callable(_ui_builder, "style_label"), "hide_selection_ui": Callable(self, "_hide_selection_ui"), "open_shop_panel": Callable(_panel_router, "open_shop_panel"), "open_warehouse_panel": Callable(_panel_router, "open_warehouse_panel"), "open_friend_panel": Callable(_panel_router, "open_friend_panel"), "open_pet_panel": Callable(_panel_router, "open_pet_panel"), "open_player_info_panel": Callable(_panel_router, "open_player_info_panel"), "open_market_panel": Callable(_panel_router, "open_market_panel"), "pet_panel_view": Callable(_panel_router, "pet_panel_view"), "get_land_node": Callable(self, "_get_land_node"), "render_add_land_indicator": Callable(self, "_render_add_land_indicator"), "cool_menu_pressed": Callable(self, "_on_cool_menu_pressed"), "menu_dismiss_pressed": Callable(self, "_on_menu_dismiss_pressed"), "get_item_icon_texture": Callable(_texture_provider, "item_icon"), "seed_icon_id": Callable(self, "_seed_icon_id"), "bag_item_pressed": Callable(self, "_on_bag_item_pressed"), "format_duration": Callable(self, "_format_duration"), "plant_phase_timer_timeout": Callable(self, "_on_plant_phase_timer_timeout"), "land_data": Callable(self, "_get_land_data"), "land_actions": Callable(self, "_get_land_actions"), "is_visiting_friend": Callable(self, "_is_visiting_friend"), "is_operation_in_progress": Callable(self, "_is_operation_in_progress"), "open_land_extend_panel": Callable(_panel_router, "open_land_extend_panel"), "seed_items": Callable(self, "_seed_items_ref"), "fertilizer_items": Callable(self, "_fertilizer_items_ref"), "refresh_store_house": Callable(self, "_refresh_store_house"), "sow_seed": Callable(self, "_sow_seed"), "use_fertilizer": Callable(self, "_use_fertilizer"), "gather_crop": Callable(self, "_gather_crop"), "clear_land": Callable(self, "_clear_land"), "kick_disease": Callable(self, "_kick_disease"), "interact_friend_land": Callable(self, "_interact_friend_land"), "keep_modal_layers_on_top": Callable(self, "_keep_modal_layers_on_top"), "open_log_panel": Callable(_panel_router, "open_log_panel"), "open_land_upgrade_panel": Callable(_panel_router, "open_land_upgrade_panel"), "open_pay_panel": Callable(_panel_router, "open_pay_panel"), "open_level_gift_panel": Callable(_panel_router, "open_level_gift_panel"), "open_lottery_panel": Callable(_panel_router, "open_lottery_panel"), "toggle_top_button_group": Callable(self, "_toggle_top_button_group"), "open_notice_panel": Callable(_panel_router, "open_notice_panel"), "open_sign_panel": Callable(_panel_router, "open_sign_panel"), "open_online_gift_panel": Callable(_panel_router, "open_online_gift_panel"), "open_gold_exchange_panel": Callable(_panel_router, "open_gold_exchange_panel"), "open_house_upgrade_panel": Callable(_panel_router, "open_house_upgrade_panel"), "back_to_own_farm": Callable(self, "_back_to_own_farm"), }, })) func _apply_runtime_modules(modules: Dictionary) -> void: _top_bar_view = modules.get("top_bar_view") _main_menu_controller = modules.get("main_menu_controller") _modal_controller = modules.get("modal_controller") _modal_layer = modules.get("modal_layer") _pet_layer = modules.get("pet_layer") _pet_display = modules.get("pet_display") _pet_battle_vs_overlay = modules.get("pet_battle_vs_overlay") _farm_viewport_controller = modules.get("farm_viewport_controller") _farm_scene_root = modules.get("farm_scene_root") _land_grid_view = modules.get("land_grid_view") _land_action_menu = modules.get("land_action_menu") _long_press_timer = modules.get("long_press_timer") _seed_bag_view = modules.get("seed_bag_view") _bag_panel = modules.get("bag_panel") _plant_phase_timer = modules.get("plant_phase_timer") _farm_interaction_coordinator = modules.get("farm_interaction_coordinator") _scene_hit_router = modules.get("scene_hit_router") _add_land_hit = modules.get("add_land_hit") if _panel_router != null: _panel_router.set_modal_controller(_modal_controller) func _setup_newbie_seed_guide() -> void: _newbie_seed_guide = NewbieSeedGuideScript.new() _newbie_seed_guide.setup({ "show_toast": Callable(self, "_show_toast"), }) func _render_player() -> void: if _top_bar_view != null: var own_player_data := _player_state.player_data() if _player_state != null else {} _top_bar_view.render(_player_data, own_player_data) func _toggle_top_button_group() -> void: if _top_bar_view == null: return var expanded := bool(_top_bar_view.toggle_top_button_group()) if _scene_hit_router != null: _scene_hit_router.set_top_button_group_hits_enabled(expanded) func _set_scene_hit_layers_enabled(enabled: bool) -> void: if _scene_hit_router != null: _scene_hit_router.set_hit_layers_enabled(enabled) func _play_scene_enter_animation() -> void: if top_layer == null: return top_layer.position.y = -220.0 var tween := create_tween() tween.tween_property(top_layer, "position:y", 0.0, 0.25).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) func _show_toast(message: String) -> void: if _modal_controller != null: _modal_controller.show_toast(message) func _collect_land_data() -> void: _farm_state.setup_from_player_data(_player_data, LAND_COUNT) _lands_by_position = _farm_state.snapshot() func _render_house_art() -> void: if house_art == null: return var farm_level := clampi(int(_player_data.get("farm_level", 1)), 1, MAX_FARM_LEVEL) house_art.texture = _texture_provider.house(farm_level, MAX_FARM_LEVEL) if _texture_provider != null else null func _render_scene_skin() -> void: var graphic_id := _current_skin_graphic_id() var ground_texture := _load_skin_texture(graphic_id, ".jpg") if ground_art != null: ground_art.texture = ground_texture var fence_back_texture := _load_skin_texture(graphic_id, "_0.png") if fence_back_art != null: fence_back_art.texture = fence_back_texture var fence_front_texture := _load_skin_texture(graphic_id, "_1.png") if fence_front_art != null: fence_front_art.texture = fence_front_texture if _farm_viewport_controller != null: var backdrop: TextureRect = _farm_viewport_controller.zoom_backdrop() if backdrop != null: backdrop.texture = ground_texture func _render_add_land_indicator() -> void: if add_land_art == null: return if _visiting_friend: add_land_art.visible = false if _add_land_hit != null: _add_land_hit.visible = false return var next_position := -1 for position in range(LAND_COUNT): if not _lands_by_position.has(position): next_position = position break if next_position < 0: add_land_art.visible = false if _add_land_hit != null: _add_land_hit.visible = false return var land_node := _get_land_node(next_position) if land_node == null: add_land_art.visible = false if _add_land_hit != null: _add_land_hit.visible = false return add_land_art.visible = true add_land_art.position = land_node.position + Vector2(40.0, -36.0) if _add_land_hit != null: _add_land_hit.visible = true var hit_global_position := add_land_art.get_global_rect().position _add_land_hit.position = _farm_scene_root.get_global_transform().affine_inverse() * hit_global_position if _farm_scene_root != null else add_land_art.position _add_land_hit.size = add_land_art.size func _keep_modal_layers_on_top() -> void: if _modal_controller != null: _modal_controller.bring_to_front() func _render_lands() -> void: if _land_grid_view == null: return # 土地渲染集中交给 LandGridView;主场景只维护后端归一化后的 position -> land_data。 _land_grid_view.render(_lands_by_position) # 记录视觉签名,计时器只在成熟阶段变化时重绘,避免每秒全量创建土地节点。 _last_land_visual_signature = _land_grid_view.build_signature(_lands_by_position) if _newbie_seed_guide != null: _newbie_seed_guide.on_lands_changed(_lands_by_position) func _on_plant_phase_timer_timeout() -> void: if _land_grid_view == null: return # 成熟倒计时会影响纹理阶段和倒计时文本;签名没变化时不刷新 UI。 var signature: String = _land_grid_view.build_signature(_lands_by_position) if signature == _last_land_visual_signature: return _render_lands() func _get_land_actions(land_data: Dictionary) -> Array[String]: if _visiting_friend and _land_action_rules.has_method("get_friend_actions"): return _land_action_rules.get_friend_actions(land_data) return _land_action_rules.get_actions(land_data) func _hide_land_light() -> void: if _farm_interaction_coordinator != null: _farm_interaction_coordinator.hide_land_light() func _on_cool_menu_pressed(action: String) -> void: if _farm_interaction_coordinator != null: _farm_interaction_coordinator.on_cool_menu_pressed(action) func _on_menu_dismiss_pressed() -> void: if _farm_interaction_coordinator != null: _farm_interaction_coordinator.on_menu_dismiss_pressed() func _process(_delta: float) -> void: if _farm_interaction_coordinator != null: _farm_interaction_coordinator.tick(_lands_by_position) func _show_plant_tip(position: int, land_data: Dictionary) -> void: if _farm_interaction_coordinator != null: _farm_interaction_coordinator.show_plant_tip(position, land_data) func _format_duration(seconds: int) -> String: return I18nScript.format_duration(seconds) func _refresh_store_house() -> void: if _player_inventory_facade == null: return # 背包统一从 Facade 刷新:它会拉后端、归一化为三类列表,并按当前可见面板触发局部重绘。 await _player_inventory_facade.refresh_store_house(_token()) if _newbie_seed_guide != null: _newbie_seed_guide.on_inventory_changed(_seed_items) func _decrease_warehouse_item(item_id: int, count: int) -> void: if _player_inventory_facade == null: return _player_inventory_facade.decrease_warehouse_item(item_id, count) func _parse_cost_string(cost_text: String) -> Array[Dictionary]: # 配置表里的消耗/奖励通常是 "item_id:count,item_id:count",优先走 Facade 保持规则入口一致。 var costs: Array[Dictionary] = [] if _player_inventory_facade != null: costs = _player_inventory_facade.parse_cost_string(cost_text) else: costs = CostRulesScript.parse_cost_string(cost_text) return _filter_removed_item_costs(costs) func _has_costs(cost_text: String) -> bool: # 所有购买、升级前置检查都走同一套库存 + 货币判断,避免面板各自实现导致数值漂移。 var filtered_cost_text := _costs_to_text(_parse_cost_string(cost_text)) if _player_inventory_facade != null: return _player_inventory_facade.has_costs(filtered_cost_text) return CostRulesScript.has_costs(filtered_cost_text, _player_data, _warehouse_items) func _apply_cost_deduction(cost_text: String) -> void: if _player_inventory_facade == null: return # 本地先扣减用于立即反馈;后续接口失败的路径必须自行回滚或重新刷新仓库。 _player_inventory_facade.apply_cost_deduction(_costs_to_text(_parse_cost_string(cost_text))) func _get_owned_amount(item_id: int) -> int: if _player_inventory_facade != null: return _player_inventory_facade.owned_amount(item_id) return CostRulesScript.get_owned_amount(item_id, _player_data, _warehouse_items) func _player_data_ref() -> Dictionary: # 面板模块读取的是引用,不复制;这样等级、金豆、宠物饥饿时间等 UI 能即时看到最新值。 return _player_data func _warehouse_items_ref() -> Array[Dictionary]: # 仓库数据由 InventoryState 持有,这里只提供只读式引用给旧面板兼容层。 return _warehouse_items func _seed_items_ref() -> Array[Dictionary]: return _seed_items func _fertilizer_items_ref() -> Array[Dictionary]: return _fertilizer_items func _lands_by_position_ref() -> Dictionary: return _lands_by_position func _decrease_player_currency(gold_cost: int, gem_cost: int) -> void: if _player_inventory_facade == null: return _player_inventory_facade.decrease_player_currency(gold_cost, gem_cost) func _add_player_gold(amount: int) -> void: if _player_inventory_facade == null: return _player_inventory_facade.add_player_gold(amount) func _set_dog_hunger_end_time(end_time: int) -> void: if _player_inventory_facade == null: if _player_state != null: _player_state.set_value("dog_hunger_end_time", end_time) else: _player_inventory_facade.set_dog_hunger_end_time(end_time) if _panel_router != null: _panel_router.render_pet_display() func _set_use_dog_item_id(item_id: int) -> void: if _player_state != null: _player_state.set_value("use_dog_item_id", item_id) _player_data = _player_state.player_data() else: _player_data["use_dog_item_id"] = item_id if _player_inventory_facade != null: _player_inventory_facade.set_player_data(_player_data) _render_player() if _panel_router != null: _panel_router.render_pet_display() func _get_selected_position() -> int: if _farm_interaction_coordinator != null: return _farm_interaction_coordinator.selected_position() return -1 func _get_land_data(position: int) -> Dictionary: return _lands_by_position.get(position, {}) func _is_operation_in_progress() -> bool: return _operation_in_progress func _set_operation_in_progress(value: bool) -> void: _operation_in_progress = value func _token() -> String: if _player_state == null: return "" return _player_state.token() func _player_id() -> int: if _player_state == null: return 0 var id: int = _player_state.int_value("id", 0) if id <= 0: id = _player_state.int_value("user_id", 0) return id func _own_player_data() -> Dictionary: if _player_state == null: return _player_data return _player_state.player_data() func _apply_player_stamina(stamina_data: Dictionary) -> void: if stamina_data.is_empty(): return var next_data := _own_player_data().duplicate(true) for key in STAMINA_KEYS: if stamina_data.has(key): next_data[key] = stamina_data.get(key) if _player_state != null: _player_state.set_data(next_data) _player_data = next_data if not _visiting_friend else _player_data else: _player_data = next_data if _player_inventory_facade != null: _player_inventory_facade.set_player_data(next_data) _render_player() func _apply_pet_battle_data(battle_data: Dictionary) -> void: if battle_data.is_empty(): return var attacker = battle_data.get("attacker_pet", {}) if attacker is Dictionary: _apply_pet_snapshot_to_player_data(_own_player_data(), attacker, true) var defender = battle_data.get("defender_pet", {}) if defender is Dictionary: _apply_pet_snapshot_to_player_data(_player_data, defender, false) if battle_data.has("weak_until"): var weak_until := int(battle_data.get("weak_until", 0)) if _visiting_friend and weak_until > 0: _player_data["pet_weak_until"] = weak_until _player_data["pet_weak"] = 1 if _panel_router != null: _panel_router.render_pet_display() func _play_pet_battle_vs() -> void: if _pet_battle_vs_overlay == null: return await _pet_battle_vs_overlay.play() func _apply_pet_snapshot_to_player_data(target_data: Dictionary, pet_data: Dictionary, own_player := false) -> void: var target_pet = target_data.get("pet", {}) if not target_pet is Dictionary: return if int(target_pet.get("id", 0)) != int(pet_data.get("id", 0)): return target_pet["hp"] = int(pet_data.get("hp", target_pet.get("hp", 0))) target_pet["max_hp"] = int(pet_data.get("max_hp", target_pet.get("max_hp", 1))) target_pet["fighting_capacity"] = int(pet_data.get("fighting_capacity", target_pet.get("fighting_capacity", 0))) target_data["pet"] = target_pet if own_player and _player_state != null: _player_state.set_data(target_data) if _player_inventory_facade != null: _player_inventory_facade.set_player_data(target_data) func _is_visiting_friend() -> bool: return _visiting_friend func _current_visited_friend_id() -> int: return _visited_friend_id func _visit_friend_home(row: Dictionary) -> void: if _friend_visit_controller == null: return await _friend_visit_controller.visit_friend_home(row) func _back_to_own_farm() -> void: if not _visiting_friend: return _visiting_friend = false _visited_friend_id = 0 _visited_friend_data.clear() _player_data = _player_state.player_data() if _player_state != null else {} _farm_state.setup_from_player_data(_player_data, LAND_COUNT) _lands_by_position = _farm_state.snapshot() _hide_selection_ui() _render_scene_skin() _render_house_art() _render_player() _render_lands() _apply_visit_mode_visuals() _refresh_scene_pet_display() func _enter_friend_farm(friend_data: Dictionary) -> void: var scene_data := friend_data.duplicate(true) var target_id := 0 if _friend_visit_controller != null and _friend_visit_controller.has_method("target_user_id"): target_id = int(_friend_visit_controller.target_user_id(scene_data)) if target_id <= 0: for key in ["uid", "user_id", "id"]: target_id = int(scene_data.get(key, 0)) if target_id > 0: break if target_id <= 0: _show_toast("好友数据异常") return scene_data["id"] = target_id scene_data["user_id"] = target_id _visiting_friend = true _visited_friend_id = target_id _visited_friend_data = scene_data _player_data = _visited_friend_data if _panel_router != null: _panel_router.close_modal_panel(false) _farm_state.setup_from_player_data(_player_data, LAND_COUNT) _lands_by_position = _farm_state.snapshot() _hide_selection_ui() _render_scene_skin() _render_house_art() _render_player() _render_lands() _apply_visit_mode_visuals() _refresh_scene_pet_display() _show_toast("正在访问:%s" % str(_player_data.get("nickname", _player_data.get("username", ""))).left(8)) func _apply_visit_mode_visuals() -> void: if back_home_art != null: back_home_art.visible = _visiting_friend if main_menu != null: main_menu.visible = not _visiting_friend if left_menu != null: left_menu.visible = not _visiting_friend if left_middle_menu != null: left_middle_menu.visible = not _visiting_friend if exchange_gold_art != null: exchange_gold_art.visible = false if add_land_art != null and _visiting_friend: add_land_art.visible = false if _add_land_hit != null and _visiting_friend: _add_land_hit.visible = false if _top_bar_view != null and _top_bar_view.has_method("set_visitor_mode"): _top_bar_view.set_visitor_mode(_visiting_friend) if _scene_hit_router != null and _scene_hit_router.has_method("set_visitor_mode"): _scene_hit_router.set_visitor_mode(_visiting_friend) func _refresh_scene_pet_display() -> void: if _panel_router != null: _panel_router.render_pet_display() if _visiting_friend and _pet_display != null: _pet_display.visible = false func _response_data(result: Dictionary, fallback: Variant = {}) -> Variant: # 服务层保留完整 HTTP 结果,UI 只关心业务 body.data;缺失时返回调用方指定兜底值。 var body = result.get("body", {}) if body is Dictionary and body.has("data"): return body.get("data") return fallback func _response_status_text(result: Dictionary, fallback := "操作失败") -> String: var status = result.get("game_status", result.get("status", 0)) if status == null: return I18nScript.t(fallback) var status_code := _status_code(status) if SERVER_STATUS_TEXT.has(status_code): return I18nScript.t(str(SERVER_STATUS_TEXT.get(status_code))) var body = result.get("body", {}) if body is Dictionary: var info := str(body.get("info", "")).strip_edges() if not info.is_empty(): return I18nScript.t(info) return "%s:%s" % [I18nScript.t(fallback), status] func _status_code(status: Variant) -> int: if status is int: return int(status) if status is float: return int(status) var text := str(status) return int(text) if text.is_valid_int() else -1 func _reward_fields_to_text(row: Dictionary, first_field: int, last_field: int, multiplier := 1) -> String: var parts: Array[String] = [] # 很多老配置把奖励拆成 fruit_id1...fruit_idN,这里统一拼回 CostRules 可解析的文本。 for index in range(first_field, last_field + 1): var raw := str(row.get("fruit_id%s" % index, "")).strip_edges() if raw.is_empty(): continue var pair := raw.split(":", false) if pair.size() != 2: continue var item_id := int(pair[0]) var count := int(pair[1]) * multiplier if item_id > 0 and count > 0: parts.append("%s:%s" % [item_id, count]) return ",".join(parts) func _format_reward_text(cost_text: String) -> String: var labels: Array[String] = [] for cost in _parse_cost_string(cost_text): var item_id := int(cost.get("item_id", 0)) var name: String = _game_config.get_item_name(item_id) labels.append("%s x%s" % [name, NumberFormatterScript.compact(int(cost.get("count", 0)))]) return I18nScript.list_separator().join(labels) func _add_reward_items(cost_text: String) -> void: if _player_inventory_facade == null: return # 活动和任务奖励统一进 Facade,货币和仓库道具会在同一个地方落账。 _player_inventory_facade.add_reward_items(_costs_to_text(_parse_cost_string(cost_text))) func _filter_removed_item_costs(costs: Array[Dictionary]) -> Array[Dictionary]: var result: Array[Dictionary] = [] for cost in costs: var item_id := int(cost.get("item_id", 0)) if _is_removed_item(item_id): continue result.append(cost) return result func _is_removed_item(item_id: int) -> bool: if item_id in REMOVED_BOX_ITEM_IDS: return true if _game_config == null: return false return _game_config.get_item_type2(item_id) == REMOVED_BOX_ITEM_TYPE func _costs_to_text(costs: Array[Dictionary]) -> String: var parts: Array[String] = [] for cost in costs: var item_id := int(cost.get("item_id", 0)) var count := int(cost.get("count", 0)) if item_id > 0 and count > 0: parts.append("%s:%s" % [item_id, count]) return ",".join(parts) func _set_texture_button_enabled(button_holder: Control, enabled: bool, normal_texture: Texture2D = null, disabled_texture: Texture2D = null) -> void: if button_holder == null: return button_holder.modulate = Color.WHITE if enabled else Color(0.78, 0.78, 0.78, 1.0) var art := button_holder.get_node_or_null("Art") as TextureRect if art != null: if enabled and normal_texture != null: art.texture = normal_texture elif not enabled and disabled_texture != null: art.texture = disabled_texture var hit := button_holder.get_node_or_null("Hit") as Button if hit != null: hit.disabled = not enabled func _sync_inventory_from_facade() -> void: if _player_inventory_facade == null: return # Facade 内部状态变化后回写主场景字段,保留旧面板仍然依赖的成员变量接口。 if not _visiting_friend: _player_data = _player_inventory_facade.player_data() _seed_items = _player_inventory_facade.seed_items() _fertilizer_items = _player_inventory_facade.fertilizer_items() _warehouse_items = _player_inventory_facade.warehouse_items() func _should_render_bag() -> bool: return (_seed_bag_view != null and _seed_bag_view.is_visible()) or (_bag_panel != null and _bag_panel.visible) func _render_bag() -> void: if _farm_interaction_coordinator != null: _farm_interaction_coordinator.render_bag(_seed_items, _fertilizer_items) func _on_bag_item_pressed(item: Dictionary, mode := "") -> void: if _farm_interaction_coordinator != null: _farm_interaction_coordinator.on_bag_item_pressed(item, mode) func _sow_seed(item: Dictionary) -> void: if _farm_controller == null: return await _farm_controller.sow_seed(item) func _gather_crop() -> void: if _farm_controller == null: return await _farm_controller.gather_crop() func _clear_land() -> void: if _farm_controller == null: return await _farm_controller.clear_land() func _use_fertilizer(item: Dictionary) -> void: if _farm_controller == null: return await _farm_controller.use_fertilizer(item) func _kick_disease(type: int) -> void: if _farm_controller == null: return await _farm_controller.kick_disease(type) func _interact_friend_land(action: String) -> void: if _friend_visit_controller == null: return await _friend_visit_controller.interact_land(action) func _apply_land_data(land_data: Dictionary) -> void: if land_data.is_empty(): return var position := int(land_data.get("position", -1)) if position < 0: return _farm_state.set_land(land_data) _lands_by_position = _farm_state.snapshot() if _visiting_friend: _replace_current_scene_land(land_data) else: _replace_player_land(land_data) func _add_player_exp(add_exp: int) -> void: if add_exp <= 0: return _player_state.add_exp(add_exp) _player_data = _player_state.player_data() if _player_inventory_facade != null: _player_inventory_facade.set_player_data(_player_data) _render_player() func _replace_player_land(land_data: Dictionary) -> void: _player_state.replace_land(land_data) _player_data = _player_state.player_data() if _player_inventory_facade != null: _player_inventory_facade.set_player_data(_player_data) func _replace_current_scene_land(land_data: Dictionary) -> void: var land_list = _player_data.get("landList", []) if not land_list is Array: _player_data["landList"] = [land_data.duplicate(true)] return var target_position := int(land_data.get("position", -1)) var replaced := false for index in range(land_list.size()): if land_list[index] is Dictionary and int(land_list[index].get("position", -1)) == target_position: land_list[index] = land_data.duplicate(true) replaced = true break if not replaced: land_list.append(land_data.duplicate(true)) _player_data["landList"] = land_list func _decrease_seed_item(item_id: int) -> void: if _player_inventory_facade == null: return _player_inventory_facade.decrease_seed_item(item_id) func _decrease_fertilizer_item(item_id: int) -> void: if _player_inventory_facade == null: return _player_inventory_facade.decrease_fertilizer_item(item_id) func _hide_selection_ui() -> void: if _main_menu_controller != null and _main_menu_controller.is_open(): _main_menu_controller.hide(true) if _farm_interaction_coordinator != null: _farm_interaction_coordinator.hide_selection_ui() func _hide_bag_panel() -> void: if _farm_interaction_coordinator != null: _farm_interaction_coordinator.hide_bag_panel() func _current_skin_graphic_id() -> int: var skin_item_id: int = int(_player_data.get("bg_image_id", 104004)) var config: Dictionary = _game_config.get_item_config(skin_item_id) if _game_config != null else {} if not config.is_empty(): return int(config.get("graphical_id", 5004)) return skin_item_id if skin_item_id > 0 else 5004 func _load_skin_texture(graphic_id: int, suffix: String) -> Texture2D: var path := "res://assets/egret/skin/%s%s" % [graphic_id, suffix] if not ResourceLoader.exists(path): path = "res://assets/egret/skin/5004%s" % suffix if _texture_provider != null and _texture_provider.has_method("standalone"): return _texture_provider.standalone(path) return load(path) func _seed_icon_id(item_id: int) -> int: return _game_config.get_item_icon_id(item_id) func _item_icon_id(item_id: int) -> int: return _game_config.get_item_icon_id(item_id) func _get_land_node(position: int) -> TextureRect: return game_view.get_node_or_null("Land%s" % position) as TextureRect