LifeGame / app.js
Lukeetah's picture
Upload 3 files
aaffe69 verified
// 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 = `<p>${scenario.narrative}</p>`;
if (decisionsContainer) {
decisionsContainer.innerHTML = `
<div class="decision-prompt">
<p>${scenario.prompt}</p>
</div>
<div class="decisions-list">
${scenario.decisions.map(decision => `
<button class="decision-btn" data-decision="${decision.key}">
<span class="decision-text">${decision.text}</span>
<span class="decision-impact">${this.formatDecisionImpact(decision.effects)}</span>
</button>
`).join('')}
</div>
`;
}
}
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 => `
<div class="event-item ${event.available ? 'available' : 'locked'}">
<h5>${event.name}</h5>
<p>${event.description}</p>
${event.available
? `<span class="event-date">${event.date}</span>
<button class="btn btn--sm btn--secondary event-join">Asistir</button>`
: `<span class="event-requirement">${event.requirement}</span>`
}
</div>
`).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 = `
<div class="event-result">
<p>${result.narrative}</p>
<div class="event-outcomes">
<h4>Resultados:</h4>
<ul>
${result.outcomes.map(outcome => `<li>${outcome}</li>`).join('')}
</ul>
</div>
${result.newConnections ? `
<div class="new-connections">
<h4>Nuevas Conexiones:</h4>
<p>${result.newConnections}</p>
</div>
` : ''}
</div>
`;
}
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
};
}
}