Compare commits

...

10 Commits

Author SHA1 Message Date
Mak
505b507aa4 done 2026-05-10 22:41:30 +09:00
Mak
fd145fe48f filled in the readme 2026-05-10 22:31:15 +09:00
Mak
2d932c5a42 Your commit message 2026-05-10 18:31:59 +09:00
Mak
fc4e4807fe Your commit message 2026-05-10 18:26:24 +09:00
Mak
1e37ad41e5 fixed case sensitive controls 2026-05-10 18:23:40 +09:00
Mak
dac0d37d53 Your commit message 2026-05-10 18:08:18 +09:00
Mak
0ddf026543 Your commit message 2026-05-10 17:53:29 +09:00
Mak
39153ec658 Your commit message 2026-05-10 17:48:19 +09:00
Mak
0701f1274a fixed base in vite 2026-05-10 17:42:55 +09:00
Mak
2f04d4f905 cleaned up a bit 2026-05-10 17:39:40 +09:00
29 changed files with 37363 additions and 1188 deletions

160
README.md
View File

@@ -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 isnt 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>
<!-- ![alt text](image.png) -->

36399
cats.ai Normal file

File diff suppressed because one or more lines are too long

BIN
image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 KiB

614
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
},

View File

Before

Width:  |  Height:  |  Size: 614 KiB

After

Width:  |  Height:  |  Size: 614 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

Before

Width:  |  Height:  |  Size: 717 KiB

After

Width:  |  Height:  |  Size: 717 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

Before

Width:  |  Height:  |  Size: 331 KiB

After

Width:  |  Height:  |  Size: 331 KiB

View File

Before

Width:  |  Height:  |  Size: 819 KiB

After

Width:  |  Height:  |  Size: 819 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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 */

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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;
// }
// }

View File

@@ -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;

View File

@@ -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

View File

@@ -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/',
})