//! Types shared by every frontend (graphic main, headless runner, TUI bin). use bevy::prelude::*; /// Lines typed by the human "manager" (stdin) or delivered by the TUI via /// work/commands.txt. Applied on top of the manager LLM's behavior; the sim /// never waits for them. Receiver sits behind a mutex because `Receiver` is /// `Send` but not `Sync`. #[derive(Resource)] pub struct HumanInbox(pub std::sync::Mutex>); impl HumanInbox { /// MUST be non-blocking — this runs every frame; blocking recv would /// freeze the loop until someone types a line. pub fn try_recv(&self) -> Result { let rx = self.0.lock().unwrap_or_else(|e| e.into_inner()); rx.try_recv() } } /// Font handle covering arrows/stars/speech-market glyphs (DejaVu fallback). #[derive(Resource, Clone)] pub struct UIFont(pub Handle); // --------------------------------------------------------------------------- // Social memory: per-person bonds (trust + last note) keyed by lowercase name. // Run-scoped (component-free so no query arity changes anywhere). // --------------------------------------------------------------------------- #[derive(Clone, Debug)] pub struct Bond { pub trust: i8, pub note: String, } #[derive(Resource, Default)] pub struct Bonds( pub std::collections::HashMap>, ); impl Bonds { pub fn trust_of(&self, me: &str, other: &str) -> i8 { self.0 .get(&me.to_lowercase()) .and_then(|m| m.get(&other.to_lowercase())) .map(|b| b.trust) .unwrap_or(2) } pub fn adjust(&mut self, me: &str, other: &str, delta: i8, note: Option) { let key_me = me.to_lowercase(); let key_other = other.to_lowercase(); let b = self .0 .entry(key_me) .or_default() .entry(key_other.clone()) .or_insert(Bond { trust: 2, note: "?".into() }); b.trust = (b.trust + delta).clamp(0, 10); if let Some(n) = note { let short: String = n.chars().take(70).collect(); b.note = short; } } /// Rendered for a person's prompt: their strongest bonds first. pub fn lines_for(&self, me: &str) -> String { let Some(map) = self.0.get(&me.to_lowercase()) else { return "(no working relationships formed yet — talk to people!)".into(); }; let mut rows: Vec<(&String, &Bond)> = map.iter().collect(); rows.sort_by_key(|(_, b)| -b.trust); rows.iter() .take(6) .map(|(name, b)| format!("- {}: trust {}/10 — \"{}\"", name, b.trust, b.note)) .collect::>() .join("\n") } } // --------------------------------------------------------------------------- // Desk chatter + meeting logs (work/log/, work/meetings/) — read by the TUI. // --------------------------------------------------------------------------- fn append_file(path: &std::path::Path, text: &str) { use std::io::Write; if let Ok(mut f) = std::fs::OpenOptions::new() .create(true) .append(true) .open(path) { let _ = f.write_all(text.as_bytes()); } } /// Live desk chat: every in-person utterance, wherever it happens — desks, /// break room or meetings. The TUI tails work/log/desk.md; meetings ALSO /// write their own curated record. pub fn desk_log( workspace: &crate::company::Workspace, clock: &crate::sim::SimClock, from: &str, to: &str, message: &str, ) { let dir = workspace.root.join("log"); std::fs::create_dir_all(&dir).ok(); append_file( &dir.join("desk.md"), &format!( "[day {} {}] {from} → {to}: {message}\n\n", clock.day(), clock.hhmm() ), ); } fn meeting_open_path(workspace: &crate::company::Workspace) -> std::path::PathBuf { workspace.root.join("log").join("meeting_open.md") } /// Called when a meeting starts: fresh record with header. pub fn start_meeting_log( workspace: &crate::company::Workspace, clock: &crate::sim::SimClock, topic: &str, ) { std::fs::create_dir_all(workspace.root.join("log")).ok(); std::fs::write( meeting_open_path(workspace), format!("# {}\nday {} · started {}\n\n", topic, clock.day(), clock.hhmm()), ) .ok(); } /// Append one in-meeting utterance/action to the open record. pub fn note_meeting( workspace: &crate::company::Workspace, clock: &crate::sim::SimClock, line: &str, ) { append_file(&meeting_open_path(workspace), &format!("- {line}\n")); } /// Called when a meeting ends: move the open record into work/meetings/. pub fn end_meeting_log( workspace: &crate::company::Workspace, clock: &crate::sim::SimClock, topic: &str, ) { let open = meeting_open_path(workspace); if !open.exists() { return; } let dir = workspace.root.join("meetings"); std::fs::create_dir_all(&dir).ok(); let name = format!( "day{:02}_{}_{}.md", clock.day(), clock.hhmm().replace(':', ""), crate::company::slug(topic) ); std::fs::rename(&open, dir.join(&name)).ok(); }