diff --git a/.emergent/emergent.yml b/.emergent/emergent.yml index 6274ff4..06634b6 100644 --- a/.emergent/emergent.yml +++ b/.emergent/emergent.yml @@ -1,4 +1,4 @@ { "job_id": "fc0ddb3f-43c1-4a3d-a1b6-f0b4e0393b91", - "created_at": "2026-01-27T09:56:49.970825+00:00Z" + "created_at": "2026-01-27T12:01:15.014238+00:00Z" } diff --git a/.gitignore b/.gitignore index 5254964..4f8c687 100644 --- a/.gitignore +++ b/.gitignore @@ -72,123 +72,3 @@ frontend/node_modules/.cache/default-development/6.pack # Environment files *.env *.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* --e -# Environment files -*.env -*.env.* diff --git a/frontend/src/components/MissionManager.jsx b/frontend/src/components/MissionManager.jsx index dae250e..d1a6fb2 100644 --- a/frontend/src/components/MissionManager.jsx +++ b/frontend/src/components/MissionManager.jsx @@ -19,7 +19,7 @@ import { isElementalAvailableForMission, getMissionObjectiveReward, } from '../lib/missions'; -import { findPilotForMech, getMechAdjustedBV } from '../lib/mechs'; +import { findPilotForMech, getMechAdjustedBV, getAdjustedBV } from '../lib/mechs'; import { getPilotDisplayName } from '../lib/pilots'; import { getStatusBadgeVariant, UNIT_STATUS } from '../lib/constants'; import { createSnapshot, advanceDateString, createFullSnapshot, addFullSnapshot } from '../lib/snapshots'; @@ -38,6 +38,21 @@ const emptyMissionForm = { spBudget: 0, spPurchases: [], totalTonnage: 0, + opForUnits: [], +}; + +// Calculate adjusted BV for an OpFor unit +const getOpForAdjustedBV = (unit) => { + if (!unit) return 0; + // Support both old format (bv) and new format (baseBv) + const baseBv = unit.baseBv ?? unit.bv ?? 0; + return getAdjustedBV(baseBv, unit.gunnery ?? 4, unit.piloting ?? 5); +}; + +// Calculate total adjusted BV for all OpFor units +const calculateOpForTotalBV = (opForUnits) => { + if (!opForUnits || opForUnits.length === 0) return 0; + return opForUnits.reduce((sum, unit) => sum + getOpForAdjustedBV(unit), 0); }; export default function MissionManager({ force, onUpdate }) { @@ -112,6 +127,7 @@ export default function MissionManager({ force, onUpdate }) { spBudget: mission.spBudget || 0, spPurchases: mission.spPurchases || [], totalTonnage: mission.totalTonnage || 0, + opForUnits: mission.opForUnits || [], }); } else { setEditingMission(null); @@ -214,6 +230,42 @@ export default function MissionManager({ force, onUpdate }) { })); }; + // OpFor unit management + const [opForSearchInput, setOpForSearchInput] = useState(''); + + const addOpForUnit = (mechData) => { + const newUnit = { + id: `opfor-${Date.now()}`, + name: mechData.name, + tonnage: mechData.weight || 0, + baseBv: mechData.bv || 0, + gunnery: 4, + piloting: 5, + status: 'active', + }; + setFormData((prev) => ({ + ...prev, + opForUnits: [...(prev.opForUnits || []), newUnit], + })); + setOpForSearchInput(''); + }; + + const updateOpForUnit = (unitId, field, value) => { + setFormData((prev) => ({ + ...prev, + opForUnits: (prev.opForUnits || []).map((unit) => + unit.id === unitId ? { ...unit, [field]: value } : unit + ), + })); + }; + + const removeOpForUnit = (unitId) => { + setFormData((prev) => ({ + ...prev, + opForUnits: (prev.opForUnits || []).filter((u) => u.id !== unitId), + })); + }; + const saveMission = () => { const timestamp = force.currentDate; @@ -235,6 +287,7 @@ export default function MissionManager({ force, onUpdate }) { spBudget: formData.spBudget || 0, spPurchases: formData.spPurchases || [], totalTonnage, + opForUnits: formData.opForUnits || [], }; if (editingMission) { @@ -367,7 +420,7 @@ export default function MissionManager({ force, onUpdate }) { }; // Add a kill to a pilot's combat updates - const addPilotKill = (pilotId, mechData) => { + const addPilotKill = (pilotId, mechData, opForUnitId = null) => { const missionName = missionBeingCompleted?.name || 'Mission'; const missionDate = force.currentDate; @@ -383,6 +436,7 @@ export default function MissionManager({ force, onUpdate }) { tonnage: mechData.weight || 0, mission: missionName, date: missionDate, + opForUnitId: opForUnitId, }, ], }, @@ -391,6 +445,19 @@ export default function MissionManager({ force, onUpdate }) { setKillSearchInput((prev) => ({ ...prev, [pilotId]: '' })); }; + // Get all OpFor unit IDs that have already been claimed as kills + const getClaimedOpForUnitIds = () => { + const claimed = new Set(); + Object.values(pilotCombatUpdates).forEach((update) => { + (update.kills || []).forEach((kill) => { + if (kill.opForUnitId) { + claimed.add(kill.opForUnitId); + } + }); + }); + return claimed; + }; + // Remove a kill from a pilot's combat updates const removePilotKill = (pilotId, killId) => { setPilotCombatUpdates((prev) => ({ @@ -736,6 +803,29 @@ export default function MissionManager({ force, onUpdate }) { )} + {/* OpFor Roster */} + {mission.opForUnits && mission.opForUnits.length > 0 && ( +
+
+ + + Opposing Force + + + {mission.opForUnits.reduce((sum, u) => sum + (u.tonnage || 0), 0)}t |{' '} + {formatNumber(calculateOpForTotalBV(mission.opForUnits))} BV + +
+
+ {mission.opForUnits.map((unit) => ( + + {unit.name} ({unit.tonnage}t, {unit.gunnery ?? 4}/{unit.piloting ?? 5}, {formatNumber(getOpForAdjustedBV(unit))} BV) + + ))} +
+
+ )} + {mission.recap && (
Mission Recap: @@ -1099,6 +1189,101 @@ export default function MissionManager({ force, onUpdate }) {
)} + {/* OpFor Roster */} +
+
+ + {formData.opForUnits && formData.opForUnits.length > 0 && ( +
+ + {formData.opForUnits.reduce((sum, u) => sum + (u.tonnage || 0), 0)}t + + | + + {formatNumber(calculateOpForTotalBV(formData.opForUnits))} BV + +
+ )} +
+ +
+
+ +
+
+ + {formData.opForUnits && formData.opForUnits.length > 0 ? ( +
+ {formData.opForUnits.map((unit) => ( +
+
+ {unit.name} + + {unit.tonnage}t + +
+
+
+ G + { + const value = parseInt(e.target.value, 10); + updateOpForUnit(unit.id, 'gunnery', Number.isNaN(value) ? 4 : Math.max(0, Math.min(8, value))); + }} + /> +
+
+ P + { + const value = parseInt(e.target.value, 10); + updateOpForUnit(unit.id, 'piloting', Number.isNaN(value) ? 5 : Math.max(0, Math.min(8, value))); + }} + /> +
+ + {formatNumber(getOpForAdjustedBV(unit))} BV + + +
+
+ ))} +
+ ) : ( +

+ No OpFor units added. Search above to add enemy mechs encountered in this mission. +

+ )} +
+