314 lines
11 KiB
Python
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()
|