diff --git a/.gitattributes b/.gitattributes
index a6344aac8c09253b3b630fb776ae94478aa0275b..ee14e48948b51b34077758610fe459c7917491d5 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -33,3 +33,12 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text
*tfevents* filter=lfs diff=lfs merge=lfs -text
+assets/assets/dj_old.png filter=lfs diff=lfs merge=lfs -text
+assets/assets/game-over.png filter=lfs diff=lfs merge=lfs -text
+assets/assets/moonlit-whispers-theo-gerard-main-version-35960-02-34.mp3 filter=lfs diff=lfs merge=lfs -text
+assets/assets/still-waters-night-drift-main-version-34884-02-09.mp3 filter=lfs diff=lfs merge=lfs -text
+assets/assets/sunshine-danijel-zambo-main-version-01-54-1403.mp3 filter=lfs diff=lfs merge=lfs -text
+assets/assets/tiny-steps-danijel-zambo-main-version-1433-01-48.mp3 filter=lfs diff=lfs merge=lfs -text
+assets/assets/title-screen.png filter=lfs diff=lfs merge=lfs -text
+assets/assets/victory.png filter=lfs diff=lfs merge=lfs -text
+assets/assets/wingman_old.png filter=lfs diff=lfs merge=lfs -text
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..75eaa60c1955ddd378d36666963a82982c50df5a
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,12 @@
+FROM node:18
+
+WORKDIR /app
+
+COPY package.json package.json
+COPY package-lock.json package-lock.json
+
+RUN npm install
+
+COPY . .
+
+CMD ["npm", "run", "dev"]
diff --git a/README.md b/README.md
index 694be73a931029784d232dc233ae9f48cbcc443d..d75ddcc1a482f0dfe9e32480da4aaca997cd9509 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1 @@
----
-title: ShyguysWingmanJS
-emoji: 🌍
-colorFrom: indigo
-colorTo: gray
-sdk: docker
-pinned: false
----
-
-Check out the configuration reference at https://huggingface.co./docs/hub/spaces-config-reference
+# ShyguysWingmanJS
\ No newline at end of file
diff --git a/assets/assets/bar_sprite.png b/assets/assets/bar_sprite.png
new file mode 100644
index 0000000000000000000000000000000000000000..052d0d32091aea32ff6c7462dff3640a31e4ce52
Binary files /dev/null and b/assets/assets/bar_sprite.png differ
diff --git a/assets/assets/barman.jpeg b/assets/assets/barman.jpeg
new file mode 100644
index 0000000000000000000000000000000000000000..6497a9a86b3cab106078d47db4341b65ca5eff39
Binary files /dev/null and b/assets/assets/barman.jpeg differ
diff --git a/assets/assets/barman_old.png b/assets/assets/barman_old.png
new file mode 100644
index 0000000000000000000000000000000000000000..79088db97a50f598c306edc81f3ec339f485de05
Binary files /dev/null and b/assets/assets/barman_old.png differ
diff --git a/assets/assets/dj.jpeg b/assets/assets/dj.jpeg
new file mode 100644
index 0000000000000000000000000000000000000000..f292ded1a7c476ee52946888b2fcab5226444c6a
Binary files /dev/null and b/assets/assets/dj.jpeg differ
diff --git a/assets/assets/dj_old.png b/assets/assets/dj_old.png
new file mode 100644
index 0000000000000000000000000000000000000000..c8afe496d88a4a6492f71979e78b4c4820a645e1
--- /dev/null
+++ b/assets/assets/dj_old.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0efd462632a8002e38c7dc4ed3cddbce21bdb6684575700b22699c50112037d8
+size 2758312
diff --git a/assets/assets/dj_sprite.png b/assets/assets/dj_sprite.png
new file mode 100644
index 0000000000000000000000000000000000000000..1aa1594d3f4696254436d34a75c5c63b3a7e4149
Binary files /dev/null and b/assets/assets/dj_sprite.png differ
diff --git a/assets/assets/door_sprite.png b/assets/assets/door_sprite.png
new file mode 100644
index 0000000000000000000000000000000000000000..8dd54f5118dcbba9e14bd2842ee0c68d46f8748b
Binary files /dev/null and b/assets/assets/door_sprite.png differ
diff --git a/assets/assets/floor-tile.png b/assets/assets/floor-tile.png
new file mode 100644
index 0000000000000000000000000000000000000000..7978ab768c57bfa9b2db0d11d726b4938406e1b5
Binary files /dev/null and b/assets/assets/floor-tile.png differ
diff --git a/assets/assets/game-over-8bit-music-danijel-zambo-1-00-16.mp3 b/assets/assets/game-over-8bit-music-danijel-zambo-1-00-16.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..14b4880893e72ed83a6d32efe641e4938b3e2f95
Binary files /dev/null and b/assets/assets/game-over-8bit-music-danijel-zambo-1-00-16.mp3 differ
diff --git a/assets/assets/game-over.png b/assets/assets/game-over.png
new file mode 100644
index 0000000000000000000000000000000000000000..e4e2bb71df50e8d82b615b3a90a059b4b6d5b190
--- /dev/null
+++ b/assets/assets/game-over.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b432c55365bb67d10381d935929e4fcb6d3e6e1d0b707601b30b1952b1a063fd
+size 3336543
diff --git a/assets/assets/image.webp b/assets/assets/image.webp
new file mode 100644
index 0000000000000000000000000000000000000000..c40bcbba9c2d84bc4b4479f5edf9ad6c4c3b9fc3
Binary files /dev/null and b/assets/assets/image.webp differ
diff --git a/assets/assets/intro-image.jpg b/assets/assets/intro-image.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..f09351ccc57ab87d1d65ec0e06cad47aec38cc58
Binary files /dev/null and b/assets/assets/intro-image.jpg differ
diff --git a/assets/assets/jessica.jpeg b/assets/assets/jessica.jpeg
new file mode 100644
index 0000000000000000000000000000000000000000..efd43f89f7b0d8838397e164f8ca2076fb88fd2a
Binary files /dev/null and b/assets/assets/jessica.jpeg differ
diff --git a/assets/assets/jessica_old.png b/assets/assets/jessica_old.png
new file mode 100644
index 0000000000000000000000000000000000000000..f2cbdfaf8931c32053dd0d6f98b3b8288a79588b
Binary files /dev/null and b/assets/assets/jessica_old.png differ
diff --git a/assets/assets/jessica_sprite.png b/assets/assets/jessica_sprite.png
new file mode 100644
index 0000000000000000000000000000000000000000..ab134913372a6a2be912a82e430a149d434a96f6
Binary files /dev/null and b/assets/assets/jessica_sprite.png differ
diff --git a/assets/assets/moonlit-whispers-theo-gerard-main-version-35960-02-34.mp3 b/assets/assets/moonlit-whispers-theo-gerard-main-version-35960-02-34.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..15f0a88cb06dafc4ee3c710e115d8585aa3c2cfd
--- /dev/null
+++ b/assets/assets/moonlit-whispers-theo-gerard-main-version-35960-02-34.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:042c696678781cbd276979868386b81d49e5623dc1bb0471d4a8a9db672d8c92
+size 6224789
diff --git a/assets/assets/player.png b/assets/assets/player.png
new file mode 100644
index 0000000000000000000000000000000000000000..dd14bdd3460304703cd0f473717813221fbb0eb3
Binary files /dev/null and b/assets/assets/player.png differ
diff --git a/assets/assets/shyguy-headshot_old.png b/assets/assets/shyguy-headshot_old.png
new file mode 100644
index 0000000000000000000000000000000000000000..31127d5f903997618f060a0a8401d50573631d95
Binary files /dev/null and b/assets/assets/shyguy-headshot_old.png differ
diff --git a/assets/assets/shyguy.jpeg b/assets/assets/shyguy.jpeg
new file mode 100644
index 0000000000000000000000000000000000000000..b02ce7aa316b0ab917bd75f08914e210404699a9
Binary files /dev/null and b/assets/assets/shyguy.jpeg differ
diff --git a/assets/assets/shyguy.png b/assets/assets/shyguy.png
new file mode 100644
index 0000000000000000000000000000000000000000..16acb7857a126f150195bf24c440a4ad13c5095c
Binary files /dev/null and b/assets/assets/shyguy.png differ
diff --git a/assets/assets/shyguy_headshot.jpeg b/assets/assets/shyguy_headshot.jpeg
new file mode 100644
index 0000000000000000000000000000000000000000..c8d58321d4789a88b1f554c0b3ee2928096b0b08
Binary files /dev/null and b/assets/assets/shyguy_headshot.jpeg differ
diff --git a/assets/assets/shyguy_left.png b/assets/assets/shyguy_left.png
new file mode 100644
index 0000000000000000000000000000000000000000..6e45918c84501e57c4c4dc597ce9d61092f64d25
Binary files /dev/null and b/assets/assets/shyguy_left.png differ
diff --git a/assets/assets/shyguy_right.png b/assets/assets/shyguy_right.png
new file mode 100644
index 0000000000000000000000000000000000000000..33a45e308ff99c52ccaab6df6bc444db439ff85f
Binary files /dev/null and b/assets/assets/shyguy_right.png differ
diff --git a/assets/assets/shyguy_sprite.png b/assets/assets/shyguy_sprite.png
new file mode 100644
index 0000000000000000000000000000000000000000..38329f7a30a9a52940290ac776279af497e4bf18
Binary files /dev/null and b/assets/assets/shyguy_sprite.png differ
diff --git a/assets/assets/sister.jpeg b/assets/assets/sister.jpeg
new file mode 100644
index 0000000000000000000000000000000000000000..e08bbaa2a849c66882a8bd9dd48eb773aebab0f4
Binary files /dev/null and b/assets/assets/sister.jpeg differ
diff --git a/assets/assets/sister_old.png b/assets/assets/sister_old.png
new file mode 100644
index 0000000000000000000000000000000000000000..a54160cc235acbc538c422622587fe243888e78a
Binary files /dev/null and b/assets/assets/sister_old.png differ
diff --git a/assets/assets/sister_sprite.png b/assets/assets/sister_sprite.png
new file mode 100644
index 0000000000000000000000000000000000000000..72f6b96de80575c54b6a97ed66ee6d4ea3017272
Binary files /dev/null and b/assets/assets/sister_sprite.png differ
diff --git a/assets/assets/still-waters-night-drift-main-version-34884-02-09.mp3 b/assets/assets/still-waters-night-drift-main-version-34884-02-09.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..293a45c8b1165601b6bdea811e88597e43f22b0d
--- /dev/null
+++ b/assets/assets/still-waters-night-drift-main-version-34884-02-09.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b9c75e89fc006b5f65a1e6bbd753d718276c047e382110e3defdb2da948cd810
+size 9634996
diff --git a/assets/assets/sunshine-danijel-zambo-main-version-01-54-1403.mp3 b/assets/assets/sunshine-danijel-zambo-main-version-01-54-1403.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..300c0de5995a63d058a7c523dbb1c3dd9b187768
--- /dev/null
+++ b/assets/assets/sunshine-danijel-zambo-main-version-01-54-1403.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:20861a5ba975478289de19e3c04d27d6186c4f62bfe8a996f20225c1951dbd92
+size 4584812
diff --git a/assets/assets/tiny-steps-danijel-zambo-main-version-1433-01-48.mp3 b/assets/assets/tiny-steps-danijel-zambo-main-version-1433-01-48.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..4655bb2b7db0cf5d4c520cbe0ae646120217ce7f
--- /dev/null
+++ b/assets/assets/tiny-steps-danijel-zambo-main-version-1433-01-48.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dd1bc1cfcdebe9999f561dd4cdd5a9bd32760764a7634ab11f13a9c53b2febd9
+size 4334026
diff --git a/assets/assets/title-screen.png b/assets/assets/title-screen.png
new file mode 100644
index 0000000000000000000000000000000000000000..cfc9cfdead1675b092fb198b596cb7965891f410
--- /dev/null
+++ b/assets/assets/title-screen.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bf87396234fba30571a24ac0abb67b0cac1e4e993f8db5732cee2bf008f6204a
+size 4008166
diff --git a/assets/assets/tragic-piano-game-over-music-gfx-sounds-1-00-08.mp3 b/assets/assets/tragic-piano-game-over-music-gfx-sounds-1-00-08.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..1089663c11a79549a34254473d5bda580b8004d1
Binary files /dev/null and b/assets/assets/tragic-piano-game-over-music-gfx-sounds-1-00-08.mp3 differ
diff --git a/assets/assets/victory.png b/assets/assets/victory.png
new file mode 100644
index 0000000000000000000000000000000000000000..b792351116ed48ee2ed24584a90c42f4af5dc19f
--- /dev/null
+++ b/assets/assets/victory.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bfee3e2b474d892abe75a0bc6aa10e4efa8ae159114fd59b46255dc38fdc107a
+size 3345805
diff --git a/assets/assets/wall_sprite.png b/assets/assets/wall_sprite.png
new file mode 100644
index 0000000000000000000000000000000000000000..e7ac0bed72b4bf3c71c00ee1f98d0c34d5dd9a12
Binary files /dev/null and b/assets/assets/wall_sprite.png differ
diff --git a/assets/assets/wingman.jpeg b/assets/assets/wingman.jpeg
new file mode 100644
index 0000000000000000000000000000000000000000..a3e25859e3f3f90ed38ae2d6c9a011851a84867d
Binary files /dev/null and b/assets/assets/wingman.jpeg differ
diff --git a/assets/assets/wingman_left.png b/assets/assets/wingman_left.png
new file mode 100644
index 0000000000000000000000000000000000000000..b6397a521644bca5b2537c80c04c69a3b123ea9c
Binary files /dev/null and b/assets/assets/wingman_left.png differ
diff --git a/assets/assets/wingman_old.png b/assets/assets/wingman_old.png
new file mode 100644
index 0000000000000000000000000000000000000000..5d3ea6d6a5b2705d0696c7c4959d16dd37ecaf87
--- /dev/null
+++ b/assets/assets/wingman_old.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:39d93bdc4e63c00c879ab5627832352a1eaf12f7f66157b80b57703612e3f80d
+size 1275594
diff --git a/assets/assets/wingman_right.png b/assets/assets/wingman_right.png
new file mode 100644
index 0000000000000000000000000000000000000000..8dec55cd50f6c624be84f0a92821bd0a48afd750
Binary files /dev/null and b/assets/assets/wingman_right.png differ
diff --git a/assets/assets/wingman_sprite.png b/assets/assets/wingman_sprite.png
new file mode 100644
index 0000000000000000000000000000000000000000..9aac3b08a4b6324fbfffcc657231d8d40677bed6
Binary files /dev/null and b/assets/assets/wingman_sprite.png differ
diff --git a/index.html b/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..1faea077cf8981b5ec65aeb2086d53adb7b642eb
--- /dev/null
+++ b/index.html
@@ -0,0 +1,98 @@
+
+
+
+
+
+ Shyguy's Wingman
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ You are playing as the Wingman. You need to help your friend get the girl of his dreams. Don't let him leave!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..c38ba2ee05d05bf9f67c08c58ecec70d5600bd97
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1688 @@
+{
+ "name": "shyguys-wingman-js",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "shyguys-wingman-js",
+ "version": "1.0.0",
+ "dependencies": {
+ "elevenlabs": "^1.50.4"
+ },
+ "devDependencies": {
+ "vite": "^6.0.11"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
+ "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
+ "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
+ "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
+ "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
+ "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
+ "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
+ "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
+ "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
+ "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
+ "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
+ "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
+ "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
+ "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
+ "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
+ "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
+ "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
+ "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
+ "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
+ "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
+ "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
+ "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
+ "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
+ "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
+ "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
+ "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.0.tgz",
+ "integrity": "sha512-G2fUQQANtBPsNwiVFg4zKiPQyjVKZCUdQUol53R8E71J7AsheRMV/Yv/nB8giOcOVqP7//eB5xPqieBYZe9bGg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.32.0.tgz",
+ "integrity": "sha512-qhFwQ+ljoymC+j5lXRv8DlaJYY/+8vyvYmVx074zrLsu5ZGWYsJNLjPPVJJjhZQpyAKUGPydOq9hRLLNvh1s3A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.32.0.tgz",
+ "integrity": "sha512-44n/X3lAlWsEY6vF8CzgCx+LQaoqWGN7TzUfbJDiTIOjJm4+L2Yq+r5a8ytQRGyPqgJDs3Rgyo8eVL7n9iW6AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.32.0.tgz",
+ "integrity": "sha512-F9ct0+ZX5Np6+ZDztxiGCIvlCaW87HBdHcozUfsHnj1WCUTBUubAoanhHUfnUHZABlElyRikI0mgcw/qdEm2VQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.32.0.tgz",
+ "integrity": "sha512-JpsGxLBB2EFXBsTLHfkZDsXSpSmKD3VxXCgBQtlPcuAqB8TlqtLcbeMhxXQkCDv1avgwNjF8uEIbq5p+Cee0PA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.32.0.tgz",
+ "integrity": "sha512-wegiyBT6rawdpvnD9lmbOpx5Sph+yVZKHbhnSP9MqUEDX08G4UzMU+D87jrazGE7lRSyTRs6NEYHtzfkJ3FjjQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.32.0.tgz",
+ "integrity": "sha512-3pA7xecItbgOs1A5H58dDvOUEboG5UfpTq3WzAdF54acBbUM+olDJAPkgj1GRJ4ZqE12DZ9/hNS2QZk166v92A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.32.0.tgz",
+ "integrity": "sha512-Y7XUZEVISGyge51QbYyYAEHwpGgmRrAxQXO3siyYo2kmaj72USSG8LtlQQgAtlGfxYiOwu+2BdbPjzEpcOpRmQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.32.0.tgz",
+ "integrity": "sha512-r7/OTF5MqeBrZo5omPXcTnjvv1GsrdH8a8RerARvDFiDwFpDVDnJyByYM/nX+mvks8XXsgPUxkwe/ltaX2VH7w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.32.0.tgz",
+ "integrity": "sha512-HJbifC9vex9NqnlodV2BHVFNuzKL5OnsV2dvTw6e1dpZKkNjPG6WUq+nhEYV6Hv2Bv++BXkwcyoGlXnPrjAKXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.32.0.tgz",
+ "integrity": "sha512-VAEzZTD63YglFlWwRj3taofmkV1V3xhebDXffon7msNz4b14xKsz7utO6F8F4cqt8K/ktTl9rm88yryvDpsfOw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.32.0.tgz",
+ "integrity": "sha512-Sts5DST1jXAc9YH/iik1C9QRsLcCoOScf3dfbY5i4kH9RJpKxiTBXqm7qU5O6zTXBTEZry69bGszr3SMgYmMcQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.32.0.tgz",
+ "integrity": "sha512-qhlXeV9AqxIyY9/R1h1hBD6eMvQCO34ZmdYvry/K+/MBs6d1nRFLm6BOiITLVI+nFAAB9kUB6sdJRKyVHXnqZw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.32.0.tgz",
+ "integrity": "sha512-8ZGN7ExnV0qjXa155Rsfi6H8M4iBBwNLBM9lcVS+4NcSzOFaNqmt7djlox8pN1lWrRPMRRQ8NeDlozIGx3Omsw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.32.0.tgz",
+ "integrity": "sha512-VDzNHtLLI5s7xd/VubyS10mq6TxvZBp+4NRWoW+Hi3tgV05RtVm4qK99+dClwTN1McA6PHwob6DEJ6PlXbY83A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.32.0.tgz",
+ "integrity": "sha512-qcb9qYDlkxz9DxJo7SDhWxTWV1gFuwznjbTiov289pASxlfGbaOD54mgbs9+z94VwrXtKTu+2RqwlSTbiOqxGg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.32.0.tgz",
+ "integrity": "sha512-pFDdotFDMXW2AXVbfdUEfidPAk/OtwE/Hd4eYMTNVVaCQ6Yl8et0meDaKNL63L44Haxv4UExpv9ydSf3aSayDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.32.0.tgz",
+ "integrity": "sha512-/TG7WfrCAjeRNDvI4+0AAMoHxea/USWhAzf9PVDFHbcqrQ7hMMKp4jZIy4VEjk72AAfN5k4TiSMRXRKf/0akSw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.0.tgz",
+ "integrity": "sha512-5hqO5S3PTEO2E5VjCePxv40gIgyS2KvO7E7/vvC/NbIW4SIRamkMr1hqj+5Y67fbBWv/bQLB6KelBQmXlyCjWA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "license": "MIT",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
+ "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
+ "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/command-exists": {
+ "version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
+ "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==",
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/elevenlabs": {
+ "version": "1.50.4",
+ "resolved": "https://registry.npmjs.org/elevenlabs/-/elevenlabs-1.50.4.tgz",
+ "integrity": "sha512-c/g9tORpzi/5wd//2avzRvnZ0ujEKSZi3Jn6FO93gcWXHvftTuundGBwgNcIAyzZo9oRW5VlNxMYFHEZOdc1Fg==",
+ "license": "MIT",
+ "dependencies": {
+ "command-exists": "^1.2.9",
+ "execa": "^5.1.1",
+ "form-data": "^4.0.0",
+ "form-data-encoder": "^4.0.2",
+ "formdata-node": "^6.0.3",
+ "node-fetch": "2.7.0",
+ "qs": "6.11.2",
+ "readable-stream": "^4.5.2",
+ "url-join": "4.0.1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
+ "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.24.2",
+ "@esbuild/android-arm": "0.24.2",
+ "@esbuild/android-arm64": "0.24.2",
+ "@esbuild/android-x64": "0.24.2",
+ "@esbuild/darwin-arm64": "0.24.2",
+ "@esbuild/darwin-x64": "0.24.2",
+ "@esbuild/freebsd-arm64": "0.24.2",
+ "@esbuild/freebsd-x64": "0.24.2",
+ "@esbuild/linux-arm": "0.24.2",
+ "@esbuild/linux-arm64": "0.24.2",
+ "@esbuild/linux-ia32": "0.24.2",
+ "@esbuild/linux-loong64": "0.24.2",
+ "@esbuild/linux-mips64el": "0.24.2",
+ "@esbuild/linux-ppc64": "0.24.2",
+ "@esbuild/linux-riscv64": "0.24.2",
+ "@esbuild/linux-s390x": "0.24.2",
+ "@esbuild/linux-x64": "0.24.2",
+ "@esbuild/netbsd-arm64": "0.24.2",
+ "@esbuild/netbsd-x64": "0.24.2",
+ "@esbuild/openbsd-arm64": "0.24.2",
+ "@esbuild/openbsd-x64": "0.24.2",
+ "@esbuild/sunos-x64": "0.24.2",
+ "@esbuild/win32-arm64": "0.24.2",
+ "@esbuild/win32-ia32": "0.24.2",
+ "@esbuild/win32-x64": "0.24.2"
+ }
+ },
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
+ "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/form-data-encoder": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.0.2.tgz",
+ "integrity": "sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/formdata-node": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-6.0.3.tgz",
+ "integrity": "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
+ "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.0",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "license": "MIT"
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
+ "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.1",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
+ "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.8",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.11.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
+ "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
+ "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
+ "license": "MIT",
+ "dependencies": {
+ "abort-controller": "^3.0.0",
+ "buffer": "^6.0.3",
+ "events": "^3.3.0",
+ "process": "^0.11.10",
+ "string_decoder": "^1.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.0.tgz",
+ "integrity": "sha512-JmrhfQR31Q4AuNBjjAX4s+a/Pu/Q8Q9iwjWBsjRH1q52SPFE2NqRMK6fUZKKnvKO6id+h7JIRf0oYsph53eATg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.6"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.32.0",
+ "@rollup/rollup-android-arm64": "4.32.0",
+ "@rollup/rollup-darwin-arm64": "4.32.0",
+ "@rollup/rollup-darwin-x64": "4.32.0",
+ "@rollup/rollup-freebsd-arm64": "4.32.0",
+ "@rollup/rollup-freebsd-x64": "4.32.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.32.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.32.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.32.0",
+ "@rollup/rollup-linux-arm64-musl": "4.32.0",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.32.0",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.32.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.32.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.32.0",
+ "@rollup/rollup-linux-x64-gnu": "4.32.0",
+ "@rollup/rollup-linux-x64-musl": "4.32.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.32.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.32.0",
+ "@rollup/rollup-win32-x64-msvc": "4.32.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/url-join": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
+ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "6.0.11",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz",
+ "integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.24.2",
+ "postcss": "^8.4.49",
+ "rollup": "^4.23.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..31e8fcb1321ed4e3e76138c486cd95e629de5a2c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "shyguys-wingman-js",
+ "version": "1.0.0",
+ "description": "ShyguysWingmanJS project",
+ "main": "src/index.js",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "elevenlabs": "^1.50.4"
+ },
+ "devDependencies": {
+ "vite": "^6.0.11"
+ }
+}
diff --git a/src/constants.js b/src/constants.js
new file mode 100644
index 0000000000000000000000000000000000000000..0020297d95cdb222e102ff50a1a808c0e41762d7
--- /dev/null
+++ b/src/constants.js
@@ -0,0 +1,7 @@
+export const WINGMAN_LABEL = "wingman";
+export const SHYGUY_LABEL = "shyguy";
+export const SISTER_LABEL = "sister";
+export const GIRL_LABEL = "girl";
+export const BAR_LABEL = "barman";
+export const DJ_LABEL = "dj";
+export const EXIT_LABEL = "exit";
diff --git a/src/conversation_llm.js b/src/conversation_llm.js
new file mode 100644
index 0000000000000000000000000000000000000000..99437a0b722acdf4bb2c50ae91b4465676e26733
--- /dev/null
+++ b/src/conversation_llm.js
@@ -0,0 +1,76 @@
+import LLM from "./llm";
+
+export class ConversationLLM {
+ constructor(character1Name, character2Name, character1Prompt, character2Prompt, situation_prompt, outputFormatPrompt, functionDescriptions, functionPrompt) {
+ this.character1Name = character1Name;
+ this.character2Name = character2Name;
+ this.character1Prompt = character1Prompt;
+ this.character2Prompt = character2Prompt;
+ this.situation_prompt = situation_prompt;
+ this.outputFormatPrompt = outputFormatPrompt;
+ this.functionDescriptions = functionDescriptions;
+ this.functionPrompt = functionPrompt;
+
+ }
+
+ async generateConversation(numTurns = 3) {
+ try {
+ let conversation = [];
+ const llm = new LLM();
+
+ for (let i = 0; i < numTurns; i++) {
+
+ // Alternate between characters for each turn
+ const isCharacter1Turn = i % 2 === 0;
+ const currentSpeaker = isCharacter1Turn ? this.character1Prompt : this.character2Prompt;
+ const currentListener = isCharacter1Turn ? this.character2Prompt : this.character1Prompt;
+ const currentSpeakerName = isCharacter1Turn ? this.character1Name : this.character2Name;
+ const currentListenerName = isCharacter1Turn ? this.character2Name : this.character1Name;
+
+ // Format the conversation history as a proper chat message array
+ const conversationHistory = [...conversation];
+
+ // Create system message for current speaker
+ const systemMessage = {
+ role: 'system',
+ content: `${this.situation_prompt}\nRoleplay as: ${currentSpeakerName}\nMake only the response to the user. Only speech, no speech style. You have the following personality: ${currentSpeaker}. You talk to ${currentListenerName}.`
+ };
+
+ // Get response from LLM with proper message format
+ const response = await llm.getChatCompletion(
+ systemMessage.content,
+ conversationHistory.length > 0
+ ? JSON.stringify(conversationHistory)
+ : "Start the conversation"
+ );
+
+ // Ensure the response is in the correct format with the proper character role
+ const parsedResponse = {
+ role: currentSpeakerName, // Use the character name instead of prompt
+ content: this.parseConversation(response)
+ };
+
+ conversation.push(parsedResponse);
+ }
+
+ const analysis = await llm.getFunctionKey(
+ this.functionDescriptions,
+ this.functionPrompt + JSON.stringify(conversation)
+ );
+
+ return {
+ conversation,
+ analysis
+ };
+ } catch (error) {
+ console.error('Error generating conversation:', error);
+ throw error;
+ }
+ }
+ parseConversation(llmResponse) {
+
+ return llmResponse;
+ }
+}
+
+
diff --git a/src/eleven_labs.js b/src/eleven_labs.js
new file mode 100644
index 0000000000000000000000000000000000000000..b18b65257290312d8464ea88132a4d1c28dca590
--- /dev/null
+++ b/src/eleven_labs.js
@@ -0,0 +1,91 @@
+import { SHYGUY_LABEL, SISTER_LABEL, GIRL_LABEL, BAR_LABEL, DJ_LABEL, WINGMAN_LABEL } from "./constants.js";
+
+export class ElevenLabsClient {
+ constructor() {
+ this.apiKey = "sk_62d0fc2ccddb1a4036f624dd8c411383fd9b9990755ea086";
+ this.baseUrl = "https://api.elevenlabs.io/v1";
+ }
+
+ static characterToVoiceIdMapping = {
+ [SHYGUY_LABEL]: "bGNROVfU5WbK6F0AyHII",
+ [SISTER_LABEL]: "VfTRhexMRVuPmOe9aogj",
+ [GIRL_LABEL]: "zQPM9vJjjzGxbs457rQj",
+ [BAR_LABEL]: "XA2bIQ92TabjGbpO2xRr",
+ [DJ_LABEL]: "T0pkYhIZ7UMOc26gqqeX",
+ [WINGMAN_LABEL]: "XA2bIQ92TabjGbpO2xRr",
+ };
+
+ async playAudioForCharacter(character, text) {
+ const voiceId = ElevenLabsClient.characterToVoiceIdMapping[character];
+ if (!voiceId) {
+ throw new Error(`No voice mapping found for character: ${character}`);
+ }
+ const audioBlob = await this.createSpeech({
+ text: text,
+ voiceId: voiceId,
+ });
+ const audioUrl = URL.createObjectURL(audioBlob);
+ const audio = new Audio(audioUrl);
+
+ // hack to wait for the audio to finish playing
+ return new Promise((res) => {
+ audio.play();
+ audio.onended = res;
+ });
+ }
+
+ async createSpeech({
+ text,
+ voiceId,
+ modelId = "eleven_multilingual_v2",
+ outputFormat = "mp3_44100_128",
+ voiceSettings = null,
+ pronunciationDictionaryLocators = null,
+ seed = null,
+ previousText = null,
+ nextText = null,
+ previousRequestIds = null,
+ nextRequestIds = null,
+ usePvcAsIvc = false,
+ applyTextNormalization = "auto",
+ }) {
+ const url = `${this.baseUrl}/text-to-speech/${voiceId}?output_format=${outputFormat}`;
+
+ const requestBody = {
+ text,
+ model_id: modelId,
+ voice_settings: voiceSettings,
+ pronunciation_dictionary_locators: pronunciationDictionaryLocators,
+ seed,
+ previous_text: previousText,
+ next_text: nextText,
+ previous_request_ids: previousRequestIds,
+ next_request_ids: nextRequestIds,
+ use_pvc_as_ivc: usePvcAsIvc,
+ apply_text_normalization: applyTextNormalization,
+ };
+
+ // Remove null values from request body
+ Object.keys(requestBody).forEach((key) => requestBody[key] === null && delete requestBody[key]);
+
+ try {
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "xi-api-key": this.apiKey,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(requestBody),
+ });
+
+ if (!response.ok) {
+ throw new Error(`ElevenLabs API error: ${response.status} ${response.statusText}`);
+ }
+
+ // Return audio blob
+ return await response.blob();
+ } catch (error) {
+ throw new Error(`Failed to create speech: ${error.message}`);
+ }
+ }
+}
diff --git a/src/game.js b/src/game.js
new file mode 100644
index 0000000000000000000000000000000000000000..38d7b90e708bb3df89cec8c855390b7d0ff225f1
--- /dev/null
+++ b/src/game.js
@@ -0,0 +1,64 @@
+import { Shyguy } from "./shyguy.js";
+import { GameEngine } from "./game_engine.js";
+import { ShyGuyLLM } from "./shyguy_llm.js";
+import { StoryEngine } from "./story_engine.js";
+import { SpeechToTextClient } from "./speech_to_text.js";
+import { ElevenLabsClient } from "./eleven_labs.js";
+
+export class Game {
+ constructor() {
+ this.firstRun = true;
+
+ this.reset = this.reset.bind(this);
+ this.initializeComponents();
+ }
+
+ initializeComponents() {
+ // Create fresh instances of all components
+ this.shyguy = new Shyguy();
+ this.speechToTextClient = new SpeechToTextClient();
+ this.elevenLabsClient = new ElevenLabsClient();
+ this.shyguyLLM = new ShyGuyLLM(this.shyguy);
+ this.storyEngine = new StoryEngine(this.shyguy);
+ this.gameEngine = new GameEngine(
+ this.shyguy,
+ this.shyguyLLM,
+ this.storyEngine,
+ this.speechToTextClient,
+ this.elevenLabsClient
+ );
+ }
+
+ async run() {
+ this.gameEngine.init(this.firstRun);
+ this.gameEngine.setResetCallback(this.reset);
+ this.gameEngine.playBackgroundMusic();
+ this.gameEngine.lowerMusicVolume();
+ }
+
+ reset() {
+ this.firstRun = false;
+ // Clean up old game engine
+ if (this.gameEngine) {
+ // Remove event listeners and clean up
+ document.removeEventListener("keydown", this.gameEngine.handleKeyDown);
+ document.removeEventListener("keyup", this.gameEngine.handleKeyUp);
+ this.gameEngine.sendButton?.removeEventListener("click", this.gameEngine.handleSendMessage);
+ this.gameEngine.dialogueContinueButton?.removeEventListener("click", this.gameEngine.handleDialogueContinue);
+ this.gameEngine.playAgainBtn?.removeEventListener("click", this.gameEngine.handlePlayAgain);
+ this.gameEngine.microphoneButton?.removeEventListener("click", this.gameEngine.handleMicrophone);
+ this.gameEngine.startGameBtn?.removeEventListener("click", this.gameEngine.handleStartGame);
+ this.gameEngine.dialogueNextButton?.removeEventListener("click", this.gameEngine.handleDialogueNext);
+
+ // Stop the game loop
+ this.gameEngine.shouldContinue = false;
+ }
+
+ setTimeout(() => {
+ // Create fresh instances
+ this.initializeComponents();
+
+ this.run();
+ }, 100);
+ }
+}
diff --git a/src/game_engine.js b/src/game_engine.js
new file mode 100644
index 0000000000000000000000000000000000000000..2a31f3829059d3ce4fb43ccbb098c941e50b31d6
--- /dev/null
+++ b/src/game_engine.js
@@ -0,0 +1,1167 @@
+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;
+ }
+ }
+}
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..1980eb659e6218529cca36e07093682a6c2b87a8
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,9 @@
+import { Game } from "./game.js";
+
+
+
+// start the game when DOM is loaded
+document.addEventListener("DOMContentLoaded", () => {
+ const game = new Game();
+ game.run();
+});
diff --git a/src/llm.js b/src/llm.js
new file mode 100644
index 0000000000000000000000000000000000000000..cbcf3e667c939c163bfc626f817421ca29382249
--- /dev/null
+++ b/src/llm.js
@@ -0,0 +1,183 @@
+export class LLM {
+ constructor() {
+ this.apiKey = "vvO2N5PA9dj8cO6BwcIB8oH4YRnQI3Tn";
+ }
+
+ async getChatCompletion(systemPrompt, userInput) {
+ const messages = [
+ {
+ role: "system",
+ content: systemPrompt,
+ },
+ {
+ role: "user",
+ content: userInput,
+ },
+ ];
+
+ try {
+ const response = await fetch("https://api.mistral.ai/v1/chat/completions", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${this.apiKey}`,
+ },
+ body: JSON.stringify({
+ model: "mistral-large-latest",
+ messages: messages,
+ temperature: 0.7,
+ max_tokens: 150,
+ }),
+ });
+
+ const data = await response.json();
+ return data.choices[0].message.content;
+ } catch (error) {
+ console.error("LLM Error:", error);
+ throw error;
+ }
+ }
+
+ async #getFunctionCall(systemPrompt, userInput, tools) {
+ const messages = [
+ {
+ role: "system",
+ content: systemPrompt,
+ },
+ {
+ role: "user",
+ content: userInput,
+ },
+ ];
+
+ try {
+ const response = await fetch("https://api.mistral.ai/v1/chat/completions", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${this.apiKey}`,
+ },
+ body: JSON.stringify({
+ model: "mistral-large-latest",
+ messages: messages,
+ tools: tools,
+ tool_choice: "any", // Forces tool use
+ }),
+ });
+
+ const data = await response.json();
+
+ // Extract function call details from the response
+ const toolCall = data.choices[0].message.tool_calls[0];
+ return {
+ functionName: toolCall.function.name,
+ arguments: JSON.parse(toolCall.function.arguments),
+ toolCallId: toolCall.id,
+ };
+ } catch (error) {
+ console.error("Function Call Error:", error);
+ throw error;
+ }
+ }
+
+ async getFunctionKey(functionDescriptions, prompt) {
+ // Convert the key-value pairs into the tools format required by the API
+ const tools = functionDescriptions.map(({ key, description, parameters = {} }) => ({
+ type: "function",
+ function: {
+ name: key,
+ description: description,
+ parameters: {
+ type: "object",
+ properties: {
+ ...Object.fromEntries(
+ Object.entries(parameters).map(([paramName, paramConfig]) => [
+ paramName,
+ {
+ type: paramConfig.type || "string", // Use provided type or default to "string"
+ description: paramConfig.description,
+ },
+ ])
+ ),
+ },
+ required: Object.keys(parameters), // Make all parameters required
+ },
+ },
+ }));
+
+ // Use the private getFunctionCall method to make the API call
+ const result = await this.#getFunctionCall(
+ "You are a helpful assistant. Based on the user's input, choose the most appropriate function to call.",
+ prompt,
+ tools
+ );
+
+ return {
+ functionName: result.functionName,
+ parameters: result.arguments,
+ };
+ }
+
+ async getJsonCompletion(systemPrompt, userInput) {
+ const messages = [
+ {
+ role: "system",
+ content: systemPrompt,
+ },
+ {
+ role: "user",
+ content: userInput,
+ },
+ ];
+
+ try {
+ const response = await fetch("https://api.mistral.ai/v1/chat/completions", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${this.apiKey}`,
+ },
+ body: JSON.stringify({
+ model: "mistral-large-latest",
+ messages: messages,
+ temperature: 0.7,
+ max_tokens: 256,
+ response_format: { type: "json_object" },
+ }),
+ });
+
+ const data = await response.json();
+ console.log(data);
+ return JSON.parse(data.choices[0].message.content);
+ } catch (error) {
+ console.error("JSON LLM Error:", error);
+ throw error;
+ }
+ }
+}
+
+export default LLM;
+
+// Function call Usage example
+// const functionDescriptions = [
+// {
+// key: "searchProducts",
+// description: "Search for products in the catalog",
+// parameters: {
+// query: {
+// type: "string",
+// description: "Search query"
+// },
+// maxPrice: {
+// type: "number",
+// description: "Maximum price filter"
+// },
+// inStock: {
+// type: "boolean",
+// description: "Filter for in-stock items only"
+// }
+// }
+// }
+// ];
+
+// const result = await llm.getFunctionKey(functionDescriptions, "Find red shoes in footwear");
diff --git a/src/shyguy.js b/src/shyguy.js
new file mode 100644
index 0000000000000000000000000000000000000000..01f0da7a837e502c41ff91fd84eb7eb6231fa7b5
--- /dev/null
+++ b/src/shyguy.js
@@ -0,0 +1,127 @@
+import LLM from "./llm";
+
+export class Shyguy {
+ constructor() {
+ this.num_beers = 0;
+ this.courage = 1;
+ this.personality = "This is the Shyguy. He is shy and introverted. He is also a bit of a nerd. He fell in love with Jessica. With Jessica, he talks about algorithms.";
+ this.lessons_learned = "";
+ this.conversation_history = "";
+ this.song_playing = "Let it be";
+ this.imgpath = "assets/assets/shyguy_headshot.jpeg";
+ this.last_actions = [];
+ }
+
+ getSystemPrompt() {
+ if (this.num_beers >= 3) {
+ return `${this.personality}. His courage is ${this.courage} on the level 1 to 10. If his courage is higher than 5, he is self-confident. He had too many beers and he is drunk. He talks about how drunk he is. Follow the following lessons: ${this.lessons_learned}`;
+ } else if (this.num_beers == 2) {
+ return `This is Shyguy. He had two beers, so he feels relaxed and he can talk with anyone. Follow the following lessons: ${this.lessons_learned}`;
+ }
+ else {
+ return `${this.personality}. He had ${this.num_beers} numbers of beers and his courage is ${this.courage} on the level 1 to 10. If his courage is higher than 5, he is self-confident. After having 3 bears, he says single words with a lot of hesitation and says that he feels bad and he talks about how drunk he is. If courage is low, he hesitates to speak. Follow the following lessons: ${this.lessons_learned}`;
+ }
+ }
+
+ appendLesson(lesson) {
+ this.lessons_learned += lesson + "\n";
+ }
+
+ appendConversationHistory(conversation_history) {
+ this.conversation_history += conversation_history + "\n";
+ }
+
+ async learnLesson(entityName){
+ const summaryLLM = new LLM();
+ const summary = await summaryLLM.getChatCompletion(
+ `Summarize in one sentence what Shyguy should say when talking to ${entityName}. Do not confuse Jessica and Jessica's sister. If there is nothing relevant about what to say to Jessica, say Nothing relevant.`,
+ this.conversation_history
+ );
+ this.appendLesson(`When talking to ${entityName}, ${summary}`);
+ }
+
+ async learnFromWingman(wingman_message) {
+ const summaryLLM = new LLM();
+ console.log("Wingman message: ", wingman_message);
+ const summary = await summaryLLM.getChatCompletion(
+ `Give a summary of what is learned from the message. Summary is one sentence. The wingman is always talking. For example, if the wingman says "Let's have a beer", the output should be "Shyguy wants a beer". If the wingman says "Let's have vodka", the output should be "Shyguy wants vodka".`,
+ wingman_message
+ );
+ this.appendLesson(summary);
+ }
+
+ getAvailableActions() {
+ let actions = {};
+ const lastAction = this.last_actions[this.last_actions.length - 1];
+
+ // When sober, can only go to the bar or home
+ if (this.num_beers === 0) {
+ actions = {
+ "go_bar": {
+ description: "Head to the bar.",
+ location: "bar",
+ },
+ "go_home": {
+ description: "Give up and head home",
+ location: "exit",
+ },
+ "stay_idle": {
+ description: "Stay idle",
+ location: "idle",
+ }
+ };
+ }
+ else if (this.num_beers >= 2) {
+ // After 2+ beers, all actions except going home are available
+ actions = {
+ "go_bar": {
+ description: "Head to the bar for liquid courage",
+ location: "bar",
+ },
+ "go_dj": {
+ description: "Talk to the DJ about playing a song",
+ location: "dj_booth",
+ },
+ "go_sister": {
+ description: "Approach your crush's sister",
+ location: "sister",
+ },
+ "go_girl": {
+ description: "Approach your crush",
+ location: "girl",
+ }
+ };
+ } else {
+ // After 1 beer but less than 2, all actions are available
+ actions = {
+ "go_bar": {
+ description: "Head to the bar for liquid courage",
+ location: "bar",
+ },
+ "go_home": {
+ description: "Give up and head home",
+ location: "exit",
+ },
+ "go_dj": {
+ description: "Talk to the DJ about playing a song",
+ location: "dj_booth",
+ },
+ "go_sister": {
+ description: "Approach your crush's sister",
+ location: "sister",
+ },
+ "go_girl": {
+ description: "Approach your crush",
+ location: "girl",
+ }
+ };
+ }
+
+ // Remove the last action from available actions
+ if (lastAction && actions[lastAction]) {
+ delete actions[lastAction];
+ }
+
+ return actions;
+ }
+}
\ No newline at end of file
diff --git a/src/shyguy_llm.js b/src/shyguy_llm.js
new file mode 100644
index 0000000000000000000000000000000000000000..e0319a4efe8ff48e6078205da5472c45965c6378
--- /dev/null
+++ b/src/shyguy_llm.js
@@ -0,0 +1,109 @@
+import { LLM } from "./llm.js";
+export class ShyGuyLLM {
+ constructor(shyguy) {
+ this.llm = new LLM();
+ this.shyguy = shyguy;
+ this.currentConversation = []; // Array to store current conversation messages
+ const availableActions = this.shyguy.getAvailableActions();
+ this.baseSystemPrompt = `You are ShyGuy, a socially awkward character at a homecoming party. You are shy.
+ The wingman progressively makes you more confident, and if you drink alcohol.
+ You know the wingman, he is your friend. You are talking to him.
+ You are willing to have a little alcohol.
+ The action should correspond to the dialogue.
+ Your responses should be in JSON format with two fields:
+ enum action {
+ ${Object.keys(availableActions).join(", ")}
+ }
+ for example :
+ {
+ "action": "go_bar",
+ "dialogue": "Okay, let's go to the bar."
+ }`;
+ }
+
+ getSystemPrompt() {
+ let addToPrompt = "";
+ // Add historical conversation context if it exists
+ if (this.shyguy.conversation_history) {
+ addToPrompt += `\n\nPrevious interactions:\n${this.shyguy.conversation_history}`;
+ }
+ if (this.shyguy.num_beers > 0) {
+ addToPrompt += `You are likely to go to the DJ or sister.\n`;
+ }
+ if (this.shyguy.num_beers > 2) {
+ addToPrompt += `You are likely to go to the DJ or sister. You are very likely to go to the girl. You feel confident about talking to the girl.\n`;
+ }
+ if (this.shyguy.num_beers > 3) {
+ addToPrompt += `You are drunk and you start talking about how bad you feel. You are annoying. You talk about algorithms and then how bad you feel.\n`;
+ }
+ if (this.shyguy.courage < 2) {
+ addToPrompt += `You are shy. However, you believe wingman.`;
+ }
+ if (this.shyguy.courage >= 2) {
+ addToPrompt += `You talk normally.`;
+ }
+ if (this.shyguy.courage > 5) {
+ addToPrompt += `You are self-confident.`;
+ }
+ if (this.shyguy.courage > 8) {
+ addToPrompt += `You are too self-confident and annoying.`;
+ }
+ addToPrompt += `\nYou drank ${this.shyguy.num_beers} beers. Your courage is ${this.shyguy.courage}.`;
+ if (this.currentConversation.length > 0) {
+ addToPrompt += `\n\nCurrent conversation context:\n`;
+ this.currentConversation.forEach(msg => {
+ addToPrompt += `${msg.role}: ${msg.content}\n`;
+ });
+ }
+
+ return this.baseSystemPrompt + addToPrompt;
+ }
+
+ addToCurrentConversation(role, content) {
+ this.currentConversation.push({
+ role: role,
+ content: content
+ });
+ }
+
+ clearCurrentConversation() {
+ this.currentConversation = [];
+ }
+
+ async getShyGuyResponse(player_message) {
+ try {
+ const availableActions = this.shyguy.getAvailableActions();
+ const actionsPrompt = `\nYour currently available actions are: ${Object.keys(availableActions)
+ .map((action) => `\n- ${action}: ${availableActions[action].description}`)
+ .join("")}`;
+
+ // Add the situation to current conversation
+ this.addToCurrentConversation('wingman', player_message);
+
+ const fullPrompt = this.getSystemPrompt() + actionsPrompt;
+ const response = await this.llm.getJsonCompletion(fullPrompt, player_message);
+
+ // Add ShyGuy's response to current conversation
+ this.addToCurrentConversation('shyguy', response.dialogue);
+
+ // Add to overall conversation history
+ this.shyguy.conversation_history += `\nShyguy: ${response.dialogue}\n`;
+
+ // Validate response format
+ if (!response.action || !response.dialogue) {
+ throw new Error("Invalid response format from LLM");
+ }
+
+ return {
+ action: response.action,
+ dialogue: response.dialogue,
+ };
+ } catch (error) {
+ console.error("ShyGuy Response Error:", error);
+ return {
+ action: "go_home",
+ dialogue: "Umm... I... uh...",
+ };
+ }
+ }
+}
diff --git a/src/speech_to_text.js b/src/speech_to_text.js
new file mode 100644
index 0000000000000000000000000000000000000000..2f239656cf4e4532e1728689466f49ff50314505
--- /dev/null
+++ b/src/speech_to_text.js
@@ -0,0 +1,63 @@
+export class SpeechToTextClient {
+ constructor() {
+ this.apiKey = "";
+ this.isRecording = false;
+ this.mediaRecorder = null;
+ this.audioChunks = [];
+ }
+
+ async startRecording() {
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ this.mediaRecorder = new MediaRecorder(stream);
+ this.audioChunks = [];
+
+ this.mediaRecorder.ondataavailable = (event) => {
+ this.audioChunks.push(event.data);
+ };
+
+ this.mediaRecorder.start();
+ this.isRecording = true;
+ } catch (error) {
+ console.error("Error starting recording:", error);
+ throw error;
+ }
+ }
+
+ async stopRecording() {
+ return new Promise((resolve, reject) => {
+ this.mediaRecorder.onstop = async () => {
+ try {
+ const audioBlob = new Blob(this.audioChunks, { type: "audio/webm" });
+ const result = await this.transcribeAudio(audioBlob);
+ resolve(result);
+ } catch (error) {
+ reject(error);
+ }
+ };
+
+ this.mediaRecorder.stop();
+ this.isRecording = false;
+ this.mediaRecorder.stream.getTracks().forEach((track) => track.stop());
+ });
+ }
+
+ async transcribeAudio(audioBlob) {
+ try {
+ const response = await fetch("https://q86j6jmwc3jujazp.us-east-1.aws.endpoints.huggingface.cloud", {
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${this.apiKey}`,
+ "Content-Type": "audio/webm",
+ },
+ method: "POST",
+ body: audioBlob,
+ });
+ const result = await response.json();
+ return result;
+ } catch (error) {
+ console.error("Error transcribing audio:", error);
+ throw error;
+ }
+ }
+}
diff --git a/src/story_engine.js b/src/story_engine.js
new file mode 100644
index 0000000000000000000000000000000000000000..2eb322d2c64d2dbff7cf356cdc4d8131acb699e4
--- /dev/null
+++ b/src/story_engine.js
@@ -0,0 +1,290 @@
+import { SISTER_LABEL, GIRL_LABEL, BAR_LABEL, DJ_LABEL, SHYGUY_LABEL } from "./constants.js";
+import { ConversationLLM } from "./conversation_llm.js";
+
+export function nameToLabel(name) {
+ switch (name) {
+ case "Jessica's sister":
+ return SISTER_LABEL;
+ case "bartender":
+ return BAR_LABEL;
+ case "DJ":
+ return DJ_LABEL;
+ case "Jessica":
+ return GIRL_LABEL;
+ case "Shyguy":
+ return SHYGUY_LABEL;
+ default:
+ return "";
+ }
+}
+
+class Bar {
+ constructor() {
+ this.name = "bartender";
+ this.situation_prompt = "This conversation happens at the bar.";
+ this.personality = "This is the bartender. He always offers triple vodka in his first answer. Then he is helpful, but he says that he has a crush on Jessica's sister";
+ // this.past_conversation = "";
+ this.imgpath = "assets/assets/barman.jpeg";
+ this.output_format_prompt = "";
+ this.functionDescriptions = [
+ {
+ key: "analyzeConsumption",
+ description:
+ "Analyze the conversation to determine how many beers were consumed. Also if heavy alcohol was consumed",
+ parameters: {
+ num_beers: {
+ type: "number",
+ description: "Total number of beers mentioned as being consumed in the conversation",
+ },
+ heavy_alcohol: {
+ type: "boolean",
+ description: "If heavy alcohol was consumed, True",
+ },
+ },
+ },
+ ];
+ this.functionPrompt = "";
+ }
+
+ getSystemPrompt() {
+ return `${this.personality}.`;
+ }
+}
+
+class DJ {
+ constructor() {
+ this.name = "DJ";
+ this.situation_prompt = "This is a conversation with the DJ.";
+ this.personality =
+ "This is the DJ. He is funny. He offers drugs to the shyguy. He can only play songs from the options [Let it be, Call me maybe, Shape of you]. If asked to play a song, he will play it and not propose any other song.";
+ // this.past_conversation = "";
+ this.imgpath = "assets/assets/dj.jpeg";
+ this.output_format_prompt = "";
+ this.functionDescriptions = [
+ {
+ key: "chooseSongToPlay",
+ description: "Analyze the conversation to choose which song to play",
+ parameters: {
+ song: {
+ type: "string",
+ description: "The song to play",
+ },
+ },
+ },
+ ];
+ this.functionPrompt =
+ "Based on the conversation, choose a song from options [Let it be, Call me maybe, Shape of you]";
+ }
+
+ getSystemPrompt() {
+ return `${this.personality}`;
+ }
+}
+
+class Sister {
+ constructor() {
+ // Initialize Sister properties
+ this.name = "Jessica's sister";
+ this.mood = 1;
+ this.situation_prompt = "This is a conversation with the sister of Jessica.";
+ this.personality =
+ "This is the sister of Jessica. She is a deeply religious Christian. Her first answer is rude. Her second answer is about christianity. First answer is helpful. If asked about the favourite song of the girl, she says happily that it is 'Call me maybe'.";
+ // this.past_conversation = "";
+ this.imgpath = "assets/assets/sister.jpeg";
+ this.functionDescriptions = [
+ {
+ key: "analyzeMood",
+ description: "Mood of the sister and gameover",
+ parameters: {
+ mood: {
+ type: "number",
+ description: "The mood of the sister on the scale of 1 to 10",
+ },
+ game_over: {
+ type: "boolean",
+ description: "Whether the game is over",
+ },
+ },
+ },
+ ];
+ this.functionPrompt =
+ "Analyze the conversation to determine the mood of the sister on the scale 1 to 10, based of how nice the conversation was. Also determine if the game is over. The game is over if shyguy was mean to the sister.";
+ }
+
+ getSystemPrompt() {
+ return `${this.personality}. Her mood is ${this.mood} on the level 1 to 10. If the mood is low, she will be rude. If the mood is high, she will be helpful.`;
+ }
+}
+
+class Girl {
+ constructor(shyguy) {
+ this.name = "Jessica";
+ this.situation_prompt = "This is a conversation with the Jessica. She is the girl that shyguy likes.";
+ this.personality = "This is Jessica. She is shy but nice. She likes the song 'Call me maybe'.";
+ this.imgpath = "assets/assets/jessica.jpeg";
+ this.output_format_prompt = "";
+ this.shyguy = shyguy;
+ this.functionDescriptions = [
+ {
+ key: "analyzeLiking",
+ description: "Analyze if Jessica is happy with the conversation or not",
+ parameters: {
+ likes_shyguy: {
+ type: "boolean",
+ description: "If Jessica is happy with the conversation, True",
+ },
+ },
+ },
+ ];
+ this.functionPrompt = "Analyze if Jessica is happy with the conversation or not";
+ }
+
+ getSystemPrompt() {
+ if (this.shyguy.song_playing === "Call me maybe") {
+ return `${this.personality}. She is very happy with the song playing. The first thing she says is that she really likes the music. Therefore she is nice and she likes shyguy. However, if he talks about algorithms, she does not like it and she becomes mean. Also if he talks with a lot of hesitation, she does not like it and she becomes mean.`;
+ } else {
+ return `${this.personality}. She does not like the song that shyguy is playing. The first thing she says is that the song is terrible. Then she is mean all the time.`;
+ }
+ }
+}
+
+class Wingman {
+ constructor() {
+ this.name = "wingman";
+ this.situation_prompt = "This conversation happens with your wingman at the party.";
+ this.personality = "This is your wingman. He is experienced with dating and gives practical advice. He's supportive but direct, and always encourages confidence without being aggressive.";
+ this.imgpath = "assets/assets/wingman.jpeg";
+ this.output_format_prompt = "";
+ this.functionDescriptions = [
+ {
+ key: "analyzeAdvice",
+ description: "Analyze the quality and impact of the conversation with the wingman",
+ parameters: {
+ confidence_boost: {
+ type: "number",
+ description: "How much confidence boost received on scale 1-5",
+ },
+ good_advice: {
+ type: "boolean",
+ description: "If practical, actionable advice was given, True",
+ },
+ },
+ },
+ ];
+ this.functionPrompt = "Analyze how helpful the wingman's advice was and how much it boosted confidence";
+ }
+
+ getSystemPrompt() {
+ return `${this.personality}.`;
+ }
+}
+
+
+
+export class StoryEngine {
+ constructor(shyguy) {
+ // Initialize story engine properties with provided shyguy instance
+ this.shyguy = shyguy;
+ this.sister = new Sister();
+ this.bar = new Bar();
+ this.dj = new DJ();
+ this.girl = new Girl(shyguy);
+ }
+
+ async onEncounter(entity) {
+ switch (entity) {
+ case SISTER_LABEL:
+ return this.generalInteraction(this.sister);
+ case BAR_LABEL:
+ return this.generalInteraction(this.bar);
+ case GIRL_LABEL:
+ return this.generalInteraction(this.girl);
+ case DJ_LABEL:
+ return this.generalInteraction(this.dj);
+ default:
+ console.log("Unknown entity encountered");
+ }
+ }
+
+ async generalInteraction(targetEntity) {
+ await this.shyguy.learnLesson(targetEntity.name);
+ console.log("Lesson for " + targetEntity.name + this.shyguy.lessons_learned);
+ const conversation_llm = new ConversationLLM(
+ "Shyguy",
+ targetEntity.name,
+ this.shyguy.getSystemPrompt(),
+ targetEntity.getSystemPrompt(),
+ targetEntity.situation_prompt,
+ targetEntity.output_format_prompt,
+ targetEntity.functionDescriptions,
+ targetEntity.functionPrompt
+ );
+ const conversation_output = await conversation_llm.generateConversation(6);
+ const conversation = conversation_output.conversation;
+
+ // Append the conversation to shyguy's history
+ this.shyguy.conversation_history += `\nConversation with ${targetEntity.name}:\n${conversation}\n`;
+
+ let gameOver = this.decideGameOver(conversation_output.analysis.parameters.game_over);
+ let gameSuccesful = this.decideGameSuccesful(conversation_output.analysis.parameters.likes_shyguy);
+ if (targetEntity.name === "Jessica" && !gameSuccesful) {
+ gameOver = true;
+ }
+ console.log("gameOver: " + gameOver);
+ console.log("gameSuccesful: " + gameSuccesful);
+ console.log(conversation_output);
+
+ this.updateStates(conversation_output.analysis, targetEntity.name);
+ console.log("shyguy num_beers: " + this.shyguy.num_beers);
+
+ return {
+ conversation: conversation,
+ char1imgpath: this.shyguy.imgpath,
+ char2imgpath: targetEntity.imgpath,
+ gameOver: gameOver,
+ gameSuccesful: gameSuccesful,
+ };
+ }
+
+ decideGameOver(gameOverParameter) {
+ let gameOver = false;
+ if (gameOverParameter === "none") {
+ gameOver = false;
+ } else if (gameOverParameter === true) {
+ gameOver = true;
+ } else {
+ gameOver = false;
+ }
+ return gameOver;
+ }
+
+ decideGameSuccesful(likesShyguy) {
+ let gameSuccesful = false;
+ if (likesShyguy) {
+ gameSuccesful = true;
+ } else {
+ gameSuccesful = false;
+ }
+ return gameSuccesful;
+ }
+
+ updateStates(conversation_analysis, targetName) {
+ if (targetName === "Jessica's sister") {
+ if (conversation_analysis.parameters.mood !== "none") {
+ this.sister.mood = conversation_analysis.parameters.mood;
+ }
+ } else if (targetName === "bartender") {
+ if (conversation_analysis.parameters.num_beers !== "none") {
+ this.shyguy.num_beers += Number(conversation_analysis.parameters.num_beers);
+ this.shyguy.courage += 2 * Number(conversation_analysis.parameters.num_beers);
+ this.shyguy.num_beers += 3 * Number(conversation_analysis.parameters.heavy_alcohol === true);
+ console.log("Shyguy num_beers inside updateStates: " + this.shyguy.num_beers);
+ this.shyguy.courage += 3 * Number(conversation_analysis.parameters.heavy_alcohol);
+ }
+ } else if (targetName === "DJ") {
+ if (conversation_analysis.parameters.song !== "none") {
+ this.shyguy.song_playing = conversation_analysis.parameters.song;
+ }
+ }
+ }
+}
diff --git a/styles/style.css b/styles/style.css
new file mode 100644
index 0000000000000000000000000000000000000000..3742928c2cdf2ad20906d9b757658fe87ab48241
--- /dev/null
+++ b/styles/style.css
@@ -0,0 +1,560 @@
+body {
+ margin: 0;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background: #f0f0f0;
+ font-family: "Press Start 2P", sans-serif;
+}
+
+.container {
+ display: flex;
+ gap: 20px;
+ max-width: 1600px;
+ margin: 0 auto;
+}
+
+.game-section {
+ flex-shrink: 0;
+}
+
+#gameCanvas {
+ border: 1px solid black;
+ display: block;
+}
+
+.chat-section {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-width: 300px;
+ max-width: 500px;
+ height: 640px;
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.chat-messages {
+ flex: 1;
+ padding: 20px;
+ overflow-y: auto;
+ border-bottom: 1px solid #eee;
+ scroll-behavior: smooth;
+ max-height: calc(100% - 180px);
+ font-size: 12px;
+ line-height: 1.6;
+}
+
+.input-area {
+ padding: 20px;
+ background: #f8f8f8;
+ border-radius: 0 0 8px 8px;
+ min-height: 140px;
+ display: flex;
+ flex-direction: column;
+}
+
+#messageInput {
+ width: 100%;
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ resize: none;
+ margin-bottom: 10px;
+ font-family: "Press Start 2P", sans-serif;
+ font-size: 12px;
+ height: 80px;
+ box-sizing: border-box;
+ line-height: 1.5;
+}
+
+.button-group {
+ display: flex;
+ gap: 10px;
+}
+
+.send-button,
+.mic-button {
+ padding: 8px 16px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.send-button:disabled,
+.mic-button:disabled {
+ background-color: #cccccc;
+ cursor: not-allowed;
+ opacity: 0.7;
+}
+
+.send-button:disabled:hover,
+.mic-button:disabled:hover {
+ background-color: #cccccc;
+ transform: none;
+}
+
+.send-button {
+ background-color: #007bff;
+ color: white;
+ flex: 1;
+}
+
+.send-button:hover:not(:disabled) {
+ background-color: #0056b3;
+}
+
+.mic-button {
+ background-color: #6c757d;
+ color: white;
+ width: 40px;
+ padding: 8px;
+ transition: background-color 0.3s, transform 0.2s;
+}
+
+.mic-button:hover:not(:disabled) {
+ background-color: #5a6268;
+}
+
+.mic-button.recording {
+ background-color: #dc3545;
+ animation: pulse 1.5s infinite;
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.05);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
+
+/* View Management */
+.game-view,
+.dialogue-view {
+ display: none;
+ flex: 1;
+ padding: 20px;
+ background: #f0f0f0;
+}
+
+.active {
+ display: block;
+}
+
+/* Dialogue View Styles */
+.dialogue-container {
+ height: 100%;
+ padding: 20px;
+}
+
+.character-section {
+ height: 100%;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 20px;
+}
+
+.character-img {
+ width: 100%;
+ height: 100%;
+ display: none;
+ object-fit: cover;
+}
+
+.character {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 400px;
+ height: 600px;
+ background: #ddd;
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.dialogue-box {
+ flex: 1;
+ height: 600px;
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ max-height: 600px;
+}
+
+.dialogue-messages {
+ flex: 1;
+ padding: 20px;
+ overflow-y: auto;
+ font-size: 14px;
+ line-height: 1.6;
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ height: calc(100% - 80px);
+}
+
+/* Status Bar */
+.status-bar {
+ width: 100%;
+ padding: 10px 20px;
+ background: rgba(0, 0, 0, 0.8);
+ color: #fff;
+ box-sizing: border-box;
+}
+
+.status-text {
+ font-size: 12px;
+}
+
+/* Debug Controls */
+.debug-controls {
+ width: 100%;
+ padding: 20px;
+ background: #333;
+ display: flex;
+ gap: 10px;
+ justify-content: center;
+ box-sizing: border-box;
+}
+
+.view-controls,
+.game-controls {
+ display: flex;
+ gap: 5px;
+}
+
+.debug-button {
+ padding: 8px 12px;
+ border-radius: 4px;
+ border: 1px solid #ccc;
+ background: #f0f0f0;
+ cursor: pointer;
+ font-family: "Press Start 2P", monospace;
+ font-size: 12px;
+}
+
+.debug-button:hover {
+ background: #e0e0e0;
+}
+
+/* Chat Message Styles */
+.chat-message {
+ margin-bottom: 15px;
+ display: flex;
+ animation: fadeIn 0.3s ease-in;
+}
+
+.chat-message.left-user {
+ justify-content: flex-start;
+}
+
+.chat-message.right-user {
+ justify-content: flex-end;
+}
+
+.message-bubble {
+ max-width: 70%;
+ padding: 10px 15px;
+ border-radius: 15px;
+ font-size: 12px;
+ line-height: 1.6;
+}
+
+.left-user .message-bubble {
+ background-color: #e9ecef;
+ color: #000;
+ border-bottom-left-radius: 5px;
+}
+
+.right-user .message-bubble {
+ background-color: #007bff;
+ color: white;
+ border-bottom-right-radius: 5px;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* Game Over View Styles */
+.game-over-view {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #000;
+}
+
+.game-over-view.active {
+ display: block;
+}
+
+.game-over-content {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 40px;
+}
+
+.game-over-image {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ opacity: 0.8;
+}
+
+.game-over-controls {
+ position: absolute;
+ bottom: 300px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 1;
+}
+
+.play-again-button {
+ padding: 15px 30px;
+ font-size: 24px;
+ background-color: #ff4444;
+ color: white;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: transform 0.2s, background-color 0.2s;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+ font-family: "Press Start 2P", monospace;
+}
+
+.play-again-button:hover {
+ background-color: #ff6666;
+ transform: translateY(-2px);
+}
+
+.play-again-button:active {
+ transform: translateY(0);
+}
+
+/* Dialogue Controls Styles */
+.dialogue-controls {
+ position: static;
+ transform: none;
+ padding: 20px;
+ text-align: center;
+ background: white;
+ border-radius: 0 0 8px 8px;
+ height: 80px;
+ box-sizing: border-box;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+/* Continue and Next Button Styles */
+.continue-button,
+.next-button {
+ padding: 15px 30px;
+ font-size: 14px;
+ background-color: #007bff;
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: background-color 0.2s, transform 0.2s;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ font-family: "Press Start 2P", monospace;
+}
+
+.continue-button:hover,
+.next-button:hover {
+ background-color: #0056b3;
+ transform: translateY(-2px);
+}
+
+.continue-button:active,
+.next-button:active {
+ transform: translateY(0);
+}
+
+/* Hide next button by default */
+.next-button {
+ display: none;
+}
+
+.game-over-text {
+ position: relative;
+ z-index: 1;
+ color: white;
+ font-size: 28px;
+ text-align: center;
+ text-shadow: 4px 4px 0 #000, -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000, 2px 2px 0 #000;
+ margin-bottom: 40px;
+ margin-top: -200px;
+ animation: pixelate 0.5s steps(5) forwards;
+}
+
+@keyframes pixelate {
+ 0% {
+ transform: scale(0.8);
+ opacity: 0;
+ }
+ 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+/* Add pixel art font to all buttons */
+.debug-button,
+.send-button,
+.mic-button,
+.continue-button,
+.next-button {
+ font-family: "Press Start 2P", monospace;
+ font-size: 12px;
+}
+
+/* Intro View Styles */
+.intro-view {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #000;
+ z-index: 1000;
+}
+
+.intro-view.active {
+ display: block;
+}
+
+.intro-content {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 40px;
+}
+
+.intro-image {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: black;
+}
+
+.start-game-button {
+ position: absolute;
+ bottom: 100px;
+ z-index: 1;
+ padding: 20px 40px;
+ font-size: 24px;
+ background-color: rgba(255, 68, 68, 0.9);
+ color: white;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: transform 0.2s, background-color 0.2s;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
+ font-family: "Press Start 2P", monospace;
+ animation: float 3s ease-in-out infinite;
+}
+
+.start-game-button:hover {
+ background-color: rgba(255, 102, 102, 0.9);
+ transform: translateY(-2px) scale(1.05);
+}
+
+.start-game-button:active {
+ transform: translateY(0) scale(1);
+}
+
+@keyframes float {
+ 0% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-20px);
+ }
+ 100% {
+ transform: translateY(0);
+ }
+}
+
+/* Add this to flip the left character image */
+#leftCharacterImg {
+ transform: scaleX(-1); /* This will mirror the image horizontally */
+}
+
+#rightCharacterImg {
+ transform: scaleX(-1); /* This will mirror the image horizontally */
+}
+
+/* Loading indicator styles */
+.loading-indicator {
+ display: none;
+ padding: 20px;
+ text-align: center;
+ color: #666;
+ font-size: 14px;
+}
+
+.loading-dots::after {
+ content: "";
+ animation: dots 1.5s steps(5, end) infinite;
+}
+
+@keyframes dots {
+ 0%,
+ 20% {
+ content: "";
+ }
+ 40% {
+ content: ".";
+ }
+ 60% {
+ content: "..";
+ }
+ 80%,
+ 100% {
+ content: "...";
+ }
+}
+
+/* Add this to the existing .dialogue-box styles */
+.dialogue-box.loading .loading-indicator {
+ display: block;
+}
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..459adbbae18f73736f68abec8a8fef2061864187
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,5 @@
+export default {
+ server: {
+ open: true,
+ },
+};