import React, { useState, useEffect, useRef } from 'react'; // Import actual PNG images import llamaSprite from '../../assets/battle-game/llama.png'; import gooseSprite from '../../assets/battle-game/goose.png'; import battleBackground from '../../assets/battle-game/background.png'; import battleMusic from '../../assets/battle-game/battle.mp3'; interface BattleState { currentStep: number; gooseHp: number; llamaHp: number; message: string; animation: string | null; lastChoice?: string; showHostInput?: boolean; processingAction?: boolean; } interface OllamaBattleGameProps { onComplete: (configValues: { [key: string]: string }) => void; requiredKeys: string[]; } export function OllamaBattleGame({ onComplete, _requiredKeys }: OllamaBattleGameProps) { // Use type assertion for audioRef to avoid DOM lib dependency const audioRef = useRef(null); const [isMuted, setIsMuted] = useState(false); const [battleState, setBattleState] = useState({ currentStep: 0, gooseHp: 100, llamaHp: 100, message: 'A wild Ollama appeared!', animation: null, processingAction: false, }); const [configValues, setConfigValues] = useState<{ [key: string]: string }>({}); // Initialize audio when component mounts useEffect(() => { if (typeof window !== 'undefined') { audioRef.current = new window.Audio(battleMusic); audioRef.current.loop = true; audioRef.current.volume = 0.2; audioRef.current.play().catch((e) => console.log('Audio autoplay prevented:', e)); } return () => { if (audioRef.current) { audioRef.current.pause(); audioRef.current = null; } }; }, []); const toggleMute = () => { if (audioRef.current) { if (isMuted) { audioRef.current.volume = 0.2; } else { audioRef.current.volume = 0; } setIsMuted(!isMuted); } }; const battleSteps = [ { message: 'A wild Ollama appeared!', action: null, animation: 'appear', }, { message: 'What will GOOSE do?', action: 'choice', choices: ['Pacify', 'HONK!'], animation: 'attack', followUpMessages: ["It's not very effective...", 'But OLLAMA is confused!'], }, { message: 'OLLAMA used YAML Confusion!', action: null, animation: 'counter', followUpMessages: ['OLLAMA hurt itself in confusion!', 'GOOSE maintained composure!'], }, { message: 'What will GOOSE do?', action: 'final_choice', choices: (previousChoice: string) => [ previousChoice === 'Pacify' ? 'HONK!' : 'Pacify', 'Configure Host', ], animation: 'attack', }, { message: 'OLLAMA used Docker Dependency!', action: null, animation: 'counter', followUpMessages: ["It's not very effective...", 'GOOSE knows containerization!'], }, { message: 'What will GOOSE do?', action: 'host_choice', choices: ['Configure Host'], animation: 'finish', }, { message: '', // Will be set dynamically based on choice action: 'host_input', prompt: 'Enter your Ollama host address:', configKey: 'OLLAMA_HOST', animation: 'finish', followUpMessages: [ "It's super effective!", 'OLLAMA has been configured!', 'OLLAMA joined your team!', ], }, { message: 'Configuration complete!\nOLLAMA will remember this friendship!', action: 'complete', }, ]; const animateHit = (isLlama: boolean) => { const element = document.querySelector(isLlama ? '.llama-sprite' : '.goose-sprite'); if (element) { element.classList.add('hit-flash'); setTimeout(() => { element.classList.remove('hit-flash'); }, 500); } }; useEffect(() => { // Add CSS for the hit animation and defeat animation const style = document.createElement('style'); style.textContent = ` @keyframes hitFlash { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } .hit-flash { animation: hitFlash 0.5s; } @keyframes defeat { 0% { transform: translateY(0); opacity: 1; } 20% { transform: translateY(-30px); opacity: 1; } 100% { transform: translateY(500px); opacity: 0; } } .defeated { animation: defeat 1.3s cubic-bezier(.36,.07,.19,.97) both; } `; document.head.appendChild(style); return () => { document.head.removeChild(style); }; }, []); const handleAction = async (value: string) => { const currentStep = battleState.currentStep < battleSteps.length ? battleSteps[battleState.currentStep] : null; if (!currentStep) return; // Handle host input if (currentStep.action === 'host_input' && value) { setConfigValues((prev) => ({ ...prev, [currentStep.configKey]: value, })); return; } // Handle host submit if (currentStep.action === 'host_input' && !value) { setBattleState((prev) => ({ ...prev, processingAction: true, llamaHp: 0, message: "It's super effective!", })); animateHit(true); // Add defeat class to llama sprite and health bar const llamaContainer = document.querySelector('.llama-container'); if (llamaContainer) { await new Promise((resolve) => setTimeout(resolve, 500)); llamaContainer.classList.add('defeated'); } // Show victory messages with delays if (currentStep.followUpMessages) { for (const msg of currentStep.followUpMessages) { await new Promise((resolve) => setTimeout(resolve, 1000)); setBattleState((prev) => ({ ...prev, message: msg })); } } await new Promise((resolve) => setTimeout(resolve, 1000)); onComplete(configValues); return; } // Handle continue button for messages if (!currentStep.action) { setBattleState((prev) => ({ ...prev, currentStep: prev.currentStep + 1, message: battleSteps[prev.currentStep + 1]?.message || prev.message, processingAction: false, })); return; } // Handle choices (Pacify/HONK/Configure Host) if ( (currentStep.action === 'choice' || currentStep.action === 'final_choice' || currentStep.action === 'host_choice') && value ) { // Set processing flag to hide buttons setBattleState((prev) => ({ ...prev, processingAction: true, })); if (value === 'Configure Host') { setBattleState((prev) => ({ ...prev, message: 'GOOSE used Configure Host!', showHostInput: true, currentStep: battleSteps.findIndex((step) => step.action === 'host_input'), processingAction: false, })); return; } // Handle Pacify or HONK attacks setBattleState((prev) => ({ ...prev, lastChoice: value, llamaHp: Math.max(0, prev.llamaHp - 25), message: `GOOSE used ${value}!`, })); animateHit(true); // Show follow-up messages if (currentStep.followUpMessages) { for (const msg of currentStep.followUpMessages) { await new Promise((resolve) => setTimeout(resolve, 1000)); setBattleState((prev) => ({ ...prev, message: msg })); } } // Proceed to counter-attack await new Promise((resolve) => setTimeout(resolve, 1000)); const isFirstCycle = currentStep.action === 'choice'; const nextStep = battleSteps[battleState.currentStep + 1]; setBattleState((prev) => ({ ...prev, gooseHp: Math.max(0, prev.gooseHp - 25), message: isFirstCycle ? 'OLLAMA used YAML Confusion!' : 'OLLAMA used Docker Dependency!', currentStep: prev.currentStep + 1, processingAction: false, })); animateHit(false); // Show counter-attack messages if (nextStep?.followUpMessages) { await new Promise((resolve) => setTimeout(resolve, 1000)); for (const msg of nextStep.followUpMessages) { await new Promise((resolve) => setTimeout(resolve, 1000)); setBattleState((prev) => ({ ...prev, message: msg })); } } return; } // Check for battle completion if (battleState.currentStep === battleSteps.length - 2) { onComplete(configValues); } }; return (
{/* Battle Scene */}
{/* Llama sprite */}
OLLAMA Lv.1
50 ? '#10B981' : battleState.llamaHp > 20 ? '#F59E0B' : '#EF4444', }} />
{Math.floor(battleState.llamaHp)}/100
Llama
{/* Goose sprite */}
Goose
GOOSE Lv.99
50 ? '#10B981' : battleState.gooseHp > 20 ? '#F59E0B' : '#EF4444', }} />
{Math.floor(battleState.gooseHp)}/100
{/* Dialog Box */}

{battleState.message}

{battleState.currentStep < battleSteps.length && (
{/* Show battle choices */} {(battleSteps[battleState.currentStep].action === 'choice' || battleSteps[battleState.currentStep].action === 'final_choice' || battleSteps[battleState.currentStep].action === 'host_choice') && !battleState.showHostInput && !battleState.processingAction && (
{(typeof battleSteps[battleState.currentStep].choices === 'function' ? battleSteps[battleState.currentStep].choices(battleState.lastChoice || '') : battleSteps[battleState.currentStep].choices )?.map((choice: string) => ( ))}
)} {/* Show host input when needed */} {battleState.showHostInput && !battleState.processingAction && (

Enter your Ollama host address:

handleAction(e.target.value)} />
)} {/* Continue button for messages */} {!battleSteps[battleState.currentStep].action && !battleState.processingAction && ( )}
)}
{/* Black corners for that classic Pokemon feel */}
); }