Compare commits
10 Commits
845c4de6ee
...
505b507aa4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
505b507aa4 | ||
|
|
fd145fe48f | ||
|
|
2d932c5a42 | ||
|
|
fc4e4807fe | ||
|
|
1e37ad41e5 | ||
|
|
dac0d37d53 | ||
|
|
0ddf026543 | ||
|
|
39153ec658 | ||
|
|
0701f1274a | ||
|
|
2f04d4f905 |
160
README.md
@@ -1,4 +1,158 @@
|
||||
Commodore 64 by Jason Toff [CC-BY] via Poly Pizza
|
||||
https://freefrontend.com/code/procedural-3d-endless-runner-game-2026-02-24/?utm_source=chatgpt.com
|
||||
using perplexity, gemini, google ai studio
|
||||
# Overload: A Multitasking Cognitive Training Game
|
||||
|
||||
**Student:** Makhabbat (ID: 20240935)
|
||||
**Department:** Industrial Design, KAIST
|
||||
**Contact:** [mako1004@kaist.ac.kr](mailto:mako1004@kaist.ac.kr)
|
||||
**Links:** [Git Repository](https://github.com/yoondzhy/infinite-runner-multitasking-game) | [Video Demo](https://youtu.be/vDffRuz51hA)
|
||||
|
||||
## Project Overview
|
||||
|
||||
Overload is an atypical, **cognitively demanding** *infinite runner* inspired by digital therapeutics (DTx) designed for children with ADHD.
|
||||
|
||||
During the Zhejiang University Summer Program, I had the chance to play a clinically tested ADHD-focused game that significantly improved sustained attention. However, the **game isn’t publicly available** and information about it exists only inside [research papers](https://pubmed.ncbi.nlm.nih.gov/41490776/).
|
||||
|
||||
This project is my attempt to recreate that idea for myself—as someone who often drifts and wants a way to actively train attention through gameplay. The result is an extreme multitasking runner built with **Three.js**, designed to **overload** and strengthen attentional control, working memory, and task-switching.
|
||||
|
||||
## Core Concept
|
||||
|
||||
Overload is not a typical runner with 3 lanes—it uses **5 lanes**, faster pacing (over time), and continuous task switching. Players must constantly track rules and obstacle avoidance at the same time, rewarding precision and penalizing mind-wandering. The goal is to maintain total cognitive engagement.
|
||||
|
||||
## Gameplay Description
|
||||
|
||||
### 1. Five-Lane Movement
|
||||
* Move between 5 horizontal lanes instead of the standard 3.
|
||||
* Forces higher spatial attention and faster reaction times.
|
||||
|
||||
### 2. Instruction Phase & Target Collecting
|
||||
At the start of each run:
|
||||
* The player is shown a **specific type of target item** they must collect.
|
||||
* Throughout the game, objects fall randomly on the screen.
|
||||
* Players must collect only the instructed items and avoid irrelevant ones using a hammer (the cursor).
|
||||
* **Lives:** 5 lives are given for target hitting (top-left corner). Missing or hitting wrong targets reduces these.
|
||||
* *Note: These lives are separate from obstacle collisions, which result in an immediate game over.*
|
||||
* **Booster:** A special booster appears every 10,000 points, doubling the general score and target points for 20 seconds.
|
||||
|
||||
### 3. Obstacles
|
||||
* **Small Computers:** Can be avoided or jumped over.
|
||||
* **Tall Claw Machines:** Cannot be jumped over; must be dodged.
|
||||
* Hitting any obstacle leads to an immediate game over.
|
||||
|
||||
---
|
||||
|
||||
## Library Used
|
||||
|
||||
* **Three.js:** Used for all 3D rendering, including the world environment, character models, and skeletal animations.
|
||||
* **p5.js:** Used for the 2D HUD overlay, fruit/target rendering, and the interactive hammer mechanics.
|
||||
|
||||
---
|
||||
|
||||
## The Core Organization (MVC Pattern)
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td valign="top" width="410">
|
||||
<img src="image.png" alt="MVC Diagram" width="400" style="margin-right: 10px;">
|
||||
</td>
|
||||
<td valign="top">
|
||||
|
||||
**The Model (State)**: Managed in `App.svelte` and `GameManager.js`. This holds the "truth" about the game: player health, current score, active target type, and game speed.
|
||||
|
||||
**The View (Rendering)**: Split into two layers:
|
||||
|
||||
1. **Three.js Layer** (`WorldScene.js`, `ObstacleFactory.js`): Handles the physical 3D world.
|
||||
|
||||
2. **p5.js Layer** (`p5overlay.js`): Handles the 2D cognitive task overlay.
|
||||
|
||||
**The Controller (Logic)**: Managed in `GameController.js`. It listens for user inputs (A/W/D, arrows, and spacebar) and updates the Model accordingly.
|
||||
|
||||
***
|
||||
|
||||
|
||||
To ensure that only a single instance of the game timer (`uTime`), score, and active speed exists at any given time, I implemented a **Singleton approach**. This was also utilized for the `glbCache` to optimize memory management by preventing redundant asset loading. Additionally, I prioritized the use of **Higher-Order Functions** over traditional loops to ensure more declarative code.
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Key Functions and Modules
|
||||
|
||||
* **GameManager.js / GameController.js (The Brain):**
|
||||
* `updatePhysics()`: Calculates gravity, jump velocity, and lane-shifting interpolation.
|
||||
* `processInteraction()`: Determines if a mouse click on a fruit was "Correct" or "Incorrect."
|
||||
* `updateGameFlow()`: Manages transitions from Landing Page to Instructions and Game Loop.
|
||||
* **ObstacleFactory.js:** Uses a factory pattern to load obstacles based on spawn probability (tall obstacles are rarer).
|
||||
* **Reactive UI:** The HUD and Leaderboard "observe" the state. When values change, **Svelte automatically updates** the HTML without the game engine talking to the DOM.
|
||||
* **Token System:** Used in `swapCharacter()` to prevent "race conditions" between animations (e.g., jumping vs. running).
|
||||
* **Bridge Pattern:** Since p5.js and Three.js are separate engines, they communicate via a **Shared State Object**.
|
||||
|
||||
---
|
||||
|
||||
## Challenges
|
||||
|
||||
1. **Working with GLB Files:**
|
||||
This was my first time using a 3D .glb model, so figuring out how to load it, play different animations, and switch between them smoothly took a while. I kept running into random issues with the skeleton and animation states, and it was a lot of trial and error until things finally worked the way I wanted.
|
||||
2. **Infinite World & Grass Rendering:**
|
||||
I originally wanted thin, pretty grass that moved nicely with wind. But every version I tried ended up breaking — the grass kept showing weird horizontal lines, especially when I attempted an infinite world where the player stands still and the world generates forward. I even tried using different grass shaders and examples from GitHub, but none of them fixed the glitch.
|
||||
Eventually I changed the idea: instead of generating an infinite world, I made three ground sections that loop as the player moves. Surprisingly, this not only simplified things but also completely fixed the grass issue.
|
||||
3. **The "Gate Test" Idea:**
|
||||
I also wanted to add a gate section where a question pops up and five gates appear, and the player has to go through the correct one. But adding this meant removing obstacles for a while, updating lives/score logic, and dealing with a bunch of timing issues. After testing, I realized it made the game too complicated and stressful for my friends who tried it, so I decided to keep the game simpler for now.
|
||||
|
||||
# Resources Used
|
||||
As it was hard to start from scratch I used [Commodore 64 by Jason Toff [CC-BY]](https://freefrontend.com/code/procedural-3d-endless-runner-game-2026-02-24/?utm_source=chatgpt.com) to understand how this would be carried out, but eventually ended up changing most of the things.
|
||||
|
||||
I used free 3D models as making some by myseld would take a lot of time:
|
||||
"Bird in a claw machine" (https://skfb.ly/otMUN) by Tin Pui-yiu is licensed under CC Attribution-NonCommercial-ShareAlike (http://creativecommons.org/licenses/by-nc-sa/4.0/).
|
||||
|
||||
Grass taken from https://github.com/DavisHYang/Grass.
|
||||
|
||||
Also downloaded the character with animation from [mixamo.com](https://www.mixamo.com/#/?page=2&type=Character) and converted to glb from fbx file.
|
||||
Overall, I used 4 animations of the main character: dancing(landing page), running, jumping, falling.
|
||||
|
||||
The Landing page logo and background were AI generated by Gemini, while the targets were taken from Pinterest where I couldn't find specific owner of paintings.
|
||||
To carry out this project, I made use of multiple AI agents: mainly google gemini, perplexity for some debugging of stubborn issues and google ai studio for reference. The list of prompts used are presented below.
|
||||
|
||||
<details>
|
||||
<summary>Click to view AI prompts used</summary>
|
||||
|
||||
1. It kinda works but it's just the animation keeps looping in the weirdest unnatural way possible, not like I wanted. Also character is running towards me not the lanes, also when jumping he just disappears.
|
||||
2. The running model appears perfectly at place, you don't need to change the position! Maybe when he's jumping there are issues but these fixes you are proposing are actually making my runner sink, while jump still doesn't appear. The problem is in jump positions or whatever.
|
||||
3. Let's just add an animation of the character falling, which is another GLB named "Falling Back Death.glb" and just add that when hit, do the flash effect, give 1-2 seconds before showing the gameover that's it!
|
||||
4. Copy the grass from here: [https://github.com/DavisHYang/Grass]. Make the grass much much thinner, and only around the lanes not on it, and it also has to move with obstacles to give that illusion that we're running.
|
||||
5. The grass is not evenly distributed along the entire floor (keep in mind to not go into the lanes). Also there are some lines amidst grass weirdly. Don't add clouds just yet, and you changed the player's position I guess it's off; don't change it nor the scale from the original.
|
||||
6. The lines are actually gone!! But now the camera moves sides when I change lanes and grass stops generating like it just stops at some point.
|
||||
7. Can you make the clouds with fuzzy texture instead of those hard 3D edges? And make each clustered with one or two big spheres and a collaboration of smaller ones too.
|
||||
8. Make the balls smaller, and more balls per cluster, and more lighting on top so there aren't shadows on the clouds. Maybe place clouds a little higher cause it seems too close to the ground.
|
||||
9. This is how I fixed it: now instead of spheres, let's make the clouds out of clusters of squares.
|
||||
10. Let's change the entire strategy and make the clouds Minecraft-like, and instead of having horizontal direction make it vertical directed clouds.
|
||||
11. I don't know how to fix its position because the computer is crooked, 20% in the floor, and I see the back not front.
|
||||
12. I need A, W, D keyboard to work on the same level as arrows.
|
||||
13. Now using p5.js we need stuff falling from sky randomly, and a first 3 seconds screen where game shows which ones to collect. The mouse shape should now be a 3D hammer, so I need to find a GLB of a hammer, and around 3 types of things to collect. Only one will be shown and it will need to be collected by pressing, while others also fall but hitting them will result in punishments in attentiveness. If too much wrong ones were hit, attentiveness tanks, making player lose.
|
||||
14. Let's make the hammer 2D too.
|
||||
15. Landing page: first of all, the background is gonna be a 3D forest, and on the right there will be our runner, making some pose, and moving all the time (I will need to download some more GLBs). On the left we will do some cool name and big play button, plus a small description of the game.
|
||||
16. Why can't I see my GLB here?
|
||||
17. I also don't even see the OVERLOAD logo, and yes, the GLB is nowhere to be seen. For some reason the "NeuroRunner Ready to Focus" window is also here. The background is not grass at all, it's just blue.
|
||||
18. Add overload_trans.png image to the code, on top of the button, and give it some movement (just some wiggling 2D is enough).
|
||||
19. Can you bring back the instructions (only what type of targets they have to collect) that appear for a few seconds in the beginning?
|
||||
20. Now we have to make speed progressive, like it starts slow at first but then it gets faster, but at some point it can't get faster than that cause that would've been chaotic right.
|
||||
21. My obstacles disappeared after I reached around 4000 in scores.
|
||||
22. The hearts have to decrease when you hit the wrong target and when you miss the right ones as well, and when hearts die just make them grey and when you're out of hearts you die.
|
||||
23. It keeps making me die at random spots when the hearts aren't even over. Heart greying out is not working it just disappears. Should we just remove whatever that attentiveness meter was?
|
||||
24. Why didn't my score update when i got a higher score later?
|
||||
25. Can you please show the parts that I have to fix?
|
||||
26. How do I clear the leaderboard now? I have 3 duplicate entries, I can't check if the new logic works or not.
|
||||
27. Add gates like in the image I attached—they're kinda an opaque surface to go through with answers shown. So about every 5000 score, 5 gates appear on 5 lanes. Before they appear the question will appear in the top center, and before and during the gates' appearance, all obstacles disappear for some time so the player can go through the gate with the right answer without stumbling upon an obstacle.
|
||||
28. Gates are not moving with the world/ground so I can't go through them at all and I don't even see the mission text on top. The obstacles are still there.
|
||||
29. We add sun with clouds, adjust the lighting and then after 7k score smooth change to moon and dark sky and adjust lighting accordingly as well.
|
||||
30. Make the moon and sun not perfectly round but low-poly textured round. Also at night add lighting from the camera's standpoint because it gets too dark.
|
||||
31. Let's make a visual representation of that next score as yellow and bigger +100.
|
||||
32. I have an issue... the page before game starts for 3 seconds to show the target? It can't be seen when first entered through the landing page because countdown counts from the time screen loads not from when you pressed play and entered the page. But when you press retry it appears properly.
|
||||
33. List of improvements we will be implementing: showing on the side which fruit we are collecting so we always know without forgetting; add more types of obstacles so staying on one line throughout the whole game is impossible; maybe changing environments from time to time like adding a city or something; adding golden limited edition fruits falling that will pass quickly but for example boost your scores by 2 for some time. Let's start with adding one more type of obstacle that is bigger than existing computer, any suggestions?
|
||||
34. New obstacle has to be about 4 times rarer than the main computer obstacle.
|
||||
35. These proportions worked, but I still can jump through the claw machine, which I don't want to be possible.
|
||||
36. When I was in star multiplier mode, the score from hitting the right target still was 100.
|
||||
37. I don't know what I did but now when I hit the right target all floating images freeze and hammer disappears.
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
<!--  -->
|
||||
|
||||
|
||||
614
package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||
"gh-pages": "^6.3.0",
|
||||
"svelte": "^5.55.4",
|
||||
"vite": "^8.0.9"
|
||||
}
|
||||
@@ -141,6 +142,44 @@
|
||||
"@emnapi/runtime": "^1.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
"run-parallel": "^1.1.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.stat": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.walk": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
"fastq": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz",
|
||||
@@ -504,6 +543,23 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/array-union": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
@@ -514,6 +570,19 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@@ -534,6 +603,23 @@
|
||||
"url": "https://opencollective.com/color"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
|
||||
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/commondir": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
@@ -561,6 +647,36 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-type": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/email-addresses": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz",
|
||||
"integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/escodegen": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
|
||||
@@ -638,6 +754,33 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"glob-parent": "^5.1.2",
|
||||
"merge2": "^1.3.0",
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
||||
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -656,6 +799,94 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/filename-reserved-regex": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz",
|
||||
"integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/filenamify": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz",
|
||||
"integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"filename-reserved-regex": "^2.0.0",
|
||||
"strip-outer": "^1.0.1",
|
||||
"trim-repeated": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/find-cache-dir": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
|
||||
"integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commondir": "^1.0.1",
|
||||
"make-dir": "^3.0.2",
|
||||
"pkg-dir": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/avajs/find-cache-dir?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.3.5",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz",
|
||||
"integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -671,12 +902,76 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gh-pages": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.3.0.tgz",
|
||||
"integrity": "sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"async": "^3.2.4",
|
||||
"commander": "^13.0.0",
|
||||
"email-addresses": "^5.0.0",
|
||||
"filenamify": "^4.3.0",
|
||||
"find-cache-dir": "^3.3.1",
|
||||
"fs-extra": "^11.1.1",
|
||||
"globby": "^11.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"gh-pages": "bin/gh-pages.js",
|
||||
"gh-pages-clean": "bin/gh-pages-clean.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/gifenc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/gifenc/-/gifenc-1.0.3.tgz",
|
||||
"integrity": "sha512-xdr6AdrfGBcfzncONUOlXMBuc5wJDtOueE3c5rdG0oNgtINLD+f2iFZltrBRZYzACRbKr+mSVU/x98zv2u3jmw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/globby": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
|
||||
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-union": "^2.1.0",
|
||||
"dir-glob": "^3.0.1",
|
||||
"fast-glob": "^3.2.9",
|
||||
"ignore": "^5.2.0",
|
||||
"merge2": "^1.4.1",
|
||||
"slash": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "19.9.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-19.9.2.tgz",
|
||||
@@ -695,6 +990,49 @@
|
||||
"@babel/runtime": "^7.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-reference": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||
@@ -705,6 +1043,19 @@
|
||||
"@types/estree": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
|
||||
"integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/libtess": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/libtess/-/libtess-1.2.2.tgz",
|
||||
@@ -979,6 +1330,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@@ -989,6 +1353,59 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch/node_modules/picomatch": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -1025,6 +1442,45 @@
|
||||
"integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/p5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/p5/-/p5-2.2.3.tgz",
|
||||
@@ -1052,6 +1508,26 @@
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -1072,6 +1548,19 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-dir": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
|
||||
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"find-up": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
||||
@@ -1101,6 +1590,38 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true,
|
||||
"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/reusify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz",
|
||||
@@ -1135,6 +1656,50 @@
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"dev": true,
|
||||
"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": {
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -1155,6 +1720,19 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-outer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz",
|
||||
"integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.55.4",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.4.tgz",
|
||||
@@ -1206,6 +1784,32 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/trim-repeated": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz",
|
||||
"integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
@@ -1214,6 +1818,16 @@
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.9",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz",
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"deploy": "gh-pages -d dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||
"gh-pages": "^6.3.0",
|
||||
"svelte": "^5.55.4",
|
||||
"vite": "^8.0.9"
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 614 KiB After Width: | Height: | Size: 614 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 717 KiB After Width: | Height: | Size: 717 KiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 331 KiB After Width: | Height: | Size: 331 KiB |
|
Before Width: | Height: | Size: 819 KiB After Width: | Height: | Size: 819 KiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
168
src/App.svelte
@@ -11,31 +11,16 @@ import { loadLeaderboard, saveScore, playerName, leaderboard , hasSubmitted } fr
|
||||
import { updateEnvironment, skyColors } from './environment.js';
|
||||
import { createObstacle, handleCollisions } from './obstacles.js';
|
||||
import { createSketch } from './p5overlay.js';
|
||||
import {
|
||||
handleInput,
|
||||
processInteraction,
|
||||
updatePhysics,
|
||||
updateGameFlow // Add this
|
||||
} from './GameController.js';
|
||||
|
||||
import {
|
||||
createWorldChunk,
|
||||
createClouds,
|
||||
moveWorld,
|
||||
animateClouds // Added this
|
||||
} from './WorldScene.js';
|
||||
import { handleInput, processInteraction, updatePhysics, updateGameFlow } from './GameController.js';
|
||||
import { createWorldChunk, createClouds, moveWorld,animateClouds } from './WorldScene.js';
|
||||
import { ObstacleFactory } from './ObstacleFactory.js';
|
||||
import { GameManager } from './GameManager.js';
|
||||
|
||||
let showLanding = true;
|
||||
let lastTime = performance.now();
|
||||
|
||||
async function handleStart() {
|
||||
showLanding = false;
|
||||
await tick();
|
||||
init();
|
||||
|
||||
// 3. Setup the Bridge Object
|
||||
const gameState = {
|
||||
get isPlaying() { return isPlaying; },
|
||||
get score() { return score; },
|
||||
@@ -50,13 +35,10 @@ async function handleStart() {
|
||||
set gamePhase(v) { gamePhase = v; },
|
||||
get lastStarScore() { return lastStarScore; },
|
||||
set lastStarScore(v) { lastStarScore = v; },
|
||||
|
||||
// FIX: Use getters for the arrays so they don't get stale!
|
||||
get targets() { return targets; },
|
||||
get scorePopups() { return scorePopups; },
|
||||
|
||||
onHit: (t) => {
|
||||
// MVC: Delegate the 'Decision' to the Controller
|
||||
processInteraction(t, gameState, scoreMultiplier, BOOST_DURATION);
|
||||
},
|
||||
onActivateBoost: (mult, dur) => {
|
||||
@@ -64,8 +46,6 @@ async function handleStart() {
|
||||
multiplierTimer = dur;
|
||||
}
|
||||
};
|
||||
|
||||
// 4. Initialize p5 now that p5Container exists
|
||||
p5Instance = new p5(createSketch(gameState, textures), p5Container);
|
||||
|
||||
lastTime = performance.now();
|
||||
@@ -81,72 +61,87 @@ async function handleStart() {
|
||||
loop();
|
||||
}
|
||||
|
||||
//App flow & timing
|
||||
let showLanding = true; // Toggles between LandingPage and the Game Wrapper
|
||||
let lastTime = performance.now(); // High-resolution timestamp for delta time calculation
|
||||
|
||||
// Game Constants & State Variables for physical rules of the world
|
||||
const CONFIG = {
|
||||
lane: 2.5,
|
||||
jump: 0.35,
|
||||
grav: 0.015,
|
||||
playerScale: 1.7,
|
||||
grav: 0.015, // Gravity constant applied per frame
|
||||
playerScale: 1.7, // Visual scale multiplier for the 3D character
|
||||
START_SPEED: 45, // Initial slow speed
|
||||
MAX_SPEED: 95, // The "chaos" threshold
|
||||
MAX_SPEED: 95, // The maximum speed threshold
|
||||
ACCELERATION: 1, // Speed added per second
|
||||
CYCLE_INTERVAL: 7000
|
||||
};
|
||||
const BOOST_DURATION = 20; // Length of the 'Star' multiplier in seconds
|
||||
|
||||
let currentSpeed = CONFIG.START_SPEED;
|
||||
// RENDERING REFERENCES
|
||||
//View references used to bridge Svelte with Three.js and P5.js
|
||||
let container, canvas, p5Container; // HTML Element bindings
|
||||
let scene, camera, renderer; // Three.js Core components
|
||||
let p5Instance; // p5.js Overlay instance
|
||||
let animationFrame; // ID for the requestAnimationFrame loop
|
||||
let uTime = { value: 0 }; // Global time tracker for shader animations and world logic
|
||||
|
||||
// Game State Variables
|
||||
let score = 0, isPlaying = false, gameOver = false, startScreen = true;
|
||||
let attentiveness = 100;
|
||||
let lives = 5;
|
||||
let lives = 5; let isDying = false, hitFlash = false;
|
||||
let currentSpeed = CONFIG.START_SPEED; // The active world velocity
|
||||
|
||||
//Player physics, tracks the 3D position and physics state of the character.
|
||||
let lane = 0, currX = 0, isJumping = false, jumpV = 0, playerY = 0;
|
||||
let container, canvas, scene, camera, renderer, p5Container;
|
||||
let worldObjects = [], animationFrame, p5Instance;
|
||||
let isDying = false, hitFlash = false;
|
||||
let playerAnchor, currentModel = null, currentMixer = null, swapToken = 0;
|
||||
|
||||
let spawnDistanceTracker = 0;
|
||||
// WORLD OBJECTS & GENERATION
|
||||
let worldObjects = [];
|
||||
let spawnDistanceTracker = 0; // Tracks distance traveled since last spawn
|
||||
const SPAWN_INTERVAL = 40; // Physical distance between obstacles
|
||||
let cloudGroup;
|
||||
let cloudGroup; // Container for background parallax clouds
|
||||
let CHUNKS = []; // Ground segments for the infinite loop
|
||||
const CHUNK_COUNT = 3; // Number of segments in the pool
|
||||
const CHUNK_SIZE = 140;
|
||||
|
||||
// ENVIRONMENT & LIGHTING
|
||||
let sun, moon, ambientLight, sunLight, headLight;
|
||||
|
||||
// 2D Game Logic
|
||||
let gamePhase = "START";
|
||||
let instructionTimer = 3;
|
||||
let targetType = "STRAWBERRY";
|
||||
let targets = [];
|
||||
let targets = []; // Active 2D floating target objects
|
||||
let scorePopups = []; // To track the floating +100 labels
|
||||
let scoreMultiplier = 1; //to double the score when star is active
|
||||
let multiplierTimer = 0; // Countdown for active Star power-up
|
||||
let lastStarScore = 0; // Checkpoint to trigger Star spawn every 10k points
|
||||
|
||||
let playerAnchor, currentModel = null, currentMixer = null, swapToken = 0;
|
||||
let CHUNKS = [];
|
||||
const CHUNK_COUNT = 3;
|
||||
const CHUNK_SIZE = 140;
|
||||
|
||||
let uTime = { value: 0 };
|
||||
// ASSET CACHE & LOADING
|
||||
// Memory management for models and textures to prevent redundant loads.
|
||||
const loader = new GLTFLoader();
|
||||
const glbCache = new Map();
|
||||
|
||||
let textures = {};
|
||||
|
||||
let scoreMultiplier = 1;
|
||||
let multiplierTimer = 0; // Remaining seconds of boost
|
||||
let lastStarScore = 0; // Add this line to prevent the crash
|
||||
const BOOST_DURATION = 20; // 20 seconds
|
||||
|
||||
async function getCachedGLTF(file) {
|
||||
if (!glbCache.has(file)) glbCache.set(file, await loader.loadAsync(file));
|
||||
return glbCache.get(file);
|
||||
}
|
||||
|
||||
// Asynchronously swaps the 3D player model and manages its animations
|
||||
async function swapCharacter(file, isDeathAnimation = false) {
|
||||
// Incrementing a token ensures that if multiple swaps are called rapidly,
|
||||
// only the most recent request (the latest token) actually updates the scene.
|
||||
const myToken = ++swapToken;
|
||||
const source = await getCachedGLTF(`3dmodels/${file}`);
|
||||
if (myToken !== swapToken) return;
|
||||
if (myToken !== swapToken) return; // Exit if a newer swap request has already started (stale request prevention)
|
||||
const model = cloneSkeleton(source.scene);
|
||||
model.scale.setScalar(CONFIG.playerScale);
|
||||
model.rotation.y = Math.PI;
|
||||
const mixer = new THREE.AnimationMixer(model);
|
||||
if (source.animations?.length) {
|
||||
const action = mixer.clipAction(source.animations[0]);
|
||||
if (isDeathAnimation) { action.setLoop(THREE.LoopOnce, 1); action.clampWhenFinished = true; }
|
||||
if (isDeathAnimation) { action.setLoop(THREE.LoopOnce, 1); action.clampWhenFinished = true; } // Stop on the last frame instead of resetting
|
||||
action.play();
|
||||
}
|
||||
if (currentModel) playerAnchor.remove(currentModel);
|
||||
@@ -154,6 +149,7 @@ async function swapCharacter(file, isDeathAnimation = false) {
|
||||
playerAnchor.add(currentModel);
|
||||
}
|
||||
|
||||
// Initializes the Three.js engine, environment, and procedural world elements
|
||||
function init() {
|
||||
if (!canvas || !container) return; // Safety check
|
||||
scene = new THREE.Scene();
|
||||
@@ -189,29 +185,15 @@ function init() {
|
||||
playerAnchor = new THREE.Group();
|
||||
scene.add(playerAnchor);
|
||||
|
||||
const ro = new ResizeObserver(() => {
|
||||
if (!container || !renderer) return;
|
||||
const { width, height } = container.getBoundingClientRect();
|
||||
renderer.setSize(width, height);
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
});
|
||||
ro.observe(container);
|
||||
|
||||
// 1. Update Fog to use the day color
|
||||
scene.fog = new THREE.Fog(skyColors.day, 150, 300);
|
||||
|
||||
// 2. Setup Lights
|
||||
ambientLight = new THREE.AmbientLight(0xffffff, 1.5);
|
||||
sunLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||
sunLight.position.set(0, 50, -50);
|
||||
|
||||
// 3. Add the "Headlight" to the camera (stays off during day)
|
||||
headLight = new THREE.PointLight(0x00d2ff, 0, 40);
|
||||
camera.add(headLight);
|
||||
scene.add(camera, ambientLight, sunLight);
|
||||
|
||||
// 4. Create Low-Poly Sun and Moon
|
||||
// Create Low-Poly Sun and Moon
|
||||
const lowPolyGeo = new THREE.IcosahedronGeometry(10, 1);
|
||||
sun = new THREE.Mesh(lowPolyGeo, new THREE.MeshBasicMaterial({ color: 0xffffcc }));
|
||||
moon = new THREE.Mesh(lowPolyGeo, new THREE.MeshBasicMaterial({ color: 0x94b0ff }));
|
||||
@@ -220,30 +202,40 @@ function init() {
|
||||
sun.position.set(60, 100, -250);
|
||||
moon.position.set(60, -100, -250);
|
||||
scene.add(sun, moon);
|
||||
|
||||
// Responsive Design (Viewport Observer)
|
||||
// Automatically handles canvas resizing without reloading the engine
|
||||
const ro = new ResizeObserver(() => {
|
||||
if (!container || !renderer) return;
|
||||
const { width, height } = container.getBoundingClientRect();
|
||||
renderer.setSize(width, height);
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
});
|
||||
ro.observe(container);
|
||||
}
|
||||
|
||||
async function spawn() {
|
||||
async function spawn() { //spawning obstacles with bigger one having 0.2 chance, smaller ones 0.8 chance
|
||||
const isRare = Math.random() < 0.2;
|
||||
const modelFile = isRare ? "bird_in_a_claw_machine.glb" : "Simple computer.glb";
|
||||
const modelFile = isRare ? "3dmodels/bird_in_a_claw_machine.glb" : "3dmodels/Simple computer.glb";
|
||||
|
||||
const source = await getCachedGLTF(modelFile);
|
||||
// Call the module function
|
||||
const obstacleData = createObstacle(isRare, source, CONFIG.lane);
|
||||
|
||||
scene.add(obstacleData.mesh);
|
||||
worldObjects = [...worldObjects, obstacleData];
|
||||
}
|
||||
|
||||
// Main Animation Loop: Executed every frame to update game state and rendering
|
||||
function update() {
|
||||
const now = performance.now();
|
||||
const now = performance.now(); // Time Synchronization
|
||||
const delta = (now - lastTime) / 1000;
|
||||
lastTime = now;
|
||||
|
||||
uTime.value += delta;
|
||||
uTime.value += delta; // Update global shader uniforms and active 3D animations
|
||||
if (currentMixer) currentMixer.update(delta);
|
||||
if (!isPlaying) return;
|
||||
|
||||
// --- ADD THIS LINE HERE ---
|
||||
// This updates the instruction timer and switches the phase
|
||||
updateGameFlow({
|
||||
get gamePhase() { return gamePhase; },
|
||||
@@ -252,21 +244,18 @@ function update() {
|
||||
set instructionTimer(v) { instructionTimer = v; }
|
||||
}, delta);
|
||||
|
||||
// If we are still in instructions, stop the rest of the game logic
|
||||
// (like movement and spawning) so the player doesn't die while reading.
|
||||
if (gamePhase === "INSTRUCTIONS") return;
|
||||
if (gamePhase === "INSTRUCTIONS") return; // Halt world movement and physics during the instruction countdown
|
||||
|
||||
const moveStep = currentSpeed * delta;
|
||||
const moveStep = currentSpeed * delta; // Determine distance traveled this frame based on current velocity
|
||||
|
||||
// 1. Environment Controller
|
||||
// Environment: Updates Day/Night cycle, lighting, and celestial movement
|
||||
updateEnvironment(uTime.value, scene, { ambientLight, sunLight, headLight }, { sun, moon });
|
||||
|
||||
// 2. World Controller
|
||||
// Loops ground segments and animates background parallax clouds
|
||||
moveWorld(CHUNKS, moveStep, CHUNK_SIZE, CHUNK_COUNT);
|
||||
animateClouds(cloudGroup, moveStep);
|
||||
|
||||
// 3. Player Physics Controller
|
||||
updatePhysics(
|
||||
updatePhysics( // Processes gravity, jumping, and lane-shifting kinematics
|
||||
{
|
||||
get lane() { return lane; },
|
||||
get currX() { return currX; }, set currX(v) { currX = v; },
|
||||
@@ -282,22 +271,24 @@ function update() {
|
||||
playerAnchor.position.x = currX;
|
||||
playerAnchor.position.y = playerY;
|
||||
|
||||
// 4. Obstacle Controller
|
||||
worldObjects.forEach(obj => { obj.mesh.position.z += moveStep; });
|
||||
// Obstacle Controller
|
||||
worldObjects.forEach(obj => { obj.mesh.position.z += moveStep; }); // Move obstacles toward the player and check for bounding-box intersections
|
||||
worldObjects = handleCollisions(worldObjects, lane, playerY, triggerGameOver);
|
||||
worldObjects = worldObjects.filter(obj => {
|
||||
worldObjects = worldObjects.filter(obj => { // Garbage Collection: Remove obstacles that have passed behind the camera to free memory
|
||||
const active = obj.mesh.position.z < 25;
|
||||
if (!active) scene.remove(obj.mesh);
|
||||
return active;
|
||||
});
|
||||
|
||||
// 5. Scoring & Spawning Logic
|
||||
// Manage active multiplier power-ups (Star/Boost)
|
||||
if (multiplierTimer > 0) {
|
||||
multiplierTimer -= delta;
|
||||
if (multiplierTimer <= 0) { multiplierTimer = 0; scoreMultiplier = 1; }
|
||||
}
|
||||
|
||||
// Calculate score based on speed and active multiplier
|
||||
score += Math.floor((currentSpeed / 40) * scoreMultiplier);
|
||||
// Gradually increase velocity to scale game difficulty
|
||||
if (currentSpeed < CONFIG.MAX_SPEED) currentSpeed += CONFIG.ACCELERATION * delta;
|
||||
|
||||
// 1. Ask the Static Manager if we should spawn
|
||||
@@ -308,11 +299,6 @@ function update() {
|
||||
worldObjects = [...worldObjects, obstacle];
|
||||
});
|
||||
}
|
||||
// spawnDistanceTracker += moveStep;
|
||||
// if (spawnDistanceTracker >= SPAWN_INTERVAL) {
|
||||
// spawn();
|
||||
// spawnDistanceTracker = 0;
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
@@ -327,8 +313,8 @@ function triggerGameOver() {
|
||||
async function startGame() {
|
||||
if (!scene) return;
|
||||
currentSpeed = CONFIG.START_SPEED;
|
||||
// Choose a random target type for this mission
|
||||
const types = ["STRAWBERRY", "WATERMELON", "BLUEBERRY"];
|
||||
|
||||
const types = ["STRAWBERRY", "WATERMELON", "BLUEBERRY"]; // Choose a random target type for this mission
|
||||
targetType = types[Math.floor(Math.random() * types.length)];
|
||||
worldObjects.forEach(obj => scene.remove(obj.mesh));
|
||||
targets.length = 0;
|
||||
@@ -343,7 +329,7 @@ async function startGame() {
|
||||
}
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
// MVC: Delegate keyboard input to the Controller
|
||||
// Delegate keyboard input to the Controller
|
||||
handleInput(e,
|
||||
{
|
||||
isPlaying,
|
||||
@@ -364,7 +350,7 @@ const handleKeyDown = (e) => {
|
||||
onMount(() => {
|
||||
loadLeaderboard();
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
return () => { // This return block executes when the component is destroyed (e.g., navigating away)
|
||||
cancelAnimationFrame(animationFrame);
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
if (p5Instance) p5Instance.remove();
|
||||
@@ -479,7 +465,7 @@ onMount(() => {
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 2px;
|
||||
color: hsl(308, 100%, 87%); /* Change color to cyan to match your theme */
|
||||
color: hsl(308, 100%, 87%);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// GameController.js
|
||||
// @ts-nocheck
|
||||
export function handleInput(event, state, config, swapFn) {
|
||||
if (!state.isPlaying || state.isDying) return;
|
||||
@@ -6,10 +5,13 @@ export function handleInput(event, state, config, swapFn) {
|
||||
const actions = {
|
||||
ArrowLeft: () => state.lane > -2 && state.lane--,
|
||||
a: () => state.lane > -2 && state.lane--,
|
||||
A: () => state.lane > -2 && state.lane--,
|
||||
ArrowRight: () => state.lane < 2 && state.lane++,
|
||||
d: () => state.lane < 2 && state.lane++,
|
||||
D: () => state.lane < 2 && state.lane++,
|
||||
" ": () => !state.isJumping && triggerJump(state, config, swapFn),
|
||||
w: () => !state.isJumping && triggerJump(state, config, swapFn),
|
||||
W: () => !state.isJumping && triggerJump(state, config, swapFn),
|
||||
ArrowUp: () => !state.isJumping && triggerJump(state, config, swapFn)
|
||||
};
|
||||
|
||||
@@ -53,7 +55,6 @@ export function updatePhysics(state, config, delta, swapFn) {
|
||||
}
|
||||
}
|
||||
|
||||
// GameController.js
|
||||
export function updateGameFlow(state, delta) {
|
||||
if (state.gamePhase === "INSTRUCTIONS") {
|
||||
state.instructionTimer -= delta;
|
||||
|
||||
@@ -8,29 +8,29 @@
|
||||
|
||||
let container;
|
||||
let charCanvas;
|
||||
let animFrame;
|
||||
let animFrame; // Animation and rendering state
|
||||
let charMixer;
|
||||
|
||||
onMount(() => {
|
||||
const clock = new THREE.Clock();
|
||||
|
||||
// Alpha: true allows the CSS background-image to show through the canvas
|
||||
const charRenderer = new THREE.WebGLRenderer({
|
||||
canvas: charCanvas,
|
||||
antialias: true,
|
||||
alpha: true
|
||||
});
|
||||
const charScene = new THREE.Scene();
|
||||
const charScene = new THREE.Scene(); //Scene and Camera Configuration
|
||||
const charCam = new THREE.PerspectiveCamera(40, charCanvas.clientWidth / charCanvas.clientHeight, 0.1, 200);
|
||||
|
||||
charCam.position.set(0, 3, 13);
|
||||
charCam.lookAt(0, 2.5, 0);
|
||||
|
||||
// Lighting Setup
|
||||
charScene.add(new THREE.AmbientLight(0xffffff, 7));
|
||||
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||
dirLight.position.set(5, 5, 5);
|
||||
charScene.add(dirLight);
|
||||
|
||||
const loader = new GLTFLoader();
|
||||
const loader = new GLTFLoader(); //Asset Loading (3D Model & Animation)
|
||||
loader.load("3dmodels/hiphop.glb", (gltf) => {
|
||||
const model = gltf.scene;
|
||||
model.scale.setScalar(3.5);
|
||||
@@ -38,13 +38,13 @@
|
||||
model.position.x = -1;
|
||||
charScene.add(model);
|
||||
|
||||
if (gltf.animations.length) {
|
||||
if (gltf.animations.length) { // Initialize AnimationMixer if clips are present in the GLB
|
||||
charMixer = new THREE.AnimationMixer(model);
|
||||
charMixer.clipAction(gltf.animations[0]).play();
|
||||
}
|
||||
});
|
||||
|
||||
const handleResize = () => {
|
||||
const handleResize = () => { //Responsive Handling
|
||||
charRenderer.setSize(charCanvas.clientWidth, charCanvas.clientHeight);
|
||||
charCam.aspect = charCanvas.clientWidth / charCanvas.clientHeight;
|
||||
charCam.updateProjectionMatrix();
|
||||
@@ -53,7 +53,7 @@
|
||||
window.addEventListener('resize', handleResize);
|
||||
handleResize();
|
||||
|
||||
function loop() {
|
||||
function loop() { //Component Animation Loop
|
||||
animFrame = requestAnimationFrame(loop);
|
||||
const delta = clock.getDelta();
|
||||
charRenderer.render(charScene, charCam);
|
||||
@@ -61,7 +61,7 @@
|
||||
}
|
||||
loop();
|
||||
|
||||
return () => {
|
||||
return () => { //Cleanup & Resource Disposal
|
||||
cancelAnimationFrame(animFrame);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
@@ -75,6 +75,12 @@
|
||||
|
||||
<img src="images/overload_trans.png" alt="Overload Logo" class="game-logo" />
|
||||
|
||||
<p class="game-description">
|
||||
Collect targets to maintain focus while navigating through obstacles. Hearts are only for missed or wrong targets,
|
||||
it won't save you from crashing into obstacles. The higher the score, you might get reward boosters for setting records!
|
||||
Rapid task-switching trains mental flexibility, attention, and working memory.
|
||||
</p>
|
||||
|
||||
<button class="start-btn" on:click={onStart}>
|
||||
START RUN
|
||||
</button>
|
||||
@@ -87,13 +93,65 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.game-description {
|
||||
max-width: 600px; /* Widened slightly for better monospaced flow */
|
||||
color: #fff82e;
|
||||
/* Retro/Computer game font stack */
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-weight: 900;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
margin-bottom: 50px;
|
||||
padding: 0; /* Removed padding since background is gone */
|
||||
background: none; /* Background removed */
|
||||
backdrop-filter: none;
|
||||
border: none;
|
||||
|
||||
/* Strong black outline to keep it readable against the background */
|
||||
text-shadow:
|
||||
-2px -2px 0 #0c430c,
|
||||
2px -2px 0 #0b5b26,
|
||||
-2px 2px 0 #0b5b26,
|
||||
2px 2px 0 #0b5b26,
|
||||
0px 4px 10px rgba(0,0,0,1);
|
||||
|
||||
text-transform: uppercase; /* Makes it feel more like a retro game UI */
|
||||
animation: fadeIn 1s ease-out;
|
||||
}
|
||||
|
||||
.left-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
/* Adjust padding to balance the logo, text, and button */
|
||||
padding: 0 0 10vh 5vw;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Ensure center alignment for the description in the left section */
|
||||
.left-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 0 0 15vh 10vw; /* Reduced padding slightly to fit the text */
|
||||
}
|
||||
|
||||
#landing-ui {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 9999;
|
||||
background-image: url('images/bggame.png');
|
||||
background-image: url('/infinite-runner-multitasking-game/images/bggame.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
display: flex;
|
||||
@@ -108,15 +166,6 @@
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.left-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: center; /* Changed to center to align logo and button */
|
||||
padding: 0 0 25vh 10vw;
|
||||
}
|
||||
|
||||
/* LOGO STYLING & ANIMATION */
|
||||
.game-logo {
|
||||
width: 700px; /* Adjust size as needed */
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// ObstacleFactory.js
|
||||
// @ts-nocheck
|
||||
import { createObstacle } from './obstacles.js';
|
||||
|
||||
@@ -7,11 +6,10 @@ export class ObstacleFactory {
|
||||
const isRare = Math.random() < 0.2;
|
||||
const modelFile = isRare ? "bird_in_a_claw_machine.glb" : "Simple computer.glb";
|
||||
const fullPath = `3dmodels/${modelFile}`;
|
||||
// Check if we have it, if not, load it (Standard Factory behavior)
|
||||
// Check if we have it, if not, load it
|
||||
if (!glbCache.has(fullPath)) {
|
||||
glbCache.set(fullPath, await loader.loadAsync(fullPath));
|
||||
}
|
||||
|
||||
const source = glbCache.get(fullPath);
|
||||
return createObstacle(isRare, source, laneWidth);
|
||||
}
|
||||
|
||||
@@ -61,17 +61,11 @@ export function createClouds(group) {
|
||||
});
|
||||
|
||||
const thickness = 2;
|
||||
// Increase count to 40 for a denser sky
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const w = 10 + Math.random() * 20;
|
||||
const d = 10 + Math.random() * 20;
|
||||
const cloud = new THREE.Mesh(new THREE.BoxGeometry(w, thickness, d), cloudMaterial);
|
||||
|
||||
const y = 30 + Math.random() * 25;
|
||||
|
||||
// FIX: Spread clouds from +50 (behind camera) to -400 (deep horizon)
|
||||
// This ensures that as soon as the game starts, there are clouds
|
||||
// already "waiting" far in the distance.
|
||||
const z = (Math.random() * -450) + 50;
|
||||
|
||||
cloud.position.set((Math.random() - 0.5) * 280, y, z);
|
||||
|
||||
296
src/app.css
@@ -1,296 +0,0 @@
|
||||
:root {
|
||||
--text: #6b6375;
|
||||
--text-h: #08060d;
|
||||
--bg: #fff;
|
||||
--border: #e5e4e7;
|
||||
--code-bg: #f4f3ec;
|
||||
--accent: #aa3bff;
|
||||
--accent-bg: rgba(170, 59, 255, 0.1);
|
||||
--accent-border: rgba(170, 59, 255, 0.5);
|
||||
--social-bg: rgba(244, 243, 236, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
||||
|
||||
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--mono: ui-monospace, Consolas, monospace;
|
||||
|
||||
font: 18px/145% var(--sans);
|
||||
letter-spacing: 0.18px;
|
||||
color-scheme: light dark;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text: #9ca3af;
|
||||
--text-h: #f3f4f6;
|
||||
--bg: #16171d;
|
||||
--border: #2e303a;
|
||||
--code-bg: #1f2028;
|
||||
--accent: #c084fc;
|
||||
--accent-bg: rgba(192, 132, 252, 0.15);
|
||||
--accent-border: rgba(192, 132, 252, 0.5);
|
||||
--social-bg: rgba(47, 48, 58, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
||||
}
|
||||
|
||||
#social .button-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-family: var(--heading);
|
||||
font-weight: 500;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 56px;
|
||||
letter-spacing: -1.68px;
|
||||
margin: 32px 0;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 36px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
line-height: 118%;
|
||||
letter-spacing: -0.24px;
|
||||
margin: 0 0 8px;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code,
|
||||
.counter {
|
||||
font-family: var(--mono);
|
||||
display: inline-flex;
|
||||
border-radius: 4px;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 15px;
|
||||
line-height: 135%;
|
||||
padding: 4px 8px;
|
||||
background: var(--code-bg);
|
||||
}
|
||||
|
||||
.counter {
|
||||
font-size: 16px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
color: var(--accent);
|
||||
background: var(--accent-bg);
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.3s;
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--accent-border);
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
|
||||
.base,
|
||||
.framework,
|
||||
.vite {
|
||||
inset-inline: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.base {
|
||||
width: 170px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.framework,
|
||||
.vite {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.framework {
|
||||
z-index: 1;
|
||||
top: 34px;
|
||||
height: 28px;
|
||||
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
|
||||
scale(1.4);
|
||||
}
|
||||
|
||||
.vite {
|
||||
z-index: 0;
|
||||
top: 107px;
|
||||
height: 26px;
|
||||
width: auto;
|
||||
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
|
||||
scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 1126px;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
border-inline: 1px solid var(--border);
|
||||
min-height: 100svh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
flex-grow: 1;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
padding: 32px 20px 24px;
|
||||
gap: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
#next-steps {
|
||||
display: flex;
|
||||
border-top: 1px solid var(--border);
|
||||
text-align: left;
|
||||
|
||||
& > div {
|
||||
flex: 1 1 0;
|
||||
padding: 32px;
|
||||
@media (max-width: 1024px) {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-bottom: 16px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
#docs {
|
||||
border-right: 1px solid var(--border);
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
}
|
||||
|
||||
#next-steps ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 32px 0 0;
|
||||
|
||||
.logo {
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-h);
|
||||
font-size: 16px;
|
||||
border-radius: 6px;
|
||||
background: var(--social-bg);
|
||||
display: flex;
|
||||
padding: 6px 12px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
transition: box-shadow 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.button-icon {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
li {
|
||||
flex: 1 1 calc(50% - 8px);
|
||||
}
|
||||
|
||||
a {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#spacer {
|
||||
height: 88px;
|
||||
border-top: 1px solid var(--border);
|
||||
@media (max-width: 1024px) {
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.ticks {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4.5px;
|
||||
border: 5px solid transparent;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: 0;
|
||||
border-left-color: var(--border);
|
||||
}
|
||||
&::after {
|
||||
right: 0;
|
||||
border-right-color: var(--border);
|
||||
}
|
||||
}
|
||||
748
src/cool.svelte
@@ -1,748 +0,0 @@
|
||||
<script>
|
||||
// @ts-nocheck
|
||||
import LandingPage from './LandingPage.svelte';
|
||||
import { onMount, tick } from "svelte";
|
||||
import * as THREE from "three";
|
||||
import p5 from "p5";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import { clone as cloneSkeleton } from "three/examples/jsm/utils/SkeletonUtils.js";
|
||||
|
||||
let showLanding = true;
|
||||
let lastTime = performance.now();
|
||||
|
||||
// --- LEADERBOARD STATE ---
|
||||
let leaderboard = [];
|
||||
let playerName = "";
|
||||
let hasSubmitted = false;
|
||||
|
||||
async function handleStart() {
|
||||
showLanding = false;
|
||||
// Wait for Svelte to render the #wrapper and canvas
|
||||
await tick();
|
||||
|
||||
init();
|
||||
p5Instance = new p5(sketch, p5Container);
|
||||
|
||||
// Start the game logic
|
||||
startGame();
|
||||
|
||||
// Start the render loop
|
||||
const loop = () => {
|
||||
animationFrame = requestAnimationFrame(loop);
|
||||
update();
|
||||
if (renderer && scene && camera) {
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
};
|
||||
loop();
|
||||
}
|
||||
|
||||
// --- LEADERBOARD LOGIC ---
|
||||
function saveScore() {
|
||||
if (hasSubmitted) return;
|
||||
|
||||
const name = playerName.trim() || "Anonymous";
|
||||
let currentBoard = JSON.parse(localStorage.getItem("neuro_leaderboard") || "[]");
|
||||
|
||||
// Check if this player already has a record (case-insensitive)
|
||||
const existingIndex = currentBoard.findIndex(
|
||||
entry => entry.name.toLowerCase() === name.toLowerCase()
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// Only update if the new score is actually higher
|
||||
if (score > currentBoard[existingIndex].score) {
|
||||
currentBoard[existingIndex].score = score;
|
||||
}
|
||||
} else {
|
||||
// New player, just add them
|
||||
currentBoard.push({ name: name, score: score });
|
||||
}
|
||||
|
||||
// Sort by highest score first and keep only top 5
|
||||
currentBoard.sort((a, b) => b.score - a.score);
|
||||
currentBoard = currentBoard.slice(0, 5);
|
||||
|
||||
localStorage.setItem("neuro_leaderboard", JSON.stringify(currentBoard));
|
||||
leaderboard = currentBoard;
|
||||
hasSubmitted = true;
|
||||
playerName = ""; // Reset for next time
|
||||
}
|
||||
|
||||
function loadLeaderboard() {
|
||||
leaderboard = JSON.parse(localStorage.getItem("neuro_leaderboard") || "[]");
|
||||
}
|
||||
|
||||
const CONFIG = {
|
||||
lane: 2.5,
|
||||
jump: 0.35,
|
||||
grav: 0.015,
|
||||
playerScale: 1.7,
|
||||
START_SPEED: 45, // Initial slow speed
|
||||
MAX_SPEED: 95, // The "chaos" threshold
|
||||
ACCELERATION: 1 // Speed added per second
|
||||
};
|
||||
|
||||
let currentSpeed = CONFIG.START_SPEED;
|
||||
let score = 0, isPlaying = false, gameOver = false, startScreen = true;
|
||||
let attentiveness = 100;
|
||||
let lives = 5;
|
||||
let lane = 0, currX = 0, isJumping = false, jumpV = 0, playerY = 0;
|
||||
let container, canvas, scene, camera, renderer, p5Container;
|
||||
let worldObjects = [], animationFrame, p5Instance;
|
||||
let isDying = false, hitFlash = false;
|
||||
|
||||
let spawnDistanceTracker = 0;
|
||||
const SPAWN_INTERVAL = 40; // Physical distance between obstacles
|
||||
|
||||
// 2D Game Logic
|
||||
let gamePhase = "START";
|
||||
let instructionTimer = 3;
|
||||
let targetType = "STRAWBERRY";
|
||||
let targets = [];
|
||||
|
||||
let playerAnchor, currentModel = null, currentMixer = null, swapToken = 0;
|
||||
let CHUNKS = [];
|
||||
const CHUNK_COUNT = 3;
|
||||
const CHUNK_SIZE = 140;
|
||||
|
||||
let uTime = { value: 0 };
|
||||
const loader = new GLTFLoader();
|
||||
const glbCache = new Map();
|
||||
|
||||
let textures = {};
|
||||
|
||||
const sketch = (p) => {
|
||||
// --- FOOLPROOF IMAGE LOADER ---
|
||||
p.setup = async () => {
|
||||
const w = container?.clientWidth || p.windowWidth;
|
||||
const h = container?.clientHeight || p.windowHeight;
|
||||
p.createCanvas(w, h);
|
||||
|
||||
const loadImg = (path) => new Promise(resolve => {
|
||||
p.loadImage(path, img => resolve(img), () => resolve(null));
|
||||
});
|
||||
|
||||
textures.STRAWBERRY = await loadImg('strawberry.png');
|
||||
textures.BANANA = await loadImg('banana.png');
|
||||
textures.BLUEBERRY = await loadImg('blubb.png');
|
||||
};
|
||||
|
||||
const drawHeart = (x, y, size, active) => {
|
||||
p.push();
|
||||
p.noStroke();
|
||||
// Use red if active, grey if dead
|
||||
p.fill(active ? [255, 50, 50] : [100, 100, 100, 150]);
|
||||
const s = size / 5;
|
||||
p.rect(x + s, y, s, s); p.rect(x + 3 * s, y, s, s);
|
||||
p.rect(x, y + s, 5 * s, s);
|
||||
p.rect(x, y + 2 * s, 5 * s, s);
|
||||
p.rect(x + s, y + 3 * s, 3 * s, s);
|
||||
p.rect(x + 2 * s, y + 4 * s, s, s);
|
||||
p.pop();
|
||||
};
|
||||
|
||||
p.draw = () => {
|
||||
p.clear();
|
||||
if (!isPlaying) return;
|
||||
if (gamePhase === "INSTRUCTIONS") {
|
||||
p.fill(0, 200); // Darken background
|
||||
p.rect(0, 0, p.width, p.height);
|
||||
|
||||
p.fill(255);
|
||||
p.textAlign(p.CENTER);
|
||||
p.textFont('Segoe UI');
|
||||
p.textStyle(p.BOLD);
|
||||
|
||||
// The Mission Text
|
||||
p.textSize(28);
|
||||
p.text(`MISSION: COLLECT`, p.width / 2, p.height / 2 - 100);
|
||||
|
||||
// Draw the target icon to collect
|
||||
const targetImg = textures[targetType];
|
||||
if (targetImg) {
|
||||
p.imageMode(p.CENTER);
|
||||
p.image(targetImg, p.width / 2, p.height / 2 - 20, 80, 80);
|
||||
}
|
||||
|
||||
p.textSize(32);
|
||||
p.fill(0, 255, 200); // Cyan color for the target name
|
||||
p.text(targetType, p.width / 2, p.height / 2 + 60);
|
||||
|
||||
// Countdown
|
||||
p.fill(255);
|
||||
p.textSize(80);
|
||||
p.text(Math.ceil(instructionTimer), p.width / 2, p.height / 2 + 160);
|
||||
return;
|
||||
}
|
||||
// --- RENDER HEARTS ---
|
||||
for (let i = 0; i < 5; i++) { // Always run 5 times
|
||||
drawHeart(20 + (i * 35), 20, 25, i < lives);
|
||||
}
|
||||
|
||||
|
||||
if (p.random(1) < 0.004) {
|
||||
const types = ["STRAWBERRY", "BANANA", "BLUEBERRY"];
|
||||
targets.push({
|
||||
x: p.random(p.width * 0.2, p.width * 0.8),
|
||||
y: -50,
|
||||
type: types[p.floor(p.random(types.length))],
|
||||
speed: p.random(1.5, 3),
|
||||
rot: 0
|
||||
});
|
||||
}
|
||||
|
||||
// --- RENDER IMAGES ---
|
||||
for (let i = targets.length - 1; i >= 0; i--) {
|
||||
let t = targets[i];
|
||||
t.y += t.speed;
|
||||
t.rot += 0.02;
|
||||
|
||||
p.push();
|
||||
p.translate(t.x, t.y);
|
||||
p.rotate(t.rot);
|
||||
p.imageMode(p.CENTER);
|
||||
|
||||
// Check if the texture exists before trying to draw it
|
||||
const img = textures[t.type];
|
||||
if (img) {
|
||||
// Draw the image. Scale it to 40x40 pixels (adjust as needed)
|
||||
p.image(img, 0, 0, 60, 60);
|
||||
} else {
|
||||
// Fallback: draw a small circle if image fails to load
|
||||
p.fill(255);
|
||||
p.ellipse(0, 0, 10);
|
||||
}
|
||||
p.pop();
|
||||
|
||||
if (t.y > p.height + 50) {
|
||||
// If the one we missed was the target, lose a life
|
||||
if (t.type === targetType && lives > 0) {
|
||||
lives--;
|
||||
}
|
||||
targets.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2D HAMMER ---
|
||||
p.push();
|
||||
p.translate(p.mouseX, p.mouseY);
|
||||
p.rotate(-0.4);
|
||||
p.fill(120, 80, 50); p.noStroke();
|
||||
p.rect(-5, 0, 10, 40, 2);
|
||||
p.fill(100);
|
||||
p.rect(-20, -10, 40, 20, 4);
|
||||
p.pop();
|
||||
};
|
||||
|
||||
p.mousePressed = () => {
|
||||
if (gamePhase !== "PLAYING") return;
|
||||
for (let i = targets.length - 1; i >= 0; i--) {
|
||||
let t = targets[i];
|
||||
if (p.dist(p.mouseX, p.mouseY, t.x, t.y) < 40) {
|
||||
if (t.type === targetType) {
|
||||
score += 100;
|
||||
} else {
|
||||
if (lives > 0) lives--;
|
||||
}
|
||||
targets.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const grassVertex = `
|
||||
varying vec2 vUv;
|
||||
uniform float uTime;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
vec3 pos = position;
|
||||
float sway = sin(uTime * 2.0 + (instanceMatrix[3][0] * 0.5) + (instanceMatrix[3][2] * 0.5)) * 0.15 * uv.y;
|
||||
pos.x += sway;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(pos, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const grassFragment = `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
gl_FragColor = vec4(mix(vec3(0.12, 0.28, 0.18), vec3(0.5, 0.72, 0.4), vUv.y), 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
const createClouds = (group) => {
|
||||
const cloudMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff, transparent: true, opacity: 0.8 });
|
||||
const thickness = 2;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const w = 10 + Math.random() * 20;
|
||||
const d = 10 + Math.random() * 20;
|
||||
const cloud = new THREE.Mesh(new THREE.BoxGeometry(w, thickness, d), cloudMaterial);
|
||||
cloud.position.set((Math.random() - 0.5) * 280, 35, (Math.random() - 0.5) * 300);
|
||||
group.add(cloud);
|
||||
}
|
||||
};
|
||||
|
||||
const createWorldChunk = (zOffset) => {
|
||||
const group = new THREE.Group();
|
||||
group.position.z = zOffset;
|
||||
const floor = new THREE.Mesh(new THREE.PlaneGeometry(160, CHUNK_SIZE + 0.1), new THREE.MeshStandardMaterial({ color: 0x1e2b21 }));
|
||||
floor.rotation.x = -Math.PI / 2;
|
||||
group.add(floor);
|
||||
|
||||
const count = 7000;
|
||||
const geo = new THREE.PlaneGeometry(0.4, 0.9, 1, 2);
|
||||
geo.translate(0, 0.45, 0);
|
||||
const mat = new THREE.ShaderMaterial({
|
||||
uniforms: { uTime }, vertexShader: grassVertex, fragmentShader: grassFragment,
|
||||
side: THREE.DoubleSide, alphaToCoverage: true
|
||||
});
|
||||
const mesh = new THREE.InstancedMesh(geo, mat, count);
|
||||
const dummy = new THREE.Object3D();
|
||||
for(let i=0; i<count; i++) {
|
||||
let x = (Math.random() - 0.5) * 120;
|
||||
if (x > -10 && x < 10) x += (x > 0) ? 10 : -10;
|
||||
dummy.position.set(x, 0, (Math.random() - 0.5) * CHUNK_SIZE);
|
||||
dummy.rotation.y = Math.random() * Math.PI;
|
||||
dummy.scale.setScalar(0.7 + Math.random() * 1.6);
|
||||
dummy.updateMatrix();
|
||||
mesh.setMatrixAt(i, dummy.matrix);
|
||||
}
|
||||
group.add(mesh);
|
||||
return group;
|
||||
};
|
||||
|
||||
async function getCachedGLTF(file) {
|
||||
if (!glbCache.has(file)) glbCache.set(file, await loader.loadAsync(file));
|
||||
return glbCache.get(file);
|
||||
}
|
||||
|
||||
async function swapCharacter(file, isDeathAnimation = false) {
|
||||
const myToken = ++swapToken;
|
||||
const source = await getCachedGLTF(file);
|
||||
if (myToken !== swapToken) return;
|
||||
const model = cloneSkeleton(source.scene);
|
||||
model.scale.setScalar(CONFIG.playerScale);
|
||||
model.rotation.y = Math.PI;
|
||||
const mixer = new THREE.AnimationMixer(model);
|
||||
if (source.animations?.length) {
|
||||
const action = mixer.clipAction(source.animations[0]);
|
||||
if (isDeathAnimation) { action.setLoop(THREE.LoopOnce, 1); action.clampWhenFinished = true; }
|
||||
action.play();
|
||||
}
|
||||
if (currentModel) playerAnchor.remove(currentModel);
|
||||
currentModel = model; currentMixer = mixer;
|
||||
playerAnchor.add(currentModel);
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (!canvas || !container) return; // Safety check
|
||||
scene = new THREE.Scene();
|
||||
const skyColor = 0x87CEFA;
|
||||
scene.background = new THREE.Color(skyColor);
|
||||
scene.fog = new THREE.Fog(skyColor, 150, 350);
|
||||
|
||||
camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
|
||||
camera.position.set(0, 4.5, 13);
|
||||
camera.lookAt(0, 1, -5);
|
||||
|
||||
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
|
||||
renderer.setSize(container.clientWidth, container.clientHeight);
|
||||
|
||||
const lights = [
|
||||
new THREE.AmbientLight(0xffffff, 1.8),
|
||||
new THREE.DirectionalLight(0xffffff, 1.2)
|
||||
];
|
||||
lights[1].position.set(0, 50, 0);
|
||||
scene.add(...lights);
|
||||
|
||||
const cloudGroup = new THREE.Group();
|
||||
createClouds(cloudGroup);
|
||||
scene.add(cloudGroup);
|
||||
|
||||
CHUNKS = Array.from({ length: CHUNK_COUNT }).map((_, i) => {
|
||||
const chunk = createWorldChunk(-i * CHUNK_SIZE);
|
||||
scene.add(chunk);
|
||||
return chunk;
|
||||
});
|
||||
|
||||
playerAnchor = new THREE.Group();
|
||||
scene.add(playerAnchor);
|
||||
|
||||
const ro = new ResizeObserver(() => {
|
||||
if (!container || !renderer) return;
|
||||
const { width, height } = container.getBoundingClientRect();
|
||||
renderer.setSize(width, height);
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
});
|
||||
ro.observe(container);
|
||||
}
|
||||
|
||||
async function spawn() {
|
||||
const l = Math.floor(Math.random() * 5) - 2;
|
||||
const source = await getCachedGLTF("Simple computer.glb");
|
||||
const model = cloneSkeleton(source.scene);
|
||||
const pivot = new THREE.Group();
|
||||
pivot.position.set(l * CONFIG.lane, 0, -130);
|
||||
model.position.set(0, 0.6, 0);
|
||||
model.rotation.y = Math.PI;
|
||||
model.scale.setScalar(5.5);
|
||||
pivot.add(model);
|
||||
scene.add(pivot);
|
||||
worldObjects = [...worldObjects, { mesh: pivot, lane: l }];
|
||||
}
|
||||
|
||||
|
||||
function update() {
|
||||
const now = performance.now();
|
||||
const delta = (now - lastTime) / 1000;
|
||||
lastTime = now;
|
||||
|
||||
uTime.value += delta;
|
||||
if (currentMixer) currentMixer.update(delta);
|
||||
if (!isPlaying) return;
|
||||
|
||||
if (gamePhase === "INSTRUCTIONS") {
|
||||
instructionTimer -= delta;
|
||||
if (instructionTimer <= 0) gamePhase = "PLAYING";
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentSpeed < CONFIG.MAX_SPEED) {
|
||||
currentSpeed += CONFIG.ACCELERATION * delta;
|
||||
}
|
||||
|
||||
const moveStep = currentSpeed * delta;
|
||||
score += Math.floor(currentSpeed / 40);
|
||||
|
||||
if (lives <= 0) triggerGameOver();
|
||||
|
||||
CHUNKS.forEach(chunk => {
|
||||
chunk.position.z += moveStep;
|
||||
if (chunk.position.z > CHUNK_SIZE) chunk.position.z -= CHUNK_SIZE * CHUNK_COUNT;
|
||||
});
|
||||
|
||||
currX += (lane * CONFIG.lane - currX) * 0.18;
|
||||
playerAnchor.position.x = currX;
|
||||
|
||||
if (isJumping) {
|
||||
jumpV -= CONFIG.grav;
|
||||
playerY += jumpV;
|
||||
if (playerY <= 0) {
|
||||
playerY = 0;
|
||||
isJumping = false;
|
||||
if (!isDying) swapCharacter("Running.glb");
|
||||
}
|
||||
}
|
||||
playerAnchor.position.y = playerY;
|
||||
|
||||
worldObjects = worldObjects.map(obj => {
|
||||
obj.mesh.position.z += moveStep;
|
||||
if (Math.abs(obj.mesh.position.z) < 1.5 && obj.lane === lane && playerY < 1.5) triggerGameOver();
|
||||
return obj;
|
||||
/*
|
||||
if (Math.abs(obj.mesh.position.z) < 1.5 && obj.lane === lane && playerY < 1.5) {
|
||||
lives--;
|
||||
hitFlash = true;
|
||||
setTimeout(() => hitFlash = false, 150);
|
||||
// Remove hit object to prevent multi-hits
|
||||
scene.remove(obj.mesh);
|
||||
return null;
|
||||
}
|
||||
return obj;
|
||||
*/
|
||||
}).filter(obj => {
|
||||
const active = obj.mesh.position.z < 25;
|
||||
if (!active) scene.remove(obj.mesh);
|
||||
return active;
|
||||
});
|
||||
|
||||
// Normal Obstacle Spawning
|
||||
spawnDistanceTracker += moveStep;
|
||||
if (spawnDistanceTracker >= SPAWN_INTERVAL) {
|
||||
spawn();
|
||||
spawnDistanceTracker = 0;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function triggerGameOver() {
|
||||
isPlaying = false; gameOver = true; isDying = true; hitFlash = true;
|
||||
hasSubmitted = false; // Allow a new submission for this game over
|
||||
loadLeaderboard(); // Refresh board to show latest rankings
|
||||
swapCharacter("Falling Back Death.glb", true);
|
||||
setTimeout(() => hitFlash = false, 150);
|
||||
}
|
||||
|
||||
async function startGame() {
|
||||
if (!scene) return;
|
||||
currentSpeed = CONFIG.START_SPEED;
|
||||
// Choose a random target type for this mission
|
||||
const types = ["STRAWBERRY", "BANANA", "BLUEBERRY"];
|
||||
targetType = types[Math.floor(Math.random() * types.length)];
|
||||
worldObjects.forEach(obj => scene.remove(obj.mesh));
|
||||
worldObjects = [];
|
||||
targets = [];
|
||||
spawnDistanceTracker = 0;
|
||||
score = 0; isPlaying = true; gameOver = false; startScreen = false; lives = 5; // Reset lives
|
||||
gamePhase = "INSTRUCTIONS"; instructionTimer = 3;
|
||||
lane = 0; currX = 0; isJumping = false; jumpV = 0; playerY = 0; isDying = false;
|
||||
CHUNKS.forEach((chunk, i) => { chunk.position.z = -i * CHUNK_SIZE; });
|
||||
await swapCharacter("Running.glb");
|
||||
}
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (!isPlaying || isDying) return;
|
||||
const actions = {
|
||||
ArrowLeft: () => lane > -2 && lane--, a: () => lane > -2 && lane--, A: () => lane > -2 && lane--,
|
||||
ArrowRight: () => lane < 2 && lane++, d: () => lane < 2 && lane++, D: () => lane < 2 && lane++,
|
||||
" ": () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jumping.glb")),
|
||||
ArrowUp: () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jumping.glb")),
|
||||
w: () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jumping.glb")),
|
||||
W: () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jumping.glb"))
|
||||
};
|
||||
actions[e.key]?.();
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
loadLeaderboard();
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
cancelAnimationFrame(animationFrame);
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
if (p5Instance) p5Instance.remove();
|
||||
if (renderer) renderer.dispose();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if showLanding}
|
||||
<LandingPage onStart={handleStart} />
|
||||
{:else}
|
||||
<div id="wrapper" bind:this={container}>
|
||||
<!-- The 3D Game World -->
|
||||
<canvas bind:this={canvas}></canvas>
|
||||
|
||||
<!-- The 2D P5.js Overlay (Hearts, Hammer, Fruit) -->
|
||||
<div class="p5-hud" bind:this={p5Container}></div>
|
||||
|
||||
{#if hitFlash} <div class="flash"></div> {/if}
|
||||
|
||||
<!-- TOP-RIGHT LEADERBOARD -->
|
||||
<div class="side-hud">
|
||||
<div class="leaderboard-view">
|
||||
<h3>TOP RUNNERS</h3>
|
||||
<ul>
|
||||
{#if leaderboard.length > 0}
|
||||
{#each leaderboard as entry, i}
|
||||
<li>
|
||||
<span>{i + 1}. {entry.name}</span>
|
||||
<span>{entry.score}</span>
|
||||
</li>
|
||||
{/each}
|
||||
{:else}
|
||||
<li style="opacity: 0.5; justify-content: center;">No scores yet</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. GAME UI OVERLAY -->
|
||||
<div class="ui">
|
||||
<!-- Live Score -->
|
||||
<div class="score">{score}</div>
|
||||
|
||||
<!-- Game Over Modal -->
|
||||
{#if gameOver}
|
||||
<div class="modal">
|
||||
<h1>YOU LOST</h1>
|
||||
<p>Final Score: <strong>{score}</strong></p>
|
||||
|
||||
<!-- Only show the save input if they haven't submitted yet -->
|
||||
{#if !hasSubmitted}
|
||||
<div class="leaderboard-entry">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={playerName}
|
||||
placeholder="ENTER NAME"
|
||||
maxlength="10"
|
||||
/>
|
||||
<button class="save-btn" on:click={saveScore}>SAVE TO BOARD</button>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="saved-msg">SCORE SAVED!</p>
|
||||
{/if}
|
||||
|
||||
<button class="retry-btn" on:click={startGame}>PLAY AGAIN</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
:global(body, html) { margin: 0; padding: 0; height: 100%; overflow: hidden; background: #8cd0f8; cursor: none; }
|
||||
#wrapper { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; width: 100vw; height: 100vh; }
|
||||
canvas { width: 100% !important; height: 100% !important; display: block; }
|
||||
.p5-hud { position: absolute; top: 0; left: 0; pointer-events: auto; z-index: 10; width: 100%; height: 100%; }
|
||||
.ui { position: absolute; inset: 0; pointer-events: none; color: white; text-align: center; font-family: 'Segoe UI', sans-serif; z-index: 11 }
|
||||
.score { font-size: 2.5rem; margin-top: 60px; font-weight: 800; text-shadow: 0 4px 10px rgba(0,0,0,0.2); }
|
||||
|
||||
.modal { pointer-events: auto; background: rgba(255, 255, 255, 0.98); padding: 40px; border-radius: 30px; margin-top: 5vh; color: #1e2b21; display: inline-block; box-shadow: 0 25px 60px rgba(0,0,0,0.15); width: 320px; }
|
||||
|
||||
.leaderboard-entry { margin: 20px 0; }
|
||||
input { width: 80%; padding: 12px; border: 2px solid #eee; border-radius: 10px; font-family: inherit; font-weight: bold; text-align: center; margin-bottom: 10px; }
|
||||
|
||||
/* New Sidebar Container */
|
||||
.side-hud {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 220px;
|
||||
z-index: 15;
|
||||
pointer-events: none; /* Let clicks pass through to the game if needed */
|
||||
}
|
||||
|
||||
/* Updated Leaderboard View */
|
||||
.leaderboard-view {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
border-radius: 15px;
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
color: white;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Ensure buttons and inputs still work */
|
||||
input, button {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaderboard-view h3 {
|
||||
margin-top: 0;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 2px;
|
||||
color: #00d2ff; /* Change color to cyan to match your theme */
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1); /* Lighter border */
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
ul { list-style: none; padding: 0; margin: 0; }
|
||||
|
||||
button { width: 100%; padding: 15px; border: none; border-radius: 12px; cursor: pointer; font-weight: bold; font-size: 1rem; transition: all 0.2s; }
|
||||
.save-btn { background: #00d2ff; color: white; margin-bottom: 5px; }
|
||||
.retry-btn { background: #1e2b21; color: white; margin-top: 10px; }
|
||||
button:hover { transform: translateY(-2px); filter: brightness(1.1); }
|
||||
|
||||
.flash { position: absolute; inset: 0; background: white; z-index: 20; pointer-events: none; }
|
||||
</style>
|
||||
|
||||
// function update() {
|
||||
// const now = performance.now();
|
||||
// const delta = (now - lastTime) / 1000;
|
||||
// lastTime = now;
|
||||
|
||||
// uTime.value += delta;
|
||||
// if (currentMixer) currentMixer.update(delta);
|
||||
// if (!isPlaying) return;
|
||||
|
||||
// // --- MULTIPLIER COUNTDOWN ---
|
||||
// if (multiplierTimer > 0) {
|
||||
// multiplierTimer -= delta;
|
||||
// if (multiplierTimer <= 0) {
|
||||
// multiplierTimer = 0;
|
||||
// scoreMultiplier = 1;
|
||||
// }
|
||||
// }
|
||||
|
||||
// // --- NEW MODULAR ENVIRONMENT CALL ---
|
||||
// updateEnvironment(uTime.value, scene,
|
||||
// { ambientLight, sunLight, headLight },
|
||||
// { sun, moon }
|
||||
// );
|
||||
|
||||
// if (gamePhase === "INSTRUCTIONS") {
|
||||
// instructionTimer -= delta;
|
||||
// if (instructionTimer <= 0) gamePhase = "PLAYING";
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (currentSpeed < CONFIG.MAX_SPEED) {
|
||||
// currentSpeed += CONFIG.ACCELERATION * delta;
|
||||
// }
|
||||
|
||||
// const moveStep = currentSpeed * delta;
|
||||
// // --- BOOSTED DISTANCE SCORE ---
|
||||
// // We apply the multiplier to the floor calculation
|
||||
// score += Math.floor((currentSpeed / 40) * scoreMultiplier);
|
||||
|
||||
|
||||
|
||||
// if (cloudGroup) {
|
||||
// // Moving at 40% speed (moveStep * 0.4) creates a nice parallax depth
|
||||
// cloudGroup.children.forEach(cloud => {
|
||||
// cloud.position.z += moveStep * 0.4;
|
||||
|
||||
// // Reset cloud position if it goes too far behind the camera
|
||||
// if (cloud.position.z > 50) {
|
||||
// cloud.position.z = -250;
|
||||
// cloud.position.x = (Math.random() - 0.5) * 280; // Randomize X again for variety
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (lives <= 0) triggerGameOver();
|
||||
|
||||
// CHUNKS.forEach(chunk => {
|
||||
// chunk.position.z += moveStep;
|
||||
// if (chunk.position.z > CHUNK_SIZE) chunk.position.z -= CHUNK_SIZE * CHUNK_COUNT;
|
||||
// });
|
||||
|
||||
// currX += (lane * CONFIG.lane - currX) * 0.18;
|
||||
// playerAnchor.position.x = currX;
|
||||
|
||||
// if (isJumping) {
|
||||
// jumpV -= CONFIG.grav;
|
||||
// playerY += jumpV;
|
||||
// if (playerY <= 0) {
|
||||
// playerY = 0;
|
||||
// isJumping = false;
|
||||
// if (!isDying) swapCharacter("Running.glb");
|
||||
// }
|
||||
// }
|
||||
// playerAnchor.position.y = playerY;
|
||||
|
||||
// // Update object positions
|
||||
// worldObjects.forEach(obj => { obj.mesh.position.z += moveStep; });
|
||||
|
||||
// // Handle Collisions using the module
|
||||
// worldObjects = handleCollisions(worldObjects, lane, playerY, triggerGameOver);
|
||||
|
||||
// // Filter out-of-bounds objects
|
||||
// worldObjects = worldObjects.filter(obj => {
|
||||
// const active = obj.mesh.position.z < 25;
|
||||
// if (!active) scene.remove(obj.mesh);
|
||||
// return active;
|
||||
// });
|
||||
|
||||
// // Normal Obstacle Spawning
|
||||
// spawnDistanceTracker += moveStep;
|
||||
// if (spawnDistanceTracker >= SPAWN_INTERVAL) {
|
||||
// spawn();
|
||||
// spawnDistanceTracker = 0;
|
||||
|
||||
// }
|
||||
// }
|
||||
10
src/main.js
@@ -1,9 +1,9 @@
|
||||
import { mount } from 'svelte'
|
||||
import './app.css'
|
||||
import App from './App.svelte'
|
||||
// @ts-nocheck
|
||||
import { mount } from 'svelte';
|
||||
import App from './App.svelte';
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.getElementById('app'),
|
||||
})
|
||||
});
|
||||
|
||||
export default app
|
||||
export default app;
|
||||
@@ -3,25 +3,46 @@ export const createSketch = (state, texturesRef) => {
|
||||
return (p) => {
|
||||
// --- PRIVATE UTILITIES ---
|
||||
const drawHeart = (x, y, size, active) => {
|
||||
const s = size / 5;
|
||||
|
||||
// Helper to draw the 8-bit grid shape
|
||||
const drawShape = (posX, posY, pixelSize) => {
|
||||
p.rect(posX + pixelSize, posY, pixelSize, pixelSize);
|
||||
p.rect(posX + 3 * pixelSize, posY, pixelSize, pixelSize);
|
||||
p.rect(posX, posY + pixelSize, 5 * pixelSize, pixelSize);
|
||||
p.rect(posX, posY + 2 * pixelSize, 5 * pixelSize, pixelSize);
|
||||
p.rect(posX + pixelSize, posY + 3 * pixelSize, 3 * pixelSize, pixelSize);
|
||||
p.rect(posX + 2 * pixelSize, posY + 4 * pixelSize, pixelSize, pixelSize);
|
||||
};
|
||||
p.push();
|
||||
p.noStroke();
|
||||
p.fill(active ? [255, 50, 50] : [100, 100, 100, 150]);
|
||||
const s = size / 5;
|
||||
p.rect(x + s, y, s, s); p.rect(x + 3 * s, y, s, s);
|
||||
p.rect(x, y + s, 5 * s, s);
|
||||
p.rect(x, y + 2 * s, 5 * s, s);
|
||||
p.rect(x + s, y + 3 * s, 3 * s, s);
|
||||
p.rect(x + 2 * s, y + 4 * s, s, s);
|
||||
// 1. Draw the Outline (Black, slightly offset/larger)
|
||||
p.fill(0);
|
||||
// We draw it 2 pixels wider in all directions for that thick Minecraft border
|
||||
drawShape(x - 2, y - 2, (size + 4) / 5);
|
||||
|
||||
// 2. Draw the Inner Heart
|
||||
p.fill(active ? [255, 50, 50] : [60, 60, 60, 180]);
|
||||
drawShape(x, y, s);
|
||||
|
||||
// 3. Add a "Highlight" pixel (Minecraft hearts have a little white glint)
|
||||
if (active) {
|
||||
p.fill(255, 150);
|
||||
p.rect(x + s, y + s, s, s);
|
||||
}
|
||||
p.pop();
|
||||
};
|
||||
|
||||
p.setup = async () => {
|
||||
p.createCanvas(p.windowWidth, p.windowHeight);
|
||||
const loadImg = (path) => new Promise(resolve => {
|
||||
p.loadImage(`images/${path}`, img => resolve(img), () => resolve(null));
|
||||
p.loadImage(`images/${path}`, img => resolve(img),
|
||||
(err) => {
|
||||
console.error(`Failed to load image at: ${path}`, err);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
|
||||
// Load into the reference object passed from App.svelte
|
||||
texturesRef.STRAWBERRY = await loadImg('strawberry.png');
|
||||
texturesRef.WATERMELON = await loadImg('watermelon.png');
|
||||
texturesRef.BLUEBERRY = await loadImg('blubb.png');
|
||||
@@ -70,7 +91,7 @@ export const createSketch = (state, texturesRef) => {
|
||||
}
|
||||
|
||||
// 4. Render Hearts
|
||||
for (let i = 0; i < 5; i++) drawHeart(20 + (i * 35), 20, 25, i < state.lives);
|
||||
Array.from({ length: 5 }).map((_, i) => drawHeart(25 + (i * 60), 25, 45, i < state.lives));
|
||||
|
||||
// 5. Random Target Spawning
|
||||
if (p.random(1) < 0.004) {
|
||||
@@ -83,12 +104,12 @@ export const createSketch = (state, texturesRef) => {
|
||||
|
||||
// 6. Current Target Box
|
||||
p.push();
|
||||
p.fill(0, 150); p.rect(10, 60, 120, 140, 15);
|
||||
p.fill(0, 150); p.rect(10, 80, 140, 140, 15);
|
||||
p.fill(255); p.textSize(14); p.textAlign(p.CENTER);
|
||||
p.text("CURRENT TARGET", 70, 85);
|
||||
p.text("CURRENT TARGET",80, 105);
|
||||
const boxImg = texturesRef[state.targetType];
|
||||
if (boxImg) p.image(boxImg, 40, 95, 60, 60);
|
||||
p.fill(0, 255, 200); p.text(state.targetType, 70, 185);
|
||||
if (boxImg) p.image(boxImg, 40, 120, 70, 70);
|
||||
p.fill(0, 255, 200); p.text(state.targetType, 80, 210);
|
||||
p.pop();
|
||||
|
||||
// 7. Render/Move Targets
|
||||
|
||||
@@ -4,4 +4,5 @@ import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
base: '/infinite-runner-multitasking-game/',
|
||||
})
|
||||
|
||||