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!