import { BAR_LABEL, DJ_LABEL, EXIT_LABEL, GIRL_LABEL, SISTER_LABEL, WINGMAN_LABEL, SHYGUY_LABEL } from "./constants"; import { nameToLabel } from "./story_engine.js"; const WINGMAN_SPEED = 5; const SHYGUY_SPEED = 1; const IS_DEBUG = false; class SpriteEntity { constructor(x0, y0, imageSrc, speed = 0, width = 24, height = 64, frameRate = 8, frameCount = 1) { this.x = x0; this.y = y0; this.width = width; this.height = height; this.image = new Image(); this.image.src = imageSrc; this.frameRate = frameRate; this.frameCount = frameCount; // properties for the game engine this.moving = false; this.speed = speed; // frame index in the sprite sheet this.frameX = 0; this.frameY = 0; // 0 for right, 1 for left } stop() { this.moving = false; } start() { this.moving = true; } setSpeed(speed) { this.speed = speed; } } class GuidedSpriteEntity extends SpriteEntity { constructor(x0, y0, imageSrc, speed = 0, width = 24, height = 64, frameRate = 8, frameCount = 1) { super(x0, y0, imageSrc, speed, width, height, frameRate, frameCount); this.target = null; } setTarget(target) { this.target = target; } } class SpriteImage { constructor(imageSrc, width = 32, height = 32) { this.image = new Image(); this.image.src = imageSrc; this.width = width; this.height = height; } } class Target { constructor(label, x, y, width, height, color, enabled = true) { this.label = label; this.x = x; this.y = y; this.width = width; this.height = height; this.debugColor = color; this.enabled = enabled; } } export class GameEngine { static introMessages = [ { message: "Hey man, this is really not my cup of tea. I see Jessica in the corner, I wonder if I can finally tell her I love her.", character: SHYGUY_LABEL, }, { message: "Man, tonight is your night. I'll get you through it and you'll go home with Jessica.", character: WINGMAN_LABEL, }, { message: "Geez, that's impossible! Even if I replay the night a million times, I couldn't do it.", character: SHYGUY_LABEL, }, { message: "Okay, just follow my advice! I'll push you around if needed.", character: WINGMAN_LABEL, }, ]; constructor(shyguy, shyguyLLM, storyEngine, speechToTextClient, elevenLabsClient) { this.shyguy = shyguy; this.shyguyLLM = shyguyLLM; this.storyEngine = storyEngine; this.speechToTextClient = speechToTextClient; this.elevenLabsClient = elevenLabsClient; this.canvasWidth = 960; this.canvasHeight = 640; this.canvas = document.getElementById("gameCanvas"); if (!this.canvas) { console.error("Canvas not found"); } this.ctx = this.canvas.getContext("2d"); // View management this.gameView = document.getElementById("gameView"); this.dialogueView = document.getElementById("dialogueView"); this.currentView = "game"; this.shouldContinue = true; this.gameOver = false; this.gameSuccessful = false; this.gameChatContainer = document.getElementById("chatMessages"); this.messageInput = document.getElementById("messageInput"); this.sendButton = document.getElementById("sendButton"); this.microphoneButton = document.getElementById("micButton"); this.gameOverImage = document.getElementById("gameOverImage"); this.gameOverText = document.getElementById("gameOverText"); this.dialogueChatContainer = document.getElementById("dialogueMessages"); this.dialogueContinueButton = document.getElementById("dialogueContinueButton"); this.dialogueNextButton = document.getElementById("dialogueNextButton"); this.gameFrame = 0; this.keys = { ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false, }; // Bind methods this.switchView = this.switchView.bind(this); this.update = this.update.bind(this); this.draw = this.draw.bind(this); this.run = this.run.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); this.handleKeyUp = this.handleKeyUp.bind(this); this.setNewTarget = this.setNewTarget.bind(this); this.checkTargetReached = this.checkTargetReached.bind(this); this.updateGuidedSpriteDirection = this.updateGuidedSpriteDirection.bind(this); this.updateSprite = this.updateSprite.bind(this); this.handleSpriteCollision = this.handleSpriteCollision.bind(this); this.initDebugControls = this.initDebugControls.bind(this); this.stopShyguyAnimation = this.stopShyguyAnimation.bind(this); this.handlePlayAgain = this.handlePlayAgain.bind(this); this.handleMicrophone = this.handleMicrophone.bind(this); this.handleSendMessage = this.handleSendMessage.bind(this); this.handleMicrophone = this.handleMicrophone.bind(this); this.handleDialogueContinue = this.handleDialogueContinue.bind(this); this.handleFirstStartGame = this.handleFirstStartGame.bind(this); this.setGameOver = this.setGameOver.bind(this); this.handleDialogueNext = this.handleDialogueNext.bind(this); this.pushEnabled = false; this.voiceEnabled = !IS_DEBUG; // Debug controls this.initDebugControls(); // configure environment building blocks and enable passing them this.gridMapTypes = { floor: { index: 0, passable: true }, wall: { index: 1, passable: false }, door: { index: 2, passable: false }, }; // load assets for drawing the scene this.wall = new SpriteImage("/assets/assets/wall_sprite.png"); this.floor = new SpriteImage("/assets/assets/floor-tile.png"); this.door = new SpriteImage("/assets/assets/door_sprite.png"); this.gridCols = Math.ceil(this.canvasWidth / this.wall.width); this.gridRows = Math.ceil(this.canvasHeight / this.wall.height); // initialize grid map this.backgroundGridMap = []; this.initBackgroundGridMap(); // initialize players const cx = this.canvasWidth / 2; const cy = this.canvasHeight / 2; this.shyguySprite = new GuidedSpriteEntity(cx, cy, "/assets/assets/shyguy_sprite.png", SHYGUY_SPEED); this.wingmanSprite = new SpriteEntity( this.wall.width, this.canvasHeight - this.wall.height - 64, "/assets/assets/wingman_sprite.png", WINGMAN_SPEED ); this.jessicaSprite = new SpriteImage("/assets/assets/jessica_sprite.png", 64, 64); this.djSprite = new SpriteImage("/assets/assets/dj_sprite.png", 64, 64); this.barSprite = new SpriteImage("/assets/assets/bar_sprite.png", 64, 64); this.sisterSprite = new SpriteImage("/assets/assets/sister_sprite.png", 64, 64); this.targets = { exit: new Target(EXIT_LABEL, this.wall.width, this.wall.height, this.wall.width, this.wall.height, "red", true), girl: new Target( GIRL_LABEL, this.canvasWidth - this.wall.width - this.jessicaSprite.width, (this.canvasHeight - this.wall.height - this.jessicaSprite.height) / 2, this.jessicaSprite.width, this.jessicaSprite.height, "pink", true ), bar: new Target( BAR_LABEL, (this.canvasWidth - this.wall.width - this.barSprite.width) / 2, this.wall.height, this.barSprite.width, this.barSprite.height, "blue", true ), dj: new Target( DJ_LABEL, this.wall.width, (this.canvasHeight - this.wall.height - this.djSprite.height) / 2, this.djSprite.width, this.djSprite.height, "green", true ), sister: new Target( SISTER_LABEL, this.canvasWidth - this.wall.width - this.sisterSprite.width, this.wall.height, this.sisterSprite.width, this.sisterSprite.height, "yellow", true ), }; // Add game over view this.gameOverView = document.getElementById("gameOverView"); this.playAgainBtn = document.getElementById("playAgainBtn"); this.isRecording = false; // Add these lines this.introView = document.getElementById("introView"); this.startGameBtn = document.getElementById("startGameBtn"); this.backgroundMusic = new Audio("assets/assets/tiny-steps-danijel-zambo-main-version-1433-01-48.mp3"); this.backgroundMusic.loop = true; this.gameOverMusic = new Audio("/assets/assets/game-over-8bit-music-danijel-zambo-1-00-16.mp3"); this.gameOverMusic.loop = false; this.victoryMusic = new Audio("/assets/assets/moonlit-whispers-theo-gerard-main-version-35960-02-34.mp3"); this.victoryMusic.loop = false; // Move character images to class state this.leftCharacterImg = document.getElementById("leftCharacterImg"); this.rightCharacterImg = document.getElementById("rightCharacterImg"); this.hideCharacterImages(); } showCharacterImages() { this.leftCharacterImg.style.display = "block"; this.rightCharacterImg.style.display = "block"; } hideCharacterImages() { this.leftCharacterImg.style.display = "none"; this.rightCharacterImg.style.display = "none"; } init(firstRun = true) { this.canvas.width = this.canvasWidth; this.canvas.height = this.canvasHeight; document.addEventListener("keydown", this.handleKeyDown); document.addEventListener("keyup", this.handleKeyUp); // Initialize with game view const intialStatusText = "You are playing as the Wingman. You can move around using arrow keys. Maybe Shyguy will listen to you or let you follow him around. Don't let him leave without the girl!"; this.updateStatus(intialStatusText); this.sendButton.addEventListener("click", this.handleSendMessage); this.dialogueContinueButton.addEventListener("click", this.handleDialogueContinue); this.dialogueNextButton.addEventListener("click", this.handleDialogueNext); this.playAgainBtn.addEventListener("click", this.handlePlayAgain); this.microphoneButton.addEventListener("click", this.handleMicrophone); if (firstRun) { this.startGameBtn.addEventListener("click", this.handleFirstStartGame); this.switchView("intro"); } else { if (this.currentView !== "game") { this.switchView("game"); } this.run(); this.shyguySprite.setTarget(this.targets.exit); } } async handleFirstStartGame() { this.switchView("dialogue"); this.leftCharacterImg.src = "/assets/assets/wingman.jpeg"; this.rightCharacterImg.src = "/assets/assets/shyguy_headshot.jpeg"; this.showCharacterImages(); this.hideContinueButton(); for (const introMessage of GameEngine.introMessages) { const { message, character } = introMessage; this.addChatMessage(this.dialogueChatContainer, message, character, true); if (this.voiceEnabled) { await this.elevenLabsClient.playAudioForCharacter(character, message); } else { await new Promise((resolve) => setTimeout(resolve, 1000)); } } this.showNextButton(); } showNextButton() { if (this.dialogueNextButton) { this.dialogueNextButton.style.display = "block"; } } hideNextButton() { if (this.dialogueNextButton) { this.dialogueNextButton.style.display = "none"; } } handleDialogueNext() { this.clearChat(this.dialogueChatContainer); this.leftCharacterImg.src = ""; this.rightCharacterImg.src = ""; this.hideCharacterImages(); this.hideNextButton(); this.showContinueButton(); this.handleStartGame(); } async handleStartGame() { this.switchView("game"); this.playBackgroundMusic(); this.run(); this.shyguySprite.setTarget(this.targets.exit); } setResetCallback(func) { this.resetCallback = func; } resetGame() { if (this.resetCallback) { this.resetCallback(); } } initBackgroundGridMap() { for (let row = 0; row < this.gridRows; row++) { this.backgroundGridMap[row] = []; for (let col = 0; col < this.gridCols; col++) { // Set walls and obstacles (in future) if (row === 0 || row === this.gridRows - 1 || col === 0 || col === this.gridCols - 1) { this.backgroundGridMap[row][col] = this.gridMapTypes.wall.index; } else { this.backgroundGridMap[row][col] = this.gridMapTypes.floor.index; } } } this.backgroundGridMap[0][1] = this.gridMapTypes.door.index; } checkWallCollision(sprite, newX, newY) { const x = newX; const y = newY; // For a sprite twice as big as grid, divide by half the sprite width/height const gridX = Math.floor(x / (sprite.width * 1.33)); const gridY = Math.floor(y / (sprite.height / 2)); // Check all grid cells the sprite overlaps // For a sprite twice as big, it can overlap up to 4 cells for (let row = gridY; row <= Math.floor((y + sprite.height) / (sprite.height / 2)); row++) { for (let col = gridX; col <= Math.floor((x + sprite.width) / (sprite.width * 1.33)); col++) { if (row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols) { const cellType = this.backgroundGridMap[row][col]; const typeInfo = Object.values(this.gridMapTypes).find((type) => type.index === cellType); if (typeInfo && !typeInfo.passable) { return true; } } } } return false; } checkSpriteCollision(newX, newY, sprite1, sprite2) { return ( newX < sprite2.x + sprite2.width && newX + sprite1.width > sprite2.x && newY < sprite2.y + sprite2.height && newY + sprite1.height > sprite2.y ); } handleSpriteCollision(sprite1, sprite2) { if (!this.pushEnabled) { return true; // Return true to block movement as before } // Calculate velocity difference let dx = 0; let dy = 0; if (this.keys.ArrowUp) dy = -sprite1.speed; else if (this.keys.ArrowDown) dy = sprite1.speed; else if (this.keys.ArrowLeft) dx = -sprite1.speed; else if (this.keys.ArrowRight) dx = sprite1.speed; // If arrow player isn't moving, stop button player if (dx === 0 && dy === 0) { return true; } // Calculate effective push speed (difference in velocities) const pushSpeed = Math.max(0, sprite1.speed - sprite2.speed); // If arrow player is faster, push button player if (pushSpeed > 0) { let newX = sprite2.x + (dx !== 0 ? dx : 0); let newY = sprite2.y + (dy !== 0 ? dy : 0); // Only apply the push if it won't result in a wall collision if (!this.checkWallCollision(sprite2, newX, newY)) { sprite2.x = newX; sprite2.y = newY; } } return true; // Still prevent arrow player from moving through button player } updateGuidedSprite() { if (!this.shyguySprite.target) return; const dx = this.shyguySprite.target.x - this.shyguySprite.x; const dy = this.shyguySprite.target.y - this.shyguySprite.y; const distance = Math.sqrt(dx * dx + dy * dy); const moveX = (dx / distance) * this.shyguySprite.speed; const moveY = (dy / distance) * this.shyguySprite.speed; let newX = this.shyguySprite.x + moveX; let newY = this.shyguySprite.y + moveY; // Check wall collision first if (!this.checkWallCollision(this.shyguySprite, newX, newY)) { const willCollide = this.checkSpriteCollision(newX, newY, this.shyguySprite, this.wingmanSprite); if (willCollide) { if (this.pushEnabled) { // Push mechanics enabled - try to push wingman const pushSpeed = Math.max(0, this.shyguySprite.speed - this.wingmanSprite.speed); if (pushSpeed > 0) { let wingmanNewX = this.wingmanSprite.x + moveX; let wingmanNewY = this.wingmanSprite.y + moveY; if (!this.checkWallCollision(this.wingmanSprite, wingmanNewX, wingmanNewY)) { this.wingmanSprite.x = wingmanNewX; this.wingmanSprite.y = wingmanNewY; this.shyguySprite.x = newX; this.shyguySprite.y = newY; this.shyguySprite.moving = true; } } } // If push is disabled or push failed, try to path around if (this.shyguySprite.x === newX && this.shyguySprite.y === newY) { const leftPath = { x: newX - this.wingmanSprite.width, y: newY }; const rightPath = { x: newX + this.wingmanSprite.width, y: newY }; const upPath = { x: newX, y: newY - this.wingmanSprite.height }; const downPath = { x: newX, y: newY + this.wingmanSprite.height }; const paths = [leftPath, rightPath, upPath, downPath]; let bestPath = null; let bestDistance = Infinity; for (const path of paths) { if ( !this.checkWallCollision(this.shyguySprite, path.x, path.y) && !this.checkSpriteCollision(path.x, path.y, this.shyguySprite, this.wingmanSprite) ) { const pathDistance = Math.sqrt( Math.pow(this.shyguySprite.target.x - path.x, 2) + Math.pow(this.shyguySprite.target.y - path.y, 2) ); if (pathDistance < bestDistance) { bestDistance = pathDistance; bestPath = path; } } } if (bestPath) { this.shyguySprite.x = bestPath.x; this.shyguySprite.y = bestPath.y; this.shyguySprite.moving = true; } } } else { // No collision, proceed normally this.shyguySprite.x = newX; this.shyguySprite.y = newY; this.shyguySprite.moving = true; } } } updateSprite() { let newX = this.wingmanSprite.x; let newY = this.wingmanSprite.y; let isMoving = false; if (this.keys.ArrowUp) { newY -= this.wingmanSprite.speed; isMoving = true; } if (this.keys.ArrowDown) { newY += this.wingmanSprite.speed; isMoving = true; } if (this.keys.ArrowLeft) { newX -= this.wingmanSprite.speed; this.wingmanSprite.frameY = 0; // left isMoving = true; } if (this.keys.ArrowRight) { newX += this.wingmanSprite.speed; this.wingmanSprite.frameY = 1; // right isMoving = true; } // Check wall collision first if (!this.checkWallCollision(this.wingmanSprite, newX, newY)) { // Check collision with shyguy const willCollide = this.checkSpriteCollision(newX, newY, this.wingmanSprite, this.shyguySprite); if (willCollide) { if (this.pushEnabled) { // Try to push shyguy if push is enabled this.handleSpriteCollision(this.wingmanSprite, this.shyguySprite); } // If push is disabled or push failed, don't move return; } // No collision, proceed with movement this.wingmanSprite.x = newX; this.wingmanSprite.y = newY; } this.wingmanSprite.moving = isMoving; } handleKeyDown(e) { // Only handle arrow keys for player movement if the text input is not focused if (e.key in this.keys && !document.activeElement.matches('input[type="text"], textarea')) { this.keys[e.key] = true; this.wingmanSprite.moving = true; } else if (e.key === "Enter" && this.currentView === "game" && !e.shiftKey) { e.preventDefault(); this.handleSendMessage(); } } handleKeyUp(e) { if (e.key in this.keys) { this.keys[e.key] = false; this.wingmanSprite.moving = Object.values(this.keys).some((key) => key); } } setNewTarget(target) { if (target && target.enabled) { this.shyguySprite.setTarget(target); this.updateGuidedSpriteDirection(this.shyguySprite); } if (!target) { this.shyguySprite.setTarget(null); } } checkTargetReached(sprite, target) { // Check if sprite overlaps with target using AABB collision detection const spriteLeft = sprite.x; const spriteRight = sprite.x + sprite.width; const spriteTop = sprite.y; const spriteBottom = sprite.y + sprite.height; const targetLeft = target.x; const targetRight = target.x + target.width; const targetTop = target.y; const targetBottom = target.y + target.height; // Check for overlap on both x and y axes const xOverlap = spriteRight >= targetLeft && spriteLeft <= targetRight; const yOverlap = spriteBottom >= targetTop && spriteTop <= targetBottom; return xOverlap && yOverlap; } updateGuidedSpriteDirection(sprite) { if (!sprite.target) return; const dx = sprite.target.x - sprite.x; // Update direction based only on horizontal movement if (dx !== 0) { sprite.frameY = dx > 0 ? 1 : 0; // 0 for right, 1 for left } } updateSpriteAnimation(sprite) { if (sprite.moving) { if (this.gameFrame % sprite.frameRate === 0) { sprite.frameX = (sprite.frameX + 1) % sprite.frameCount; } } else { sprite.frameX = 0; } } async update() { this.gameFrame++; // Update Shyguy position if (this.shyguySprite.target && this.shyguySprite.target.enabled) { this.updateGuidedSprite(this.shyguySprite); if (this.shyguySprite.moving) { this.updateSpriteAnimation(this.shyguySprite); } } // update Wingman position this.updateSprite(this.wingmanSprite); if (this.wingmanSprite.moving) { this.updateSpriteAnimation(this.wingmanSprite); } for (const target of Object.values(this.targets)) { const isClose = this.checkTargetReached(this.shyguySprite, target); // TODO: reenable the target so the player can visit it again if (!target.enabled) { if (!isClose) { target.enabled = true; } continue; } if (isClose) { // pause the game target.enabled = false; this.stopShyguyAnimation(); if (target.label === EXIT_LABEL) { this.gameOver = true; this.gameSuccessful = false; this.setGameOver(true); this.switchView("gameOver"); } else { await this.handleDialogueWithStoryEngine(target.label); } break; } } } async handleDialogueWithStoryEngine(label) { this.switchView("dialogue"); this.hideContinueButton(); // Show loading indicator const dialogueBox = document.querySelector(".dialogue-box"); dialogueBox.classList.add("loading"); const response = await this.storyEngine.onEncounter(label); // Hide loading indicator dialogueBox.classList.remove("loading"); // Update character images using class properties if (this.leftCharacterImg && response.char2imgpath) { this.leftCharacterImg.src = response.char2imgpath; this.leftCharacterImg.style.display = "block"; } if (this.rightCharacterImg && response.char1imgpath) { this.rightCharacterImg.src = response.char1imgpath; this.rightCharacterImg.style.display = "block"; } const conversation = response.conversation; // TODO: set the images if they are available for (const message of conversation) { const { role, content } = message; const label = nameToLabel(role); this.addChatMessage(this.dialogueChatContainer, content, label, true); // Only play audio if voice is enabled if (this.voiceEnabled) { try { this.lowerMusicVolumeALot(); await this.elevenLabsClient.playAudioForCharacter(label, content); this.restoreMusicVolume(); } catch (error) { console.error("Error playing audio:", label); } } } if (response.gameSuccesful) { this.gameOver = true; this.gameSuccessful = true; } else if (response.gameOver) { this.gameOver = true; this.gameSuccessful = false; } else { this.gameOver = false; this.gameSuccessful = false; } this.showContinueButton(); } stopShyguyAnimation() { this.shyguySprite.moving = false; this.shyguySprite.frameX = 0; this.shyguySprite.target = null; } draw() { this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); // Draw grid map for (let row = 0; row < this.gridRows; row++) { for (let col = 0; col < this.gridCols; col++) { const x = col * this.wall.width; const y = row * this.wall.height; if (this.backgroundGridMap[row][col] === this.gridMapTypes.wall.index) { this.ctx.drawImage(this.wall.image, x, y, this.wall.width, this.wall.height); } else if (this.backgroundGridMap[row][col] === this.gridMapTypes.floor.index) { this.ctx.drawImage(this.floor.image, x, y, this.floor.width, this.floor.height); } else if (this.backgroundGridMap[row][col] === this.gridMapTypes.door.index) { this.ctx.drawImage(this.door.image, x, y, this.door.width, this.door.height); } } } // Draw npcs with targets this.drawTargetSprite(this.jessicaSprite, this.targets.girl); this.drawTargetSprite(this.barSprite, this.targets.bar); this.drawTargetSprite(this.djSprite, this.targets.dj); this.drawTargetSprite(this.sisterSprite, this.targets.sister); // Draw shyguy this.drawPlayerSprite(this.shyguySprite); // Draw wingman this.drawPlayerSprite(this.wingmanSprite); } drawTargetSprite(sprite, target) { this.ctx.drawImage(sprite.image, target.x, target.y, target.width, target.height); } drawPlayerSprite(sprite) { this.ctx.drawImage( sprite.image, sprite.frameX * sprite.width, sprite.frameY * sprite.height, sprite.width, sprite.height, sprite.x, sprite.y, sprite.width, sprite.height ); } switchView(viewName) { if (viewName === this.currentView) return; this.currentView = viewName; // Hide all views first this.introView.classList.remove("active"); this.gameView.classList.remove("active"); this.dialogueView.classList.remove("active"); this.gameOverView.classList.remove("active"); // Show the requested view switch (viewName) { case "intro": this.introView.classList.add("active"); break; case "game": this.gameView.classList.add("active"); break; case "dialogue": this.dialogueView.classList.add("active"); break; case "gameOver": this.gameOverView.classList.add("active"); break; } } enablePush() { this.pushEnabled = true; } disablePush() { this.pushEnabled = false; } initDebugControls() { const debugControls = document.getElementById("debugControls"); if (!IS_DEBUG) { if (debugControls) { debugControls.style.display = "none"; } return; } const targetDoorBtn = document.getElementById("targetDoorBtn"); const targetGirlBtn = document.getElementById("targetGirlBtn"); const targetBarBtn = document.getElementById("targetBarBtn"); const targetDjBtn = document.getElementById("targetDjBtn"); const targetSisterBtn = document.getElementById("targetSisterBtn"); const stopNavBtn = document.getElementById("stopNavBtn"); const togglePushBtn = document.getElementById("togglePushBtn"); const speedBoostBtn = document.getElementById("speedBoostBtn"); const toggleVoiceBtn = document.getElementById("toggleVoiceBtn"); targetDoorBtn.addEventListener("click", () => this.setNewTarget(this.targets.exit)); targetGirlBtn.addEventListener("click", () => this.setNewTarget(this.targets.girl)); targetBarBtn.addEventListener("click", () => this.setNewTarget(this.targets.bar)); targetDjBtn.addEventListener("click", () => this.setNewTarget(this.targets.dj)); targetSisterBtn.addEventListener("click", () => this.setNewTarget(this.targets.sister)); stopNavBtn.addEventListener("click", () => this.setNewTarget(null)); // Add push mechanics toggle togglePushBtn.addEventListener("click", () => { if (this.pushEnabled) { this.disablePush(); } else { this.enablePush(); } togglePushBtn.textContent = this.pushEnabled ? "Disable Push" : "Enable Push"; }); // Add speed boost toggle speedBoostBtn.addEventListener("click", () => { if (this.shyguySprite.speed === SHYGUY_SPEED) { this.shyguySprite.setSpeed(10); speedBoostBtn.textContent = "Normal Speed"; } else { this.shyguySprite.setSpeed(SHYGUY_SPEED); speedBoostBtn.textContent = "Speed Boost"; } }); // Add voice toggle handler toggleVoiceBtn.addEventListener("click", () => { this.voiceEnabled = !this.voiceEnabled; toggleVoiceBtn.textContent = this.voiceEnabled ? "Disable Voice" : "Enable Voice"; }); } // Update status text updateStatus(message) { const statusText = document.getElementById("statusText"); if (statusText) { statusText.textContent = message; } } clearChat(container) { if (container) { container.innerHTML = ""; } } addChatMessage(container, message, character, shyguyIsMain) { if (!container) return; const isMain = shyguyIsMain ? character === SHYGUY_LABEL : character !== SHYGUY_LABEL; const messageDiv = document.createElement("div"); messageDiv.className = `chat-message ${isMain ? "right-user" : "left-user"}`; const bubble = document.createElement("div"); bubble.className = "message-bubble"; bubble.textContent = message; messageDiv.appendChild(bubble); container.appendChild(messageDiv); // Auto scroll to bottom container.scrollTop = container.scrollHeight; } resolveAction(action) { switch (action) { case "stay_idle": this.setNewTarget(null); break; case "go_bar": this.setNewTarget(this.targets.bar); break; case "go_dj": this.setNewTarget(this.targets.dj); break; case "go_sister": this.setNewTarget(this.targets.sister); break; case "go_girl": this.setNewTarget(this.targets.girl); break; case "go_home": this.setNewTarget(this.targets.exit); break; default: break; } } async sendMessageToShyguy(message) { this.addChatMessage(this.gameChatContainer, message, WINGMAN_LABEL, false); this.messageInput.value = ""; this.shyguyLLM.getShyGuyResponse(message).then(async (response) => { const dialogue = response.dialogue; const action = response.action; this.addChatMessage(this.gameChatContainer, dialogue, SHYGUY_LABEL, false); // Only play audio if voice is enabled if (this.voiceEnabled) { this.disableGameInput(); this.lowerMusicVolumeALot(); await this.elevenLabsClient.playAudioForCharacter(SHYGUY_LABEL, dialogue); this.enableGameInput(); this.restoreMusicVolume(); } // TODO: save conversation history await this.shyguy.learnFromWingman(message); console.log("[ShyguyLLM]: Next action: ", action); this.shyguy.last_actions.push(action); if (this.shyguy.num_beers >= 1) { console.log("Updating status to: Shyguy is drunk. Try pushing him."); this.updateStatus("Shyguy is drunk. Try pushing him."); } this.resolveAction(action); }); } async handleSendMessage() { const message = this.messageInput.value.trim(); if (message.length === 0) return; this.sendMessageToShyguy(message); } async run() { // wait for 16ms await new Promise((resolve) => setTimeout(resolve, 16)); await this.update(); this.draw(); if (this.shouldContinue) { requestAnimationFrame(this.run); } } handlePlayAgain() { this.clearChat(this.gameChatContainer); this.resetGame(); this.stopGameOverMusic(); this.switchView("game"); } async handleMicrophone() { if (!this.isRecording) { // Start recording this.isRecording = true; this.microphoneButton.classList.add("recording"); this.microphoneButton.innerHTML = ''; // Lower music volume while recording this.lowerMusicVolumeALot(); await this.speechToTextClient.startRecording(); } else { // Stop recording this.isRecording = false; this.microphoneButton.classList.remove("recording"); this.microphoneButton.innerHTML = ''; const result = await this.speechToTextClient.stopRecording(); // Restore music volume after recording this.restoreMusicVolume(); this.sendMessageToShyguy(result.text); } } showContinueButton() { this.dialogueContinueButton.style.display = "block"; } hideContinueButton() { this.dialogueContinueButton.style.display = "none"; } setGameOver(fromExit) { this.stopBackgroundMusic(); if (this.gameSuccessful) { this.gameOverImage.src = "assets/assets/victory.png"; this.playVictoryMusic(); } else { this.gameOverImage.src = "assets/assets/game-over.png"; this.playGameOverMusic(); } if (fromExit) { this.gameOverText.textContent = "You lost! Shyguy ran away!"; return; } this.gameOverText.textContent = this.gameSuccessful ? "You won! Shyguy got a date!" : "You lost! Shyguy got rejected!"; } handleDialogueContinue() { this.clearChat(this.dialogueChatContainer); // Hide character images const leftCharacterImg = document.getElementById("leftCharacterImg"); const rightCharacterImg = document.getElementById("rightCharacterImg"); if (leftCharacterImg) { leftCharacterImg.style.display = "none"; } if (rightCharacterImg) { rightCharacterImg.style.display = "none"; } // decide if game is over if (this.gameOver) { this.setGameOver(false); this.switchView("gameOver"); return; } // Enable push if shyguy has had at least one beer if (this.shyguy.num_beers > 0) { this.enablePush(); } this.switchView("game"); this.shyguyLLM.getShyGuyResponse("Where do you go next? Your available actions are: " + this.shyguy.getAvailableActions()).then((response) => { const next_action = response.action; if (this.shyguy.num_beers >= 1) { console.log("Updating status to: Shyguy is drunk. Try pushing him."); this.updateStatus("Shyguy is drunk. Try pushing him."); } this.resolveAction(next_action); }); } disableGameInput() { this.sendButton.setAttribute("disabled", ""); this.microphoneButton.setAttribute("disabled", ""); this.messageInput.setAttribute("disabled", ""); } enableGameInput() { this.sendButton.removeAttribute("disabled"); this.microphoneButton.removeAttribute("disabled"); this.messageInput.removeAttribute("disabled"); } playBackgroundMusic() { this.backgroundMusic.play().catch((error) => { console.error("Error playing background music:", error); }); } stopBackgroundMusic() { this.backgroundMusic.pause(); this.backgroundMusic.currentTime = 0; } playGameOverMusic() { this.gameOverMusic.play().catch((error) => { console.error("Error playing game over music:", error); }); } playVictoryMusic() { this.victoryMusic.play().catch((error) => { console.error("Error playing victory music:", error); }); } stopGameOverMusic() { this.gameOverMusic.pause(); this.gameOverMusic.currentTime = 0; this.victoryMusic.pause(); this.victoryMusic.currentTime = 0; } stopAllMusic() { this.stopBackgroundMusic(); this.stopGameOverMusic(); } lowerMusicVolume() { // Store original volumes if not already stored if (!this.originalVolumes) { this.originalVolumes = { background: this.backgroundMusic.volume, gameOver: this.gameOverMusic.volume, victory: this.victoryMusic.volume, }; } // Lower all music volumes to 20% of their original values this.backgroundMusic.volume = this.originalVolumes.background * 0.2; this.gameOverMusic.volume = this.originalVolumes.gameOver * 0.2; this.victoryMusic.volume = this.originalVolumes.victory * 0.2; } lowerMusicVolumeALot() { // Store original volumes if not already stored if (!this.originalVolumes) { this.originalVolumes = { background: this.backgroundMusic.volume, gameOver: this.gameOverMusic.volume, victory: this.victoryMusic.volume, }; } // Lower all music volumes to 20% of their original values this.backgroundMusic.volume = this.originalVolumes.background * 0.01; this.gameOverMusic.volume = this.originalVolumes.gameOver * 0.01; this.victoryMusic.volume = this.originalVolumes.victory * 0.01; } restoreMusicVolume() { // Restore original volumes if they exist if (this.originalVolumes) { this.backgroundMusic.volume = this.originalVolumes.background * 0.2; this.gameOverMusic.volume = this.originalVolumes.gameOver * 0.2; this.victoryMusic.volume = this.originalVolumes.victory * 0.2; } } }