import React, { useState, useEffect, useRef } from 'react'; import { HeartPulse, Activity, AlertTriangle, Zap, Clock, Stethoscope, ChevronRight, ChevronLeft, FileText, Wind, ShieldAlert, Skull, CheckCircle2, Circle, Check } from 'lucide-react'; // --- SYNTHESIZED HIGH-FIDELITY AUDIO EFFECTS --- const getAudioCtx = () => { const AudioContext = window.AudioContext || window.webkitAudioContext; if (!AudioContext) return null; return new AudioContext(); }; const playPowerOnSound = () => { const ctx = getAudioCtx(); if (!ctx) return; const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(300, ctx.currentTime); osc.frequency.exponentialRampToValueAtTime(800, ctx.currentTime + 0.1); const gain = ctx.createGain(); gain.gain.setValueAtTime(0, ctx.currentTime); gain.gain.linearRampToValueAtTime(0.5, ctx.currentTime + 0.05); gain.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.3); osc.connect(gain); gain.connect(ctx.destination); osc.start(); osc.stop(ctx.currentTime + 0.3); }; const playPadsSound = () => { const ctx = getAudioCtx(); if (!ctx) return; // Wrapper Ripping Sound (Bandpass filtered white noise with rapid envelope) const bufferSize = ctx.sampleRate * 0.6; const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { data[i] = Math.random() * 2 - 1; // White noise } const noise = ctx.createBufferSource(); noise.buffer = buffer; const filter = ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.setValueAtTime(1200, ctx.currentTime); filter.Q.setValueAtTime(0.5, ctx.currentTime); const noiseGain = ctx.createGain(); noiseGain.gain.setValueAtTime(0, ctx.currentTime); // Simulate tearing: quick peaks and valleys noiseGain.gain.linearRampToValueAtTime(0.9, ctx.currentTime + 0.05); noiseGain.gain.exponentialRampToValueAtTime(0.3, ctx.currentTime + 0.15); noiseGain.gain.linearRampToValueAtTime(1.0, ctx.currentTime + 0.25); noiseGain.gain.exponentialRampToValueAtTime(0.1, ctx.currentTime + 0.4); noiseGain.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.6); noise.connect(filter); filter.connect(noiseGain); noiseGain.connect(ctx.destination); noise.start(); }; const playSyncSound = () => { const ctx = getAudioCtx(); if (!ctx) return; const playBeep = (time) => { const osc = ctx.createOscillator(); osc.type = 'square'; osc.frequency.setValueAtTime(1200, time); const gain = ctx.createGain(); gain.gain.setValueAtTime(0.05, time); gain.gain.exponentialRampToValueAtTime(0.01, time + 0.1); osc.connect(gain); gain.connect(ctx.destination); osc.start(time); osc.stop(time + 0.1); }; playBeep(ctx.currentTime); playBeep(ctx.currentTime + 0.15); }; const playSedationSound = () => { const ctx = getAudioCtx(); if (!ctx) return; const bufferSize = ctx.sampleRate * 0.5; const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1; const noise = ctx.createBufferSource(); noise.buffer = buffer; const filter = ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.setValueAtTime(400, ctx.currentTime); filter.frequency.exponentialRampToValueAtTime(100, ctx.currentTime + 0.5); const gain = ctx.createGain(); gain.gain.setValueAtTime(0.5, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5); noise.connect(filter); filter.connect(gain); gain.connect(ctx.destination); noise.start(); }; const playChargeSound = () => { const ctx = getAudioCtx(); if (!ctx) return; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(400, ctx.currentTime); osc.frequency.exponentialRampToValueAtTime(1200, ctx.currentTime + 2); gain.gain.setValueAtTime(0, ctx.currentTime); gain.gain.linearRampToValueAtTime(0.3, ctx.currentTime + 0.2); gain.gain.linearRampToValueAtTime(0.5, ctx.currentTime + 1.8); gain.gain.linearRampToValueAtTime(0, ctx.currentTime + 2.1); osc.connect(gain); gain.connect(ctx.destination); osc.start(); osc.stop(ctx.currentTime + 2.1); }; const playShockSound = () => { const ctx = getAudioCtx(); if (!ctx) return; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'square'; osc.frequency.setValueAtTime(150, ctx.currentTime); osc.frequency.exponentialRampToValueAtTime(20, ctx.currentTime + 0.3); gain.gain.setValueAtTime(1, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3); osc.connect(gain); gain.connect(ctx.destination); osc.start(); osc.stop(ctx.currentTime + 0.3); }; // --- DATA STRUCTURES & SCENARIO DECISION TREE --- const INITIAL_VITALS = { hr: 180, bpSys: 94, bpDia: 52, rr: 27, spo2: 96, etco2: '- -', rhythm: 'SVT', mentalStatus: 'Awake, complains of palpitations', pulse: true, syncMode: false }; const SCENARIO_STEPS = [ { id: 1, title: "Initial Rhythm Identification", prompt: "A 78-year-old male arrives at your ED complaining of palpitations & dizziness. HR 180, BP 94/52, RR 27, SpO2 96% on room air. You place him on the monitor. What rhythm do you see?", options: [ { text: "Normal Sinus Rhythm", isCorrect: false, feedback: "Incorrect. The rate is 180, which is far too fast for NSR." }, { text: "Supraventricular Tachycardia (SVT)", isCorrect: true, feedback: "Correct! The monitor shows SVT and the patient has unstable tachycardia." }, { text: "Ventricular Fibrillation", isCorrect: false, feedback: "Incorrect. VFib has no organized complexes and the patient would be unconscious." } ] }, { id: 2, title: "Initial Intervention", prompt: "You identify SVT. You have established 2 IVs and are preparing to help. What intervention are you preparing to do?", options: [ { text: "Administer Adenosine 6mg rapid IVP", isCorrect: false, feedback: "Incorrect. The patient is hypotensive and dizzy (unstable). Electricity is the priority." }, { text: "Synchronized Cardioversion", isCorrect: true, feedback: "Rationale: Synchronized cardioversion is the FIRST line of treatment for unstable tachycardia." }, { text: "Start CPR", isCorrect: false, feedback: "Incorrect. The patient still has a pulse and is breathing." } ] }, { id: 3, title: "Cardioversion Execution", prompt: "Arrange the correct steps to perform Synchronized Cardioversion in the proper order.", type: "sequence", correctSequence: [ { id: 'power', text: "Turn Defibrillator on", feedback: "Defibrillator powered on." }, { id: 'pads', text: "Attach the pads", feedback: "Pads attached to patient." }, { id: 'sync', text: "Set device to sync mode & Achieve Capture", feedback: "Sync mode activated. R-R capture achieved." }, { id: 'sedate', text: "Give IV sedation/Pain meds (if stable enough)", feedback: "IV Sedation administered." }, { id: 'shock', text: "Clear, Charge 150j, and Shock", feedback: "Clear! Shock delivered." } ], // Deliberately scrambled for the user to pick from options: [ { id: 'sedate', text: "Give IV sedation/Pain meds (if stable enough)" }, { id: 'pads', text: "Attach the pads" }, { id: 'shock', text: "Clear, Charge 150j, and Shock" }, { id: 'sync', text: "Set device to sync mode & Achieve Capture" }, { id: 'power', text: "Turn Defibrillator on" } ] }, { id: 4, title: "Post-Cardioversion Deterioration", prompt: "After cardioverting, the victim becomes unresponsive. You look at the monitor. What new rhythm do you see?", vitalsChange: { hr: 0, bpSys: 0, bpDia: 0, rr: 0, spo2: 0, etco2: '- -', rhythm: 'VFIB', mentalStatus: 'Unresponsive', pulse: false, syncMode: false }, options: [ { text: "Ventricular Fibrillation", isCorrect: true, feedback: "Correct. The patient has deteriorated into fine VFib." }, { text: "Asystole", isCorrect: false, feedback: "Incorrect. There is chaotic, low-amplitude electrical activity visible." }, { text: "Normal Sinus Rhythm", isCorrect: false, feedback: "Incorrect. The rhythm is highly disorganized." } ] }, { id: 5, title: "Cardiac Arrest Action", prompt: "The victim is not breathing and has no pulse. What action do you perform next?", options: [ { text: "Give Epinephrine 1mg", isCorrect: false, feedback: "Incorrect. Compressions take priority over medications." }, { text: "Intubate immediately", isCorrect: false, feedback: "Incorrect. Delaying compressions for an airway decreases survival chances." }, { text: "Begin High Quality CPR immediately", isCorrect: true, feedback: "Correct! You immediately start chest compressions." } ] }, { id: 6, title: "High Quality CPR", prompt: "Which of these options best describes High Quality CPR?", vitalsChange: { etco2: 15 }, // CPR capnography options: [ { text: "80 compressions/min, 15:2 ratio, 1 inch depth", isCorrect: false, feedback: "Incorrect. Too slow, wrong ratio, too shallow." }, { text: "100-120 compressions/min, 30:2 ratio, 2 inch depth, allowing for complete chest recoil", isCorrect: true, feedback: "Correct! This maximizes perfusion to the heart and brain." }, { text: "Continuous compressions with asynchronous ventilations every 2 seconds", isCorrect: false, feedback: "Incorrect until an advanced airway is placed." } ] }, { id: 7, title: "Defibrillation Preparation", prompt: "After starting CPR, which intervention do you prepare to do, and how do you do it?", options: [ { text: "Sync Mode -> Charge -> Shock -> Check Pulse", isCorrect: false, feedback: "Incorrect. Do not use sync mode for VFib, and do NOT check a pulse immediately after." }, { text: "Defibrillate (Defib mode, Charge, Clear & Shock) and immediately resume CPR", isCorrect: true, effect: 'SHOCK', feedback: "Correct! Unsynchronized shock delivered, compressions immediately resumed." }, { text: "Charge to 50J -> Shock -> Resume CPR", isCorrect: false, feedback: "Incorrect. 50J is too low for adult defibrillation." } ] }, { id: 8, title: "Two-Minute Rhythm Check", prompt: "It's been two minutes. Time for a pulse and rhythm check. The rhythm remains unchanged (VFib) and no pulse detected. What do you recommend?", options: [ { text: "Shock again, resume CPR, switch compressors at this time", isCorrect: true, effect: 'SHOCK', feedback: "Correct! Shock the shockable rhythm, rotate staff to maintain CPR quality." }, { text: "Check blood pressure", isCorrect: false, feedback: "Incorrect. The patient is pulseless." }, { text: "Give Atropine 1mg", isCorrect: false, feedback: "Incorrect. Atropine is not indicated in the pulseless arrest algorithm." } ] }, { id: 9, title: "First Medication", prompt: "What medication and dose do you suggest we give at this time?", options: [ { text: "Amiodarone 300mg IVP", isCorrect: false, feedback: "Incorrect. Epinephrine comes first." }, { text: "1mg of Epi IVP (Give 1 dose every 3-5min)", isCorrect: true, feedback: "Correct! Epinephrine is the primary vasopressor in arrest." }, { text: "Lidocaine 100mg IVP", isCorrect: false, feedback: "Incorrect. Epinephrine comes first." } ] }, { id: 10, title: "Antiarrhythmic Administration", prompt: "You prepare to give another medication for this refractory VFib. What OTHER medication do you prepare to give?", options: [ { text: "Amiodarone 300mg Bolus OR Lidocaine 1-1.5mg/kg", isCorrect: true, feedback: "Correct. These are the recommended antiarrhythmics for refractory VFib." }, { text: "Magnesium Sulfate 2g", isCorrect: false, feedback: "Incorrect. Mg is for Torsades de Pointes." }, { text: "Epinephrine 2mg", isCorrect: false, feedback: "Incorrect dose." } ] }, { id: 11, title: "Antiarrhythmic Second Dose", prompt: "If you have to give an additional dose of these medications later in the code, what is the correct medication dose?", options: [ { text: "Amiodarone 150mg Bolus OR Lidocaine 0.5-0.75mg/kg", isCorrect: true, feedback: "Correct. The second dose is halved." }, { text: "Amiodarone 300mg Bolus OR Lidocaine 1.5mg/kg", isCorrect: false, feedback: "Incorrect. This is the initial dose, not the secondary dose." }, { text: "You cannot give a second dose", isCorrect: false, feedback: "Incorrect. A second dose is allowed." } ] }, { id: 12, title: "Rhythm Change", prompt: "2 minutes pass. Rhythm check: The monitor shows Sinus Bradycardia (HR 40). Upon assessment, the victim has NO PULSE and is NOT breathing. What rhythm are you seeing?", vitalsChange: { rhythm: 'PEA', hr: 40 }, options: [ { text: "Symptomatic Bradycardia", isCorrect: false, feedback: "Incorrect. The patient has NO PULSE." }, { text: "PEA (Pulseless Electrical Activity)", isCorrect: true, feedback: "Correct. We intubate the victim, place them on waveform capnography, and consider H&Ts." }, { text: "Asystole", isCorrect: false, feedback: "Incorrect. Electrical activity is visible on the monitor." } ] }, { id: 13, title: "PEA Intervention", prompt: "The rhythm is PEA. What intervention do you do next?", options: [ { text: "Defibrillate at 200J", isCorrect: false, feedback: "FATAL ERROR. You cannot shock PEA." }, { text: "Resume CPR", isCorrect: true, feedback: "Correct! Immediate resumption of compressions is critical for PEA." }, { text: "Check pulse for 30 seconds", isCorrect: false, feedback: "Incorrect. Pulse checks should take no more than 10 seconds." } ] }, { id: 14, title: "PEA Medication", prompt: "While performing CPR for PEA, what other intervention can you perform?", options: [ { text: "Give Amiodarone or Lidocaine", isCorrect: false, feedback: "Incorrect. Antiarrhythmics are for shockable rhythms (VF/pVT)." }, { text: "Defibrillate", isCorrect: false, feedback: "Incorrect. PEA is non-shockable." }, { text: "Give 1mg of Epinephrine", isCorrect: true, feedback: "Correct! Epinephrine every 3-5 minutes is the drug of choice for PEA." } ] }, { id: 15, title: "ROSC & Capnography", prompt: "While performing CPR, there is a sudden spike in Waveform Capnography to 40mmHg. A pulse check determines the victim has a strong pulse. Monitor shows HR 70 with a prolonged PR interval. What rhythm is this?", vitalsChange: { rhythm: 'NSR_BLOCK', hr: 70, bpSys: 80, bpDia: 40, rr: 16, spo2: 99, etco2: 40, pulse: true }, options: [ { text: "Normal Sinus Rhythm with a 1st degree heart block", isCorrect: true, feedback: "Correct! HR 70 is a normal rate, and the prolonged PR interval indicates a 1st degree block. ROSC achieved." }, { text: "Complete Heart Block", isCorrect: false, feedback: "Incorrect. P waves are tracking with QRS complexes." }, { text: "Ventricular Tachycardia", isCorrect: false, feedback: "Incorrect. This is a narrow-complex, organized rhythm." } ] }, { id: 16, title: "Post-Cardiac Arrest Care", prompt: "Vitals: HR 70, BP 80/40, RR 16 (vented), SpO2 99%. What is your first step in post-cardiac arrest care?", options: [ { text: "Administer Amiodarone drip", isCorrect: false, feedback: "Incorrect. Managing hemodynamics (blood pressure) is the priority." }, { text: "Manage Blood Pressure", isCorrect: true, feedback: "Correct! Treating hypotension is the immediate next priority." }, { text: "Extubate the patient", isCorrect: false, feedback: "Incorrect. The patient requires airway protection." } ] }, { id: 17, title: "Blood Pressure Management", prompt: "Select all that apply for managing BP post-cardiac arrest:", type: "select-all", feedback: "Correct! First-line for post-arrest hypotension is fluid bolus, followed by titratable vasopressors like Levophed or Dopamine.", options: [ { id: 'fluids', text: "1-2L Lactated ringers or Normal Saline", isCorrect: true }, { id: 'amio', text: "Amiodarone 150mg over 10 minutes", isCorrect: false }, { id: 'pressors', text: "Dopamine @ 5-20mcg/kg or Levophed @ 0.1-0.5mcg/kg", isCorrect: true }, { id: 'atropine', text: "Atropine 1mg IVP", isCorrect: false } ] }, { id: 18, title: "Targeted Temperature Management", prompt: "Blood pressure is stabilized. The victim is still unable to follow commands and remains unresponsive. Which intervention do you recommend?", vitalsChange: { bpSys: 110, bpDia: 70 }, options: [ { text: "Wait and observe neurological status", isCorrect: false, feedback: "Incorrect. Active intervention is required." }, { text: "Targeted Temperature Management (32-36°C for at least 24 hours measured via esophageal/core thermometer)", isCorrect: true, feedback: "Correct! TTM is the only thing we can do to improve neurologic outcome post SCA." }, { text: "Induce pharmacological paralysis indefinitely", isCorrect: false, feedback: "Incorrect. TTM is the primary goal." } ] } ]; // --- MAIN APPLICATION --- export default function App() { const [gameState, setGameState] = useState('intro'); // intro, playing, handoff, debrief, gameover const [stepIndex, setStepIndex] = useState(0); const [sequenceProgress, setSequenceProgress] = useState(0); const [selectedMulti, setSelectedMulti] = useState([]); // Track options for select-all type questions const [vitals, setVitals] = useState(INITIAL_VITALS); const [score, setScore] = useState(1000); const [patientViability, setPatientViability] = useState(100); const [gameLog, setGameLog] = useState([]); const [elapsedSeconds, setElapsedSeconds] = useState(0); const [history, setHistory] = useState([]); // UI Effects State const [damageFlash, setDamageFlash] = useState(false); const [shockFlash, setShockFlash] = useState(false); const [isCharging, setIsCharging] = useState(false); const [feedbackMsg, setFeedbackMsg] = useState(null); const currentStepData = SCENARIO_STEPS[stepIndex]; // CSS for visual shock effect useEffect(() => { const style = document.createElement('style'); style.innerHTML = ` @keyframes violent-shake { 0% { transform: translate(1px, 1px) rotate(0deg); } 10% { transform: translate(-10px, -12px) rotate(-1deg); } 20% { transform: translate(-15px, 0px) rotate(1deg); } 30% { transform: translate(15px, 12px) rotate(0deg); } 40% { transform: translate(10px, -10px) rotate(1deg); } 50% { transform: translate(-10px, 10px) rotate(-1deg); } 60% { transform: translate(-15px, 5px) rotate(0deg); } 70% { transform: translate(15px, 1px) rotate(-1deg); } 80% { transform: translate(-5px, -5px) rotate(1deg); } 90% { transform: translate(10px, 10px) rotate(0deg); } 100% { transform: translate(1px, -2px) rotate(-1deg); } } .animate-shock { animation: violent-shake 0.3s cubic-bezier(.36,.07,.19,.97) both; } .bg-shock-flash { background-color: #ffffff !important; } .ecg-grid { background-image: linear-gradient(rgba(0, 50, 0, 0.2) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 50, 0, 0.2) 1px, transparent 1px), linear-gradient(rgba(0, 100, 0, 0.3) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 100, 0, 0.3) 1px, transparent 1px); background-size: 10px 10px, 10px 10px, 50px 50px, 50px 50px; background-position: -1px -1px, -1px -1px, -1px -1px, -1px -1px; } .sequence-btn-completed { opacity: 0.5; pointer-events: none; background-color: #064e3b !important; border-color: #059669 !important; color: #6ee7b7 !important; } `; document.head.appendChild(style); return () => document.head.removeChild(style); }, []); // Global Timer useEffect(() => { let timer; if (gameState === 'playing') { timer = setInterval(() => setElapsedSeconds(prev => prev + 1), 1000); } return () => clearInterval(timer); }, [gameState]); const addLog = (msg, type = 'info') => { setGameLog(prev => [{ time: formatTime(elapsedSeconds), msg, type }, ...prev]); }; const handleStart = () => { setGameState('playing'); setStepIndex(0); setSequenceProgress(0); setSelectedMulti([]); setVitals(INITIAL_VITALS); setScore(1000); setPatientViability(100); setElapsedSeconds(0); setGameLog([]); setHistory([]); addLog("Scenario Started. Code Team Leader Active.", "system"); addLog("Pt presents with palpitations. Monitor attached.", "info"); }; const triggerDamage = () => { setDamageFlash(true); setTimeout(() => setDamageFlash(false), 300); }; const triggerAudioVisualShock = async (isSync) => { setIsCharging(true); addLog("Charging defibrillator...", "warning"); playChargeSound(); await new Promise(r => setTimeout(r, 2000)); setIsCharging(false); playShockSound(); setShockFlash(true); addLog(isSync ? "CLEAR! Synchronized Shock Delivered!" : "CLEAR! Unsynchronized Shock Delivered!", "warning"); setTimeout(() => setShockFlash(false), 300); await new Promise(r => setTimeout(r, 1000)); // Pause for dramatic effect }; const applyDamage = () => { setScore(prev => Math.max(0, prev - 150)); triggerDamage(); const damage = 20; setPatientViability(prev => { const next = Math.max(0, prev - damage); if (next <= 0) { setTimeout(() => setGameState('gameover'), 1500); addLog("FATAL ERROR. PATIENT LOST.", "error"); } else { addLog(`Patient viability dropped by ${damage}%!`, "error"); } return next; }); if (vitals.bpSys > 60 && vitals.pulse) { setVitals(prev => ({...prev, bpSys: prev.bpSys - 10, bpDia: prev.bpDia - 5, spo2: prev.spo2 - 2})); } }; const handleBack = () => { if (history.length === 0 || isCharging) return; const lastState = history[history.length - 1]; setStepIndex(lastState.stepIndex); setSequenceProgress(lastState.sequenceProgress); setVitals(lastState.vitals); setScore(lastState.score); setPatientViability(lastState.patientViability); setSelectedMulti([]); // Reset selections on back setGameLog(prev => { const diff = prev.length - lastState.logLength; return diff > 0 ? prev.slice(diff) : prev; }); setHistory(prev => prev.slice(0, -1)); setFeedbackMsg(null); }; // Standard multiple choice action const handleAction = async (option) => { if (isCharging) return; setHistory(prev => [...prev, { stepIndex, sequenceProgress, vitals: { ...vitals }, score, patientViability, logLength: gameLog.length }]); if (option.isCorrect) { setFeedbackMsg({ text: option.feedback, type: 'success' }); addLog(`Selected: ${option.text}`, 'success'); if (option.effect === 'CARDIOVERT') { setVitals(prev => ({ ...prev, syncMode: true })); await triggerAudioVisualShock(true); } else if (option.effect === 'SHOCK') { await triggerAudioVisualShock(false); } advanceStep(); } else { setFeedbackMsg({ text: option.feedback, type: 'error' }); addLog(`Selected: ${option.text}`, 'error'); addLog(option.feedback, 'error'); applyDamage(); } setTimeout(() => setFeedbackMsg(null), 5000); }; // Sequence "Arrange in Order" action const handleSequenceAction = async (option) => { if (isCharging) return; setHistory(prev => [...prev, { stepIndex, sequenceProgress, vitals: { ...vitals }, score, patientViability, logLength: gameLog.length }]); const currentTarget = currentStepData.correctSequence[sequenceProgress]; if (option.id === currentTarget.id) { setFeedbackMsg({ text: currentTarget.feedback, type: 'success' }); addLog(`Executed: ${currentTarget.text}`, 'success'); if (option.id === 'power') playPowerOnSound(); if (option.id === 'pads') playPadsSound(); if (option.id === 'sedate') playSedationSound(); if (option.id === 'sync') { playSyncSound(); setVitals(prev => ({...prev, syncMode: true})); } if (option.id === 'shock') { await triggerAudioVisualShock(true); } if (sequenceProgress + 1 >= currentStepData.correctSequence.length) { setTimeout(() => { setSequenceProgress(0); advanceStep(); }, 1000); } else { setSequenceProgress(prev => prev + 1); } } else { setFeedbackMsg({ text: "Incorrect action. You must follow the exact proper cardioversion order.", type: 'error' }); addLog(`Failed step: ${option.text} (Wrong Order)`, 'error'); applyDamage(); } setTimeout(() => setFeedbackMsg(null), 5000); }; // Select All That Apply action const handleSataSubmit = () => { if (isCharging) return; setHistory(prev => [...prev, { stepIndex, sequenceProgress, vitals: { ...vitals }, score, patientViability, logLength: gameLog.length }]); const correctIds = currentStepData.options.filter(o => o.isCorrect).map(o => o.id); const isPerfectMatch = correctIds.every(id => selectedMulti.includes(id)) && selectedMulti.every(id => correctIds.includes(id)); if (isPerfectMatch) { setFeedbackMsg({ text: currentStepData.feedback, type: 'success' }); addLog(`Successfully identified proper BP Management protocols.`, 'success'); advanceStep(); } else { setFeedbackMsg({ text: "Incorrect. Please review the proper ACLS post-arrest pressors and fluids.", type: 'error' }); addLog(`Failed SATA: Selected incorrect combination for BP management.`, 'error'); applyDamage(); } setTimeout(() => setFeedbackMsg(null), 5000); }; const advanceStep = () => { setSelectedMulti([]); // Reset SATA selections if (stepIndex < SCENARIO_STEPS.length - 1) { const nextStep = SCENARIO_STEPS[stepIndex + 1]; if (nextStep.vitalsChange) { setVitals(prev => ({ ...prev, ...nextStep.vitalsChange })); addLog("Patient condition changed.", "warning"); } setStepIndex(stepIndex + 1); } else { setGameState('handoff'); addLog("Scenario Completed Successfully.", "success"); } }; const formatTime = (totalSeconds) => { const m = Math.floor(totalSeconds / 60).toString().padStart(2, '0'); const s = (totalSeconds % 60).toString().padStart(2, '0'); return `${m}:${s}`; }; // --- UI SCREENS --- const renderIntro = () => (

ACLS Megacode RPG

High-Fidelity Code Simulation

Scenario Briefing

A 78-year-old male arrives at your Emergency Department complaining of palpitations and dizziness. Initial Assessment Reveals: HR 180, BP 94/52, RR 27, SpO2 96% on RA.

Game Rules: You are the Team Leader. Incorrect decisions will drop the Patient Viability. If it hits 0%, the patient expires.

WARNING

Turn on your audio. This simulation features generated sound effects for defibrillator charging and shocks. Based on AHA ACLS guidelines.
); const renderGameOver = () => (

CODE CALLED

Patient Viability reached 0% due to critical medical errors.

Post-Mortem Review

In ACLS, following the algorithm strictly is the difference between life and death. Shocking non-shockable rhythms, delaying high-quality CPR, or administering incorrect medications rapidly depletes the patient's chance of survival.

Final Score: {score} / 1000

{history.length > 0 && ( )}
); const renderHandoff = () => (

SBAR Handoff

Situation & Background

78yo M presented with palpitations/dizziness in SVT. Cardioverted, but deteriorated to VFib arrest. Shocked, received Epi/Amiodarone. Transitioned to PEA, then achieved ROSC.

Assessment

ROSC confirmed. Current rhythm: NSR with 1st degree block. BP stabilized on fluids/pressors. Patient remains unresponsive.

Recommendation (Plan of Care)

  • Initiate Targeted Temperature Management (32-36°C for 24h via esophageal thermometer).
  • Obtain stat 12-lead EKG searching for cardiac ischemia/infarction.
  • Chest X-ray, CT, Labs, EEG.
  • Cath Lab consult for possible Percutaneous Coronary Intervention (PCI).
  • Admit to ICU.
); const renderDebrief = () => (

Megacode Complete

{score} / 1000

Congrats you have passed The ACLS Megacode course!

); return (
{gameState === 'intro' && renderIntro()} {gameState === 'handoff' && renderHandoff()} {gameState === 'debrief' && renderDebrief()} {gameState === 'gameover' && renderGameOver()} {gameState === 'playing' && (
{/* Top HUD */}
{isCharging && (
CHARGING DEFIBRILLATOR... STAND CLEAR
)}

ACLS Simulator

Pt: 78yo M

{/* Viability RPG Bar */}
Patient Viability 50 ? 'text-green-400' : 'text-red-500'}>{patientViability}%
50 ? 'bg-green-500' : patientViability > 20 ? 'bg-yellow-500' : 'bg-red-600 animate-pulse'}`} style={{ width: `${patientViability}%` }} >

Score

{score}

Elapsed Time

{formatTime(elapsedSeconds)}

{/* LEFT COLUMN: Monitor & Vitals */}
{/* Animated ECG with Authentic Grid */}
ECG Lead II
{vitals.syncMode &&
SYNC ON
}
{/* Vitals Grid */}
150 || vitals.hr === 0} /> 0} /> 0} /> 24 || vitals.rr === 0} /> } />
{/* Status Board */}
{patientViability <= 30 &&
}

Clinical Assessment

Mental Status:
{vitals.mentalStatus}
Central Pulse:
{vitals.pulse ? 'PALPABLE' : 'ABSENT - ARREST'}
{/* RIGHT COLUMN: RPG Action Menu & Logs */}
{/* Action Menu */}
Slide {currentStepData.id} of {SCENARIO_STEPS.length}

{currentStepData.title}

{history.length > 0 && ( )}

{currentStepData.prompt}

{currentStepData.type === 'sequence' ? ( // --- RENDER SEQUENCE MINI-GAME ---
{/* Completed Steps */}
{currentStepData.correctSequence.map((step, idx) => { const isCompleted = idx < sequenceProgress; if (!isCompleted) return null; return (
{step.text}
); })}
{/* Available Steps */}
Select next step:
{currentStepData.options.map((opt, i) => { const isCompleted = currentStepData.correctSequence.findIndex(s => s.id === opt.id) < sequenceProgress; if (isCompleted) return null; return ( ); })}
) : currentStepData.type === 'select-all' ? ( // --- RENDER SELECT ALL THAT APPLY ---
Select all that apply:
{currentStepData.options.map((opt) => { const isSelected = selectedMulti.includes(opt.id); return ( ); })}
) : ( // --- RENDER STANDARD MULTIPLE CHOICE ---
{currentStepData.options.map((opt, i) => ( ))}
)} {feedbackMsg && (
{feedbackMsg.text}
)}
{/* Combat Log */}

Action Log

{gameLog.map((log, i) => (
[{log.time}] {log.msg}
))}
)}
); } // --- SUBCOMPONENTS --- const VitalBox = ({ label, value, unit, color, warning, icon }) => (
{warning &&
}
{label} {icon}
{value}
{unit}
); // --- PROCEDURAL ECG CANVAS GENERATOR --- const ECGMonitor = ({ rhythm, syncMode }) => { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); let animationFrameId; const resize = () => { canvas.width = canvas.parentElement.clientWidth; canvas.height = canvas.parentElement.clientHeight; }; window.addEventListener('resize', resize); resize(); let x = 0; let y = canvas.height / 2; const baseline = canvas.height / 2; const speed = 2.5; let phase = 0; ctx.clearRect(0, 0, canvas.width, canvas.height); const generateWavePoint = () => { let dy = 0; let isRWavePeak = false; switch (rhythm) { case 'SVT': phase = (phase + 1) % 40; if (phase === 0) dy = -30; else if (phase === 2) { dy = 90; isRWavePeak = true; } else if (phase === 4) dy = -20; else if (phase > 10 && phase < 20) dy = Math.sin((phase-10)*Math.PI/10) * 15; break; case 'VFIB': // Fine VFib: reduced amplitude, higher phase shifts for chaos if (Math.random() > 0.1) { phase += Math.random() * 0.7; dy = Math.sin(phase) * (8 + Math.random() * 12) + (Math.random() - 0.5) * 6; } break; case 'PEA': phase = (phase + 1) % 150; if (phase > 10 && phase < 25) dy = Math.sin((phase-10)*Math.PI/15) * 8; else if (phase === 40) dy = -10; else if (phase === 43) { dy = 50; isRWavePeak = true; } else if (phase === 46) dy = -15; else if (phase > 70 && phase < 100) dy = Math.sin((phase-70)*Math.PI/30) * 12; break; case 'NSR_BLOCK': phase = (phase + 1) % 85; if (phase > 5 && phase < 15) dy = Math.sin((phase-5)*Math.PI/10) * 10; else if (phase === 35) dy = -15; else if (phase === 37) { dy = 70; isRWavePeak = true; } else if (phase === 39) dy = -10; else if (phase > 50 && phase < 70) dy = Math.sin((phase-50)*Math.PI/20) * 15; break; default: dy = (Math.random() - 0.5) * 2; } let noise = (Math.random() - 0.5) * 3; return { y: baseline - dy + noise, isRWavePeak }; }; const renderLoop = () => { ctx.clearRect(x, 0, 20, canvas.height); const waveData = generateWavePoint(); const nextY = waveData.y; ctx.beginPath(); ctx.shadowBlur = 5; ctx.shadowColor = '#22c55e'; ctx.strokeStyle = '#22c55e'; ctx.lineWidth = 2.5; ctx.lineJoin = 'round'; ctx.moveTo(x - speed, y); ctx.lineTo(x, nextY); ctx.stroke(); if (syncMode && waveData.isRWavePeak) { ctx.shadowBlur = 8; ctx.shadowColor = '#facc15'; ctx.beginPath(); ctx.strokeStyle = '#facc15'; ctx.lineWidth = 2; ctx.moveTo(x, nextY - 15); ctx.lineTo(x, nextY + 15); ctx.stroke(); ctx.fillStyle = '#facc15'; ctx.beginPath(); ctx.arc(x, nextY - 20, 3, 0, Math.PI * 2); ctx.fill(); } ctx.shadowBlur = 0; y = nextY; x += speed; if (x > canvas.width) { x = 0; } animationFrameId = requestAnimationFrame(renderLoop); }; renderLoop(); return () => { window.removeEventListener('resize', resize); cancelAnimationFrame(animationFrameId); }; }, [rhythm, syncMode]); return ; };