From 0f598009ae2434da363a3dde2f7bf4705794d5f0 Mon Sep 17 00:00:00 2001 From: exeea Date: Thu, 20 Aug 2026 02:04:42 +0200 Subject: [PATCH 1/3] . --- src/app/services/unit-search-filters.model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/services/unit-search-filters.model.ts b/src/app/services/unit-search-filters.model.ts index 7b1288a5c..1ca6fb1e0 100644 --- a/src/app/services/unit-search-filters.model.ts +++ b/src/app/services/unit-search-filters.model.ts @@ -539,7 +539,7 @@ export const SORT_OPTIONS: SortOption[] = [ { key: 'name', label: 'Name' }, ...ADVANCED_FILTERS .filter(f => f.type !== AdvFilterType.BOOLEAN) - .filter(f => !['era', 'faction', 'availabilityRarity', 'availabilityFrom', 'forcePack', 'componentName', 'weaponType', 'source', '_tags', 'as.specials', 'name', 'chassis', 'model', 'as._motive', 'quirks', 'features'].includes(f.key)) + .filter(f => !['era', 'faction', 'availabilityRarity', 'availabilityFrom', 'forcePack', 'componentName', 'weaponType', 'source', 'rulesRefs', '_tags', 'as.specials', 'name', 'chassis', 'model', 'as._motive', 'quirks', 'features'].includes(f.key)) .map(f => ({ key: f.key, label: f.label, From 7cd996957eab5f89a522fc4b49165df0d3ec1a83 Mon Sep 17 00:00:00 2001 From: exeea Date: Thu, 20 Aug 2026 02:15:28 +0200 Subject: [PATCH 2/3] print fix --- src/app/utils/asprint.util.spec.ts | 71 ++++++++++++++++++++++++++++-- src/app/utils/asprint.util.ts | 2 - 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/app/utils/asprint.util.spec.ts b/src/app/utils/asprint.util.spec.ts index 26123a137..6bd1c3d52 100644 --- a/src/app/utils/asprint.util.spec.ts +++ b/src/app/utils/asprint.util.spec.ts @@ -13,6 +13,12 @@ interface TestPrintLayout { } describe('ASPrintUtil', () => { + afterEach(() => { + window.dispatchEvent(new Event('afterprint')); + document.getElementById('as-multipage-container')?.remove(); + document.body.classList.remove('as-multipage-container-active'); + }); + it('keeps the standard 2 by 4 layout as the default-size preset', () => { const layout = getPrintLayout('standard'); const styles = getFixedPrintStyles('none', 'standard'); @@ -131,19 +137,76 @@ describe('ASPrintUtil', () => { ); expect(renderedHeat).toEqual([0, 0]); - expect(unit.update).toHaveBeenCalledWith(serialized); - expect(heat).toBe(2); - expect(pendingHeat).toBe(1); - expect(unit.disabledSaving).toBeFalse(); + expect(unit.update).not.toHaveBeenCalled(); + expect(heat).toBe(0); + expect(pendingHeat).toBe(0); + expect(unit.disabledSaving).toBeTrue(); window.dispatchEvent(new Event('afterprint')); + expect(unit.update).toHaveBeenCalledWith(serialized); expect(unit.update).toHaveBeenCalledTimes(1); expect(heat).toBe(2); expect(pendingHeat).toBe(1); expect(unit.disabledSaving).toBeFalse(); }); + it('keeps dynamically rendered card hosts mounted until print cleanup', async () => { + const cardHost = document.createElement('alpha-strike-card'); + const hostView = {}; + const destroy = jasmine.createSpy('destroy'); + const createContainer = (options: { componentRefs: unknown[] }) => { + const overlay = document.createElement('div'); + overlay.id = 'as-multipage-container'; + const cardCell = document.createElement('div'); + cardCell.className = 'as-card-cell'; + cardCell.appendChild(cardHost); + overlay.appendChild(cardCell); + options.componentRefs.push({ hostView, destroy }); + return overlay; + }; + spyOn(ASPrintUtil, 'createFixedPrintContainer').and.callFake(createContainer); + spyOn(ASPrintUtil, 'createFlexPrintContainer').and.callFake(createContainer); + + const detachView = jasmine.createSpy('detachView').and.callFake(() => cardHost.remove()); + const appRef = { + tick: jasmine.createSpy('tick'), + detachView, + }; + const unit = { + disabledSaving: false, + serialize: () => ({ state: 'original' }), + update: jasmine.createSpy('update'), + repairAll: jasmine.createSpy('repairAll'), + getUnit: () => ({ as: { TP: 'BM' } }), + }; + const group = { units: () => [unit] }; + + await ASPrintUtil.multipagePrint( + appRef as never, + {} as never, + {} as never, + [group] as never, + { + clean: true, + ASPrintPageBreakOnGroups: false, + ASPrintCardSize: 'standard', + printMargin: 'none', + }, + false, + ); + + expect(document.querySelector('#as-multipage-container alpha-strike-card')).toBe(cardHost); + expect(detachView).not.toHaveBeenCalled(); + expect(destroy).not.toHaveBeenCalled(); + + window.dispatchEvent(new Event('afterprint')); + + expect(detachView).toHaveBeenCalledOnceWith(hostView); + expect(destroy).toHaveBeenCalledTimes(1); + expect(document.getElementById('as-multipage-container')).toBeNull(); + }); + it('restores unit state and removes the overlay when rendering fails', async () => { const createContainer = () => { const overlay = document.createElement('div'); diff --git a/src/app/utils/asprint.util.ts b/src/app/utils/asprint.util.ts index cdb33369d..345fb4035 100644 --- a/src/app/utils/asprint.util.ts +++ b/src/app/utils/asprint.util.ts @@ -166,8 +166,6 @@ export class ASPrintUtil { triggerPrint, onMount: () => { appRef.tick(); - detachViews(); - restoreUnits(); }, onCleanup: cleanup, }); From e387aecc40395aad8c90e84f98fc227e24c9c3f7 Mon Sep 17 00:00:00 2001 From: exeea Date: Thu, 20 Aug 2026 02:15:37 +0200 Subject: [PATCH 3/3] equip --- .../floating-comp-info.component.spec.ts | 53 +++++++++++++++++++ .../floating-comp-info.component.ts | 6 +-- .../advancement-timeline.component.spec.ts | 2 +- .../set-ammo.dialog.component.spec.ts | 4 +- src/app/models/equipment.model.spec.ts | 30 +++++++++++ src/app/models/equipment.model.ts | 19 +++++-- 6 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 src/app/components/floating-comp-info/floating-comp-info.component.spec.ts diff --git a/src/app/components/floating-comp-info/floating-comp-info.component.spec.ts b/src/app/components/floating-comp-info/floating-comp-info.component.spec.ts new file mode 100644 index 000000000..c85e91c90 --- /dev/null +++ b/src/app/components/floating-comp-info/floating-comp-info.component.spec.ts @@ -0,0 +1,53 @@ +// Copyright (C) 2026 The MegaMek Team +// SPDX-License-Identifier: GPL-3.0-or-later +// Author: Drake + +import { TestBed } from '@angular/core/testing'; +import { Equipment } from '../../models/equipment.model'; +import { CBTGameRulesService } from '../../services/cbt-game-rules.service'; +import { DataService } from '../../services/data.service'; +import { createEmptyUnit } from '../../testing/unit-test-helpers'; +import { FloatingCompInfoComponent } from './floating-comp-info.component'; + +describe('FloatingCompInfoComponent', () => { + it('renders structured equipment rules references', () => { + const equipment = new Equipment({ + id: 'test-equipment', + name: 'Test Equipment', + type: 'misc', + rulesRefs: [ + { book: 'TO:AUE', page: 181 }, + { book: 'TM', page: null }, + { book: 'BMM' }, + ], + }); + TestBed.configureTestingModule({ + imports: [FloatingCompInfoComponent], + providers: [ + { provide: DataService, useValue: { findEquipment: () => equipment } }, + { + provide: CBTGameRulesService, + useValue: { gameRules: () => ({ resolveToHit: () => ({ profile: [0] }) }) }, + }, + ], + }); + const fixture = TestBed.createComponent(FloatingCompInfoComponent); + fixture.componentRef.setInput('unit', createEmptyUnit()); + fixture.componentRef.setInput('comp', { + id: equipment.id, + q: 1, + n: equipment.name, + t: 'C', + p: 0, + l: 'CT', + }); + + fixture.detectChanges(); + + const root = fixture.nativeElement as HTMLElement; + const reference = Array.from(root.querySelectorAll('.equip-item')) + .find(item => item.querySelector('.equip-label')?.textContent?.trim() === 'Reference:'); + expect(reference?.querySelector('.equip-value')?.textContent?.trim()) + .toBe('TO:AUE, 181; TM; BMM'); + }); +}); diff --git a/src/app/components/floating-comp-info/floating-comp-info.component.ts b/src/app/components/floating-comp-info/floating-comp-info.component.ts index eb967d0fa..a63694844 100644 --- a/src/app/components/floating-comp-info/floating-comp-info.component.ts +++ b/src/app/components/floating-comp-info/floating-comp-info.component.ts @@ -7,7 +7,7 @@ import { Component, input, computed, inject, ChangeDetectionStrategy } from '@an import type { UnitComponent } from '../../models/units.model'; import { DataService } from '../../services/data.service'; import type { Unit } from '../../models/units.model'; -import { AmmoEquipment, type Equipment, WeaponEquipment } from '../../models/equipment.model'; +import { AmmoEquipment, type Equipment, formatEquipmentRulesRefs, WeaponEquipment } from '../../models/equipment.model'; import { TechDate, TechAdvancementDates, techDateYear, formatTechDate } from '../../models/entity'; import { getWeaponTypeCSSClass } from '../../utils/equipment.util'; import { CBTGameRulesService } from '../../services/cbt-game-rules.service'; @@ -237,7 +237,7 @@ export class FloatingCompInfoComponent { { label: 'Cost', value: eq.cost }, { label: 'Tonnage', value: eq.tonnage }, { label: 'Criticals', value: eq.critSlots }, - { label: 'Reference', value: eq.rulesRefs } + { label: 'Reference', value: formatEquipmentRulesRefs(eq.rulesRefs) } ] }, { @@ -258,4 +258,4 @@ export class FloatingCompInfoComponent { return result; } -} \ No newline at end of file +} diff --git a/src/app/components/set-ammo-dialog/advancement-timeline.component.spec.ts b/src/app/components/set-ammo-dialog/advancement-timeline.component.spec.ts index 4a8f402e4..5e5c45760 100644 --- a/src/app/components/set-ammo-dialog/advancement-timeline.component.spec.ts +++ b/src/app/components/set-ammo-dialog/advancement-timeline.component.spec.ts @@ -13,7 +13,7 @@ describe('AdvancementTimelineComponent', () => { id, name: id, type: 'misc', - rulesRefs: 'Test Rules', + rulesRefs: [{ book: 'Test Rules', page: null }], tech, }); } diff --git a/src/app/components/set-ammo-dialog/set-ammo.dialog.component.spec.ts b/src/app/components/set-ammo-dialog/set-ammo.dialog.component.spec.ts index 7bb357647..ba0d4bf80 100644 --- a/src/app/components/set-ammo-dialog/set-ammo.dialog.component.spec.ts +++ b/src/app/components/set-ammo-dialog/set-ammo.dialog.component.spec.ts @@ -26,7 +26,7 @@ function createAmmo(id: string, kgPerShot = 100, ammo: Partial { expect(getAmmoInfoItems(ammo).find(item => item.label === 'Damage')?.value).toBe(40); }); -}); \ No newline at end of file +}); diff --git a/src/app/models/equipment.model.spec.ts b/src/app/models/equipment.model.spec.ts index 00e4fd68b..40ac4e0a6 100644 --- a/src/app/models/equipment.model.spec.ts +++ b/src/app/models/equipment.model.spec.ts @@ -13,6 +13,7 @@ import { EquipmentMap, findStandardAmmoForWeapon, findIntrinsicAmmoForWeapon, + formatEquipmentRulesRefs, isBombEquipment, MiscEquipment, resolveWeaponDamage, @@ -29,6 +30,35 @@ function catalog(equipment: EquipmentMap = {}): EquipmentRegistry { } describe('equipment model', () => { + it('formats structured equipment rules references', () => { + expect(formatEquipmentRulesRefs([ + { book: 'TO:AUE', page: 181 }, + { book: 'TM', page: null }, + { book: 'BMM' }, + ])).toBe('TO:AUE, 181; TM; BMM'); + expect(formatEquipmentRulesRefs([])).toBe(''); + }); + + it('defaults missing equipment rules references to an empty array', () => { + const equipment = createEquipment({ id: 'test', name: 'Test', type: 'misc' }); + + expect(equipment.rulesRefs).toEqual([]); + }); + + it('hydrates structured equipment rules references', () => { + const equipment = createEquipment({ + id: 'test', + name: 'Test', + type: 'misc', + rulesRefs: [{ book: 'TO:AUE', page: 181 }, { book: 'TM', page: null }], + }); + + expect(equipment.rulesRefs).toEqual([ + { book: 'TO:AUE', page: 181 }, + { book: 'TM', page: null }, + ]); + }); + it('identifies fixed and variable equipment stats in one place', () => { const fixed = createEquipment({ id: 'fixed', name: 'Fixed', type: 'misc', diff --git a/src/app/models/equipment.model.ts b/src/app/models/equipment.model.ts index 87f5714b2..36dd1ea79 100644 --- a/src/app/models/equipment.model.ts +++ b/src/app/models/equipment.model.ts @@ -41,6 +41,19 @@ export type WeaponCategory = 'energy' | 'missile' | 'ballistic' | 'artillery' | export type WeaponDamageUnit = 'missile' | 'shot' | 'artillery'; +/** A rulebook containing rules for an equipment entry, optionally at a specific page. */ +export interface EquipmentRulesReference { + readonly book: string; + readonly page?: number | null; +} + +/** Formats equipment rule references for display. */ +export function formatEquipmentRulesRefs(references: readonly EquipmentRulesReference[]): string { + return references + .map(reference => reference.page == null ? reference.book : `${reference.book}, ${reference.page}`) + .join('; '); +} + /** Resolved damage values, using zero when the source has no intrinsic numeric damage. */ export interface WeaponDamage { readonly values: readonly number[]; @@ -306,7 +319,7 @@ export interface EquipmentRawData { name: string; shortName?: string; sortingName?: string; - rulesRefs?: string; + rulesRefs?: EquipmentRulesReference[]; aliases?: string[]; stats?: Partial; tech?: Partial; @@ -432,7 +445,7 @@ export class Equipment { readonly name: string; readonly shortName: string; readonly sortingName: string; - readonly rulesRefs: string; + readonly rulesRefs: EquipmentRulesReference[]; readonly aliases: string[]; protected readonly stats: EquipmentStats; readonly tech: TechData; @@ -446,7 +459,7 @@ export class Equipment { this.name = data.name; this.shortName = data.shortName ?? data.name; this.sortingName = data.sortingName ?? data.name; - this.rulesRefs = data.rulesRefs ?? ''; + this.rulesRefs = Array.isArray(data.rulesRefs) ? data.rulesRefs : []; this.aliases = data.aliases ?? []; this.type = data.type; this.modes = data.modes ?? [];