237 lines
6.9 KiB
Python
237 lines
6.9 KiB
Python
"""
|
|
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
|