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();
// if we have other obstacles, we can add them here
this.gridMapTypes = {
floor: 0,
wall: 1,
door: 2,
};
// 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
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;
} else {
this.backgroundGridMap[row][col] = this.gridMapTypes.floor;
}
}
}
this.backgroundGridMap[0][1] = this.gridMapTypes.door;
}
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) {
if (this.backgroundGridMap[row][col] === this.gridMapTypes.wall) {
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) {
if (e.key in this.keys) {
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(target);
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(target) {
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) {
this.ctx.drawImage(this.wall.image, x, y, this.wall.width, this.wall.height);
} else if (this.backgroundGridMap[row][col] === this.gridMapTypes.floor) {
this.ctx.drawImage(this.floor.image, x, y, this.floor.width, this.floor.height);
} else if (this.backgroundGridMap[row][col] === this.gridMapTypes.door) {
this.ctx.drawImage(this.door.image, x, y, this.door.width, this.door.height);
}
}
}
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.ctx.drawImage(
this.shyguySprite.image,
this.shyguySprite.frameX * this.shyguySprite.width,
this.shyguySprite.frameY * this.shyguySprite.height,
this.shyguySprite.width,
this.shyguySprite.height,
this.shyguySprite.x,
this.shyguySprite.y,
this.shyguySprite.width,
this.shyguySprite.height
);
// Draw wingman
this.ctx.drawImage(
this.wingmanSprite.image,
this.wingmanSprite.frameX * this.wingmanSprite.width,
this.wingmanSprite.frameY * this.wingmanSprite.height,
this.wingmanSprite.width,
this.wingmanSprite.height,
this.wingmanSprite.x,
this.wingmanSprite.y,
this.wingmanSprite.width,
this.wingmanSprite.height
);
}
drawTargetSprite(sprite, target) {
this.ctx.drawImage(sprite.image, target.x, target.y, target.width, target.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) {
// TODO: resolve the 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.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.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("").then((response) => {
const next_action = response.action;
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);
});
}
stopAllMusic() {
this.stopBackgroundMusic();
this.gameOverMusic.pause();
this.gameOverMusic.currentTime = 0;
this.victoryMusic.pause();
this.victoryMusic.currentTime = 0;
}
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;
}
}
}