frontengine 1.0.9__tar.gz → 1.0.11__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.9 → frontengine-1.0.11}/PKG-INFO +1 -1
  2. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/pet/desktop_pet.py +214 -29
  3. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/pet/pet_setting_ui.py +20 -0
  4. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/multi_language/english.py +2 -0
  5. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/multi_language/traditional_chinese.py +2 -0
  6. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine.egg-info/PKG-INFO +1 -1
  7. {frontengine-1.0.9 → frontengine-1.0.11}/pyproject.toml +1 -1
  8. {frontengine-1.0.9 → frontengine-1.0.11}/LICENSE +0 -0
  9. {frontengine-1.0.9 → frontengine-1.0.11}/README.md +0 -0
  10. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/__init__.py +0 -0
  11. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/__main__.py +0 -0
  12. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/__init__.py +0 -0
  13. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/base_widget.py +0 -0
  14. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/gif/__init__.py +0 -0
  15. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/gif/paint_gif.py +0 -0
  16. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/image/__init__.py +0 -0
  17. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/image/paint_image.py +0 -0
  18. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/load/__init__.py +0 -0
  19. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/load/load_someone_make_ui.py +0 -0
  20. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/overlay_factory.py +0 -0
  21. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/particle/__init__.py +0 -0
  22. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/particle/particle_ui.py +0 -0
  23. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/pet/__init__.py +0 -0
  24. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/scene/__init__.py +0 -0
  25. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/scene/extend_graphic_scene.py +0 -0
  26. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/scene/extend_graphic_view.py +0 -0
  27. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/scene/scene.py +0 -0
  28. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/sound_player/__init__.py +0 -0
  29. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/sound_player/sound_effect.py +0 -0
  30. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/sound_player/sound_player.py +0 -0
  31. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/text/__init__.py +0 -0
  32. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/text/draw_text.py +0 -0
  33. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/video/__init__.py +0 -0
  34. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/video/video_player.py +0 -0
  35. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/web/__init__.py +0 -0
  36. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/web/webview.py +0 -0
  37. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/show/window_helpers.py +0 -0
  38. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/system_tray/__init__.py +0 -0
  39. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/system_tray/extend_system_tray.py +0 -0
  40. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/__init__.py +0 -0
  41. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/color/__init__.py +0 -0
  42. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/color/global_color.py +0 -0
  43. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/dialog/__init__.py +0 -0
  44. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
  45. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
  46. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/dialog/save_file_dialog.py +0 -0
  47. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/main_ui.py +0 -0
  48. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/menu/__init__.py +0 -0
  49. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/menu/help_menu.py +0 -0
  50. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/menu/how_to_menu.py +0 -0
  51. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/menu/language_menu.py +0 -0
  52. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/menu/preset_menu.py +0 -0
  53. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/menu/settings_menu.py +0 -0
  54. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/__init__.py +0 -0
  55. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/control_center/__init__.py +0 -0
  56. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/control_center/control_center_ui.py +0 -0
  57. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/gif/__init__.py +0 -0
  58. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
  59. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/image/__init__.py +0 -0
  60. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/image/image_setting_ui.py +0 -0
  61. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/particle/__init__.py +0 -0
  62. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
  63. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/pet/__init__.py +0 -0
  64. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/scene_setting/__init__.py +0 -0
  65. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
  66. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/scene_setting/scene_page/__init__.py +0 -0
  67. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
  68. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
  69. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
  70. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
  71. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
  72. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
  73. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
  74. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
  75. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
  76. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/sound_player/__init__.py +0 -0
  77. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
  78. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/text/__init__.py +0 -0
  79. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/text/text_setting_ui.py +0 -0
  80. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/utils.py +0 -0
  81. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/video/__init__.py +0 -0
  82. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/video/video_setting_ui.py +0 -0
  83. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/web/__init__.py +0 -0
  84. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/ui/page/web/web_setting_ui.py +0 -0
  85. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/user_setting/__init__.py +0 -0
  86. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/user_setting/preset_repository.py +0 -0
  87. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/user_setting/scene_setting.py +0 -0
  88. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/user_setting/user_setting_file.py +0 -0
  89. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/__init__.py +0 -0
  90. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/browser/__init__.py +0 -0
  91. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/browser/browser.py +0 -0
  92. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/critical_exit/__init__.py +0 -0
  93. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
  94. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/critical_exit/critical_exit.py +0 -0
  95. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/critical_exit/win32_vk.py +0 -0
  96. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/exception/__init__.py +0 -0
  97. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/exception/exception_tags.py +0 -0
  98. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/exception/exceptions.py +0 -0
  99. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/file/__init__.py +0 -0
  100. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/file/open/__init__.py +0 -0
  101. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/file/open/open_file.py +0 -0
  102. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/file/save/__init__.py +0 -0
  103. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/file/save/save_file.py +0 -0
  104. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/hotkey/__init__.py +0 -0
  105. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/hotkey/hotkey_service.py +0 -0
  106. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/json/__init__.py +0 -0
  107. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/json/json_file.py +0 -0
  108. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/json/json_repository.py +0 -0
  109. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/json_format/__init__.py +0 -0
  110. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/json_format/json_process.py +0 -0
  111. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/logging/__init__.py +0 -0
  112. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/logging/loggin_instance.py +0 -0
  113. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/multi_language/__init__.py +0 -0
  114. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/multi_language/france.py +0 -0
  115. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/multi_language/germany.py +0 -0
  116. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/multi_language/italy.py +0 -0
  117. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/multi_language/language_wrapper.py +0 -0
  118. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/multi_language/russian.py +0 -0
  119. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
  120. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/preset_schedule/__init__.py +0 -0
  121. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
  122. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/redirect_manager/__init__.py +0 -0
  123. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
  124. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/smart_pause/__init__.py +0 -0
  125. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
  126. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/theme_schedule/__init__.py +0 -0
  127. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
  128. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/utils/web_url.py +0 -0
  129. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/worker/__init__.py +0 -0
  130. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine/worker/qthread_worker.py +0 -0
  131. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine.egg-info/SOURCES.txt +0 -0
  132. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine.egg-info/dependency_links.txt +0 -0
  133. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine.egg-info/entry_points.txt +0 -0
  134. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine.egg-info/requires.txt +0 -0
  135. {frontengine-1.0.9 → frontengine-1.0.11}/frontengine.egg-info/top_level.txt +0 -0
  136. {frontengine-1.0.9 → frontengine-1.0.11}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.9
3
+ Version: 1.0.11
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,4 +1,5 @@
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
@@ -96,6 +97,70 @@ def derive_visual_state(dragging: bool, airborne: bool, surface: str, motion_sta
96
97
  return STATE_WALK
97
98
 
98
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
+
147
+ def nearest_peer(center, peers):
148
+ """
149
+ 回傳離 center 最近的同伴座標與距離;無同伴回傳 (None, inf)。
150
+ Return the nearest peer center and its distance to `center`, or
151
+ (None, inf) when there are no peers.
152
+ """
153
+ best = None
154
+ best_distance = float("inf")
155
+ cx, cy = center
156
+ for peer_x, peer_y in peers:
157
+ distance = ((peer_x - cx) ** 2 + (peer_y - cy) ** 2) ** 0.5
158
+ if distance < best_distance:
159
+ best_distance = distance
160
+ best = (peer_x, peer_y)
161
+ return best, best_distance
162
+
163
+
99
164
  def message_bucket(hour: int) -> str:
100
165
  """依小時回傳時段 / Time-of-day bucket for the given hour."""
101
166
  hour = int(hour) % 24
@@ -252,6 +317,12 @@ class PetMotion:
252
317
  self.asleep = False
253
318
  self.climb = bool(climb)
254
319
  self.surface = SURFACE_FLOOR
320
+ # 可站立的視窗平台 (left, right, top_y);站在平台上時的狀態
321
+ # Standable window platforms (left, right, top_y) and platform state.
322
+ self.platforms: list = []
323
+ self.on_platform = False
324
+ self.platform_span: Optional[Tuple[float, float]] = None
325
+ self.ground_feet = 0.0
255
326
 
256
327
  @property
257
328
  def facing_left(self) -> bool:
@@ -260,6 +331,10 @@ class PetMotion:
260
331
  def set_bounds(self, bounds: Tuple[int, int, int, int]) -> None:
261
332
  self.bounds = bounds
262
333
 
334
+ def set_platforms(self, platforms) -> None:
335
+ """設定可站立的視窗上緣平台 / Set standable window top-edge platforms."""
336
+ self.platforms = list(platforms)
337
+
263
338
  def set_target(self, x: float, y: float) -> None:
264
339
  self.target = (float(x), float(y))
265
340
 
@@ -270,6 +345,8 @@ class PetMotion:
270
345
  self._airborne = True
271
346
  self.asleep = False
272
347
  self.surface = SURFACE_FLOOR
348
+ self.on_platform = False
349
+ self.platform_span = None
273
350
 
274
351
  def step(self) -> Tuple[int, int]:
275
352
  left, top, right, bottom = self.bounds
@@ -280,38 +357,19 @@ class PetMotion:
280
357
  return self._step_wander(left, top, right, bottom)
281
358
  return self._step_floor(left, top, right, bottom, floor_y)
282
359
 
283
- # --- floor / gravity / climbing ---
360
+ # --- floor / gravity / climbing / window platforms ---
284
361
  def _step_floor(self, left, top, right, bottom, floor_y) -> Tuple[int, int]:
285
- airborne = self._airborne or (self.surface == SURFACE_FLOOR and self.y < floor_y - 0.5)
362
+ airborne = self._airborne or (
363
+ not self.on_platform and self.surface == SURFACE_FLOOR and self.y < floor_y - 0.5
364
+ )
286
365
  if airborne:
287
- self.vy += self.GRAVITY
288
- self.x += self.vx
289
- self.y += self.vy
290
- if self.x <= left:
291
- self.x = float(left)
292
- self.vx = abs(self.vx) * self.WALL_DAMPING
293
- elif self.x + self.width >= right:
294
- self.x = float(right - self.width)
295
- self.vx = -abs(self.vx) * self.WALL_DAMPING
296
- if self.y <= top:
297
- self.y = float(top)
298
- self.vy = abs(self.vy) * self.WALL_DAMPING
299
- if self.y >= floor_y:
300
- self.y = float(floor_y)
301
- if abs(self.vy) > self.BOUNCE_THRESHOLD:
302
- self.vy = -self.vy * self.BOUNCE_DAMPING
303
- else:
304
- self.vy = 0.0
305
- self._airborne = False
306
- self.surface = SURFACE_FLOOR
307
- self.vx = float(self.speed) if self.vx >= 0 else float(-self.speed)
308
- self._new_state(force_walk=True)
309
- return int(self.x), int(self.y)
310
-
366
+ return self._step_fall(left, top, right, bottom, floor_y)
367
+ if self.on_platform:
368
+ return self._step_platform(left, top, right, bottom, floor_y)
311
369
  if self.surface != SURFACE_FLOOR:
312
370
  return self._step_climb(left, top, right, bottom, floor_y)
313
371
 
314
- # grounded on the floor: random walk / idle, with a chance to climb a wall
372
+ # grounded on the screen floor: random walk / idle, with a chance to climb a wall
315
373
  self.y = float(floor_y)
316
374
  if self._state_ticks <= 0:
317
375
  self._new_state()
@@ -335,6 +393,84 @@ class PetMotion:
335
393
  self.vx = -abs(self.vx)
336
394
  return int(self.x), int(self.y)
337
395
 
396
+ def _step_fall(self, left, top, right, bottom, floor_y) -> Tuple[int, int]:
397
+ """落下:撞牆/天花板反彈,並在越過視窗平台或螢幕地板時落地。"""
398
+ prev_feet = self.y + self.height
399
+ self.vy += self.GRAVITY
400
+ self.x += self.vx
401
+ self.y += self.vy
402
+ if self.x <= left:
403
+ self.x = float(left)
404
+ self.vx = abs(self.vx) * self.WALL_DAMPING
405
+ elif self.x + self.width >= right:
406
+ self.x = float(right - self.width)
407
+ self.vx = -abs(self.vx) * self.WALL_DAMPING
408
+ if self.y <= top:
409
+ self.y = float(top)
410
+ self.vy = abs(self.vy) * self.WALL_DAMPING
411
+ new_feet = self.y + self.height
412
+ center_x = self.x + self.width / 2.0
413
+
414
+ land_feet = None
415
+ land_span = None
416
+ for platform_left, platform_right, platform_y in self.platforms:
417
+ if platform_left <= center_x <= platform_right and prev_feet <= platform_y <= new_feet:
418
+ if land_feet is None or platform_y < land_feet:
419
+ land_feet = float(platform_y)
420
+ land_span = (float(platform_left), float(platform_right))
421
+ if prev_feet <= bottom <= new_feet and (land_feet is None or bottom < land_feet):
422
+ land_feet = float(bottom)
423
+ land_span = None
424
+
425
+ if land_feet is not None:
426
+ self.y = float(land_feet - self.height)
427
+ if abs(self.vy) > self.BOUNCE_THRESHOLD:
428
+ self.vy = -self.vy * self.BOUNCE_DAMPING
429
+ else:
430
+ self.vy = 0.0
431
+ self._airborne = False
432
+ self.surface = SURFACE_FLOOR
433
+ self.on_platform = land_span is not None
434
+ self.platform_span = land_span
435
+ self.ground_feet = land_feet
436
+ self.vx = float(self.speed) if self.vx >= 0 else float(-self.speed)
437
+ self._new_state(force_walk=True)
438
+ return int(self.x), int(self.y)
439
+
440
+ def _platform_under(self) -> Optional[Tuple[float, float]]:
441
+ """回傳目前站立平台的水平範圍;平台已消失(視窗移走/關閉)則回傳 None。"""
442
+ body_left = self.x
443
+ body_right = self.x + self.width
444
+ for platform_left, platform_right, platform_y in self.platforms:
445
+ if abs(platform_y - self.ground_feet) <= 2.0 and not (platform_right < body_left or platform_left > body_right):
446
+ return (float(platform_left), float(platform_right))
447
+ return None
448
+
449
+ def _step_platform(self, left, top, right, bottom, floor_y) -> Tuple[int, int]:
450
+ """站在視窗平台上走動;到平台兩端轉向;平台消失則掉落。"""
451
+ span = self._platform_under()
452
+ if span is None:
453
+ self._airborne = True
454
+ self.on_platform = False
455
+ self.platform_span = None
456
+ return self._step_fall(left, top, right, bottom, floor_y)
457
+ self.platform_span = span
458
+ span_left, span_right = span
459
+ self.y = float(self.ground_feet - self.height)
460
+ if self._state_ticks <= 0:
461
+ self._new_state()
462
+ self._state_ticks -= 1
463
+ if self.state != "walk":
464
+ return int(self.x), int(self.y)
465
+ self.x += self.vx
466
+ if self.x <= span_left:
467
+ self.x = float(span_left)
468
+ self.vx = abs(self.vx)
469
+ elif self.x + self.width >= span_right:
470
+ self.x = float(span_right - self.width)
471
+ self.vx = -abs(self.vx)
472
+ return int(self.x), int(self.y)
473
+
338
474
  def _step_climb(self, left, top, right, bottom, floor_y) -> Tuple[int, int]:
339
475
  """沿螢幕邊緣爬行;隨機放手則掉落 / Crawl the screen perimeter; may let go and fall."""
340
476
  if self._rng.random() < self.DROP_CHANCE:
@@ -453,10 +589,11 @@ class DesktopPetWidget(BaseWidget):
453
589
 
454
590
  def __init__(self, image_path: str, size: int = 128, speed: int = 3,
455
591
  behaviour: str = BEHAVIOUR_FLOOR, climb: bool = True, talk: bool = True,
456
- sound_path: Optional[str] = None):
592
+ sound_path: Optional[str] = None, sit_on_windows: bool = True):
457
593
  front_engine_logger.info(
458
594
  f"[DesktopPetWidget] Init | path={image_path}, size={size}, speed={speed}, "
459
- f"behaviour={behaviour}, climb={climb}, talk={talk}, sound={sound_path}"
595
+ f"behaviour={behaviour}, climb={climb}, talk={talk}, sound={sound_path}, "
596
+ f"sit_on_windows={sit_on_windows}"
460
597
  )
461
598
  super().__init__()
462
599
  self.opacity = 1.0
@@ -494,6 +631,12 @@ class DesktopPetWidget(BaseWidget):
494
631
  self._messages = self._load_messages()
495
632
  self._sound: Optional[QSoundEffect] = None
496
633
  self._init_sound(sound_path)
634
+ self._sit_on_windows = bool(sit_on_windows)
635
+ self._platform_tick = 0
636
+ # 多寵物互動 / Multi-pet interaction
637
+ self._peers_provider = None
638
+ self._peer_greeted = False
639
+ self._peer_tick = 0
497
640
  self._bubble = SpeechBubble()
498
641
  self._chatter_timer = QTimer(self)
499
642
  self._chatter_timer.setSingleShot(True)
@@ -507,10 +650,42 @@ class DesktopPetWidget(BaseWidget):
507
650
  self.close_action.triggered.connect(self.close)
508
651
  self.menu.addAction(self.close_action)
509
652
 
653
+ MEET_DISTANCE = 90.0
654
+
510
655
  def set_pet_window_flag(self) -> None:
511
656
  """寵物需接收滑鼠以便拖曳,故 allow_input=True 並置頂。"""
512
657
  apply_overlay_window_flags(self, show_on_bottom=False, allow_input=True)
513
658
 
659
+ def center(self) -> Tuple[int, int]:
660
+ return (self.x() + self.width() // 2, self.y() + self.height() // 2)
661
+
662
+ def set_peers_provider(self, provider) -> None:
663
+ """設定回傳其他寵物中心座標清單的函式 / Provider of other pets' centres."""
664
+ self._peers_provider = provider
665
+
666
+ def _check_peers(self) -> None:
667
+ """靠近其他寵物時轉向並打招呼(帶遲滯,避免重複)。"""
668
+ if self._peers_provider is None or not self._talk:
669
+ return
670
+ try:
671
+ peers = self._peers_provider()
672
+ except Exception: # pragma: no cover - defensive
673
+ return
674
+ my_center = self.center()
675
+ peer, distance = nearest_peer(my_center, peers)
676
+ if peer is None:
677
+ self._peer_greeted = False
678
+ return
679
+ if distance <= self.MEET_DISTANCE:
680
+ if not self._peer_greeted:
681
+ self._peer_greeted = True
682
+ self.motion.vx = abs(self.motion.vx) if peer[0] >= my_center[0] else -abs(self.motion.vx)
683
+ pool = self._messages.get("meet") or []
684
+ if pool:
685
+ self.say(pool[self._chatter_rng.randrange(len(pool))])
686
+ elif distance > self.MEET_DISTANCE * 1.6:
687
+ self._peer_greeted = False
688
+
514
689
  def _load_sprite(self, path: str):
515
690
  if Path(path).suffix.lower() in (".gif", ".webp"):
516
691
  movie = QMovie(str(path))
@@ -590,6 +765,7 @@ class DesktopPetWidget(BaseWidget):
590
765
  "night": lines("pet_chatter_night", "It's late - get some rest!|Still up?"),
591
766
  "happy": lines("pet_chatter_happy", "I'm so happy!|You're the best!|(^o^)"),
592
767
  "sad": lines("pet_chatter_sad", "Feeling a bit lonely...|Pet me?|(._.)"),
768
+ "meet": lines("pet_chatter_meet", "Hi friend!|Hello!|Let's play!"),
593
769
  "any": lines("pet_chatter_any", "Hi there!|(^_^)|Keep going!"),
594
770
  }
595
771
 
@@ -645,9 +821,18 @@ class DesktopPetWidget(BaseWidget):
645
821
  if self.motion.behaviour == BEHAVIOUR_CHASE:
646
822
  cursor = QCursor.pos()
647
823
  self.motion.set_target(cursor.x(), cursor.y())
824
+ elif self._sit_on_windows and self.motion.behaviour == BEHAVIOUR_FLOOR:
825
+ self._platform_tick += 1
826
+ if self._platform_tick >= 10:
827
+ self._platform_tick = 0
828
+ self.motion.set_platforms(windows_platforms(exclude_hwnds=(int(self.winId()),)))
648
829
  x, y = self.motion.step()
649
830
  self.move(x, y)
650
831
  self._set_active_sprite(self._visual_state())
832
+ self._peer_tick += 1
833
+ if self._peer_tick >= 5:
834
+ self._peer_tick = 0
835
+ self._check_peers()
651
836
  if self._bubble.isVisible():
652
837
  self._bubble.reposition(self)
653
838
 
@@ -76,6 +76,8 @@ class PetSettingUI(QWidget):
76
76
  self.climb_checkbox.setChecked(True)
77
77
  self.talk_checkbox = QCheckBox(language_wrapper.language_word_dict.get("pet_talk_label", "Speech bubbles"))
78
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)
79
81
 
80
82
  # Start
81
83
  self.start_button = QPushButton(language_wrapper.language_word_dict.get("pet_start", "Spawn pet"))
@@ -102,6 +104,7 @@ class PetSettingUI(QWidget):
102
104
  self.grid_layout.addWidget(self.behaviour_combobox, 3, 1)
103
105
  self.grid_layout.addWidget(self.climb_checkbox, 3, 2)
104
106
  self.grid_layout.addWidget(self.talk_checkbox, 2, 2)
107
+ self.grid_layout.addWidget(self.sit_checkbox, 4, 2)
105
108
  self.grid_layout.addWidget(self.start_button, 4, 0)
106
109
  self.grid_layout.addWidget(self.recent_files_label, 5, 0)
107
110
  self.grid_layout.addWidget(self.recent_files_combobox, 5, 1)
@@ -118,8 +121,10 @@ class PetSettingUI(QWidget):
118
121
  climb=self.climb_checkbox.isChecked(),
119
122
  talk=self.talk_checkbox.isChecked(),
120
123
  sound_path=self.pet_sound_path,
124
+ sit_on_windows=self.sit_checkbox.isChecked(),
121
125
  )
122
126
  pet.clone_requested.connect(self._spawn_pet)
127
+ pet.set_peers_provider(lambda me=pet: self._peer_centers(me))
123
128
  pet.set_pet_window_flag()
124
129
  self.pet_list.append(pet)
125
130
  screen = QGuiApplication.primaryScreen()
@@ -128,6 +133,18 @@ class PetSettingUI(QWidget):
128
133
  if geometry is not None:
129
134
  pet.start_moving((geometry.left(), geometry.top(), geometry.right(), geometry.bottom()))
130
135
 
136
+ def _peer_centers(self, me) -> list:
137
+ """回傳其他仍存活寵物的中心座標(略過已被銷毀者)。"""
138
+ centers = []
139
+ for other in list(self.pet_list):
140
+ if other is me:
141
+ continue
142
+ try:
143
+ centers.append(other.center())
144
+ except RuntimeError:
145
+ pass # underlying C++ object already deleted
146
+ return centers
147
+
131
148
  def start_play_pet(self) -> None:
132
149
  front_engine_logger.info("[PetSettingUI] start_play_pet")
133
150
  if not self.pet_image_path or not self.ready_to_play:
@@ -203,6 +220,7 @@ class PetSettingUI(QWidget):
203
220
  "climb": self.climb_checkbox.isChecked(),
204
221
  "talk": self.talk_checkbox.isChecked(),
205
222
  "sound": self.pet_sound_path,
223
+ "sit": self.sit_checkbox.isChecked(),
206
224
  }
207
225
 
208
226
  def set_state(self, state: dict) -> None:
@@ -228,3 +246,5 @@ class PetSettingUI(QWidget):
228
246
  self.talk_checkbox.setChecked(bool(state["talk"]))
229
247
  if state.get("sound"):
230
248
  self.pet_sound_path = str(state["sound"])
249
+ if "sit" in state:
250
+ self.sit_checkbox.setChecked(bool(state["sit"]))
@@ -229,6 +229,7 @@ english_word_dict = {
229
229
  "pet_choose_sound": "Choose sound (optional)",
230
230
  "pet_pack_empty": "No sprites (walk/idle/sleep/climb/fall/drag) found in that folder.",
231
231
  "pet_talk_label": "Speech bubbles",
232
+ "pet_sit_label": "Sit on windows",
232
233
  "pet_chatter_morning": "Good morning!|Rise and shine!|Ready for the day?",
233
234
  "pet_chatter_afternoon": "Good afternoon!|Taking a break?|Keep it up!",
234
235
  "pet_chatter_evening": "Good evening!|How was your day?|Almost done?",
@@ -236,6 +237,7 @@ english_word_dict = {
236
237
  "pet_chatter_any": "Hi there!|(^_^)|Keep going!|You've got this!",
237
238
  "pet_chatter_happy": "I'm so happy!|You're the best!|(^o^)/|Best day ever!",
238
239
  "pet_chatter_sad": "Feeling a bit lonely...|Pet me?|(._.)|Don't forget about me~",
240
+ "pet_chatter_meet": "Hi friend!|Hello there!|Let's play!|Fancy meeting you here!",
239
241
  "pet_start": "Spawn pet",
240
242
  "pet_message_box_text": "Pet image not found",
241
243
  }
@@ -230,6 +230,7 @@ traditional_chinese_word_dict = {
230
230
  "pet_choose_sound": "選擇音效(選填)",
231
231
  "pet_pack_empty": "該資料夾找不到精靈檔(walk/idle/sleep/climb/fall/drag)。",
232
232
  "pet_talk_label": "對話泡泡",
233
+ "pet_sit_label": "站在視窗上",
233
234
  "pet_chatter_morning": "早安!|新的一天開始囉!|準備好了嗎?",
234
235
  "pet_chatter_afternoon": "午安!|要不要休息一下?|繼續加油!",
235
236
  "pet_chatter_evening": "晚上好!|今天過得如何?|快完成了嗎?",
@@ -237,6 +238,7 @@ traditional_chinese_word_dict = {
237
238
  "pet_chatter_any": "嗨!|(^_^)|加油!|你可以的!",
238
239
  "pet_chatter_happy": "我好開心!|你最棒了!|(^o^)/|今天真美好!",
239
240
  "pet_chatter_sad": "有點寂寞…|摸摸我好嗎?|(._.)|別忘了我~",
241
+ "pet_chatter_meet": "嗨朋友!|你好呀!|一起玩吧!|在這遇到你真巧!",
240
242
  "pet_start": "召喚寵物",
241
243
  "pet_message_box_text": "找不到寵物圖片",
242
244
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.9
3
+ Version: 1.0.11
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.9"
9
+ version = "1.0.11"
10
10
  authors = [
11
11
  { name = "JE-Chen", email = "jechenmailman@gmail.com" },
12
12
  ]
File without changes
File without changes
File without changes