Files
BT411/engine/MUNGA_L4/L4RIOBANK.cpp
T
CydandClaude Opus 4.8 8ae74cd2f1 bigger RIO buttons: the red + yellow lamps clear the glass by +10px
Player request: more of the RIO buttons should protrude from the displays so a
FLASHING lamp is easier to notice.  BTRioBankMetricsFor now adds a flat +10px
to `strip` -- the lamp bar -- AFTER the scale; BT_RIO_PROTRUDE=<px> tunes it
and 0 restores the pre-2026-08-14 geometry.

FLAT, not scaled, deliberately: the ask is on-screen noticeability, and the
surround runs the glass at HALF scale where the strip lands on the 6px floor --
a scaled bonus would give the least help exactly where the lamps are smallest.
The floor above it already trades strict proportionality for legibility on this
field; this is the same trade, sized by the players.

Scope falls out of the geometry -- only `strip` moves, so:
  - RED MFD buttons and YELLOW radar buttons (+ the radar foot row) grow;
  - they grow OUTWARD ONLY (the layout anchors the inboard edge), so
    under-glass depth, button WIDTH and every press target are unchanged --
    this is a LONGER bar, not a fatter face;
  - BLUE flight cells lay out from cellW/cellH and never see it.

Verified [T2] by BT_RIOBANK_LOG A/B + surround screenshots: Heat MFD top button
y 21->11, h 126->136 with its bottom edge FIXED at 147; bank bounds 252->272
tall (all five MFDs), radar 252->272 wide; flight-bank bounds byte-identical;
no negative coordinates on any bank; imagery still wins every pixel it covers.
The exploded glass windows size their client to bank.bounds, so they simply
grow with it.

Relation to #172: the alert RING (BT_ALERT_RING, default off) answered the same
night-16 miss after a "+8px grown-face bloom" was rejected on the bench as "the
button got fat".  This is a different shape -- the lamp keeps its width and
shows more of its own bar -- so the ring stays optional.  Desktop-glass
affordance; the pod's buttons are physical and backlit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 13:23:17 -05:00

341 lines
12 KiB
C++

#include "mungal4.h"
#pragma hdrstop
//###########################################################################
// L4RIOBANK -- the ONE button-bank geometry for a pod display. Design +
// the under-glass rule: l4riobank.h. Consumed by the cockpit surround
// (L4VB16.cpp, canvas space) and the exploded per-display windows
// (L4GLASSWIN.cpp, client space); neither owns a copy any more.
//###########################################################################
#include "l4riobank.h"
#include <string.h>
#include <stdlib.h>
namespace
{
//-------------------------------------------------------------------
// The lamp strip at NATIVE glass size, scaled down with the display
// but floored so a half-scale surround lamp is still a bar and not a
// smudge. RP412 L4MFDVIEW works at native only and hardcodes 10.
//-------------------------------------------------------------------
const int nativeShortAxis = 480; // MFD 640x480 / map 480x640
const int nativeStrip = 10;
const int minimumStrip = 6;
const int defaultGap = 4;
//-------------------------------------------------------------------
// PROTRUSION BONUS (2026-08-14, player request). Flat pixels added to
// the lamp strip AFTER the scale, so the visible bar reaches this much
// further out of the glass on EVERY renderer.
//
// WHY FLAT, not scaled: the ask is on-screen noticeability of a
// FLASHING lamp, and the surround runs the glass at half scale, where
// the strip lands on the `minimumStrip` floor (~6px) -- a scaled bonus
// would give the surround half the help exactly where the lamps are
// smallest. The floor above already establishes legibility over strict
// proportionality for this field; this is the same trade, sized by the
// players. It is a DESKTOP-GLASS affordance: the pod's buttons are
// physical and backlit and have never had this problem.
//
// Scope: only the strip -- so only the RED MFD buttons and the YELLOW
// radar buttons/foot row grow, and they grow OUTWARD ONLY (the layout
// anchors the inboard edge; under-glass depth is unchanged). The BLUE
// flight cells are laid out from cellW/cellH and never see this.
//
// Relation to #172: the alert RING (BT_ALERT_RING, default off) was the
// answer to the same night-16 complaint after a "+8px grown-face bloom"
// was rejected on the bench as "the button got fat". This is a
// different shape -- the lamp keeps its width and simply shows more of
// its own bar -- and it is unconditional, so the ring stays optional.
//-------------------------------------------------------------------
const int defaultProtrude = 10;
int ProtrudeBonus()
{
static int cached = -1;
if (cached < 0)
{
const char *e = getenv("BT_RIO_PROTRUDE"); // tune by eye; 0 = pre-2026-08-14
cached = (e != 0) ? atoi(e) : defaultProtrude;
if (cached < 0) cached = 0;
if (cached > 200) cached = 200;
}
return cached;
}
//-------------------------------------------------------------------
// Per-column horizontal nudge for the 4 MFD buttons, left->right, so
// they line up with the (unevenly spaced) DISPLAY / PROGRAM / ...
// legends painted in the imagery. Measured against the native 640
// glass in L4GLASSWIN (RedOffsetX + kRedColDX) and scaled here, so
// both renderers inherit the same alignment. Tunable.
//-------------------------------------------------------------------
const int nativeMfdW = 640;
const int mfdColumnDX[4] = { 0, 1, 5, 5 };
//-------------------------------------------------------------------
// The map paints its own legend beside each side button (the six
// cells: MAP+, MAP-, IR, crouch, searchlight, display-mode), and the
// buttons have to line up with THAT grid, not with height/6.
//
// MEASURED off a native 480x640 capture (scratchpad/measurelegend.py
// over an exploded Secondary/Radar window): the first cell border sits
// 3 rows down and the six cells are 102 tall on a 107 pitch. RP412's
// map measures 13 + 6x102 on a 105 pitch -- same cell height, different
// top and pitch, so its numbers do NOT transfer; these are ours.
//
// Our previous even division (displayH/6 + 1 = 107) had the pitch right
// by luck and sat 3px high of the labels.
//-------------------------------------------------------------------
const int mapLegendSpan = 640;
const int mapLegendTop = 3;
const int mapLegendCell = 102;
const int mapLegendPitch = 107;
int Scaled(int nativeValue, int actual, int nativeExtent)
{
if (nativeExtent <= 0) return nativeValue;
return (nativeValue * actual) / nativeExtent;
}
void Push(
BTRioBank *bank,
int address, int x, int y, int w, int h,
int colorClass, int inert, const char *label
)
{
if (bank->buttonCount >= BTRioBankMaxButtons) return;
if (w <= 0 || h <= 0) return;
BTRioButton *button = &bank->buttons[bank->buttonCount++];
button->address = address;
button->x = x;
button->y = y;
button->w = w;
button->h = h;
button->colorClass = colorClass;
button->inert = inert;
button->label = label;
}
void GrowBounds(BTRioBank *bank, int x, int y, int w, int h)
{
if (bank->boundsW == 0 && bank->boundsH == 0)
{
bank->boundsX = x; bank->boundsY = y;
bank->boundsW = w; bank->boundsH = h;
return;
}
int left = (x < bank->boundsX) ? x : bank->boundsX;
int top = (y < bank->boundsY) ? y : bank->boundsY;
int right = bank->boundsX + bank->boundsW;
int bottom = bank->boundsY + bank->boundsH;
if (x + w > right) right = x + w;
if (y + h > bottom) bottom = y + h;
bank->boundsX = left;
bank->boundsY = top;
bank->boundsW = right - left;
bank->boundsH = bottom - top;
}
}
//###########################################################################
void
BTRioBankMetricsFor(int displayW, int displayH, BTRioBankMetrics *out)
{
if (out == 0) return;
int shortAxis = (displayW < displayH) ? displayW : displayH;
int strip = Scaled(nativeStrip, shortAxis, nativeShortAxis);
if (strip < minimumStrip) strip = minimumStrip;
strip += ProtrudeBonus(); // player-requested reach (see above)
out->strip = strip;
out->gap = defaultGap;
// The cells with no glass behind them (the flight bank, the map's foot
// row) are drawn in full, so they get a plain readable size scaled off
// the same axis rather than a derived depth.
out->cellW = Scaled(58, shortAxis, nativeShortAxis);
out->cellH = Scaled(28, shortAxis, nativeShortAxis);
if (out->cellW < 12) out->cellW = 12;
if (out->cellH < 12) out->cellH = 12;
}
//###########################################################################
void
BTRioBankLayout(
BTRioBankStyle style,
int originX,
int originY,
int displayW,
int displayH,
int anchorA,
int anchorB,
const BTRioBankMetrics *metrics,
const int *bottomAddrs,
BTRioBank *out
)
{
if (out == 0 || metrics == 0) return;
memset(out, 0, sizeof(*out));
const int strip = metrics->strip;
const int gap = metrics->gap;
// the glass itself is always inside the bounds
GrowBounds(out, originX, originY, displayW, displayH);
if (style == BTRioBankMfd)
{
//---------------------------------------------------------------
// 4 above the glass, 4 below. Addresses descend from the anchor
// row-major (vRIO CockpitLayout::Mfd): top = anchorA-i, bottom =
// anchorA-4-i. Each button reaches HALF THE GLASS in and clears
// the edge by `strip`, so the two banks meet in the middle and
// every pixel of the display is some button's press target.
//---------------------------------------------------------------
int depth = displayH / 2;
if (depth < strip) depth = strip;
int slotW = displayW / 4;
for (int i = 0; i < 4; ++i)
{
int dx = Scaled(mfdColumnDX[i], displayW, nativeMfdW);
int x = originX + i * slotW + dx;
int w = slotW - gap;
int topY = originY - strip;
int botY = originY + displayH - depth;
Push(out, anchorA - i, x, topY, w, depth + strip, 0, 0, 0);
Push(out, anchorA - 4 - i, x, botY, w, depth + strip, 0, 0, 0);
GrowBounds(out, x, topY, w, depth + strip);
GrowBounds(out, x, botY, w, depth + strip);
}
}
else if (style == BTRioBankRadar)
{
//---------------------------------------------------------------
// The portrait map: 6 buttons down each side (anchorA left,
// anchorB right) plus an optional 4 along the foot. Same rule
// turned on its side -- each column reaches half the map in and
// leaves `strip` clearing the edge.
//
// The FOOT ROW IS PUSHED FIRST, deliberately: the columns now
// reach half the width each, so they cover the foot band too,
// and the hit test takes the first match. Foot-first keeps
// those four addresses reachable.
//---------------------------------------------------------------
int depth = displayW / 2;
if (depth < strip) depth = strip;
if (bottomAddrs != 0)
{
int bandH = metrics->cellH;
int bandY = originY + displayH - bandH;
int bandW = displayW / 4;
for (int i = 0; i < 4; ++i)
{
int x = originX + i * bandW;
Push(out, bottomAddrs[i], x, bandY, bandW - gap,
bandH + strip, 1, 0, 0);
GrowBounds(out, x, bandY, bandW - gap, bandH + strip);
}
}
//---------------------------------------------------------------
// Six cells down the height, on the map's MEASURED legend grid
// (see mapLegend* above). Top and bottom are scaled SEPARATELY
// and subtracted rather than scaling a height directly: rounding
// a height would let the buttons drift out of step with the
// labels a display or two down the column.
//---------------------------------------------------------------
int leftX = originX - strip;
int rightX = originX + displayW - depth;
for (int i = 0; i < 6; ++i)
{
int cellTop = mapLegendTop + i * mapLegendPitch;
int top = (cellTop * displayH) / mapLegendSpan;
int bottom = ((cellTop + mapLegendCell) * displayH) / mapLegendSpan;
int h = bottom - top;
if (h < 1) h = 1;
int y = originY + top;
Push(out, anchorA + i, leftX, y, depth + strip, h, 1, 0, 0);
Push(out, anchorB + i, rightX, y, depth + strip, h, 1, 0, 0);
GrowBounds(out, leftX, y, depth + strip, h);
GrowBounds(out, rightX, y, depth + strip, h);
}
}
}
//###########################################################################
void
BTRioBankFlightGrid(
int originX,
int originY,
int baseAddress,
int count,
int columns,
const char *const *labels,
const int *inert,
const BTRioBankMetrics *metrics,
BTRioBank *out
)
{
if (out == 0 || metrics == 0 || columns <= 0) return;
const int cw = metrics->cellW;
const int ch = metrics->cellH;
const int gap = metrics->gap;
for (int i = 0; i < count; ++i)
{
int column = i % columns;
int row = i / columns;
int x = originX + column * (cw + gap);
int y = originY + row * (ch + gap);
// Flight blocks are blue (colorClass 2), EXCEPT the Panic/Eject at 0x3D:
// red (colorClass 0) so the eject stands out on the blue panel.
int cc = (baseAddress + i == 0x3D) ? 0 : 2;
Push(out, baseAddress + i, x, y, cw, ch, cc,
(inert != 0) ? inert[i] : 0,
(labels != 0) ? labels[i] : 0);
GrowBounds(out, x, y, cw, ch);
}
}
//###########################################################################
void
BTRioBankDump(const char *tag, const BTRioBank *bank)
{
static int enabled = -1;
static int printed = 0;
if (enabled < 0)
{
const char *e = getenv("BT_RIOBANK_LOG");
enabled = (e != 0 && e[0] != '0') ? 1 : 0;
}
if (!enabled || bank == 0 || printed >= 16) return;
++printed;
DEBUG_STREAM << "[riobank] " << (tag ? tag : "?")
<< " bounds=(" << bank->boundsX << "," << bank->boundsY << ","
<< bank->boundsW << "," << bank->boundsH << ") "
<< bank->buttonCount << " buttons\n";
for (int i = 0; i < bank->buttonCount; ++i)
{
const BTRioButton &b = bank->buttons[i];
DEBUG_STREAM << "[riobank] addr=0x" << std::hex << b.address << std::dec
<< " rect=(" << b.x << "," << b.y << "," << b.w << "," << b.h << ")"
<< " class=" << b.colorClass << "\n";
}
DEBUG_STREAM << std::flush;
}