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()