// Resonancia Rioplatense - Game Engine // Rewritten with robust event handling and debugging let game; // Initialize when DOM is loaded document.addEventListener('DOMContentLoaded', function() { console.log('DOM loaded, initializing game...'); game = new GameEngine(); }); class GameEngine { constructor() { this.gameState = 'landing'; this.player = null; this.resonanceSystem = new ResonanceSystem(); this.narrativeEngine = new NarrativeEngine(); this.eventSystem = new EventSystem(); console.log('GameEngine constructor called'); this.init(); } init() { console.log('Initializing game...'); this.setupEventListeners(); this.loadGameData(); this.startGameLoop(); console.log('Game initialized'); } setupEventListeners() { console.log('Setting up event listeners...'); // Use setTimeout to ensure DOM is ready setTimeout(() => { // Landing page button const startButton = document.getElementById('start-game'); console.log('Start button:', startButton); if (startButton) { startButton.onclick = () => { console.log('Start button clicked'); this.transitionToScreen('character-creation'); }; } // Character creation button const createButton = document.getElementById('create-character'); console.log('Create button:', createButton); if (createButton) { createButton.onclick = () => { console.log('Create character button clicked'); this.createCharacter(); }; } // Decision buttons - use event delegation document.onclick = (e) => { if (e.target.classList.contains('decision-btn')) { console.log('Decision button clicked:', e.target.dataset.decision); const decision = e.target.dataset.decision; this.processDecision(decision); } // Event joining if (e.target.classList.contains('event-join')) { console.log('Event join button clicked'); const eventElement = e.target.closest('.event-item'); const eventName = eventElement.querySelector('h5').textContent; this.joinEvent(eventName); } // Modal controls if (e.target.classList.contains('modal-close')) { console.log('Modal close clicked'); this.closeModal(); } if (e.target.id === 'continue-game') { console.log('Continue game clicked'); this.closeModal(); this.generateNextScenario(); } // NPC interactions if (e.target.closest('.npc-item')) { console.log('NPC clicked'); const npcElement = e.target.closest('.npc-item'); const npcId = npcElement.dataset.npc; this.showNPCInfo(npcId); } }; }, 100); } transitionToScreen(screenId) { console.log('Transitioning to screen:', screenId); // Hide all screens const screens = document.querySelectorAll('.screen'); screens.forEach(screen => { screen.classList.remove('active'); }); // Show target screen const targetScreen = document.getElementById(screenId); if (targetScreen) { targetScreen.classList.add('active'); this.gameState = screenId; console.log('Successfully transitioned to:', screenId); } else { console.error('Screen not found:', screenId); } } createCharacter() { console.log('Creating character...'); const nameInput = document.getElementById('player-name'); const techSelect = document.getElementById('tech-background'); const culturalRadio = document.querySelector('input[name="cultural-preference"]:checked'); const socialSelect = document.getElementById('social-style'); const name = nameInput ? nameInput.value || 'Jugador' : 'Jugador'; const techBackground = techSelect ? techSelect.value : 'frontend'; const culturalPreference = culturalRadio ? culturalRadio.value : 'mixed'; const socialStyle = socialSelect ? socialSelect.value : 'adaptable'; console.log('Character data:', { name, techBackground, culturalPreference, socialStyle }); this.player = new Player({ name, techBackground, culturalPreference, socialStyle }); this.updatePlayerDisplay(); this.transitionToScreen('game-interface'); // Give some time for the transition, then generate scenario setTimeout(() => { this.generateNextScenario(); }, 500); } updatePlayerDisplay() { if (!this.player) return; console.log('Updating player display'); const nameElement = document.getElementById('player-display-name'); const roleElement = document.getElementById('player-role'); if (nameElement) nameElement.textContent = this.player.name; if (roleElement) roleElement.textContent = this.getRoleDescription(this.player.techBackground); this.updateStats(); this.updateNPCRelationships(); this.updateResonanceDisplay(); } updateStats() { if (!this.player) return; const stats = ['carrera', 'cultura', 'social', 'bienestar']; stats.forEach(stat => { const value = this.player.stats[stat]; const fillElement = document.getElementById(`${stat}-fill`); const valueElement = document.getElementById(`${stat}-value`); if (fillElement && valueElement) { fillElement.style.width = `${value}%`; valueElement.textContent = value; } }); } updateNPCRelationships() { if (!this.player) return; Object.entries(this.player.relationships).forEach(([npcId, relationship]) => { const npcElement = document.querySelector(`[data-npc="${npcId}"]`); if (npcElement) { const fillElement = npcElement.querySelector('.relationship-fill'); if (fillElement) { fillElement.style.width = `${relationship}%`; } } }); } updateResonanceDisplay() { const resonanceWaves = document.querySelectorAll('.resonance-wave'); const activeWaves = Math.min(this.resonanceSystem.getResonanceLevel(), resonanceWaves.length); resonanceWaves.forEach((wave, index) => { if (index < activeWaves) { wave.classList.add('active'); } else { wave.classList.remove('active'); } }); const resonanceText = document.querySelector('.resonance-text'); if (resonanceText) { resonanceText.textContent = this.resonanceSystem.getResonanceDescription(); } } processDecision(decisionKey) { console.log('Processing decision:', decisionKey); const currentScenario = this.narrativeEngine.getCurrentScenario(); if (!currentScenario) { console.log('No current scenario'); return; } const decision = currentScenario.decisions.find(d => d.key === decisionKey); if (!decision) { console.log('Decision not found:', decisionKey); return; } console.log('Found decision:', decision); // Apply stat changes if (decision.effects) { Object.entries(decision.effects).forEach(([stat, change]) => { this.player.changeStat(stat, change); }); } // Update NPC relationships if (decision.relationshipChanges) { Object.entries(decision.relationshipChanges).forEach(([npcId, change]) => { this.player.changeRelationship(npcId, change); }); } // Add to resonance system this.resonanceSystem.addDecision(decision); // Show resonance visualization this.showResonanceEffect(decision); // Update displays this.updatePlayerDisplay(); // Generate next scenario after a delay setTimeout(() => { this.generateNextScenario(); }, 3000); } showResonanceEffect(decision) { console.log('Showing resonance effect'); const modal = document.getElementById('resonance-modal'); if (modal) { modal.classList.add('active'); this.drawResonanceVisualization(decision); // Auto-close after 2 seconds setTimeout(() => { this.closeModal(); }, 2000); } } drawResonanceVisualization(decision) { const canvas = document.getElementById('resonance-canvas'); if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); const centerX = canvas.width / 2; const centerY = canvas.height / 2; // Draw decision impact as expanding circles let radius = 10; const maxRadius = 150; const animate = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw multiple resonance waves for (let i = 0; i < 3; i++) { const waveRadius = radius - (i * 30); if (waveRadius > 0) { ctx.beginPath(); ctx.arc(centerX, centerY, waveRadius, 0, 2 * Math.PI); ctx.strokeStyle = `rgba(33, 128, 141, ${0.8 - (waveRadius / maxRadius)})`; ctx.lineWidth = 2; ctx.stroke(); } } radius += 2; if (radius < maxRadius) { requestAnimationFrame(animate); } }; animate(); } generateNextScenario() { console.log('Generating next scenario'); const scenario = this.narrativeEngine.generateScenario(this.player, this.resonanceSystem); this.displayScenario(scenario); this.updateAvailableEvents(); } displayScenario(scenario) { console.log('Displaying scenario:', scenario); const locationElement = document.getElementById('current-location'); const descriptionElement = document.getElementById('location-description'); const storyElement = document.getElementById('story-content'); const decisionsContainer = document.getElementById('decisions-container'); if (locationElement) locationElement.textContent = scenario.location.name; if (descriptionElement) descriptionElement.textContent = scenario.location.description; if (storyElement) storyElement.innerHTML = `

${scenario.narrative}

`; if (decisionsContainer) { decisionsContainer.innerHTML = `

${scenario.prompt}

${scenario.decisions.map(decision => ` `).join('')}
`; } } formatDecisionImpact(effects) { if (!effects) return ''; return Object.entries(effects) .map(([stat, change]) => { const prefix = change > 0 ? '+' : ''; const statName = stat.charAt(0).toUpperCase() + stat.slice(1); return `${prefix}${statName}`; }) .join(' '); } updateAvailableEvents() { const eventsList = document.getElementById('events-list'); if (!eventsList || !this.player) return; const availableEvents = this.eventSystem.getAvailableEvents(this.player); eventsList.innerHTML = availableEvents.map(event => `
${event.name}

${event.description}

${event.available ? `${event.date} ` : `${event.requirement}` }
`).join(''); } joinEvent(eventName) { console.log('Joining event:', eventName); const result = this.eventSystem.joinEvent(eventName, this.player); if (result) { this.showEventResult(result); this.resonanceSystem.addEvent(result); this.updatePlayerDisplay(); } } showEventResult(result) { const modal = document.getElementById('event-modal'); const title = document.getElementById('event-title'); const content = document.getElementById('event-result-content'); if (title) title.textContent = result.eventName; if (content) { content.innerHTML = `

${result.narrative}

Resultados:

${result.newConnections ? `

Nuevas Conexiones:

${result.newConnections}

` : ''}
`; } if (modal) modal.classList.add('active'); } closeModal() { document.querySelectorAll('.modal').forEach(modal => { modal.classList.remove('active'); }); } showNPCInfo(npcId) { const npcData = { sofia: "Sofía es una frontend developer especializada en React. Viene de trabajar en San Francisco y lidera el tech team.", mateo: "Mateo es product manager, ex-consultor de BCG obsesionado con métricas y user acquisition.", luna: "Luna es artista digital freelance que organiza eventos culturales alternativos en la escena underground." }; const info = npcData[npcId] || "Información no disponible"; alert(info); } getRoleDescription(techBackground) { const roles = { frontend: 'Frontend Developer', backend: 'Backend Developer', fullstack: 'Full Stack Developer', product: 'Product Manager', design: 'UX/UI Designer', data: 'Data Scientist' }; return roles[techBackground] || 'Desarrollador'; } loadGameData() { this.gameData = { locations: [ { name: "Vicente López", description: "Hub tech moderno con startups y coworking spaces", events: ["North Valley Meetup", "Startup Showcase", "After Office Tech"] }, { name: "San Isidro", description: "Zona cultural con venues indies y espacios de arte", events: ["Showcase Indie", "Exhibición Arte Digital", "Centro Cultural"] }, { name: "Olivos", description: "Barrio residencial con cafeterías y espacios de networking", events: ["Coffee Networking", "Encuentro Freelancers", "Workshop Design"] } ], culturalPhrases: [ "¿Todo bien?", "Dale, joya", "Un golazo ese proyecto", "Re copado el evento", "¿Qué tal?", "Está bárbaro", "Me parece genial", "Súper interesante" ] }; } startGameLoop() { setInterval(() => { if (this.gameState === 'game-interface' && this.player) { this.resonanceSystem.update(); this.updateResonanceDisplay(); } }, 1000); } } class Player { constructor({ name, techBackground, culturalPreference, socialStyle }) { this.name = name; this.techBackground = techBackground; this.culturalPreference = culturalPreference; this.socialStyle = socialStyle; this.stats = { carrera: this.getInitialStat('carrera'), cultura: this.getInitialStat('cultura'), social: this.getInitialStat('social'), bienestar: this.getInitialStat('bienestar') }; this.relationships = { sofia: 50, mateo: 40, luna: 30 }; this.memory = []; this.achievements = []; console.log('Player created:', this); } getInitialStat(statName) { const baseStats = { carrera: 50, cultura: 30, social: 40, bienestar: 60 }; let value = baseStats[statName]; if (statName === 'carrera') { value += ['frontend', 'backend', 'fullstack'].includes(this.techBackground) ? 10 : 0; value += this.techBackground === 'product' ? 15 : 0; } if (statName === 'cultura') { value += this.culturalPreference === 'music' ? 20 : 0; value += this.culturalPreference === 'art' ? 15 : 0; } if (statName === 'social') { value += this.socialStyle === 'networking' ? 20 : 0; value += this.socialStyle === 'introvert' ? -10 : 0; } return Math.max(0, Math.min(100, value)); } changeStat(statName, change) { if (this.stats[statName] !== undefined) { this.stats[statName] = Math.max(0, Math.min(100, this.stats[statName] + change)); const fillElement = document.getElementById(`${statName}-fill`); if (fillElement) { fillElement.classList.add(change > 0 ? 'increase' : 'decrease'); setTimeout(() => { fillElement.classList.remove('increase', 'decrease'); }, 500); } } } changeRelationship(npcId, change) { if (this.relationships[npcId] !== undefined) { this.relationships[npcId] = Math.max(0, Math.min(100, this.relationships[npcId] + change)); } } addMemory(event) { this.memory.push({ event, timestamp: Date.now(), impact: event.impact || 'minor' }); } } class ResonanceSystem { constructor() { this.decisions = []; this.resonanceLevel = 0; this.activeWaves = []; } addDecision(decision) { const resonanceWave = { decision, timestamp: Date.now(), strength: this.calculateDecisionStrength(decision), decayRate: 0.1 }; this.activeWaves.push(resonanceWave); this.updateResonanceLevel(); } addEvent(event) { const eventWave = { event, timestamp: Date.now(), strength: event.resonanceImpact || 0.5, decayRate: 0.05 }; this.activeWaves.push(eventWave); this.updateResonanceLevel(); } calculateDecisionStrength(decision) { const weights = { minor: 0.3, moderate: 0.6, major: 1.0, life_changing: 1.5 }; return weights[decision.weight] || 0.6; } update() { const now = Date.now(); this.activeWaves = this.activeWaves.filter(wave => { const age = (now - wave.timestamp) / 1000; wave.strength *= Math.exp(-wave.decayRate * age); return wave.strength > 0.01; }); this.updateResonanceLevel(); } updateResonanceLevel() { this.resonanceLevel = this.activeWaves.reduce((total, wave) => total + wave.strength, 0); } getResonanceLevel() { return Math.min(3, Math.floor(this.resonanceLevel)); } getResonanceDescription() { const level = this.getResonanceLevel(); const descriptions = [ "Calma total, sin ondas activas", "Ligeras ondas de resonancia", "Resonancia moderada en progreso", "Intensa actividad de resonancia" ]; return descriptions[level] || descriptions[0]; } getResonanceEffects(player) { const effects = []; if (this.resonanceLevel > 1.5) { effects.push("unexpected_opportunity"); } if (this.resonanceLevel > 2.0) { effects.push("personality_echo"); } return effects; } } class NarrativeEngine { constructor() { this.currentScenario = null; this.scenarioHistory = []; this.scenarioTemplates = this.initializeScenarios(); } initializeScenarios() { return [ { id: 'startup_first_day', location: { name: 'Vicente López', description: 'Hub tech moderno con startups y coworking spaces' }, narrative: 'Llegás a tu primer día en la nueva startup en Vicente López. El coworking space está lleno de energía, pantallas con código y el aroma de café de especialidad.', prompt: 'Sofía, la frontend lead, se acerca durante el coffee break. Te comenta sobre un proyecto React que está armando y menciona que buscan alguien para el equipo.', decisions: [ { key: 'collaborate', text: '"Me copa la propuesta, ¿cuándo arrancamos?"', effects: { carrera: 10, social: 5 }, relationshipChanges: { sofia: 15 }, weight: 'moderate' }, { key: 'cautious', text: '"Suena interesante, ¿me contás más detalles?"', effects: { bienestar: 5 }, relationshipChanges: { sofia: 5 }, weight: 'minor' }, { key: 'independent', text: '"Estoy enfocado en mi proyecto actual, pero gracias"', effects: { carrera: 5, social: -5 }, relationshipChanges: { sofia: -10 }, weight: 'moderate' } ] }, { id: 'cultural_invitation', location: { name: 'San Isidro', description: 'Zona cultural con venues indies y espacios de arte' }, narrative: 'Luna te escribe por Slack sobre un showcase de El Mató a un Policía Motorizado en un venue íntimo de San Isidro.', prompt: 'Es viernes por la tarde y tenés que elegir entre quedarte terminando un sprint o ir al show.', decisions: [ { key: 'show', text: '"Dale, me re copa. ¿Nos vemos ahí?"', effects: { cultura: 15, bienestar: 10, carrera: -5 }, relationshipChanges: { luna: 20 }, weight: 'moderate' }, { key: 'work_first', text: '"Me encantaría, pero tengo que cerrar este sprint"', effects: { carrera: 10, bienestar: -5 }, relationshipChanges: { luna: -5 }, weight: 'minor' }, { key: 'compromise', text: '"Si termino temprano, me sumo al after"', effects: { carrera: 5, cultura: 5 }, relationshipChanges: { luna: 5 }, weight: 'minor' } ] } ]; } generateScenario(player, resonanceSystem) { const availableScenarios = this.scenarioTemplates.filter(scenario => !this.scenarioHistory.includes(scenario.id) ); let selectedScenario; if (availableScenarios.length === 0) { selectedScenario = this.generateProceduralScenario(player, resonanceSystem); } else { selectedScenario = availableScenarios[0]; // Simple selection for now } this.currentScenario = selectedScenario; this.scenarioHistory.push(selectedScenario.id); return selectedScenario; } generateProceduralScenario(player, resonanceSystem) { const locations = [ { name: 'Vicente López', description: 'Hub tech moderno con startups y coworking spaces' }, { name: 'San Isidro', description: 'Zona cultural con venues indies y espacios de arte' }, { name: 'Olivos', description: 'Barrio residencial con cafeterías y espacios de networking' } ]; return { id: `procedural_${Date.now()}`, location: locations[Math.floor(Math.random() * locations.length)], narrative: `Una nueva oportunidad aparece en tu camino. Las ondas de resonancia de tus decisiones pasadas están convergiendo...`, prompt: "¿Cómo vas a responder a esta nueva situación?", decisions: [ { key: 'bold', text: '"Voy con todo, sin dudas"', effects: { carrera: 10, social: 5, bienestar: -5 }, weight: 'moderate' }, { key: 'balanced', text: '"Analizo bien antes de decidir"', effects: { carrera: 5, bienestar: 5 }, weight: 'minor' }, { key: 'creative', text: '"Busco una solución creativa"', effects: { cultura: 10, social: 5 }, weight: 'moderate' } ] }; } getCurrentScenario() { return this.currentScenario; } } class EventSystem { constructor() { this.events = [ { name: "North Valley Meetup", description: "Networking tech en Vicente López", date: "Viernes 18:30", requirements: { carrera: 20, social: 15 }, outcomes: ["networking_boost", "tech_knowledge", "startup_contacts"], resonanceImpact: 0.7 }, { name: "Innovation Tech Week", description: "Semana tech más importante de BA", date: "Próxima semana", requirements: { carrera: 40, social: 30 }, outcomes: ["major_networking", "tech_skills_gain", "startup_opportunity"], resonanceImpact: 1.2 }, { name: "Showcase Indie", description: "Show en venue de San Isidro", date: "Sábado 21:00", requirements: { cultura: 25, social: 20 }, outcomes: ["cultural_inspiration", "creative_boost", "underground_contacts"], resonanceImpact: 0.8 } ]; } getAvailableEvents(player) { return this.events.map(event => { const available = this.checkRequirements(event.requirements, player); return { ...event, available, requirement: available ? null : this.formatRequirements(event.requirements, player) }; }); } checkRequirements(requirements, player) { return Object.entries(requirements).every(([stat, required]) => player.stats[stat] >= required ); } formatRequirements(requirements, player) { const unmet = Object.entries(requirements).find(([stat, required]) => player.stats[stat] < required ); if (unmet) { const [stat, required] = unmet; const statName = stat.charAt(0).toUpperCase() + stat.slice(1); return `Requiere ${statName}: ${required}`; } return ""; } joinEvent(eventName, player) { const event = this.events.find(e => e.name === eventName); if (!event || !this.checkRequirements(event.requirements, player)) { return null; } return { eventName: event.name, narrative: `Participás en ${event.name} y la experiencia es increíble. Conectás con gente nueva y aprendés un montón.`, outcomes: ["Expandiste tu red de contactos", "Aprendiste sobre nuevas tecnologías"], statChanges: { carrera: 10, social: 8 }, relationshipChanges: {}, newConnections: "Conociste a un founder de una startup prometedora", resonanceImpact: event.resonanceImpact }; } }