Files
echoes-of-the-ash/api/world_loader.py
2026-02-23 15:42:21 +01:00

305 lines
11 KiB
Python

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