""" Standalone world loader for the API. Loads game data from JSON files without bot dependencies. """ import json from pathlib import Path from typing import Dict, List, Any, Optional, Union from dataclasses import dataclass, field @dataclass class Outcome: """Represents an outcome of an action""" text: Union[str, Dict[str, str]] items_reward: Dict[str, int] = field(default_factory=dict) damage_taken: int = 0 @dataclass class Action: """Represents an action that can be performed on an interactable""" id: str label: Union[str, Dict[str, str]] stamina_cost: int = 2 outcomes: Dict[str, Outcome] = field(default_factory=dict) def add_outcome(self, outcome_type: str, outcome: Outcome): self.outcomes[outcome_type] = outcome @dataclass class Interactable: """Represents an interactable object""" id: str name: Union[str, Dict[str, str]] image_path: str = "" actions: List[Action] = field(default_factory=list) unlocked_by: str = "" locked: bool = False def add_action(self, action: Action): self.actions.append(action) @dataclass class Exit: """Represents an exit from a location""" direction: str destination: str description: str = "" @dataclass class Location: """Represents a location in the game world""" id: str name: Union[str, Dict[str, str]] description: Union[str, Dict[str, str]] image_path: str = "" exits: Dict[str, str] = field(default_factory=dict) # direction -> destination_id exit_stamina: Dict[str, int] = field(default_factory=dict) # direction -> stamina_cost interactables: List[Interactable] = field(default_factory=list) npcs: List[str] = field(default_factory=list) tags: List[str] = field(default_factory=list) # Location tags like 'workbench', 'safe_zone' x: float = 0.0 # X coordinate for distance calculations y: float = 0.0 # Y coordinate for distance calculations danger_level: int = 0 # Danger level (0-5) unlocked_by: str = "" locked: bool = False def add_exit(self, direction: str, destination: str, stamina_cost: int = 5): self.exits[direction] = destination self.exit_stamina[direction] = stamina_cost def add_interactable(self, interactable: Interactable): self.interactables.append(interactable) @dataclass class World: """Represents the entire game world""" locations: Dict[str, Location] = field(default_factory=dict) def add_location(self, location: Location): self.locations[location.id] = location class WorldLoader: """Loads world data from JSON files""" def __init__(self, gamedata_path: str = "./gamedata"): self.gamedata_path = Path(gamedata_path) self.interactable_templates = {} def load_interactable_templates(self) -> Dict[str, Any]: """Load interactable templates from interactables.json""" json_path = self.gamedata_path / 'interactables.json' try: with open(json_path, 'r') as f: data = json.load(f) self.interactable_templates = data.get('interactables', {}) print(f"📦 Loaded {len(self.interactable_templates)} interactable templates") except FileNotFoundError: print("⚠️ interactables.json not found") except Exception as e: print(f"⚠️ Error loading interactables.json: {e}") return self.interactable_templates def create_interactable_from_template( self, template_id: str, template_data: Dict[str, Any], instance_data: Dict[str, Any] ) -> Interactable: """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', ''), unlocked_by=instance_data.get('unlocked_by', template_data.get('unlocked_by', '')), ) # Set locked status if unlocked_by is present if interactable.unlocked_by: interactable.locked = True # Get actions from template template_actions = template_data.get('actions', {}) # Get outcomes from instance 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 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_locations(self) -> Dict[str, Location]: """Load all locations from locations.json""" json_path = self.gamedata_path / 'locations.json' locations = {} try: with open(json_path, 'r') as f: data = json.load(f) # Get danger config danger_config = data.get('danger_config', {}) # First pass: create all locations locations_data = data.get('locations', []) if isinstance(locations_data, dict): # Old format: dict of locations locations_iter = locations_data.items() else: # New format: list of locations locations_iter = [(loc['id'], loc) for loc in locations_data] for loc_id, loc_data in locations_iter: # Get danger level from danger_config danger_level = 0 if loc_id in danger_config: danger_level = danger_config[loc_id].get('danger_level', 0) location = Location( id=loc_id, name=loc_data.get('name', 'Unknown Location'), description=loc_data.get('description', ''), image_path=loc_data.get('image_path', ''), x=float(loc_data.get('x', 0.0)), y=float(loc_data.get('y', 0.0)), danger_level=danger_level, tags=loc_data.get('tags', []), npcs=loc_data.get('npcs', []), unlocked_by=loc_data.get('unlocked_by', '') ) # Set locked status if unlocked_by is present if location.unlocked_by: location.locked = True # Add exits for direction, destination in loc_data.get('exits', {}).items(): location.add_exit(direction, destination) # Add NPCs location.npcs = loc_data.get('npcs', []) # Add interactables interactables_data = loc_data.get('interactables', {}) if isinstance(interactables_data, dict): # New format: dict of interactables interactables_list = [ {**data, 'instance_id': inst_id, 'id': data.get('template_id', inst_id)} for inst_id, data in interactables_data.items() ] else: # Old format: list of interactables interactables_list = interactables_data for interactable_data in interactables_list: template_id = interactable_data.get('id') instance_id = interactable_data.get('instance_id', template_id) if template_id in self.interactable_templates: template = self.interactable_templates[template_id] interactable = self.create_interactable_from_template( instance_id, template, interactable_data ) location.add_interactable(interactable) locations[loc_id] = location # Second pass: add connections from the connections array connections = data.get('connections', []) for conn in connections: from_id = conn.get('from') to_id = conn.get('to') direction = conn.get('direction') stamina_cost = conn.get('stamina_cost', 5) # Default 5 if not specified if from_id in locations and direction: locations[from_id].add_exit(direction, to_id, stamina_cost) print(f"🗺️ Loaded {len(locations)} locations with {len(connections)} connections") except FileNotFoundError: print("⚠️ locations.json not found") except Exception as e: print(f"⚠️ Error loading locations.json: {e}") import traceback traceback.print_exc() return locations def load_world(self) -> World: """Load the entire world""" world = World() # Load interactable templates first self.load_interactable_templates() # Load locations locations = self.load_locations() for location in locations.values(): world.add_location(location) return world def load_world() -> World: """Convenience function to load the world""" loader = WorldLoader() return loader.load_world()