Initial commit: Echoes of the Ashes - Telegram RPG Bot
This commit is contained in:
236
data/npcs.py
Normal file
236
data/npcs.py
Normal file
@@ -0,0 +1,236 @@
|
||||
"""
|
||||
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_url: 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_url=npc_data.get('image_url'),
|
||||
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_url=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
|
||||
Reference in New Issue
Block a user