From 996ebfc83e21a74b5424dd05cd56a55c676fb76c Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Wed, 19 Aug 2026 10:14:40 -0400 Subject: [PATCH] fix(daily-challenge): let the card name the AI knobs it is given The preset table and the knob names were keyed in camel case while a scenario writes them lowercase, so presetNamed could never match and only bravery got a readable name. Fold the case on both sides, drop the bot type from the list, and stop a long value squeezing the label out of its own box. --- web/src/content/megamek-text.ts | 14 +++++++++++- web/src/content/overrides.ts | 38 +++++++++++++++++++++++++++------ web/src/styles.css | 7 ++++++ 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/web/src/content/megamek-text.ts b/web/src/content/megamek-text.ts index fc38e8c..d2cbcfd 100644 --- a/web/src/content/megamek-text.ts +++ b/web/src/content/megamek-text.ts @@ -195,8 +195,20 @@ export const PRESETS: readonly Preset[] = [ export function presetNamed(knobs: Record): string | null { const entries = Object.entries(knobs); if (entries.length === 0) return null; + // Case-folded on both sides. These knobs are written in the scenario file + // and arrive spelled the way MegaMek's own parser reads them - all + // lowercase, `hyperaggression` - while the presets here carry MegaMek's + // camel-cased field names. Comparing the two directly matched nothing but + // `bravery`, which is the one knob spelled the same either way, so no + // scenario could ever be named as a preset. for (const preset of PRESETS) { - if (entries.every(([k, v]) => String(preset.knobs[k]) === v)) { + const folded = new Map( + Object.entries(preset.knobs).map(([k, v]) => [ + k.toLowerCase(), + String(v), + ]), + ); + if (entries.every(([k, v]) => folded.get(k.toLowerCase()) === v)) { return preset.name; } } diff --git a/web/src/content/overrides.ts b/web/src/content/overrides.ts index 5f98457..d87d4e0 100644 --- a/web/src/content/overrides.ts +++ b/web/src/content/overrides.ts @@ -78,12 +78,24 @@ const RULE_NAMES: Record = { * it presses, how much it values its own skin, how tightly it moves as a * group. The numbers are MegaMek's 0-10 indices. */ +/** + * A knob's name for a reader, looked up case-folded. + * + * Keyed lowercase because that is how a scenario writes them and how + * `mms index` carries them through. The retreat knobs are here too: a bot + * that will not withdraw fights a different fight from one that will, and + * that is the player's business before they commit an attempt. + */ const AI_NAMES: Record = { - hyperAggression: "Aggression", - selfPreservation: "Self-preservation", - herdMentality: "Herding", + hyperaggression: "Aggression", + selfpreservation: "Self-preservation", + herdmentality: "Herding", bravery: "Bravery", - fallShame: "Fall caution", + fallshame: "Fall caution", + forcedwithdraw: "Forced withdrawal", + withdrawto: "Withdraws to", + flee: "Flees the field", + fleeto: "Flees to", }; /** The shape `mms index` writes into daily.json. */ @@ -130,12 +142,24 @@ export function allOverrides(raw: RawOverrides | undefined): Override[] { // against MegaMek's shipped behaviours, and if they are one of them the // card names it - "Striker" says what "aggression 9, self-preservation 2" // means, to a reader who has never opened the bot config dialog. - const ai = raw.ai ?? []; + // `type` says which bot, not how it plays. It is the one entry in the block + // that is not a behaviour, and naming princess to a player who has no other + // bot to compare it against says nothing. + const ai = (raw.ai ?? []).filter(({ knob }) => knob.toLowerCase() !== "type"); if (ai.length > 0) { - const knobs = Object.fromEntries(ai.map((a) => [a.knob, a.value])); + const knobs = Object.fromEntries( + ai.map((a) => [a.knob.toLowerCase(), a.value]), + ); const named = presetNamed(knobs); + // "Forced withdrawal off", not "Forced withdrawal false". Every other + // knob is a 0-10 dial and reads as one; the two flags are switches. const spelled = ai - .map(({ knob, value }) => `${AI_NAMES[knob] ?? knob} ${value}`) + .map(({ knob, value }) => { + const name = AI_NAMES[knob.toLowerCase()] ?? knob; + const shown = + value === "true" ? "on" : value === "false" ? "off" : value; + return `${name} ${shown}`; + }) .join(" ยท "); out.push({ kind: "ai", diff --git a/web/src/styles.css b/web/src/styles.css index 1846e60..bd993a3 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -5444,8 +5444,15 @@ footer .debug { border-radius: 50%; } +/* The name keeps its own width. `min-width: 0` alone let a flex row squeeze + it to a few characters when the value beside it was long - a spelled-out + behaviour runs to a line and a half - and the label then overflowed its box + and printed on top of the value. */ .daily-over-name { min-width: 0; + flex: 0 0 auto; + max-width: 45%; + overflow-wrap: break-word; } /* MegaMek marks some rules unofficial. A scored fight leaning on one should -- 2.51.2