Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | 1x 1x 1x 1x 2x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 1x 1x 1x 1x | import { MissionElement } from './types';
import { MissionManager } from './mission-manager';
import { CustomImportedMissionElement } from './custom-imported-element';
export interface MissionElementLayoutEntry {
elementId: string;
parentMissionId?: string;
name: string;
position: { x: number; y: number; z: number };
yawDegrees: number;
isDualLocked: boolean;
dualLockPosition?: { x: number; z: number } | null;
anchoredClusterId?: string | null;
}
export interface FieldLayoutData {
version: number;
name: string;
timestamp: number;
elements: MissionElementLayoutEntry[];
}
const STORAGE_KEY = 'fll_field_custom_layout_v1';
export class FieldLayoutManager {
/**
* Saves the current positions and Dual Lock states of all elements to LocalStorage
*/
public static saveCurrentLayout(elements: MissionElement[]): FieldLayoutData {
const layoutEntries: MissionElementLayoutEntry[] = elements.map((elem) => {
const pos = elem.getPosition();
const yaw = elem.getYawDegrees ? elem.getYawDegrees() : 0;
const isLocked = elem.isDualLocked ?? (elem instanceof CustomImportedMissionElement ? elem.getIsBaseFixed() : true);
const lockPos = elem.getDualLockPosition ? elem.getDualLockPosition() : (isLocked ? { x: pos.x, z: pos.z } : null);
const clusterId = (elem as any).getAnchoredClusterId ? (elem as any).getAnchoredClusterId() : null;
return {
elementId: elem.id,
parentMissionId: elem.parentMissionId,
name: elem.name,
position: { x: pos.x, y: pos.y, z: pos.z },
yawDegrees: yaw,
isDualLocked: isLocked,
dualLockPosition: lockPos,
anchoredClusterId: clusterId,
};
});
const layoutData: FieldLayoutData = {
version: 1,
name: `Custom FLL Layout (${new Date().toLocaleTimeString()})`,
timestamp: Date.now(),
elements: layoutEntries,
};
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(layoutData));
} catch (e) {
console.warn('[FieldLayoutManager] Failed to save to localStorage:', e);
}
return layoutData;
}
/**
* Loads saved layout from LocalStorage
*/
public static loadSavedLayout(): FieldLayoutData | null {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
return JSON.parse(raw) as FieldLayoutData;
} catch {
return null;
}
}
/**
* Returns true if a layout is saved in LocalStorage
*/
public static hasSavedLayout(): boolean {
return localStorage.getItem(STORAGE_KEY) !== null;
}
/**
* Exports a layout as a downloadable JSON file
*/
public static exportLayoutToFile(layout: FieldLayoutData, filename = 'fll-competition-layout.json'): void {
const blob = new Blob([JSON.stringify(layout, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
* Reads and parses a layout JSON file from local disk
*/
public static async importLayoutFromFile(file: File): Promise<FieldLayoutData> {
const text = await file.text();
const data = JSON.parse(text) as FieldLayoutData;
if (!data.elements || !Array.isArray(data.elements)) {
throw new Error('Invalid layout file format: missing elements array.');
}
return data;
}
/**
* Applies layout coordinates and Dual Lock anchor states across MissionManager elements
*/
public static async applyLayout(
layout: FieldLayoutData,
missionManager: MissionManager
): Promise<{ restoredCount: number; dualLockedCount: number }> {
let restoredCount = 0;
let dualLockedCount = 0;
for (const entry of layout.elements) {
let elem = missionManager.getElement(entry.elementId);
// If the parent mission is not currently loaded, attempt to load it
if (!elem && entry.parentMissionId && entry.parentMissionId === entry.elementId) {
await missionManager.toggleSeasonMission(entry.elementId, true);
elem = missionManager.getElement(entry.elementId);
}
if (elem) {
// Set transform
elem.setPosition(entry.position, entry.yawDegrees);
// Apply Dual Lock status
if (elem.setDualLocked) {
elem.setDualLocked(entry.isDualLocked, entry.dualLockPosition || undefined, entry.anchoredClusterId || undefined);
} else if (elem instanceof CustomImportedMissionElement) {
elem.setDualLocked(entry.isDualLocked, entry.dualLockPosition || undefined, entry.anchoredClusterId || undefined);
}
if (entry.isDualLocked) {
dualLockedCount++;
}
restoredCount++;
}
}
missionManager.onMissionListChanged?.();
return { restoredCount, dualLockedCount };
}
}
|