""" 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), tags=loc_data.get('tags', []), npcs=loc_data.get('npcs', []), danger_level=loc_data.get('danger_level', 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.webp", x=0.0, y=0.0 ) # Add a simple interactable rubble = Interactable( id="rubble", name="Pile of Rubble", image_path="images/interactables/rubble.webp" ) 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()