frontengine 1.0.5__tar.gz → 1.0.7__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.5 → frontengine-1.0.7}/PKG-INFO +1 -1
  2. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/pet/desktop_pet.py +158 -10
  3. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/pet/pet_setting_ui.py +7 -0
  4. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/multi_language/english.py +7 -1
  5. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/multi_language/traditional_chinese.py +7 -1
  6. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine.egg-info/PKG-INFO +1 -1
  7. {frontengine-1.0.5 → frontengine-1.0.7}/pyproject.toml +1 -1
  8. {frontengine-1.0.5 → frontengine-1.0.7}/LICENSE +0 -0
  9. {frontengine-1.0.5 → frontengine-1.0.7}/README.md +0 -0
  10. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/__init__.py +0 -0
  11. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/__main__.py +0 -0
  12. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/__init__.py +0 -0
  13. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/base_widget.py +0 -0
  14. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/gif/__init__.py +0 -0
  15. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/gif/paint_gif.py +0 -0
  16. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/image/__init__.py +0 -0
  17. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/image/paint_image.py +0 -0
  18. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/load/__init__.py +0 -0
  19. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/load/load_someone_make_ui.py +0 -0
  20. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/overlay_factory.py +0 -0
  21. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/particle/__init__.py +0 -0
  22. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/particle/particle_ui.py +0 -0
  23. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/pet/__init__.py +0 -0
  24. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/scene/__init__.py +0 -0
  25. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/scene/extend_graphic_scene.py +0 -0
  26. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/scene/extend_graphic_view.py +0 -0
  27. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/scene/scene.py +0 -0
  28. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/sound_player/__init__.py +0 -0
  29. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/sound_player/sound_effect.py +0 -0
  30. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/sound_player/sound_player.py +0 -0
  31. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/text/__init__.py +0 -0
  32. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/text/draw_text.py +0 -0
  33. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/video/__init__.py +0 -0
  34. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/video/video_player.py +0 -0
  35. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/web/__init__.py +0 -0
  36. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/web/webview.py +0 -0
  37. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/show/window_helpers.py +0 -0
  38. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/system_tray/__init__.py +0 -0
  39. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/system_tray/extend_system_tray.py +0 -0
  40. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/__init__.py +0 -0
  41. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/color/__init__.py +0 -0
  42. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/color/global_color.py +0 -0
  43. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/dialog/__init__.py +0 -0
  44. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
  45. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
  46. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/dialog/save_file_dialog.py +0 -0
  47. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/main_ui.py +0 -0
  48. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/menu/__init__.py +0 -0
  49. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/menu/help_menu.py +0 -0
  50. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/menu/how_to_menu.py +0 -0
  51. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/menu/language_menu.py +0 -0
  52. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/menu/preset_menu.py +0 -0
  53. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/menu/settings_menu.py +0 -0
  54. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/__init__.py +0 -0
  55. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/control_center/__init__.py +0 -0
  56. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/control_center/control_center_ui.py +0 -0
  57. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/gif/__init__.py +0 -0
  58. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
  59. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/image/__init__.py +0 -0
  60. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/image/image_setting_ui.py +0 -0
  61. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/particle/__init__.py +0 -0
  62. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
  63. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/pet/__init__.py +0 -0
  64. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/scene_setting/__init__.py +0 -0
  65. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
  66. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/scene_setting/scene_page/__init__.py +0 -0
  67. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
  68. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
  69. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
  70. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
  71. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
  72. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
  73. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
  74. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
  75. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
  76. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/sound_player/__init__.py +0 -0
  77. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
  78. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/text/__init__.py +0 -0
  79. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/text/text_setting_ui.py +0 -0
  80. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/utils.py +0 -0
  81. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/video/__init__.py +0 -0
  82. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/video/video_setting_ui.py +0 -0
  83. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/web/__init__.py +0 -0
  84. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/ui/page/web/web_setting_ui.py +0 -0
  85. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/user_setting/__init__.py +0 -0
  86. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/user_setting/preset_repository.py +0 -0
  87. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/user_setting/scene_setting.py +0 -0
  88. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/user_setting/user_setting_file.py +0 -0
  89. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/__init__.py +0 -0
  90. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/browser/__init__.py +0 -0
  91. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/browser/browser.py +0 -0
  92. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/critical_exit/__init__.py +0 -0
  93. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
  94. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/critical_exit/critical_exit.py +0 -0
  95. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/critical_exit/win32_vk.py +0 -0
  96. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/exception/__init__.py +0 -0
  97. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/exception/exception_tags.py +0 -0
  98. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/exception/exceptions.py +0 -0
  99. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/file/__init__.py +0 -0
  100. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/file/open/__init__.py +0 -0
  101. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/file/open/open_file.py +0 -0
  102. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/file/save/__init__.py +0 -0
  103. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/file/save/save_file.py +0 -0
  104. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/hotkey/__init__.py +0 -0
  105. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/hotkey/hotkey_service.py +0 -0
  106. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/json/__init__.py +0 -0
  107. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/json/json_file.py +0 -0
  108. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/json/json_repository.py +0 -0
  109. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/json_format/__init__.py +0 -0
  110. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/json_format/json_process.py +0 -0
  111. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/logging/__init__.py +0 -0
  112. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/logging/loggin_instance.py +0 -0
  113. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/multi_language/__init__.py +0 -0
  114. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/multi_language/france.py +0 -0
  115. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/multi_language/germany.py +0 -0
  116. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/multi_language/italy.py +0 -0
  117. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/multi_language/language_wrapper.py +0 -0
  118. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/multi_language/russian.py +0 -0
  119. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
  120. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/preset_schedule/__init__.py +0 -0
  121. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
  122. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/redirect_manager/__init__.py +0 -0
  123. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
  124. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/smart_pause/__init__.py +0 -0
  125. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
  126. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/theme_schedule/__init__.py +0 -0
  127. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
  128. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/utils/web_url.py +0 -0
  129. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/worker/__init__.py +0 -0
  130. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine/worker/qthread_worker.py +0 -0
  131. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine.egg-info/SOURCES.txt +0 -0
  132. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine.egg-info/dependency_links.txt +0 -0
  133. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine.egg-info/entry_points.txt +0 -0
  134. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine.egg-info/requires.txt +0 -0
  135. {frontengine-1.0.5 → frontengine-1.0.7}/frontengine.egg-info/top_level.txt +0 -0
  136. {frontengine-1.0.5 → frontengine-1.0.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.5
3
+ Version: 1.0.7
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,11 @@
1
1
  import random as _random_module
2
+ from datetime import datetime
2
3
  from pathlib import Path
3
4
  from typing import Optional, Tuple
4
5
 
5
6
  from PySide6.QtCore import Qt, QRect, QTimer, Signal
6
- from PySide6.QtGui import QAction, QCursor, QMovie, QPainter, QPixmap, QTransform
7
- from PySide6.QtWidgets import QMenu, QMessageBox
7
+ from PySide6.QtGui import QAction, QColor, QCursor, QFont, QFontMetrics, QMovie, QPainter, QPixmap, QTransform
8
+ from PySide6.QtWidgets import QMenu, QMessageBox, QWidget
8
9
 
9
10
  from frontengine.show.base_widget import BaseWidget
10
11
  from frontengine.show.window_helpers import apply_overlay_window_flags
@@ -25,6 +26,7 @@ SURFACE_RIGHT = "right"
25
26
  # 視覺狀態(決定顯示哪張精靈)/ Visual states (which sprite to show)
26
27
  STATE_WALK = "walk"
27
28
  STATE_IDLE = "idle"
29
+ STATE_SLEEP = "sleep"
28
30
  STATE_CLIMB = "climb"
29
31
  STATE_FALL = "fall"
30
32
  STATE_DRAG = "drag"
@@ -33,13 +35,23 @@ STATE_DRAG = "drag"
33
35
  # Accepted file stems per state inside a pet pack folder (first match wins).
34
36
  _PACK_STATE_ALIASES = {
35
37
  STATE_WALK: ("walk", "run", "move"),
36
- STATE_IDLE: ("idle", "sit", "stand", "sleep"),
38
+ STATE_IDLE: ("idle", "sit", "stand"),
39
+ STATE_SLEEP: ("sleep", "zzz", "rest"),
37
40
  STATE_CLIMB: ("climb", "grab", "wall"),
38
41
  STATE_FALL: ("fall", "jump"),
39
42
  STATE_DRAG: ("drag", "pinch", "grabbed", "held"),
40
43
  }
41
44
  _PACK_EXTS = (".gif", ".webp", ".png", ".jpg", ".jpeg")
42
45
 
46
+ # 缺該狀態精靈時的退回順序 / Fallback order when a state's sprite is missing
47
+ _STATE_FALLBACKS = {
48
+ STATE_SLEEP: (STATE_IDLE, STATE_WALK),
49
+ STATE_IDLE: (STATE_WALK,),
50
+ STATE_CLIMB: (STATE_WALK,),
51
+ STATE_FALL: (STATE_WALK,),
52
+ STATE_DRAG: (STATE_WALK,),
53
+ }
54
+
43
55
 
44
56
  def scan_pet_pack(folder: str) -> dict:
45
57
  """
@@ -75,11 +87,81 @@ def derive_visual_state(dragging: bool, airborne: bool, surface: str, motion_sta
75
87
  return STATE_FALL
76
88
  if surface in (SURFACE_LEFT, SURFACE_RIGHT, SURFACE_CEILING):
77
89
  return STATE_CLIMB
90
+ if surface == SURFACE_FLOOR and motion_state == STATE_SLEEP:
91
+ return STATE_SLEEP
78
92
  if surface == SURFACE_FLOOR and motion_state == "idle":
79
93
  return STATE_IDLE
80
94
  return STATE_WALK
81
95
 
82
96
 
97
+ def message_bucket(hour: int) -> str:
98
+ """依小時回傳時段 / Time-of-day bucket for the given hour."""
99
+ hour = int(hour) % 24
100
+ if 5 <= hour < 12:
101
+ return "morning"
102
+ if 12 <= hour < 18:
103
+ return "afternoon"
104
+ if 18 <= hour < 22:
105
+ return "evening"
106
+ return "night"
107
+
108
+
109
+ def pick_message(hour: int, rng, messages: dict) -> str:
110
+ """
111
+ 依時段從對應句庫(加上通用句庫)隨機挑一句台詞;句庫皆空回傳空字串。
112
+ Pick a chatter line from the time bucket plus the generic pool. rng needs
113
+ a ``randrange`` method (injectable for tests).
114
+ """
115
+ pool = list(messages.get(message_bucket(hour), [])) + list(messages.get("any", []))
116
+ if not pool:
117
+ return ""
118
+ return pool[rng.randrange(len(pool))]
119
+
120
+
121
+ class SpeechBubble(QWidget):
122
+ """寵物頭上的小對話泡泡,數秒後自動消失 / A small speech bubble that auto-hides."""
123
+
124
+ def __init__(self) -> None:
125
+ super().__init__()
126
+ self._text = ""
127
+ self._font = QFont()
128
+ self._font.setPointSize(11)
129
+ self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
130
+ apply_overlay_window_flags(self, show_on_bottom=False, allow_input=False)
131
+ self._hide_timer = QTimer(self)
132
+ self._hide_timer.setSingleShot(True)
133
+ self._hide_timer.timeout.connect(self.hide)
134
+
135
+ def show_message(self, text: str, anchor: QWidget, duration_ms: int = 4000) -> None:
136
+ self._text = text
137
+ width = min(260, QFontMetrics(self._font).horizontalAdvance(text) + 28)
138
+ self.resize(max(60, width), 44)
139
+ self.reposition(anchor)
140
+ self.show()
141
+ self.raise_()
142
+ self.update()
143
+ self._hide_timer.start(max(500, int(duration_ms)))
144
+
145
+ def reposition(self, anchor: QWidget) -> None:
146
+ geo = anchor.frameGeometry()
147
+ self.move(geo.center().x() - self.width() // 2, geo.top() - self.height() - 6)
148
+
149
+ def paintEvent(self, event) -> None:
150
+ painter = QPainter(self)
151
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
152
+ rect = self.rect().adjusted(1, 1, -1, -1)
153
+ painter.setBrush(QColor(255, 255, 240, 235))
154
+ painter.setPen(QColor(90, 90, 90))
155
+ painter.drawRoundedRect(rect, 8, 8)
156
+ painter.setPen(QColor(30, 30, 30))
157
+ painter.setFont(self._font)
158
+ painter.drawText(
159
+ rect.adjusted(8, 2, -8, -2),
160
+ int(Qt.AlignmentFlag.AlignCenter | Qt.TextFlag.TextWordWrap),
161
+ self._text,
162
+ )
163
+
164
+
83
165
  class PetMotion:
84
166
  """
85
167
  純邏輯的寵物移動模型(不依賴 Qt,完全可測試)。包含:
@@ -123,8 +205,9 @@ class PetMotion:
123
205
  self.vx = float(self.speed)
124
206
  self.vy = 0.0 if behaviour != BEHAVIOUR_WANDER else float(self.speed)
125
207
  self._airborne = False
126
- self.state = "walk" # walk | idle
208
+ self.state = "walk" # walk | idle | sleep
127
209
  self._state_ticks = 0
210
+ self._idle_count = 0
128
211
  self.target: Optional[Tuple[float, float]] = None
129
212
  self.asleep = False
130
213
  self.climb = bool(climb)
@@ -260,13 +343,25 @@ class PetMotion:
260
343
  def _new_state(self, force_walk: bool = False) -> None:
261
344
  if force_walk or self._rng.random() < 0.65:
262
345
  self.state = "walk"
346
+ self._idle_count = 0
263
347
  if not force_walk:
264
348
  self.vx = float(self.speed) if self._rng.random() < 0.5 else float(-self.speed)
265
349
  self._state_ticks = self._rng.randint(30, 120)
350
+ elif self._idle_count >= 2:
351
+ # been idling a while -> doze off
352
+ self.state = STATE_SLEEP
353
+ self._idle_count = 0
354
+ self._state_ticks = self._rng.randint(120, 300)
266
355
  else:
267
356
  self.state = "idle"
357
+ self._idle_count += 1
268
358
  self._state_ticks = self._rng.randint(20, 80)
269
359
 
360
+ def wake(self) -> None:
361
+ """喚醒寵物並恢復走動(互動時呼叫)/ Wake the pet and resume walking."""
362
+ self._idle_count = 0
363
+ self._new_state(force_walk=True)
364
+
270
365
  # --- free wander ---
271
366
  def _step_wander(self, left, top, right, bottom) -> Tuple[int, int]:
272
367
  if abs(self.vy) < 1e-9:
@@ -317,10 +412,10 @@ class DesktopPetWidget(BaseWidget):
317
412
  clone_requested = Signal()
318
413
 
319
414
  def __init__(self, image_path: str, size: int = 128, speed: int = 3,
320
- behaviour: str = BEHAVIOUR_FLOOR, climb: bool = True):
415
+ behaviour: str = BEHAVIOUR_FLOOR, climb: bool = True, talk: bool = True):
321
416
  front_engine_logger.info(
322
417
  f"[DesktopPetWidget] Init | path={image_path}, size={size}, speed={speed}, "
323
- f"behaviour={behaviour}, climb={climb}"
418
+ f"behaviour={behaviour}, climb={climb}, talk={talk}"
324
419
  )
325
420
  super().__init__()
326
421
  self.opacity = 1.0
@@ -350,6 +445,16 @@ class DesktopPetWidget(BaseWidget):
350
445
  self._timer = QTimer(self)
351
446
  self._timer.timeout.connect(self._on_tick)
352
447
 
448
+ # Speech bubbles / chatter
449
+ self._talk = bool(talk)
450
+ self._moved = False
451
+ self._chatter_rng = _random_module.SystemRandom()
452
+ self._messages = self._load_messages()
453
+ self._bubble = SpeechBubble()
454
+ self._chatter_timer = QTimer(self)
455
+ self._chatter_timer.setSingleShot(True)
456
+ self._chatter_timer.timeout.connect(self._on_chatter)
457
+
353
458
  self.menu = QMenu(self)
354
459
  self.clone_action = QAction(language_wrapper.language_word_dict.get("pet_clone", "Clone"), self)
355
460
  self.clone_action.triggered.connect(self.clone_requested.emit)
@@ -382,7 +487,7 @@ class DesktopPetWidget(BaseWidget):
382
487
  return self._sprites["default"]
383
488
  if state in self._sprites:
384
489
  return self._sprites[state]
385
- for fallback in (STATE_WALK, STATE_IDLE):
490
+ for fallback in _STATE_FALLBACKS.get(state, (STATE_WALK,)):
386
491
  if fallback in self._sprites:
387
492
  return self._sprites[fallback]
388
493
  return next(iter(self._sprites.values()), None)
@@ -426,6 +531,38 @@ class DesktopPetWidget(BaseWidget):
426
531
  self.motion.y = float(bottom - self.pet_size)
427
532
  self.move(int(self.motion.x), int(self.motion.y))
428
533
  self._timer.start(max(10, int(interval_ms)))
534
+ self._schedule_chatter()
535
+
536
+ def _load_messages(self) -> dict:
537
+ d = language_wrapper.language_word_dict
538
+
539
+ def lines(key: str, fallback: str) -> list:
540
+ return [part.strip() for part in str(d.get(key, fallback)).split("|") if part.strip()]
541
+
542
+ return {
543
+ "morning": lines("pet_chatter_morning", "Good morning!|Ready for the day?"),
544
+ "afternoon": lines("pet_chatter_afternoon", "Good afternoon!|Taking a break?"),
545
+ "evening": lines("pet_chatter_evening", "Good evening!|How was your day?"),
546
+ "night": lines("pet_chatter_night", "It's late - get some rest!|Still up?"),
547
+ "any": lines("pet_chatter_any", "Hi there!|(^_^)|Keep going!"),
548
+ }
549
+
550
+ def _schedule_chatter(self) -> None:
551
+ if self._talk:
552
+ self._chatter_timer.start(self._chatter_rng.randint(15000, 35000))
553
+
554
+ def _on_chatter(self) -> None:
555
+ self.say()
556
+ self._schedule_chatter()
557
+
558
+ def say(self, text: Optional[str] = None) -> None:
559
+ """顯示一句台詞(text 為 None 時依時段隨機挑)。"""
560
+ if not self._talk:
561
+ return
562
+ if text is None:
563
+ text = pick_message(datetime.now().hour, self._chatter_rng, self._messages)
564
+ if text:
565
+ self._bubble.show_message(text, self)
429
566
 
430
567
  def _on_tick(self) -> None:
431
568
  if self._dragging:
@@ -437,13 +574,17 @@ class DesktopPetWidget(BaseWidget):
437
574
  x, y = self.motion.step()
438
575
  self.move(x, y)
439
576
  self._set_active_sprite(self._visual_state())
577
+ if self._bubble.isVisible():
578
+ self._bubble.reposition(self)
440
579
 
441
580
  # --- dragging + throwing ---
442
581
  def mousePressEvent(self, event) -> None:
443
582
  if event.button() == Qt.MouseButton.LeftButton:
444
583
  self._dragging = True
584
+ self._moved = False
445
585
  self._last_move_delta = (0, 0)
446
586
  self._drag_offset = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
587
+ self.motion.wake() # interacting wakes a sleeping pet
447
588
  self._set_active_sprite(STATE_DRAG)
448
589
  super().mousePressEvent(event)
449
590
 
@@ -451,6 +592,7 @@ class DesktopPetWidget(BaseWidget):
451
592
  if self._dragging and self._drag_offset is not None:
452
593
  new_top_left = event.globalPosition().toPoint() - self._drag_offset
453
594
  self._last_move_delta = (new_top_left.x() - self.x(), new_top_left.y() - self.y())
595
+ self._moved = True
454
596
  self.move(new_top_left)
455
597
  super().mouseMoveEvent(event)
456
598
 
@@ -459,9 +601,12 @@ class DesktopPetWidget(BaseWidget):
459
601
  self._dragging = False
460
602
  self.motion.x = float(self.x())
461
603
  self.motion.y = float(self.y())
462
- # Fling with the momentum of the last drag movement (floor mode).
463
- if self.motion.behaviour == BEHAVIOUR_FLOOR:
464
- self.motion.throw(self._last_move_delta[0], self._last_move_delta[1])
604
+ if self._moved:
605
+ # Fling with the momentum of the last drag movement (floor mode).
606
+ if self.motion.behaviour == BEHAVIOUR_FLOOR:
607
+ self.motion.throw(self._last_move_delta[0], self._last_move_delta[1])
608
+ else:
609
+ self.say() # a click (no drag) makes the pet talk
465
610
  super().mouseReleaseEvent(event)
466
611
 
467
612
  def contextMenuEvent(self, event) -> None:
@@ -470,6 +615,9 @@ class DesktopPetWidget(BaseWidget):
470
615
  def closeEvent(self, event) -> None:
471
616
  if self._timer.isActive():
472
617
  self._timer.stop()
618
+ if self._chatter_timer.isActive():
619
+ self._chatter_timer.stop()
620
+ self._bubble.close()
473
621
  for kind, obj in self._sprites.values():
474
622
  if kind == "movie":
475
623
  obj.stop()
@@ -69,6 +69,8 @@ class PetSettingUI(QWidget):
69
69
  # Climb walls / ceiling (only meaningful in floor mode)
70
70
  self.climb_checkbox = QCheckBox(language_wrapper.language_word_dict.get("pet_climb_label", "Climb walls"))
71
71
  self.climb_checkbox.setChecked(True)
72
+ self.talk_checkbox = QCheckBox(language_wrapper.language_word_dict.get("pet_talk_label", "Speech bubbles"))
73
+ self.talk_checkbox.setChecked(True)
72
74
 
73
75
  # Start
74
76
  self.start_button = QPushButton(language_wrapper.language_word_dict.get("pet_start", "Spawn pet"))
@@ -93,6 +95,7 @@ class PetSettingUI(QWidget):
93
95
  self.grid_layout.addWidget(self.behaviour_label, 3, 0)
94
96
  self.grid_layout.addWidget(self.behaviour_combobox, 3, 1)
95
97
  self.grid_layout.addWidget(self.climb_checkbox, 3, 2)
98
+ self.grid_layout.addWidget(self.talk_checkbox, 2, 2)
96
99
  self.grid_layout.addWidget(self.start_button, 4, 0)
97
100
  self.grid_layout.addWidget(self.recent_files_label, 5, 0)
98
101
  self.grid_layout.addWidget(self.recent_files_combobox, 5, 1)
@@ -107,6 +110,7 @@ class PetSettingUI(QWidget):
107
110
  speed=int(self.speed_combobox.currentText()),
108
111
  behaviour=self.behaviour_combobox.currentData(),
109
112
  climb=self.climb_checkbox.isChecked(),
113
+ talk=self.talk_checkbox.isChecked(),
110
114
  )
111
115
  pet.clone_requested.connect(self._spawn_pet)
112
116
  pet.set_pet_window_flag()
@@ -183,6 +187,7 @@ class PetSettingUI(QWidget):
183
187
  "speed": self.speed_combobox.currentText(),
184
188
  "behaviour": self.behaviour_combobox.currentData(),
185
189
  "climb": self.climb_checkbox.isChecked(),
190
+ "talk": self.talk_checkbox.isChecked(),
186
191
  }
187
192
 
188
193
  def set_state(self, state: dict) -> None:
@@ -204,3 +209,5 @@ class PetSettingUI(QWidget):
204
209
  self.behaviour_combobox.setCurrentIndex(index)
205
210
  if "climb" in state:
206
211
  self.climb_checkbox.setChecked(bool(state["climb"]))
212
+ if "talk" in state:
213
+ self.talk_checkbox.setChecked(bool(state["talk"]))
@@ -226,7 +226,13 @@ 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_pack_empty": "No sprites (walk/idle/climb/fall/drag) found in that folder.",
229
+ "pet_pack_empty": "No sprites (walk/idle/sleep/climb/fall/drag) found in that folder.",
230
+ "pet_talk_label": "Speech bubbles",
231
+ "pet_chatter_morning": "Good morning!|Rise and shine!|Ready for the day?",
232
+ "pet_chatter_afternoon": "Good afternoon!|Taking a break?|Keep it up!",
233
+ "pet_chatter_evening": "Good evening!|How was your day?|Almost done?",
234
+ "pet_chatter_night": "It's late - get some rest!|Still up?|Sweet dreams soon~",
235
+ "pet_chatter_any": "Hi there!|(^_^)|Keep going!|You've got this!",
230
236
  "pet_start": "Spawn pet",
231
237
  "pet_message_box_text": "Pet image not found",
232
238
  }
@@ -227,7 +227,13 @@ traditional_chinese_word_dict = {
227
227
  "pet_climb_label": "攀爬牆壁",
228
228
  "pet_clone": "複製",
229
229
  "pet_choose_pack": "選擇動作包資料夾...",
230
- "pet_pack_empty": "該資料夾找不到精靈檔(walk/idle/climb/fall/drag)。",
230
+ "pet_pack_empty": "該資料夾找不到精靈檔(walk/idle/sleep/climb/fall/drag)。",
231
+ "pet_talk_label": "對話泡泡",
232
+ "pet_chatter_morning": "早安!|新的一天開始囉!|準備好了嗎?",
233
+ "pet_chatter_afternoon": "午安!|要不要休息一下?|繼續加油!",
234
+ "pet_chatter_evening": "晚上好!|今天過得如何?|快完成了嗎?",
235
+ "pet_chatter_night": "很晚了,早點休息!|還沒睡嗎?|祝好夢~",
236
+ "pet_chatter_any": "嗨!|(^_^)|加油!|你可以的!",
231
237
  "pet_start": "召喚寵物",
232
238
  "pet_message_box_text": "找不到寵物圖片",
233
239
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.5
3
+ Version: 1.0.7
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.5"
9
+ version = "1.0.7"
10
10
  authors = [
11
11
  { name = "JE-Chen", email = "jechenmailman@gmail.com" },
12
12
  ]
File without changes
File without changes
File without changes