import json
import random
import math
import time
import logging
import sqlite3
from copy import deepcopy
from activity_manager import activity_manager
from zodiac import zodiac_manager
from datetime import datetime
from static_db import db
from notification import send_system
from config_center import CONFIG

logger = logging.getLogger(__name__)

def generate_uuid():
    """生成10位数字字符串UUID（仅用于怪物掉落装备）"""
    return str(random.randint(1000000000, 9999999999))

class GameEngine:
    def __init__(self, monster_pool_getter=None, monster_pool_returner=None, use_db=True, map_stats_callback=None, task_update_callback=None):
        self.config = {}
        self.monster_pool_getter = monster_pool_getter
        self.monster_pool_returner = monster_pool_returner
        self.use_db = use_db
        self.map_stats_callback = map_stats_callback
        self.task_update_callback = task_update_callback

        # 从配置中心加载所有静态配置
        self._load_from_config_center()
        self.new_equip_templates = self.config.get('newEquipTemplates', {})

        # 初始化地图怪物缓存字典
        self._map_monster_cache = {}

        if use_db:
            self.load_from_db()

        # 基础配置属性（直接从 CONFIG 读取）
        self.drop_configs = CONFIG.get('drops', [])
        self.level_hit_modifier = CONFIG.get('levelHitModifier', 0.02)
        self.equip_fragment_cost = CONFIG.get('enhanceCost', {}).get('equip_fragment_cost', 10)
        self.zodiac_fragment_cost = CONFIG.get('enhanceCost', {}).get('zodiac_fragment_cost', 5)

        self.fragment_templates = self.config.get('fragmentTemplates', {})
        if not self.fragment_templates:
            logger.warning("fragmentTemplates 配置缺失，碎片合成功能将不可用")

        # 炼化配置（已包含 attribute_pool）
        self.refine_config = self.config.get('refine', {})
        self.max_refine_attrs = self.refine_config.get('max_attrs', 5)
        self.refine_cost_base_gold = self.refine_config.get('cost_gold', 50000)
        self.refine_cost_fragment = self.refine_config.get('cost_fragment', 5)
        self.refine_quality_mult = self.refine_config.get('quality_multiplier', {'普通': 1, '极品': 1.2, '神品': 1.5, '超神品': 2})
        self.refine_attr_pool = self.refine_config.get('attribute_pool', [])
        if not self.refine_attr_pool:
            logger.error("refine.attribute_pool 配置缺失，炼化功能将无法正常工作")
            self.refine_attr_pool = []
        self.player_starting_stats = self.config.get('player_starting_stats', {})
        self.hero_starting_stats = self.config.get('hero_starting_stats', {})
        self.level_up_bonuses = self.config.get('levelUpBonuses', {})
        self.rebirth_config = self.config.get('rebirth', {})
        self.enhancement_config = self.config.get('enhancement', {})

        # 升级加成
        self.hp_growth_base = self.level_up_bonuses.get('hp', {}).get('base', 20)
        self.hp_growth_per_rebirth = self.level_up_bonuses.get('hp', {}).get('perRebirth', 10)
        self.mp_growth_base = self.level_up_bonuses.get('mp', {}).get('base', 20)
        self.mp_growth_per_rebirth = self.level_up_bonuses.get('mp', {}).get('perRebirth', 5)
        self.ad_growth_per_level = self.level_up_bonuses.get('attackDefense', {}).get('perLevel', 1)
        self.ad_growth_per_rebirth = self.level_up_bonuses.get('attackDefense', {}).get('perRebirth', 1)
        self.exp_growth_factor = self.level_up_bonuses.get('expGrowthFactor', 1.25)

        # 转生配置
        self.rebirth_level_base = self.rebirth_config.get('levelRequirement', {}).get('base', 60)
        self.rebirth_level_increment = self.rebirth_config.get('levelRequirement', {}).get('increment', 20)
        self.rebirth_cost_base = self.rebirth_config.get('cost', {}).get('base', 5_000_000)
        self.rebirth_cost_increment = self.rebirth_config.get('cost', {}).get('increment', 1_000_000)
        self.rebirth_max_exp_base = self.rebirth_config.get('maxExp', {}).get('base', 100)
        self.rebirth_max_exp_growth = self.rebirth_config.get('maxExp', {}).get('growthRate', 0.1)
        self.rebirth_base_mp = self.rebirth_config.get('baseMp', 100)
        self.rebirth_mp_per = self.rebirth_config.get('mpPerRebirth', 20)
        self.rebirth_hp_per = self.rebirth_config.get('hpPerRebirth', 100)
        self.rebirth_ad_per = self.rebirth_config.get('attackDefensePerRebirth', 1)

        # 装备强化配置
        self.enhance_cost_base = self.enhancement_config.get('equipEnhanceCostBase', 100_000)
        self.enhance_cost_exponent = self.enhancement_config.get('equipEnhanceCostExponent', 1.1)
        self.enhance_success_decay = self.enhancement_config.get('equipEnhanceSuccessRateDecay', 0.05)
        self.enhance_quality_multiplier = self.enhancement_config.get('qualityCostMultiplier', {'普通': 1.0})
        combat_cfg = CONFIG.get('combat', {})
        self.player_base_attack_interval = combat_cfg.get('player_base_attack_interval', 0.7)
        self.hero_base_attack_interval = combat_cfg.get('hero_base_attack_interval', 0.7)
        self.monster_base_attack_interval = combat_cfg.get('monster_base_attack_interval', 1.2)
        self.game_speed_reduction_per_point = combat_cfg.get('game_speed_reduction_per_point', 0.05)
        self.min_attack_interval = combat_cfg.get('min_attack_interval', 0.1)
        
    def _get_cached_attack(self, state, target='player'):
        """带缓存的攻击力获取"""
        cache_key = f"attack_{target}"
        version = self._get_stats_version(state, target)
        cached = state.get('_stats_cache', {}).get(cache_key)
        
        if cached and cached.get('version') == version:
            return cached['value']
        
        # 重新计算
        attack_min, attack_max = self._calculate_attack(state, target)
        
        if '_stats_cache' not in state:
            state['_stats_cache'] = {}
        state['_stats_cache'][cache_key] = {
            'value': (attack_min, attack_max),
            'version': version
        }
        return attack_min, attack_max

    def _get_stats_version(self, state, target='player'):
        """计算当前玩家/英雄的属性版本哈希"""
        entity = state[target]
        if target == 'hero' and not entity.get('summoned'):
            return 0
        # 提取影响属性的所有字段
        equip = state['equipment'][target]
        equip_fingerprint = []
        for slot, item in sorted(equip.items()):
            if item is None:
                equip_fingerprint.append((slot, 0, ()))
            else:
                refine = item.get('refine', {}).get('attrs', [])
                refine_tuple = tuple((ra['type'], ra['value']) for ra in refine)
                equip_fingerprint.append((slot, item.get('enhanceLevel', 0), refine_tuple))
        zodiac = state.get('zodiac_equipped', {})
        zodiac_ids = tuple(sorted(zodiac.keys()))
        
        # 将临时增益转换为可哈希的表示
        buffs_items = []
        for key, buf in state.get('temporary_buffs', {}).items():
            target_buf = buf.get('target')
            attrs = buf.get('attributes', {})
            expire_time = buf.get('expire_time', 0)
            attrs_tuple = tuple(sorted((k, v) for k, v in attrs.items()))
            buffs_items.append((target_buf, attrs_tuple, expire_time))
        buffs = tuple(sorted(buffs_items, key=lambda x: (x[0], x[1], x[2])))
        
        return hash((
            entity.get('level', 0),
            entity.get('rebirth', 0),
            entity.get('attackEnhance', 0),
            entity.get('defenseEnhance', 0),
            entity.get('baseGameSpeed', 1.0),
            entity.get('baseExpMultiplier', 1.0),
            entity.get('baseAccuracy', 1),
            entity.get('baseDodge', 1),
            entity.get('critRate', 0.0),
            entity.get('lethalRate', 0.0),
            entity.get('poisonRate', 0.0),
            tuple(equip_fingerprint),
            zodiac_ids,
            buffs
        ))
        
    def _calculate_attack(self, state, target='player'):
        """计算实际攻击力（不依赖缓存）"""
        if target == 'player':
            entity = state['player']
            equipment = state['equipment']['player']
        else:
            if not state['hero'].get('summoned'):
                return 0, 0
            entity = state['hero']
            equipment = state['equipment']['hero']
        
        base_min = entity.get('attackMin', 0) + entity.get('attackEnhance', 0)
        base_max = entity.get('attackMax', 0) + entity.get('attackEnhance', 0)
        
        for slot in self.attack_slots:
            item = equipment.get(slot)
            if item:
                base_min += item.get('attackMin', 0)
                base_max += item.get('attackMax', 0)
        
        return base_min, base_max
        
    def _load_from_config_center(self):
        """从 ConfigCenter 加载所有静态配置（maps、worldBosses、tasks、zodiac等）"""
        self.config['maps'] = CONFIG.get('maps', [])
        self.config['worldBosses'] = CONFIG.get('worldBosses', [])
        self.config['worldBoss'] = CONFIG.get('worldBoss', {})
        self.config['tasks'] = CONFIG.get('tasks', {})
        self.config['enhanceCost'] = CONFIG.get('enhanceCost', {})
        self.config['dropConfig'] = CONFIG.get('dropConfig', {})
        self.config['lottery'] = CONFIG.get('lottery', {})
        self.config['maxMonstersPerBattle'] = CONFIG.get('maxMonstersPerBattle', 5)
        self.config['setBonuses'] = CONFIG.get('setBonuses', [])
        self.config['levelHitModifier'] = CONFIG.get('levelHitModifier', 0.02)
        self.config['zodiac_full_bonus'] = CONFIG.get('zodiac_full_bonus', {})
        self.config['drops'] = CONFIG.get('drops', [])
        self.config['zodiac'] = CONFIG.get('zodiac', {})
        self.config['initialItems'] = CONFIG.get('initialItems', {})
        self.config['activities'] = CONFIG.get('activities', {})
        self.config['seeds'] = CONFIG.get('seeds', [])
        self.config['crops'] = CONFIG.get('crops', [])
        self.config['newEquipTemplates'] = CONFIG.get('newEquipTemplates', {})
        self.config['fragmentTemplates'] = CONFIG.get('fragmentTemplates', {})
        self.config['refine'] = CONFIG.get('refine', {})
        self.config['craft'] = CONFIG.get('craft', {})
        self.config['levelUpBonuses'] = CONFIG.get('levelUpBonuses', {})
        self.config['rebirth'] = CONFIG.get('rebirth', {})
        self.config['enhancement'] = CONFIG.get('enhancement', {})
        self.config['player_starting_stats'] = CONFIG.get('player_starting_stats', {})
        self.config['hero_starting_stats'] = CONFIG.get('hero_starting_stats', {})
        self.config['farm_tools'] = CONFIG.get('farm_tools', [])
        self.config['equipment_slots'] = CONFIG.get('equipment_slots', {})
        self.attack_slots = []
        self.defense_slots = []
        self._init_slots()
        from wjsx import (
            BaseStatsContributor, GlobalEnhanceContributor, EquipmentContributor,
            SetBonusContributor, ZodiacContributor, TemporaryBuffContributor
        )
        self.contributors = [
            BaseStatsContributor(),
            GlobalEnhanceContributor(),
            EquipmentContributor(),
            SetBonusContributor(),
            ZodiacContributor(),
            TemporaryBuffContributor(),
        ]
        combat_cfg = CONFIG.get('combat', {})
        self.poison_duration = combat_cfg.get('poison_duration', 10)
        self.poison_tick_interval = combat_cfg.get('poison_tick_interval', 2)
        self.burn_duration = combat_cfg.get('burn_duration', 8)
        self.burn_tick_interval = combat_cfg.get('burn_tick_interval', 2)
        
    def _init_slots(self):
        slot_cfg = self.config.get('equipment_slots', {})
        for slot, cfg in slot_cfg.items():
            if cfg.get('attack_slot'):
                self.attack_slots.append(slot)
            if cfg.get('defense_slot'):
                self.defense_slots.append(slot)
        if not self.attack_slots:
            self.attack_slots = ['weapon','necklace','leftBracelet','rightBracelet','leftRing','rightRing','medal','bambooHat']
        if not self.defense_slots:
            self.defense_slots = ['armor','helmet','leftBracelet','rightBracelet','belt','shoes','bambooHat']
        
    def _invalidate_equipment_cache(self, item):
        """删除装备的缓存属性，强制下次属性计算时重新生成"""
        if item and '_cached_contrib' in item:
            del item['_cached_contrib']
        
    def _get_attack(self, state, target='player'):
        """获取玩家或英雄的最终攻击力"""
        if target == 'player':
            return state['player'].get('finalAttackMin', 0), state['player'].get('finalAttackMax', 0)
        elif target == 'hero':
            if state['hero'].get('summoned'):
                return state['hero'].get('finalAttackMin', 0), state['hero'].get('finalAttackMax', 0)
        return 0, 0

    def _get_defense(self, state, target='player'):
        """获取玩家或英雄的最终防御力"""
        if target == 'player':
            return state['player'].get('finalDefenseMin', 0), state['player'].get('finalDefenseMax', 0)
        elif target == 'hero':
            if state['hero'].get('summoned'):
                return state['hero'].get('finalDefenseMin', 0), state['hero'].get('finalDefenseMax', 0)
        return 0, 0
        
    def _build_map_monster_cache(self):
        """预加载每个地图可能刷新的怪物模板列表（使用 CONFIG 中的 maps 和 monsters）"""
        self._map_monster_cache.clear()
        monsters_list = self.config.get('monsters', [])   # monsters 由 load_from_db 填充
        for map_data in self.config.get('maps', []):
            map_id = map_data['id']
            monsters = []
            for m_id in map_data.get('monsters', []):
                m_info = next((m for m in monsters_list if m['id'] == m_id), None)
                if m_info:
                    monsters.append(m_info)
            self._map_monster_cache[map_id] = monsters

    def load_from_db(self):
        """从数据库加载装备、药水、技能、怪物（保留原有逻辑）"""
        equipment_data = db.get_equipment()
        potions = db.get_potions()
        skills = db.get_skills()
        monsters = db.get_monsters()

        self.config['equipmentData'] = equipment_data
        self.config['potions'] = potions
        self.config['playerSkills'] = skills['playerSkills']
        self.config['heroSkills'] = skills['heroSkills']
        self.config['monsters'] = monsters
        self._build_map_monster_cache()

    def reload_static_data(self):
        if self.use_db:
            self.load_from_db()
        self._load_from_config_center()
        self.drop_configs = self.config.get('drops', [])
        self.level_hit_modifier = self.config.get('levelHitModifier', 0.02)
        self.equip_fragment_cost = self.config.get('enhanceCost', {}).get('equip_fragment_cost', 10)
        self.zodiac_fragment_cost = self.config.get('enhanceCost', {}).get('zodiac_fragment_cost', 5)
        self.config['craft'] = CONFIG.get('craft', {})
        self.fragment_templates = self.config.get('fragmentTemplates', {})
        self.refine_config = self.config.get('refine', {})
        self.max_refine_attrs = self.refine_config.get('max_attrs', 5)
        self.refine_cost_base_gold = self.refine_config.get('cost_gold', 50000)
        self.refine_cost_fragment = self.refine_config.get('cost_fragment', 5)
        self.refine_quality_mult = self.refine_config.get('quality_multiplier', {'普通': 1, '极品': 1.2, '神品': 1.5, '超神品': 2})
        self.refine_attr_pool = self.refine_config.get('attribute_pool', [])
        self._build_map_monster_cache()
        logger.info("静态数据已重新加载")
    
    def _ensure_config_fresh(self):
        """原30秒自动重载逻辑已取消，改为仅在启动时读取一次，通过管理后台手动重载"""
        pass

    def _calc_attack_interval(self, entity, base_interval):
        """根据 gameSpeed 计算实际攻击间隔（秒）"""
        game_speed = entity.get('gameSpeed', 1)
        interval = base_interval - (game_speed - 1) * self.game_speed_reduction_per_point
        return max(self.min_attack_interval, interval)
    
    def _select_monster_group_for_map(self, map_id):
        """根据地图选择一组怪物（1-5只），使用缓存模板"""
        monsters_info = self._map_monster_cache.get(map_id, [])
        if not monsters_info:
            return []
    
        normal_monsters = [m for m in monsters_info if not m.get('isBoss', False)]
        boss_monsters = [m for m in monsters_info if m.get('isBoss', False)]
    
        has_boss = boss_monsters and random.random() < 0.01
        monster_count = random.randint(1, self.config.get('maxMonstersPerBattle', 5))
    
        selected_monsters = []
        if has_boss:
            boss = random.choice(boss_monsters)
            selected_monsters.append(self._create_monster_instance(boss))
        else:
            weights = [max(0, m.get('spawnRate', 1)) for m in normal_monsters]
            if sum(weights) == 0:
                weights = [1] * len(normal_monsters)
            for _ in range(monster_count):
                chosen = random.choices(normal_monsters, weights=weights)[0]
                selected_monsters.append(self._create_monster_instance(chosen))
    
        return selected_monsters
    
    def _create_monster_instance(self, template):
        monster = template.copy()
        monster['currentHp'] = monster.get('hp', monster.get('maxHp', 100))
        monster['currentMaxHp'] = monster.get('maxHp', monster.get('hp', 100))
        monster['uuid'] = generate_uuid()
        monster['isAlive'] = True
        monster['freezeTurns'] = 0
        monster['threat_table'] = {'player': 0, 'hero': 0}
        monster['active'] = False
        monster['last_attack_time'] = time.time() + random.uniform(0, 0.5)
        return monster
    
    def _select_monster_for_map(self, map_id):
        """根据地图ID按概率选择怪物（BOSS概率1%），返回怪物对象"""
        map_data = next((m for m in self.config['maps'] if m['id'] == map_id), None)
        if not map_data or not map_data.get('monsters'):
            return None
        
        monsters_info = []
        for m_id in map_data['monsters']:
            m_info = next((m for m in self.config['monsters'] if m['id'] == m_id), None)
            if m_info:
                monsters_info.append(m_info)
        if not monsters_info:
            return None
        
        normal_monsters = [m for m in monsters_info if not m.get('isBoss', False)]
        boss_monsters = [m for m in monsters_info if m.get('isBoss', False)]
        
        if boss_monsters and random.random() < 0.01:
            return random.choice(boss_monsters)
        else:
            weights = [max(0, m.get('spawnRate', 1)) for m in normal_monsters]
            if sum(weights) == 0:
                weights = [1] * len(normal_monsters)
            return random.choices(normal_monsters, weights=weights)[0]
    
    def calc_damage(self, attack_min, attack_max, defense_min, defense_max):
        attack = random.randint(attack_min, attack_max)
        defense = random.randint(defense_min, defense_max)
        return max(1, attack - defense)
    
    def _get_set_bonuses(self, equipment_dict):
        """根据装备字典返回总加成（支持按件数分档激活）"""
        set_configs = self.config.get('setBonuses', [])
        if not set_configs:
            return {'attack': 0, 'defense': 0, 'hp': 0, 'skillDamage': 0}
        
        equipped_ids = set()
        for slot, item in equipment_dict.items():
            if item and item.get('id'):
                equipped_ids.add(item['id'])
        
        total_bonus = {'attack': 0, 'defense': 0, 'hp': 0, 'skillDamage': 0}
        for set_conf in set_configs:
            required = set(set_conf['required'])
            worn_count = len(required & equipped_ids)
            total_required = len(required)
            
            bonus_by_count = set_conf.get('bonusByCount')
            if bonus_by_count:
                thresholds = []
                for key, bonus in bonus_by_count.items():
                    if key == 'full':
                        thresholds.append((total_required, bonus, 'full'))
                    else:
                        thresholds.append((int(key), bonus, key))
                thresholds.sort(reverse=True, key=lambda x: x[0])
                for need, bonus, _ in thresholds:
                    if worn_count >= need:
                        for attr, val in bonus.items():
                            if attr in total_bonus:
                                total_bonus[attr] += val
                        break
            else:
                if required.issubset(equipped_ids):
                    bonus = set_conf.get('bonus', {})
                    for attr, val in bonus.items():
                        if attr in total_bonus:
                            total_bonus[attr] += val
        
        return total_bonus
    
    def _apply_ring_special_effects(self, character_type, character_ring, target_monster, damage, state, log_messages, attacker_attack_max):
        if character_ring.get('fire', 0) > 0 and random.random() < 0.1:
            burn_base = max(1, int(attacker_attack_max * 0.8))
            now = time.time()
            target_monster['burn_end_time'] = now + self.burn_duration
            target_monster['burn_tick_interval'] = self.burn_tick_interval
            target_monster['burn_damage_per_tick'] = burn_base
            target_monster['burn_last_tick_time'] = now
            target_monster['burn_owner'] = state['player']['name']   # 新增：设置归属玩家
            log_messages.append(('skill', f'{character_type}的火焰戒指触发，{target_monster["name"]} 被点燃（伤害{burn_base}，持续{self.burn_duration}秒）'))
        
        if character_ring.get('criticalDamage', 0) > 0 and random.random() < 0.05:
            extra = math.floor(target_monster.get('currentHp', 0) * 0.01)
            damage += extra
            log_messages.append(('damage', f'{character_type}的爆伤戒指触发，造成 {extra} 点额外伤害！'))
        
        if character_ring.get('critical', 0) > 0 and random.random() < 0.1:
            damage = math.floor(damage * 2.0)
            log_messages.append(('damage', f'{character_type}的暴击戒指触发，造成双倍伤害！'))
        
        return damage
    
    
    def _apply_weapon_special(self, weapon_item, damage, log_messages):
        """应用武器特效（双倍伤害），返回可能的新伤害值，并记录日志"""
        if not weapon_item:
            return damage
        special = weapon_item.get('special')
        if special == '双倍伤害' and random.random() < 0.1:
            damage *= 2
            log_messages.append(('damage', '武器双倍伤害触发！'))
        return damage
    
    def _apply_vampire(self, character_type, damage, character, character_ring, weapon_item, log_messages):
        """应用吸血效果，返回是否吸血以及回复量"""
        total_vampire_pct = 0
        if character_ring.get('vampire', 0) > 0 and random.random() < 0.3:
            total_vampire_pct += 0.2
            log_messages.append(('heal', f'{character_type}吸血戒指触发！'))
        if weapon_item and weapon_item.get('special') == '攻击吸血' and random.random() < 0.1:
            total_vampire_pct += 0.1
            log_messages.append(('heal', f'{character_type}武器吸血触发！'))
        if total_vampire_pct > 0:
            heal = math.floor(damage * total_vampire_pct)
            if heal > 0:
                character['hp'] = min(character.get('maxHp', 100), character['hp'] + heal)
                log_messages.append(('heal', f'{character_type}共恢复 {heal} 生命值'))
    
    def _apply_skill_effects(self, skill, target_monster, log_messages):
        """应用技能效果（冰冻等）"""
        if skill and skill.get('freezeChance') and random.random() < skill.get('freezeChance', 0):
            target_monster['freezeTurns'] = skill.get('freezeTurns', 2)
            log_messages.append(('skill', f'{target_monster["name"]} 被冰冻了！'))
    
    def _cast_boss_skill(self, monster, state, target_type, target, def_min, def_max, target_ring, hero_ring, skill, log_messages):
        """世界Boss释放技能"""
        skill_name = skill.get('name', '未知技能')
        log_messages.append(('skill', f'世界BOSS {monster.get("name", "未知BOSS")} 使用了【{skill_name}】！'))

        targets = []
        if skill.get('targetType') == 'all':
            if state['player'].get('hp', 0) > 0:
                targets.append(('player', state['player'], state['player'].get('finalDefenseMin', 0),
                                state['player'].get('finalDefenseMax', 0), target_ring))
            hero = state['hero']
            if hero.get('summoned', False) and hero.get('hp', 0) > 0:
                targets.append(('hero', hero, hero.get('finalDefenseMin', 0),
                                hero.get('finalDefenseMax', 0), hero_ring))
        else:  # single
            if target and target.get('hp', 0) > 0:
                targets.append((target_type, target, def_min, def_max, target_ring))
        
        for t_type, t, t_def_min, t_def_max, t_ring in targets:
            base_damage = self.calc_damage(
                monster['attackMin'], monster['attackMax'],
                t_def_min, t_def_max
            )
            damage = int(base_damage * skill.get('damageMultiplier', 1.0))

            if t_ring.get('protection', 0) > 0:
                damage = int(damage * (1 - t_ring['protection'] * 0.15))
            if t_ring.get('defense', 0) > 0:
                damage = int(damage * 0.9)

            t['hp'] = max(0, t['hp'] - damage)
            t_name = t.get('name', t_type)
            log_messages.append(('damage', f'{skill_name} 对 {t_name} 造成 {damage} 伤害'))

            if t_ring.get('reflect', 0) > 0:
                reflect_damage = int(damage * 0.15)
                if reflect_damage > 0:
                    monster['currentHp'] = max(0, monster['currentHp'] - reflect_damage)
                    log_messages.append(('damage', f'反射戒指对 {monster["name"]} 造成 {reflect_damage} 伤害'))
                    if monster['currentHp'] <= 0:
                        self._handle_monster_death(monster, state, killer_is_player=True)
            effect = skill.get('effect')
            if effect == 'stun':
                t['freezeTurns'] = skill.get('effectTurns', 1)
                log_messages.append(('skill', f'{t.get("name", t_type)} 被眩晕 {skill.get("effectTurns", 1)} 回合'))
            elif effect == 'fear':
                t['freezeTurns'] = skill.get('effectTurns', 2)
                log_messages.append(('skill', f'{t.get("name", t_type)} 陷入恐惧，无法行动'))
            elif effect == 'burn':
                burn_damage = skill.get('effectValue', int(monster['attackMax'] * 0.8))
                now = time.time()
                duration = skill.get('effectTurns', 3) * 2.0
                t['burn_end_time'] = now + duration
                t['burn_tick_interval'] = 2.0
                t['burn_damage_per_tick'] = burn_damage
                t['burn_last_tick_time'] = now
                t['burn_owner'] = state['player']['name']
                log_messages.append(('skill', f'{t.get("name", t_type)} 被点燃，每2秒损失 {burn_damage} 生命，持续 {duration} 秒'))
            if t_type == 'player' and t['hp'] <= 0:
                self._handle_player_death(state, log_messages)
            elif t_type == 'hero' and t['hp'] <= 0:
                self._handle_hero_death(state, log_messages)
    
    def _monster_perform_attack(self, monster, state, target_type=None, target=None, def_min=None, def_max=None, target_ring=None, hero_ring=None, log_messages=None):
        if log_messages is None:
            log_messages = state['log_messages']
        if target is None:
            if monster.get('isWorldBoss'):
                player = state['player']
                hero = state['hero']
                hero_alive = hero.get('summoned', False) and hero.get('hp', 0) > 0
                candidates = []
                if player.get('hp', 0) > 0:
                    candidates.append(('player', player))
                if hero_alive:
                    candidates.append(('hero', hero))
                if not candidates:
                    return
                target_type, target = random.choice(candidates)
            else:

                threat = monster.get('threat_table', {'player': 0, 'hero': 0})
                player_hp = state['player'].get('hp', 0)
                hero_alive = state['hero'].get('summoned', False) and state['hero'].get('hp', 0) > 0
                candidates = []
                if player_hp > 0:
                    candidates.append(('player', state['player'], threat.get('player', 0)))
                if hero_alive:
                    candidates.append(('hero', state['hero'], threat.get('hero', 0)))
                if not candidates:
                    return
                all_zero = all(score == 0 for _, _, score in candidates)
                if all_zero:
                    target_type, target, _ = random.choice(candidates)
                else:
                    candidates.sort(key=lambda x: (-x[2], x[0] != 'player'))
                    target_type, target, _ = candidates[0]

            if target_type == 'player':
                def_min, def_max = state['player'].get('finalDefenseMin', 0), state['player'].get('finalDefenseMax', 0)
                target_ring = self._get_special_ring_effect(state['equipment'], 'player')
            else:
                def_min, def_max = state['hero'].get('finalDefenseMin', 0), state['hero'].get('finalDefenseMax', 0)
                target_ring = self._get_special_ring_effect(state['equipment'], 'hero')

        monster_acc = monster.get('accuracy', 1)
        target_dodge = target.get('dodge', 1)
        base_hit = 0.95
        hit_rate_per_point = 0.05
        hit_chance = base_hit + (monster_acc - target_dodge) * hit_rate_per_point
        hit_chance = max(0.05, min(0.95, hit_chance))

        attacker_level = monster.get('level', 1)
        defender_level = target.get('level', 1)
        level_penalty = max(0, defender_level - attacker_level)
        acc_bonus = max(0, monster_acc - target_dodge)
        mitigation_per_point = 0.2
        effective_penalty = max(0, level_penalty - acc_bonus * mitigation_per_point)
        modifier = 1 - effective_penalty * self.level_hit_modifier
        modifier = max(0.05, min(1.0, modifier))

        final_hit = hit_chance * modifier
        final_hit = max(0.05, min(0.95, final_hit))

        if random.random() > final_hit:
            target_display = '玩家' if target_type == 'player' else '英雄'
            state['log_messages'].append(('miss', f'{monster.get("name", "未知怪物")} 的攻击未命中 {target_display}'))
            return
        if monster.get('isWorldBoss'):
            skills = monster.get('skills', [])
            for skill in skills:
                if random.random() < skill.get('chance', 0):
                    self._cast_boss_skill(monster, state, target_type, target, def_min, def_max,
                                          target_ring, hero_ring, skill, log_messages)
                    return
        if monster.get('freezeTurns', 0) > 0:
            log_messages.append(('info', f'{monster.get("name", "未知怪物")} 被冰冻，无法攻击'))
            return
        if target_ring.get('paralyze', 0) > 0 and random.random() < 0.1:
            now = time.time()
            monster['last_attack_time'] = now + 3
            log_messages.append(('skill', f'{target_type}的麻痹戒指触发，{monster.get("name")}({monster["uuid"]}) 无法攻击，持续3秒'))
            return

        dmg = self.calc_damage(monster['attackMin'], monster['attackMax'], def_min, def_max)

        if target_ring.get('protection', 0) > 0:
            dmg = math.floor(dmg * (1 - target_ring['protection'] * 0.15))
        if target_ring.get('defense', 0) > 0:
            dmg = math.floor(dmg * 0.9)

        target['hp'] = max(0, target['hp'] - dmg)

        target_display = '玩家' if target_type == 'player' else '英雄'
        if target.get('name'):
            target_display = target['name']
        log_messages.append(('damage', f'{monster.get("name", "未知怪物")} 攻击 {target_display} 造成 {dmg} 伤害'))
        if target_ring.get('reflect', 0) > 0:
            reflect_damage = math.floor(dmg * 0.15)
            if reflect_damage > 0:
                if monster.get('isWorldBoss'):
                    attacker_name = state['player']['name']
                    self.world_boss_manager.take_damage(attacker_name, state['currentMap'], reflect_damage, record=True)
                    boss = self.world_boss_manager.get_boss_raw(state['currentMap'])
                    if boss:
                        monster['currentHp'] = boss.get('currentHp', 0)
                else:
                    monster['currentHp'] = max(0, monster['currentHp'] - reflect_damage)
                log_messages.append(('damage', f'反射戒指对 {monster.get("name", "未知怪物")} 造成 {reflect_damage} 伤害'))
                if monster['currentHp'] <= 0:
                    self._handle_monster_death(monster, state, killer_is_player=True)
    
    def _handle_player_death(self, state, log_messages):
        player = state['player']
        equipment = state['equipment']['player']
        for slot in ['leftRing', 'rightRing']:
            item = equipment.get(slot)
            if item and item.get('specialEffect') == 'revive':
                player['hp'] = int(player.get('maxHp', 100) * 0.8)
                equipment[slot] = None
                log_messages.append(('combat', '复活戒指触发，玩家复活！'))
                return
        player_name = player.get('name', '玩家')
        log_messages.append(('player_death', f'💀 {player["name"]} 不幸阵亡！'))
    
    def _handle_hero_death(self, state, log_messages):
        hero = state['hero']
        equipment = state['equipment']['hero']
        for slot in ['leftRing', 'rightRing']:
            item = equipment.get(slot)
            if item and item.get('specialEffect') == 'revive':
                hero['hp'] = int(hero.get('maxHp', 80) * 0.8)
                equipment[slot] = None
                log_messages.append(('combat', '英雄复活戒指触发，英雄复活！'))
                return
        hero_name = hero.get('name', '英雄')
        log_messages.append(('combat', f'{hero_name} 战死！'))
    
    def _handle_monster_death(self, monster, state, killer_is_player):
        if monster.get('currentHp', 0) > 0:
            return
        if not monster.get('isAlive', True):
            return
        monster['isAlive'] = False

        if monster.get('isWorldBoss'):
            state['log_messages'].append(('loot', f'✨ 世界Boss {monster["name"]} 已被击败！将在结算后消失'))
            mg = state.get('monsterGroup')
            if mg and mg.get('monsters'):
                mg['monsters'] = [m for m in mg['monsters'] if m.get('uuid') != monster.get('uuid')]
                if not mg['monsters']:
                    state['monsterGroup'] = None
            return
    
        player = state['player']
        hero = state['hero']
        hero_alive = hero.get('summoned', False) and hero.get('hp', 0) > 0

        base_exp = monster.get('exp', 10)

        if hero_alive:
            player_base = base_exp // 2
            hero_base = base_exp - player_base
        else:
            player_base = base_exp
            hero_base = 0
        player_exp_mult = player.get('expMultiplier', 1)
        player_exp_bonus = player.get('total_exp_bonus', 0)
        player_exp = int(player_base * player_exp_mult * (1 + player_exp_bonus))

        hero_exp = 0
        if hero_alive:
            hero_exp_mult = hero.get('expMultiplier', 1)
            hero_exp_bonus = hero.get('total_exp_bonus', 0)
            hero_exp = int(hero_base * hero_exp_mult * (1 + hero_exp_bonus))

        player['exp'] += player_exp
        if hero_alive:
            hero['exp'] += hero_exp

        monster_gold = monster.get('gold', 5)
        player['gold'] += monster_gold

        state['log_messages'].append(('loot', f'击败 {monster["name"]}，获得 {player_exp} 经验, {monster_gold} 金币'))
        if hero_alive and hero_exp > 0:
            state['log_messages'].append(('loot', f'英雄获得 {hero_exp} 经验'))
        if monster.get('isBoss', False):
            if 'task' not in state:
                state['task'] = {'points': 0}
            points_gain = max(1, player['level'] // 20)
            state['task']['points'] = state['task'].get('points', 0) + points_gain
            state['log_messages'].append(('loot', f'击败BOSS获得 {points_gain} 积分！'))
        self._process_drops(state, monster)
        if self.map_stats_callback:
            self.map_stats_callback(state['currentMap'])
        self.update_task_progress(state, state['currentMap'], killed_count=1)
        self._clear_target_uuids(state, monster.get('uuid'))

    def _clear_target_uuids(self, state, monster_uuid):
        """清除指定怪物的UUID如果匹配当前目标"""
        player_uuid = state.get('player_current_target_uuid')
        if player_uuid and player_uuid == monster_uuid:
            state['player_current_target_uuid'] = None
        hero_uuid = state.get('hero_current_target_uuid')
        if hero_uuid and hero_uuid == monster_uuid:
            state['hero_current_target_uuid'] = None

    def combat(self, state):
        state.setdefault('log_messages', [])
        now = time.time()
        state = self._clean_expired_buffs(state)
        state = self._process_dot_effects(state)
        if 'finalAttackMin' not in state.get('player', {}):
            state = self._update_final_stats(state)
        if 'last_player_attack_time' not in state:
            state['last_player_attack_time'] = now
        if 'last_hero_attack_time' not in state:
            state['last_hero_attack_time'] = now

        player = state['player']
        hero = state['hero']
        equipment = state['equipment']
        current_map = state['currentMap']

        # ------------------ 怪物组管理 ------------------
        monster_group = state.get('monsterGroup')
        if not monster_group or not monster_group.get('monsters'):
            if self.monster_pool_getter:
                count = random.randint(1, self.config.get('maxMonstersPerBattle', 5))
                monster_list = self.monster_pool_getter(current_map, count)
                if not monster_list:
                    monster_list = self._select_monster_group_for_map(current_map)
            else:
                monster_list = self._select_monster_group_for_map(current_map)
            for m in monster_list:
                if 'last_attack_time' not in m:
                    m['last_attack_time'] = time.time() + random.uniform(0, 0.5)
            if not monster_list:
                return state
            monster_group = {'monsters': monster_list, 'uuid': generate_uuid()}
            state['monsterGroup'] = monster_group
            for m in monster_group['monsters']:
                m['last_attack_time'] = time.time() + random.uniform(0, 0.5)

        # 获取存活怪物列表（实时刷新）
        alive_monsters = [m for m in monster_group['monsters'] if m.get('currentHp', 0) > 0 and m.get('isAlive', True)]
        if not alive_monsters:
            state['monsterGroup'] = None
            return self.combat(state)   # 刷新怪物组

        hero_alive = hero.get('summoned', False) and hero.get('hp', 0) > 0

        # 预计算攻击/防御属性（避免重复计算）
        player_attack_min, player_attack_max = self._get_attack(state, 'player')
        player_defense_min, player_defense_max = self._get_defense(state, 'player')
        hero_attack_min = hero_attack_max = hero_defense_min = hero_defense_max = 0
        if hero_alive:
            hero_attack_min, hero_attack_max = self._get_attack(state, 'hero')
            hero_defense_min, hero_defense_max = self._get_defense(state, 'hero')

        player_ring = self._get_special_ring_effect(equipment, 'player')
        hero_ring = self._get_special_ring_effect(equipment, 'hero') if hero_alive else {}
        player_weapon = equipment['player'].get('weapon')
        hero_weapon = equipment['hero'].get('weapon') if hero_alive else None

        # 攻击间隔（秒）
        player_interval = self._calc_attack_interval(player, self.player_base_attack_interval)
        hero_interval = self._calc_attack_interval(hero, self.hero_base_attack_interval) if hero_alive else float('inf')
        monster_interval = self.monster_base_attack_interval

        def get_target(attacker_type, monsters):
            if attacker_type == 'player':
                target_uuid = state.get('player_current_target_uuid')
            else:
                target_uuid = state.get('hero_current_target_uuid')
            if target_uuid:
                for m in monsters:
                    if m.get('uuid') == target_uuid:
                        return m
            if not monsters:
                return None
            new_target = random.choice(monsters)
            if attacker_type == 'player':
                state['player_current_target_uuid'] = new_target.get('uuid')
            else:
                state['hero_current_target_uuid'] = new_target.get('uuid')
            return new_target

        # ------------------ 攻击执行函数（复用原逻辑） ------------------
        def perform_attack(attacker_type, entity, att_min, att_max, ring, weapon, skill_list, target_monster):
            """执行一次攻击，不处理死亡后的刷新（由外层负责）"""
            # 技能判定
            skill_used = None
            if skill_list and random.random() < 0.3:
                skill_entry = random.choice(skill_list)   # skill_entry 现在是轻量对象
                skill_id = skill_entry['id']
                full_skill = next((s for s in (self.config['playerSkills'] if attacker_type == '玩家' else self.config['heroSkills'])
                                   if s['id'] == skill_id), None)
                if full_skill and entity.get('mp', 0) >= full_skill.get('mp_cost', 0):
                    skill_used = full_skill.copy()        # 复制一份，避免修改静态数据
                    skill_used['enhance_level'] = skill_entry.get('enhance_level', 0)
                    skill_used['proficiency'] = skill_entry.get('proficiency', 0)
                    entity['mp'] -= full_skill.get('mp_cost', 0)
                    state['log_messages'].append(('skill', f'{attacker_type}使用【{skill_used["name"]}】（消耗{full_skill["mp_cost"]}MP）'))

            # 群攻 vs 单攻
            if skill_used and skill_used.get('targetType') in ('aoe', 'all'):
                targets = [m for m in alive_monsters if m.get('currentHp', 0) > 0]
                for t in targets:
                    self._character_attack(attacker_type, entity, att_min, att_max, ring, weapon, t, skill_used, state)
                    if t['currentHp'] <= 0:
                        self._handle_monster_death(t, state, killer_is_player=(attacker_type == '玩家'))
            else:
                if target_monster:
                    self._character_attack(attacker_type, entity, att_min, att_max, ring, weapon, target_monster, skill_used, state)
                    if target_monster['currentHp'] <= 0:
                        self._handle_monster_death(target_monster, state, killer_is_player=(attacker_type == '玩家'))

        any_attacked = False

        # 1. 玩家攻击判定
        if player['hp'] > 0 and (now - state['last_player_attack_time']) >= player_interval:
            # 获取当前存活怪物（实时）
            current_alive = [m for m in monster_group['monsters'] if m.get('currentHp', 0) > 0]
            if current_alive:
                target = get_target('player', current_alive)
                if target:
                    perform_attack('玩家', player, player_attack_min, player_attack_max,
                                   player_ring, player_weapon, player.get('skills', []), target)
                    state['last_player_attack_time'] = now
                    self._check_level_up(state, 'player')
                    state = self._update_final_stats(state)
                    any_attacked = True
                    # 玩家攻击后可能需要重新获取存活怪物（因为可能击杀）
                    monster_group = state['monsterGroup']
                    if monster_group:
                        alive_monsters = [m for m in monster_group['monsters'] if m.get('currentHp', 0) > 0]
                    else:
                        alive_monsters = []

        # 2. 英雄攻击判定（如果英雄存活且已召唤）
        if hero_alive and hero['hp'] > 0 and (now - state['last_hero_attack_time']) >= hero_interval:
            current_alive = [m for m in monster_group['monsters'] if m.get('currentHp', 0) > 0] if monster_group else []
            if current_alive:
                target = get_target('hero', current_alive)
                if target:
                    perform_attack('英雄', hero, hero_attack_min, hero_attack_max,
                                   hero_ring, hero_weapon, hero.get('skills', []), target)
                    state['last_hero_attack_time'] = now
                    self._check_level_up(state, 'hero')
                    state = self._update_final_stats(state)
                    any_attacked = True
                    # 更新存活怪物
                    if monster_group:
                        alive_monsters = [m for m in monster_group['monsters'] if m.get('currentHp', 0) > 0]

        # 3. 所有怪物攻击判定（每个怪物独立判断冷却）
        if monster_group:
            for monster in monster_group['monsters'][:]:
                if not monster.get('isAlive', True) or monster.get('currentHp', 0) <= 0:
                    continue
                if monster.get('freezeTurns', 0) > 0:
                    continue
                if not monster.get('active', False):
                    continue
                last_attack = monster.get('last_attack_time', 0)
                monster_interval_actual = monster.get('attack_interval', self.monster_base_attack_interval)
                if now - last_attack >= monster_interval_actual:
                    if player['hp'] <= 0 and (not hero_alive or hero['hp'] <= 0):
                        break
                    self._monster_perform_attack(monster, state, None, None, 0, 0,
                                                 player_ring, hero_ring, state['log_messages'])
                    monster['last_attack_time'] = now
                    any_attacked = True
                    if player['hp'] <= 0:
                        self._handle_player_death(state, state['log_messages'])
                        return state
                    if hero_alive and hero['hp'] <= 0:
                        self._handle_hero_death(state, state['log_messages'])
                        hero_alive = False
                    monster_group = state['monsterGroup']
                    if not monster_group:
                        break
                    now = time.time()

        return state
        
    def _character_attack(self, character_type, attacker, att_min, att_max, ring_effects, weapon_item, target, skill_used, state):
        attacker_acc = attacker.get('accuracy', 1)
        defender_dodge = target.get('dodge', 1)
        base_hit = 0.95
        hit_rate_per_point = 0.05
        hit_chance = base_hit + (attacker_acc - defender_dodge) * hit_rate_per_point
        hit_chance = max(0.05, min(0.95, hit_chance))
        attacker_level = attacker.get('level', 1)
        defender_level = target.get('level', 1)
        level_penalty = max(0, defender_level - attacker_level)
        acc_bonus = max(0, attacker_acc - defender_dodge)
        mitigation_per_point = 0.2
        effective_penalty = max(0, level_penalty - acc_bonus * mitigation_per_point)
        modifier = 1 - effective_penalty * self.level_hit_modifier
        modifier = max(0.05, min(1.0, modifier))
        hit_chance = hit_chance * modifier
        hit_chance = max(0.05, min(0.95, hit_chance))
        if random.random() > hit_chance:
            state['log_messages'].append(('miss', f'{character_type} 的攻击未命中 {target["name"]}'))
            return

        # 伤害计算（无视防御判断）
        ignore_defense = False
        if weapon_item and weapon_item.get('special') == '无视防御' and random.random() < 0.1:
            ignore_defense = True
            state['log_messages'].append(('damage', f'{character_type}武器无视防御触发！'))
        if ignore_defense:
            base_damage = self.calc_damage(att_min, att_max, 0, 0)
        else:
            base_damage = self.calc_damage(att_min, att_max,
                                           target.get('defenseMin', 1),
                                           target.get('defenseMax', 3))

        damage = base_damage
        if skill_used:
            mult = skill_used.get('damageMultiplier', 1.0)
            damage = math.floor(base_damage * mult)

            skill_damage_bonus = attacker.get('setBonusSkillDamage', 0)
            damage = math.floor(damage * (1 + skill_damage_bonus))

        damage = self._apply_weapon_special(weapon_item, damage, state['log_messages'])

        attacker_attack_max = attacker.get('finalAttackMax', att_max)
        damage = self._apply_ring_special_effects(character_type, ring_effects, target, damage, state, state['log_messages'], attacker_attack_max)

        if not target.get('isWorldBoss') and target.get('currentHp', 0) > 0:
            if random.random() < attacker.get('poisonRate', 0):
                dmg_per_tick = attacker.get('poisonDamage', 0)
                if dmg_per_tick > 0:
                    now = time.time()
                    target['poison_end_time'] = now + self.poison_duration
                    target['poison_tick_interval'] = self.poison_tick_interval
                    target['poison_damage_per_tick'] = dmg_per_tick
                    target['poison_last_tick_time'] = now
                    target['poison_owner'] = state['player']['name']
                    state['log_messages'].append(('skill', f'{character_type}的毒素使 {target["name"]} 中毒，每{self.poison_tick_interval}秒损失 {dmg_per_tick} 生命'))

        hit_crit = False
        if random.random() < attacker.get('critRate', 0):
            mult = attacker.get('critDamage', 1.5)
            damage = int(damage * mult)
            hit_crit = True
            state['log_messages'].append(('crit', f'{character_type} 暴击！造成 {damage} 伤害'))

        hit_lethal = False
        if random.random() < attacker.get('lethalRate', 0):
            mult = attacker.get('lethalDamage', 3.0)
            damage = int(damage * mult)
            hit_lethal = True
            state['log_messages'].append(('crit', f'{character_type} 致命一击！造成 {damage} 伤害'))

        crit_suffix = ''
        if hit_lethal:
            crit_suffix = '（致命一击）'
        elif hit_crit:
            crit_suffix = '（暴击）'

        if target.get('isWorldBoss'):
            player_name = state['player']['name']
            exists, killed = self.world_boss_manager.take_damage(player_name, state['currentMap'], damage, record=True)
            if not exists:
                target['currentHp'] = 0
                target['isAlive'] = False
                state['log_messages'].append(('damage', f'{target["name"]} 已被其他玩家击败！'))
            else:
                boss = self.world_boss_manager.get_boss_raw(state['currentMap'])
                if boss:
                    target['currentHp'] = boss.get('currentHp', 0)
                    target['currentMaxHp'] = boss.get('maxHp', 0)
                if target.get('currentHp', 0) > 0 and not target.get('active', False):
                    target['active'] = True
                    target['last_attack_time'] = time.time()
                state['log_messages'].append(('damage', f'{character_type}对 {target["name"]}({target["uuid"]}) 造成 {damage} 伤害{crit_suffix}'))
                if killed:
                    state['log_messages'].append(('combat', f'世界Boss {target["name"]} 已被击败，战斗即将结束'))
        else:
            target['currentHp'] = max(0, target['currentHp'] - damage)
            if target.get('currentHp', 0) > 0 and not target.get('active', False):
                target['active'] = True
                target['last_attack_time'] = time.time()
            if not target.get('isWorldBoss'):
                attacker_key = 'player' if character_type == '玩家' else 'hero'
                target['threat_table'][attacker_key] = target['threat_table'].get(attacker_key, 0) + damage
            state['log_messages'].append(('damage', f'{character_type}对 {target["name"]}({target["uuid"]}) 造成 {damage} 伤害{crit_suffix}'))

        # 吸血
        self._apply_vampire(character_type, damage, attacker, ring_effects, weapon_item, state['log_messages'])

        # 回蓝
        mana_recover = 0
        if weapon_item and weapon_item.get('special') == '攻击回蓝' and random.random() < 0.1:
            mana_recover = max(1, math.floor(damage * 0.1))
        if mana_recover > 0:
            max_mp = attacker.get('maxMp', 100)
            attacker['mp'] = min(max_mp, attacker.get('mp', 0) + mana_recover)
            state['log_messages'].append(('heal', f'{character_type}武器攻击回蓝，恢复 {mana_recover} 魔法'))

        # 技能效果（冰冻等）
        if skill_used:
            self._apply_skill_effects(skill_used, target, state['log_messages'])
    
    def _get_special_ring_effect(self, equipment, target):
        """获取特戒效果（一个装备仅生效一种效果）"""
        eq = equipment.get(target, {})
        item = eq.get('special_ring')
        if not item:
            return {}
        effect = item.get('specialEffect')
        if not effect:
            return {}
        # 效果映射（与原有保持一致）
        effect_map = {
            'paralyze': {'paralyze': 1},
            'vampire': {'vampire': 1},
            'critical': {'critical': 1},
            'fire': {'fire': 1},
            'criticalDamage': {'criticalDamage': 1},
            'protection': {'protection': 1},
            'defense': {'defense': 1},
            'reflect': {'reflect': 1}
        }
        return effect_map.get(effect, {})
    
    def _grant_exp_to_player(self, state, base_exp_value):
        player = state['player']
        hero = state['hero']
        hero_alive = hero.get('summoned', False) and hero.get('hp', 0) > 0
        if hero_alive:
            player_base = base_exp_value // 2
            hero_base = base_exp_value - player_base
        else:
            player_base = base_exp_value
            hero_base = 0
        player_exp = int(player_base * player.get('expMultiplier', 1) * (1 + player.get('total_exp_bonus', 0)))
        hero_exp = 0
        if hero_alive:
            hero_exp = int(hero_base * hero.get('expMultiplier', 1) * (1 + hero.get('total_exp_bonus', 0)))
        player['exp'] += player_exp
        if hero_alive:
            hero['exp'] += hero_exp
        return player_exp
    
    def _estimate_monster_damage(self, state):
        """估算当前地图怪物的平均单次伤害（用于动态阈值）"""
        monster_group = state.get('monsterGroup')
        if not monster_group or not monster_group.get('monsters'):
            return 0

        total_damage = 0
        count = 0
        # 获取玩家的最终防御（中值）
        player_def_min = state['player'].get('finalDefenseMin', 0)
        player_def_max = state['player'].get('finalDefenseMax', 0)
        player_def_avg = (player_def_min + player_def_max) / 2

        for monster in monster_group['monsters']:
            if monster.get('currentHp', 0) > 0:
                # 怪物攻击力中值
                mon_atk_min = monster.get('attackMin', 1)
                mon_atk_max = monster.get('attackMax', 1)
                mon_atk_avg = (mon_atk_min + mon_atk_max) / 2
                # 单次伤害估算（忽略命中、暴击等）
                dmg = max(1, mon_atk_avg - player_def_avg)
                total_damage += dmg
                count += 1

        return int(total_damage / count) if count > 0 else 0
    
    def _auto_use_potion(self, state, target):
        state.setdefault('log_messages', [])
        player = state['player']
        hero = state['hero']
        inventory = state['inventory']

        if target == 'player':
            entity = player
            max_hp = entity.get('maxHp', 100)
            max_mp = entity.get('maxMp', 100)
            name = '玩家'
        else:
            if not hero.get('summoned') or hero.get('hp', 0) <= 0:
                return
            entity = hero
            max_hp = entity.get('maxHp', 80)
            max_mp = entity.get('maxMp', 100)
            name = '英雄'

        hp_ratio = entity['hp'] / max_hp
        mp_ratio = entity.get('mp', 0) / max_mp
        if hp_ratio > 0.8 and mp_ratio > 0.5:
            return
        missing_hp = max_hp - entity['hp']
        missing_mp = max_mp - entity.get('mp', 0)
        candidates = []  # (score, index, item, hp_heal, mp_heal)
        for i, it in enumerate(inventory):
            if it.get('type') != 'potion':
                continue
            hp_val = it.get('hp', 0)
            mp_val = it.get('mp', 0)
            if hp_val == 0 and mp_val == 0:
                continue
            hp_heal = min(hp_val, missing_hp)
            mp_heal = min(mp_val, missing_mp)
            if hp_heal <= 0 and mp_heal <= 0:
                continue

            # 评分：双加药水优先（加一个较大的基数），单加按实际恢复量
            if hp_val > 0 and mp_val > 0:
                score = 10000 + hp_heal + mp_heal
            elif hp_val > 0:
                score = hp_heal
            else:
                score = mp_heal
            candidates.append((score, i, it, hp_heal, mp_heal))

        if not candidates:
            return

        # 按评分降序取最佳药水
        candidates.sort(key=lambda x: x[0], reverse=True)
        _, potion_idx, potion, hp_heal, mp_heal = candidates[0]

        # 应用恢复
        entity['hp'] = min(max_hp, entity['hp'] + potion.get('hp', 0))
        if potion.get('mp', 0) > 0:
            entity['mp'] = min(max_mp, entity.get('mp', 0) + potion['mp'])

        # 扣除药水
        if potion.get('quantity', 1) > 1:
            potion['quantity'] -= 1
        else:
            inventory.pop(potion_idx)

        # 日志
        msg_parts = []
        if hp_heal > 0:
            msg_parts.append(f'恢复{hp_heal}生命')
        if mp_heal > 0:
            msg_parts.append(f'恢复{mp_heal}魔法')
        msg = f'{name}自动使用{potion["name"]}，{"，".join(msg_parts)}'
        state['log_messages'].append(('heal', msg))
    
    def apply_quality_bonus(self, item, quality):
        drop_config = self.config.get('dropConfig', {}).get('equipmentDrop', {})
        quality_percent_bonus = drop_config.get('qualityPercentBonus')
        quality_min_bonus = drop_config.get('qualityBonus')
        
        if quality_percent_bonus is None or quality_min_bonus is None:
            logger.warning("品质百分比加成或最小加成配置缺失，将不应用品质加成")
            quality_percent_bonus = {}
            quality_min_bonus = {}
        
        bonus_percent = quality_percent_bonus.get(quality, 0)
        min_bonus_value = quality_min_bonus.get(quality, 0)
        
        # 判断原始模板是否存在有效攻击属性（攻击最小值或最大值 > 0）
        has_attack = (item.get('attackMin', 0) > 0) or (item.get('attackMax', 0) > 0)
        # 判断原始模板是否存在有效防御属性
        has_defense = (item.get('defenseMin', 0) > 0) or (item.get('defenseMax', 0) > 0)
        
        if bonus_percent > 0:
            if has_attack:
                base_attack_min = item.get('attackMin', 0)
                attack_bonus = max(min_bonus_value, math.floor(base_attack_min * bonus_percent))
                item['attackMin'] += attack_bonus
                item['attackMax'] += attack_bonus
            
            if has_defense:
                base_defense_min = item.get('defenseMin', 0)
                defense_bonus = max(min_bonus_value, math.floor(base_defense_min * bonus_percent))
                item['defenseMin'] += defense_bonus
                item['defenseMax'] += defense_bonus
        
        # 返回前不做过滤，过滤由调用方（生成装备后）统一处理
        return item
    
    def _drop_activity_item(self, state, activity_id, monster=None):
        item_info = activity_manager.get_activity_item(activity_id)
        if not item_info:
            return
        # 先尝试从现有的药水配置中查找模板
        potions = self.config.get('potions', [])
        template = next((p for p in potions if p.get('id') == item_info['id']), None)
        if not template:
            max_stack = item_info.get('max_stack', 9999)
            template = {
                'id': item_info['id'],
                'name': item_info['name'],
                'type': 'potion',
                'hp': 0,
                'price': 10,
                'description': item_info.get('description', '活动物品'),
                'maxStack': max_stack,
                'level': 1,
                'activity_item': True,
                'activity_id': activity_id,
                'icon': item_info.get('icon', '')
            }
        new_item = template.copy()
        new_item['uuid'] = generate_uuid()
        new_item['quantity'] = 1
        new_item['activity_id'] = activity_id
        new_item['droppedBy'] = monster.get('name', '活动掉落') if monster else '活动掉落'
        if 'icon' not in new_item or not new_item['icon']:
            new_item['icon'] = item_info.get('icon', '')
        # 确保 maxStack 存在
        if 'maxStack' not in new_item:
            new_item['maxStack'] = 9999
        if self._add_item_to_inventory(state, new_item):
            state['log_messages'].append(('loot', f'获得活动物品：{new_item["name"]}'))
    
    # 统一掉落处理（唯一版本）
    def _process_drops(self, state, monster):
        player = state['player']
        now = datetime.now()
        for drop_cfg in self.drop_configs:
            if not drop_cfg.get('enabled', True):
                continue
    
            # 时间范围检查（支持 time_range 或 start_time/end_time 两种写法）
            time_range = drop_cfg.get('time_range')
            if time_range:
                start_h, start_m = map(int, time_range['start'].split(':'))
                end_h, end_m = map(int, time_range['end'].split(':'))
                start_time = now.replace(hour=start_h, minute=start_m, second=0)
                end_time = now.replace(hour=end_h, minute=end_m, second=0)
                if not (start_time <= now <= end_time):
                    continue
            else:
                start_str = drop_cfg.get('start_time')
                end_str = drop_cfg.get('end_time')
                if start_str and end_str:
                    start_dt = datetime.strptime(start_str, '%Y-%m-%d %H:%M:%S')
                    end_dt = datetime.strptime(end_str, '%Y-%m-%d %H:%M:%S')
                    if not (start_dt <= now <= end_dt):
                        continue
    
            # 怪物ID检查
            monster_ids = drop_cfg.get('monster_ids')
            if monster_ids and monster.get('id') not in monster_ids:
                continue
    
            # 怪物等级区间（修正字段名）
            monster_level = monster.get('level', 0)
            min_level = drop_cfg.get('monster_level_min', 0)
            max_level = drop_cfg.get('monster_level_max', 9999)
            if not (min_level <= monster_level <= max_level):
                continue
    
            # 玩家等级区间（修正字段名）
            player_level = player.get('level', 0)
            player_min = drop_cfg.get('player_level_min', 0)
            player_max = drop_cfg.get('player_level_max', 9999)
            if not (player_min <= player_level <= player_max):
                continue
    
            # 整体掉落概率判定
            drop_chance = drop_cfg.get('drop_chance', 0)
            drop_bonus = state['player'].get('total_drop_bonus', 0)
            final_chance = drop_chance * (1 + drop_bonus)
            if random.random() >= final_chance:
                continue
    
            # 遍历物品配置，每个物品独立概率触发
            for item_cfg in drop_cfg.get('items', []):
                # 物品独立掉落概率（未配置则默认为1，即必定掉落）
                item_chance = item_cfg.get('item_drop_chance', 1.0)
                if random.random() >= item_chance:
                    continue
                self._create_drop_item(state, monster, item_cfg)
    
        # 斗笠/披风/马牌碎片随机掉落（5%）
        if random.random() < 0.05:
            frag_key = random.choice(['bambooHat_fragment', 'cloak_fragment', 'horseToken_fragment'])
            fragment = self.fragment_templates[frag_key].copy()
            fragment['uuid'] = generate_uuid()
            fragment['quantity'] = 1
            fragment['acquiredTime'] = int(time.time())
            fragment['droppedBy'] = monster.get('name', '未知怪物')
            if self._add_item_to_inventory(state, fragment):
                state['log_messages'].append(('loot', f'获得 {fragment["name"]}'))
    
        # ----- 活动物品掉落（新增）-----
        active_activities = self._get_active_drop_activities()
        for act_id in active_activities:
            # 活动配置中可自定义 drop_chance，默认为 0.1
            act_cfg = self.config.get('activities', {}).get(act_id, {})
            drop_chance = act_cfg.get('drop_chance', 0.1)
            if random.random() < drop_chance:
                self._drop_activity_item(state, act_id, monster)
                

    def _get_active_drop_activities(self):
        from datetime import datetime
        now = datetime.now()
        active_ids = []
        activities = self.config.get('activities', {})
        for act_id, act_cfg in activities.items():
            if not act_cfg.get('enabled', False):
                continue
            if act_cfg.get('type') != 'drop_and_use':
                continue
            start_str = act_cfg.get('start_time')
            end_str = act_cfg.get('end_time')
            if start_str and end_str:
                try:
                    start_dt = datetime.strptime(start_str, '%Y-%m-%d %H:%M:%S')
                    end_dt = datetime.strptime(end_str, '%Y-%m-%d %H:%M:%S')
                    if start_dt <= now <= end_dt:
                        active_ids.append(act_id)
                except:
                    continue
        return active_ids

    def generate_boss_drops(self, boss):
        from datetime import datetime
        now = datetime.now()
        drop_items = []

        for drop_cfg in self.drop_configs:
            if not drop_cfg.get('enabled', True):
                continue
            # 时间范围检查（略，与 _process_drops 相同）
            time_range = drop_cfg.get('time_range')
            if time_range:
                start_h, start_m = map(int, time_range['start'].split(':'))
                end_h, end_m = map(int, time_range['end'].split(':'))
                start_time = now.replace(hour=start_h, minute=start_m, second=0)
                end_time = now.replace(hour=end_h, minute=end_m, second=0)
                if not (start_time <= now <= end_time):
                    continue
            else:
                start_str = drop_cfg.get('start_time')
                end_str = drop_cfg.get('end_time')
                if start_str and end_str:
                    start_dt = datetime.strptime(start_str, '%Y-%m-%d %H:%M:%S')
                    end_dt = datetime.strptime(end_str, '%Y-%m-%d %H:%M:%S')
                    if not (start_dt <= now <= end_dt):
                        continue

            # 怪物ID检查（世界 Boss 的 id）
            monster_ids = drop_cfg.get('monster_ids')
            if monster_ids and boss.get('id') not in monster_ids:
                continue

            # 怪物等级区间
            boss_level = boss.get('level', 0)
            min_level = drop_cfg.get('monster_min_level', 0)
            max_level = drop_cfg.get('monster_max_level', 9999)
            if not (min_level <= boss_level <= max_level):
                continue

            # 整体掉落概率判定
            if random.random() >= drop_cfg.get('drop_chance', 0):
                continue

            # 遍历物品配置，每个物品独立概率触发
            for item_cfg in drop_cfg.get('items', []):
                item_chance = item_cfg.get('item_drop_chance', 1.0)
                if random.random() >= item_chance:
                    continue
                # 生成单个物品（不指定怪物，传入 None 或假怪物）
                item = self._create_drop_item_raw(boss, item_cfg)
                if item:
                    drop_items.append(item)

        return drop_items

    def _generate_potion_item(self, item_cfg, dropped_by=None):
        """生成药水物品（不处理数量），并设置来源"""
        item_type = item_cfg['type']
        if item_type == 'potion':
            item = self._generate_potion_by_config(item_cfg)
        elif item_type == 'random_potion':
            filter_ids = item_cfg.get('filter_ids', [])
            item = self._generate_random_potion(filter_ids if filter_ids else None)
        else:
            return None
        if item:
            if 'uuid' not in item:
                item['uuid'] = generate_uuid()
            if dropped_by:
                item['droppedBy'] = dropped_by
        return item
        
    def _create_drop_item_raw(self, boss, item_cfg):
        """与 _create_drop_item 类似，但不添加进背包，只返回物品对象"""
        item_type = item_cfg['type']
        quantity = item_cfg.get('quantity', 1)
        if 'quantity_min' in item_cfg:
            quantity = random.randint(item_cfg['quantity_min'], item_cfg['quantity_max'])

        # 药水/随机药水单独处理，直接返回（避免与其他装备逻辑混淆）
        if item_type in ('potion', 'random_potion'):
            dropped_by = boss.get('name', '世界Boss')
            item = self._generate_potion_item(item_cfg, dropped_by=dropped_by)
            if item:
                item['quantity'] = quantity
                if 'uuid' not in item:
                    item['uuid'] = generate_uuid()
                if 'droppedBy' not in item:
                    item['droppedBy'] = dropped_by
                return item
            return None

        # 装备/生肖/特戒等其他物品
        if item_type == 'equipment':
            item = self._generate_equipment_by_config(boss, item_cfg)
        elif item_type == 'equipment_random':
            item = self._generate_random_equipment_by_config(boss, item_cfg)
        elif item_type == 'special_ring':
            item = self._generate_special_ring()
        elif item_type == 'zodiac':
            item = self._generate_zodiac_by_config(item_cfg)
        elif item_type == 'random_zodiac':
            item = self._generate_random_zodiac(
                min_level=item_cfg.get('level_required_min', 1),
                max_level=item_cfg.get('level_required_max', 999)
            )
        elif item_type == 'random_special_ring':
            item = self._generate_special_ring()
        elif item_type == 'activity_item':
            item = self._generate_activity_item(item_cfg)
        else:
            return None

        if not item:
            return None

        # 非药水物品，quantity 通常为 1
        item['quantity'] = 1
        if 'uuid' not in item:
            item['uuid'] = generate_uuid()
        if 'droppedBy' not in item:
            item['droppedBy'] = boss.get('name', '世界Boss')
        return item
                
    def _create_drop_item(self, state, monster, item_cfg):
        if 'log_messages' not in state:
            state['log_messages'] = []
        item_type = item_cfg['type']
        quantity = item_cfg.get('quantity', 1)
        if 'quantity_min' in item_cfg:
            quantity = random.randint(item_cfg['quantity_min'], item_cfg['quantity_max'])

        # 药水单独处理
        if item_type in ('potion', 'random_potion'):
            dropped_by = monster.get('name', '未知怪物')
            item = self._generate_potion_item(item_cfg, dropped_by=dropped_by)
            if item:
                item['quantity'] = quantity
                if self._add_item_to_inventory(state, item):
                    state['log_messages'].append(('loot', f'获得 {item["name"]} x{quantity}'))
            return

        # 其他物品
        if item_type == 'equipment':
            item = self._generate_equipment_by_config(monster, item_cfg)
        elif item_type == 'equipment_random':
            item = self._generate_random_equipment_by_config(monster, item_cfg)
        elif item_type == 'special_ring':
            item = self._generate_special_ring()
        elif item_type == 'zodiac':
            item = self._generate_zodiac_by_config(item_cfg)
        elif item_type == 'random_zodiac':
            item = self._generate_random_zodiac(
                min_level=item_cfg.get('level_required_min', 1),
                max_level=item_cfg.get('level_required_max', 999)
            )
        elif item_type == 'random_special_ring':
            item = self._generate_special_ring()
        elif item_type == 'activity_item':
            item = self._generate_activity_item(item_cfg)
        else:
            return

        if not item:
            return

        for _ in range(quantity):
            new_item = item.copy()
            new_item['uuid'] = generate_uuid()
            if self._add_item_to_inventory(state, new_item):
                state['log_messages'].append(('loot', f'获得 {new_item["name"]}'))

    def _generate_random_potion(self, filter_ids=None, dropped_by=None):
        potions = self.config.get('potions', [])
        if not potions:
            return None
        if filter_ids:
            candidates = [p for p in potions if p.get('id') in filter_ids]
            if not candidates:
                candidates = potions
        else:
            candidates = potions
        potion = random.choice(candidates).copy()
        potion['uuid'] = generate_uuid()
        potion['quantity'] = 1
        if dropped_by:
            potion['droppedBy'] = dropped_by
        return potion
              
    def _generate_random_zodiac(self, min_level=None, max_level=None):
        """随机生成一个生肖，忽略等级限制，完全随机掉落"""
        zodiacs = self.config.get('zodiac', {}).get('zodiacs', [])
        if not zodiacs:
            logger.warning("没有可用的生肖配置")
            return None
        # 忽略 min_level/max_level，直接从所有生肖中随机
        zodiac = random.choice(zodiacs).copy()
        from zodiac import zodiac_manager
        return zodiac_manager.create_zodiac_item(zodiac['id'])
              
    def _generate_random_equipment_by_config(self, monster, item_cfg):
        """根据配置随机生成一件装备（从等级区间内随机）"""
        eq_data = self.config['equipmentData']
        all_items = []
        for cat, items in eq_data.items():
            if cat == 'specialRings':
                continue
            all_items.extend(items)

        # 兼容两种字段命名：level_range 或 levelRange
        level_range = item_cfg.get('level_range') or item_cfg.get('levelRange')
        if not level_range:
            level_range = [-10, 5]  # 默认范围

        player_level = monster.get('level', 1)
        min_lv = max(1, player_level + level_range[0])
        max_lv = player_level + level_range[1]
        # 防止等级范围过宽：限制最大等级不超过玩家等级+50（可配置）
        max_allowed = player_level + 50
        if max_lv > max_allowed:
            max_lv = max_allowed
        candidates = [item for item in all_items if min_lv <= item.get('level', 0) <= max_lv]
        if not candidates:
            # 如果没有符合等级的，回退到所有装备
            candidates = all_items

        template = random.choice(candidates)

        # 品质随机
        drop_config = self.config.get('dropConfig', {}).get('equipmentDrop', {})
        quality_weights = item_cfg.get('qualityWeights') or drop_config.get('qualityWeights', {'普通': 100, '极品': 5, '神品': 1, '超神品': 0.1})
        qualities = list(quality_weights.keys())
        weights = list(quality_weights.values())
        final_quality = random.choices(qualities, weights=weights)[0]

        new_item = template.copy()
        new_item['uuid'] = generate_uuid()
        new_item['enhanceLevel'] = 0
        new_item['quality'] = final_quality
        new_item['acquiredTime'] = int(time.time())
        new_item['droppedBy'] = monster.get('name', '未知怪物')

        if final_quality != '普通':
            self.apply_quality_bonus(new_item, final_quality)

        # 过滤零属性
        if new_item.get('attackMin', 0) == 0 and new_item.get('attackMax', 0) == 0:
            new_item.pop('attackMin', None)
            new_item.pop('attackMax', None)
        if new_item.get('defenseMin', 0) == 0 and new_item.get('defenseMax', 0) == 0:
            new_item.pop('defenseMin', None)
            new_item.pop('defenseMax', None)

        return new_item
        
    def _generate_activity_item(self, item_cfg):
        """根据配置生成活动物品"""
        activity_id = item_cfg.get('activity_id')
        if not activity_id:
            logger.warning("活动物品配置缺少 activity_id")
            return None
        
        item_info = activity_manager.get_activity_item(activity_id)
        if not item_info:
            logger.warning(f"活动 {activity_id} 未配置物品信息")
            return None
        
        potions = self.config.get('potions', [])
        template = next((p for p in potions if p.get('id') == item_info['id']), None)
        if not template:
            template = {
                'id': item_info['id'],
                'name': item_info['name'],
                'type': 'potion',
                'hp': 0,
                'price': 10,
                'description': item_info.get('description', '活动物品'),
                'maxStack': 999,
                'level': 1,
                'activity_item': True,
                'activity_id': activity_id,
                'icon': item_info.get('icon', '')
            }
        
        new_item = template.copy()
        new_item['uuid'] = generate_uuid()
        new_item['quantity'] = 1
        new_item['activity_id'] = activity_id
        new_item['droppedBy'] = '活动掉落'
        if 'icon' not in new_item or not new_item['icon']:
            new_item['icon'] = item_info.get('icon', '')
        return new_item
    
    def _check_level_up(self, state, target):
        if target == 'player':
            entity = state['player']
            skills_config = self.config['playerSkills']
        else:
            entity = state['hero']
            skills_config = self.config['heroSkills']
        
        if 'skills' not in entity:
            entity['skills'] = []
        
        rebirth = entity.get('rebirth', 0)
        max_level = self.rebirth_level_base + rebirth * self.rebirth_level_increment
        
        if entity['level'] >= max_level:
            if entity['exp'] >= entity['maxExp']:
                entity['exp'] = entity['maxExp'] - 1
            return state
        
        while entity['exp'] >= entity['maxExp']:
            if entity['level'] >= max_level:
                entity['exp'] = entity['maxExp'] - 1
                break
            
            entity['level'] += 1
            entity['exp'] -= entity['maxExp']
            entity['maxExp'] = math.floor(entity['maxExp'] * self.exp_growth_factor)
            
            # 生命值成长
            if 'baseMaxHp' not in entity:
                entity['baseMaxHp'] = entity.get('maxHp', 0)
            hp_increase = self.hp_growth_base + rebirth * self.hp_growth_per_rebirth
            entity['baseMaxHp'] += hp_increase
            entity['maxHp'] = entity['baseMaxHp']
            entity['hp'] = entity['maxHp']
            
            # 魔法值成长
            if 'baseMaxMp' not in entity:
                entity['baseMaxMp'] = entity.get('maxMp', 0)
            mp_increase = self.mp_growth_base + rebirth * self.mp_growth_per_rebirth
            entity['baseMaxMp'] += mp_increase
            entity['maxMp'] = entity['baseMaxMp']
            entity['mp'] = entity['maxMp']
            
            # 攻防成长
            ad_increase = self.ad_growth_per_level + rebirth * self.ad_growth_per_rebirth
            entity['attackMin'] += ad_increase
            entity['attackMax'] += ad_increase
            entity['defenseMin'] += ad_increase
            entity['defenseMax'] += ad_increase
            
            # 日志
            name = state['player']['name'] if target == 'player' else '英雄'
            state['log_messages'].append(('level', f'{name} 升级到 {entity["level"]} 级'))
            if entity['level'] % 10 == 0:
                state['log_messages'].append(('level_announce', f'✨ 恭喜 {name} 达到 {entity["level"]} 级！'))
            
            # 学习技能
            new_skill = next((s for s in skills_config if s['level'] == entity['level']), None)
            if new_skill:
                # 检查是否已学习（轻量格式）
                skill_exists = False
                for sk in entity['skills']:
                    sk_id = sk.get('id') if isinstance(sk, dict) else sk
                    if sk_id == new_skill['id']:
                        skill_exists = True
                        break
                if not skill_exists:
                    entity['skills'].append({
                        "id": new_skill['id'],
                        "enhance_level": 0,
                        "proficiency": 0
                    })
                    state['log_messages'].append(('skill', f'学习技能：{new_skill["name"]}'))
        
        if entity['level'] >= max_level and entity['exp'] >= entity['maxExp']:
            entity['exp'] = entity['maxExp'] - 1
        
        state = self._update_final_stats(state)
        return state
    
    def sell_all_by_quality(self, state, quality):
        inventory = state['inventory']
        total_gold = 0
        to_remove = []
        for i, item in enumerate(inventory):
            # 过滤掉药水、碎片、种子和果实
            if item.get('type') not in ('potion', 'fragment', 'seed', 'fruit') and item.get('quality', '普通') == quality:
                total_gold += math.floor(item['price'] / 2) * (item.get('quantity', 1))
                to_remove.append(i)
        for i in reversed(to_remove):
            inventory.pop(i)
        state['player']['gold'] += total_gold
        return state, f'出售{len(to_remove)}件{quality}装备，获得{total_gold}金币'
    
    def get_slot_keys_by_type(self, type_, item=None):
        mapping = {
            'weapon': ['weapon'],
            'armor': ['armor'],
            'helmet': ['helmet'],
            'necklace': ['necklace'],
            'medal': ['medal'],
            'belt': ['belt'],
            'shoes': ['shoes'],
            'bracelet': ['leftBracelet', 'rightBracelet'],
            'ring': ['leftRing', 'rightRing'],
            'special_ring': ['special_ring'],
            'bambooHat': ['bambooHat'],
            'cloak': ['cloak'],
            'horseToken': ['horseToken']
        }
        # 如果传入 item 且是特戒（type=special_ring 或者 ring带特效），则强制返回 special_ring 槽位
        if item and ((item.get('type') == 'special_ring') or 
                     (item.get('type') == 'ring' and item.get('specialEffect'))):
            return ['special_ring']
        return mapping.get(type_, [])
    
    def _add_item_to_inventory(self, state, item):
        inventory = state['inventory']
        max_slots = state.get('maxInventorySlots', 500)
        
        # 可堆叠物品类型
        if item['type'] in ('potion', 'seed', 'fruit', 'fragment'):
            remaining = item.get('quantity', 1)
            item_id = item.get('id')
            if item_id is None:
                # 缺少 ID 的物品直接添加（不堆叠）
                if len(inventory) >= max_slots:
                    return False
                inventory.append(item)
                return True
            
            # 确定最大堆叠数量
            max_stack = item.get('maxStack')
            if max_stack is None:
                if item['type'] == 'fragment':
                    max_stack = 9999
                else:
                    max_stack = 9999
            
            # 1. 尝试合并到已有格子
            for cand in inventory:
                if (cand.get('type') == item['type'] and 
                    str(cand.get('id')) == str(item_id) and   # 双方都转为字符串
                    cand.get('quantity', 0) < max_stack):
                    space = max_stack - cand['quantity']
                    if remaining <= space:
                        cand['quantity'] += remaining
                        remaining = 0
                        break
                    else:
                        cand['quantity'] = max_stack
                        remaining -= space
            
            # 2. 剩余数量创建新格子
            while remaining > 0:
                if len(inventory) >= max_slots:
                    return False
                new_qty = min(remaining, max_stack)
                new_item = item.copy()
                new_item['quantity'] = new_qty
                new_item['uuid'] = generate_uuid()
                inventory.append(new_item)
                remaining -= new_qty
            return True
        
        else:
            # 不可堆叠物品
            if len(inventory) >= max_slots:
                return False
            inventory.append(item)
            return True
    
    def _calc_enhance_cost(self, current_level, quality='普通'):
        base = self.enhance_cost_base
        multiplier = self.enhance_quality_multiplier.get(quality, 1.0)
        return int(base * (self.enhance_cost_exponent ** current_level) * multiplier)

    def _calc_enhance_success_rate(self, quality, current_level):
        # quality 暂未使用，但可扩展
        rate = 1.0 - self.enhance_success_decay * current_level
        return max(0.05, min(rate, 1.0))
    
    def _consume_fragment(self, state, fragment_id, required):
        inventory = state['inventory']
        remaining = required
        for i in range(len(inventory)-1, -1, -1):
            item = inventory[i]
            if item.get('type') == 'fragment' and str(item.get('id')) == str(fragment_id):
                qty = item.get('quantity', 1)
                if qty >= remaining:
                    if qty == remaining:
                        inventory.pop(i)
                    else:
                        item['quantity'] = qty - remaining
                    return True
                else:
                    remaining -= qty
                    inventory.pop(i)
        return False
    
    def get_enhance_info(self, state, slot_key, target):
        if target == 'player':
            equipment = state['equipment']['player']
        elif target == 'hero' and state['hero'].get('summoned'):
            equipment = state['equipment']['hero']
        else:
            return None, "目标无效"
        
        item = equipment.get(slot_key)
        if not item:
            return None, "该位置无装备"
        
        current_level = item.get('enhanceLevel', 0)
        quality = item.get('quality', '普通')
        cost = self._calc_enhance_cost(current_level, quality)
        success_rate = self._calc_enhance_success_rate(quality, current_level)
        
        # 判断是否为特戒
        is_special_ring = (item.get('type') == 'special_ring') or (item.get('type') == 'ring' and item.get('specialEffect'))
        if is_special_ring:
            fragment_type = 'special_ring_fragment'
            fragment_cost = self.config.get('enhanceCost', {}).get('special_ring_fragment_cost', 1)
            fragment_name = '特戒碎片'
        else:
            fragment_type = 'equip_fragment'
            fragment_cost = self.equip_fragment_cost
            fragment_name = '装备碎片'
        
        return {
            'currentLevel': current_level,
            'quality': quality,
            'cost': cost,
            'successRate': success_rate,
            'fragmentCost': fragment_cost,
            'fragmentType': fragment_type,
            'fragmentName': fragment_name
        }, None
    
    def enhance_equipment(self, state, slot_key, target, use_protect=False):
        last_enhance = state['player'].get('last_enhance_time', 0)
        current_time = time.time()
        if current_time - last_enhance < 1:
            return state, '操作过于频繁，请稍后再试'
        
        if target == 'player':
            equipment = state['equipment']['player']
            entity = state['player']
        elif target == 'hero' and state['hero'].get('summoned'):
            equipment = state['equipment']['hero']
            entity = state['hero']
        else:
            return state, '目标无效'
        
        item = equipment.get(slot_key)
        if not item:
            return state, '该位置无装备'
        
        current_level = item.get('enhanceLevel', 0)
        if current_level >= 20:
            return state, '装备强化等级已达最高（20级）'
        quality = item.get('quality', '普通')
        cost = self._calc_enhance_cost(current_level, quality)
        
        # 1. 检查金币
        if state['player']['gold'] < cost:
            return state, f'金币不足，需要{cost}金币'
        
        points = 0
        if use_protect:
            points = state.get('task', {}).get('points', 0)
            if points < 50:
                return state, '积分不足，需要50积分'
        
        # 2. 判断是否为特戒
        is_special_ring = (item.get('type') == 'special_ring') or (item.get('type') == 'ring' and item.get('specialEffect'))
        if is_special_ring:
            fragment_id = 'special_ring_fragment'
            fragment_cost = self.config.get('enhanceCost', {}).get('special_ring_fragment_cost', 1)
            fragment_name = '特戒碎片'
        else:
            fragment_id = 'equip_fragment'
            fragment_cost = self.equip_fragment_cost
            fragment_name = '装备碎片'
        
        # 3. 提前检查碎片是否足够（不实际扣除）
        total_fragments = 0
        for inv_item in state['inventory']:
            if inv_item.get('type') == 'fragment' and inv_item.get('id') == fragment_id:
                total_fragments += inv_item.get('quantity', 1)
        if total_fragments < fragment_cost:
            return state, f'{fragment_name}不足，需要{fragment_cost}个'
        
        # 4. 所有资源充足，开始扣除金币和积分
        state['player']['gold'] -= cost
        if use_protect:
            if 'task' not in state:
                state['task'] = {'points': 0}
            state['task']['points'] = points - 50
        
        # 5. 扣除碎片（前面已确保足够，一般不会失败）
        if not self._consume_fragment(state, fragment_id, fragment_cost):
            # 理论上不会失败，但为安全起见回滚资源
            state['player']['gold'] += cost
            if use_protect:
                state['task']['points'] += 50
            return state, f'{fragment_name}扣除失败，请重试'
        
        # 6. 继续原有强化逻辑（成功/失败判定）
        quality = item.get('quality', '普通')
        success_rate = self._calc_enhance_success_rate(quality, current_level)
        success = random.random() < success_rate
        
        old_level = current_level
        if success:
            new_level = current_level + 1
            item['enhanceLevel'] = new_level
            base_msg = f'{item["name"]} 强化至 +{new_level}（花费{cost}金币）'
            success_phrases = ["恭喜！", "太棒了！", "欧皇附体！", "人品爆发！", "幸运女神眷顾！"]
            message = random.choice(success_phrases) + base_msg
        else:
            if use_protect:
                new_level = current_level
                message = f'{item["name"]} 强化失败但保护生效，等级不变（花费{cost}金币）'
            else:
                if current_level > 0:
                    new_level = current_level - 1
                    item['enhanceLevel'] = new_level
                    base_msg = f'{item["name"]} 等级降为 +{new_level}（花费{cost}金币）'
                else:
                    new_level = 0
                    base_msg = f'{item["name"]} 等级不变（花费{cost}金币）'
                fail_phrases = ["惨了！", "悲剧了！", "天呐！", "这波亏大了！", "心疼！"]
                message = random.choice(fail_phrases) + base_msg
        
        # 属性更新
        level_diff = new_level - old_level
        if level_diff != 0:
            # 攻击类装备：仅增加攻击最大值
            if item['type'] in ['weapon', 'necklace', 'bracelet', 'ring', 'medal', 'special_ring', 'bambooHat']:
                if 'attackMax' in item and item['attackMax'] > 0:
                    item['attackMax'] += level_diff
            # 防御类装备：仅增加防御最大值
            if item['type'] in ['armor', 'helmet', 'belt', 'shoes', 'bambooHat']:
                if 'defenseMax' in item and item['defenseMax'] > 0:
                    item['defenseMax'] += level_diff
            # 特戒（specialEffect 兼容）
            if item.get('specialEffect'):
                if 'attackMax' in item and item['attackMax'] > 0:
                    item['attackMax'] += level_diff
                if 'defenseMax' in item and item['defenseMax'] > 0:
                    item['defenseMax'] += level_diff
            # 披风/马牌的生命/魔法加成不受影响，保持不变
            if item['type'] == 'cloak' and 'hpBonus' in item:
                item['hpBonus'] += level_diff
            if item['type'] == 'horseToken' and 'mpBonus' in item:
                item['mpBonus'] += level_diff
            if item.get('specialEffect'):
                if 'attackMin' in item and item['attackMin'] > 0:
                    item['attackMin'] += level_diff
                if 'attackMax' in item and item['attackMax'] > 0:
                    item['attackMax'] += level_diff
                if 'defenseMin' in item and item['defenseMin'] > 0:
                    item['defenseMin'] += level_diff
                if 'defenseMax' in item and item['defenseMax'] > 0:
                    item['defenseMax'] += level_diff
        self._invalidate_equipment_cache(item)
        state = self._update_final_stats(state)
        state['player']['last_enhance_time'] = current_time
        return state, message
    
    def _get_ring_special_effects(self, equipment):
        effects = []
        for slot in ['leftRing', 'rightRing']:
            item = equipment.get(slot)
            if item and item.get('type') == 'ring' and 'specialEffect' in item:
                effects.append(item['specialEffect'])
        return effects
    
    def _update_final_stats(self, state):
        """更新玩家和英雄的最终属性（带缓存）"""
        for target in ('player', 'hero'):
            if target == 'hero' and not state['hero'].get('summoned'):
                continue
            
            new_version = self._get_stats_version(state, target)
            cache_key = f'final_stats_{target}'
            cached = state.get('_cached_final_stats', {}).get(cache_key)
            
            if cached and cached.get('version') == new_version:
                final = cached['data']
            else:
                # 临时设置 _engine 供贡献者使用
                state['_engine'] = self
                
                final = {
                    'attack_min': 0, 'attack_max': 0,
                    'defense_min': 0, 'defense_max': 0,
                    'hp_max': 0, 'mp_max': 0,
                    'accuracy': 0, 'dodge': 0,
                    'game_speed': 1.0, 'exp_multiplier': 1.0,
                    'skill_damage': 0.0,
                    'exp_bonus': 0.0, 'drop_rate': 0.0,
                    'crit_rate': 0.0, 'crit_damage': 1.5,
                    'lethal_rate': 0.0, 'lethal_damage': 3.0,
                    'poison_rate': 0.0, 'poison_damage': 0,
                }
                for contributor in self.contributors:
                    contributor.contribute(state, target, final)
                
                # 属性边界处理
                final['game_speed'] = max(1.0, final['game_speed'])
                final['exp_multiplier'] = max(1.0, final['exp_multiplier'])
                final['accuracy'] = max(1, final['accuracy'])
                final['dodge'] = max(1, final['dodge'])
                final['exp_bonus'] = min(5.0, final['exp_bonus'])
                final['drop_rate'] = min(5.0, final['drop_rate'])
                final['skill_damage'] = min(2.0, final['skill_damage'])
                final['crit_rate'] = max(0.0, min(1.0, final['crit_rate']))
                final['crit_damage'] = max(1.0, final['crit_damage'])
                final['lethal_rate'] = max(0.0, min(1.0, final['lethal_rate']))
                final['lethal_damage'] = max(1.0, final['lethal_damage'])
                final['poison_rate'] = max(0.0, min(1.0, final['poison_rate']))
                final['poison_damage'] = max(0, final['poison_damage'])
                
                state.setdefault('_cached_final_stats', {})[cache_key] = {
                    'version': new_version,
                    'data': final
                }
                # 清除临时字段
                state.pop('_engine', None)
            
            # 将最终属性写回 entity
            entity = state[target]
            entity['finalAttackMin'] = final['attack_min']
            entity['finalAttackMax'] = final['attack_max']
            entity['finalDefenseMin'] = final['defense_min']
            entity['finalDefenseMax'] = final['defense_max']
            entity['maxHp'] = final['hp_max']
            if entity['hp'] > entity['maxHp']:
                entity['hp'] = entity['maxHp']
            entity['maxMp'] = final['mp_max']
            if entity['mp'] > entity['maxMp']:
                entity['mp'] = entity['maxMp']
            entity['accuracy'] = final['accuracy']
            entity['dodge'] = final['dodge']
            entity['gameSpeed'] = final['game_speed']
            entity['expMultiplier'] = final['exp_multiplier']
            entity['total_exp_bonus'] = final['exp_bonus']
            entity['total_drop_bonus'] = final['drop_rate']
            entity['setBonusSkillDamage'] = final['skill_damage']
            entity['critRate'] = final['crit_rate']
            entity['critDamage'] = final['crit_damage']
            entity['lethalRate'] = final['lethal_rate']
            entity['lethalDamage'] = final['lethal_damage']
            entity['poisonRate'] = final['poison_rate']
            entity['poisonDamage'] = final['poison_damage']
            # 保留基础值（用于临时增益叠加）
            if 'baseGameSpeed' not in entity:
                entity['baseGameSpeed'] = final['game_speed']
            if 'baseExpMultiplier' not in entity:
                entity['baseExpMultiplier'] = final['exp_multiplier']
            if 'baseAccuracy' not in entity:
                entity['baseAccuracy'] = final['accuracy']
            if 'baseDodge' not in entity:
                entity['baseDodge'] = final['dodge']
        
        return state

    def _compute_state_fingerprint(self, state):
        player = state['player']
        equip = state['equipment']['player']

        # 生成装备指纹：包括槽位、强化等级以及 refine 属性
        equip_fingerprint = []
        for k, v in sorted(equip.items()):
            if v is None:
                equip_fingerprint.append((k, 0, ()))  # 无装备
            else:
                # 提取 refine 属性列表（类型+值）
                refine_attrs = v.get('refine', {}).get('attrs', [])
                refine_tuple = tuple((ra['type'], ra['value']) for ra in refine_attrs)
                equip_fingerprint.append((k, v.get('enhanceLevel', 0), refine_tuple))
        equip_fingerprint = tuple(equip_fingerprint)

        data = (
            player.get('level', 0),
            player.get('rebirth', 0),
            player.get('attackEnhance', 0),
            player.get('defenseEnhance', 0),
            player.get('baseGameSpeed', 1),
            player.get('baseExpMultiplier', 1),
            player.get('baseAccuracy', 1),
            player.get('baseDodge', 1),
            equip_fingerprint,
            tuple(sorted(state.get('temporary_buffs', {}).items()))
        )
        return str(data)
        
    def _capture_combat_snapshot(self, state):
        """战斗前快照，仅记录会变化的少量字段"""
        return {
            'player_gold': state['player']['gold'],
            'player_hp': state['player']['hp'],
            'hero_hp': state['hero'].get('hp'),
            'inventory_len': len(state['inventory']),
            'warehouse_len': len(state.get('warehouse', [])),
            'monster_group': state.get('monsterGroup'),
            'player_exp': state['player']['exp'],
            'hero_exp': state['hero'].get('exp'),
        }

    def _compute_combat_diff(self, old_snapshot, new_state):
        """从快照和新状态计算差异（用于减少网络传输）"""
        diff = {}
        if old_snapshot['player_gold'] != new_state['player']['gold']:
            diff.setdefault('player', {})['gold'] = new_state['player']['gold']
        if old_snapshot['player_hp'] != new_state['player']['hp']:
            diff.setdefault('player', {})['hp'] = new_state['player']['hp']
        if old_snapshot['hero_hp'] != new_state['hero'].get('hp'):
            diff.setdefault('hero', {})['hp'] = new_state['hero'].get('hp')
        if old_snapshot['inventory_len'] != len(new_state['inventory']):
            diff['inventory'] = new_state['inventory']
        if old_snapshot['warehouse_len'] != len(new_state.get('warehouse', [])):
            diff['warehouse'] = new_state.get('warehouse')
        if old_snapshot['monster_group'] != new_state.get('monsterGroup'):
            diff['monsterGroup'] = new_state.get('monsterGroup')
        if old_snapshot['player_exp'] != new_state['player']['exp']:
            diff.setdefault('player', {})['exp'] = new_state['player']['exp']
        if old_snapshot['hero_exp'] != new_state['hero'].get('exp'):
            diff.setdefault('hero', {})['exp'] = new_state['hero'].get('exp')
        return diff
    
    def lottery(self, state, times):
        config = self.config
        lottery_conf = config.get('lottery', {})
        cost = lottery_conf.get('costPerTime', 1000)
        probabilities = lottery_conf.get('probabilities', [])
        
        total_cost = cost * times
        if state['player']['gold'] < total_cost:
            return state, {'error': f'金币不足，需要 {total_cost} 金币'}
        
        state['player']['gold'] -= total_cost
        
        results = []
        total_gold_reward = 0
        special_rings = []
        
        special_rings_config = config.get('equipmentData', {}).get('specialRings', [])
        
        if not special_rings_config:
            return state, {'error': '特戒配置为空'}
        
        for _ in range(times):
            r = random.random()
            selected_type = 'gold'
            for prob in probabilities:
                low, high = prob['range']
                if low <= r < high:
                    selected_type = prob['type']
                    break
            
            if selected_type == 'gold':
                gold_min = prob.get('min', 1)
                gold_max = prob.get('max', 500000)
                gold_reward = random.randint(gold_min, gold_max)
                state['player']['gold'] += gold_reward
                total_gold_reward += gold_reward
                results.append(('gold', gold_reward))
            
            elif selected_type == 'specialRing':
                ring = deepcopy(random.choice(special_rings_config))
                ring['uuid'] = generate_uuid()
                ring['enhanceLevel'] = 0
                ring['acquiredTime'] = int(time.time())
                ring['droppedBy'] = '抽奖获得'
                
                if len(state['inventory']) < state.get('maxInventorySlots', 500):
                    state['inventory'].append(ring)
                    special_rings.append(ring['name'])
                    results.append(('specialRing', ring['name']))
                    send_system(f"{state['player']['name']} 在抽奖中获得了特戒【{ring['name']}】！")
                else:
                    results.append(('info', '背包已满，特戒丢失'))
        
        result = {
            'results': results,
            'totalGoldReward': total_gold_reward,
            'specialRings': special_rings
        }
        
        return state, result
    
    def get_task(self, state):
        if 'task' not in state:
            state['task'] = {
                'completedCount': 0,
                'lastResetDate': time.strftime('%Y-%m-%d'),
                'lastAbandoned': 0,
                'currentTask': None,
                'points': 0
            }
        
        task_state = state['task']
        today = time.strftime('%Y-%m-%d')
        if task_state.get('lastResetDate') != today:
            task_state['completedCount'] = 0
            task_state['lastResetDate'] = today
        
        current_task = task_state.get('currentTask')
        return state, {'task': current_task, 'completedCount': task_state.get('completedCount', 0)}
    
    def generate_task(self, state):
        tasks_config = self.config.get('tasks', {})
        daily_limit = tasks_config.get('dailyLimit', 100)
        abandon_cooldown = tasks_config.get('abandonCooldown', 300)
        
        if 'task' not in state:
            state['task'] = {
                'completedCount': 0,
                'lastResetDate': time.strftime('%Y-%m-%d'),
                'lastAbandoned': 0,
                'currentTask': None,
                'points': 0
            }
        
        task_state = state['task']
        today = time.strftime('%Y-%m-%d')
        if task_state.get('lastResetDate') != today:
            task_state['completedCount'] = 0
            task_state['lastResetDate'] = today
        
        current_task = task_state.get('currentTask')
        if current_task:
            return state, {'task': current_task, 'completedCount': task_state.get('completedCount', 0)}
        
        if task_state.get('completedCount', 0) >= daily_limit:
            return state, {'error': '今日任务已达上限'}
        
        now = time.time()
        last_abandoned = task_state.get('lastAbandoned', 0)
        if now - last_abandoned < abandon_cooldown:
            remaining = int(abandon_cooldown - (now - last_abandoned))
            return state, {'error': f'任务冷却中，剩余{remaining}秒'}
        
        task_types = ['kill_monsters', 'submit_equipment']
        task_weights = [0.6, 0.4]
        task_type = random.choices(task_types, weights=task_weights)[0]
        
        if task_type == 'kill_monsters':
            eligible_maps = [m for m in self.config.get('maps', []) if m['minLevel'] <= state['player']['level']]
            if not eligible_maps:
                return state, {'error': '没有可用的地图'}
            chosen_map = random.choice(eligible_maps)
            task = {
                'type': 'kill_monsters',
                'map': chosen_map['id'],
                'required': tasks_config.get('killTaskRequired', 100),
                'current': 0,
                'difficultyLevel': chosen_map['minLevel']
            }
        else:
            all_equipment = []
            for cat, items in self.config['equipmentData'].items():
                if cat == 'specialRings':
                    continue
                all_equipment.extend(items)
            if not all_equipment:
                return state, {'error': '装备配置为空'}
            
            player_level = state['player']['level']
            eligible_equipment = [item for item in all_equipment if item['level'] <= player_level]
            
            chosen_item = random.choice(eligible_equipment)
            base_name = chosen_item['name']
            base_level = chosen_item['level']
            
            quality_weights = tasks_config.get('equipmentQualityWeights', {})
            qualities = list(quality_weights.keys())
            weights = list(quality_weights.values())
            required_quality = random.choices(qualities, weights=weights)[0]
            
            task = {
                'type': 'submit_equipment',
                'equipmentName': base_name,
                'requiredQuality': required_quality,
                'equipmentLevel': base_level,
                'difficultyLevel': base_level,
                'equipment': {
                    'name': base_name,
                    'quality': required_quality,
                    'level': base_level
                }
            }
        
        task_state['currentTask'] = task
        state['task'] = task_state
        return state, {'task': task, 'completedCount': task_state.get('completedCount', 0)}
    
    def complete_task(self, state):
        task_state = state.get('task', {})
        task = task_state.get('currentTask')
        if not task:
            return state, {'error': '没有当前任务'}
        
        player_level = state['player']['level']
        difficulty = task.get('difficultyLevel', player_level)
        
        difficulty_factor = max(0.5, min(1.0, difficulty / player_level))
        
        tasks_config = self.config.get('tasks', {})
        quality_multiplier = tasks_config.get('qualityRewardMultiplier',
                                              {'普通': 1, '极品': 1.5, '神品': 2, '超神品': 3})
        
        if task['type'] == 'kill_monsters':
            if task['current'] < task['required']:
                return state, {'error': '任务未完成'}
            base_gold = player_level * 10000
            base_exp = player_level * 10000
            reward_gold = int(base_gold * difficulty_factor)
            reward_exp = int(base_exp * difficulty_factor)
        else:
            equipment_name = task['equipmentName']
            required_quality = task.get('requiredQuality')
            found_index = None
            for i, item in enumerate(state['inventory']):
                # 必须是非药水、非碎片的装备
                if item.get('type') in ('potion', 'fragment'):
                    continue
                # 原逻辑：名称匹配 + 品质匹配（如果要求品质）
                if item.get('name') == equipment_name:
                    if required_quality is None or item.get('quality') == required_quality:
                        found_index = i
                        break
            if found_index is None:
                return state, {'error': f'背包中没有符合要求的{equipment_name}{"（"+required_quality+"）" if required_quality else ""}'}
            removed_item = state['inventory'].pop(found_index)
            actual_quality = removed_item.get('quality', '普通')
            mult = quality_multiplier.get(actual_quality, 1)
            
            base_gold = player_level * 1000
            base_exp = player_level * 1000
            reward_gold = int(base_gold * difficulty_factor * mult)
            reward_exp = int(base_exp * difficulty_factor * mult)
        
        state['player']['gold'] += reward_gold
        state['player']['exp'] += reward_exp
        if state['hero'].get('summoned'):
            state['hero']['exp'] += reward_exp
        
        if 'points' not in task_state:
            task_state['points'] = 0
        task_state['points'] += 1
        
        task_state['completedCount'] += 1
        daily_limit = tasks_config.get('dailyLimit', 100)
        
        if task_state['completedCount'] == daily_limit:
            fake_monster = {'level': player_level, 'name': '每日任务奖励'}
            self._drop_random_equipment(state, fake_monster)
            task_state['points'] += 10
            state['log_messages'].append(('loot', '今日任务完成100次，额外获得一件装备和10积分！'))
        
        task_state['currentTask'] = None
        state['task'] = task_state
        
        message = f'获得{reward_gold}金币和{reward_exp}经验，积分+1'
        if task_state['completedCount'] == daily_limit:
            message += '，并触发每日额外奖励！'
        return state, {'message': message}
    
    def abandon_task(self, state):
        task_state = state.get('task', {})
        if not task_state.get('currentTask'):
            return state, {'error': '没有当前任务'}
        task_state['lastAbandoned'] = time.time()
        task_state['currentTask'] = None
        state['task'] = task_state
        return state, {'message': '任务已放弃'}
    
    def update_task_progress(self, state, map_id, killed_count=1):
        task_state = state.get('task', {})
        task = task_state.get('currentTask')
        if task and task.get('type') == 'kill_monsters':
            if str(task.get('map')) == str(map_id):
                old_current = task.get('current', 0)
                new_current = min(old_current + killed_count, task.get('required', 100))
                if new_current != old_current:   # 只有进度变化时才触发回调
                    task['current'] = new_current
                    task_state['currentTask'] = task
                    state['task'] = task_state
                    if self.task_update_callback:
                        self.task_update_callback(state['player']['name'], task_state)
        return state
    
    def _drop_random_equipment(self, state, fake_monster):
        """每日任务完成100次时赠送一件随机装备（等级与玩家相关）"""
        state.setdefault('log_messages', [])   # 确保 log_messages 存在
        all_equipment = []
        for cat, items in self.config['equipmentData'].items():
            if cat == 'specialRings':
                continue
            all_equipment.extend(items)
        if not all_equipment:
            logger.warning("无装备配置，无法掉落奖励装备")
            return
        player_level = state['player']['level']
        # 装备等级范围：玩家等级 ±5，至少1级
        min_lv = max(1, player_level - 5)
        max_lv = player_level + 5
        candidates = [eq for eq in all_equipment if min_lv <= eq.get('level', 1) <= max_lv]
        if not candidates:
            candidates = all_equipment
        template = random.choice(candidates)
        new_item = template.copy()
        new_item['uuid'] = generate_uuid()
        new_item['enhanceLevel'] = 0
        new_item['quality'] = '普通'
        new_item['acquiredTime'] = int(time.time())
        new_item['droppedBy'] = '每日任务奖励'
        if self._add_item_to_inventory(state, new_item):
            state['log_messages'].append(('loot', f'获得任务奖励装备：{new_item["name"]}'))
        else:
            state['log_messages'].append(('error', '背包已满，无法获得奖励装备'))

    def _generate_equipment_by_level(self, min_level, max_level, quality):
        """根据等级区间和品质生成一件随机装备"""
        eq_data = self.config['equipmentData']
        all_items = []
        for cat, items in eq_data.items():
            if cat == 'specialRings':
                continue
            for item in items:
                if min_level <= item['level'] <= max_level:
                    all_items.append(item)
        if not all_items:
            return None
        base = random.choice(all_items).copy()
        base['uuid'] = generate_uuid()
        base['quality'] = quality
        base['enhanceLevel'] = 0
        base['acquiredTime'] = int(time.time())
        # 品质属性加成（简单示例）
        bonus_map = {'极品': 2, '神品': 5, '超神品': 10}
        bonus = bonus_map.get(quality, 0)
        if bonus:
            if 'attackMin' in base:
                base['attackMin'] += bonus
                base['attackMax'] += bonus
            if 'defenseMin' in base:
                base['defenseMin'] += bonus
                base['defenseMax'] += bonus
        return base
    
    def use_potion(self, state, identifier, target, expected_id=None, expected_name=None, is_uuid=False):
        inventory = state['inventory']
        if is_uuid:
            idx, item = self._find_item_by_uuid(inventory, identifier)
            if idx is None:
                return state, '物品不存在或已变化'
        else:
            idx = identifier
            if idx < 0 or idx >= len(inventory):
                return state, '物品不存在'
            item = inventory[idx]
            if expected_id is not None and item.get('id') != expected_id:
                return state, '物品已变化，请刷新后重试'
            if expected_name is not None and item.get('name') != expected_name:
                return state, '物品已变化，请刷新后重试'

        if item['type'] != 'potion':
            return state, '不是药水'

        if target == 'player':
            entity = state['player']
            max_hp = entity.get('maxHp', 100)
        elif target == 'hero' and state['hero'].get('summoned'):
            entity = state['hero']
            max_hp = entity.get('maxHp', 80)
        else:
            return state, '目标无效'

        entity['hp'] = min(max_hp, entity['hp'] + item.get('hp', 0))
        # 同时恢复 MP（只要物品有 mp 属性且数值 > 0）
        if item.get('mp', 0) > 0:
            max_mp = entity.get('maxMp', 100)
            entity['mp'] = min(max_mp, entity['mp'] + item['mp'])

        if item.get('quantity', 1) > 1:
            item['quantity'] -= 1
        else:
            inventory.pop(idx)
        
        return state, f'使用{item["name"]}成功'
    
    def equip_item(self, state, uuid, target, slot_key=None):
        inventory = state['inventory']
        idx, item = self._find_item_by_uuid(inventory, uuid)
        if idx is None:
            return state, '物品不存在或已变化'
        if item.get('type') == 'potion':
            return state, '不能装备药水'

        # 确定目标装备槽位字典和角色实体
        if target == 'player':
            equipment = state['equipment']['player']
            entity = state['player']
        else:
            equipment = state['equipment']['hero']
            entity = state['hero']

        allowed_slots = self.get_slot_keys_by_type(item['type'], item=item)
        if not allowed_slots:
            return state, '未知装备类型'

        # 如果没有指定 slot_key（例如双槽位物品由前端选择后传入），则取第一个可用槽位
        if not slot_key:
            slot_key = allowed_slots[0]
        elif slot_key not in allowed_slots:
            return state, '指定的槽位无效'

        # 等级检查（支持炼化降低需求）
        level_reduce = 0
        refine_attrs = item.get('refine', {}).get('attrs', [])
        for ra in refine_attrs:
            if ra['type'] == 'level_reduce':
                level_reduce += int(ra['value'])
        required_level = max(1, item.get('level', 1) - level_reduce)
        if entity['level'] < required_level:
            return state, f'等级不足，需要 {required_level} 级'

        # --- 核心替换逻辑 ---
        max_slots = state.get('maxInventorySlots', 500)
        old_item = equipment.get(slot_key)

        # 如果该槽位已有装备，尝试将其放回背包
        if old_item:
            if len(inventory) >= max_slots:
                return state, '背包已满，无法替换旧装备'
            self._invalidate_equipment_cache(old_item)   # 旧装备放回前失效缓存
            inventory.append(old_item)

        # 装备新物品前，也清理其可能遗留的缓存（安全起见）
        self._invalidate_equipment_cache(item)
        new_item = item.copy()
        equipment[slot_key] = new_item
        inventory.pop(idx)                     # 从背包移除

        # 重新计算角色最终属性
        state = self._update_final_stats(state)

        if old_item:
            return state, f'替换装备成功：{item["name"]} 已装备，{old_item["name"]} 已放入背包'
        else:
            return state, f'装备{item["name"]}成功'
    
    def unequip_item(self, state, slot_key, target):
        if target == 'player':
            equipment = state['equipment']['player']
        else:
            equipment = state['equipment']['hero']
        
        if slot_key not in equipment:
            return state, '无效槽位'
        
        item = equipment.get(slot_key)
        if not item:
            return state, '该位置无装备'
        
        if len(state['inventory']) >= state.get('maxInventorySlots', 500):
            return state, '背包已满'
        
        self._invalidate_equipment_cache(item)
        state['inventory'].append(item)
        equipment[slot_key] = None
        state = self._update_final_stats(state)
        return state, f'卸下{item["name"]}成功'
    
    def sell_item(self, state, identifier, is_uuid=False, expected_id=None, expected_name=None):
        inventory = state['inventory']
        if is_uuid:
            idx, item = self._find_item_by_uuid(inventory, identifier)
            if idx is None:
                return state, '物品不存在或已变化'
        else:
            idx = identifier
            if idx < 0 or idx >= len(inventory):
                return state, '物品不存在'
            item = inventory[idx]
            if expected_id is not None and item.get('id') != expected_id:
                return state, '物品已变化，请刷新后重试'
            if expected_name is not None and item.get('name') != expected_name:
                return state, '物品已变化，请刷新后重试'
        
        price = math.floor(item['price'] / 2) * (item.get('quantity', 1))
        state['player']['gold'] += price
        inventory.pop(idx)
        
        return state, f'出售{item["name"]}，获得{price}金币'
    
    def enhance(self, state, enhance_type):
        player = state['player']
        hero = state['hero']
        gold = player['gold']
        enhance_config = self.config.get('enhanceCost', {})
        
        if enhance_type == 'player_attack':
            current = player.get('attackEnhance', 0)
            if current >= 9999:
                return state, '攻击强化已达到最高'
            conf = enhance_config.get(enhance_type)
            if not isinstance(conf, dict) or 'base' not in conf or 'increment' not in conf:
                return state, '配置错误：攻击强化配置无效'
            cost = conf['base'] + current * conf['increment']
            if gold < cost:
                return state, f'金币不足，需要{cost}金币'
            player['gold'] -= cost
            player['attackEnhance'] = current + 1
            state = self._update_final_stats(state)
            return state, f'攻击强化提升到{current + 1}级，消耗{cost}金币'
        
        elif enhance_type == 'player_defense':
            current = player.get('defenseEnhance', 0)
            if current >= 9999:
                return state, '防御强化已达到最高'
            conf = enhance_config.get(enhance_type)
            if not isinstance(conf, dict) or 'base' not in conf or 'increment' not in conf:
                return state, '配置错误：防御强化配置无效'
            cost = conf['base'] + current * conf['increment']
            if gold < cost:
                return state, f'金币不足，需要{cost}金币'
            player['gold'] -= cost
            player['defenseEnhance'] = current + 1
            state = self._update_final_stats(state)
            return state, f'防御强化提升到{current + 1}级，消耗{cost}金币'
        
        elif enhance_type == 'hero_attack':
            if not hero.get('summoned'):
                return state, '请先召唤英雄'
            current = hero.get('attackEnhance', 0)
            if current >= 9999:
                return state, '英雄攻击强化已达到最高'
            conf = enhance_config.get(enhance_type)
            if not isinstance(conf, dict) or 'base' not in conf or 'increment' not in conf:
                return state, '配置错误：英雄攻击强化配置无效'
            cost = conf['base'] + current * conf['increment']
            if gold < cost:
                return state, f'金币不足，需要{cost}金币'
            player['gold'] -= cost
            hero['attackEnhance'] = current + 1
            state = self._update_final_stats(state)
            return state, f'英雄攻击强化提升到{current + 1}级，消耗{cost}金币'
        
        elif enhance_type == 'hero_defense':
            if not hero.get('summoned'):
                return state, '请先召唤英雄'
            current = hero.get('defenseEnhance', 0)
            if current >= 9999:
                return state, '英雄防御强化已达到最高'
            conf = enhance_config.get(enhance_type)
            if not isinstance(conf, dict) or 'base' not in conf or 'increment' not in conf:
                return state, '配置错误：英雄防御强化配置无效'
            cost = conf['base'] + current * conf['increment']
            if gold < cost:
                return state, f'金币不足，需要{cost}金币'
            player['gold'] -= cost
            hero['defenseEnhance'] = current + 1
            state = self._update_final_stats(state)
            return state, f'英雄防御强化提升到{current + 1}级，消耗{cost}金币'
        
        elif enhance_type == 'exp':
            exp_costs = enhance_config.get('exp_multiplier')
            if exp_costs is None or not isinstance(exp_costs, list):
                return state, '配置错误：缺少经验倍率强化配置'
            current_mult = player.get('expMultiplier', 1)
            if current_mult > len(exp_costs):
                return state, '经验倍率已达到最高'
            cost = exp_costs[current_mult - 1]
            if gold < cost:
                return state, f'金币不足，需要{cost}金币'
            player['gold'] -= cost
            new_mult = current_mult + 1
            player['expMultiplier'] = new_mult
            player['baseExpMultiplier'] = new_mult          # ✅ 同步基础值
            state = self._update_final_stats(state)
            return state, f'经验倍率提升到{new_mult}倍'
        
        elif enhance_type == 'speed':
            speed_costs = enhance_config.get('game_speed')
            if speed_costs is None or not isinstance(speed_costs, list):
                return state, '配置错误：缺少游戏速度强化配置'
            current_speed = player.get('baseGameSpeed', 1)
            if current_speed > len(speed_costs):
                return state, '游戏速度已达到最高'
            cost = speed_costs[current_speed - 1]
            if gold < cost:
                return state, f'金币不足，需要{cost}金币'
            player['gold'] -= cost
            player['baseGameSpeed'] = current_speed + 1
            state = self._update_final_stats(state)
            return state, f'游戏速度提升到{current_speed + 1}倍'
            
        elif enhance_type == 'player_accuracy':
            # 使用基础命中值，而不是最终命中值
            current = player.get('baseAccuracy', 1)
            if current >= 100:
                return state, '玩家命中已达到最高'
            conf = enhance_config.get(enhance_type)
            if not isinstance(conf, dict) or 'base' not in conf:
                return state, '配置错误：玩家命中强化配置无效'
            cost = conf['base'] + (current - 1) * conf.get('increment', 0)
            if gold < cost:
                return state, f'金币不足，需要{cost}金币'
            player['gold'] -= cost
            new_acc = current + 1
            player['accuracy'] = player.get('accuracy', 1) + 1   # 最终值同步增加
            player['baseAccuracy'] = new_acc
            state = self._update_final_stats(state)
            return state, f'玩家命中强化到{new_acc}级，消耗{cost}金币'

        elif enhance_type == 'player_dodge':
            current = player.get('baseDodge', 1)
            if current >= 100:
                return state, '玩家躲闪已达到最高'
            conf = enhance_config.get(enhance_type)
            if not isinstance(conf, dict) or 'base' not in conf:
                return state, '配置错误：玩家躲闪强化配置无效'
            cost = conf['base'] + (current - 1) * conf.get('increment', 0)
            if gold < cost:
                return state, f'金币不足，需要{cost}金币'
            player['gold'] -= cost
            new_dodge = current + 1
            player['dodge'] = player.get('dodge', 1) + 1
            player['baseDodge'] = new_dodge
            state = self._update_final_stats(state)
            return state, f'玩家躲闪强化到{new_dodge}级，消耗{cost}金币'

        elif enhance_type == 'hero_accuracy':
            if not hero.get('summoned'):
                return state, '请先召唤英雄'
            current = hero.get('baseAccuracy', 1)
            if current >= 100:
                return state, '英雄命中已达到最高'
            conf = enhance_config.get(enhance_type)
            if not isinstance(conf, dict) or 'base' not in conf:
                return state, '配置错误：英雄命中强化配置无效'
            cost = conf['base'] + (current - 1) * conf.get('increment', 0)
            if gold < cost:
                return state, f'金币不足，需要{cost}金币'
            player['gold'] -= cost
            new_acc = current + 1
            hero['accuracy'] = hero.get('accuracy', 1) + 1
            hero['baseAccuracy'] = new_acc
            state = self._update_final_stats(state)
            return state, f'英雄命中强化到{new_acc}级，消耗{cost}金币'

        elif enhance_type == 'hero_dodge':
            if not hero.get('summoned'):
                return state, '请先召唤英雄'
            current = hero.get('baseDodge', 1)
            if current >= 100:
                return state, '英雄躲闪已达到最高'
            conf = enhance_config.get(enhance_type)
            if not isinstance(conf, dict) or 'base' not in conf:
                return state, '配置错误：英雄躲闪强化配置无效'
            cost = conf['base'] + (current - 1) * conf.get('increment', 0)
            if gold < cost:
                return state, f'金币不足，需要{cost}金币'
            player['gold'] -= cost
            new_dodge = current + 1
            hero['dodge'] = hero.get('dodge', 1) + 1
            hero['baseDodge'] = new_dodge
            state = self._update_final_stats(state)
            return state, f'英雄躲闪强化到{new_dodge}级，消耗{cost}金币'
    
    def summon_hero(self, state):
        player = state['player']
        hero = state['hero']
        if player['level'] < 40:
            return state, '需要40级才能召唤英雄'
        if hero.get('summoned'):
            return state, '英雄已召唤'

        starter = self.hero_starting_stats.copy()
        starter['name'] = f"{player['name']}的英雄"
        starter['summoned'] = True
        # 确保所有必要字段存在
        starter.setdefault('skills', [])
        starter.setdefault('baseGameSpeed', 1)
        starter.setdefault('baseExpMultiplier', 1)
        starter.setdefault('baseAccuracy', 1)
        starter.setdefault('baseDodge', 1)
        starter.setdefault('maxExp', 100)
        starter.setdefault('exp', 0)
        starter.setdefault('hp', 80)
        starter.setdefault('maxHp', 80)
        starter.setdefault('mp', 40)
        starter.setdefault('maxMp', 40)
        starter.setdefault('attackMin', 4)
        starter.setdefault('attackMax', 8)
        starter.setdefault('defenseMin', 1)
        starter.setdefault('defenseMax', 3)
        starter.setdefault('rebirth', 0)
        starter.setdefault('attackEnhance', 0)
        starter.setdefault('defenseEnhance', 0)
        starter.setdefault('expMultiplier', 1.0)
        starter.setdefault('gameSpeed', 1.0)
        starter.setdefault('accuracy', 1)
        starter.setdefault('dodge', 1)
        starter.setdefault('critRate', 0.0)
        starter.setdefault('critDamage', 1.5)
        starter.setdefault('lethalRate', 0.0)
        starter.setdefault('lethalDamage', 3.0)
        starter.setdefault('poisonRate', 0.0)
        starter.setdefault('poisonDamage', 0)

        hero.clear()
        hero.update(starter)

        basic_skill = next((s for s in self.config['heroSkills'] if s['id'] == 1), None)
        if basic_skill:
            hero['skills'].append({"id": basic_skill['id'], "enhance_level": 0, "proficiency": 0})

        state = self._update_final_stats(state)
        return state, '召唤英雄成功'
    
    def _calc_base_stats(self, entity_type, rebirth_count):
        """根据转生次数计算1级时的基础属性（不含装备、强化）"""
        if entity_type == 'player':
            base = self.player_starting_stats.copy()
        else:
            base = self.hero_starting_stats.copy()
        
        # 应用转生加成
        base['attackMin'] += rebirth_count * self.rebirth_ad_per
        base['attackMax'] += rebirth_count * self.rebirth_ad_per
        base['defenseMin'] += rebirth_count * self.rebirth_ad_per
        base['defenseMax'] += rebirth_count * self.rebirth_ad_per
        base['maxHp'] += rebirth_count * self.rebirth_hp_per
        base['hp'] = base['maxHp']
        base['maxMp'] = self.rebirth_base_mp + rebirth_count * self.rebirth_mp_per
        base['mp'] = base['maxMp']
        return base
    
    def rebirth(self, state, target=None):
        player = state['player']
        hero = state['hero']
        if 'log_messages' not in state:
            state['log_messages'] = []

        def do_rebirth(entity, entity_type):
            rebirth_count = entity.get('rebirth', 0)
            req_level = self.rebirth_level_base + rebirth_count * self.rebirth_level_increment
            if entity['level'] < req_level:
                return False, f'需要{req_level}级才能转生'
            cost = self.rebirth_cost_base + rebirth_count * self.rebirth_cost_increment
            if state['player']['gold'] < cost:
                return False, f'金币不足，需要{cost}金币'

            state['player']['gold'] -= cost
            new_rebirth = rebirth_count + 1
            entity['rebirth'] = new_rebirth
            entity['level'] = 1
            entity['exp'] = 0
            entity['maxExp'] = int(self.rebirth_max_exp_base * (1 + self.rebirth_max_exp_growth * new_rebirth))

            base = self._calc_base_stats(entity_type, new_rebirth)
            for key in ['attackMin', 'attackMax', 'defenseMin', 'defenseMax', 'maxHp', 'hp', 'maxMp', 'mp']:
                if key in base:
                    entity[key] = base[key]
            entity['baseMaxHp'] = entity['maxHp']
            entity['baseMaxMp'] = entity['maxMp']
            entity['skills'] = []

            entity['expMultiplier'] = entity.get('expMultiplier', 1) + 1
            entity['baseExpMultiplier'] = entity.get('baseExpMultiplier', 1) + 1
            entity['gameSpeed'] = entity.get('gameSpeed', 1) + 1
            entity['baseGameSpeed'] = entity.get('baseGameSpeed', 1) + 1

            return True, f'成功转生至 {new_rebirth} 转'

        if target == 'player':
            success, msg = do_rebirth(player, 'player')
            if success:
                state['currentMap'] = 1
                state['monsterGroup'] = None
                map_data = next((m for m in self.config['maps'] if m['id'] == 1), None)
                if map_data and map_data['monsters']:
                    monster = self._select_monster_for_map(1)
                    if monster:
                        state['currentMonster'] = monster['id']
                        state['currentCombat'] = {
                            'monsterHp': monster['hp'],
                            'monsterMaxHp': monster['maxHp'],
                            'monsterUuid': generate_uuid()
                        }
                state['monsterBurnTurns'] = 0
                state = self._update_final_stats(state)
                state['log_messages'].append(('rebirth', f'🌟 {player["name"]} {msg}'))
                return state, msg
            else:
                return state, msg

        elif target == 'hero':
            if not hero.get('summoned'):
                return state, '英雄未召唤'
            success, msg = do_rebirth(hero, 'hero')
            if success:
                state = self._update_final_stats(state)
                state['log_messages'].append(('rebirth', f'🌟 {hero["name"]} {msg}'))
                return state, msg
            else:
                return state, msg

        else:   # 同时转生玩家和英雄
            messages = []
            player_req = self.rebirth_level_base + player.get('rebirth', 0) * self.rebirth_level_increment
            if player['level'] >= player_req:
                player_cost = self.rebirth_cost_base + player.get('rebirth', 0) * self.rebirth_cost_increment
                if state['player']['gold'] >= player_cost:
                    success, msg = do_rebirth(player, 'player')
                    if success:
                        messages.append(f'玩家{msg}')
                    else:
                        messages.append(f'玩家{msg}')
                else:
                    messages.append(f'玩家金币不足，需要{player_cost}金币')
            else:
                messages.append(f'玩家需要{player_req}级才能转生')

            if hero.get('summoned'):
                hero_req = self.rebirth_level_base + hero.get('rebirth', 0) * self.rebirth_level_increment
                if hero['level'] >= hero_req:
                    hero_cost = self.rebirth_cost_base + hero.get('rebirth', 0) * self.rebirth_cost_increment
                    if state['player']['gold'] >= hero_cost:
                        success, msg = do_rebirth(hero, 'hero')
                        if success:
                            messages.append(f'英雄{msg}')
                        else:
                            messages.append(f'英雄{msg}')
                    else:
                        messages.append(f'英雄金币不足，需要{hero_cost}金币')
                else:
                    messages.append(f'英雄需要{hero_req}级才能转生')
            else:
                messages.append('英雄未召唤')

            if any('转生' in m for m in messages):
                state['currentMap'] = 1
                state['monsterGroup'] = None
                state = self._update_final_stats(state)

            result_msg = '；'.join(messages)
            return state, result_msg
    
    def change_map(self, state, map_id):
        map_data = next((m for m in self.config['maps'] if m['id'] == map_id), None)
        if not map_data:
            return state, '地图不存在'

        old_map = state['currentMap']
        state['currentMap'] = map_id
    
        # 检查世界Boss
        boss = None
        if hasattr(self, 'world_boss_manager') and self.world_boss_manager:
            boss = self.world_boss_manager.get_boss_raw(map_id)
            if boss and boss.get('currentHp', 0) <= 0:
                boss = None
    
        if boss:
            boss_copy = deepcopy(boss)
            boss_copy['isWorldBoss'] = True
            boss_copy['isBoss'] = True
            boss_copy['currentHp'] = boss_copy.get('currentHp', boss_copy.get('maxHp', 0))
            boss_copy['isAlive'] = True
            boss_copy['last_attack_time'] = time.time()
            state['monsterGroup'] = {'monsters': [boss_copy], 'uuid': generate_uuid()}
        else:
            if self.monster_pool_getter:
                count = random.randint(1, self.config.get('maxMonstersPerBattle', 5))
                monster_list = self.monster_pool_getter(map_id, count)
                if not monster_list:
                    monster_list = self._select_monster_group_for_map(map_id)
            else:
                monster_list = self._select_monster_group_for_map(map_id)
            if monster_list:
                state['monsterGroup'] = {'monsters': monster_list, 'uuid': generate_uuid()}
            else:
                state['monsterGroup'] = None
    
        state['monsterBurnTurns'] = 0
        return state, f'传送到{map_data["name"]}'
    
    def buy_potion(self, state, potion_id, quantity=1):
        potion_id_str = str(potion_id)
        potion = next((p for p in self.config['potions'] if str(p.get('id', '')) == potion_id_str), None)
        if not potion:
            potion = next((p for p in self.config['potions'] if p.get('name') == potion_id_str), None)
        if not potion:
            return state, f'药水不存在 (id={potion_id})'
        
        total_price = potion['price'] * quantity
        if state['player']['gold'] < total_price:
            return state, f'金币不足，需要{total_price}金币'
        
        state['player']['gold'] -= total_price
        success_count = 0
        for i in range(quantity):
            item = potion.copy()
            item['quantity'] = 1
            item['uuid'] = generate_uuid()  # 确保有 uuid
            if self._add_item_to_inventory(state, item):
                success_count += 1
            else:
                break
        
        if success_count < quantity:
            refund = (quantity - success_count) * potion['price']
            state['player']['gold'] += refund
            return state, f'背包空间不足，只成功购买了{success_count}个{potion["name"]}，已退还{refund}金币'
        else:
            return state, f'购买{quantity}个{potion["name"]}成功'
    
    def sell_all_equipment(self, state):
        inventory = state['inventory']
        total_gold = 0
        to_remove = []
        for i, item in enumerate(inventory):
            # 过滤掉药水、碎片、种子和果实
            if item['type'] not in ('potion', 'fragment', 'seed', 'fruit'):
                total_gold += math.floor(item['price'] / 2) * (item.get('quantity', 1))
                to_remove.append(i)
        for i in reversed(to_remove):
            inventory.pop(i)
        state['player']['gold'] += total_gold
        return state, f'出售{len(to_remove)}件装备，获得{total_gold}金币'
    
    def sell_all_items(self, state):
        inventory = state['inventory']
        total_gold = 0
        to_remove = []
        for i, item in enumerate(inventory):
            if item['type'] == 'potion':
                total_gold += math.floor(item['price'] / 2) * (item.get('quantity', 1))
                to_remove.append(i)
        for i in reversed(to_remove):
            inventory.pop(i)
        state['player']['gold'] += total_gold
        return state, f'出售{len(to_remove)}件物品，获得{total_gold}金币'
    
    def repair_all(self, state):
        cost = 1000
        if state['player']['gold'] < cost:
            return state, f'金币不足，需要{cost}金币'
        state['player']['gold'] -= cost
        return state, f'维修完成，花费{cost}金币'
    
    def _find_item_by_uuid(self, items, uuid):
        for i, item in enumerate(items):
            if item.get('uuid') == uuid:
                return i, item
        return None, None
    
    def unlock_warehouse(self, state):
        if state['player']['level'] < 20:
            return state, '需要20级才能解锁仓库'
        if state.get('warehouseUnlocked'):
            return state, '仓库已解锁'
        state['warehouseUnlocked'] = True
        state['maxWarehouseSlots'] = 500          # 改为500
        return state, '仓库解锁成功'
    
    def warehouse_deposit(self, state, uuid=None, index=None, expected_id=None, expected_name=None):
        if not state.get('warehouseUnlocked'):
            return state, '仓库未解锁，请先解锁仓库'
    
        inventory = state['inventory']
        warehouse = state['warehouse']
        max_slots = state.get('maxWarehouseSlots', 500)
    
        # 定位背包中的物品
        if uuid is not None:
            idx, item = self._find_item_by_uuid(inventory, uuid)
            if idx is None:
                return state, '物品不存在或已变化'
        else:
            if index is None or index < 0 or index >= len(inventory):
                return state, '无效的物品索引'
            item = inventory[index]
            if expected_id is not None and item.get('id') != expected_id:
                return state, '物品已变化，请刷新后重试'
            if expected_name is not None and item.get('name') != expected_name:
                return state, '物品已变化，请刷新后重试'
            idx = index
    
        # 药品不能存入仓库
        if item.get('type') == 'potion':
            return state, '药品不能存入仓库'
    
        # ---------- 碎片合并 ----------
        if item.get('type') == 'fragment':
            fragment_id = item.get('id')
            max_stack = item.get('maxStack', 9999)
            quantity = item.get('quantity', 1)
    
            for existing in warehouse:
                if existing.get('type') == 'fragment' and existing.get('id') == fragment_id:
                    existing_qty = existing.get('quantity', 0)
                    if existing_qty < max_stack:
                        space = max_stack - existing_qty
                        if quantity <= space:
                            existing['quantity'] = existing_qty + quantity
                            inventory.pop(idx)
                            return state, f'{item["name"]}已存入仓库（合并）'
                        else:
                            existing['quantity'] = max_stack
                            quantity -= space
    
            if quantity > 0:
                if len(warehouse) >= max_slots:
                    return state, '仓库已满'
                new_item = item.copy()
                new_item['quantity'] = quantity
                new_item['uuid'] = generate_uuid()
                warehouse.append(new_item)
                inventory.pop(idx)
                return state, f'{item["name"]}已存入仓库'
            else:
                inventory.pop(idx)
                return state, f'{item["name"]}已全部合并存入仓库'
    
        # ---------- 果实合并（新增） ----------
        elif item.get('type') == 'fruit':
            fruit_id = item.get('id')
            max_stack = item.get('maxStack', 9999)
            quantity = item.get('quantity', 1)
    
            for existing in warehouse:
                if existing.get('type') == 'fruit' and existing.get('id') == fruit_id:
                    existing_qty = existing.get('quantity', 0)
                    if existing_qty < max_stack:
                        space = max_stack - existing_qty
                        if quantity <= space:
                            existing['quantity'] = existing_qty + quantity
                            inventory.pop(idx)
                            return state, f'{item["name"]}已存入仓库（合并）'
                        else:
                            existing['quantity'] = max_stack
                            quantity -= space
    
            if quantity > 0:
                if len(warehouse) >= max_slots:
                    return state, '仓库已满'
                new_item = item.copy()
                new_item['quantity'] = quantity
                new_item['uuid'] = generate_uuid()
                warehouse.append(new_item)
                inventory.pop(idx)
                return state, f'{item["name"]}已存入仓库'
            else:
                inventory.pop(idx)
                return state, f'{item["name"]}已全部合并存入仓库'
    
        # ---------- 其他物品（装备等）直接存入 ----------
        else:
            if len(warehouse) >= max_slots:
                return state, '仓库已满'
            warehouse.append(item.copy())
            inventory.pop(idx)
            return state, f'{item["name"]}已存入仓库'
    
    def warehouse_withdraw(self, state, uuid=None, index=None, expected_id=None, expected_name=None):
        if not state.get('warehouseUnlocked'):
            return state, '仓库未解锁'
        
        warehouse = state['warehouse']
        inventory = state['inventory']
        max_inv = state.get('maxInventorySlots', 500)
        
        # 根据参数确定物品位置
        if uuid is not None:
            idx, item = self._find_item_by_uuid(warehouse, uuid)
            if idx is None:
                return state, '物品不存在或已变化'
        else:
            if index is None or index < 0 or index >= len(warehouse):
                return state, '无效的物品索引'
            item = warehouse[index]
            if expected_id is not None and item.get('id') != expected_id:
                return state, '物品已变化，请刷新后重试'
            if expected_name is not None and item.get('name') != expected_name:
                return state, '物品已变化，请刷新后重试'
            idx = index
        
        if len(inventory) >= max_inv:
            return state, '背包已满'
        
        inventory.append(item)
        warehouse.pop(idx)
        
        return state, f'{item["name"]}已取出到背包'
    
    def warehouse_sell(self, state, uuid=None, index=None, expected_id=None, expected_name=None):
        if not state.get('warehouseUnlocked'):
            return state, '仓库未解锁'
        
        warehouse = state['warehouse']
        
        # 根据参数确定物品位置
        if uuid is not None:
            idx, item = self._find_item_by_uuid(warehouse, uuid)
            if idx is None:
                return state, '物品不存在或已变化'
        else:
            if index is None or index < 0 or index >= len(warehouse):
                return state, '无效的物品索引'
            item = warehouse[index]
            if expected_id is not None and item.get('id') != expected_id:
                return state, '物品已变化，请刷新后重试'
            if expected_name is not None and item.get('name') != expected_name:
                return state, '物品已变化，请刷新后重试'
            idx = index
        
        price = math.floor(item['price'] / 2) * (item.get('quantity', 1))
        state['player']['gold'] += price
        warehouse.pop(idx)
        
        return state, f'出售{item["name"]}，获得{price}金币'
        
    def equip_zodiac(self, state, zodiac_uuid, zodiac_id):
        """装备生肖：从背包取出，放入 zodiac_equipped 字典，需检查槽位是否已激活"""
        inventory = state['inventory']
        idx, item = self._find_item_by_uuid(inventory, zodiac_uuid)
        if idx is None:
            return state, '物品不存在'
        if item.get('type') != 'zodiac':
            return state, '这不是生肖物品'
        if item.get('id') != zodiac_id:
            return state, f'该物品不属于{zodiac_id}号生肖槽位'
    
        # 统一使用字符串键
        zodiac_id_str = str(zodiac_id)
    
        # 1. 根据转生次数确定已激活的槽位集合
        rebirth = state['player'].get('rebirth', 0)
        if rebirth == 0:
            active_slots = set()
        elif rebirth == 1:
            active_slots = {str(i) for i in range(1, 5)}   # "1","2","3","4"
        elif rebirth == 2:
            active_slots = {str(i) for i in range(1, 9)}   # "1"-"8"
        else:
            active_slots = {str(i) for i in range(1, 13)}  # "1"-"12"
    
        if zodiac_id_str not in active_slots:
            return state, f'当前转生{rebirth}转未激活生肖槽位{zodiac_id}，无法装备'
    
        # 2. 检查是否已被装备
        zodiac_equipped = state.get('zodiac_equipped', {})
        if zodiac_id_str in zodiac_equipped:
            return state, '该生肖槽位已装备'
    
        # 3. 检查等级要求
        if state['player']['level'] < item.get('level_required', 1):
            return state, f'等级不足，需要{item.get("level_required")}级'
    
        # 执行装备
        inventory.pop(idx)
        zodiac_equipped[zodiac_id_str] = item
        state['zodiac_equipped'] = zodiac_equipped
        state = self._update_final_stats(state)
        
        return state, f'成功装备生肖：{item["name"]}'

    def unequip_zodiac(self, state, zodiac_id):
        """卸下生肖：从 zodiac_equipped 取出放回背包"""
        zodiac_equipped = state.get('zodiac_equipped', {})
        zodiac_id_str = str(zodiac_id)   # 统一转为字符串
        if zodiac_id_str not in zodiac_equipped:
            return state, '该槽位未装备生肖'
        item = zodiac_equipped.pop(zodiac_id_str)
        if not self._add_item_to_inventory(state, item):
            zodiac_equipped[zodiac_id_str] = item
            return state, '背包已满，无法卸下'
        state['zodiac_equipped'] = zodiac_equipped
        state = self._update_final_stats(state)
        
        return state, f'成功卸下生肖：{item["name"]}'

    def _generate_equipment_by_config(self, monster, item_cfg):
        equip_name = item_cfg.get('name')
        equip_id = item_cfg.get('id')
        if equip_id is not None:
            equip_id = str(equip_id)
        quality = item_cfg.get('quality', 'random')

        eq_data = self.config['equipmentData']
        all_items = []
        for cat, items in eq_data.items():
            if cat == 'specialRings':
                continue
            all_items.extend(items)

        template = None
        if equip_name:
            template = next((item for item in all_items if item.get('name') == equip_name), None)
            if not template:
                logger.warning(f"未找到名称为 {equip_name} 的装备，使用随机装备")
        elif equip_id:
            template = next((item for item in all_items if item.get('id') == equip_id), None)
            if not template:
                logger.warning(f"未找到ID为 {equip_id} 的装备，使用随机装备")

        if not template:
            # 回退到随机装备逻辑
            level_range = item_cfg.get('level_range') or item_cfg.get('levelRange') or [-10, 5]
            player_level = monster.get('level', 1)
            min_lv = max(1, player_level + level_range[0])
            max_lv = player_level + level_range[1]
            max_allowed = player_level + 50
            if max_lv > max_allowed:
                max_lv = max_allowed
            candidates = [item for item in all_items if min_lv <= item.get('level', 0) <= max_lv]
            if not candidates:
                candidates = all_items
            template = random.choice(candidates)

        # 确定品质
        if quality == 'random':
            drop_config = self.config.get('dropConfig', {}).get('equipmentDrop', {})
            quality_weights = item_cfg.get('qualityWeights') or drop_config.get('qualityWeights', {'普通': 100, '极品': 5, '神品': 1, '超神品': 0.1})
            qualities = list(quality_weights.keys())
            weights = list(quality_weights.values())
            final_quality = random.choices(qualities, weights=weights)[0]
        else:
            final_quality = quality

        new_item = template.copy()
        new_item['uuid'] = generate_uuid()
        new_item['enhanceLevel'] = 0
        new_item['quality'] = final_quality
        new_item['acquiredTime'] = int(time.time())
        new_item['droppedBy'] = monster.get('name', '未知怪物')

        if final_quality != '普通':
            self.apply_quality_bonus(new_item, final_quality)

        # 过滤零属性
        if new_item.get('attackMin', 0) == 0 and new_item.get('attackMax', 0) == 0:
            new_item.pop('attackMin', None)
            new_item.pop('attackMax', None)
        if new_item.get('defenseMin', 0) == 0 and new_item.get('defenseMax', 0) == 0:
            new_item.pop('defenseMin', None)
            new_item.pop('defenseMax', None)

        return new_item

    def _generate_potion_by_config(self, item_cfg, dropped_by=None):
        """根据配置生成药水，可指定掉落来源"""
        potion_id = item_cfg.get('id')
        if not potion_id:
            return None
        potion = next((p for p in self.config['potions'] if p.get('id') == potion_id), None)
        if not potion:
            return None
        new_item = potion.copy()
        new_item['uuid'] = generate_uuid()
        if dropped_by:
            new_item['droppedBy'] = dropped_by
        return new_item

    def _generate_special_ring(self):
        """随机生成一个特戒"""
        special_rings = self.config.get('equipmentData', {}).get('specialRings', [])
        if not special_rings:
            return None
        ring = random.choice(special_rings).copy()
        ring['uuid'] = generate_uuid()
        ring['enhanceLevel'] = 0
        ring['acquiredTime'] = int(time.time())
        ring['droppedBy'] = '世界Boss掉落'
        return ring

    def _generate_zodiac_by_config(self, item_cfg):
        """根据配置生成生肖"""
        zodiac_id = item_cfg.get('zodiac_id')
        if zodiac_id == 'random':
            # 完全随机选择一个生肖，忽略等级限制
            zodiacs = self.config.get('zodiac', {}).get('zodiacs', [])
            if not zodiacs:
                return None
            zodiac = random.choice(zodiacs)
            zodiac_id = zodiac['id']
        else:
            zodiac_id = int(zodiac_id)

        from zodiac import zodiac_manager
        return zodiac_manager.create_zodiac_item(zodiac_id)
        
    def enhance_zodiac(self, state, slot_id):
        zodiac_equipped = state.get('zodiac_equipped', {})
        slot_id_str = str(slot_id)
        item = zodiac_equipped.get(slot_id_str)
        if not item:
            return state, "该槽位未装备生肖"
        
        current_level = item.get('enhanceLevel', 0)
        if current_level >= 20:
            return state, "生肖已达最高强化等级20级"
        
        cost = 100000 * (current_level + 1)
        if state['player']['gold'] < cost:
            return state, f"金币不足，需要{cost}金币"
        
        # 新增：扣除生肖碎片
        if not self._consume_fragment(state, 'zodiac_fragment', self.zodiac_fragment_cost):
            return state, f"生肖碎片不足，需要{self.zodiac_fragment_cost}个"
        
        success_rate = max(0.05, 1.0 - 0.05 * current_level)
        success = random.random() < success_rate
        
        if success:
            new_level = current_level + 1
            item['enhanceLevel'] = new_level
            item['attackMin'] = item.get('attackMin', 0) + 1
            item['attackMax'] = item.get('attackMax', 0) + 1
            item['defenseMin'] = item.get('defenseMin', 0) + 1
            item['defenseMax'] = item.get('defenseMax', 0) + 1
            item['hpBonus'] = item.get('hpBonus', 0) + 50
            self._invalidate_equipment_cache(item)
            message = f"生肖 {item['name']} 强化成功，等级 +{new_level}"
        else:
            message = f"生肖 {item['name']} 强化失败，消耗{cost}金币"
        
        state['player']['gold'] -= cost
        state['zodiac_equipped'][slot_id_str] = item
        state = self._update_final_stats(state)
        
        return state, message
        
    def decompose_item(self, state, uuid):
        """分解物品：装备按等级*品质倍数得装备碎片，特戒得特戒碎片，生肖得生肖碎片。装备需≥40级"""
        inventory = state['inventory']
        idx, item = self._find_item_by_uuid(inventory, uuid)
        if idx is None:
            return state, '物品不存在'

        item_type = item.get('type')
        # 禁止分解斗笠、披风、马牌
        if item_type in ('bambooHat', 'cloak', 'horseToken'):
            return state, '该装备无法分解'

        equip_types = ['weapon', 'armor', 'helmet', 'necklace', 'medal', 'bracelet', 'ring', 'belt', 'shoes']
        is_special_ring = item_type == 'special_ring' or (item_type == 'ring' and item.get('specialEffect'))
        is_equip = item_type in equip_types and not is_special_ring
        is_zodiac = item_type == 'zodiac'

        if not is_equip and not is_zodiac and not is_special_ring:
            return state, '该物品无法分解'

        # 装备分解必须 ≥40 级
        if is_equip:
            equip_level = item.get('level', 1)
            if equip_level < 40:
                return state, '装备等级不足40级，无法分解'
        
        # 计算碎片类型和数量
        if is_zodiac:
            fragment_id = 'zodiac_fragment'
            fragment_name = '生肖碎片'
            quantity = 1
            emoji = '🐉'
        elif is_special_ring:
            fragment_id = 'special_ring_fragment'
            fragment_name = '特戒碎片'
            quantity = 1
            emoji = '💍'
        else:  # 普通装备
            fragment_id = 'equip_fragment'
            fragment_name = '装备碎片'
            level = item.get('level', 1)
            quality = item.get('quality', '普通')
            quality_mult = {'普通': 1, '极品': 2, '神品': 3, '超神品': 4}.get(quality, 1)
            quantity = level * quality_mult
            emoji = '🔧'
        
        # 创建碎片物品（支持堆叠）
        fragment = {
            'id': fragment_id,
            'name': fragment_name,
            'type': 'fragment',
            'subtype': fragment_id,          # 便于区分
            'quantity': quantity,
            'price': 0,
            'maxStack': 9999,
            'description': f'分解获得，可用于合成',
            'icon': emoji,                   # 直接存储 emoji
            'acquiredTime': int(time.time()),
            'droppedBy': '分解获得'
        }
        
        # 添加到背包（自动堆叠）
        success = self._add_item_to_inventory(state, fragment)
        if not success:
            return state, '背包已满，无法分解'
        
        # 移除原物品
        inventory.pop(idx)
        msg = f'分解 {item["name"]} 获得 {quantity} 个{fragment_name}'
        
        return state, msg
        
    def generate_boss_drops_for_player(self, boss, player_state):
        """
        根据世界Boss掉落配置，为单个玩家生成掉落物品列表
        每个物品独立判断概率，返回可能包含0~N件物品的列表
        """
        from datetime import datetime
        now = datetime.now()
        drop_items = []

        for drop_cfg in self.drop_configs:
            if not drop_cfg.get('enabled', True):
                continue

            # 时间范围检查（同原有逻辑）
            time_range = drop_cfg.get('time_range')
            if time_range:
                start_h, start_m = map(int, time_range['start'].split(':'))
                end_h, end_m = map(int, time_range['end'].split(':'))
                start_time = now.replace(hour=start_h, minute=start_m, second=0)
                end_time = now.replace(hour=end_h, minute=end_m, second=0)
                if not (start_time <= now <= end_time):
                    continue
            else:
                start_str = drop_cfg.get('start_time')
                end_str = drop_cfg.get('end_time')
                if start_str and end_str:
                    start_dt = datetime.strptime(start_str, '%Y-%m-%d %H:%M:%S')
                    end_dt = datetime.strptime(end_str, '%Y-%m-%d %H:%M:%S')
                    if not (start_dt <= now <= end_dt):
                        continue

            # 怪物ID检查
            monster_ids = drop_cfg.get('monster_ids')
            if monster_ids and boss.get('id') not in monster_ids:
                continue

            # 怪物等级区间（注意字段名统一）
            boss_level = boss.get('level', 0)
            min_level = drop_cfg.get('monster_level_min', 0)
            max_level = drop_cfg.get('monster_level_max', 9999)
            if not (min_level <= boss_level <= max_level):
                continue

            # 玩家等级区间
            player_level = player_state.get('level', 0)
            player_min = drop_cfg.get('player_level_min', 0)
            player_max = drop_cfg.get('player_level_max', 9999)
            if not (player_min <= player_level <= player_max):
                continue

            # 整体掉落概率判定（一般boss掉落整体概率为1，但保留）
            if random.random() >= drop_cfg.get('drop_chance', 0):
                continue

            # 遍历物品配置，每个物品独立概率触发
            for item_cfg in drop_cfg.get('items', []):
                item_chance = item_cfg.get('item_drop_chance', 1.0)
                if random.random() >= item_chance:
                    continue
                # 生成单个物品（使用原有的 _create_drop_item_raw，但需要传入一个假的怪物对象）
                # 注意：_create_drop_item_raw 需要怪物对象，这里传入 boss
                item = self._create_drop_item_raw(boss, item_cfg)
                if item:
                    drop_items.append(item)

        return drop_items
        
    def _add_buff(self, state, buff_type, value, duration, target='player'):
        if 'temporary_buffs' not in state:
            state['temporary_buffs'] = {}

        now = time.time()
        type_map = {
            'attack': '攻击', 'defense': '防御', 'hp': '生命',
            'gameSpeed': '攻击速度',
            'expMultiplier': '经验倍率',
            'accuracy': '命中', 'dodge': '躲闪'
        }
        chinese_type = type_map.get(buff_type, buff_type)

        if target == 'both':
            # 只创建一条 target='both' 的记录，同时作用于玩家和英雄
            unique_id = f"{buff_type}_both_{int(now * 1000000)}"
            # 检查是否已存在同类型的 both buff
            existing_key = None
            for key, buf in state['temporary_buffs'].items():
                if buf.get('target') == 'both' and buff_type in buf.get('attributes', {}):
                    existing_key = key
                    break
            if existing_key:
                # 延长过期时间（取最大值）
                old_expire = state['temporary_buffs'][existing_key]['expire_time']
                new_expire = max(old_expire, now + duration)
                state['temporary_buffs'][existing_key]['expire_time'] = new_expire
                msg = f'全体{chinese_type}效果已延长 {duration} 秒'
            else:
                state['temporary_buffs'][unique_id] = {
                    'target': 'both',
                    'attributes': {buff_type: value},
                    'expire_time': now + duration,
                    'source': 'normal'
                }
                msg = f'全体{chinese_type} +{value}，持续 {duration} 秒'
            state = self._update_final_stats(state)
            return state, msg, {'buff': {'type': buff_type, 'value': value, 'duration': duration, 'target': target}}

        # 单目标处理（player 或 hero）
        unique_id = f"{buff_type}_{target}_{int(now * 1000000)}"
        state['temporary_buffs'][unique_id] = {
            'target': target,
            'attributes': {buff_type: value},
            'expire_time': now + duration
        }
        state = self._update_final_stats(state)
        msg = f'{"玩家" if target=="player" else "英雄"}{chinese_type} +{value}，持续 {duration} 秒'
        return state, msg, {'buff': {'type': buff_type, 'value': value, 'duration': duration, 'target': target}}
        
    def _clean_expired_buffs(self, state):
        now = time.time()
        if 'temporary_buffs' in state:
            before = len(state['temporary_buffs'])
            state['temporary_buffs'] = {
                k: v for k, v in state['temporary_buffs'].items()
                if v.get('expire_time', 0) > now
            }
            if len(state['temporary_buffs']) != before:
                state = self._update_final_stats(state)
        return state
        
    def use_activity_item(self, state, uuid):
        idx, item = self._find_item_by_uuid(state['inventory'], uuid)
        if idx is None:
            return state, '物品不存在', None
        if not item.get('activity_item'):
            return state, '这不是活动物品', None

        msg = ''
        reward_info = {}

        # 新手加速卡特殊处理：应用配置中的所有奖励（支持多个buff）
        if item.get('activity_id') == 'newbie_speed_card':
            activity_id = item.get('activity_id')
            act_cfg = self.config.get('activities', {}).get(activity_id, {})
            rewards = act_cfg.get('rewards', [])
            if not rewards:
                msg = '活动配置错误，无奖励'
                reward_info = {'error': msg}
            else:
                player = state['player']
                hero = state['hero']
                applied = []
                # 确保临时buff字典存在
                if 'temporary_buffs' not in state:
                    state['temporary_buffs'] = {}

                for reward in rewards:
                    rtype = reward['type']
                    if rtype == 'gold':
                        gold = reward.get('min', 10000)
                        player['gold'] += gold
                        applied.append(f'获得 {gold} 金币')
                        reward_info['gold'] = reward_info.get('gold', 0) + gold
                    elif rtype == 'exp':
                        exp = random.randint(reward['min'], reward['max'])
                        player['exp'] += exp
                        if hero.get('summoned'):
                            hero['exp'] += exp
                        applied.append(f'获得 {exp} 经验')
                        reward_info['exp'] = reward_info.get('exp', 0) + exp

                    elif rtype == 'buff':
                        buff_type = reward['buff_type']
                        value = reward['value']
                        duration = reward['duration']
                        # 直接使用修复后的 _add_buff 方法，target='both'
                        state, buff_msg, _ = self._add_buff(state, buff_type, value, duration, 'both')
                        applied.append(buff_msg)
                        reward_info.setdefault('buff', []).append({
                            'type': buff_type, 'value': value, 'duration': duration, 'target': 'both'
                        })
                        
                    elif rtype == 'equipment':
                        # 装备奖励（如有配置）
                        quality = reward['quality']
                        level_range = reward['level_range']
                        min_lv = max(1, player['level'] + level_range[0])
                        max_lv = player['level'] + level_range[1]
                        equip = self._generate_equipment_by_level(min_lv, max_lv, quality)
                        if equip and self._add_item_to_inventory(state, equip):
                            applied.append(f'获得装备：{equip["name"]}（{quality}）')
                            reward_info['equipment'] = reward_info.get('equipment', []) + [equip['name']]
                        else:
                            applied.append('获得装备失败（背包已满）')
                            reward_info['equipment_error'] = True

                msg = '；'.join(applied)
        else:
            # 其他活动物品保持原有随机逻辑（不变）
            if item.get('buff'):
                buff = item['buff']
                buff_type = buff.get('type')
                buff_value = buff.get('value', 0)
                buff_duration = buff.get('duration', 0)
                buff_target = buff.get('target', 'both')
                if buff_type:
                    state, buff_msg, _ = self._add_buff(state, buff_type, buff_value, buff_duration, buff_target)
                    msg += buff_msg
                    reward_info['buff'] = {'type': buff_type, 'value': buff_value, 'duration': buff_duration, 'target': buff_target}
            else:
                activity_id = item.get('activity_id')
                rewards = activity_manager.get_rewards(activity_id)
                if rewards:
                    total_weight = sum(r['weight'] for r in rewards)
                    r = random.uniform(0, total_weight)
                    cum = 0
                    chosen = None
                    for reward in rewards:
                        cum += reward['weight']
                        if r <= cum:
                            chosen = reward
                            break
                    if chosen:
                        player = state['player']
                        hero = state['hero']
                        if chosen['type'] == 'gold':
                            gold = random.randint(chosen['min'], chosen['max'])
                            player['gold'] += gold
                            msg = f'获得 {gold} 金币'
                            reward_info = {'gold': gold}
                        elif chosen['type'] == 'exp':
                            exp = random.randint(chosen['min'], chosen['max'])
                            player['exp'] += exp
                            if hero.get('summoned'):
                                hero['exp'] += exp
                            msg = f'获得 {exp} 经验'
                            reward_info = {'exp': exp}
                        elif chosen['type'] == 'buff':
                            buff_type = chosen['buff_type']
                            value = chosen['value']
                            duration = chosen['duration']
                            target = chosen.get('target', 'both')
                            state, buff_msg, _ = self._add_buff(state, buff_type, value, duration, target)
                            msg = buff_msg
                            reward_info = {'buff': {'type': buff_type, 'value': value, 'duration': duration, 'target': target}}
                        elif chosen['type'] == 'equipment':
                            quality = chosen.get('quality', 'random')
                            level_range = chosen.get('level_range', [-5, 5])
                            equip = self._generate_equipment_by_level_and_quality(player['level'], quality, level_range)
                            if equip and self._add_item_to_inventory(state, equip):
                                msg = f'获得装备：{equip["name"]}（{equip.get("quality","普通")}）'
                                reward_info = {'equipment': equip['name'], 'quality': equip.get('quality','普通')}
                            else:
                                msg = '获得装备失败（背包已满）'
                                reward_info = {'error': '背包已满'}
                else:
                    msg = '活动配置错误，无奖励'
                    reward_info = {'error': msg}

        quantity = item.get('quantity', 1)
        if quantity > 1:
            item['quantity'] = quantity - 1
        else:
            state['inventory'].pop(idx)

        state = self._update_final_stats(state)
        return state, msg, reward_info
        
    def use_consumable(self, state, uuid):
        """通用消耗品使用（存根方法，避免前端调用报错）"""
        idx, item = self._find_item_by_uuid(state['inventory'], uuid)
        if idx is None:
            return state, '物品不存在', None
        return state, '该物品无法使用', None
        
    def auto_sell(self, state):
        """后端执行自动出售（根据玩家配置出售符合条件的装备）"""
        auto_config = state.get('autoSellConfig', {})
        selected_sets = auto_config.get('selectedSets', [])
        quality_map = auto_config.get('qualityMap', {})
        
        if not selected_sets:
            return state, "没有选中的套装", 0, 0

        # 收集所有需要出售的物品uuid
        uuids = []
        for item in state['inventory']:
            # 跳过非装备类物品
            if item.get('type') in ('potion', 'fragment', 'seed', 'fruit', 'consumable') or item.get('activity_item'):
                continue
            item_name = item.get('name')
            for set_name in selected_sets:
                set_conf = next((s for s in self.config.get('setBonuses', []) if s.get('name') == set_name), None)
                if set_conf and item_name in set_conf.get('required', []):
                    allowed_qualities = quality_map.get(set_name, [])
                    if not allowed_qualities or item.get('quality', '普通') in allowed_qualities:
                        uuids.append(item['uuid'])
                        break

        if not uuids:
            return state, "没有符合条件的可出售物品", 0, 0

        # 调用批量出售方法
        new_state, msg, sold_count, total_gold = self.batch_sell_by_uuids(state, uuids)
        return new_state, msg, sold_count, total_gold
        
    def batch_sell_by_uuids(self, state, uuids):
        """根据UUID列表批量出售物品，返回 (new_state, message, sold_count, total_gold)"""
        inventory = state['inventory']
        total_gold = 0
        sold_count = 0
        failed_uuids = []
        for uuid in uuids:
            idx, item = self._find_item_by_uuid(inventory, uuid)
            if idx is None:
                failed_uuids.append(uuid)
                continue
            price = math.floor(item['price'] / 2) * (item.get('quantity', 1))
            state['player']['gold'] += price
            total_gold += price
            inventory.pop(idx)
            sold_count += 1
        if sold_count == 0:
            return state, "没有可出售的物品", 0, 0
        msg = f"批量出售 {sold_count} 件物品，获得 {total_gold} 金币"
        if failed_uuids:
            msg += f"，{len(failed_uuids)} 件物品已不存在"
        return state, msg, sold_count, total_gold
        
    def craft_from_fragments(self, state, equip_type):
        """
        使用仓库中的碎片合成斗笠/披风/马牌
        :param equip_type: 'bambooHat', 'cloak', 'horseToken'
        """
        equip_map = {
            'bambooHat': ('bambooHat_fragment', self.new_equip_templates.get('bambooHat')),
            'cloak': ('cloak_fragment', self.new_equip_templates.get('cloak')),
            'horseToken': ('horseToken_fragment', self.new_equip_templates.get('horseToken'))
        }
        if equip_type not in equip_map:
            return state, f'无效的装备类型: {equip_type}'

        fragment_id, template = equip_map[equip_type]
        if not template:
            return state, f'装备模板缺失: {equip_type}'

        warehouse = state.get('warehouse', [])
        max_slots = state.get('maxWarehouseSlots', 500)

        # 从配置读取所需碎片数量，默认 30
        craft_cfg = self.config.get('craft', {})
        required = craft_cfg.get('fragment_required_for_special_equip', 30)

        # 统计碎片总数
        total = 0
        for item in warehouse:
            if item.get('type') == 'fragment' and item.get('id') == fragment_id:
                total += item.get('quantity', 1)

        if total < required:
            return state, f'{template["name"]}碎片不足，需要{required}个，当前{total}个'

        if len(warehouse) >= max_slots:
            return state, '仓库已满，无法合成'

        # 扣除碎片（从后向前遍历）
        remain = required
        for i in range(len(warehouse) - 1, -1, -1):
            item = warehouse[i]
            if item.get('type') == 'fragment' and item.get('id') == fragment_id:
                qty = item.get('quantity', 1)
                if qty >= remain:
                    if qty == remain:
                        warehouse.pop(i)
                    else:
                        item['quantity'] = qty - remain
                    remain = 0
                    break
                else:
                    remain -= qty
                    warehouse.pop(i)

        # 创建装备
        new_equip = template.copy()
        new_equip['uuid'] = generate_uuid()
        new_equip['enhanceLevel'] = 0
        new_equip['acquiredTime'] = int(time.time())
        new_equip['droppedBy'] = f"{state['player']['name']}合成"
        new_equip['quantity'] = 1

        # 确保 type 字段存在（模板中可能缺失）
        if 'type' not in new_equip or not new_equip['type']:
            type_map = {
                'bambooHat': 'bambooHat',
                'cloak': 'cloak',
                'horseToken': 'horseToken'
            }
            new_equip['type'] = type_map.get(equip_type, equip_type)

        warehouse.append(new_equip)

        state['warehouse'] = warehouse
        state = self._update_final_stats(state)
        return state, f'✨ 成功合成 {new_equip["name"]}，消耗 {required} 个碎片'
        
    def _generate_equipment_by_level_and_quality(self, player_level, quality='random', level_range=None):
        """
        生成与怪物掉落品质和属性完全一致的装备。
        quality: 'random' 表示按掉落配置的品质权重随机，否则为具体品质。
        level_range: [min_offset, max_offset] 相对玩家等级的偏移。
        """
        # 从配置的装备库中提取所有非特戒装备
        all_items = []
        for cat, items in self.config['equipmentData'].items():
            if cat == 'specialRings':
                continue
            all_items.extend(items)

        if not all_items:
            return None

        # 确定装备等级区间
        if level_range is None:
            level_range = [-5, 5]
        min_lv = max(1, player_level + level_range[0])
        max_lv = player_level + level_range[1]
        max_allowed = player_level + 50
        if max_lv > max_allowed:
            max_lv = max_allowed

        candidates = [item for item in all_items if min_lv <= item.get('level', 0) <= max_lv]
        if not candidates:
            candidates = all_items

        template = random.choice(candidates).copy()
        template['uuid'] = generate_uuid()
        template['enhanceLevel'] = 0
        template['acquiredTime'] = int(time.time())
        template['droppedBy'] = '活动物品使用'

        # 品质处理
        if quality == 'random':
            drop_config = self.config.get('dropConfig', {}).get('equipmentDrop', {})
            quality_weights = drop_config.get('qualityWeights', {'普通': 100, '极品': 5, '神品': 1, '超神品': 0.1})
            qualities = list(quality_weights.keys())
            weights = list(quality_weights.values())
            final_quality = random.choices(qualities, weights=weights)[0]
        else:
            final_quality = quality

        template['quality'] = final_quality

        # 品质属性加成（与怪物掉落完全一致）
        if final_quality != '普通':
            self.apply_quality_bonus(template, final_quality)

        # 过滤零属性
        if template.get('attackMin', 0) == 0 and template.get('attackMax', 0) == 0:
            template.pop('attackMin', None)
            template.pop('attackMax', None)
        if template.get('defenseMin', 0) == 0 and template.get('defenseMax', 0) == 0:
            template.pop('defenseMin', None)
            template.pop('defenseMax', None)

        return template
        
    def refine_equipment(self, state, target, slot_key):
        """炼化装备（直接从装备栏获取装备），不限次数，每次随机1-3条属性，品质越高属性越好"""
        if target not in ('player', 'hero'):
            return state, "无效的目标"
        if target == 'hero' and not state['hero'].get('summoned'):
            return state, "英雄未召唤"

        equipment = state['equipment'][target]
        item = equipment.get(slot_key)
        if not item:
            return state, "该位置无装备"

        # 检查是否可炼化（药水、碎片等不能炼化）
        if item.get('type') in ('potion', 'fragment', 'seed', 'fruit', 'consumable'):
            return state, "该物品无法炼化"

        # 计算消耗
        quality = item.get('quality', '普通')
        gold_cost = int(self.refine_cost_base_gold * self.refine_quality_mult.get(quality, 1))
        fragment_cost = self.refine_cost_fragment

        if state['player']['gold'] < gold_cost:
            return state, f"金币不足，需要 {gold_cost} 金币"
        # 检查碎片数量（装备碎片）
        total_fragments = 0
        for i in state['inventory']:
            if i.get('type') == 'fragment' and i.get('id') == 'equip_fragment':
                total_fragments += i.get('quantity', 1)
        if total_fragments < fragment_cost:
            return state, f"装备碎片不足，需要 {fragment_cost} 个"

        # 扣除金币和碎片
        state['player']['gold'] -= gold_cost
        self._consume_fragment(state, 'equip_fragment', fragment_cost)

        # 根据品质决定随机属性条数范围
        quality = item.get('quality', '普通')
        if quality == '普通':
            min_attrs, max_attrs = 1, 2
        elif quality == '极品':
            min_attrs, max_attrs = 1, 3
        elif quality == '神品':
            min_attrs, max_attrs = 2, 4
        elif quality == '超神品':
            min_attrs, max_attrs = 2, 5
        else:
            min_attrs, max_attrs = 1, 3   # 防御性默认值

        # 确保条数不超过配置的最大炼化属性数（max_refine_attrs，默认为5）
        max_allowed = self.max_refine_attrs
        max_attrs = min(max_attrs, max_allowed)
        min_attrs = min(min_attrs, max_allowed)

        attr_count = random.randint(min_attrs, max_attrs)
        new_attrs = []
        for _ in range(attr_count):
            # 按权重选取属性类型
            total_weight = sum(p['weight'] for p in self.refine_attr_pool)
            r = random.random() * total_weight
            cum = 0
            chosen = None
            for attr in self.refine_attr_pool:
                cum += attr['weight']
                if r <= cum:
                    chosen = attr
                    break
            if not chosen:
                chosen = self.refine_attr_pool[0]

            # 数值生成，并依据品质放大
            quality_factor = self.refine_quality_mult.get(quality, 1)
            base_min = chosen['min']
            base_max = chosen['max']
            if chosen.get('is_percent'):
                value = random.randint(base_min, base_max) * quality_factor
                value = min(value, 50)   # 百分比最高50%
                value = value / 100.0
            else:
                value = random.randint(base_min, base_max) * quality_factor
                value = int(value)

            new_attrs.append({
                'type': chosen['type'],
                'value': value,
                'display': self._get_refine_display(chosen['type'], value)
            })

        # 直接替换原有炼化属性（覆盖，不叠加）
        item['refine'] = {'attrs': new_attrs}
        self._invalidate_equipment_cache(item)
        # 写回装备
        equipment[slot_key] = item
        state['equipment'][target] = equipment

        # 重新计算最终属性
        state = self._update_final_stats(state)

        attr_str = '，'.join([a['display'] for a in new_attrs])
        return state, f"炼化成功！{item['name']} 获得 {attr_count} 条属性：{attr_str}"

    def _get_refine_display(self, attr_type, value):
        """属性显示文本"""
        display_map = {
            'attack': f'攻击力 +{int(value)}',
            'defense': f'防御力 +{int(value)}',
            'hp': f'生命上限 +{int(value)}',
            'mp': f'魔法上限 +{int(value)}',
            'exp_bonus': f'击杀经验 +{value*100:.0f}%',
            'drop_rate': f'怪物爆率 +{value*100:.0f}%',
            'level_reduce': f'穿戴等级 -{int(value)}',
            'accuracy': f'命中 +{int(value)}',
            'dodge': f'躲闪 +{int(value)}',
            'skill_damage': f'技能伤害 +{value*100:.0f}%',
            'crit_rate': f'暴击率 +{value*100:.1f}%',
            'crit_damage': f'暴击伤害 +{value*100:.1f}%',
            'lethal_rate': f'致命一击率 +{value*100:.1f}%',
            'lethal_damage': f'致命伤害 +{value*100:.1f}%',
            'poison_rate': f'附毒几率 +{value*100:.1f}%',
            'poison_damage': f'强毒伤害 +{int(value)}'
        }
        return display_map.get(attr_type, f'{attr_type}+{value}')
        
    def _process_dot_effects(self, state):
        now = time.time()
        log = state.get('log_messages', [])

        def process(target, target_type, is_world_boss=False, map_id=None):
            if target is None or (target.get('hp' if 'hp' in target else 'currentHp', 0) <= 0):
                return False
            killed = False
            # 中毒
            if 'poison_end_time' in target:
                if now >= target['poison_end_time']:
                    for k in ['poison_end_time', 'poison_tick_interval', 'poison_damage_per_tick', 'poison_last_tick_time', 'poison_owner']:
                        target.pop(k, None)
                else:
                    last = target.get('poison_last_tick_time', 0)
                    interval = target.get('poison_tick_interval', 2.0)
                    if now - last >= interval:
                        dmg = target.get('poison_damage_per_tick', 0)
                        if dmg > 0:
                            if is_world_boss and hasattr(self, 'world_boss_manager'):
                                owner = target.get('poison_owner', 'unknown')
                                self.world_boss_manager.take_damage(owner, map_id, dmg, record=True)
                                boss = self.world_boss_manager.get_boss_raw(map_id)
                                if boss:
                                    target['currentHp'] = boss.get('currentHp', 0)
                                else:
                                    target['currentHp'] = 0
                            else:
                                target['currentHp'] = max(0, target['currentHp'] - dmg)
                            target['poison_last_tick_time'] = now
                            log.append(('damage', f'{target.get("name", target_type)} 中毒损失 {dmg} 生命值'))
                            if target.get('currentHp', 0) <= 0:
                                if not is_world_boss:
                                    if target_type == 'player':
                                        self._handle_player_death(state, log)
                                    elif target_type == 'hero':
                                        self._handle_hero_death(state, log)
                                    else:
                                        self._handle_monster_death(target, state, killer_is_player=True)
                                killed = True
            # 燃烧处理 (新增)
            if 'burn_end_time' in target:
                if now >= target['burn_end_time']:
                    for k in ['burn_end_time', 'burn_tick_interval', 'burn_damage_per_tick', 'burn_last_tick_time', 'burn_owner', 'burnOwner']:
                        target.pop(k, None)
                else:
                    last = target.get('burn_last_tick_time', 0)
                    interval = target.get('burn_tick_interval', 2.0)
                    if now - last >= interval:
                        dmg = target.get('burn_damage_per_tick', 0)
                        if dmg > 0:
                            if is_world_boss and hasattr(self, 'world_boss_manager'):
                                # 兼容旧字段 burnOwner
                                owner = target.get('burn_owner') or target.get('burnOwner', 'unknown')
                                self.world_boss_manager.take_damage(owner, map_id, dmg, record=True)
                                boss = self.world_boss_manager.get_boss_raw(map_id)
                                target['currentHp'] = boss.get('currentHp', 0) if boss else 0
                            else:
                                target['currentHp'] = max(0, target['currentHp'] - dmg)
                            target['burn_last_tick_time'] = now
                            log.append(('damage', f'{target.get("name", target_type)} 燃烧损失 {dmg} 生命值'))
                            if target.get('currentHp', 0) <= 0:
                                if not is_world_boss:
                                    if target_type == 'player':
                                        self._handle_player_death(state, log)
                                    elif target_type == 'hero':
                                        self._handle_hero_death(state, log)
                                    else:
                                        self._handle_monster_death(target, state, killer_is_player=True)
                                killed = True
            return killed

        # 处理玩家
        process(state['player'], 'player')
        # 处理英雄
        hero = state['hero']
        if hero.get('summoned') and hero.get('hp', 0) > 0:
            process(hero, 'hero')
        # 处理怪物组
        mg = state.get('monsterGroup')
        if mg:
            cur_map = state['currentMap']
            for mon in mg.get('monsters', []):
                if mon.get('currentHp', 0) <= 0:
                    continue
                is_wb = mon.get('isWorldBoss', False)
                process(mon, mon.get('name', '怪物'), is_world_boss=is_wb, map_id=cur_map if is_wb else None)

        state['log_messages'] = log
        return state