271 lines
10 KiB
GDScript
271 lines
10 KiB
GDScript
class_name PanelModule
|
||
extends RefCounted
|
||
|
||
const I18nScript := preload("res://scripts/core/i18n.gd")
|
||
|
||
# 面板模块基类。
|
||
#
|
||
# 设计目标:
|
||
# - 子面板只关心自己的 UI 和业务流程,不直接依赖 GameScene 的大量字段。
|
||
# - GameScene 通过 callbacks 暴露必要能力,例如玩家数据、库存刷新、Toast、弹窗开关。
|
||
# - 所有跨模块状态写入都从这里走统一 helper,后续要替换成更严格的 State/Facade 时只改这一层。
|
||
|
||
# 公共服务和工具引用由 GameScene 注入。
|
||
# 这里故意不写成强类型,避免每个面板都需要 preload 具体实现,降低模块之间的编译耦合。
|
||
var _ui
|
||
var _texture_provider
|
||
var _game_config
|
||
var _activity_api
|
||
var _owner: Node
|
||
var _context
|
||
|
||
# context 是面板到宿主的统一桥;callbacks 只保留给未迁完的旧面板兼容。
|
||
# 约定:子类不能直接访问 GameScene 字段,只能通过 _call/_call_async 间接调用。
|
||
var _callbacks: Dictionary = {}
|
||
var _panel_screen_size := Vector2(640.0, 960.0)
|
||
|
||
|
||
func setup(options: Dictionary) -> void:
|
||
# 所有面板统一使用字典注入,方便每个功能只拿自己需要的依赖。
|
||
_ui = options.get("ui_builder")
|
||
_texture_provider = options.get("texture_provider")
|
||
_game_config = options.get("game_config")
|
||
_activity_api = options.get("activity_api")
|
||
_owner = options.get("owner") as Node
|
||
_context = options.get("context")
|
||
_callbacks = options.get("callbacks", {})
|
||
if _callbacks.is_empty() and _context != null and _context.has_method("legacy_callbacks"):
|
||
_callbacks = _context.legacy_callbacks()
|
||
_panel_screen_size = options.get("panel_screen_size", _panel_screen_size)
|
||
|
||
|
||
func _clear_children(parent: Node) -> void:
|
||
# 面板多次 render 时统一清理动态节点,避免旧按钮信号和旧列表项残留。
|
||
for child in parent.get_children():
|
||
child.queue_free()
|
||
|
||
|
||
func _is_live_node(node: Variant) -> bool:
|
||
return node != null and is_instance_valid(node) and node is Node
|
||
|
||
|
||
func _is_current_node(candidate: Variant, current: Variant) -> bool:
|
||
return _is_live_node(candidate) and _is_live_node(current) and candidate == current
|
||
|
||
|
||
func _clear_children_if_live(parent: Variant) -> bool:
|
||
if not _is_live_node(parent):
|
||
return false
|
||
_clear_children(parent as Node)
|
||
return true
|
||
|
||
|
||
func _format_timestamp(unix_time: int) -> String:
|
||
if unix_time <= 0:
|
||
return ""
|
||
var dt := Time.get_datetime_dict_from_unix_time(unix_time)
|
||
return "%02d-%02d %02d:%02d" % [int(dt.get("month", 1)), int(dt.get("day", 1)), int(dt.get("hour", 0)), int(dt.get("minute", 0))]
|
||
|
||
|
||
func _player_data() -> Dictionary:
|
||
# 返回的是宿主玩家字典引用,面板内修改会直接反映到 HUD/后续接口参数。
|
||
# 后续如果引入 PlayerInventoryFacade,这里是替换入口。
|
||
var result = _call("player_data", [], {})
|
||
return result if result is Dictionary else {}
|
||
|
||
|
||
func _token() -> String:
|
||
return str(_call("token", [], ""))
|
||
|
||
|
||
func _player_id() -> int:
|
||
return int(_call("player_id", [], 0))
|
||
|
||
|
||
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 _show_toast(message: String) -> void:
|
||
_call("show_toast", [message])
|
||
|
||
|
||
func _open_modal(panel: Control) -> void:
|
||
_call("open_modal_panel", [panel])
|
||
|
||
|
||
func _close_modal(animated := true) -> void:
|
||
_call("close_modal_panel", [animated])
|
||
|
||
|
||
func _response_data(result: Dictionary, fallback: Variant = {}) -> Variant:
|
||
# ApiClient 返回统一包裹结构,业务数据固定从 body.data 取。
|
||
return _call("response_data", [result, fallback], fallback)
|
||
|
||
|
||
func _response_status_text(result: Dictionary, fallback := "操作失败") -> String:
|
||
return I18nScript.t(str(_call("response_status_text", [result, fallback], fallback)))
|
||
|
||
|
||
func _parse_cost_string(cost_text: String) -> Array[Dictionary]:
|
||
# 旧配置大量使用 "item_id:count,item_id:count" 字符串。
|
||
# 面板不要自己 split,统一走 CostRules,兼容金豆/钻石/道具的后续特殊规则。
|
||
var result = _call("parse_cost_string", [cost_text], [])
|
||
var costs: Array[Dictionary] = []
|
||
if result is Array:
|
||
for cost in result:
|
||
if cost is Dictionary:
|
||
costs.append(cost)
|
||
return costs
|
||
|
||
|
||
func _reward_fields_to_text(row: Dictionary, first_field: int, last_field: int, multiplier := 1) -> String:
|
||
# 旧配置常用 fruit_id1..fruit_idN 表示奖励或消耗。
|
||
# 返回统一 cost_text,方便复用库存校验、奖励展示和道具格渲染。
|
||
return str(_call("reward_fields_to_text", [row, first_field, last_field, multiplier], ""))
|
||
|
||
|
||
func _format_reward_text(cost_text: String) -> String:
|
||
return str(_call("format_reward_text", [cost_text], ""))
|
||
|
||
|
||
func _add_reward_items(cost_text: String) -> void:
|
||
_call("add_reward_items", [cost_text])
|
||
|
||
|
||
func _render_player() -> void:
|
||
_call("render_player")
|
||
|
||
|
||
func _add_player_exp(amount: int) -> void:
|
||
_call("add_player_exp", [amount])
|
||
|
||
|
||
func _refresh_store_house() -> Variant:
|
||
# 库存刷新是异步接口,面板领取/购买/消耗后都要 await,保证再次 render 时数量正确。
|
||
return await _call_async("refresh_store_house")
|
||
|
||
|
||
func _has_costs(cost_text: String) -> bool:
|
||
# 所有按钮可用态都应该走统一成本校验,避免不同面板对材料数量理解不一致。
|
||
return bool(_call("has_costs", [cost_text], false))
|
||
|
||
|
||
func _apply_cost_deduction(cost_text: String) -> void:
|
||
# 后端成功后才本地扣减,用于让 UI 立即刷新;失败时不能提前扣。
|
||
_call("apply_cost_deduction", [cost_text])
|
||
|
||
|
||
func _owned_amount(item_id: int) -> int:
|
||
return int(_call("owned_amount", [item_id], 0))
|
||
|
||
|
||
func _decrease_warehouse_item(item_id: int, count: int) -> void:
|
||
_call("decrease_warehouse_item", [item_id, count])
|
||
|
||
|
||
func _set_texture_button_enabled(button_holder: Control, enabled: bool, normal_texture: Texture2D = null, disabled_texture: Texture2D = null) -> void:
|
||
# TextureButton 是自定义 Control + Button 组合,禁用逻辑需要同时改透明度、贴图和 hit.disabled。
|
||
_call("set_texture_button_enabled", [button_holder, enabled, normal_texture, disabled_texture])
|
||
|
||
|
||
func _format_duration(seconds: int) -> String:
|
||
return I18nScript.t(str(_call("format_duration", [seconds], I18nScript.format_duration(seconds))))
|
||
|
||
|
||
func _get_item_icon_texture(icon_id: int) -> Texture2D:
|
||
# 图标路径兼容 item_icon、icon、petIcon、plant 多个旧资源目录。
|
||
if _texture_provider != null and _texture_provider.has_method("item_icon"):
|
||
return _texture_provider.item_icon(icon_id)
|
||
var result = _call("get_item_icon_texture", [icon_id], null)
|
||
return result as Texture2D
|
||
|
||
|
||
func _style_label(label: Label, size: int, color: Color, outline: int, outline_color: Color) -> void:
|
||
_call("style_label", [label, size, color, outline, outline_color])
|
||
|
||
|
||
func _apply_empty_button_style(button: Button) -> void:
|
||
_call("apply_empty_button_style", [button])
|
||
|
||
|
||
func _create_tab_bar(parent: Control, name: String, labels: Array, position: Vector2, selected_index: int, callback: Callable, base_height := 40.0) -> Control:
|
||
return _ui.create_tab_bar(parent, name, labels, position, selected_index, callback, base_height)
|
||
|
||
|
||
func _update_tab_bar(group: Control, selected_index: int) -> void:
|
||
_ui.update_tab_bar(group, selected_index)
|
||
|
||
|
||
func _add_cost_slots(parent: Control, cost_text: String, position: Vector2, max_count: int, gap := 84.0, size := Vector2(76.0, 76.0)) -> void:
|
||
# 多数旧面板都用横向材料格。这里只负责展示,是否足够由 _has_costs 决定。
|
||
var costs := _parse_cost_string(cost_text)
|
||
for index in range(mini(costs.size(), max_count)):
|
||
var cost := costs[index]
|
||
_ui.add_item_slot(parent, "CostSlot%s" % index, int(cost.get("item_id", 0)), int(cost.get("count", 0)), position + Vector2(gap * index, 0.0), size)
|
||
|
||
|
||
func _find_config_by_id(rows: Array[Dictionary], id: int) -> Dictionary:
|
||
for row in rows:
|
||
if int(row.get("id", 0)) == id:
|
||
return row
|
||
return {}
|
||
|
||
|
||
func _first_cost(cost_text: String) -> Dictionary:
|
||
# 周卡/月卡、签到等配置只取首个奖励时使用。
|
||
var costs := _parse_cost_string(cost_text)
|
||
return costs[0] if not costs.is_empty() else {}
|
||
|
||
|
||
func _same_local_day(left_time: int, right_time: int) -> bool:
|
||
# 旧后端用 Unix 秒记录领取时间,日卡类奖励需要按本地日期判断是否已领。
|
||
if left_time <= 0 or right_time <= 0:
|
||
return false
|
||
var left := Time.get_datetime_dict_from_unix_time(left_time)
|
||
var right := Time.get_datetime_dict_from_unix_time(right_time)
|
||
return int(left.get("year", 0)) == int(right.get("year", 0)) and int(left.get("month", 0)) == int(right.get("month", 0)) and int(left.get("day", 0)) == int(right.get("day", 0))
|
||
|
||
|
||
func _normalize_reward_triplets(raw: String) -> String:
|
||
# 游乐园奖励配置可能是 item:type:count,这里转成统一 item:count 文本给奖励展示层。
|
||
var parts: Array[String] = []
|
||
for piece in raw.split(",", false):
|
||
var fields := piece.split(":", false)
|
||
if fields.size() >= 3:
|
||
parts.append("%s:%s" % [fields[0], fields[2]])
|
||
elif fields.size() == 2:
|
||
parts.append(piece)
|
||
return ",".join(parts)
|
||
|
||
|
||
func _callback(name: String) -> Callable:
|
||
if _context != null and _context.has_method("callback"):
|
||
return _context.callback(name)
|
||
return _callbacks.get(name, Callable()) as Callable
|
||
|
||
|
||
func _call(name: String, args: Array = [], fallback: Variant = null) -> Variant:
|
||
# 同步 callback 的统一入口。无效 callback 必须静默返回 fallback,保证面板可单独 check-only。
|
||
if _context != null and _context.has_method("call_action"):
|
||
return _context.call_action(name, args, fallback)
|
||
var callable := _callback(name)
|
||
if not callable.is_valid():
|
||
return fallback
|
||
return callable.callv(args)
|
||
|
||
|
||
func _call_async(name: String, args: Array = []) -> Variant:
|
||
# Godot 允许 await Callable.callv() 返回的异步函数;这里集中处理缺失 callback。
|
||
if _context != null and _context.has_method("call_action_async"):
|
||
return await _context.call_action_async(name, args)
|
||
var callable := _callback(name)
|
||
if not callable.is_valid():
|
||
return null
|
||
return await callable.callv(args)
|