use thread_engine::glam::Vec3;
use thread_engine::infinite_manifest::WorldManifest;
use thread_engine::navigator::{InteractableNav, LoadedWorldMeta, PortalNav, SkyNav, WorldLoader};
use thread_engine::{AssetSource, Loom};
struct TextLoader;
impl WorldLoader for TextLoader {
fn load(
&mut self,
manifest: &WorldManifest,
_assets: &AssetSource,
anchor: Vec3,
) -> LoadedWorldMeta {
let spawn = manifest.default_spawn();
let portals: Vec<PortalNav> = manifest
.portals
.iter()
.map(|p| PortalNav {
id: p.id.clone(),
position: Vec3::from_array(p.position),
to: p.to.clone(),
label: p.label.clone(),
preview: p.preview != infinite_manifest::PreviewPolicy::None,
yaw: 0.0,
})
.collect();
let interactables: Vec<InteractableNav> = manifest
.placements
.iter()
.filter(|pl| pl.codex.is_some() || pl.behavior.is_some())
.map(|pl| InteractableNav {
position: Vec3::from_array(pl.position),
label: pl.name.clone(),
codex: pl.codex.clone(),
web: pl
.data
.get("url")
.and_then(|u| u.as_str())
.map(str::to_string),
name: pl.name.clone(),
behavior: pl.behavior.clone(),
interaction: pl.interaction.clone(),
hits_done: 0,
data: pl.data.clone(),
})
.collect();
println!(
" ┌─ entered '{}' — {} placement(s), {} veil(s)",
manifest.world.title,
manifest.placements.len(),
portals.len()
);
for p in &portals {
println!(" │ veil '{}' → {}", p.label, p.to);
}
let _ = anchor;
LoadedWorldMeta {
title: manifest.world.title.clone(),
world_id: manifest.world.id.clone(),
spawn: Vec3::from_array(spawn.position),
portals,
interactables,
sky: SkyNav::default(),
presence_relays: manifest
.presence
.as_ref()
.map(|p| p.relay_list())
.unwrap_or_default(),
colliders: vec![],
}
}
}
fn main() {
let worlds_root = format!("{}/../../worlds", env!("CARGO_MANIFEST_DIR"));
let start = std::env::args()
.nth(1)
.unwrap_or_else(|| "thread://nexus.pixygon.io/nexus".to_string());
let mut loom = Loom::new(&worlds_root, TextLoader);
let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
println!("headless browser · opening {start}\n");
loom.open(&start, Vec3::ZERO);
let dt = 1.0 / 30.0;
let mut player = Vec3::ZERO;
let mut hops = 0;
let max_hops = 6; let mut idle = 0;
for _frame in 0..100_000 {
if let Some(spawn) = loom.update(dt, player) {
player = spawn;
hops += 1;
if hops > max_hops {
println!("\n └─ toured {max_hops} worlds. The engine drove them all; this browser only printed.");
break;
}
idle = 0;
continue;
}
if loom.is_traveling() {
std::thread::sleep(std::time::Duration::from_millis(5));
continue;
}
if !loom.armed() {
continue;
}
let veils = loom.portals();
let nearest = |p: &&PortalNav| (p.position - player).length();
let pick = veils
.iter()
.filter(|p| !visited.contains(&p.to))
.min_by(|a, b| nearest(a).total_cmp(&nearest(b)))
.or_else(|| {
veils
.iter()
.min_by(|a, b| nearest(a).total_cmp(&nearest(b)))
});
match pick {
Some(target) => {
visited.insert(target.to.clone());
player = target.position;
idle = 0;
}
None => {
idle += 1;
if idle > 4 {
println!("\n └─ this world has no onward veils — end of the walk.");
break;
}
}
}
}
}