frontengine 1.0.27__tar.gz → 1.0.28__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.

Potentially problematic release.


This version of frontengine might be problematic. Click here for more details.

Files changed (140) hide show
  1. {frontengine-1.0.27 → frontengine-1.0.28}/PKG-INFO +7 -2
  2. {frontengine-1.0.27 → frontengine-1.0.28}/README.md +6 -1
  3. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/pet/desktop_pet.py +165 -7
  4. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/pet/pet_setting_ui.py +66 -10
  5. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/multi_language/english.py +6 -0
  6. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/multi_language/traditional_chinese.py +6 -0
  7. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine.egg-info/PKG-INFO +7 -2
  8. {frontengine-1.0.27 → frontengine-1.0.28}/pyproject.toml +1 -1
  9. {frontengine-1.0.27 → frontengine-1.0.28}/LICENSE +0 -0
  10. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/__init__.py +0 -0
  11. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/__main__.py +0 -0
  12. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/__init__.py +0 -0
  13. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/base_widget.py +0 -0
  14. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/gif/__init__.py +0 -0
  15. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/gif/paint_gif.py +0 -0
  16. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/image/__init__.py +0 -0
  17. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/image/paint_image.py +0 -0
  18. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/load/__init__.py +0 -0
  19. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/load/load_someone_make_ui.py +0 -0
  20. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/overlay_factory.py +0 -0
  21. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/particle/__init__.py +0 -0
  22. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/particle/particle_ui.py +0 -0
  23. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/pet/__init__.py +0 -0
  24. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/scene/__init__.py +0 -0
  25. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/scene/extend_graphic_scene.py +0 -0
  26. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/scene/extend_graphic_view.py +0 -0
  27. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/scene/scene.py +0 -0
  28. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/sound_player/__init__.py +0 -0
  29. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/sound_player/sound_effect.py +0 -0
  30. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/sound_player/sound_player.py +0 -0
  31. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/text/__init__.py +0 -0
  32. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/text/draw_text.py +0 -0
  33. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/video/__init__.py +0 -0
  34. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/video/video_player.py +0 -0
  35. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/web/__init__.py +0 -0
  36. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/web/webview.py +0 -0
  37. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/show/window_helpers.py +0 -0
  38. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/system_tray/__init__.py +0 -0
  39. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/system_tray/extend_system_tray.py +0 -0
  40. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/__init__.py +0 -0
  41. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/color/__init__.py +0 -0
  42. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/color/global_color.py +0 -0
  43. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/dialog/__init__.py +0 -0
  44. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
  45. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
  46. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/dialog/save_file_dialog.py +0 -0
  47. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/main_ui.py +0 -0
  48. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/menu/__init__.py +0 -0
  49. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/menu/help_menu.py +0 -0
  50. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/menu/how_to_menu.py +0 -0
  51. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/menu/language_menu.py +0 -0
  52. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/menu/preset_menu.py +0 -0
  53. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/menu/settings_menu.py +0 -0
  54. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/__init__.py +0 -0
  55. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/control_center/__init__.py +0 -0
  56. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/control_center/control_center_ui.py +0 -0
  57. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/gif/__init__.py +0 -0
  58. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
  59. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/image/__init__.py +0 -0
  60. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/image/image_setting_ui.py +0 -0
  61. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/particle/__init__.py +0 -0
  62. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
  63. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/pet/__init__.py +0 -0
  64. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/__init__.py +0 -0
  65. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
  66. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/__init__.py +0 -0
  67. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
  68. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
  69. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
  70. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
  71. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
  72. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
  73. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
  74. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
  75. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
  76. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/sound_player/__init__.py +0 -0
  77. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
  78. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/text/__init__.py +0 -0
  79. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/text/text_setting_ui.py +0 -0
  80. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/utils.py +0 -0
  81. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/video/__init__.py +0 -0
  82. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/video/video_setting_ui.py +0 -0
  83. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/web/__init__.py +0 -0
  84. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/ui/page/web/web_setting_ui.py +0 -0
  85. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/user_setting/__init__.py +0 -0
  86. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/user_setting/preset_repository.py +0 -0
  87. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/user_setting/scene_setting.py +0 -0
  88. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/user_setting/user_setting_file.py +0 -0
  89. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/__init__.py +0 -0
  90. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/audio_meter/__init__.py +0 -0
  91. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/audio_meter/audio_envelope.py +0 -0
  92. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/audio_meter/screen_audio.py +0 -0
  93. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/audio_meter/system_audio_meter.py +0 -0
  94. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/browser/__init__.py +0 -0
  95. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/browser/browser.py +0 -0
  96. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/critical_exit/__init__.py +0 -0
  97. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
  98. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/critical_exit/critical_exit.py +0 -0
  99. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/critical_exit/win32_vk.py +0 -0
  100. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/exception/__init__.py +0 -0
  101. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/exception/exception_tags.py +0 -0
  102. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/exception/exceptions.py +0 -0
  103. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/file/__init__.py +0 -0
  104. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/file/open/__init__.py +0 -0
  105. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/file/open/open_file.py +0 -0
  106. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/file/save/__init__.py +0 -0
  107. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/file/save/save_file.py +0 -0
  108. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/hotkey/__init__.py +0 -0
  109. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/hotkey/hotkey_service.py +0 -0
  110. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/json/__init__.py +0 -0
  111. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/json/json_file.py +0 -0
  112. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/json/json_repository.py +0 -0
  113. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/json_format/__init__.py +0 -0
  114. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/json_format/json_process.py +0 -0
  115. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/logging/__init__.py +0 -0
  116. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/logging/loggin_instance.py +0 -0
  117. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/multi_language/__init__.py +0 -0
  118. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/multi_language/france.py +0 -0
  119. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/multi_language/germany.py +0 -0
  120. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/multi_language/italy.py +0 -0
  121. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/multi_language/language_wrapper.py +0 -0
  122. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/multi_language/russian.py +0 -0
  123. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
  124. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/preset_schedule/__init__.py +0 -0
  125. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
  126. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/redirect_manager/__init__.py +0 -0
  127. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
  128. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/smart_pause/__init__.py +0 -0
  129. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
  130. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/theme_schedule/__init__.py +0 -0
  131. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
  132. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/utils/web_url.py +0 -0
  133. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/worker/__init__.py +0 -0
  134. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine/worker/qthread_worker.py +0 -0
  135. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine.egg-info/SOURCES.txt +0 -0
  136. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine.egg-info/dependency_links.txt +0 -0
  137. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine.egg-info/entry_points.txt +0 -0
  138. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine.egg-info/requires.txt +0 -0
  139. {frontengine-1.0.27 → frontengine-1.0.28}/frontengine.egg-info/top_level.txt +0 -0
  140. {frontengine-1.0.27 → frontengine-1.0.28}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.27
3
+ Version: 1.0.28
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
@@ -68,7 +68,12 @@ Spawn an animated sprite that lives on your desktop, from the **Pet** tab.
68
68
  away, and warns you about a low battery.
69
69
  - **Interaction** — drag it around, right-click to clone/feed/set a reminder,
70
70
  and **drop a file onto it**: an image or pet pack becomes its new look,
71
- anything else is eaten as a snack.
71
+ anything else is eaten. What it eats matters — an archive is a feast, music
72
+ cheers it up more than it fills, a document is a modest meal, and a binary is
73
+ too hard to chew.
74
+ - **Tag** — with two or more pets on screen, tick *Play tag with each other* and
75
+ one becomes "it": it walks toward its nearest neighbour while the others run
76
+ the other way, and catching someone passes the tag on.
72
77
  - **React to audio** — the pet pulses to your speakers' output level. It reads
73
78
  only the Windows output *meter* (WASAPI `IAudioMeterInformation`); no audio
74
79
  is captured or recorded. Peaks are smoothed with an RMS window and a
@@ -44,7 +44,12 @@ Spawn an animated sprite that lives on your desktop, from the **Pet** tab.
44
44
  away, and warns you about a low battery.
45
45
  - **Interaction** — drag it around, right-click to clone/feed/set a reminder,
46
46
  and **drop a file onto it**: an image or pet pack becomes its new look,
47
- anything else is eaten as a snack.
47
+ anything else is eaten. What it eats matters — an archive is a feast, music
48
+ cheers it up more than it fills, a document is a modest meal, and a binary is
49
+ too hard to chew.
50
+ - **Tag** — with two or more pets on screen, tick *Play tag with each other* and
51
+ one becomes "it": it walks toward its nearest neighbour while the others run
52
+ the other way, and catching someone passes the tag on.
48
53
  - **React to audio** — the pet pulses to your speakers' output level. It reads
49
54
  only the Windows output *meter* (WASAPI `IAudioMeterInformation`); no audio
50
55
  is captured or recorded. Peaks are smoothed with an RMS window and a
@@ -105,6 +105,43 @@ def classify_drop(path) -> Optional[str]:
105
105
  return DROP_SPRITE if target.suffix.lower() in _PACK_EXTS else DROP_FOOD
106
106
 
107
107
 
108
+ # 食物種類(依副檔名猜)與各自的效果 / Food kinds guessed from the suffix
109
+ FOOD_SNACK = "snack" # 一般點心 / an ordinary snack
110
+ FOOD_FEAST = "feast" # 壓縮檔=大餐 / an archive is a hearty meal
111
+ FOOD_MUSIC = "music" # 音樂檔=聽了很開心 / music cheers it up more than it fills
112
+ FOOD_BOOK = "book" # 文件檔=細嚼慢嚥 / a document is chewed slowly
113
+ FOOD_HARD = "hard" # 可執行檔=咬不動 / a binary is too hard to chew
114
+
115
+ _FOOD_SUFFIXES = {
116
+ FOOD_FEAST: (".zip", ".7z", ".rar", ".tar", ".gz", ".iso"),
117
+ FOOD_MUSIC: (".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac"),
118
+ FOOD_BOOK: (".txt", ".md", ".pdf", ".doc", ".docx", ".rtf", ".epub"),
119
+ FOOD_HARD: (".exe", ".dll", ".msi", ".sys", ".bat"),
120
+ }
121
+
122
+ # 每種食物的飽足/心情/親密度變化(心情為負代表不喜歡)
123
+ # Fullness / mood / affection change per food kind (negative mood = dislikes it).
124
+ FOOD_EFFECTS = {
125
+ FOOD_SNACK: {"fullness": 30, "mood": 12, "affection": 5},
126
+ FOOD_FEAST: {"fullness": 45, "mood": 8, "affection": 6},
127
+ FOOD_MUSIC: {"fullness": 15, "mood": 20, "affection": 5},
128
+ FOOD_BOOK: {"fullness": 20, "mood": 6, "affection": 4},
129
+ FOOD_HARD: {"fullness": 2, "mood": -6, "affection": 1},
130
+ }
131
+
132
+
133
+ def classify_food(path) -> str:
134
+ """依副檔名判斷食物種類;認不出來就當一般點心 / Guess the food kind from the suffix."""
135
+ try:
136
+ suffix = Path(path).suffix.lower()
137
+ except (OSError, TypeError, ValueError):
138
+ return FOOD_SNACK
139
+ for kind, suffixes in _FOOD_SUFFIXES.items():
140
+ if suffix in suffixes:
141
+ return kind
142
+ return FOOD_SNACK
143
+
144
+
108
145
  def derive_visual_state(dragging: bool, airborne: bool, surface: str, motion_state: str) -> str:
109
146
  """依目前情形推導應顯示的視覺狀態 / Derive which sprite state to show."""
110
147
  if dragging:
@@ -291,6 +328,92 @@ def nearest_peer(center, peers):
291
328
  return best, best_distance
292
329
 
293
330
 
331
+ # 鬼抓人角色 / Roles in the tag game
332
+ TAG_CHASE = "chase"
333
+ TAG_FLEE = "flee"
334
+
335
+
336
+ class PetTagGame:
337
+ """
338
+ 多隻寵物的鬼抓人:一隻當鬼追最近的同伴,其他往反方向逃;抓到就換人當鬼,
339
+ 並有短暫冷卻避免立刻抓回去。純邏輯、不依賴 Qt,由外部每一拍餵入座標。
340
+
341
+ Tag between pets: one is "it" and chases its nearest peer while the others
342
+ run the other way; catching someone passes "it" on, with a short cooldown so
343
+ the tag cannot bounce straight back. Pure logic — positions are fed in each
344
+ tick from outside.
345
+ """
346
+
347
+ CATCH_DISTANCE = 60.0
348
+ FLEE_DISTANCE = 220.0
349
+ COOLDOWN_TICKS = 20
350
+
351
+ def __init__(self, catch_distance: float = CATCH_DISTANCE, flee_distance: float = FLEE_DISTANCE,
352
+ cooldown_ticks: int = COOLDOWN_TICKS) -> None:
353
+ self.catch_distance = float(catch_distance)
354
+ self.flee_distance = float(flee_distance)
355
+ self.cooldown_ticks = max(0, int(cooldown_ticks))
356
+ self.it = None
357
+ self.just_tagged = None
358
+ self._cooldown = 0
359
+
360
+ def reset(self) -> None:
361
+ """重置遊戲(參與者換人或停玩時呼叫)。"""
362
+ self.it = None
363
+ self.just_tagged = None
364
+ self._cooldown = 0
365
+
366
+ @staticmethod
367
+ def _nearest(positions: dict, source):
368
+ """回傳離 source 最近的其他參與者 (id, 距離);只有一人時回傳 (None, inf)。"""
369
+ source_x, source_y = positions[source]
370
+ best, best_distance = None, float("inf")
371
+ for participant, (x, y) in positions.items():
372
+ if participant == source:
373
+ continue
374
+ distance = ((x - source_x) ** 2 + (y - source_y) ** 2) ** 0.5
375
+ if distance < best_distance:
376
+ best, best_distance = participant, distance
377
+ return best, best_distance
378
+
379
+ def update(self, positions: dict) -> dict:
380
+ """
381
+ 餵入 {參與者: (x, y)},回傳 {參與者: (角色, 目標 x)}。少於兩人時不開局。
382
+ Feed in {participant: (x, y)} and get {participant: (role, target_x)}.
383
+ """
384
+ self.just_tagged = None
385
+ if not positions or len(positions) < 2:
386
+ self.reset()
387
+ return {}
388
+ if self.it not in positions:
389
+ self.it = sorted(positions)[0]
390
+ self._cooldown = self.cooldown_ticks
391
+ # 冷卻期間只是跑給對方追,不換鬼;先判斷再遞減,冷卻 N 拍就真的保護 N 拍。
392
+ # While cooling down nobody can be tagged; check before decrementing so a
393
+ # cooldown of N protects exactly N updates.
394
+ can_tag = self._cooldown <= 0
395
+ if self._cooldown > 0:
396
+ self._cooldown -= 1
397
+ prey, distance = self._nearest(positions, self.it)
398
+ if prey is not None and can_tag and distance <= self.catch_distance:
399
+ self.it = prey
400
+ self.just_tagged = prey
401
+ self._cooldown = self.cooldown_ticks
402
+ prey, _distance = self._nearest(positions, self.it)
403
+ return self._roles(positions, prey)
404
+
405
+ def _roles(self, positions: dict, prey) -> dict:
406
+ """依目前的鬼與獵物組出每個人的角色與要走向的 x。"""
407
+ it_x, _it_y = positions[self.it]
408
+ roles = {self.it: (TAG_CHASE, positions[prey][0] if prey is not None else it_x)}
409
+ for participant, (x, _y) in positions.items():
410
+ if participant == self.it:
411
+ continue
412
+ away = self.flee_distance if x >= it_x else -self.flee_distance
413
+ roles[participant] = (TAG_FLEE, x + away)
414
+ return roles
415
+
416
+
294
417
  def message_bucket(hour: int) -> str:
295
418
  """依小時回傳時段 / Time-of-day bucket for the given hour."""
296
419
  hour = int(hour) % 24
@@ -932,6 +1055,8 @@ class DesktopPetWidget(BaseWidget):
932
1055
  self._peers_provider = None
933
1056
  self._peer_greeted = False
934
1057
  self._peer_tick = 0
1058
+ # 鬼抓人角色(由外部的 PetTagGame 指派)/ Tag role assigned by a PetTagGame
1059
+ self._tag_role = None
935
1060
  # 電量提醒 / Low-battery reaction
936
1061
  self._battery_provider = read_battery
937
1062
  self._battery_warned = False
@@ -984,8 +1109,31 @@ class DesktopPetWidget(BaseWidget):
984
1109
  """設定回傳其他寵物中心座標清單的函式 / Provider of other pets' centres."""
985
1110
  self._peers_provider = provider
986
1111
 
1112
+ def apply_tag_role(self, role) -> None:
1113
+ """
1114
+ 套用鬼抓人角色 (角色, 目標 x);None 代表沒在玩,恢復平常的同伴互動。
1115
+ Apply a tag role; None means the game is off and normal peer play resumes.
1116
+ """
1117
+ if role is None:
1118
+ if self._tag_role is not None:
1119
+ self._tag_role = None
1120
+ self.motion.clear_follow()
1121
+ return
1122
+ self._tag_role, target_x = role
1123
+ self.motion.wake()
1124
+ self.motion.set_follow(target_x)
1125
+
1126
+ def on_tagged(self) -> None:
1127
+ """換自己當鬼時喊一句 / Shout when the tag lands on this pet."""
1128
+ self._gain_affection(1)
1129
+ pool = self._messages.get("tag") or []
1130
+ if pool:
1131
+ self.say(pool[self._chatter_rng.randrange(len(pool))])
1132
+
987
1133
  def _check_peers(self) -> None:
988
1134
  """靠近其他寵物時走過去玩耍、抵達後轉向打招呼(帶遲滯,避免重複)。"""
1135
+ if self._tag_role is not None:
1136
+ return # the tag game owns where this pet is heading
989
1137
  if self._peers_provider is None:
990
1138
  return
991
1139
  try:
@@ -1164,9 +1312,14 @@ class DesktopPetWidget(BaseWidget):
1164
1312
  "battery": lines("pet_chatter_battery", "Battery's low - plug me in?|Low power!|Find a charger?"),
1165
1313
  "hungry": lines("pet_chatter_hungry", "I'm hungry...|Got a snack?|Feed me?"),
1166
1314
  "fed": lines("pet_chatter_fed", "Yum!|Thank you!|Delicious!"),
1315
+ "fed_feast": lines("pet_chatter_fed_feast", "What a feast!|So full!|That was huge!"),
1316
+ "fed_music": lines("pet_chatter_fed_music", "Tasty tune!|That one sings!|Encore!"),
1317
+ "fed_book": lines("pet_chatter_fed_book", "Food for thought.|Chewy words!|Mmm, a story."),
1318
+ "fed_hard": lines("pet_chatter_fed_hard", "Too hard to chew!|Ouch, my teeth!|Not food!"),
1167
1319
  "away": lines("pet_chatter_away", "Still there?|I'll nap till you're back~|Zzz..."),
1168
1320
  "levelup": lines("pet_chatter_levelup", "Level up!|I'm growing!|We're getting closer!"),
1169
1321
  "costume": lines("pet_chatter_costume", "New look!|How do I look?|Nice fit!"),
1322
+ "tag": lines("pet_chatter_tag", "You're it!|Caught you!|My turn to chase!"),
1170
1323
  "any": lines("pet_chatter_any", "Hi there!|(^_^)|Keep going!"),
1171
1324
  }
1172
1325
 
@@ -1333,14 +1486,19 @@ class DesktopPetWidget(BaseWidget):
1333
1486
  def _persist_hunger(self) -> None:
1334
1487
  user_setting_dict["pet_hunger"] = self._hunger.value
1335
1488
 
1336
- def feed(self) -> None:
1337
- """餵食:補足飽足並讓心情變好,說一句道謝。"""
1338
- self._hunger.feed()
1339
- self._mood.pet()
1489
+ def feed(self, kind: str = FOOD_SNACK) -> None:
1490
+ """餵食:依食物種類調整飽足與心情,並說一句對應的話。"""
1491
+ effect = FOOD_EFFECTS.get(kind, FOOD_EFFECTS[FOOD_SNACK])
1492
+ self._hunger.feed(effect["fullness"])
1493
+ mood_change = effect["mood"]
1494
+ if mood_change >= 0:
1495
+ self._mood.pet(mood_change)
1496
+ else:
1497
+ self._mood.decay(-mood_change)
1340
1498
  self._persist_hunger()
1341
1499
  self._persist_mood()
1342
- self._gain_affection(5)
1343
- pool = self._messages.get("fed") or []
1500
+ self._gain_affection(effect["affection"])
1501
+ pool = self._messages.get(f"fed_{kind}") or self._messages.get("fed") or []
1344
1502
  if pool:
1345
1503
  self.say(pool[self._chatter_rng.randrange(len(pool))])
1346
1504
 
@@ -1484,7 +1642,7 @@ class DesktopPetWidget(BaseWidget):
1484
1642
  if classify_drop(path) == DROP_SPRITE:
1485
1643
  self.change_sprite(path)
1486
1644
  else:
1487
- self.feed()
1645
+ self.feed(classify_food(path))
1488
1646
  event.acceptProposedAction()
1489
1647
 
1490
1648
  def contextMenuEvent(self, event) -> None:
@@ -1,12 +1,13 @@
1
1
  from typing import Optional
2
2
 
3
+ from PySide6.QtCore import QTimer
3
4
  from PySide6.QtGui import QGuiApplication
4
5
  from PySide6.QtWidgets import (
5
6
  QWidget, QGridLayout, QLabel, QPushButton, QMessageBox, QComboBox, QCheckBox, QFileDialog,
6
7
  )
7
8
 
8
9
  from frontengine.show.pet.desktop_pet import (
9
- DesktopPetWidget, BEHAVIOUR_FLOOR, BEHAVIOUR_WANDER, BEHAVIOUR_CHASE, scan_pet_pack,
10
+ DesktopPetWidget, BEHAVIOUR_FLOOR, BEHAVIOUR_WANDER, BEHAVIOUR_CHASE, PetTagGame, scan_pet_pack,
10
11
  )
11
12
  from frontengine.ui.dialog.choose_file_dialog import choose_pet, choose_wav_sound
12
13
  from frontengine.ui.page.utils import (
@@ -36,6 +37,11 @@ class PetSettingUI(QWidget):
36
37
  self.ready_to_play = False
37
38
  self.pet_image_path: Optional[str] = None
38
39
  self.pet_sound_path: Optional[str] = None
40
+ # 鬼抓人:由本頁統一驅動,寵物只負責套用被指派的角色
41
+ # Tag game: driven from this page, pets only apply the role they are given.
42
+ self.tag_game = PetTagGame()
43
+ self.tag_timer = QTimer(self)
44
+ self.tag_timer.timeout.connect(self._tick_tag_game)
39
45
 
40
46
  # Choose file
41
47
  self.choose_file_button = QPushButton(
@@ -81,6 +87,9 @@ class PetSettingUI(QWidget):
81
87
  self.talk_checkbox.setChecked(True)
82
88
  self.sit_checkbox = QCheckBox(language_wrapper.language_word_dict.get("pet_sit_label", "Sit on windows"))
83
89
  self.sit_checkbox.setChecked(True)
90
+ self.tag_checkbox = QCheckBox(
91
+ language_wrapper.language_word_dict.get("pet_tag_label", "Play tag with each other"))
92
+ self.tag_checkbox.toggled.connect(self._on_tag_toggled)
84
93
 
85
94
  # Target monitor + sound volume
86
95
  self.target_monitor_label = QLabel(
@@ -134,6 +143,7 @@ class PetSettingUI(QWidget):
134
143
  self.grid_layout.addWidget(self.volume_label, 6, 2)
135
144
  self.grid_layout.addWidget(self.volume_combobox, 7, 2)
136
145
  self.grid_layout.addWidget(self.audio_react_checkbox, 7, 0)
146
+ self.grid_layout.addWidget(self.tag_checkbox, 7, 1)
137
147
  self.grid_layout.addWidget(self.drop_hint_label, 8, 0, 1, 3)
138
148
 
139
149
  def _spawn_pet(self) -> None:
@@ -163,6 +173,7 @@ class PetSettingUI(QWidget):
163
173
  pet.set_pet_window_flag()
164
174
  self.pet_list.append(pet)
165
175
  pet.show()
176
+ self._start_tag_game()
166
177
  if geometry is not None:
167
178
  pet.setScreen(geometry[1])
168
179
  bounds = geometry[0]
@@ -180,17 +191,59 @@ class PetSettingUI(QWidget):
180
191
  return None
181
192
  return (screen.availableGeometry(), screen)
182
193
 
183
- def _peer_centers(self, me) -> list:
184
- """回傳其他仍存活寵物的中心座標(略過已被銷毀者)。"""
185
- centers = []
186
- for other in list(self.pet_list):
187
- if other is me:
188
- continue
194
+ def _alive_pets(self) -> list:
195
+ """回傳仍存活的寵物,並把已被銷毀者從清單移除。"""
196
+ alive = []
197
+ for pet in list(self.pet_list):
189
198
  try:
190
- centers.append(other.center())
199
+ pet.center()
191
200
  except RuntimeError:
192
- pass # underlying C++ object already deleted
193
- return centers
201
+ self.pet_list.remove(pet) # underlying C++ object already deleted
202
+ continue
203
+ alive.append(pet)
204
+ return alive
205
+
206
+ def _peer_centers(self, me) -> list:
207
+ """回傳其他仍存活寵物的中心座標(略過已被銷毀者)。"""
208
+ return [pet.center() for pet in self._alive_pets() if pet is not me]
209
+
210
+ TAG_INTERVAL_MS = 100
211
+
212
+ def _on_tag_toggled(self, checked: bool) -> None:
213
+ """切換鬼抓人:關掉時停錶並把所有寵物放回平常的行為。"""
214
+ front_engine_logger.info(f"[PetSettingUI] tag game | enabled={checked}")
215
+ if checked:
216
+ self._start_tag_game()
217
+ return
218
+ self.tag_timer.stop()
219
+ self.tag_game.reset()
220
+ for pet in self._alive_pets():
221
+ pet.apply_tag_role(None)
222
+
223
+ def _start_tag_game(self) -> None:
224
+ """有兩隻以上寵物且已勾選時才需要開錶 / Only run the game with 2+ pets."""
225
+ if self.tag_checkbox.isChecked() and len(self._alive_pets()) >= 2:
226
+ if not self.tag_timer.isActive():
227
+ self.tag_timer.start(self.TAG_INTERVAL_MS)
228
+
229
+ def _tick_tag_game(self) -> None:
230
+ """每一拍把座標餵給遊戲,再把角色發回各隻寵物。"""
231
+ pets = self._alive_pets()
232
+ if len(pets) < 2:
233
+ self.tag_timer.stop()
234
+ self.tag_game.reset()
235
+ for pet in pets:
236
+ pet.apply_tag_role(None)
237
+ return
238
+ # 用每隻寵物的物件識別碼當鍵,避免有寵物關閉後索引位移導致「鬼」換人
239
+ # Key by object identity so closing a pet cannot shift who is "it".
240
+ by_key = {id(pet): pet for pet in pets}
241
+ roles = self.tag_game.update({key: pet.center() for key, pet in by_key.items()})
242
+ tagged = self.tag_game.just_tagged
243
+ for key, pet in by_key.items():
244
+ pet.apply_tag_role(roles.get(key))
245
+ if key == tagged:
246
+ pet.on_tagged()
194
247
 
195
248
  def start_play_pet(self) -> None:
196
249
  front_engine_logger.info("[PetSettingUI] start_play_pet")
@@ -271,6 +324,7 @@ class PetSettingUI(QWidget):
271
324
  "target_monitor": self.target_monitor_combobox.currentText(),
272
325
  "volume": self.volume_combobox.currentText(),
273
326
  "audio_react": self.audio_react_checkbox.isChecked(),
327
+ "tag": self.tag_checkbox.isChecked(),
274
328
  }
275
329
 
276
330
  def set_state(self, state: dict) -> None:
@@ -308,3 +362,5 @@ class PetSettingUI(QWidget):
308
362
  self.volume_combobox.setCurrentIndex(index)
309
363
  if "audio_react" in state:
310
364
  self.audio_react_checkbox.setChecked(bool(state["audio_react"]))
365
+ if "tag" in state:
366
+ self.tag_checkbox.setChecked(bool(state["tag"]))
@@ -236,9 +236,15 @@ english_word_dict = {
236
236
  "pet_hunger_hungry": "hungry",
237
237
  "pet_chatter_hungry": "I'm hungry...|Got a snack?|Feed me, please?|My tummy's rumbling~",
238
238
  "pet_chatter_fed": "Yum!|Thank you!|Delicious!|Nom nom (^o^)",
239
+ "pet_chatter_fed_feast": "What a feast!|So full!|That was a big one!|I could nap now...",
240
+ "pet_chatter_fed_music": "Tasty tune!|That one sings!|Encore!|My favourite flavour ♪",
241
+ "pet_chatter_fed_book": "Food for thought.|Chewy words!|Mmm, a story.|I read every bite.",
242
+ "pet_chatter_fed_hard": "Too hard to chew!|Ouch, my teeth!|That is not food!|Yuck (>_<)",
239
243
  "pet_chatter_away": "Still there?|I'll nap till you're back~|Zzz...|Take your time!",
240
244
  "pet_chatter_levelup": "Level up!|I'm growing!|We're getting closer!|Yay, thank you!",
241
245
  "pet_chatter_costume": "New look!|How do I look?|Nice fit!|I like this one!",
246
+ "pet_chatter_tag": "You're it!|Caught you!|My turn to chase!|Hey, no fair!",
247
+ "pet_tag_label": "Play tag with each other",
242
248
  "pet_drop_hint": "Tip: drop an image or pet pack onto the pet to change its look, "
243
249
  "or any other file to feed it.",
244
250
  "pet_set_reminder": "Set reminder...",
@@ -237,9 +237,15 @@ traditional_chinese_word_dict = {
237
237
  "pet_hunger_hungry": "肚子餓",
238
238
  "pet_chatter_hungry": "我餓了…|有點心嗎?|餵餵我好嗎?|肚子咕咕叫~",
239
239
  "pet_chatter_fed": "好吃!|謝謝你!|真美味!|嗯嗯 (^o^)",
240
+ "pet_chatter_fed_feast": "好大一餐!|吃得好飽!|這份量夠了!|想睡午覺了…",
241
+ "pet_chatter_fed_music": "這口有旋律!|好聽又好吃!|再來一首!|我最愛這個味道 ♪",
242
+ "pet_chatter_fed_book": "很有味道的一段。|字字都要嚼。|嗯,有故事的味道。|我一口一句讀完了。",
243
+ "pet_chatter_fed_hard": "太硬了咬不動!|我的牙齒啊!|這不是食物吧!|嘔 (>_<)",
240
244
  "pet_chatter_away": "還在嗎?|我先打個盹等你回來~|Zzz…|慢慢來沒關係!",
241
245
  "pet_chatter_levelup": "升級了!|我長大了!|我們更親近了!|耶,謝謝你!",
242
246
  "pet_chatter_costume": "換新裝!|好看嗎?|很合身耶!|我喜歡這套!",
247
+ "pet_chatter_tag": "換你當鬼!|抓到你了!|輪到我追了!|喂,不公平啦!",
248
+ "pet_tag_label": "寵物玩鬼抓人",
243
249
  "pet_drop_hint": "小提示:把圖片或動作包拖到寵物身上可以換裝,拖其他檔案則是餵牠吃東西。",
244
250
  "pet_set_reminder": "設定提醒...",
245
251
  "pet_reminder_prompt": "提醒我:",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.27
3
+ Version: 1.0.28
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
@@ -68,7 +68,12 @@ Spawn an animated sprite that lives on your desktop, from the **Pet** tab.
68
68
  away, and warns you about a low battery.
69
69
  - **Interaction** — drag it around, right-click to clone/feed/set a reminder,
70
70
  and **drop a file onto it**: an image or pet pack becomes its new look,
71
- anything else is eaten as a snack.
71
+ anything else is eaten. What it eats matters — an archive is a feast, music
72
+ cheers it up more than it fills, a document is a modest meal, and a binary is
73
+ too hard to chew.
74
+ - **Tag** — with two or more pets on screen, tick *Play tag with each other* and
75
+ one becomes "it": it walks toward its nearest neighbour while the others run
76
+ the other way, and catching someone passes the tag on.
72
77
  - **React to audio** — the pet pulses to your speakers' output level. It reads
73
78
  only the Windows output *meter* (WASAPI `IAudioMeterInformation`); no audio
74
79
  is captured or recorded. Peaks are smoothed with an RMS window and a
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "frontengine"
9
- version = "1.0.27"
9
+ version = "1.0.28"
10
10
  authors = [
11
11
  { name = "JE-Chen", email = "jechenmailman@gmail.com" },
12
12
  ]
File without changes
File without changes