001/************************* PROJECT RON *************************/
002/* Copyright (c) 2026 StuyPulse Robotics. All rights reserved. */
003/* Use of this source code is governed by an MIT-style license */
004/* that can be found in the repository LICENSE file.           */
005/***************************************************************/
006package com.stuypulse.robot.util.simulation;
007
008import static edu.wpi.first.units.Units.*;
009
010import com.stuypulse.robot.constants.Settings;
011import com.stuypulse.robot.subsystems.intake.Intake;
012import com.stuypulse.robot.subsystems.intake.Intake.IntakeState;
013import com.stuypulse.robot.subsystems.shooter.Shooter;
014import com.stuypulse.robot.subsystems.shooter.Shooter.ShooterState;
015import com.stuypulse.robot.subsystems.swerve.CommandSwerveDrivetrain;
016import dev.doglog.DogLog;
017import edu.wpi.first.math.geometry.Pose2d;
018import edu.wpi.first.math.geometry.Pose3d;
019import edu.wpi.first.math.geometry.Rotation2d;
020import edu.wpi.first.math.geometry.Rotation3d;
021import edu.wpi.first.math.geometry.Translation2d;
022import edu.wpi.first.networktables.NetworkTableInstance;
023import edu.wpi.first.networktables.StructArrayPublisher;
024import edu.wpi.first.networktables.StructPublisher;
025import edu.wpi.first.units.measure.Angle;
026import edu.wpi.first.units.measure.Distance;
027import edu.wpi.first.units.measure.LinearVelocity;
028import edu.wpi.first.wpilibj.Notifier;
029import org.ironmaple.simulation.IntakeSimulation;
030import org.ironmaple.simulation.SimulatedArena;
031import org.ironmaple.simulation.drivesims.SwerveDriveSimulation;
032import org.ironmaple.simulation.seasonspecific.rebuilt2026.Arena2026Rebuilt;
033import org.ironmaple.simulation.seasonspecific.rebuilt2026.RebuiltFuelOnFly;
034
035public class Simulation {
036
037    private static final Simulation instance;
038
039    public final Arena2026Rebuilt arenaInstance;
040
041    private final Notifier shotLoop;
042
043    private final SwerveDriveSimulation swerveMSim;
044
045    private final IntakeSimulation intakeMSim;
046
047    private final Intake intakeSim;
048
049    private final Shooter shooterSim;
050
051    private final StructArrayPublisher<Pose3d> fuel;
052
053    private final StructPublisher<Pose3d> intakePivot;
054
055    private final StructPublisher<Pose3d> hopper;
056
057    private final StructArrayPublisher<Pose3d> fuelLayers;
058
059    private final StructPublisher<Pose3d> shooter;
060
061    static {
062        if (CommandSwerveDrivetrain.getInstance().getMapleSimDrive() != null)
063            instance = new Simulation();
064        else
065            // extra safeguarding to ensure NO overlap between sim code and actual code
066            instance = null;
067    }
068
069    public static Simulation getInstance() {
070        return instance;
071    }
072
073    private Simulation() {
074        intakeSim = Intake.getInstance();
075        shooterSim = Shooter.getInstance();
076        swerveMSim = CommandSwerveDrivetrain.getInstance().getMapleSimDrive();
077        arenaInstance = new Arena2026Rebuilt(false);
078        configureArena();
079        intakeMSim = createIntakeSimulation();
080        intakeMSim.addGamePiecesToIntake(SimulationConstants.Hopper.FUEL_CAPACITY);
081        shotLoop = new Notifier(this::updateShooting);
082        shotLoop.startPeriodic(1.0 / SimulationConstants.Shooter.BPS);
083        NetworkTableInstance table = NetworkTableInstance.getDefault();
084        fuel = table.getStructArrayTopic("AdvScope/FuelPoses", Pose3d.struct).publish();
085        intakePivot = table.getStructTopic("AdvScope/IntakePose", Pose3d.struct).publish();
086        hopper = table.getStructTopic("AdvScope/HopperPose", Pose3d.struct).publish();
087        fuelLayers = table.getStructArrayTopic("AdvScope/FuelLayers", Pose3d.struct).publish();
088        shooter = table.getStructTopic("AdvScope/ShooterPose", Pose3d.struct).publish();
089    }
090
091    private void configureArena() {
092        arenaInstance.setEfficiencyMode(SimulationConstants.SPAWN_GAMEPIECES_SPARSELY);
093        arenaInstance.resetFieldForAuto();
094        arenaInstance.addDriveTrainSimulation(this.swerveMSim);
095        SimulatedArena.overrideInstance(arenaInstance);
096    }
097
098    private IntakeSimulation createIntakeSimulation() {
099        return IntakeSimulation.OverTheBumperIntake(
100                "Fuel",
101                swerveMSim,
102                Meters.of(SimulationConstants.Intake.INTAKE_WIDTH),
103                Meters.of(SimulationConstants.Intake.INTAKE_LENGTH),
104                IntakeSimulation.IntakeSide.FRONT,
105                SimulationConstants.Hopper.FUEL_CAPACITY);
106    }
107
108    private Pose3d getIntakePivotPose() {
109        return SimulationConstants.Intake.PIVOT_OFFSETS.withRotation(
110                new Rotation3d(
111                        0, // inverts the angle
112                        intakeSim.getRelativePosition().in(Radians),
113                        0));
114    }
115
116    private double getIntakeArmEndX() {
117        return SimulationConstants.Intake.PIVOT_END_X
118                + // sin works because we're zeroed at horizontal
119                Settings.Intake.Pivot.PIVOT_ARM_LENGTH.in(Meters)
120                        * Math.sin(
121                                intakeSim.getRelativePosition().in(Radians)
122                                        + SimulationConstants.Intake.PIVOT_OFFSETS.toRotation3d().getX());
123    }
124
125    private void updateIntakeEnabled(boolean enabled) {
126        if (enabled) {
127            intakeMSim.startIntake();
128        } else {
129            intakeMSim.stopIntake();
130        }
131    }
132
133    private void updateIntake() {
134        boolean intakeEnabled = intakeSim.atTargetAngle()
135                && (intakeSim.getState() == IntakeState.DOWN)
136                && Settings.EnabledSubsystems.INTAKE.get();
137        updateIntakeEnabled(intakeEnabled);
138    }
139
140    private void updateHopperFuel() {
141        final double hopperPercentage = (double) intakeMSim.getGamePiecesAmount()
142                / (double) SimulationConstants.Hopper.FUEL_CAPACITY;
143        final int layers = SimulationConstants.Hopper.FUEL_LAYERS;
144        final Pose3d visiblePose = SimulationConstants.Hopper.VISIBLE_POSE;
145        final Pose3d hiddenPose = SimulationConstants.Hopper.HIDDEN_POSE;
146        final Pose3d[] poses = new Pose3d[layers];
147        for (int i = 0; i < layers; i++) {
148            poses[i] = hopperPercentage >= (1 / (double) layers) * (i + 1) ? visiblePose : hiddenPose;
149        }
150        fuelLayers.set(poses);
151        DogLog.log("Intake/hopperpercentage", hopperPercentage);
152    }
153
154    /**
155     * <h4>Extension of {@link Arena2026Rebuilt#addPieceWithVariance} that uses
156     * chassis speeds</h4>
157     *
158     * <p>
159     * Adds a gamepiece too the arena with a certain random variance.
160     *
161     * @param piecePose the field relative position at which to spawn the gamepiece
162     * @param yaw the initial yaw of the gamepiece
163     * @param height the initial height of the gamepiece above the field
164     * @param speed the initial speed of the gamepiece
165     * @param pitch the initial pitch of the gamepiece
166     * @param xVariance the maximum random offset applied to the x coordinate
167     * @param yVariance the maximum random offset applied to the y coordinate
168     * @param yawVariance the maximum random offset applied to the yaw, in degrees
169     * @param speedVariance the maximum random offset applied to the speed, in m/s
170     * @param pitchVariance the maximum random offset applied to the pitch, in degrees
171     */
172    private void robotRelativeAddPieceWithVariance(
173            Translation2d piecePose,
174            Rotation2d yaw,
175            Distance height,
176            LinearVelocity speed,
177            Angle pitch,
178            double xVariance,
179            double yVariance,
180            double yawVariance,
181            double speedVariance,
182            double pitchVariance) {
183        arenaInstance.addGamePieceProjectile(
184                new RebuiltFuelOnFly(
185                        piecePose.plus(
186                                new Translation2d(
187                                        Arena2026Rebuilt.randomInRange(xVariance),
188                                        Arena2026Rebuilt.randomInRange(yVariance))),
189                        new Translation2d(),
190                        swerveMSim.getDriveTrainSimulatedChassisSpeedsFieldRelative(),
191                        yaw.plus(Rotation2d.fromDegrees(Arena2026Rebuilt.randomInRange(yawVariance))),
192                        height,
193                        speed.plus(MetersPerSecond.of(Arena2026Rebuilt.randomInRange(speedVariance))),
194                        Degrees.of(pitch.in(Degrees) + Arena2026Rebuilt.randomInRange(pitchVariance))));
195    }
196
197    private void summonFuelAtIntake() {
198        robotRelativeAddPieceWithVariance(
199                swerveMSim
200                        .getSimulatedDriveTrainPose()
201                        .getTranslation()
202                        .plus(
203                                SimulationConstants.Intake.OUTTAKE_OFFSETS
204                                        .applyToPose3dRobotRelative(
205                                                new Pose3d(
206                                                        getIntakeArmEndX(),
207                                                        0,
208                                                        0,
209                                                        new Rotation3d(
210                                                                swerveMSim.getSimulatedDriveTrainPose().getRotation())))
211                                        .getTranslation()
212                                        .toTranslation2d()),
213                swerveMSim.getSimulatedDriveTrainPose().getRotation(),
214                Meters.of(0),
215                MetersPerSecond.of(2),
216                Radians.of(0), // x
217                SimulationConstants.Intake.INTAKE_WIDTH,
218                0.0,
219                0.0, // speed
220                1.0,
221                0.0);
222    }
223
224    private void updateShooting() {
225        if (intakeSim.getState() == IntakeState.OUTTAKE
226                && Settings.EnabledSubsystems.INTAKE.get()
227                && intakeMSim.obtainGamePieceFromIntake()) {
228            summonFuelAtIntake();
229        } else if ((shooterSim.getState() == ShooterState.SHOOT
230                || shooterSim.getState() == ShooterState.FERRY)
231                && Settings.EnabledSubsystems.SHOOTER.get()) {
232            final Pose2d shooterPose = SimulationConstants.Shooter.OFFSETS.applyToPose2d(
233                    swerveMSim.getSimulatedDriveTrainPose());
234            final double launchAngle = 67.67;
235            robotRelativeAddPieceWithVariance(
236                    shooterPose.getTranslation(),
237                    swerveMSim.getSimulatedDriveTrainPose().getRotation(),
238                    Meters.of(SimulationConstants.Shooter.OFFSETS.toPose3d().getZ()),
239                    MetersPerSecond.of(
240                            SimulationConstants.Shooter.angularVelocityToMps(
241                                    shooterSim.getCurrentAngularVelocity())),
242                    Degrees.of(launchAngle),
243                    SimulationConstants.Intake.INTAKE_WIDTH,
244                    0,
245                    0,
246                    0.5,
247                    0);
248        }
249    }
250
251    public synchronized void update() {
252        if (swerveMSim == null)
253            return;
254        fuel.set(arenaInstance.getGamePiecesArrayByType("Fuel"));
255        updateIntake();
256        updateHopperFuel();
257        double armEndX = getIntakeArmEndX();
258        intakePivot.set(getIntakePivotPose());
259        hopper.set(
260                SimulationConstants.Hopper.OFFSETS.applyToPose3d(
261                        new Pose3d(armEndX, 0, 0, new Rotation3d())));
262        // Translation2d outtakeTranslationRobotRelative =
263        // swerveMSim.getSimulatedDriveTrainPose().getTranslation().plus(
264        // SimulationConstants.Intake.OUTTAKE_OFFSETS.applyToPose3dRobotRelative(
265        // new Pose3d(getIntakeArmEndX(), 0, 0, new
266        // Rotation3d(swerveMSim.getSimulatedDriveTrainPose().getRotation()))).getTranslation().toTranslation2d());
267        // shooter.set(new Pose3d(tra.getX(), tra.getY(), 0, new Rotation3d()));
268        // shooter.set(SimulationConstants.Shooter.OFFSETS.applyToPose3dRobotRelative(new
269        // Pose3d(swerveMSim.getSimulatedDriveTrainPose())));
270    }
271}