2026-05-29 19:54:56 +08:00

271 lines
10 KiB
GDScript
Raw Permalink 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.

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)