Initial commit: Echoes of the Ashes - Telegram RPG Bot

This commit is contained in:
Joan
2025-10-18 19:21:19 +02:00
commit 3ab412bc09
65 changed files with 14484 additions and 0 deletions

236
data/npcs.py Normal file
View 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