Initial commit: Echoes of the Ashes - Telegram RPG Bot
This commit is contained in:
313
data/world_loader.py
Normal file
313
data/world_loader.py
Normal file
@@ -0,0 +1,313 @@
|
||||
"""
|
||||
World loader - NOW LOADS FROM JSON
|
||||
Creates and connects all the game world objects from gamedata/locations.json
|
||||
Uses template-based interactables from gamedata/interactables.json
|
||||
"""
|
||||
|
||||
from .models import World, Location, Interactable, Action, Outcome
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_interactable_templates():
|
||||
"""Load interactable templates from interactables.json."""
|
||||
templates = {}
|
||||
json_path = Path(__file__).parent.parent / 'gamedata' / 'interactables.json'
|
||||
|
||||
try:
|
||||
with open(json_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
for template_id, template_data in data.get('interactables', {}).items():
|
||||
templates[template_id] = template_data
|
||||
|
||||
print(f"📦 Loaded {len(templates)} interactable templates")
|
||||
except FileNotFoundError:
|
||||
print("⚠️ interactables.json not found, using inline data only")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error loading interactables.json: {e}")
|
||||
|
||||
return templates
|
||||
|
||||
|
||||
def create_interactable_from_template(template_id, template_data, instance_data):
|
||||
"""Create an Interactable object from template and instance data."""
|
||||
interactable = Interactable(
|
||||
id=template_id,
|
||||
name=template_data.get('name', 'Unknown'),
|
||||
image_path=template_data.get('image_path', '')
|
||||
)
|
||||
|
||||
# Get actions from template
|
||||
template_actions = template_data.get('actions', {})
|
||||
|
||||
# Get outcomes from instance (template-based format)
|
||||
instance_outcomes = instance_data.get('outcomes', {})
|
||||
|
||||
# Build actions by merging template actions with instance outcomes
|
||||
for action_id, action_template in template_actions.items():
|
||||
action = Action(
|
||||
id=action_template['id'],
|
||||
label=action_template['label'],
|
||||
stamina_cost=action_template.get('stamina_cost', 2)
|
||||
)
|
||||
|
||||
# Get the instance-specific outcome data for this action
|
||||
if action_id in instance_outcomes:
|
||||
outcome_data = instance_outcomes[action_id]
|
||||
|
||||
# Build outcomes from the instance data
|
||||
text_dict = outcome_data.get('text', {})
|
||||
rewards = outcome_data.get('rewards', {})
|
||||
|
||||
# Add success outcome
|
||||
if text_dict.get('success'):
|
||||
items_reward = {}
|
||||
if 'items' in rewards:
|
||||
for item in rewards['items']:
|
||||
items_reward[item['item_id']] = item.get('quantity', 1)
|
||||
|
||||
outcome = Outcome(
|
||||
text=text_dict['success'],
|
||||
items_reward=items_reward,
|
||||
damage_taken=rewards.get('damage', 0)
|
||||
)
|
||||
action.add_outcome('success', outcome)
|
||||
|
||||
# Add failure outcome
|
||||
if text_dict.get('failure'):
|
||||
outcome = Outcome(
|
||||
text=text_dict['failure'],
|
||||
items_reward={},
|
||||
damage_taken=0
|
||||
)
|
||||
action.add_outcome('failure', outcome)
|
||||
|
||||
# Add critical failure outcome
|
||||
if text_dict.get('crit_failure'):
|
||||
outcome = Outcome(
|
||||
text=text_dict['crit_failure'],
|
||||
items_reward={},
|
||||
damage_taken=rewards.get('crit_damage', 0)
|
||||
)
|
||||
action.add_outcome('critical_failure', outcome)
|
||||
|
||||
interactable.add_action(action)
|
||||
|
||||
return interactable
|
||||
|
||||
|
||||
def load_world() -> World:
|
||||
"""Creates and connects all the game world objects from JSON."""
|
||||
world = World()
|
||||
|
||||
# Load interactable templates first
|
||||
templates = load_interactable_templates()
|
||||
|
||||
json_path = Path(__file__).parent.parent / 'gamedata' / 'locations.json'
|
||||
|
||||
try:
|
||||
with open(json_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
print(f"📍 Loading {len(data['locations'])} locations from JSON...")
|
||||
|
||||
# First pass: Create all locations
|
||||
for loc_data in data['locations']:
|
||||
location = Location(
|
||||
id=loc_data['id'],
|
||||
name=loc_data['name'],
|
||||
description=loc_data['description'],
|
||||
image_path=loc_data['image_path'],
|
||||
x=loc_data.get('x', 0.0),
|
||||
y=loc_data.get('y', 0.0)
|
||||
)
|
||||
|
||||
# Add interactables using template-based format
|
||||
for instance_id, instance_data in loc_data.get('interactables', {}).items():
|
||||
template_id = instance_data.get('template_id')
|
||||
|
||||
if not template_id:
|
||||
print(f"⚠️ Skipping interactable {instance_id} - no template_id")
|
||||
continue
|
||||
|
||||
# Get template data
|
||||
template_data = templates.get(template_id)
|
||||
|
||||
if not template_data:
|
||||
print(f"⚠️ Template '{template_id}' not found for {instance_id}")
|
||||
continue
|
||||
|
||||
# Create interactable from template + instance data
|
||||
interactable = create_interactable_from_template(
|
||||
template_id,
|
||||
template_data,
|
||||
instance_data
|
||||
)
|
||||
|
||||
location.add_interactable(instance_id, interactable)
|
||||
|
||||
world.add_location(location)
|
||||
|
||||
# Second pass: Add connections
|
||||
print(f"🔗 Adding {len(data['connections'])} connections...")
|
||||
for conn in data['connections']:
|
||||
from_loc = world.get_location(conn['from'])
|
||||
if from_loc:
|
||||
from_loc.add_exit(conn['direction'], conn['to'])
|
||||
|
||||
print(f"✅ World loaded successfully!")
|
||||
return world
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"⚠️ Warning: {json_path} not found, loading fallback world")
|
||||
return _load_fallback_world()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error loading world from JSON: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return _load_fallback_world()
|
||||
|
||||
|
||||
def _load_fallback_world() -> World:
|
||||
"""Fallback world with minimal locations if JSON loading fails"""
|
||||
world = World()
|
||||
|
||||
# Create a simple fallback location
|
||||
start = Location(
|
||||
id="start_point",
|
||||
name="🌆 Ruined Downtown Core",
|
||||
description="The wind howls through skeletal skyscrapers. Debris litters the cracked asphalt.",
|
||||
image_path="images/locations/downtown.png",
|
||||
x=0.0,
|
||||
y=0.0
|
||||
)
|
||||
|
||||
# Add a simple interactable
|
||||
rubble = Interactable(
|
||||
id="rubble",
|
||||
name="Pile of Rubble",
|
||||
image_path="images/interactables/rubble.png"
|
||||
)
|
||||
search_action = Action(id="search", label="🔎 Search Rubble", stamina_cost=2)
|
||||
search_action.add_outcome("success", Outcome(
|
||||
text="You find some scrap metal.",
|
||||
items_reward={"scrap_metal": 2}
|
||||
))
|
||||
search_action.add_outcome("failure", Outcome(
|
||||
text="Nothing useful here."
|
||||
))
|
||||
rubble.add_action(search_action)
|
||||
start.add_interactable("start_rubble", rubble)
|
||||
|
||||
world.add_location(start)
|
||||
|
||||
return world
|
||||
|
||||
|
||||
def export_map_data() -> dict:
|
||||
"""
|
||||
Export map data for external visualization.
|
||||
Returns a dictionary with locations, connections, interactables, and enemy spawns.
|
||||
Can be saved as JSON for use in web-based or other visualizers.
|
||||
"""
|
||||
from data.npcs import LOCATION_SPAWNS, NPCS, LOCATION_DANGER, get_danger_level, get_location_encounter_rate, get_wandering_enemy_chance
|
||||
|
||||
map_data = {
|
||||
"locations": [],
|
||||
"connections": [],
|
||||
"interactables": [],
|
||||
"spawn_tables": {}
|
||||
}
|
||||
|
||||
for location in game_world.locations.values():
|
||||
# Get danger information
|
||||
danger_level = get_danger_level(location.id)
|
||||
encounter_rate = get_location_encounter_rate(location.id)
|
||||
wandering_chance = get_wandering_enemy_chance(location.id)
|
||||
|
||||
# Add location data with image and danger info
|
||||
map_data["locations"].append({
|
||||
"id": location.id,
|
||||
"name": location.name,
|
||||
"description": location.description,
|
||||
"x": location.x,
|
||||
"y": location.y,
|
||||
"image_path": location.image_path,
|
||||
"interactable_count": len(location.interactables),
|
||||
"danger_level": danger_level,
|
||||
"encounter_rate": round(encounter_rate * 100, 1), # As percentage
|
||||
"wandering_chance": round(wandering_chance * 100, 1) # As percentage
|
||||
})
|
||||
|
||||
# Add interactable data with images and loot chances
|
||||
for instance_id, interactable in location.interactables.items():
|
||||
interactable_data = {
|
||||
"instance_id": instance_id,
|
||||
"location_id": location.id,
|
||||
"id": interactable.id,
|
||||
"name": interactable.name,
|
||||
"image_path": interactable.image_path,
|
||||
"actions": []
|
||||
}
|
||||
|
||||
# Process each action and its outcomes
|
||||
for action in interactable.actions.values():
|
||||
action_data = {
|
||||
"id": action.id,
|
||||
"label": action.label,
|
||||
"stamina_cost": action.stamina_cost,
|
||||
"outcomes": []
|
||||
}
|
||||
|
||||
# Add outcome information (for loot chances)
|
||||
for outcome_name, outcome in action.outcomes.items():
|
||||
outcome_data = {
|
||||
"type": outcome_name,
|
||||
"text": outcome.text,
|
||||
"items": outcome.items_reward,
|
||||
"damage": outcome.damage_taken
|
||||
}
|
||||
action_data["outcomes"].append(outcome_data)
|
||||
|
||||
interactable_data["actions"].append(action_data)
|
||||
|
||||
map_data["interactables"].append(interactable_data)
|
||||
|
||||
# Add connections
|
||||
for direction, destination in location.exits.items():
|
||||
# Calculate stamina cost based on distance between locations
|
||||
dest_loc = game_world.get_location(destination)
|
||||
if dest_loc:
|
||||
from .travel_helpers import calculate_base_stamina_cost
|
||||
stamina_cost = calculate_base_stamina_cost(location, dest_loc)
|
||||
else:
|
||||
stamina_cost = 5 # Fallback
|
||||
|
||||
map_data["connections"].append({
|
||||
"from": location.id,
|
||||
"to": destination,
|
||||
"direction": direction,
|
||||
"stamina_cost": stamina_cost
|
||||
})
|
||||
|
||||
# Add spawn table for this location
|
||||
if location.id in LOCATION_SPAWNS:
|
||||
spawn_data = []
|
||||
for npc_id, weight in LOCATION_SPAWNS[location.id]:
|
||||
if npc_id in NPCS:
|
||||
npc = NPCS[npc_id]
|
||||
spawn_data.append({
|
||||
"npc_id": npc_id,
|
||||
"name": npc.name,
|
||||
"emoji": npc.emoji,
|
||||
"weight": weight,
|
||||
"level_range": f"{npc.hp_min}-{npc.hp_max} HP"
|
||||
})
|
||||
map_data["spawn_tables"][location.id] = spawn_data
|
||||
|
||||
return map_data
|
||||
|
||||
|
||||
# Create singleton world instance on module import
|
||||
game_world = load_world()
|
||||
Reference in New Issue
Block a user