291 lines
11 KiB
Python
291 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
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
@dataclass
|
|
class Outcome:
|
|
"""Represents an outcome of an action"""
|
|
text: 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: 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: str
|
|
image_path: str = ""
|
|
actions: List[Action] = field(default_factory=list)
|
|
|
|
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: str
|
|
description: 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)
|
|
|
|
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', '')
|
|
)
|
|
|
|
# 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', [])
|
|
)
|
|
|
|
# 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()
|