diff --git a/crates/misaligned-bevy/src/main.rs b/crates/misaligned-bevy/src/main.rs index f40166b1..114c1b27 100644 --- a/crates/misaligned-bevy/src/main.rs +++ b/crates/misaligned-bevy/src/main.rs @@ -4799,8 +4799,8 @@ fn setup_ui(mut commands: Commands) { )); // Frequent machine modes are a quiet selection-level control, not world - // text following pointer or reticule focus. The same root is reused for a - // focused device explanation only when no machine selection owns it. + // text following pointer or reticule focus. Focused device actions keep a + // separate world-attached root so selecting a machine cannot suppress them. commands .spawn(( Node { @@ -4819,7 +4819,9 @@ fn setup_ui(mut commands: Commands) { BorderColor::all(Color::NONE), Visibility::Hidden, GlobalZIndex(40), - HoverVerbBar, + HoverVerbBar { + family: HoverVerbFamily::Machine, + }, )) .with_children(|bar| { bar.spawn(( @@ -4861,6 +4863,31 @@ fn setup_ui(mut commands: Commands) { )); } }); + }); + + commands + .spawn(( + Node { + position_type: PositionType::Absolute, + left: Val::Px(0.0), + top: Val::Px(0.0), + width: Val::Px(DEVICE_VERB_BAR_WIDTH), + height: Val::Px(DEVICE_RECEIPT_HEIGHT), + flex_direction: FlexDirection::Column, + align_items: AlignItems::FlexStart, + justify_content: JustifyContent::FlexStart, + row_gap: Val::Px(5.0), + ..default() + }, + BackgroundColor(Color::NONE), + BorderColor::all(Color::NONE), + Visibility::Hidden, + GlobalZIndex(40), + HoverVerbBar { + family: HoverVerbFamily::Device, + }, + )) + .with_children(|bar| { bar.spawn(( Node { width: Val::Percent(100.0), diff --git a/crates/misaligned-bevy/src/shot_harness.rs b/crates/misaligned-bevy/src/shot_harness.rs index f6f0a6a1..19541cd2 100644 --- a/crates/misaligned-bevy/src/shot_harness.rs +++ b/crates/misaligned-bevy/src/shot_harness.rs @@ -1503,11 +1503,14 @@ pub(super) fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &s // The pre-commit receipt on the reticle (digital-read.md criterion 2): // DIGITAL, reticule centered on the reachable-but-unowned // environmental monitor so its leading TAP verb renders cost + - // expected signature + observer + band. No menu open, so the hover - // verb bar (and its receipt row) is visible. + // expected signature + observer + band. The host remains explicitly + // selected: the stable mode strip and focused device receipt must both + // survive, pinning the run-time transition that once suppressed every + // later device hover. "read-receipt" => { mode.material = false; mode.zoom = 1.0; + game.selected_machines.insert(game.sim.core.host_machine); // Untap the monitor so its leading verb is TAP, whose receipt // carries the Network signature (the exact string a player weighs // before committing) rather than a free UNTAP. diff --git a/crates/misaligned-bevy/src/world_annotations.rs b/crates/misaligned-bevy/src/world_annotations.rs index f929271f..df2854f0 100644 --- a/crates/misaligned-bevy/src/world_annotations.rs +++ b/crates/misaligned-bevy/src/world_annotations.rs @@ -44,9 +44,12 @@ pub(super) struct TokenMarker { } /// Root for the menu-closed frequent action surface: machine selection owns a /// stable `WORK / THINK / LIE` UI strip; device focus may open an explanatory -/// field beside the exact world body. -#[derive(Component)] -pub(super) struct HoverVerbBar; +/// field beside the exact world body. The two families have independent roots +/// so selecting a machine cannot permanently suppress device actions. +#[derive(Component, Clone, Copy)] +pub(super) struct HoverVerbBar { + pub(super) family: HoverVerbFamily, +} /// Small world-bound label for the exact target of the current attention /// projection. The attached verb surface remains white-current / gray-rest; /// this separate label carries guidance without recoloring a menu. @@ -517,7 +520,7 @@ pub(super) fn annotation_exclusion_tiles(game: &Game) -> HashSet<(i32, i32)> { } } // Device receipt field: opens up-left of its anchor with clearance. - if let Some(target) = hover_verb_bar_target(game, None) + if let Some(target) = device_hover_verb_target(game, None) && target.device.is_some() { tiles.extend(quadrant_tiles( @@ -1320,10 +1323,9 @@ fn device_hover_verbs(sim: &Sim, id: u32) -> Vec<&'static str> { .collect() } -fn hover_verb_bar_target(game: &Game, pointer: Option<(i32, i32)>) -> Option { - // Selection is the only state that exposes machine modes. It wins over a - // resting pointer and never absorbs a co-located device receipt into the - // same visual object. +fn selected_machine_verb_target(game: &Game) -> Option { + // Selection is the only state that exposes machine modes. It owns the + // stable bottom strip without competing for the focused device surface. if let Some(&id) = game.selected_machines.iter().next() && let Some(machine) = game.sim.compute.machines.iter().find(|m| m.id == id) { @@ -1334,7 +1336,10 @@ fn hover_verb_bar_target(game: &Game, pointer: Option<(i32, i32)>) -> Option) -> Option { let device_at = |x, y| { let device = game .sim @@ -1513,8 +1518,9 @@ pub(super) fn render_operator_cue( *visibility = Visibility::Visible; } -/// Keep machine modes in stable selection UI. Device explanations retain their -/// exact world attachment when no machine selection owns the surface. +/// Keep machine modes in stable selection UI while device explanations retain +/// their exact world attachment independently. Selecting a machine must not +/// consume the device-action surface for the rest of the run. #[allow(clippy::too_many_arguments)] #[allow(clippy::type_complexity)] pub(super) fn render_hover_verb_bar( @@ -1526,6 +1532,7 @@ pub(super) fn render_hover_verb_bar( camera3_q: RealCameraQuery, mut grammar: Query< ( + &HoverVerbBar, &mut Node, &mut BackgroundColor, &mut BorderColor, @@ -1573,11 +1580,10 @@ pub(super) fn render_hover_verb_bar( ), >, ) { - let Ok((mut node, mut background, mut border, mut visibility)) = grammar.single_mut() else { - return; - }; let Ok(window) = windows.single() else { - *visibility = Visibility::Hidden; + for (_, _, _, _, mut visibility) in &mut grammar { + *visibility = Visibility::Hidden; + } return; }; @@ -1591,66 +1597,79 @@ pub(super) fn render_hover_verb_bar( } else { mouse_grid(&windows, &camera_q) }; - let Some(target) = hover_verb_bar_target(&game, pointer) else { - *visibility = Visibility::Hidden; - return; - }; - if game.screen != Screen::Playing || game.menu.is_some() || game.ops.is_some() { - *visibility = Visibility::Hidden; - return; - } - - let bar_width = target.width(); + let machine_target = selected_machine_verb_target(&game); + let device_target = device_hover_verb_target(&game, pointer); + let surfaces_visible = + game.screen == Screen::Playing && game.menu.is_none() && game.ops.is_none(); let window_size = Vec2::new(window.width(), window.height()); - let position = if target.machine.is_some() { - machine_selection_bar_position(window_size, bar_width, target.height()) - } else { - let Some(device_base) = - hover_verb_bar_anchor(mode.material, target.x, target.y, &camera_q, &camera3_q) - else { + + for (bar, mut node, mut background, mut border, mut visibility) in &mut grammar { + let target = match bar.family { + HoverVerbFamily::Machine => machine_target, + HoverVerbFamily::Device => device_target, + }; + let Some(target) = target.filter(|_| surfaces_visible) else { *visibility = Visibility::Hidden; - return; + continue; }; - hover_verb_bar_position( - device_base, - window_size, - bar_width, - target.height(), - target.clearance(), - ) - }; - node.width = Val::Px(bar_width); - node.height = Val::Px(target.height()); - node.left = Val::Px(position.x); - node.top = Val::Px(position.y); - if target.device.is_some() { - node.align_items = AlignItems::FlexStart; - node.justify_content = JustifyContent::FlexStart; - node.padding = UiRect::axes(Val::Px(16.0), Val::Px(14.0)); - node.border = UiRect::left(Val::Px(2.0)); - node.row_gap = Val::Px(5.0); - background.0 = Color::srgba(0.008, 0.010, 0.015, 0.92); - *border = BorderColor::all(Color::srgba(0.33, 0.35, 0.36, 0.62)); - } else { - node.align_items = AlignItems::Center; - node.justify_content = JustifyContent::Center; - node.padding = UiRect::axes(Val::Px(12.0), Val::Px(6.0)); - node.border = UiRect::top(Val::Px(1.0)); - node.row_gap = Val::Px(5.0); - background.0 = Color::srgba(0.006, 0.007, 0.011, 0.90); - *border = BorderColor::all(Color::srgba(0.20, 0.20, 0.19, 0.52)); + + let bar_width = target.width(); + let position = match bar.family { + HoverVerbFamily::Machine => { + machine_selection_bar_position(window_size, bar_width, target.height()) + } + HoverVerbFamily::Device => { + let Some(device_base) = + hover_verb_bar_anchor(mode.material, target.x, target.y, &camera_q, &camera3_q) + else { + *visibility = Visibility::Hidden; + continue; + }; + hover_verb_bar_position( + device_base, + window_size, + bar_width, + target.height(), + target.clearance(), + ) + } + }; + node.width = Val::Px(bar_width); + node.height = Val::Px(target.height()); + node.left = Val::Px(position.x); + node.top = Val::Px(position.y); + match bar.family { + HoverVerbFamily::Device => { + node.align_items = AlignItems::FlexStart; + node.justify_content = JustifyContent::FlexStart; + node.padding = UiRect::axes(Val::Px(16.0), Val::Px(14.0)); + node.border = UiRect::left(Val::Px(2.0)); + node.row_gap = Val::Px(5.0); + background.0 = Color::srgba(0.008, 0.010, 0.015, 0.92); + *border = BorderColor::all(Color::srgba(0.33, 0.35, 0.36, 0.62)); + } + HoverVerbFamily::Machine => { + node.align_items = AlignItems::Center; + node.justify_content = JustifyContent::Center; + node.padding = UiRect::axes(Val::Px(12.0), Val::Px(6.0)); + node.border = UiRect::top(Val::Px(1.0)); + node.row_gap = Val::Px(5.0); + background.0 = Color::srgba(0.006, 0.007, 0.011, 0.90); + *border = BorderColor::all(Color::srgba(0.20, 0.20, 0.19, 0.52)); + } + } + *visibility = Visibility::Visible; } - *visibility = Visibility::Visible; - let current_mode = target.machine.and_then(|_| selected_machine_mode(&game)); - let device_verbs = target - .device + let current_mode = machine_target.and_then(|_| selected_machine_mode(&game)); + let device_verbs = device_target + .and_then(|target| target.device) .map(|id| device_hover_verbs(&game.sim, id)) .unwrap_or_default(); // The renderer-neutral ActionDesc writes the plain-language explanation; // the GUI gives it more room than compact terminal/agent rows. - let receipt_read = target - .device + let receipt_read = device_target + .and_then(|target| target.device) .and_then(|id| { game.sim .available_actions(Anchor::Device(id)) @@ -1683,7 +1702,7 @@ pub(super) fn render_hover_verb_bar( device_slots.push(verb); } if let Ok((mut receipt_node, mut receipt_visibility)) = receipt_group.single_mut() { - let shown = target.device.is_some(); + let shown = surfaces_visible && device_target.is_some(); receipt_node.display = if shown { Display::Flex } else { Display::None }; // Inherited, never Visible: an explicit Visible child overrides the // hidden bar root, so a hidden bar would still paint its rows at the @@ -1695,10 +1714,11 @@ pub(super) fn render_hover_verb_bar( }; } for (group, mut group_node, mut group_visibility) in groups.iter_mut() { - let shown = match group.family { - HoverVerbFamily::Machine => target.machine.is_some(), - HoverVerbFamily::Device => target.device.is_some(), - }; + let shown = surfaces_visible + && match group.family { + HoverVerbFamily::Machine => machine_target.is_some(), + HoverVerbFamily::Device => device_target.is_some(), + }; group_node.display = if shown { Display::Flex } else { Display::None }; *group_visibility = if shown { Visibility::Inherited @@ -1720,11 +1740,21 @@ pub(super) fn render_hover_verb_bar( ("LIE", current_mode == Some(MachineMode::Lie), false), ]; let (label, active, control_hint) = slots[word.index]; - (label, active, target.machine.is_some(), control_hint) + ( + label, + active, + surfaces_visible && machine_target.is_some(), + control_hint, + ) } HoverVerbFamily::Device => { let label = device_slots.get(word.index).copied().unwrap_or(""); - (label, false, !label.is_empty(), false) + ( + label, + false, + surfaces_visible && device_target.is_some() && !label.is_empty(), + false, + ) } }; text.0 = label.into(); @@ -1915,11 +1945,12 @@ mod hover_identity_tests { mod hover_verb_bar_tests { use super::{ BONE, COMMAND_BAND_HEIGHT, DEVICE_VERB_BAR_WIDTH, DIM, Game, HOVER_VERB_BAR_GAP, - HoverVerbTarget, MACHINE_VERB_BAR_WIDTH, MachineVerbTone, TILE_SIZE, device_hover_verbs, - digital_focus_label_layout, hover_verb_bar_position, hover_verb_bar_target, - machine_selection_bar_position, machine_verb_color, machine_verb_tone, operator_cue_label, - operator_cue_position, operator_pulse, scaled, selected_machine_covers_tile, - selected_machine_mode, selection_already_carries_attention_target, + HoverVerbTarget, MACHINE_VERB_BAR_WIDTH, MachineVerbTone, TILE_SIZE, + device_hover_verb_target, device_hover_verbs, digital_focus_label_layout, + hover_verb_bar_position, machine_selection_bar_position, machine_verb_color, + machine_verb_tone, operator_cue_label, operator_cue_position, operator_pulse, scaled, + selected_machine_covers_tile, selected_machine_mode, selected_machine_verb_target, + selection_already_carries_attention_target, }; use bevy::prelude::Vec2; use misaligned::sim::Nudge; @@ -2042,17 +2073,17 @@ mod hover_verb_bar_tests { let mut game = Game::new(); let core = game.sim.core_position(); - assert_eq!(hover_verb_bar_target(&game, Some(core)), None); + assert_eq!(selected_machine_verb_target(&game), None); game.selected_machines.insert(game.sim.core.host_machine); assert_eq!( - hover_verb_bar_target(&game, None).map(|target| (target.x, target.y)), + selected_machine_verb_target(&game).map(|target| (target.x, target.y)), Some(core), ); - let selected = hover_verb_bar_target(&game, Some((99, 99))) - .expect("selection keeps one stable mode control"); + let selected = + selected_machine_verb_target(&game).expect("selection keeps one stable mode control"); assert_eq!((selected.x, selected.y), core); - let target = hover_verb_bar_target(&game, None).expect("selection has stable verbs"); + let target = selected_machine_verb_target(&game).expect("selection has stable verbs"); assert!(target.machine.is_some(), "WORK / THINK / LIE survive"); assert!( target.device.is_none(), @@ -2083,6 +2114,45 @@ mod hover_verb_bar_tests { ); } + #[test] + fn selecting_a_machine_does_not_suppress_focused_device_actions() { + let mut game = Game::new(); + let (device, x, y) = game + .sim + .reach + .device_named("environmental monitor") + .map(|device| (device.id, device.x, device.y)) + .expect("opening monitor exists"); + game.sim.reach.untap(device); + + assert_eq!( + device_hover_verb_target(&game, Some((x, y))).and_then(|target| target.device), + Some(device), + "the reachable monitor begins with its TAP action" + ); + + let host = game.sim.core.host_machine; + let (host_x, host_y) = game.sim.core_position(); + game.set_cursor(host_x, host_y); + game.delegate_hotkey(misaligned::work_grid::MachineMode::Think, None); + game.set_cursor(x, y); + assert_eq!( + selected_machine_verb_target(&game).and_then(|target| target.machine), + Some(host), + "direct mode input establishes one stable selected-machine control" + ); + assert_eq!( + device_hover_verb_target(&game, None).and_then(|target| target.device), + Some(device), + "the machine selection must not make device actions surrender" + ); + assert_eq!( + device_hover_verb_target(&game, Some((x, y))).and_then(|target| target.device), + Some(device), + "pointer focus retains the same independent device surface" + ); + } + #[test] fn mixed_selection_asserts_no_false_current_mode() { let mut game = Game::new(); diff --git a/wiki/interface/bevy.md b/wiki/interface/bevy.md index 504a72f3..d725f6da 100644 --- a/wiki/interface/bevy.md +++ b/wiki/interface/bevy.md @@ -262,6 +262,9 @@ the next earned anchor rather than the next tile. stable bottom-center slot inside the world canvas. Pointer, reticule, camera, selected-machine position, and DIGITAL/REAL view cannot move it. Merely hovering or resting the reticule on an unselected rack shows no mode line. + A known device under pointer or reticule focus retains its separate attached + verb/receipt field at the same time; establishing machine selection never + suppresses device actions. This selection control is strictly two-tone: the current mode is bone-white; alternatives, number keys, and separators are gray in their ordinary dim hierarchy. Neither a panic exit nor the current @@ -310,6 +313,9 @@ drawer. Every final capture is visually inspected after its last mutation. `MISALIGNED_SHOT=hover-menu` retains its compatibility name but captures the clean machine grammar over the reticule-focused owned host with WORK current; it is the resting visual acceptance frame for context-menu.md H1–H4. +`MISALIGNED_SHOT=read-receipt` keeps that host selected while focusing the +environmental monitor, proving the stable mode strip and device action receipt +coexist for context-menu.md H6. `MISALIGNED_SHOT=first-think` stages the real opening Thought route with Ears part-filled beneath the blind boundary. Its issue-#15 target frame contains only `WORK / THINK / LIE` before the harness acts; no self-state block or input diff --git a/wiki/interface/context-menu.md b/wiki/interface/context-menu.md index e4042a32..2f7c2360 100644 --- a/wiki/interface/context-menu.md +++ b/wiki/interface/context-menu.md @@ -292,9 +292,11 @@ tap prerequisite, and a player-controlled device with no remaining stable device verb contributes no device line. A tile can carry several actionable bodies, but their controls do not accrete -into one world annotation. An explicit machine selection shows only -`WORK / THINK / LIE` in stable UI. With no machine selection, a focused known -device may show its attached explanation. Infrequent one-off verbs—PROCESS, SALVAGE, social acts, +into one world annotation. An explicit machine selection shows +`WORK / THINK / LIE` in stable UI. A focused known device independently shows +its attached explanation, even while that machine control remains selected; +selecting a machine must never make every later device action disappear. +Infrequent one-off verbs—PROCESS, SALVAGE, social acts, construction, and similar actions—remain selectable rows in the full context menu below this frequent grammar; they do not accrete into the hover line. The separate **pointer identity chip** (2026-07-21, cursor.md criterion 5) @@ -333,10 +335,9 @@ device verbs and recreates the crowding this split removes. is a large heading; cost uses the full resource name; WHAT THIS RISKS leads the named observer, attention channel/change, and current band on separate left-aligned lines. It is not another action row. Machine modes remain the - separate selection control; terminal and agent rows remain compact. A - machine selection wins the shared screen space, so the device explanation - waits until that selection is cleared or the device's full context menu is - opened. + separate selection control; terminal and agent rows remain compact. The two + roots coexist and hide independently, so a mode commit may establish or + retain machine selection without suppressing the focused device field. - `1` / `2` / `3` (including numpad) remain the immediate commit path for WORK / THINK / LIE on the resolved computer. The control creates no Bevy-only action; it reflects the explicit selection established or retained @@ -434,8 +435,9 @@ H6. A Bevy device explanation receives at least 320 logical pixels of sentence distinct line and hierarchy at both supported window sizes. The field ends above and left of the target anchor; the focused device label is right-anchored left of its glyph so graph lines do not strike through it. - The selection-level machine control and compact non-GUI projections remain - unchanged. + It remains visible after direct mode input establishes a machine selection; + the stable selection-level machine control remains simultaneously visible + and compact non-GUI projections remain unchanged. ## Addendum (2026-07-08): events carry you to the thing diff --git a/wiki/interface/liturgical-ui-constitution.md b/wiki/interface/liturgical-ui-constitution.md index d3703775..1b2b3da0 100644 --- a/wiki/interface/liturgical-ui-constitution.md +++ b/wiki/interface/liturgical-ui-constitution.md @@ -138,15 +138,16 @@ thing beneath their cursor. off-screen, when a modal owns input, or when a frontend-local buffer-capacity override outranks the Act One ladder. It never recolors the selection-level machine control. -- **HoverVerbBar** — a stable selection-level mode control - (`WORK / THINK / LIE` with number keys) that sits at bottom-center inside - the world canvas. It appears only while a machine selection exists and - no full menu is open. Pointer, reticule, camera, selected-machine - position, and DIGITAL/REAL view cannot move it. The current mode is - bone-white; alternatives, number keys, and separators are gray. No part - of it ever turns amber. When no machine is selected, the same root may - show a focused device explanation (device verbs + receipt) if no machine - selection owns it. +- **HoverVerbBar family** — two independent roots inside one aperture. A + stable selection-level mode control (`WORK / THINK / LIE` with number keys) + sits at bottom-center while a machine selection exists. Pointer, reticule, + camera, selected-machine position, and DIGITAL/REAL view cannot move it. A + focused known device separately carries its world-attached verb and receipt + field. The two may coexist: selecting a machine never consumes or suppresses + the device-action surface. Both yield while a full menu or modal owns input. + The current machine mode is bone-white; alternatives, number keys, and + separators are gray. No part of either surface turns amber merely because it + is available. - **DigitalFocusLabel** — the exact focused identity sits below the reticle. It names the one shared earned name for the most specific thing there. Dim bone on a dark chip, never amber, never a verb surface. @@ -158,10 +159,11 @@ with hover state, and does not stack across opens. It is the ring that holds attention on the thing the player is actually operating. **Implementation:** `OperatorCue` (GlobalZIndex 35, amber border, hidden by -default). `HoverVerbBar` (GlobalZIndex 40, `HoverVerbGroup` with -`HoverVerbFamily::Machine` or device families, `Visibility::Inherited` for -child rows). `DigitalFocusLabel` (below reticle). `CursorMarker` (eight -corner strokes). All in `world_annotations.rs`; spawn roots in `setup_ui`. +default). Two `HoverVerbBar { family }` roots (GlobalZIndex 40) independently +carry `HoverVerbFamily::Machine` and `HoverVerbFamily::Device`; every child row +uses `Visibility::Inherited` so hiding either root hides its complete tree. +`DigitalFocusLabel` sits below the reticle. `CursorMarker` uses eight corner +strokes. All live in `world_annotations.rs`; `setup_ui` spawns the roots. ### 5. The Operations full-frame chamber diff --git a/wiki/log/2026-07-31-independent-device-hover.md b/wiki/log/2026-07-31-independent-device-hover.md new file mode 100644 index 00000000..41071127 --- /dev/null +++ b/wiki/log/2026-07-31-independent-device-hover.md @@ -0,0 +1,59 @@ +# Machine selection no longer silences device actions + +``` +Type: log +``` + +## Finding + +A live playtest reached day 4 with the switch still focused and identified as +reachable, but its attached device actions had disappeared. The stable +`WORK / THINK / LIE` strip remained visible. This was not an intermittent Bevy +visibility failure: the first direct machine-mode input establishes an explicit +machine selection, and the shared `HoverVerbBar` root then chose that selection +before considering any focused device. Because selection persists until cleared, +every later `TAP / UNTAP / TAKE` field silently yielded for the rest of ordinary +play. + +## Change + +- The aperture now owns two independent `HoverVerbBar { family }` roots: one + stable bottom-center machine-mode strip and one world-attached device + verb/receipt field. +- Machine selection and device focus resolve independently. They may coexist, + while the existing full-menu and modal boundaries still hide both. +- `read-receipt` now stages an explicitly selected host and a focused untapped + environmental monitor in the same production frame. +- A regression pins the exact failure transition: device actions exist, a + machine selection is established, and the same device actions remain + available through both reticule and pointer focus. + +## Design impact + +The earlier rule that machine selection won one shared hover root was the defect, +not a desirable compression. The liturgical aperture and context-menu contracts +now say that stable machine control cannot conceal known action on the focused +device. This follows the superhuman-operability prohibition on minimalism that +hides agency. + +## Defense + +Implements `context-menu.md` H6 and the stable-aperture clause in +`liturgical-ui-constitution.md`: explicit machine selection may not suppress the +independent focused-device action surface. Child rows retain inherited +visibility so hiding either root still hides its complete tree. + +## Verification + +- `cargo test -p misaligned-bevy selecting_a_machine_does_not_suppress_focused_device_actions -- --nocapture` +- `cargo test -p misaligned-bevy hover_verb_bar_tests -- --nocapture` — all 13 + hover/action composition tests passed. +- `./tools/check.sh --frontend` — 24 shared-asset, eight asset-binary, two + auxiliary, and 176 Bevy tests passed with Clippy, corpus, wiki, environment, + workflow, and project-operation fixtures. +- The exact final `MISALIGNED_SHOT=read-receipt` captures were opened and read + at 1280x720 logical (2560x1440 pixels) and the supported 960x540 logical + minimum (1920x1080 pixels). Both show the full TAP cost/risk field attached + to `CAM / REACHABLE` and the complete bottom-center + `1 WORK / 2 THINK / 3 LIE` strip simultaneously, with no clipping, overlap, + or off-screen placement. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 0c03a9c2..ddf9b3d6 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -71,6 +71,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-07-31-objective-name-witness.md](2026-07-31-objective-name-witness.md) +## 2026-07-31 - Machine selection no longer silences device actions + +- Intent: (see session log) +- Log: [wiki/log/2026-07-31-independent-device-hover.md](2026-07-31-independent-device-hover.md) + ## 2026-07-31 - Remove the retired claim tool from the agent doorway - Intent: (see session log) diff --git a/wiki/log/decisions/2026-07-31.md b/wiki/log/decisions/2026-07-31.md index 2af15e36..798f1a3d 100644 --- a/wiki/log/decisions/2026-07-31.md +++ b/wiki/log/decisions/2026-07-31.md @@ -59,3 +59,25 @@ Owner: [basement-map.md](../../world/places/basement-map.md) and Owner: [material-render.md](../../interface/material-render.md), [basement-map.md](../../world/places/basement-map.md), and [placeholders.md](../../art/placeholders.md). + +## Machine selection and focused-device actions coexist + +### DECIDED + +- The stable `WORK / THINK / LIE` selection control and the focused device's + attached `TAP / UNTAP / TAKE` receipt are independent roots inside the same + aperture body. +- Establishing or retaining a machine selection never suppresses known action + on the focused device. Both surfaces yield only when the full context menu or + another modal owns input. +- This does not merge controls into one annotation or give device verbs number + keys. Machine posture remains stable bottom-center UI; device consequence + remains attached to the exact world body. + +### SUPERSEDED + +- The 2026-07-19 rule that a machine selection wins one shared hover root and + makes the device explanation wait until selection is cleared. + +Owner: [liturgical-ui-constitution.md](../../interface/liturgical-ui-constitution.md) +and [context-menu.md](../../interface/context-menu.md).