Skip to content
Merged

Next #320

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
decc78b
options
exeea Aug 21, 2026
7a39e1d
Merge branch 'criticals' of https://github.com/MegaMek/mekbay into cr…
exeea Aug 21, 2026
f0fd1e8
karma no gpu
exeea Aug 21, 2026
9f357c1
merge from main
exeea Aug 21, 2026
2f48f10
merge from main
exeea Aug 21, 2026
bb13281
.
exeea Aug 21, 2026
61310d5
merge
exeea Aug 21, 2026
961c3fe
options
exeea Aug 21, 2026
016b682
merge from main
exeea Aug 21, 2026
7cb6bf4
merge from main
exeea Aug 21, 2026
540b83d
merge from main
exeea Aug 21, 2026
ff292d8
merge main
exeea Aug 21, 2026
2dc64cc
style
exeea Aug 21, 2026
a2e061f
critical hit dialog
exeea Aug 21, 2026
00acb7a
critical chance/hit
exeea Aug 21, 2026
b908466
options
exeea Aug 21, 2026
5cfaf22
.
exeea Aug 21, 2026
47073df
.
exeea Aug 21, 2026
2faa55f
Merge branch 'next' into criticals
exeea Aug 22, 2026
627ffa8
styles
exeea Aug 22, 2026
e3b1a00
as summary print portrait
exeea Aug 23, 2026
004c697
manifest
exeea Aug 23, 2026
5f89a67
ld
exeea Aug 23, 2026
177c194
canonical
exeea Aug 23, 2026
8b8a43c
noindex
exeea Aug 23, 2026
5c40b6a
routing
exeea Aug 23, 2026
1ca8e7f
expand button
exeea Aug 23, 2026
ea38daf
index
exeea Aug 23, 2026
7f09608
style
exeea Aug 23, 2026
7cd8657
pip
exeea Aug 26, 2026
9a0e31c
.
exeea Aug 26, 2026
09fd6ba
roller + chance/hit cleanup
exeea Aug 26, 2026
3561a7c
aggregated consciousness
exeea Aug 26, 2026
b52eb6d
chance/hit
exeea Aug 26, 2026
1fc295d
automations
exeea Aug 27, 2026
bee6312
test
exeea Aug 27, 2026
4106c2b
types
exeea Aug 27, 2026
6f6bc58
quad hip effect core vs TW
exeea Aug 27, 2026
3788ade
patchwork/hybrid
exeea Aug 27, 2026
255f0c5
options
exeea Aug 27, 2026
06d7bfa
fixed lost selection in chassis view
exeea Aug 27, 2026
5d356e1
options
exeea Aug 27, 2026
c6e1005
patchwork/hybrid
exeea Aug 27, 2026
e556b4f
Merge branch 'next' into criticals
exeea Aug 27, 2026
bf7ff4d
toasts stack
exeea Aug 27, 2026
89e68f5
hex slider threshold
exeea Aug 27, 2026
54575fd
cleanup
exeea Aug 27, 2026
be9cd50
.
exeea Aug 27, 2026
4ee8622
breach check
exeea Aug 27, 2026
f4260d0
unified
exeea Aug 27, 2026
647d284
cleanup
exeea Aug 27, 2026
24acf5c
Merge pull request #315 from MegaMek/criticals
exeea Aug 27, 2026
23e5d4b
Merge branch 'main' into next
exeea Aug 27, 2026
0aec870
roll all results button
exeea Aug 28, 2026
64963e2
.
exeea Aug 28, 2026
4956f12
notification.count
exeea Aug 28, 2026
b9650fd
.
exeea Aug 28, 2026
9dcc89a
cleanup
exeea Aug 28, 2026
10e72ae
turn-movement indicator
exeea Aug 28, 2026
458218b
movement state
exeea Aug 28, 2026
8edd52b
movement
exeea Aug 28, 2026
8886ef6
colors
exeea Aug 28, 2026
342ffd5
separator
exeea Aug 28, 2026
0fd2b7d
styling
exeea Aug 28, 2026
0cb5202
show total tonnage
exeea Aug 28, 2026
e75e209
z-index
exeea Aug 28, 2026
23bbbc2
zindex
exeea Aug 28, 2026
9e8a9e0
collection manager and TOE filter
exeea Aug 28, 2026
689976c
tons in overview
exeea Aug 28, 2026
7fafc63
overview fixes
exeea Aug 28, 2026
9291a3b
fixed drag'n'drop in overview
exeea Aug 28, 2026
ecaa1e3
Sr color
exeea Aug 28, 2026
ca93cda
Sr options
exeea Aug 28, 2026
90dc753
sprint
exeea Aug 28, 2026
89458dd
sprint PSR fix and Quad with destroyed legs fix
exeea Aug 28, 2026
8256a6e
nova cews
exeea Aug 29, 2026
8ac0ce4
as.specials
exeea Aug 29, 2026
7b08ddb
nova cews + stealth
exeea Aug 29, 2026
8811334
wildcard when locs>3
exeea Aug 29, 2026
fcb6424
armor wildcard
exeea Aug 29, 2026
4b4af8f
UnitEngineType
exeea Aug 29, 2026
32a9e66
TMM visible in badge
exeea Aug 29, 2026
0dbeb49
TMM
exeea Aug 29, 2026
f91911c
font 16
exeea Aug 29, 2026
5c85d56
more equipment handlers and fixed MGA
exeea Aug 29, 2026
e8a3368
test fix
exeea Aug 29, 2026
d923711
.
exeea Aug 29, 2026
0dd9699
run2 filter
exeea Aug 30, 2026
fded5cd
rototype Medium Pulse Laser: +6 heat (1D6 roll: 6)
exeea Aug 30, 2026
80e0b63
Merge branch 'main' into next
exeea Aug 30, 2026
fefe3cf
faction exclusive filter is now era-aware
exeea Aug 30, 2026
e11f8a0
test fix
exeea Aug 30, 2026
2f71701
Merge branch 'main' into next
exeea Aug 30, 2026
4fa65d2
Merge branch 'main' into next
exeea Aug 31, 2026
dd2ae15
phase and turn buttons
exeea Aug 31, 2026
b9bde71
BV Optimizer fix for fixed piloting units
exeea Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
"prestart": "npm run gen-force-name-words",
"start": "ng serve --host 0.0.0.0",
"build": "npm run prerun && ng build --configuration=production",
"postbuild": "node scripts/generate-seo-pages.js",
"build:next": "npm run prerun && ng build --configuration=next",
"test:ratgenerator": "tsx scripts/ratgenerator_build_table.test.ts",
"postbuild:next": "npm run postbuild",
"test:force-name-words": "tsx scripts/generate-force-name-words.test.ts",
"test:unit-report-oracles": "tsx scripts/unit-report-oracles.test.ts",
"test:unit-diagnostics": "tsx scripts/unit-diagnostics.test.ts",
Expand Down
6 changes: 3 additions & 3 deletions public/manifest.webmanifest
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"lang": "en-US",
"name": "MekBay",
"name": "MekBay: BattleTech Force Builder",
"short_name": "MekBay",
"description": "A web-based application for managing forces and viewing record sheets.",
"description": "Build and manage BattleTech forces for Classic and Alpha Strike with interactive or printable record sheets.",
"theme_color": "#292929",
"background_color": "#292929",
"display": "standalone",
Expand Down Expand Up @@ -34,4 +34,4 @@
"launch_handler": {
"client_mode": "auto"
}
}
}
3 changes: 2 additions & 1 deletion public/privacy.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MekBay Privacy Policy</title>
<meta name="description" content="Privacy Policy for the hosted MekBay service.">
<meta name="robots" content="noindex, follow">
<link rel="icon" href="/favicon.ico">
<link rel="stylesheet" href="/legal.css">
</head>
Expand Down Expand Up @@ -125,4 +126,4 @@ <h2>Contact</h2>
<p class="fine-print">MekBay is a fan-made project and is not affiliated with Catalyst Game Labs, The Topps Company, Inc., or Microsoft.</p>
</main>
</body>
</html>
</html>
4 changes: 4 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
User-agent: *
Allow: /

Sitemap: https://mekbay.com/sitemap.xml
12 changes: 12 additions & 0 deletions public/sitemap.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://mekbay.com/</loc>
</url>
<url>
<loc>https://mekbay.com/forcegenerator</loc>
</url>
<url>
<loc>https://mekbay.com/?expanded=true</loc>
</url>
</urlset>
3 changes: 2 additions & 1 deletion public/terms.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MekBay Terms of Service</title>
<meta name="description" content="Terms of Service for the hosted MekBay service.">
<meta name="robots" content="noindex, follow">
<link rel="icon" href="/favicon.ico">
<link rel="stylesheet" href="/legal.css">
</head>
Expand Down Expand Up @@ -123,4 +124,4 @@ <h2>Contact</h2>
<p class="fine-print">MekBay is a fan-made project and is not affiliated with Catalyst Game Labs, The Topps Company, Inc., or Microsoft.</p>
</main>
</body>
</html>
</html>
59 changes: 59 additions & 0 deletions scripts/generate-seo-pages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (C) 2026 The MegaMek Team
// SPDX-License-Identifier: GPL-3.0-or-later
// Author: Drake

const fs = require('node:fs');
const path = require('node:path');

const outputDirectory = path.resolve(__dirname, '..', 'dist', 'browser');
const indexPath = path.join(outputDirectory, 'index.html');

function replaceRequired(html, pattern, replacement, label, pageName) {
if (!pattern.test(html)) {
throw new Error(`Could not generate ${pageName} SEO page: ${label} was not found in index.html.`);
}
return html.replace(pattern, replacement);
}

const sourceHtml = fs.readFileSync(indexPath, 'utf8');
const pages = [
{
outputFile: 'forcegenerator.html',
pageName: 'Force Generator',
schemaName: 'MekBay BattleTech Force Generator',
title: 'BattleTech Force Generator | MekBay',
description: 'Generate balanced BattleTech forces for Classic and Alpha Strike by faction, era, unit type, Battle Value, or Point Value.',
canonicalUrl: 'https://mekbay.com/forcegenerator',
},
{
outputFile: 'search.html',
pageName: 'Unit Search',
schemaName: 'MekBay BattleTech Unit Search',
title: 'BattleTech Unit Search | MekBay',
description: 'Search and compare BattleTech units for Classic and Alpha Strike by name, faction, era, role, Battle Value, Point Value, and more.',
canonicalUrl: 'https://mekbay.com/?expanded=true',
},
];

function generateSeoPage(page) {
let html = sourceHtml;
html = replaceRequired(html, /<title>[^<]*<\/title>/, `<title>${page.title}</title>`, 'title', page.pageName);
html = replaceRequired(html, /<link rel="canonical" href="[^"]*"\s*\/?>/, `<link rel="canonical" href="${page.canonicalUrl}">`, 'canonical link', page.pageName);
html = replaceRequired(html, /<meta name="description" content="[^"]*"\s*\/?>/, `<meta name="description" content="${page.description}">`, 'description', page.pageName);
html = replaceRequired(html, /<meta property="og:title" content="[^"]*"\s*\/?>/, `<meta property="og:title" content="${page.title}">`, 'Open Graph title', page.pageName);
html = replaceRequired(html, /<meta property="og:description" content="[^"]*"\s*\/?>/, `<meta property="og:description" content="${page.description}">`, 'Open Graph description', page.pageName);
html = replaceRequired(html, /<meta property="og:url" content="[^"]*"\s*\/?>/, `<meta property="og:url" content="${page.canonicalUrl}">`, 'Open Graph URL', page.pageName);
html = replaceRequired(html, /<meta name="twitter:title" content="[^"]*"\s*\/?>/, `<meta name="twitter:title" content="${page.title}">`, 'Twitter title', page.pageName);
html = replaceRequired(html, /<meta name="twitter:description" content="[^"]*"\s*\/?>/, `<meta name="twitter:description" content="${page.description}">`, 'Twitter description', page.pageName);
html = replaceRequired(html, /"name": "MekBay",/, `"name": "${page.schemaName}",`, 'structured-data name', page.pageName);
html = replaceRequired(html, /"url": "https:\/\/mekbay\.com\/",/, `"url": "${page.canonicalUrl}",`, 'structured-data URL', page.pageName);
html = replaceRequired(html, /"description": "[^"]*",/, `"description": "${page.description}",`, 'structured-data description', page.pageName);

const outputPath = path.join(outputDirectory, page.outputFile);
fs.writeFileSync(outputPath, html);
console.log(`Generated ${path.relative(process.cwd(), outputPath)}`);
}

for (const page of pages) {
generateSeoPage(page);
}
2 changes: 1 addition & 1 deletion scripts/lib/bfs-generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ export function renderBfsGenerationReport(files: readonly PlannedBfsFile[], aero
`- Supported definitions: **${files.length}** (${files.length - existing} new, ${existing} existing)`,
`- Linked MTF/BLK definitions: **${linked}**`,
`- Standalone emplacements: **${files.length - linked}**`,
`- skipped Aerospace: **${aerospace.length}**`,
`- Aerospace rows blocked: **${aerospace.length}**`,
'',
'## Generated definitions',
'',
Expand Down
40 changes: 0 additions & 40 deletions scripts/ratgenerator_build_table.test.ts

This file was deleted.

6 changes: 3 additions & 3 deletions src/app/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@
</a>
</div>
<div class="right home-actions">
<button type="button" class="home-button home-action-button" (click)="showForceGeneratorDialog()" aria-label="Force Generator">
<a class="home-button home-action-button" routerLink="/forcegenerator" queryParamsHandling="preserve" aria-label="Force Generator">
<svg fill="currentColor" width="32px" height="32px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M30.8,2.29A.49.49,0,0,0,30.35,2H16.42a.5.5,0,0,0-.42.23l-10.71,17A.49.49,0,0,0,5.7,20h7.67L6.6,33.25a.52.52,0,0,0,.46.75h3a.5.5,0,0,0,.37-.16L28,14.85a.5.5,0,0,0-.37-.85H20.89L30.72,2.82A.49.49,0,0,0,30.8,2.29Z"></path>
</svg>
<span class="home-action-label">Force Generator</span>
</button>
</a>
<button type="button" class="home-button home-action-button" (click)="showLoadForceDialog()" aria-label="Load Force">
<svg fill="currentColor" width="32px" height="32px" viewBox="0 0 1920 1920" xmlns="http://www.w3.org/2000/svg">
<path d="m572.501 747-254.933 815.893-101.867-31.786 278.507-890.774h1105.813v-320H783.808L612.181 107H.021v1546.667c0 88.213 71.787 160 160 160h1388.054c75.946 0 141.973-54.08 156.906-128.64L1892.608 747H572.501Z" fill-rule="evenodd" />
Expand Down Expand Up @@ -183,7 +183,7 @@
</div>
</div>

<div class="footer">
<div class="footer" data-nosnippet>
@if (showInstallButton()) {
<div class="install-container">
<button class="bt-button install-button" (click)="installPwa()">
Expand Down
1 change: 1 addition & 0 deletions src/app/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ svg-viewer {
line-height: 1;
display: flex;
align-items: center;
text-decoration: none;
}

.home-action-button svg {
Expand Down
6 changes: 5 additions & 1 deletion src/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ import { LoadingSpinnerComponent } from './components/loading-spinner/loading-sp
import { LobbyService } from './services/lobby.service';

import { GameSystem } from './models/common.model';
import { Router, RouterOutlet } from '@angular/router';
import { Router, RouterLink, RouterOutlet } from '@angular/router';
import { UrlService } from './services/url.service';
import { SeoService } from './services/seo.service';

const ANDROID_PWA_BACK_EXIT_HISTORY_STATE_KEY = 'mekbayAndroidPwaBackExit';
const ANDROID_PWA_BACK_RESTORE_GUARD_MS = 1000;
Expand All @@ -62,6 +63,7 @@ const PENDING_UPDATE_RELOAD_AFTER_NO_FOCUS_MS = 6 * 60 * 60 * 1000; // 6 hours
OverlayModule,
PortalModule,
LoadingSpinnerComponent,
RouterLink,
RouterOutlet
],
templateUrl: './app.html',
Expand All @@ -82,6 +84,7 @@ export class App {
private wsService = inject(WsService);
private dialogService = inject(DialogsService);
private toastService = inject(ToastService);
private seoService = inject(SeoService);
protected optionsService = inject(OptionsService);
public unitSearchFiltersService = inject(UnitSearchFiltersService);
public injector = inject(Injector);
Expand Down Expand Up @@ -130,6 +133,7 @@ export class App {
protected unitSearchPortalForceBuilder = signal<DomPortal<any> | undefined>(undefined);

constructor() {
this.seoService.initialize();
// if ("virtualKeyboard" in navigator) {
// (navigator as any).virtualKeyboard.overlaysContent = true; // Opt out of the automatic handling.
// }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<div class="panel glass preventZoomReset framed-borders has-shadow" (click)="$event.stopPropagation()">
<div class="header">{{ data.title }}</div>
<div class="body automation-review-body">
<p class="automation-review-message">{{ data.message }}</p>

<div class="automation-event-list">
@for (item of data.events; track item.id) {
<article class="automation-event"
[class.single-event]="data.events.length === 1"
[class.accepted]="decision(item.id) === true"
[class.rejected]="decision(item.id) === false">
<div class="automation-event-info">
<h3>{{ item.subject }}</h3>
<div class="automation-event-name">{{ item.event }}</div>
<div class="automation-event-description">
<span>{{ item.description }}</span>
@if (item.delta) {
<strong class="automation-event-delta"
[class.cooling]="item.delta < 0"
[class.heating]="item.delta > 0">
({{ signed(item.delta) }})
</strong>
}
</div>
@if (item.effects?.length) {
<div class="automation-event-effects" aria-label="Effects">
@for (effect of item.effects; track effect) {
<span>{{ effect }}</span>
}
</div>
}
</div>
@if (item.breakdown?.length || data.events.length > 1) {
<div class="automation-event-side">
@if (item.breakdown?.length) {
<div class="automation-event-breakdown" aria-label="Heat breakdown">
@for (detail of item.breakdown; track detail.id) {
<div class="automation-event-breakdown-row">
<span>{{ detail.label }}</span>
<strong [class.cooling]="detail.value < 0"
[class.heating]="detail.value > 0">
{{ signed(detail.value) }}
</strong>
</div>
}
</div>
}
@if (data.events.length > 1) {
<div class="automation-event-actions" role="radiogroup"
[attr.aria-label]="item.subject + ': ' + item.event">
<button type="button" class="bt-button success"
role="radio"
[class.selected]="decision(item.id) === true"
[attr.aria-checked]="decision(item.id) === true"
(click)="choose(item.id, true)">ACCEPT</button>
<button type="button" class="bt-button danger"
role="radio"
[class.selected]="decision(item.id) === false"
[attr.aria-checked]="decision(item.id) === false"
(click)="choose(item.id, false)">SKIP</button>
</div>
}
</div>
}
</article>
}
</div>
</div>

<div class="actions automation-review-actions">
@if (data.events.length === 1) {
<button type="button" class="bt-button primary" (click)="resolveSingle(true)">ACCEPT</button>
<button type="button" class="bt-button" (click)="resolveSingle(false)">SKIP</button>
} @else {
<button type="button" class="bt-button"
[class.success]="reviewAction().tone === 'success'"
[class.primary]="reviewAction().tone === 'primary'"
[class.danger]="reviewAction().tone === 'danger'"
[disabled]="reviewAction().disabled"
(click)="performReviewAction()">
{{ reviewAction().label }}
</button>
}
@if (data.allowCancel) {
<button type="button" class="bt-button" (click)="cancel()">CANCEL</button>
}
</div>
</div>
Loading
Loading