class_name FarmController extends RefCounted # 土地玩法控制器。 # # 这个类只处理“用户对某块土地执行动作”的业务闭环: # 1. 读取当前选中土地和 token。 # 2. 调后端接口。 # 3. 用 FarmMapper 把后端返回归一化成土地数据。 # 4. 通过 callbacks 通知 GameScene 更新状态、库存、UI。 # # 它不直接持有节点,也不直接画 UI;这样土地玩法可以独立于具体场景布局维护。 var _farm_api var _farm_mapper # callbacks 是控制器和 UI/状态层的边界。 # 目前由 GameScene 提供,后续可以替换成专门的 FarmInteractionCoordinator。 var _callbacks: Dictionary = {} var _disease_type_bug := 1 var _disease_type_grass := 2 var _disease_type_water := 0 var _item_id_bug_killer := 108001 var _item_id_grass_killer := 109001 func setup(options: Dictionary) -> void: # 疾病类型和道具 id 由宿主注入,避免在控制器里散落硬编码。 _farm_api = options.get("farm_api") _farm_mapper = options.get("farm_mapper") _callbacks = options.get("callbacks", {}) _disease_type_bug = int(options.get("disease_type_bug", _disease_type_bug)) _disease_type_grass = int(options.get("disease_type_grass", _disease_type_grass)) _disease_type_water = int(options.get("disease_type_water", _disease_type_water)) _item_id_bug_killer = int(options.get("item_id_bug_killer", _item_id_bug_killer)) _item_id_grass_killer = int(options.get("item_id_grass_killer", _item_id_grass_killer)) func sow_seed(item: Dictionary) -> void: # 播种成功后,后端可能只返回部分字段;FarmMapper 会用旧土地数据补齐位置/等级等信息。 var position := _selected_position() if position < 0 or _is_busy(): return var token := _token() var item_id := int(item.get("item_id", 0)) if token.is_empty() or item_id <= 0 or _farm_api == null: return _set_busy(true) var result: Dictionary = await _farm_api.sow_seed(token, position, item_id) _set_busy(false) if not result.get("ok", false): _warn_and_show("farm/sow-seeds", result, "播种失败") return # UI 先用接口结果立即刷新,再异步刷新仓库,保证播种体验不被库存接口阻塞太久。 var land_data: Dictionary = _farm_mapper.land_data_from_sow_result(result, position, item_id, _land_data(position)) _call("apply_land_data", [land_data]) _call("decrease_seed_item", [item_id]) _call("hide_bag_panel") _call("hide_land_light") _call("render_lands") _show_toast("播种成功") await _call_async("refresh_store_house") func gather_crop() -> void: # 收获是土地状态和玩家经验同时变化的动作,因此成功后要刷新土地、经验和仓库。 var position := _selected_position() if position < 0 or _is_busy(): return var token := _token() if token.is_empty() or _farm_api == null: return _set_busy(true) var result: Dictionary = await _farm_api.gather_crop(token, position) _set_busy(false) if not result.get("ok", false): _warn_and_show("farm/gather-crop", result, "收获失败") return var land_data: Dictionary = _farm_mapper.land_data_from_result(result, position, _land_data(position)) _call("apply_land_data", [land_data]) _call("add_player_exp", [int(land_data.get("add_exp", 0))]) var reward_text := str(land_data.get("reward_text", "")) if reward_text.is_empty(): var item_id := int(land_data.get("item_id", 0)) var item_count := int(land_data.get("item_add_num", 0)) if item_id > 0 and item_count > 0: reward_text = "%s:%s" % [item_id, item_count] if not reward_text.is_empty(): _call("add_reward_items", [reward_text]) _call("hide_selection_ui") _call("render_lands") await _call_async("refresh_store_house") func clear_land() -> void: # 铲除只改变土地本身,不产出奖励;后端成功后关闭选择态并重绘土地。 var position := _selected_position() if position < 0 or _is_busy(): return var token := _token() if token.is_empty() or _farm_api == null: return _set_busy(true) var result: Dictionary = await _farm_api.clear_land(token, position) _set_busy(false) if not result.get("ok", false): _warn_and_show("farm/clear-land", result, "铲除失败") return var land_data: Dictionary = _farm_mapper.land_data_from_result(result, position, _land_data(position)) _call("apply_land_data", [land_data]) _call("hide_selection_ui") _call("render_lands") func use_fertilizer(item: Dictionary) -> void: # 施肥后需要重新打开植物提示,因为成熟时间/阶段可能立即变化。 var position := _selected_position() if position < 0 or _is_busy(): return var token := _token() var item_id := int(item.get("item_id", 0)) if token.is_empty() or item_id <= 0 or _farm_api == null: return _set_busy(true) var result: Dictionary = await _farm_api.fertilize(token, position, item_id) _set_busy(false) if not result.get("ok", false): _warn_and_show("farm/fertilize", result, "施肥失败") return var land_data: Dictionary = _farm_mapper.land_data_from_result(result, position, _land_data(position)) _call("apply_land_data", [land_data]) _call("decrease_fertilizer_item", [item_id]) _call("hide_bag_panel") _call("render_lands") await _call_async("refresh_store_house") _call("show_plant_tip", [position, land_data]) func kick_disease(type: int) -> void: # 原版把浇水、除虫、除草都走 farm/disease,通过 type 区分。 # 只有除虫/除草消耗仓库道具;浇水不扣除道具。 var position := _selected_position() if position < 0 or _is_busy(): return var token := _token() if token.is_empty() or _farm_api == null: return _set_busy(true) var result: Dictionary = await _farm_api.kick_disease(token, position, type) _set_busy(false) if not result.get("ok", false): _warn_and_show("farm/disease", result, _disease_failure_text(type)) return var land_data: Dictionary = _farm_mapper.land_data_from_result(result, position, _land_data(position)) _call("apply_land_data", [land_data]) if type == _disease_type_bug: _call("decrease_warehouse_item", [_item_id_bug_killer, 1]) elif type == _disease_type_grass: _call("decrease_warehouse_item", [_item_id_grass_killer, 1]) _call("hide_selection_ui") _call("render_lands") await _call_async("refresh_store_house") _call("show_toast", ["浇水成功" if type == _disease_type_water else "操作成功"]) func _selected_position() -> int: return int(_call("selected_position", [], -1)) func _token() -> String: return str(_call("token", [], "")) func _land_data(position: int) -> Dictionary: var result = _call("land_data", [position], {}) return result if result is Dictionary else {} func _is_busy() -> bool: return bool(_call("is_operation_in_progress", [], false)) func _set_busy(value: bool) -> void: _call("set_operation_in_progress", [value]) func _response_status_text(result: Dictionary, fallback := "操作失败") -> String: return str(_call("response_status_text", [result, fallback], fallback)) func _show_toast(message: String) -> void: if not message.is_empty(): _call("show_toast", [message]) func _warn_and_show(endpoint: String, result: Dictionary, fallback: String) -> void: var status = result.get("game_status", result.get("status", 0)) print_verbose("%s failed: %s" % [endpoint, status]) _show_toast(_response_status_text(result, fallback)) func _disease_failure_text(type: int) -> String: if type == _disease_type_water: return "浇水失败" if type == _disease_type_bug: return "除虫失败" if type == _disease_type_grass: return "除草失败" return "操作失败" func _call(name: String, args: Array = [], fallback: Variant = null) -> Variant: # 控制器可以独立 check-only;缺失 callback 时返回 fallback,不抛异常。 var callable: Callable = _callbacks.get(name, Callable()) if not callable.is_valid(): return fallback return callable.callv(args) func _call_async(name: String, args: Array = []) -> Variant: # 异步 callback 统一 await,避免调用方混用 call/callv 产生不可见时序问题。 var callable: Callable = _callbacks.get(name, Callable()) if not callable.is_valid(): return null return await callable.callv(args)