|
| 1 | +""" |
| 2 | +DragonSoul Server Emulator - Turn-based Combat Simulation |
| 3 | +Simple combat engine for campaign battles and PvP. |
| 4 | +""" |
| 5 | + |
| 6 | +import random |
| 7 | +import logging |
| 8 | + |
| 9 | +logger = logging.getLogger("combat") |
| 10 | + |
| 11 | + |
| 12 | +def _make_combatant(unit, team_id): |
| 13 | + """Create a combatant dict from a hero or NPC dict.""" |
| 14 | + return { |
| 15 | + "name": unit.get("name", "Unknown"), |
| 16 | + "team": team_id, |
| 17 | + "role": unit.get("role", "dps"), |
| 18 | + "hp": unit.get("health", 1000), |
| 19 | + "max_hp": unit.get("health", 1000), |
| 20 | + "atk": unit.get("attack", 100), |
| 21 | + "armor": unit.get("armor", 30), |
| 22 | + "magic_resist": unit.get("magic_resist", 20), |
| 23 | + "intellect": unit.get("intellect", 50), |
| 24 | + "speed": unit.get("speed", 50), |
| 25 | + "energy": 0, |
| 26 | + "alive": True, |
| 27 | + } |
| 28 | + |
| 29 | + |
| 30 | +def simulate_battle(attacker_heroes, defender_heroes): |
| 31 | + """ |
| 32 | + Run a turn-based combat simulation. |
| 33 | +
|
| 34 | + Args: |
| 35 | + attacker_heroes: list of hero stat dicts (from get_hero_stats or NPC dicts) |
| 36 | + defender_heroes: list of defender stat dicts |
| 37 | +
|
| 38 | + Returns: |
| 39 | + (won: bool, battle_log: list of log entry strings) |
| 40 | + """ |
| 41 | + # Build combatants |
| 42 | + attackers = [_make_combatant(h, 0) for h in attacker_heroes] |
| 43 | + defenders = [_make_combatant(h, 1) for h in defender_heroes] |
| 44 | + all_units = attackers + defenders |
| 45 | + |
| 46 | + log = [] |
| 47 | + turn = 0 |
| 48 | + max_turns = 100 |
| 49 | + |
| 50 | + while turn < max_turns: |
| 51 | + turn += 1 |
| 52 | + |
| 53 | + # Sort by speed (descending), alive only |
| 54 | + alive_units = [u for u in all_units if u["alive"]] |
| 55 | + alive_units.sort(key=lambda u: u["speed"], reverse=True) |
| 56 | + |
| 57 | + if not alive_units: |
| 58 | + break |
| 59 | + |
| 60 | + for unit in alive_units: |
| 61 | + if not unit["alive"]: |
| 62 | + continue |
| 63 | + |
| 64 | + # Determine enemy team |
| 65 | + if unit["team"] == 0: |
| 66 | + enemies = [u for u in defenders if u["alive"]] |
| 67 | + else: |
| 68 | + enemies = [u for u in attackers if u["alive"]] |
| 69 | + |
| 70 | + allies = [u for u in all_units if u["alive"] and u["team"] == unit["team"] and u is not unit] |
| 71 | + |
| 72 | + if not enemies: |
| 73 | + break |
| 74 | + |
| 75 | + # Healers heal lowest HP ally (or self) instead of attacking |
| 76 | + if unit["role"] == "healer": |
| 77 | + # Heal lowest HP ally |
| 78 | + heal_targets = [u for u in all_units if u["alive"] and u["team"] == unit["team"]] |
| 79 | + if heal_targets: |
| 80 | + target = min(heal_targets, key=lambda u: u["hp"] / max(u["max_hp"], 1)) |
| 81 | + heal_amount = max(1, unit["intellect"] // 2) |
| 82 | + target["hp"] = min(target["max_hp"], target["hp"] + heal_amount) |
| 83 | + log.append( |
| 84 | + f"T{turn}: {unit['name']} heals {target['name']} for {heal_amount} HP" |
| 85 | + ) |
| 86 | + unit["energy"] = min(100, unit["energy"] + 20) |
| 87 | + continue |
| 88 | + |
| 89 | + # Pick target: enemy with lowest HP |
| 90 | + target = min(enemies, key=lambda u: u["hp"]) |
| 91 | + |
| 92 | + # Check for ultimate (energy >= 100) |
| 93 | + is_ultimate = unit["energy"] >= 100 |
| 94 | + damage_mult = 3 if is_ultimate else 1 |
| 95 | + |
| 96 | + # Calculate damage |
| 97 | + raw_damage = unit["atk"] * damage_mult |
| 98 | + damage = max(1, raw_damage - target["armor"]) |
| 99 | + |
| 100 | + # Critical hit: 15% chance, 2x damage |
| 101 | + is_crit = random.random() < 0.15 |
| 102 | + if is_crit: |
| 103 | + damage *= 2 |
| 104 | + |
| 105 | + target["hp"] -= damage |
| 106 | + |
| 107 | + # Log the action |
| 108 | + action = "ULTIMATE" if is_ultimate else "attacks" |
| 109 | + crit_text = " (CRIT!)" if is_crit else "" |
| 110 | + log.append( |
| 111 | + f"T{turn}: {unit['name']} {action} {target['name']} for {damage} damage{crit_text}" |
| 112 | + ) |
| 113 | + |
| 114 | + # Energy management |
| 115 | + if is_ultimate: |
| 116 | + unit["energy"] = 0 |
| 117 | + else: |
| 118 | + unit["energy"] = min(100, unit["energy"] + 20) |
| 119 | + |
| 120 | + # Target takes energy gain from being hit |
| 121 | + target["energy"] = min(100, target["energy"] + 10) |
| 122 | + |
| 123 | + # Check if target died |
| 124 | + if target["hp"] <= 0: |
| 125 | + target["alive"] = False |
| 126 | + log.append(f"T{turn}: {target['name']} is defeated!") |
| 127 | + |
| 128 | + # Check win conditions |
| 129 | + attacker_alive = any(u["alive"] for u in attackers) |
| 130 | + defender_alive = any(u["alive"] for u in defenders) |
| 131 | + |
| 132 | + if not defender_alive: |
| 133 | + log.append(f"Battle won in {turn} turns!") |
| 134 | + return True, log |
| 135 | + if not attacker_alive: |
| 136 | + log.append(f"Battle lost in {turn} turns.") |
| 137 | + return False, log |
| 138 | + |
| 139 | + # Timeout - defender wins |
| 140 | + log.append(f"Battle timed out after {max_turns} turns. Defender wins.") |
| 141 | + return False, log |
| 142 | + |
| 143 | + |
| 144 | +def calculate_drops(chapter, level, won): |
| 145 | + """ |
| 146 | + Calculate rewards for a campaign battle. |
| 147 | + Returns a dict with gold, xp, hero_xp, and possible item drops. |
| 148 | + |
| 149 | + Reward scaling (based on original DragonSoul values): |
| 150 | + - Chapter 0: 150-530 gold, 60-240 XP per level |
| 151 | + - Chapter 1+: scales up significantly |
| 152 | + """ |
| 153 | + if not won: |
| 154 | + return {"gold": 0, "xp": 0, "hero_xp": 0, "items": []} |
| 155 | + |
| 156 | + # Gold: base 150, scales with chapter and level |
| 157 | + base_gold = 150 + 200 * chapter + 20 * level |
| 158 | + # Add some randomness (±20%) |
| 159 | + gold = int(base_gold * (0.8 + random.random() * 0.4)) |
| 160 | + |
| 161 | + # Team XP: base 60, scales with chapter and level |
| 162 | + base_xp = 60 + 100 * chapter + 10 * level |
| 163 | + xp = int(base_xp * (0.9 + random.random() * 0.2)) |
| 164 | + |
| 165 | + # Hero XP: heroes in the lineup get XP from combat |
| 166 | + # Original game gives ~100-500 hero XP per campaign level |
| 167 | + hero_xp = 100 + 50 * chapter + 25 * level |
| 168 | + |
| 169 | + # Random item drops (50% chance for 1 item, 15% chance for 2) |
| 170 | + items = [] |
| 171 | + if random.random() < 0.50: |
| 172 | + item_id = random.randint(1, 20) |
| 173 | + items.append(item_id) |
| 174 | + if random.random() < 0.15: |
| 175 | + item_id = random.randint(1, 20) |
| 176 | + items.append(item_id) |
| 177 | + |
| 178 | + return { |
| 179 | + "gold": gold, |
| 180 | + "xp": xp, |
| 181 | + "hero_xp": hero_xp, |
| 182 | + "items": items, |
| 183 | + } |
0 commit comments