frontengine 1.0.26__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.
- {frontengine-1.0.26 → frontengine-1.0.28}/PKG-INFO +33 -1
- {frontengine-1.0.26 → frontengine-1.0.28}/README.md +32 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/pet/desktop_pet.py +267 -11
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/pet/pet_setting_ui.py +79 -13
- frontengine-1.0.28/frontengine/utils/audio_meter/audio_envelope.py +56 -0
- frontengine-1.0.28/frontengine/utils/audio_meter/screen_audio.py +130 -0
- frontengine-1.0.28/frontengine/utils/audio_meter/system_audio_meter.py +298 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/multi_language/english.py +9 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/multi_language/traditional_chinese.py +8 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine.egg-info/PKG-INFO +33 -1
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine.egg-info/SOURCES.txt +2 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/pyproject.toml +1 -1
- frontengine-1.0.26/frontengine/utils/audio_meter/system_audio_meter.py +0 -136
- {frontengine-1.0.26 → frontengine-1.0.28}/LICENSE +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/__main__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/base_widget.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/gif/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/gif/paint_gif.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/image/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/image/paint_image.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/load/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/load/load_someone_make_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/overlay_factory.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/particle/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/particle/particle_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/pet/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/scene/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/scene/extend_graphic_scene.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/scene/extend_graphic_view.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/scene/scene.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/sound_player/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/sound_player/sound_effect.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/sound_player/sound_player.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/text/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/text/draw_text.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/video/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/video/video_player.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/web/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/web/webview.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/show/window_helpers.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/system_tray/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/system_tray/extend_system_tray.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/color/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/color/global_color.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/dialog/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/dialog/save_file_dialog.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/main_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/menu/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/menu/help_menu.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/menu/how_to_menu.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/menu/language_menu.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/menu/preset_menu.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/menu/settings_menu.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/control_center/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/control_center/control_center_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/gif/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/image/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/image/image_setting_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/particle/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/pet/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/sound_player/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/text/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/text/text_setting_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/utils.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/video/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/video/video_setting_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/web/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/ui/page/web/web_setting_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/user_setting/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/user_setting/preset_repository.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/user_setting/scene_setting.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/user_setting/user_setting_file.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/audio_meter/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/browser/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/browser/browser.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/critical_exit/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/critical_exit/critical_exit.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/critical_exit/win32_vk.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/exception/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/exception/exception_tags.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/exception/exceptions.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/file/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/file/open/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/file/open/open_file.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/file/save/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/file/save/save_file.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/hotkey/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/hotkey/hotkey_service.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/json/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/json/json_file.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/json/json_repository.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/json_format/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/json_format/json_process.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/logging/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/logging/loggin_instance.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/multi_language/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/multi_language/france.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/multi_language/germany.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/multi_language/italy.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/multi_language/language_wrapper.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/multi_language/russian.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/preset_schedule/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/redirect_manager/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/smart_pause/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/theme_schedule/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/utils/web_url.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/worker/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine/worker/qthread_worker.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine.egg-info/dependency_links.txt +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine.egg-info/entry_points.txt +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine.egg-info/requires.txt +0 -0
- {frontengine-1.0.26 → frontengine-1.0.28}/frontengine.egg-info/top_level.txt +0 -0
- {frontengine-1.0.26 → 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.
|
|
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
|
|
@@ -53,6 +53,38 @@ It provides an intuitive interface and supports multiple media formats for inter
|
|
|
53
53
|
|
|
54
54
|
---
|
|
55
55
|
|
|
56
|
+
## Desktop pet
|
|
57
|
+
|
|
58
|
+
Spawn an animated sprite that lives on your desktop, from the **Pet** tab.
|
|
59
|
+
|
|
60
|
+
- **Sprites** — pick a single GIF/WebP/PNG, or a *pet pack* folder whose file
|
|
61
|
+
names map to states: `walk`, `idle`, `sleep`, `climb`, `fall`, `drag`
|
|
62
|
+
(missing states fall back to `walk`).
|
|
63
|
+
- **Behaviour** — walk on the floor with gravity (throw it and it bounces),
|
|
64
|
+
wander freely, or chase the cursor. Floor pets can climb screen edges and
|
|
65
|
+
stand on the top edge of other windows.
|
|
66
|
+
- **Life** — mood, fullness and an affection level that persist between runs;
|
|
67
|
+
the pet grows as it levels up, chats in speech bubbles, naps while you are
|
|
68
|
+
away, and warns you about a low battery.
|
|
69
|
+
- **Interaction** — drag it around, right-click to clone/feed/set a reminder,
|
|
70
|
+
and **drop a file onto it**: an image or pet pack becomes its new look,
|
|
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.
|
|
77
|
+
- **React to audio** — the pet pulses to your speakers' output level. It reads
|
|
78
|
+
only the Windows output *meter* (WASAPI `IAudioMeterInformation`); no audio
|
|
79
|
+
is captured or recorded. Peaks are smoothed with an RMS window and a
|
|
80
|
+
fast-attack/slow-decay envelope so the pulse breathes instead of flickering.
|
|
81
|
+
On multi-monitor setups each pet follows the audio endpoint that matches its
|
|
82
|
+
own screen, falling back to the default output device.
|
|
83
|
+
|
|
84
|
+
Audio features are Windows-only and degrade to "no pulse" elsewhere.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
56
88
|
## Install
|
|
57
89
|
|
|
58
90
|
- **System Requirements**
|
|
@@ -29,6 +29,38 @@ It provides an intuitive interface and supports multiple media formats for inter
|
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
32
|
+
## Desktop pet
|
|
33
|
+
|
|
34
|
+
Spawn an animated sprite that lives on your desktop, from the **Pet** tab.
|
|
35
|
+
|
|
36
|
+
- **Sprites** — pick a single GIF/WebP/PNG, or a *pet pack* folder whose file
|
|
37
|
+
names map to states: `walk`, `idle`, `sleep`, `climb`, `fall`, `drag`
|
|
38
|
+
(missing states fall back to `walk`).
|
|
39
|
+
- **Behaviour** — walk on the floor with gravity (throw it and it bounces),
|
|
40
|
+
wander freely, or chase the cursor. Floor pets can climb screen edges and
|
|
41
|
+
stand on the top edge of other windows.
|
|
42
|
+
- **Life** — mood, fullness and an affection level that persist between runs;
|
|
43
|
+
the pet grows as it levels up, chats in speech bubbles, naps while you are
|
|
44
|
+
away, and warns you about a low battery.
|
|
45
|
+
- **Interaction** — drag it around, right-click to clone/feed/set a reminder,
|
|
46
|
+
and **drop a file onto it**: an image or pet pack becomes its new look,
|
|
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.
|
|
53
|
+
- **React to audio** — the pet pulses to your speakers' output level. It reads
|
|
54
|
+
only the Windows output *meter* (WASAPI `IAudioMeterInformation`); no audio
|
|
55
|
+
is captured or recorded. Peaks are smoothed with an RMS window and a
|
|
56
|
+
fast-attack/slow-decay envelope so the pulse breathes instead of flickering.
|
|
57
|
+
On multi-monitor setups each pet follows the audio endpoint that matches its
|
|
58
|
+
own screen, falling back to the default output device.
|
|
59
|
+
|
|
60
|
+
Audio features are Windows-only and degrade to "no pulse" elsewhere.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
32
64
|
## Install
|
|
33
65
|
|
|
34
66
|
- **System Requirements**
|
|
@@ -12,6 +12,7 @@ from PySide6.QtWidgets import QMenu, QMessageBox, QWidget
|
|
|
12
12
|
from frontengine.show.base_widget import BaseWidget
|
|
13
13
|
from frontengine.show.window_helpers import apply_overlay_window_flags
|
|
14
14
|
from frontengine.user_setting.user_setting_file import user_setting_dict
|
|
15
|
+
from frontengine.utils.audio_meter.audio_envelope import AudioEnvelope
|
|
15
16
|
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
16
17
|
from frontengine.utils.multi_language.language_wrapper import language_wrapper
|
|
17
18
|
|
|
@@ -82,6 +83,65 @@ def scan_pet_pack(folder: str) -> dict:
|
|
|
82
83
|
return result
|
|
83
84
|
|
|
84
85
|
|
|
86
|
+
# 拖進寵物的檔案分類 / What a file dropped onto the pet means
|
|
87
|
+
DROP_SPRITE = "sprite" # 圖片或動作包 -> 換一套外觀
|
|
88
|
+
DROP_FOOD = "food" # 其他檔案 -> 當成食物餵給寵物
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def classify_drop(path) -> Optional[str]:
|
|
92
|
+
"""
|
|
93
|
+
判斷拖到寵物身上的檔案要換裝還是當食物;路徑無效或空資料夾回傳 None。
|
|
94
|
+
Classify a file dropped onto the pet: a sprite/pack to wear, food to eat,
|
|
95
|
+
or None when the path is unusable.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
target = Path(path)
|
|
99
|
+
if not target.exists():
|
|
100
|
+
return None
|
|
101
|
+
except (OSError, TypeError, ValueError):
|
|
102
|
+
return None
|
|
103
|
+
if target.is_dir():
|
|
104
|
+
return DROP_SPRITE if scan_pet_pack(str(target)) else None
|
|
105
|
+
return DROP_SPRITE if target.suffix.lower() in _PACK_EXTS else DROP_FOOD
|
|
106
|
+
|
|
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
|
+
|
|
85
145
|
def derive_visual_state(dragging: bool, airborne: bool, surface: str, motion_state: str) -> str:
|
|
86
146
|
"""依目前情形推導應顯示的視覺狀態 / Derive which sprite state to show."""
|
|
87
147
|
if dragging:
|
|
@@ -268,6 +328,92 @@ def nearest_peer(center, peers):
|
|
|
268
328
|
return best, best_distance
|
|
269
329
|
|
|
270
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
|
+
|
|
271
417
|
def message_bucket(hour: int) -> str:
|
|
272
418
|
"""依小時回傳時段 / Time-of-day bucket for the given hour."""
|
|
273
419
|
hour = int(hour) % 24
|
|
@@ -876,6 +1022,7 @@ class DesktopPetWidget(BaseWidget):
|
|
|
876
1022
|
message_box.show()
|
|
877
1023
|
|
|
878
1024
|
self.resize(self.pet_size, self.pet_size)
|
|
1025
|
+
self.setAcceptDrops(True) # drop an image to dress it up, anything else to feed it
|
|
879
1026
|
self.motion = PetMotion(
|
|
880
1027
|
0, 0, self.pet_size, self.pet_size, (0, 0, 1920, 1080), speed, behaviour, climb=climb
|
|
881
1028
|
)
|
|
@@ -901,10 +1048,15 @@ class DesktopPetWidget(BaseWidget):
|
|
|
901
1048
|
self._audio_react = bool(audio_react)
|
|
902
1049
|
self._audio_level_provider = None # callable -> level 0..1 or None
|
|
903
1050
|
self._audio_scale = 1.0
|
|
1051
|
+
# 峰值抖動大,先過 RMS + 快起慢降包絡再驅動脈動
|
|
1052
|
+
# Raw peaks jitter, so smooth them through an RMS + attack/decay envelope.
|
|
1053
|
+
self._audio_envelope = AudioEnvelope()
|
|
904
1054
|
# 多寵物互動 / Multi-pet interaction
|
|
905
1055
|
self._peers_provider = None
|
|
906
1056
|
self._peer_greeted = False
|
|
907
1057
|
self._peer_tick = 0
|
|
1058
|
+
# 鬼抓人角色(由外部的 PetTagGame 指派)/ Tag role assigned by a PetTagGame
|
|
1059
|
+
self._tag_role = None
|
|
908
1060
|
# 電量提醒 / Low-battery reaction
|
|
909
1061
|
self._battery_provider = read_battery
|
|
910
1062
|
self._battery_warned = False
|
|
@@ -957,8 +1109,31 @@ class DesktopPetWidget(BaseWidget):
|
|
|
957
1109
|
"""設定回傳其他寵物中心座標清單的函式 / Provider of other pets' centres."""
|
|
958
1110
|
self._peers_provider = provider
|
|
959
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
|
+
|
|
960
1133
|
def _check_peers(self) -> None:
|
|
961
1134
|
"""靠近其他寵物時走過去玩耍、抵達後轉向打招呼(帶遲滯,避免重複)。"""
|
|
1135
|
+
if self._tag_role is not None:
|
|
1136
|
+
return # the tag game owns where this pet is heading
|
|
962
1137
|
if self._peers_provider is None:
|
|
963
1138
|
return
|
|
964
1139
|
try:
|
|
@@ -992,19 +1167,50 @@ class DesktopPetWidget(BaseWidget):
|
|
|
992
1167
|
self._peer_greeted = False
|
|
993
1168
|
|
|
994
1169
|
def _load_sprite(self, path: str):
|
|
1170
|
+
"""載入一張精靈;檔案損毀或格式不支援時回傳 None。"""
|
|
995
1171
|
if Path(path).suffix.lower() in (".gif", ".webp"):
|
|
996
1172
|
movie = QMovie(str(path))
|
|
1173
|
+
if not movie.isValid():
|
|
1174
|
+
return None
|
|
997
1175
|
movie.frameChanged.connect(self.update)
|
|
998
1176
|
return ("movie", movie)
|
|
999
|
-
|
|
1177
|
+
pixmap = QPixmap(str(path))
|
|
1178
|
+
return None if pixmap.isNull() else ("pixmap", pixmap)
|
|
1000
1179
|
|
|
1001
1180
|
def _load_sprites(self, image_path: Path) -> None:
|
|
1002
|
-
"""單一檔 -> 所有狀態共用;資料夾 ->
|
|
1181
|
+
"""單一檔 -> 所有狀態共用;資料夾 -> 依檔名對應各狀態;載不起來的略過。"""
|
|
1003
1182
|
if image_path.is_dir():
|
|
1004
1183
|
for state, path in scan_pet_pack(str(image_path)).items():
|
|
1005
|
-
|
|
1184
|
+
sprite = self._load_sprite(path)
|
|
1185
|
+
if sprite is not None:
|
|
1186
|
+
self._sprites[state] = sprite
|
|
1006
1187
|
elif image_path.is_file():
|
|
1007
|
-
|
|
1188
|
+
sprite = self._load_sprite(str(image_path))
|
|
1189
|
+
if sprite is not None:
|
|
1190
|
+
self._sprites["default"] = sprite
|
|
1191
|
+
|
|
1192
|
+
def change_sprite(self, path) -> None:
|
|
1193
|
+
"""換一套外觀(拖圖片或動作包到寵物身上時觸發);載不到就維持原樣。"""
|
|
1194
|
+
image_path = Path(path)
|
|
1195
|
+
previous = self._sprites
|
|
1196
|
+
self._sprites = {}
|
|
1197
|
+
self._load_sprites(image_path)
|
|
1198
|
+
if not self._sprites:
|
|
1199
|
+
front_engine_logger.warning(f"[DesktopPetWidget] change_sprite ignored: {image_path}")
|
|
1200
|
+
self._sprites = previous
|
|
1201
|
+
return
|
|
1202
|
+
front_engine_logger.info(f"[DesktopPetWidget] change_sprite | path={image_path}")
|
|
1203
|
+
for kind, obj in previous.values():
|
|
1204
|
+
if kind == "movie":
|
|
1205
|
+
obj.stop()
|
|
1206
|
+
self.image_path = image_path
|
|
1207
|
+
self._active = None
|
|
1208
|
+
self._active_state = None
|
|
1209
|
+
self._set_active_sprite(self._visual_state())
|
|
1210
|
+
self._gain_affection(2)
|
|
1211
|
+
pool = self._messages.get("costume") or []
|
|
1212
|
+
if pool:
|
|
1213
|
+
self.say(pool[self._chatter_rng.randrange(len(pool))])
|
|
1008
1214
|
|
|
1009
1215
|
def _sprite_for(self, state: str):
|
|
1010
1216
|
if "default" in self._sprites:
|
|
@@ -1042,16 +1248,23 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1042
1248
|
def set_audio_level_provider(self, provider) -> None:
|
|
1043
1249
|
"""設定回傳目前音量 (0~1 或 None) 的函式 / Provider of the current audio level."""
|
|
1044
1250
|
self._audio_level_provider = provider
|
|
1251
|
+
self._audio_envelope.reset()
|
|
1045
1252
|
|
|
1046
1253
|
def _update_audio_scale(self) -> None:
|
|
1254
|
+
"""取一次音量取樣,經包絡平滑後換算成脈動比例(無音源則不脈動)。"""
|
|
1047
1255
|
if not self._audio_react or self._audio_level_provider is None:
|
|
1048
1256
|
self._audio_scale = 1.0
|
|
1257
|
+
self._audio_envelope.reset()
|
|
1049
1258
|
return
|
|
1050
1259
|
try:
|
|
1051
1260
|
level = self._audio_level_provider()
|
|
1052
1261
|
except Exception: # pragma: no cover - defensive
|
|
1053
1262
|
level = None
|
|
1054
|
-
|
|
1263
|
+
if level is None:
|
|
1264
|
+
self._audio_scale = 1.0
|
|
1265
|
+
self._audio_envelope.reset()
|
|
1266
|
+
return
|
|
1267
|
+
self._audio_scale = audio_pulse_scale(self._audio_envelope.push(level))
|
|
1055
1268
|
|
|
1056
1269
|
def draw_content(self, painter: QPainter) -> None:
|
|
1057
1270
|
pixmap = self._current_pixmap()
|
|
@@ -1099,8 +1312,14 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1099
1312
|
"battery": lines("pet_chatter_battery", "Battery's low - plug me in?|Low power!|Find a charger?"),
|
|
1100
1313
|
"hungry": lines("pet_chatter_hungry", "I'm hungry...|Got a snack?|Feed me?"),
|
|
1101
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!"),
|
|
1102
1319
|
"away": lines("pet_chatter_away", "Still there?|I'll nap till you're back~|Zzz..."),
|
|
1103
1320
|
"levelup": lines("pet_chatter_levelup", "Level up!|I'm growing!|We're getting closer!"),
|
|
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!"),
|
|
1104
1323
|
"any": lines("pet_chatter_any", "Hi there!|(^_^)|Keep going!"),
|
|
1105
1324
|
}
|
|
1106
1325
|
|
|
@@ -1267,14 +1486,19 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1267
1486
|
def _persist_hunger(self) -> None:
|
|
1268
1487
|
user_setting_dict["pet_hunger"] = self._hunger.value
|
|
1269
1488
|
|
|
1270
|
-
def feed(self) -> None:
|
|
1271
|
-
"""
|
|
1272
|
-
|
|
1273
|
-
self.
|
|
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)
|
|
1274
1498
|
self._persist_hunger()
|
|
1275
1499
|
self._persist_mood()
|
|
1276
|
-
self._gain_affection(
|
|
1277
|
-
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 []
|
|
1278
1502
|
if pool:
|
|
1279
1503
|
self.say(pool[self._chatter_rng.randrange(len(pool))])
|
|
1280
1504
|
|
|
@@ -1389,6 +1613,38 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1389
1613
|
self.say() # a click (no drag) makes the pet talk
|
|
1390
1614
|
super().mouseReleaseEvent(event)
|
|
1391
1615
|
|
|
1616
|
+
# --- drop a file onto the pet: wear it (image / pack) or eat it ---
|
|
1617
|
+
@staticmethod
|
|
1618
|
+
def dropped_path(mime_data) -> Optional[str]:
|
|
1619
|
+
"""從拖曳資料取出第一個可用的本機路徑;沒有可用者回傳 None。"""
|
|
1620
|
+
if mime_data is None or not mime_data.hasUrls():
|
|
1621
|
+
return None
|
|
1622
|
+
for url in mime_data.urls():
|
|
1623
|
+
local_path = url.toLocalFile()
|
|
1624
|
+
if local_path and classify_drop(local_path) is not None:
|
|
1625
|
+
return local_path
|
|
1626
|
+
return None
|
|
1627
|
+
|
|
1628
|
+
def dragEnterEvent(self, event) -> None:
|
|
1629
|
+
if self.dropped_path(event.mimeData()) is not None:
|
|
1630
|
+
event.acceptProposedAction()
|
|
1631
|
+
|
|
1632
|
+
def dragMoveEvent(self, event) -> None:
|
|
1633
|
+
if self.dropped_path(event.mimeData()) is not None:
|
|
1634
|
+
event.acceptProposedAction()
|
|
1635
|
+
|
|
1636
|
+
def dropEvent(self, event) -> None:
|
|
1637
|
+
path = self.dropped_path(event.mimeData())
|
|
1638
|
+
if path is None:
|
|
1639
|
+
return
|
|
1640
|
+
front_engine_logger.info(f"[DesktopPetWidget] dropEvent | path={path}")
|
|
1641
|
+
self.motion.wake()
|
|
1642
|
+
if classify_drop(path) == DROP_SPRITE:
|
|
1643
|
+
self.change_sprite(path)
|
|
1644
|
+
else:
|
|
1645
|
+
self.feed(classify_food(path))
|
|
1646
|
+
event.acceptProposedAction()
|
|
1647
|
+
|
|
1392
1648
|
def contextMenuEvent(self, event) -> None:
|
|
1393
1649
|
self.menu.popup(event.globalPos())
|
|
1394
1650
|
|
|
@@ -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 (
|
|
@@ -17,7 +18,7 @@ from frontengine.ui.page.utils import (
|
|
|
17
18
|
resolve_preferred_monitor,
|
|
18
19
|
)
|
|
19
20
|
from frontengine.user_setting.user_setting_file import add_recent_file
|
|
20
|
-
from frontengine.utils.audio_meter.
|
|
21
|
+
from frontengine.utils.audio_meter.screen_audio import audio_level_provider_for_screen
|
|
21
22
|
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
22
23
|
from frontengine.utils.multi_language.language_wrapper import language_wrapper
|
|
23
24
|
|
|
@@ -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(
|
|
@@ -93,6 +102,12 @@ class PetSettingUI(QWidget):
|
|
|
93
102
|
self.volume_combobox.setCurrentText("100%")
|
|
94
103
|
self.audio_react_checkbox = QCheckBox(
|
|
95
104
|
language_wrapper.language_word_dict.get("pet_audio_label", "React to audio"))
|
|
105
|
+
self.drop_hint_label = QLabel(
|
|
106
|
+
language_wrapper.language_word_dict.get(
|
|
107
|
+
"pet_drop_hint",
|
|
108
|
+
"Tip: drop an image or pet pack onto the pet to change its look, "
|
|
109
|
+
"or any other file to feed it."))
|
|
110
|
+
self.drop_hint_label.setWordWrap(True)
|
|
96
111
|
|
|
97
112
|
# Start
|
|
98
113
|
self.start_button = QPushButton(language_wrapper.language_word_dict.get("pet_start", "Spawn pet"))
|
|
@@ -128,6 +143,8 @@ class PetSettingUI(QWidget):
|
|
|
128
143
|
self.grid_layout.addWidget(self.volume_label, 6, 2)
|
|
129
144
|
self.grid_layout.addWidget(self.volume_combobox, 7, 2)
|
|
130
145
|
self.grid_layout.addWidget(self.audio_react_checkbox, 7, 0)
|
|
146
|
+
self.grid_layout.addWidget(self.tag_checkbox, 7, 1)
|
|
147
|
+
self.grid_layout.addWidget(self.drop_hint_label, 8, 0, 1, 3)
|
|
131
148
|
|
|
132
149
|
def _spawn_pet(self) -> None:
|
|
133
150
|
"""建立、顯示並開始移動一隻寵物(供 Start 與右鍵複製共用)。"""
|
|
@@ -147,12 +164,16 @@ class PetSettingUI(QWidget):
|
|
|
147
164
|
)
|
|
148
165
|
pet.clone_requested.connect(self._spawn_pet)
|
|
149
166
|
pet.set_peers_provider(lambda me=pet: self._peer_centers(me))
|
|
167
|
+
geometry = self._target_geometry()
|
|
150
168
|
if self.audio_react_checkbox.isChecked():
|
|
151
|
-
|
|
169
|
+
# 跟隨寵物所在螢幕的音源(比對不到就用系統預設輸出裝置)
|
|
170
|
+
# Follow the audio of the screen the pet lives on; default endpoint otherwise.
|
|
171
|
+
pet.set_audio_level_provider(
|
|
172
|
+
audio_level_provider_for_screen(geometry[1] if geometry is not None else None))
|
|
152
173
|
pet.set_pet_window_flag()
|
|
153
174
|
self.pet_list.append(pet)
|
|
154
175
|
pet.show()
|
|
155
|
-
|
|
176
|
+
self._start_tag_game()
|
|
156
177
|
if geometry is not None:
|
|
157
178
|
pet.setScreen(geometry[1])
|
|
158
179
|
bounds = geometry[0]
|
|
@@ -170,17 +191,59 @@ class PetSettingUI(QWidget):
|
|
|
170
191
|
return None
|
|
171
192
|
return (screen.availableGeometry(), screen)
|
|
172
193
|
|
|
173
|
-
def
|
|
174
|
-
"""
|
|
175
|
-
|
|
176
|
-
for
|
|
177
|
-
if other is me:
|
|
178
|
-
continue
|
|
194
|
+
def _alive_pets(self) -> list:
|
|
195
|
+
"""回傳仍存活的寵物,並把已被銷毀者從清單移除。"""
|
|
196
|
+
alive = []
|
|
197
|
+
for pet in list(self.pet_list):
|
|
179
198
|
try:
|
|
180
|
-
|
|
199
|
+
pet.center()
|
|
181
200
|
except RuntimeError:
|
|
182
|
-
|
|
183
|
-
|
|
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()
|
|
184
247
|
|
|
185
248
|
def start_play_pet(self) -> None:
|
|
186
249
|
front_engine_logger.info("[PetSettingUI] start_play_pet")
|
|
@@ -261,6 +324,7 @@ class PetSettingUI(QWidget):
|
|
|
261
324
|
"target_monitor": self.target_monitor_combobox.currentText(),
|
|
262
325
|
"volume": self.volume_combobox.currentText(),
|
|
263
326
|
"audio_react": self.audio_react_checkbox.isChecked(),
|
|
327
|
+
"tag": self.tag_checkbox.isChecked(),
|
|
264
328
|
}
|
|
265
329
|
|
|
266
330
|
def set_state(self, state: dict) -> None:
|
|
@@ -298,3 +362,5 @@ class PetSettingUI(QWidget):
|
|
|
298
362
|
self.volume_combobox.setCurrentIndex(index)
|
|
299
363
|
if "audio_react" in state:
|
|
300
364
|
self.audio_react_checkbox.setChecked(bool(state["audio_react"]))
|
|
365
|
+
if "tag" in state:
|
|
366
|
+
self.tag_checkbox.setChecked(bool(state["tag"]))
|