From 391e97734d51b98356455fbaf33e8a8a21f28d14 Mon Sep 17 00:00:00 2001 From: a2893005741 Date: Tue, 18 Aug 2026 15:44:50 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix(emotion):=20=E5=8F=96=E6=B6=88=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=90=8E=E6=8C=89=E7=A6=BB=E7=BA=BF=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E5=BF=83=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- module/combat/emotion.py | 30 +++-------- module/config/config_updater.py | 7 ++- module/config/emotion_recovery.py | 85 +++++++++++++++++++++++++++++++ tests/test_emotion_recovery.py | 80 +++++++++++++++++++++++++++++ 4 files changed, 177 insertions(+), 25 deletions(-) create mode 100644 module/config/emotion_recovery.py create mode 100644 tests/test_emotion_recovery.py diff --git a/module/combat/emotion.py b/module/combat/emotion.py index bbddabf7e..dcde1b65b 100644 --- a/module/combat/emotion.py +++ b/module/combat/emotion.py @@ -17,14 +17,17 @@ 游戏客户端存在已知 bug:长时间运行后情绪计算不准确,需要定期重启。 """ -from datetime import datetime, timedelta +from datetime import timedelta from time import sleep import numpy as np from module.base.decorator import cached_property from module.base.utils import random_normal_distribution_int -from module.config.config import AzurLaneConfig +from module.config.emotion_recovery import ( + DIC_RECOVER_MAX, + emotion_recovery_speed, +) from module.config.time_source import now as current_time from module.exception import ScriptEnd, ScriptError, RequestHumanTakeover from module.logger import logger @@ -36,22 +39,6 @@ 'prevent_yellow_face': 30, # 防止黄脸 'prevent_red_face': 2, # 防止红脸 } -# 情绪恢复速度:每 6 分钟恢复的点数 -DIC_RECOVER = { - 'not_in_dormitory': 20, # 港区休息 - 'dormitory_floor_1': 40, # 后宅一楼 - 'dormitory_floor_2': 50, # 后宅二楼 -} -# 情绪上限 -DIC_RECOVER_MAX = { - 'not_in_dormitory': 119, - 'dormitory_floor_1': 150, - 'dormitory_floor_2': 150, -} -OATH_RECOVER = 10 # 誓约额外恢复速度 -ONSEN_RECOVER = 10 # 温泉额外恢复速度 - - class FleetEmotion: """单个舰队的情绪追踪器。 @@ -142,12 +129,7 @@ def speed(self): Returns: int: 每 6 分钟的恢复速度。 """ - speed = DIC_RECOVER[self.recover] - if self.oath: - speed += OATH_RECOVER - if self.onsen: - speed += ONSEN_RECOVER - return speed // 10 + return emotion_recovery_speed(self.recover, self.oath, self.onsen) @property def limit(self): diff --git a/module/config/config_updater.py b/module/config/config_updater.py index 1bdfdb38f..5cd0c97ca 100644 --- a/module/config/config_updater.py +++ b/module/config/config_updater.py @@ -35,9 +35,11 @@ from deploy.utils import DEPLOY_TEMPLATE, poor_yaml_read, poor_yaml_write from module.base.timer import timer from module.config.deep import deep_default, deep_get, deep_iter, deep_set +from module.config.emotion_recovery import recover_emotion_config from module.config.env import IS_ON_PHONE_CLOUD from module.config.server import VALID_CHANNEL_PACKAGE, VALID_PACKAGE, VALID_SERVER_LIST, to_package, to_server from module.config.task_priority import get_scheduler_tasks, merge_task_priority +from module.config.time_source import now as current_time from module.config.utils import * from module.config.redirect_utils.utils import * @@ -706,11 +708,12 @@ class ConfigUpdater: def args(self): return read_file(filepath_args()) - def config_update(self, old, is_template=False): + def config_update(self, old, is_template=False, now=None): """ Args: old: 旧配置字典。 is_template: 是否为模板配置。 + now: 用于刷新心情值的当前时间;测试可传入固定时间。 Returns: 更新后的配置字典。 @@ -804,6 +807,8 @@ def default_stage(t, stage): merge_task_priority(new_priority, template_priority, get_scheduler_tasks(self.args)), ) new = self._override(new) + if not is_template: + recover_emotion_config(new, now or current_time()) return new diff --git a/module/config/emotion_recovery.py b/module/config/emotion_recovery.py new file mode 100644 index 000000000..27546d2e5 --- /dev/null +++ b/module/config/emotion_recovery.py @@ -0,0 +1,85 @@ +"""配置加载阶段的心情恢复计算。""" + +from datetime import datetime, timedelta + +DIC_RECOVER = { + 'not_in_dormitory': 20, + 'dormitory_floor_1': 40, + 'dormitory_floor_2': 50, +} +DIC_RECOVER_MAX = { + 'not_in_dormitory': 119, + 'dormitory_floor_1': 150, + 'dormitory_floor_2': 150, +} +OATH_RECOVER = 10 +ONSEN_RECOVER = 10 + + +def emotion_recovery_speed(recover, oath=False, onsen=False): + """返回每 6 分钟恢复的心情点数。""" + speed = DIC_RECOVER[recover] + if oath: + speed += OATH_RECOVER + if onsen: + speed += ONSEN_RECOVER + return speed // 10 + + +def _recover_fleet(group, prefix, now): + value_key = f'{prefix}Value' + record_key = f'{prefix}Record' + recover_key = f'{prefix}Recover' + if value_key not in group or record_key not in group or recover_key not in group: + return + + value = group[value_key] + record = group[record_key] + recover = group[recover_key] + if not isinstance(value, (int, float)) or not isinstance(record, datetime): + return + if recover not in DIC_RECOVER: + return + + elapsed = (now - record).total_seconds() + if elapsed <= 0: + return + + speed = emotion_recovery_speed( + recover, + oath=bool(group.get(f'{prefix}Oath', False)), + onsen=bool(group.get(f'{prefix}Onsen', False)), + ) + maximum = DIC_RECOVER_MAX[recover] + recovery = speed * elapsed / 360 + recovered_points = int(recovery) + new_value = min(max(int(value), 0) + recovered_points, maximum) + + group[value_key] = new_value + if new_value >= maximum: + group[record_key] = now.replace(microsecond=0) + return + + fractional = recovery - recovered_points + record_time = now.replace(microsecond=0) + if fractional > 0: + record_time -= timedelta(seconds=fractional * 360 / speed) + group[record_key] = record_time + + +def recover_emotion_config(data, now): + """把任务配置中的持久化心情更新到 ``now`` 对应的当前值。""" + for task in data.values(): + if not isinstance(task, dict): + continue + + emotion = task.get('Emotion') + if isinstance(emotion, dict): + _recover_fleet(emotion, 'Fleet1', now) + _recover_fleet(emotion, 'Fleet2', now) + + public_emotion = task.get('PublicEmotion') + if isinstance(public_emotion, dict): + _recover_fleet(public_emotion, 'Fleet', now) + + return data diff --git a/tests/test_emotion_recovery.py b/tests/test_emotion_recovery.py new file mode 100644 index 000000000..681fb5c73 --- /dev/null +++ b/tests/test_emotion_recovery.py @@ -0,0 +1,80 @@ +import unittest +from datetime import datetime, timedelta + +from module.config.config_updater import ConfigUpdater + + +class TestEmotionConfigRecovery(unittest.TestCase): + def setUp(self): + self.now = datetime(2026, 8, 18, 12, 0, 0) + + def test_config_reload_recovers_stale_task_emotion_to_maximum(self): + old = { + 'Main': { + 'Emotion': { + 'Fleet1Value': 39, + 'Fleet1Record': self.now - timedelta(hours=12), + 'Fleet1Recover': 'not_in_dormitory', + 'Fleet1Oath': False, + 'Fleet1Onsen': False, + }, + }, + } + + new = ConfigUpdater().config_update(old, now=self.now) + + self.assertEqual(new['Main']['Emotion']['Fleet1Value'], 119) + self.assertEqual(new['Main']['Emotion']['Fleet1Record'], self.now) + + def test_config_reload_preserves_fractional_recovery_time(self): + old = { + 'Event': { + 'Emotion': { + 'Fleet1Value': 40, + 'Fleet1Record': self.now - timedelta(minutes=4), + 'Fleet1Recover': 'not_in_dormitory', + 'Fleet1Oath': False, + 'Fleet1Onsen': False, + }, + }, + } + + new = ConfigUpdater().config_update(old, now=self.now) + + self.assertEqual(new['Event']['Emotion']['Fleet1Value'], 41) + self.assertEqual( + new['Event']['Emotion']['Fleet1Record'], + self.now - timedelta(minutes=1), + ) + + reloaded = ConfigUpdater().config_update( + new, + now=self.now + timedelta(minutes=2), + ) + self.assertEqual(reloaded['Event']['Emotion']['Fleet1Value'], 42) + self.assertEqual( + reloaded['Event']['Emotion']['Fleet1Record'], + self.now + timedelta(minutes=2), + ) + + def test_config_reload_recovers_public_emotion(self): + old = { + 'General': { + 'PublicEmotion': { + 'FleetValue': 100, + 'FleetRecord': self.now - timedelta(hours=2), + 'FleetRecover': 'dormitory_floor_1', + 'FleetOath': True, + 'FleetOnsen': False, + }, + }, + } + + new = ConfigUpdater().config_update(old, now=self.now) + + self.assertEqual(new['General']['PublicEmotion']['FleetValue'], 150) + self.assertEqual(new['General']['PublicEmotion']['FleetRecord'], self.now) + + +if __name__ == '__main__': + unittest.main() From 06f899c4f7431c0d19e3c4b69d8d1496e8707a7b Mon Sep 17 00:00:00 2001 From: a2893005741 Date: Tue, 18 Aug 2026 16:20:26 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix(emotion):=20=E7=BB=9F=E4=B8=80=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E5=91=A8=E6=9C=9F=E5=B9=B6=E5=9B=BA=E5=AE=9A=E5=90=8E?= =?UTF-8?q?=E5=AE=85=E4=B8=8A=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- module/combat/emotion.py | 13 +++++---- module/config/emotion_recovery.py | 6 ++-- tests/test_emotion_recovery.py | 47 +++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/module/combat/emotion.py b/module/combat/emotion.py index dcde1b65b..970e4131d 100644 --- a/module/combat/emotion.py +++ b/module/combat/emotion.py @@ -26,6 +26,7 @@ from module.base.utils import random_normal_distribution_int from module.config.emotion_recovery import ( DIC_RECOVER_MAX, + SECONDS_PER_TICK, emotion_recovery_speed, ) from module.config.time_source import now as current_time @@ -159,8 +160,8 @@ def update(self): """ time_diff = current_time().timestamp() - self.record.timestamp() time_diff = max(time_diff, 0) - # speed 为每360秒的恢复量,换算为每秒恢复 speed/360 点 - recovery = self.speed * time_diff / 360 + # speed 为每个恢复周期的恢复量,换算为每秒恢复 speed/SECONDS_PER_TICK 点 + recovery = self.speed * time_diff / SECONDS_PER_TICK self.current = min(max(self.value, 0) + int(recovery), self.max) # 保留未满1点的恢复余数对应的秒数,用于 record() 回扣 self._fractional_seconds = recovery - int(recovery) @@ -187,8 +188,8 @@ def get_recovered(self, expected_reduce=0): emotion_needed = self.limit + expected_reduce - self.current if emotion_needed <= 0: return current_time() - # speed 为每360秒的恢复量,换算恢复所需秒数 - seconds_needed = emotion_needed * 360 / self.speed + # speed 为每个恢复周期的恢复量,换算恢复所需秒数 + seconds_needed = emotion_needed * SECONDS_PER_TICK / self.speed return current_time() + timedelta(seconds=seconds_needed) class Emotion: @@ -273,7 +274,7 @@ def record(self): fractional = getattr(fleet, '_fractional_seconds', 0) if fractional > 0: # 回扣 fractional_seconds 对应的秒数 - record_time = record_time - timedelta(seconds=fractional * 360 / fleet.speed) + record_time = record_time - timedelta(seconds=fractional * SECONDS_PER_TICK / fleet.speed) with self.config.multi_set(): setattr(self.config, fleet.value_name, new_value) setattr(self.config, fleet.value_name.replace('Value', 'Record'), record_time) @@ -285,7 +286,7 @@ def record(self): record_time = current_time().replace(microsecond=0) fractional = getattr(fleet, '_fractional_seconds', 0) if fractional > 0: - record_time = record_time - timedelta(seconds=fractional * 360 / fleet.speed) + record_time = record_time - timedelta(seconds=fractional * SECONDS_PER_TICK / fleet.speed) setattr(self.config, fleet.value_name, new_value) setattr(self.config, fleet.value_name.replace('Value', 'Record'), record_time) diff --git a/module/config/emotion_recovery.py b/module/config/emotion_recovery.py index 27546d2e5..e24196eac 100644 --- a/module/config/emotion_recovery.py +++ b/module/config/emotion_recovery.py @@ -2,6 +2,8 @@ from datetime import datetime, timedelta +SECONDS_PER_TICK = 6 * 60 + DIC_RECOVER = { 'not_in_dormitory': 20, 'dormitory_floor_1': 40, @@ -51,7 +53,7 @@ def _recover_fleet(group, prefix, now): onsen=bool(group.get(f'{prefix}Onsen', False)), ) maximum = DIC_RECOVER_MAX[recover] - recovery = speed * elapsed / 360 + recovery = speed * elapsed / SECONDS_PER_TICK recovered_points = int(recovery) new_value = min(max(int(value), 0) + recovered_points, maximum) @@ -63,7 +65,7 @@ def _recover_fleet(group, prefix, now): fractional = recovery - recovered_points record_time = now.replace(microsecond=0) if fractional > 0: - record_time -= timedelta(seconds=fractional * 360 / speed) + record_time -= timedelta(seconds=fractional * SECONDS_PER_TICK / speed) group[record_key] = record_time diff --git a/tests/test_emotion_recovery.py b/tests/test_emotion_recovery.py index 681fb5c73..d907a9562 100644 --- a/tests/test_emotion_recovery.py +++ b/tests/test_emotion_recovery.py @@ -75,6 +75,53 @@ def test_config_reload_recovers_public_emotion(self): self.assertEqual(new['General']['PublicEmotion']['FleetValue'], 150) self.assertEqual(new['General']['PublicEmotion']['FleetRecord'], self.now) + def test_dormitory_recovery_caps_fleet_emotion_at_150(self): + old = { + 'Main': { + 'Emotion': { + 'Fleet1Value': 39, + 'Fleet1Record': self.now - timedelta(hours=12), + 'Fleet1Recover': 'dormitory_floor_1', + 'Fleet1Oath': False, + 'Fleet1Onsen': False, + 'Fleet2Value': 39, + 'Fleet2Record': self.now - timedelta(hours=12), + 'Fleet2Recover': 'dormitory_floor_2', + 'Fleet2Oath': False, + 'Fleet2Onsen': False, + }, + }, + } + + new = ConfigUpdater().config_update(old, now=self.now) + emotion = new['Main']['Emotion'] + + self.assertEqual(emotion['Fleet1Value'], 150) + self.assertEqual(emotion['Fleet2Value'], 150) + self.assertEqual(emotion['Fleet1Record'], self.now) + self.assertEqual(emotion['Fleet2Record'], self.now) + + def test_template_config_skips_emotion_recovery(self): + old_record = self.now - timedelta(hours=12) + old = { + 'Main': { + 'Emotion': { + 'Fleet1Value': 119, + 'Fleet1Record': old_record, + 'Fleet1Recover': 'not_in_dormitory', + 'Fleet1Oath': False, + 'Fleet1Onsen': False, + }, + }, + } + + new = ConfigUpdater().config_update(old, now=self.now, is_template=True) + emotion = new['Main']['Emotion'] + + self.assertEqual(emotion['Fleet1Value'], 119) + self.assertEqual(emotion['Fleet1Record'], datetime(2020, 1, 1)) + self.assertNotEqual(emotion['Fleet1Record'], self.now) + if __name__ == '__main__': unittest.main() From c26d420c4355d31942efe73ab6f98adccd8bb9f7 Mon Sep 17 00:00:00 2001 From: a2893005741 Date: Tue, 18 Aug 2026 16:27:10 +0800 Subject: [PATCH 3/5] =?UTF-8?q?test(emotion):=20=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E9=9D=9E=E6=AD=A3=E6=97=B6=E9=97=B4=E5=B7=AE=E7=9A=84=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_emotion_recovery.py | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/test_emotion_recovery.py b/tests/test_emotion_recovery.py index d907a9562..7599c63af 100644 --- a/tests/test_emotion_recovery.py +++ b/tests/test_emotion_recovery.py @@ -122,6 +122,45 @@ def test_template_config_skips_emotion_recovery(self): self.assertEqual(emotion['Fleet1Record'], datetime(2020, 1, 1)) self.assertNotEqual(emotion['Fleet1Record'], self.now) + def test_equal_record_time_does_not_recover_emotion(self): + old = { + 'Main': { + 'Emotion': { + 'Fleet1Value': 39, + 'Fleet1Record': self.now, + 'Fleet1Recover': 'not_in_dormitory', + 'Fleet1Oath': False, + 'Fleet1Onsen': False, + }, + }, + } + + new = ConfigUpdater().config_update(old, now=self.now) + emotion = new['Main']['Emotion'] + + self.assertEqual(emotion['Fleet1Value'], 39) + self.assertEqual(emotion['Fleet1Record'], self.now) + + def test_future_record_time_does_not_recover_emotion(self): + future_record = self.now + timedelta(minutes=1) + old = { + 'Main': { + 'Emotion': { + 'Fleet1Value': 39, + 'Fleet1Record': future_record, + 'Fleet1Recover': 'not_in_dormitory', + 'Fleet1Oath': False, + 'Fleet1Onsen': False, + }, + }, + } + + new = ConfigUpdater().config_update(old, now=self.now) + emotion = new['Main']['Emotion'] + + self.assertEqual(emotion['Fleet1Value'], 39) + self.assertEqual(emotion['Fleet1Record'], future_record) + if __name__ == '__main__': unittest.main() From 1ce07042c92fcde1945ee1c665a90f116a861466 Mon Sep 17 00:00:00 2001 From: a2893005741 Date: Tue, 18 Aug 2026 16:39:54 +0800 Subject: [PATCH 4/5] =?UTF-8?q?refactor(emotion):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=B8=8E=E8=BF=90=E8=A1=8C=E6=97=B6=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- module/base/emotion.py | 35 ++++++++++++++++++++++ module/combat/emotion.py | 18 +++++++----- module/config/emotion_recovery.py | 48 +++++++++++-------------------- 3 files changed, 62 insertions(+), 39 deletions(-) create mode 100644 module/base/emotion.py diff --git a/module/base/emotion.py b/module/base/emotion.py new file mode 100644 index 000000000..5f38670b5 --- /dev/null +++ b/module/base/emotion.py @@ -0,0 +1,35 @@ +"""心情恢复的共享规则和纯计算函数。""" + +SECONDS_PER_TICK = 6 * 60 + +DIC_RECOVER = { + 'not_in_dormitory': 20, + 'dormitory_floor_1': 40, + 'dormitory_floor_2': 50, +} +DIC_RECOVER_MAX = { + 'not_in_dormitory': 119, + 'dormitory_floor_1': 150, + 'dormitory_floor_2': 150, +} +OATH_RECOVER = 10 +ONSEN_RECOVER = 10 + + +def emotion_recovery_speed(recover, oath=False, onsen=False): + """返回每个 6 分钟周期恢复的心情点数。""" + speed = DIC_RECOVER[recover] + if oath: + speed += OATH_RECOVER + if onsen: + speed += ONSEN_RECOVER + return speed // 10 + + +def calculate_emotion_recovery(value, recover, elapsed, oath=False, onsen=False): + """根据经过秒数返回当前心情值和未满一点的恢复余数。""" + speed = emotion_recovery_speed(recover, oath=oath, onsen=onsen) + recovery = speed * max(elapsed, 0) / SECONDS_PER_TICK + recovered_points = int(recovery) + current = min(max(int(value), 0) + recovered_points, DIC_RECOVER_MAX[recover]) + return current, recovery - recovered_points diff --git a/module/combat/emotion.py b/module/combat/emotion.py index 970e4131d..be1ca21df 100644 --- a/module/combat/emotion.py +++ b/module/combat/emotion.py @@ -23,12 +23,13 @@ import numpy as np from module.base.decorator import cached_property -from module.base.utils import random_normal_distribution_int -from module.config.emotion_recovery import ( +from module.base.emotion import ( DIC_RECOVER_MAX, SECONDS_PER_TICK, + calculate_emotion_recovery, emotion_recovery_speed, ) +from module.base.utils import random_normal_distribution_int from module.config.time_source import now as current_time from module.exception import ScriptEnd, ScriptError, RequestHumanTakeover from module.logger import logger @@ -159,12 +160,15 @@ def update(self): 符合情绪控制的安全方向(宁可低估也不高估)。 """ time_diff = current_time().timestamp() - self.record.timestamp() - time_diff = max(time_diff, 0) - # speed 为每个恢复周期的恢复量,换算为每秒恢复 speed/SECONDS_PER_TICK 点 - recovery = self.speed * time_diff / SECONDS_PER_TICK - self.current = min(max(self.value, 0) + int(recovery), self.max) + self.current, fractional = calculate_emotion_recovery( + self.value, + self.recover, + time_diff, + oath=self.oath, + onsen=self.onsen, + ) # 保留未满1点的恢复余数对应的秒数,用于 record() 回扣 - self._fractional_seconds = recovery - int(recovery) + self._fractional_seconds = fractional def get_recovered(self, expected_reduce=0): """计算情绪恢复到控制阈值的时间。 diff --git a/module/config/emotion_recovery.py b/module/config/emotion_recovery.py index e24196eac..5f83eb610 100644 --- a/module/config/emotion_recovery.py +++ b/module/config/emotion_recovery.py @@ -2,30 +2,13 @@ from datetime import datetime, timedelta -SECONDS_PER_TICK = 6 * 60 - -DIC_RECOVER = { - 'not_in_dormitory': 20, - 'dormitory_floor_1': 40, - 'dormitory_floor_2': 50, -} -DIC_RECOVER_MAX = { - 'not_in_dormitory': 119, - 'dormitory_floor_1': 150, - 'dormitory_floor_2': 150, -} -OATH_RECOVER = 10 -ONSEN_RECOVER = 10 - - -def emotion_recovery_speed(recover, oath=False, onsen=False): - """返回每 6 分钟恢复的心情点数。""" - speed = DIC_RECOVER[recover] - if oath: - speed += OATH_RECOVER - if onsen: - speed += ONSEN_RECOVER - return speed // 10 +from module.base.emotion import ( + DIC_RECOVER, + DIC_RECOVER_MAX, + SECONDS_PER_TICK, + calculate_emotion_recovery, + emotion_recovery_speed, +) def _recover_fleet(group, prefix, now): @@ -47,22 +30,23 @@ def _recover_fleet(group, prefix, now): if elapsed <= 0: return - speed = emotion_recovery_speed( + oath = bool(group.get(f'{prefix}Oath', False)) + onsen = bool(group.get(f'{prefix}Onsen', False)) + speed = emotion_recovery_speed(recover, oath=oath, onsen=onsen) + maximum = DIC_RECOVER_MAX[recover] + new_value, fractional = calculate_emotion_recovery( + value, recover, - oath=bool(group.get(f'{prefix}Oath', False)), - onsen=bool(group.get(f'{prefix}Onsen', False)), + elapsed, + oath=oath, + onsen=onsen, ) - maximum = DIC_RECOVER_MAX[recover] - recovery = speed * elapsed / SECONDS_PER_TICK - recovered_points = int(recovery) - new_value = min(max(int(value), 0) + recovered_points, maximum) group[value_key] = new_value if new_value >= maximum: group[record_key] = now.replace(microsecond=0) return - fractional = recovery - recovered_points record_time = now.replace(microsecond=0) if fractional > 0: record_time -= timedelta(seconds=fractional * SECONDS_PER_TICK / speed) From fde559b0f30f967749ed8b371bcedabeead28bcc Mon Sep 17 00:00:00 2001 From: a2893005741 Date: Tue, 18 Aug 2026 20:34:49 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix(emotion):=20=E7=BB=9F=E4=B8=80=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E6=97=B6=E9=97=B4=E6=88=B3=E8=AE=A1=E7=AE=97=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E9=97=B4=E9=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- module/config/emotion_recovery.py | 2 +- tests/test_emotion_recovery.py | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/module/config/emotion_recovery.py b/module/config/emotion_recovery.py index 5f83eb610..208dc33f1 100644 --- a/module/config/emotion_recovery.py +++ b/module/config/emotion_recovery.py @@ -26,7 +26,7 @@ def _recover_fleet(group, prefix, now): if recover not in DIC_RECOVER: return - elapsed = (now - record).total_seconds() + elapsed = now.timestamp() - record.timestamp() if elapsed <= 0: return diff --git a/tests/test_emotion_recovery.py b/tests/test_emotion_recovery.py index 7599c63af..64a8e7cac 100644 --- a/tests/test_emotion_recovery.py +++ b/tests/test_emotion_recovery.py @@ -1,5 +1,5 @@ import unittest -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from module.config.config_updater import ConfigUpdater @@ -161,6 +161,26 @@ def test_future_record_time_does_not_recover_emotion(self): self.assertEqual(emotion['Fleet1Value'], 39) self.assertEqual(emotion['Fleet1Record'], future_record) + def test_offset_aware_record_recovers_from_naive_current_time(self): + record = datetime.fromtimestamp(self.now.timestamp() - 6 * 60, timezone.utc) + old = { + 'Main': { + 'Emotion': { + 'Fleet1Value': 39, + 'Fleet1Record': record, + 'Fleet1Recover': 'not_in_dormitory', + 'Fleet1Oath': False, + 'Fleet1Onsen': False, + }, + }, + } + + new = ConfigUpdater().config_update(old, now=self.now) + emotion = new['Main']['Emotion'] + + self.assertEqual(emotion['Fleet1Value'], 41) + self.assertEqual(emotion['Fleet1Record'], self.now) + if __name__ == '__main__': unittest.main()