python-library-ff14-the-hunt 0.0.0__tar.gz → 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. python_library_ff14_the_hunt-0.0.2/.cursor/skills/ff14_the_hunt-package-changelog/SKILL.md +39 -0
  2. python_library_ff14_the_hunt-0.0.2/.cursor/skills/ff14_the_hunt-package-design/SKILL.md +150 -0
  3. {python_library_ff14_the_hunt-0.0.0 → python_library_ff14_the_hunt-0.0.2}/PKG-INFO +1 -1
  4. python_library_ff14_the_hunt-0.0.2/README.md +93 -0
  5. python_library_ff14_the_hunt-0.0.2/example/__main__.py +99 -0
  6. python_library_ff14_the_hunt-0.0.2/example/spawn_map_io.py +35 -0
  7. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/__init__.py +62 -0
  8. {python_library_ff14_the_hunt-0.0.0 → python_library_ff14_the_hunt-0.0.2}/ff14_the_hunt/bear_tracker/client.py +8 -0
  9. {python_library_ff14_the_hunt-0.0.0 → python_library_ff14_the_hunt-0.0.2}/ff14_the_hunt/bear_tracker/enrich.py +14 -12
  10. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/bear_tracker/fate_timer.py +65 -0
  11. {python_library_ff14_the_hunt-0.0.0 → python_library_ff14_the_hunt-0.0.2}/ff14_the_hunt/bear_tracker/spawn_window.py +19 -7
  12. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/bear_tracker/timer_theme.py +44 -0
  13. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/ff14_the_hunt.py +365 -0
  14. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/locale/__init__.py +17 -0
  15. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/locale/cn.py +193 -0
  16. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/locale/data/hunts_zh.json +221 -0
  17. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/locale/data/regions_zh.json +49 -0
  18. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/locale/detect.py +35 -0
  19. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/locale/display.py +66 -0
  20. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/locale/en.py +179 -0
  21. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/locale/names.py +43 -0
  22. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/locale/spawn_map_display.py +72 -0
  23. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/locale/tag.py +10 -0
  24. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/models.py +197 -0
  25. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/poll/__init__.py +18 -0
  26. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/poll/loop.py +18 -0
  27. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/poll/monitor_mode.py +65 -0
  28. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/poll/scheduler.py +354 -0
  29. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/poll/sleep_plan.py +88 -0
  30. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/poll/sleep_settings.py +23 -0
  31. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/poll/spawn_dedup.py +29 -0
  32. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/poll/window_remaining.py +57 -0
  33. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/spawn_map/__init__.py +23 -0
  34. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/spawn_map/attach.py +96 -0
  35. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/spawn_map/coordinates.py +25 -0
  36. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/spawn_map/layout.py +22 -0
  37. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/spawn_map/png_meta.py +24 -0
  38. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/spawn_map/points.py +18 -0
  39. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/spawn_map/region_fetch.py +76 -0
  40. python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/spawn_map/region_image.py +30 -0
  41. {python_library_ff14_the_hunt-0.0.0 → python_library_ff14_the_hunt-0.0.2}/pyproject.toml +4 -1
  42. python_library_ff14_the_hunt-0.0.2/tests/__init__.py +0 -0
  43. python_library_ff14_the_hunt-0.0.2/tests/test_fate_timer.py +29 -0
  44. python_library_ff14_the_hunt-0.0.2/tests/test_ff14_the_hunt_service.py +109 -0
  45. python_library_ff14_the_hunt-0.0.2/tests/test_locale_cn.py +90 -0
  46. python_library_ff14_the_hunt-0.0.2/tests/test_monitor_mode.py +94 -0
  47. python_library_ff14_the_hunt-0.0.2/tests/test_poll_scheduler.py +51 -0
  48. python_library_ff14_the_hunt-0.0.2/tests/test_poll_sleep.py +139 -0
  49. python_library_ff14_the_hunt-0.0.2/tests/test_spawn_dedup.py +55 -0
  50. python_library_ff14_the_hunt-0.0.2/tests/test_spawn_map_data.py +62 -0
  51. python_library_ff14_the_hunt-0.0.2/tests/test_spawn_map_points.py +21 -0
  52. python_library_ff14_the_hunt-0.0.2/tests/test_spawn_window.py +48 -0
  53. python_library_ff14_the_hunt-0.0.2/tests/test_untimed_marks_filter.py +117 -0
  54. python_library_ff14_the_hunt-0.0.2/tools/build_locale_data.py +172 -0
  55. python_library_ff14_the_hunt-0.0.0/.cursor/skills/ff14_the_hunt-package-changelog/SKILL.md +0 -13
  56. python_library_ff14_the_hunt-0.0.0/.cursor/skills/ff14_the_hunt-package-design/SKILL.md +0 -22
  57. python_library_ff14_the_hunt-0.0.0/README.md +0 -50
  58. python_library_ff14_the_hunt-0.0.0/example/__main__.py +0 -69
  59. python_library_ff14_the_hunt-0.0.0/ff14_the_hunt/__init__.py +0 -19
  60. python_library_ff14_the_hunt-0.0.0/ff14_the_hunt/bear_tracker/fate_timer.py +0 -41
  61. python_library_ff14_the_hunt-0.0.0/ff14_the_hunt/ff14_the_hunt.py +0 -126
  62. python_library_ff14_the_hunt-0.0.0/ff14_the_hunt/models.py +0 -104
  63. python_library_ff14_the_hunt-0.0.0/tests/test_spawn_window.py +0 -16
  64. {python_library_ff14_the_hunt-0.0.0 → python_library_ff14_the_hunt-0.0.2}/.cursor/skills/ff14_the_hunt-package-preload/SKILL.md +0 -0
  65. {python_library_ff14_the_hunt-0.0.0 → python_library_ff14_the_hunt-0.0.2}/.gitignore +0 -0
  66. {python_library_ff14_the_hunt-0.0.0 → python_library_ff14_the_hunt-0.0.2}/example.bat +0 -0
  67. {python_library_ff14_the_hunt-0.0.0 → python_library_ff14_the_hunt-0.0.2}/ff14_the_hunt/bear_tracker/__init__.py +0 -0
  68. {python_library_ff14_the_hunt-0.0.0 → python_library_ff14_the_hunt-0.0.2}/ff14_the_hunt/bear_tracker/resources.py +0 -0
  69. {python_library_ff14_the_hunt-0.0.0 → python_library_ff14_the_hunt-0.0.2}/ff14_the_hunt/bear_tracker/spawn_points.py +0 -0
  70. {python_library_ff14_the_hunt-0.0.0/tests → python_library_ff14_the_hunt-0.0.2/ff14_the_hunt/locale/data}/__init__.py +0 -0
  71. {python_library_ff14_the_hunt-0.0.0 → python_library_ff14_the_hunt-0.0.2}/test.bat +0 -0
  72. {python_library_ff14_the_hunt-0.0.0 → python_library_ff14_the_hunt-0.0.2}/update.bat +0 -0
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: ff14-the-hunt-package-changelog
3
+ description: ff14_the_hunt 包:要求与决议;最新在上。
4
+ ---
5
+
6
+ # ff14_the_hunt · Changelog
7
+
8
+ (规则见 `~/.cursor/skills/agent-project-changelog/SKILL.md`。)
9
+
10
+ ## 2026-06-08
11
+
12
+ - **决议**:发布 ``0.0.2``:包级导出 ``HuntCrawlPacket``、locale、poll 等;与 ff14hub 首版所需 API 对齐。
13
+
14
+ ## 2026-06-07
15
+
16
+ - **决议**:发布 ``0.0.1``,与 PyPI 及 ``ff14-agent`` 依赖下限对齐。
17
+
18
+ - **决议**:移除包内地图标点/裁剪与 Pillow 依赖。刚刷新记录输出 ``spawn_map_layout``、含 ``pixel_x/y`` 的 ``spawn_points``、站点 ``region_map`` 原图 base64;外部自行叠加标点。
19
+
20
+ - **决议**:仅 ``recently_spawned`` 记录填充刷点与原图;有 ``active`` 存活点时只保留存活点。``include_spawn_maps`` 默认 True(拉取区域原图)。
21
+
22
+ - **决议**:轮询改为三模式状态机(``PollMonitorMode``):全 ``ALMOST_OPEN`` 时睡到最近开窗(``WAIT_UNTIL_OPEN``),不再叠 30 分钟固定间隔;存在开窗/强制期/刚刷新/FATE 进行中时用短间隔(``ACTIVE``);无触发计时用 ``fallback_poll_interval_seconds``(``FALLBACK``)。实现分层于 ``poll/monitor_mode``、``window_remaining``、``sleep_settings``、``sleep_plan``。门面新增 ``active_poll_interval_seconds``(默认 600)、``recent_poll_interval_seconds``(默认 300)、``fallback_poll_interval_seconds``(默认 1800);``poll_interval_seconds`` 废弃为 fallback 别名。
23
+
24
+ - **决议**:``HuntQueryFilter.include_untimed_marks`` / 构造参数 ``include_untimed_marks`` 默认 ``False``,过滤无触发/条件/FATE 计时的占位行(SS 级噬灵王、维护占位等);设为 ``True`` 时恢复返回 Bear Tracker 全部计时行。
25
+
26
+ - **决议**:``crawl_packet_to_display_dict`` 默认按数据中心区域选择 ``HuntDisplayLocale``(中国区输出中文狩猎名与地图区域,国际区输出英文);名称对照表打包在 ``locale/data/*.json``,可用 ``tools/build_locale_data.py`` 从 ffxiv-datamining-mixed 重建。
27
+
28
+ - **决议**:``locale.cn`` 提供 ``crawl_packet_to_display_dict`` 等中文导出;``patches`` 构造参数接受中文资料片名(如金曦之遗辉);example 演示猫小胖 / 静语庄园 / 金曦之遗辉 / S 级并输出该条件下全部怪物。
29
+
30
+ - **决议**:``TimerDisplay`` 增加 ``bar_color`` / ``hex_color`` / ``counts_up`` / ``kind``,色值对齐 Bear Tracker 前端 MUI 主题;FATE 链写入 ``fate_timer``(不再占用 ``condition_timer``);条件时间(天气/月相)仍待移植。
31
+
32
+ - **决议**:`FF14TheHunt` 构造时以平铺关键字参数配置筛选与轮询(非传入 `HuntQueryFilter`);对外提供 `crawl_once`、`start` / `run` / `stop`、`on_crawl`(实例可作装饰器);回调入参为 `HuntCrawlPacket`。`HuntPollScheduler` 仍保留供底层调度使用。
33
+
34
+ - **决议**:新增 `HuntPollScheduler` 与 `compute_poll_sleep_seconds`:固定爬取间隔默认 30 分钟;按上次结果中最近「距离开窗」触发计时提前唤醒;每次爬取后重置等待;触发唤醒下限默认 2 分钟。
35
+
36
+ ## 2026-06-04
37
+
38
+ - **决议**:包目录由 **`ff14_world_boss`** 重命名为 **`ff14_the_hunt`**;PyPI 名 **`python-library-ff14-the-hunt`**;包级 skill 同步为 **`ff14_the_hunt-package-*`**。不考虑旧名兼容。
39
+ - **决议**:在 monorepo 初始化空包(初名 **ff14_world_boss**),并建立包级三件套。
@@ -0,0 +1,150 @@
1
+ ---
2
+
3
+ name: ff14-the-hunt-package-design
4
+
5
+ description: >-
6
+
7
+ ff14_the_hunt 包:当前有效的设计意图;变更见 ff14_the_hunt-package-changelog。
8
+
9
+ ---
10
+
11
+
12
+
13
+ # ff14_the_hunt · 设计笔记
14
+
15
+
16
+
17
+ > 变更见 `.cursor/skills/ff14_the_hunt-package-changelog/SKILL.md`;矛盾以 changelog 最新为准。
18
+
19
+
20
+
21
+ ## 设计意图
22
+
23
+
24
+
25
+ - **FF14 狩猎(The Hunt)**:从 Bear Tracker 拉取狩猎计时、触发窗与刷点,供 Agent 或本地工具轮询与提醒。
26
+
27
+
28
+
29
+ ## 硬性要求
30
+
31
+
32
+
33
+ - 包目录名 **`ff14_the_hunt`**(snake_case);PyPI 名 **`python-library-ff14-the-hunt`**。
34
+
35
+ - 中文禁用词见 `~/.cursor/skills/forbidden-doc-comment-vocabulary/SKILL.md`。
36
+
37
+ - 对外门面 **`FF14TheHunt`**:构造时以平铺参数配置筛选与轮询;``crawl_once`` 手动爬取;``start`` / ``run`` / ``stop`` 自动服务(``run`` 支持 Ctrl+C);``on_crawl`` / 实例装饰器登记回调,入参 **`HuntCrawlPacket`**。
38
+
39
+ - 计时展示与 Bear Tracker 页面同色:``TimerDisplay.bar_color`` + ``hex_color`` + ``counts_up``;``trigger_timer`` / ``condition_timer`` / ``fate_timer`` 分列。
40
+
41
+
42
+
43
+ ## 自动轮询节奏
44
+
45
+
46
+
47
+ 实现拆在 ``ff14_the_hunt/poll/``:
48
+
49
+
50
+
51
+ | 模块 | 职责 |
52
+
53
+ | --- | --- |
54
+
55
+ | ``monitor_mode.py`` | 判定 ``PollMonitorMode``(活跃 / 睡到开窗 / 兜底) |
56
+
57
+ | ``window_remaining.py`` | 最短「距离开窗」剩余秒数 |
58
+
59
+ | ``sleep_settings.py`` | ``PollSleepSettings`` 间隔与下限 |
60
+
61
+ | ``sleep_plan.py`` | ``compute_poll_sleep_seconds`` |
62
+
63
+ | ``scheduler.py`` | ``HuntPollScheduler`` 爬取循环 |
64
+
65
+
66
+
67
+ 每次 ``fetch`` 后根据**上次** ``marks`` 选模式,再算下次休眠。
68
+
69
+
70
+
71
+ ### 模式判定
72
+
73
+
74
+
75
+ 1. **``ACTIVE``(活跃轮询)** — 下列任一成立:
76
+
77
+ - ``trigger_timer.phase`` 为 ``OPEN``(开窗中)或 ``CAPPED``(强制期)
78
+
79
+ - ``recently_spawned`` 为 True(含刚标记)
80
+
81
+ - ``fate_timer.phase`` 为 ``OPEN``(FATE 进行中)
82
+
83
+
84
+
85
+ 2. **``WAIT_UNTIL_OPEN``(睡到开窗)** — 无活跃项,且至少一条 ``trigger_timer.phase`` 为 ``ALMOST_OPEN``(即将开窗)。
86
+
87
+ 此模式下按触发机制尚不能刷新,休眠取 ``min(各只 remaining_seconds)``(扣掉距上次爬取已过时间),**不再**叠 ``fallback`` 固定间隔。
88
+
89
+ 距开窗不足 ``min_wakeup_seconds`` 时钳到该下限。
90
+
91
+
92
+
93
+ 3. **``FALLBACK``(兜底)** — 无触发计时(占位行等);按 ``fallback_poll_interval_seconds`` 固定间隔。
94
+
95
+
96
+
97
+ ### 休眠秒数
98
+
99
+
100
+
101
+ | 模式 | 计算 |
102
+
103
+ | --- | --- |
104
+
105
+ | ``ACTIVE`` | 距上次爬取已过时间后,``recent_poll_interval_seconds``(有刚刷新)或 ``active_poll_interval_seconds`` |
106
+
107
+ | ``WAIT_UNTIL_OPEN`` | ``max(min_wakeup_seconds, 最近开窗剩余)`` |
108
+
109
+ | ``FALLBACK`` | ``fallback_poll_interval_seconds`` 减去已过时间 |
110
+
111
+
112
+
113
+ 默认值:活跃 600s、刚刷新 300s、兜底 1800s、开窗下限 120s。门面构造参数与 ``PollSleepSettings`` 一致。
114
+
115
+
116
+
117
+ ### 边界备忘
118
+
119
+
120
+
121
+ - **强制期**归入 ``ACTIVE``,不可与「全未开窗」合并长睡。
122
+
123
+ - 长睡期间站上他人更新死亡/标记时间,本地 ``marks`` 会滞后,到点唤醒后才纠正。
124
+
125
+ - 天气/月相 ``condition_timer`` 未移植;带条件老式 S 不能单靠开窗剩余覆盖。
126
+
127
+ - ``poll_interval_seconds`` 构造参数已废弃,等同 ``fallback_poll_interval_seconds``。
128
+
129
+ ## 刷点与区域原图
130
+
131
+ 实现于 ``ff14_the_hunt/spawn_map/``;**不在包内标点或裁剪**,交给外部。
132
+
133
+ | 模块 | 职责 |
134
+ | --- | --- |
135
+ | ``points.py`` | 刚刷新刷点筛选 |
136
+ | ``layout.py`` | ``SpawnMapLayout``(格点尺度、展示点数等) |
137
+ | ``coordinates.py`` | ``pixel_x/y = norm * 原图宽高`` |
138
+ | ``png_meta.py`` | 无第三方库读 PNG 尺寸 |
139
+ | ``region_fetch.py`` / ``region_image.py`` | 拉取站点 ``HuntRegions`` 原图 |
140
+ | ``attach.py`` | 补全 ``spawn_points`` / ``spawn_map_layout`` / ``region_map`` |
141
+
142
+ - **非刚刷新**:上述字段均为空。
143
+ - **刚刷新**:``querySpawnPoints`` 取存活点;``include_spawn_maps``(默认 True)拉取 ``RegionMapImage``。
144
+ - 外部叠加标点:用 ``地图布局`` 公式 + ``刷点`` 的 ``像素X/Y`` 或 ``地图X/Y``。
145
+
146
+ ## 备忘与待定
147
+
148
+ - 提醒方式、上层 Agent 集成待定。
149
+ - 长睡期间可选低频校验爬取(防死亡时间被他人更新)尚未实现。
150
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-library-ff14-the-hunt
3
- Version: 0.0.0
3
+ Version: 0.0.2
4
4
  Requires-Python: >=3.10
5
5
  Requires-Dist: pydantic>=2.0
6
6
  Provides-Extra: dev
@@ -0,0 +1,93 @@
1
+ # ff14_the_hunt
2
+
3
+ 从 [Bear Tracker](https://tracker.beartoolkit.com/timer) 拉取狩猎计时,解析触发窗/条件窗,并判断「刚刷新」。
4
+
5
+ ## 用法
6
+
7
+ 构造时直接传入数据中心、世界、Rank 等参数;之后可手动爬取或启动自动轮询。
8
+
9
+ ```python
10
+ from ff14_the_hunt import FF14TheHunt, HuntRankKind
11
+
12
+ hunt = FF14TheHunt(
13
+ data_centers=["猫小胖"],
14
+ worlds=["静语庄园"],
15
+ rank_kinds=[HuntRankKind.S, HuntRankKind.A],
16
+ patches=["金曦之遗辉"],
17
+ )
18
+
19
+ packet = hunt.crawl_once()
20
+ recent = hunt.recently_spawned()
21
+ ```
22
+
23
+ 临时换筛选条件时仍可用 ``HuntQueryFilter`` 传给 ``query_marks`` / ``recently_spawned``。
24
+
25
+ 默认 **不** 返回无触发/条件/FATE 计时的占位行(如 SS 级噬灵王、维护占位);与 Bear Tracker 主列表一致。需要完整 API 原始行时设 ``include_untimed_marks=True``(构造参数或 ``HuntQueryFilter`` 字段均可)。
26
+
27
+ ## 自动轮询与回调
28
+
29
+ 轮询按上次结果分三种模式(见包内设计笔记 ``ff14_the_hunt-package-design``):
30
+
31
+ - **全未开窗**:睡到筛选内最短「距离开窗」再爬(下限默认 2 分钟),此期间不叠固定间隔。
32
+ - **已有开窗/强制期/刚刷新/FATE 进行中**:短间隔轮询(默认 10 分钟;有刚刷新条目时默认 5 分钟)。
33
+ - **无触发计时**:退回固定间隔(默认 30 分钟)。
34
+
35
+ 构造参数 ``active_poll_interval_seconds``、``recent_poll_interval_seconds``、``fallback_poll_interval_seconds``、``min_wakeup_seconds`` 可配。``poll_interval_seconds`` 已废弃,等同 ``fallback_poll_interval_seconds``。
36
+
37
+ ```python
38
+ @hunt.on_crawl
39
+ def on_hunt(packet):
40
+ print(len(packet.marks), "条", packet.crawled_at)
41
+
42
+ # 后台线程
43
+ hunt.start()
44
+ # ...
45
+ hunt.stop()
46
+
47
+ # 或阻塞到 Ctrl+C
48
+ hunt.run()
49
+ ```
50
+
51
+ 也可用实例作装饰器:`@hunt`。
52
+
53
+ 单次爬取结果在 ``HuntCrawlPacket`` 里,含 ``crawled_at``、``marks``、``query``。
54
+
55
+ ## 示例
56
+
57
+ ```bat
58
+ example.bat
59
+ ```
60
+
61
+ 或 `python -m example`。输出写入 `example/output/`(已列入 `.gitignore`),JSON 使用 ``crawl_packet_to_display_dict`` 生成;语言默认跟随所选数据中心(中国区为中文狩猎名与地图区域,国际区为英文)。
62
+
63
+ ## API 说明
64
+
65
+ | 站点接口 | 用途 |
66
+ | --- | --- |
67
+ | `POST /api/syncSession` | 狩猎库、刷点坐标、数据中心/世界列表 |
68
+ | `POST /api/lastDeathTimers` | 计时行;`RankType` 为 `aRank` / `sRank` / `fate` |
69
+ | `POST /api/querySpawnPoints` | 刚刷新记录的各刷点存活状态(需 `LastDeath`) |
70
+
71
+ 刚刷新记录才填充 ``spawn_points``、``spawn_map_layout`` 与 ``region_map``。包内不标点、不裁剪;``region_map`` 为站点 ``HuntRegions`` 原图 base64。刷点含 ``地图X/Y``、``格点X/Y``、``像素X/Y``(按原图尺寸算好);``地图布局`` 含格点尺度与归一化公式。默认 ``include_spawn_maps=True`` 拉取原图。示例 ``python -m example`` 将 PNG 写入 ``example/output/maps/``,JSON 用 ``区域地图.地图文件`` 引用。
72
+
73
+ `QueryDeathTimers` 为世界名列表(由所选数据中心展开)。
74
+
75
+ 构造参数 ``patches`` 可写 Bear Tracker 缩写(如 ``DT``)或中文资料片名(如 ``金曦之遗辉``);内部统一为 `Patch` 字段。``crawl_packet_to_display_dict`` 可按 ``locale`` 或 ``detect_display_locale`` 推断的语言导出;也可显式传入 ``HuntDisplayLocale.ZH`` / ``EN``。
76
+
77
+ 触发时间窗算法与站点前端主计时列一致;带 `fateLastSeen` / `fateLastDeath` 的 7.0 S 链写入 ``fate_timer``。部分老式 S(如 Laideronnette)的天气条件窗尚未移植。
78
+
79
+ 每条 ``TimerDisplay`` 含 ``bar_color``(站点 MUI 色键)与 ``hex_color``(Bear Tracker 主题主色),便于自建界面与网页同色展示:
80
+
81
+ | 表头 | 站点说明 | bar_color | hex |
82
+ | --- | --- | --- | --- |
83
+ | 触发时间 | 红:不可触发期(倒计时) | error | #e31a1a |
84
+ | 触发时间 | 绿:可触发期(正计时,% 为进度) | success | #01b574 |
85
+ | 触发时间 | 蓝:强制期(正计时) | info | #0075ff |
86
+ | 条件时间 | 红:未满足条件(倒计时) | error / warning | #e31a1a / #ffb547 |
87
+ | 条件时间 | 绿:已满足条件(倒计时) | success | #01b574 |
88
+ | FATE | 绿:距最近发现 | success | #01b574 |
89
+ | FATE | 红:距上次死亡 | error | #e31a1a |
90
+
91
+ ``counts_up`` 标明正计时或倒计时。条件时间行待天气/月相逻辑移植后填充 ``condition_timer``。
92
+
93
+ 「刚刷新」:触发窗开启后默认 15 分钟内,或 `lastMarkTime` 在默认 15 分钟内。
@@ -0,0 +1,99 @@
1
+ """示例:中国服务器 · 猫小胖 · 静语庄园 · 金曦之遗辉 · S 狩猎(全部怪物)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from .spawn_map_io import write_region_map_files
9
+
10
+ from ff14_the_hunt import (
11
+ FF14TheHunt,
12
+ HuntRankKind,
13
+ crawl_packet_to_display_dict,
14
+ detect_display_locale,
15
+ )
16
+ from ff14_the_hunt.locale.names import translate_hunt_name, translate_region
17
+
18
+ DC_MOOGLE = "\u732b\u5c0f\u80d6"
19
+ WORLD_MANOR = "\u9759\u8bed\u5e84\u56ed"
20
+ PATCH_DAWNTRAIL_CN = "\u91d1\u66e6\u4e4b\u9057\u8f89"
21
+
22
+
23
+ def main() -> None:
24
+ hunt = FF14TheHunt(
25
+ data_centers=[DC_MOOGLE],
26
+ worlds=[WORLD_MANOR],
27
+ rank_kinds=[HuntRankKind.S],
28
+ patches=[PATCH_DAWNTRAIL_CN],
29
+ include_spawn_maps=True,
30
+ )
31
+ print("中国区数据中心:", hunt.list_data_centers(region="CN"))
32
+
33
+ packet = hunt.crawl_once()
34
+ marks = packet.marks
35
+ recent = hunt.recently_spawned()
36
+
37
+ out_dir = Path(__file__).resolve().parent / "output"
38
+ out_dir.mkdir(parents=True, exist_ok=True)
39
+
40
+ region_map_files = write_region_map_files(marks, out_dir)
41
+
42
+ locale = detect_display_locale(packet.query)
43
+ payload = crawl_packet_to_display_dict(
44
+ packet,
45
+ recently_spawned=recent,
46
+ locale=locale,
47
+ resources=hunt.ensure_resources(),
48
+ embed_region_map_data=False,
49
+ region_map_file_names=region_map_files,
50
+ )
51
+ out_path = out_dir / "bear_moogle_manor_dt_s.json"
52
+ out_path.write_text(
53
+ json.dumps(payload, ensure_ascii=False, indent=2),
54
+ encoding="utf-8",
55
+ )
56
+
57
+ print(f"写入 {out_path}")
58
+ if region_map_files:
59
+ print(f"区域原图 {len(region_map_files)} 张 -> {out_dir / 'maps'}")
60
+ print(f"配置: {payload['配置']}")
61
+ print(f"共 {len(marks)} 条怪物")
62
+ for mark in marks:
63
+ print("---")
64
+ print(
65
+ translate_hunt_name(mark.hunt_key, locale),
66
+ mark.world_name,
67
+ mark.patch,
68
+ translate_region(mark.region, locale),
69
+ )
70
+ if mark.trigger_timer:
71
+ t = mark.trigger_timer
72
+ print(f" 触发 [{t.bar_color.value} {t.hex_color}]:", t.summary)
73
+ if mark.condition_timer:
74
+ t = mark.condition_timer
75
+ print(f" 条件 [{t.bar_color.value} {t.hex_color}]:", t.summary)
76
+ if mark.fate_timer:
77
+ t = mark.fate_timer
78
+ print(f" FATE [{t.bar_color.value} {t.hex_color}]:", t.summary)
79
+ if mark.spawn_points:
80
+ pt = mark.spawn_points[0]
81
+ print(
82
+ f" 刷点: {pt.point_key} "
83
+ f"grid=({pt.grid_x}, {pt.grid_y}) "
84
+ f"norm=({pt.x:.3f}, {pt.y:.3f}) "
85
+ f"px=({pt.pixel_x}, {pt.pixel_y})"
86
+ )
87
+ if mark.region_map is not None:
88
+ rel = region_map_files.get((mark.hunt_key, mark.world_name), "")
89
+ print(
90
+ f" 区域原图: {rel} "
91
+ f"({mark.region_map.width}x{mark.region_map.height})"
92
+ )
93
+ print(" 刚刷新:", mark.recently_spawned)
94
+
95
+ print(f"同条件下刚刷新 {len(recent)} 条")
96
+
97
+
98
+ if __name__ == "__main__":
99
+ main()
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import re
5
+ from pathlib import Path
6
+
7
+ from ff14_the_hunt.models import HuntMarkRecord
8
+
9
+
10
+ def _safe_map_basename(hunt_key: str, world_name: str) -> str:
11
+ raw = f"{hunt_key}_{world_name}"
12
+ cleaned = re.sub(r"[^\w.\-]+", "_", raw, flags=re.UNICODE)
13
+ return cleaned.strip("_") or "region_map"
14
+
15
+
16
+ def write_region_map_files(
17
+ marks: list[HuntMarkRecord],
18
+ out_dir: Path,
19
+ ) -> dict[tuple[str, str], str]:
20
+ """将 ``region_map`` 原图解码为 PNG 文件。
21
+
22
+ Returns:
23
+ ``(hunt_key, world_name) -> 相对 out_dir 的路径``。
24
+ """
25
+ map_dir = out_dir / "maps"
26
+ map_dir.mkdir(parents=True, exist_ok=True)
27
+ names: dict[tuple[str, str], str] = {}
28
+ for mark in marks:
29
+ if mark.region_map is None:
30
+ continue
31
+ file_name = f"{_safe_map_basename(mark.hunt_key, mark.world_name)}.png"
32
+ path = map_dir / file_name
33
+ path.write_bytes(base64.b64decode(mark.region_map.data_base64))
34
+ names[(mark.hunt_key, mark.world_name)] = f"maps/{file_name}"
35
+ return names
@@ -0,0 +1,62 @@
1
+ from ff14_the_hunt.ff14_the_hunt import FF14TheHunt
2
+ from ff14_the_hunt.bear_tracker.timer_theme import BEAR_TRACKER_BAR_HEX, bar_hex
3
+ from ff14_the_hunt.models import (
4
+ HuntCrawlPacket,
5
+ HuntMarkRecord,
6
+ HuntQueryFilter,
7
+ HuntRankKind,
8
+ MapCoordinate,
9
+ RegionMapImage,
10
+ SpawnMapLayout,
11
+ SpawnWindowPhase,
12
+ TimerBarColor,
13
+ TimerDisplay,
14
+ TimerKind,
15
+ )
16
+ from ff14_the_hunt.locale import (
17
+ HuntDisplayLocale,
18
+ crawl_packet_to_display_dict,
19
+ detect_display_locale,
20
+ mark_to_display_dict,
21
+ normalize_patch_codes,
22
+ query_to_display_dict,
23
+ )
24
+ from ff14_the_hunt.poll import (
25
+ HuntPollScheduler,
26
+ PollMonitorMode,
27
+ PollSleepSettings,
28
+ compute_poll_sleep_seconds,
29
+ nearest_trigger_remaining_seconds,
30
+ nearest_window_open_remaining_seconds,
31
+ resolve_poll_monitor_mode,
32
+ )
33
+
34
+ __all__ = [
35
+ "BEAR_TRACKER_BAR_HEX",
36
+ "FF14TheHunt",
37
+ "HuntDisplayLocale",
38
+ "HuntCrawlPacket",
39
+ "HuntMarkRecord",
40
+ "HuntQueryFilter",
41
+ "HuntPollScheduler",
42
+ "HuntRankKind",
43
+ "PollMonitorMode",
44
+ "PollSleepSettings",
45
+ "MapCoordinate",
46
+ "RegionMapImage",
47
+ "SpawnMapLayout",
48
+ "SpawnWindowPhase",
49
+ "TimerBarColor",
50
+ "TimerDisplay",
51
+ "TimerKind",
52
+ "bar_hex",
53
+ "compute_poll_sleep_seconds",
54
+ "crawl_packet_to_display_dict",
55
+ "detect_display_locale",
56
+ "mark_to_display_dict",
57
+ "nearest_trigger_remaining_seconds",
58
+ "nearest_window_open_remaining_seconds",
59
+ "resolve_poll_monitor_mode",
60
+ "normalize_patch_codes",
61
+ "query_to_display_dict",
62
+ ]
@@ -20,6 +20,14 @@ class BearTrackerClient:
20
20
  self._base_url = base_url.rstrip("/")
21
21
  self._timeout_seconds = timeout_seconds
22
22
 
23
+ @property
24
+ def base_url(self) -> str:
25
+ return self._base_url
26
+
27
+ @property
28
+ def timeout_seconds(self) -> float:
29
+ return self._timeout_seconds
30
+
23
31
  def sync_session(self) -> dict[str, Any]:
24
32
  """拉取会话与 ``resources``(含 DatabaseHunt、SpawnPoint、DataCenters)。"""
25
33
  return self._post("/syncSession", {})
@@ -3,17 +3,25 @@ from __future__ import annotations
3
3
  import time
4
4
  from typing import Any
5
5
 
6
- from ff14_the_hunt.models import HuntMarkRecord, HuntQueryFilter, TimerDisplay
6
+ from ff14_the_hunt.models import HuntMarkRecord, HuntQueryFilter
7
7
 
8
- from ff14_the_hunt.bear_tracker.fate_timer import compute_fate_condition_timer
8
+ from ff14_the_hunt.bear_tracker.fate_timer import compute_fate_timer
9
9
  from ff14_the_hunt.bear_tracker.resources import BearResources
10
- from ff14_the_hunt.bear_tracker.spawn_points import list_map_coordinates
11
10
  from ff14_the_hunt.bear_tracker.spawn_window import (
12
11
  compute_trigger_timer,
13
12
  is_recently_in_window,
14
13
  )
15
14
 
16
15
 
16
+ def mark_has_display_timer(record: HuntMarkRecord) -> bool:
17
+ """记录是否带有站点主列表会展示的任一条计时。"""
18
+ return (
19
+ record.trigger_timer is not None
20
+ or record.condition_timer is not None
21
+ or record.fate_timer is not None
22
+ )
23
+
24
+
17
25
  def _passes_filter(
18
26
  *,
19
27
  hunt_key: str,
@@ -40,7 +48,6 @@ def build_hunt_record(
40
48
  *,
41
49
  timer_row: dict[str, Any],
42
50
  resources: BearResources,
43
- spawn_states: dict[str, Any] | None,
44
51
  query: HuntQueryFilter,
45
52
  now: float | None = None,
46
53
  recent_grace_seconds: float = 900.0,
@@ -75,17 +82,12 @@ def build_hunt_record(
75
82
  now=now,
76
83
  )
77
84
 
78
- condition = compute_fate_condition_timer(
79
- last_death_time=float(last_death or 0.0),
85
+ fate_timer = compute_fate_timer(
80
86
  fate_last_seen=timer_row.get("fateLastSeen"),
81
87
  fate_last_death=timer_row.get("fateLastDeath"),
82
88
  now=now,
83
89
  )
84
90
 
85
- map_key = resources.spawn_map_key(hunt_key, meta)
86
- spawn_entry = resources.spawn_point.get(map_key) if map_key else None
87
- points = list_map_coordinates(spawn_entry, api_states=spawn_states)
88
-
89
91
  recently = is_recently_in_window(trigger, grace_seconds=recent_grace_seconds)
90
92
  if last_mark and now is not None:
91
93
  if now - float(last_mark) <= recent_grace_seconds:
@@ -109,8 +111,8 @@ def build_hunt_record(
109
111
  fate_last_seen=timer_row.get("fateLastSeen"),
110
112
  fate_last_death=timer_row.get("fateLastDeath"),
111
113
  trigger_timer=trigger,
112
- condition_timer=condition,
113
- spawn_points=points,
114
+ condition_timer=None,
115
+ fate_timer=fate_timer,
114
116
  recently_spawned=recently,
115
117
  raw_timer=dict(timer_row),
116
118
  )
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+
5
+ from ff14_the_hunt.bear_tracker.spawn_window import _format_duration
6
+ from ff14_the_hunt.bear_tracker.timer_theme import build_timer_display
7
+ from ff14_the_hunt.models import (
8
+ SpawnWindowPhase,
9
+ TimerBarColor,
10
+ TimerDisplay,
11
+ TimerKind,
12
+ )
13
+
14
+
15
+ def compute_fate_timer(
16
+ *,
17
+ fate_last_seen: float | None,
18
+ fate_last_death: float | None,
19
+ now: float | None = None,
20
+ ) -> TimerDisplay | None:
21
+ """7.0 部分 S 猎 FATE 链计时(站点组件 ``lo`` 的 FATE 分支)。"""
22
+ if fate_last_seen is None or fate_last_death is None:
23
+ return None
24
+ if now is None:
25
+ now = time.time()
26
+ now_ms = now * 1000.0
27
+ seen_ms = fate_last_seen * 1000.0
28
+ death_ms = fate_last_death * 1000.0
29
+
30
+ if death_ms > seen_ms:
31
+ delta = now_ms - death_ms
32
+ return build_timer_display(
33
+ kind=TimerKind.FATE,
34
+ phase=SpawnWindowPhase.CLOSED,
35
+ bar_color=TimerBarColor.ERROR,
36
+ counts_up=True,
37
+ elapsed_seconds=delta / 1000.0,
38
+ summary=f"FATE 已结束 {_format_duration(delta / 1000.0)}",
39
+ )
40
+
41
+ delta = now_ms - seen_ms
42
+ return build_timer_display(
43
+ kind=TimerKind.FATE,
44
+ phase=SpawnWindowPhase.OPEN,
45
+ bar_color=TimerBarColor.SUCCESS,
46
+ counts_up=True,
47
+ elapsed_seconds=delta / 1000.0,
48
+ summary=f"FATE 进行中 {_format_duration(delta / 1000.0)}",
49
+ )
50
+
51
+
52
+ def compute_fate_condition_timer(
53
+ *,
54
+ last_death_time: float,
55
+ fate_last_seen: float | None,
56
+ fate_last_death: float | None,
57
+ now: float | None = None,
58
+ ) -> TimerDisplay | None:
59
+ """兼容旧名;``last_death_time`` 未使用。"""
60
+ del last_death_time
61
+ return compute_fate_timer(
62
+ fate_last_seen=fate_last_seen,
63
+ fate_last_death=fate_last_death,
64
+ now=now,
65
+ )