Files
echoes-of-the-ash/data/world_loader.py

314 lines
11 KiB
Python

"""
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()