Objective fixes

This commit is contained in:
Kalle
2026-08-06 13:04:21 +03:00
parent d9770a3d78
commit 2d4743d098
12 changed files with 110 additions and 59 deletions

View File

@@ -806,10 +806,7 @@ app/styles/vars.css (the emberz copies of those tokens were dropped).
flex-shrink: 0;
}
/* frosted-glass plate: keeps the score readable over vivid banner art
without dimming the image itself */
.scanner-app .match-score {
font-size: var(--font-lg);
font-weight: var(--weight-extra);
font-variant-numeric: tabular-nums;
line-height: 1;

View File

@@ -1,17 +1,18 @@
/**
* ObjectiveDetector: parses the ranked in-match counter overlay top-center —
* each team's count plate, the penalty pill under it, which team is in
* control (the controlling team's plate swaps to a near-black fill with the
* digits in the team's ink color; see rois.ts for the layout), and the M:SS
* match timer above the plates.
* control (the controlling team's plate keeps its saturated team-color
* fill; a non-controlling team's — or both, while the objective is neutral
* — swaps to a near-black fill with the digits in the team's ink color;
* see rois.ts for the layout), and the M:SS match timer above the plates.
*
* Digits are read as the trailing digit run (banner.ts) of the band under
* several channel extractions: team-color ink on the black plate needs the
* brightest channel (dark blue ink is near-black in luminance), white ink
* on a team-color fill needs the darkest channel (which drops the fill and
* keeps white) — every extraction is tried at each threshold/size and the
* best-scoring read wins. A gate hit that yields no readable count on
* either side emits nothing (lookalike frame).
* brightest channel (dark blue ink is near-black in luminance), white or
* near-black ink on a team-color fill needs the darkest channel (which
* drops the fill and keeps the contrast) — every extraction is tried at
* each threshold/size and the best-scoring read wins. A gate hit that
* yields no readable count on either side emits nothing (lookalike frame).
*
* The data is a discriminated union on `mode`, prepared for the counter
* semantics of the other modes; only the SZ member exists so far and every
@@ -41,9 +42,7 @@ import {
import type { ScoreboardResources } from "../scoreboard/index";
import type { DetectedEvent, Detector, GateResult } from "../types";
import {
CONTROL_PLATE_MAX_MEAN,
CONTROL_PLATE_MAX_SATURATION,
GATE_PLATE_MAX_MEAN,
CONTROL_PLATE_MIN_SATURATION,
GATE_PLATE_MAX_STD,
GATE_SCORE_MIN_MAX_BRIGHTNESS,
GATE_TIMER_MAX_MEAN,
@@ -80,7 +79,7 @@ export interface SplatZonesObjectiveData {
score: [number | null, number | null];
/** penalty pill value per team; null = no pill (or unreadable) */
penalty: [number | null, number | null];
/** which team currently holds the zone (team-ink digits on black plate) */
/** which team currently holds the zone (team-color plate fill) */
control: [boolean, boolean];
}
@@ -156,8 +155,7 @@ export function createObjectiveDetector(
}
function plateProbeOk(gray: Mat, roi: Roi): boolean {
const { mean, std } = meanStd(gray, roi);
return mean <= GATE_PLATE_MAX_MEAN && std <= GATE_PLATE_MAX_STD;
return meanStd(gray, roi).std <= GATE_PLATE_MAX_STD;
}
function scoreInkOk(frame: Mat, roi: Roi): boolean {
@@ -277,8 +275,8 @@ export function createObjectiveDetector(
/**
* Control: the plate's fill (sampled over the probe strip) is the
* neutral near-black style — dark AND unsaturated — instead of the
* non-controlling team-color fill (see CONTROL_PLATE_MAX_* in rois.ts).
* saturated team-color style instead of the near-black fill shown while
* not in control (see CONTROL_PLATE_MIN_SATURATION in rois.ts).
*/
function plateFill(
frame: Mat,
@@ -315,8 +313,7 @@ export function createObjectiveDetector(
penalty,
control:
score.value !== null &&
fill.mean <= CONTROL_PLATE_MAX_MEAN &&
fill.saturation <= CONTROL_PLATE_MAX_SATURATION,
fill.saturation >= CONTROL_PLATE_MIN_SATURATION,
fill,
};
}) as [SideRead, SideRead];

View File

@@ -1,25 +1,23 @@
/**
* ALL objective-counter ROI coordinates, in canonical 1920x1080 space.
* Calibrated against the objective/ fixtures (pixel maps over the top-center
* band; the layout is mirror-symmetric around x=960, so right-side ROIs are
* derived with mirrorRoi and stay locked to the left-side measurements).
* band). The layout is only roughly mirror-symmetric around x=960: the
* widest attested digit run ("100") measures x816..888 on the left plate
* but x1039..1112 on the right — a few px right of mirror-center — so the
* score and plate-probe ROIs are measured per side instead of mirrored.
*
* The ranked in-match HUD shows one counter plate per team flanking the
* mode's objective badge, under the timer strip:
* - each plate is a rounded box with a small localized label line
* ("Remaining", のこり, ...) over big BlitzBold count digits (~42px);
* - the team currently in control has its plate restyled: near-black fill
* with the digits in the team's ink color; the other plate keeps a
* team-color fill with white digits. The fill/ink swap — not the digit
* values — is what identifies control;
* ("Remaining", のこり, ...) over big BlitzBold count digits (~41px);
* - the team currently in control has its plate in its team-color fill
* (digits near-black on bright inks, white on dark inks); a team not in
* control — including both teams while the objective is neutral — has a
* near-black plate with the digits in its team ink color. The fill swap —
* not the digit values — is what identifies control;
* - a penalty adds a translucent dark pill under the plate with a white
* "+N" (~26px digits; the '+' is shorter than any digit and is dropped by
* "+N" (~29px digits; the '+' is shorter than any digit and is dropped by
* the trailing-digit height floor).
*
* Only one fixture is attested so far (SZ, two-digit counts on both sides).
* Assumed until more fixtures land: the plates keep their geometry with
* three-digit counts ("100"), and the penalty pill sits mirror-symmetric
* under the right plate.
*/
import { CANONICAL_WIDTH, type Roi } from "../../canonical";
@@ -28,11 +26,16 @@ function mirrorRoi(roi: Roi): Roi {
return { ...roi, x: CANONICAL_WIDTH - roi.x - roi.w };
}
/** Count digit band, inside the plate below the label line. */
export const SCORE_ROIS: readonly [Roi, Roi] = (() => {
const left: Roi = { x: 812, y: 156, w: 84, h: 50 };
return [left, mirrorRoi(left)];
})();
/**
* Count digit band, inside the plate below the label line. Wide enough for
* the full three-digit "100" run on each side (measured x816..888 left,
* x1039..1112 right) while staying clear of the plate rims, whose edge glow
* against a bright scene can read as ink.
*/
export const SCORE_ROIS: readonly [Roi, Roi] = [
{ x: 810, y: 156, w: 90, h: 46 },
{ x: 1034, y: 156, w: 84, h: 46 },
];
/**
* Penalty "+N" band inside the pill. The pill measures x 826..920,
@@ -44,17 +47,20 @@ export const PENALTY_ROIS: readonly [Roi, Roi] = (() => {
})();
/**
* Plate-fill probe strips at the plates' outer edges (x 796..903 is the
* left plate; the strip sits inside its left rim, clear of the widest
* attested digit run). Both plate styles are a flat fill there — near-black
* when in control, saturated team color when not — while gameplay showing
* through a lookalike frame is busy: the gate accepts a strip on mean
* brightness plus flatness (std).
* Plate-fill probe strips inside each plate's LEFT rim (x 796..903 is the
* left plate, x 1017..1124 the right), clear of the widest attested digit
* run ("100", see SCORE_ROIS) — the right strip is not mirrored because the
* right plate's digit run reaches x1112, into where a mirrored strip would
* sit. Height stops at y192 to stay above the bottom-corner edge glow.
* Both plate styles are a flat fill there — saturated team color when in
* control, near-black when not — while gameplay showing through a
* lookalike frame is busy: the gate accepts a strip on flatness (std)
* alone, since fill brightness spans near-black to bright yellow.
*/
export const PLATE_PROBE_ROIS: readonly [Roi, Roi] = (() => {
const left: Roi = { x: 800, y: 162, w: 10, h: 36 };
return [left, mirrorRoi(left)];
})();
export const PLATE_PROBE_ROIS: readonly [Roi, Roi] = [
{ x: 800, y: 162, w: 10, h: 30 },
{ x: 1021, y: 162, w: 10, h: 30 },
];
/**
* Penalty-pill presence probes at the pill's rounded ends, clear of the
@@ -69,7 +75,6 @@ export const PENALTY_PROBE_ROIS: readonly [[Roi, Roi], [Roi, Roi]] = (() => {
return [leftPill, [mirrorRoi(leftPill[1]), mirrorRoi(leftPill[0])]];
})();
export const GATE_PLATE_MAX_MEAN = 165;
export const GATE_PLATE_MAX_STD = 30;
/** A count digit's brightest channel clears this on either plate style. */
export const GATE_SCORE_MIN_MAX_BRIGHTNESS = 200;
@@ -110,7 +115,7 @@ export const TIMER_DIGIT_MIN_HEIGHT_RATIO = 0.82;
* banner's settled/mid-pop sizes).
*/
export const SCORE_TEXT_HEIGHTS = [40, 44] as const;
export const PENALTY_TEXT_HEIGHT = 26;
export const PENALTY_TEXT_HEIGHT = 29;
/**
* Binarization thresholds tried per channel extraction. The ink/background
@@ -126,11 +131,11 @@ export const PENALTY_PROBE_MAX_MEAN = 165;
export const PENALTY_PROBE_MAX_STD = 30;
/**
* Control = the plate's fill is the near-black style (measured over the
* plate probe strip): a controlling team's plate is neutral dark — low
* brightness AND low saturation — while the team-color fill of a
* non-controlling plate stays saturated even for dark inks (deep blue
* measures ~130 spread at ~50 brightness).
* Control = the plate's fill is the team-color style (measured over the
* plate probe strip): a controlling team's plate keeps its saturated ink
* fill even for dark inks (deep blue measures ~130 spread at ~50
* brightness), while the near-black plate of a non-controlling team — or
* of both teams while the objective is neutral — is unsaturated (attested
* fills measure >=112 vs <=19).
*/
export const CONTROL_PLATE_MAX_MEAN = 90;
export const CONTROL_PLATE_MAX_SATURATION = 60;
export const CONTROL_PLATE_MIN_SATURATION = 60;

View File

@@ -0,0 +1,13 @@
{
"event": "Objective",
"data": {
"mode": "SZ",
"time": 277,
"score": [97, 100],
"penalty": [null, null],
"control": [true, false]
},
"options": {
"notes": "Alpha (bright yellow-green ink) in control: their plate keeps the bright team-color fill (POV frame shows the We're in control! banner). Bravo still at the three-digit 100."
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1002 KiB

View File

@@ -0,0 +1,13 @@
{
"event": "Objective",
"data": {
"mode": "SZ",
"time": 157,
"score": [2, 15],
"penalty": [64, 65],
"control": [true, false]
},
"options": {
"notes": "Penalty pills under both plates at once; alpha (bright green fill) in control."
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 KiB

View File

@@ -0,0 +1,13 @@
{
"event": "Objective",
"data": {
"mode": "SZ",
"time": 209,
"score": [2, 84],
"penalty": [75, null],
"control": [false, true]
},
"options": {
"notes": "Single-digit count with a large penalty on alpha; bravo (purple) in control with the team-color fill."
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -0,0 +1,13 @@
{
"event": "Objective",
"data": {
"mode": "SZ",
"time": 299,
"score": [100, 100],
"penalty": [null, null],
"control": [false, false]
},
"options": {
"notes": "Match start: zone neutral, both plates near-black with three-digit 100s (green/pink digits)."
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -5,6 +5,6 @@
"time": 215,
"score": [95, 53],
"penalty": [4, null],
"control": [true, false]
"control": [false, true]
}
}