//! 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<std::sync::mpsc::Receiver<String>>);
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<String, std::sync::mpsc::TryRecvError> {
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<Font>);
// ---------------------------------------------------------------------------
// 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<String, std::collections::HashMap<String, Bond>>,
);
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<String>) {
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::<Vec<_>>()
.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();
}