frontengine 1.0.3__tar.gz → 1.0.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. {frontengine-1.0.3 → frontengine-1.0.5}/PKG-INFO +1 -1
  2. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/pet/desktop_pet.py +209 -30
  3. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/pet/pet_setting_ui.py +49 -11
  4. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/multi_language/english.py +4 -0
  5. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/multi_language/traditional_chinese.py +4 -0
  6. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine.egg-info/PKG-INFO +1 -1
  7. {frontengine-1.0.3 → frontengine-1.0.5}/pyproject.toml +1 -1
  8. {frontengine-1.0.3 → frontengine-1.0.5}/LICENSE +0 -0
  9. {frontengine-1.0.3 → frontengine-1.0.5}/README.md +0 -0
  10. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/__init__.py +0 -0
  11. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/__main__.py +0 -0
  12. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/__init__.py +0 -0
  13. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/base_widget.py +0 -0
  14. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/gif/__init__.py +0 -0
  15. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/gif/paint_gif.py +0 -0
  16. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/image/__init__.py +0 -0
  17. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/image/paint_image.py +0 -0
  18. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/load/__init__.py +0 -0
  19. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/load/load_someone_make_ui.py +0 -0
  20. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/overlay_factory.py +0 -0
  21. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/particle/__init__.py +0 -0
  22. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/particle/particle_ui.py +0 -0
  23. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/pet/__init__.py +0 -0
  24. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/scene/__init__.py +0 -0
  25. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/scene/extend_graphic_scene.py +0 -0
  26. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/scene/extend_graphic_view.py +0 -0
  27. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/scene/scene.py +0 -0
  28. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/sound_player/__init__.py +0 -0
  29. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/sound_player/sound_effect.py +0 -0
  30. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/sound_player/sound_player.py +0 -0
  31. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/text/__init__.py +0 -0
  32. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/text/draw_text.py +0 -0
  33. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/video/__init__.py +0 -0
  34. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/video/video_player.py +0 -0
  35. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/web/__init__.py +0 -0
  36. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/web/webview.py +0 -0
  37. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/show/window_helpers.py +0 -0
  38. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/system_tray/__init__.py +0 -0
  39. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/system_tray/extend_system_tray.py +0 -0
  40. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/__init__.py +0 -0
  41. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/color/__init__.py +0 -0
  42. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/color/global_color.py +0 -0
  43. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/dialog/__init__.py +0 -0
  44. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
  45. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
  46. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/dialog/save_file_dialog.py +0 -0
  47. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/main_ui.py +0 -0
  48. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/menu/__init__.py +0 -0
  49. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/menu/help_menu.py +0 -0
  50. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/menu/how_to_menu.py +0 -0
  51. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/menu/language_menu.py +0 -0
  52. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/menu/preset_menu.py +0 -0
  53. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/menu/settings_menu.py +0 -0
  54. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/__init__.py +0 -0
  55. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/control_center/__init__.py +0 -0
  56. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/control_center/control_center_ui.py +0 -0
  57. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/gif/__init__.py +0 -0
  58. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
  59. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/image/__init__.py +0 -0
  60. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/image/image_setting_ui.py +0 -0
  61. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/particle/__init__.py +0 -0
  62. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
  63. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/pet/__init__.py +0 -0
  64. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/scene_setting/__init__.py +0 -0
  65. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
  66. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/scene_setting/scene_page/__init__.py +0 -0
  67. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
  68. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
  69. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
  70. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
  71. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
  72. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
  73. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
  74. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
  75. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
  76. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/sound_player/__init__.py +0 -0
  77. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
  78. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/text/__init__.py +0 -0
  79. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/text/text_setting_ui.py +0 -0
  80. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/utils.py +0 -0
  81. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/video/__init__.py +0 -0
  82. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/video/video_setting_ui.py +0 -0
  83. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/web/__init__.py +0 -0
  84. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/ui/page/web/web_setting_ui.py +0 -0
  85. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/user_setting/__init__.py +0 -0
  86. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/user_setting/preset_repository.py +0 -0
  87. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/user_setting/scene_setting.py +0 -0
  88. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/user_setting/user_setting_file.py +0 -0
  89. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/__init__.py +0 -0
  90. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/browser/__init__.py +0 -0
  91. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/browser/browser.py +0 -0
  92. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/critical_exit/__init__.py +0 -0
  93. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
  94. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/critical_exit/critical_exit.py +0 -0
  95. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/critical_exit/win32_vk.py +0 -0
  96. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/exception/__init__.py +0 -0
  97. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/exception/exception_tags.py +0 -0
  98. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/exception/exceptions.py +0 -0
  99. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/file/__init__.py +0 -0
  100. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/file/open/__init__.py +0 -0
  101. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/file/open/open_file.py +0 -0
  102. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/file/save/__init__.py +0 -0
  103. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/file/save/save_file.py +0 -0
  104. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/hotkey/__init__.py +0 -0
  105. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/hotkey/hotkey_service.py +0 -0
  106. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/json/__init__.py +0 -0
  107. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/json/json_file.py +0 -0
  108. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/json/json_repository.py +0 -0
  109. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/json_format/__init__.py +0 -0
  110. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/json_format/json_process.py +0 -0
  111. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/logging/__init__.py +0 -0
  112. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/logging/loggin_instance.py +0 -0
  113. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/multi_language/__init__.py +0 -0
  114. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/multi_language/france.py +0 -0
  115. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/multi_language/germany.py +0 -0
  116. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/multi_language/italy.py +0 -0
  117. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/multi_language/language_wrapper.py +0 -0
  118. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/multi_language/russian.py +0 -0
  119. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
  120. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/preset_schedule/__init__.py +0 -0
  121. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
  122. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/redirect_manager/__init__.py +0 -0
  123. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
  124. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/smart_pause/__init__.py +0 -0
  125. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
  126. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/theme_schedule/__init__.py +0 -0
  127. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
  128. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/utils/web_url.py +0 -0
  129. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/worker/__init__.py +0 -0
  130. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine/worker/qthread_worker.py +0 -0
  131. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine.egg-info/SOURCES.txt +0 -0
  132. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine.egg-info/dependency_links.txt +0 -0
  133. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine.egg-info/entry_points.txt +0 -0
  134. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine.egg-info/requires.txt +0 -0
  135. {frontengine-1.0.3 → frontengine-1.0.5}/frontengine.egg-info/top_level.txt +0 -0
  136. {frontengine-1.0.3 → frontengine-1.0.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.3
3
+ Version: 1.0.5
4
4
  Summary: FrontEngine is use-define frontview or only use like screen saver
5
5
  Author-email: JE-Chen <jechenmailman@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/Intergration-Automation-Testing/FrontEngine
@@ -2,7 +2,7 @@ import random as _random_module
2
2
  from pathlib import Path
3
3
  from typing import Optional, Tuple
4
4
 
5
- from PySide6.QtCore import Qt, QRect, QTimer
5
+ from PySide6.QtCore import Qt, QRect, QTimer, Signal
6
6
  from PySide6.QtGui import QAction, QCursor, QMovie, QPainter, QPixmap, QTransform
7
7
  from PySide6.QtWidgets import QMenu, QMessageBox
8
8
 
@@ -16,6 +16,69 @@ BEHAVIOUR_FLOOR = "floor" # walk on the floor with gravity, throw & bounce
16
16
  BEHAVIOUR_WANDER = "wander" # free 2D bounce, no gravity
17
17
  BEHAVIOUR_CHASE = "chase" # chase the mouse cursor, sleep when caught
18
18
 
19
+ # 攀爬時所在的表面 / Surface the pet is crawling on while climbing
20
+ SURFACE_FLOOR = "floor"
21
+ SURFACE_LEFT = "left"
22
+ SURFACE_CEILING = "ceiling"
23
+ SURFACE_RIGHT = "right"
24
+
25
+ # 視覺狀態(決定顯示哪張精靈)/ Visual states (which sprite to show)
26
+ STATE_WALK = "walk"
27
+ STATE_IDLE = "idle"
28
+ STATE_CLIMB = "climb"
29
+ STATE_FALL = "fall"
30
+ STATE_DRAG = "drag"
31
+
32
+ # 動作包(資料夾)中,各狀態可接受的檔名(不含副檔名,先命中者為準)
33
+ # Accepted file stems per state inside a pet pack folder (first match wins).
34
+ _PACK_STATE_ALIASES = {
35
+ STATE_WALK: ("walk", "run", "move"),
36
+ STATE_IDLE: ("idle", "sit", "stand", "sleep"),
37
+ STATE_CLIMB: ("climb", "grab", "wall"),
38
+ STATE_FALL: ("fall", "jump"),
39
+ STATE_DRAG: ("drag", "pinch", "grabbed", "held"),
40
+ }
41
+ _PACK_EXTS = (".gif", ".webp", ".png", ".jpg", ".jpeg")
42
+
43
+
44
+ def scan_pet_pack(folder: str) -> dict:
45
+ """
46
+ 掃描動作包資料夾,回傳「視覺狀態 -> 圖片路徑」對應。非資料夾回傳空 dict。
47
+ Scan a pet-pack folder and return a ``state -> image path`` mapping.
48
+ Returns {} when `folder` is not a directory.
49
+ """
50
+ result: dict = {}
51
+ try:
52
+ folder_path = Path(folder)
53
+ if not folder_path.is_dir():
54
+ return result
55
+ by_stem = {
56
+ path.stem.lower(): path
57
+ for path in sorted(folder_path.iterdir())
58
+ if path.is_file() and path.suffix.lower() in _PACK_EXTS
59
+ }
60
+ for state, aliases in _PACK_STATE_ALIASES.items():
61
+ for alias in aliases:
62
+ if alias in by_stem:
63
+ result[state] = str(by_stem[alias])
64
+ break
65
+ except OSError:
66
+ pass
67
+ return result
68
+
69
+
70
+ def derive_visual_state(dragging: bool, airborne: bool, surface: str, motion_state: str) -> str:
71
+ """依目前情形推導應顯示的視覺狀態 / Derive which sprite state to show."""
72
+ if dragging:
73
+ return STATE_DRAG
74
+ if airborne:
75
+ return STATE_FALL
76
+ if surface in (SURFACE_LEFT, SURFACE_RIGHT, SURFACE_CEILING):
77
+ return STATE_CLIMB
78
+ if surface == SURFACE_FLOOR and motion_state == "idle":
79
+ return STATE_IDLE
80
+ return STATE_WALK
81
+
19
82
 
20
83
  class PetMotion:
21
84
  """
@@ -34,6 +97,8 @@ class PetMotion:
34
97
  BOUNCE_THRESHOLD = 8.0
35
98
  CHASE_STOP_DISTANCE = 10.0
36
99
  CHASE_SPEED_FACTOR = 2.0
100
+ CLIMB_CHANCE = 0.5 # chance to grab a wall (vs. turn around) at a floor corner
101
+ DROP_CHANCE = 0.008 # chance per step to let go while on a wall/ceiling
37
102
 
38
103
  def __init__(
39
104
  self,
@@ -45,6 +110,7 @@ class PetMotion:
45
110
  speed: int = 3,
46
111
  behaviour: str = BEHAVIOUR_FLOOR,
47
112
  rng=None,
113
+ climb: bool = True,
48
114
  ) -> None:
49
115
  self.x = float(x)
50
116
  self.y = float(y)
@@ -61,6 +127,8 @@ class PetMotion:
61
127
  self._state_ticks = 0
62
128
  self.target: Optional[Tuple[float, float]] = None
63
129
  self.asleep = False
130
+ self.climb = bool(climb)
131
+ self.surface = SURFACE_FLOOR
64
132
 
65
133
  @property
66
134
  def facing_left(self) -> bool:
@@ -78,6 +146,7 @@ class PetMotion:
78
146
  self.vy = float(vy)
79
147
  self._airborne = True
80
148
  self.asleep = False
149
+ self.surface = SURFACE_FLOOR
81
150
 
82
151
  def step(self) -> Tuple[int, int]:
83
152
  left, top, right, bottom = self.bounds
@@ -88,9 +157,9 @@ class PetMotion:
88
157
  return self._step_wander(left, top, right, bottom)
89
158
  return self._step_floor(left, top, right, bottom, floor_y)
90
159
 
91
- # --- floor / gravity ---
160
+ # --- floor / gravity / climbing ---
92
161
  def _step_floor(self, left, top, right, bottom, floor_y) -> Tuple[int, int]:
93
- airborne = self._airborne or self.y < floor_y - 0.5
162
+ airborne = self._airborne or (self.surface == SURFACE_FLOOR and self.y < floor_y - 0.5)
94
163
  if airborne:
95
164
  self.vy += self.GRAVITY
96
165
  self.x += self.vx
@@ -111,23 +180,81 @@ class PetMotion:
111
180
  else:
112
181
  self.vy = 0.0
113
182
  self._airborne = False
183
+ self.surface = SURFACE_FLOOR
114
184
  self.vx = float(self.speed) if self.vx >= 0 else float(-self.speed)
115
185
  self._new_state(force_walk=True)
116
186
  return int(self.x), int(self.y)
117
187
 
118
- # grounded: random walk / idle
188
+ if self.surface != SURFACE_FLOOR:
189
+ return self._step_climb(left, top, right, bottom, floor_y)
190
+
191
+ # grounded on the floor: random walk / idle, with a chance to climb a wall
119
192
  self.y = float(floor_y)
120
193
  if self._state_ticks <= 0:
121
194
  self._new_state()
122
195
  self._state_ticks -= 1
123
- if self.state == "walk":
196
+ if self.state != "walk":
197
+ return int(self.x), int(self.y)
198
+ self.x += self.vx
199
+ if self.x <= left:
200
+ self.x = float(left)
201
+ if self.climb and self._rng.random() < self.CLIMB_CHANCE:
202
+ self.surface = SURFACE_LEFT
203
+ self.vy = float(-self.speed)
204
+ else:
205
+ self.vx = abs(self.vx)
206
+ elif self.x + self.width >= right:
207
+ self.x = float(right - self.width)
208
+ if self.climb and self._rng.random() < self.CLIMB_CHANCE:
209
+ self.surface = SURFACE_RIGHT
210
+ self.vy = float(-self.speed)
211
+ else:
212
+ self.vx = -abs(self.vx)
213
+ return int(self.x), int(self.y)
214
+
215
+ def _step_climb(self, left, top, right, bottom, floor_y) -> Tuple[int, int]:
216
+ """沿螢幕邊緣爬行;隨機放手則掉落 / Crawl the screen perimeter; may let go and fall."""
217
+ if self._rng.random() < self.DROP_CHANCE:
218
+ self.surface = SURFACE_FLOOR
219
+ self._airborne = True
220
+ self.vy = 1.0
221
+ return int(self.x), int(self.y)
222
+
223
+ if self.surface == SURFACE_RIGHT:
224
+ self.x = float(right - self.width)
225
+ self.y += self.vy
226
+ if self.vy < 0 and self.y <= top: # reached the ceiling, crawl left
227
+ self.y = float(top)
228
+ self.surface = SURFACE_CEILING
229
+ self.vx = float(-self.speed)
230
+ elif self.vy > 0 and self.y >= floor_y: # climbed back down
231
+ self.y = float(floor_y)
232
+ self.surface = SURFACE_FLOOR
233
+ self.vx = float(-self.speed)
234
+ self._new_state(force_walk=True)
235
+ elif self.surface == SURFACE_LEFT:
236
+ self.x = float(left)
237
+ self.y += self.vy
238
+ if self.vy < 0 and self.y <= top:
239
+ self.y = float(top)
240
+ self.surface = SURFACE_CEILING
241
+ self.vx = float(self.speed)
242
+ elif self.vy > 0 and self.y >= floor_y:
243
+ self.y = float(floor_y)
244
+ self.surface = SURFACE_FLOOR
245
+ self.vx = float(self.speed)
246
+ self._new_state(force_walk=True)
247
+ elif self.surface == SURFACE_CEILING:
248
+ self.y = float(top)
124
249
  self.x += self.vx
125
- if self.x <= left:
250
+ if self.vx < 0 and self.x <= left: # corner -> climb down the left wall
126
251
  self.x = float(left)
127
- self.vx = abs(self.vx)
128
- elif self.x + self.width >= right:
252
+ self.surface = SURFACE_LEFT
253
+ self.vy = float(self.speed)
254
+ elif self.vx > 0 and self.x + self.width >= right:
129
255
  self.x = float(right - self.width)
130
- self.vx = -abs(self.vx)
256
+ self.surface = SURFACE_RIGHT
257
+ self.vy = float(self.speed)
131
258
  return int(self.x), int(self.y)
132
259
 
133
260
  def _new_state(self, force_walk: bool = False) -> None:
@@ -187,53 +314,101 @@ class DesktopPetWidget(BaseWidget):
187
314
  A draggable, throwable, cursor-chasing animated sprite (GIF or image).
188
315
  """
189
316
 
317
+ clone_requested = Signal()
318
+
190
319
  def __init__(self, image_path: str, size: int = 128, speed: int = 3,
191
- behaviour: str = BEHAVIOUR_FLOOR):
320
+ behaviour: str = BEHAVIOUR_FLOOR, climb: bool = True):
192
321
  front_engine_logger.info(
193
- f"[DesktopPetWidget] Init | path={image_path}, size={size}, speed={speed}, behaviour={behaviour}"
322
+ f"[DesktopPetWidget] Init | path={image_path}, size={size}, speed={speed}, "
323
+ f"behaviour={behaviour}, climb={climb}"
194
324
  )
195
325
  super().__init__()
196
326
  self.opacity = 1.0
197
327
  self.pet_size: int = max(16, int(size))
198
328
  self.image_path: Path = Path(image_path)
199
- self.movie: Optional[QMovie] = None
200
- self.pixmap: Optional[QPixmap] = None
201
329
  self._dragging: bool = False
202
330
  self._drag_offset = None
203
331
  self._last_move_delta: Tuple[int, int] = (0, 0)
204
-
205
- if self.image_path.exists() and self.image_path.is_file():
206
- if self.image_path.suffix.lower() in (".gif", ".webp"):
207
- self.movie = QMovie(str(self.image_path))
208
- self.movie.frameChanged.connect(self.update)
209
- self.movie.start()
210
- else:
211
- self.pixmap = QPixmap(str(self.image_path))
212
- else:
213
- front_engine_logger.error(f"[DesktopPetWidget] File not found: {self.image_path}")
332
+ # 視覺狀態 -> ("movie"|"pixmap", 物件) / state -> sprite
333
+ self._sprites: dict = {}
334
+ self._active = None
335
+ self._active_state = None
336
+
337
+ self._load_sprites(self.image_path)
338
+ if not self._sprites:
339
+ front_engine_logger.error(f"[DesktopPetWidget] No sprite found: {self.image_path}")
214
340
  message_box = QMessageBox(self)
215
341
  message_box.setText(language_wrapper.language_word_dict.get("pet_message_box_text", "Pet image not found"))
216
342
  message_box.show()
217
343
 
218
344
  self.resize(self.pet_size, self.pet_size)
219
- self.motion = PetMotion(0, 0, self.pet_size, self.pet_size, (0, 0, 1920, 1080), speed, behaviour)
345
+ self.motion = PetMotion(
346
+ 0, 0, self.pet_size, self.pet_size, (0, 0, 1920, 1080), speed, behaviour, climb=climb
347
+ )
348
+ self._set_active_sprite(STATE_WALK)
220
349
 
221
350
  self._timer = QTimer(self)
222
351
  self._timer.timeout.connect(self._on_tick)
223
352
 
353
+ self.menu = QMenu(self)
354
+ self.clone_action = QAction(language_wrapper.language_word_dict.get("pet_clone", "Clone"), self)
355
+ self.clone_action.triggered.connect(self.clone_requested.emit)
356
+ self.menu.addAction(self.clone_action)
224
357
  self.close_action = QAction(language_wrapper.language_word_dict.get("control_center_close_all", "Close"), self)
225
358
  self.close_action.triggered.connect(self.close)
226
- self.menu = QMenu(self)
227
359
  self.menu.addAction(self.close_action)
228
360
 
229
361
  def set_pet_window_flag(self) -> None:
230
362
  """寵物需接收滑鼠以便拖曳,故 allow_input=True 並置頂。"""
231
363
  apply_overlay_window_flags(self, show_on_bottom=False, allow_input=True)
232
364
 
365
+ def _load_sprite(self, path: str):
366
+ if Path(path).suffix.lower() in (".gif", ".webp"):
367
+ movie = QMovie(str(path))
368
+ movie.frameChanged.connect(self.update)
369
+ return ("movie", movie)
370
+ return ("pixmap", QPixmap(str(path)))
371
+
372
+ def _load_sprites(self, image_path: Path) -> None:
373
+ """單一檔 -> 所有狀態共用;資料夾 -> 依檔名對應各狀態。"""
374
+ if image_path.is_dir():
375
+ for state, path in scan_pet_pack(str(image_path)).items():
376
+ self._sprites[state] = self._load_sprite(path)
377
+ elif image_path.is_file():
378
+ self._sprites["default"] = self._load_sprite(str(image_path))
379
+
380
+ def _sprite_for(self, state: str):
381
+ if "default" in self._sprites:
382
+ return self._sprites["default"]
383
+ if state in self._sprites:
384
+ return self._sprites[state]
385
+ for fallback in (STATE_WALK, STATE_IDLE):
386
+ if fallback in self._sprites:
387
+ return self._sprites[fallback]
388
+ return next(iter(self._sprites.values()), None)
389
+
390
+ def _set_active_sprite(self, state: str) -> None:
391
+ sprite = self._sprite_for(state)
392
+ if sprite is None or sprite is self._active:
393
+ return
394
+ if self._active is not None and self._active[0] == "movie":
395
+ self._active[1].stop()
396
+ self._active = sprite
397
+ self._active_state = state
398
+ if sprite[0] == "movie":
399
+ sprite[1].start()
400
+ self.update()
401
+
402
+ def _visual_state(self) -> str:
403
+ return derive_visual_state(
404
+ self._dragging, self.motion._airborne, self.motion.surface, self.motion.state
405
+ )
406
+
233
407
  def _current_pixmap(self) -> Optional[QPixmap]:
234
- if self.movie is not None:
235
- return self.movie.currentPixmap()
236
- return self.pixmap
408
+ if self._active is None:
409
+ return None
410
+ kind, obj = self._active
411
+ return obj.currentPixmap() if kind == "movie" else obj
237
412
 
238
413
  def draw_content(self, painter: QPainter) -> None:
239
414
  pixmap = self._current_pixmap()
@@ -254,12 +429,14 @@ class DesktopPetWidget(BaseWidget):
254
429
 
255
430
  def _on_tick(self) -> None:
256
431
  if self._dragging:
432
+ self._set_active_sprite(STATE_DRAG)
257
433
  return
258
434
  if self.motion.behaviour == BEHAVIOUR_CHASE:
259
435
  cursor = QCursor.pos()
260
436
  self.motion.set_target(cursor.x(), cursor.y())
261
437
  x, y = self.motion.step()
262
438
  self.move(x, y)
439
+ self._set_active_sprite(self._visual_state())
263
440
 
264
441
  # --- dragging + throwing ---
265
442
  def mousePressEvent(self, event) -> None:
@@ -267,6 +444,7 @@ class DesktopPetWidget(BaseWidget):
267
444
  self._dragging = True
268
445
  self._last_move_delta = (0, 0)
269
446
  self._drag_offset = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
447
+ self._set_active_sprite(STATE_DRAG)
270
448
  super().mousePressEvent(event)
271
449
 
272
450
  def mouseMoveEvent(self, event) -> None:
@@ -292,6 +470,7 @@ class DesktopPetWidget(BaseWidget):
292
470
  def closeEvent(self, event) -> None:
293
471
  if self._timer.isActive():
294
472
  self._timer.stop()
295
- if self.movie is not None:
296
- self.movie.stop()
473
+ for kind, obj in self._sprites.values():
474
+ if kind == "movie":
475
+ obj.stop()
297
476
  super().closeEvent(event)
@@ -2,11 +2,11 @@ from typing import Optional
2
2
 
3
3
  from PySide6.QtGui import QGuiApplication
4
4
  from PySide6.QtWidgets import (
5
- QWidget, QGridLayout, QLabel, QPushButton, QMessageBox, QComboBox,
5
+ QWidget, QGridLayout, QLabel, QPushButton, QMessageBox, QComboBox, QCheckBox, QFileDialog,
6
6
  )
7
7
 
8
8
  from frontengine.show.pet.desktop_pet import (
9
- DesktopPetWidget, BEHAVIOUR_FLOOR, BEHAVIOUR_WANDER, BEHAVIOUR_CHASE,
9
+ DesktopPetWidget, BEHAVIOUR_FLOOR, BEHAVIOUR_WANDER, BEHAVIOUR_CHASE, scan_pet_pack,
10
10
  )
11
11
  from frontengine.ui.dialog.choose_file_dialog import choose_pet
12
12
  from frontengine.ui.page.utils import (
@@ -38,6 +38,10 @@ class PetSettingUI(QWidget):
38
38
  language_wrapper.language_word_dict.get("pet_choose_file", "Choose pet sprite")
39
39
  )
40
40
  self.choose_file_button.clicked.connect(self.choose_and_play)
41
+ self.choose_pack_button = QPushButton(
42
+ language_wrapper.language_word_dict.get("pet_choose_pack", "Choose pet pack...")
43
+ )
44
+ self.choose_pack_button.clicked.connect(self.choose_pack)
41
45
  self.ready_label = QLabel(language_wrapper.language_word_dict.get("Not Ready"))
42
46
 
43
47
  # Size
@@ -62,6 +66,10 @@ class PetSettingUI(QWidget):
62
66
  self.behaviour_combobox.addItem(
63
67
  language_wrapper.language_word_dict.get("pet_behaviour_chase", "Chase cursor"), BEHAVIOUR_CHASE)
64
68
 
69
+ # Climb walls / ceiling (only meaningful in floor mode)
70
+ self.climb_checkbox = QCheckBox(language_wrapper.language_word_dict.get("pet_climb_label", "Climb walls"))
71
+ self.climb_checkbox.setChecked(True)
72
+
65
73
  # Start
66
74
  self.start_button = QPushButton(language_wrapper.language_word_dict.get("pet_start", "Spawn pet"))
67
75
  self.start_button.clicked.connect(self.start_play_pet)
@@ -77,26 +85,37 @@ class PetSettingUI(QWidget):
77
85
  # Layout
78
86
  self.grid_layout.addWidget(self.choose_file_button, 0, 0)
79
87
  self.grid_layout.addWidget(self.ready_label, 0, 1)
88
+ self.grid_layout.addWidget(self.choose_pack_button, 0, 2)
80
89
  self.grid_layout.addWidget(self.size_label, 1, 0)
81
90
  self.grid_layout.addWidget(self.size_combobox, 1, 1)
82
91
  self.grid_layout.addWidget(self.speed_label, 2, 0)
83
92
  self.grid_layout.addWidget(self.speed_combobox, 2, 1)
84
93
  self.grid_layout.addWidget(self.behaviour_label, 3, 0)
85
94
  self.grid_layout.addWidget(self.behaviour_combobox, 3, 1)
95
+ self.grid_layout.addWidget(self.climb_checkbox, 3, 2)
86
96
  self.grid_layout.addWidget(self.start_button, 4, 0)
87
97
  self.grid_layout.addWidget(self.recent_files_label, 5, 0)
88
98
  self.grid_layout.addWidget(self.recent_files_combobox, 5, 1)
89
99
 
90
- def _create_pet_widget(self) -> DesktopPetWidget:
100
+ def _spawn_pet(self) -> None:
101
+ """建立、顯示並開始移動一隻寵物(供 Start 與右鍵複製共用)。"""
102
+ if not self.pet_image_path:
103
+ return
91
104
  pet = DesktopPetWidget(
92
105
  image_path=self.pet_image_path,
93
106
  size=int(self.size_combobox.currentText()),
94
107
  speed=int(self.speed_combobox.currentText()),
95
108
  behaviour=self.behaviour_combobox.currentData(),
109
+ climb=self.climb_checkbox.isChecked(),
96
110
  )
111
+ pet.clone_requested.connect(self._spawn_pet)
97
112
  pet.set_pet_window_flag()
98
113
  self.pet_list.append(pet)
99
- return pet
114
+ screen = QGuiApplication.primaryScreen()
115
+ geometry = screen.availableGeometry() if screen is not None else None
116
+ pet.show()
117
+ if geometry is not None:
118
+ pet.start_moving((geometry.left(), geometry.top(), geometry.right(), geometry.bottom()))
100
119
 
101
120
  def start_play_pet(self) -> None:
102
121
  front_engine_logger.info("[PetSettingUI] start_play_pet")
@@ -105,13 +124,7 @@ class PetSettingUI(QWidget):
105
124
  message_box.setText(language_wrapper.language_word_dict.get("not_prepare"))
106
125
  message_box.exec()
107
126
  return
108
- pet = self._create_pet_widget()
109
- screen = QGuiApplication.primaryScreen()
110
- geometry = screen.availableGeometry() if screen is not None else None
111
- pet.show()
112
- if geometry is not None:
113
- bounds = (geometry.left(), geometry.top(), geometry.right(), geometry.bottom())
114
- pet.start_moving(bounds)
127
+ self._spawn_pet()
115
128
 
116
129
  def choose_and_play(self) -> None:
117
130
  front_engine_logger.info("[PetSettingUI] choose_and_play")
@@ -124,6 +137,28 @@ class PetSettingUI(QWidget):
124
137
  add_recent_file("pet", self.pet_image_path)
125
138
  reload_recent_combobox(self.recent_files_combobox, "pet")
126
139
 
140
+ def choose_pack(self) -> None:
141
+ """選擇動作包資料夾(依 walk/idle/climb/fall/drag 檔名對應狀態)。"""
142
+ front_engine_logger.info("[PetSettingUI] choose_pack")
143
+ folder = QFileDialog.getExistingDirectory(
144
+ self, language_wrapper.language_word_dict.get("pet_choose_pack", "Choose pet pack...")
145
+ )
146
+ if not folder:
147
+ return
148
+ if not scan_pet_pack(folder):
149
+ QMessageBox.warning(
150
+ self,
151
+ language_wrapper.language_word_dict.get("pet_choose_pack", "Choose pet pack..."),
152
+ language_wrapper.language_word_dict.get(
153
+ "pet_pack_empty", "No sprites (walk/idle/climb/fall/drag) found in that folder."),
154
+ )
155
+ return
156
+ self.pet_image_path = folder
157
+ self.ready_to_play = True
158
+ self.ready_label.setText(language_wrapper.language_word_dict.get("Ready"))
159
+ add_recent_file("pet", folder)
160
+ reload_recent_combobox(self.recent_files_combobox, "pet")
161
+
127
162
  def _apply_recent_file(self, _index: int = 0) -> None:
128
163
  path = self.recent_files_combobox.currentData()
129
164
  self.recent_files_combobox.setCurrentIndex(0)
@@ -147,6 +182,7 @@ class PetSettingUI(QWidget):
147
182
  "size": self.size_combobox.currentText(),
148
183
  "speed": self.speed_combobox.currentText(),
149
184
  "behaviour": self.behaviour_combobox.currentData(),
185
+ "climb": self.climb_checkbox.isChecked(),
150
186
  }
151
187
 
152
188
  def set_state(self, state: dict) -> None:
@@ -166,3 +202,5 @@ class PetSettingUI(QWidget):
166
202
  index = self.behaviour_combobox.findData(behaviour)
167
203
  if index >= 0:
168
204
  self.behaviour_combobox.setCurrentIndex(index)
205
+ if "climb" in state:
206
+ self.climb_checkbox.setChecked(bool(state["climb"]))
@@ -223,6 +223,10 @@ english_word_dict = {
223
223
  "pet_behaviour_floor": "Walk on floor",
224
224
  "pet_behaviour_wander": "Free wander",
225
225
  "pet_behaviour_chase": "Chase cursor",
226
+ "pet_climb_label": "Climb walls",
227
+ "pet_clone": "Clone",
228
+ "pet_choose_pack": "Choose pet pack...",
229
+ "pet_pack_empty": "No sprites (walk/idle/climb/fall/drag) found in that folder.",
226
230
  "pet_start": "Spawn pet",
227
231
  "pet_message_box_text": "Pet image not found",
228
232
  }
@@ -224,6 +224,10 @@ traditional_chinese_word_dict = {
224
224
  "pet_behaviour_floor": "沿底部行走",
225
225
  "pet_behaviour_wander": "自由漫遊",
226
226
  "pet_behaviour_chase": "追逐游標",
227
+ "pet_climb_label": "攀爬牆壁",
228
+ "pet_clone": "複製",
229
+ "pet_choose_pack": "選擇動作包資料夾...",
230
+ "pet_pack_empty": "該資料夾找不到精靈檔(walk/idle/climb/fall/drag)。",
227
231
  "pet_start": "召喚寵物",
228
232
  "pet_message_box_text": "找不到寵物圖片",
229
233
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.3
3
+ Version: 1.0.5
4
4
  Summary: FrontEngine is use-define frontview or only use like screen saver
5
5
  Author-email: JE-Chen <jechenmailman@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/Intergration-Automation-Testing/FrontEngine
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "frontengine"
9
- version = "1.0.3"
9
+ version = "1.0.5"
10
10
  authors = [
11
11
  { name = "JE-Chen", email = "jechenmailman@gmail.com" },
12
12
  ]
File without changes
File without changes
File without changes