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

921 lines
41 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 PetPanel
extends PanelModule
const EgretMovieClipCacheScript := preload("res://scripts/resources/egret_movie_clip_cache.gd")
const NumberFormatterScript := preload("res://scripts/core/number_formatter.gd")
const UiMotionScript := preload("res://scripts/ui/common/ui_motion.gd")
const BATTLE_PET_SCENE_ATLAS_PATH := "res://assets/egret/assets/swf/dog1.png"
const BATTLE_PET_BG_DIR := "res://assets/egret/assets/game/battlePet"
const PET_PATROL_START := Vector2(269.34, 490.0)
const PET_PATROL_END := Vector2(599.34, 625.0)
const PET_SCENE_POSITION := Vector2(182.0, 374.0)
const PET_SCENE_SIZE := Vector2(132.0, 112.0)
const PET_SCENE_ORIGIN := Vector2(72.0, 86.0)
const PET_SCENE_SCALE := 0.68
const PET_PATROL_SCENE_SCALE := 0.8
const DEFAULT_PET_HP := 100
const PET_TYPE_TEXT := ["无类型", "近身攻击", "远程攻击"]
# 宠物面板:
# - 负责读取宠物列表、场景宠物牌展示、出战/喂养等宠物相关入口。
# - 场景上的宠物展示节点由外部注入,本面板只重绘其子节点,避免重复创建全局层。
# - 宠物排序优先出战状态,再按 id保证主场景默认展示当前出战宠物。
var _pet_api
var _shop_api
var _dog_food_item_ids: Array = []
var _movie_clip_cache = EgretMovieClipCacheScript.new()
var _rows: Array[Dictionary] = []
var _selected_id := 0
var _tab_index := 0
var _display: Control
var _list_container: Control
var _panel: Control
var _food_popup: Control
var _selected_food_item_id := 0
var _feeding_pet_id := 0
var _display_sprite: TextureRect
var _display_frames: Array[Dictionary] = []
var _display_frame_index := 0
var _display_frame_rate := 12
var _display_frame_timer_node: Timer
var _display_frame_origin := PET_SCENE_ORIGIN
var _display_frame_scale := PET_SCENE_SCALE
var _display_fallback_icon_id := 0
func setup(options: Dictionary) -> void:
super.setup(options)
_pet_api = options.get("pet_api")
_shop_api = options.get("shop_api")
_dog_food_item_ids = options.get("dog_food_item_ids", _dog_food_item_ids)
func set_display(display: Control) -> void:
_display = display
func refresh_display() -> void:
await _refresh_pet_rows()
render_display()
func _refresh_pet_rows() -> void:
var token := _token()
if token.is_empty() or _pet_api == null:
return
var result: Dictionary = await _pet_api.fetch_list(token)
if not _is_live_node(_display):
return
if not result.get("ok", false):
push_warning("pet/get-list failed: %s" % result.get("game_status", result.get("status", 0)))
return
var data = _response_data(result, {})
_rows.clear()
if data is Dictionary:
for row in data.get("list", []):
if row is Dictionary:
_rows.append(_normalize_pet_row(row))
_rows.sort_custom(func(left: Dictionary, right: Dictionary) -> bool:
var left_war := int(left.get("is_enter_war", 0))
var right_war := int(right.get("is_enter_war", 0))
if left_war == right_war:
return int(left.get("id", 0)) < int(right.get("id", 0))
return left_war > right_war
)
if not _rows.is_empty():
_selected_id = int(_rows[0].get("id", 0))
func render_display() -> void:
var display := _display
if not _is_live_node(display):
return
_stop_display_animation()
_clear_children(display)
if _rows.is_empty():
display.visible = false
return
display.visible = true
var pet: Dictionary = _rows[0]
var config: Dictionary = _pet_config(pet)
var icon_id: int = int(config.get("graphical_id", pet.get("graphical_id", 11001)))
var can_patrol := _pet_can_patrol(pet)
if can_patrol:
display.position = PET_PATROL_START
display.size = Vector2(140.0, 140.0)
display.pivot_offset = Vector2.ZERO
display.scale = Vector2.ONE
else:
display.position = PET_SCENE_POSITION
display.size = PET_SCENE_SIZE
display.pivot_offset = display.size * 0.5
display.scale = Vector2.ONE
_ui.add_texture(_display, "Shadow", _texture_provider.common("top_cir"), Vector2(28.0, 84.0), Vector2(78.0, 28.0))
_render_scene_pet(pet, icon_id)
_render_scene_speech(pet)
if can_patrol:
_start_battle_pet_patrol()
else:
_start_scene_motion(false)
func open() -> void:
_close_modal(false)
await _refresh_store_house()
if _rows.is_empty():
await _refresh_pet_rows()
_panel = _ui.create_panel_shell("PetPanel", Vector2(640.0, 900.0), _texture_provider.frame(_texture_provider.atlas_path("battle_pet"), "battle_pet_canzhan_26_png"), _texture_provider.frame(_texture_provider.atlas_path("battle_pet"), "battle_pet_canzhan12_png"))
var icon_node := _panel.get_node_or_null("PanelIcon") as TextureRect
if icon_node != null:
icon_node.visible = false
_ui.add_content_panel_background(_panel, "PetPageBg", Vector2(50.0, 167.0), Vector2(540.0, 714.0))
_tab_index = 0
_ui.create_tab_bar(_panel, "PetTabs", ["战宠", "宠物列表", "宠物商店"], Vector2(89.0, 118.0), _tab_index, Callable(self, "_on_tab_pressed"), 40.0)
_list_container = Control.new()
_list_container.name = "PetList"
_list_container.position = Vector2.ZERO
_list_container.size = _panel_screen_size
_list_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
_panel.add_child(_list_container)
_open_modal(_panel)
render()
func render() -> void:
var list_container := _list_container
if not _clear_children_if_live(list_container):
return
match _tab_index:
0:
_render_current_pet_page(list_container)
1:
_render_pet_list_page(list_container)
2:
_render_pet_shop_page(list_container)
func _render_current_pet_page(list_container: Control) -> void:
if _rows.is_empty():
_ui.add_panel_label(list_container, "Empty", "您没有宠物", Vector2(120.0, 430.0), Vector2(400.0, 60.0), 28, Color.WHITE, 2, Color.html("#7d1c1c"))
return
var pet: Dictionary = _selected_pet()
if pet.is_empty():
pet = _rows[0]
_selected_id = int(pet.get("id", 0))
var config: Dictionary = _pet_config(pet)
var icon_id: int = int(config.get("graphical_id", pet.get("graphical_id", 11001)))
_render_base_panel(pet, config, icon_id)
_render_pet_radio_list()
func _on_tab_pressed(index: int) -> void:
_tab_index = index
_update_tab_bar(_panel.get_node_or_null("PetTabs") as Control, _tab_index)
render()
func _selected_pet() -> Dictionary:
for pet in _rows:
if int(pet.get("id", 0)) == _selected_id:
return pet
return {}
func _select(pet_id: int) -> void:
_selected_id = pet_id
render()
func _power(pet: Dictionary) -> int:
if int(pet.get("fighting_capacity", 0)) > 0:
return int(pet.get("fighting_capacity", 0))
var config := _pet_config(pet)
return ceili(0.2 * _pet_attr(pet, config, "blood") + _pet_attr(pet, config, "attack") + 2.0 * _pet_attr(pet, config, "anti") + 3.0 * _pet_attr(pet, config, "crit") + 3.0 * _pet_attr(pet, config, "dodge"))
func _render_scene_pet(pet: Dictionary, icon_id: int) -> void:
var clip_name := "hungry" if int(pet.get("feed", 0)) <= 0 else "idle"
var label_name := "0" if int(pet.get("feed", 0)) > 0 else ""
_display_sprite = TextureRect.new()
_display_sprite.name = "BattlePetMovieClip"
_display_sprite.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
_display_sprite.stretch_mode = TextureRect.STRETCH_SCALE
_display_sprite.mouse_filter = Control.MOUSE_FILTER_IGNORE
_display.add_child(_display_sprite)
var origin := Vector2.ZERO if _pet_can_patrol(pet) else PET_SCENE_ORIGIN
var scale := PET_PATROL_SCENE_SCALE if _pet_can_patrol(pet) else PET_SCENE_SCALE
_set_display_clip(BATTLE_PET_SCENE_ATLAS_PATH, clip_name, label_name, origin, scale, icon_id)
_ensure_display_frame_timer()
func _render_scene_speech(pet: Dictionary) -> void:
var label := Label.new()
label.name = "Speech"
label.text = I18nScript.t("饿了..." if int(pet.get("feed", 0)) <= 0 else "汪~")
label.position = Vector2(18.0, 0.0)
label.size = Vector2(94.0, 28.0)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.modulate.a = 0.0
_style_label(label, 18, Color.html("#fffaf0"), 2, Color.html("#7d1c1c"))
_display.add_child(label)
if _owner == null:
label.modulate.a = 1.0
return
var tween := _owner.create_tween().set_loops()
tween.tween_interval(0.4)
tween.tween_property(label, "modulate:a", 1.0, 0.22)
tween.tween_interval(1.3)
tween.tween_property(label, "modulate:a", 0.0, 0.28)
tween.tween_interval(3.4)
_display.set_meta("speech_tween", tween)
func _set_display_clip(atlas_path: String, clip_name: String, label_name: String, origin: Vector2, frame_scale: float, fallback_icon_id: int) -> void:
if _display_sprite == null or not is_instance_valid(_display_sprite):
return
_display_frame_origin = origin
_display_frame_scale = frame_scale
_display_fallback_icon_id = fallback_icon_id
_display_frames = _movie_clip_cache.frames_for_label(atlas_path, clip_name, label_name).duplicate(false) if not label_name.is_empty() else _movie_clip_cache.frames(atlas_path, clip_name).duplicate(false)
_display_frame_index = 0
if _display_frames.is_empty():
_display_sprite.texture = _get_item_icon_texture(_display_fallback_icon_id)
_display_sprite.position = _display_frame_origin + Vector2(16.0, 10.0) * _display_frame_scale
_display_sprite.size = Vector2(98.0, 98.0) * _display_frame_scale
return
_display_frame_rate = int(_display_frames[0].get("frame_rate", 12))
_apply_display_frame()
var timer := _display_frame_timer()
if timer != null:
timer.wait_time = 1.0 / maxf(float(_display_frame_rate), 1.0)
func _ensure_display_frame_timer() -> void:
if _display == null or not is_instance_valid(_display):
return
var timer := _display_frame_timer()
if timer != null:
timer.wait_time = 1.0 / maxf(float(_display_frame_rate), 1.0)
if timer.is_stopped():
timer.start()
return
timer = Timer.new()
timer.name = "BattlePetFrameTimer"
timer.wait_time = 1.0 / maxf(float(_display_frame_rate), 1.0)
timer.timeout.connect(Callable(self, "_advance_display_frame"))
_display.add_child(timer)
_display_frame_timer_node = timer
timer.start()
func _display_frame_timer() -> Timer:
if _display_frame_timer_node != null and is_instance_valid(_display_frame_timer_node) and not _display_frame_timer_node.is_queued_for_deletion():
return _display_frame_timer_node
if _display == null or not is_instance_valid(_display):
_display_frame_timer_node = null
return null
var timer := _display.get_node_or_null("BattlePetFrameTimer") as Timer
if timer == null or timer.is_queued_for_deletion():
_display_frame_timer_node = null
return null
_display_frame_timer_node = timer
return timer
func _stop_display_frame_timer() -> void:
var timer := _display_frame_timer()
if timer != null:
timer.stop()
var parent := timer.get_parent()
if parent != null:
parent.remove_child(timer)
timer.queue_free()
_display_frame_timer_node = null
func _start_battle_pet_patrol() -> void:
if _owner == null or _display == null:
return
UiMotionScript.play_patrol_loop(_owner, _display, PET_PATROL_START, PET_PATROL_END, {
"start_delay": 3.0,
"forward_duration": 5.0,
"back_duration": 5.0,
"idle_delay": 2.0,
"before_forward": Callable(self, "_on_battle_pet_walk_forward"),
"before_back": Callable(self, "_on_battle_pet_walk_back"),
"before_idle_tail": Callable(self, "_on_battle_pet_idle_tail"),
"before_idle": Callable(self, "_on_battle_pet_idle"),
"meta_name": "scene_tween",
})
func _on_battle_pet_walk_forward() -> void:
_set_display_clip(BATTLE_PET_SCENE_ATLAS_PATH, "idle", "1", Vector2.ZERO, PET_PATROL_SCENE_SCALE, _display_fallback_icon_id)
func _on_battle_pet_walk_back() -> void:
_set_display_clip(BATTLE_PET_SCENE_ATLAS_PATH, "idle", "3", Vector2.ZERO, PET_PATROL_SCENE_SCALE, _display_fallback_icon_id)
func _on_battle_pet_idle_tail() -> void:
_set_display_clip(BATTLE_PET_SCENE_ATLAS_PATH, "idle", "4", Vector2.ZERO, PET_PATROL_SCENE_SCALE, _display_fallback_icon_id)
func _on_battle_pet_idle() -> void:
_set_display_clip(BATTLE_PET_SCENE_ATLAS_PATH, "idle", "0", Vector2.ZERO, PET_PATROL_SCENE_SCALE, _display_fallback_icon_id)
func _start_scene_motion(can_walk: bool) -> void:
if _owner == null or _display == null:
return
var tween := _owner.create_tween().set_loops()
if can_walk:
tween.tween_property(_display, "position:x", PET_SCENE_POSITION.x + 8.0, 1.8).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
tween.tween_property(_display, "position:x", PET_SCENE_POSITION.x, 1.8).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
else:
tween.tween_property(_display, "position:y", PET_SCENE_POSITION.y - 2.0, 1.1).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
tween.tween_property(_display, "position:y", PET_SCENE_POSITION.y, 1.1).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
_display.set_meta("scene_tween", tween)
func _stop_display_animation() -> void:
if not _is_live_node(_display):
return
_stop_display_frame_timer()
for meta_name in ["scene_tween", "speech_tween"]:
var value = _display.get_meta(meta_name) if _display.has_meta(meta_name) else null
if value is Tween:
(value as Tween).kill()
_display.set_meta(meta_name, null)
_display_sprite = null
_display_frames = []
_display_frame_index = 0
func _advance_display_frame() -> void:
if _display_sprite == null or not is_instance_valid(_display_sprite) or _display_frames.is_empty():
return
_display_frame_index = (_display_frame_index + 1) % _display_frames.size()
_apply_display_frame()
func _apply_display_frame() -> void:
if _display_sprite == null or _display_frames.is_empty():
return
var frame: Dictionary = _display_frames[_display_frame_index]
var texture := frame.get("texture") as Texture2D
if texture == null:
return
var offset := Vector2.ZERO
var offset_value = frame.get("offset", Vector2.ZERO)
if offset_value is Vector2:
offset = offset_value
var frame_size := Vector2(1.0, 1.0)
var size_value = frame.get("size", frame_size)
if size_value is Vector2:
frame_size = size_value
_display_sprite.texture = texture
_display_sprite.position = _display_frame_origin + offset * _display_frame_scale
_display_sprite.size = frame_size * _display_frame_scale
func _render_base_panel(pet: Dictionary, config: Dictionary, icon_id: int) -> void:
_ui.add_texture(_list_container, "PetHeadBg", _battle_bg("battle_pet_bg8"), Vector2(56.0, 168.0), Vector2(163.0, 216.0))
_ui.add_texture(_list_container, "PetHead", _get_item_icon_texture(icon_id), Vector2(79.0, 194.0), Vector2(120.0, 120.0), TextureRect.STRETCH_KEEP_ASPECT_CENTERED)
_ui.add_panel_label(_list_container, "PowerText", "宠物战力:", Vector2(234.0, 196.0), Vector2(110.0, 30.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(_list_container, "PowerValue", str(_power(pet)), Vector2(340.0, 196.0), Vector2(170.0, 30.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(_list_container, "WayText", "攻击方式:", Vector2(236.0, 232.0), Vector2(104.0, 30.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(_list_container, "WayValue", _pet_type_name(int(config.get("type", 0))), Vector2(340.0, 233.0), Vector2(160.0, 30.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(_list_container, "LevelText", "宠物等级:", Vector2(236.0, 270.0), Vector2(104.0, 30.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(_list_container, "LevelValue", str(int(pet.get("level", 1))), Vector2(340.0, 270.0), Vector2(70.0, 30.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(_list_container, "FightFeedText", "战斗消耗:", Vector2(419.0, 271.0), Vector2(108.0, 30.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(_list_container, "FightFeedValue", str(int(config.get("hunger", 0))), Vector2(523.0, 271.0), Vector2(48.0, 30.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
var war_texture := _battle_frame("battle_pet_canzhan24_png") if int(pet.get("is_enter_war", 0)) == 1 else _battle_frame("battle_pet_canzhan_png")
_ui.add_texture_button(_list_container, "SetWar", war_texture, Vector2(96.5, 358.0), Vector2(90.0, 38.0), "", Callable(self, "_set_war"), 1)
_ui.add_texture_button(_list_container, "FeedPet", _texture_provider.common("c_btn2"), Vector2(202.0, 356.0), Vector2(86.0, 42.0), "喂养", Callable(self, "_open_food_popup").bind(int(pet.get("id", 0))), 20)
_ui.add_panel_label(_list_container, "ExpText", "经验值:", Vector2(237.0, 317.0), Vector2(86.0, 28.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_add_battle_progress_bar(_list_container, Vector2(326.0, 312.0), int(pet.get("exp", 0)), _next_level_exp(int(pet.get("level", 1))))
_ui.add_panel_label(_list_container, "HungerText", "饱食度:", Vector2(237.5, 352.5), Vector2(88.0, 28.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(_list_container, "HungerValue", str(int(pet.get("feed", 0))), Vector2(332.0, 352.5), Vector2(92.0, 28.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_texture(_list_container, "AttrBg", _battle_bg("battle_pet_bg3"), Vector2(74.0, 403.0), Vector2(498.0, 128.0))
_ui.add_panel_label(_list_container, "HpText", "血量:", Vector2(106.5, 418.0), Vector2(70.0, 28.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(_list_container, "HpValue", _pet_health_text(pet), Vector2(175.5, 418.0), Vector2(132.0, 28.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(_list_container, "AttackText", "攻击:", Vector2(107.0, 456.0), Vector2(70.0, 28.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(_list_container, "AttackValue", str(_pet_attr(pet, config, "attack")), Vector2(175.5, 454.0), Vector2(132.0, 28.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(_list_container, "CritText", "暴击:", Vector2(107.0, 495.0), Vector2(70.0, 28.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(_list_container, "CritValue", str(_pet_attr(pet, config, "crit")), Vector2(176.5, 491.0), Vector2(132.0, 28.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(_list_container, "DodgeText", "闪避:", Vector2(332.0, 417.0), Vector2(70.0, 28.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(_list_container, "DodgeValue", str(_pet_attr(pet, config, "dodge")), Vector2(388.5, 416.0), Vector2(132.0, 28.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(_list_container, "DefenseText", "防御:", Vector2(332.0, 456.0), Vector2(70.0, 28.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(_list_container, "DefenseValue", str(_pet_attr(pet, config, "anti")), Vector2(388.5, 455.0), Vector2(132.0, 28.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_texture(_list_container, "DescTitle", _battle_frame("battle_pet_canzhan7_png"), Vector2(214.5, 537.0), Vector2(211.0, 39.0))
_ui.add_texture(_list_container, "DescBg", _battle_bg("battle_pet_bg9"), Vector2(62.0, 562.0), Vector2(516.0, 162.0))
var desc: Label = _ui.add_panel_label(_list_container, "Desc", str(config.get("desc", "")), Vector2(93.0, 589.0), Vector2(454.0, 106.0), 22, Color.WHITE, 2, Color.html("#7d1c1c"), HORIZONTAL_ALIGNMENT_LEFT)
desc.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
desc.clip_text = false
func _render_pet_list_page(parent: Control) -> void:
_ui.add_content_panel_background(parent, "PetListBg", Vector2(50.0, 237.0), Vector2(540.0, 507.0))
if _rows.is_empty():
_ui.add_panel_label(parent, "PetListEmpty", "暂无战宠,请到宠物商店购买宠物蛋。", Vector2(92.0, 445.0), Vector2(456.0, 50.0), 24, Color.html("#ce5f5f"), 0, Color.TRANSPARENT)
return
var scroll := ScrollContainer.new()
scroll.name = "PetListScroll"
scroll.position = Vector2(62.0, 249.0)
scroll.size = Vector2(516.0, 488.0)
scroll.clip_contents = true
_ui.configure_vertical_scroll(scroll)
parent.add_child(scroll)
var list := VBoxContainer.new()
list.name = "PetListItems"
list.custom_minimum_size = Vector2(516.0, 0.0)
list.add_theme_constant_override("separation", 0)
scroll.add_child(list)
for pet in _rows:
list.add_child(_create_pet_list_item(pet))
func _create_pet_list_item(pet: Dictionary) -> Control:
var config := _pet_config(pet)
var pet_id := int(pet.get("id", 0))
var holder := Control.new()
holder.name = "PetListItem%s" % pet_id
holder.custom_minimum_size = Vector2(508.0, 244.0)
holder.size = Vector2(508.0, 244.0)
_ui.add_scale9_patch(holder, "Bg", _texture_provider.common("shop_itembg"), Vector2(8.0, 6.0), Vector2(500.0, 230.0), Rect2(90.0, 57.0, 4.0, 2.0))
_ui.add_panel_label(holder, "Name", str(config.get("name", pet.get("name", "战宠"))), Vector2(18.0, 41.0), Vector2(132.0, 26.0), 18, Color.html("#784226"), 2, Color.WHITE)
var icon_id := int(config.get("graphical_id", pet.get("graphical_id", 11001)))
_ui.add_texture(holder, "Head", _get_item_icon_texture(icon_id), Vector2(26.0, 72.5), Vector2(95.0, 95.0), TextureRect.STRETCH_KEEP_ASPECT_CENTERED)
_ui.add_panel_label(holder, "PowerText", "战力:", Vector2(142.5, 52.0), Vector2(82.0, 27.0), 19, Color.html("#f8c903"), 3, Color.html("#97480e"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(holder, "Power", str(_power(pet)), Vector2(220.0, 52.0), Vector2(94.0, 27.0), 19, Color.html("#fffef7"), 2, Color.html("#685036"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(holder, "LevelText", "等级:", Vector2(318.0, 52.0), Vector2(72.0, 27.0), 19, Color.html("#f8c903"), 3, Color.html("#97480e"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(holder, "Level", str(int(pet.get("level", 1))), Vector2(388.0, 52.0), Vector2(80.0, 27.0), 19, Color.html("#fffef7"), 2, Color.html("#685036"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(holder, "FeedText", "巡逻时间:", Vector2(142.5, 86.0), Vector2(112.0, 27.0), 19, Color.html("#f8c903"), 3, Color.html("#97480e"), HORIZONTAL_ALIGNMENT_LEFT)
_ui.add_panel_label(holder, "FeedTime", _pet_feed_time_text(pet), Vector2(252.0, 86.0), Vector2(194.0, 27.0), 19, Color.html("#fffef7"), 2, Color.html("#685036"), HORIZONTAL_ALIGNMENT_LEFT)
var desc: Label = _ui.add_panel_label(holder, "Desc", str(config.get("desc", "")), Vector2(26.0, 188.0), Vector2(456.0, 42.0), 19, Color.html("#f8c903"), 3, Color.html("#97480e"), HORIZONTAL_ALIGNMENT_LEFT)
desc.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
var active := int(pet.get("is_enter_war", 0)) == 1
var button_label := "喂养" if active else "参战"
var button_callback := Callable(self, "_open_food_popup").bind(pet_id) if active else Callable(self, "_set_war_for_pet").bind(pet_id)
_ui.add_texture_button(holder, "Action", _texture_provider.common("c_btn2"), Vector2(144.0, 124.0), Vector2(110.0, 45.0), button_label, button_callback, 22)
return holder
func _render_pet_shop_page(parent: Control) -> void:
_ui.add_content_panel_background(parent, "PetShopBg", Vector2(50.0, 237.0), Vector2(540.0, 507.0))
var items := _pet_shop_items()
if items.is_empty():
_ui.add_panel_label(parent, "PetShopEmpty", "暂无战宠商品", Vector2(120.0, 445.0), Vector2(400.0, 50.0), 24, Color.html("#ce5f5f"), 0, Color.TRANSPARENT)
return
var scroll := ScrollContainer.new()
scroll.name = "PetShopScroll"
scroll.position = Vector2(62.0, 249.0)
scroll.size = Vector2(516.0, 488.0)
scroll.clip_contents = true
_ui.configure_vertical_scroll(scroll)
parent.add_child(scroll)
var holder := MarginContainer.new()
holder.name = "PetShopListHolder"
holder.custom_minimum_size = Vector2(516.0, 0.0)
holder.add_theme_constant_override("margin_left", 20)
holder.add_theme_constant_override("margin_top", 8)
holder.add_theme_constant_override("margin_right", 8)
holder.add_theme_constant_override("margin_bottom", 8)
scroll.add_child(holder)
var grid := GridContainer.new()
grid.name = "PetShopList"
grid.columns = 2
grid.custom_minimum_size = Vector2(464.0, 0.0)
grid.add_theme_constant_override("h_separation", 16)
grid.add_theme_constant_override("v_separation", 12)
holder.add_child(grid)
for shop_item in items:
grid.add_child(_create_pet_shop_card(shop_item))
func _create_pet_shop_card(shop_item: Dictionary) -> Control:
var holder := Control.new()
holder.name = "PetShopItem%s" % int(shop_item.get("id", 0))
holder.custom_minimum_size = Vector2(224.0, 163.0)
holder.size = Vector2(224.0, 163.0)
_ui.add_nine_patch(holder, "Bg", _texture_provider.common("tipbg"), Vector2.ZERO, holder.size, Vector4(45.0, 21.0, 57.0, 69.0))
_ui.add_nine_patch(holder, "PriceBg", _texture_provider.common("blue_rect"), Vector2(116.0, 7.0), Vector2(89.0, 21.0), Vector4(22.0, 10.0, 13.0, 1.0))
_ui.add_panel_label(holder, "PriceTitle", "单价", Vector2(146.0, 10.0), Vector2(48.0, 18.0), 16, Color.WHITE, 0, Color.TRANSPARENT)
var item_config: Dictionary = shop_item.get("item", {})
var item_id := int(shop_item.get("item_id", item_config.get("id", 0)))
_ui.add_panel_label(holder, "Name", str(item_config.get("name", "物品")), Vector2(13.0, 121.0), Vector2(193.0, 28.0), 24, Color.html("#753f24"), 0, Color.TRANSPARENT)
var group_icon := Control.new()
group_icon.name = "IconGroup"
group_icon.position = Vector2(14.0, 22.0)
group_icon.size = Vector2(86.0, 91.0)
holder.add_child(group_icon)
_ui.add_texture(group_icon, "ItemBg", _texture_provider.common("item_bg"), Vector2.ZERO, Vector2(90.0, 90.0))
_ui.add_texture(group_icon, "Icon", _get_item_icon_texture(int(item_config.get("icon", item_id))), Vector2(8.0, 8.0), Vector2(74.0, 70.0), TextureRect.STRETCH_KEEP_ASPECT_CENTERED)
_ui.add_nine_patch(group_icon, "LvBg", _texture_provider.common("blue_rect"), Vector2(-1.0, -1.0), Vector2(52.0, 21.0), Vector4(0.0, 2.0, 5.0, 17.0))
var level_required := int(shop_item.get("playerLevel", shop_item.get("player_level", 1)))
_ui.add_panel_label(group_icon, "Lv", "Lv.%s" % level_required, Vector2(6.0, 1.0), Vector2(50.0, 20.0), 16, Color.WHITE, 0, Color.TRANSPARENT, HORIZONTAL_ALIGNMENT_LEFT)
var is_locked := int(_player_data().get("level", 1)) < level_required
var lock: TextureRect = _ui.add_texture(group_icon, "Lock", _texture_provider.common("img_lock"), Vector2(53.0, 49.0), Vector2(35.0, 40.0))
lock.visible = is_locked
var price_icon_key := "sicon_gold" if int(shop_item.get("gold", 0)) > 0 else "sicon_diamond"
_ui.add_texture(holder, "PriceIcon", _texture_provider.common(price_icon_key), Vector2(125.0, 42.0), Vector2(35.0, 35.0), TextureRect.STRETCH_KEEP_ASPECT_CENTERED)
var price := int(shop_item.get("gold", 0)) if int(shop_item.get("gold", 0)) > 0 else int(shop_item.get("diamond", shop_item.get("gem", 0)))
_ui.add_panel_label(holder, "Price", NumberFormatterScript.compact(price), Vector2(150.0, 43.0), Vector2(68.0, 28.0), 22, Color.html("#fffef7"), 2, Color.html("#685036"), HORIZONTAL_ALIGNMENT_LEFT)
var buy_button: Control = _ui.add_texture_button(holder, "Buy", _texture_provider.common("c_btn2"), Vector2(123.0, 70.0), Vector2(88.0, 44.0), "购买", Callable(self, "_buy_shop_item").bind(shop_item.duplicate(true)), 22)
if is_locked:
buy_button.modulate = Color(0.72, 0.72, 0.72, 1.0)
var hit := buy_button.get_node_or_null("Hit") as Button
if hit != null:
hit.disabled = true
return holder
func _render_pet_radio_list() -> void:
for index in range(mini(_rows.size(), 4)):
var row_pet: Dictionary = _rows[index]
var row_config := _pet_config(row_pet)
var row_icon := int(row_config.get("graphical_id", row_pet.get("graphical_id", 11001)))
var holder := Control.new()
holder.name = "PetRadio%s" % index
holder.position = Vector2(65.0 + index * 122.0, 753.0)
holder.size = Vector2(87.0, 99.0)
holder.mouse_filter = Control.MOUSE_FILTER_STOP
_list_container.add_child(holder)
_ui.add_texture(holder, "Bg", _battle_bg("battle_pet_bg4"), Vector2.ZERO, Vector2(87.0, 99.0))
if int(row_pet.get("id", 0)) == _selected_id:
_ui.add_texture(holder, "Selected", _battle_frame("battle_pet_canzhan_26_png"), Vector2.ZERO, Vector2(87.0, 99.0))
_ui.add_texture(holder, "Head", _get_item_icon_texture(row_icon), Vector2(13.0, 11.0), Vector2(61.0, 63.0), TextureRect.STRETCH_KEEP_ASPECT_CENTERED)
_ui.add_panel_label(holder, "Name", str(row_config.get("name", row_pet.get("name", "战宠"))), Vector2(0.0, 79.0), Vector2(87.0, 20.0), 14, Color.WHITE, 2, Color.html("#7d1c1c"))
var hit := Button.new()
hit.name = "Hit"
hit.size = holder.size
hit.text = ""
hit.focus_mode = Control.FOCUS_NONE
_apply_empty_button_style(hit)
hit.pressed.connect(Callable(self, "_select").bind(int(row_pet.get("id", 0))))
holder.add_child(hit)
func _add_battle_progress_bar(parent: Control, position: Vector2, current: int, total: int) -> void:
_ui.add_scale9_patch(parent, "ExpBarBg", _battle_frame("battle_pet_canzhan13_png"), position, Vector2(234.0, 30.0), Rect2(44.0, 8.0, 268.0, 14.0))
var ratio := clampf(float(current) / maxf(float(total), 1.0), 0.0, 1.0)
var fill_width := maxf(1.0, 230.0 * ratio)
if ratio > 0.0:
var fill_clip := Control.new()
fill_clip.name = "ExpBarClip"
fill_clip.position = position + Vector2(2.0, 2.0)
fill_clip.size = Vector2(fill_width, 26.0)
fill_clip.clip_contents = true
fill_clip.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(fill_clip)
_ui.add_scale9_patch(fill_clip, "ExpBarFill", _battle_frame("battle_pet_canzhan14_png"), Vector2.ZERO, Vector2(230.0, 26.0), Rect2(43.0, 3.0, 262.0, 20.0))
_ui.add_panel_label(parent, "ExpBarText", "%s / %s" % [current, total], position + Vector2(17.0, 5.0), Vector2(200.0, 19.0), 20, Color.WHITE, 2, Color.html("#7d1c1c"))
func _battle_frame(name: String) -> Texture2D:
return _texture_provider.frame(_texture_provider.atlas_path("battle_pet"), name)
func _battle_bg(name: String) -> Texture2D:
return _texture_provider.standalone("%s/%s.png" % [BATTLE_PET_BG_DIR, name])
func _pet_config(pet: Dictionary) -> Dictionary:
if _game_config == null:
return {}
return _game_config.get_pet_config(int(pet.get("pet_config_id", pet.get("item_id", 0))))
func _next_level_exp(level: int) -> int:
if _game_config == null:
return 1
var next_level := clampi(level + 1, 1, 60)
var config: Dictionary = _game_config.get_pet_level_config(next_level)
return maxi(int(config.get("exp", 1)), 1)
func _pet_type_name(type_id: int) -> String:
if type_id >= 0 and type_id < PET_TYPE_TEXT.size():
return str(PET_TYPE_TEXT[type_id])
return str(PET_TYPE_TEXT[0])
func _pet_attr(pet: Dictionary, config: Dictionary, field: String) -> int:
var pet_data = pet.get("petData", {})
var base := int(config.get(field, 0))
if pet.has(field):
base = int(pet.get(field, base))
elif pet_data is Dictionary and pet_data.has(field):
base = int(pet_data.get(field, base))
return base + int(pet.get("%s_add" % field, 0))
func _normalize_pet_row(row: Dictionary) -> Dictionary:
var pet := row.duplicate(true)
var max_hp := _pet_health_max(pet)
var current_hp := _pet_health_current(pet, max_hp)
pet["max_hp"] = max_hp
pet["hp"] = current_hp
return pet
func _pet_health_text(pet: Dictionary) -> String:
var max_hp := _pet_health_max(pet)
var current_hp := _pet_health_current(pet, max_hp)
return "%s/%s" % [current_hp, max_hp]
func _pet_health_current(pet: Dictionary, max_hp := DEFAULT_PET_HP) -> int:
var value := _first_pet_int(pet, ["hp", "current_hp", "health", "current_health", "cur_hp", "curBlood", "cur_blood"], max_hp)
return clampi(value, 0, maxi(max_hp, 1))
func _pet_health_max(pet: Dictionary) -> int:
var value := _first_pet_int(pet, ["max_hp", "hp_max", "health_max", "max_health", "maxBlood", "max_blood"], DEFAULT_PET_HP)
return maxi(value, 1)
func _first_pet_int(pet: Dictionary, keys: Array, fallback: int) -> int:
for key in keys:
var key_text := str(key)
if pet.has(key_text):
return int(pet.get(key_text, fallback))
var pet_data = pet.get("petData", {})
if pet_data is Dictionary:
for key in keys:
var key_text := str(key)
if pet_data.has(key_text):
return int(pet_data.get(key_text, fallback))
return fallback
func _pet_can_patrol(pet: Dictionary) -> bool:
return int(pet.get("is_enter_war", 0)) == 1 and int(pet.get("feed", 0)) > 0
func _pet_feed_time_text(pet: Dictionary) -> String:
var feed_left := int(pet.get("feed", 0))
if feed_left <= 0:
return "0秒"
return _format_duration(feed_left)
func _set_war_for_pet(pet_id: int) -> void:
_selected_id = pet_id
await _set_war()
func _set_war() -> void:
if _is_busy():
return
var pet := _selected_pet()
if pet.is_empty():
return
_set_busy(true)
var result: Dictionary = await _pet_api.set_enter_war(_token(), int(pet.get("id", 0)))
_set_busy(false)
if not result.get("ok", false):
_show_toast(_response_status_text(result, "参战失败"))
return
_show_toast("参战成功")
await refresh_display()
render()
func _open_food_popup(pet_id := 0) -> void:
if _is_busy():
return
var target_pet_id := int(pet_id)
if target_pet_id <= 0:
target_pet_id = _selected_id
if target_pet_id <= 0:
_show_toast("请选择战宠")
return
_feeding_pet_id = target_pet_id
if _food_popup != null and is_instance_valid(_food_popup):
_food_popup.queue_free()
await _refresh_store_house()
var foods := _pet_food_items()
_selected_food_item_id = 0
for item in foods:
if int(item.get("num", 0)) > 0:
_selected_food_item_id = int(item.get("item_id", 0))
break
if _selected_food_item_id == 0 and not foods.is_empty():
_selected_food_item_id = int(foods[0].get("item_id", 0))
if foods.is_empty():
_show_toast("没有可用宠物粮")
return
if _panel == null or not is_instance_valid(_panel):
return
var popup := Control.new()
popup.name = "PetFoodPopup"
popup.size = _panel_screen_size
popup.mouse_filter = Control.MOUSE_FILTER_STOP
popup.z_index = 50
_food_popup = popup
_panel.add_child(popup)
var mask := Button.new()
mask.name = "PetFoodMask"
mask.set_anchors_preset(Control.PRESET_FULL_RECT)
mask.text = ""
mask.focus_mode = Control.FOCUS_NONE
_apply_empty_button_style(mask)
mask.pressed.connect(func() -> void: popup.queue_free())
popup.add_child(mask)
_ui.add_nine_patch(popup, "Bg", _texture_provider.common("c_panel2"), Vector2(88.0, 302.0), Vector2(464.0, 316.0), Vector4(136.0, 128.0, 4.0, 2.0))
_ui.add_texture(popup, "Trim1", _texture_provider.common("trimming1"), Vector2(127.0, 279.0), Vector2(70.0, 63.0))
_ui.add_texture(popup, "Trim2", _texture_provider.common("trimming2"), Vector2(441.0, 302.0), Vector2(35.0, 26.0))
_ui.add_panel_label(popup, "Title", "选择宠物粮", Vector2(190.0, 284.0), Vector2(260.0, 42.0), 30, Color.WHITE, 2, Color.html("#7d1c1c"))
_ui.add_texture_button(popup, "Close", _texture_provider.common("btn_close"), Vector2(480.0, 281.0), Vector2(80.0, 74.0), "", func() -> void: popup.queue_free(), 20)
for index in range(foods.size()):
_add_food_option(popup, foods[index], index)
var confirm: Control = _ui.add_texture_button(popup, "Eat", _texture_provider.common("c_btn3"), Vector2(237.0, 554.0), Vector2(166.0, 58.0), "确定", Callable(self, "_feed_selected_food"), 30)
_set_texture_button_enabled(confirm, _owned_amount(_selected_food_item_id) > 0, _texture_provider.common("c_btn3"), _texture_provider.common("c_btn3_gray"))
_update_food_selection(popup)
func _add_food_option(popup: Control, item: Dictionary, index: int) -> void:
var positions := [Vector2(140.0, 371.0), Vector2(260.0, 371.0), Vector2(380.0, 371.0)]
if index >= positions.size():
return
var holder := Control.new()
holder.name = "Food%s" % index
holder.position = positions[index]
holder.size = Vector2(104.0, 144.0)
holder.set_meta("item_id", int(item.get("item_id", 0)))
holder.mouse_filter = Control.MOUSE_FILTER_STOP
popup.add_child(holder)
_ui.add_texture(holder, "ItemBg", _texture_provider.common("item_bg"), Vector2(7.0, 7.0), Vector2(90.0, 90.0))
_ui.add_texture(holder, "Selected", _texture_provider.common("icon_bg_selected"), Vector2.ZERO, Vector2(104.0, 104.0))
_ui.add_texture(holder, "Icon", _get_item_icon_texture(int(item.get("icon_id", 0))), Vector2(15.0, 15.0), Vector2(74.0, 70.0), TextureRect.STRETCH_KEEP_ASPECT_CENTERED)
_ui.add_panel_label(holder, "Num", "x%s" % int(item.get("num", 0)), Vector2(19.0, 79.0), Vector2(76.0, 24.0), 20, Color.html("#b36011"), 1, Color.html("#ffe59d"), HORIZONTAL_ALIGNMENT_RIGHT)
_ui.add_panel_label(holder, "Name", _pet_food_name(int(item.get("item_id", 0))), Vector2(-6.0, 113.0), Vector2(116.0, 26.0), 18, Color.html("#b36011"), 0, Color.TRANSPARENT)
var hit := Button.new()
hit.name = "Hit"
hit.size = holder.size
hit.text = ""
hit.focus_mode = Control.FOCUS_NONE
_apply_empty_button_style(hit)
hit.pressed.connect(Callable(self, "_select_food").bind(int(item.get("item_id", 0)), popup))
holder.add_child(hit)
func _select_food(item_id: int, popup: Control) -> void:
_selected_food_item_id = item_id
_update_food_selection(popup)
var confirm := popup.get_node_or_null("Eat") as Control
_set_texture_button_enabled(confirm, _owned_amount(item_id) > 0, _texture_provider.common("c_btn3"), _texture_provider.common("c_btn3_gray"))
func _update_food_selection(popup: Control) -> void:
if popup == null:
return
for child in popup.get_children():
if not child is Control or not child.has_meta("item_id"):
continue
var selected := child.get_node_or_null("Selected") as TextureRect
if selected != null:
selected.visible = int(child.get_meta("item_id", 0)) == _selected_food_item_id
func _feed_selected_food() -> void:
if _is_busy():
return
if _feeding_pet_id <= 0:
_show_toast("请选择战宠")
return
var item_id := _selected_food_item_id
if item_id <= 0 or _owned_amount(item_id) <= 0:
_show_toast("没有可用宠物粮")
return
_set_busy(true)
var result: Dictionary = await _pet_api.feed(_token(), _feeding_pet_id, item_id)
_set_busy(false)
if not result.get("ok", false):
_show_toast(_response_status_text(result, "喂养失败"))
return
_decrease_warehouse_item(item_id, 1)
if _food_popup != null and is_instance_valid(_food_popup):
_food_popup.queue_free()
_food_popup = null
_show_toast("喂养成功")
await refresh_display()
await _refresh_store_house()
render()
func _feed() -> void:
if _is_busy():
return
var pet := _selected_pet()
if pet.is_empty():
return
await _open_food_popup(int(pet.get("id", 0)))
func _pet_food_items() -> Array[Dictionary]:
var foods: Array[Dictionary] = []
for item_id in _dog_food_item_ids:
var normalized_id := int(item_id)
var config: Dictionary = _game_config.normalize_item_config(_game_config.get_item_config(normalized_id)) if _game_config != null else {}
if config.is_empty():
continue
foods.append({
"item_id": normalized_id,
"num": _owned_amount(normalized_id),
"icon_id": int(config.get("icon", normalized_id)),
"config": config,
})
return foods
func _pet_food_name(item_id: int) -> String:
if _game_config == null:
return "宠物粮"
return _game_config.get_item_name(item_id)
func _pet_shop_items() -> Array[Dictionary]:
if _game_config == null:
return []
var result: Array[Dictionary] = []
var seen: Dictionary = {}
for shop_item in _game_config.get_shop_config_by_type(3):
var store_id := int(shop_item.get("id", 0))
if seen.has(store_id):
continue
var item_config: Dictionary = shop_item.get("item", {})
if not _is_pet_shop_item(shop_item, item_config):
continue
seen[store_id] = true
result.append(shop_item)
result.sort_custom(func(left: Dictionary, right: Dictionary) -> bool:
var left_level := int(left.get("playerLevel", left.get("player_level", 1)))
var right_level := int(right.get("playerLevel", right.get("player_level", 1)))
if left_level == right_level:
return int(left.get("id", 0)) < int(right.get("id", 0))
return left_level < right_level
)
return result
func _is_pet_shop_item(shop_item: Dictionary, item_config: Dictionary) -> bool:
var item_id := int(shop_item.get("item_id", item_config.get("id", 0)))
var small_type := int(item_config.get("smallType", 0))
return small_type == 500 or _dog_food_item_ids.has(item_id)
func _buy_shop_item(shop_item: Dictionary) -> void:
if _is_busy():
return
if _shop_api == null:
_show_toast("购买失败:商店接口未初始化")
return
var level_required := int(shop_item.get("playerLevel", shop_item.get("player_level", 1)))
if int(_player_data().get("level", 1)) < level_required:
_show_toast("购买失败:等级不足")
return
var gold_cost := int(shop_item.get("gold", 0))
var gem_cost := int(shop_item.get("diamond", shop_item.get("gem", 0)))
if gold_cost > int(_player_data().get("gold", 0)):
_show_toast("购买失败:金豆不足")
return
if gem_cost > int(_player_data().get("gem", 0)):
_show_toast("购买失败:资源不足,请充值!")
return
_set_busy(true)
var result: Dictionary = await _shop_api.buy(_token(), int(shop_item.get("id", 0)), 1)
_set_busy(false)
if not result.get("ok", false):
_show_toast(_response_status_text(result, "购买失败"))
return
_call("decrease_player_currency", [gold_cost, gem_cost])
_render_player()
_show_toast("购买成功")
await _refresh_store_house()
render()