frontengine 1.0.27__tar.gz → 1.0.29__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.29}/PKG-INFO +7 -2
  2. {frontengine-1.0.27 → frontengine-1.0.29}/README.md +6 -1
  3. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/pet/desktop_pet.py +230 -10
  4. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/pet/pet_setting_ui.py +66 -10
  5. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/multi_language/english.py +6 -0
  6. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/multi_language/traditional_chinese.py +6 -0
  7. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine.egg-info/PKG-INFO +7 -2
  8. {frontengine-1.0.27 → frontengine-1.0.29}/pyproject.toml +1 -1
  9. {frontengine-1.0.27 → frontengine-1.0.29}/LICENSE +0 -0
  10. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/__init__.py +0 -0
  11. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/__main__.py +0 -0
  12. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/__init__.py +0 -0
  13. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/base_widget.py +0 -0
  14. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/gif/__init__.py +0 -0
  15. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/gif/paint_gif.py +0 -0
  16. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/image/__init__.py +0 -0
  17. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/image/paint_image.py +0 -0
  18. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/load/__init__.py +0 -0
  19. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/load/load_someone_make_ui.py +0 -0
  20. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/overlay_factory.py +0 -0
  21. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/particle/__init__.py +0 -0
  22. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/particle/particle_ui.py +0 -0
  23. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/pet/__init__.py +0 -0
  24. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/scene/__init__.py +0 -0
  25. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/scene/extend_graphic_scene.py +0 -0
  26. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/scene/extend_graphic_view.py +0 -0
  27. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/scene/scene.py +0 -0
  28. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/sound_player/__init__.py +0 -0
  29. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/sound_player/sound_effect.py +0 -0
  30. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/sound_player/sound_player.py +0 -0
  31. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/text/__init__.py +0 -0
  32. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/text/draw_text.py +0 -0
  33. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/video/__init__.py +0 -0
  34. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/video/video_player.py +0 -0
  35. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/web/__init__.py +0 -0
  36. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/web/webview.py +0 -0
  37. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/show/window_helpers.py +0 -0
  38. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/system_tray/__init__.py +0 -0
  39. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/system_tray/extend_system_tray.py +0 -0
  40. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/__init__.py +0 -0
  41. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/color/__init__.py +0 -0
  42. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/color/global_color.py +0 -0
  43. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/dialog/__init__.py +0 -0
  44. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
  45. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
  46. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/dialog/save_file_dialog.py +0 -0
  47. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/main_ui.py +0 -0
  48. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/menu/__init__.py +0 -0
  49. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/menu/help_menu.py +0 -0
  50. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/menu/how_to_menu.py +0 -0
  51. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/menu/language_menu.py +0 -0
  52. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/menu/preset_menu.py +0 -0
  53. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/menu/settings_menu.py +0 -0
  54. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/__init__.py +0 -0
  55. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/control_center/__init__.py +0 -0
  56. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/control_center/control_center_ui.py +0 -0
  57. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/gif/__init__.py +0 -0
  58. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
  59. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/image/__init__.py +0 -0
  60. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/image/image_setting_ui.py +0 -0
  61. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/particle/__init__.py +0 -0
  62. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
  63. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/pet/__init__.py +0 -0
  64. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/scene_setting/__init__.py +0 -0
  65. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
  66. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/scene_setting/scene_page/__init__.py +0 -0
  67. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
  68. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
  69. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
  70. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
  71. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
  72. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
  73. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
  74. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
  75. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
  76. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/sound_player/__init__.py +0 -0
  77. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
  78. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/text/__init__.py +0 -0
  79. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/text/text_setting_ui.py +0 -0
  80. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/utils.py +0 -0
  81. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/video/__init__.py +0 -0
  82. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/video/video_setting_ui.py +0 -0
  83. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/web/__init__.py +0 -0
  84. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/ui/page/web/web_setting_ui.py +0 -0
  85. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/user_setting/__init__.py +0 -0
  86. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/user_setting/preset_repository.py +0 -0
  87. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/user_setting/scene_setting.py +0 -0
  88. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/user_setting/user_setting_file.py +0 -0
  89. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/__init__.py +0 -0
  90. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/audio_meter/__init__.py +0 -0
  91. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/audio_meter/audio_envelope.py +0 -0
  92. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/audio_meter/screen_audio.py +0 -0
  93. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/audio_meter/system_audio_meter.py +0 -0
  94. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/browser/__init__.py +0 -0
  95. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/browser/browser.py +0 -0
  96. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/critical_exit/__init__.py +0 -0
  97. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
  98. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/critical_exit/critical_exit.py +0 -0
  99. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/critical_exit/win32_vk.py +0 -0
  100. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/exception/__init__.py +0 -0
  101. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/exception/exception_tags.py +0 -0
  102. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/exception/exceptions.py +0 -0
  103. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/file/__init__.py +0 -0
  104. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/file/open/__init__.py +0 -0
  105. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/file/open/open_file.py +0 -0
  106. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/file/save/__init__.py +0 -0
  107. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/file/save/save_file.py +0 -0
  108. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/hotkey/__init__.py +0 -0
  109. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/hotkey/hotkey_service.py +0 -0
  110. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/json/__init__.py +0 -0
  111. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/json/json_file.py +0 -0
  112. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/json/json_repository.py +0 -0
  113. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/json_format/__init__.py +0 -0
  114. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/json_format/json_process.py +0 -0
  115. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/logging/__init__.py +0 -0
  116. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/logging/loggin_instance.py +0 -0
  117. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/multi_language/__init__.py +0 -0
  118. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/multi_language/france.py +0 -0
  119. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/multi_language/germany.py +0 -0
  120. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/multi_language/italy.py +0 -0
  121. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/multi_language/language_wrapper.py +0 -0
  122. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/multi_language/russian.py +0 -0
  123. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
  124. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/preset_schedule/__init__.py +0 -0
  125. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
  126. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/redirect_manager/__init__.py +0 -0
  127. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
  128. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/smart_pause/__init__.py +0 -0
  129. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
  130. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/theme_schedule/__init__.py +0 -0
  131. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
  132. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/utils/web_url.py +0 -0
  133. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/worker/__init__.py +0 -0
  134. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine/worker/qthread_worker.py +0 -0
  135. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine.egg-info/SOURCES.txt +0 -0
  136. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine.egg-info/dependency_links.txt +0 -0
  137. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine.egg-info/entry_points.txt +0 -0
  138. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine.egg-info/requires.txt +0 -0
  139. {frontengine-1.0.27 → frontengine-1.0.29}/frontengine.egg-info/top_level.txt +0 -0
  140. {frontengine-1.0.27 → frontengine-1.0.29}/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.29
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,106 @@ 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
+ """
407
+ 依目前的鬼與獵物組出每個人的 (角色, 目標 x, 目標 y)。逃跑方向取兩點連線的反向,
408
+ 讓地板模式(只用 x)與飄移/追游標模式(用整個點)都有合理目標。
409
+ Build each participant's (role, target_x, target_y). Runners head along
410
+ the line away from "it", which suits floor pets (x only) and wander or
411
+ chase pets (the whole point) alike.
412
+ """
413
+ it_x, it_y = positions[self.it]
414
+ prey_position = positions[prey] if prey is not None else (it_x, it_y)
415
+ roles = {self.it: (TAG_CHASE, float(prey_position[0]), float(prey_position[1]))}
416
+ for participant, (x, y) in positions.items():
417
+ if participant == self.it:
418
+ continue
419
+ dx, dy = x - it_x, y - it_y
420
+ length = (dx * dx + dy * dy) ** 0.5
421
+ if length < 1e-9: # standing on each other: just pick a direction
422
+ dx, dy, length = 1.0, 0.0, 1.0
423
+ roles[participant] = (
424
+ TAG_FLEE,
425
+ x + self.flee_distance * dx / length,
426
+ y + self.flee_distance * dy / length,
427
+ )
428
+ return roles
429
+
430
+
294
431
  def message_bucket(hour: int) -> str:
295
432
  """依小時回傳時段 / Time-of-day bucket for the given hour."""
296
433
  hour = int(hour) % 24
@@ -543,6 +680,10 @@ class PetMotion:
543
680
  self.ground_feet = 0.0
544
681
  # 追向某個 x(用於朝同伴走過去玩耍)/ Horizontal target to walk toward (play).
545
682
  self.follow_target_x: Optional[float] = None
683
+ # 引導目標 (x, y):三種行為模式都會朝它移動(鬼抓人用),優先於各自的預設動線。
684
+ # Guidance point (x, y) honoured by every behaviour (used by the tag
685
+ # game); it takes precedence over each mode's own default movement.
686
+ self.guidance: Optional[Tuple[float, float]] = None
546
687
 
547
688
  @property
548
689
  def facing_left(self) -> bool:
@@ -590,6 +731,43 @@ class PetMotion:
590
731
  def set_target(self, x: float, y: float) -> None:
591
732
  self.target = (float(x), float(y))
592
733
 
734
+ def set_guidance(self, x: float, y: Optional[float] = None) -> None:
735
+ """
736
+ 設定引導目標;地板模式走向該 x,飄移/追游標模式則朝該點移動。
737
+ Set the guidance point: floor pets walk toward its x, wander and chase
738
+ pets steer toward the point itself.
739
+ """
740
+ centre_y = self.y + self.height / 2.0
741
+ self.guidance = (float(x), centre_y if y is None else float(y))
742
+ if self.behaviour == BEHAVIOUR_FLOOR:
743
+ self.follow_target_x = self.guidance[0]
744
+
745
+ def clear_guidance(self) -> None:
746
+ """取消引導,回到該模式原本的動線 / Drop the guidance and resume normal movement."""
747
+ self.guidance = None
748
+ self.follow_target_x = None
749
+
750
+ STEER_BLEND = 0.4
751
+
752
+ def _steer_toward(self, point: Tuple[float, float]) -> None:
753
+ """把速度往目標點轉一些(保持速率),轉彎自然而不是瞬間轉向。"""
754
+ target_x, target_y = point
755
+ dx = target_x - (self.x + self.width / 2.0)
756
+ dy = target_y - (self.y + self.height / 2.0)
757
+ distance = (dx * dx + dy * dy) ** 0.5
758
+ if distance < 1e-9:
759
+ return
760
+ desired_x = self.speed * dx / distance
761
+ desired_y = self.speed * dy / distance
762
+ blended_x = self.vx + self.STEER_BLEND * (desired_x - self.vx)
763
+ blended_y = self.vy + self.STEER_BLEND * (desired_y - self.vy)
764
+ magnitude = (blended_x * blended_x + blended_y * blended_y) ** 0.5
765
+ if magnitude < 1e-9:
766
+ self.vx, self.vy = desired_x, desired_y
767
+ return
768
+ self.vx = self.speed * blended_x / magnitude
769
+ self.vy = self.speed * blended_y / magnitude
770
+
593
771
  def throw(self, vx: float, vy: float) -> None:
594
772
  """拖曳放開後以動量丟出(只在重力模式有意義)。"""
595
773
  self.vx = float(vx)
@@ -822,6 +1000,8 @@ class PetMotion:
822
1000
  def _step_wander(self, left, top, right, bottom) -> Tuple[int, int]:
823
1001
  if abs(self.vy) < 1e-9:
824
1002
  self.vy = float(self.speed)
1003
+ if self.guidance is not None:
1004
+ self._steer_toward(self.guidance)
825
1005
  self.x += self.vx
826
1006
  self.y += self.vy
827
1007
  if self.x <= left:
@@ -840,14 +1020,19 @@ class PetMotion:
840
1020
 
841
1021
  # --- chase cursor ---
842
1022
  def _step_chase(self, left, top, right, bottom) -> Tuple[int, int]:
843
- if self.target is None:
1023
+ # 被引導時(鬼抓人)以引導目標為準,而不是游標
1024
+ # While guided (tag game) the guidance point wins over the cursor.
1025
+ chase_target = self.guidance if self.guidance is not None else self.target
1026
+ if chase_target is None:
844
1027
  return int(self.x), int(self.y)
845
- target_x, target_y = self.target
1028
+ target_x, target_y = chase_target
846
1029
  dx = target_x - (self.x + self.width / 2)
847
1030
  dy = target_y - (self.y + self.height / 2)
848
1031
  distance = (dx * dx + dy * dy) ** 0.5
849
1032
  if distance <= self.CHASE_STOP_DISTANCE:
850
- self.asleep = True
1033
+ # 追到游標會睡著;玩鬼抓人時只是站在對方身上,不睡。
1034
+ # Catching the cursor means a nap; catching a playmate does not.
1035
+ self.asleep = self.guidance is None
851
1036
  return int(self.x), int(self.y)
852
1037
  self.asleep = False
853
1038
  step = float(self.speed) * self.CHASE_SPEED_FACTOR
@@ -932,6 +1117,8 @@ class DesktopPetWidget(BaseWidget):
932
1117
  self._peers_provider = None
933
1118
  self._peer_greeted = False
934
1119
  self._peer_tick = 0
1120
+ # 鬼抓人角色(由外部的 PetTagGame 指派)/ Tag role assigned by a PetTagGame
1121
+ self._tag_role = None
935
1122
  # 電量提醒 / Low-battery reaction
936
1123
  self._battery_provider = read_battery
937
1124
  self._battery_warned = False
@@ -984,8 +1171,31 @@ class DesktopPetWidget(BaseWidget):
984
1171
  """設定回傳其他寵物中心座標清單的函式 / Provider of other pets' centres."""
985
1172
  self._peers_provider = provider
986
1173
 
1174
+ def apply_tag_role(self, role) -> None:
1175
+ """
1176
+ 套用鬼抓人角色 (角色, 目標 x, 目標 y);None 代表沒在玩,恢復平常的同伴互動。
1177
+ Apply a tag role; None means the game is off and normal peer play resumes.
1178
+ """
1179
+ if role is None:
1180
+ if self._tag_role is not None:
1181
+ self._tag_role = None
1182
+ self.motion.clear_guidance()
1183
+ return
1184
+ self._tag_role, target_x, target_y = role
1185
+ self.motion.wake()
1186
+ self.motion.set_guidance(target_x, target_y)
1187
+
1188
+ def on_tagged(self) -> None:
1189
+ """換自己當鬼時喊一句 / Shout when the tag lands on this pet."""
1190
+ self._gain_affection(1)
1191
+ pool = self._messages.get("tag") or []
1192
+ if pool:
1193
+ self.say(pool[self._chatter_rng.randrange(len(pool))])
1194
+
987
1195
  def _check_peers(self) -> None:
988
1196
  """靠近其他寵物時走過去玩耍、抵達後轉向打招呼(帶遲滯,避免重複)。"""
1197
+ if self._tag_role is not None:
1198
+ return # the tag game owns where this pet is heading
989
1199
  if self._peers_provider is None:
990
1200
  return
991
1201
  try:
@@ -1164,9 +1374,14 @@ class DesktopPetWidget(BaseWidget):
1164
1374
  "battery": lines("pet_chatter_battery", "Battery's low - plug me in?|Low power!|Find a charger?"),
1165
1375
  "hungry": lines("pet_chatter_hungry", "I'm hungry...|Got a snack?|Feed me?"),
1166
1376
  "fed": lines("pet_chatter_fed", "Yum!|Thank you!|Delicious!"),
1377
+ "fed_feast": lines("pet_chatter_fed_feast", "What a feast!|So full!|That was huge!"),
1378
+ "fed_music": lines("pet_chatter_fed_music", "Tasty tune!|That one sings!|Encore!"),
1379
+ "fed_book": lines("pet_chatter_fed_book", "Food for thought.|Chewy words!|Mmm, a story."),
1380
+ "fed_hard": lines("pet_chatter_fed_hard", "Too hard to chew!|Ouch, my teeth!|Not food!"),
1167
1381
  "away": lines("pet_chatter_away", "Still there?|I'll nap till you're back~|Zzz..."),
1168
1382
  "levelup": lines("pet_chatter_levelup", "Level up!|I'm growing!|We're getting closer!"),
1169
1383
  "costume": lines("pet_chatter_costume", "New look!|How do I look?|Nice fit!"),
1384
+ "tag": lines("pet_chatter_tag", "You're it!|Caught you!|My turn to chase!"),
1170
1385
  "any": lines("pet_chatter_any", "Hi there!|(^_^)|Keep going!"),
1171
1386
  }
1172
1387
 
@@ -1333,14 +1548,19 @@ class DesktopPetWidget(BaseWidget):
1333
1548
  def _persist_hunger(self) -> None:
1334
1549
  user_setting_dict["pet_hunger"] = self._hunger.value
1335
1550
 
1336
- def feed(self) -> None:
1337
- """餵食:補足飽足並讓心情變好,說一句道謝。"""
1338
- self._hunger.feed()
1339
- self._mood.pet()
1551
+ def feed(self, kind: str = FOOD_SNACK) -> None:
1552
+ """餵食:依食物種類調整飽足與心情,並說一句對應的話。"""
1553
+ effect = FOOD_EFFECTS.get(kind, FOOD_EFFECTS[FOOD_SNACK])
1554
+ self._hunger.feed(effect["fullness"])
1555
+ mood_change = effect["mood"]
1556
+ if mood_change >= 0:
1557
+ self._mood.pet(mood_change)
1558
+ else:
1559
+ self._mood.decay(-mood_change)
1340
1560
  self._persist_hunger()
1341
1561
  self._persist_mood()
1342
- self._gain_affection(5)
1343
- pool = self._messages.get("fed") or []
1562
+ self._gain_affection(effect["affection"])
1563
+ pool = self._messages.get(f"fed_{kind}") or self._messages.get("fed") or []
1344
1564
  if pool:
1345
1565
  self.say(pool[self._chatter_rng.randrange(len(pool))])
1346
1566
 
@@ -1484,7 +1704,7 @@ class DesktopPetWidget(BaseWidget):
1484
1704
  if classify_drop(path) == DROP_SPRITE:
1485
1705
  self.change_sprite(path)
1486
1706
  else:
1487
- self.feed()
1707
+ self.feed(classify_food(path))
1488
1708
  event.acceptProposedAction()
1489
1709
 
1490
1710
  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.29
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.29"
10
10
  authors = [
11
11
  { name = "JE-Chen", email = "jechenmailman@gmail.com" },
12
12
  ]
File without changes
File without changes