""" NPC definitions for combat encounters - NOW LOADED FROM JSON Each NPC has stats, loot tables, and combat behavior. """ from dataclasses import dataclass from typing import Dict, List, Optional import json from pathlib import Path @dataclass class LootItem: """Item that can be dropped by NPCs""" item_id: str quantity_min: int quantity_max: int drop_chance: float # 0.0 to 1.0 @dataclass class CorpseLoot: """Item that can be scavenged from a corpse""" item_id: str quantity_min: int quantity_max: int required_tool: Optional[str] = None # item_id of required tool @dataclass class StatusEffect: """Status effect data""" name: str duration_turns: int damage_per_turn: int = 0 # For bleeding stun: bool = False # Prevents action @dataclass class NPCDefinition: """Complete NPC definition""" npc_id: str name: str description: str emoji: str # Combat stats hp_min: int hp_max: int damage_min: int damage_max: int defense: int # Reduces incoming damage # Rewards xp_reward: int loot_table: List[LootItem] corpse_loot: List[CorpseLoot] # Behavior flee_chance: float # NPC's chance to flee if low HP status_inflict_chance: float # Chance to inflict status on player # Visuals image_path: Optional[str] = None death_message: str = "The enemy falls defeated." def load_npcs_from_json(): """Load NPCs, danger levels, and spawn tables from JSON""" json_path = Path(__file__).parent.parent / 'gamedata' / 'npcs.json' try: with open(json_path, 'r') as f: data = json.load(f) # Convert JSON to NPCDefinition objects npcs = {} for npc_id, npc_data in data['npcs'].items(): # Convert loot tables loot_table = [ LootItem(**loot) for loot in npc_data.get('loot_table', []) ] corpse_loot = [ CorpseLoot(**loot) for loot in npc_data.get('corpse_loot', []) ] # Create NPC definition npcs[npc_id] = NPCDefinition( npc_id=npc_data['npc_id'], name=npc_data['name'], description=npc_data['description'], emoji=npc_data['emoji'], hp_min=npc_data['hp_min'], hp_max=npc_data['hp_max'], damage_min=npc_data['damage_min'], damage_max=npc_data['damage_max'], defense=npc_data['defense'], xp_reward=npc_data['xp_reward'], loot_table=loot_table, corpse_loot=corpse_loot, flee_chance=npc_data['flee_chance'], status_inflict_chance=npc_data['status_inflict_chance'], image_path=npc_data.get('image_path'), death_message=npc_data.get('death_message', "The enemy falls defeated.") ) # Load danger levels - convert to tuple format (danger_level, encounter_rate, wandering_chance) danger_levels = {} for loc_id, danger_data in data['danger_levels'].items(): danger_levels[loc_id] = ( danger_data['danger_level'], danger_data['encounter_rate'], danger_data['wandering_chance'] ) # Load spawn tables - convert to list of tuples format spawn_tables = {} for loc_id, spawns in data['spawn_tables'].items(): spawn_tables[loc_id] = [ (spawn['npc_id'], spawn['weight']) for spawn in spawns ] print(f"✅ Loaded {len(npcs)} NPCs, {len(danger_levels)} danger configs, {len(spawn_tables)} spawn tables from JSON") return npcs, danger_levels, spawn_tables except FileNotFoundError: print(f"⚠️ Warning: {json_path} not found, using fallback NPCs") return _get_fallback_npcs() except Exception as e: print(f"⚠️ Warning: Error loading NPCs from JSON: {e}") import traceback traceback.print_exc() return _get_fallback_npcs() def _get_fallback_npcs(): """Fallback NPCs if JSON loading fails""" npcs = { "feral_dog": NPCDefinition( npc_id="feral_dog", name="Feral Dog", description="A wild, mangy dog with desperate hunger in its eyes.", emoji="🐕", hp_min=15, hp_max=25, damage_min=3, damage_max=7, defense=0, xp_reward=10, flee_chance=0.3, status_inflict_chance=0.15, loot_table=[ LootItem("raw_meat", 1, 2, 0.6), LootItem("bone", 1, 1, 0.4), LootItem("animal_hide", 1, 1, 0.3) ], corpse_loot=[ CorpseLoot("raw_meat", 1, 2), CorpseLoot("bone", 1, 1), CorpseLoot("animal_hide", 1, 1, required_tool="knife") ], image_path=None, death_message="The feral dog whimpers and collapses." ) } danger_levels = { "start_point": (0, 0.0, 0.0), } spawn_tables = { "start_point": [], } return npcs, danger_levels, spawn_tables # Load on module import NPCS, LOCATION_DANGER, LOCATION_SPAWNS = load_npcs_from_json() # Status effects that can be applied in combat STATUS_EFFECTS = { "bleeding": StatusEffect( name="Bleeding", duration_turns=3, damage_per_turn=2, stun=False ), "stunned": StatusEffect( name="Stunned", duration_turns=1, damage_per_turn=0, stun=True ), "infected": StatusEffect( name="Infected", duration_turns=5, damage_per_turn=1, stun=False ), } # Helper functions def get_danger_level(location_id: str) -> int: """Get danger level for a location (0-4)""" return LOCATION_DANGER.get(location_id, (0, 0.0, 0.0))[0] def get_location_encounter_rate(location_id: str) -> float: """Get base encounter rate for a location""" return LOCATION_DANGER.get(location_id, (0, 0.0, 0.0))[1] def get_wandering_enemy_chance(location_id: str) -> float: """Get chance for wandering enemy to spawn""" return LOCATION_DANGER.get(location_id, (0, 0.0, 0.0))[2] def get_random_npc_for_location(location_id: str) -> str: """ Get a random NPC ID for the given location based on spawn weights. Returns None if no NPCs can spawn at this location. """ import random spawn_table = LOCATION_SPAWNS.get(location_id, []) if not spawn_table: return None # Extract NPCs and weights npcs = [npc_id for npc_id, weight in spawn_table] weights = [weight for npc_id, weight in spawn_table] # Use random.choices with weights return random.choices(npcs, weights=weights, k=1)[0] if npcs else None