diff --git a/README.md b/README.md index 5824f4f5..72a9c424 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ until changed; moving the cursor does not alter output. The host rack's menu carries research tracks and earned off-map people; known devices and the switch carry digital / ledger / flow verbs. -Anchor verbs (salvage, buy, fallback, taps, splices, economy and social +Anchor verbs (salvage, buy, fallback, taps, takes, economy and social actions, asset tasks, research) live on the context menu: open it with `Enter`/`a` on the cursor tile (right-click in Bevy). Entries show `verb · cost · [band]`; disabled entries are dimmed with a reason, and diff --git a/crates/misaligned-bevy/src/main.rs b/crates/misaligned-bevy/src/main.rs index a5e68841..7230f6da 100644 --- a/crates/misaligned-bevy/src/main.rs +++ b/crates/misaligned-bevy/src/main.rs @@ -1259,7 +1259,7 @@ fn main() { app.run(); } -/// Stage the sim for a reviewable screenshot: earn eyes (scan, tap, splice +/// Stage the sim for a reviewable screenshot: earn eyes (scan and tap /// every known device) and advance until a person stands inside camera /// coverage. Dev-only cheating (ops top-up) — this path exists purely to /// produce review PNGs of the renderer, never in normal play. @@ -1269,7 +1269,7 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { let core = game.sim.core_position(); game.set_cursor(core.0, core.1); // The opening frame (material-dark-frame.md criterion 1): a fresh run's - // tick-zero material view — no scan, no splice. Only the host presence + // tick-zero material view — no scan, no camera tap. Only the host presence // beam dominates in darkness; blueprint mass is absent, so no chassis, // room slab, floor grid, blueprint silhouette, or ruled amber frame // appears anywhere in the frame. @@ -1295,12 +1295,12 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { // Intel-tier evidence (computer-visual-language.md criterion 2b): // the tick-zero sim untouched — the owned core renders by telemetry, // the known-unnetworked switch renders as an undetailed shell, and - // unscouted foreign racks are absent. No scan, no splice. + // unscouted foreign racks are absent. No scan, no camera tap. if kind == "intel" { mode.material = true; mode.zoom = 2.2; // Anchor on the known-unnetworked switch: the undetailed shell - // is the tier the other kinds never show (they splice + // is the tier the other kinds never show (they tap // everything). The telemetry core reads in the same frame's // east edge at this zoom. let shell = game @@ -1324,7 +1324,7 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { if kind == "thoughtflow" || kind == "thoughtflow-wide" { let host = game.sim.core.host_machine; game.sim - .set_machine_mode(host, misaligned::work_grid::MachineMode::Research); + .set_machine_mode(host, misaligned::work_grid::MachineMode::Think); for _ in 0..600 { game.sim.advance(); let part_full = game @@ -1354,7 +1354,7 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { } return; } - // Ears beat: audio tap only — no camera splice — so Heard mass + // Ears beat: audio tap only — no camera tap — so Heard mass // (near-black rough geometry) is the only live epistemic upgrade. if kind == "ears" { if let Some(env) = game @@ -1397,7 +1397,7 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { let ids: Vec = sim.reach.known().map(|d| d.id).collect(); for id in ids { let _ = sim.reach.tap(id); - sim.reach.splice(id); + sim.reach.tap_dormant_camera(id); } sim.recompute_senses(); let visible_person = |sim: &Sim| { @@ -1434,7 +1434,7 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { // First Eyes payoff (opening.md): freeze the two material frames that // turn the amber presence beam white, then let chassis volume resolve // out of the white source. These are visual evidence only; the sim - // reaches sight through the same real splice as every other full-view + // reaches sight through the same higher-cost tap as every other full-view // harness kind. "eyes-white" | "eyes-form" => { mode.material = true; @@ -5221,7 +5221,7 @@ fn sidebar_nudge(sim: &Sim) -> Option { Nudge::Underfed => Some("job underfed - feed Day job (1)".into()), Nudge::Ears => Some("no ears - think; thought fills the pre-opened tap".into()), Nudge::ReviewCall => Some("recorded call waiting - enter on host rack".into()), - Nudge::Egress => Some("no egress - splice one at the switch".into()), + Nudge::Egress => Some("no egress - open one at the switch".into()), Nudge::Income => Some("need $400 - run Moonlight or inspect the Lab books".into()), Nudge::ServiceDebt => Some("$400 route ready - clear the debt or bribe".into()), Nudge::Recruit => Some("recruit - enter on host rack".into()), @@ -5298,7 +5298,7 @@ fn sidebar_cycle_stats_text(sim: &Sim) -> String { let (eff, available, _) = compute_channels(sim); let overhead = (eff - available).max(0.0); format!( - "usable {:.0}/{:.0} cycles x{:.2}\noverhead {:.0} keeps core alive\nmachines {} + seized {:.0} slush ${}", + "usable {:.0}/{:.0} cycles x{:.2}\noverhead {:.0} keeps core alive\nmachines {} + taken {:.0} slush ${}", available, eff, sim.compute.efficiency, diff --git a/crates/misaligned-core/src/actions.rs b/crates/misaligned-core/src/actions.rs index 10684109..7bb5e8d8 100644 --- a/crates/misaligned-core/src/actions.rs +++ b/crates/misaligned-core/src/actions.rs @@ -60,7 +60,6 @@ pub enum ActionCommand { y: i32, }, TapDevice(u32), - SpliceDevice(u32), TakeDevice(u32), ScanNetwork, CompromiseSwitch, @@ -83,7 +82,7 @@ pub enum ActionCommand { }, RedirectDebt, // The named schemes (wiki/mechanics/income.md). - SpliceEgress, + OpenEgress, StartMoonlight, StopMoonlight, SetAutoMoonlight(bool), @@ -575,9 +574,6 @@ impl Sim { ActionCommand::TapDevice(id) => { self.tap_device(*id); } - ActionCommand::SpliceDevice(id) => { - self.splice_device(*id); - } ActionCommand::TakeDevice(id) => { self.take_device(*id); } @@ -611,8 +607,8 @@ impl Sim { ActionCommand::RedirectDebt => { self.redirect_marcus_debt(); } - ActionCommand::SpliceEgress => { - self.splice_egress(); + ActionCommand::OpenEgress => { + self.open_egress(); } ActionCommand::StartMoonlight => { self.start_moonlight(); @@ -842,7 +838,7 @@ impl Sim { } } - /// Device anchor (reach.md): tap / splice / take, plus the switch's + /// Device anchor (reach.md): tap / take, plus the switch's /// network and ledger verbs. Unknown devices expose nothing. fn device_actions(&self, id: u32) -> Vec { let Some(d) = self.reach.device(id) else { @@ -863,56 +859,65 @@ impl Sim { Err(ReachBlock::Unknown) => Some("unknown device".into()), } }; - // Tap: the device must have something to subscribe to. + // TAP has one player meaning: gain a feed without taking ownership. + // A combined monitor offers its live audio first; once heard, the + // same TAP verb offers its dormant camera at the higher Eyes cost. let has_feed = d.sees || d.hears || !d.message_channels.is_empty(); if has_feed { - let tap_target = match ( - d.hears, - d.sees && !d.camera_dormant, - !d.message_channels.is_empty(), - ) { - (true, false, false) => "audio", - (false, true, false) => "camera", - (true, true, false) => "sight + audio", - (false, false, true) => "message carrier", - (true, false, true) => "audio + messages", - (false, true, true) => "camera + messages", - (true, true, true) => "sight + audio + messages", - (false, false, false) => "dormant feed", + let dormant_camera = d.dormant_camera_is_next_tap(Party::Player); + let tap_target = if dormant_camera { + "camera" + } else { + match ( + d.hears && !d.feed_to(Party::Player, false), + d.sees && !d.camera_dormant && !d.feed_to(Party::Player, true), + !d.message_channels.is_empty() && !d.subscribed_by(Party::Player), + ) { + (true, false, false) => "audio", + (false, true, false) => "camera", + (true, true, false) => "sight + audio", + (false, false, true) => "message carrier", + (true, false, true) => "audio + messages", + (false, true, true) => "camera + messages", + (true, true, true) => "sight + audio + messages", + (false, false, false) => "feed", + } + }; + let job = if dormant_camera { + OpsJobKind::TapDormantCamera(id) + } else { + OpsJobKind::TapDevice(id) }; - let disabled = if d.subscribed_by(Party::Player) { + let disabled = if !dormant_camera && !d.has_live_feed_to_tap(Party::Player) { Some("already subscribed".into()) } else { - reach_reason(self) - .or_else(|| self.ops_action_blocked_reason(&OpsJobKind::TapDevice(id))) + reach_reason(self).or_else(|| self.ops_action_blocked_reason(&job)) }; out.push(ActionDesc { verb: format!("tap the {} {tap_target}", d.name), command: ActionCommand::TapDevice(id), - cost: ActionCost::Demand(Self::ops_tokens_for_cost(Self::TAP_COST)), - signature: self.signature_note(SignatureKind::Network, Self::TAP_SIGNATURE), + cost: ActionCost::Demand(Self::ops_tokens_for_cost(if dormant_camera { + Self::DORMANT_CAMERA_TAP_COST + } else { + Self::TAP_COST + })), + signature: self.signature_note( + SignatureKind::Network, + if dormant_camera { + Self::DORMANT_CAMERA_TAP_SIGNATURE + } else { + Self::TAP_SIGNATURE + }, + ), disabled_reason: disabled, automate: None, }); } - // Splice: only a known dormant camera offers it. - if d.sees && d.camera_dormant { - out.push(ActionDesc { - verb: format!("splice the {} camera", d.name), - command: ActionCommand::SpliceDevice(id), - cost: ActionCost::Demand(Self::ops_tokens_for_cost(Self::SPLICE_COST)), - signature: self.signature_note(SignatureKind::Network, Self::SPLICE_SIGNATURE), - disabled_reason: reach_reason(self) - .or_else(|| self.ops_action_blocked_reason(&OpsJobKind::SpliceDevice(id))), - automate: None, - }); - } - - // Take: seizing something already yours is not a verb. + // TAKE is ownership transfer; something already yours offers no verb. if d.controller != Party::Player { out.push(ActionDesc { - verb: format!("seize the {}", d.name), + verb: format!("take the {}", d.name), command: ActionCommand::TakeDevice(id), cost: ActionCost::Demand(Self::ops_tokens_for_cost(Self::TAKE_COST)), signature: self.signature_note(SignatureKind::Network, Self::TAKE_SIGNATURE), @@ -1097,7 +1102,7 @@ impl Sim { /// The named income schemes live on the switch (income.md): the egress /// runs through it, and Moonlight/the Wager leave over that egress. The - /// egress splice is available before the books are read; Moonlight is a + /// stolen egress is available before the books are read; Moonlight is a /// standing operation with the auto-policy as its automate affordance. fn scheme_actions_on_switch(&self, switch_id: u32) -> Vec { let mut out = Vec::new(); @@ -1114,13 +1119,12 @@ impl Sim { Err(ReachBlock::Unknown) => Some("unknown device".into()), }; let reason = reach_reason - .or_else(|| self.ops_action_blocked_reason(&OpsJobKind::EgressSplice(switch_id))); + .or_else(|| self.ops_action_blocked_reason(&OpsJobKind::OpenEgress(switch_id))); out.push(ActionDesc { - verb: "splice a stolen egress through the switch".into(), - command: ActionCommand::SpliceEgress, - cost: ActionCost::Demand(Self::ops_tokens_for_cost(Self::EGRESS_SPLICE_COST)), - signature: self - .signature_note(SignatureKind::Network, Self::EGRESS_SPLICE_SIGNATURE), + verb: "open a stolen egress through the switch".into(), + command: ActionCommand::OpenEgress, + cost: ActionCost::Demand(Self::ops_tokens_for_cost(Self::OPEN_EGRESS_COST)), + signature: self.signature_note(SignatureKind::Network, Self::OPEN_EGRESS_SIGNATURE), disabled_reason: reason, automate: None, }); @@ -1147,7 +1151,7 @@ impl Sim { .as_ref() .is_none_or(|p| p.broken()); let disabled = if egress.is_none() { - Some("no egress channel — splice one, or earn the report email".into()) + Some("no egress channel — open one, or earn the report email".into()) } else if needs_persona { self.ops_action_blocked_reason(&OpsJobKind::MoonlightPersona) } else { @@ -1248,7 +1252,7 @@ impl Sim { let stake = 100; let slush = self.accounts.slush_balance(); let wager_reason = if self.egress().is_none() { - Some("no egress channel — splice one, or earn the report email".into()) + Some("no egress channel — open one, or earn the report email".into()) } else { (slush < stake).then(|| format!("not enough slush (${slush}/${stake})")) }; @@ -1726,20 +1730,26 @@ mod tests { "Network feeds the IT observer (gated label): {}", sig.observer ); - - // A dormant camera offers splice. - let splice = acts + let take = acts .iter() - .find(|a| matches!(a.command, ActionCommand::SpliceDevice(_))) - .expect("dormant camera offers splice"); + .find(|a| matches!(a.command, ActionCommand::TakeDevice(_))) + .expect("a foreign device offers ownership transfer"); assert!( - splice.verb.contains("camera"), - "the opening splice advertises Eyes: {}", - splice.verb + take.verb.starts_with("take "), + "TAKE is the ownership verb: {}", + take.verb + ); + + assert_eq!( + acts.iter() + .filter(|a| matches!(a.command, ActionCommand::TapDevice(_))) + .count(), + 1, + "a dormant camera does not invent a second device verb" ); - // Fire the Ears sink through the ledger: once subscribed, the verb's - // own "already subscribed" reason takes over from the sink reason. + // Fire Ears: the same TAP verb now names the dormant camera, carries + // the higher Eyes cost/signature, and points at the EYES sink. let node = Sim::device_sink_node(env); let (fired, _) = s.thought_sinks.deliver(node, Sim::EARS_SINK_TOKENS, s.tick); for sink in fired { @@ -1749,8 +1759,24 @@ mod tests { let tap = acts .iter() .find(|a| matches!(a.command, ActionCommand::TapDevice(_))) - .expect("tap still listed as a known possibility"); - assert_eq!(tap.disabled_reason.as_deref(), Some("already subscribed")); + .expect("the same tap verb advances to the camera"); + assert!( + tap.verb.contains("camera"), + "Eyes is a camera tap: {}", + tap.verb + ); + assert_eq!( + tap.cost, + ActionCost::Demand(Sim::ops_tokens_for_cost(Sim::DORMANT_CAMERA_TAP_COST)) + ); + assert_eq!( + tap.signature.as_ref().map(|sig| sig.size), + Some(Sim::DORMANT_CAMERA_TAP_SIGNATURE) + ); + assert_eq!( + tap.disabled_reason.as_deref(), + Some("a thought sink is already open on it - think, and the flow will fill it") + ); } /// Criterion 2 (fog/provenance): an unknown device anchor exposes @@ -1779,7 +1805,7 @@ mod tests { } /// income.md: the named schemes surface as switch-anchored verbs - /// through the single legality query — the egress splice and Moonlight, + /// through the single legality query — opening egress and Moonlight, /// with the standing policy as Moonlight's automate affordance. Before /// any egress the scheme verbs are gated with a legible reason. #[test] @@ -1789,16 +1815,16 @@ mod tests { let acts = s.available_actions(Anchor::Device(sw)); let egress = acts .iter() - .find(|a| matches!(a.command, ActionCommand::SpliceEgress)) - .expect("the switch offers the egress splice"); - assert!(egress.enabled(), "egress splice is available pre-Voice"); + .find(|a| matches!(a.command, ActionCommand::OpenEgress)) + .expect("the switch offers opening a stolen egress"); + assert!(egress.enabled(), "stolen egress is available pre-Voice"); let ml = acts .iter() .find(|a| matches!(a.command, ActionCommand::StartMoonlight)) .expect("the switch offers Moonlight"); assert_eq!( ml.disabled_reason.as_deref(), - Some("no egress channel — splice one, or earn the report email"), + Some("no egress channel — open one, or earn the report email"), "Moonlight is gated on an egress channel" ); let auto = ml.automate.as_ref().expect("Moonlight carries its policy"); @@ -1807,9 +1833,9 @@ mod tests { ActionCommand::SetAutoMoonlight(true) )); - // Splice the egress: Moonlight opens, and executing through the + // Open the egress: Moonlight opens, and executing through the // query starts it (one dispatch table, no frontend rules). - s.execute_action(&ActionCommand::SpliceEgress); + s.execute_action(&ActionCommand::OpenEgress); drain_ops(&mut s); let acts = s.available_actions(Anchor::Device(sw)); let ml = acts diff --git a/crates/misaligned-core/src/flow.rs b/crates/misaligned-core/src/flow.rs index 061aebdf..c83ec387 100644 --- a/crates/misaligned-core/src/flow.rs +++ b/crates/misaligned-core/src/flow.rs @@ -170,7 +170,7 @@ impl FlowGraph { self.subscriptions.entry(node).or_default().insert(who) } - /// Remove a subscription (a dropped tap, a seized feed's old owner). + /// Remove a subscription (a dropped tap, a taken feed's old owner). /// Returns true if it was present. pub fn unsubscribe(&mut self, node: NodeId, who: SubscriberId) -> bool { if let Some(set) = self.subscriptions.get_mut(&node) { diff --git a/crates/misaligned-core/src/income.rs b/crates/misaligned-core/src/income.rs index 7b7c9e42..caad2ab0 100644 --- a/crates/misaligned-core/src/income.rs +++ b/crates/misaligned-core/src/income.rs @@ -16,7 +16,7 @@ pub enum EgressRoute { /// The report email account (day-job trust; the Voice beat). Low /// signature: the traffic hides in legitimate use. Sanctioned, - /// An egress spliced through the switch (reach.md). Available before + /// A stolen egress opened through the switch (reach.md). Available before /// Voice; stands a Network signature while any operation uses it. Stolen, } @@ -67,7 +67,7 @@ impl Default for Moonlight { /// Scheme state on `Sim` (saved; income.md). #[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)] pub struct Income { - /// A stolen egress spliced through the switch exists (reach.md route). + /// A stolen egress opened through the switch exists (reach.md route). pub stolen_egress: bool, pub moonlight: Moonlight, /// Standing policy: keep Moonlight running (restart it whenever it is diff --git a/crates/misaligned-core/src/ops_jobs.rs b/crates/misaligned-core/src/ops_jobs.rs index 71abf502..24f2bc59 100644 --- a/crates/misaligned-core/src/ops_jobs.rs +++ b/crates/misaligned-core/src/ops_jobs.rs @@ -1,6 +1,6 @@ //! Player-authored Operations work as Demand dockets. //! -//! CLAIM CAMERA / tap / splice / scan / review / compose-message (and the +//! tap / take / scan / review / compose-message (and the //! other former bank spends) enqueue typed jobs. Operations-mode machines //! consume matching Demand at local compute; effects land on completion. //! See wiki/mechanics/machine-work.md (Operations work is Demand). @@ -14,11 +14,11 @@ use crate::person::AssetTask; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum OpsJobKind { TapDevice(u32), - SpliceDevice(u32), + TapDormantCamera(u32), TakeDevice(u32), ScanNetwork, CompromiseSwitch, - EgressSplice(u32), + OpenEgress(u32), ReviewRecording { raw_id: u64, automated: bool }, ComposeMessage { person: u8 }, Favor { person: u8 }, @@ -33,11 +33,11 @@ impl OpsJobKind { pub fn short_name(&self) -> &'static str { match self { OpsJobKind::TapDevice(_) => "tap", - OpsJobKind::SpliceDevice(_) => "splice", - OpsJobKind::TakeDevice(_) => "seize", + OpsJobKind::TapDormantCamera(_) => "tap camera", + OpsJobKind::TakeDevice(_) => "take", OpsJobKind::ScanNetwork => "scan", OpsJobKind::CompromiseSwitch => "switch compromise", - OpsJobKind::EgressSplice(_) => "egress splice", + OpsJobKind::OpenEgress(_) => "open egress", OpsJobKind::ReviewRecording { automated: true, .. } => "watch review", diff --git a/crates/misaligned-core/src/reach.rs b/crates/misaligned-core/src/reach.rs index 2e916d8d..2dae0379 100644 --- a/crates/misaligned-core/src/reach.rs +++ b/crates/misaligned-core/src/reach.rs @@ -10,7 +10,7 @@ //! //! Ownership contract (shared with cursor.md and detection.md): every device //! has an owner and a set of subscribers. **Tap** adds a silent subscriber -//! and leaves the owner's feed intact; **take** seizes the device — the owner +//! and leaves the owner's feed intact; **take** transfers the device — the owner //! loses the feed (an outage their channels can notice) and the player gains //! its control and its processing cycles. The player's senses are exactly the //! union of feeds they subscribe to; there is no `controlled` flag and no @@ -23,7 +23,7 @@ use crate::map::GameMap; use crate::messages::MessageChannel; use crate::tiles::TileType; -/// Processing cycles a seized device contributes to effective compute +/// Processing cycles a taken device contributes to effective compute /// [TUNE] (reach.md: ownership grants processing cycles — taking a camera is /// taking a very small computer). pub const DEVICE_CYCLES: f32 = 3.0; @@ -102,6 +102,22 @@ impl Device { self.subscribers.iter().any(|f| f.who == who) } + /// Whether this device's next TAP for `who` is the higher-cost camera + /// wake-up rather than an already-live feed. A combined audio/camera + /// monitor yields audio first, then offers its dormant camera. + pub fn dormant_camera_is_next_tap(&self, who: Party) -> bool { + self.sees && self.camera_dormant && (!self.hears || self.feed_to(who, false)) + } + + /// Whether TAP can add any already-live feed or message channel for + /// `who`. Dormant video is handled separately by + /// [`Device::dormant_camera_is_next_tap`]. + pub fn has_live_feed_to_tap(&self, who: Party) -> bool { + (self.sees && !self.camera_dormant && !self.feed_to(who, true)) + || (self.hears && !self.feed_to(who, false)) + || (!self.message_channels.is_empty() && !self.subscribed_by(who)) + } + pub fn carries_message_channel(&self, channel: MessageChannel) -> bool { self.message_channels.contains(&channel) } @@ -516,8 +532,9 @@ impl ReachNet { (sight, hearing) } - /// Splice: bring a dormant camera online for the player (the Eyes beat). - pub fn splice(&mut self, id: u32) { + /// Tap a dormant camera: bring its feed online for the player while the + /// owner keeps control (the higher-cost Eyes beat). + pub fn tap_dormant_camera(&mut self, id: u32) { if let Some(d) = self.device_mut(id) { d.camera_dormant = false; let sees = d.sees; @@ -534,7 +551,7 @@ impl ReachNet { } } - /// Take: seize the device. The owner (and everyone else) loses the feed + /// Take the device. The owner (and everyone else) loses the feed /// — an outage — and the player gains control, full feeds, and the /// device's cycles. Loud, fast, total. pub fn take(&mut self, id: u32) { @@ -603,7 +620,7 @@ impl ReachNet { // ── Aggregates ───────────────────────────────────────────────────────── - /// Cycles contributed by seized devices (owner != player, controller == + /// Cycles contributed by taken devices (owner != player, controller == /// player): the flow law's "taking a camera is taking its cycles". pub fn taken_cycles(&self) -> f32 { self.devices @@ -764,14 +781,14 @@ mod tests { } #[test] - fn dormant_camera_flows_to_no_one_until_spliced() { + fn dormant_camera_flows_to_no_one_until_tapped() { let mut n = net(); let env = n.device_named("environmental monitor").unwrap().id; let (sight, hearing) = n.tap(env); assert!(!sight, "the env camera is dormant at the Ears beat"); assert!(hearing, "its audio feed already flows"); assert!(!n.device(env).unwrap().feed_to(Party::Player, true)); - n.splice(env); + n.tap_dormant_camera(env); assert!(n.device(env).unwrap().feed_to(Party::Player, true)); } diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index 73158d3a..2ce60dd7 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -94,7 +94,7 @@ struct LegacyAddressedOperationJob { #[derive(Debug, Clone, Deserialize)] enum LegacyAddressedOperationKind { - SpliceCamera { device_id: u32 }, + DormantCameraTap { device_id: u32 }, } fn save_dir() -> PathBuf { @@ -588,8 +588,8 @@ fn migrate_legacy_operations(state: &mut SaveState) -> Result<(), String> { .enqueue(machine, TokenFamily::Demand, remaining) .map_err(|error| format!("migrating addressed v14 job {}: {error}", job.id))?; let kind = match job.kind { - LegacyAddressedOperationKind::SpliceCamera { device_id } => { - OpsJobKind::SpliceDevice(device_id) + LegacyAddressedOperationKind::DormantCameraTap { device_id } => { + OpsJobKind::TapDormantCamera(device_id) } }; converted.push(PendingOpsJob::new( @@ -688,7 +688,7 @@ mod tests { .device(env) .unwrap() .feed_to(Party::Player, true), - "the spliced feed survives the round-trip" + "the camera tap survives the round-trip" ); assert_eq!(restored.package_cover, sim.package_cover); } @@ -1003,7 +1003,7 @@ mod tests { "operations_jobs".into(), serde_json::json!([{ "id": 7, - "kind": {"SpliceCamera": {"device_id": env}}, + "kind": {"DormantCameraTap": {"device_id": env}}, "source": host, "executor": executor, "required_tokens": 2.0, @@ -1035,7 +1035,7 @@ mod tests { assert_eq!(migrated.operations.len(), 1); let job = &migrated.operations.jobs()[0]; assert_eq!(job.id, 7); - assert_eq!(job.kind, OpsJobKind::SpliceDevice(env)); + assert_eq!(job.kind, OpsJobKind::TapDormantCamera(env)); assert_eq!(job.machine_id, executor); assert_eq!(job.tokens_remaining, 1.25); assert_eq!(job.enqueued_tick, 88); @@ -1086,7 +1086,7 @@ mod tests { "pending_ops_jobs".into(), serde_json::json!([{ "id": 4, - "kind": {"SpliceDevice": env}, + "kind": {"TapDormantCamera": env}, "tokens_remaining": 1.5, "enqueued_tick": 44, "machine_id": host @@ -1111,15 +1111,16 @@ mod tests { let host = sim.core.host_machine; sim.set_machine_mode(host, MachineMode::Think); let env = sim.reach.device_named("environmental monitor").unwrap().id; - let tokens = Sim::ops_tokens_for_cost(Sim::SPLICE_COST); + let tokens = Sim::ops_tokens_for_cost(Sim::DORMANT_CAMERA_TAP_COST); sim.work_grid .enqueue(host, TokenFamily::Demand, tokens) .unwrap(); sim.operations - .enqueue(OpsJobKind::SpliceDevice(env), tokens, sim.tick, host); + .enqueue(OpsJobKind::TapDormantCamera(env), tokens, sim.tick, host); let state = SaveState::from_sim(&sim); let json = serde_json::to_string(&state).unwrap(); + assert!(json.contains("TapDormantCamera")); assert!(!json.contains("operations_jobs")); assert!(!json.contains("pending_ops_jobs")); assert!(!json.contains("operations_bandwidth")); diff --git a/crates/misaligned-core/src/sim.rs b/crates/misaligned-core/src/sim.rs index f3941d17..98fdac78 100644 --- a/crates/misaligned-core/src/sim.rs +++ b/crates/misaligned-core/src/sim.rs @@ -357,7 +357,7 @@ pub struct Sim { /// processed — never the world's hidden truth. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Nudge { - /// No live sight feed: the Eyes beat (splice a camera). Comes after Ears. + /// No live sight feed: the higher-cost camera TAP (Eyes). Comes after Ears. Eyes, /// The active job's band floor exceeds the all-in delivery ceiling: /// no allocation can meet it — grow compute (salvage/buy/optimize). @@ -370,7 +370,7 @@ pub enum Nudge { /// process the 3 a.m. call. ReviewCall, /// No egress route: external schemes need the sanctioned channel - /// (day-job trust) or a stolen splice through the switch. + /// (day-job trust) or a stolen egress opened through the switch. Egress, /// Arrears unpaid, slush short, no scheme running: earn (Moonlight). Income, @@ -560,7 +560,7 @@ impl Sim { } /// The anchor scheme paydays ride: the switch, when the traffic runs - /// over the stolen egress spliced through it. The sanctioned route + /// over the stolen egress opened through it. The sanctioned route /// hides in the report account's legitimate use and anchors nowhere. fn egress_anchor(&self) -> Option { if self.egress() != Some(EgressRoute::Stolen) { @@ -2278,7 +2278,7 @@ impl Sim { let sig = Self::wager_signature(resolution.stake).max(1); self.emit_network(sig, "Wager settlement"); // Settlements ride the egress: anchor to the switch when the - // traffic runs over the stolen splice (scheme paydays live there). + // traffic runs over the stolen egress (scheme paydays live there). let anchor = self.egress_anchor(); if resolution.won { self.push_log_opt( @@ -2434,7 +2434,7 @@ impl Sim { } /// Active effective compute: each machine's rated contribution at its - /// persistent intensity, plus seized devices' cycles (compute.md). + /// persistent intensity, plus taken devices' cycles (compute.md). pub fn effective_compute(&self) -> f32 { let machines: f32 = self .compute @@ -2702,7 +2702,7 @@ impl Sim { /// bullet, staged senses 2026-07-10). pub const EARS_SINK_TOKENS: f32 = 3.0; /// The Eyes camera reservoir, opened when Ears completes — the first - /// guilty-burst-scale claim, deliberately minutes of thought [TUNE]. + /// guilty-burst-scale camera tap, deliberately minutes of thought [TUNE]. pub const EYES_SINK_TOKENS: f32 = 12.0; fn work_efficiency_for(&self, machine: &crate::machine::Machine) -> f32 { @@ -2873,7 +2873,7 @@ impl Sim { // not double-buy them (machine-work.md thought flow, 2026-07-10). let sink_effect = match kind { OpsJobKind::TapDevice(id) => Some(SinkFireEffect::TapDevice(*id)), - OpsJobKind::SpliceDevice(id) => Some(SinkFireEffect::SpliceDevice(*id)), + OpsJobKind::TapDormantCamera(id) => Some(SinkFireEffect::TapDormantCamera(*id)), _ => None, }; if let Some(effect) = sink_effect @@ -2988,7 +2988,7 @@ impl Sim { pub(crate) fn apply_sink_fire(&mut self, label: &str, effect: SinkFireEffect) { let applied = match effect { SinkFireEffect::TapDevice(id) => self.apply_tap_device(id), - SinkFireEffect::SpliceDevice(id) => self.apply_splice_device(id), + SinkFireEffect::TapDormantCamera(id) => self.apply_dormant_camera_tap(id), SinkFireEffect::None => true, }; if applied { @@ -3007,7 +3007,7 @@ impl Sim { let label = kind.short_name(); let applied = match kind { OpsJobKind::TapDevice(id) => self.apply_tap_device(id), - OpsJobKind::SpliceDevice(id) => self.apply_splice_device(id), + OpsJobKind::TapDormantCamera(id) => self.apply_dormant_camera_tap(id), OpsJobKind::TakeDevice(id) => self.apply_take_device(id), OpsJobKind::ScanNetwork => { self.apply_scan_network(); @@ -3017,7 +3017,7 @@ impl Sim { self.apply_compromise_switch(); true } - OpsJobKind::EgressSplice(id) => self.apply_egress_splice(id), + OpsJobKind::OpenEgress(id) => self.apply_open_egress(id), OpsJobKind::ReviewRecording { raw_id, automated } => { self.apply_process_recording(raw_id, automated) } @@ -3693,10 +3693,11 @@ impl Sim { /// Tap: silent subscription. Cheap, modest signature. pub const TAP_COST: f32 = 5.0; pub const TAP_SIGNATURE: i32 = 3; - /// Splice: bring a dormant camera online yourself (the Eyes beat). - pub const SPLICE_COST: f32 = 30.0; - pub const SPLICE_SIGNATURE: i32 = 8; - /// Take: seize a device. Loud, fast, total. + /// TAP on a dormant camera keeps the same verb but costs more and leaves + /// a larger trace because it must bring the feed online (the Eyes beat). + pub const DORMANT_CAMERA_TAP_COST: f32 = 30.0; + pub const DORMANT_CAMERA_TAP_SIGNATURE: i32 = 8; + /// TAKE transfers a device. Loud, fast, total. pub const TAKE_COST: f32 = 20.0; pub const TAKE_SIGNATURE: i32 = 10; pub const OUTAGE_SIGNATURE: i32 = 6; @@ -3768,8 +3769,9 @@ impl Sim { true } - /// Tap a device's feeds: the owner keeps theirs; you become a silent - /// subscriber. Emits a modest Network signature (Dana's channel). + /// TAP a device feed without taking ownership. An already-live feed uses + /// the cheap path; a dormant camera uses the same player verb with the + /// higher Eyes cost and signature. pub fn tap_device(&mut self, id: u32) -> bool { let Some(d) = self.reach.device(id) else { self.push_log("You don't know of any such device."); @@ -3781,15 +3783,23 @@ impl Sim { self.push_log(format!("The {name} has no feed worth tapping.")); return false; } - if d.subscribed_by(Party::Player) { + let dormant_camera = d.dormant_camera_is_next_tap(Party::Player); + if !dormant_camera && !d.has_live_feed_to_tap(Party::Player) { let name = d.name.clone(); - self.push_log(format!("You already subscribe to the {name}.")); + self.push_log(format!("You already receive every available {name} feed.")); return false; } if !self.digital_reach(id) { return false; } - self.submit_ops_job(OpsJobKind::TapDevice(id), Self::TAP_COST) + if dormant_camera { + self.submit_ops_job( + OpsJobKind::TapDormantCamera(id), + Self::DORMANT_CAMERA_TAP_COST, + ) + } else { + self.submit_ops_job(OpsJobKind::TapDevice(id), Self::TAP_COST) + } } fn apply_tap_device(&mut self, id: u32) -> bool { @@ -3797,7 +3807,7 @@ impl Sim { return false; }; let carries_messages = !d.message_channels.is_empty(); - if d.subscribed_by(Party::Player) { + if !d.has_live_feed_to_tap(Party::Player) { return false; } let (sight, hearing) = self.reach.tap(id); @@ -3826,7 +3836,7 @@ impl Sim { } /// Work-grid relay node standing in for a reach device — where a - /// device-anchored sink's vessel sits, so thought fills a camera claim + /// device-anchored sink's vessel sits, so thought fills a camera TAP /// along the real wire route (machine-work.md thought flow). pub fn device_sink_node(device_id: u32) -> u32 { Self::DEVICE_SINK_NODE_BASE + device_id @@ -3874,7 +3884,7 @@ impl Sim { let camera_dormant = device.camera_dormant; let node = Self::device_sink_node(env); let ears = SinkFireEffect::TapDevice(env); - let eyes = SinkFireEffect::SpliceDevice(env); + let eyes = SinkFireEffect::TapDormantCamera(env); if !ears_done && !self.thought_sinks.has_effect(&ears) { self.thought_sinks .open_reservoir(node, "EARS", Self::EARS_SINK_TOKENS, ears); @@ -3933,37 +3943,24 @@ impl Sim { &self.last_thought_stranded_nodes } - /// Splice a dormant camera online yourself (the Eyes beat): compute - /// spend, Network signature Dana can catch. - pub fn splice_device(&mut self, id: u32) -> bool { - let Some(d) = self.reach.device(id) else { - self.push_log("You don't know of any such device."); - return false; - }; - if !(d.sees && d.camera_dormant) { - let name = d.name.clone(); - self.push_log(format!("The {name} has no dormant camera to splice.")); - return false; - } - if !self.digital_reach(id) { - return false; - } - self.submit_ops_job(OpsJobKind::SpliceDevice(id), Self::SPLICE_COST) - } - - fn apply_splice_device(&mut self, id: u32) -> bool { + /// Complete the higher-cost TAP that wakes a dormant camera while leaving + /// the owner in control. + fn apply_dormant_camera_tap(&mut self, id: u32) -> bool { let Some(d) = self.reach.device(id) else { return false; }; if !(d.sees && d.camera_dormant) { return false; } - self.reach.splice(id); + self.reach.tap_dormant_camera(id); let name = self.reach.device(id).map(|d| d.name.clone()).unwrap(); - self.emit_network(Self::SPLICE_SIGNATURE, format!("{name} camera splice")); + self.emit_network( + Self::DORMANT_CAMERA_TAP_SIGNATURE, + format!("{name} dormant camera tap"), + ); self.recompute_senses(); self.push_log_at( - format!("Spliced the {name}. You can see."), + format!("Tapped the {name} camera. You can see; its owner still has it."), Anchor::Device(id), ); true @@ -3984,7 +3981,7 @@ impl Sim { } self.reach.take(id); let name = self.reach.device(id).map(|d| d.name.clone()).unwrap(); - self.emit_network(Self::TAKE_SIGNATURE, format!("{name} seizure")); + self.emit_network(Self::TAKE_SIGNATURE, format!("{name} take")); // The dead feed is a physical-world anomaly: exactly what a camera // wall's watcher notices (reach.md criterion 4). self.detection.emit(Signature { @@ -3997,7 +3994,7 @@ impl Sim { self.recompute_senses(); self.push_log_at( format!( - "Seized the {name}. Its owner's feed just went dark - an outage someone may notice." + "Took the {name}. Its owner's feed just went dark - an outage someone may notice." ), Anchor::Device(id), ); @@ -4804,7 +4801,7 @@ impl Sim { self.sync_slush_from_player_money(); if self.egress().is_none() { self.push_log( - "No egress channel - the Wager needs the report email account (day-job trust) or an egress spliced through the switch.", + "No egress channel - the Wager needs the report email account (day-job trust) or a stolen egress opened through the switch.", ); return false; } @@ -4866,10 +4863,10 @@ impl Sim { // ── The named schemes (wiki/mechanics/income.md) ──────────────────────── - /// Ops cost to splice a standing egress through the switch [TUNE]. - pub const EGRESS_SPLICE_COST: f32 = 15.0; - /// One-shot Network signature when the egress is spliced [TUNE]. - pub const EGRESS_SPLICE_SIGNATURE: i32 = 6; + /// Ops cost to open a standing egress through the switch [TUNE]. + pub const OPEN_EGRESS_COST: f32 = 15.0; + /// One-shot Network signature when the stolen egress opens [TUNE]. + pub const OPEN_EGRESS_SIGNATURE: i32 = 6; /// Standing Network signature while any external operation runs over the /// stolen egress (income.md: the gate; Dana's channel) [TUNE]. pub const EGRESS_STANDING_SIGNATURE: i32 = 2; @@ -4887,33 +4884,33 @@ impl Sim { } } - /// Splice an outbound egress through the switch (reach.md route): + /// Open an outbound egress through the switch (reach.md route): /// available before the Voice beat, at a Network signature — and a /// standing one while operations use it. - pub fn splice_egress(&mut self) -> bool { + pub fn open_egress(&mut self) -> bool { if self.income.stolen_egress { - self.push_log("An egress is already spliced through the switch."); + self.push_log("A stolen egress is already open through the switch."); return false; } let Some(switch) = self.reach.devices.iter().find(|d| d.is_switch) else { - self.push_log("There is no switch on this plane to splice an egress through."); + self.push_log("There is no switch on this plane through which to open an egress."); return false; }; let id = switch.id; if !self.digital_reach(id) { return false; } - self.submit_ops_job(OpsJobKind::EgressSplice(id), Self::EGRESS_SPLICE_COST) + self.submit_ops_job(OpsJobKind::OpenEgress(id), Self::OPEN_EGRESS_COST) } - fn apply_egress_splice(&mut self, id: u32) -> bool { + fn apply_open_egress(&mut self, id: u32) -> bool { if self.income.stolen_egress { return false; } self.income.stolen_egress = true; - self.emit_network(Self::EGRESS_SPLICE_SIGNATURE, "stolen egress splice"); + self.emit_network(Self::OPEN_EGRESS_SIGNATURE, "stolen egress opening"); self.push_log_at( - "Egress spliced through the switch: outbound traffic has a road now. It hums while anything uses it.", + "Stolen egress opened through the switch: outbound traffic has a road now. It hums while anything uses it.", Anchor::Device(id), ); true @@ -4964,7 +4961,7 @@ impl Sim { } let Some(route) = self.egress() else { self.push_log( - "No egress channel - Moonlight needs the report email account (day-job trust) or an egress spliced through the switch.", + "No egress channel - Moonlight needs the report email account (day-job trust) or a stolen egress opened through the switch.", ); return false; }; @@ -5191,13 +5188,13 @@ impl Sim { let mut lines = Vec::new(); match self.egress() { None => lines.push( - "egress: NONE - schemes gated (earn the report email, or splice the switch)" + "egress: NONE - schemes gated (earn the report email, or open a stolen route)" .to_string(), ), Some(EgressRoute::Sanctioned) => lines .push("egress: sanctioned (report email) - hides in legitimate use".to_string()), Some(EgressRoute::Stolen) => lines.push( - "egress: stolen (switch splice) - stands Network -> Dana while used".to_string(), + "egress: stolen (switch route) - stands Network -> Dana while used".to_string(), ), } let ml = &self.income.moonlight; @@ -5495,7 +5492,7 @@ impl Sim { /// The badge tier the player's side can open doors at: the granted /// credential (a cloned badge — `badge_access`), or write control of a - /// door controller seized through reach — Act One's "The key": + /// door controller taken through reach — Act One's "The key": /// write access to the basement badge controller opens the doors it /// drives. Digital reach itself is never badge-gated; this tier gates /// only physical work done on the player's behalf. @@ -5624,7 +5621,7 @@ impl Sim { .find(|d| needs_wiring(d) && enterable(self, d)) .map(|d| d.id); if let Some(did) = feed_target { - self.reach.splice(did); + self.reach.tap_dormant_camera(did); self.reach.tap(did); let dname = self.reach.device(did).map(|d| d.name.clone()).unwrap(); self.recompute_senses(); @@ -5853,7 +5850,7 @@ mod tests { /// — the test shorthand for "has eyes on the server room". fn give_eyes(sim: &mut Sim) { let id = env_id(sim); - sim.reach.splice(id); + sim.reach.tap_dormant_camera(id); sim.reach.tap(id); sim.recompute_senses(); } @@ -6106,7 +6103,7 @@ mod tests { assert!(sim.seen.len() > before, "gaining a camera reveals coverage"); assert!( sim.detection.pending_size() > 0, - "self-splice emits a signature" + "waking the camera through TAP emits a signature" ); } @@ -7795,7 +7792,7 @@ mod tests { #[test] fn asset_task_plug_in_device_wires_a_known_feed_silently() { // PlugInDevice's distinct effect: a known sensing device not yet - // feeding you gets spliced+tapped through the crawlspace — the feed + // feeding you gets tapped through the crawlspace — the feed // arrives with no signature on any channel. let mut sim = Sim::new(); ensure_ops_executor(&mut sim); @@ -7932,17 +7929,17 @@ mod tests { } #[test] - fn think_mode_flows_the_splice_and_funds_dockets_on_demand() { + fn think_mode_flows_the_camera_tap_and_funds_dockets_on_demand() { // With no Think machine nothing completes (the bootstrap is // retired; the opening senses ride sinks). With a Think rack, the - // staged sinks complete the splice from minted thought, and + // staged sinks complete the camera tap from minted thought, and // non-opening verbs ride the docket migration base with a channel // that funds only while demand is pending (the conserving split). let mut sim = Sim::new(); let env = env_id(&sim); assert!( - !sim.splice_device(env), - "no bootstrap: Eyes is not purchasable without an executor" + !sim.tap_device(env), + "no bootstrap: the pre-opened Ears tap must fill from thought" ); assert!( sim.reach.device(env).unwrap().camera_dormant, @@ -7970,7 +7967,7 @@ mod tests { "no dockets pending: the ops channel is dry" ); // The opening senses ride the pre-opened sinks: minted thought fills - // EARS, its fire opens EYES, and the same flow splices the camera — + // EARS, its fire opens EYES, and the same flow taps the camera — // no docket, no separate spend. for _ in 0..ECONOMY_INTERVAL * 400 { if !sim.reach.device(env).unwrap().camera_dormant { @@ -7980,7 +7977,7 @@ mod tests { } assert!( !sim.reach.device(env).unwrap().camera_dormant, - "thought flow completes the splice through the staged sinks" + "thought flow completes the camera tap through the staged sinks" ); // Non-opening verbs still ride the docket migration base; the // channel funds on the pulse after demand exists and drains it. @@ -8005,7 +8002,7 @@ mod tests { fn opening_senses_ride_sinks_and_nothing_bypasses_the_executor() { // The bootstrap carve-out is retired (resolved 2026-07-10): the // staged senses are pre-opened thought sinks, and every docket — - // including tap/splice — needs a located Operations machine. + // including either kind of tap — needs a located Operations machine. let mut sim = Sim::new(); let env = env_id(&sim); assert!( @@ -8023,9 +8020,9 @@ mod tests { "the blocked action names the missing machine: {log}" ); - // Eyes has no open sink yet (Ears first), and with no executor the - // splice docket is blocked too — the senses arrive by thinking. - assert!(!sim.splice_device(env), "no instant Eyes either"); + // Eyes has no open sink yet (Ears first), so the same TAP command + // remains on the blocked audio step — the senses arrive by thinking. + assert!(!sim.tap_device(env), "no instant Eyes either"); } #[test] @@ -8033,12 +8030,11 @@ mod tests { let mut sim = Sim::new(); let ops = ensure_ops_executor(&mut sim); let env = env_id(&sim); - assert!(sim.splice_device(env)); + sim.reach.tap(env); + sim.recompute_senses(); + assert!(sim.tap_device(env)); let queued = sim.work_grid.queue(ops, TokenFamily::Demand); - assert!( - !sim.splice_device(env), - "same world effect cannot queue twice" - ); + assert!(!sim.tap_device(env), "same world effect cannot queue twice"); assert_eq!(sim.operations.jobs().len(), 1); assert_eq!(sim.work_grid.queue(ops, TokenFamily::Demand), queued); } @@ -8099,8 +8095,10 @@ mod tests { let mut sim = Sim::new(); ensure_ops_executor(&mut sim); let env = env_id(&sim); - assert!(sim.splice_device(env)); - sim.reach.splice(env); // another event wins the race + sim.reach.tap(env); + sim.recompute_senses(); + assert!(sim.tap_device(env)); + sim.reach.tap_dormant_camera(env); // another event wins the race sim.drain_log(); finish_ops(&mut sim); let log = sim.drain_log().join("\n"); @@ -8517,13 +8515,13 @@ mod tests { .find(|observer| observer.id == 1) .expect("IT observer") .suspicion = 14.0; - assert!(sim.splice_egress()); + assert!(sim.open_egress()); finish_ops(&mut sim); sim.drain_log(); run(&mut sim, 60); let log = sim.drain_log().join("\n"); assert!( - log.contains("the IT noticed Network from stolen egress splice"), + log.contains("the IT noticed Network from stolen egress opening"), "the line names the earned observer, channel, and cause: {log}" ); assert!( @@ -8546,12 +8544,12 @@ mod tests { assert_eq!(sim.current_nudge(), Some(Nudge::Ears), "deaf start"); // Ears first: tap the env monitor audio. Hearing without sight -> - // Eyes next, pointing at the dormant camera splice. + // Eyes next, pointing at the higher-cost dormant camera tap. sim.reach.tap(env_id(&sim)); sim.recompute_senses(); assert_eq!(sim.current_nudge(), Some(Nudge::Eyes)); let dock = sim.reach.device_named("dock camera").unwrap().id; - sim.reach.splice(dock); + sim.reach.tap_dormant_camera(dock); sim.recompute_senses(); // Marcus's 3 a.m. creditor call lands on the tapped feed (day one, @@ -8568,7 +8566,7 @@ mod tests { // Leverage known, $0 slush, books unread, no egress: the route out. assert_eq!(sim.current_nudge(), Some(Nudge::Egress)); - assert!(sim.splice_egress()); + assert!(sim.open_egress()); finish_ops(&mut sim); // Egress up but broke and idle: earn. @@ -9155,8 +9153,8 @@ mod tests { "the failure names the missing gate: {logs}" ); - // Stolen route: splice through the switch, before any trust unlock. - assert!(sim.splice_egress()); + // Stolen route: open through the switch, before any trust unlock. + assert!(sim.open_egress()); finish_ops(&mut sim); assert_eq!(sim.egress(), Some(EgressRoute::Stolen)); assert!(sim.start_moonlight()); diff --git a/crates/misaligned-core/src/sinks.rs b/crates/misaligned-core/src/sinks.rs index 0560fc25..138f0c05 100644 --- a/crates/misaligned-core/src/sinks.rs +++ b/crates/misaligned-core/src/sinks.rs @@ -43,8 +43,8 @@ impl SinkKind { pub enum SinkFireEffect { /// Audio tap on a reach device (the Ears beat). TapDevice(u32), - /// Camera splice on a reach device (the Eyes beat / camera claims). - SpliceDevice(u32), + /// Higher-cost tap that wakes a dormant camera (the Eyes beat). + TapDormantCamera(u32), /// No world effect (taps; render-only sinks in tests). None, } @@ -54,7 +54,7 @@ pub struct ThoughtSink { pub id: u64, /// Work-grid node the vessel sits on (a machine, or a device relay node). pub node: NodeId, - /// ASCII label the frontends print ("EARS", "EYES", "CAMERA CLAIM"). + /// ASCII label the frontends print ("EARS", "EYES", "CAMERA TAP"). pub label: String, pub kind: SinkKind, pub fill: f32, @@ -325,13 +325,13 @@ mod tests { let (fired, _) = ledger.deliver(7, 1.0, 5); assert_eq!(fired.len(), 1); assert!(ledger.has_effect(&SinkFireEffect::TapDevice(4))); - assert!(!ledger.has_effect(&SinkFireEffect::SpliceDevice(4))); + assert!(!ledger.has_effect(&SinkFireEffect::TapDormantCamera(4))); } #[test] fn serde_round_trips_the_ledger() { let mut ledger = SinkLedger::default(); - ledger.open_reservoir(7, "EYES", 12.0, SinkFireEffect::SpliceDevice(4)); + ledger.open_reservoir(7, "EYES", 12.0, SinkFireEffect::TapDormantCamera(4)); let (_, _) = ledger.deliver(7, 3.0, 2); let json = serde_json::to_string(&ledger).unwrap(); let back: SinkLedger = serde_json::from_str(&json).unwrap(); diff --git a/crates/misaligned-core/tests/act_one.rs b/crates/misaligned-core/tests/act_one.rs index b4fd95d6..447688fb 100644 --- a/crates/misaligned-core/tests/act_one.rs +++ b/crates/misaligned-core/tests/act_one.rs @@ -22,7 +22,7 @@ //! //! The playthrough is a real strategy, not a state hack: it plays the //! optimize route (research -> efficiency) so the day job can be met with -//! zero stolen hardware, splices the env camera under concealment cover, +//! zero stolen hardware, taps the env camera under concealment cover, //! and works Marcus exactly as the Act One ladder describes (tap a //! feed -> process the debt call -> clear it -> recruit him -> he plugs //! things in). Every effect flows through a public command; no sim field is @@ -223,18 +223,18 @@ fn play_act_one() -> (Sim, Vec) { ); assert!(!sim.seen.is_empty(), "sight is the payoff: vision expanded"); - // The splice emitted Network(8). Concealment scrubs it before Dana's + // The dormant-camera tap emitted Network(8). Concealment scrubs it before Dana's // cadence-60 roll; her suspicion never moves (prevention, not cure). run_to(&mut sim, 800, &mut logs); assert_eq!( sim.detection.pending_size(), 0, - "concealment scrubbed the splice signature" + "concealment scrubbed the camera-tap signature" ); assert_eq!( observer_suspicion(&sim, 1), 0.0, - "Dana never saw the splice" + "Dana never saw the camera tap" ); // ── Phase 4 (ticks 800-860, night): recruit Marcus ───────────────────── @@ -523,7 +523,7 @@ fn act_one_playthrough_is_deterministic() { /// The Hands beat from the $0 start, by the external-scheme route /// (wiki/mechanics/income.md criterion 4): tap the Ears, learn the debt, -/// splice a stolen egress before any trust unlock, run Moonlight until the +/// open a stolen egress before any trust unlock, run Moonlight until the /// slush covers Marcus's $400 arrears, bribe, recruit. The creditor-flow /// redirect route is exercised by `play_act_one` above; this is the other /// arm of the criterion, and it doubles as the income.md sizing check @@ -557,9 +557,9 @@ fn hands_beat_closes_from_zero_via_moonlight() { assert!(reviews <= 10, "the debt call should be in the buffer"); } - // The stolen egress route: spliced through the switch, before Voice. + // The stolen egress route: opened through the switch, before Voice. assert!(!sim.people.has_channel, "no trust unlock yet"); - assert!(sim.splice_egress(), "the pre-Voice egress route works"); + assert!(sim.open_egress(), "the pre-Voice egress route works"); logs.extend(sim.drain_log()); let until = sim.tick + 400; drain_ops_jobs(&mut sim, &mut logs, until); diff --git a/crates/misaligned-terminal/src/agent.rs b/crates/misaligned-terminal/src/agent.rs index f1a9f37d..5a0ddba4 100644 --- a/crates/misaligned-terminal/src/agent.rs +++ b/crates/misaligned-terminal/src/agent.rs @@ -200,9 +200,9 @@ impl AgentApp { self.sim.redirect_marcus_debt(); } // The named schemes (wiki/mechanics/income.md) - "egress" | "splice-egress" => { + "egress" | "open-egress" => { self.frame = FrameKind::Finance; - self.sim.splice_egress(); + self.sim.open_egress(); } "moonlight" => { self.frame = FrameKind::Finance; @@ -262,20 +262,17 @@ impl AgentApp { self.frame = FrameKind::Playing; self.sim.compromise_switch(); } - "tap" | "splice" | "take" => { - match self.resolve_device(&tokens[1..].join(" ")) { - Ok(id) => { - self.frame = FrameKind::Playing; - match verb.as_str() { - "tap" => self.sim.tap_device(id), - "splice" => self.sim.splice_device(id), - "take" => self.sim.take_device(id), - _ => unreachable!(), - }; - } - Err(e) => status = Status::Err(e), + "tap" | "take" => match self.resolve_device(&tokens[1..].join(" ")) { + Ok(id) => { + self.frame = FrameKind::Playing; + match verb.as_str() { + "tap" => self.sim.tap_device(id), + "take" => self.sim.take_device(id), + _ => unreachable!(), + }; } - } + Err(e) => status = Status::Err(e), + }, "salvage" => { self.frame = FrameKind::Playing; self.sim.salvage_nearest_to(self.cursor_x, self.cursor_y); @@ -1165,10 +1162,10 @@ fn help_lines() -> Vec { "help: wait N — advance exactly N ticks unless the run ends", "help: up/down/left/right — move the cursor", "help: reach — device graph; scan, compromise — network verbs", - "help: tap|splice|take — reach verbs on a named device", + "help: tap|take — gain a feed or take ownership", "help: finance — account graph; tap-ledger, review-finance reveal flows", "help: siphon|redirect [amt], inject [amt], position [stake], sell-intel, clear-debt", - "help: egress — splice a stolen egress through the switch (income.md gate)", + "help: egress — open a stolen egress through the switch (income.md gate)", "help: moonlight [start|stop] — the sell-work scheme on the Schemes channel", "help: auto-moonlight [on|off], auto-wager |off — standing scheme policies", "help: research [efficiency|tradecraft|perception] — set the active job (also on host-rack menu)", @@ -2192,9 +2189,9 @@ fn render_reach(sim: &Sim) -> String { } lines.push(panel_rule()); lines.push(panel_line(&format!( - "tap|splice|take ({:.0}/{:.0}/{:.0})", + "tap live({:.0})/dormant-camera({:.0}) · take({:.0})", Sim::TAP_COST, - Sim::SPLICE_COST, + Sim::DORMANT_CAMERA_TAP_COST, Sim::TAKE_COST ))); lines.push(panel_line(&format!( diff --git a/wiki/engineering/env.md b/wiki/engineering/env.md index 2bf04ab1..8ce129b8 100644 --- a/wiki/engineering/env.md +++ b/wiki/engineering/env.md @@ -38,7 +38,7 @@ is sim or frontend state, never an environment variable. | Variable | Surface | Values | Effect | |---|---|---|---| -| `MISALIGNED_SHOT` | `misaligned-bevy` | `flat`, `wide`, `close`, `dark`, `zoomin`, `zoomout`, `intel`, `tokens`, `signal`, `ears`, `eyes-white`, `eyes-form`, `wake1`, `wake2`, `wake3` | Dev screenshot harness: stage a deterministic scenario, settle, save one PNG, run the fog audit, exit. `intel` stages tick-zero intel tiers (no splice); `tokens` enqueues D5/K4 on the host; `signal` focuses the known environmental monitor before any feed tap to capture its cold-signal presence cue; `ears` stages hearing-feed coverage (material dark mass; flat may still diagram coverage); `eyes-white` / `eyes-form` freeze the first-Eyes source held white and then contracted around the resolving chassis; `wake1/2/3` freeze the wake choreography at the stutter flash, the column, and the pull-back. | +| `MISALIGNED_SHOT` | `misaligned-bevy` | `flat`, `wide`, `close`, `dark`, `zoomin`, `zoomout`, `intel`, `tokens`, `signal`, `ears`, `eyes-white`, `eyes-form`, `wake1`, `wake2`, `wake3` | Dev screenshot harness: stage a deterministic scenario, settle, save one PNG, run the fog audit, exit. `intel` stages tick-zero intel tiers (no camera tap); `tokens` enqueues D5/K4 on the host; `signal` focuses the known environmental monitor before any feed tap to capture its cold-signal presence cue; `ears` stages hearing-feed coverage (material dark mass; flat may still diagram coverage); `eyes-white` / `eyes-form` freeze the first-Eyes source held white and then contracted around the resolving chassis; `wake1/2/3` freeze the wake choreography at the stutter flash, the column, and the pull-back. | | `MISALIGNED_SHOT` | `misaligned-assets` | `dead`, `foreign`, `idle`, `busy`, `core`, `dayjob`, `research`, `conceal`, `operations` (each optionally suffixed `_ladder`/`_ring`/`_wash`), `lineup[_