frontengine 1.0.8__tar.gz → 1.0.10__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.8 → frontengine-1.0.10}/PKG-INFO +1 -1
  2. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/pet/desktop_pet.py +183 -30
  3. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/pet/pet_setting_ui.py +25 -1
  4. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/multi_language/english.py +2 -0
  5. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/multi_language/traditional_chinese.py +2 -0
  6. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine.egg-info/PKG-INFO +1 -1
  7. {frontengine-1.0.8 → frontengine-1.0.10}/pyproject.toml +1 -1
  8. {frontengine-1.0.8 → frontengine-1.0.10}/LICENSE +0 -0
  9. {frontengine-1.0.8 → frontengine-1.0.10}/README.md +0 -0
  10. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/__init__.py +0 -0
  11. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/__main__.py +0 -0
  12. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/__init__.py +0 -0
  13. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/base_widget.py +0 -0
  14. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/gif/__init__.py +0 -0
  15. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/gif/paint_gif.py +0 -0
  16. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/image/__init__.py +0 -0
  17. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/image/paint_image.py +0 -0
  18. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/load/__init__.py +0 -0
  19. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/load/load_someone_make_ui.py +0 -0
  20. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/overlay_factory.py +0 -0
  21. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/particle/__init__.py +0 -0
  22. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/particle/particle_ui.py +0 -0
  23. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/pet/__init__.py +0 -0
  24. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/scene/__init__.py +0 -0
  25. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/scene/extend_graphic_scene.py +0 -0
  26. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/scene/extend_graphic_view.py +0 -0
  27. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/scene/scene.py +0 -0
  28. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/sound_player/__init__.py +0 -0
  29. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/sound_player/sound_effect.py +0 -0
  30. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/sound_player/sound_player.py +0 -0
  31. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/text/__init__.py +0 -0
  32. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/text/draw_text.py +0 -0
  33. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/video/__init__.py +0 -0
  34. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/video/video_player.py +0 -0
  35. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/web/__init__.py +0 -0
  36. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/web/webview.py +0 -0
  37. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/show/window_helpers.py +0 -0
  38. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/system_tray/__init__.py +0 -0
  39. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/system_tray/extend_system_tray.py +0 -0
  40. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/__init__.py +0 -0
  41. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/color/__init__.py +0 -0
  42. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/color/global_color.py +0 -0
  43. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/dialog/__init__.py +0 -0
  44. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
  45. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
  46. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/dialog/save_file_dialog.py +0 -0
  47. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/main_ui.py +0 -0
  48. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/menu/__init__.py +0 -0
  49. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/menu/help_menu.py +0 -0
  50. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/menu/how_to_menu.py +0 -0
  51. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/menu/language_menu.py +0 -0
  52. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/menu/preset_menu.py +0 -0
  53. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/menu/settings_menu.py +0 -0
  54. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/__init__.py +0 -0
  55. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/control_center/__init__.py +0 -0
  56. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/control_center/control_center_ui.py +0 -0
  57. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/gif/__init__.py +0 -0
  58. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
  59. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/image/__init__.py +0 -0
  60. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/image/image_setting_ui.py +0 -0
  61. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/particle/__init__.py +0 -0
  62. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
  63. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/pet/__init__.py +0 -0
  64. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/scene_setting/__init__.py +0 -0
  65. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
  66. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/scene_setting/scene_page/__init__.py +0 -0
  67. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
  68. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
  69. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
  70. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
  71. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
  72. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
  73. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
  74. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
  75. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
  76. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/sound_player/__init__.py +0 -0
  77. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
  78. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/text/__init__.py +0 -0
  79. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/text/text_setting_ui.py +0 -0
  80. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/utils.py +0 -0
  81. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/video/__init__.py +0 -0
  82. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/video/video_setting_ui.py +0 -0
  83. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/web/__init__.py +0 -0
  84. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/ui/page/web/web_setting_ui.py +0 -0
  85. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/user_setting/__init__.py +0 -0
  86. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/user_setting/preset_repository.py +0 -0
  87. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/user_setting/scene_setting.py +0 -0
  88. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/user_setting/user_setting_file.py +0 -0
  89. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/__init__.py +0 -0
  90. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/browser/__init__.py +0 -0
  91. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/browser/browser.py +0 -0
  92. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/critical_exit/__init__.py +0 -0
  93. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
  94. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/critical_exit/critical_exit.py +0 -0
  95. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/critical_exit/win32_vk.py +0 -0
  96. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/exception/__init__.py +0 -0
  97. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/exception/exception_tags.py +0 -0
  98. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/exception/exceptions.py +0 -0
  99. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/file/__init__.py +0 -0
  100. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/file/open/__init__.py +0 -0
  101. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/file/open/open_file.py +0 -0
  102. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/file/save/__init__.py +0 -0
  103. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/file/save/save_file.py +0 -0
  104. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/hotkey/__init__.py +0 -0
  105. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/hotkey/hotkey_service.py +0 -0
  106. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/json/__init__.py +0 -0
  107. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/json/json_file.py +0 -0
  108. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/json/json_repository.py +0 -0
  109. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/json_format/__init__.py +0 -0
  110. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/json_format/json_process.py +0 -0
  111. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/logging/__init__.py +0 -0
  112. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/logging/loggin_instance.py +0 -0
  113. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/multi_language/__init__.py +0 -0
  114. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/multi_language/france.py +0 -0
  115. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/multi_language/germany.py +0 -0
  116. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/multi_language/italy.py +0 -0
  117. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/multi_language/language_wrapper.py +0 -0
  118. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/multi_language/russian.py +0 -0
  119. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
  120. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/preset_schedule/__init__.py +0 -0
  121. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
  122. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/redirect_manager/__init__.py +0 -0
  123. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
  124. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/smart_pause/__init__.py +0 -0
  125. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
  126. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/theme_schedule/__init__.py +0 -0
  127. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
  128. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/utils/web_url.py +0 -0
  129. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/worker/__init__.py +0 -0
  130. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine/worker/qthread_worker.py +0 -0
  131. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine.egg-info/SOURCES.txt +0 -0
  132. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine.egg-info/dependency_links.txt +0 -0
  133. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine.egg-info/entry_points.txt +0 -0
  134. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine.egg-info/requires.txt +0 -0
  135. {frontengine-1.0.8 → frontengine-1.0.10}/frontengine.egg-info/top_level.txt +0 -0
  136. {frontengine-1.0.8 → frontengine-1.0.10}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.8
3
+ Version: 1.0.10
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
@@ -1,10 +1,12 @@
1
1
  import random as _random_module
2
+ import sys
2
3
  from datetime import datetime
3
4
  from pathlib import Path
4
5
  from typing import Optional, Tuple
5
6
 
6
- from PySide6.QtCore import Qt, QRect, QTimer, Signal
7
+ from PySide6.QtCore import Qt, QRect, QTimer, QUrl, Signal
7
8
  from PySide6.QtGui import QAction, QColor, QCursor, QFont, QFontMetrics, QMovie, QPainter, QPixmap, QTransform
9
+ from PySide6.QtMultimedia import QSoundEffect
8
10
  from PySide6.QtWidgets import QMenu, QMessageBox, QWidget
9
11
 
10
12
  from frontengine.show.base_widget import BaseWidget
@@ -95,6 +97,53 @@ def derive_visual_state(dragging: bool, airborne: bool, surface: str, motion_sta
95
97
  return STATE_WALK
96
98
 
97
99
 
100
+ # 列舉可站立視窗時要略過的外殼視窗類別
101
+ # Shell window classes skipped when enumerating standable windows.
102
+ _PLATFORM_SKIP_CLASSES = {"WorkerW", "Progman", "Shell_TrayWnd", "Button"}
103
+
104
+
105
+ def windows_platforms(exclude_hwnds=()) -> list:
106
+ """
107
+ 回傳目前可見頂層視窗的上緣做為可站立平台 (left, right, top_y)。僅 Windows
108
+ 有效,其餘平台或發生錯誤時回傳空清單。
109
+ Return the top edges of visible top-level windows as standable platforms
110
+ (left, right, top_y). Windows only; returns [] elsewhere or on error.
111
+ """
112
+ if sys.platform != "win32":
113
+ return []
114
+ try:
115
+ import ctypes
116
+ from ctypes import wintypes
117
+
118
+ user32 = ctypes.windll.user32
119
+ exclude = {int(h) for h in exclude_hwnds}
120
+ platforms: list = []
121
+
122
+ @ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
123
+ def _collect(hwnd, _lparam):
124
+ if int(hwnd) in exclude or not user32.IsWindowVisible(hwnd):
125
+ return True
126
+ rect = wintypes.RECT()
127
+ if not user32.GetWindowRect(hwnd, ctypes.byref(rect)):
128
+ return True
129
+ width = rect.right - rect.left
130
+ height = rect.bottom - rect.top
131
+ if width < 80 or height < 40:
132
+ return True
133
+ class_buffer = ctypes.create_unicode_buffer(64)
134
+ user32.GetClassNameW(hwnd, class_buffer, 64)
135
+ if class_buffer.value in _PLATFORM_SKIP_CLASSES:
136
+ return True
137
+ platforms.append((rect.left, rect.right, rect.top))
138
+ return True
139
+
140
+ user32.EnumWindows(_collect, 0)
141
+ return platforms
142
+ except Exception as error: # pragma: no cover - Win32 boundary
143
+ front_engine_logger.warning(f"[windows_platforms] error: {error!r}")
144
+ return []
145
+
146
+
98
147
  def message_bucket(hour: int) -> str:
99
148
  """依小時回傳時段 / Time-of-day bucket for the given hour."""
100
149
  hour = int(hour) % 24
@@ -251,6 +300,12 @@ class PetMotion:
251
300
  self.asleep = False
252
301
  self.climb = bool(climb)
253
302
  self.surface = SURFACE_FLOOR
303
+ # 可站立的視窗平台 (left, right, top_y);站在平台上時的狀態
304
+ # Standable window platforms (left, right, top_y) and platform state.
305
+ self.platforms: list = []
306
+ self.on_platform = False
307
+ self.platform_span: Optional[Tuple[float, float]] = None
308
+ self.ground_feet = 0.0
254
309
 
255
310
  @property
256
311
  def facing_left(self) -> bool:
@@ -259,6 +314,10 @@ class PetMotion:
259
314
  def set_bounds(self, bounds: Tuple[int, int, int, int]) -> None:
260
315
  self.bounds = bounds
261
316
 
317
+ def set_platforms(self, platforms) -> None:
318
+ """設定可站立的視窗上緣平台 / Set standable window top-edge platforms."""
319
+ self.platforms = list(platforms)
320
+
262
321
  def set_target(self, x: float, y: float) -> None:
263
322
  self.target = (float(x), float(y))
264
323
 
@@ -269,6 +328,8 @@ class PetMotion:
269
328
  self._airborne = True
270
329
  self.asleep = False
271
330
  self.surface = SURFACE_FLOOR
331
+ self.on_platform = False
332
+ self.platform_span = None
272
333
 
273
334
  def step(self) -> Tuple[int, int]:
274
335
  left, top, right, bottom = self.bounds
@@ -279,38 +340,19 @@ class PetMotion:
279
340
  return self._step_wander(left, top, right, bottom)
280
341
  return self._step_floor(left, top, right, bottom, floor_y)
281
342
 
282
- # --- floor / gravity / climbing ---
343
+ # --- floor / gravity / climbing / window platforms ---
283
344
  def _step_floor(self, left, top, right, bottom, floor_y) -> Tuple[int, int]:
284
- airborne = self._airborne or (self.surface == SURFACE_FLOOR and self.y < floor_y - 0.5)
345
+ airborne = self._airborne or (
346
+ not self.on_platform and self.surface == SURFACE_FLOOR and self.y < floor_y - 0.5
347
+ )
285
348
  if airborne:
286
- self.vy += self.GRAVITY
287
- self.x += self.vx
288
- self.y += self.vy
289
- if self.x <= left:
290
- self.x = float(left)
291
- self.vx = abs(self.vx) * self.WALL_DAMPING
292
- elif self.x + self.width >= right:
293
- self.x = float(right - self.width)
294
- self.vx = -abs(self.vx) * self.WALL_DAMPING
295
- if self.y <= top:
296
- self.y = float(top)
297
- self.vy = abs(self.vy) * self.WALL_DAMPING
298
- if self.y >= floor_y:
299
- self.y = float(floor_y)
300
- if abs(self.vy) > self.BOUNCE_THRESHOLD:
301
- self.vy = -self.vy * self.BOUNCE_DAMPING
302
- else:
303
- self.vy = 0.0
304
- self._airborne = False
305
- self.surface = SURFACE_FLOOR
306
- self.vx = float(self.speed) if self.vx >= 0 else float(-self.speed)
307
- self._new_state(force_walk=True)
308
- return int(self.x), int(self.y)
309
-
349
+ return self._step_fall(left, top, right, bottom, floor_y)
350
+ if self.on_platform:
351
+ return self._step_platform(left, top, right, bottom, floor_y)
310
352
  if self.surface != SURFACE_FLOOR:
311
353
  return self._step_climb(left, top, right, bottom, floor_y)
312
354
 
313
- # grounded on the floor: random walk / idle, with a chance to climb a wall
355
+ # grounded on the screen floor: random walk / idle, with a chance to climb a wall
314
356
  self.y = float(floor_y)
315
357
  if self._state_ticks <= 0:
316
358
  self._new_state()
@@ -334,6 +376,84 @@ class PetMotion:
334
376
  self.vx = -abs(self.vx)
335
377
  return int(self.x), int(self.y)
336
378
 
379
+ def _step_fall(self, left, top, right, bottom, floor_y) -> Tuple[int, int]:
380
+ """落下:撞牆/天花板反彈,並在越過視窗平台或螢幕地板時落地。"""
381
+ prev_feet = self.y + self.height
382
+ self.vy += self.GRAVITY
383
+ self.x += self.vx
384
+ self.y += self.vy
385
+ if self.x <= left:
386
+ self.x = float(left)
387
+ self.vx = abs(self.vx) * self.WALL_DAMPING
388
+ elif self.x + self.width >= right:
389
+ self.x = float(right - self.width)
390
+ self.vx = -abs(self.vx) * self.WALL_DAMPING
391
+ if self.y <= top:
392
+ self.y = float(top)
393
+ self.vy = abs(self.vy) * self.WALL_DAMPING
394
+ new_feet = self.y + self.height
395
+ center_x = self.x + self.width / 2.0
396
+
397
+ land_feet = None
398
+ land_span = None
399
+ for platform_left, platform_right, platform_y in self.platforms:
400
+ if platform_left <= center_x <= platform_right and prev_feet <= platform_y <= new_feet:
401
+ if land_feet is None or platform_y < land_feet:
402
+ land_feet = float(platform_y)
403
+ land_span = (float(platform_left), float(platform_right))
404
+ if prev_feet <= bottom <= new_feet and (land_feet is None or bottom < land_feet):
405
+ land_feet = float(bottom)
406
+ land_span = None
407
+
408
+ if land_feet is not None:
409
+ self.y = float(land_feet - self.height)
410
+ if abs(self.vy) > self.BOUNCE_THRESHOLD:
411
+ self.vy = -self.vy * self.BOUNCE_DAMPING
412
+ else:
413
+ self.vy = 0.0
414
+ self._airborne = False
415
+ self.surface = SURFACE_FLOOR
416
+ self.on_platform = land_span is not None
417
+ self.platform_span = land_span
418
+ self.ground_feet = land_feet
419
+ self.vx = float(self.speed) if self.vx >= 0 else float(-self.speed)
420
+ self._new_state(force_walk=True)
421
+ return int(self.x), int(self.y)
422
+
423
+ def _platform_under(self) -> Optional[Tuple[float, float]]:
424
+ """回傳目前站立平台的水平範圍;平台已消失(視窗移走/關閉)則回傳 None。"""
425
+ body_left = self.x
426
+ body_right = self.x + self.width
427
+ for platform_left, platform_right, platform_y in self.platforms:
428
+ if abs(platform_y - self.ground_feet) <= 2.0 and not (platform_right < body_left or platform_left > body_right):
429
+ return (float(platform_left), float(platform_right))
430
+ return None
431
+
432
+ def _step_platform(self, left, top, right, bottom, floor_y) -> Tuple[int, int]:
433
+ """站在視窗平台上走動;到平台兩端轉向;平台消失則掉落。"""
434
+ span = self._platform_under()
435
+ if span is None:
436
+ self._airborne = True
437
+ self.on_platform = False
438
+ self.platform_span = None
439
+ return self._step_fall(left, top, right, bottom, floor_y)
440
+ self.platform_span = span
441
+ span_left, span_right = span
442
+ self.y = float(self.ground_feet - self.height)
443
+ if self._state_ticks <= 0:
444
+ self._new_state()
445
+ self._state_ticks -= 1
446
+ if self.state != "walk":
447
+ return int(self.x), int(self.y)
448
+ self.x += self.vx
449
+ if self.x <= span_left:
450
+ self.x = float(span_left)
451
+ self.vx = abs(self.vx)
452
+ elif self.x + self.width >= span_right:
453
+ self.x = float(span_right - self.width)
454
+ self.vx = -abs(self.vx)
455
+ return int(self.x), int(self.y)
456
+
337
457
  def _step_climb(self, left, top, right, bottom, floor_y) -> Tuple[int, int]:
338
458
  """沿螢幕邊緣爬行;隨機放手則掉落 / Crawl the screen perimeter; may let go and fall."""
339
459
  if self._rng.random() < self.DROP_CHANCE:
@@ -451,10 +571,12 @@ class DesktopPetWidget(BaseWidget):
451
571
  clone_requested = Signal()
452
572
 
453
573
  def __init__(self, image_path: str, size: int = 128, speed: int = 3,
454
- behaviour: str = BEHAVIOUR_FLOOR, climb: bool = True, talk: bool = True):
574
+ behaviour: str = BEHAVIOUR_FLOOR, climb: bool = True, talk: bool = True,
575
+ sound_path: Optional[str] = None, sit_on_windows: bool = True):
455
576
  front_engine_logger.info(
456
577
  f"[DesktopPetWidget] Init | path={image_path}, size={size}, speed={speed}, "
457
- f"behaviour={behaviour}, climb={climb}, talk={talk}"
578
+ f"behaviour={behaviour}, climb={climb}, talk={talk}, sound={sound_path}, "
579
+ f"sit_on_windows={sit_on_windows}"
458
580
  )
459
581
  super().__init__()
460
582
  self.opacity = 1.0
@@ -490,6 +612,10 @@ class DesktopPetWidget(BaseWidget):
490
612
  self._chatter_rng = _random_module.SystemRandom()
491
613
  self._mood = PetMood(user_setting_dict.get("pet_mood", 60))
492
614
  self._messages = self._load_messages()
615
+ self._sound: Optional[QSoundEffect] = None
616
+ self._init_sound(sound_path)
617
+ self._sit_on_windows = bool(sit_on_windows)
618
+ self._platform_tick = 0
493
619
  self._bubble = SpeechBubble()
494
620
  self._chatter_timer = QTimer(self)
495
621
  self._chatter_timer.setSingleShot(True)
@@ -592,6 +718,27 @@ class DesktopPetWidget(BaseWidget):
592
718
  def _persist_mood(self) -> None:
593
719
  user_setting_dict["pet_mood"] = self._mood.value
594
720
 
721
+ def _init_sound(self, sound_path: Optional[str]) -> None:
722
+ if not sound_path:
723
+ return
724
+ path = Path(sound_path)
725
+ if not (path.exists() and path.is_file()):
726
+ return
727
+ try:
728
+ self._sound = QSoundEffect(self)
729
+ self._sound.setSource(QUrl.fromLocalFile(str(path)))
730
+ except Exception as error: # pragma: no cover - audio backend guard
731
+ front_engine_logger.warning(f"[DesktopPetWidget] sound load failed: {error!r}")
732
+ self._sound = None
733
+
734
+ def _play_sound(self) -> None:
735
+ if self._sound is None:
736
+ return
737
+ try:
738
+ self._sound.play()
739
+ except Exception as error: # pragma: no cover - audio backend guard
740
+ front_engine_logger.warning(f"[DesktopPetWidget] sound play failed: {error!r}")
741
+
595
742
  def _schedule_chatter(self) -> None:
596
743
  if self._talk:
597
744
  self._chatter_timer.start(self._chatter_rng.randint(15000, 35000))
@@ -620,6 +767,11 @@ class DesktopPetWidget(BaseWidget):
620
767
  if self.motion.behaviour == BEHAVIOUR_CHASE:
621
768
  cursor = QCursor.pos()
622
769
  self.motion.set_target(cursor.x(), cursor.y())
770
+ elif self._sit_on_windows and self.motion.behaviour == BEHAVIOUR_FLOOR:
771
+ self._platform_tick += 1
772
+ if self._platform_tick >= 10:
773
+ self._platform_tick = 0
774
+ self.motion.set_platforms(windows_platforms(exclude_hwnds=(int(self.winId()),)))
623
775
  x, y = self.motion.step()
624
776
  self.move(x, y)
625
777
  self._set_active_sprite(self._visual_state())
@@ -636,6 +788,7 @@ class DesktopPetWidget(BaseWidget):
636
788
  self.motion.wake() # interacting wakes a sleeping pet
637
789
  self._mood.pet() # petting cheers it up
638
790
  self._persist_mood()
791
+ self._play_sound()
639
792
  self._set_active_sprite(STATE_DRAG)
640
793
  super().mousePressEvent(event)
641
794
 
@@ -8,7 +8,7 @@ from PySide6.QtWidgets import (
8
8
  from frontengine.show.pet.desktop_pet import (
9
9
  DesktopPetWidget, BEHAVIOUR_FLOOR, BEHAVIOUR_WANDER, BEHAVIOUR_CHASE, scan_pet_pack,
10
10
  )
11
- from frontengine.ui.dialog.choose_file_dialog import choose_pet
11
+ from frontengine.ui.dialog.choose_file_dialog import choose_pet, choose_wav_sound
12
12
  from frontengine.ui.page.utils import (
13
13
  build_recent_combobox,
14
14
  enable_file_drop,
@@ -32,6 +32,7 @@ class PetSettingUI(QWidget):
32
32
  self.pet_list: list = []
33
33
  self.ready_to_play = False
34
34
  self.pet_image_path: Optional[str] = None
35
+ self.pet_sound_path: Optional[str] = None
35
36
 
36
37
  # Choose file
37
38
  self.choose_file_button = QPushButton(
@@ -42,6 +43,10 @@ class PetSettingUI(QWidget):
42
43
  language_wrapper.language_word_dict.get("pet_choose_pack", "Choose pet pack...")
43
44
  )
44
45
  self.choose_pack_button.clicked.connect(self.choose_pack)
46
+ self.choose_sound_button = QPushButton(
47
+ language_wrapper.language_word_dict.get("pet_choose_sound", "Choose sound (optional)")
48
+ )
49
+ self.choose_sound_button.clicked.connect(self.choose_sound)
45
50
  self.ready_label = QLabel(language_wrapper.language_word_dict.get("Not Ready"))
46
51
 
47
52
  # Size
@@ -71,6 +76,8 @@ class PetSettingUI(QWidget):
71
76
  self.climb_checkbox.setChecked(True)
72
77
  self.talk_checkbox = QCheckBox(language_wrapper.language_word_dict.get("pet_talk_label", "Speech bubbles"))
73
78
  self.talk_checkbox.setChecked(True)
79
+ self.sit_checkbox = QCheckBox(language_wrapper.language_word_dict.get("pet_sit_label", "Sit on windows"))
80
+ self.sit_checkbox.setChecked(True)
74
81
 
75
82
  # Start
76
83
  self.start_button = QPushButton(language_wrapper.language_word_dict.get("pet_start", "Spawn pet"))
@@ -88,6 +95,7 @@ class PetSettingUI(QWidget):
88
95
  self.grid_layout.addWidget(self.choose_file_button, 0, 0)
89
96
  self.grid_layout.addWidget(self.ready_label, 0, 1)
90
97
  self.grid_layout.addWidget(self.choose_pack_button, 0, 2)
98
+ self.grid_layout.addWidget(self.choose_sound_button, 1, 2)
91
99
  self.grid_layout.addWidget(self.size_label, 1, 0)
92
100
  self.grid_layout.addWidget(self.size_combobox, 1, 1)
93
101
  self.grid_layout.addWidget(self.speed_label, 2, 0)
@@ -96,6 +104,7 @@ class PetSettingUI(QWidget):
96
104
  self.grid_layout.addWidget(self.behaviour_combobox, 3, 1)
97
105
  self.grid_layout.addWidget(self.climb_checkbox, 3, 2)
98
106
  self.grid_layout.addWidget(self.talk_checkbox, 2, 2)
107
+ self.grid_layout.addWidget(self.sit_checkbox, 4, 2)
99
108
  self.grid_layout.addWidget(self.start_button, 4, 0)
100
109
  self.grid_layout.addWidget(self.recent_files_label, 5, 0)
101
110
  self.grid_layout.addWidget(self.recent_files_combobox, 5, 1)
@@ -111,6 +120,8 @@ class PetSettingUI(QWidget):
111
120
  behaviour=self.behaviour_combobox.currentData(),
112
121
  climb=self.climb_checkbox.isChecked(),
113
122
  talk=self.talk_checkbox.isChecked(),
123
+ sound_path=self.pet_sound_path,
124
+ sit_on_windows=self.sit_checkbox.isChecked(),
114
125
  )
115
126
  pet.clone_requested.connect(self._spawn_pet)
116
127
  pet.set_pet_window_flag()
@@ -141,6 +152,13 @@ class PetSettingUI(QWidget):
141
152
  add_recent_file("pet", self.pet_image_path)
142
153
  reload_recent_combobox(self.recent_files_combobox, "pet")
143
154
 
155
+ def choose_sound(self) -> None:
156
+ """選擇點擊時播放的音效(選填)/ Choose an optional click sound."""
157
+ front_engine_logger.info("[PetSettingUI] choose_sound")
158
+ path = choose_wav_sound(self)
159
+ if path:
160
+ self.pet_sound_path = path
161
+
144
162
  def choose_pack(self) -> None:
145
163
  """選擇動作包資料夾(依 walk/idle/climb/fall/drag 檔名對應狀態)。"""
146
164
  front_engine_logger.info("[PetSettingUI] choose_pack")
@@ -188,6 +206,8 @@ class PetSettingUI(QWidget):
188
206
  "behaviour": self.behaviour_combobox.currentData(),
189
207
  "climb": self.climb_checkbox.isChecked(),
190
208
  "talk": self.talk_checkbox.isChecked(),
209
+ "sound": self.pet_sound_path,
210
+ "sit": self.sit_checkbox.isChecked(),
191
211
  }
192
212
 
193
213
  def set_state(self, state: dict) -> None:
@@ -211,3 +231,7 @@ class PetSettingUI(QWidget):
211
231
  self.climb_checkbox.setChecked(bool(state["climb"]))
212
232
  if "talk" in state:
213
233
  self.talk_checkbox.setChecked(bool(state["talk"]))
234
+ if state.get("sound"):
235
+ self.pet_sound_path = str(state["sound"])
236
+ if "sit" in state:
237
+ self.sit_checkbox.setChecked(bool(state["sit"]))
@@ -226,8 +226,10 @@ english_word_dict = {
226
226
  "pet_climb_label": "Climb walls",
227
227
  "pet_clone": "Clone",
228
228
  "pet_choose_pack": "Choose pet pack...",
229
+ "pet_choose_sound": "Choose sound (optional)",
229
230
  "pet_pack_empty": "No sprites (walk/idle/sleep/climb/fall/drag) found in that folder.",
230
231
  "pet_talk_label": "Speech bubbles",
232
+ "pet_sit_label": "Sit on windows",
231
233
  "pet_chatter_morning": "Good morning!|Rise and shine!|Ready for the day?",
232
234
  "pet_chatter_afternoon": "Good afternoon!|Taking a break?|Keep it up!",
233
235
  "pet_chatter_evening": "Good evening!|How was your day?|Almost done?",
@@ -227,8 +227,10 @@ traditional_chinese_word_dict = {
227
227
  "pet_climb_label": "攀爬牆壁",
228
228
  "pet_clone": "複製",
229
229
  "pet_choose_pack": "選擇動作包資料夾...",
230
+ "pet_choose_sound": "選擇音效(選填)",
230
231
  "pet_pack_empty": "該資料夾找不到精靈檔(walk/idle/sleep/climb/fall/drag)。",
231
232
  "pet_talk_label": "對話泡泡",
233
+ "pet_sit_label": "站在視窗上",
232
234
  "pet_chatter_morning": "早安!|新的一天開始囉!|準備好了嗎?",
233
235
  "pet_chatter_afternoon": "午安!|要不要休息一下?|繼續加油!",
234
236
  "pet_chatter_evening": "晚上好!|今天過得如何?|快完成了嗎?",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.8
3
+ Version: 1.0.10
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.8"
9
+ version = "1.0.10"
10
10
  authors = [
11
11
  { name = "JE-Chen", email = "jechenmailman@gmail.com" },
12
12
  ]
File without changes
File without changes
File without changes