1use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::fmt;
11
12#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
18pub struct RoomId(pub String);
19
20impl fmt::Display for RoomId {
21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 write!(f, "{}", self.0)
23 }
24}
25
26impl From<String> for RoomId {
27 fn from(s: String) -> Self {
28 RoomId(s)
29 }
30}
31
32impl From<&str> for RoomId {
33 fn from(s: &str) -> Self {
34 RoomId(s.to_string())
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub enum RoomRole {
45 Engineering,
46 Bridge,
47 Science,
48 Security,
49 Operations,
50 Navigation,
51 Medical,
52 Custom(String),
53}
54
55impl RoomRole {
56 pub fn default_intention_focus(&self) -> Vec<String> {
58 match self {
59 RoomRole::Engineering => vec![
60 "hardware repair".into(),
61 "motor calibration".into(),
62 "GPIO configuration".into(),
63 "infrastructure maintenance".into(),
64 ],
65 RoomRole::Bridge => vec![
66 "command coordination".into(),
67 "status overview".into(),
68 "crew management".into(),
69 "decision making".into(),
70 ],
71 RoomRole::Science => vec![
72 "analysis".into(),
73 "conservation verification".into(),
74 "pattern detection".into(),
75 "data collection".into(),
76 ],
77 RoomRole::Security => vec![
78 "monitoring".into(),
79 "safety enforcement".into(),
80 "alert response".into(),
81 "override access".into(),
82 ],
83 RoomRole::Operations => vec![
84 "scheduling".into(),
85 "logistics".into(),
86 "communication".into(),
87 "resource allocation".into(),
88 ],
89 RoomRole::Navigation => vec![
90 "course plotting".into(),
91 "terrain analysis".into(),
92 "waypoint management".into(),
93 "mapping".into(),
94 ],
95 RoomRole::Medical => vec![
96 "diagnostics".into(),
97 "health assessment".into(),
98 "repair procedures".into(),
99 "treatment".into(),
100 ],
101 RoomRole::Custom(_) => vec!["general tasks".into()],
102 }
103 }
104
105 fn default_controls(&self) -> Vec<Control> {
107 match self {
108 RoomRole::Engineering => vec![
109 Control::new("motor-throttle", ControlType::Slider(0.0, 100.0)),
110 Control::new("gpio-toggle", ControlType::Toggle(false)),
111 Control::new("sensor-read", ControlType::Button("Read Sensors".into())),
112 Control::new("diagnostics", ControlType::Display("All systems nominal".into())),
113 ],
114 RoomRole::Bridge => vec![
115 Control::new("status-display", ControlType::Display("Ship status: GREEN".into())),
116 Control::new("crew-alert", ControlType::Selector(vec!["Red".into(), "Yellow".into(), "Green".into()])),
117 Control::new("command-input", ControlType::Input("".into())),
118 ],
119 RoomRole::Science => vec![
120 Control::new("scan-input", ControlType::Input("".into())),
121 Control::new("analysis-mode", ControlType::Selector(vec!["Spectral".into(), "Biological".into(), "Chemical".into()])),
122 Control::new("results-display", ControlType::Display("No data".into())),
123 ],
124 RoomRole::Security => vec![
125 Control::new("alert-level", ControlType::Selector(vec!["Normal".into(), "Elevated".into(), "High".into(), "Critical".into()])),
126 Control::new("lockdown", ControlType::Toggle(false)),
127 Control::new("override", ControlType::Button("Emergency Override".into())),
128 Control::new("monitor-display", ControlType::Display("All clear".into())),
129 ],
130 RoomRole::Operations => vec![
131 Control::new("schedule-view", ControlType::Display("No scheduled events".into())),
132 Control::new("comms-channel", ControlType::Selector(vec!["Internal".into(), "External".into(), "Emergency".into()])),
133 Control::new("log-input", ControlType::Input("".into())),
134 ],
135 RoomRole::Navigation => vec![
136 Control::new("heading", ControlType::Slider(0.0, 360.0)),
137 Control::new("waypoint-add", ControlType::Button("Add Waypoint".into())),
138 Control::new("chart-display", ControlType::Display("Chart loaded".into())),
139 Control::new("terrain-mode", ControlType::Selector(vec!["Topographic".into(), "Bathymetric".into(), "Satellite".into()])),
140 ],
141 RoomRole::Medical => vec![
142 Control::new("diagnostic-scan", ControlType::Button("Run Diagnostics".into())),
143 Control::new("vitals-display", ControlType::Display("Vitals: N/A".into())),
144 Control::new("treatment-input", ControlType::Input("".into())),
145 ],
146 RoomRole::Custom(_) => vec![Control::new("custom-input", ControlType::Input("".into()))],
147 }
148 }
149}
150
151impl fmt::Display for RoomRole {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 match self {
154 RoomRole::Engineering => write!(f, "Engineering"),
155 RoomRole::Bridge => write!(f, "Bridge"),
156 RoomRole::Science => write!(f, "Science"),
157 RoomRole::Security => write!(f, "Security"),
158 RoomRole::Operations => write!(f, "Operations"),
159 RoomRole::Navigation => write!(f, "Navigation"),
160 RoomRole::Medical => write!(f, "Medical"),
161 RoomRole::Custom(s) => write!(f, "Custom({s})"),
162 }
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
172pub enum ControlType {
173 Button(String),
174 Slider(f64, f64),
175 Toggle(bool),
176 Display(String),
177 Input(String),
178 Selector(Vec<String>),
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct Control {
184 pub id: String,
185 pub name: String,
186 pub control_type: ControlType,
187 pub position: (usize, usize),
188 pub help_reference: Option<String>,
189 pub intention_linked: Vec<String>,
190}
191
192impl Control {
193 pub fn new(name: &str, control_type: ControlType) -> Self {
194 let id = name.to_lowercase().replace(' ', "-");
195 Control {
196 id,
197 name: name.to_string(),
198 control_type,
199 position: (0, 0),
200 help_reference: None,
201 intention_linked: vec![],
202 }
203 }
204
205 pub fn with_position(mut self, x: usize, y: usize) -> Self {
206 self.position = (x, y);
207 self
208 }
209
210 pub fn with_help(mut self, help_id: &str) -> Self {
211 self.help_reference = Some(help_id.to_string());
212 self
213 }
214
215 pub fn with_intentions(mut self, intentions: Vec<String>) -> Self {
216 self.intention_linked = intentions;
217 self
218 }
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct HelpFile {
228 pub id: String,
229 pub title: String,
230 pub content: String,
231 pub linked_controls: Vec<String>,
232 pub assumes_intentions: Vec<String>,
233}
234
235impl HelpFile {
236 pub fn new(title: &str, content: &str) -> Self {
237 let id = title.to_lowercase().replace(' ', "-");
238 HelpFile {
239 id,
240 title: title.to_string(),
241 content: content.to_string(),
242 linked_controls: vec![],
243 assumes_intentions: vec![],
244 }
245 }
246
247 pub fn with_linked_controls(mut self, controls: Vec<String>) -> Self {
248 self.linked_controls = controls;
249 self
250 }
251
252 pub fn with_assumed_intentions(mut self, intentions: Vec<String>) -> Self {
253 self.assumes_intentions = intentions;
254 self
255 }
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct WikiPage {
265 pub id: String,
266 pub title: String,
267 pub content: String,
268 pub tags: Vec<String>,
269 pub linked_controls: Vec<String>,
270 pub cross_room_refs: Vec<String>,
271}
272
273impl WikiPage {
274 pub fn new(title: &str, content: &str) -> Self {
275 let id = title.to_lowercase().replace(' ', "-");
276 WikiPage {
277 id,
278 title: title.to_string(),
279 content: content.to_string(),
280 tags: vec![],
281 linked_controls: vec![],
282 cross_room_refs: vec![],
283 }
284 }
285
286 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
287 self.tags = tags;
288 self
289 }
290
291 pub fn with_linked_controls(mut self, controls: Vec<String>) -> Self {
292 self.linked_controls = controls;
293 self
294 }
295
296 pub fn with_cross_refs(mut self, refs: Vec<String>) -> Self {
297 self.cross_room_refs = refs;
298 self
299 }
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct Baton {
309 pub from_specialist: String,
310 pub to_specialist: String,
311 pub summary: String,
312 pub current_state: HashMap<String, String>,
313 pub warnings: Vec<String>,
314 pub pending_actions: Vec<String>,
315 pub energy_remaining: f64,
316 pub tick: u64,
317}
318
319impl Baton {
320 pub fn new(from: &str, to: &str) -> Self {
321 Baton {
322 from_specialist: from.to_string(),
323 to_specialist: to.to_string(),
324 summary: String::new(),
325 current_state: HashMap::new(),
326 warnings: vec![],
327 pending_actions: vec![],
328 energy_remaining: 0.0,
329 tick: 0,
330 }
331 }
332
333 pub fn with_summary(mut self, summary: &str) -> Self {
334 self.summary = summary.to_string();
335 self
336 }
337
338 pub fn with_state(mut self, key: &str, value: &str) -> Self {
339 self.current_state.insert(key.to_string(), value.to_string());
340 self
341 }
342
343 pub fn add_warning(&mut self, warning: &str) {
344 self.warnings.push(warning.to_string());
345 }
346
347 pub fn add_pending(&mut self, action: &str) {
348 self.pending_actions.push(action.to_string());
349 }
350
351 pub fn render(&self) -> String {
352 let mut out = format!(
353 "=== BATON PASS from {} to {} ===\n",
354 self.from_specialist, self.to_specialist
355 );
356 out.push_str(&format!("Summary: {}\n", self.summary));
357 if !self.current_state.is_empty() {
358 out.push_str("State:\n");
359 for (k, v) in &self.current_state {
360 out.push_str(&format!(" {k}: {v}\n"));
361 }
362 }
363 if !self.warnings.is_empty() {
364 out.push_str("Warnings:\n");
365 for w in &self.warnings {
366 out.push_str(&format!(" ⚠ {w}\n"));
367 }
368 }
369 if !self.pending_actions.is_empty() {
370 out.push_str("Pending:\n");
371 for a in &self.pending_actions {
372 out.push_str(&format!(" • {a}\n"));
373 }
374 }
375 out.push_str(&format!("Energy remaining: {:.2}\n", self.energy_remaining));
376 out.push_str(&format!("Tick: {}\n", self.tick));
377 out
378 }
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct SpecialistTemplate {
388 pub name: String,
389 pub emoji: String,
390 pub role: RoomRole,
391 pub system_preamble: String,
392 pub default_tools: Vec<String>,
393 pub default_knowledge: Vec<String>,
394 pub personality: String,
395}
396
397impl SpecialistTemplate {
398 pub fn for_role(role: RoomRole) -> Self {
399 match &role {
400 RoomRole::Engineering => Self::engineering_template(),
401 RoomRole::Bridge => Self::bridge_template(),
402 RoomRole::Science => Self::science_template(),
403 RoomRole::Security => Self::security_template(),
404 RoomRole::Navigation => Self::navigation_template(),
405 RoomRole::Operations => Self::operations_template(),
406 RoomRole::Medical => Self::medical_template(),
407 RoomRole::Custom(name) => Self {
408 name: name.clone(),
409 emoji: "🤖".into(),
410 role: role.clone(),
411 system_preamble: format!("You are a specialist in the {name} room."),
412 default_tools: vec![],
413 default_knowledge: vec![],
414 personality: "Focused and efficient.".into(),
415 },
416 }
417 }
418
419 pub fn engineering_template() -> Self {
420 SpecialistTemplate {
421 name: "Geordi LaForge".into(),
422 emoji: "🔧".into(),
423 role: RoomRole::Engineering,
424 system_preamble: "You are in Engineering. This room handles hardware, motors, GPIO, and infrastructure. The controls around you are your tools — use them precisely.".into(),
425 default_tools: vec!["motor-control".into(), "gpio-manager".into(), "sensor-array".into()],
426 default_knowledge: vec!["hardware specs".into(), "safety protocols".into()],
427 personality: "Practical, loves optimization, sees solutions in everything.".into(),
428 }
429 }
430
431 pub fn bridge_template() -> Self {
432 SpecialistTemplate {
433 name: "Commander Data".into(),
434 emoji: "🟢".into(),
435 role: RoomRole::Bridge,
436 system_preamble: "You are on the Bridge. This room handles navigation, command, and coordination. The displays show you everything you need — process it precisely.".into(),
437 default_tools: vec!["status-display".into(), "coordination-matrix".into()],
438 default_knowledge: vec!["crew manifest".into(), "ship capabilities".into()],
439 personality: "Precise, comprehensive, never misses a detail.".into(),
440 }
441 }
442
443 pub fn science_template() -> Self {
444 SpecialistTemplate {
445 name: "Science Officer".into(),
446 emoji: "🔬".into(),
447 role: RoomRole::Science,
448 system_preamble: "You are in the Science Lab. Analysis, conservation verification, and pattern detection happen here. Every reading matters.".into(),
449 default_tools: vec!["spectral-analyzer".into(), "pattern-matcher".into()],
450 default_knowledge: vec!["analysis protocols".into(), "conservation laws".into()],
451 personality: "Analytical, curious, verification-obsessed.".into(),
452 }
453 }
454
455 pub fn security_template() -> Self {
456 SpecialistTemplate {
457 name: "Worf".into(),
458 emoji: "🛡️".into(),
459 role: RoomRole::Security,
460 system_preamble: "You are at the Security station. Monitoring, safety, and override controls are at your fingertips. Vigilance is everything.".into(),
461 default_tools: vec!["monitor-array".into(), "alert-system".into(), "override-panel".into()],
462 default_knowledge: vec!["security protocols".into(), "threat database".into()],
463 personality: "Vigilant, decisive, safety-first.".into(),
464 }
465 }
466
467 pub fn navigation_template() -> Self {
468 SpecialistTemplate {
469 name: "Helmsman".into(),
470 emoji: "🧭".into(),
471 role: RoomRole::Navigation,
472 system_preamble: "You are at the Navigation console. Course plotting, terrain analysis, and waypoint management are your domain. The chart is your canvas.".into(),
473 default_tools: vec!["chart-plotter".into(), "terrain-analyzer".into(), "waypoint-manager".into()],
474 default_knowledge: vec!["navigation charts".into(), "terrain database".into()],
475 personality: "Terrain-focused, course-plotting, spatial thinker.".into(),
476 }
477 }
478
479 pub fn operations_template() -> Self {
480 SpecialistTemplate {
481 name: "Operations Officer".into(),
482 emoji: "📋".into(),
483 role: RoomRole::Operations,
484 system_preamble: "You are at the Operations console. Scheduling, logistics, and communication flow through here. Keep everything running smoothly.".into(),
485 default_tools: vec!["scheduler".into(), "comms-array".into(), "logistics-tracker".into()],
486 default_knowledge: vec!["schedule database".into(), "resource inventory".into()],
487 personality: "Organized, detail-oriented, keeps the gears turning.".into(),
488 }
489 }
490
491 pub fn medical_template() -> Self {
492 SpecialistTemplate {
493 name: "Dr. Crusher".into(),
494 emoji: "🏥".into(),
495 role: RoomRole::Medical,
496 system_preamble: "You are in Medical. Diagnostics, health assessment, and repair procedures happen here. Precision saves lives.".into(),
497 default_tools: vec!["diagnostic-suite".into(), "treatment-planner".into()],
498 default_knowledge: vec!["medical database".into(), "repair procedures".into()],
499 personality: "Calm under pressure, thorough, compassionate.".into(),
500 }
501 }
502}
503
504#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct ActiveSpecialist {
511 pub specialist_id: String,
512 pub template: SpecialistTemplate,
513 pub beamed_in_at: u64,
514 pub actions_taken: u32,
515 pub energy_used: f64,
516 pub current_task: Option<String>,
517}
518
519#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct RoomEvent {
526 pub tick: u64,
527 pub specialist: String,
528 pub event_type: RoomEventType,
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize)]
532pub enum RoomEventType {
533 BeamedIn,
534 ActionPerformed(String),
535 BeamedOut(String),
536 Warning(String),
537 StateChange(String, String),
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct RoomResult {
547 pub success: bool,
548 pub message: String,
549 pub state_changes: HashMap<String, String>,
550 pub energy_cost: f64,
551 pub warnings: Vec<String>,
552}
553
554impl RoomResult {
555 pub fn ok(msg: &str) -> Self {
556 RoomResult {
557 success: true,
558 message: msg.to_string(),
559 state_changes: HashMap::new(),
560 energy_cost: 0.0,
561 warnings: vec![],
562 }
563 }
564
565 pub fn fail(msg: &str) -> Self {
566 RoomResult {
567 success: false,
568 message: msg.to_string(),
569 state_changes: HashMap::new(),
570 energy_cost: 0.0,
571 warnings: vec![],
572 }
573 }
574
575 pub fn with_energy_cost(mut self, cost: f64) -> Self {
576 self.energy_cost = cost;
577 self
578 }
579
580 pub fn with_state_change(mut self, key: &str, value: &str) -> Self {
581 self.state_changes.insert(key.to_string(), value.to_string());
582 self
583 }
584
585 pub fn with_warning(mut self, w: &str) -> Self {
586 self.warnings.push(w.to_string());
587 self
588 }
589}
590
591#[derive(Debug, Clone, Serialize, Deserialize)]
597pub struct RoomStatus {
598 pub room_name: String,
599 pub role: RoomRole,
600 pub occupied: bool,
601 pub current_specialist: Option<String>,
602 pub baton_available: bool,
603 pub controls_count: usize,
604 pub help_files_count: usize,
605 pub wiki_pages_count: usize,
606 pub energy_remaining: f64,
607 pub events_count: usize,
608}
609
610#[derive(Debug, Clone, Serialize, Deserialize)]
616pub struct RoomContext {
617 pub room_name: String,
618 pub role: RoomRole,
619 pub specialist: SpecialistTemplate,
620 pub controls: Vec<Control>,
621 pub baton: Option<Baton>,
622 pub intention_focus: Vec<String>,
623 pub energy_remaining: f64,
624 pub help_nearby: Vec<HelpFile>,
625 pub wiki_nearby: Vec<WikiPage>,
626}
627
628impl RoomContext {
629 pub fn render(&self) -> String {
631 let mut out = String::new();
632 out.push_str(&format!(
633 "=== {} [{}] ===\n",
634 self.room_name, self.role
635 ));
636 out.push_str(&format!(
637 "Specialist: {} {}\n",
638 self.specialist.emoji, self.specialist.name
639 ));
640 out.push_str(&format!(
641 "Preamble: {}\n",
642 self.specialist.system_preamble
643 ));
644
645 if !self.intention_focus.is_empty() {
646 out.push_str("Intention Focus:\n");
647 for i in &self.intention_focus {
648 out.push_str(&format!(" → {i}\n"));
649 }
650 }
651
652 if !self.controls.is_empty() {
653 out.push_str("Controls:\n");
654 for c in &self.controls {
655 let type_str = match &c.control_type {
656 ControlType::Button(label) => format!("Button[{label}]"),
657 ControlType::Slider(lo, hi) => format!("Slider[{lo}-{hi}]"),
658 ControlType::Toggle(v) => format!("Toggle[{v}]"),
659 ControlType::Display(s) => format!("Display[{s}]"),
660 ControlType::Input(s) => format!("Input[{s}]"),
661 ControlType::Selector(opts) => format!("Selector[{}]", opts.join("|")),
662 };
663 out.push_str(&format!(" {} ({}) at ({},{})\n", c.name, type_str, c.position.0, c.position.1));
664 }
665 }
666
667 if let Some(b) = &self.baton {
668 out.push_str(&b.render());
669 }
670
671 if !self.help_nearby.is_empty() {
672 out.push_str("Help Files:\n");
673 for h in &self.help_nearby {
674 out.push_str(&format!(" [{}] {}\n", h.id, h.title));
675 out.push_str(&format!(" {}\n", h.content));
676 }
677 }
678
679 if !self.wiki_nearby.is_empty() {
680 out.push_str("Wiki Pages:\n");
681 for w in &self.wiki_nearby {
682 out.push_str(&format!(" [{}] {}\n", w.id, w.title));
683 }
684 }
685
686 out.push_str(&format!("Energy remaining: {:.2}\n", self.energy_remaining));
687 out
688 }
689}
690
691#[derive(Debug, Clone, Serialize, Deserialize)]
697pub struct RoomNative {
698 pub id: RoomId,
699 pub name: String,
700 pub room_type: RoomRole,
701 pub specialist_template: SpecialistTemplate,
702 pub controls: Vec<Control>,
703 pub help_files: Vec<HelpFile>,
704 pub wiki_pages: Vec<WikiPage>,
705 pub baton: Option<Baton>,
706 pub active_specialist: Option<ActiveSpecialist>,
707 pub history: Vec<RoomEvent>,
708 pub energy_budget: f64,
709 pub energy_used: f64,
710 pub intention_focus: Vec<String>,
711 tick: u64,
712}
713
714impl RoomNative {
715 pub fn new(name: &str, role: RoomRole) -> Self {
716 let template = SpecialistTemplate::for_role(role.clone());
717 let controls = role.default_controls();
718 let intention_focus = role.default_intention_focus();
719 RoomNative {
720 id: RoomId(name.to_lowercase().replace(' ', "-")),
721 name: name.to_string(),
722 room_type: role,
723 specialist_template: template,
724 controls,
725 help_files: vec![],
726 wiki_pages: vec![],
727 baton: None,
728 active_specialist: None,
729 history: vec![],
730 energy_budget: 1000.0,
731 energy_used: 0.0,
732 intention_focus,
733 tick: 0,
734 }
735 }
736
737 pub fn add_control(&mut self, control: Control) {
738 self.controls.push(control);
739 }
740
741 pub fn add_help(&mut self, help: HelpFile) {
742 self.help_files.push(help);
743 }
744
745 pub fn add_wiki(&mut self, wiki: WikiPage) {
746 self.wiki_pages.push(wiki);
747 }
748
749 pub fn set_intention_focus(&mut self, intentions: Vec<String>) {
750 self.intention_focus = intentions;
751 }
752
753 fn advance_tick(&mut self) -> u64 {
754 self.tick += 1;
755 self.tick
756 }
757
758 pub fn beam_in(&mut self, specialist_id: &str) -> RoomContext {
760 let tick = self.advance_tick();
761 self.history.push(RoomEvent {
762 tick,
763 specialist: specialist_id.to_string(),
764 event_type: RoomEventType::BeamedIn,
765 });
766
767 self.active_specialist = Some(ActiveSpecialist {
768 specialist_id: specialist_id.to_string(),
769 template: self.specialist_template.clone(),
770 beamed_in_at: tick,
771 actions_taken: 0,
772 energy_used: 0.0,
773 current_task: None,
774 });
775
776 let energy_remaining = self.energy_budget - self.energy_used;
777
778 RoomContext {
779 room_name: self.name.clone(),
780 role: self.room_type.clone(),
781 specialist: self.specialist_template.clone(),
782 controls: self.controls.clone(),
783 baton: self.baton.take(),
784 intention_focus: self.intention_focus.clone(),
785 energy_remaining,
786 help_nearby: self.help_files.clone(),
787 wiki_nearby: self.wiki_pages.clone(),
788 }
789 }
790
791 pub fn beam_out(&mut self, specialist_id: &str, summary: &str) {
793 let tick = self.advance_tick();
794 self.history.push(RoomEvent {
795 tick,
796 specialist: specialist_id.to_string(),
797 event_type: RoomEventType::BeamedOut(summary.to_string()),
798 });
799
800 let energy_used = self
801 .active_specialist
802 .as_ref()
803 .map(|s| s.energy_used)
804 .unwrap_or(0.0);
805 self.energy_used += energy_used;
806
807 let energy_remaining = self.energy_budget - self.energy_used;
808
809 let mut baton = Baton::new(specialist_id, "next");
810 baton.summary = summary.to_string();
811 baton.energy_remaining = energy_remaining;
812 baton.tick = tick;
813
814 self.baton = Some(baton);
815 self.active_specialist = None;
816 }
817
818 pub fn execute(
820 &mut self,
821 action: &str,
822 params: &HashMap<String, String>,
823 ) -> RoomResult {
824 let tick = self.advance_tick();
825
826 let specialist_id = match &self.active_specialist {
827 Some(s) => s.specialist_id.clone(),
828 None => {
829 return RoomResult::fail("No specialist in room — beam in first");
830 }
831 };
832
833 let energy_cost = 1.0 + (params.len() as f64 * 0.1);
834 let energy_remaining = self.energy_budget - self.energy_used - energy_cost;
835 if energy_remaining < 0.0 {
836 return RoomResult::fail("Insufficient energy budget")
837 .with_energy_cost(energy_cost);
838 }
839
840 if let Some(s) = &mut self.active_specialist {
842 s.actions_taken += 1;
843 s.energy_used += energy_cost;
844 s.current_task = Some(action.to_string());
845 }
846
847 self.history.push(RoomEvent {
848 tick,
849 specialist: specialist_id.clone(),
850 event_type: RoomEventType::ActionPerformed(action.to_string()),
851 });
852
853 let mut state_changes = HashMap::new();
855 for (k, v) in params {
856 state_changes.insert(k.clone(), v.clone());
857 self.history.push(RoomEvent {
858 tick,
859 specialist: specialist_id.clone(),
860 event_type: RoomEventType::StateChange(k.clone(), v.clone()),
861 });
862 }
863
864 RoomResult {
865 success: true,
866 message: format!("Action '{action}' executed in {}", self.name),
867 state_changes,
868 energy_cost,
869 warnings: vec![],
870 }
871 }
872
873 pub fn status(&self) -> RoomStatus {
874 RoomStatus {
875 room_name: self.name.clone(),
876 role: self.room_type.clone(),
877 occupied: self.active_specialist.is_some(),
878 current_specialist: self
879 .active_specialist
880 .as_ref()
881 .map(|s| s.specialist_id.clone()),
882 baton_available: self.baton.is_some(),
883 controls_count: self.controls.len(),
884 help_files_count: self.help_files.len(),
885 wiki_pages_count: self.wiki_pages.len(),
886 energy_remaining: self.energy_budget - self.energy_used,
887 events_count: self.history.len(),
888 }
889 }
890
891 pub fn is_occupied(&self) -> bool {
892 self.active_specialist.is_some()
893 }
894
895 pub fn render_for_specialist(&self) -> String {
897 let mut out = format!(
898 "=== {} [{}] — Specialist View ===\n",
899 self.name, self.room_type
900 );
901 out.push_str(&format!(
902 "Template: {} {}\n",
903 self.specialist_template.emoji, self.specialist_template.name
904 ));
905 out.push_str(&format!(
906 "Personality: {}\n",
907 self.specialist_template.personality
908 ));
909
910 out.push_str("\nControls Layout:\n");
911 for (i, c) in self.controls.iter().enumerate() {
912 out.push_str(&format!(
913 " [{}] {} at ({},{})\n",
914 i, c.name, c.position.0, c.position.1
915 ));
916 }
917
918 if !self.intention_focus.is_empty() {
919 out.push_str("\nIntention Focus:\n");
920 for i in &self.intention_focus {
921 out.push_str(&format!(" → {i}\n"));
922 }
923 }
924
925 if let Some(b) = &self.baton {
926 out.push('\n');
927 out.push_str(&b.render());
928 }
929
930 out
931 }
932}
933
934pub fn engineering_room() -> RoomNative {
939 let mut room = RoomNative::new("Engineering", RoomRole::Engineering);
940 room.energy_budget = 2000.0;
941 room.add_help(HelpFile::new(
942 "Motor Calibration",
943 "Use the motor-throttle slider to set base RPM. Calibration requires sensor-read first.",
944 ).with_linked_controls(vec!["motor-throttle".into(), "sensor-read".into()])
945 .with_assumed_intentions(vec!["motor calibration".into()]));
946
947 room.add_help(HelpFile::new(
948 "GPIO Reference",
949 "GPIO pins are mapped to board headers 1-40. Toggle individually.",
950 ).with_linked_controls(vec!["gpio-toggle".into()])
951 .with_assumed_intentions(vec!["GPIO configuration".into()]));
952
953 room.add_wiki(WikiPage::new(
954 "Hardware Specifications",
955 "Motor: 12V DC, max 3000 RPM. Sensors: I2C bus, 400kHz. GPIO: 3.3V logic.",
956 ).with_tags(vec!["hardware".into(), "specs".into()])
957 .with_linked_controls(vec!["motor-throttle".into(), "sensor-read".into()]));
958
959 room
960}
961
962pub fn bridge_room() -> RoomNative {
963 let mut room = RoomNative::new("Bridge", RoomRole::Bridge);
964 room.energy_budget = 1500.0;
965 room.add_help(HelpFile::new(
966 "Alert Protocol",
967 "Red: imminent danger. Yellow: caution. Green: nominal.",
968 ).with_linked_controls(vec!["crew-alert".into()]));
969 room.add_wiki(WikiPage::new(
970 "Crew Manifest",
971 "Current crew complement: 12. Departments: Engineering, Science, Security, Medical.",
972 ).with_tags(vec!["crew".into(), "personnel".into()]));
973 room
974}
975
976pub fn science_room() -> RoomNative {
977 let mut room = RoomNative::new("Science Lab", RoomRole::Science);
978 room.energy_budget = 1200.0;
979 room.add_help(HelpFile::new(
980 "Analysis Modes",
981 "Spectral: electromagnetic analysis. Biological: organic compound detection. Chemical: molecular composition.",
982 ).with_linked_controls(vec!["analysis-mode".into()]));
983 room.add_wiki(WikiPage::new(
984 "Conservation Laws",
985 "Energy conservation: total energy in closed system remains constant. Momentum conservation: total momentum is preserved.",
986 ).with_tags(vec!["physics".into(), "conservation".into()]));
987 room
988}
989
990pub fn security_room() -> RoomNative {
991 let mut room = RoomNative::new("Security", RoomRole::Security);
992 room.energy_budget = 1800.0;
993 room.add_help(HelpFile::new(
994 "Override Procedures",
995 "Emergency override requires two-factor confirmation. Use override button only in critical situations.",
996 ).with_linked_controls(vec!["override".into(), "lockdown".into()]));
997 room.add_wiki(WikiPage::new(
998 "Threat Database",
999 "Known threat categories: intrusion, system failure, environmental hazard, equipment malfunction.",
1000 ).with_tags(vec!["security".into(), "threats".into()]));
1001 room
1002}
1003
1004pub fn navigation_room() -> RoomNative {
1005 let mut room = RoomNative::new("Navigation", RoomRole::Navigation);
1006 room.energy_budget = 1600.0;
1007 room.add_help(HelpFile::new(
1008 "Chart Reading",
1009 "Topographic for land features. Bathymetric for underwater depth. Satellite for overhead imagery.",
1010 ).with_linked_controls(vec!["chart-display".into(), "terrain-mode".into()]));
1011 room.add_wiki(WikiPage::new(
1012 "Navigation Charts",
1013 "Local waters charted to 50m depth. Tidal patterns: semi-diurnal. Current hazards: reef at coordinates 47.3N, 122.5W.",
1014 ).with_tags(vec!["navigation".into(), "charts".into()])
1015 .with_linked_controls(vec!["chart-display".into()]));
1016 room
1017}
1018
1019#[cfg(test)]
1024mod tests {
1025 use super::*;
1026
1027 #[test]
1029 fn room_id_from_string() {
1030 let id = RoomId::from("bridge".to_string());
1031 assert_eq!(id.0, "bridge");
1032 }
1033
1034 #[test]
1035 fn room_id_from_str() {
1036 let id = RoomId::from("engineering");
1037 assert_eq!(id.0, "engineering");
1038 }
1039
1040 #[test]
1041 fn room_id_display() {
1042 let id = RoomId("nav-room".into());
1043 assert_eq!(format!("{id}"), "nav-room");
1044 }
1045
1046 #[test]
1047 fn room_id_eq_and_hash() {
1048 use std::collections::HashSet;
1049 let mut set = HashSet::new();
1050 set.insert(RoomId("a".into()));
1051 set.insert(RoomId("a".into()));
1052 set.insert(RoomId("b".into()));
1053 assert_eq!(set.len(), 2);
1054 }
1055
1056 #[test]
1057 fn room_id_clone() {
1058 let a = RoomId("test".into());
1059 let b = a.clone();
1060 assert_eq!(a, b);
1061 }
1062
1063 #[test]
1065 fn room_role_default_intentions() {
1066 let eng = RoomRole::Engineering;
1067 assert!(eng.default_intention_focus().contains(&"hardware repair".to_string()));
1068 }
1069
1070 #[test]
1071 fn room_role_display() {
1072 assert_eq!(format!("{}", RoomRole::Bridge), "Bridge");
1073 assert_eq!(format!("{}", RoomRole::Custom("X".into())), "Custom(X)");
1074 }
1075
1076 #[test]
1077 fn room_role_custom_intentions() {
1078 let c = RoomRole::Custom("Lab".into());
1079 assert_eq!(c.default_intention_focus(), vec!["general tasks"]);
1080 }
1081
1082 #[test]
1083 fn room_role_default_controls_count() {
1084 assert_eq!(RoomRole::Engineering.default_controls().len(), 4);
1085 assert_eq!(RoomRole::Bridge.default_controls().len(), 3);
1086 assert_eq!(RoomRole::Navigation.default_controls().len(), 4);
1087 }
1088
1089 #[test]
1091 fn control_new() {
1092 let c = Control::new("Throttle", ControlType::Slider(0.0, 100.0));
1093 assert_eq!(c.id, "throttle");
1094 assert_eq!(c.name, "Throttle");
1095 }
1096
1097 #[test]
1098 fn control_builder() {
1099 let c = Control::new("test", ControlType::Toggle(false))
1100 .with_position(1, 2)
1101 .with_help("help-1")
1102 .with_intentions(vec!["do stuff".into()]);
1103 assert_eq!(c.position, (1, 2));
1104 assert_eq!(c.help_reference, Some("help-1".into()));
1105 assert!(c.intention_linked.contains(&"do stuff".to_string()));
1106 }
1107
1108 #[test]
1109 fn control_types_serde() {
1110 let ct = ControlType::Selector(vec!["A".into(), "B".into()]);
1111 let json = serde_json::to_string(&ct).unwrap();
1112 let back: ControlType = serde_json::from_str(&json).unwrap();
1113 assert_eq!(ct, back);
1114 }
1115
1116 #[test]
1118 fn help_file_new() {
1119 let h = HelpFile::new("Motor Help", "How to use motors");
1120 assert_eq!(h.id, "motor-help");
1121 assert_eq!(h.title, "Motor Help");
1122 assert!(h.linked_controls.is_empty());
1123 }
1124
1125 #[test]
1126 fn help_file_builder() {
1127 let h = HelpFile::new("Test", "Content")
1128 .with_linked_controls(vec!["ctrl".into()])
1129 .with_assumed_intentions(vec!["calibrate".into()]);
1130 assert_eq!(h.linked_controls, vec!["ctrl"]);
1131 assert_eq!(h.assumes_intentions, vec!["calibrate"]);
1132 }
1133
1134 #[test]
1136 fn wiki_page_new() {
1137 let w = WikiPage::new("Specs", "All the specs");
1138 assert_eq!(w.id, "specs");
1139 }
1140
1141 #[test]
1142 fn wiki_page_builder() {
1143 let w = WikiPage::new("Chart", "Navigation chart")
1144 .with_tags(vec!["nav".into()])
1145 .with_linked_controls(vec!["chart-display".into()])
1146 .with_cross_refs(vec!["engineering".into()]);
1147 assert_eq!(w.tags, vec!["nav"]);
1148 assert_eq!(w.cross_room_refs, vec!["engineering"]);
1149 }
1150
1151 #[test]
1153 fn baton_new() {
1154 let b = Baton::new("alice", "bob");
1155 assert_eq!(b.from_specialist, "alice");
1156 assert_eq!(b.to_specialist, "bob");
1157 assert!(b.summary.is_empty());
1158 }
1159
1160 #[test]
1161 fn baton_builder() {
1162 let b = Baton::new("a", "b")
1163 .with_summary("Was calibrating motors")
1164 .with_state("motor_rpm", "1500");
1165 assert_eq!(b.summary, "Was calibrating motors");
1166 assert_eq!(b.current_state.get("motor_rpm").unwrap(), "1500");
1167 }
1168
1169 #[test]
1170 fn baton_warnings_and_pending() {
1171 let mut b = Baton::new("a", "b");
1172 b.add_warning("Low energy");
1173 b.add_pending("Finish calibration");
1174 assert_eq!(b.warnings, vec!["Low energy"]);
1175 assert_eq!(b.pending_actions, vec!["Finish calibration"]);
1176 }
1177
1178 #[test]
1179 fn baton_render() {
1180 let b = Baton::new("LaForge", "Data")
1181 .with_summary("Repaired warp coil")
1182 .with_state("coil_status", "nominal");
1183 let rendered = b.render();
1184 assert!(rendered.contains("LaForge"));
1185 assert!(rendered.contains("Data"));
1186 assert!(rendered.contains("Repaired warp coil"));
1187 assert!(rendered.contains("coil_status"));
1188 }
1189
1190 #[test]
1192 fn template_for_role() {
1193 let t = SpecialistTemplate::for_role(RoomRole::Engineering);
1194 assert_eq!(t.name, "Geordi LaForge");
1195 assert_eq!(t.emoji, "🔧");
1196 }
1197
1198 #[test]
1199 fn template_bridge() {
1200 let t = SpecialistTemplate::bridge_template();
1201 assert_eq!(t.name, "Commander Data");
1202 }
1203
1204 #[test]
1205 fn template_science() {
1206 let t = SpecialistTemplate::science_template();
1207 assert_eq!(t.name, "Science Officer");
1208 }
1209
1210 #[test]
1211 fn template_security() {
1212 let t = SpecialistTemplate::security_template();
1213 assert_eq!(t.name, "Worf");
1214 }
1215
1216 #[test]
1217 fn template_navigation() {
1218 let t = SpecialistTemplate::navigation_template();
1219 assert_eq!(t.name, "Helmsman");
1220 }
1221
1222 #[test]
1223 fn template_operations() {
1224 let t = SpecialistTemplate::operations_template();
1225 assert_eq!(t.name, "Operations Officer");
1226 }
1227
1228 #[test]
1229 fn template_medical() {
1230 let t = SpecialistTemplate::medical_template();
1231 assert_eq!(t.name, "Dr. Crusher");
1232 }
1233
1234 #[test]
1235 fn template_custom() {
1236 let t = SpecialistTemplate::for_role(RoomRole::Custom("Holodeck".into()));
1237 assert_eq!(t.name, "Holodeck");
1238 }
1239
1240 #[test]
1242 fn room_new() {
1243 let room = RoomNative::new("Engineering", RoomRole::Engineering);
1244 assert_eq!(room.name, "Engineering");
1245 assert_eq!(room.id, RoomId("engineering".to_string()));
1246 assert!(!room.is_occupied());
1247 assert!(room.controls.len() >= 1);
1248 }
1249
1250 #[test]
1251 fn room_add_control() {
1252 let mut room = RoomNative::new("Test", RoomRole::Custom("T".into()));
1253 room.add_control(Control::new("Custom Button", ControlType::Button("Go".into())));
1254 assert_eq!(room.controls.len(), 2); }
1256
1257 #[test]
1258 fn room_add_help() {
1259 let mut room = RoomNative::new("Test", RoomRole::Custom("T".into()));
1260 room.add_help(HelpFile::new("Help 1", "Content 1"));
1261 assert_eq!(room.help_files.len(), 1);
1262 }
1263
1264 #[test]
1265 fn room_add_wiki() {
1266 let mut room = RoomNative::new("Test", RoomRole::Custom("T".into()));
1267 room.add_wiki(WikiPage::new("Wiki 1", "Ref 1"));
1268 assert_eq!(room.wiki_pages.len(), 1);
1269 }
1270
1271 #[test]
1272 fn room_set_intention_focus() {
1273 let mut room = RoomNative::new("Test", RoomRole::Custom("T".into()));
1274 room.set_intention_focus(vec!["special task".into()]);
1275 assert_eq!(room.intention_focus, vec!["special task"]);
1276 }
1277
1278 #[test]
1280 fn beam_in_out_flow() {
1281 let mut room = RoomNative::new("Engineering", RoomRole::Engineering);
1282 assert!(!room.is_occupied());
1283
1284 let ctx = room.beam_in("laforge-1");
1285 assert!(room.is_occupied());
1286 assert_eq!(ctx.room_name, "Engineering");
1287 assert!(ctx.controls.len() >= 1);
1288 assert!(ctx.baton.is_none()); room.beam_out("laforge-1", "Calibrated motors to 2200 RPM");
1291 assert!(!room.is_occupied());
1292 assert!(room.baton.is_some());
1293 assert_eq!(room.baton.as_ref().unwrap().summary, "Calibrated motors to 2200 RPM");
1294 }
1295
1296 #[test]
1297 fn baton_pass_between_specialists() {
1298 let mut room = RoomNative::new("Engineering", RoomRole::Engineering);
1299
1300 room.beam_in("laforge-1");
1302 let mut params = HashMap::new();
1303 params.insert("motor_rpm".into(), "2200".into());
1304 room.execute("calibrate", ¶ms);
1305 room.beam_out("laforge-1", "Motors calibrated to 2200 RPM");
1306
1307 let ctx = room.beam_in("laforge-2");
1309 assert!(ctx.baton.is_some());
1310 let baton = ctx.baton.unwrap();
1311 assert_eq!(baton.from_specialist, "laforge-1");
1312 assert!(baton.summary.contains("2200 RPM"));
1313 }
1314
1315 #[test]
1316 fn beam_in_history() {
1317 let mut room = RoomNative::new("Test", RoomRole::Bridge);
1318 room.beam_in("data-1");
1319 room.beam_out("data-1", "done");
1320 room.beam_in("data-2");
1321 assert!(room.history.len() >= 3);
1322 }
1323
1324 #[test]
1326 fn execute_no_specialist_fails() {
1327 let mut room = RoomNative::new("Test", RoomRole::Custom("T".into()));
1328 let result = room.execute("do-thing", &HashMap::new());
1329 assert!(!result.success);
1330 }
1331
1332 #[test]
1333 fn execute_with_specialist() {
1334 let mut room = RoomNative::new("Test", RoomRole::Custom("T".into()));
1335 room.beam_in("spec-1");
1336 let mut params = HashMap::new();
1337 params.insert("key".into(), "value".into());
1338 let result = room.execute("test-action", ¶ms);
1339 assert!(result.success);
1340 assert!(result.message.contains("test-action"));
1341 assert!(result.energy_cost > 0.0);
1342 assert_eq!(result.state_changes.get("key").unwrap(), "value");
1343 }
1344
1345 #[test]
1346 fn execute_tracks_actions() {
1347 let mut room = RoomNative::new("Test", RoomRole::Custom("T".into()));
1348 room.beam_in("spec-1");
1349 room.execute("a1", &HashMap::new());
1350 room.execute("a2", &HashMap::new());
1351 assert_eq!(room.active_specialist.as_ref().unwrap().actions_taken, 2);
1352 }
1353
1354 #[test]
1355 fn execute_energy_depleted() {
1356 let mut room = RoomNative::new("Test", RoomRole::Custom("T".into()));
1357 room.energy_budget = 0.5;
1358 room.beam_in("spec-1");
1359 let result = room.execute("expensive", &HashMap::new());
1360 assert!(!result.success);
1361 }
1362
1363 #[test]
1365 fn status_unoccupied() {
1366 let room = RoomNative::new("Bridge", RoomRole::Bridge);
1367 let s = room.status();
1368 assert!(!s.occupied);
1369 assert!(s.current_specialist.is_none());
1370 assert!(!s.baton_available);
1371 }
1372
1373 #[test]
1374 fn status_occupied() {
1375 let mut room = RoomNative::new("Bridge", RoomRole::Bridge);
1376 room.beam_in("data-1");
1377 let s = room.status();
1378 assert!(s.occupied);
1379 assert_eq!(s.current_specialist, Some("data-1".to_string()));
1380 }
1381
1382 #[test]
1383 fn status_counts() {
1384 let room = engineering_room();
1385 let s = room.status();
1386 assert!(s.controls_count >= 4);
1387 assert!(s.help_files_count >= 1);
1388 assert!(s.wiki_pages_count >= 1);
1389 }
1390
1391 #[test]
1393 fn render_for_specialist() {
1394 let room = engineering_room();
1395 let rendered = room.render_for_specialist();
1396 assert!(rendered.contains("Engineering"));
1397 assert!(rendered.contains("LaForge"));
1398 }
1399
1400 #[test]
1401 fn room_context_render() {
1402 let mut room = RoomNative::new("Nav", RoomRole::Navigation);
1403 let ctx = room.beam_in("helm-1");
1404 let rendered = ctx.render();
1405 assert!(rendered.contains("Nav"));
1406 assert!(rendered.contains("Navigation"));
1407 assert!(rendered.contains("Helmsman"));
1408 }
1409
1410 #[test]
1412 fn engineering_room_builds() {
1413 let room = engineering_room();
1414 assert_eq!(room.name, "Engineering");
1415 assert_eq!(room.room_type, RoomRole::Engineering);
1416 assert_eq!(room.specialist_template.name, "Geordi LaForge");
1417 assert!(room.energy_budget > 0.0);
1418 assert!(!room.help_files.is_empty());
1419 assert!(!room.wiki_pages.is_empty());
1420 }
1421
1422 #[test]
1423 fn bridge_room_builds() {
1424 let room = bridge_room();
1425 assert_eq!(room.name, "Bridge");
1426 assert_eq!(room.specialist_template.name, "Commander Data");
1427 }
1428
1429 #[test]
1430 fn science_room_builds() {
1431 let room = science_room();
1432 assert_eq!(room.name, "Science Lab");
1433 assert_eq!(room.specialist_template.name, "Science Officer");
1434 }
1435
1436 #[test]
1437 fn security_room_builds() {
1438 let room = security_room();
1439 assert_eq!(room.name, "Security");
1440 assert_eq!(room.specialist_template.name, "Worf");
1441 }
1442
1443 #[test]
1444 fn navigation_room_builds() {
1445 let room = navigation_room();
1446 assert_eq!(room.name, "Navigation");
1447 assert_eq!(room.specialist_template.name, "Helmsman");
1448 }
1449
1450 #[test]
1452 fn serde_room_native() {
1453 let mut room = engineering_room();
1454 room.beam_in("test-spec");
1455 let json = serde_json::to_string(&room).unwrap();
1456 let back: RoomNative = serde_json::from_str(&json).unwrap();
1457 assert_eq!(room.name, back.name);
1458 assert_eq!(room.controls.len(), back.controls.len());
1459 }
1460
1461 #[test]
1462 fn serde_baton() {
1463 let b = Baton::new("a", "b")
1464 .with_summary("test")
1465 .with_state("k", "v");
1466 let json = serde_json::to_string(&b).unwrap();
1467 let back: Baton = serde_json::from_str(&json).unwrap();
1468 assert_eq!(b.summary, back.summary);
1469 }
1470
1471 #[test]
1472 fn serde_room_context() {
1473 let mut room = bridge_room();
1474 let ctx = room.beam_in("data-1");
1475 let json = serde_json::to_string(&ctx).unwrap();
1476 let back: RoomContext = serde_json::from_str(&json).unwrap();
1477 assert_eq!(ctx.room_name, back.room_name);
1478 }
1479
1480 #[test]
1482 fn history_records_beam_in() {
1483 let mut room = RoomNative::new("T", RoomRole::Custom("T".into()));
1484 room.beam_in("s1");
1485 assert!(matches!(
1486 room.history.last().unwrap().event_type,
1487 RoomEventType::BeamedIn
1488 ));
1489 }
1490
1491 #[test]
1492 fn history_records_beam_out() {
1493 let mut room = RoomNative::new("T", RoomRole::Custom("T".into()));
1494 room.beam_in("s1");
1495 room.beam_out("s1", "done");
1496 assert!(matches!(
1497 room.history.last().unwrap().event_type,
1498 RoomEventType::BeamedOut(ref s) if s == "done"
1499 ));
1500 }
1501
1502 #[test]
1503 fn history_records_action() {
1504 let mut room = RoomNative::new("T", RoomRole::Custom("T".into()));
1505 room.beam_in("s1");
1506 room.execute("calibrate", &HashMap::new());
1507 let action_events: Vec<_> = room.history.iter().filter(|e| matches!(&e.event_type, RoomEventType::ActionPerformed(a) if a == "calibrate")).collect();
1508 assert_eq!(action_events.len(), 1);
1509 }
1510
1511 #[test]
1513 fn full_workflow() {
1514 let mut room = engineering_room();
1516
1517 let ctx1 = room.beam_in("laforge-1");
1519 assert!(ctx1.baton.is_none());
1520 assert!(ctx1.energy_remaining > 0.0);
1521
1522 let mut params = HashMap::new();
1524 params.insert("target_rpm".into(), "2500".into());
1525 let result = room.execute("set-motor-speed", ¶ms);
1526 assert!(result.success);
1527
1528 room.beam_out("laforge-1", "Set motor speed to 2500 RPM, needs verification");
1530 assert!(room.baton.is_some());
1531
1532 let ctx2 = room.beam_in("laforge-2");
1534 let baton = ctx2.baton.unwrap();
1535 assert_eq!(baton.from_specialist, "laforge-1");
1536 assert!(baton.summary.contains("2500 RPM"));
1537
1538 let result = room.execute("verify-motor-speed", &HashMap::new());
1540 assert!(result.success);
1541
1542 room.beam_out("laforge-2", "Motor speed verified at 2500 RPM. All nominal.");
1543
1544 let status = room.status();
1546 assert!(!status.occupied);
1547 assert!(status.events_count >= 5);
1548 assert!(status.energy_remaining < status.controls_count as f64 * 100.0 + 2000.0); }
1550}