hy-farm/godot/FarmGodot/scripts/ui/game_scene.gd
2026-05-30 20:23:54 +08:00

1251 lines
44 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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