diff --git a/.github/ISSUE_TEMPLATE/documentation_update.md b/.github/ISSUE_TEMPLATE/documentation_update.md new file mode 100644 index 00000000..a5671fbe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation_update.md @@ -0,0 +1,29 @@ +--- +name: ๐Ÿงพ Documentation Update +about: Suggest improvements or corrections to documentation +title: "[DOCS] " +labels: documentation +--- + +## ๐Ÿ“ Description + +Explain what part of the documentation needs updating. + +--- + +## ๐Ÿ“š Current Issue + +Whatโ€™s wrong, missing, or unclear in the documentation? + +--- + +## ๐Ÿง  Suggested Update + +Describe what changes should be made and where. + +--- + +## โœ… Checklist + +- [ ] I have checked if this documentation section already exists. +- [ ] I have clearly described what needs to be updated or added. diff --git a/.github/ISSUE_TEMPLATE/enhancement_request.md b/.github/ISSUE_TEMPLATE/enhancement_request.md new file mode 100644 index 00000000..1b2500cc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement_request.md @@ -0,0 +1,35 @@ +--- +name: โšก Enhancement / Optimization +about: Suggest performance improvements or code optimizations +title: "[ENHANCEMENT] " +labels: enhancement +--- + +## โš™๏ธ Current Behavior + +Describe what currently happens and where the performance/optimization issue exists. + +--- + +## ๐Ÿš€ Proposed Improvement + +Explain the improvement or optimization you suggest. + +--- + +## ๐Ÿ” Why Itโ€™s Needed + +Why is this enhancement important? + +--- + +## ๐Ÿงฉ Possible Implementation + +Share details or snippets if you have ideas on how to implement it. + +--- + +## โœ… Checklist + +- [ ] Iโ€™ve reviewed existing issues to ensure this isnโ€™t a duplicate. +- [ ] Iโ€™ve explained how this improves performance or readability. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..8d7a6da0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,42 @@ +--- +name: โœจ Feature Request +about: Suggest a new feature or enhancement for this project +title: "[FEATURE] " +labels: enhancement +--- + +## ๐Ÿ’ก Summary + +Describe the feature youโ€™d like to see added. + +--- + +## ๐Ÿ” Problem Statement + +What problem does this feature solve? +Why is it important? + +--- + +## ๐Ÿงฉ Proposed Solution + +Explain your idea or approach clearly. + +--- + +## ๐Ÿง  Alternatives Considered + +Mention any other solutions or workarounds youโ€™ve thought of. + +--- + +## ๐ŸŽจ Additional Context (Optional) + +Add any mockups, references, or related ideas here. + +--- + +## โœ… Checklist + +- [ ] I have checked existing issues for similar feature requests. +- [ ] I have clearly explained the benefit of this feature. diff --git a/.github/ISSUE_TEMPLATE/other.md b/.github/ISSUE_TEMPLATE/other.md new file mode 100644 index 00000000..ff08d003 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/other.md @@ -0,0 +1,23 @@ +--- +name: ๐Ÿ”ง Other Issue +about: For any other issue not covered by the templates above +title: "[OTHER] " +labels: help wanted +--- + +## ๐Ÿงฉ Description + +Provide details about your issue or suggestion. + +--- + +## ๐Ÿง  Context / Background + +Why are you opening this issue? + +--- + +## โœ… Checklist + +- [ ] I have checked existing issues before creating this one. +- [ ] I have included enough information for maintainers to understand. diff --git a/.github/ISSUE_TEMPLATE/refactor_request.md b/.github/ISSUE_TEMPLATE/refactor_request.md new file mode 100644 index 00000000..cb4418ff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/refactor_request.md @@ -0,0 +1,30 @@ +--- +name: ๐Ÿงฐ Refactoring +about: Propose a refactor for cleaner or more efficient code +title: "[REFACTOR] " +labels: refactor +--- + +## ๐Ÿงฉ Current Code Situation + +Briefly explain what part of the code needs refactoring and why. + +--- + +## ๐Ÿง  Reason for Refactor + +What issues does the current code have (duplication, readability, performance, etc.)? + +--- + +## ๐Ÿšง Proposed Refactor Plan + +Outline your suggested changes or structure. + +--- + +## โœ… Checklist + +- [ ] I have identified the part of the code that needs refactoring. +- [ ] I have ensured no functionality will be broken. +- [ ] I have tested similar code paths before. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6a4ce5d8..a653a8d8 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -13,7 +13,7 @@ Briefly describe your changes. - [ ] Other ## Related Issue -Link the issue this PR closes (if any): # +Closes # ## Files Added / Changed - [ ] index.html diff --git a/.github/scripts/resolve-games-conflict.js b/.github/scripts/resolve-games-conflict.js new file mode 100644 index 00000000..ffeb49bc --- /dev/null +++ b/.github/scripts/resolve-games-conflict.js @@ -0,0 +1,73 @@ +/** + * Auto-resolve merge conflicts in `script.js` limited to the `games` array. + * This script merges all game objects from both conflict sides, removes duplicates, + * and keeps valid JS syntax. + */ + +import fs from "fs"; + +const filePath = "script.js"; +let file = fs.readFileSync(filePath, "utf8"); + +if (!file.includes("<<<<<<<")) { + console.log("โœ… No conflict markers found โ€” nothing to fix."); + process.exit(0); +} + +// Safety check โ€” ensure conflict is inside `const games = [` +if (!file.includes("const games = [")) { + console.error("โŒ Conflict is not inside the games array. Exiting for safety."); + process.exit(1); +} + +/** + * Extract all `{...}` game objects inside the conflict blocks. + */ +function extractGameObjects(text) { + const objects = []; + const regex = /\{[\s\S]*?\}/g; + let match; + while ((match = regex.exec(text))) { + // Only add unique ones + const obj = match[0].trim(); + if (!objects.includes(obj)) objects.push(obj); + } + return objects; +} + +/** + * Process each conflict block and merge entries. + */ +const conflictRegex = /<<<<<<<[\s\S]*?=======([\s\S]*?)>>>>>>>[\s\S]*?/g; +let resolved = file; + +resolved = resolved.replace(conflictRegex, (block) => { + console.log("โš™๏ธ Resolving one conflict block in script.js"); + + const parts = block.split(/=======/); + const head = parts[0].split("\n").slice(1).join("\n"); + const incoming = parts[1].split("\n").slice(1, -1).join("\n"); + + const allGames = [ + ...extractGameObjects(head), + ...extractGameObjects(incoming), + ]; + + // Deduplicate by game name if possible + const uniqueGames = []; + const seen = new Set(); + + for (const obj of allGames) { + const nameMatch = obj.match(/name:\s*["'`](.*?)["'`]/); + const name = nameMatch ? nameMatch[1].toLowerCase() : obj; + if (!seen.has(name)) { + seen.add(name); + uniqueGames.push(obj); + } + } + + return uniqueGames.join(",\n"); +}); + +fs.writeFileSync(filePath, resolved); +console.log("โœ… script.js games array auto-merged successfully!"); diff --git a/.github/workflows/auto-resolve-script-conflict.yml b/.github/workflows/auto-resolve-script-conflict.yml new file mode 100644 index 00000000..219b3216 --- /dev/null +++ b/.github/workflows/auto-resolve-script-conflict.yml @@ -0,0 +1,53 @@ +name: Auto Resolve script.js Game Conflicts + +on: + pull_request: + paths: + - "script.js" + types: [opened, synchronize, reopened] + +permissions: + contents: write + pull-requests: write + +jobs: + resolve-conflicts: + runs-on: ubuntu-latest + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Attempt merge with base + run: | + git fetch origin ${{ github.base_ref }} + git merge origin/${{ github.base_ref }} --no-commit || echo "Merge conflict detected" + + - name: Check for conflict markers + id: conflict_check + run: | + if grep -q "<<<<<<<" script.js; then + echo "conflict=true" >> $GITHUB_OUTPUT + else + echo "conflict=false" >> $GITHUB_OUTPUT + fi + + - name: Auto-fix script.js conflicts (games array only) + if: steps.conflict_check.outputs.conflict == 'true' + run: node .github/scripts/resolve-games-conflict.js + + - name: Commit and push fix + if: steps.conflict_check.outputs.conflict == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add script.js + git commit -m "๐Ÿค– Auto-resolved script.js game array merge conflict" + git push diff --git a/.github/workflows/issue-create-automate-message.yml b/.github/workflows/issue-create-automate-message.yml new file mode 100644 index 00000000..d23f2f20 --- /dev/null +++ b/.github/workflows/issue-create-automate-message.yml @@ -0,0 +1,32 @@ +name: Auto Comment on Issue + +on: + issues: + types: [opened] + +permissions: + issues: write + +jobs: + comment: + runs-on: ubuntu-latest + steps: + - name: Add Comment to Issue + uses: actions/github-script@v6 + with: + script: | + const issueNumber = context.issue.number; + const commentBody1 = `### Thank you for raising this issue!\n We'll review it as soon as possible. We truly appreciate your contributions! โœจ\n\n> Meanwhile make sure you've visited the README.md, CONTRIBUTING.md, and CODE_OF_CONDUCT.md before creating a PR for this. Also, please do NOT create a PR until this issue has been assigned to you. ๐Ÿ˜Š`; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: commentBody1 + }); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: commentBody2 + }); + console.log('Both comments added successfully.'); diff --git a/.github/workflows/pr-create-automate-message.yml b/.github/workflows/pr-create-automate-message.yml new file mode 100644 index 00000000..f00d7b20 --- /dev/null +++ b/.github/workflows/pr-create-automate-message.yml @@ -0,0 +1,30 @@ +name: Auto Comment on PR + + +on: + pull_request_target: + types: [opened] + + +permissions: + issues: write + pull-requests: write + + +jobs: + comment: + runs-on: ubuntu-latest + steps: + - name: Comment on PR + uses: actions/github-script@v6 + with: + script: | + const prNumber = context.issue.number; + const commentBody = `### Thanks for creating a PR for your Issue! โ˜บ๏ธ\n\nWe'll review it as soon as possible.\nIn the meantime, please double-check the **file changes** and ensure that **all commits** are accurate.\n\nIf there are any **unresolved review comments**, feel free to resolve them. ๐Ÿ™Œ๐Ÿผ`; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: commentBody + }); + console.log('Comment added successfully.'); diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..ed67a813 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,27 @@ +name: Mark stale issues and PRs + +on: + schedule: + - cron: "0 0 * * *" # Runs every day at midnight (UTC) + workflow_dispatch: # Allows manual run + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: "This issue has been automatically marked as **stale** because it hasnโ€™t had any activity in 5 days. Please comment or remove the stale label if this is still relevant." + stale-pr-message: "This pull request has been automatically marked as **stale** because it hasnโ€™t had any activity in 5 days. Please comment or remove the stale label if this PR is still relevant." + days-before-stale: 5 + days-before-close: 2 + stale-issue-label: "stale" + stale-pr-label: "stale" + exempt-issue-labels: "pinned,security,enhancement" + exempt-pr-labels: "work-in-progress" diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..92d48486 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.vscode/ +.env +node_modules/ +dist/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 6f3a2913..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "liveServer.settings.port": 5501 -} \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..2d6bbcd9 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,110 @@ +# ๐Ÿ“œ Code of Conduct + +## Our Pledge + +As contributors, maintainers, and members of the **mini-js-games-hub** community, we are dedicated to creating a welcoming, inclusive, and harassment-free environment for everyone. + +We celebrate diversity and do not tolerate discrimination based on: + +- Age +- Body size +- Visible or invisible disability +- Ethnicity +- Sex characteristics +- Gender identity or expression +- Experience level +- Education +- Socio-economic background +- Nationality +- Personal appearance +- Race +- Religion +- Sexual identity or orientation + +We commit to fostering a positive and respectful space for collaboration and learning. + +--- + +## Our Standards + +### Examples of positive behavior: +- โœ… Using inclusive and encouraging language +- โœ… Being respectful of differing opinions and experiences +- โœ… Providing and accepting constructive feedback gracefully +- โœ… Prioritizing the well-being and growth of the community +- โœ… Demonstrating empathy and understanding toward others + +### Examples of unacceptable behavior: +- ๐Ÿšซ Harassment, trolling, or personal attacks +- ๐Ÿšซ Discriminatory language or hate speech +- ๐Ÿšซ Unwanted sexual attention or behavior +- ๐Ÿšซ Sharing personal information of others without explicit consent +- ๐Ÿšซ Persistent disruptive or disrespectful behavior + +--- + +## Our Responsibilities + +Project maintainers are responsible for clarifying and enforcing the standards of acceptable behavior. +They are empowered to take appropriate and fair actions in response to any behavior that is: + +- Inappropriate +- Threatening +- Offensive +- Harmful to the community + +Maintainers have the right to **remove**, **edit**, or **reject** comments, commits, code, issues, or any other contributions that do not align with this Code of Conduct. + +--- + +## Scope + +This Code of Conduct applies across all platforms related to the project, including: + +- GitHub repositories +- Pull requests and issues +- Discussions and comment threads +- Community channels such as Discord and Slack + +--- + +## Reporting and Enforcement + +If you witness or experience any violation of this Code of Conduct, please contact the maintainers at: + +๐Ÿ“ง **Email:** ghoshritaban2006@gmail.com + +๐Ÿ› ๏ธ You may also use GitHubโ€™s built-in **Report Abuse** feature if preferred. + +All reports will be reviewed promptly, fairly, and confidentially. We will take every step necessary to ensure the safety and privacy of reporters. + +--- + +## Enforcement Guidelines + +Community leaders will follow these guidelines to determine the consequences for behavior that violates this Code of Conduct. + +### 1. Correction +**Community Impact:** Use of inappropriate language or minor unprofessional behavior. +**Consequence:** A private warning and clarification on the nature of the violation. A public apology may be requested. + +### 2. Warning +**Community Impact:** A single significant incident or repeated minor violations. +**Consequence:** A formal written warning with potential restrictions on community engagement for a set period. + +### 3. Temporary Ban +**Community Impact:** Serious violation of community standards or ongoing inappropriate conduct. +**Consequence:** Temporary removal from all community interactions. Violation of these terms may result in a permanent ban. + +### 4. Permanent Ban +**Community Impact:** Continuous or severe harassment, discrimination, or repeated misconduct. +**Consequence:** Permanent exclusion from all community spaces and activities. + +--- + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html), version 2.1. +Enforcement guidelines are inspired by [Mozillaโ€™s Code of Conduct Enforcement Ladder](https://github.com/mozilla/diversity). + +For additional information, visit the [Contributor Covenant FAQ](https://www.contributor-covenant.org/faq). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc1a425e..7282e731 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Thank you for your interest in contributing! โค๏ธ This project is open to everyone โ€” from beginners to pros. --- - + ## ๐Ÿงฉ Ways You Can Contribute - Add a **new mini-game** - Fix **bugs or typos** @@ -56,6 +56,16 @@ This project is open to everyone โ€” from beginners to pros. --- +## โณ Stale Issue & PR Policy +### To keep the repository clean and active, we automatically mark inactive issues and pull requests as stale: +- If an issue or PR has **no activity for 5 days**, it will be **marked as stale** with a comment. +- If it remains inactive for **another 2 days**, it may be **automatically closed**. +- You can **remove the `stale` label** or **comment on the thread** to keep it open. + +### This helps contributors focus on active work and keeps the project manageable. ๐Ÿš€ + +--- + ## ๐Ÿ’ฌ Need Help? Open an [issue](https://github.com/ritaban06/mini-js-games-hub/issues) for questions or discussions. Weโ€™re happy to guide you! diff --git a/README.md b/README.md index 06d978e7..d912e1bc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +> [!NOTE] +> All issues & PRs raised after **10 PM Kolkata, India time** will not be considered as per the **Open Odyssey** event rules. + + # ๐ŸŽฎ Mini JS Games Hub A fun collection of mini JavaScript games built using **HTML**, **CSS**, and **JavaScript**! @@ -16,46 +20,335 @@ You can: --- -## ๐Ÿ“‚ Project Structure +## ๐Ÿ“‚ Project Structure ``` mini-js-games-hub/ โ”‚ โ”œโ”€โ”€ index.html # Home page listing all games โ”œโ”€โ”€ style.css # Global styling โ”œโ”€โ”€ script.js # Handles navigation and game loading -โ”‚ -โ””โ”€โ”€ games/ - โ”œโ”€โ”€ tictactoe/ - โ”‚ โ”œโ”€โ”€ index.html - โ”‚ โ”œโ”€โ”€ style.css - โ”‚ โ””โ”€โ”€ script.js - โ”œโ”€โ”€ snake/ - โ”‚ โ”œโ”€โ”€ index.html - โ”‚ โ”œโ”€โ”€ style.css - โ”‚ โ””โ”€โ”€ script.js - โ”œโ”€โ”€ memory/ - โ”‚ โ”œโ”€โ”€ index.html - โ”‚ โ”œโ”€โ”€ style.css - โ”‚ โ””โ”€โ”€ script.js - โ”œโ”€โ”€ whack-a-mole/ - โ”‚ โ”œโ”€โ”€ index.html - โ”‚ โ”œโ”€โ”€ style.css - โ”‚ โ””โ”€โ”€ script.js - โ””โ”€โ”€ reaction-timer/ - โ”œโ”€โ”€ index.html - โ”œโ”€โ”€ style.css - โ””โ”€โ”€ script.js +โ””โ”€โ”€ games/ # All game directories +โ”œโ”€โ”€ 15-puzzle/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ 2048/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ 8-ball-pool/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ asteroids/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ balloon-pop/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ boom/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ breakout/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ”œโ”€โ”€ script.js +โ”‚ โ””โ”€โ”€ README.md +โ”œโ”€โ”€ burger-builder/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ catch-the-ball/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ Catch_The_Dot/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ coin_toss_simulator/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ color-clicker/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ color-guessing-game/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ color-squid-puzzle/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ Connect-four/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ cozy-blocks/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ endless-runner/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ find-hidden-object/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ”œโ”€โ”€ script.js +โ”‚ โ””โ”€โ”€ levels/ +โ”œโ”€โ”€ flappy-bird/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ”œโ”€โ”€ script.js +โ”‚ โ”œโ”€โ”€ README.md +โ”‚ โ””โ”€โ”€ thumbnail.svg +โ”œโ”€โ”€ Frogger/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ grass-defense/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ hangman/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ island-survival/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ line-game/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ link-game/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ Logic-Chain/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ maiolike-block-puzzle/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ meme_generator/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ memory/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ merge-lab/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ Number_Gussing_game/ +โ”‚ โ”œโ”€โ”€ NGG.html +โ”‚ โ”œโ”€โ”€ NGG.css +โ”‚ โ””โ”€โ”€ NGG.js +โ”œโ”€โ”€ odd-one-out/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ peglinko/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ pixel-art-creator/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ pong/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ quiz-game/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ quote/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ reaction-timer/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ rock-paper-scissors/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ shadow-catcher/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ Simon-Says-Game/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ”œโ”€โ”€ app.js +โ”‚ โ””โ”€โ”€ images/ +โ”œโ”€โ”€ SimonSays/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ app.js +โ”œโ”€โ”€ snake/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ space-shooter/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ sudoku/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ tap-reveal/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ tap-the-bubble/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ tetris/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ The Godzilla Fights game/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ tictactoe/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ tileman/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ tiny-fishing/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ tower-defense/ +โ”‚ โ””โ”€โ”€ index.html +โ”œโ”€โ”€ typing-test/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ whack-a-mole/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ word-scramble/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ”œโ”€โ”€ words-of-wonders/ +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ script.js +โ””โ”€โ”€ worlds-easiest-game/ +โ”œโ”€โ”€ index.html +โ”œโ”€โ”€ style.css +โ””โ”€โ”€ script.js ``` --- +## ๐ŸŽฎ Games Included + +This repository currently features **58 interactive mini-games** built with HTML, CSS, and JavaScript: + +### Puzzle Games +- **15 Puzzle** - Classic sliding tile puzzle to arrange numbers in order +- **2048** - Merge tiles to reach 2048 +- **Color Squid Puzzle** - Color-based puzzle challenge +- **Logic Chain** - Solve logical sequences +- **Maiolike Block Puzzle** - Arrange blocks in patterns +- **Sudoku** - Fill the grid with numbers 1-9 +- **Word Scramble** - Unscramble the letters to form words +- **Words of Wonders** - Word puzzle game + +### Arcade Games +- **Asteroids** - Classic space shooter game +- **Breakout** - Break bricks with a bouncing ball +- **Flappy Bird** - Navigate through pipes by tapping +- **Frogger** - Help the frog cross the road +- **Pong** - Classic two-paddle arcade game +- **Snake** - Eat food and grow longer without hitting walls +- **Space Shooter** - Shoot down enemies in space +- **Tetris** - Stack falling blocks to clear lines +- **Whack-a-Mole** - Hit the moles as they pop up + +### Action & Reflexes +- **Balloon Pop** - Pop balloons as fast as you can +- **Boom** - Explosive action game +- **Catch the Ball** - Catch falling balls +- **Catch The Dot** - Click the moving dot quickly +- **Endless Runner** - Run infinitely avoiding obstacles +- **Grass Defense** - Defend your territory +- **Island Survival** - Survive on a deserted island +- **Reaction Timer** - Test your reaction speed +- **Shadow Catcher** - Catch the moving shadows +- **Tap Reveal** - Tap to reveal hidden items +- **Tap the Bubble** - Pop bubbles by tapping +- **The Godzilla Fights** - Epic monster battle game +- **Tower Defense** - Defend against waves of enemies +- **World's Easiest Game** - Deceptively challenging game + +### Strategy & Logic +- **8 Ball Pool** - Play pool/billiards +- **Connect Four** - Four-in-a-row strategy game +- **Rock Paper Scissors** - Classic hand game +- **Tic Tac Toe** - Classic X's and O's game + +### Memory & Pattern Games +- **Color Clicker** - Click matching colors +- **Color Guessing Game** - Guess the RGB color value +- **Memory** - Match pairs of cards +- **Odd One Out** - Find the different item +- **Simon Says Game** - Repeat the pattern sequence +- **SimonSays** - Follow the pattern game + +### Creative & Building +- **Burger Builder** - Create custom burgers +- **Cozy Blocks** - Build with cozy blocks +- **Meme Generator** - Create funny memes +- **Pixel Art Creator** - Draw pixel art creations + +### Word & Trivia +- **Hangman** - Guess the word letter by letter +- **Quiz Game** - Answer trivia questions +- **Typing Test** - Measure your typing speed and accuracy -## ๐Ÿง  Games Included -- ๐ŸŽฒ **Tic Tac Toe** โ€“ classic 3x3 strategy -- ๐Ÿ **Snake Game** โ€“ grow without hitting the walls -- ๏ฟฝ **Memory Game** โ€“ match all emoji pairs -- ๐Ÿ”จ **Whack-a-Mole** โ€“ hit the mole before it vanishes -- โšก **Reaction Timer** โ€“ tap as quickly as you can -- โฑ๏ธ *More coming soon! Add yours too...* +### Casual & Fun +- **Coin Toss Simulator** - Flip a virtual coin +- **Find Hidden Object** - Locate hidden objects in scenes +- **Line Game** - Draw lines without crossing +- **Link Game** - Connect matching items +- **Merge Lab** - Merge similar items together +- **Number Guessing Game** - Guess the secret number +- **Peglinko** - Drop the ball through pegs +- **Quote** - Display random inspirational quotes +- **Tileman** - Tile-based adventure +- **Tiny Fishing** - Relax with simple fishing + +--- + +**Total: 58 Games and Growing!** ๐ŸŽฎ + +Each game is self-contained with its own HTML, CSS, and JavaScript files, making it easy to play, modify, or learn from. --- @@ -90,6 +383,18 @@ To add your own game or fix an issue, please check the [CONTRIBUTING.md](CONTRIB --- +## โœจ Contributors + +#### Thanks to all the wonderful contributors ๐Ÿ’– + + + + + +#### See full list of contributor contribution [Contribution Graph](https://github.com/ritaban06/mini-js-games-hub/graphs/contributors) + +--- + ## ๐Ÿชช License This project is licensed under the [**MIT License**](LICENSE) โ€” free to use and modify. diff --git a/games/ peglinko/index.html b/games/ peglinko/index.html new file mode 100644 index 00000000..720e8aa1 --- /dev/null +++ b/games/ peglinko/index.html @@ -0,0 +1,23 @@ + + + + + + Peglinko | Mini JS Games Hub + + + +
+
+

๐ŸŽฏ Peglinko

+
+

Score: 0

+ +
+
+ +
+ + + + diff --git a/games/ peglinko/script.js b/games/ peglinko/script.js new file mode 100644 index 00000000..f679f625 --- /dev/null +++ b/games/ peglinko/script.js @@ -0,0 +1,138 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +canvas.width = 400; +canvas.height = 600; + +let pegs = []; +let balls = []; +let score = 0; + +const scoreEl = document.getElementById('score'); +const restartBtn = document.getElementById('restart'); + +class Peg { + constructor(x, y, radius, color) { + this.x = x; + this.y = y; + this.radius = radius; + this.color = color; + } + draw() { + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.shadowColor = this.color; + ctx.shadowBlur = 10; + ctx.fill(); + ctx.closePath(); + ctx.shadowBlur = 0; + } +} + +class Ball { + constructor(x, y, radius, color, speedY) { + this.x = x; + this.y = y; + this.radius = radius; + this.color = color; + this.speedX = 0; + this.speedY = speedY; + } + update() { + this.y += this.speedY; + this.x += this.speedX; + + // collision with pegs + for (let peg of pegs) { + const dx = this.x - peg.x; + const dy = this.y - peg.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist < this.radius + peg.radius) { + this.speedY = -this.speedY * 0.8; + this.speedX = (Math.random() - 0.5) * 6; + score += 10; + scoreEl.textContent = score; + peg.color = "#fff"; // flash peg + setTimeout(() => (peg.color = randomColor()), 200); + } + } + + // bounce from walls + if (this.x < this.radius || this.x > canvas.width - this.radius) + this.speedX = -this.speedX; + + // floor + if (this.y + this.radius > canvas.height) { + this.y = canvas.height - this.radius; + this.speedY = 0; + this.speedX = 0; + } + + // gravity + this.speedY += 0.2; + } + + draw() { + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.shadowColor = this.color; + ctx.shadowBlur = 15; + ctx.fill(); + ctx.closePath(); + ctx.shadowBlur = 0; + } +} + +function randomColor() { + const colors = ["#ff4081", "#00e5ff", "#76ff03", "#ffea00", "#ff9100"]; + return colors[Math.floor(Math.random() * colors.length)]; +} + +function setupPegs() { + pegs = []; + const rows = 8; + const cols = 10; + const spacingX = 40; + const spacingY = 50; + const offsetX = 40; + const offsetY = 100; + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const x = offsetX + col * spacingX + (row % 2 === 0 ? 0 : spacingX / 2); + const y = offsetY + row * spacingY; + pegs.push(new Peg(x, y, 6, randomColor())); + } + } +} + +function drawPegs() { + pegs.forEach((peg) => peg.draw()); +} + +canvas.addEventListener("click", (e) => { + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + balls.push(new Ball(x, 20, 8, randomColor(), 4)); +}); + +restartBtn.addEventListener("click", () => { + score = 0; + scoreEl.textContent = 0; + balls = []; + setupPegs(); +}); + +function animate() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawPegs(); + balls.forEach((ball) => { + ball.update(); + ball.draw(); + }); + requestAnimationFrame(animate); +} + +// Initialize +setupPegs(); +animate(); diff --git a/games/ peglinko/style.css b/games/ peglinko/style.css new file mode 100644 index 00000000..c657ae6a --- /dev/null +++ b/games/ peglinko/style.css @@ -0,0 +1,47 @@ +body { + margin: 0; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: radial-gradient(circle at top, #1d1e22, #0d0d0f); + color: #fff; + font-family: 'Poppins', sans-serif; +} + +.game-container { + text-align: center; +} + +header { + margin-bottom: 10px; +} + +.scoreboard { + display: flex; + justify-content: center; + align-items: center; + gap: 20px; +} + +button { + background: #ff4081; + border: none; + padding: 6px 14px; + color: white; + font-size: 16px; + border-radius: 6px; + cursor: pointer; + transition: 0.2s; +} + +button:hover { + background: #ff679b; +} + +canvas { + background: linear-gradient(180deg, #252631, #101015); + border: 2px solid #ff4081; + border-radius: 10px; + box-shadow: 0 0 20px rgba(255, 64, 129, 0.5); +} diff --git a/games/8-ball-pool/index.html b/games/8-ball-pool/index.html new file mode 100644 index 00000000..a5d04f48 --- /dev/null +++ b/games/8-ball-pool/index.html @@ -0,0 +1,58 @@ + + + + + + 8 Ball Pool โ€” Mini JS Games Hub + + + +
+
+ โ† Back +

8-Ball Pool โ€” Local Multiplayer

+
+ + +
+
+ +
+
+ + +
+
+
+
Player 1
+
โ€”
+
0
+
+
+
Player 2
+
โ€”
+
0
+
+
+ +
+
Turn: Player 1
+
Shot power:
+
+
+
+
Click & drag on cue to aim and set power โ†’ release to shoot
+
Solids โ€ข Stripes
+
+
+
+
+ +
+ Built with Canvas โ€ข Two-player same-device โ€ข Physics tuned for smooth play +
+
+ + + + diff --git a/games/8-ball-pool/script.js b/games/8-ball-pool/script.js new file mode 100644 index 00000000..1af726de --- /dev/null +++ b/games/8-ball-pool/script.js @@ -0,0 +1,490 @@ +// 8-Ball Pool โ€” Canvas implementation (local 2-player) +// Put this file at: games/8-ball-pool/script.js + +(() => { + /*** Constants & Utilities ***/ + const canvas = document.getElementById('table'); + const ctx = canvas.getContext('2d', { alpha: false }); + const W = canvas.width, H = canvas.height; + + // Table layout + const RAIL = 30; // margin around + const POCKET_RADIUS = 22; + const BALL_RADIUS = 12; + const FRICTION = 0.994; // per frame multiplier + const MIN_SPEED = 0.02; // below this, ball stops + const MAX_POWER = 22; // max velocity applied to cue ball + + // Pocket centers (6 pockets) + const pockets = [ + { x: RAIL + POCKET_RADIUS, y: RAIL + POCKET_RADIUS }, + { x: W / 2, y: RAIL + POCKET_RADIUS }, + { x: W - RAIL - POCKET_RADIUS, y: RAIL + POCKET_RADIUS }, + { x: RAIL + POCKET_RADIUS, y: H - RAIL - POCKET_RADIUS }, + { x: W / 2, y: H - RAIL - POCKET_RADIUS }, + { x: W - RAIL - POCKET_RADIUS, y: H - RAIL - POCKET_RADIUS }, + ]; + + // Game state + let balls = []; // ball objects + let cueBall; // reference to ball with id 0 + let animationId; + let isAiming = false; + let aimStart = null; + let aimCurrent = null; + let currentPlayer = 1; // 1 or 2 + let playerData = { + 1: { type: null, score: 0 }, + 2: { type: null, score: 0 } + }; + let isBallsMoving = false; + let messageEl = document.getElementById('message'); + let powerFill = document.getElementById('powerFill'); + const turnText = document.getElementById('turnText'); + const p1El = document.getElementById('player1'); + const p2El = document.getElementById('player2'); + + // ball color scheme: 0 cue (white), 1-7 solids, 8 eight, 9-15 stripes + const ballColors = { + 0: '#ffffff', + 1: '#d32f2f', 2: '#e53935', 3: '#ff6f00', 4: '#fbc02d', 5: '#7cb342', 6: '#039be5', 7: '#6a1b9a', + 8: '#000000', + 9: '#d32f2f', 10: '#e53935', 11: '#ff6f00', 12: '#fbc02d', 13: '#7cb342', 14: '#039be5', 15: '#6a1b9a' + }; + + // initial rack positions: we'll position balls in triangular rack on right-side table + function initBalls() { + balls = []; + // cue ball + cueBall = { + id: 0, + x: W * 0.25, + y: H / 2, + vx: 0, + vy: 0, + r: BALL_RADIUS, + potted: false, + color: ballColors[0], + number: 0, + stripe: false + }; + balls.push(cueBall); + + // rack origin + const rackX = W * 0.68; + const rackY = H / 2; + // order for a balanced rack (standard pyramid) + const order = [1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15, 8]; + let idx = 0; + const spacing = BALL_RADIUS * 2 + 1; + for (let row = 0; row < 5; row++) { + for (let col = 0; col <= row; col++) { + const x = rackX + row * (spacing * Math.cos(Math.PI / 6)); + const y = rackY + (col - row / 2) * spacing; + const num = order[idx++]; + balls.push({ + id: num, + x, y, + vx: 0, vy: 0, + r: BALL_RADIUS, + potted: false, + color: ballColors[num], + number: num, + stripe: num >= 9 + }); + } + } + + // reset players assignment + playerData[1].type = null; + playerData[2].type = null; + playerData[1].score = 0; + playerData[2].score = 0; + currentPlayer = 1; + updateUI(); + } + + /*** Draw functions ***/ + function drawTable() { + // felt + ctx.fillStyle = '#0b6b3a'; + ctx.fillRect(0, 0, W, H); + + // inner rail (felt border) + ctx.fillStyle = '#104a2a'; + ctx.fillRect(RAIL, RAIL, W - 2 * RAIL, H - 2 * RAIL); + + // draw pockets + pockets.forEach(p => { + const gradient = ctx.createRadialGradient(p.x - 6, p.y - 6, 4, p.x, p.y, POCKET_RADIUS); + gradient.addColorStop(0, 'rgba(0,0,0,0.6)'); + gradient.addColorStop(1, 'rgba(0,0,0,1)'); + ctx.fillStyle = gradient; + ctx.beginPath(); + ctx.arc(p.x, p.y, POCKET_RADIUS, 0, Math.PI * 2); + ctx.fill(); + }); + + // draw rails (beyond felt) + ctx.fillStyle = '#523426'; + // top + ctx.fillRect(0, 0, W, RAIL); + ctx.fillRect(0, H - RAIL, W, RAIL); + ctx.fillRect(0, 0, RAIL, H); + ctx.fillRect(W - RAIL, 0, RAIL, H); + } + + function drawBalls() { + balls.forEach(b => { + if (b.potted) return; + // ball shadow + ctx.beginPath(); + ctx.fillStyle = 'rgba(0,0,0,0.2)'; + ctx.ellipse(b.x + 4, b.y + 8, b.r * 1.1, b.r * 0.6, 0, 0, Math.PI * 2); + ctx.fill(); + + // ball main + ctx.beginPath(); + ctx.fillStyle = b.color; + ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); + ctx.fill(); + + // draw number/stripe for numbered balls + if (b.number !== 0) { + // stripe rendering for stripes + if (b.stripe) { + ctx.beginPath(); + ctx.fillStyle = '#fff'; + ctx.rect(b.x - b.r, b.y - 6, b.r * 2, 12); + ctx.fill(); + // circle inside + ctx.beginPath(); + ctx.fillStyle = b.color; + ctx.arc(b.x, b.y, b.r / 1.8, 0, Math.PI * 2); + ctx.fill(); + } else if (b.number !== 8) { + // solid: draw white circle with number + ctx.beginPath(); + ctx.fillStyle = '#fff'; + ctx.arc(b.x, b.y, b.r / 2.1, 0, Math.PI * 2); + ctx.fill(); + } else { + // 8-ball: white ring + ctx.beginPath(); + ctx.fillStyle = '#fff'; + ctx.arc(b.x, b.y, b.r / 2.1, 0, Math.PI * 2); + ctx.fill(); + } + + // draw number + ctx.fillStyle = '#000'; + ctx.font = '10px system-ui'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(b.number.toString(), b.x, b.y); + } + // cue ball has subtle stroke + if (b.number === 0) { + ctx.beginPath(); + ctx.strokeStyle = 'rgba(0,0,0,0.15)'; + ctx.lineWidth = 1; + ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); + ctx.stroke(); + } + }); + } + + /*** Physics ***/ + function stepPhysics() { + // move balls + isBallsMoving = false; + for (let b of balls) { + if (b.potted) continue; + b.x += b.vx; + b.y += b.vy; + // friction + b.vx *= FRICTION; + b.vy *= FRICTION; + if (Math.hypot(b.vx, b.vy) < MIN_SPEED) { + b.vx = 0; b.vy = 0; + } else { + isBallsMoving = true; + } + // rail collisions (simple reflection) + if (b.x - b.r < RAIL + 6) { b.x = RAIL + 6 + b.r; b.vx = -b.vx * 0.98; } + if (b.x + b.r > W - RAIL - 6) { b.x = W - RAIL - 6 - b.r; b.vx = -b.vx * 0.98; } + if (b.y - b.r < RAIL + 6) { b.y = RAIL + 6 + b.r; b.vy = -b.vy * 0.98; } + if (b.y + b.r > H - RAIL - 6) { b.y = H - RAIL - 6 - b.r; b.vy = -b.vy * 0.98; } + + // pocket detection + for (let p of pockets) { + const d = Math.hypot(b.x - p.x, b.y - p.y); + if (d < POCKET_RADIUS - 6) { + // potted + b.potted = true; + b.vx = b.vy = 0; + onBallPotted(b); + } + } + } + + // collisions ball-ball + for (let i = 0; i < balls.length; i++) { + for (let j = i + 1; j < balls.length; j++) { + const a = balls[i], b = balls[j]; + if (a.potted || b.potted) continue; + const dx = b.x - a.x, dy = b.y - a.y; + const dist = Math.hypot(dx, dy); + if (dist > 0 && dist < a.r + b.r) { + // resolve overlap + const overlap = a.r + b.r - dist; + const nx = dx / dist, ny = dy / dist; + a.x -= nx * overlap / 2; + a.y -= ny * overlap / 2; + b.x += nx * overlap / 2; + b.y += ny * overlap / 2; + + // compute relative velocity in normal direction + const dvx = b.vx - a.vx, dvy = b.vy - a.vy; + const rel = dvx * nx + dvy * ny; + if (rel > 0) continue; // moving away + // simple elastic collision (equal mass) + const impulse = (2 * rel) / 2; + a.vx += impulse * nx; + a.vy += impulse * ny; + b.vx -= impulse * nx; + b.vy -= impulse * ny; + + // small damping to avoid perpetual motion + a.vx *= 0.999; + a.vy *= 0.999; + b.vx *= 0.999; + b.vy *= 0.999; + } + } + } + } + + /*** Game logic (potted handling & turns) ***/ + function onBallPotted(ball) { + // handle cue ball foul: respawn cue ball near original spot (simple) + if (ball.number === 0) { + // cue ball potted => foul, respawn after brief delay + setTimeout(() => { + ball.potted = false; + ball.x = W * 0.25; + ball.y = H / 2 + 20; + ball.vx = ball.vy = 0; + // award turn to other player (foul) + currentPlayer = currentPlayer === 1 ? 2 : 1; + showMessage('Cue ball potted โ€” foul. Turn to Player ' + currentPlayer); + updateUI(); + }, 700); + return; + } + + // update score & decide assignment + const num = ball.number; + const isStripe = ball.stripe; + // if neither player assigned types yet, first potted (non-8) decides + if (!playerData[1].type && !playerData[2].type && num !== 8) { + playerData[currentPlayer].type = isStripe ? 'stripes' : 'solids'; + const other = currentPlayer === 1 ? 2 : 1; + playerData[other].type = isStripe ? 'solids' : 'stripes'; + showMessage(`Player ${currentPlayer} is ${playerData[currentPlayer].type}`); + } + + // award point if ball matches current player's type + if (playerData[currentPlayer].type && ((playerData[currentPlayer].type === 'stripes') === isStripe)) { + playerData[currentPlayer].score += 1; + showMessage(`Player ${currentPlayer} pocketed their ball!`); + } else { + // pocketed other player's ball => no point but stays potted + showMessage(`Player ${currentPlayer} pocketed opponent's ball! Turn switches.`); + currentPlayer = currentPlayer === 1 ? 2 : 1; + } + + // if 8-ball potted: check victory + if (num === 8) { + // player must have cleared their assigned balls (simple check: other balls of their suit remaining?) + const remaining = balls.filter(b => !b.potted && b.number !== 0 && b.number !== 8 && ((b.stripe && playerData[currentPlayer].type === 'stripes') || (!b.stripe && playerData[currentPlayer].type === 'solids'))); + if (remaining.length === 0) { + showMessage(`Player ${currentPlayer} sank the 8-ball and wins!`); + endGame(currentPlayer); + } else { + showMessage(`8-ball sunk prematurely โ€” Player ${currentPlayer} loses!`); + endGame(currentPlayer === 1 ? 2 : 1); // other player wins + } + return; + } + + updateUI(); + } + + function endGame(winner) { + showMessage(`Player ${winner} wins! ๐ŸŽ‰`); + // stop game - clear velocities + balls.forEach(b => { b.vx = b.vy = 0; }); + cancelAnimationFrame(animationId); + } + + /*** Input: aiming with mouse / touch ***/ + function canvasPoint(evt) { + const rect = canvas.getBoundingClientRect(); + const clientX = evt.touches ? evt.touches[0].clientX : evt.clientX; + const clientY = evt.touches ? evt.touches[0].clientY : evt.clientY; + return { x: (clientX - rect.left) * (canvas.width / rect.width), y: (clientY - rect.top) * (canvas.height / rect.height) }; + } + + canvas.addEventListener('mousedown', (e) => { if (!isBallsMoving) startAiming(e); }); + canvas.addEventListener('touchstart', (e) => { if (!isBallsMoving) startAiming(e); }, { passive: true }); + + window.addEventListener('mousemove', (e) => { if (isAiming) aimCurrent = canvasPoint(e); }); + window.addEventListener('touchmove', (e) => { if (isAiming) aimCurrent = canvasPoint(e); }, { passive: true }); + + window.addEventListener('mouseup', (e) => { if (isAiming) releaseShot(e); }); + window.addEventListener('touchend', (e) => { if (isAiming) releaseShot(e); }); + + function startAiming(e) { + // only allow aiming if balls are still + if (isBallsMoving) return; + isAiming = true; + aimStart = canvasPoint(e); + aimCurrent = aimStart; + showMessage('Aiming... drag to set power and direction'); + updatePowerVisual(); + } + + function releaseShot(e) { + isAiming = false; + const p = aimCurrent || aimStart; + const dx = cueBall.x - p.x; + const dy = cueBall.y - p.y; + const dist = Math.hypot(dx, dy); + const power = Math.min(dist / 8, MAX_POWER); + if (power < 0.5) { + showMessage('Shot cancelled (too small)'); + powerFill.style.width = '0%'; + return; + } + // impart velocity to cue ball + cueBall.vx = (dx / dist) * power; + cueBall.vy = (dy / dist) * power; + + isBallsMoving = true; + showMessage(`Player ${currentPlayer} shot (power ${power.toFixed(1)})`); + powerFill.style.width = '0%'; + // track that after balls stop, turn may change based on potted logic; we'll switch if no ball of player's type potted in the shot + lastShotHadPocket = false; + lastShotCurrentPlayer = currentPlayer; + } + + function updatePowerVisual() { + if (!isAiming || !aimStart || !aimCurrent) { powerFill.style.width = '0%'; return; } + const dx = aimStart.x - aimCurrent.x; + const dy = aimStart.y - aimCurrent.y; + const dist = Math.hypot(dx, dy); + const fraction = Math.min(1, dist / (MAX_POWER * 8)); + powerFill.style.width = (fraction * 100) + '%'; + } + + // small state to manage turn switching after shots + let lastShotHadPocket = false; + let lastShotCurrentPlayer = null; + + /*** UI updates ***/ + function updateUI() { + turnText.textContent = `Player ${currentPlayer}`; + p1El.classList.toggle('active', currentPlayer === 1); + p2El.classList.toggle('active', currentPlayer === 2); + document.querySelector('#player1 .ptype').textContent = playerData[1].type ? playerData[1].type : 'โ€”'; + document.querySelector('#player2 .ptype').textContent = playerData[2].type ? playerData[2].type : 'โ€”'; + document.querySelector('#player1 .score').textContent = playerData[1].score; + document.querySelector('#player2 .score').textContent = playerData[2].score; + } + + function showMessage(txt) { + messageEl.textContent = txt; + } + + /*** Hint: show aim line briefly ***/ + document.getElementById('hintBtn').addEventListener('click', () => { + showMessage('Hint: Click & drag behind the cue ball to set direction and power.'); + }); + + document.getElementById('restartBtn').addEventListener('click', () => { + initBalls(); + if (!animationId) loop(); + showMessage('New rack โ€” Player 1 break'); + }); + + /*** Game loop ***/ + function render() { + drawTable(); + // draw guide when aiming + if (isAiming && aimCurrent) { + // draw cue ball highlight and aim line + ctx.beginPath(); + ctx.lineWidth = 2; + ctx.strokeStyle = 'rgba(255,255,255,0.6)'; + ctx.moveTo(cueBall.x, cueBall.y); + ctx.lineTo(aimCurrent.x, aimCurrent.y); + ctx.stroke(); + + // draw power indicator circle at aimCurrent + const dx = cueBall.x - aimCurrent.x; + const dy = cueBall.y - aimCurrent.y; + const dist = Math.hypot(dx, dy); + const p = Math.min(1, dist / (MAX_POWER * 8)); + ctx.beginPath(); + ctx.fillStyle = 'rgba(255,213,79,0.18)'; + ctx.arc(cueBall.x - dx / 2, cueBall.y - dy / 2, 10 + p * 18, 0, Math.PI * 2); + ctx.fill(); + updatePowerVisual(); + } + + drawBalls(); + } + + function loop() { + animationId = requestAnimationFrame(loop); + stepPhysics(); + render(); + + // after shot has finished, evaluate turn switching + if (!isBallsMoving && lastShotCurrentPlayer !== null) { + // determine whether any ball of the player's type was potted during last shot: + // (we track by checking current scores vs previous snapshot; simpler: rely on lastShotHadPocket toggled in onBallPotted) + // For simplicity here: if last shot had no pocket, switch turn + if (!lastShotHadPocket) { + currentPlayer = lastShotCurrentPlayer === 1 ? 2 : 1; + showMessage(`No pocket โ€” Turn to Player ${currentPlayer}`); + } else { + showMessage(`Player ${lastShotCurrentPlayer} continues (pocket made)`); + } + lastShotCurrentPlayer = null; + updateUI(); + } + // reset lastShotHadPocket for next shot + lastShotHadPocket = false; + } + + // To set lastShotHadPocket true when any ball potted during a shot, modify onBallPotted: + // But since onBallPotted is called immediately on potted detection while balls are moving, set flag: + const original_onBallPotted = onBallPotted; + onBallPotted = function(ball) { + lastShotHadPocket = true; + original_onBallPotted(ball); + updateUI(); + }; + + // Initialize & run + initBalls(); + loop(); + + // expose some debug helpers (optional) + window.__pool = { + balls, initBalls, cueBall + }; + +})(); diff --git a/games/8-ball-pool/style.css b/games/8-ball-pool/style.css new file mode 100644 index 00000000..f4c4dee4 --- /dev/null +++ b/games/8-ball-pool/style.css @@ -0,0 +1,133 @@ +:root{ + --bg:#0b6b3a; + --felt:#125e2e; + --rail:#6b3b2a; + --text:#fff; + --accent:#ffd54f; + --panel: rgba(0,0,0,0.4); +} +*{box-sizing:border-box} +html,body {height:100%} +body{ + margin:0; + font-family:Inter,Segoe UI,Roboto,Arial; + background:linear-gradient(180deg,#062c1a 0%, #08361f 100%); + color:var(--text); + display:flex; + align-items:center; + justify-content:center; + padding:16px; +} +.page{ + width:100%; + max-width:1200px; + background:transparent; +} +.topbar{ + display:flex; + gap:12px; + align-items:center; + justify-content:space-between; + margin-bottom:12px; +} +.topbar h1{margin:0;font-size:20px} +.topbar .back{ + color:var(--text); + text-decoration:none; + opacity:0.9; + padding:6px 8px; + border-radius:6px; + background: rgba(255,255,255,0.03); +} +.topbar .controls button{ + background:var(--panel); + color:var(--text); + border:1px solid rgba(255,255,255,0.06); + padding:8px 10px; + border-radius:6px; + cursor:pointer; + margin-left:6px; +} + +.main{display:flex; gap:12px} +.game-area{position:relative; display:flex; gap:12px} + +canvas{ + border-radius:12px; + background: linear-gradient(180deg, #0d5f2f 0%, #0b5b2b 100%); + box-shadow: 0 10px 30px rgba(0,0,0,0.6), inset 0 3px 8px rgba(255,255,255,0.02); + margin-right:12px; +} + +/* HUD (right panel) */ +.hud{ + width:320px; + display:flex; + flex-direction:column; + gap:12px; + align-items:stretch; + justify-content:flex-start; +} +.players{display:flex; flex-direction:column; gap:8px;} +.player{ + display:flex; + gap:12px; + align-items:center; + justify-content:space-between; + padding:10px 12px; + background: rgba(0,0,0,0.25); + border-radius:8px; + border:1px solid rgba(255,255,255,0.03); +} +.player.active{box-shadow: 0 6px 18px rgba(0,0,0,0.45); transform:translateY(-2px); border:1px solid rgba(255,213,79,0.12)} +.pcolor{font-weight:600} +.ptype{opacity:0.9} +.score{font-weight:700} + +.turn-info{ + padding:12px; + background: rgba(255,255,255,0.02); + border-radius:8px; + border:1px solid rgba(255,255,255,0.03); +} +.power-meter{ + width:100%; + height:14px; + background:rgba(255,255,255,0.06); + border-radius:8px; + margin:8px 0; + overflow:hidden; +} +#powerFill{ + width:0%; + height:100%; + background:linear-gradient(90deg,var(--accent), #ffb74d); + transition: width 0.08s linear; +} + +.message{ + margin-top:6px; + font-size:13px; + opacity:0.95; +} + +/* legend */ +.legend{margin-top:8px; opacity:0.9} +.legend-dot{ + display:inline-block; + width:12px; + height:12px; + border-radius:50%; + margin-right:6px; + vertical-align:middle; + border:1px solid rgba(0,0,0,0.2); +} +.legend-dot.solids{background:linear-gradient(#d32f2f,#c62828)} +.legend-dot.stripes{background:linear-gradient(#1976d2,#1565c0)} + +/* footer */ +.footer{text-align:center;margin-top:14px;opacity:0.85;font-size:13px} +@media(max-width:1100px){ + .hud{display:none} + canvas{width:100%; height:auto} +} diff --git a/games/Aim Arena/index.html b/games/Aim Arena/index.html new file mode 100644 index 00000000..7fd74f3b --- /dev/null +++ b/games/Aim Arena/index.html @@ -0,0 +1,16 @@ + + + + + + Aim Arena + + + +

๐ŸŽฏ Aim Arena

+ +

Score: 0

+ + + + diff --git a/games/Aim Arena/script.js b/games/Aim Arena/script.js new file mode 100644 index 00000000..4d197ff9 --- /dev/null +++ b/games/Aim Arena/script.js @@ -0,0 +1,73 @@ +const canvas = document.getElementById("arena"); +const ctx = canvas.getContext("2d"); +const scoreEl = document.getElementById("score"); +const restartBtn = document.getElementById("restart"); + +let striker, coins, score, dragging; + +function resetGame() { + striker = { x: 200, y: 350, r: 15, dx: 0, dy: 0, color: "#38bdf8" }; + coins = [ + { x: 200, y: 120, r: 10, color: "#facc15" }, + { x: 180, y: 140, r: 10, color: "#f97316" }, + { x: 220, y: 140, r: 10, color: "#ef4444" } + ]; + score = 0; + dragging = false; + scoreEl.textContent = "Score: 0"; +} + +function drawCircle(x, y, r, color) { + ctx.beginPath(); + ctx.arc(x, y, r, 0, Math.PI * 2); + ctx.fillStyle = color; + ctx.fill(); +} + +function drawBoard() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + coins.forEach(c => drawCircle(c.x, c.y, c.r, c.color)); + drawCircle(striker.x, striker.y, striker.r, striker.color); +} + +function update() { + striker.x += striker.dx; + striker.y += striker.dy; + + striker.dx *= 0.98; + striker.dy *= 0.98; + + // Bounce + if (striker.x < striker.r || striker.x > 400 - striker.r) striker.dx *= -1; + if (striker.y < striker.r || striker.y > 400 - striker.r) striker.dy *= -1; + + // Collision check + coins = coins.filter(c => { + const dist = Math.hypot(striker.x - c.x, striker.y - c.y); + if (dist < striker.r + c.r) { + score += 10; + scoreEl.textContent = Score: ${score}; + return false; + } + return true; + }); + + drawBoard(); + requestAnimationFrame(update); +} + +canvas.addEventListener("mousedown", () => (dragging = true)); +canvas.addEventListener("mouseup", e => { + if (!dragging) return; + const rect = canvas.getBoundingClientRect(); + const dx = (striker.x - (e.clientX - rect.left)) / 10; + const dy = (striker.y - (e.clientY - rect.top)) / 10; + striker.dx = dx; + striker.dy = dy; + dragging = false; +}); + +restartBtn.addEventListener("click", resetGame); + +resetGame(); +update(); diff --git a/games/Aim Arena/style.css b/games/Aim Arena/style.css new file mode 100644 index 00000000..495bc596 --- /dev/null +++ b/games/Aim Arena/style.css @@ -0,0 +1,43 @@ +body { + background: radial-gradient(circle, #0f172a, #020617); + color: #fff; + text-align: center; + font-family: 'Poppins', sans-serif; +} + +h1 { + margin-top: 20px; + color: #38bdf8; + text-shadow: 0 0 15px #0ea5e9; +} + +canvas { + display: block; + margin: 30px auto; + background: #f5deb3; + border: 8px solid #a16207; + border-radius: 15px; + box-shadow: 0 0 25px #000; + cursor: crosshair; +} + +#score { + font-size: 1.2rem; + margin: 10px; +} + +button { + background: #38bdf8; + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + transition: all 0.3s ease; +} + +button:hover { + background: #0ea5e9; + transform:ย scale(1.1); +} diff --git a/games/AlphaBattle/index.html b/games/AlphaBattle/index.html new file mode 100644 index 00000000..fb0d5d54 --- /dev/null +++ b/games/AlphaBattle/index.html @@ -0,0 +1,33 @@ + + + + + + Alpha Battle โš” + + + +

โš” Alpha Battle

+

Test your speed and spelling skills before the timer runs out!

+ +
+
Press Start!
+ + + +
+

โฑ Time: 30s

+

๐Ÿ’ฏ Score: 0

+
+ +
+ + +
+
+ +

+ + + + diff --git a/games/AlphaBattle/script.js b/games/AlphaBattle/script.js new file mode 100644 index 00000000..eb5e4ce3 --- /dev/null +++ b/games/AlphaBattle/script.js @@ -0,0 +1,91 @@ +const words = [ + "adventure", "galaxy", "knowledge", "victory", "explore", + "pioneer", "courage", "creative", "dynamic", "fantasy", + "harmony", "jungle", "legend", "mystery", "nebula", + "quest", "rhythm", "universe", "wonder", "zephyr" +]; + +const wordDisplay = document.getElementById("wordDisplay"); +const userInput = document.getElementById("userInput"); +const startBtn = document.getElementById("startBtn"); +const restartBtn = document.getElementById("restartBtn"); +const scoreDisplay = document.getElementById("score"); +const timeDisplay = document.getElementById("time"); +const message = document.getElementById("message"); + +let currentWord = ""; +let score = 0; +let time = 30; +let timer; +let gameActive = false; + +function randomWord() { + return words[Math.floor(Math.random() * words.length)]; +} + +function displayWord() { + currentWord = randomWord(); + wordDisplay.textContent = currentWord; + wordDisplay.classList.add("animate"); + setTimeout(() => wordDisplay.classList.remove("animate"), 200); +} + +function startGame() { + if (gameActive) return; + gameActive = true; + score = 0; + time = 30; + userInput.disabled = false; + userInput.focus(); + message.textContent = ""; + scoreDisplay.textContent = score; + timeDisplay.textContent = time; + displayWord(); + + timer = setInterval(() => { + time--; + timeDisplay.textContent = time; + if (time <= 0) endGame(); + }, 1000); +} + +function checkInput() { + if (userInput.value.trim().toLowerCase() === currentWord.toLowerCase()) { + score++; + scoreDisplay.textContent = score; + playSound("https://cdn.pixabay.com/download/audio/2022/03/15/audio_50f34169e4.mp3?filename=click-124467.mp3"); + userInput.value = ""; + displayWord(); + } +} + +function endGame() { + clearInterval(timer); + userInput.disabled = true; + gameActive = false; + message.textContent = ๐Ÿ Time's up! Final Score: ${score}; + playSound("https://cdn.pixabay.com/download/audio/2022/03/15/audio_327e22f9d4.mp3?filename=game-over-arcade-6435.mp3"); +} + +function restartGame() { + clearInterval(timer); + score = 0; + time = 30; + scoreDisplay.textContent = score; + timeDisplay.textContent = time; + message.textContent = ""; + userInput.value = ""; + userInput.disabled = true; + wordDisplay.textContent = "Press Start!"; + gameActive = false; +} + +function playSound(url) { + const audio = new Audio(url); + audio.volume = 0.5; + audio.play(); +} + +userInput.addEventListener("input", checkInput); +startBtn.addEventListener("click", startGame); +restartBtn.addEventListener("click",ย restartGame); diff --git a/games/AlphaBattle/style.css b/games/AlphaBattle/style.css new file mode 100644 index 00000000..218c801c --- /dev/null +++ b/games/AlphaBattle/style.css @@ -0,0 +1,99 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: "Poppins", sans-serif; + background: radial-gradient(circle at top, #93c5fd, #60a5fa, #3b82f6, #1e3a8a); + color: white; + text-align: center; + min-height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; +} + +h1 { + font-size: 2.5rem; + text-shadow: 0 0 12px #fff; + margin-bottom: 10px; +} + +.tagline { + font-size: 1.1rem; + margin-bottom: 20px; +} + +.game-container { + background: rgba(255, 255, 255, 0.1); + border-radius: 20px; + padding: 30px; + width: 90%; + max-width: 500px; + margin: auto; + box-shadow: 0 0 20px rgba(255, 255, 255, 0.3); +} + +.word-box { + font-size: 2rem; + font-weight: bold; + margin: 20px 0; + background: rgba(255, 255, 255, 0.2); + border-radius: 10px; + padding: 15px; + text-shadow: 0 0 10px #fff; + transition: transform 0.3s ease; +} + +.word-box.animate { + transform: scale(1.1); +} + +#userInput { + width: 80%; + padding: 10px; + border-radius: 8px; + border: none; + font-size: 1.1rem; + outline: none; + margin-bottom: 20px; + text-align: center; +} + +.info { + display: flex; + justify-content: space-around; + font-size: 1.1rem; + margin-bottom: 20px; +} + +button { + background: linear-gradient(90deg, #f472b6, #a78bfa); + border: none; + color: white; + padding: 10px 20px; + border-radius: 10px; + font-size: 1rem; + cursor: pointer; + transition: transform 0.2s ease; + font-weight: 600; +} + +button:hover { + transform: scale(1.1); +} + +#message { + margin-top: 25px; + font-size: 1.3rem; + text-shadow: 0 0 8px #fef9c3; + font-weight: bold; + animation: fadeIn 0.5s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to {ย opacity:ย 1;ย } +} diff --git a/games/Bubble Sonata/index.html b/games/Bubble Sonata/index.html new file mode 100644 index 00000000..58936235 --- /dev/null +++ b/games/Bubble Sonata/index.html @@ -0,0 +1,31 @@ + + + + + + Bubble Sonata + + + +
+

๐Ÿซง Bubble Sonata

+

Tap the bubbles in rhythm!

+ +
+ +
+ Score: 0 + Combo: 0 +
+ +
+ + +
+
+ + + + + + diff --git a/games/Bubble Sonata/script.js b/games/Bubble Sonata/script.js new file mode 100644 index 00000000..beedf96d --- /dev/null +++ b/games/Bubble Sonata/script.js @@ -0,0 +1,83 @@ +const gameArea = document.getElementById("gameArea"); +const scoreEl = document.getElementById("score"); +const comboEl = document.getElementById("combo"); +const startBtn = document.getElementById("startBtn"); +const restartBtn = document.getElementById("restartBtn"); +const statusEl = document.getElementById("status"); +const popSound = document.getElementById("popSound"); + +let score = 0; +let combo = 0; +let gameActive = false; +let bubbleInterval; +let missed = 0; + +function startGame() { + gameActive = true; + score = 0; + combo = 0; + missed = 0; + scoreEl.textContent = score; + comboEl.textContent = combo; + startBtn.disabled = true; + restartBtn.disabled = false; + statusEl.textContent = "Pop the bubbles in rhythm! ๐ŸŽถ"; + + bubbleInterval = setInterval(createBubble, 900); +} + +function restartGame() { + clearInterval(bubbleInterval); + gameArea.innerHTML = ""; + startGame(); +} + +function createBubble() { + const bubble = document.createElement("div"); + bubble.classList.add("bubble"); + bubble.style.left = Math.random() * (gameArea.offsetWidth - 40) + "px"; + + bubble.addEventListener("click", () => popBubble(bubble)); + gameArea.appendChild(bubble); + + setTimeout(() => { + if (gameArea.contains(bubble)) { + bubble.remove(); + missed++; + combo = 0; + comboEl.textContent = combo; + if (missed > 5) gameOver(); + } + }, 4800); +} + +function popBubble(bubble) { + if (!gameActive) return; + popSound.currentTime = 0; + popSound.play(); + + score += 10; + combo++; + scoreEl.textContent = score; + comboEl.textContent = combo; + + bubble.style.transition = "transform 0.2s ease, opacity 0.2s ease"; + bubble.style.transform = "scale(1.5)"; + bubble.style.opacity = "0"; + setTimeout(() => bubble.remove(), 200); + + // Color flash + gameArea.style.boxShadow = `0 0 30px rgba(96,165,250,0.6)`; + setTimeout(() => (gameArea.style.boxShadow = `0 0 30px rgba(59,130,246,0.4)`), 200); +} + +function gameOver() { + clearInterval(bubbleInterval); + gameActive = false; + statusEl.textContent = `๐ŸŽต Game Over! Final Score: ${score}`; + startBtn.disabled = false; + restartBtn.disabled = true; +} + +startBtn.addEventListener("click", startGame); +restartBtn.addEventListener("click", restartGame); diff --git a/games/Bubble Sonata/style.css b/games/Bubble Sonata/style.css new file mode 100644 index 00000000..5c354ffd --- /dev/null +++ b/games/Bubble Sonata/style.css @@ -0,0 +1,87 @@ +:root { + --bg: radial-gradient(circle at center, #0ea5e9, #1e3a8a); + --bubble: #60a5fa; + --glow: #3b82f6; + --text: #dbeafe; + font-family: "Poppins", sans-serif; +} + +body { + background: var(--bg); + height: 100vh; + margin: 0; + display: flex; + justify-content: center; + align-items: center; +} + +.container { + text-align: center; + width: 360px; +} + +h1 { + color: var(--text); + text-shadow: 0 0 15px var(--glow); +} + +#status { + color: #bae6fd; + margin-bottom: 10px; +} + +#gameArea { + position: relative; + width: 320px; + height: 400px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.1); + overflow: hidden; + margin: 0 auto 15px; + box-shadow: 0 0 30px rgba(59, 130, 246, 0.4); +} + +.bubble { + position: absolute; + width: 40px; + height: 40px; + border-radius: 50%; + background: radial-gradient(circle at 30% 30%, #93c5fd, #3b82f6); + box-shadow: 0 0 20px var(--glow); + opacity: 0.8; + animation: floatUp 5s linear forwards; + cursor: pointer; +} + +@keyframes floatUp { + from { + bottom: -40px; + } + to { + bottom: 420px; + } +} + +.scoreboard { + display: flex; + justify-content: space-around; + color: var(--text); + margin-bottom: 10px; + font-weight: 600; +} + +.controls button { + background: linear-gradient(90deg, #60a5fa, #3b82f6); + border: none; + color: white; + padding: 10px 15px; + font-size: 1rem; + border-radius: 8px; + margin: 5px; + cursor: pointer; + font-weight: 600; +} + +.controls button:disabled { + opacity: 0.5; +} diff --git a/games/Catch_The_Dot/index.html b/games/Catch_The_Dot/index.html new file mode 100644 index 00000000..8374ff9f --- /dev/null +++ b/games/Catch_The_Dot/index.html @@ -0,0 +1,20 @@ + + + + + + Catch the Dot + + + +
+

Catch the Dot ๐ŸŽฏ

+

Score: 0

+

Time Left: 30s

+
+

+ +
+ + + diff --git a/games/Catch_The_Dot/script.js b/games/Catch_The_Dot/script.js new file mode 100644 index 00000000..5a2e673c --- /dev/null +++ b/games/Catch_The_Dot/script.js @@ -0,0 +1,74 @@ +const gameArea = document.getElementById('game-area'); +const scoreEl = document.getElementById('score'); +const timeEl = document.getElementById('time'); +const messageEl = document.getElementById('message'); +const startBtn = document.getElementById('start-btn'); + +let score = 0; +let timeLeft = 30; +let countdownInterval; +let dotInterval; +let gameActive = false; + +function startGame() { + score = 0; + timeLeft = 30; + scoreEl.innerText = score; + timeEl.innerText = timeLeft; + messageEl.innerText = ''; + startBtn.disabled = true; + gameActive = true; + + spawnDot(); + countdownInterval = setInterval(updateTimer, 1000); +} + +function updateTimer() { + timeLeft--; + timeEl.innerText = timeLeft; + if (timeLeft <= 0) { + endGame(); + } +} + +function endGame() { + gameActive = false; + clearInterval(countdownInterval); + clearInterval(dotInterval); + removeDot(); + messageEl.innerText = `โฐ Time's up! Your final score: ${score}`; + startBtn.disabled = false; +} + +function spawnDot() { + removeDot(); // Ensure only one dot exists + + const dot = document.createElement('div'); + dot.classList.add('dot'); + gameArea.appendChild(dot); + + // Random position within game area + const maxX = gameArea.clientWidth - 50; + const maxY = gameArea.clientHeight - 50; + dot.style.left = Math.random() * maxX + 'px'; + dot.style.top = Math.random() * maxY + 'px'; + + dot.addEventListener('click', () => { + if (!gameActive) return; + score++; + scoreEl.innerText = score; + spawnDot(); + }); + + // Move dot every 1 second if not clicked + dotInterval = setTimeout(() => { + if(gameActive) spawnDot(); + }, 1000); +} + +function removeDot() { + const existingDot = document.querySelector('.dot'); + if (existingDot) existingDot.remove(); +} + +startBtn.addEventListener('click', startGame); diff --git a/games/Catch_The_Dot/style.css b/games/Catch_The_Dot/style.css new file mode 100644 index 00000000..84235d46 --- /dev/null +++ b/games/Catch_The_Dot/style.css @@ -0,0 +1,69 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: #1e1e1e; + color: #e0e0e0; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + text-align: center; + width: 90%; + max-width: 450px; + background: #110000; + padding: 20px; + border-radius: 12px; +} + +h1 { + font-size: 2em; + margin-bottom: 10px; + color: #ffffff; +} + +h2, h3 { + margin: 5px 0; + color: #c0c0c0; +} + +#game-area { + width: 400px; + height: 400px; + margin: 20px auto; + position: relative; + background: #333333; + border-radius: 10px; + overflow: hidden; +} + +.dot { + width: 40px; + height: 40px; + border-radius: 50%; + position: absolute; + background: #b51111; + cursor: pointer; + transition: transform 0.1s; +} + +.dot:hover { + transform: scale(1.1); +} + +button { + padding: 10px 20px; + background: #555555; + border: none; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + color: #e0e0e0; + margin-top: 15px; +} + +button:hover { + background: #777777; +} diff --git a/games/Chromatic Clearance/index.html b/games/Chromatic Clearance/index.html new file mode 100644 index 00000000..874c70d5 --- /dev/null +++ b/games/Chromatic Clearance/index.html @@ -0,0 +1,55 @@ + + + + + + Chromatic Clearance + + + + +
+

Chromatic Clearance: The Inverse Filter Puzzle

+

Adjust the controls until the image is perfectly clear. Find the inverse filters!

+
+ +
+
+ Hidden Puzzle Image +
+
+ +
+

Clearance Filters

+ +
+ + + 0px +
+ +
+ + + 0% +
+ +
+ + + 0deg +
+ +
+ + + 0% +
+ + +

+
+ + + + \ No newline at end of file diff --git a/games/Chromatic Clearance/script.js b/games/Chromatic Clearance/script.js new file mode 100644 index 00000000..6ba7c113 --- /dev/null +++ b/games/Chromatic Clearance/script.js @@ -0,0 +1,119 @@ +document.addEventListener('DOMContentLoaded', () => { + const targetContainer = document.getElementById('target-container'); + const messageDisplay = document.getElementById('game-message'); + const checkButton = document.getElementById('check-button'); + + // Control elements and their state management + const controls = { + blur: { element: document.getElementById('blur-slider'), display: document.getElementById('blur-val'), unit: 'px', value: 0 }, + invert: { element: document.getElementById('invert-slider'), display: document.getElementById('invert-val'), unit: '%', value: 0 }, + hue: { element: document.getElementById('hue-rotate-slider'), display: document.getElementById('hue-rotate-val'), unit: 'deg', value: 0 }, + grayscale: { element: document.getElementById('grayscale-slider'), display: document.getElementById('grayscale-val'), unit: '%', value: 0 } + }; + + // --- 1. Initial Scramble Definition --- + // These are the hidden values the player MUST cancel out. + const SCRAMBLE_FILTERS = { + blur: 5, // Inverse is 0 + invert: 100, // Inverse is 100 (since 100% + 100% = 0%) + hue: 200, // Inverse is -200deg (or 160deg) + grayscale: 80 // Inverse is 0, but to cancel, player must match the initial level + }; + + /** + * Applies the initial, complex filter chain to obscure the image. + */ + function applyInitialScramble() { + const scrambleString = ` + blur(${SCRAMBLE_FILTERS.blur}px) + invert(${SCRAMBLE_FILTERS.invert}%) + hue-rotate(${SCRAMBLE_FILTERS.hue}deg) + grayscale(${SCRAMBLE_FILTERS.grayscale}%) + `; + targetContainer.style.filter = scrambleString; + messageDisplay.textContent = "The image is scrambled. Begin clearance!"; + } + + // --- 2. Player Interaction Logic --- + + /** + * Dynamically builds the combined filter string from all player controls. + * The player's filters are applied *after* the initial scramble in the CSS engine, + * so the full effect is the result of compounding operations. + */ + function updateCombinedFilter() { + // Start with the initial SCRAMBLE filter values + let filterString = ` + blur(${SCRAMBLE_FILTERS.blur}px) + invert(${SCRAMBLE_FILTERS.invert}%) + hue-rotate(${SCRAMBLE_FILTERS.hue}deg) + grayscale(${SCRAMBLE_FILTERS.grayscale}%) + `; + + // Append the player's CLEARANCE filters. Order matters! + filterString += ` + blur(${controls.blur.value}${controls.blur.unit}) + invert(${controls.invert.value}${controls.invert.unit}) + hue-rotate(${controls.hue.value}${controls.hue.unit}) + grayscale(${controls.grayscale.value}${controls.grayscale.unit}) + `; + + targetContainer.style.filter = filterString.trim(); + } + + // Attach listeners to all sliders + for (const key in controls) { + const control = controls[key]; + + // Initialize display value + control.display.textContent = control.value + control.unit; + + control.element.addEventListener('input', (e) => { + const value = parseFloat(e.target.value); + control.value = value; + control.display.textContent = value + control.unit; + + // Re-render the combined filter immediately on input + updateCombinedFilter(); + }); + } + + // --- 3. Win Condition Check (The Core Math) --- + + function checkWinCondition() { + // The check must confirm that the player's filters *perfectly* cancel the scramble. + + // 1. Blur: Inverse of blur(N) is blur(0). Player's blur must be 0. + const isBlurCleared = controls.blur.value === 0; + + // 2. Invert: Inverse of invert(100) is invert(100). (100+100 = 200. CSS clips this to 0% effect). + // Since we are applying *two* invert filters, the player must set their slider to 100%. + const isInvertCleared = controls.invert.value === SCRAMBLE_FILTERS.invert; + + // 3. Hue Rotate: Inverse of hue-rotate(N) is hue-rotate(360 - N). + // The scramble hue is 200. The inverse is 160 (or -200, but we use 0-360 scale). + const inverseHue = 360 - SCRAMBLE_FILTERS.hue; + const isHueCleared = controls.hue.value === inverseHue; + + // 4. Grayscale: Inverse of grayscale(N) is grayscale(0). + // The initial scramble applies grayscale(80). To cancel it, the player must apply + // the remaining 20% to reach 100%, and then 0% to remove it. This is tricky. + // A simpler puzzle: require the player to set grayscale to 0 to make the total effect 0. + const isGrayscaleCleared = controls.grayscale.value === 0; + + if (isBlurCleared && isInvertCleared && isHueCleared && isGrayscaleCleared) { + messageDisplay.textContent = "๐Ÿ† CONGRATULATIONS! Filter chain successfully cleared! ๐Ÿ†"; + messageDisplay.style.color = 'lime'; + checkButton.disabled = true; + } else { + messageDisplay.textContent = "Filters are not perfectly canceled. Keep adjusting!"; + messageDisplay.style.color = 'yellow'; + } + } + + checkButton.addEventListener('click', checkWinCondition); + + // Initial setup + applyInitialScramble(); + updateCombinedFilter(); // Apply initial player state (all 0s) on top of scramble +}); \ No newline at end of file diff --git a/games/Chromatic Clearance/style.css b/games/Chromatic Clearance/style.css new file mode 100644 index 00000000..4457517d --- /dev/null +++ b/games/Chromatic Clearance/style.css @@ -0,0 +1,75 @@ +body { + font-family: Arial, sans-serif; + display: flex; + flex-direction: column; + align-items: center; + background-color: #2c3e50; + color: white; + padding: 20px; +} + +#game-stage { + width: 400px; + height: 300px; + border: 5px solid #3498db; + overflow: hidden; + position: relative; + margin: 20px 0; + display: flex; + justify-content: center; + align-items: center; +} + +#target-container { + /* This element is the filter application point */ + width: 100%; + height: 100%; + transition: filter 0.1s linear; /* Smooth visual feedback */ + display: flex; + justify-content: center; + align-items: center; +} + +#hidden-image { + max-width: 100%; + max-height: 100%; + display: block; +} + +/* --- Controls Styling --- */ +#controls { + width: 400px; + padding: 20px; + background-color: #34495e; + border-radius: 5px; +} + +.control-group { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.control-group label { + flex: 2; + margin-right: 10px; +} + +.control-group input[type="range"] { + flex: 4; +} + +.control-group span { + flex: 1; + text-align: right; +} + +#check-button { + padding: 10px 20px; + background-color: #2ecc71; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + margin-top: 15px; +} \ No newline at end of file diff --git a/games/ChronoQuest/index.html b/games/ChronoQuest/index.html new file mode 100644 index 00000000..0ecb4751 --- /dev/null +++ b/games/ChronoQuest/index.html @@ -0,0 +1,58 @@ + + + + + + ChronoQuest ๐Ÿ•ฐ + + + +
+

ChronoQuest

+

Arrange historical events in the correct chronological order

+
+ +
+
+ + + + + + + + +
+ Attempts: 0 + Score: 0 +
+
+ +
+

Drag the event cards to arrange them from earliest โ†’ latest.

+ +
    + +
+
+ +
+
+ + + + + + diff --git a/games/ChronoQuest/script.js b/games/ChronoQuest/script.js new file mode 100644 index 00000000..2f04a31e --- /dev/null +++ b/games/ChronoQuest/script.js @@ -0,0 +1,186 @@ +// ChronoQuest core logic +const EVENTS = [ + { year: 1776, title: "American Declaration of Independence", desc: "Thirteen colonies declare independence." }, + { year: 1789, title: "French Revolution begins", desc: "Storming of the Bastille and revolution." }, + { year: 1865, title: "End of American Civil War", desc: "Surrender at Appomattox and abolition progress." }, + { year: 1914, title: "Start of World War I", desc: "Archduke Franz Ferdinand assassination." }, + { year: 1939, title: "Start of World War II", desc: "Invasion of Poland." }, + { year: 1969, title: "Apollo 11 Moon Landing", desc: "First humans walk on the moon." }, + { year: 1989, title: "Fall of the Berlin Wall", desc: "Symbolic end of Cold War divisions." }, + { year: 2001, title: "September 11 Attacks", desc: "Terrorist attacks in the United States." }, + { year: 2008, title: "Global Financial Crisis", desc: "Worldwide economic downturn." }, + { year: 1991, title: "Dissolution of the Soviet Union", desc: "Soviet republics declare independence." } +]; + +// DOM +const timelineEl = document.getElementById('timeline'); +const startBtn = document.getElementById('startBtn'); +const checkBtn = document.getElementById('checkBtn'); +const hintBtn = document.getElementById('hintBtn'); +const restartBtn = document.getElementById('restartBtn'); +const levelSelect = document.getElementById('level'); +const attemptsEl = document.getElementById('attempts'); +const scoreEl = document.getElementById('score'); +const messageEl = document.getElementById('message'); +const template = document.getElementById('card-template'); + +let currentSet = []; +let attempts = 0; +let score = 0; +let draggedIdx = null; + +// utilities +function pickEventsForLevel(level){ + // level 1:4, level2:6, level3:8 (random sample) + const sizes = {1:4,2:6,3:8}; + const n = sizes[level] || 4; + const clone = [...EVENTS]; + // shuffle and pick + for(let i=clone.length-1;i>0;i--){ + const j = Math.floor(Math.random()*(i+1)); + [clone[i],clone[j]]=[clone[j],clone[i]]; + } + return clone.slice(0,n); +} + +function renderTimeline(events){ + timelineEl.innerHTML=''; + events.forEach((ev, idx)=>{ + const node = template.content.cloneNode(true); + const li = node.querySelector('li'); + li.dataset.idx = idx; + li.setAttribute('draggable','true'); + node.querySelector('.evt-title').textContent = ev.title; + node.querySelector('.evt-desc').textContent = ev.desc; + node.querySelector('.evt-year').textContent = "Year: " + ev.year; + // drag handlers + li.addEventListener('dragstart', onDragStart); + li.addEventListener('dragover', onDragOver); + li.addEventListener('drop', onDrop); + li.addEventListener('dragend', onDragEnd); + timelineEl.appendChild(node); + }); +} + +function onDragStart(e){ + draggedIdx = Number(this.dataset.idx); + e.dataTransfer.effectAllowed = 'move'; + this.classList.add('dragging'); +} + +function onDragOver(e){ + e.preventDefault(); + this.classList.add('drag-over'); +} + +function onDrop(e){ + e.preventDefault(); + const targetIdx = Number(this.dataset.idx); + if(draggedIdx === null || draggedIdx === targetIdx) return; + // swap in currentSet + [currentSet[draggedIdx], currentSet[targetIdx]] = [currentSet[targetIdx], currentSet[draggedIdx]]; + // re-render + renderTimeline(currentSet); + resetDragClasses(); +} + +function onDragEnd(){ + draggedIdx = null; + resetDragClasses(); +} + +function resetDragClasses(){ + timelineEl.querySelectorAll('.card').forEach(c => c.classList.remove('drag-over','dragging')); +} + +// Game controls +function startGame(){ + const level = Number(levelSelect.value); + currentSet = pickEventsForLevel(level); + // shuffle to present to the player + currentSet = currentSet.sort(()=>Math.random()-0.5); + renderTimeline(currentSet); + attempts = 0; + updateStatus(); + checkBtn.disabled = false; + hintBtn.disabled = false; + messageEl.textContent = "Arrange the cards then click Check Order."; + messageEl.className = "message"; +} + +function checkOrder(){ + attempts++; + const correctOrder = [...currentSet].slice().sort((a,b)=>a.year-b.year); + let correctCount = 0; + for(let i=0;ia.year-b.year); + const row = document.createElement('div'); + row.className = 'hint-row'; + sorted.forEach(ev=>{ + const chip = document.createElement('div'); + chip.className = 'hint-chip'; + chip.textContent = ${ev.year} โ€” ${ev.title}; + row.appendChild(chip); + }); + board.appendChild(row); + hintOverlay.appendChild(board); + document.body.appendChild(hintOverlay); + // remove after 3s + setTimeout(()=> hintOverlay.remove(), 3000); +} + +// Restart resets the board / score? We'll reset board and keep score unless start pressed +function restart(){ + currentSet = []; + timelineEl.innerHTML = ''; + messageEl.textContent = "Game reset. Choose level and Start."; + messageEl.className = "message"; + attempts = 0; + score = 0; + updateStatus(); + checkBtn.disabled = true; + hintBtn.disabled = true; +} + +// wire UI +startBtn.addEventListener('click', startGame); +checkBtn.addEventListener('click', checkOrder); +hintBtn.addEventListener('click', showHint); +restartBtn.addEventListener('click', restart); + +// accessibility: keyboard reorder (optional simple) +timelineEl.addEventListener('keydown', (e)=>{ + // noop - could be extended to support keyboard reorderingย later +}); diff --git a/games/ChronoQuest/style.css b/games/ChronoQuest/style.css new file mode 100644 index 00000000..a53656e4 --- /dev/null +++ b/games/ChronoQuest/style.css @@ -0,0 +1,54 @@ +:root{ + --bg1:#0f172a; --bg2:#071024; + --card:#0b1220; --accent:#60a5fa; --good:#34d399; --bad:#fb7185; + --glass: rgba(255,255,255,0.04); + font-family: Inter, Poppins, system-ui, -apple-system, "Segoe UI", Roboto, Arial; +} +*{box-sizing:border-box} +html,body{height:100%;margin:0;background:linear-gradient(180deg,var(--bg1),var(--bg2));color:#e6eef8} +.topbar{padding:18px 22px;text-align:center} +.topbar h1{margin:0;font-size:28px;letter-spacing:0.6px} +.topbar .subtitle{margin:6px 0 0;color:#9fb4d9;font-size:14px} + +.wrap{max-width:1000px;margin:18px auto;padding:18px} +.controls{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between;padding:12px;background:var(--glass);border-radius:12px;border:1px solid rgba(255,255,255,0.04)} +.controls > *{margin:4px} + +.controls select, .controls button{padding:8px 12px;border-radius:8px;border:none;cursor:pointer;font-weight:600} +.controls button{background:linear-gradient(90deg,#7dd3fc,#a78bfa);color:#062033} +.controls button:disabled{opacity:0.5;cursor:not-allowed} + +.status{display:flex;gap:18px;color:#cfe6ff;font-weight:600} + +.game{margin-top:18px} +.instruction{margin:8px 12px;color:#b6cbe8} + +.timeline{list-style:none;padding:12px;display:flex;flex-direction:column;gap:12px;min-height:260px} +.card{ + display:flex;align-items:center;justify-content:space-between; + background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border-radius:12px;padding:12px 16px;cursor:grab;border:1px solid rgba(255,255,255,0.04); + transition:transform .18s ease, box-shadow .18s ease; +} +.card:active{cursor:grabbing} +.card.drag-over{transform:translateY(-6px);box-shadow:0 10px 30px rgba(2,6,23,0.7)} +.card-content{max-width:76%} +.evt-title{margin:0;font-size:16px} +.evt-desc{margin:6px 0 0;font-size:13px;color:#a9c1e6} +.evt-year{font-weight:800;color:#9fb4d9;font-size:14px;opacity:0.9} + +.message{margin-top:12px;padding:12px;border-radius:10px;min-height:36px} +.message.good{background:linear-gradient(90deg, rgba(52,211,153,0.12), rgba(34,197,94,0.06));border:1px solid rgba(52,211,153,0.12);color:var(--good)} +.message.bad{background:linear-gradient(90deg, rgba(251,113,133,0.07), rgba(251,113,133,0.02));border:1px solid rgba(251,113,133,0.08);color:var(--bad)} + +.hint-overlay{ + position:fixed;inset:0;background:linear-gradient(180deg, rgba(2,6,23,0.5), rgba(2,6,23,0.7));display:flex;align-items:center;justify-content:center; +} +.hint-board{background:#071124;padding:18px;border-radius:12px;border:1px solid rgba(255,255,255,0.06);max-width:720px;width:90%} +.hint-board h3{color:#ffd78a;margin:0 0 8px} +.hint-row{display:flex;gap:10px;flex-wrap:wrap} +.hint-chip{padding:8px 10px;background:linear-gradient(90deg,#1f2937,#0b1220);border-radius:8px;border:1px solid rgba(255,255,255,0.03);font-weight:600;color:#dbeafe} + +@media (min-width:760px){ + .timeline{flex-direction:column} +} diff --git a/games/Clicker Farmer/index.html b/games/Clicker Farmer/index.html new file mode 100644 index 00000000..bacd4be1 --- /dev/null +++ b/games/Clicker Farmer/index.html @@ -0,0 +1,35 @@ + + + + + + Clicker Farmer + + + +
โ˜€๏ธ
+
+ +

Clicker Farmer ๐Ÿง‘โ€๐ŸŒพ

+ +
+
+
๐Ÿ’ฐ Money: 10
+
โญ Level: 1 (XP: 0 / 100)
+
+
+
+ +
+ + +
+
+ +
+ +
+ + + + \ No newline at end of file diff --git a/games/Clicker Farmer/script.js b/games/Clicker Farmer/script.js new file mode 100644 index 00000000..aec0cf37 --- /dev/null +++ b/games/Clicker Farmer/script.js @@ -0,0 +1,245 @@ +// --- DOM Elements --- +const moneyDisplay = document.getElementById('money-display'); +const levelDisplay = document.getElementById('level-display'); +const xpDisplay = document.getElementById('xp-display'); +const xpNextDisplay = document.getElementById('xp-next-display'); +const farmLand = document.getElementById('farm-land'); +const buyLandBtn = document.getElementById('buy-land-btn'); +const notificationContainer = document.getElementById('notification-container'); +const cropStoreButtons = document.getElementById('crop-store-buttons'); +const resetBtn = document.getElementById('reset-btn'); + +// --- Game State --- +let gameState = {}; + +// --- Game Configuration & Data --- +const CROPS_DATA = { + wheat: { id: 'wheat', name: 'Wheat', emoji: '๐ŸŒพ', baseCost: 5, baseProfit: 15, growTime: 3000, xp: 10, unlockLevel: 1 }, + carrot: { id: 'carrot', name: 'Carrot', emoji: '๐Ÿฅ•', baseCost: 20, baseProfit: 50, growTime: 5000, xp: 25, unlockLevel: 3 }, + strawberry: { id: 'strawberry', name: 'Strawberry', emoji: '๐Ÿ“', baseCost: 50, baseProfit: 120, growTime: 8000, xp: 60, unlockLevel: 5 } +}; + +// --- Sound Effects System --- +const sounds = { plant: new Audio(''), harvest: new Audio(''), error: new Audio(''), buy: new Audio('') }; +function playSound(sound) { try { sounds[sound].currentTime = 0; sounds[sound].play(); } catch (e) {} } + +// --- Main Game Functions --- +function initializeGame() { + loadGame(); + render(); + farmLand.addEventListener('click', handlePlotClick); + buyLandBtn.addEventListener('click', buyLand); + resetBtn.addEventListener('click', resetGame); + setInterval(saveGame, 5000); + setInterval(updateTimers, 100); +} + +// --- State & Save/Load Functions --- +function setDefaultState() { + gameState = { + money: 10, level: 1, xp: 0, + plots: Array.from({ length: 6 }, () => ({ state: 'empty', cropId: null, plantedAt: null })), + selectedCropId: 'wheat', lastPlayed: Date.now() + }; +} + +function saveGame() { + gameState.lastPlayed = Date.now(); + localStorage.setItem('clickerFarmerSave', JSON.stringify(gameState)); +} + +function loadGame() { + const savedState = localStorage.getItem('clickerFarmerSave'); + if (savedState) { + gameState = JSON.parse(savedState); + calculateOfflineProgress(); + } else { setDefaultState(); } +} + +function resetGame() { + showNotification("Your progress has been reset.", "info"); + setTimeout(() => { + localStorage.removeItem('clickerFarmerSave'); + location.reload(); + }, 1000); +} + +function calculateOfflineProgress() { + const timePassed = Date.now() - (gameState.lastPlayed || Date.now()); + let offlineEarnings = 0; + gameState.plots.forEach(plot => { + if (plot.state === 'planted') { + const crop = getCrop(plot.cropId); + const timeSincePlanted = (Date.now() - plot.plantedAt); + if (timeSincePlanted >= crop.growTime) { + plot.state = 'ready'; + offlineEarnings += crop.profit; + gainXP(crop.xp, false); + } + } + }); + if (offlineEarnings > 0) { + gameState.money += offlineEarnings; + showNotification(`Welcome back! You earned ${formatNumber(offlineEarnings)} while away.`, 'info'); + } +} + +// --- UI & Rendering --- +function render() { + updateUI(); + renderStore(); + farmLand.innerHTML = ''; + gameState.plots.forEach((plot, index) => { + const plotElement = document.createElement('div'); + plotElement.className = `farm-plot ${plot.state}`; + plotElement.dataset.index = index; + plotElement.innerHTML = `
`; + if (plot.state === 'planted' || plot.state === 'ready') { + plotElement.querySelector('.plot-crop').textContent = getCrop(plot.cropId).emoji; + } + farmLand.appendChild(plotElement); + }); +} + +// REFINED: Update UI with number formatting +function updateUI() { + moneyDisplay.textContent = formatNumber(gameState.money); + levelDisplay.textContent = gameState.level; + const xpNext = gameState.level * 100; + xpDisplay.textContent = formatNumber(gameState.xp); + xpNextDisplay.textContent = formatNumber(xpNext); + const price = getLandPrice(); + buyLandBtn.innerHTML = `Buy Plot (${formatNumber(price)})`; + buyLandBtn.disabled = gameState.money < price; +} + +function updateTimers() { + document.querySelectorAll('.farm-plot.planted').forEach(plotElement => { + const index = parseInt(plotElement.dataset.index); + const plot = gameState.plots[index]; + if(!plot) return; + const crop = getCrop(plot.cropId); + const timePassed = Date.now() - plot.plantedAt; + const progress = Math.min(100, (timePassed / crop.growTime) * 100); + const timerDiv = plotElement.querySelector('.plot-timer'); + if(timerDiv) timerDiv.style.width = `${progress}%`; + }); +} + +function renderStore() { + cropStoreButtons.innerHTML = ''; + for (const cropId in CROPS_DATA) { + const crop = getCrop(cropId); + const button = document.createElement('button'); + const isUnlocked = gameState.level >= crop.unlockLevel; + button.innerHTML = `${crop.emoji}
${crop.name}`; + button.title = `Cost: ${formatNumber(crop.cost)}, Profit: ${formatNumber(crop.profit)}, Time: ${crop.growTime/1000}s`; + if (!isUnlocked) { + button.disabled = true; + button.title += ` (Unlocks at Level ${crop.unlockLevel})`; + } + if (gameState.selectedCropId === crop.id) { button.classList.add('selected'); } + button.onclick = () => { + if (isUnlocked) { gameState.selectedCropId = crop.id; playSound('buy'); renderStore(); } + else { showNotification(`Unlock ${crop.name} at Level ${crop.unlockLevel}!`, 'error'); playSound('error'); } + }; + cropStoreButtons.appendChild(button); + } +} + +// --- Game Logic & Actions --- +// REFINED: Get crop data with level scaling +function getCrop(cropId) { + const cropData = CROPS_DATA[cropId]; + const levelMultiplier = 1 + (gameState.level - 1) * 0.1; // 10% increase per level + return { + ...cropData, + cost: Math.floor(cropData.baseCost * levelMultiplier), + profit: Math.floor(cropData.baseProfit * levelMultiplier) + }; +} + +function getLandPrice() { return Math.floor(150 * Math.pow(1.5, gameState.plots.length - 6)); } + +function buyLand() { + const price = getLandPrice(); + if (gameState.money >= price) { + gameState.money -= price; + gameState.plots.push({ state: 'empty', cropId: null, plantedAt: null }); + playSound('buy'); render(); + } +} + +function handlePlotClick(event) { + const plotElement = event.target.closest('.farm-plot'); + if (!plotElement) return; + const plotIndex = parseInt(plotElement.dataset.index); + const plot = gameState.plots[plotIndex]; + if (plot.state === 'empty') plantCrop(plotIndex, plotElement); + else if (plot.state === 'ready') harvestCrop(plotIndex, plotElement); +} + +function plantCrop(plotIndex) { + const crop = getCrop(gameState.selectedCropId); + if (gameState.money >= crop.cost) { + gameState.money -= crop.cost; + const plot = gameState.plots[plotIndex]; + plot.state = 'planted'; plot.cropId = crop.id; plot.plantedAt = Date.now(); + playSound('plant'); render(); + setTimeout(() => { if (plot.plantedAt) { plot.state = 'ready'; render(); } }, crop.growTime); + } else { showNotification(`Not enough money to plant ${crop.name}!`, 'error'); playSound('error'); } +} + +function harvestCrop(plotIndex, plotElement) { + const plot = gameState.plots[plotIndex]; + const crop = getCrop(plot.cropId); + gameState.money += crop.profit; + gainXP(crop.xp); + createFloatingText(`+${formatNumber(crop.profit)}`, plotElement); + plot.state = 'empty'; plot.cropId = null; plot.plantedAt = null; + playSound('harvest'); render(); flashMoney(); +} + +// --- Helpers & Visuals --- +function gainXP(amount, showNotif = true) { + gameState.xp += amount; + const xpToNextLevel = gameState.level * 100; + if (gameState.xp >= xpToNextLevel) { + gameState.xp -= xpToNextLevel; gameState.level++; + if(showNotif) showNotification(`Congratulations! You've reached Level ${gameState.level}!`, 'success'); + renderStore(); + } +} + +// NEW: Number formatting utility +function formatNumber(num) { + if (num < 1000) return num; + if (num < 1000000) return (num / 1000).toFixed(1) + 'K'; + if (num < 1000000000) return (num / 1000000).toFixed(1) + 'M'; + return (num / 1000000000).toFixed(1) + 'B'; +} + +function showNotification(message, type = 'success') { + const notif = document.createElement('div'); + notif.className = `notification ${type}`; + notif.textContent = message; + notificationContainer.appendChild(notif); + setTimeout(() => notif.remove(), 4000); +} + +function flashMoney() { + moneyDisplay.parentElement.classList.add('ui-flash-green'); + setTimeout(() => moneyDisplay.parentElement.classList.remove('ui-flash-green'), 500); +} + +// NEW: Floating text on harvest +function createFloatingText(text, element) { + const float = document.createElement('div'); + float.className = 'harvest-float'; + float.textContent = text; + element.appendChild(float); + setTimeout(() => float.remove(), 1500); +} + +// --- Start the Game --- +initializeGame(); \ No newline at end of file diff --git a/games/Clicker Farmer/style.css b/games/Clicker Farmer/style.css new file mode 100644 index 00000000..6842644f --- /dev/null +++ b/games/Clicker Farmer/style.css @@ -0,0 +1,58 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + background-color: #87CEEB; text-align: center; color: #333; + padding-top: 20px; overflow-y: auto; user-select: none; +} +h1 { margin-bottom: 15px; color: #004d40; } + +/* --- Dashboard --- */ +#dashboard { + display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 20px; + background-color: #FFFBEA; border: 2px solid #D1B894; border-radius: 16px; + padding: 10px 20px; max-width: 800px; margin: 0 auto 30px auto; + box-shadow: 0 5px 15px rgba(0,0,0,0.15); +} +#stats { display: flex; gap: 25px; } +#stats .stat { font-size: 16px; font-weight: 600; } +#store { display: flex; align-items: center; gap: 10px; } +#crop-store-buttons { display: flex; gap: 8px; } +#store button { + padding: 10px 15px; font-size: 14px; font-weight: 600; cursor: pointer; border: 1px solid rgba(0,0,0,0.1); + border-radius: 10px; color: #333; transition: all 0.2s; +} +#store button:hover { transform: translateY(-2px); box-shadow: 0 2px 4px rgba(0,0,0,0.2); } +#store button:active { transform: translateY(0); } +#store button.selected { box-shadow: 0 0 0 3px #FBC02D; border-color: #FBC02D; } +#store button:disabled { background-color: #E0E0E0; cursor: not-allowed; opacity: 0.7; transform: none; box-shadow: none; color: #9E9E9E; } +#buy-land-btn { background-color: #CFD8DC; } +#reset-btn { background-color: #EF5350; color: white; } +#crop-store-buttons button { background-color: #FFFFFF; } + +/* --- Farm Land & Plots --- */ +#farm-land { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 15px; justify-content: center; max-width: 800px; margin: 0 auto; } +.farm-plot { border: 3px dashed #A1887F; border-radius: 16px; cursor: pointer; font-size: 50px; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; width: 110px; height: 110px; position: relative; overflow: hidden; background-color: #A1887F; } +.farm-plot.empty:hover { background-color: #BCAAA4; } +.farm-plot.planted { background-color: #6D4C41; cursor: not-allowed; } +.farm-plot.ready { background-color: #C8E6C9; animation: grow-pulse 1.5s infinite; } +.plot-timer { position: absolute; bottom: 0; left: 0; width: 0%; height: 10px; background-color: #66BB6A; transition: width 0.1s linear; } +.plot-crop { z-index: 2; } +@keyframes grow-pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } + +/* --- NEW: Floating Harvest Text --- */ +.harvest-float { + position: absolute; color: #FFD700; font-size: 20px; font-weight: bold; + z-index: 10; pointer-events: none; animation: float-up 1.5s ease-out forwards; + text-shadow: 1px 1px 2px rgba(0,0,0,0.5); +} +@keyframes float-up { 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(-60px); opacity: 0; } } + +/* --- Sun, Notifications & Animations --- */ +#sun { position: fixed; top: 20px; left: 20px; font-size: 60px; animation: pulse-sun 5s infinite; } +@keyframes pulse-sun { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } } +#notification-container { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1000; display: flex; flex-direction: column; gap: 10px; } +.notification { padding: 12px 20px; border-radius: 8px; color: white; font-weight: bold; box-shadow: 0 4px 8px rgba(0,0,0,0.2); animation: slide-down 0.5s ease, fade-out 0.5s ease 3.5s forwards; } +.notification.error { background-color: #e53935; } .notification.success { background-color: #43a047; } .notification.info { background-color: #1e88e5; } +@keyframes slide-down { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } } +@keyframes fade-out { from { opacity: 1; } to { opacity: 0; } } +.ui-flash-green { animation: flash-green 0.5s ease-out; } +@keyframes flash-green { 0% { color: #2e7d32; transform: scale(1.2); } 100% { color: #333; transform: scale(1); } } \ No newline at end of file diff --git a/games/Color Canvas/index.html b/games/Color Canvas/index.html new file mode 100644 index 00000000..553920d5 --- /dev/null +++ b/games/Color Canvas/index.html @@ -0,0 +1,29 @@ + + + + + + Color Canvas ๐ŸŽจ + + + +
+

๐ŸŽจ Color Canvas

+
+ + + + + + + + + +
+
+ + + + + + diff --git a/games/Color Canvas/script.js b/games/Color Canvas/script.js new file mode 100644 index 00000000..9b28581b --- /dev/null +++ b/games/Color Canvas/script.js @@ -0,0 +1,56 @@ +const canvas = document.getElementById("drawing-board"); +const ctx = canvas.getContext("2d"); +const brushSize = document.getElementById("brush-size"); +const colorPicker = document.getElementById("color-picker"); +const eraserBtn = document.getElementById("eraser"); +const clearBtn = document.getElementById("clear"); +const saveBtn = document.getElementById("save"); + +let drawing = false; +let currentColor = colorPicker.value; +let lineWidth = brushSize.value; +let isErasing = false; + +function resizeCanvas() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight - document.querySelector(".toolbar").offsetHeight; +} +resizeCanvas(); + +window.addEventListener("resize", resizeCanvas); + +canvas.addEventListener("mousedown", () => (drawing = true)); +canvas.addEventListener("mouseup", () => (drawing = false, ctx.beginPath())); +canvas.addEventListener("mouseout", () => (drawing = false)); +canvas.addEventListener("mousemove", draw); + +function draw(e) { + if (!drawing) return; + ctx.lineWidth = lineWidth; + ctx.lineCap = "round"; + ctx.strokeStyle = isErasing ? "#0f0c29" : currentColor; + + ctx.lineTo(e.clientX, e.clientY - document.querySelector(".toolbar").offsetHeight); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(e.clientX, e.clientY - document.querySelector(".toolbar").offsetHeight); +} + +brushSize.addEventListener("input", (e) => (lineWidth = e.target.value)); +colorPicker.addEventListener("input", (e) => { + currentColor = e.target.value; + isErasing = false; +}); + +eraserBtn.addEventListener("click", () => (isErasing = true)); + +clearBtn.addEventListener("click", () => { + ctx.clearRect(0, 0, canvas.width, canvas.height); +}); + +saveBtn.addEventListener("click", () => { + const link = document.createElement("a"); + link.download = "my_art.png"; + link.href = canvas.toDataURL(); + link.click(); +}); diff --git a/games/Color Canvas/style.css b/games/Color Canvas/style.css new file mode 100644 index 00000000..a4c10745 --- /dev/null +++ b/games/Color Canvas/style.css @@ -0,0 +1,63 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: radial-gradient(circle, #0f0c29, #302b63, #24243e); + font-family: "Poppins", sans-serif; + color: white; + overflow: hidden; + display: flex; + flex-direction: column; + height: 100vh; +} + +.toolbar { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + padding: 10px 20px; + text-align: center; + border-bottom: 2px solid rgba(255, 255, 255, 0.3); + display: flex; + justify-content: space-between; + align-items: center; +} + +.toolbar h1 { + font-size: 1.5em; + color: #ffcc70; + text-shadow: 0 0 15px #ffcc70; +} + +.controls { + display: flex; + align-items: center; + gap: 10px; +} + +input[type="range"], input[type="color"] { + cursor: pointer; +} + +button { + border: none; + padding: 8px 15px; + border-radius: 20px; + background: linear-gradient(45deg, #00dbde, #fc00ff); + color: white; + font-weight: bold; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; +} + +button:hover { + transform: scale(1.05); + box-shadow: 0 0 15px #00dbde; +} + +canvas { + flex: 1; + cursor: crosshair; + display:ย block; +} diff --git a/games/Color Sequence Memory/index.html b/games/Color Sequence Memory/index.html new file mode 100644 index 00000000..1212c5df --- /dev/null +++ b/games/Color Sequence Memory/index.html @@ -0,0 +1,31 @@ + + + + + + Color Sequence Memory + + + +
+

Color Sequence Memory

+

Level: 0

+ +
+ + + + +
+ +
+ + +
+ +

+
+ + + + diff --git a/games/Color Sequence Memory/script.js b/games/Color Sequence Memory/script.js new file mode 100644 index 00000000..f89c1e7f --- /dev/null +++ b/games/Color Sequence Memory/script.js @@ -0,0 +1,95 @@ +const colors = ["green", "red", "yellow", "blue"]; +let gameSequence = []; +let playerSequence = []; +let level = 0; +let acceptingInput = false; + +const levelDisplay = document.getElementById("level"); +const messageEl = document.getElementById("message"); +const startBtn = document.getElementById("start-btn"); +const restartBtn = document.getElementById("restart-btn"); +const colorButtons = document.querySelectorAll(".color-btn"); + +// Play sound for each color (optional) +function playSound(color) { + const audio = new Audio(`https://s3.amazonaws.com/freecodecamp/simonSound${colors.indexOf(color)+1}.mp3`); + audio.play(); +} + +// Highlight button +function flashButton(color) { + const btn = document.getElementById(color); + btn.classList.add("active"); + playSound(color); + setTimeout(() => btn.classList.remove("active"), 400); +} + +// Generate next color in sequence +function nextStep() { + acceptingInput = false; + playerSequence = []; + level++; + levelDisplay.textContent = level; + + const nextColor = colors[Math.floor(Math.random() * colors.length)]; + gameSequence.push(nextColor); + + messageEl.textContent = "Watch the sequence!"; + + // Show sequence with delays + gameSequence.forEach((color, index) => { + setTimeout(() => flashButton(color), index * 700); + }); + + setTimeout(() => { + acceptingInput = true; + messageEl.textContent = "Your turn!"; + }, gameSequence.length * 700); +} + +// Check player input +function handlePlayerInput(color) { + if (!acceptingInput) return; + + playerSequence.push(color); + flashButton(color); + + const currentIndex = playerSequence.length - 1; + if (playerSequence[currentIndex] !== gameSequence[currentIndex]) { + gameOver(); + return; + } + + if (playerSequence.length === gameSequence.length) { + acceptingInput = false; + setTimeout(nextStep, 1000); + } +} + +// Game over +function gameOver() { + messageEl.textContent = `Game Over! You reached level ${level}.`; + startBtn.disabled = false; + restartBtn.disabled = true; + gameSequence = []; + playerSequence = []; + level = 0; + acceptingInput = false; +} + +// Event listeners +colorButtons.forEach(btn => { + btn.addEventListener("click", () => handlePlayerInput(btn.dataset.color)); +}); + +startBtn.addEventListener("click", () => { + startBtn.disabled = true; + restartBtn.disabled = false; + messageEl.textContent = ""; + nextStep(); +}); + +restartBtn.addEventListener("click", () => { + gameOver(); + startBtn.click(); +}); diff --git a/games/Color Sequence Memory/style.css b/games/Color Sequence Memory/style.css new file mode 100644 index 00000000..22c78ea1 --- /dev/null +++ b/games/Color Sequence Memory/style.css @@ -0,0 +1,83 @@ +body { + margin: 0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background: linear-gradient(to bottom right, #4facfe, #00f2fe); +} + +.game-container { + text-align: center; + background-color: rgba(255,255,255,0.95); + padding: 30px 50px; + border-radius: 20px; + box-shadow: 0 10px 25px rgba(0,0,0,0.3); + width: 320px; +} + +h1 { + margin-bottom: 10px; + font-size: 1.8rem; + color: #333; +} + +.level-display { + font-weight: bold; + margin-bottom: 20px; +} + +.color-buttons { + display: grid; + grid-template-columns: repeat(2, 120px); + grid-template-rows: repeat(2, 120px); + gap: 15px; + justify-content: center; + margin-bottom: 20px; +} + +.color-btn { + border: none; + border-radius: 20px; + cursor: pointer; + transition: transform 0.2s, opacity 0.2s; + opacity: 0.9; +} + +.color-btn:active, +.color-btn.active { + transform: scale(1.1); + opacity: 1; + box-shadow: 0 5px 15px rgba(0,0,0,0.3); +} + +#green { background-color: #28a745; } +#red { background-color: #dc3545; } +#yellow { background-color: #ffc107; } +#blue { background-color: #007bff; } + +.controls button { + padding: 10px 20px; + margin: 5px; + border: none; + border-radius: 10px; + font-weight: bold; + cursor: pointer; + transition: background 0.3s; +} + +#start-btn { background-color: #28a745; color: #fff; } +#restart-btn { background-color: #dc3545; color: #fff; } + +.controls button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +#message { + margin-top: 15px; + font-weight: bold; + color: #333; + min-height: 1.2em; +} diff --git a/games/Color-Rush/index.html b/games/Color-Rush/index.html new file mode 100644 index 00000000..d64a563d --- /dev/null +++ b/games/Color-Rush/index.html @@ -0,0 +1,94 @@ + + + + + + Color Rush ๐ŸŽจ + + + + + + +
+
+
+
๐ŸŽจ
+

Color Rush

+
+ +
+
+
๐ŸŽฏ
+
Catch Color
+
+ Blue +
+
+
+ +
+
๐Ÿ†
+
Score
+
0
+
+ +
+
โฑ๏ธ
+
Time
+
01:00
+
+ +
+
๐Ÿงบ
+
Caught
+
0
+
+
+ +
+ + +
+
+ +
+
+
+
+ + +
+
+ + + + + +
+ Tip: Use arrow keys or drag inside the play area. +
+
+ + + + + \ No newline at end of file diff --git a/games/Color-Rush/script.js b/games/Color-Rush/script.js new file mode 100644 index 00000000..1d6889d1 --- /dev/null +++ b/games/Color-Rush/script.js @@ -0,0 +1,455 @@ +// Game elements + const gameArea = document.getElementById("gameArea"); + const basket = document.getElementById("basket"); + const scoreDisplay = document.getElementById("score"); + const targetColorDisplay = document.getElementById("targetColor"); + const targetSwatch = document.getElementById("targetSwatch"); + const basketGlow = document.querySelector(".basket-glow"); + + // Game state + const originalColors = [ + { name: "Red", color: "#e53935" }, + { name: "Blue", color: "#1e88e5" }, + { name: "Green", color: "#43a047" }, + { name: "Yellow", color: "#fdd835" }, + { name: "Purple", color: "#8e24aa" }, + { name: "Orange", color: "#fb8c00" } + ]; + + let targetColor = "Blue"; + let score = 0; + let caughtCount = 0; + const gameDuration = 60; + let timerRemaining = gameDuration; + let timerId = null; + let isRunning = false; + let inputEnabled = false; + // Prevent target-colored balls from spawning consecutively: set the earliest allowed time + // for the next target spawn. When a target ball is spawned we'll set this to now + 2-3s. + let nextTargetAllowedAt = 0; + + // Basket movement + let gameWidth = gameArea.clientWidth; + let maxRight = gameWidth - basket.offsetWidth; + let basketX = gameWidth / 2 - basket.offsetWidth / 2; + basket.style.transform = `translateX(${basketX}px)`; + + const basketSpeed = 20; + let leftPressed = false; + let rightPressed = false; + + // Update game area dimensions on resize + function updateSizes() { + gameWidth = gameArea.clientWidth; + maxRight = Math.max(0, gameWidth - basket.offsetWidth); + if (basketX > maxRight) { + basketX = maxRight; + basket.style.transform = `translateX(${basketX}px)`; + } + } + window.addEventListener("resize", updateSizes); + + // Keyboard controls + document.addEventListener("keydown", (e) => { + if (e.key === "p" || e.key === "P") { + const pb = document.getElementById("pauseBtn"); + if (pb) pb.click(); + return; + } + if (!inputEnabled) return; + if (e.key === "ArrowLeft") leftPressed = true; + if (e.key === "ArrowRight") rightPressed = true; + }); + + document.addEventListener("keyup", (e) => { + if (!inputEnabled) return; + if (e.key === "ArrowLeft") leftPressed = false; + if (e.key === "ArrowRight") rightPressed = false; + }); + + // Basket movement loop + function gameLoop() { + let moved = false; + if (leftPressed) { + basketX -= basketSpeed; + moved = true; + } + if (rightPressed) { + basketX += basketSpeed; + moved = true; + } + if (moved) { + basketX = Math.max(0, Math.min(basketX, maxRight)); + basket.style.transform = `translateX(${basketX}px)`; + basketGlow.style.transform = `translateX(${basketX}px)`; + } + requestAnimationFrame(gameLoop); + } + requestAnimationFrame(gameLoop); + + // Pointer/touch controls + gameArea.addEventListener("pointermove", (e) => { + if (!inputEnabled || e.isPrimary === false) return; + const rect = gameArea.getBoundingClientRect(); + const x = e.clientX - rect.left - basket.offsetWidth / 2; + basketX = Math.min(Math.max(0, x), maxRight); + basket.style.transform = `translateX(${basketX}px)`; + basketGlow.style.transform = `translateX(${basketX}px)`; + }); + + // Target color rotation + function changeTargetColor() { + const obj = originalColors[Math.floor(Math.random() * originalColors.length)]; + targetColor = obj.name; + targetColorDisplay.textContent = obj.name; + targetSwatch.style.background = obj.color; + + // Add animation to swatch + targetSwatch.style.animation = "pulse 1s ease-out"; + setTimeout(() => { + targetSwatch.style.animation = ""; + }, 1000); + } + setInterval(changeTargetColor, 5000); + changeTargetColor(); + + // Create falling balls + function createBall() { + const ball = document.createElement("div"); + ball.classList.add("ball"); + + // Slight bias toward the current target color so players see a few more desired balls + // Compute a subtle bias relative to the uniform baseline so the increase is small + const baseline = 1 / originalColors.length; // uniform probability + const biasIncrease = 0.12; // add ~12 percentage points over uniform (tweakable) + const biasToTarget = Math.min(0.6, baseline + biasIncrease); + + let colorObj; + const now = Date.now(); + // Only choose the target color if we're past the cooldown window + const chooseTarget = Math.random() < biasToTarget && now >= nextTargetAllowedAt; + if (chooseTarget) { + // pick the target color (fallback to a random color if not found) + colorObj = originalColors.find((c) => c.name === targetColor) || originalColors[Math.floor(Math.random() * originalColors.length)]; + // set a cooldown so the target color doesn't appear again immediately + const cooldownMs = 2000 + Math.random() * 1000; // 2-3 seconds + nextTargetAllowedAt = now + cooldownMs; + } else { + // pick a non-target color + const others = originalColors.filter((c) => c.name !== targetColor); + colorObj = others.length ? others[Math.floor(Math.random() * others.length)] : originalColors[Math.floor(Math.random() * originalColors.length)]; + } + ball.style.background = colorObj.color; + ball.dataset.colorName = colorObj.name; + + // Position the ball + const spawnBandWidth = gameArea.offsetWidth * 0.7; + const spawnStart = (gameArea.offsetWidth - spawnBandWidth) / 2; + const leftPos = spawnStart + Math.random() * Math.max(0, spawnBandWidth - 36); + ball.style.left = leftPos + "px"; + ball.style.animation = "none"; + + gameArea.appendChild(ball); + + // Ball animation and collision + const fallSpeed = 2; + let topPos = 0; + + ball._tick = function () { + topPos += fallSpeed; + ball._topPos = topPos; + ball.style.top = topPos + "px"; + + const ballRect = ball.getBoundingClientRect(); + const basketRect = basket.getBoundingClientRect(); + + // Collision detection + if ( + ballRect.bottom >= basketRect.top && + ballRect.left < basketRect.right && + ballRect.right > basketRect.left + ) { + const ballName = ball.dataset.colorName || ""; + + // Visual feedback for catching + basketGlow.style.opacity = "1"; + setTimeout(() => { + basketGlow.style.opacity = "0"; + }, 300); + + // Create particles + createParticles(ballRect.left + ballRect.width/2, ballRect.top + ballRect.height/2, ball.style.background); + + if (ballName === targetColor) { + score += 10; + } else { + score -= 5; + } + + scoreDisplay.textContent = score; + caughtCount += 1; + document.getElementById("caught").textContent = caughtCount; + + if (ball._fallInterval) { + clearInterval(ball._fallInterval); + ball._fallInterval = null; + } + ball.remove(); + return; + } + + // Remove ball if off-screen + if (topPos > gameArea.offsetHeight) { + if (ball._fallInterval) { + clearInterval(ball._fallInterval); + ball._fallInterval = null; + } + ball.remove(); + } + }; + + // Start the ball falling + const fallInterval = setInterval(ball._tick, 20); + ball._fallInterval = fallInterval; + ball._fallSpeed = fallSpeed; + } + + // Create particles for visual effect + function createParticles(x, y, color) { + for (let i = 0; i < 8; i++) { + const particle = document.createElement("div"); + particle.classList.add("particle"); + particle.style.background = color; + particle.style.left = x + "px"; + particle.style.top = y + "px"; + + const angle = Math.random() * Math.PI * 2; + const velocity = 2 + Math.random() * 3; + const vx = Math.cos(angle) * velocity; + const vy = Math.sin(angle) * velocity; + + gameArea.appendChild(particle); + + let opacity = 1; + const particleInterval = setInterval(() => { + const left = parseFloat(particle.style.left) + vx; + const top = parseFloat(particle.style.top) + vy; + opacity -= 0.03; + + particle.style.left = left + "px"; + particle.style.top = top + "px"; + particle.style.opacity = opacity; + + if (opacity <= 0) { + clearInterval(particleInterval); + particle.remove(); + } + }, 30); + } + } + + // Game control functions + let spawnIntervalId = null; + + function startSpawning() { + if (spawnIntervalId) return; + spawnIntervalId = setInterval(createBall, 1000); + } + + function stopSpawning() { + if (!spawnIntervalId) return; + clearInterval(spawnIntervalId); + spawnIntervalId = null; + } + + // Timer functions + function startTimer() { + if (timerId) return; + timerId = setInterval(() => { + timerRemaining -= 1; + updateTimerDisplay(); + if (timerRemaining <= 0) { + endGame(); + } + }, 1000); + } + + function pauseTimer() { + if (!timerId) return; + clearInterval(timerId); + timerId = null; + } + + function resumeTimer() { + if (timerId) return; + startTimer(); + } + + function resetTimer() { + pauseTimer(); + timerRemaining = gameDuration; + updateTimerDisplay(); + } + + function updateTimerDisplay() { + const el = document.getElementById("timer"); + if (!el) return; + const mm = String(Math.floor(timerRemaining / 60)).padStart(2, "0"); + const ss = String(timerRemaining % 60).padStart(2, "0"); + el.textContent = `${mm}:${ss}`; + + // Change color when time is running out + if (timerRemaining <= 10) { + el.style.color = "#FF4081"; + el.style.animation = "pulse 1s infinite"; + } else { + el.style.color = ""; + el.style.animation = ""; + } + } + + // Ball control functions + function pauseAllBalls() { + const balls = document.querySelectorAll(".ball"); + balls.forEach((b) => { + if (b._fallInterval) { + clearInterval(b._fallInterval); + b._fallInterval = null; + } + }); + } + + function resumeAllBalls() { + const balls = document.querySelectorAll(".ball"); + balls.forEach((b) => { + if (b._fallInterval) return; + if (typeof b._tick === "function") { + b._fallInterval = setInterval(b._tick, 20); + } + }); + } + + function removeAllBalls() { + const balls = document.querySelectorAll(".ball"); + balls.forEach((b) => { + if (b._fallInterval) clearInterval(b._fallInterval); + b.remove(); + }); + } + + // Game state functions + function resetGame() { + stopSpawning(); + pauseTimer(); + removeAllBalls(); + score = 0; + caughtCount = 0; + scoreDisplay.textContent = score; + document.getElementById("caught").textContent = caughtCount; + resetTimer(); + isRunning = false; + + const modal = document.getElementById("modalOverlay"); + if (modal) modal.classList.add("hidden"); + + const po = document.getElementById("pauseOverlay"); + if (po) po.classList.add("hidden"); + + inputEnabled = false; + document.getElementById("pauseBtn").textContent = "Pause"; + document.getElementById("startBtn").innerHTML = "โ–ถ๏ธ Start Game"; + } + + function startGame() { + resetGame(); + score = 0; + caughtCount = 0; + scoreDisplay.textContent = score; + document.getElementById("caught").textContent = caughtCount; + + startSpawning(); + startTimer(); + isRunning = true; + inputEnabled = true; + document.getElementById("startBtn").innerHTML = "๐Ÿ”„ Restart"; + } + + function endGame() { + stopSpawning(); + pauseTimer(); + isRunning = false; + inputEnabled = false; + + const modal = document.getElementById("modalOverlay"); + const modalCaught = document.getElementById("modalCaught"); + const modalScore = document.getElementById("modalScore"); + + if (modal && modalCaught && modalScore) { + modalCaught.textContent = caughtCount; + modalScore.textContent = score; + modal.classList.remove("hidden"); + } + + document.getElementById("startBtn").innerHTML = "๐Ÿ”„ Restart"; + } + + // Button event listeners + document.getElementById("startBtn").addEventListener("click", () => { + if (isRunning) { + resetGame(); + startGame(); + } else { + startGame(); + } + }); + + document.getElementById("pauseBtn").addEventListener("click", () => { + if (!isRunning) return; + + if (spawnIntervalId) { + // Pause + stopSpawning(); + pauseTimer(); + pauseAllBalls(); + + document.getElementById("pauseOverlay").classList.remove("hidden"); + document.getElementById("pauseBtn").innerHTML = "โ–ถ๏ธ Resume"; + } else { + // Resume + startSpawning(); + resumeTimer(); + resumeAllBalls(); + + document.getElementById("pauseOverlay").classList.add("hidden"); + document.getElementById("pauseBtn").innerHTML = "โธ๏ธ Pause"; + inputEnabled = true; + } + }); + + // On-screen touch arrow buttons removed; mobile users can drag inside the play area. + + // Modal buttons + document.getElementById("modalRestart").addEventListener("click", () => { + document.getElementById("modalOverlay").classList.add("hidden"); + startGame(); + }); + + document.getElementById("modalClose").addEventListener("click", () => { + document.getElementById("modalOverlay").classList.add("hidden"); + }); + + // Allow clicking the pause overlay to immediately restart the game + const pauseOverlayEl = document.getElementById("pauseOverlay"); + if (pauseOverlayEl) { + pauseOverlayEl.addEventListener("click", (e) => { + // only act if the overlay is visible (i.e. paused) + if (pauseOverlayEl.classList.contains("hidden")) return; + // Restart the game from pause + pauseOverlayEl.classList.add("hidden"); + resetGame(); + startGame(); + }); + } + // Initialize + updateTimerDisplay(); + \ No newline at end of file diff --git a/games/Color-Rush/style.css b/games/Color-Rush/style.css new file mode 100644 index 00000000..aab11f96 --- /dev/null +++ b/games/Color-Rush/style.css @@ -0,0 +1,410 @@ + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Inter, Helvetica, Arial, sans-serif; + /* professional deep gradient with a faint warm pink highlight near the top */ + background-image: + radial-gradient(ellipse at top center, rgba(255,120,170,0.08) 0%, rgba(255,120,170,0.03) 22%, transparent 42%), + linear-gradient(160deg, #0f172a 0%, #2a2d6a 45%, #064e63 100%); + background-repeat: no-repeat; + background-size: cover; + color: #e6eef8; + min-height: 100vh; + display: flex; + overflow: hidden; /* prevent page scroll */ + justify-content: center; + overflow-x: hidden; + padding: 18px; + line-height: 1.45; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + } + + .container { + width: 100%; + max-width: 900px; + display: flex; + flex-direction: column; + gap: 10px; /* slightly tighter */ + height: calc(100vh - 24px); /* occupy viewport height */ + overflow: hidden; /* keep everything fitted inside */ + justify-content: space-between; /* push footer to bottom so gameArea can expand */ + } + + .header { + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(12px); + border-radius: 12px; + padding: 8px 10px; /* much tighter padding to shrink header */ + box-shadow: 0 4px 18px rgba(0, 0, 0, 0.14); + border: 1px solid rgba(255, 255, 255, 0.10); + flex: 0 0 72px; /* fixed smaller header height */ + display: flex; + flex-direction: column; + justify-content: center; + } + + .title-container { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin-bottom: 20px; + } + + .title { + font-size: 2rem; /* increased for prominence */ + font-weight: 800; + /* subtle pinkish gradient for a gentle accent */ + background: linear-gradient(90deg, #FF9BB3 0%, #FF6FA3 60%, #C07ACB 100%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + text-align: center; + margin: 0; + line-height: 1; + } + + .title-icon { + font-size: 2.2rem; + animation: float 3s ease-in-out infinite; + } + + @keyframes float { + 0% { transform: translateY(0px); } + 50% { transform: translateY(-8px); } + 100% { transform: translateY(0px); } + } + + .stats-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 8px; /* tighter */ + margin-bottom: 6px; /* less vertical space */ + align-items: center; + } + + .stat-card { + background: rgba(255, 255, 255, 0.12); + border-radius: 10px; + padding: 8px; /* smaller */ + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08); + transition: transform 0.18s ease, box-shadow 0.18s ease; + } + + .stat-card:hover { + transform: translateY(-6px); + box-shadow: 0 14px 34px rgba(180, 90, 140, 0.12); + border-color: rgba(255, 255, 255, 0.16); + } + + .stat-icon { + font-size: 1.5rem; + margin-bottom: 4px; + } + + .stat-label { + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.8); + font-weight: 500; + } + + .stat-value { + font-size: 1.1rem; + font-weight: 700; + } + + .target-color { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + } + + .target-swatch { + width: 36px; + height: 36px; + border-radius: 50%; + box-shadow: 0 6px 18px rgba(200, 80, 140, 0.18); + border: 2px solid rgba(255, 255, 255, 0.9); + transition: transform 0.25s ease, box-shadow 0.25s ease; + } + + .target-swatch.pulse { + transform: scale(1.08); + box-shadow: 0 10px 28px rgba(255, 120, 170, 0.28); + } + + .controls { + display: flex; + justify-content: center; + gap: 16px; + margin-top: 16px; + } + + .btn { + /* warmer pink/purple CTA */ + background: linear-gradient(135deg, #FF7AA2 0%, #FF9BC0 50%, #C98BE0 100%); + color: white; + border: none; + padding: 12px 24px; + border-radius: 50px; + font-weight: 600; + font-size: 1rem; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + box-shadow: 0 6px 20px rgba(200, 80, 140, 0.12); + transition: all 0.28s cubic-bezier(.2,.9,.2,1); + } + + .btn:hover { + transform: translateY(-3px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); + } + + .btn:active { + transform: translateY(1px); + } + + .btn.outline { + background: transparent; + border: 2px solid rgba(255, 255, 255, 0.3); + color: #fff; + } + + .btn.outline:hover { + background: rgba(255, 255, 255, 0.1); + } + + #gameArea { + position: relative; + width: 100%; + /* make the game area take most of the vertical space */ + flex: 4 1 auto; /* larger grow factor */ + min-height: 380px; /* increase minimum so it's noticeably bigger */ + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(8px); + border-radius: 16px; + overflow: hidden; + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18); + border: 1px solid rgba(255, 255, 255, 0.12); + display: flex; + flex-direction: column; + } + + #basket { + position: absolute; + bottom: 20px; + left: 0; + width: 120px; + height: 30px; + /* pink-forward basket with a soft highlight */ + background: linear-gradient(90deg, #FF7AA2 0%, #FFB6D2 60%); + border-radius: 15px 15px 8px 8px; + box-shadow: 0 8px 30px rgba(180, 80, 140, 0.25), 0 2px 6px rgba(0,0,0,0.35); + border: 1px solid rgba(255, 255, 255, 0.18); + transition: transform 0.12s cubic-bezier(.2,.9,.2,1); + z-index: 10; + } + + .basket-glow { + position: absolute; + bottom: 20px; + left: 0; + width: 120px; + height: 30px; + border-radius: 15px 15px 8px 8px; + /* pinkish glow to match the basket */ + background: rgba(255, 120, 170, 0.18); + filter: blur(14px); + z-index: 5; + opacity: 0; + transition: opacity 0.28s ease; + } + + .ball { + position: absolute; + width: 36px; + height: 36px; + border-radius: 50%; + top: 0; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3), inset 0 4px 8px rgba(255, 255, 255, 0.4); + border: 2px solid rgba(255, 255, 255, 0.6); + z-index: 5; + transition: transform 0.2s ease; + } + + .ball:hover { + transform: scale(1.1); + } + + /* touch control buttons removed โ€” pointer drag and keyboard remain */ + + .pause-overlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(5px); + z-index: 100; + } + + .pause-card { + background: rgba(255, 255, 255, 0.15); + backdrop-filter: blur(20px); + color: #fff; + padding: 30px 40px; + border-radius: 20px; + font-size: 2.5rem; + font-weight: 700; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.2); + animation: pulse 2s infinite; + } + + @keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } + } + + .modal { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(10px); + z-index: 1000; + } + + .modal-card { + background: rgba(255, 255, 255, 0.15); + backdrop-filter: blur(20px); + color: #fff; + padding: 40px; + border-radius: 20px; + width: min(500px, 90%); + text-align: center; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.2); + animation: modalAppear 0.4s ease-out; + } + + @keyframes modalAppear { + 0% { opacity: 0; transform: scale(0.8); } + 100% { opacity: 1; transform: scale(1); } + } + .modal-card h2 { + font-size: 2.2rem; + margin-bottom: 16px; + background: linear-gradient(90deg, #FFD54F, #FF4081); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } + + + .modal-summary { + font-size: 1.2rem; + margin-bottom: 24px; + color: rgba(255, 255, 255, 0.8); + } + + .modal-actions { + display: flex; + gap: 16px; + justify-content: center; + } + + .modal-btn { + flex: 1; + max-width: 180px; + } + + /* footer styling kept compact so game area can be larger */ + .footer { + text-align: center; + color: rgba(255, 255, 255, 0.75); + font-size: 0.78rem; + margin: 6px 0 8px 0; + padding: 4px 0; + flex: 0 0 auto; + opacity: 0.95; + } + + /* utility hide class used by JS to toggle overlays */ + .hidden { + display: none !important; + } + + .particle { + position: absolute; + width: 8px; + height: 8px; + border-radius: 50%; + pointer-events: none; + z-index: 1; + } + + @keyframes fall { + to { + top: 500px; + opacity: 0; + } + } + + @media (max-width: 768px) { + .title { + font-size: 2rem; + } + + .stats-container { + grid-template-columns: 1fr 1fr; + } + + .stat-card { + padding: 12px; + } + + .stat-value { + font-size: 1.5rem; + } + + .controls { + flex-wrap: wrap; + } + + .btn { + flex: 1; + min-width: 140px; + justify-content: center; + } + } + + @media (max-width: 480px) { + .stats-container { + grid-template-columns: 1fr; + } + + .title { + font-size: 1.8rem; + } + + #gameArea { + min-height: 180px; + } + } \ No newline at end of file diff --git a/games/ColorSpin/index.html b/games/ColorSpin/index.html new file mode 100644 index 00000000..a82c15db --- /dev/null +++ b/games/ColorSpin/index.html @@ -0,0 +1,85 @@ + + + + + + ColorGrid โ€” 2D Cube Puzzle + + + +
+
+

ColorGrid

+

Rotate rows & columns to match all colors.

+
+ +
+
+ + + + + + + +
+ +
+
+ +
+ + + + +
+ + + +
+ +
+ + + + +
+ +
+
Moves: 0
+
Time: 00:00
+
+
+
+ +
+
+
+ +
+

Tip: Select Row or Column, set the Index (1-based), then use Rotate buttons. Shuffle to scramble, Undo to revert last move.

+

Accessible & color-blind friendly palette included (tile labels show short ids).

+
+ + + +
Made with โค๏ธ โ€” ColorGrid
+
+ + + + diff --git a/games/ColorSpin/script.js b/games/ColorSpin/script.js new file mode 100644 index 00000000..4e5f0abb --- /dev/null +++ b/games/ColorSpin/script.js @@ -0,0 +1,289 @@ +// ColorGrid - simple 2D rotate puzzle +(() => { + const boardEl = document.getElementById('board'); + const sizeSelect = document.getElementById('size'); + const newGameBtn = document.getElementById('newGame'); + const shuffleBtn = document.getElementById('shuffleBtn'); + const undoBtn = document.getElementById('undoBtn'); + const movesEl = document.getElementById('moves'); + const timeEl = document.getElementById('time'); + const selRowRadio = document.getElementById('selRow'); + const indexInput = document.getElementById('indexInput'); + const leftBtn = document.getElementById('leftBtn'); + const rightBtn = document.getElementById('rightBtn'); + const upBtn = document.getElementById('upBtn'); + const downBtn = document.getElementById('downBtn'); + const modal = document.getElementById('modal'); + const modalText = document.getElementById('modalText'); + const modalClose = document.getElementById('modalClose'); + const modalNew = document.getElementById('modalNew'); + const solveDemo = document.getElementById('solveDemo'); + + let size = parseInt(sizeSelect.value, 10); + let matrix = []; // current colors + let solvedMatrix = []; + const palette = [ + '#F6B042', // amber + '#4FC3F7', // light blue + '#A3E635', // lime + '#F472B6', // pink + '#8B5CF6', // violet + '#34D399', // teal + '#F87171', // red + '#60A5FA' // blue + ]; + let moves = 0; + let timerInterval = null; + let startTime = null; + let undoStack = []; + + function init() { + size = parseInt(sizeSelect.value, 10); + indexInput.max = size; + indexInput.value = 1; + moves = 0; + movesEl.textContent = moves; + resetTimer(); + undoStack = []; + undoBtn.disabled = true; + + // build solved matrix: each row gets a unique color (wrap if needed) + matrix = Array.from({length: size}, (_, r) => + Array.from({length: size}, (_, c) => palette[r % palette.length]) + ); + solvedMatrix = matrix.map(row => row.slice()); + renderGrid(); + } + + function startTimer() { + if (timerInterval) return; + startTime = Date.now(); + timerInterval = setInterval(() => { + const diff = Date.now() - startTime; + timeEl.textContent = formatTime(diff); + }, 500); + } + + function resetTimer() { + if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } + timeEl.textContent = '00:00'; + startTime = null; + } + + function formatTime(ms){ + const s = Math.floor(ms/1000); + const mm = Math.floor(s/60).toString().padStart(2,'0'); + const ss = (s%60).toString().padStart(2,'0'); + return `${mm}:${ss}`; + } + + function renderGrid() { + // CSS grid columns + boardEl.style.gridTemplateColumns = `repeat(${size}, var(--size))`; + // clear + boardEl.innerHTML = ''; + for (let r=0; r { + if (selRowRadio.checked) { + indexInput.value = r+1; + } else { + indexInput.value = c+1; + } + }); + + boardEl.appendChild(tile); + } + } + } + + function pushUndo() { + // deep copy + undoStack.push(matrix.map(row => row.slice())); + undoBtn.disabled = false; + if (undoStack.length > 100) undoStack.shift(); + } + + function undo() { + if (!undoStack.length) return; + matrix = undoStack.pop(); + renderGrid(); + moves = Math.max(0, moves-1); + movesEl.textContent = moves; + if (!undoStack.length) undoBtn.disabled = true; + } + + function rotateRow(rowIndex, dir) { + // dir: +1 = right, -1 = left + pushUndo(); + startTimer(); + const r = rowIndex; + const newRow = matrix[r].slice(); + for (let c=0;c${moves}
Time: ${time}`; + modal.classList.remove('hidden'); + } + } + + function shuffle(times=30) { + // random rotations to scramble while preserving solvability + pushUndo(); + startTimer(); + for (let i=0;i { + init(); + }); + sizeSelect.addEventListener('change', () => init()); + + leftBtn.addEventListener('click', () => { + const idx = parseInt(indexInput.value,10)-1; + if (isNaN(idx) || idx < 0 || idx >= size) return alert('Index out of range'); + // if Row selected -> rotate left, else rotate top-to-bottom? We'll treat left/right as row rotates + rotateRow(idx, -1); + }); + rightBtn.addEventListener('click', () => { + const idx = parseInt(indexInput.value,10)-1; + if (isNaN(idx) || idx < 0 || idx >= size) return alert('Index out of range'); + rotateRow(idx, +1); + }); + upBtn.addEventListener('click', () => { + const idx = parseInt(indexInput.value,10)-1; + if (isNaN(idx) || idx < 0 || idx >= size) return alert('Index out of range'); + rotateCol(idx, -1); + }); + downBtn.addEventListener('click', () => { + const idx = parseInt(indexInput.value,10)-1; + if (isNaN(idx) || idx < 0 || idx >= size) return alert('Index out of range'); + rotateCol(idx, +1); + }); + + shuffleBtn.addEventListener('click', () => shuffle(size * 20)); + undoBtn.addEventListener('click', undo); + + modalClose.addEventListener('click', () => { + modal.classList.add('hidden'); + }); + modalNew.addEventListener('click', () => { + modal.classList.add('hidden'); + init(); + }); + + solveDemo.addEventListener('click', async () => { + // very simple demo: perform few random reverse moves to "demo" rotations + // This is not an actual solver; it's just a visual demo of rotations. + const steps = 8; + for (let i=0;i setTimeout(res, ms)); } + + // keyboard shortcuts for quick play + document.addEventListener('keydown', (e) => { + if (e.key === 'z' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); undo(); } + }); + + // initialize + init(); +})(); diff --git a/games/ColorSpin/style.css b/games/ColorSpin/style.css new file mode 100644 index 00000000..c7cacfac --- /dev/null +++ b/games/ColorSpin/style.css @@ -0,0 +1,59 @@ +:root{ + --bg:#0f1724; + --card:#0b1220; + --accent:#7c3aed; + --panel:#0b1522; + --text:#e6eef8; + --muted:#9fb0c8; + --size:64px; + --gap:8px; +} + +/* layout */ +*{box-sizing:border-box;font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial;} +body{margin:0;background:linear-gradient(180deg,#071022,var(--bg));color:var(--text);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:28px;} +.app{width:100%;max-width:980px} +header{display:flex;align-items:flex-start;gap:16px;flex-direction:column;margin-bottom:14px} +h1{margin:0;font-size:1.8rem} +.subtitle{margin:0;color:var(--muted)} + +.controls{background:var(--panel);padding:12px;border-radius:10px;display:flex;flex-direction:column;gap:10px;margin-bottom:16px} +.controls .row{display:flex;gap:12px;align-items:center;flex-wrap:wrap} +.controls label{font-size:0.9rem;color:var(--muted)} +.controls select,input,button{padding:8px 10px;border-radius:8px;border:1px solid rgba(255,255,255,0.06);background:transparent;color:var(--text);outline:none} +.controls button{cursor:pointer} +button:disabled{opacity:0.5;cursor:default} + +.board-wrap{display:flex;justify-content:center;align-items:center;margin-bottom:12px} +.board{display:grid;gap:var(--gap);background:rgba(255,255,255,0.02);padding:12px;border-radius:12px;box-shadow:0 6px 18px rgba(2,6,23,0.6)} + +.tile{ + width:var(--size);height:var(--size);display:flex;align-items:center;justify-content:center;border-radius:8px; + font-weight:700;color:rgba(0,0,0,0.7);user-select:none; + transition:transform .18s ease, box-shadow .18s; + box-shadow: 0 4px 10px rgba(2,6,23,0.5); + border:2px solid rgba(0,0,0,0.12); +} + +/* accessible labels small */ +.tile .label{font-size:0.78rem;color:rgba(0,0,0,0.55)} + +.status{margin-left:auto;color:var(--muted);display:flex;gap:12px;align-items:center} +.rotate-controls{display:flex;align-items:center;gap:8px} +.rotate-buttons{display:flex;gap:6px} +.notes{color:var(--muted);font-size:0.9rem;margin-top:6px} + +footer{color:var(--muted);font-size:0.8rem;margin-top:12px;text-align:center} + +/* modal */ +.modal{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(2,6,23,0.6)} +.hidden{display:none} +.modal-content{background:var(--card);padding:20px;border-radius:12px;min-width:260px;text-align:center} +.modal-actions{display:flex;gap:8px;justify-content:center;margin-top:12px} + +/* responsive sizes */ +@media (max-width:640px){ + :root{--size:52px} + .controls .row{flex-direction:column;align-items:stretch} + .status{margin-left:0;justify-content:space-between} +} diff --git a/games/Connect-four/index.html b/games/Connect-four/index.html new file mode 100644 index 00000000..17f06f8a --- /dev/null +++ b/games/Connect-four/index.html @@ -0,0 +1,23 @@ + + + + + + Connect Four + + + +
+

Connect Four

+ +
+ + + + + diff --git a/games/Connect-four/script.js b/games/Connect-four/script.js new file mode 100644 index 00000000..0d157a36 --- /dev/null +++ b/games/Connect-four/script.js @@ -0,0 +1,150 @@ +document.addEventListener('DOMContentLoaded', () => { + const ROWS = 6; + const COLS = 7; + const PLAYER_RED = 'red'; + const PLAYER_YELLOW = 'yellow'; + const startGameBtn = document.getElementById('start-game-btn'); + const startScreen = document.getElementById('start-screen'); + const gameArea = document.getElementById('game-area'); + const gameBoard = document.getElementById('game-board'); + const gameStatusEl = document.getElementById('game-status'); + const playAgainBtn = document.getElementById('play-again-btn'); + + let board = []; + let currentPlayer = PLAYER_RED; + let gameOver = false; + + function init() { + gameOver = false; + currentPlayer = PLAYER_RED; + playAgainBtn.classList.add('hidden'); + gameBoard.style.cursor = 'pointer'; + createBoard(); + updateGameStatus(); + } + + startGameBtn.addEventListener('click', () => { + startScreen.classList.add('hidden'); + gameArea.classList.remove('hidden'); + init(); + }); + + function createBoard() { + gameBoard.innerHTML = ''; + board = []; + for (let c = 0; c < COLS; c++) { + board.push(Array(ROWS).fill(null)); + } + for (let r = 0; r < ROWS; r++) { + for (let c = 0; c < COLS; c++) { + const slot = document.createElement('div'); + slot.classList.add('slot'); + slot.dataset.col = c; + slot.dataset.row = (ROWS - 1) - r; + gameBoard.appendChild(slot); + } + } + } + + gameBoard.addEventListener('click', handleBoardClick); + gameBoard.addEventListener('mouseover', handleMouseOver); + gameBoard.addEventListener('mouseout', handleMouseOut); + playAgainBtn.addEventListener('click', init); + + function handleMouseOver(e) { + if (gameOver) return; + const col = e.target.closest('.slot')?.dataset.col; + if (col) { + document.querySelectorAll(`.slot[data-col='${col}']`).forEach(slot => { + slot.classList.add('column-hover'); + }); + } + } + + function handleMouseOut(e) { + const col = e.target.closest('.slot')?.dataset.col; + if (col) { + document.querySelectorAll(`.slot[data-col='${col}']`).forEach(slot => { + slot.classList.remove('column-hover'); + }); + } + } + + function handleBoardClick(e) { + if (gameOver) return; + const col = parseInt(e.target.closest('.slot')?.dataset.col); + if (isNaN(col)) return; + const row = board[col].findIndex(slot => slot === null); + + if (row === -1) { + // Column is full โ€” show feedback + indicateFullColumn(col); + return; + } + + board[col][row] = currentPlayer; + dropPiece(col, row, currentPlayer); + if (checkForWin(col, row)) { + endGame(`${currentPlayer.charAt(0).toUpperCase() + currentPlayer.slice(1)} wins!`); + } else if (board.flat().every(slot => slot !== null)) { + endGame("It's a tie!"); + } else { + switchPlayer(); + } + } + + + function dropPiece(col, row, player) { + const slot = document.querySelector(`.slot[data-col='${col}'][data-row='${row}']`); + const piece = document.createElement('div'); + piece.classList.add('piece', player); + slot.appendChild(piece); + } + + function switchPlayer() { + currentPlayer = (currentPlayer === PLAYER_RED) ? PLAYER_YELLOW : PLAYER_RED; + updateGameStatus(); + } + + function updateGameStatus() { + gameStatusEl.textContent = `${currentPlayer.charAt(0).toUpperCase() + currentPlayer.slice(1)}'s Turn`; + gameStatusEl.style.color = currentPlayer === PLAYER_RED ? '#ef4444' : '#facc15'; + } + + function checkForWin(col, row) { + const player = board[col][row]; + + function checkDirection(dc, dr) { + let count = 0; + for (let i = -3; i <= 3; i++) { + const c = col + i * dc; + const r = row + i * dr; + if (c >= 0 && c < COLS && r >= 0 && r < ROWS && board[c][r] === player) { + count++; + if (count === 4) return true; + } else { + count = 0; + } + } + return false; + } + return checkDirection(1, 0) || checkDirection(0, 1) || checkDirection(1, 1) || checkDirection(1, -1); + } + + function endGame(message) { + gameOver = true; + gameStatusEl.textContent = message; + gameStatusEl.style.color = '#ffffff'; + playAgainBtn.classList.remove('hidden'); + gameBoard.style.cursor = 'not-allowed'; + } + function indicateFullColumn(col) { + const slots = document.querySelectorAll(`.slot[data-col='${col}']`); + slots.forEach(slot => slot.classList.add('full-column-warning')); + setTimeout(() => { + slots.forEach(slot => slot.classList.remove('full-column-warning')); + }, 500); + } + +}); + diff --git a/games/Connect-four/style.css b/games/Connect-four/style.css new file mode 100644 index 00000000..232780f6 --- /dev/null +++ b/games/Connect-four/style.css @@ -0,0 +1,125 @@ +body{ + font-family: Arial, Helvetica, sans-serif; + background-color: #111827; + color: #ffffff; + margin: 0; + padding: 1rem; + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + box-sizing: border-box; +} +h1{ + font-size: 3.5rem; + font-weight: 700; + margin-bottom: 2rem; +} +h2{ + font-size: 2.5rem; + font-weight: 700; + margin-top: 0; + margin-bottom: 0.5rem; +} +.screen{ + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + width: 100%; + justify-content: center; +} +.hidden{ + display: none; +} +#game-status{ + height: 2rem; + font-size: 1.25rem; + font-weight: 500; + margin-bottom: 1rem; +} +.btn{ + border: none; + border-radius: 0.5rem; + font-weight: 600; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); +} +.btn:hover{ + transform: scale(1.05); + box-shadow: 0 6px 12px rgba(0,0,0,0.2); +} +.btn-start{ + font-size: 1.5rem; + padding: 1rem 2rem; + background-color: #4f46e5; + color: white; +} +.btn-start:hover{ + background-color: #4338ca; +} +.btn-play-again{ + font-size: 1rem; + padding: 0.75rem 1.5rem; + margin-top: 1rem; + background-color: #16a34a; + color: white; +} +.btn-play-again:hover{ + background-color: #15803d; +} +#game-board{ + display: grid; + grid-template-columns: repeat(7, 1fr); + grid-template-rows: repeat(6, 1fr); + gap: 10px; + background-color: #0d6efd; + padding: 12px; + border-radius: 16px; + box-shadow: 0 10px 20px rgba(0,0,0,0.2), 0 6px 6px rgba(0,0,0,0.23); + aspect-ratio: 7 / 6; + width: 100%; + max-width: 720px; + max-height: 70vh; + margin: 0 auto; +} +.slot{ + background-color: #1f2937; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + position: relative; + transition: background-color 0.2s ease-in-out; +} +.slot .piece{ + width: 85%; + height: 85%; + border-radius: 50%; + transform: scale(0); + transition: transform 0.3s ease-out, background-color 0.3s ease; +} +.slot .piece.red{ + background-color: #ef4444; + transform: scale(1); + box-shadow: inset 0 -4px 6px rgba(0, 0, 0, 0.3); +} +.slot .piece.yellow{ + background-color: #facc15; + transform: scale(1); + box-shadow: inset 0 -4px 6px rgba(0, 0, 0, 0.3); +} +.column-hover{ + background-color: #374151; +} +.full-column-warning { + animation: shake 0.3s; + border: 2px solid #f87171; /* Red border as warning */ +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 20%, 60% { transform: translateX(-5px); } + 40%, 80% { transform: translateX(5px); } +} + + diff --git a/games/Crystal Drift/index.html b/games/Crystal Drift/index.html new file mode 100644 index 00000000..d5fbf426 --- /dev/null +++ b/games/Crystal Drift/index.html @@ -0,0 +1,31 @@ + + + + + + Crystal Drift + + + +
+

โ„๏ธ Crystal Drift

+

Catch the snowflakes!

+ +
+
+
+ +
+ Score: 0 + Missed: 0 +
+ +
+ + +
+
+ + + + diff --git a/games/Crystal Drift/script.js b/games/Crystal Drift/script.js new file mode 100644 index 00000000..11703056 --- /dev/null +++ b/games/Crystal Drift/script.js @@ -0,0 +1,94 @@ +const basket = document.getElementById("basket"); +const gameArea = document.getElementById("gameArea"); +const scoreEl = document.getElementById("score"); +const missedEl = document.getElementById("missed"); +const startBtn = document.getElementById("startBtn"); +const restartBtn = document.getElementById("restartBtn"); +const statusEl = document.getElementById("status"); + +let score = 0; +let missed = 0; +let gameInterval; +let flakeInterval; +let speed = 2; +let gameActive = false; + +document.addEventListener("keydown", moveBasket); +startBtn.addEventListener("click", startGame); +restartBtn.addEventListener("click", restartGame); + +function startGame() { + if (gameActive) return; + gameActive = true; + statusEl.textContent = "Catch the snowflakes!"; + startBtn.disabled = true; + restartBtn.disabled = true; + flakeInterval = setInterval(createFlake, 700); + gameInterval = requestAnimationFrame(updateFlakes); +} + +function restartGame() { + score = 0; + missed = 0; + speed = 2; + scoreEl.textContent = 0; + missedEl.textContent = 0; + document.querySelectorAll(".flake").forEach(f => f.remove()); + startGame(); +} + +function moveBasket(e) { + if (!gameActive) return; + const pos = basket.offsetLeft; + if (e.key === "ArrowLeft" && pos > 0) basket.style.left = pos - 20 + "px"; + if (e.key === "ArrowRight" && pos < gameArea.offsetWidth - basket.offsetWidth) + basket.style.left = pos + 20 + "px"; +} + +function createFlake() { + const flake = document.createElement("div"); + flake.classList.add("flake"); + flake.style.left = Math.random() * (gameArea.offsetWidth - 20) + "px"; + gameArea.appendChild(flake); +} + +function updateFlakes() { + if (!gameActive) return; + + const flakes = document.querySelectorAll(".flake"); + flakes.forEach(flake => { + const top = parseFloat(flake.style.top || 0); + flake.style.top = top + speed + "px"; + + const flakeRect = flake.getBoundingClientRect(); + const basketRect = basket.getBoundingClientRect(); + + if ( + flakeRect.bottom >= basketRect.top && + flakeRect.left >= basketRect.left && + flakeRect.right <= basketRect.right + ) { + flake.remove(); + score += 10; + scoreEl.textContent = score; + + if (score % 50 === 0) speed += 0.5; + } else if (top > gameArea.offsetHeight) { + flake.remove(); + missed += 1; + missedEl.textContent = missed; + if (missed >= 5) endGame(); + } + }); + + gameInterval = requestAnimationFrame(updateFlakes); +} + +function endGame() { + cancelAnimationFrame(gameInterval); + clearInterval(flakeInterval); + gameActive = false; + statusEl.textContent = "โŒ Game Over โ€” You missed too many!"; + restartBtn.disabled = false; + startBtn.disabled = false; +} diff --git a/games/Crystal Drift/style.css b/games/Crystal Drift/style.css new file mode 100644 index 00000000..1c21091e --- /dev/null +++ b/games/Crystal Drift/style.css @@ -0,0 +1,91 @@ +:root { + --bg: radial-gradient(circle at center, #0f172a, #020617); + --flake: #bae6fd; + --basket: #38bdf8; + --accent: #93c5fd; + font-family: 'Poppins', sans-serif; +} + +body { + background: var(--bg); + color: var(--accent); + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + margin: 0; +} + +.game-container { + text-align: center; + max-width: 400px; +} + +h1 { + font-size: 2em; + color: var(--flake); + margin-bottom: 10px; +} + +#status { + color: #a1a1aa; + margin-bottom: 15px; +} + +#gameArea { + position: relative; + width: 320px; + height: 400px; + border: 2px solid var(--accent); + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; + overflow: hidden; + margin: 0 auto 15px; +} + +#basket { + position: absolute; + bottom: 10px; + left: 140px; + width: 50px; + height: 20px; + background: var(--basket); + border-radius: 6px; + box-shadow: 0 0 15px var(--basket); +} + +.flake { + position: absolute; + top: 0; + width: 20px; + height: 20px; + border-radius: 50%; + background: var(--flake); + box-shadow: 0 0 10px var(--flake); + opacity: 0.9; +} + +.controls button { + margin: 5px; + padding: 10px 16px; + font-size: 1rem; + border: none; + border-radius: 8px; + background: linear-gradient(90deg, #38bdf8, #a78bfa); + color: #06121f; + font-weight: 600; + cursor: pointer; +} + +.controls button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.info { + margin: 10px 0; + display: flex; + justify-content: space-around; + color: var(--accent); + font-size: 1.1em; +} diff --git a/games/Dodge-the-blocks/index.html b/games/Dodge-the-blocks/index.html new file mode 100644 index 00000000..a25a5e0f --- /dev/null +++ b/games/Dodge-the-blocks/index.html @@ -0,0 +1,17 @@ + + + + + + Dodge the Blocks + + + +

Dodge the Blocks!

+
+
+
+

Score: 0

+ + + diff --git a/games/Dodge-the-blocks/readme.md b/games/Dodge-the-blocks/readme.md new file mode 100644 index 00000000..f4b47b9d --- /dev/null +++ b/games/Dodge-the-blocks/readme.md @@ -0,0 +1,67 @@ +# Dodge the Blocks + +## Game Details +**Name:** Dodge the Blocks +**Description:** +*Dodge the Blocks* is a fun, browser-based arcade game where you control a green square and try to dodge falling red blocks. Every block you successfully avoid increases your score by one point. The game ends when a block collides with your player โ€” how long can you survive? + +--- + +## Files Included +- [x] **index.html** + - Sets up the main structure of the game. + - Links the CSS and JavaScript files. + - Includes the game container, player, and score display. + +- [x] **style.css** + - Defines the gameโ€™s look and layout. + - Styles the player, blocks, and game area. + - Uses a clean dark theme for contrast and readability. + +- [x] **script.js** + - Contains the gameโ€™s functionality and logic. + - Handles player movement via arrow keys. + - Spawns falling blocks at random positions. + - Detects collisions and manages the score system. + +--- + +## Additional Notes + +### **Controls** +- Press **Left Arrow (โ†)** to move left. +- Press **Right Arrow (โ†’)** to move right. + +### **Gameplay Mechanics** +- Red blocks fall continuously from the top of the game area. +- Each block avoided adds **+1** to your score. +- The game automatically restarts after a **Game Over** alert. + +### **Customization Ideas** +- Change the `fallSpeed` variable in **script.js** to adjust difficulty. +- Modify `setInterval(createBlock, 800)` to control block spawn rate. +- Add sound effects, animations, or a restart button for a polished experience. + +--- + +## How to Play +1. Open **index.html** in your web browser. +2. Use the arrow keys to dodge falling red blocks. +3. Try to get the highest score possible! + +--- + +## Built With +- **HTML5** โ€” Structure +- **CSS3** โ€” Styling +- **JavaScript (ES6)** โ€” Game Logic + +--- + +## Author Notes +This project is a great starting point for learning basic **game development with JavaScript**. +You can expand it by adding new features like multiple block types, increasing speed, or even a leaderboard! + +--- + +โœจ *Have fun dodging those blocks!* diff --git a/games/Dodge-the-blocks/script.js b/games/Dodge-the-blocks/script.js new file mode 100644 index 00000000..6c82f23d --- /dev/null +++ b/games/Dodge-the-blocks/script.js @@ -0,0 +1,57 @@ +const game = document.getElementById("gameContainer"); +const player = document.getElementById("player"); +const scoreDisplay = document.getElementById("score"); + +let playerX = 180; +let score = 0; +let gameOver = false; + +document.addEventListener("keydown", (e) => { + if (gameOver) return; + if (e.key === "ArrowLeft" && playerX > 0) playerX -= 20; + if (e.key === "ArrowRight" && playerX < 360) playerX += 20; + player.style.left = playerX + "px"; +}); + +function createBlock() { + if (gameOver) return; + const block = document.createElement("div"); + block.classList.add("block"); + block.style.left = Math.floor(Math.random() * 10) * 40 + "px"; + game.appendChild(block); + + let blockY = 0; + const fallSpeed = 4; + + function moveBlock() { + if (gameOver) { + block.remove(); + return; + } + + blockY += fallSpeed; + block.style.top = blockY + "px"; + + // collision detection + if ( + blockY + 40 >= 560 && + Math.abs(parseInt(block.style.left) - playerX) < 40 + ) { + gameOver = true; + alert("๐Ÿ’€ Game Over! Your score: " + score); + location.reload(); + } + + if (blockY > 600) { + block.remove(); + score++; + scoreDisplay.textContent = "Score: " + score; + } else { + requestAnimationFrame(moveBlock); + } + } + + requestAnimationFrame(moveBlock); +} + +setInterval(createBlock, 800); diff --git a/games/Dodge-the-blocks/style.css b/games/Dodge-the-blocks/style.css new file mode 100644 index 00000000..496f8697 --- /dev/null +++ b/games/Dodge-the-blocks/style.css @@ -0,0 +1,39 @@ +body { + margin: 0; + font-family: Arial, sans-serif; + background-color: #222; + color: white; + text-align: center; +} + +#gameContainer { + position: relative; + margin: 20px auto; + width: 400px; + height: 600px; + background-color: #333; + border: 2px solid #fff; + overflow: hidden; +} + +#player { + position: absolute; + bottom: 20px; + left: 180px; + width: 40px; + height: 40px; + background-color: #00ff99; + border-radius: 4px; +} + +.block { + position: absolute; + top: 0; + width: 40px; + height: 40px; + background-color: #ff4444; +} + +#score { + font-size: 20px; +} diff --git a/games/EchoBeats/index.html b/games/EchoBeats/index.html new file mode 100644 index 00000000..de936b26 --- /dev/null +++ b/games/EchoBeats/index.html @@ -0,0 +1,36 @@ + + + + + + Echo Beats ๐ŸŽต + + + +
+

๐ŸŽถ Echo Beats ๐ŸŽถ

+

Press Start to begin!

+ +
+
+
+
+
+
+ + + +
+

Level: 0

+

Score: 0

+
+
+ + + + + + + + + diff --git a/games/EchoBeats/script.js b/games/EchoBeats/script.js new file mode 100644 index 00000000..d593e107 --- /dev/null +++ b/games/EchoBeats/script.js @@ -0,0 +1,82 @@ +const pads = document.querySelectorAll('.pad'); +const startBtn = document.getElementById('start-btn'); +const statusText = document.getElementById('status'); +const levelText = document.getElementById('level'); +const scoreText = document.getElementById('score'); + +let sequence = []; +let playerSequence = []; +let level = 0; +let score = 0; +let canClick = false; + +function playSound(id) { + const sound = document.getElementById(id); + sound.currentTime = 0; + sound.play(); +} + +function flashPad(pad) { + pad.classList.add('active'); + playSound(pad.dataset.sound); + setTimeout(() => pad.classList.remove('active'), 500); +} + +function nextSequence() { + playerSequence = []; + canClick = false; + level++; + levelText.textContent = level; + statusText.textContent = "Watch closely..."; + + const randomPad = pads[Math.floor(Math.random() * pads.length)]; + sequence.push(randomPad); + + let i = 0; + const interval = setInterval(() => { + flashPad(sequence[i]); + i++; + if (i >= sequence.length) { + clearInterval(interval); + canClick = true; + statusText.textContent = "Now repeat the pattern!"; + } + }, 800); +} + +pads.forEach(pad => { + pad.addEventListener('click', () => { + if (!canClick) return; + + flashPad(pad); + playerSequence.push(pad); + + const index = playerSequence.length - 1; + if (playerSequence[index] !== sequence[index]) { + statusText.textContent = "โŒ Wrong! Game Over!"; + canClick = false; + sequence = []; + level = 0; + score = 0; + levelText.textContent = "0"; + scoreText.textContent = "0"; + return; + } + + if (playerSequence.length === sequence.length) { + score += level * 10; + scoreText.textContent = score; + setTimeout(nextSequence, 1000); + } + }); +}); + +startBtn.addEventListener('click', () => { + sequence = []; + level = 0; + score = 0; + scoreText.textContent = "0"; + levelText.textContent = "0"; + statusText.textContent = "Get ready..."; + setTimeout(nextSequence,ย 1000); +}); diff --git a/games/EchoBeats/style.css b/games/EchoBeats/style.css new file mode 100644 index 00000000..cd37a815 --- /dev/null +++ b/games/EchoBeats/style.css @@ -0,0 +1,75 @@ +body { + margin: 0; + height: 100vh; + font-family: 'Poppins', sans-serif; + background: radial-gradient(circle, #1a1a2e, #0f0c29); + display: flex; + justify-content: center; + align-items: center; + color: white; + overflow: hidden; +} + +.game-container { + text-align: center; +} + +h1 { + font-size: 2.5em; + color: #ffcc70; + text-shadow: 0 0 20px #ffcc70; +} + +#status { + font-size: 1.2em; + margin-bottom: 20px; + color: #ddd; +} + +.pad-container { + display: grid; + grid-template-columns: repeat(2, 150px); + grid-gap: 20px; + justify-content: center; + margin: 30px auto; +} + +.pad { + width: 150px; + height: 150px; + border-radius: 20px; + cursor: pointer; + transition: transform 0.1s ease, box-shadow 0.2s; + box-shadow: 0 0 20px rgba(255, 255, 255, 0.2); +} + +#pad1 { background: linear-gradient(135deg, #ff6a00, #ee0979); } +#pad2 { background: linear-gradient(135deg, #24c6dc, #514a9d); } +#pad3 { background: linear-gradient(135deg, #00b09b, #96c93d); } +#pad4 { background: linear-gradient(135deg, #fc5c7d, #6a82fb); } + +.pad.active { + transform: scale(1.1); + box-shadow: 0 0 40px #fff; +} + +#start-btn { + background: linear-gradient(45deg, #f7971e, #ffd200); + color: black; + border: none; + padding: 12px 25px; + font-size: 1.2em; + border-radius: 30px; + cursor: pointer; + transition: 0.3s; +} + +#start-btn:hover { + background: linear-gradient(45deg, #ffb347, #ffcc33); + transform: scale(1.05); +} + +.info { + margin-top: 20px; + font-size:ย 1.2em; +} diff --git a/games/Emoji Match Game/index.html b/games/Emoji Match Game/index.html new file mode 100644 index 00000000..926b6cfc --- /dev/null +++ b/games/Emoji Match Game/index.html @@ -0,0 +1,22 @@ + + + + + + Emoji Match Game | Mini JS Games Hub + + + +
+

Emoji Match Game

+
+

Moves: 0

+

Time: 00:00

+ +
+
+
+ + + + diff --git a/games/Emoji Match Game/script.js b/games/Emoji Match Game/script.js new file mode 100644 index 00000000..601e17da --- /dev/null +++ b/games/Emoji Match Game/script.js @@ -0,0 +1,102 @@ +const grid = document.getElementById("grid"); +const movesEl = document.getElementById("moves"); +const timerEl = document.getElementById("timer"); +const restartBtn = document.getElementById("restart-btn"); + +let moves = 0; +let time = 0; +let timerInterval; +let flippedCards = []; +let matchedCount = 0; + +// Emoji deck +const emojis = ["๐ŸŽ","๐ŸŒ","๐Ÿ‡","๐Ÿ’","๐Ÿ‰","๐Ÿ","๐Ÿฅ","๐Ÿ‘"]; +let deck = [...emojis, ...emojis]; // duplicate for pairs + +// Shuffle function +function shuffle(array) { + return array.sort(() => Math.random() - 0.5); +} + +// Create cards +function createGrid() { + grid.innerHTML = ""; + shuffle(deck).forEach((emoji) => { + const card = document.createElement("div"); + card.classList.add("card"); + card.innerHTML = ` +
${emoji}
+
โ“
+ `; + card.addEventListener("click", flipCard); + grid.appendChild(card); + }); +} + +// Flip logic +function flipCard(e) { + const card = e.currentTarget; + if (card.classList.contains("flipped") || flippedCards.length === 2) return; + + card.classList.add("flipped"); + flippedCards.push(card); + + if (flippedCards.length === 2) { + moves++; + movesEl.textContent = moves; + checkMatch(); + } +} + +// Check for match +function checkMatch() { + const [first, second] = flippedCards; + const firstEmoji = first.querySelector(".front").textContent; + const secondEmoji = second.querySelector(".front").textContent; + + if (firstEmoji === secondEmoji) { + matchedCount++; + flippedCards = []; + if (matchedCount === emojis.length) { + clearInterval(timerInterval); + setTimeout(() => alert(`๐ŸŽ‰ You won in ${moves} moves and ${formatTime(time)}!`), 200); + } + } else { + setTimeout(() => { + first.classList.remove("flipped"); + second.classList.remove("flipped"); + flippedCards = []; + }, 800); + } +} + +// Timer +function startTimer() { + clearInterval(timerInterval); + time = 0; + timerInterval = setInterval(() => { + time++; + timerEl.textContent = formatTime(time); + }, 1000); +} + +// Format time +function formatTime(seconds) { + const mins = Math.floor(seconds / 60).toString().padStart(2,"0"); + const secs = (seconds % 60).toString().padStart(2,"0"); + return `${mins}:${secs}`; +} + +// Restart +restartBtn.addEventListener("click", () => { + moves = 0; + movesEl.textContent = moves; + matchedCount = 0; + flippedCards = []; + createGrid(); + startTimer(); +}); + +// Initial setup +createGrid(); +startTimer(); diff --git a/games/Emoji Match Game/style.css b/games/Emoji Match Game/style.css new file mode 100644 index 00000000..40de8056 --- /dev/null +++ b/games/Emoji Match Game/style.css @@ -0,0 +1,98 @@ +/* General */ +body { + font-family: 'Segoe UI', sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background: linear-gradient(120deg, #f093fb, #f5576c); + margin: 0; + padding: 0; +} + +.game-container { + background: #fff; + padding: 20px 30px; + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0,0,0,0.2); + text-align: center; + max-width: 500px; + width: 100%; +} + +h1 { + margin-bottom: 15px; +} + +.game-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +button { + padding: 6px 12px; + border: none; + border-radius: 6px; + background-color: #ff6f91; + color: #fff; + font-size: 14px; + cursor: pointer; + transition: 0.2s; +} + +button:hover { + background-color: #ff4c6d; +} + +/* Grid */ +.grid { + display: grid; + grid-template-columns: repeat(4, 70px); + grid-gap: 10px; + justify-content: center; +} + +/* Cards */ +.card { + width: 70px; + height: 70px; + background-color: #f0f0f0; + border-radius: 8px; + display: flex; + justify-content: center; + align-items: center; + font-size: 32px; + cursor: pointer; + position: relative; + transform-style: preserve-3d; + transition: transform 0.4s; +} + +.card.flipped { + transform: rotateY(180deg); +} + +.card .front, +.card .back { + position: absolute; + width: 100%; + height: 100%; + backface-visibility: hidden; + display: flex; + justify-content: center; + align-items: center; + border-radius: 8px; +} + +.card .front { + background-color: #ff6f91; + color: #fff; + transform: rotateY(180deg); +} + +.card .back { + background-color: #f0f0f0; + font-size: 32px; +} diff --git a/games/Emoji Pop Quiz/index.html b/games/Emoji Pop Quiz/index.html new file mode 100644 index 00000000..83c7cad8 --- /dev/null +++ b/games/Emoji Pop Quiz/index.html @@ -0,0 +1,33 @@ + + + + + + Emoji Pop Quiz ๐ŸŽฏ + + + +
+
+

Emoji Pop Quiz ๐ŸŽ‰

+

Guess the word, movie, or phrase from the emojis!

+
+ +
+
๐Ÿฟ๐ŸŽฌ๐Ÿ‘‘
+ + + +
+ + + + +
+ + + + diff --git a/games/Emoji Pop Quiz/script.js b/games/Emoji Pop Quiz/script.js new file mode 100644 index 00000000..27fb3fc8 --- /dev/null +++ b/games/Emoji Pop Quiz/script.js @@ -0,0 +1,83 @@ +const quizData = [ + { emoji: "๐Ÿฟ๐ŸŽฌ๐Ÿ‘‘", answer: "movie" }, + { emoji: "๐Ÿ๐Ÿ–ฅ๏ธ", answer: "python" }, + { emoji: "๐Ÿ‘ธโ„๏ธ", answer: "frozen" }, + { emoji: "๐ŸŽ๐Ÿ“ฑ", answer: "apple" }, + { emoji: "๐Ÿฆ๐Ÿ‘‘", answer: "lion king" }, + { emoji: "๐ŸŒง๏ธโ˜”", answer: "rain" }, + { emoji: "๐Ÿ’ก๐Ÿ“", answer: "idea" }, +]; + +let currentQuestion = 0; +let score = 0; + +const emojiDisplay = document.getElementById("emoji-display"); +const input = document.getElementById("answer-input"); +const submitBtn = document.getElementById("submit-btn"); +const feedback = document.getElementById("feedback"); +const scoreEl = document.getElementById("score"); +const questionNumberEl = document.getElementById("question-number"); +const totalQuestionsEl = document.getElementById("total-questions"); +const restartBtn = document.getElementById("restart-btn"); + +// Initialize quiz +totalQuestionsEl.textContent = quizData.length; + +function loadQuestion() { + if (currentQuestion < quizData.length) { + emojiDisplay.textContent = quizData[currentQuestion].emoji; + questionNumberEl.textContent = currentQuestion + 1; + input.value = ""; + feedback.textContent = ""; + input.focus(); + } else { + endQuiz(); + } +} + +function checkAnswer() { + const userAnswer = input.value.trim().toLowerCase(); + const correctAnswer = quizData[currentQuestion].answer.toLowerCase(); + + if (!userAnswer) return; + + if (userAnswer === correctAnswer) { + score++; + feedback.textContent = "โœ… Correct!"; + feedback.style.color = "#00ff00"; + } else { + feedback.textContent = `โŒ Wrong! Correct: ${quizData[currentQuestion].answer}`; + feedback.style.color = "#ff4c4c"; + } + + scoreEl.textContent = score; + currentQuestion++; + + setTimeout(loadQuestion, 1000); +} + +function endQuiz() { + emojiDisplay.textContent = "๐ŸŽ‰ Quiz Completed!"; + feedback.textContent = `Your final score: ${score} / ${quizData.length}`; + input.style.display = "none"; + submitBtn.style.display = "none"; + restartBtn.style.display = "inline-block"; +} + +// Event listeners +submitBtn.addEventListener("click", checkAnswer); +input.addEventListener("keypress", function(e){ + if (e.key === "Enter") checkAnswer(); +}); +restartBtn.addEventListener("click", () => { + currentQuestion = 0; + score = 0; + input.style.display = "inline-block"; + submitBtn.style.display = "inline-block"; + restartBtn.style.display = "none"; + scoreEl.textContent = score; + loadQuestion(); +}); + +// Start the quiz +loadQuestion(); diff --git a/games/Emoji Pop Quiz/style.css b/games/Emoji Pop Quiz/style.css new file mode 100644 index 00000000..45c32b77 --- /dev/null +++ b/games/Emoji Pop Quiz/style.css @@ -0,0 +1,97 @@ +/* Reset & basic styles */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(to right, #6a11cb, #2575fc); + color: #fff; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; +} + +.quiz-container { + background-color: rgba(0,0,0,0.7); + padding: 2rem; + border-radius: 15px; + max-width: 400px; + width: 90%; + text-align: center; + box-shadow: 0 8px 20px rgba(0,0,0,0.5); +} + +header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; +} + +header p { + font-size: 0.9rem; + margin-bottom: 1.5rem; + color: #ffd700; +} + +.quiz-card { + background-color: rgba(255,255,255,0.1); + padding: 1.5rem; + border-radius: 10px; + margin-bottom: 1rem; +} + +.emoji-display { + font-size: 2rem; + margin-bottom: 1rem; +} + +input[type="text"] { + width: 80%; + padding: 0.5rem; + border-radius: 5px; + border: none; + font-size: 1rem; + margin-bottom: 0.5rem; +} + +button { + padding: 0.5rem 1rem; + border: none; + border-radius: 5px; + background-color: #ffd700; + color: #000; + font-weight: bold; + cursor: pointer; + transition: 0.3s; + margin: 0.3rem; +} + +button:hover { + background-color: #ffa500; +} + +.feedback { + margin-top: 0.5rem; + font-weight: bold; + min-height: 1.2em; +} + +.quiz-footer { + display: flex; + justify-content: space-between; + margin-top: 1rem; +} + +.restart-btn { + display: none; + margin-top: 1rem; + background-color: #ff4c4c; + color: #fff; +} + +.restart-btn:hover { + background-color: #e63946; +} diff --git a/games/Frogger/index.html b/games/Frogger/index.html new file mode 100644 index 00000000..da6064c7 --- /dev/null +++ b/games/Frogger/index.html @@ -0,0 +1,22 @@ + + + + + + Frogger | Mini JS Games Hub + + + +
+

Frogger

+

Use arrow keys (โ†‘ โ†“ โ† โ†’) to move the frog. Avoid cars and jump on logs/turtles to cross the river.

+ +
+ Score: 0 + Lives: 3 +
+ +
+ + + diff --git a/games/Frogger/script.js b/games/Frogger/script.js new file mode 100644 index 00000000..4451875e --- /dev/null +++ b/games/Frogger/script.js @@ -0,0 +1,190 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); + +const CANVAS_WIDTH = canvas.width; +const CANVAS_HEIGHT = canvas.height; + +// Game Variables +let frog = { x: 280, y: 550, width: 40, height: 40 }; +let lives = 3; +let score = 0; +let gameOver = false; + +const cars = []; +const logs = []; + +// Generate cars and logs +function initObstacles() { + cars.length = 0; + logs.length = 0; + + // Road lanes + for (let i = 0; i < 3; i++) { + for (let j = 0; j < 3; j++) { + cars.push({ + x: j * 200, + y: 400 - i * 50, + width: 50, + height: 40, + speed: (i + 1) * 2, + direction: i % 2 === 0 ? 1 : -1 + }); + } + } + + // River lanes (logs) + for (let i = 0; i < 3; i++) { + for (let j = 0; j < 2; j++) { + logs.push({ + x: j * 300, + y: 200 - i * 50, + width: 150, + height: 40, + speed: (i + 1.5) * 1.5, + direction: i % 2 === 0 ? 1 : -1 + }); + } + } +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + // Draw river + ctx.fillStyle = '#1e90ff'; + ctx.fillRect(0, 50, CANVAS_WIDTH, 150); + + // Draw road + ctx.fillStyle = '#555'; + ctx.fillRect(0, 350, CANVAS_WIDTH, 150); + + // Draw logs + logs.forEach(log => { + ctx.fillStyle = '#8b4513'; + ctx.fillRect(log.x, log.y, log.width, log.height); + }); + + // Draw cars + cars.forEach(car => { + ctx.fillStyle = '#ff0000'; + ctx.fillRect(car.x, car.y, car.width, car.height); + }); + + // Draw frog + ctx.fillStyle = '#0f0'; + ctx.fillRect(frog.x, frog.y, frog.width, frog.height); + + // Update status + document.getElementById('score').textContent = `Score: ${score}`; + document.getElementById('lives').textContent = `Lives: ${lives}`; +} + +// Move obstacles +function updateObstacles() { + cars.forEach(car => { + car.x += car.speed * car.direction; + if (car.direction === 1 && car.x > CANVAS_WIDTH) car.x = -car.width; + if (car.direction === -1 && car.x < -car.width) car.x = CANVAS_WIDTH; + }); + + logs.forEach(log => { + log.x += log.speed * log.direction; + if (log.direction === 1 && log.x > CANVAS_WIDTH) log.x = -log.width; + if (log.direction === -1 && log.x < -log.width) log.x = CANVAS_WIDTH; + }); +} + +// Check collisions +function checkCollisions() { + // Cars + for (const car of cars) { + if (frog.x < car.x + car.width && + frog.x + frog.width > car.x && + frog.y < car.y + car.height && + frog.y + frog.height > car.y) { + loseLife(); + } + } + + // River: frog must be on a log + if (frog.y >= 50 && frog.y < 200) { + let onLog = false; + for (const log of logs) { + if (frog.x < log.x + log.width && + frog.x + frog.width > log.x && + frog.y < log.y + log.height && + frog.y + frog.height > log.y) { + frog.x += log.speed * log.direction; + onLog = true; + } + } + if (!onLog) loseLife(); + } + + // Goal area + if (frog.y < 50) { + score += 1; + resetFrog(); + } +} + +// Lose life +function loseLife() { + lives -= 1; + if (lives <= 0) { + gameOver = true; + alert(`Game Over! Final Score: ${score}`); + resetGame(); + } else { + resetFrog(); + } +} + +// Reset frog +function resetFrog() { + frog.x = 280; + frog.y = 550; +} + +// Game loop +function gameLoop() { + if (!gameOver) { + updateObstacles(); + checkCollisions(); + draw(); + requestAnimationFrame(gameLoop); + } +} + +// Controls +document.addEventListener('keydown', (e) => { + const step = 40; + if (e.key === 'ArrowUp') frog.y -= step; + if (e.key === 'ArrowDown') frog.y += step; + if (e.key === 'ArrowLeft') frog.x -= step; + if (e.key === 'ArrowRight') frog.x += step; + + // Keep inside canvas + frog.x = Math.max(0, Math.min(frog.x, CANVAS_WIDTH - frog.width)); + frog.y = Math.max(0, Math.min(frog.y, CANVAS_HEIGHT - frog.height)); +}); + +// Restart +document.getElementById('restart-btn').addEventListener('click', () => { + resetGame(); +}); + +// Reset full game +function resetGame() { + lives = 3; + score = 0; + resetFrog(); + initObstacles(); + gameOver = false; + gameLoop(); +} + +// Initialize +initObstacles(); +gameLoop(); diff --git a/games/Frogger/style.css b/games/Frogger/style.css new file mode 100644 index 00000000..7906154e --- /dev/null +++ b/games/Frogger/style.css @@ -0,0 +1,54 @@ +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(to bottom, #87ceeb 0%, #f0f0f0 100%); + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; +} + +.game-container { + text-align: center; +} + +h1 { + margin-bottom: 5px; +} + +.instructions { + font-size: 14px; + margin-bottom: 10px; +} + +#gameCanvas { + border: 4px solid #333; + background-color: #222; + display: block; + margin: 0 auto; +} + +.status { + margin-top: 10px; + font-size: 16px; + display: flex; + justify-content: space-around; + width: 600px; + margin-left: auto; + margin-right: auto; +} + +button { + margin-top: 15px; + padding: 8px 16px; + font-size: 16px; + cursor: pointer; + border-radius: 5px; + border: none; + background-color: #4caf50; + color: white; +} + +button:hover { + background-color: #45a049; +} diff --git a/games/Hidden object/index.html b/games/Hidden object/index.html new file mode 100644 index 00000000..7fa4e87b --- /dev/null +++ b/games/Hidden object/index.html @@ -0,0 +1,35 @@ + + + + + + Hidden Object Game ๐Ÿ•ตโ€โ™€ + + + +

๐Ÿ” Hidden Object Game

+

Find all the hidden objects in the scene below!

+ +
+
+ Hidden Scene + + +
+
+
+
+
+
+
+ +
+

Found: 0 / 5

+ +
+ +
+ + + + diff --git a/games/Hidden object/script.js b/games/Hidden object/script.js new file mode 100644 index 00000000..d348b204 --- /dev/null +++ b/games/Hidden object/script.js @@ -0,0 +1,43 @@ +const objects = document.querySelectorAll(".object"); +const foundCountDisplay = document.getElementById("foundCount"); +const restartBtn = document.getElementById("restartBtn"); +const message = document.getElementById("message"); + +let foundCount = 0; +const totalObjects = objects.length; + +function handleObjectClick(e) { + const obj = e.target; + + if (obj.classList.contains("found")) return; + + obj.classList.add("found"); + foundCount++; + foundCountDisplay.textContent = Found: ${foundCount} / ${totalObjects}; + playSound(); + + if (foundCount === totalObjects) { + setTimeout(() => { + message.textContent = "๐ŸŽ‰ You found all the hidden objects!"; + }, 200); + } else { + message.textContent = โœ… You found: ${obj.dataset.name}; + setTimeout(() => (message.textContent = ""), 1500); + } +} + +function restartGame() { + foundCount = 0; + foundCountDisplay.textContent = Found: 0 / ${totalObjects}; + message.textContent = ""; + objects.forEach(obj => obj.classList.remove("found")); +} + +function playSound() { + const audio = new Audio("https://cdn.pixabay.com/download/audio/2022/03/15/audio_50f34169e4.mp3?filename=click-124467.mp3"); + audio.volume = 0.4; + audio.play(); +} + +objects.forEach(obj => obj.addEventListener("click", handleObjectClick)); +restartBtn.addEventListener("click",ย restartGame); diff --git a/games/Hidden object/style.css b/games/Hidden object/style.css new file mode 100644 index 00000000..f589a5d4 --- /dev/null +++ b/games/Hidden object/style.css @@ -0,0 +1,105 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: "Poppins", sans-serif; + background: radial-gradient(circle at top, #93c5fd, #60a5fa, #2563eb); + min-height: 100vh; + color: white; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; +} + +h1 { + margin-top: 20px; + text-shadow: 0 0 10px #fff; +} + +.tagline { + margin: 10px 0 20px; + font-size: 1.1rem; +} + +.game-container { + position: relative; + width: 90%; + max-width: 800px; + height: 450px; + border: 4px solid white; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 0 25px rgba(255, 255, 255, 0.3); +} + +.scene { + position: relative; + width: 100%; + height: 100%; +} + +.background { + width: 100%; + height: 100%; + object-fit: cover; +} + +.object { + position: absolute; + width: 35px; + height: 35px; + cursor: pointer; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + transition: all 0.3s ease; +} + +.object.found { + background: rgba(34, 197, 94, 0.8); + transform: scale(1.4); + box-shadow: 0 0 10px #22c55e; +} + +.status { + margin-top: 20px; + display: flex; + align-items: center; + gap: 20px; +} + +#foundCount { + font-size: 1.2rem; + font-weight: 600; +} + +#restartBtn { + background: linear-gradient(90deg, #f472b6, #a78bfa); + border: none; + color: white; + padding: 10px 16px; + border-radius: 10px; + cursor: pointer; + transition: transform 0.3s ease; + font-weight: 600; +} + +#restartBtn:hover { + transform: scale(1.1); +} + +#message { + margin-top: 25px; + font-size: 1.4rem; + text-shadow: 0 0 10px #fef9c3; + font-weight: bold; + animation: fadeIn 1s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to {ย opacity:ย 1;ย } +} diff --git a/games/Kerning Krusher/index.html b/games/Kerning Krusher/index.html new file mode 100644 index 00000000..ba04cc89 --- /dev/null +++ b/games/Kerning Krusher/index.html @@ -0,0 +1,49 @@ + + + + + + Kerning Krusher + + + + +
+

Kerning Krusher: Typography Puzzles

+

Adjust the text properties to guide the Krusher (๐Ÿ”ด) to the Goal (โญ).

+
+ +
+
๐Ÿ”ด
+ +
โญ
+ +
PLATFORM-A
+ +
_GAP_GAP_GAP_
+ +
+
+ +
+

Typography Controls

+ +
+ + + 30px +
+ +
+ + + 0px +
+ + +

+
+ + + + \ No newline at end of file diff --git a/games/Kerning Krusher/script.css b/games/Kerning Krusher/script.css new file mode 100644 index 00000000..e83b456b --- /dev/null +++ b/games/Kerning Krusher/script.css @@ -0,0 +1,115 @@ +:root { + --game-width: 500px; + --game-height: 400px; +} + +body { + font-family: Arial, sans-serif; + display: flex; + flex-direction: column; + align-items: center; + background-color: #2c3e50; + color: white; + padding: 20px; +} + +#game-container { + width: var(--game-width); + height: var(--game-height); + border: 3px solid #3498db; + background-color: #ecf0f1; + position: relative; /* Crucial for absolute positioning */ + overflow: hidden; + margin-bottom: 20px; +} + +/* --- Game Elements --- */ + +#krusher { + position: absolute; + width: 20px; + height: 20px; + font-size: 20px; + line-height: 20px; + text-align: center; + top: 50px; + left: 20px; + z-index: 10; +} + +#goal { + position: absolute; + width: 40px; + height: 40px; + font-size: 30px; + line-height: 40px; + text-align: center; + bottom: 5px; + right: 50px; + background-color: rgba(76, 175, 80, 0.5); + z-index: 5; +} + +#floor { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 5px; + background-color: #e74c3c; +} + +/* --- Text Platforms (The Malleable Barriers) --- */ + +.text-platform { + position: absolute; + color: #333; + background-color: rgba(255, 255, 255, 0.8); + border: 1px solid #7f8c8d; + white-space: nowrap; /* Prevent text wrapping */ + user-select: none; + line-height: 1.2; /* Ensures consistent line height across text blocks */ +} + +/* Platform 1: Controlled by font-size */ +#platform-size { + top: 150px; + left: 100px; + font-size: 30px; /* Initial value */ +} + +/* Platform 2: Controlled by letter-spacing */ +#platform-spacing { + bottom: 100px; + left: 250px; + font-size: 40px; + letter-spacing: 0px; /* Initial value */ +} + +/* --- Controls Styling --- */ +#controls { + width: var(--game-width); + padding: 15px; + background-color: #34495e; + border-radius: 5px; +} + +.control-group { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.control-group label { + flex: 1; + margin-right: 10px; +} + +.control-group input[type="range"] { + flex: 3; +} + +.control-group span { + flex: 0.5; + text-align: right; +} \ No newline at end of file diff --git a/games/Kerning Krusher/style.js b/games/Kerning Krusher/style.js new file mode 100644 index 00000000..c00323bb --- /dev/null +++ b/games/Kerning Krusher/style.js @@ -0,0 +1,179 @@ +document.addEventListener('DOMContentLoaded', () => { + const krusher = document.getElementById('krusher'); + const goal = document.getElementById('goal'); + const gameContainer = document.getElementById('game-container'); + const platforms = document.querySelectorAll('.text-platform'); + const messageDisplay = document.getElementById('game-message'); + const resetButton = document.getElementById('reset-button'); + + // Control Elements + const sizeSlider = document.getElementById('size-slider'); + const spacingSlider = document.getElementById('spacing-slider'); + const sizeVal = document.getElementById('size-val'); + const spacingVal = document.getElementById('spacing-val'); + + // --- Game State & Physics --- + let gameState = { + isRunning: true, + posX: 20, // Krusher initial X position (relative to container) + posY: 50, // Krusher initial Y position + vY: 0, // Vertical velocity (gravity effect) + g: 0.3 // Gravity acceleration + }; + + const KRUSHER_SIZE = 20; + + // --- Utility Functions --- + + /** + * Reads the current position and dimensions of an element as rendered by the browser. + * This is the "physics" engine. + */ + function getRect(element) { + const rect = element.getBoundingClientRect(); + const containerRect = gameContainer.getBoundingClientRect(); + + return { + left: rect.left - containerRect.left, + top: rect.top - containerRect.top, + right: rect.right - containerRect.left, + bottom: rect.bottom - containerRect.top, + width: rect.width, + height: rect.height + }; + } + + /** + * Standard AABB collision check. + */ + function checkAABBCollision(rect1, rect2) { + return rect1.left < rect2.right && + rect1.right > rect2.left && + rect1.top < rect2.bottom && + rect1.bottom > rect2.top; + } + + // --- Game Loop and Physics --- + + function checkCollisions() { + const krusherRect = { + left: gameState.posX, + top: gameState.posY, + right: gameState.posX + KRUSHER_SIZE, + bottom: gameState.posY + KRUSHER_SIZE, + width: KRUSHER_SIZE, + height: KRUSHER_SIZE + }; + + // 1. Check Goal Collision + if (checkAABBCollision(krusherRect, getRect(goal))) { + endGame("โœ… MISSION COMPLETE! Typography master!"); + return; + } + + // 2. Check Floor Collision (Loss Condition) + if (krusherRect.bottom >= gameContainer.clientHeight) { + endGame("โŒ FAILURE: You hit the floor!"); + return; + } + + // 3. Check Platform Collisions (The main interaction) + platforms.forEach(platform => { + const platformRect = getRect(platform); + + if (checkAABBCollision(krusherRect, platformRect)) { + + // If the Krusher is moving down and hits the top of the platform + if (gameState.vY > 0 && krusherRect.bottom <= platformRect.top + gameState.vY) { + + // Snap Krusher to the top of the platform + gameState.vY = 0; + gameState.posY = platformRect.top - KRUSHER_SIZE; + + } else { + // Collision from side or below (usually a loss condition or bounce) + endGame("โŒ CRASH! You hit the side of a text block!"); + } + } + }); + } + + function updateKrusherPosition() { + // Apply gravity to velocity + gameState.vY += gameState.g; + + // Apply velocity to position + gameState.posY += gameState.vY; + + // Update DOM position + krusher.style.top = `${gameState.posY}px`; + krusher.style.left = `${gameState.posX}px`; + } + + function gameLoop() { + if (!gameState.isRunning) return; + + updateKrusherPosition(); + checkCollisions(); + + requestAnimationFrame(gameLoop); + } + + // --- 4. Control Logic (The Text Manipulation) --- + + // Listener for Font Size (Vertical Movement) + sizeSlider.addEventListener('input', (e) => { + const value = e.target.value; + const platform = document.getElementById('platform-size'); + + platform.style.fontSize = `${value}px`; + sizeVal.textContent = `${value}px`; + + // Re-check physics immediately after changing the platform's dimensions + checkCollisions(); + }); + + // Listener for Letter Spacing (Horizontal Gap) + spacingSlider.addEventListener('input', (e) => { + const value = e.target.value; + const platform = document.getElementById('platform-spacing'); + + platform.style.letterSpacing = `${value}px`; + spacingVal.textContent = `${value}px`; + + // Re-check physics immediately + checkCollisions(); + }); + + // --- Game Management --- + + function endGame(message) { + gameState.isRunning = false; + messageDisplay.textContent = message; + } + + function resetGame() { + gameState.isRunning = true; + gameState.posX = 20; + gameState.posY = 50; + gameState.vY = 0; + messageDisplay.textContent = "Game reset. Start manipulating!"; + krusher.style.top = `${gameState.posY}px`; + krusher.style.left = `${gameState.posX}px`; + + // Reset controls to initial state + sizeSlider.value = 30; + spacingSlider.value = 0; + document.getElementById('platform-size').style.fontSize = '30px'; + document.getElementById('platform-spacing').style.letterSpacing = '0px'; + sizeVal.textContent = '30px'; + spacingVal.textContent = '0px'; + + requestAnimationFrame(gameLoop); + } + + resetButton.addEventListener('click', resetGame); + + // Initial setup and start + resetGame(); +}); \ No newline at end of file diff --git a/games/Key Maestro/index.html b/games/Key Maestro/index.html new file mode 100644 index 00000000..da37ed8c --- /dev/null +++ b/games/Key Maestro/index.html @@ -0,0 +1,47 @@ + + + + + + Key Maestro ๐ŸŽน + + + +
+
+

๐ŸŽน Key Maestro

+

Repeat the piano sequence โ€” use mouse or keys (A S D F G H J)

+
+ +
+ + + + + + +
+ +
+
+ +
+
+ +
+
Level: 0
+
Score: 0
+
Combo: 0
+
+ +
+
Tip: Use keyboard keys A S D F G H J mapped to notes C D E F G A B
+
+ + + + diff --git a/games/Key Maestro/script.js b/games/Key Maestro/script.js new file mode 100644 index 00000000..530f8036 --- /dev/null +++ b/games/Key Maestro/script.js @@ -0,0 +1,201 @@ +/* Key Maestro core logic + - Simon-like piano sequence memorization + - Keyboard keys A S D F G H J map to notes C D E F G A B + - Uses WebAudio to synthesize notes (no external audio files) +*/ + +const NOTES = [ + { name: 'C', freq: 261.63, key: 'A' }, + { name: 'D', freq: 293.66, key: 'S' }, + { name: 'E', freq: 329.63, key: 'D' }, + { name: 'F', freq: 349.23, key: 'F' }, + { name: 'G', freq: 392.00, key: 'G' }, + { name: 'A', freq: 440.00, key: 'H' }, + { name: 'B', freq: 493.88, key: 'J' } +]; + +const pianoEl = document.getElementById('piano'); +const startBtn = document.getElementById('startBtn'); +const restartBtn = document.getElementById('restartBtn'); +const levelEl = document.getElementById('level'); +const scoreEl = document.getElementById('score'); +const comboEl = document.getElementById('combo'); +const difficultySelect = document.getElementById('difficulty'); +const strictBtn = document.getElementById('strictBtn'); +const messageEl = document.getElementById('message'); + +let audioCtx = null; +let sequence = []; +let playerIndex = 0; +let playingSequence = false; +let level = 0; +let score = 0; +let combo = 0; +let strictMode = false; + +// build keys +NOTES.forEach((n, idx) => { + const key = document.createElement('div'); + key.className = 'key'; + key.dataset.idx = idx; + key.innerHTML =
${n.key}
${n.name}
; + pianoEl.appendChild(key); + + key.addEventListener('mousedown', () => handleUserInput(idx)); + key.addEventListener('touchstart', (e) => { e.preventDefault(); handleUserInput(idx); }, {passive:false}); +}); + +// helper: WebAudio tone +function initAudio(){ + if(!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); +} +function playTone(freq, duration = 400){ + initAudio(); + const o = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + o.type = 'sine'; + o.frequency.value = freq; + g.gain.value = 0.0001; + o.connect(g); + g.connect(audioCtx.destination); + const now = audioCtx.currentTime; + g.gain.exponentialRampToValueAtTime(0.15, now + 0.02); + o.start(now); + g.gain.exponentialRampToValueAtTime(0.0001, now + duration/1000); + o.stop(now + duration/1000 + 0.02); +} + +// visual flash +function flashKey(idx, duration=420){ + const key = pianoEl.querySelector(.key[data-idx="${idx}"]); + if(!key) return; + key.classList.add('active'); + playTone(NOTES[idx].freq, duration); + setTimeout(()=> key.classList.remove('active'), duration); +} + +// generate and play sequence +function addStep(){ + const rand = Math.floor(Math.random()*NOTES.length); + sequence.push(rand); +} +function difficultySpeed(){ + const d = difficultySelect.value; + if(d === 'easy') return 700; + if(d === 'hard') return 380; + return 520; // medium +} +async function playSequence(){ + playingSequence = true; + playerIndex = 0; + messageEl.textContent = 'Watch closely...'; + const speed = difficultySpeed(); + for(let i=0;i setTimeout(res, 180)); + flashKey(sequence[i], speed - 100); + await new Promise(res => setTimeout(res, speed)); + } + playingSequence = false; + messageEl.textContent = 'Your turn โ€” repeat the sequence!'; +} + +// user input handling +function handleUserInput(idx){ + if(playingSequence) return; + if(!sequence.length) return; + flashKey(idx, 240); + // check + if(idx === sequence[playerIndex]){ + playerIndex++; + combo++; + score += 10; + updateStats(); + if(playerIndex === sequence.length){ + // success for this level + level++; + messageEl.textContent = Good! Level ${level} complete.; + comboEl.textContent = combo; + setTimeout(() => nextLevel(), 700); + } + } else { + // wrong + combo = 0; + updateStats(); + const keyEl = pianoEl.querySelector(.key[data-idx="${idx}"]); + if(keyEl) keyEl.classList.add('wrong'); + setTimeout(()=> keyEl && keyEl.classList.remove('wrong'), 360); + messageEl.textContent = 'Wrong note!'; + if(strictMode){ + // end game + messageEl.textContent = Game Over โ€” wrong note. Final score: ${score}; + playingSequence = true; // block input + } else { + // replay sequence for retry + playerIndex = 0; + messageEl.textContent = 'Try again โ€” watch the sequence.'; + setTimeout(()=> playSequence(), 800); + } + } +} + +// progression +function updateStats(){ + levelEl.textContent = level; + scoreEl.textContent = score; + comboEl.textContent = combo; +} + +function nextLevel(){ + addStep(); + updateStats(); + setTimeout(()=> playSequence(), 450); +} + +function startGame(){ + // reset + sequence = []; + playerIndex = 0; + level = 0; + score = 0; + combo = 0; + playingSequence = false; + messageEl.textContent = 'Get ready...'; + updateStats(); + // first step + addStep(); + setTimeout(()=> playSequence(), 700); +} + +function resetGame(){ + sequence = []; + playerIndex = 0; + level = 0; + score = 0; + combo = 0; + playingSequence = false; + messageEl.textContent = 'Game reset. Press Start.'; + updateStats(); +} + +// keyboard support +window.addEventListener('keydown', (e)=>{ + const key = e.key.toUpperCase(); + const note = NOTES.find(n => n.key === key); + if(note){ + const idx = NOTES.indexOf(note); + handleUserInput(idx); + } +}); + +// UI wiring +startBtn.addEventListener('click', startGame); +restartBtn.addEventListener('click', resetGame); +strictBtn.addEventListener('click', () => { + strictMode = !strictMode; + strictBtn.textContent = Strict: ${strictMode ? 'On' : 'Off'}; + strictBtn.style.background = strictMode ? 'linear-gradient(90deg,#fb7185,#f97316)' : ''; +}); + +// initial message +messageEl.textContent = 'Press Start to play Key Maestro โ€” use A S D F G H J or click keys'; +updateStats(); diff --git a/games/Key Maestro/style.css b/games/Key Maestro/style.css new file mode 100644 index 00000000..f691cc62 --- /dev/null +++ b/games/Key Maestro/style.css @@ -0,0 +1,41 @@ +:root{ + --bg1:#0b1020; --bg2:#0f172a; + --accent:#ffd166; --accent2:#60a5fa; + --white: #f8fafc; +} +*{box-sizing:border-box} +html,body{height:100%;margin:0;font-family:Inter, Poppins, system-ui;background:linear-gradient(180deg,var(--bg1),var(--bg2));color:var(--white)} +.container{max-width:960px;margin:28px auto;padding:18px;} +header h1{margin:0;font-size:28px;color:var(--accent);text-shadow:0 4px 20px rgba(255,209,102,0.06)} +.tag{margin:6px 0 14px;color:#bcd7ff} + +.controls{display:flex;gap:10px;align-items:center;margin-bottom:18px;background:rgba(255,255,255,0.02);padding:10px;border-radius:10px} +.controls select, .controls button{padding:8px 12px;border-radius:8px;border:none;cursor:pointer;font-weight:600} +#startBtn{background:linear-gradient(90deg,#7dd3fc,#a78bfa);color:#062033} +#strictBtn{background:transparent;border:1px solid rgba(255,255,255,0.06);color:var(--white)} +#restartBtn{background:linear-gradient(90deg,#fb7185,#f97316);color:white} + +.piano-area{display:flex;justify-content:center;margin-top:8px} +.piano{display:flex;gap:8px;padding:20px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));border-radius:12px;border:1px solid rgba(255,255,255,0.03)} + +.key{ + width:88px;height:260px;border-radius:10px;display:flex;align-items:flex-end;justify-content:center; + cursor:pointer; user-select:none; box-shadow:0 8px 25px rgba(2,6,23,0.6); + transition: transform .12s ease, box-shadow .14s ease; + position:relative;padding-bottom:14px;font-weight:700;color:#062033; + background:linear-gradient(180deg,#fff,#f3f4f6); +} +.key .label{position:absolute;top:8px;left:8px;font-size:13px;color:#1e293b;font-weight:700;opacity:0.85} +.key.active{transform:translateY(-8px);box-shadow:0 18px 40px rgba(96,165,250,0.18);outline:6px solid rgba(96,165,250,0.06)} +.key.wrong{animation:shake .36s} +@keyframes shake{0%{transform:translateX(0)}25%{transform:translateX(-6px)}50%{transform:translateX(6px)}75%{transform:translateX(-4px)}100%{transform:translateX(0)}} + +.status{display:flex;gap:20px;margin-top:16px;font-weight:700} +.message{margin-top:14px;padding:12px;border-radius:10px;min-height:36px} +.hint{margin-top:18px;color:#9fb4d9;font-size:13px;text-align:center} + +/* responsive */ +@media (max-width:720px){ + .piano{gap:8px;padding:12px} + .key{width:60px;height:200px;font-size:14px} +} diff --git a/games/Keyframe Kaleidoscope/index.html b/games/Keyframe Kaleidoscope/index.html new file mode 100644 index 00000000..4c260f17 --- /dev/null +++ b/games/Keyframe Kaleidoscope/index.html @@ -0,0 +1,30 @@ + + + + + + Keyframe Kaleidoscope + + + + +
+

Keyframe Kaleidoscope

+

Press the SPACEBAR at the precise moment the box animation completes a cycle!

+
+ +
+
+
+ +
+

Rhythm Score

+

Score: 0

+

Last Accuracy: N/A

+

Hit the **SPACEBAR** when the box is on the **LEFT** side!

+

+
+ + + + \ No newline at end of file diff --git a/games/Keyframe Kaleidoscope/script.js b/games/Keyframe Kaleidoscope/script.js new file mode 100644 index 00000000..2e4587b4 --- /dev/null +++ b/games/Keyframe Kaleidoscope/script.js @@ -0,0 +1,118 @@ +document.addEventListener('DOMContentLoaded', () => { + const targetBox = document.getElementById('target-box'); + const scoreDisplay = document.getElementById('score'); + const accuracyDisplay = document.getElementById('accuracy'); + const feedbackMessage = document.getElementById('feedback-message'); + const instructionMessage = document.getElementById('instruction-message'); + + // --- Core Timing and State Variables --- + const ANIMATION_DURATION_MS = 2500; // Must match CSS --animation-duration (2.5s) + let score = 0; + let lastIterationEndTime = performance.now(); // Tracks when the last animation cycle finished + let requiredHitState = 'start'; // Can be 'start', 'mid', or 'end' + const HIT_TOLERANCE_MS = 50; // Max deviation allowed (e.g., 50ms) + + // --- Utility Functions --- + + function getTheoreticalHitTime(state) { + // Calculate the theoretical timestamp when the target state should occur + let offset = 0; + if (state === 'start') { + // Hit at 0% (which is the animation-iteration event) + offset = 0; + } else if (state === 'end') { + // Hit at 100% (which is the animation-end event) + offset = ANIMATION_DURATION_MS; + } else if (state === 'mid') { + // Hit at 50% mark + offset = ANIMATION_DURATION_MS / 2; + } + + return lastIterationEndTime + offset; + } + + function updateRequiredState() { + // Cycle the required hit state to increase complexity + if (requiredHitState === 'start') { + requiredHitState = 'end'; + targetBox.className = 'state-end'; + instructionMessage.textContent = "Hit the **SPACEBAR** when the box is on the **RIGHT** side!"; + } else if (requiredHitState === 'end') { + requiredHitState = 'mid'; + targetBox.className = 'state-mid'; + instructionMessage.textContent = "Hit the **SPACEBAR** when the box is **GREEN** (midpoint)!"; + } else { + requiredHitState = 'start'; + targetBox.className = 'state-start'; + instructionMessage.textContent = "Hit the **SPACEBAR** when the box is on the **LEFT** side!"; + } + } + + + // --- 1. Player Input Logic (The Challenge) --- + document.addEventListener('keydown', (event) => { + if (event.code === 'Space') { + event.preventDefault(); // Stop scrolling the page + checkTiming(); + } + }); + + function checkTiming() { + const playerTime = performance.now(); + const theoreticalTime = getTheoreticalHitTime(requiredHitState); + const timeDifference = Math.abs(playerTime - theoreticalTime); + + if (timeDifference <= HIT_TOLERANCE_MS) { + // Success! + score++; + scoreDisplay.textContent = score; + accuracyDisplay.textContent = `${timeDifference.toFixed(2)}ms OFF`; + + // Visual/Audio feedback for success + feedbackMessage.textContent = `PERFECT RHYTHM! (${timeDifference.toFixed(2)}ms off)`; + feedbackMessage.style.color = 'lime'; + + // Advance the required state immediately after a successful hit + updateRequiredState(); + + } else { + // Failure + feedbackMessage.textContent = `MISSED! Too far off (${timeDifference.toFixed(2)}ms off)`; + feedbackMessage.style.color = 'red'; + + // Optionally, penalize score or pause the game + // score = Math.max(0, score - 1); // Simple penalty + scoreDisplay.textContent = score; + } + } + + + // --- 2. CSS Event Listeners (The Metronome) --- + + targetBox.addEventListener('animationiteration', (event) => { + // This fires when the animation cycle returns to 0% (the 'start' state). + // Update the reference time for the *next* cycle's calculations. + lastIterationEndTime = performance.now(); + + // If the player was supposed to hit the 'start' state but didn't, it's a passive failure. + if (requiredHitState === 'start') { + // Since the event already fired, we check for a *previous* unsuccessful check. + // This is complex, so we'll rely mainly on the user's manual keypress check. + } + }); + + targetBox.addEventListener('animationend', (event) => { + // This fires when the animation cycle hits 100% (the 'end' state) in the 'alternate' direction. + // Update the reference time again. + lastIterationEndTime = performance.now(); + + // The animation restarts immediately due to 'infinite', so we rely on this event + // to reset our timing clock for the next calculation. + }); + + + // --- Initialization --- + targetBox.className = 'state-start'; // Set initial required state and class + // Start the timing loop by setting the initial reference point + lastIterationEndTime = performance.now(); +}); \ No newline at end of file diff --git a/games/Keyframe Kaleidoscope/style.css b/games/Keyframe Kaleidoscope/style.css new file mode 100644 index 00000000..8254a6a4 --- /dev/null +++ b/games/Keyframe Kaleidoscope/style.css @@ -0,0 +1,83 @@ +:root { + --animation-duration: 2.5s; /* The core rhythm duration */ + /* A complex, custom cubic-bezier function for non-linear, tricky timing */ + --animation-timing: cubic-bezier(0.8, -0.5, 0.2, 1.5); + --game-width: 500px; +} + +body { + font-family: monospace, sans-serif; + display: flex; + flex-direction: column; + align-items: center; + background-color: #212121; + color: #f0f0f0; + padding: 20px; +} + +#game-area { + width: var(--game-width); + height: 100px; + border: 3px solid #6c5ce7; + margin: 20px 0; + position: relative; + overflow: hidden; +} + +#target-box { + position: absolute; + width: 50px; + height: 50px; + background-color: #ff5722; + border-radius: 5px; + top: 25px; + + /* --- THE CORE ANIMATION RULE --- */ + animation: bounce-rhythm var(--animation-duration) var(--animation-timing) infinite alternate; + /* The animation-play-state will be controlled by JavaScript for pausing */ + animation-play-state: running; +} + +/* --- Keyframes Definition --- */ +@keyframes bounce-rhythm { + /* 0% is the start state (left side) */ + 0% { + left: 0; + transform: scale(1); + } + + /* A mid-point to introduce a false rhythm/specific state change */ + 50% { + background-color: #4CAF50; /* Green at midpoint */ + transform: scale(0.8); + } + + /* 100% is the end state (right side) */ + 100% { + left: calc(var(--game-width) - 50px); + transform: scale(1.2); + } +} + + +/* --- State Classes (Used to change the required hit point) --- */ +.state-end { + /* Default state, requires hitting 100% (right side) */ + border: 2px solid yellow; +} + +.state-start { + /* Requires hitting 0% (left side) on animationiteration */ + border: 2px solid cyan; + background-color: #ffc107; +} + +.state-mid { + /* Requires hitting 50% state (Green, scaled down) */ + border: 2px solid lime; +} + +/* Pause state */ +.paused #target-box { + animation-play-state: paused; +} \ No newline at end of file diff --git a/games/LexiQuest/index.html b/games/LexiQuest/index.html new file mode 100644 index 00000000..3c7382c6 --- /dev/null +++ b/games/LexiQuest/index.html @@ -0,0 +1,41 @@ + + + + + + LexiQuest ๐Ÿง  + + + +

๐Ÿง  LexiQuest

+

Test your English vocabulary! Choose the correct meaning or synonym of the word shown.

+ +
+
+

Loading...

+
+
+ +
+

Score: 0

+

Question: 1 / 10

+
+ + +
+ +
+ + + +
+ +
+ + + + diff --git a/games/LexiQuest/script.js b/games/LexiQuest/script.js new file mode 100644 index 00000000..17ef11f7 --- /dev/null +++ b/games/LexiQuest/script.js @@ -0,0 +1,100 @@ +const wordBox = document.getElementById("word"); +const optionsBox = document.getElementById("options"); +const scoreDisplay = document.getElementById("score"); +const questionNumDisplay = document.getElementById("question-number"); +const nextBtn = document.getElementById("nextBtn"); +const message = document.getElementById("message"); +const startBtn = document.getElementById("startBtn"); +const difficultySelect = document.getElementById("difficulty"); + +let currentQuestion = 0; +let score = 0; +let questions = []; + +const questionBank = { + easy: [ + { word: "Happy", options: ["Joyful", "Sad", "Angry", "Tired"], answer: "Joyful" }, + { word: "Big", options: ["Large", "Tiny", "Thin", "Short"], answer: "Large" }, + { word: "Cold", options: ["Hot", "Freezing", "Warm", "Dry"], answer: "Freezing" }, + { word: "Fast", options: ["Quick", "Slow", "Late", "Lazy"], answer: "Quick" }, + { word: "Beautiful", options: ["Ugly", "Pretty", "Dirty", "Plain"], answer: "Pretty" }, + ], + medium: [ + { word: "Courage", options: ["Fear", "Bravery", "Cowardice", "Hate"], answer: "Bravery" }, + { word: "Ancient", options: ["Old", "Modern", "Recent", "Future"], answer: "Old" }, + { word: "Fragile", options: ["Delicate", "Strong", "Solid", "Tough"], answer: "Delicate" }, + { word: "Polite", options: ["Rude", "Courteous", "Selfish", "Cruel"], answer: "Courteous" }, + { word: "Bizarre", options: ["Strange", "Normal", "Usual", "Plain"], answer: "Strange" }, + ], + hard: [ + { word: "Eloquent", options: ["Fluent", "Silent", "Weak", "Dull"], answer: "Fluent" }, + { word: "Obsolete", options: ["Outdated", "Current", "New", "Trendy"], answer: "Outdated" }, + { word: "Melancholy", options: ["Sadness", "Joy", "Excitement", "Anger"], answer: "Sadness" }, + { word: "Voracious", options: ["Greedy", "Lazy", "Calm", "Sleepy"], answer: "Greedy" }, + { word: "Astute", options: ["Clever", "Foolish", "Clumsy", "Slow"], answer: "Clever" }, + ], +}; + +function startGame() { + const difficulty = difficultySelect.value; + questions = [...questionBank[difficulty]]; + currentQuestion = 0; + score = 0; + scoreDisplay.textContent = "Score: 0"; + questionNumDisplay.textContent = "Question: 1 / " + questions.length; + message.textContent = ""; + nextBtn.disabled = true; + renderQuestion(); +} + +function renderQuestion() { + const q = questions[currentQuestion]; + wordBox.textContent = q.word; + optionsBox.innerHTML = ""; + q.options.forEach(option => { + const btn = document.createElement("div"); + btn.classList.add("option"); + btn.textContent = option; + btn.onclick = () => selectAnswer(btn, q.answer); + optionsBox.appendChild(btn); + }); +} + +function selectAnswer(selectedBtn, correctAnswer) { + const options = document.querySelectorAll(".option"); + options.forEach(opt => { + opt.onclick = null; + if (opt.textContent === correctAnswer) opt.classList.add("correct"); + else if (opt === selectedBtn) opt.classList.add("wrong"); + }); + + if (selectedBtn.textContent === correctAnswer) { + score++; + message.textContent = "โœ… Correct!"; + } else { + message.textContent = "โŒ Incorrect!"; + } + + scoreDisplay.textContent = "Score: " + score; + nextBtn.disabled = false; +} + +nextBtn.addEventListener("click", () => { + currentQuestion++; + message.textContent = ""; + if (currentQuestion < questions.length) { + questionNumDisplay.textContent = Question: ${currentQuestion + 1} / ${questions.length}; + nextBtn.disabled = true; + renderQuestion(); + } else { + showResult(); + } +}); + +function showResult() { + wordBox.textContent = "๐ŸŽ‰ Quiz Completed!"; + optionsBox.innerHTML =

Your final score is ${score}/${questions.length}

; + nextBtn.disabled = true; +} + +startBtn.addEventListener("click",ย startGame); diff --git a/games/LexiQuest/style.css b/games/LexiQuest/style.css new file mode 100644 index 00000000..cb4644d5 --- /dev/null +++ b/games/LexiQuest/style.css @@ -0,0 +1,118 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: "Poppins", sans-serif; + background: linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6); + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + color: white; + overflow-x: hidden; +} + +h1 { + margin-top: 30px; + font-size: 2.4rem; + text-shadow: 0 0 15px #fff; +} + +.tagline { + margin-top: 10px; + font-size: 1.1rem; +} + +.game-container { + background: rgba(255, 255, 255, 0.15); + padding: 30px; + border-radius: 20px; + box-shadow: 0 4px 25px rgba(0, 0, 0, 0.3); + width: 90%; + max-width: 500px; + margin-top: 30px; + text-align: center; + backdrop-filter: blur(10px); +} + +.question-box h2 { + font-size: 1.8rem; + margin-bottom: 20px; +} + +.options { + display: flex; + flex-direction: column; + gap: 10px; +} + +.option { + background: white; + color: #1e3a8a; + padding: 12px; + border-radius: 10px; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s ease, background 0.3s ease; +} + +.option:hover { + transform: scale(1.03); + background: #c7d2fe; +} + +.option.correct { + background: #4ade80; + color: white; + box-shadow: 0 0 15px #22c55e; +} + +.option.wrong { + background: #f87171; + color: white; + box-shadow: 0 0 15px #ef4444; +} + +.status { + display: flex; + justify-content: space-between; + margin-top: 20px; +} + +button { + margin-top: 20px; + padding: 10px 18px; + border: none; + background: linear-gradient(90deg, #a78bfa, #f472b6); + color: white; + font-weight: bold; + border-radius: 10px; + cursor: pointer; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +button:hover { + transform: scale(1.05); + box-shadow: 0 0 15px rgba(255, 255, 255, 0.5); +} + +.controls { + display: flex; + gap: 15px; + align-items: center; + margin-top: 25px; + background: rgba(255, 255, 255, 0.1); + padding: 10px 15px; + border-radius: 10px; +} + +#message { + margin-top: 25px; + font-size: 1.3rem; + font-weight: bold; + text-shadow: 0 0ย 10pxย #fef9c3; +} diff --git a/games/Logic-Chain/index.html b/games/Logic-Chain/index.html new file mode 100644 index 00000000..767df202 --- /dev/null +++ b/games/Logic-Chain/index.html @@ -0,0 +1,47 @@ + + + + + + Enhanced Logic Chain + + + +

๐Ÿงฉ Enhanced Logic Chain

+

Create complex logic chains with multiple components

+ +
Place objects and connect them to create a chain
+ +
+
+
+ +
+ + + +
+ +
+
+
Objects
+
๐Ÿ”ด
+
+
+
+
๐Ÿšช
+
โšซ
+
+ +
+ +
+
+ +

+ How to play: Drag objects from the palette to the board. Connect them by placing connectors between objects. + Click Start to trigger the chain reaction. The ball will roll to the switch, which activates the gate. +

+ + + \ No newline at end of file diff --git a/games/Logic-Chain/script.js b/games/Logic-Chain/script.js new file mode 100644 index 00000000..a15c4001 --- /dev/null +++ b/games/Logic-Chain/script.js @@ -0,0 +1,294 @@ + // Enhanced Logic Chain Simulation + const board = document.getElementById("game-board"); + const startBtn = document.getElementById("start"); + const resetBtn = document.getElementById("reset"); + const clearBtn = document.getElementById("clear"); + const statusEl = document.getElementById("status"); + const progressBar = document.getElementById("progress-bar"); + + let objects = []; + let isSimulating = false; + let objectCount = 0; + let connections = []; + + // --- Object creation --- + document.querySelectorAll(".palette-object").forEach(obj => { + obj.addEventListener("dragstart", dragStart); + }); + + board.addEventListener("dragover", e => e.preventDefault()); + board.addEventListener("drop", drop); + + function dragStart(e) { + e.dataTransfer.setData("type", e.target.dataset.type); + e.dataTransfer.setData("source", "palette"); + } + + function drop(e) { + if (isSimulating) return; + + e.preventDefault(); + const type = e.dataTransfer.getData("type"); + const source = e.dataTransfer.getData("source"); + + if (source === "palette") { + createObject(type, e.clientX, e.clientY); + } else { + // Moving existing object + const id = e.dataTransfer.getData("id"); + const obj = document.getElementById(id); + const rect = board.getBoundingClientRect(); + obj.style.left = e.clientX - rect.left - parseInt(obj.style.width || obj.offsetWidth) / 2 + "px"; + obj.style.top = e.clientY - rect.top - parseInt(obj.style.height || obj.offsetHeight) / 2 + "px"; + + updateConnections(); + } + } + + function createObject(type, clientX, clientY) { + const rect = board.getBoundingClientRect(); + const id = `obj-${objectCount++}`; + const obj = document.createElement("div"); + + obj.id = id; + obj.className = `object ${type}`; + obj.draggable = true; + obj.dataset.type = type; + + // Set position + const width = type === 'gate' ? 80 : type === 'switch' ? 70 : type === 'connector' ? 20 : 50; + const height = type === 'gate' ? 30 : type === 'switch' ? 40 : type === 'connector' ? 20 : 50; + + obj.style.left = clientX - rect.left - width/2 + "px"; + obj.style.top = clientY - rect.top - height/2 + "px"; + obj.style.width = width + "px"; + obj.style.height = height + "px"; + + // Add inner content based on type + if (type === 'switch') { + const knob = document.createElement("div"); + knob.className = "switch-knob"; + obj.appendChild(knob); + } else if (type === 'gate') { + obj.textContent = "CLOSED"; + } + + // Add event listeners + obj.addEventListener("dragstart", function(e) { + e.dataTransfer.setData("type", type); + e.dataTransfer.setData("id", id); + e.dataTransfer.setData("source", "board"); + }); + + obj.addEventListener("dblclick", function() { + if (isSimulating) return; + if (type === 'connector') { + obj.remove(); + updateConnections(); + } + }); + + board.appendChild(obj); + objects.push({id, type, element: obj}); + updateStatus(`Added ${type} to the board`); + } + + // --- Connection logic --- + function updateConnections() { + // Remove existing connection lines + document.querySelectorAll('.connector-line').forEach(line => line.remove()); + connections = []; + + // Find all connectors + const connectors = objects.filter(obj => obj.type === 'connector'); + + connectors.forEach(connector => { + const connectorEl = connector.element; + const connectorRect = connectorEl.getBoundingClientRect(); + const connectorX = connectorRect.left + connectorRect.width/2; + const connectorY = connectorRect.top + connectorRect.height/2; + + // Find the closest objects to connect to + let closestObj1 = null, closestObj2 = null; + let minDist1 = Infinity, minDist2 = Infinity; + + objects.forEach(obj => { + if (obj.id === connector.id || obj.type === 'connector') return; + + const objRect = obj.element.getBoundingClientRect(); + const objX = objRect.left + objRect.width/2; + const objY = objRect.top + objRect.height/2; + + const dist = Math.sqrt((objX - connectorX)**2 + (objY - connectorY)**2); + + if (dist < minDist1) { + minDist2 = minDist1; + closestObj2 = closestObj1; + minDist1 = dist; + closestObj1 = obj; + } else if (dist < minDist2) { + minDist2 = dist; + closestObj2 = obj; + } + }); + + if (closestObj1 && closestObj2) { + // Create connection line + const obj1Rect = closestObj1.element.getBoundingClientRect(); + const obj2Rect = closestObj2.element.getBoundingClientRect(); + + const x1 = obj1Rect.left + obj1Rect.width/2; + const y1 = obj1Rect.top + obj1Rect.height/2; + const x2 = obj2Rect.left + obj2Rect.width/2; + const y2 = obj2Rect.top + obj2Rect.height/2; + + const length = Math.sqrt((x2-x1)**2 + (y2-y1)**2); + const angle = Math.atan2(y2-y1, x2-x1) * 180 / Math.PI; + + const line = document.createElement("div"); + line.className = "connector-line"; + line.style.width = length + "px"; + line.style.left = x1 + "px"; + line.style.top = y1 + "px"; + line.style.transform = `rotate(${angle}deg)`; + + board.appendChild(line); + + // Store connection + connections.push({ + from: closestObj1.id, + to: closestObj2.id, + through: connector.id + }); + } + }); + } + + // --- Simulation logic --- + startBtn.addEventListener("click", runSimulation); + resetBtn.addEventListener("click", resetGame); + clearBtn.addEventListener("click", clearBoard); + + function runSimulation() { + if (isSimulating) return; + + isSimulating = true; + updateStatus("Simulation running..."); + progressBar.style.width = "0%"; + + // Find the ball, switch and gate + const ball = objects.find(obj => obj.type === 'ball'); + const sw = objects.find(obj => obj.type === 'switch'); + const gate = objects.find(obj => obj.type === 'gate'); + + if (!ball || !sw || !gate) { + updateStatus("โŒ You need at least a ball, switch and gate to run the simulation"); + isSimulating = false; + return; + } + + // Check if they're connected + const ballToSwitch = connections.find(conn => + (conn.from === ball.id && conn.to === sw.id) || + (conn.from === sw.id && conn.to === ball.id) + ); + + const switchToGate = connections.find(conn => + (conn.from === sw.id && conn.to === gate.id) || + (conn.from === gate.id && conn.to === sw.id) + ); + + if (!ballToSwitch || !switchToGate) { + updateStatus("โŒ Objects need to be connected with connectors"); + isSimulating = false; + return; + } + + // Start the simulation + ball.element.classList.add("ball-rolling", "active"); + progressBar.style.width = "33%"; + + // Simulate ball rolling to switch + setTimeout(() => { + ball.element.classList.remove("ball-rolling", "active"); + sw.element.classList.add("active", "pulse"); + progressBar.style.width = "66%"; + updateStatus("Ball reached the switch!"); + }, 1500); + + // Switch activates gate + setTimeout(() => { + sw.element.classList.remove("pulse"); + const knob = sw.element.querySelector('.switch-knob'); + if (knob) knob.style.transform = "translateX(30px)"; + + gate.element.classList.add("active", "open"); + gate.element.textContent = "OPEN"; + progressBar.style.width = "100%"; + updateStatus("Switch activated the gate!"); + }, 2500); + + // Completion + setTimeout(() => { + gate.element.classList.remove("active"); + updateStatus("โœ… Chain completed! The gate is open!"); + isSimulating = false; + }, 3500); + } + + function resetGame() { + isSimulating = false; + + // Reset all objects + objects.forEach(obj => { + const element = obj.element; + element.classList.remove("active", "ball-rolling", "pulse", "open"); + + if (obj.type === 'switch') { + const knob = element.querySelector('.switch-knob'); + if (knob) knob.style.transform = "translateX(0)"; + } else if (obj.type === 'gate') { + element.textContent = "CLOSED"; + } + }); + + progressBar.style.width = "0%"; + updateStatus("Simulation reset. Rearrange objects and try again!"); + } + + function clearBoard() { + if (isSimulating) return; + + // Remove all objects from board + document.querySelectorAll('.object').forEach(obj => { + if (!obj.parentElement.isSameNode(document.getElementById('objects-palette'))) { + obj.remove(); + } + }); + + // Remove connection lines + document.querySelectorAll('.connector-line').forEach(line => line.remove()); + + objects = []; + connections = []; + objectCount = 0; + updateStatus("Board cleared. Add new objects to create a chain."); + } + + function updateStatus(message) { + statusEl.textContent = message; + } + + // Initialize with some objects + window.addEventListener('load', () => { + // Create initial objects + const rect = board.getBoundingClientRect(); + + createObject('ball', rect.left + 100, rect.top + 100); + createObject('switch', rect.left + 300, rect.top + 150); + createObject('gate', rect.left + 500, rect.top + 100); + createObject('connector', rect.left + 200, rect.top + 120); + createObject('connector', rect.left + 400, rect.top + 120); + + setTimeout(updateConnections, 100); + }); diff --git a/games/Logic-Chain/style.css b/games/Logic-Chain/style.css new file mode 100644 index 00000000..223855e4 --- /dev/null +++ b/games/Logic-Chain/style.css @@ -0,0 +1,261 @@ + + body { + font-family: "Segoe UI", sans-serif; + background: linear-gradient(135deg, #f6f8fa, #e1e5eb); + text-align: center; + margin: 0; + padding: 20px; + min-height: 100vh; + } + + h1 { + color: #333; + margin-bottom: 10px; + text-shadow: 1px 1px 3px rgba(0,0,0,0.1); + } + + .subtitle { + color: #666; + margin-bottom: 20px; + } + + .controls { + margin: 10px 0 20px; + display: flex; + justify-content: center; + gap: 15px; + } + + button { + padding: 10px 20px; + font-size: 16px; + border: none; + border-radius: 8px; + cursor: pointer; + background-color: #0078d7; + color: white; + transition: all 0.2s; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + } + + button:hover { + background-color: #005fa3; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.15); + } + + button:active { + transform: translateY(0); + } + + #start { + background-color: #28a745; + } + + #start:hover { + background-color: #218838; + } + + #reset { + background-color: #dc3545; + } + + #reset:hover { + background-color: #c82333; + } + + .game-container { + display: flex; + max-width: 1200px; + margin: 0 auto; + gap: 20px; + } + + #objects-palette { + width: 150px; + background: white; + border-radius: 10px; + padding: 15px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + } + + .palette-title { + font-weight: bold; + margin-bottom: 15px; + color: #444; + } + + .palette-object { + width: 60px; + height: 60px; + margin: 10px auto; + border-radius: 8px; + cursor: grab; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + transition: transform 0.2s; + } + + .palette-object:hover { + transform: scale(1.05); + } + + #game-board { + position: relative; + flex: 1; + height: 60vh; + background: #ffffff; + border: 2px solid #ccc; + border-radius: 10px; + overflow: hidden; + .object { + position: absolute; + cursor: grab; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + transition: transform 0.3s ease; + z-index: 10; + } + transition: transform 0.3s ease; + z-index: 10; + } + + /* Object types */ + .ball { + width: 50px; + height: 50px; + border-radius: 50%; + background: radial-gradient(circle at 30% 30%, #ff4d4d, #b30000); + box-shadow: 0 3px 8px rgba(179, 0, 0, 0.3); + } + + .switch { + width: 70px; + height: 40px; + border-radius: 20px; + background: linear-gradient(145deg, #ffc107, #ff9800); + box-shadow: 0 3px 8px rgba(255, 152, 0, 0.3); + display: flex; + align-items: center; + justify-content: flex-start; + padding: 0 5px; + } + + .switch-knob { + width: 30px; + height: 30px; + border-radius: 50%; + background: white; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + transition: transform 0.3s; + } + + .switch.active .switch-knob { + transform: translateX(30px); + } + + .gate { + width: 80px; + height: 30px; + border-radius: 5px; + background: linear-gradient(145deg, #4caf50, #2e7d32); + box-shadow: 0 3px 8px rgba(46, 125, 50, 0.3); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: bold; + font-size: 14px; + } + + .gate.open { + background: linear-gradient(145deg, #9e9e9e, #757575); + } + + .connector { + width: 20px; + height: 20px; + border-radius: 50%; + background: #6c757d; + box-shadow: 0 2px 5px rgba(108, 117, 125, 0.3); + z-index: 5; + } + + .connector-line { + position: absolute; + background: #6c757d; + height: 3px; + transform-origin: 0 0; + z-index: 1; + } + + .instructions { + color: #555; + margin-top: 20px; + font-size: 15px; + max-width: 800px; + margin-left: auto; + margin-right: auto; + background: white; + padding: 15px; + border-radius: 8px; + box-shadow: 0 2px 5px rgba(0,0,0,0.05); + } + + /* Animation effects */ + .active { + transform: scale(1.1); + box-shadow: 0 0 15px rgba(0,0,0,0.3); + } + + .ball-rolling { + animation: roll 0.5s linear infinite; + } + + @keyframes roll { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + .pulse { + animation: pulse 1s infinite; + } + + @keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); } + } + + /* Status display */ + .status { + margin: 15px 0; + font-weight: bold; + color: #333; + min-height: 24px; + } + + .chain-progress { + width: 80%; + height: 10px; + background: #e9ecef; + border-radius: 5px; + margin: 15px auto; + overflow: hidden; + } + + .progress-bar { + height: 100%; + background: linear-gradient(to right, #28a745, #20c997); + width: 0%; + transition: width 0.5s; + } diff --git a/games/Luma Maze/index.html b/games/Luma Maze/index.html new file mode 100644 index 00000000..5d192da1 --- /dev/null +++ b/games/Luma Maze/index.html @@ -0,0 +1,28 @@ + + + + + + Luma Maze + + + +
+

โœจ Luma Maze

+

Guide the glowing orb to the goal!

+ +
+
+
+ +
+ +
+ + +
+
+ + + + diff --git a/games/Luma Maze/script.js b/games/Luma Maze/script.js new file mode 100644 index 00000000..63925408 --- /dev/null +++ b/games/Luma Maze/script.js @@ -0,0 +1,134 @@ +const maze = document.getElementById("maze"); +const player = document.getElementById("player"); +const goal = document.getElementById("goal"); +const startBtn = document.getElementById("startBtn"); +const restartBtn = document.getElementById("restartBtn"); +const message = document.getElementById("message"); +const trailCanvas = document.getElementById("trail"); +const ctx = trailCanvas.getContext("2d"); + +trailCanvas.width = maze.offsetWidth; +trailCanvas.height = maze.offsetHeight; + +let x = 10, y = 10; +let speed = 3; +let gameActive = false; +let walls = []; + +function createWalls() { + const wallData = [ + { x: 0, y: 80, w: 200, h: 10 }, + { x: 100, y: 160, w: 200, h: 10 }, + { x: 0, y: 240, w: 200, h: 10 }, + { x: 200, y: 0, w: 10, h: 200 } + ]; + + wallData.forEach(data => { + const wall = document.createElement("div"); + wall.classList.add("wall"); + Object.assign(wall.style, { + left: `${data.x}px`, + top: `${data.y}px`, + width: `${data.w}px`, + height: `${data.h}px`, + }); + maze.appendChild(wall); + walls.push(wall); + }); +} + +function resetGame() { + player.style.left = "10px"; + player.style.top = "10px"; + x = 10; + y = 10; + ctx.clearRect(0, 0, trailCanvas.width, trailCanvas.height); +} + +function startGame() { + startBtn.disabled = true; + restartBtn.disabled = false; + message.textContent = "Navigate carefully!"; + resetGame(); + gameActive = true; +} + +function restartGame() { + message.textContent = "Maze restarted!"; + resetGame(); +} + +function movePlayer(e) { + if (!gameActive) return; + + let nextX = x; + let nextY = y; + + if (e.key === "ArrowUp") nextY -= speed; + if (e.key === "ArrowDown") nextY += speed; + if (e.key === "ArrowLeft") nextX -= speed; + if (e.key === "ArrowRight") nextX += speed; + + if (!checkCollision(nextX, nextY)) { + x = nextX; + y = nextY; + player.style.left = x + "px"; + player.style.top = y + "px"; + drawTrail(); + } + + if (reachedGoal()) { + message.textContent = "โœจ You escaped the maze!"; + gameActive = false; + } +} + +function checkCollision(nextX, nextY) { + const playerRect = { + left: nextX, + top: nextY, + right: nextX + 20, + bottom: nextY + 20, + }; + + return walls.some(wall => { + const rect = wall.getBoundingClientRect(); + const mazeRect = maze.getBoundingClientRect(); + const wx = rect.left - mazeRect.left; + const wy = rect.top - mazeRect.top; + const wr = wx + rect.width; + const wb = wy + rect.height; + + return ( + playerRect.right > wx && + playerRect.left < wr && + playerRect.bottom > wy && + playerRect.top < wb + ); + }); +} + +function reachedGoal() { + const playerRect = player.getBoundingClientRect(); + const goalRect = goal.getBoundingClientRect(); + return !( + playerRect.right < goalRect.left || + playerRect.left > goalRect.right || + playerRect.bottom < goalRect.top || + playerRect.top > goalRect.bottom + ); +} + +function drawTrail() { + ctx.fillStyle = "rgba(56, 189, 248, 0.4)"; + ctx.beginPath(); + ctx.arc(x + 10, y + 10, 6, 0, Math.PI * 2); + ctx.fill(); +} + +document.addEventListener("keydown", movePlayer); +startBtn.addEventListener("click", startGame); +restartBtn.addEventListener("click", restartGame); + +createWalls(); +resetGame(); diff --git a/games/Luma Maze/style.css b/games/Luma Maze/style.css new file mode 100644 index 00000000..7dbe6b0f --- /dev/null +++ b/games/Luma Maze/style.css @@ -0,0 +1,86 @@ +:root { + --bg: radial-gradient(circle at top, #0f172a, #1e293b, #334155); + --wall: #0ea5e9; + --player: #38bdf8; + --goal: #a855f7; +} + +body { + background: var(--bg); + color: white; + font-family: "Poppins", sans-serif; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + margin: 0; +} + +.container { + text-align: center; + width: 360px; +} + +h1 { + color: #67e8f9; +} + +#maze { + position: relative; + width: 300px; + height: 300px; + margin: 20px auto; + border: 2px solid #22d3ee; + box-shadow: 0 0 30px #0891b2; + overflow: hidden; + background: rgba(15, 23, 42, 0.9); +} + +#player { + position: absolute; + width: 20px; + height: 20px; + background: var(--player); + border-radius: 50%; + box-shadow: 0 0 15px var(--player); +} + +#goal { + position: absolute; + bottom: 20px; + right: 20px; + width: 25px; + height: 25px; + border-radius: 50%; + background: var(--goal); + box-shadow: 0 0 20px var(--goal); +} + +.wall { + position: absolute; + background: var(--wall); + box-shadow: 0 0 15px var(--wall); +} + +#trail { + position: absolute; + top: 0; + left: 0; + pointer-events: none; +} + +.controls button { + background: linear-gradient(90deg, #38bdf8, #818cf8); + border: none; + color: white; + padding: 10px 15px; + font-size: 1rem; + border-radius: 8px; + margin: 5px; + cursor: pointer; + font-weight: 600; +} + +.controls button:disabled { + opacity: 0.5; +} diff --git a/games/Memory matrix/index.html b/games/Memory matrix/index.html new file mode 100644 index 00000000..6bc137cb --- /dev/null +++ b/games/Memory matrix/index.html @@ -0,0 +1,30 @@ + + + + + + Memory Matrix + + + +
+

๐Ÿง  Memory Matrix

+

Click "Start" to begin!

+ +
+ +
+ + + +
+ +
+ Level: 1 + Score: 0 +
+
+ + + + diff --git a/games/Memory matrix/script.js b/games/Memory matrix/script.js new file mode 100644 index 00000000..cf526b48 --- /dev/null +++ b/games/Memory matrix/script.js @@ -0,0 +1,120 @@ +let level = 1; +let score = 0; +let pattern = []; +let playerMoves = []; +let canClick = false; + +const gridEl = document.getElementById("grid"); +const startBtn = document.getElementById("startBtn"); +const nextBtn = document.getElementById("nextBtn"); +const retryBtn = document.getElementById("retryBtn"); +const statusEl = document.getElementById("status"); +const levelEl = document.getElementById("level"); +const scoreEl = document.getElementById("score"); + +startBtn.addEventListener("click", startGame); +nextBtn.addEventListener("click", nextLevel); +retryBtn.addEventListener("click", retryLevel); + +function startGame() { + level = 1; + score = 0; + levelEl.textContent = level; + scoreEl.textContent = score; + nextBtn.disabled = true; + retryBtn.disabled = true; + createGrid(level + 2); + startPattern(); +} + +function nextLevel() { + level++; + levelEl.textContent = level; + nextBtn.disabled = true; + retryBtn.disabled = true; + createGrid(level + 2); + startPattern(); +} + +function retryLevel() { + nextBtn.disabled = true; + retryBtn.disabled = true; + createGrid(level + 2); + startPattern(); +} + +function createGrid(size) { + gridEl.innerHTML = ""; + gridEl.style.gridTemplateColumns = `repeat(${size}, 1fr)`; + for (let i = 0; i < size * size; i++) { + const cell = document.createElement("div"); + cell.classList.add("cell"); + cell.dataset.index = i; + cell.addEventListener("click", handleClick); + gridEl.appendChild(cell); + } +} + +function startPattern() { + const cells = [...gridEl.children]; + const gridSize = Math.sqrt(cells.length); + const patternCount = Math.min(gridSize + 1, cells.length); + pattern = []; + + // Choose random unique pattern + while (pattern.length < patternCount) { + const r = Math.floor(Math.random() * cells.length); + if (!pattern.includes(r)) pattern.push(r); + } + + statusEl.textContent = "Memorize the glowing tiles!"; + showPattern(cells); +} + +function showPattern(cells) { + let i = 0; + canClick = false; + playerMoves = []; + + const interval = setInterval(() => { + if (i > 0) cells[pattern[i - 1]].classList.remove("active"); + if (i < pattern.length) { + cells[pattern[i]].classList.add("active"); + i++; + } else { + clearInterval(interval); + setTimeout(() => { + cells.forEach((c) => c.classList.remove("active")); + statusEl.textContent = "Now reproduce the pattern!"; + canClick = true; + }, 400); + } + }, 600); +} + +function handleClick(e) { + if (!canClick) return; + const index = Number(e.target.dataset.index); + const cell = e.target; + + playerMoves.push(index); + const currentIndex = playerMoves.length - 1; + + if (index === pattern[currentIndex]) { + cell.classList.add("correct"); + setTimeout(() => cell.classList.remove("correct"), 300); + if (playerMoves.length === pattern.length) { + score += level * 10; + scoreEl.textContent = score; + statusEl.textContent = "โœ… Correct! Get ready for the next level!"; + canClick = false; + nextBtn.disabled = false; + retryBtn.disabled = true; + } + } else { + cell.classList.add("wrong"); + statusEl.textContent = "โŒ Wrong tile! Try again."; + canClick = false; + retryBtn.disabled = false; + } +} diff --git a/games/Memory matrix/style.css b/games/Memory matrix/style.css new file mode 100644 index 00000000..1c4d47a4 --- /dev/null +++ b/games/Memory matrix/style.css @@ -0,0 +1,96 @@ +:root { + --bg: #070b1a; + --accent: #7dd3fc; + --wrong: #fb7185; + --good: #34d399; + font-family: 'Poppins', sans-serif; +} + +body { + background: radial-gradient(circle at top, #0b132b, #000); + color: #dbeafe; + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + margin: 0; +} + +.container { + text-align: center; + width: 90%; + max-width: 500px; +} + +h1 { + color: var(--accent); + margin-bottom: 10px; + font-size: 2em; +} + +#status { + color: #9ca3af; + margin-bottom: 20px; +} + +.grid { + display: grid; + gap: 8px; + justify-content: center; + margin: 20px auto; +} + +.cell { + width: 60px; + height: 60px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.06); + box-shadow: 0 0 6px rgba(255, 255, 255, 0.05); + cursor: pointer; + transition: background 0.25s, transform 0.2s; +} + +.cell:hover { + transform: scale(1.05); +} + +.cell.active { + background: var(--accent); + box-shadow: 0 0 18px var(--accent); +} + +.cell.correct { + background: var(--good); + box-shadow: 0 0 18px var(--good); +} + +.cell.wrong { + background: var(--wrong); + box-shadow: 0 0 18px var(--wrong); +} + +.controls button { + margin: 5px; + padding: 10px 16px; + font-size: 1rem; + border: none; + border-radius: 8px; + background: linear-gradient(90deg, var(--accent), #a78bfa); + color: #06121f; + font-weight: 600; + cursor: pointer; + transition: opacity 0.2s; +} + +.controls button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.info { + margin-top: 15px; + color: #93c5fd; + display: flex; + justify-content: space-around; + font-size: 1.1em; +} diff --git a/games/Memory-Game/index.html b/games/Memory-Game/index.html new file mode 100644 index 00000000..e9662f84 --- /dev/null +++ b/games/Memory-Game/index.html @@ -0,0 +1,15 @@ + + + + + + Memory Flip Game + + + +

๐Ÿง  Memory Flip Game

+
+

+ + + diff --git a/games/Memory-Game/readme.md b/games/Memory-Game/readme.md new file mode 100644 index 00000000..1754772f --- /dev/null +++ b/games/Memory-Game/readme.md @@ -0,0 +1,44 @@ +# Memory Flip Game + +A fun and simple browser-based memory puzzle game built with **HTML**, **CSS**, and **JavaScript**. +Test your memory skills by matching pairs of emoji cards! + +--- + +## Game Details + +**Name:** Memory Flip Game +**Description:** +Flip over two cards at a time to find matching pairs. Remember where each emoji is hidden and match all pairs to win the game. The cards shuffle every time you reload, making each game unique! + +--- + +## How to Play + +1. Click on a card to flip it and reveal the emoji. +2. Flip another card โ€” if both match, they stay open. +3. If not, they flip back after a short delay. +4. Keep matching until all pairs are revealed. +5. When all matches are found, youโ€™ll see a **๐ŸŽ‰ victory message!** + +--- + +## Files Included + +| File | Description | +|------|--------------| +| `index.html` | Sets up the game structure and layout. | +| `style.css` | Handles the grid design, card styles, and flip effects. | +| `script.js` | Contains the main game logic (shuffle, flip, and match checking). | + +--- + +## Features + +- Smooth card flip animation +- Randomized card layout on each refresh +- Clean and responsive UI +- Simple emoji-based visuals +- Instant win message when all pairs are matched + +--- diff --git a/games/Memory-Game/script.js b/games/Memory-Game/script.js new file mode 100644 index 00000000..0db2aab4 --- /dev/null +++ b/games/Memory-Game/script.js @@ -0,0 +1,50 @@ +const emojis = ["๐ŸŽ", "๐ŸŒ", "๐Ÿ‡", "๐Ÿ’", "๐Ÿ‰", "๐Ÿ", "๐Ÿ“", "๐Ÿฅ"]; +let cards = [...emojis, ...emojis]; +let flipped = []; +let matched = []; + +const board = document.getElementById("game-board"); +const statusText = document.getElementById("status"); + +shuffle(cards); +cards.forEach((emoji) => { + const card = document.createElement("div"); + card.classList.add("card"); + card.dataset.emoji = emoji; + card.addEventListener("click", flipCard); + board.appendChild(card); +}); + +function flipCard() { + if (flipped.length === 2 || this.classList.contains("flipped")) return; + this.classList.add("flipped"); + this.textContent = this.dataset.emoji; + flipped.push(this); + + if (flipped.length === 2) { + setTimeout(checkMatch, 700); + } +} + +function checkMatch() { + const [card1, card2] = flipped; + if (card1.dataset.emoji === card2.dataset.emoji) { + matched.push(card1, card2); + if (matched.length === cards.length) { + statusText.textContent = "๐ŸŽ‰ You matched them all!"; + } + } else { + card1.classList.remove("flipped"); + card2.classList.remove("flipped"); + card1.textContent = ""; + card2.textContent = ""; + } + flipped = []; +} + +function shuffle(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } +} diff --git a/games/Memory-Game/style.css b/games/Memory-Game/style.css new file mode 100644 index 00000000..1b3b5861 --- /dev/null +++ b/games/Memory-Game/style.css @@ -0,0 +1,40 @@ +body { + background: linear-gradient(135deg, #1e3c72, #2a5298); + color: white; + font-family: Arial, sans-serif; + text-align: center; + margin: 0; + padding: 20px; +} + +#game-board { + display: grid; + grid-template-columns: repeat(4, 100px); + gap: 10px; + justify-content: center; + margin-top: 40px; +} + +.card { + width: 100px; + height: 100px; + background: #444; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 2rem; + cursor: pointer; + transition: transform 0.3s, background 0.3s; +} + +.card.flipped { + background: white; + color: black; + transform: rotateY(180deg); +} + +#status { + margin-top: 20px; + font-size: 1.2rem; +} diff --git a/games/Number Nexus/index.html b/games/Number Nexus/index.html new file mode 100644 index 00000000..b84b0b5b --- /dev/null +++ b/games/Number Nexus/index.html @@ -0,0 +1,50 @@ + + + + + + Number Nexus โ€” Sudoku + + + +
+
+

Number Nexus

+

Classic Sudoku โ€” fill every row, column & 3ร—3 box with digits 1โ€“9

+
+ +
+ + + + + + + + +
+ Time: 00:00 + Errors: 0 +
+
+ +
+ + +
+
+ +
+

Keyboard: type 1โ€“9 to fill, Backspace/Delete to clear, arrow keys move.

+
+ +
+
+ + + + diff --git a/games/Number Nexus/script.js b/games/Number Nexus/script.js new file mode 100644 index 00000000..3ccdfbca --- /dev/null +++ b/games/Number Nexus/script.js @@ -0,0 +1,398 @@ +/* Number Nexus - Sudoku (preset puzzles + solver + hint) + Features: + - Preset puzzles (easy/medium/hard) + - Render board, allow typing 1-9, arrow navigation + - Highlight conflicts in rows/cols/boxes + - Timer, error counter + - Hint fills one correct cell using solver + - Check & Solve (auto-fill) for convenience +*/ + +// ----- Preset puzzles (0 = empty) ----- +// Each is a 9x9 array flattened to 81 numbers +const PRESETS = { + easy: [ + // easy example + 0,0,3, 0,2,0, 6,0,0, + 9,0,0, 3,0,5, 0,0,1, + 0,0,1, 8,0,6, 4,0,0, + 0,0,8, 1,0,2, 9,0,0, + 7,0,0, 0,0,0, 0,0,8, + 0,0,6, 7,0,8, 2,0,0, + 0,0,2, 6,0,9, 5,0,0, + 8,0,0, 2,0,3, 0,0,9, + 0,0,5, 0,1,0, 3,0,0 + ], + medium: [ + 0,0,0, 2,6,0, 7,0,1, + 6,8,0, 0,7,0, 0,9,0, + 1,9,0, 0,0,4, 5,0,0, + 8,2,0, 1,0,0, 0,4,0, + 0,0,4, 6,0,2, 9,0,0, + 0,5,0, 0,0,3, 0,2,8, + 0,0,9, 3,0,0, 0,7,4, + 0,4,0, 0,5,0, 0,3,6, + 7,0,3, 0,1,8, 0,0,0 + ], + hard: [ + 0,0,0, 0,0,0, 0,1,2, + 0,0,0, 0,0,7, 0,0,0, + 0,0,1, 0,9,0, 5,0,0, + 0,0,0, 5,0,0, 0,0,0, + 0,4,0, 0,0,0, 0,6,0, + 0,0,0, 0,0,3, 0,0,0, + 0,0,5, 0,2,0, 3,0,0, + 0,0,0, 8,0,0, 0,0,0, + 4,2,0, 0,0,0, 0,0,0 + ] +}; + +// state +let board = new Array(81).fill(0); // current values +let given = new Array(81).fill(false); // fixed numbers +let selectedIndex = 0; +let timerInterval = null; +let secondsElapsed = 0; +let errors = 0; + +// DOM +const boardEl = document.getElementById('board'); +const newBtn = document.getElementById('newBtn'); +const checkBtn = document.getElementById('checkBtn'); +const hintBtn = document.getElementById('hintBtn'); +const solveBtn = document.getElementById('solveBtn'); +const difficultySel = document.getElementById('difficulty'); +const timerEl = document.getElementById('timer'); +const errorsEl = document.getElementById('errors'); +const messageEl = document.getElementById('message'); + +// build board table +function buildBoard(){ + boardEl.innerHTML = ''; + for(let r=0;r<9;r++){ + const tr = document.createElement('tr'); + for(let c=0;c<9;c++){ + const td = document.createElement('td'); + // add bold borders for boxes + if((c+1)%3===0 && c<8) td.classList.add('bold-right'); + if((r+1)%3===0 && r<8) td.classList.add('bold-bottom'); + + const cell = document.createElement('div'); + cell.className = 'cell'; + cell.tabIndex = 0; + cell.dataset.index = r*9 + c; + + const input = document.createElement('input'); + input.type = 'text'; + input.maxLength = 1; + input.dataset.index = r*9 + c; + + // events + cell.addEventListener('click', () => selectCell(Number(cell.dataset.index))); + input.addEventListener('keydown', onKeyDown); + input.addEventListener('input', onInput); + + cell.appendChild(input); + td.appendChild(cell); + tr.appendChild(td); + } + boardEl.appendChild(tr); + } +} + +// load preset into board +function loadPreset(preset){ + for(let i=0;i<81;i++){ + const val = preset[i] || 0; + board[i] = val; + given[i] = val !== 0; + } + renderBoard(); + resetTimer(); + startTimer(); + errors = 0; + updateMeta(); + message(''); +} + +// render board to DOM +function renderBoard(){ + const inputs = boardEl.querySelectorAll('input'); + inputs.forEach(inp => { + const idx = Number(inp.dataset.index); + const val = board[idx]; + inp.value = val === 0 ? '' : String(val); + inp.readOnly = given[idx]; + inp.className = ''; + if(given[idx]) inp.classList.add('given'); + }); + clearHighlights(); + highlightConflicts(); + highlightSelected(); +} + +// helpers: row/col/box indices +function rowOf(idx){ return Math.floor(idx/9); } +function colOf(idx){ return idx%9; } +function boxStart(idx){ const r = rowOf(idx), c = colOf(idx); return { br: Math.floor(r/3)*3, bc: Math.floor(c/3)*3 }; } + +// validation utilities +function isValidPlacement(arr, idx, val){ + if(val===0) return true; + const r = rowOf(idx), c = colOf(idx); + // row + for(let cc=0;cc<9;cc++){ + const id = r*9+cc; + if(id!==idx && arr[id]===val) return false; + } + // col + for(let rr=0;rr<9;rr++){ + const id = rr*9+c; + if(id!==idx && arr[id]===val) return false; + } + // box + const {br,bc} = boxStart(idx); + for(let rr=br; rr i.classList.remove('conflict','selected','hint')); +} + +// selection +function selectCell(idx){ + selectedIndex = idx; + const input = boardEl.querySelector(input[data-index="${idx}"]); + input.focus(); + input.select(); + clearHighlights(); + highlightSelected(); + highlightConflicts(); +} +function highlightSelected(){ + const s = boardEl.querySelector(input[data-index="${selectedIndex}"]); + if(s) s.classList.add('selected'); +} + +// keyboard input +function onKeyDown(e){ + const idx = Number(e.target.dataset.index); + if(!isFinite(idx)) return; + // navigation + if(e.key === 'ArrowRight') { moveSelection(idx, 0, 1); e.preventDefault(); return; } + if(e.key === 'ArrowLeft') { moveSelection(idx, 0, -1); e.preventDefault(); return; } + if(e.key === 'ArrowUp') { moveSelection(idx, -1, 0); e.preventDefault(); return; } + if(e.key === 'ArrowDown') { moveSelection(idx, 1, 0); e.preventDefault(); return; } + + if(e.key === 'Backspace' || e.key === 'Delete') { + setCell(idx, 0); + e.target.value = ''; + e.preventDefault(); + return; + } + if(/^[1-9]$/.test(e.key)){ + setCell(idx, Number(e.key)); + e.target.value = e.key; + e.preventDefault(); + return; + } +} +function moveSelection(idx, dr, dc){ + const r = rowOf(idx)+dr, c = colOf(idx)+dc; + if(r<0||r>8||c<0||c>8) return; + selectCell(r*9+c); +} + +// on manual input (paste or mobile) +function onInput(e){ + const val = e.target.value.trim(); + const idx = Number(e.target.dataset.index); + if(val === '') { setCell(idx, 0); return; } + const ch = val.slice(-1); + if(/^[1-9]$/.test(ch)){ + e.target.value = ch; + setCell(idx, Number(ch)); + } else { + e.target.value = ''; + setCell(idx, 0); + } +} + +// set cell value (if not given) +function setCell(idx, val){ + if(given[idx]) return; + const prev = board[idx]; + board[idx] = val; + // check validity + if(val !== 0 && !isValidPlacement(board, idx, val)){ + errors++; + errorsEl.textContent = errors; + message('Conflict detected. Check highlighted cells.', 'warn'); + } else { + message(''); + } + renderBoard(); +} + +// timer +function startTimer(){ + clearInterval(timerInterval); + secondsElapsed = 0; + timerInterval = setInterval(()=> { + secondsElapsed++; + timerEl.textContent = formatTime(secondsElapsed); + },1000); +} +function resetTimer(){ + clearInterval(timerInterval); + secondsElapsed = 0; + timerEl.textContent = formatTime(0); +} +function formatTime(s){ + const mm = String(Math.floor(s/60)).padStart(2,'0'); + const ss = String(s%60).padStart(2,'0'); + return ${mm}:${ss}; +} + +// --- solver (backtracking) --- +function findEmpty(arr){ + for(let i=0;i<81;i++) if(arr[i]===0) return i; + return -1; +} +function solveSudoku(arr){ + const idx = findEmpty(arr); + if(idx === -1) return true; + for(let num=1;num<=9;num++){ + if(isValidPlacement(arr, idx, num)){ + arr[idx] = num; + if(solveSudoku(arr)) return true; + arr[idx] = 0; + } + } + return false; +} +function getSolution(){ + const copy = board.slice(); + if(solveSudoku(copy)) return copy; + // try solving from original preset (some puzzles are solvable) + return null; +} + +// hint: fill one empty cell with a correct number using solver from the preset state +function giveHint(){ + // use the solved version of the ORIGINAL preset (reconstruct by applying given numbers) + const preset = board.map((v,i)=> given[i] ? v : 0); + const sol = preset.slice(); + if(!solveSudoku(sol)){ + message('No solution available for hint.', 'warn'); + return; + } + // find an index that's empty in current board and fill with sol value + const empties = []; + for(let i=0;i<81;i++) if(board[i]===0) empties.push(i); + if(empties.length===0){ message('Board already full.'); return; } + const idx = empties[Math.floor(Math.random()*empties.length)]; + board[idx] = sol[idx]; + message('Filled one correct cell (hint).'); + renderBoard(); +} + +// check correctness +function checkBoard(){ + // any zero? if so warn + if(board.some(v=>v===0)){ message('Board incomplete โ€” fill all cells before final check.','warn'); return; } + // validate all placements + for(let i=0;i<81;i++){ + if(!isValidPlacement(board, i, board[i])) { + message('There are conflicts โ€” find highlighted cells.','warn'); + return; + } + } + // solved + clearInterval(timerInterval); + message(๐ŸŽ‰ Puzzle solved! Time: ${formatTime(secondsElapsed)} Errors: ${errors}, 'success'); +} + +// solve (fill entire board using solver) +function solveAndFill(){ + // start from given cells only so we avoid overwriting user entries with contradictory values + const preset = board.map((v,i)=> given[i] ? v : 0); + const sol = preset.slice(); + if(!solveSudoku(sol)){ message('No valid solution found.', 'warn'); return; } + board = sol; + renderBoard(); + clearInterval(timerInterval); + message('Solved (filled entire board).', 'success'); +} + +// new puzzle loader +function newPuzzle(){ + const diff = difficultySel.value; + const arr = PRESETS[diff]; + if(!arr) return; + loadPreset(arr); + message('New puzzle loaded. Good luck!'); +} + +// message helper +function message(txt='', type=''){ + messageEl.textContent = txt; + messageEl.className = 'message' + (type ? ' ' + (type==='success' ? 'success' : 'warn') : ''); +} + +// update meta +function updateMeta(){ errorsEl.textContent = errors; timerEl.textContent = formatTime(secondsElapsed); } + +// wire up +buildBoard(); +newBtn.addEventListener('click', newPuzzle); +checkBtn.addEventListener('click', checkBoard); +hintBtn.addEventListener('click', giveHint); +solveBtn.addEventListener('click', solveAndFill); + +// start with medium preset +loadPreset(PRESETS['medium']); + +// allow clicking cell to select +boardEl.addEventListener('click', (e) => { + const t = e.target.closest('.cell'); + if(!t) return; + const idx = Number(t.dataset.index); + selectCell(idx); +}); diff --git a/games/Number Nexus/style.css b/games/Number Nexus/style.css new file mode 100644 index 00000000..d42d00dc --- /dev/null +++ b/games/Number Nexus/style.css @@ -0,0 +1,49 @@ +:root{ + --bg1:#071023; --panel:#0b1220; --accent:#60a5fa; --good:#34d399; --bad:#fb7185; + --cell:#e6eef8; --given:#9fb4d9; + --glass: rgba(255,255,255,0.03); + font-family: Inter, Poppins, system-ui, -apple-system, "Segoe UI", Roboto, Arial; +} +*{box-sizing:border-box} +html,body{height:100%;margin:0;background:linear-gradient(180deg,var(--bg1),#021021);color:#e6eef8} +.app{max-width:920px;margin:28px auto;padding:18px} +header{ text-align:center } +h1{margin:0;font-size:28px;color:var(--accent)} +.subtitle{margin:6px 0 12px;color:#9fb4d9} + +.controls{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between;background:var(--glass);padding:12px;border-radius:10px;border:1px solid rgba(255,255,255,0.04)} +.controls select, .controls button{padding:8px 12px;border-radius:8px;border:none;background:linear-gradient(90deg,#7dd3fc,#a78bfa);color:#062033;font-weight:600;cursor:pointer} +.controls button:disabled{opacity:0.5;cursor:not-allowed} +.meta{display:flex;gap:16px;color:#cfe6ff;font-weight:700} + +.board-wrap{display:flex;justify-content:center;margin-top:18px} +.board{border-collapse:collapse;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));padding:6px;border-radius:8px} +.board td{width:48px;height:48px;border:1px solid rgba(255,255,255,0.06);text-align:center;vertical-align:middle;position:relative} +.cell{ + width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:20px;color:var(--cell); + cursor:pointer; user-select:none; + background:transparent;border-radius:4px; +} +.cell input{ + width:100%;height:100%;text-align:center;border:none;background:transparent;color:inherit;font-size:20px;outline:none; +} +.given{color:var(--given);font-weight:700} +.subtle{opacity:0.18} +.bold-right{border-right:3px solid rgba(255,255,255,0.08)} +.bold-bottom{border-bottom:3px solid rgba(255,255,255,0.08)} +.selected{background:linear-gradient(90deg, rgba(96,165,250,0.08), rgba(167,139,250,0.06));box-shadow:inset 0 0 12px rgba(96,165,250,0.03)} +.conflict{background:linear-gradient(90deg, rgba(251,113,133,0.12), rgba(251,113,133,0.06));outline:2px solid rgba(251,113,133,0.08)} +.hint{background:linear-gradient(90deg, rgba(34,197,94,0.12), rgba(34,197,94,0.06))} + +.message{margin-top:12px;padding:10px;border-radius:8px;min-height:36px} +.message.success{background:linear-gradient(90deg, rgba(34,197,94,0.08), rgba(34,197,94,0.02));color:var(--good)} +.message.warn{background:linear-gradient(90deg, rgba(250,204,21,0.06), rgba(250,204,21,0.02));color:#fbbf24} + +.notes{margin-top:12px;color:#9fb4d9;font-size:13px;text-align:center} + +/* responsive */ +@media (max-width:560px){ + .board td{width:36px;height:36px} + .cell input{font-size:16px} + .controls{gap:8px} +} diff --git a/games/Number-Pop/index.html b/games/Number-Pop/index.html new file mode 100644 index 00000000..839bb57f --- /dev/null +++ b/games/Number-Pop/index.html @@ -0,0 +1,34 @@ + + + + + + Number Pop | Mini JS Games Hub + + + +
+
+

Number Pop

+

Click only the even numbers! Avoid odd numbers.

+
+ Score: 0 + High Score: 0 + Time: 60s +
+
+ +
+ +
+ +
+ + +

+
+
+ + + + diff --git a/games/Number-Pop/script.js b/games/Number-Pop/script.js new file mode 100644 index 00000000..f99e3330 --- /dev/null +++ b/games/Number-Pop/script.js @@ -0,0 +1,84 @@ +const gameArea = document.getElementById('game-area'); +const scoreEl = document.getElementById('score'); +const highScoreEl = document.getElementById('high-score'); +const timeEl = document.getElementById('time'); +const startBtn = document.getElementById('start-btn'); +const restartBtn = document.getElementById('restart-btn'); +const messageEl = document.getElementById('message'); + +let score = 0; +let highScore = localStorage.getItem('numberPopHighScore') || 0; +let time = 60; +let gameInterval; +let spawnInterval; + +highScoreEl.textContent = highScore; + +function spawnNumber() { + const numberEl = document.createElement('div'); + const num = Math.floor(Math.random() * 9) + 1; + numberEl.textContent = num; + numberEl.classList.add('number'); + numberEl.classList.add(num % 2 === 0 ? 'even' : 'odd'); + + const x = Math.random() * (gameArea.offsetWidth - 50); + const y = Math.random() * (gameArea.offsetHeight - 50); + numberEl.style.left = `${x}px`; + numberEl.style.top = `${y}px`; + + numberEl.addEventListener('click', () => { + if (num % 2 === 0) { + score += 1; + scoreEl.textContent = score; + } else { + score = Math.max(0, score - 1); + scoreEl.textContent = score; + } + gameArea.removeChild(numberEl); + }); + + gameArea.appendChild(numberEl); + + setTimeout(() => { + if (gameArea.contains(numberEl)) { + gameArea.removeChild(numberEl); + } + }, 1500); +} + +function startGame() { + score = 0; + time = 60; + scoreEl.textContent = score; + timeEl.textContent = time; + messageEl.textContent = ''; + startBtn.style.display = 'none'; + restartBtn.style.display = 'none'; + gameArea.innerHTML = ''; + + spawnInterval = setInterval(spawnNumber, 800); + + gameInterval = setInterval(() => { + time -= 1; + timeEl.textContent = time; + if (time <= 0) { + endGame(); + } + }, 1000); +} + +function endGame() { + clearInterval(gameInterval); + clearInterval(spawnInterval); + gameArea.innerHTML = ''; + messageEl.textContent = `Game Over! Your score: ${score}`; + if (score > highScore) { + highScore = score; + localStorage.setItem('numberPopHighScore', highScore); + highScoreEl.textContent = highScore; + } + restartBtn.style.display = 'inline-block'; +} + +startBtn.addEventListener('click', startGame); +restartBtn.addEventListener('click', startGame); diff --git a/games/Number-Pop/style.css b/games/Number-Pop/style.css new file mode 100644 index 00000000..6c8da58c --- /dev/null +++ b/games/Number-Pop/style.css @@ -0,0 +1,89 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + margin: 0; + padding: 0; + background: linear-gradient(to bottom, #4facfe, #00f2fe); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + background-color: #fff; + width: 100%; + max-width: 500px; + border-radius: 15px; + box-shadow: 0 8px 20px rgba(0,0,0,0.25); + padding: 20px; + text-align: center; +} + +header h1 { + margin: 0; + font-size: 2em; + color: #333; +} + +header p { + color: #555; +} + +.scoreboard { + display: flex; + justify-content: space-around; + margin: 15px 0; + font-weight: bold; + font-size: 1.1em; +} + +#game-area { + position: relative; + height: 300px; + background-color: #f0f8ff; + border-radius: 10px; + overflow: hidden; + margin-bottom: 15px; +} + +.number { + position: absolute; + font-size: 1.5em; + font-weight: bold; + padding: 10px 15px; + border-radius: 50%; + cursor: pointer; + transition: transform 0.2s ease, opacity 0.2s ease; +} + +.number.even { + background-color: #4caf50; + color: #fff; +} + +.number.odd { + background-color: #f44336; + color: #fff; +} + +footer button { + padding: 10px 20px; + margin: 5px; + font-size: 1em; + cursor: pointer; + border: none; + border-radius: 8px; + background: #2196f3; + color: #fff; + transition: background 0.2s ease; +} + +footer button:hover { + background: #1976d2; +} + +#message { + margin-top: 10px; + font-weight: bold; + color: #333; +} diff --git a/games/Number_Gussing_game/NGG.css b/games/Number_Gussing_game/NGG.css new file mode 100644 index 00000000..f5f866ea --- /dev/null +++ b/games/Number_Gussing_game/NGG.css @@ -0,0 +1,183 @@ + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + } + + .container { + background: rgba(255, 255, 255, 0.95); + padding: 40px; + border-radius: 20px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + max-width: 500px; + width: 100%; + text-align: center; + position: relative; + overflow: hidden; + } + + h1 { + color: #667eea; + margin-bottom: 10px; + font-size: 2.5em; + } + + .subtitle { + color: #666; + margin-bottom: 30px; + font-size: 1.1em; + } + + .game-info { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 20px; + border-radius: 15px; + margin-bottom: 30px; + } + + .attempts { + font-size: 1.2em; + font-weight: bold; + } + + .input-section { + margin-bottom: 20px; + } + + input[type="number"] { + width: 100%; + padding: 15px; + font-size: 1.2em; + border: 3px solid #667eea; + border-radius: 10px; + text-align: center; + transition: all 0.3s; + } + + input[type="number"]:focus { + outline: none; + border-color: #764ba2; + box-shadow: 0 0 20px rgba(102, 126, 234, 0.3); + } + + .btn { + padding: 15px 40px; + font-size: 1.1em; + border: none; + border-radius: 10px; + cursor: pointer; + transition: all 0.3s; + font-weight: bold; + margin: 5px; + } + + .btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + } + + .btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4); + } + + .btn-secondary { + background: #f0f0f0; + color: #666; + } + + .btn-secondary:hover { + background: #e0e0e0; + transform: translateY(-2px); + } + + .feedback { + margin: 20px 0; + padding: 20px; + border-radius: 10px; + font-size: 1.3em; + font-weight: bold; + transition: all 0.3s; + } + + .feedback.higher { + background: #ffe0e0; + color: #d32f2f; + } + + .feedback.lower { + background: #e0f2ff; + color: #1976d2; + } + + .feedback.correct { + background: #e0ffe0; + color: #388e3c; + } + + .hidden { + display: none; + } + + .balloon { + position: absolute; + width: 60px; + height: 80px; + border-radius: 50% 50% 45% 45%; + animation: float 3s ease-in-out; + pointer-events: none; + } + + .balloon::after { + content: ''; + position: absolute; + width: 2px; + height: 30px; + background: rgba(0, 0, 0, 0.2); + bottom: -30px; + left: 50%; + } + + @keyframes float { + 0% { + transform: translateY(0) rotate(0deg); + opacity: 1; + } + 100% { + transform: translateY(-600px) rotate(20deg); + opacity: 0; + } + } + + .celebration { + font-size: 3em; + margin: 20px 0; + } + + .result-card { + background: linear-gradient(135deg, #ffd89b 0%, #19547b 100%); + color: white; + padding: 30px; + border-radius: 15px; + margin: 20px 0; + } + + .result-card h2 { + margin-bottom: 10px; + } + + .result-card .score { + font-size: 3em; + font-weight: bold; + } \ No newline at end of file diff --git a/games/Number_Gussing_game/NGG.html b/games/Number_Gussing_game/NGG.html new file mode 100644 index 00000000..ec6118e0 --- /dev/null +++ b/games/Number_Gussing_game/NGG.html @@ -0,0 +1,43 @@ + + + + + + + Number Guessing Game + + +
+

๐ŸŽฏ Guess the Number

+

I'm thinking of a number between 0 and 100

+ +
+
Attempts: 0
+
+ +
+
+ +
+ + + + +
+ + + + +
+ + + \ No newline at end of file diff --git a/games/Number_Gussing_game/NGG.js b/games/Number_Gussing_game/NGG.js new file mode 100644 index 00000000..abb9db13 --- /dev/null +++ b/games/Number_Gussing_game/NGG.js @@ -0,0 +1,90 @@ +let targetNumber; +let attempts; + +function initGame() { + targetNumber = Math.floor(Math.random() * 101); + attempts = 0; + document.getElementById('attempts').textContent = attempts; + document.getElementById('guessInput').value = ''; + document.getElementById('feedback').classList.add('hidden'); + document.getElementById('gameArea').classList.remove('hidden'); + document.getElementById('resultArea').classList.add('hidden'); + // Remove any previous balloons + const container = document.querySelector('.container'); + if (container) { + container.querySelectorAll('.balloon').forEach(b => b.remove()); + } + + document.getElementById('guessInput').focus(); +} + +function checkGuess() { + const input = document.getElementById('guessInput'); + const guess = parseInt(input.value); + const feedback = document.getElementById('feedback'); + + if (isNaN(guess) || guess < 0 || guess > 100) { + feedback.className = 'feedback higher'; + feedback.textContent = 'โš ๏ธ Please enter a valid number between 0 and 100'; + feedback.classList.remove('hidden'); + return; + } + + attempts++; + document.getElementById('attempts').textContent = attempts; + + if (guess === targetNumber) { + winGame(); + } else if (guess < targetNumber) { + feedback.className = 'feedback lower'; + feedback.textContent = '๐Ÿ“ˆ Too Low! Try a higher number'; + feedback.classList.remove('hidden'); + } else { + feedback.className = 'feedback higher'; + feedback.textContent = '๐Ÿ“‰ Too High! Try a lower number'; + feedback.classList.remove('hidden'); + } + + input.value = ''; + input.focus(); +} + +function winGame() { + document.getElementById('gameArea').classList.add('hidden'); + document.getElementById('resultArea').classList.remove('hidden'); + document.getElementById('finalAttempts').textContent = attempts; + + createBalloons(); +} + +function createBalloons() { + const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24', '#6c5ce7', '#fd79a8']; + const container = document.querySelector('.container'); + + for (let i = 0; i < 15; i++) { + setTimeout(() => { + const balloon = document.createElement('div'); + balloon.className = 'balloon'; + balloon.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)]; + balloon.style.left = Math.random() * 100 + '%'; + balloon.style.bottom = '0'; + container.appendChild(balloon); + + setTimeout(() => { + balloon.remove(); + }, 3000); + }, i * 200); + } +} + +function resetGame() { + initGame(); +} + +document.getElementById('guessInput').addEventListener('keypress', function (e) { + if (e.key === 'Enter') { + checkGuess(); + } +}); + +initGame(); \ No newline at end of file diff --git a/games/Orb Pop Mania/index.html b/games/Orb Pop Mania/index.html new file mode 100644 index 00000000..ca8ddfe6 --- /dev/null +++ b/games/Orb Pop Mania/index.html @@ -0,0 +1,18 @@ + + + + + + Orb Pop Mania + + + +

๐Ÿชฉ Orb Pop Mania

+ +
+

Score: 0

+ +
+ + + diff --git a/games/Orb Pop Mania/script.js b/games/Orb Pop Mania/script.js new file mode 100644 index 00000000..cd305a69 --- /dev/null +++ b/games/Orb Pop Mania/script.js @@ -0,0 +1,124 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +const scoreDisplay = document.getElementById("score"); +const restartBtn = document.getElementById("restartBtn"); + +const bubbles = []; +const colors = ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"]; +let shooter = { x: 240, y: 600, radius: 10, color: "#ff69b4", angle: 0 }; +let firedBubble = null; +let score = 0; + +function createBubble(x, y) { + return { + x, + y, + radius: 15, + color: colors[Math.floor(Math.random() * colors.length)], + }; +} + +function initGrid() { + for (let row = 0; row < 6; row++) { + for (let col = 0; col < 8; col++) { + bubbles.push(createBubble(30 + col * 55, 30 + row * 55)); + } + } +} + +function drawBubbles() { + for (let b of bubbles) { + ctx.beginPath(); + ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2); + ctx.fillStyle = b.color; + ctx.fill(); + } +} + +function drawShooter() { + ctx.beginPath(); + ctx.arc(shooter.x, shooter.y, shooter.radius, 0, Math.PI * 2); + ctx.fillStyle = shooter.color; + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(shooter.x, shooter.y); + ctx.lineTo( + shooter.x + Math.cos(shooter.angle) * 30, + shooter.y + Math.sin(shooter.angle) * 30 + ); + ctx.strokeStyle = "#fff"; + ctx.stroke(); +} + +function shootBubble() { + if (!firedBubble) { + firedBubble = { + x: shooter.x, + y: shooter.y, + radius: 15, + color: shooter.color, + dx: Math.cos(shooter.angle) * 6, + dy: Math.sin(shooter.angle) * 6, + }; + } +} + +function drawFiredBubble() { + if (firedBubble) { + firedBubble.x += firedBubble.dx; + firedBubble.y += firedBubble.dy; + + if (firedBubble.x < 15 || firedBubble.x > canvas.width - 15) + firedBubble.dx *= -1; + + for (let b of bubbles) { + const dist = Math.hypot(firedBubble.x - b.x, firedBubble.y - b.y); + if (dist < b.radius + firedBubble.radius) { + const idx = bubbles.indexOf(b); + bubbles.splice(idx, 1); + score += 10; + scoreDisplay.textContent = score; + firedBubble = null; + shooter.color = colors[Math.floor(Math.random() * colors.length)]; + return; + } + } + + if (firedBubble.y < 20) firedBubble = null; + + ctx.beginPath(); + ctx.arc(firedBubble.x, firedBubble.y, firedBubble.radius, 0, Math.PI * 2); + ctx.fillStyle = firedBubble.color; + ctx.fill(); + } +} + +canvas.addEventListener("mousemove", (e) => { + const rect = canvas.getBoundingClientRect(); + const dx = e.clientX - rect.left - shooter.x; + const dy = e.clientY - rect.top - shooter.y; + shooter.angle = Math.atan2(dy, dx); +}); + +canvas.addEventListener("click", shootBubble); + +restartBtn.addEventListener("click", () => { + bubbles.length = 0; + firedBubble = null; + score = 0; + scoreDisplay.textContent = score; + initGrid(); +}); + +function animate() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawBubbles(); + drawShooter(); + drawFiredBubble(); + requestAnimationFrame(animate); +} + +initGrid(); +animate(); diff --git a/games/Orb Pop Mania/style.css b/games/Orb Pop Mania/style.css new file mode 100644 index 00000000..f4cdbc51 --- /dev/null +++ b/games/Orb Pop Mania/style.css @@ -0,0 +1,37 @@ +body { + background: radial-gradient(circle at top, #83a4d4, #b6fbff); + text-align: center; + font-family: "Poppins", sans-serif; + color: #fff; + margin: 0; + padding: 10px; +} + +h1 { + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +#gameCanvas { + background: #1e1e2f; + border: 4px solid #fff; + border-radius: 10px; +} + +#info { + margin-top: 10px; +} + +button { + background: #ff69b4; + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + transition: 0.3s; +} + +button:hover { + background: #ff8fc1; +} diff --git a/games/Pattern Pulse/index.html b/games/Pattern Pulse/index.html new file mode 100644 index 00000000..56fbf022 --- /dev/null +++ b/games/Pattern Pulse/index.html @@ -0,0 +1,30 @@ + + + + + + Pattern Pulse + + + +
+

๐ŸŽต Pattern Pulse

+

Press Start to begin!

+ +
+
+
+
+
+
+ +
+ + +

Score: 0

+
+
+ + + + diff --git a/games/Pattern Pulse/script.js b/games/Pattern Pulse/script.js new file mode 100644 index 00000000..8585cd48 --- /dev/null +++ b/games/Pattern Pulse/script.js @@ -0,0 +1,100 @@ +const pads = document.querySelectorAll(".pad"); +const startBtn = document.getElementById("startBtn"); +const restartBtn = document.getElementById("restartBtn"); +const message = document.getElementById("message"); +const scoreEl = document.getElementById("score"); + +let sequence = []; +let userSequence = []; +let score = 0; +let round = 0; +let active = false; + +// Play tone for a specific frequency +function playSound(freq) { + const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + const oscillator = audioCtx.createOscillator(); + const gainNode = audioCtx.createGain(); + oscillator.connect(gainNode); + gainNode.connect(audioCtx.destination); + oscillator.type = "sine"; + oscillator.frequency.value = freq; + oscillator.start(); + gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime); + oscillator.stop(audioCtx.currentTime + 0.2); +} + +// Flash effect +function flashPad(pad) { + pad.classList.add("active"); + playSound(pad.dataset.sound); + setTimeout(() => pad.classList.remove("active"), 300); +} + +// Generate a new step in the pattern +function addToSequence() { + const randomPad = pads[Math.floor(Math.random() * pads.length)]; + sequence.push(randomPad); +} + +// Play full pattern sequence +function playSequence() { + let delay = 600; + sequence.forEach((pad, i) => { + setTimeout(() => flashPad(pad), i * delay); + }); +} + +// Start the game +function startGame() { + sequence = []; + userSequence = []; + score = 0; + round = 0; + active = true; + startBtn.disabled = true; + restartBtn.disabled = false; + message.textContent = "Watch carefully..."; + nextRound(); +} + +// Next round +function nextRound() { + userSequence = []; + addToSequence(); + round++; + message.textContent = `Round ${round}`; + playSequence(); +} + +// Check user input +function handleUserInput(pad) { + if (!active) return; + const index = userSequence.length; + userSequence.push(pad); + flashPad(pad); + + if (pad !== sequence[index]) { + message.textContent = "โŒ Wrong pattern! Game Over!"; + active = false; + startBtn.disabled = false; + restartBtn.disabled = true; + return; + } + + if (userSequence.length === sequence.length) { + score++; + scoreEl.textContent = score; + setTimeout(nextRound, 1000); + } +} + +// Restart the game +function restartGame() { + startGame(); +} + +// Event listeners +pads.forEach(pad => pad.addEventListener("click", () => handleUserInput(pad))); +startBtn.addEventListener("click", startGame); +restartBtn.addEventListener("click", restartGame); diff --git a/games/Pattern Pulse/style.css b/games/Pattern Pulse/style.css new file mode 100644 index 00000000..66704d89 --- /dev/null +++ b/games/Pattern Pulse/style.css @@ -0,0 +1,69 @@ +body { + margin: 0; + height: 100vh; + background: radial-gradient(circle at center, #0f172a, #1e3a8a); + font-family: "Poppins", sans-serif; + color: #f8fafc; + display: flex; + justify-content: center; + align-items: center; +} + +.game-container { + text-align: center; + background: rgba(255, 255, 255, 0.08); + border-radius: 20px; + padding: 30px; + box-shadow: 0 0 25px rgba(59, 130, 246, 0.5); + width: 350px; +} + +h1 { + margin-bottom: 10px; + color: #93c5fd; + text-shadow: 0 0 15px #60a5fa; +} + +.pads { + display: grid; + grid-template-columns: repeat(2, 120px); + gap: 20px; + justify-content: center; + margin: 20px 0; +} + +.pad { + width: 100px; + height: 100px; + border-radius: 15px; + cursor: pointer; + transition: all 0.3s; + box-shadow: 0 0 15px rgba(255, 255, 255, 0.2); +} + +#pad1 { background: #ef4444; } +#pad2 { background: #facc15; } +#pad3 { background: #22c55e; } +#pad4 { background: #3b82f6; } + +.pad.active { + box-shadow: 0 0 30px white; + transform: scale(1.1); + opacity: 0.9; +} + +button { + background: linear-gradient(90deg, #3b82f6, #2563eb); + border: none; + color: white; + padding: 10px 20px; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + margin: 5px; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/games/Petal Painter/index.html b/games/Petal Painter/index.html new file mode 100644 index 00000000..06445a25 --- /dev/null +++ b/games/Petal Painter/index.html @@ -0,0 +1,30 @@ + + + + + + Petal Painter + + + +
+

๐ŸŒธ Petal Painter

+

Collect petals to create art!

+ +
+
+
+ +
+ Score: 0 +
+ +
+ + +
+
+ + + + diff --git a/games/Petal Painter/script.js b/games/Petal Painter/script.js new file mode 100644 index 00000000..802dcd10 --- /dev/null +++ b/games/Petal Painter/script.js @@ -0,0 +1,98 @@ +const brush = document.getElementById("brush"); +const gameArea = document.getElementById("gameArea"); +const scoreEl = document.getElementById("score"); +const startBtn = document.getElementById("startBtn"); +const restartBtn = document.getElementById("restartBtn"); +const statusEl = document.getElementById("status"); + +let score = 0; +let gameActive = false; +let petalInterval; +let gameLoop; +let speed = 2; + +document.addEventListener("keydown", moveBrush); +gameArea.addEventListener("mousemove", (e) => { + if (!gameActive) return; + const rect = gameArea.getBoundingClientRect(); + const x = e.clientX - rect.left - brush.offsetWidth / 2; + brush.style.left = Math.max(0, Math.min(x, gameArea.offsetWidth - brush.offsetWidth)) + "px"; +}); +startBtn.addEventListener("click", startGame); +restartBtn.addEventListener("click", restartGame); + +function startGame() { + if (gameActive) return; + gameActive = true; + statusEl.textContent = "Catch petals to paint!"; + startBtn.disabled = true; + restartBtn.disabled = true; + petalInterval = setInterval(createPetal, 600); + gameLoop = requestAnimationFrame(updatePetals); +} + +function restartGame() { + score = 0; + scoreEl.textContent = 0; + speed = 2; + document.querySelectorAll(".petal").forEach(p => p.remove()); + startGame(); +} + +function moveBrush(e) { + if (!gameActive) return; + let pos = brush.offsetLeft; + if (e.key === "ArrowLeft" && pos > 0) brush.style.left = pos - 20 + "px"; + if (e.key === "ArrowRight" && pos < gameArea.offsetWidth - brush.offsetWidth) + brush.style.left = pos + 20 + "px"; +} + +function createPetal() { + const petal = document.createElement("div"); + petal.classList.add("petal"); + petal.style.left = Math.random() * (gameArea.offsetWidth - 20) + "px"; + gameArea.appendChild(petal); +} + +function updatePetals() { + if (!gameActive) return; + + document.querySelectorAll(".petal").forEach(petal => { + const top = parseFloat(petal.style.top || 0); + petal.style.top = top + speed + "px"; + + const petalRect = petal.getBoundingClientRect(); + const brushRect = brush.getBoundingClientRect(); + + if ( + petalRect.bottom >= brushRect.top && + petalRect.left >= brushRect.left && + petalRect.right <= brushRect.right + ) { + petal.remove(); + score += 5; + scoreEl.textContent = score; + + // Add color splatter effect + const paint = document.createElement("div"); + paint.classList.add("paint"); + paint.style.left = petal.style.left; + paint.style.top = petal.style.top; + paint.style.backgroundColor = randomColor(); + gameArea.appendChild(paint); + + setTimeout(() => paint.remove(), 2000); + + if (score % 50 === 0) speed += 0.5; + } else if (top > gameArea.offsetHeight) { + petal.remove(); + } + }); + + gameLoop = requestAnimationFrame(updatePetals); +} + +function randomColor() { + const colors = ["#f472b6", "#fb7185", "#f9a8d4", "#fbcfe8", "#fda4af"]; + return colors[Math.floor(Math.random() * colors.length)]; +} diff --git a/games/Petal Painter/style.css b/games/Petal Painter/style.css new file mode 100644 index 00000000..fec66531 --- /dev/null +++ b/games/Petal Painter/style.css @@ -0,0 +1,91 @@ +:root { + --bg: radial-gradient(circle, #fff1f2, #fce7f3, #fbcfe8); + --petal: #f9a8d4; + --brush: #ec4899; + --text: #831843; + font-family: "Poppins", sans-serif; +} + +body { + background: var(--bg); + height: 100vh; + margin: 0; + display: flex; + justify-content: center; + align-items: center; +} + +.game-container { + text-align: center; + max-width: 400px; +} + +h1 { + color: var(--text); + font-size: 2rem; +} + +#status { + color: #9d174d; + margin-bottom: 10px; +} + +#gameArea { + position: relative; + width: 320px; + height: 400px; + border: 2px solid #f472b6; + border-radius: 10px; + background: rgba(255, 255, 255, 0.5); + overflow: hidden; + margin: 0 auto 15px; + box-shadow: 0 0 20px rgba(236, 72, 153, 0.3); +} + +#brush { + position: absolute; + bottom: 10px; + left: 140px; + width: 40px; + height: 40px; + border-radius: 50%; + background: var(--brush); + box-shadow: 0 0 20px var(--brush); + transition: left 0.1s ease; +} + +.petal { + position: absolute; + top: 0; + width: 20px; + height: 20px; + border-radius: 50%; + background: var(--petal); + box-shadow: 0 0 10px var(--petal); + opacity: 0.9; +} + +.info { + display: flex; + justify-content: space-around; + color: var(--text); + font-weight: 600; + margin-bottom: 10px; +} + +.controls button { + margin: 5px; + padding: 10px 16px; + font-size: 1rem; + border: none; + border-radius: 8px; + background: linear-gradient(90deg, #f9a8d4, #f472b6); + color: white; + font-weight: 600; + cursor: pointer; +} + +.controls button:disabled { + opacity: 0.6; + cursor: not-allowed; +} diff --git a/games/Pipe Craze/index.html b/games/Pipe Craze/index.html new file mode 100644 index 00000000..381e183e --- /dev/null +++ b/games/Pipe Craze/index.html @@ -0,0 +1,21 @@ + + + + + + Pipe Craze + + + +
+

๐Ÿšฐ Pipe Craze

+

Connect the flow!

+
+
+ +

Time: 60s

+
+
+ + + diff --git a/games/Pipe Craze/script.js b/games/Pipe Craze/script.js new file mode 100644 index 00000000..44adf221 --- /dev/null +++ b/games/Pipe Craze/script.js @@ -0,0 +1,67 @@ +const grid = document.getElementById("grid"); +const restartBtn = document.getElementById("restart"); +const statusEl = document.getElementById("status"); +const timerEl = document.getElementById("timer"); + +let timer; +let timeLeft = 60; +let gridSize = 5; +let cells = []; + +// Pipe image set +const pipeImages = [ + "url('https://i.imgur.com/pQz1w6O.png')", // straight + "url('https://i.imgur.com/LMHwtC4.png')", // corner + "url('https://i.imgur.com/7G7AmDQ.png')", // T junction + "url('https://i.imgur.com/NhJxPlq.png')" // cross +]; + +// Initialize game +function initGame() { + grid.innerHTML = ""; + cells = []; + statusEl.textContent = "Connect the flow!"; + timeLeft = 60; + timerEl.textContent = timeLeft; + clearInterval(timer); + timer = setInterval(countdown, 1000); + + for (let i = 0; i < gridSize * gridSize; i++) { + const cell = document.createElement("div"); + cell.classList.add("cell"); + + const pipe = document.createElement("div"); + pipe.classList.add("pipe"); + pipe.style.backgroundImage = pipeImages[Math.floor(Math.random() * pipeImages.length)]; + pipe.style.transform = `rotate(${Math.floor(Math.random() * 4) * 90}deg)`; + + cell.appendChild(pipe); + grid.appendChild(cell); + + cell.addEventListener("click", () => rotatePipe(pipe)); + cells.push(pipe); + } +} + +function rotatePipe(pipe) { + let rotation = parseInt(pipe.getAttribute("data-rotation") || "0"); + rotation = (rotation + 90) % 360; + pipe.style.transform = `rotate(${rotation}deg)`; + pipe.setAttribute("data-rotation", rotation); +} + +// Timer countdown +function countdown() { + timeLeft--; + timerEl.textContent = timeLeft; + if (timeLeft <= 0) { + clearInterval(timer); + statusEl.textContent = "โณ Time's up! Try again!"; + } +} + +// Restart game +restartBtn.addEventListener("click", initGame); + +// Start initially +initGame(); diff --git a/games/Pipe Craze/style.css b/games/Pipe Craze/style.css new file mode 100644 index 00000000..25bff82d --- /dev/null +++ b/games/Pipe Craze/style.css @@ -0,0 +1,82 @@ +body { + background: radial-gradient(circle at center, #0a0a0f, #1a1a40); + color: #fff; + font-family: 'Poppins', sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.container { + text-align: center; + background: rgba(255, 255, 255, 0.1); + border-radius: 15px; + padding: 20px 30px; + box-shadow: 0 0 20px rgba(0, 255, 255, 0.3); +} + +h1 { + color: #00e6ff; + text-shadow: 0 0 20px #00e6ff; +} + +.grid { + display: grid; + grid-template-columns: repeat(5, 70px); + grid-gap: 8px; + justify-content: center; + margin: 20px auto; +} + +.cell { + width: 70px; + height: 70px; + background: #0d2740; + border-radius: 10px; + position: relative; + cursor: pointer; + transition: transform 0.3s; +} + +.cell:hover { + transform: scale(1.05); +} + +.pipe { + width: 100%; + height: 100%; + background-size: cover; + transform-origin: center; + transition: transform 0.3s ease; + border-radius: 10px; +} + +.controls { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 10px; +} + +button { + background: #00e6ff; + border: none; + color: #000; + font-weight: 600; + border-radius: 8px; + padding: 8px 16px; + cursor: pointer; + transition: all 0.3s; +} + +button:hover { + background: #66f2ff; +} + +#status { + font-weight: 500; + margin-bottom: 10px; + color: #7dd3fc; +} diff --git a/games/Rhythm catcher/index.html b/games/Rhythm catcher/index.html new file mode 100644 index 00000000..e0f044e4 --- /dev/null +++ b/games/Rhythm catcher/index.html @@ -0,0 +1,34 @@ + + + + + + Rhythm Catcher + + + +
+

๐ŸŽถ Rhythm Catcher

+

Press Start to begin the beat!

+ +
+
A
+
S
+
D
+
F
+
+ +
+ + +
+ +
+ Level: 1 + Score: 0 +
+
+ + + + diff --git a/games/Rhythm catcher/script.js b/games/Rhythm catcher/script.js new file mode 100644 index 00000000..e95a1d38 --- /dev/null +++ b/games/Rhythm catcher/script.js @@ -0,0 +1,115 @@ +const tracks = document.querySelectorAll(".track"); +const statusEl = document.getElementById("status"); +const scoreEl = document.getElementById("score"); +const levelEl = document.getElementById("level"); +const startBtn = document.getElementById("startBtn"); +const retryBtn = document.getElementById("retryBtn"); + +let score = 0; +let level = 1; +let gameInterval; +let noteSpeed = 3000; // fall duration in ms +let spawnRate = 1200; // ms between notes +let isPlaying = false; + +startBtn.addEventListener("click", startGame); +retryBtn.addEventListener("click", restartGame); +document.addEventListener("keydown", handleKey); + +function startGame() { + if (isPlaying) return; + isPlaying = true; + statusEl.textContent = "Catch the rhythm!"; + startBtn.disabled = true; + retryBtn.disabled = true; + gameLoop(); +} + +function restartGame() { + score = 0; + level = 1; + scoreEl.textContent = score; + levelEl.textContent = level; + noteSpeed = 3000; + spawnRate = 1200; + isPlaying = true; + statusEl.textContent = "Catch the rhythm!"; + retryBtn.disabled = true; + gameLoop(); +} + +function gameLoop() { + gameInterval = setInterval(() => { + const randomTrack = tracks[Math.floor(Math.random() * tracks.length)]; + spawnNote(randomTrack); + }, spawnRate); +} + +function spawnNote(track) { + const note = document.createElement("div"); + note.classList.add("note"); + note.style.animationDuration = `${noteSpeed}ms`; + track.appendChild(note); + + const fallTimer = setTimeout(() => { + if (track.contains(note)) { + note.remove(); + track.classList.add("miss"); + setTimeout(() => track.classList.remove("miss"), 200); + losePoints(); + } + }, noteSpeed); +} + +function handleKey(e) { + if (!isPlaying) return; + + const key = e.key.toUpperCase(); + const track = [...tracks].find(t => t.dataset.key === key); + if (!track) return; + + track.classList.add("flash"); + setTimeout(() => track.classList.remove("flash"), 200); + + const note = [...track.querySelectorAll(".note")].find(n => { + const rect = n.getBoundingClientRect(); + return rect.bottom > 450 && rect.bottom < 520; + }); + + if (note) { + note.remove(); + gainPoints(); + } +} + +function gainPoints() { + score += 10; + scoreEl.textContent = score; + + if (score % 100 === 0) { + level++; + levelEl.textContent = level; + noteSpeed = Math.max(1000, noteSpeed - 300); + spawnRate = Math.max(700, spawnRate - 100); + clearInterval(gameInterval); + gameLoop(); + statusEl.textContent = "๐ŸŽต Level Up!"; + } +} + +function losePoints() { + score = Math.max(0, score - 5); + scoreEl.textContent = score; + + if (score === 0 && level === 1) { + endGame(); + } +} + +function endGame() { + clearInterval(gameInterval); + isPlaying = false; + statusEl.textContent = "โŒ Missed too many beats! Game Over."; + retryBtn.disabled = false; + startBtn.disabled = false; +} diff --git a/games/Rhythm catcher/style.css b/games/Rhythm catcher/style.css new file mode 100644 index 00000000..896dda63 --- /dev/null +++ b/games/Rhythm catcher/style.css @@ -0,0 +1,110 @@ +:root { + --bg: #0f172a; + --accent: #38bdf8; + --good: #22c55e; + --bad: #ef4444; + --note: #f472b6; + font-family: 'Poppins', sans-serif; +} + +body { + background: radial-gradient(circle at center, #0f172a, #000); + color: #e2e8f0; + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + margin: 0; +} + +.game-container { + text-align: center; + max-width: 400px; +} + +h1 { + color: var(--accent); + font-size: 2em; + margin-bottom: 10px; +} + +#status { + color: #a1a1aa; + margin-bottom: 15px; +} + +.track-container { + display: flex; + justify-content: space-between; + gap: 8px; + position: relative; + height: 400px; + margin-bottom: 15px; +} + +.track { + flex: 1; + border: 2px solid var(--accent); + border-radius: 10px; + position: relative; + display: flex; + align-items: flex-end; + justify-content: center; + font-weight: bold; + color: #bae6fd; + background: rgba(255, 255, 255, 0.05); + overflow: hidden; +} + +.note { + width: 80%; + height: 30px; + background: var(--note); + border-radius: 8px; + position: absolute; + top: 0; + left: 10%; + animation: fall linear; +} + +@keyframes fall { + 0% { top: 0; opacity: 1; } + 100% { top: 370px; opacity: 0.7; } +} + +.controls button { + margin: 5px; + padding: 10px 16px; + font-size: 1rem; + border: none; + border-radius: 8px; + background: linear-gradient(90deg, var(--accent), #a78bfa); + color: #06121f; + font-weight: 600; + cursor: pointer; + transition: opacity 0.2s; +} + +.controls button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.info { + margin-top: 10px; + color: #93c5fd; + display: flex; + justify-content: space-around; + font-size: 1.1em; +} + +.flash { + background: var(--good) !important; + color: #fff !important; + box-shadow: 0 0 15px var(--good); +} + +.miss { + background: var(--bad) !important; + box-shadow: 0 0 15px var(--bad); +} diff --git a/games/Rock_Paper_Scissors/index.html b/games/Rock_Paper_Scissors/index.html new file mode 100644 index 00000000..4dab820f --- /dev/null +++ b/games/Rock_Paper_Scissors/index.html @@ -0,0 +1,32 @@ + + + + + + Rock Paper Scissors + + + +

โœŠโœ‹โœŒ๏ธ Rock Paper Scissors

+
+
+ + + +
+ +
+

You chose: -

+

Computer chose: -

+

Make your move!

+
+ +
+

Player: 0

+

Computer: 0

+
+
+ + + + diff --git a/games/Rock_Paper_Scissors/readme.md b/games/Rock_Paper_Scissors/readme.md new file mode 100644 index 00000000..cf5f1ba0 --- /dev/null +++ b/games/Rock_Paper_Scissors/readme.md @@ -0,0 +1,53 @@ +# โœŠโœ‹โœŒ๏ธ Rock Paper Scissors + +A classic **Rock Paper Scissors** game built using **HTML**, **CSS**, and **JavaScript**. +Play against the computer, test your luck, and see who wins each round! + +--- + +## ๐ŸŽฎ Game Details + +**Name:** Rock Paper Scissors +**Description:** +Choose **Rock**, **Paper**, or **Scissors** to compete against the computer. +Each round displays both your choice and the computerโ€™s, then shows whether you **win**, **lose**, or **draw**. +Scores are updated dynamically after every round! + +--- + +## How to Play + +1. Click on one of the three buttons: + - Rock + - Paper + - Scissors +2. The computer makes its random choice. +3. The winner is determined based on the rules: + - Rock beats Scissors + - Paper beats Rock + - Scissors beats Paper +4. Your score and the computerโ€™s score are updated after each round. +5. Refresh the page anytime to reset the game. + +--- + +## Files Included + +| File | Description | +|------|--------------| +| `index.html` | Defines the game layout, buttons, and score display. | +| `style.css` | Styles the page with gradients, button hover effects, and typography. | +| `script.js` | Contains all logic for generating random choices, comparing outcomes, and updating scores. | + +--- + +## Features + +- Instant results after every move +- Animated buttons with hover effects +- Dynamic score tracking +- Simple, clean, and responsive design +- Emoji-based visuals for a fun experience + +--- + diff --git a/games/Rock_Paper_Scissors/script.js b/games/Rock_Paper_Scissors/script.js new file mode 100644 index 00000000..cf161f6c --- /dev/null +++ b/games/Rock_Paper_Scissors/script.js @@ -0,0 +1,54 @@ +const choices = document.querySelectorAll('.choice'); +const playerChoiceText = document.getElementById('player-choice'); +const computerChoiceText = document.getElementById('computer-choice'); +const resultText = document.getElementById('result-text'); +const playerScoreSpan = document.getElementById('player-score'); +const computerScoreSpan = document.getElementById('computer-score'); + +let playerScore = 0; +let computerScore = 0; + +choices.forEach(button => { + button.addEventListener('click', () => { + const playerChoice = button.dataset.choice; + const computerChoice = getComputerChoice(); + const result = getWinner(playerChoice, computerChoice); + + playerChoiceText.textContent = `You chose: ${formatChoice(playerChoice)}`; + computerChoiceText.textContent = `Computer chose: ${formatChoice(computerChoice)}`; + resultText.textContent = result; + + updateScore(result); + }); +}); + +function getComputerChoice() { + const options = ['rock', 'paper', 'scissors']; + return options[Math.floor(Math.random() * 3)]; +} + +function getWinner(player, computer) { + if (player === computer) return "It's a draw ๐Ÿ˜"; + if ( + (player === 'rock' && computer === 'scissors') || + (player === 'paper' && computer === 'rock') || + (player === 'scissors' && computer === 'paper') + ) return "You win! ๐ŸŽ‰"; + return "You lose! ๐Ÿ’€"; +} + +function updateScore(result) { + if (result.includes("win")) { + playerScore++; + playerScoreSpan.textContent = playerScore; + } else if (result.includes("lose")) { + computerScore++; + computerScoreSpan.textContent = computerScore; + } +} + +function formatChoice(choice) { + if (choice === 'rock') return 'โœŠ Rock'; + if (choice === 'paper') return 'โœ‹ Paper'; + return 'โœŒ๏ธ Scissors'; +} diff --git a/games/Rock_Paper_Scissors/style.css b/games/Rock_Paper_Scissors/style.css new file mode 100644 index 00000000..1e7172a7 --- /dev/null +++ b/games/Rock_Paper_Scissors/style.css @@ -0,0 +1,53 @@ +body { + background: linear-gradient(135deg, #6a11cb, #2575fc); + color: white; + font-family: 'Poppins', sans-serif; + text-align: center; + margin: 0; + padding: 0; +} + +h1 { + margin-top: 30px; + font-size: 2.5rem; +} + +#game { + margin-top: 50px; +} + +#choices { + display: flex; + justify-content: center; + gap: 20px; +} + +.choice { + background: white; + color: #2575fc; + border: none; + padding: 15px 25px; + border-radius: 12px; + font-size: 1.2rem; + cursor: pointer; + transition: transform 0.2s, background 0.2s; +} + +.choice:hover { + background: #e0e0e0; + transform: scale(1.05); +} + +#results { + margin-top: 40px; +} + +#result-text { + font-size: 1.8rem; + margin-top: 15px; +} + +#scoreboard { + margin-top: 30px; + font-size: 1.2rem; +} diff --git a/games/Rock_Scissor_Paper/index.html b/games/Rock_Scissor_Paper/index.html new file mode 100644 index 00000000..4dab820f --- /dev/null +++ b/games/Rock_Scissor_Paper/index.html @@ -0,0 +1,32 @@ + + + + + + Rock Paper Scissors + + + +

โœŠโœ‹โœŒ๏ธ Rock Paper Scissors

+
+
+ + + +
+ +
+

You chose: -

+

Computer chose: -

+

Make your move!

+
+ +
+

Player: 0

+

Computer: 0

+
+
+ + + + diff --git a/games/Rock_Scissor_Paper/readme.md b/games/Rock_Scissor_Paper/readme.md new file mode 100644 index 00000000..bd083ed0 --- /dev/null +++ b/games/Rock_Scissor_Paper/readme.md @@ -0,0 +1,53 @@ +# โœŠโœ‹โœŒ๏ธ Rock Paper Scissors + +A classic **Rock Paper Scissors** game built using **HTML**, **CSS**, and **JavaScript**. +Play against the computer, test your luck, and see who wins each round! + +--- + +## Game Details + +**Name:** Rock Paper Scissors +**Description:** +Choose **Rock**, **Paper**, or **Scissors** to compete against the computer. +Each round displays both your choice and the computerโ€™s, then shows whether you **win**, **lose**, or **draw**. +Scores are updated dynamically after every round! + +--- + +## How to Play + +1. Click on one of the three buttons: + - โœŠ Rock + - โœ‹ Paper + - โœŒ๏ธ Scissors +2. The computer makes its random choice. +3. The winner is determined based on the rules: + - Rock beats Scissors + - Paper beats Rock + - Scissors beats Paper +4. Your score and the computerโ€™s score are updated after each round. +5. Refresh the page anytime to reset the game. + +--- + +## Files Included + +| File | Description | +|------|--------------| +| `index.html` | Defines the game layout, buttons, and score display. | +| `style.css` | Styles the page with gradients, button hover effects, and typography. | +| `script.js` | Contains all logic for generating random choices, comparing outcomes, and updating scores. | + +--- + +## Features + +- Instant results after every move +- Animated buttons with hover effects +- Dynamic score tracking +- Simple, clean, and responsive design +- Emoji-based visuals for a fun experience + +--- + diff --git a/games/Rock_Scissor_Paper/script.js b/games/Rock_Scissor_Paper/script.js new file mode 100644 index 00000000..cf161f6c --- /dev/null +++ b/games/Rock_Scissor_Paper/script.js @@ -0,0 +1,54 @@ +const choices = document.querySelectorAll('.choice'); +const playerChoiceText = document.getElementById('player-choice'); +const computerChoiceText = document.getElementById('computer-choice'); +const resultText = document.getElementById('result-text'); +const playerScoreSpan = document.getElementById('player-score'); +const computerScoreSpan = document.getElementById('computer-score'); + +let playerScore = 0; +let computerScore = 0; + +choices.forEach(button => { + button.addEventListener('click', () => { + const playerChoice = button.dataset.choice; + const computerChoice = getComputerChoice(); + const result = getWinner(playerChoice, computerChoice); + + playerChoiceText.textContent = `You chose: ${formatChoice(playerChoice)}`; + computerChoiceText.textContent = `Computer chose: ${formatChoice(computerChoice)}`; + resultText.textContent = result; + + updateScore(result); + }); +}); + +function getComputerChoice() { + const options = ['rock', 'paper', 'scissors']; + return options[Math.floor(Math.random() * 3)]; +} + +function getWinner(player, computer) { + if (player === computer) return "It's a draw ๐Ÿ˜"; + if ( + (player === 'rock' && computer === 'scissors') || + (player === 'paper' && computer === 'rock') || + (player === 'scissors' && computer === 'paper') + ) return "You win! ๐ŸŽ‰"; + return "You lose! ๐Ÿ’€"; +} + +function updateScore(result) { + if (result.includes("win")) { + playerScore++; + playerScoreSpan.textContent = playerScore; + } else if (result.includes("lose")) { + computerScore++; + computerScoreSpan.textContent = computerScore; + } +} + +function formatChoice(choice) { + if (choice === 'rock') return 'โœŠ Rock'; + if (choice === 'paper') return 'โœ‹ Paper'; + return 'โœŒ๏ธ Scissors'; +} diff --git a/games/Rock_Scissor_Paper/style.css b/games/Rock_Scissor_Paper/style.css new file mode 100644 index 00000000..1e7172a7 --- /dev/null +++ b/games/Rock_Scissor_Paper/style.css @@ -0,0 +1,53 @@ +body { + background: linear-gradient(135deg, #6a11cb, #2575fc); + color: white; + font-family: 'Poppins', sans-serif; + text-align: center; + margin: 0; + padding: 0; +} + +h1 { + margin-top: 30px; + font-size: 2.5rem; +} + +#game { + margin-top: 50px; +} + +#choices { + display: flex; + justify-content: center; + gap: 20px; +} + +.choice { + background: white; + color: #2575fc; + border: none; + padding: 15px 25px; + border-radius: 12px; + font-size: 1.2rem; + cursor: pointer; + transition: transform 0.2s, background 0.2s; +} + +.choice:hover { + background: #e0e0e0; + transform: scale(1.05); +} + +#results { + margin-top: 40px; +} + +#result-text { + font-size: 1.8rem; + margin-top: 15px; +} + +#scoreboard { + margin-top: 30px; + font-size: 1.2rem; +} diff --git a/games/Shadow Sprint/index.html b/games/Shadow Sprint/index.html new file mode 100644 index 00000000..28239909 --- /dev/null +++ b/games/Shadow Sprint/index.html @@ -0,0 +1,44 @@ + + + + + + Shadow Sprint + + + +
+
+

Shadow Sprint

+

Jump the shadows โ€” tap or press Space / โ†‘ to leap!

+
+ +
+
+
+
+
Score: 0
+
High: 0
+
+ +
+ +
+ + +
+ +
+ Controls: Space / Up to jump โ€” Tap screen on mobile. +
+
+ + + + diff --git a/games/Shadow Sprint/script.js b/games/Shadow Sprint/script.js new file mode 100644 index 00000000..c5981133 --- /dev/null +++ b/games/Shadow Sprint/script.js @@ -0,0 +1,202 @@ +/* Shadow Sprint - simple endless runner + Controls: + - Space / ArrowUp to jump (mobile tap on game area) + Features: + - obstacle spawning, jump physics, score by distance, adaptive speed + - simple sounds using WebAudio (optional) +*/ + +const gameArea = document.getElementById('gameArea'); +const playerEl = document.getElementById('player'); +const startBtn = document.getElementById('startBtn'); +const restartBtn = document.getElementById('restartBtn'); +const scoreEl = document.getElementById('score'); +const highEl = document.getElementById('high'); +const overlay = document.getElementById('overlay'); +const finalScore = document.getElementById('finalScore'); +const resultTitle = document.getElementById('resultTitle'); +const status = document.querySelector('.tagline'); +const muteBtn = document.getElementById('muteBtn'); + +let gravity = 0.9; +let velocity = 0; +let isJumping = false; +let playerY = 0; // px from ground +let groundY = 90; // ground height in CSS +let obstacles = []; +let gameLoopId = null; +let spawnTimer = 0; +let spawnInterval = 1500; // ms +let speed = 3; +let running = false; +let startTime = 0; +let distance = 0; +let highScore = Number(localStorage.getItem('shadowSprintHigh') || 0); +highEl.textContent = highScore; +let muted = false; + +// audio +let audioCtx = null; +function initAudio(){ if(!audioCtx) audioCtx = new (window.AudioContext||window.webkitAudioContext)(); } +function playBeep(freq=260, time=0.06){ + if(muted) return; + initAudio(); + const o = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + o.type = 'sine'; o.frequency.value = freq; + g.gain.value = 0.0001; + o.connect(g); g.connect(audioCtx.destination); + const now = audioCtx.currentTime; + g.gain.exponentialRampToValueAtTime(0.12, now + 0.005); + o.start(now); o.stop(now + time); +} + +// jump +function jump() { + if(!running) return; + if (isJumping) return; + velocity = -12; + isJumping = true; + playerEl.classList.remove('run'); + playBeep(520,0.08); +} + +// spawn obstacle +function spawnObstacle(){ + const ob = document.createElement('div'); + ob.className = 'obstacle'; + const size = 28 + Math.floor(Math.random()*20); + ob.style.width = size + 'px'; + ob.style.height = (size - 6) + 'px'; + ob.style.right = '-80px'; + // attach position property + gameArea.appendChild(ob); + obstacles.push({el: ob, x: gameArea.clientWidth + 20, w: size, passed: false}); +} + +// update loop +function update(timestamp){ + if(!running) return; + const now = Date.now(); + // physics + velocity += gravity * 0.6; + playerY += velocity; + if(playerY > 0){ + // cap with ground at 0 (playerY measured upwards, 0 = standing) + playerY = 0; + velocity = 0; + isJumping = false; + playerEl.classList.add('run'); + } + // translate element + playerEl.style.bottom = (groundY + playerY) + 'px'; + + // obstacles move left + for(let i = obstacles.length-1; i>=0; i--){ + const o = obstacles[i]; + o.x -= (speed + (distance/1000)); + o.el.style.right = o.x + 'px'; + // check collision with player + const pRect = playerEl.getBoundingClientRect(); + const oRect = o.el.getBoundingClientRect(); + if(!(pRect.right < oRect.left || pRect.left > oRect.right || pRect.bottom < oRect.top || pRect.top > oRect.bottom)){ + // collision + endGame(); + return; + } + // remove off-screen elements + if((o.x + o.w) < -120){ + o.el.remove(); + obstacles.splice(i,1); + } else { + // mark passed to increase score + if(!o.passed && o.x + o.w < (gameArea.clientWidth - 120)){ + o.passed = true; + distance += 10; + } + } + } + + // spawn logic + if(Date.now() - spawnTimer > spawnInterval){ + spawnTimer = Date.now(); + spawnObstacle(); + // gradually speed up spawn and speed + if(spawnInterval > 700) spawnInterval -= 30; + if(speed < 9) speed += 0.1; + } + + // update score display (distance ~ score) + scoreEl.textContent = Math.floor(distance/10); + gameLoopId = requestAnimationFrame(update); +} + +// start game +function startGame(){ + if(running) return; + // reset state + obstacles.forEach(o => o.el.remove()); + obstacles = []; + velocity = 0; playerY = 0; isJumping = false; + spawnInterval = 1500; speed = 3; distance = 0; + spawnTimer = Date.now(); + running = true; + startTime = Date.now(); + overlay.classList.add('hidden'); + playerEl.classList.add('run'); + startBtn.disabled = true; + playBeep(780,0.06); + gameLoopId = requestAnimationFrame(update); +} + +// end game +function endGame(){ + running = false; + cancelAnimationFrame(gameLoopId); + // remove remaining obstacles after short delay to freeze view + playBeep(140,0.18); + finalScore.textContent = Math.floor(distance/10); + if(Math.floor(distance/10) > highScore){ + highScore = Math.floor(distance/10); + localStorage.setItem('shadowSprintHigh', highScore); + highEl.textContent = highScore; + resultTitle.textContent = "New High Score!"; + } else { + resultTitle.textContent = "Game Over"; + } + overlay.classList.remove('hidden'); + startBtn.disabled = false; +} + +// input handlers +document.addEventListener('keydown', (e) => { + if(e.code === 'Space' || e.code === 'ArrowUp') { + e.preventDefault(); + jump(); + } + if(e.code === 'KeyM') { + muted = !muted; + muteBtn.textContent = muted ? '๐Ÿ”‡ Muted' : '๐Ÿ”Š Sound'; + } +}); + +gameArea.addEventListener('touchstart', (e) => { + e.preventDefault(); + if(!running) startGame(); + jump(); +}, {passive:false}); + +gameArea.addEventListener('click', (e)=>{ + if(!running) startGame(); + jump(); +}); + +// UI +startBtn.addEventListener('click', startGame); +restartBtn.addEventListener('click', () => { overlay.classList.add('hidden'); startGame(); }); +muteBtn.addEventListener('click', ()=>{ muted = !muted; muteBtn.textContent = muted ? '๐Ÿ”‡ Muted' : '๐Ÿ”Š Sound'; }); + +// initialize +playerEl.style.bottom = groundY + 'px'; +playerEl.classList.add('run'); +overlay.classList.add('hidden'); diff --git a/games/Shadow Sprint/style.css b/games/Shadow Sprint/style.css new file mode 100644 index 00000000..fb235f61 --- /dev/null +++ b/games/Shadow Sprint/style.css @@ -0,0 +1,66 @@ +:root{ + --bg1:#071024; --bg2:#0b1226; + --shadow:#0b1020; --accent:#7dd3fc; --danger:#fb7185; + --ground:#0f1724; + font-family: Inter, Poppins, system-ui, -apple-system, "Segoe UI", Roboto, Arial; +} +*{box-sizing:border-box} +html,body{height:100%;margin:0;background:linear-gradient(180deg,var(--bg1),var(--bg2));color:#e6eef8} +.wrap{max-width:980px;margin:18px auto;padding:16px;text-align:center} +header h1{margin:10px 0;font-size:28px;color:var(--accent)} +.tagline{color:#9fb4d9;margin-bottom:12px} + +.game-area{ + position:relative; + width: 360px; + max-width:90vw; + height: 420px; + margin: 0 auto; + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border-radius:12px; + overflow:hidden; + border:1px solid rgba(255,255,255,0.04); + box-shadow: 0 10px 40px rgba(2,6,23,0.6); +} + +/* ground */ +#ground{ + position:absolute;left:0;right:0;bottom:0;height:90px;background:linear-gradient(180deg,var(--ground),#020617); + box-shadow: inset 0 8px 20px rgba(0,0,0,0.6); +} + +/* player */ +.player{ + position:absolute; + width:48px;height:48px;border-radius:10px;background:linear-gradient(180deg,#ffffff,#dbeafe); + left:60px; bottom:90px; transform-origin:center; + box-shadow:0 8px 18px rgba(0,0,0,0.6); + display:flex;align-items:center;justify-content:center;font-weight:800;color:#062033; +} +.player.run { animation: bob 350ms infinite; } +@keyframes bob { 0%{transform:translateY(0)}50%{transform:translateY(-4px)}100%{transform:translateY(0)}} + +/* obstacles (spawned dynamically) */ +.obstacle{ + position:absolute; + bottom:90px; + width:36px; + height:36px; + border-radius:6px; + background:linear-gradient(180deg,#0f1724,#020617); + box-shadow: 0 8px 14px rgba(11,20,34,0.6), inset 0 -6px 12px rgba(125,211,252,0.06); + border:3px solid rgba(125,211,252,0.06); +} + +/* HUD */ +.hud{position:absolute;left:12px;top:12px;display:flex;gap:12px;font-weight:700;color:#cfe6ff} +.controls{margin-top:12px;display:flex;gap:10px;justify-content:center} +.controls button{padding:8px 14px;border-radius:8px;border:none;cursor:pointer;font-weight:700;background:linear-gradient(90deg,#7dd3fc,#a78bfa);color:#062033} +.controls button:disabled{opacity:0.5} + +/* overlay */ +.overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:linear-gradient(180deg,rgba(2,6,23,0.6),rgba(2,6,23,0.8))} +.overlay .panel{background:linear-gradient(180deg,#061529,#0b2340);padding:18px;border-radius:12px;border:1px solid rgba(255,255,255,0.04);text-align:center} +.hidden{display:none} + +.footer{margin-top:10px;color:#94a9c9} diff --git a/games/Simon-Says-Game/app.js b/games/Simon-Says-Game/app.js new file mode 100644 index 00000000..3d6f62c1 --- /dev/null +++ b/games/Simon-Says-Game/app.js @@ -0,0 +1,80 @@ +let h2 = document.querySelector("h2"); +let gameSqe = []; +let userSqe = []; + +let started = false; +let hightestScore = 0; +let level = 0; + +let btns = ["red", "yellow", "green", "blue"] +document.addEventListener("keypress", function () { + if (started == false) { + started = true; + levelUp(); + } +}); + +function gameFlash(btn) { + btn.classList.add("gameFlash"); + setTimeout(function () { + btn.classList.remove("gameFlash"); + }, 300); +} + +function userFlash(btn) { + btn.classList.add("userFlash"); + setTimeout(function () { + btn.classList.remove("userFlash"); + }, 300); +} + +function levelUp() { + userSqe = []; + level++; + h2.innerText = `Level ${level}`; + + let randIndex = Math.floor(Math.random() * 4); + let randColor = btns[randIndex]; + let randBtn = document.querySelector(`.${randColor}`); + gameSqe.push(randColor); + gameFlash(randBtn); +} + +function checkSqe(index) { + if (userSqe[index] == gameSqe[index]) { + if (userSqe.length == gameSqe.length) { + setTimeout(levelUp(), 1000); + } + } else { + h2.innerHTML = `Game Over! Your Score is ${level}
Press any key to start the game.. `; + let body = document.querySelector("body"); + let score = document.querySelector(".score"); + hightestScore = Math.max(hightestScore, level); + score.innerHTML = `Your hightest score : ${hightestScore}` + body.style.backgroundColor = "#FF0B55"; + setTimeout(function () { + body.style.backgroundColor = "white"; + }, 200) + restart(); + } +} + +function btnPress() { + userFlash(this); + + let btnColor = this.getAttribute("id"); + userSqe.push(btnColor); + checkSqe(btnColor.length - 1); +} + +let allBtn = document.querySelectorAll(".btn"); +for (btn of allBtn) { + btn.addEventListener("click", btnPress); +} + +function restart() { + started = false; + level = 0; + gameSqe = []; + userSqe = []; +} \ No newline at end of file diff --git a/games/Simon-Says-Game/images/favicon.png b/games/Simon-Says-Game/images/favicon.png new file mode 100644 index 00000000..7deca738 Binary files /dev/null and b/games/Simon-Says-Game/images/favicon.png differ diff --git a/games/Simon-Says-Game/index.html b/games/Simon-Says-Game/index.html new file mode 100644 index 00000000..a152d618 --- /dev/null +++ b/games/Simon-Says-Game/index.html @@ -0,0 +1,30 @@ + + + + + + + + + Simon say game + + + + +

Simon say game

+

+

Press any key to start the game..

+
+
+
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/games/Simon-Says-Game/style.css b/games/Simon-Says-Game/style.css new file mode 100644 index 00000000..61d89a61 --- /dev/null +++ b/games/Simon-Says-Game/style.css @@ -0,0 +1,67 @@ +body { + font-family: Arial; + text-align: center; +} + +h1 { + font-size: 40px; +} + +h2 { + font-size: 20px; +} + +.main { + margin-top: 40px; + gap: 20px; + display: flex; + align-items: center; + justify-content: center; +} + +.up { + display: flex; + gap: 20px; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.down { + display: flex; + gap: 20px; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.btn { + width: 200px; + height: 200px; + border: 6px solid black; + border-radius: 16px; +} + +.red { + background-color: red; +} + +.green { + background-color: green; +} + +.yellow { + background-color: yellow; +} + +.blue { + background-color: blue; +} + +.gameFlash { + background-color: whitesmoke; +} + +.userFlash { + background-color: #6FE6FC; +} \ No newline at end of file diff --git a/games/Spot the Odd Emoji/index.html b/games/Spot the Odd Emoji/index.html new file mode 100644 index 00000000..85ddde40 --- /dev/null +++ b/games/Spot the Odd Emoji/index.html @@ -0,0 +1,29 @@ + + + + + + Spot the Odd Emoji + + + +
+

Spot the Odd Emoji ๐ŸŽฏ

+
+
Level: 1
+
Score: 0
+
Time: 10s
+
+ +
+ +
+ +
+ +
+
+ + + + diff --git a/games/Spot the Odd Emoji/script.js b/games/Spot the Odd Emoji/script.js new file mode 100644 index 00000000..27f3683a --- /dev/null +++ b/games/Spot the Odd Emoji/script.js @@ -0,0 +1,86 @@ +const grid = document.getElementById("grid"); +const levelEl = document.getElementById("level"); +const scoreEl = document.getElementById("score"); +const timerEl = document.getElementById("timer"); +const messageEl = document.getElementById("message"); +const restartBtn = document.getElementById("restart-btn"); + +let level = 1; +let score = 0; +let time = 10; +let timerId; + +const emojiSets = [ + "๐ŸŽ", "๐ŸŒ", "๐Ÿ‡", "๐Ÿ“", "๐Ÿ‰", "๐Ÿ’", "๐Ÿฅ", "๐Ÿฅ‘", "๐Ÿ", "๐Ÿฅ•", + "๐Ÿถ", "๐Ÿฑ", "๐Ÿญ", "๐Ÿน", "๐Ÿฐ", "๐ŸฆŠ", "๐Ÿป", "๐Ÿผ", "๐Ÿจ", "๐Ÿฏ", + "๐Ÿ˜€","๐Ÿ˜Ž","๐Ÿ˜‚","๐Ÿฅฐ","๐Ÿคฉ","๐Ÿ˜‡","๐Ÿค“","๐Ÿ˜ด","๐Ÿฅณ","๐Ÿ˜ฑ" +]; + +function generateGrid() { + grid.innerHTML = ""; + messageEl.textContent = ""; + const gridSize = Math.min(3 + level, 8); // grid increases up to 8x8 + grid.style.gridTemplateColumns = `repeat(${gridSize}, 1fr)`; + + const normalEmoji = emojiSets[Math.floor(Math.random() * emojiSets.length)]; + let oddEmoji = normalEmoji; + while(oddEmoji === normalEmoji) { + oddEmoji = emojiSets[Math.floor(Math.random() * emojiSets.length)]; + } + + const totalCells = gridSize * gridSize; + const oddIndex = Math.floor(Math.random() * totalCells); + + for(let i = 0; i < totalCells; i++) { + const cell = document.createElement("div"); + cell.textContent = i === oddIndex ? oddEmoji : normalEmoji; + cell.addEventListener("click", () => handleClick(i === oddIndex)); + grid.appendChild(cell); + } +} + +function handleClick(isCorrect) { + if(isCorrect) { + score += level * 10; + level++; + resetTimer(); + generateGrid(); + } else { + score = Math.max(0, score - 5); + messageEl.textContent = "โŒ Wrong! Try again!"; + } + updateStats(); +} + +function updateStats() { + levelEl.textContent = level; + scoreEl.textContent = score; +} + +function resetTimer() { + clearInterval(timerId); + time = Math.max(5, 10 - level); // faster as level increases + timerEl.textContent = time; + timerId = setInterval(() => { + time--; + timerEl.textContent = time; + if(time <= 0) { + clearInterval(timerId); + messageEl.textContent = `โฐ Time's up! Final Score: ${score}`; + Array.from(grid.children).forEach(cell => cell.removeEventListener("click", () => {})); + } + }, 1000); +} + +restartBtn.addEventListener("click", () => { + level = 1; + score = 0; + updateStats(); + generateGrid(); + resetTimer(); +}); + +// Start game +generateGrid(); +updateStats(); +resetTimer(); diff --git a/games/Spot the Odd Emoji/style.css b/games/Spot the Odd Emoji/style.css new file mode 100644 index 00000000..cde4656d --- /dev/null +++ b/games/Spot the Odd Emoji/style.css @@ -0,0 +1,81 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +body { + background: linear-gradient(135deg, #f5f7fa, #c3cfe2); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + background: #fff; + padding: 30px; + border-radius: 15px; + text-align: center; + box-shadow: 0 8px 25px rgba(0,0,0,0.2); + width: 350px; +} + +h1 { + margin-bottom: 20px; + color: #333; +} + +.stats { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + font-weight: bold; + color: #555; +} + +.grid { + display: grid; + gap: 10px; + margin-bottom: 20px; +} + +.grid div { + background: #f0f0f0; + padding: 15px; + font-size: 2rem; + border-radius: 10px; + cursor: pointer; + transition: transform 0.2s, background 0.3s; +} + +.grid div:hover { + transform: scale(1.2); + background: #d0ebff; +} + +.controls { + margin-top: 10px; +} + +button { + padding: 10px 20px; + border: none; + background: #007bff; + color: #fff; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + transition: background 0.3s; +} + +button:hover { + background: #0056b3; +} + +.message { + margin-top: 15px; + font-weight: bold; + color: #d9534f; +} diff --git a/games/Sweet Swap Saga/index.html b/games/Sweet Swap Saga/index.html new file mode 100644 index 00000000..dfe390ed --- /dev/null +++ b/games/Sweet Swap Saga/index.html @@ -0,0 +1,21 @@ + + + + + + Sweet Swap Saga + + + +

๐Ÿฌ Sweet Swap Saga

+
+
+
+
+

Score: 0

+

Moves: 15

+ +
+ + + diff --git a/games/Sweet Swap Saga/script.js b/games/Sweet Swap Saga/script.js new file mode 100644 index 00000000..e6475316 --- /dev/null +++ b/games/Sweet Swap Saga/script.js @@ -0,0 +1,120 @@ +const grid = document.getElementById("grid"); +const scoreDisplay = document.getElementById("score"); +const movesDisplay = document.getElementById("moves"); +const restartBtn = document.getElementById("restart"); + +const width = 8; +const colors = ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"]; +let squares = []; +let score = 0; +let moves = 15; +let candyBeingDragged = null; +let candyBeingReplaced = null; + +function createBoard() { + for (let i = 0; i < width * width; i++) { + const square = document.createElement("div"); + square.setAttribute("draggable", true); + square.setAttribute("id", i); + const randomColor = colors[Math.floor(Math.random() * colors.length)]; + square.style.backgroundColor = randomColor; + square.classList.add("candy"); + grid.appendChild(square); + squares.push(square); + } +} + +function dragStart() { + candyBeingDragged = this; +} + +function dragDrop() { + candyBeingReplaced = this; +} + +function dragEnd() { + const draggedId = parseInt(candyBeingDragged.id); + const replacedId = parseInt(candyBeingReplaced.id); + const validMoves = [ + draggedId - 1, + draggedId + 1, + draggedId - width, + draggedId + width, + ]; + const validMove = validMoves.includes(replacedId); + + if (validMove && moves > 0) { + moves--; + movesDisplay.textContent = moves; + + let tempColor = candyBeingReplaced.style.backgroundColor; + candyBeingReplaced.style.backgroundColor = + candyBeingDragged.style.backgroundColor; + candyBeingDragged.style.backgroundColor = tempColor; + + checkMatches(); + } +} + +function checkMatches() { + for (let i = 0; i < squares.length - 2; i++) { + let rowOfThree = [i, i + 1, i + 2]; + let color = squares[i].style.backgroundColor; + + if ( + rowOfThree.every( + (index) => squares[index] && squares[index].style.backgroundColor === color + ) + ) { + score += 10; + scoreDisplay.textContent = score; + rowOfThree.forEach((index) => { + squares[index].style.backgroundColor = ""; + }); + } + } + dropCandies(); +} + +function dropCandies() { + for (let i = 0; i < 56; i++) { + if (squares[i + width].style.backgroundColor === "") { + squares[i + width].style.backgroundColor = + squares[i].style.backgroundColor; + squares[i].style.backgroundColor = ""; + } + } + + for (let i = 0; i < width; i++) { + if (squares[i].style.backgroundColor === "") { + const randomColor = colors[Math.floor(Math.random() * colors.length)]; + squares[i].style.backgroundColor = randomColor; + } + } +} + +function restartGame() { + grid.innerHTML = ""; + squares = []; + score = 0; + moves = 15; + scoreDisplay.textContent = score; + movesDisplay.textContent = moves; + createBoard(); + addDragListeners(); +} + +function addDragListeners() { + squares.forEach((square) => { + square.addEventListener("dragstart", dragStart); + square.addEventListener("dragover", (e) => e.preventDefault()); + square.addEventListener("drop", dragDrop); + square.addEventListener("dragend", dragEnd); + }); +} + +createBoard(); +addDragListeners(); +restartBtn.addEventListener("click", restartGame); + +setInterval(checkMatches, 100); diff --git a/games/Sweet Swap Saga/style.css b/games/Sweet Swap Saga/style.css new file mode 100644 index 00000000..59ad0c8e --- /dev/null +++ b/games/Sweet Swap Saga/style.css @@ -0,0 +1,57 @@ +body { + font-family: "Poppins", sans-serif; + background: linear-gradient(135deg, #ffb6c1, #ffd1dc); + text-align: center; + margin: 0; + padding: 20px; +} + +h1 { + color: #fff; + text-shadow: 2px 2px 6px rgba(0, 0, 0, 0.3); +} + +#game { + display: flex; + justify-content: center; + margin-top: 20px; +} + +#grid { + display: grid; + grid-template-columns: repeat(8, 60px); + grid-template-rows: repeat(8, 60px); + gap: 5px; +} + +.candy { + width: 60px; + height: 60px; + border-radius: 12px; + cursor: pointer; + transition: transform 0.2s ease; +} + +.candy:hover { + transform: scale(1.1); +} + +.info { + margin-top: 20px; + color: #fff; + font-size: 18px; +} + +button { + padding: 10px 20px; + border: none; + border-radius: 8px; + background-color: #ff69b4; + color: white; + cursor: pointer; + transition: 0.3s; +} + +button:hover { + background-color: #ff85c1; +} diff --git a/games/Tap Challenge/index.html b/games/Tap Challenge/index.html new file mode 100644 index 00000000..8ac0e7b8 --- /dev/null +++ b/games/Tap Challenge/index.html @@ -0,0 +1,23 @@ + + + + + + Tap Challenge | Mini JS Games Hub + + + +
+

๐ŸŽฏ Tap Challenge

+

Tap the button as many times as you can in 20 seconds!

+
+ Score: 0 + High Score: 0 +
+ +

+ +
+ + + diff --git a/games/Tap Challenge/script.js b/games/Tap Challenge/script.js new file mode 100644 index 00000000..9a7412fd --- /dev/null +++ b/games/Tap Challenge/script.js @@ -0,0 +1,62 @@ +let score = 0; +let time = 20; // Game duration in seconds +let timer; +const scoreEl = document.getElementById("score"); +const timeEl = document.getElementById("time"); +const highScoreEl = document.getElementById("highScore"); +const messageEl = document.getElementById("message"); +const tapBtn = document.getElementById("tap-btn"); +const restartBtn = document.getElementById("restart-btn"); + +// Get high score from localStorage +let highScore = localStorage.getItem("tapHighScore") || 0; +highScoreEl.textContent = highScore; + +// Start the game timer +function startTimer() { + timer = setInterval(() => { + time--; + timeEl.textContent = time; + if(time <= 0) { + endGame(); + } + }, 1000); +} + +// End the game +function endGame() { + clearInterval(timer); + tapBtn.disabled = true; + messageEl.textContent = `โฐ Time's up! Your score: ${score}`; + if(score > highScore) { + localStorage.setItem("tapHighScore", score); + highScoreEl.textContent = score; + messageEl.textContent += " ๐ŸŽ‰ New High Score!"; + } +} + +// Handle tap button click +tapBtn.addEventListener("click", () => { + if(time === 20 && score === 0) startTimer(); // Start timer on first tap + score++; + scoreEl.textContent = score; + + // Optional: button moves randomly for extra challenge + const maxX = tapBtn.parentElement.offsetWidth - tapBtn.offsetWidth; + const maxY = tapBtn.parentElement.offsetHeight - tapBtn.offsetHeight - 60; // padding + const randomX = Math.floor(Math.random() * maxX); + const randomY = Math.floor(Math.random() * maxY); + tapBtn.style.transform = `translate(${randomX}px, ${randomY}px)`; +}); + +// Restart game +restartBtn.addEventListener("click", () => { + clearInterval(timer); + score = 0; + time = 20; + scoreEl.textContent = score; + timeEl.textContent = time; + tapBtn.disabled = false; + tapBtn.style.transform = `translate(0,0)`; + messageEl.textContent = ""; +}); diff --git a/games/Tap Challenge/style.css b/games/Tap Challenge/style.css new file mode 100644 index 00000000..637896ed --- /dev/null +++ b/games/Tap Challenge/style.css @@ -0,0 +1,74 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #ff9a9e, #fad0c4); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.tap-container { + background: rgba(255, 255, 255, 0.9); + padding: 40px; + border-radius: 20px; + text-align: center; + box-shadow: 0 10px 25px rgba(0,0,0,0.2); + width: 90%; + max-width: 400px; +} + +h1 { + margin-bottom: 10px; + font-size: 2em; +} + +p { + font-size: 1.1em; + margin: 10px 0; +} + +.score-board { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + font-weight: bold; +} + +#tap-btn { + padding: 20px 40px; + font-size: 1.5em; + background: #ff6f61; + border: none; + border-radius: 12px; + cursor: pointer; + transition: transform 0.1s, background 0.2s; +} + +#tap-btn:active { + transform: scale(0.9); + background: #ff3b2e; +} + +#restart-btn { + margin-top: 20px; + padding: 10px 20px; + font-size: 1em; + border-radius: 10px; + cursor: pointer; + border: none; + background: #4caf50; + color: white; + transition: background 0.2s; +} + +#restart-btn:hover { + background: #45a049; +} + +#message { + font-weight: bold; + margin-top: 15px; + font-size: 1.2em; + color: #333; +} diff --git a/games/Target-focus/index.html b/games/Target-focus/index.html new file mode 100644 index 00000000..f9f7ca94 --- /dev/null +++ b/games/Target-focus/index.html @@ -0,0 +1,13 @@ + + + + + + Target Focus + + + +
Score: 0
+ + + diff --git a/games/Target-focus/script.js b/games/Target-focus/script.js new file mode 100644 index 00000000..2e882b8a --- /dev/null +++ b/games/Target-focus/script.js @@ -0,0 +1,33 @@ +let score = 0; + +function spawnTarget() { + const target = document.createElement("div"); + target.classList.add("target"); + + // Random position + const x = Math.random() * (window.innerWidth - 60); + const y = Math.random() * (window.innerHeight - 60); + target.style.left = `${x}px`; + target.style.top = `${y}px`; + + document.body.appendChild(target); + + // Click event + target.onclick = () => { + score++; + document.getElementById("score").innerText = `Score: ${score}`; + target.remove(); + }; + + // Remove if not clicked in 800ms + setTimeout(() => { + if (document.body.contains(target)) { + target.remove(); + score = Math.max(0, score - 1); // Penalty for missing + document.getElementById("score").innerText = `Score: ${score}`; + } + }, 800); +} + +// Spawn target every second +setInterval(spawnTarget, 1000); diff --git a/games/Target-focus/style.css b/games/Target-focus/style.css new file mode 100644 index 00000000..91809503 --- /dev/null +++ b/games/Target-focus/style.css @@ -0,0 +1,29 @@ +body { + margin: 0; + overflow: hidden; + background: #111; + color: #fff; + font-family: Poppins, sans-serif; + text-align: center; +} + +#score { + position: absolute; + top: 10px; + left: 10px; + font-size: 1.5em; +} + +.target { + width: 50px; + height: 50px; + background: radial-gradient(circle, red 40%, transparent 45%); + border-radius: 50%; + position: absolute; + cursor: pointer; + transition: transform 0.1s; +} + +.target:active { + transform: scale(0.8); +} diff --git a/games/The Godzilla Fights game(html,css,js)/.vscode/extensions.json b/games/The Godzilla Fights game(html,css,js)/.vscode/extensions.json new file mode 100644 index 00000000..60d2386e --- /dev/null +++ b/games/The Godzilla Fights game(html,css,js)/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "ritwickdey.liveserver" + ] +} \ No newline at end of file diff --git a/games/The Godzilla Fights game(html,css,js)/index.html b/games/The Godzilla Fights game(html,css,js)/index.html new file mode 100644 index 00000000..296d3b82 --- /dev/null +++ b/games/The Godzilla Fights game(html,css,js)/index.html @@ -0,0 +1,98 @@ + + + + + + + + Kongs fight game + + + + + + + + + + + + + + + + + + + + + + +
Wind Speed: 0
+ +
+

Player

+

Angle: 0ยฐ

+

Velocity: 0

+
+ +
+

Computer

+

Angle: 0ยฐ

+

Velocity: 0

+
+ +
+

Player vs. Computer

+

Drag the bomb to aim!

+
+ +
+ +
+

? won!

+ + +
+ +
+ + + + + +
+ + + + + + + \ No newline at end of file diff --git a/games/The Godzilla Fights game(html,css,js)/script.js b/games/The Godzilla Fights game(html,css,js)/script.js new file mode 100644 index 00000000..d9198eeb --- /dev/null +++ b/games/The Godzilla Fights game(html,css,js)/script.js @@ -0,0 +1,1041 @@ + +let state = {}; + +let isDragging = false; +let dragStartX = undefined; +let dragStartY = undefined; + +let previousAnimationTimestamp = undefined; +let animationFrameRequestID = undefined; +let delayTimeoutID = undefined; + +let simulationMode = false; +let simulationImpact = {}; + +const darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + +// Settings +const settings = { + numberOfPlayers: 1, // 0 means two computers are playing against each other + mode: darkModeMediaQuery.matches ? "dark" : "light", +}; + +const blastHoleRadius = 18; + +// The main canvas element and its drawing context +const canvas = document.getElementById("game"); +canvas.width = window.innerWidth * window.devicePixelRatio; +canvas.height = window.innerHeight * window.devicePixelRatio; +canvas.style.width = window.innerWidth + "px"; +canvas.style.height = window.innerHeight + "px"; +const ctx = canvas.getContext("2d"); + +// Windmill +const windmillDOM = document.getElementById("windmill"); +const windmillHeadDOM = document.getElementById("windmill-head"); +const windInfoDOM = document.getElementById("wind-info"); +const windSpeedDOM = document.getElementById("wind-speed"); + +// Left info panel +const info1DOM = document.getElementById("info-left"); +const name1DOM = document.querySelector("#info-left .name"); +const angle1DOM = document.querySelector("#info-left .angle"); +const velocity1DOM = document.querySelector("#info-left .velocity"); + +// Right info panel +const info2DOM = document.getElementById("info-right"); +const name2DOM = document.querySelector("#info-right .name"); +const angle2DOM = document.querySelector("#info-right .angle"); +const velocity2DOM = document.querySelector("#info-right .velocity"); + +// Instructions panel +const instructionsDOM = document.getElementById("instructions"); +const gameModeDOM = document.getElementById("game-mode"); + +// The bomb's grab area +const bombGrabAreaDOM = document.getElementById("bomb-grab-area"); + +// Congratulations panel +const congratulationsDOM = document.getElementById("congratulations"); +const winnerDOM = document.getElementById("winner"); + +// Settings toolbar +const settingsDOM = document.getElementById("settings"); +const singlePlayerButtonDOM = document.querySelectorAll(".single-player"); +const twoPlayersButtonDOM = document.querySelectorAll(".two-players"); +const autoPlayButtonDOM = document.querySelectorAll(".auto-play"); +const colorModeButtonDOM = document.getElementById("color-mode"); + +colorModeButtonDOM.addEventListener("click", () => { + if (settings.mode === "dark") { + settings.mode = "light"; + colorModeButtonDOM.innerText = "Dark Mode"; + } else { + settings.mode = "dark"; + colorModeButtonDOM.innerText = "Light Mode"; + } + draw(); +}); + +darkModeMediaQuery.addEventListener("change", (e) => { + settings.mode = e.matches ? "dark" : "light"; + if (settings.mode === "dark") { + colorModeButtonDOM.innerText = "Light Mode"; + } else { + colorModeButtonDOM.innerText = "Dark Mode"; + } + draw(); +}); + +newGame(); + +function newGame() { + // Reset game state + state = { + phase: "aiming", // aiming | in flight | celebrating + currentPlayer: 1, + round: 1, + windSpeed: generateWindSpeed(), + bomb: { + x: undefined, + y: undefined, + rotation: 0, + velocity: { x: 0, y: 0 }, + highlight: true, + }, + + // Buildings + backgroundBuildings: [], + buildings: [], + blastHoles: [], + + stars: [], + + scale: 1, + shift: 0, + }; + + // Generate stars + for (let i = 0; i < (window.innerWidth * window.innerHeight) / 12000; i++) { + const x = Math.floor(Math.random() * window.innerWidth); + const y = Math.floor(Math.random() * window.innerHeight); + state.stars.push({ x, y }); + } + + // Generate background buildings + for (let i = 0; i < 17; i++) { + generateBackgroundBuilding(i); + } + + // Generate buildings + for (let i = 0; i < 8; i++) { + generateBuilding(i); + } + + calculateScaleAndShift(); + initializeBombPosition(); + initializeWindmillPosition(); + setWindMillRotation(); + + // Cancel any ongoing animation and timeout + cancelAnimationFrame(animationFrameRequestID); + clearTimeout(delayTimeoutID); + + // Reset HTML elements + if (settings.numberOfPlayers > 0) { + showInstructions(); + } else { + hideInstructions(); + } + hideCongratulations(); + angle1DOM.innerText = 0; + velocity1DOM.innerText = 0; + angle2DOM.innerText = 0; + velocity2DOM.innerText = 0; + + // Reset simulation mode + simulationMode = false; + simulationImpact = {}; + + draw(); + + if (settings.numberOfPlayers === 0) { + computerThrow(); + } +} + +function showInstructions() { + singlePlayerButtonDOM.checked = true; + instructionsDOM.style.opacity = 1; + instructionsDOM.style.visibility = "visible"; +} + +function hideInstructions() { + state.bomb.highlight = false; + instructionsDOM.style.opacity = 0; + instructionsDOM.style.visibility = "hidden"; +} + +function showCongratulations() { + congratulationsDOM.style.opacity = 1; + congratulationsDOM.style.visibility = "visible"; +} + +function hideCongratulations() { + congratulationsDOM.style.opacity = 0; + congratulationsDOM.style.visibility = "hidden"; +} + +function generateBackgroundBuilding(index) { + const previousBuilding = state.backgroundBuildings[index - 1]; + + const x = previousBuilding + ? previousBuilding.x + previousBuilding.width + 4 + : -300; + + const minWidth = 60; + const maxWidth = 110; + const width = minWidth + Math.random() * (maxWidth - minWidth); + + const smallerBuilding = index < 4 || index >= 13; + + const minHeight = 80; + const maxHeight = 350; + const smallMinHeight = 20; + const smallMaxHeight = 150; + const height = smallerBuilding + ? smallMinHeight + Math.random() * (smallMaxHeight - smallMinHeight) + : minHeight + Math.random() * (maxHeight - minHeight); + + state.backgroundBuildings.push({ x, width, height }); +} + +function generateBuilding(index) { + const previousBuilding = state.buildings[index - 1]; + + const x = previousBuilding + ? previousBuilding.x + previousBuilding.width + 4 + : 0; + + const minWidth = 80; + const maxWidth = 130; + const width = minWidth + Math.random() * (maxWidth - minWidth); + + const smallerBuilding = index <= 1 || index >= 6; + + const minHeight = 40; + const maxHeight = 300; + const minHeightGorilla = 30; + const maxHeightGorilla = 150; + + const height = smallerBuilding + ? minHeightGorilla + Math.random() * (maxHeightGorilla - minHeightGorilla) + : minHeight + Math.random() * (maxHeight - minHeight); + + // Generate an array of booleans to show if the light is on or off in a room + const lightsOn = []; + for (let i = 0; i < 50; i++) { + const light = Math.random() <= 0.33 ? true : false; + lightsOn.push(light); + } + + state.buildings.push({ x, width, height, lightsOn }); +} + +function calculateScaleAndShift() { + const lastBuilding = state.buildings.at(-1); + const totalWidthOfTheCity = lastBuilding.x + lastBuilding.width; + + const horizontalScale = window.innerWidth / totalWidthOfTheCity ?? 1; + const verticalScale = window.innerHeight / 500; + + state.scale = Math.min(horizontalScale, verticalScale); + + const sceneNeedsToBeShifted = horizontalScale > verticalScale; + + state.shift = sceneNeedsToBeShifted + ? (window.innerWidth - totalWidthOfTheCity * state.scale) / 2 + : 0; +} + +window.addEventListener("resize", () => { + canvas.width = window.innerWidth * window.devicePixelRatio; + canvas.height = window.innerHeight * window.devicePixelRatio; + canvas.style.width = window.innerWidth + "px"; + canvas.style.height = window.innerHeight + "px"; + calculateScaleAndShift(); + initializeBombPosition(); + initializeWindmillPosition(); + draw(); +}); + +function initializeBombPosition() { + const building = + state.currentPlayer === 1 + ? state.buildings.at(1) // Second building + : state.buildings.at(-2); // Second last building + + const gorillaX = building.x + building.width / 2; + const gorillaY = building.height; + + const gorillaHandOffsetX = state.currentPlayer === 1 ? -28 : 28; + const gorillaHandOffsetY = 107; + + state.bomb.x = gorillaX + gorillaHandOffsetX; + state.bomb.y = gorillaY + gorillaHandOffsetY; + state.bomb.velocity.x = 0; + state.bomb.velocity.y = 0; + state.bomb.rotation = 0; + + // Initialize the position of the grab area in HTML + const grabAreaRadius = 15; + const left = state.bomb.x * state.scale + state.shift - grabAreaRadius; + const bottom = state.bomb.y * state.scale - grabAreaRadius; + + bombGrabAreaDOM.style.left = `${left}px`; + bombGrabAreaDOM.style.bottom = `${bottom}px`; +} + +function initializeWindmillPosition() { + // Move windmill into position + const lastBuilding = state.buildings.at(-1); + let rooftopY = lastBuilding.height * state.scale; + let rooftopX = + (lastBuilding.x + lastBuilding.width / 2) * state.scale + state.shift; + + windmillDOM.style.bottom = `${rooftopY}px`; + windmillDOM.style.left = `${rooftopX - 100}px`; + + windmillDOM.style.scale = state.scale; + + windInfoDOM.style.bottom = `${rooftopY}px`; + windInfoDOM.style.left = `${rooftopX - 50}px`; +} + +function draw() { + ctx.save(); + + ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + + drawBackgroundSky(); + + // Flip coordinate system upside down + ctx.translate(0, window.innerHeight); + ctx.scale(1, -1); + + // Scale and shift view to center + ctx.translate(state.shift, 0); + ctx.scale(state.scale, state.scale); + + // Draw scene + drawBackgroundMoon(); + drawBackgroundBuildings(); + drawBuildingsWithBlastHoles(); + drawGorilla(1); + drawGorilla(2); + drawBomb(); + + // Restore transformation + ctx.restore(); +} + +function drawBackgroundSky() { + const gradient = ctx.createLinearGradient(0, 0, 0, window.innerHeight); + if (settings.mode === "dark") { + gradient.addColorStop(1, "#27507F"); + gradient.addColorStop(0, "#58A8D8"); + } else { + gradient.addColorStop(1, "#F8BA85"); + gradient.addColorStop(0, "#FFC28E"); + } + + // Draw sky + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, window.innerWidth, window.innerHeight); + + // Draw stars + if (settings.mode === "dark") { + ctx.fillStyle = "white"; + state.stars.forEach((star) => { + ctx.fillRect(star.x, star.y, 1, 1); + }); + } +} + +function drawBackgroundMoon() { + if (settings.mode === "dark") { + ctx.fillStyle = "rgba(255, 255, 255, 0.6)"; + ctx.beginPath(); + ctx.arc( + window.innerWidth / state.scale - state.shift - 200, + window.innerHeight / state.scale - 100, + 30, + 0, + 2 * Math.PI + ); + ctx.fill(); + } else { + ctx.fillStyle = "rgba(255, 255, 255, 0.6)"; + ctx.beginPath(); + ctx.arc(300, 350, 60, 0, 2 * Math.PI); + ctx.fill(); + } +} + +function drawBackgroundBuildings() { + state.backgroundBuildings.forEach((building) => { + ctx.fillStyle = settings.mode === "dark" ? "#254D7E" : "#947285"; + ctx.fillRect(building.x, 0, building.width, building.height); + }); +} + +function drawBuildingsWithBlastHoles() { + ctx.save(); + + state.blastHoles.forEach((blastHole) => { + ctx.beginPath(); + + // Outer shape clockwise + ctx.rect( + 0, + 0, + window.innerWidth / state.scale, + window.innerHeight / state.scale + ); + + // Inner shape counterclockwise + ctx.arc(blastHole.x, blastHole.y, blastHoleRadius, 0, 2 * Math.PI, true); + + ctx.clip(); + }); + + drawBuildings(); + + ctx.restore(); +} + +function drawBuildings() { + state.buildings.forEach((building) => { + // Draw building + ctx.fillStyle = settings.mode === "dark" ? "#152A47" : "#4A3C68"; + ctx.fillRect(building.x, 0, building.width, building.height); + + // Draw windows + const windowWidth = 10; + const windowHeight = 12; + const gap = 15; + + const numberOfFloors = Math.ceil( + (building.height - gap) / (windowHeight + gap) + ); + const numberOfRoomsPerFloor = Math.floor( + (building.width - gap) / (windowWidth + gap) + ); + + for (let floor = 0; floor < numberOfFloors; floor++) { + for (let room = 0; room < numberOfRoomsPerFloor; room++) { + if (building.lightsOn[floor * numberOfRoomsPerFloor + room]) { + ctx.save(); + + ctx.translate(building.x + gap, building.height - gap); + ctx.scale(1, -1); + + const x = room * (windowWidth + gap); + const y = floor * (windowHeight + gap); + + ctx.fillStyle = settings.mode === "dark" ? "#5F76AB" : "#EBB6A2"; + ctx.fillRect(x, y, windowWidth, windowHeight); + + ctx.restore(); + } + } + } + }); +} + +function drawGorilla(player) { + ctx.save(); + + const building = + player === 1 + ? state.buildings.at(1) // Second building + : state.buildings.at(-2); // Second last building + + ctx.translate(building.x + building.width / 2, building.height); + + drawGorillaBody(); + drawGorillaLeftArm(player); + drawGorillaRightArm(player); + drawGorillaFace(player); + drawGorillaThoughtBubbles(player); + + ctx.restore(); +} + +function drawGorillaBody() { + ctx.fillStyle = "black"; + + ctx.beginPath(); + ctx.moveTo(0, 15); + ctx.lineTo(-7, 0); + ctx.lineTo(-20, 0); + ctx.lineTo(-17, 18); + ctx.lineTo(-20, 44); + + ctx.lineTo(-11, 77); + ctx.lineTo(0, 84); + ctx.lineTo(11, 77); + + ctx.lineTo(20, 44); + ctx.lineTo(17, 18); + ctx.lineTo(20, 0); + ctx.lineTo(7, 0); + ctx.fill(); +} + +function drawGorillaLeftArm(player) { + ctx.strokeStyle = "black"; + ctx.lineWidth = 18; + + ctx.beginPath(); + ctx.moveTo(-14, 50); + + if (state.phase === "aiming" && state.currentPlayer === 1 && player === 1) { + ctx.quadraticCurveTo( + -44, + 63, + -28 - state.bomb.velocity.x / 6.25, + 107 - state.bomb.velocity.y / 6.25 + ); + } else if (state.phase === "celebrating" && state.currentPlayer === player) { + ctx.quadraticCurveTo(-44, 63, -28, 107); + } else { + ctx.quadraticCurveTo(-44, 45, -28, 12); + } + + ctx.stroke(); +} + +function drawGorillaRightArm(player) { + ctx.strokeStyle = "black"; + ctx.lineWidth = 18; + + ctx.beginPath(); + ctx.moveTo(+14, 50); + + if (state.phase === "aiming" && state.currentPlayer === 2 && player === 2) { + ctx.quadraticCurveTo( + +44, + 63, + +28 - state.bomb.velocity.x / 6.25, + 107 - state.bomb.velocity.y / 6.25 + ); + } else if (state.phase === "celebrating" && state.currentPlayer === player) { + ctx.quadraticCurveTo(+44, 63, +28, 107); + } else { + ctx.quadraticCurveTo(+44, 45, +28, 12); + } + + ctx.stroke(); +} + +function drawGorillaFace(player) { + // Face + ctx.fillStyle = settings.mode === "dark" ? "gray" : "lightgray"; + ctx.beginPath(); + ctx.arc(0, 63, 9, 0, 2 * Math.PI); + ctx.moveTo(-3.5, 70); + ctx.arc(-3.5, 70, 4, 0, 2 * Math.PI); + ctx.moveTo(+3.5, 70); + ctx.arc(+3.5, 70, 4, 0, 2 * Math.PI); + ctx.fill(); + + // Eyes + ctx.fillStyle = "black"; + ctx.beginPath(); + ctx.arc(-3.5, 70, 1.4, 0, 2 * Math.PI); + ctx.moveTo(+3.5, 70); + ctx.arc(+3.5, 70, 1.4, 0, 2 * Math.PI); + ctx.fill(); + + ctx.strokeStyle = "black"; + ctx.lineWidth = 1.4; + + // Nose + ctx.beginPath(); + ctx.moveTo(-3.5, 66.5); + ctx.lineTo(-1.5, 65); + ctx.moveTo(3.5, 66.5); + ctx.lineTo(1.5, 65); + ctx.stroke(); + + // Mouth + ctx.beginPath(); + if (state.phase === "celebrating" && state.currentPlayer === player) { + ctx.moveTo(-5, 60); + ctx.quadraticCurveTo(0, 56, 5, 60); + } else { + ctx.moveTo(-5, 56); + ctx.quadraticCurveTo(0, 60, 5, 56); + } + ctx.stroke(); +} + +function drawGorillaThoughtBubbles(player) { + if (state.phase === "aiming") { + const currentPlayerIsComputer = + (settings.numberOfPlayers === 0 && + state.currentPlayer === 1 && + player === 1) || + (settings.numberOfPlayers !== 2 && + state.currentPlayer === 2 && + player === 2); + + if (currentPlayerIsComputer) { + ctx.save(); + ctx.scale(1, -1); + + ctx.font = "20px sans-serif"; + ctx.textAlign = "center"; + ctx.fillText("?", 0, -90); + + ctx.font = "10px sans-serif"; + + ctx.rotate((5 / 180) * Math.PI); + ctx.fillText("?", 0, -90); + + ctx.rotate((-10 / 180) * Math.PI); + ctx.fillText("?", 0, -90); + + ctx.restore(); + } + } +} + +function drawBomb() { + ctx.save(); + ctx.translate(state.bomb.x, state.bomb.y); + + if (state.phase === "aiming") { + // Move the bomb with the mouse while aiming + ctx.translate(-state.bomb.velocity.x / 6.25, -state.bomb.velocity.y / 6.25); + + // Draw throwing trajectory + ctx.strokeStyle = "rgba(255, 255, 255, 0.7)"; + ctx.setLineDash([3, 8]); + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(state.bomb.velocity.x, state.bomb.velocity.y); + ctx.stroke(); + + // Draw circle + ctx.fillStyle = "white"; + ctx.beginPath(); + ctx.arc(0, 0, 6, 0, 2 * Math.PI); + ctx.fill(); + } else if (state.phase === "in flight") { + // Draw rotated banana + ctx.fillStyle = "white"; + ctx.rotate(state.bomb.rotation); + ctx.beginPath(); + ctx.moveTo(-8, -2); + ctx.quadraticCurveTo(0, 12, 8, -2); + ctx.quadraticCurveTo(0, 2, -8, -2); + ctx.fill(); + } else { + // Draw circle + ctx.fillStyle = "white"; + ctx.beginPath(); + ctx.arc(0, 0, 6, 0, 2 * Math.PI); + ctx.fill(); + } + + // Restore transformation + ctx.restore(); + + // Indicator showing if the bomb is above the screen + if (state.bomb.y > window.innerHeight / state.scale) { + ctx.beginPath(); + ctx.strokeStyle = "white"; + const distance = state.bomb.y - window.innerHeight / state.scale; + ctx.moveTo(state.bomb.x, window.innerHeight / state.scale - 10); + ctx.lineTo(state.bomb.x, window.innerHeight / state.scale - distance); + ctx.moveTo(state.bomb.x, window.innerHeight / state.scale - 10); + ctx.lineTo(state.bomb.x - 5, window.innerHeight / state.scale - 15); + ctx.moveTo(state.bomb.x, window.innerHeight / state.scale - 10); + ctx.lineTo(state.bomb.x + 5, window.innerHeight / state.scale - 15); + ctx.stroke(); + } + + // Indicator showing the starting position of the bomb + if (state.bomb.highlight) { + ctx.beginPath(); + ctx.strokeStyle = "white"; + ctx.lineWidth = 2; + ctx.moveTo(state.bomb.x, state.bomb.y + 20); + ctx.lineTo(state.bomb.x, state.bomb.y + 120); + ctx.moveTo(state.bomb.x, state.bomb.y + 20); + ctx.lineTo(state.bomb.x - 5, state.bomb.y + 25); + ctx.moveTo(state.bomb.x, state.bomb.y + 20); + ctx.lineTo(state.bomb.x + 5, state.bomb.y + 25); + ctx.stroke(); + } +} + +// Event handlers +bombGrabAreaDOM.addEventListener("mousedown", function (e) { + hideInstructions(); + if (state.phase === "aiming") { + isDragging = true; + dragStartX = e.clientX; + dragStartY = e.clientY; + document.body.style.cursor = "grabbing"; + } +}); + +window.addEventListener("mousemove", function (e) { + if (isDragging) { + let deltaX = e.clientX - dragStartX; + let deltaY = e.clientY - dragStartY; + + state.bomb.velocity.x = -deltaX; + state.bomb.velocity.y = deltaY; + setInfo(deltaX, deltaY); + + draw(); + } +}); + +// Set values on the info panel +function setInfo(deltaX, deltaY) { + const hypotenuse = Math.sqrt(deltaX ** 2 + deltaY ** 2); + const angleInRadians = Math.asin(deltaY / hypotenuse); + const angleInDegrees = (angleInRadians / Math.PI) * 180; + + if (state.currentPlayer === 1) { + angle1DOM.innerText = Math.round(angleInDegrees); + velocity1DOM.innerText = Math.round(hypotenuse); + } else { + angle2DOM.innerText = Math.round(angleInDegrees); + velocity2DOM.innerText = Math.round(hypotenuse); + } +} + +window.addEventListener("mouseup", function () { + if (isDragging) { + isDragging = false; + document.body.style.cursor = "default"; + + throwBomb(); + } +}); + +function computerThrow() { + const numberOfSimulations = 2 + state.round * 3; + const bestThrow = runSimulations(numberOfSimulations); + + initializeBombPosition(); + state.bomb.velocity.x = bestThrow.velocityX; + state.bomb.velocity.y = bestThrow.velocityY; + setInfo(bestThrow.velocityX, bestThrow.velocityY); + + // Draw the aiming gorilla + draw(); + + // Make it look like the computer is thinking for a second + delayTimeoutID = setTimeout(throwBomb, 1000); +} + +// Simulate multiple throws and pick the best +function runSimulations(numberOfSimulations) { + let bestThrow = { + velocityX: undefined, + velocityY: undefined, + distance: Infinity, + }; + simulationMode = true; + + // Calculating the center position of the enemy + const enemyBuilding = + state.currentPlayer === 1 + ? state.buildings.at(-2) // Second last building + : state.buildings.at(1); // Second building + const enemyX = enemyBuilding.x + enemyBuilding.width / 2; + const enemyY = enemyBuilding.height + 30; + + for (let i = 0; i < numberOfSimulations; i++) { + // Pick a random angle and velocity + const angleInDegrees = -10 + Math.random() * 100; + const angleInRadians = (angleInDegrees / 180) * Math.PI; + const velocity = 40 + Math.random() * 130; + + // Calculate the horizontal and vertical velocity + const direction = state.currentPlayer === 1 ? 1 : -1; + const velocityX = Math.cos(angleInRadians) * velocity * direction; + const velocityY = Math.sin(angleInRadians) * velocity; + + initializeBombPosition(); + state.bomb.velocity.x = velocityX; + state.bomb.velocity.y = velocityY; + + throwBomb(); + + // Calculating the distance between the simulated impact and the enemy + const distance = Math.sqrt( + (enemyX - simulationImpact.x) ** 2 + (enemyY - simulationImpact.y) ** 2 + ); + + // If the current impact is closer to the enemy + // than any of the previous simulations then pick this one + if (distance < bestThrow.distance) { + bestThrow = { velocityX, velocityY, distance }; + } + } + + simulationMode = false; + return bestThrow; +} + +function throwBomb() { + if (simulationMode) { + previousAnimationTimestamp = 0; + animate(16); + } else { + state.phase = "in flight"; + previousAnimationTimestamp = undefined; + animationFrameRequestID = requestAnimationFrame(animate); + } +} + +function animate(timestamp) { + if (previousAnimationTimestamp === undefined) { + previousAnimationTimestamp = timestamp; + animationFrameRequestID = requestAnimationFrame(animate); + return; + } + + const elapsedTime = timestamp - previousAnimationTimestamp; + + // We break down every animation cycle into 10 tiny movements for greater hit detection precision + const hitDetectionPrecision = 10; + for (let i = 0; i < hitDetectionPrecision; i++) { + moveBomb(elapsedTime / hitDetectionPrecision); + + // Hit detection + const miss = checkFrameHit() || checkBuildingHit(); // Bomb got off-screen or hit a building + const hit = checkGorillaHit(); // Bomb hit the enemy + + if (simulationMode && (hit || miss)) { + simulationImpact = { x: state.bomb.x, y: state.bomb.y }; + return; // Simulation ended, return from the loop + } + + // Handle the case when we hit a building or the bomb got off-screen + if (miss) { + state.currentPlayer = state.currentPlayer === 1 ? 2 : 1; // Switch players + if (state.currentPlayer === 1) state.round++; + state.phase = "aiming"; + initializeBombPosition(); + + draw(); + + const computerThrowsNext = + settings.numberOfPlayers === 0 || + (settings.numberOfPlayers === 1 && state.currentPlayer === 2); + + if (computerThrowsNext) setTimeout(computerThrow, 50); + + return; + } + + // Handle the case when we hit the enemy + if (hit) { + state.phase = "celebrating"; + announceWinner(); + + draw(); + return; + } + } + + if (!simulationMode) draw(); + + // Continue the animation loop + previousAnimationTimestamp = timestamp; + if (simulationMode) { + animate(timestamp + 16); + } else { + animationFrameRequestID = requestAnimationFrame(animate); + } +} + +function moveBomb(elapsedTime) { + const multiplier = elapsedTime / 200; + + // Adjust trajectory by wind + state.bomb.velocity.x += state.windSpeed * multiplier; + + // Adjust trajectory by gravity + state.bomb.velocity.y -= 20 * multiplier; + + // Calculate new position + state.bomb.x += state.bomb.velocity.x * multiplier; + state.bomb.y += state.bomb.velocity.y * multiplier; + + // Rotate according to the direction + const direction = state.currentPlayer === 1 ? -1 : +1; + state.bomb.rotation += direction * 5 * multiplier; +} + +function checkFrameHit() { + // Stop throw animation once the bomb gets out of the left, bottom, or right edge of the screen + if ( + state.bomb.y < 0 || + state.bomb.x < -state.shift / state.scale || + state.bomb.x > (window.innerWidth - state.shift) / state.scale + ) { + return true; // The bomb is off-screen + } +} + +function checkBuildingHit() { + for (let i = 0; i < state.buildings.length; i++) { + const building = state.buildings[i]; + if ( + state.bomb.x + 4 > building.x && + state.bomb.x - 4 < building.x + building.width && + state.bomb.y - 4 < 0 + building.height + ) { + // Check if the bomb is inside the blast hole of a previous impact + for (let j = 0; j < state.blastHoles.length; j++) { + const blastHole = state.blastHoles[j]; + + // Check how far the bomb is from the center of a previous blast hole + const horizontalDistance = state.bomb.x - blastHole.x; + const verticalDistance = state.bomb.y - blastHole.y; + const distance = Math.sqrt( + horizontalDistance ** 2 + verticalDistance ** 2 + ); + if (distance < blastHoleRadius) { + // The bomb is inside of the rectangle of a building, + // but a previous bomb already blew off this part of the building + return false; + } + } + + if (!simulationMode) { + state.blastHoles.push({ x: state.bomb.x, y: state.bomb.y }); + } + return true; // Building hit + } + } +} + +function checkGorillaHit() { + const enemyPlayer = state.currentPlayer === 1 ? 2 : 1; + const enemyBuilding = + enemyPlayer === 1 + ? state.buildings.at(1) // Second building + : state.buildings.at(-2); // Second last building + + ctx.save(); + + ctx.translate( + enemyBuilding.x + enemyBuilding.width / 2, + enemyBuilding.height + ); + + drawGorillaBody(); + let hit = ctx.isPointInPath(state.bomb.x, state.bomb.y); + + drawGorillaLeftArm(enemyPlayer); + hit ||= ctx.isPointInStroke(state.bomb.x, state.bomb.y); + + drawGorillaRightArm(enemyPlayer); + hit ||= ctx.isPointInStroke(state.bomb.x, state.bomb.y); + + ctx.restore(); + + return hit; +} + +function announceWinner() { + if (settings.numberOfPlayers === 0) { + winnerDOM.innerText = `Computer ${state.currentPlayer}`; + } else if (settings.numberOfPlayers === 1 && state.currentPlayer === 1) { + winnerDOM.innerText = `You`; + } else if (settings.numberOfPlayers === 1 && state.currentPlayer === 2) { + winnerDOM.innerText = `Computer`; + } else { + winnerDOM.innerText = `Player ${state.currentPlayer}`; + } + showCongratulations(); +} + +singlePlayerButtonDOM.forEach((button) => + button.addEventListener("click", () => { + settings.numberOfPlayers = 1; + gameModeDOM.innerHTML = "Player vs. Computer"; + name1DOM.innerText = "Player"; + name2DOM.innerText = "Computer"; + + newGame(); + }) +); + +twoPlayersButtonDOM.forEach((button) => + button.addEventListener("click", () => { + settings.numberOfPlayers = 2; + gameModeDOM.innerHTML = "Player vs. Player"; + name1DOM.innerText = "Player 1"; + name2DOM.innerText = "Player 2"; + + newGame(); + }) +); + +autoPlayButtonDOM.forEach((button) => + button.addEventListener("click", () => { + settings.numberOfPlayers = 0; + name1DOM.innerText = "Computer 1"; + name2DOM.innerText = "Computer 2"; + + newGame(); + }) +); + +function generateWindSpeed() { + // Generate a random number between -10 and +10 + return -10 + Math.random() * 20; +} + +function setWindMillRotation() { + const rotationSpeed = Math.abs(50 / state.windSpeed); + windmillHeadDOM.style.animationDirection = + state.windSpeed > 0 ? "normal" : "reverse"; + windmillHeadDOM.style.animationDuration = `${rotationSpeed}s`; + + windSpeedDOM.innerText = Math.round(state.windSpeed); +} + +window.addEventListener("mousemove", function (e) { + settingsDOM.style.opacity = 1; + info1DOM.style.opacity = 1; + info2DOM.style.opacity = 1; +}); + +const enterFullscreen = document.getElementById("enter-fullscreen"); +const exitFullscreen = document.getElementById("exit-fullscreen"); + +function toggleFullscreen() { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen(); + enterFullscreen.setAttribute("stroke", "transparent"); + exitFullscreen.setAttribute("stroke", "white"); + } else { + document.exitFullscreen(); + enterFullscreen.setAttribute("stroke", "white"); + exitFullscreen.setAttribute("stroke", "transparent"); + } +} diff --git a/games/The Godzilla Fights game(html,css,js)/style.css b/games/The Godzilla Fights game(html,css,js)/style.css new file mode 100644 index 00000000..3c48b9a2 --- /dev/null +++ b/games/The Godzilla Fights game(html,css,js)/style.css @@ -0,0 +1,256 @@ +@import url("https://fonts.googleapis.com/css2?family=Inconsolata:wght@400;700&display=swap"); + +body { + margin: 0; + padding: 0; + font-family: "Inconsolata", monospace; + font-size: 14px; + color: white; + user-select: none; + -webkit-user-select: none; + + display: flex; + justify-content: center; + align-items: center; + height: 100%; + overflow: hidden; +} + +button { + cursor: pointer; + border: none; + color: white; + background: transparent; + font-family: "Inconsolata", monospace; + padding: 10px; + font-size: 1em; +} + +button:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +#info-left, +#info-right { + position: absolute; + top: 20px; +} + +#info-left { + left: 25px; +} + +#info-right { + right: 25px; + text-align: right; +} + +#bomb-grab-area { + position: absolute; + width: 30px; + height: 30px; + border-radius: 50%; + background-color: transparent; + cursor: grab; +} + +#instructions, +#congratulations { + position: absolute; + transition: visibility 0s, opacity 0.5s linear; +} + +@media (min-height: 535px) { + #instructions { + min-height: 200px; + } +} + +#congratulations { + background-color: rgba(255, 255, 255, 0.9); + color: black; + padding: 50px 80px; + opacity: 0; + visibility: hidden; + max-width: 300px; + backdrop-filter: blur(5px); +} + +#congratulations p a { + color: inherit; +} + +#congratulations button { + border: 1px solid rgba(0, 0, 0, 0.9); + color: inherit; +} + +#settings { + position: absolute; + top: calc(20px + 16.385px - 10px); + display: flex; + align-items: center; + gap: 10px; + right: 11em; +} + +#settings, +#info-left, +#info-right { + opacity: 0; + transition: opacity 3s; +} + +@media (max-width: 450px) { + #settings, + #info-left, + #info-right { + opacity: 0; + } + #instructions { + visibility: hidden; + } +} + +/* Basic CSS for the dropdown */ +.dropdown { + position: relative; + display: inline-block; +} + +.dropbtn:after { + content: "โ–ผ"; + margin-left: 7px; + font-size: 0.8em; + vertical-align: text-top; +} + +.dropdown-content { + display: none; + position: absolute; + background-color: #f9f9f9; + min-width: 120px; + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); +} + +.dropdown-content a { + color: black; + padding: 12px 16px; + text-decoration: none; + display: block; + white-space: nowrap; + font-size: 0.9em; +} + +.dropdown-content a:hover { + background-color: #f1f1f1; +} + +/* Show dropdown content when hovering over the button */ +.dropdown:hover .dropdown-content { + display: block; +} + +#windmill { + position: absolute; + right: 0; + fill: rgba(255, 255, 255, 0.5); + transform-origin: bottom; +} + +#windmill-head { + animation-name: rotate; + animation-duration: 4s; + animation-iteration-count: infinite; + animation-timing-function: linear; +} + +@keyframes rotate { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +#wind-info { + position: absolute; + width: 100px; + text-align: center; + margin-bottom: 30px; +} + +/** Youtube logo by https://codepen.io/alvaromontoro */ +#youtube { + z-index: 2; + display: block; + width: 30px; + height: 21px; + background: rgb(0, 26, 255); + position: relative; + border-radius: 50% / 11%; + transition: transform 0.5s; + color: black; +} + +#youtube:hover, +#youtube:focus { + transform: scale(1.2); +} + +#youtube::before { + content: ""; + display: block; + position: absolute; + top: 7.5%; + left: -6%; + width: 112%; + height: 85%; + background: red; + border-radius: 9% / 50%; +} + +#youtube::after { + content: ""; + display: block; + position: absolute; + top: 6px; + left: 11px; + width: 15px; + height: 10px; + border: 5px solid transparent; + box-sizing: border-box; + border-left: 10px solid white; +} + +#youtube span { + font-size: 0; + position: absolute; + width: 0; + height: 0; + overflow: hidden; +} + +#youtube-card { + display: none; +} + +#youtube:hover + #youtube-card { + color: black; + display: block; + position: absolute; + top: -20px; + right: -20px; + padding: 25px 60px 25px 25px; + width: 200px; + background-color: white; +} + +#fullscreen { + all: unset; + cursor: pointer; + position: absolute; + right: 10px; + bottom: 10px; +} \ No newline at end of file diff --git a/games/Timing-Bar/index.html b/games/Timing-Bar/index.html new file mode 100644 index 00000000..fa9141a3 --- /dev/null +++ b/games/Timing-Bar/index.html @@ -0,0 +1,38 @@ + + + + + + Timing Bar | Mini JS Games Hub + + + +
+

๐ŸŽฏ Timing Bar

+ +
+
+
+
+ +
+ + + + +
+ +
+

Score: 0

+

Level: 1

+

Status: Waiting...

+
+
+ + + + + + + + diff --git a/games/Timing-Bar/script.js b/games/Timing-Bar/script.js new file mode 100644 index 00000000..8bb2dba3 --- /dev/null +++ b/games/Timing-Bar/script.js @@ -0,0 +1,116 @@ +const movingBar = document.querySelector(".moving-bar"); +const targetZone = document.querySelector(".target-zone"); +const scoreEl = document.getElementById("score"); +const levelEl = document.getElementById("level"); +const statusEl = document.getElementById("status"); +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const resumeBtn = document.getElementById("resume-btn"); +const restartBtn = document.getElementById("restart-btn"); + +const hitSound = document.getElementById("hit-sound"); +const missSound = document.getElementById("miss-sound"); +const bgMusic = document.getElementById("bg-music"); + +let score = 0; +let level = 1; +let isRunning = false; +let animationId = null; +let speed = 3; +let direction = 1; +let position = 0; + +function startGame() { + if (!isRunning) { + isRunning = true; + bgMusic.volume = 0.3; + bgMusic.play(); + animateBar(); + statusEl.textContent = "Running..."; + } +} + +function animateBar() { + const barAreaWidth = document.querySelector(".bar-area").offsetWidth; + const barWidth = movingBar.offsetWidth; + + position += direction * speed; + if (position <= 0 || position >= barAreaWidth - barWidth) { + direction *= -1; + } + movingBar.style.left = position + "px"; + + animationId = requestAnimationFrame(animateBar); +} + +function stopBar() { + if (!isRunning) return; + + cancelAnimationFrame(animationId); + const targetRect = targetZone.getBoundingClientRect(); + const barRect = movingBar.getBoundingClientRect(); + const overlap = Math.min(targetRect.right, barRect.right) - Math.max(targetRect.left, barRect.left); + + if (overlap > 0) { + hitSound.play(); + const accuracy = overlap / targetRect.width; + const points = Math.round(accuracy * 100); + score += points; + statusEl.textContent = `๐ŸŽฏ Perfect! +${points}`; + if (points > 90) { + level++; + speed += 0.5; + } + } else { + missSound.play(); + statusEl.textContent = "โŒ Missed!"; + score = Math.max(0, score - 50); + } + + scoreEl.textContent = score; + levelEl.textContent = level; + isRunning = false; +} + +function pauseGame() { + if (isRunning) { + cancelAnimationFrame(animationId); + bgMusic.pause(); + isRunning = false; + statusEl.textContent = "Paused"; + } +} + +function resumeGame() { + if (!isRunning) { + bgMusic.play(); + isRunning = true; + animateBar(); + statusEl.textContent = "Resumed"; + } +} + +function restartGame() { + cancelAnimationFrame(animationId); + position = 0; + score = 0; + level = 1; + speed = 3; + movingBar.style.left = "0px"; + scoreEl.textContent = score; + levelEl.textContent = level; + statusEl.textContent = "Restarted!"; + bgMusic.currentTime = 0; + bgMusic.play(); + isRunning = true; + animateBar(); +} + +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", pauseGame); +resumeBtn.addEventListener("click", resumeGame); +restartBtn.addEventListener("click", restartGame); +document.body.addEventListener("keydown", (e) => { + if (e.code === "Space") stopBar(); +}); +document.body.addEventListener("click", stopBar); diff --git a/games/Timing-Bar/style.css b/games/Timing-Bar/style.css new file mode 100644 index 00000000..f82d59d6 --- /dev/null +++ b/games/Timing-Bar/style.css @@ -0,0 +1,86 @@ +body { + margin: 0; + font-family: "Poppins", sans-serif; + background: radial-gradient(circle at center, #0f0c29, #302b63, #24243e); + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + overflow: hidden; +} + +.game-container { + text-align: center; + background: rgba(255, 255, 255, 0.05); + padding: 30px; + border-radius: 15px; + box-shadow: 0 0 30px rgba(0, 255, 255, 0.4); + width: 400px; +} + +h1 { + text-shadow: 0 0 15px cyan; + margin-bottom: 20px; +} + +.bar-area { + position: relative; + width: 100%; + height: 40px; + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + overflow: hidden; + margin-bottom: 25px; + box-shadow: inset 0 0 15px rgba(0, 255, 255, 0.2); +} + +.target-zone { + position: absolute; + width: 60px; + height: 100%; + left: 50%; + transform: translateX(-50%); + background: rgba(0, 255, 0, 0.5); + border-radius: 10px; + box-shadow: 0 0 20px lime; +} + +.moving-bar { + position: absolute; + width: 50px; + height: 100%; + left: 0; + background: cyan; + border-radius: 10px; + box-shadow: 0 0 20px cyan; + animation: moveBar 2s linear infinite; +} + +@keyframes moveBar { + 0% { left: 0; } + 100% { left: calc(100% - 50px); } +} + +.controls button { + background: none; + border: 2px solid cyan; + color: white; + padding: 8px 15px; + margin: 5px; + border-radius: 8px; + cursor: pointer; + transition: 0.3s; +} + +.controls button:hover { + background: cyan; + color: black; + box-shadow: 0 0 20px cyan; +} + +.scoreboard { + margin-top: 15px; + font-size: 1rem; + text-shadow: 0 0 10px cyan; +} diff --git a/games/Tower of Hanoi/index.html b/games/Tower of Hanoi/index.html new file mode 100644 index 00000000..0570a5d2 --- /dev/null +++ b/games/Tower of Hanoi/index.html @@ -0,0 +1,38 @@ + + + + + + Tower of Hanoi ๐Ÿฐ + + + +
+

๐Ÿฐ Tower of Hanoi

+

Move all disks from Tower A to Tower C โ€” using the fewest moves possible!

+
+ +
+ + + + +

Moves: 0

+
+ +
+
A
+
B
+
C
+
+ +
+ + + + diff --git a/games/Tower of Hanoi/script.js b/games/Tower of Hanoi/script.js new file mode 100644 index 00000000..9ff1304c --- /dev/null +++ b/games/Tower of Hanoi/script.js @@ -0,0 +1,94 @@ +const towers = { + A: document.getElementById("towerA"), + B: document.getElementById("towerB"), + C: document.getElementById("towerC") +}; + +let selectedDisk = null; +let moves = 0; +let diskCount = 3; + +const moveCount = document.getElementById("moveCount"); +const message = document.getElementById("message"); +const startBtn = document.getElementById("startBtn"); +const diskSelector = document.getElementById("diskCount"); + +const colors = ["#38bdf8", "#818cf8", "#f472b6", "#fb923c", "#4ade80", "#eab308"]; + +function createDisks(n) { + for (let tower of Object.values(towers)) tower.innerHTML = '
' + tower.id.slice(-1) + '
'; + for (let i = n; i > 0; i--) { + const disk = document.createElement("div"); + disk.className = "disk"; + disk.style.width = 60 + i * 20 + "px"; + disk.style.background = colors[i % colors.length]; + towers.A.appendChild(disk); + } + moves = 0; + moveCount.textContent = "Moves: 0"; + message.textContent = ""; +} + +function handleTowerClick(e) { + const tower = e.currentTarget; + const topDisk = tower.lastElementChild?.classList.contains("label") ? null : tower.lastElementChild; + + if (!selectedDisk) { + if (!topDisk) return; + selectedDisk = topDisk; + selectedDisk.style.transform = "translateY(-20px)"; + selectedDisk.style.filter = "brightness(1.3)"; + } else { + if (tower === selectedDisk.parentElement) { + resetDiskSelection(); + return; + } + + const destTop = topDisk; + if (!destTop || selectedDisk.offsetWidth < destTop.offsetWidth) { + tower.appendChild(selectedDisk); + moves++; + moveCount.textContent = `Moves: ${moves}`; + playSound(); + resetDiskSelection(); + checkWin(); + } else { + shakeTower(tower); + resetDiskSelection(); + } + } +} + +function shakeTower(tower) { + tower.style.animation = "shake 0.3s"; + tower.addEventListener("animationend", () => (tower.style.animation = ""), { once: true }); +} + +function resetDiskSelection() { + if (selectedDisk) { + selectedDisk.style.transform = ""; + selectedDisk.style.filter = ""; + selectedDisk = null; + } +} + +function checkWin() { + if (towers.C.childElementCount - 1 === diskCount) { + message.textContent = `๐ŸŽ‰ You solved it in ${moves} moves!`; + } +} + +function playSound() { + const audio = new Audio("https://cdn.pixabay.com/download/audio/2022/03/15/audio_50f34169e4.mp3?filename=click-124467.mp3"); + audio.volume = 0.4; + audio.play(); +} + +startBtn.addEventListener("click", () => { + diskCount = parseInt(diskSelector.value); + createDisks(diskCount); +}); + +Object.values(towers).forEach(t => t.addEventListener("click", handleTowerClick)); + +createDisks(diskCount); diff --git a/games/Tower of Hanoi/style.css b/games/Tower of Hanoi/style.css new file mode 100644 index 00000000..d8da4d80 --- /dev/null +++ b/games/Tower of Hanoi/style.css @@ -0,0 +1,134 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: "Poppins", sans-serif; + background: radial-gradient(circle at top, #60a5fa, #3b82f6, #1e3a8a); + min-height: 100vh; + color: white; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + overflow-x: hidden; +} + +.game-header { + text-align: center; + margin-top: 30px; +} + +.game-header h1 { + font-size: 2.5rem; + text-shadow: 0 0 15px #93c5fd; +} + +.tagline { + margin-top: 8px; + font-size: 1.1rem; +} + +.controls { + display: flex; + align-items: center; + gap: 20px; + background: rgba(255,255,255,0.1); + padding: 10px 20px; + border-radius: 15px; + box-shadow: 0 4px 15px rgba(0,0,0,0.3); + margin: 20px 0; +} + +.controls label, .controls select, .controls button, .controls p { + font-size: 1rem; +} + +select, button { + border: none; + outline: none; + padding: 8px 14px; + border-radius: 10px; + font-weight: 600; + cursor: pointer; +} + +select { + background: #2563eb; + color: white; +} + +button { + background: linear-gradient(90deg, #f472b6, #a78bfa); + color: white; + transition: transform 0.2s ease, box-shadow 0.3s ease; +} + +button:hover { + transform: scale(1.05); + box-shadow: 0 0 15px rgba(255,255,255,0.5); +} + +#moveCount { + font-weight: bold; +} + +.game-area { + display: flex; + justify-content: center; + align-items: flex-end; + gap: 60px; + width: 90%; + max-width: 900px; + height: 350px; + margin-top: 20px; +} + +.tower { + width: 140px; + height: 300px; + background: rgba(0,0,0,0.25); + border-radius: 10px; + border: 3px solid rgba(255,255,255,0.2); + display: flex; + flex-direction: column-reverse; + align-items: center; + justify-content: flex-start; + position: relative; + cursor: pointer; + transition: all 0.3s ease; +} + +.tower:hover { + box-shadow: 0 0 15px rgba(255,255,255,0.4); +} + +.label { + position: absolute; + bottom: -30px; + font-weight: bold; + font-size: 1.2rem; +} + +.disk { + height: 25px; + border-radius: 8px; + margin-bottom: 5px; + transition: all 0.3s ease-in-out; + box-shadow: 0 4px 10px rgba(0,0,0,0.4); +} + +#message { + margin-top: 30px; + font-size: 1.4rem; + font-weight: bold; + text-shadow: 0 0 10px #fef9c3; + animation: fadeIn 1s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} diff --git a/games/Twister Talk/index.html b/games/Twister Talk/index.html new file mode 100644 index 00000000..130939b9 --- /dev/null +++ b/games/Twister Talk/index.html @@ -0,0 +1,31 @@ + + + + + + Twister Talk + + + +
+

๐Ÿ‘… Twister Talk

+

Try to type the tongue twister exactly before time runs out!

+ +
+

Press Start to get a tongue twister!

+ +

+

โณ Time Left: 10s

+
+ +
+ + +
+ +

Score: 0

+
+ + + + diff --git a/games/Twister Talk/script.js b/games/Twister Talk/script.js new file mode 100644 index 00000000..cfaa27cb --- /dev/null +++ b/games/Twister Talk/script.js @@ -0,0 +1,72 @@ +const twisters = [ + "She sells seashells by the seashore.", + "How much wood would a woodchuck chuck if a woodchuck could chuck wood?", + "Peter Piper picked a peck of pickled peppers.", + "Fuzzy Wuzzy was a bear. Fuzzy Wuzzy had no hair.", + "Red lorry, yellow lorry, red lorry, yellow lorry.", + "Betty bought a bit of butter but the butter was bitter." +]; + +const twisterEl = document.getElementById("twister"); +const userInput = document.getElementById("userInput"); +const startBtn = document.getElementById("startBtn"); +const restartBtn = document.getElementById("restartBtn"); +const feedback = document.getElementById("feedback"); +const timerEl = document.getElementById("time"); +const scoreEl = document.getElementById("score"); + +let timeLeft = 10; +let timer; +let currentTwister = ""; +let score = 0; + +function startGame() { + feedback.textContent = ""; + startBtn.disabled = true; + restartBtn.disabled = false; + userInput.disabled = false; + userInput.value = ""; + timeLeft = 10; + timerEl.textContent = timeLeft; + + // Random twister + currentTwister = twisters[Math.floor(Math.random() * twisters.length)]; + twisterEl.textContent = currentTwister; + + timer = setInterval(() => { + timeLeft--; + timerEl.textContent = timeLeft; + if (timeLeft <= 0) { + endGame(false); + } + }, 1000); +} + +function endGame(success) { + clearInterval(timer); + userInput.disabled = true; + startBtn.disabled = false; + restartBtn.disabled = true; + + if (success) { + feedback.textContent = "โœ… Perfect! You nailed it!"; + feedback.style.color = "#7CFC00"; + score++; + scoreEl.textContent = score; + } else { + feedback.textContent = "โŒ Time's up! Try again."; + feedback.style.color = "#FF6B6B"; + } +} + +userInput.addEventListener("input", () => { + if (userInput.value.trim() === currentTwister) { + endGame(true); + } +}); + +startBtn.addEventListener("click", startGame); +restartBtn.addEventListener("click", () => { + userInput.value = ""; + startGame(); +}); diff --git a/games/Twister Talk/style.css b/games/Twister Talk/style.css new file mode 100644 index 00000000..a01150d4 --- /dev/null +++ b/games/Twister Talk/style.css @@ -0,0 +1,74 @@ +body { + font-family: 'Poppins', sans-serif; + background: radial-gradient(circle, #1e1e2f, #12121a); + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.container { + text-align: center; + background: rgba(255, 255, 255, 0.1); + padding: 30px; + border-radius: 15px; + width: 350px; + box-shadow: 0 0 20px rgba(255, 0, 150, 0.3); +} + +h1 { + color: #ff4fd8; + text-shadow: 0 0 15px #ff4fd8; +} + +.instructions { + font-size: 0.9rem; + color: #ccc; +} + +.twister-text { + font-size: 1rem; + margin: 20px 0; + color: #ffb3f5; +} + +input { + width: 90%; + padding: 10px; + font-size: 1rem; + border-radius: 8px; + border: none; + outline: none; + text-align: center; + background-color: #222; + color: #fff; + box-shadow: inset 0 0 8px #ff4fd8; +} + +button { + background-color: #ff4fd8; + border: none; + padding: 10px 18px; + border-radius: 10px; + color: white; + font-weight: 600; + margin: 10px; + cursor: pointer; + transition: 0.3s; +} + +button:hover { + background-color: #ff7ae6; +} + +.timer, .score { + margin-top: 10px; + font-weight: 500; +} + +#feedback { + margin-top: 10px; + font-weight: 600; +} diff --git a/games/World-Match-up/index.html b/games/World-Match-up/index.html new file mode 100644 index 00000000..342ddf81 --- /dev/null +++ b/games/World-Match-up/index.html @@ -0,0 +1,34 @@ + + + + + + +World Match Up โ€” Memory: Country โ‡„ Capital + + + +
+

World Match Up

+

Match each country with its capital. Click to flip two cards.

+ + +
+ +
+Moves: 0 +Matches: 0/6 +
+
+ + +
+ + +
Made with โค โ€” Simple HTML/CSS/JS
+
+ + + + + diff --git a/games/World-Match-up/script.js b/games/World-Match-up/script.js new file mode 100644 index 00000000..81352202 --- /dev/null +++ b/games/World-Match-up/script.js @@ -0,0 +1,66 @@ +// FILE: script.js +return; +} + + +secondCard = {el:cardEl, data:cardData}; +lockBoard = true; +moves++; +movesEl.textContent = moves; + + +// check match: pairId equal but types must differ (country vs capital) +if(firstCard.data.pairId === secondCard.data.pairId && firstCard.data.type !== secondCard.data.type){ +// match +matches++; +matchesEl.textContent = matches; +// keep them flipped and disable +firstCard.el.disabled = true; +secondCard.el.disabled = true; +resetSelection(); +if(matches === pairs.length){ +setTimeout(()=>{ +alert(Congratulations! You matched all pairs in ${moves} moves.); +},200); +} +} else { +// not a match โ€” flip back +setTimeout(()=>{ +firstCard.el.classList.remove('is-flipped'); +secondCard.el.classList.remove('is-flipped'); +resetSelection(); +},700); +} +} + + +function resetSelection(){ +[firstCard, secondCard] = [null, null]; +lockBoard = false; +} + + +function startGame(){ +buildDeck(); +render(); +moves = 0; matches = 0; +movesEl.textContent = moves; +matchesEl.textContent = matches; +firstCard = secondCard = null; +lockBoard = false; +} + + +restartBtn.addEventListener('click', startGame); + + +// small HTML-escape helper +function escapeHtml(str){ +return String(str).replace(/[&<>"]+/g, (m)=>{ +return ({'&':'&','<':'<','>':'>','"':'"'}[m]); +}); +} + + +// start +startGame(); diff --git a/games/World-Match-up/style.css b/games/World-Match-up/style.css new file mode 100644 index 00000000..779c9ff9 --- /dev/null +++ b/games/World-Match-up/style.css @@ -0,0 +1,67 @@ +--card:#ffffff; +--accent:#06b6d4; +--muted:#94a3b8; +} +*{box-sizing:border-box} +html,body{height:100%} +body{ +margin:0; +font-family:Inter, system-ui, -apple-system, "Segoe UI", Roboto, 'Helvetica Neue', Arial; +background:linear-gradient(180deg, #071021 0%, #0b1220 100%); +color:var(--card); +display:flex; +align-items:center; +justify-content:center; +padding:24px; +} +.container{ +width:100%; +max-width:900px; +text-align:center; +} +h1{font-size:clamp(1.4rem,2.5vw,2rem);margin:0 0 6px} +.subtitle{color:var(--muted);margin-top:0;margin-bottom:16px} +.controls{display:flex;gap:12px;align-items:center;justify-content:center;margin-bottom:14px} +.controls button{background:transparent;border:1px solid rgba(255,255,255,0.12);padding:8px 14px;border-radius:10px;color:var(--card);cursor:pointer} +.controls button:hover{border-color:var(--accent)} +.stats{color:var(--muted);display:flex;gap:16px;align-items:center} +.grid{ +display:grid; +grid-template-columns:repeat(4, 1fr); +gap:14px; +padding:6px; +} +.card{ +perspective:1000px; +} +.card-inner{ +position:relative; +width:100%; +padding-top:75%; /* aspect ratio */ +transform-style:preserve-3d; +transition:transform 400ms cubic-bezier(.2,.9,.3,1); +border-radius:12px; +} +.card.is-flipped .card-inner{transform:rotateY(180deg)} +.face{ +position:absolute;inset:0;border-radius:12px;display:flex;align-items:center;justify-content:center;padding:8px; +backface-visibility:hidden;box-shadow:0 6px 18px rgba(2,6,23,0.6); +} +.face.front{ +background:linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02)); +color:var(--card);transform:rotateY(180deg); +border:1px solid rgba(255,255,255,0.05); +} +.face.back{ +background:linear-gradient(180deg, #0b1220, #061021); +color:var(--muted); +border:2px dashed rgba(255,255,255,0.03); +font-weight:600; +} +.card-label{font-size:0.9rem;line-height:1.1} +.footer{margin-top:18px;color:var(--muted);font-size:0.9rem} + + +@media (max-width:640px){ +.grid{grid-template-columns:repeat(2,1fr)} +} diff --git a/games/adventure_game/index.html b/games/adventure_game/index.html new file mode 100644 index 00000000..9c9a6a18 --- /dev/null +++ b/games/adventure_game/index.html @@ -0,0 +1,34 @@ + + + + + + The Lost Artifact - Text Adventure + + + +
+

The Lost Artifact

+ +
+

Health: 10

+

Inventory: Empty

+
+ +
+ +
+

The adventure is loading...

+
+ +
+ +
+
+ + +
+ + + + \ No newline at end of file diff --git a/games/adventure_game/script.js b/games/adventure_game/script.js new file mode 100644 index 00000000..891d45bd --- /dev/null +++ b/games/adventure_game/script.js @@ -0,0 +1,185 @@ +// --- DOM Elements --- +const storyText = document.getElementById('story-text'); +const choicesContainer = document.getElementById('choices-container'); +const healthDisplay = document.getElementById('health'); +const inventoryDisplay = document.getElementById('inventory'); +const restartButton = document.getElementById('restart-button'); + +// --- Game State Variables --- +let player = { + health: 10, + inventory: [] +}; + +let currentScene = 'start'; + +// --- Game World Definition (The Story/State Object) --- + +const gameScenes = { + start: { + text: "You awaken in a damp, cold chamber. A faint glow emanates from two passages: one leading **North** and one leading **East**.", + choices: [ + { text: "Go North, toward the faint light.", nextScene: "passage_north" }, + { text: "Go East, toward the deeper shadows.", nextScene: "passage_east" }, + { text: "Check your pockets.", nextScene: "start", effect: () => alert("You find nothing but lint and despair.") } + ] + }, + + passage_north: { + text: "The northern passage opens into a circular room. In the center is a **pedestal** holding a small, rusty **key**. A large, snoring **troll** blocks the exit to the West.", + choices: [ + { text: "Take the Key.", nextScene: "passage_north", effect: () => takeItem('Rusty Key', 1) }, + { text: "Try to sneak past the Troll (requires Key).", nextScene: "troll_fight", condition: () => player.inventory.includes('Rusty Key') }, + { text: "Go Back (South).", nextScene: "start" } + ] + }, + + passage_east: { + text: "The eastern passage is a dead-end. You find an ancient, ornate **dagger** lying in the dust. You hear faint whispers coming from the North.", + choices: [ + { text: "Take the Dagger.", nextScene: "passage_east", effect: () => takeItem('Dagger', 0) }, + { text: "Go Back (West).", nextScene: "start" } + ] + }, + + troll_fight: { + text: "", // Text will be set dynamically based on outcome + choices: [ + { text: "Continue...", nextScene: "passage_west" } + ], + // Special function to handle a combat/conditional outcome + onEnter: () => { + if (player.inventory.includes('Dagger')) { + player.health -= 2; + gameScenes.troll_fight.text = "You distract the troll with the key, then plunge the dagger into its soft belly! The troll roars and collapses. You take 2 damage in the struggle."; + } else { + player.health -= 5; + gameScenes.troll_fight.text = "You manage to distract the troll with the key, but without a weapon, the troll swipes you hard before you escape. You barely slip by, taking 5 damage."; + } + updateStatus(); + if (player.health <= 0) { + return 'death'; + } + return null; // Continue to the next scene as normal + } + }, + + passage_west: { + text: "You stagger into a large cavern. You see the **Lost Artifact** shimmering on a ledge! You are victorious!", + choices: [], + onEnter: () => endGame("Victory! You found the Lost Artifact.") + }, + + death: { + text: "Your health has fallen to zero. Your quest ends here.", + choices: [], + onEnter: () => endGame("Defeated.") + } +}; + +// --- Game Logic Functions --- + +/** + * Updates the UI based on the player's current health and inventory. + */ +function updateStatus() { + healthDisplay.textContent = player.health; + inventoryDisplay.textContent = player.inventory.length > 0 ? player.inventory.join(', ') : 'Empty'; +} + +/** + * Handles picking up an item. + * @param {string} item The name of the item. + * @param {number} healthBoost Optional health increase. + */ +function takeItem(item, healthBoost = 0) { + if (!player.inventory.includes(item)) { + player.inventory.push(item); + player.health += healthBoost; + alert(`${item} added to inventory!`); + } else { + alert(`You already have the ${item}.`); + } + updateStatus(); +} + +/** + * Renders the story text and choices for the current scene. + */ +function renderScene() { + const scene = gameScenes[currentScene]; + storyText.innerHTML = `

${scene.text}

`; + choicesContainer.innerHTML = ''; // Clear old choices + restartButton.style.display = 'none'; + + // Handle special scene logic (like combat resolution) + if (scene.onEnter) { + const nextOverride = scene.onEnter(); + if (nextOverride) { + currentScene = nextOverride; + renderScene(); + return; + } + } + + // Render choices as buttons + scene.choices.forEach((choice) => { + const button = document.createElement('button'); + button.classList.add('choice-button'); + button.textContent = choice.text; + + // Check for conditions (e.g., must have key) + if (choice.condition && !choice.condition()) { + button.disabled = true; + button.title = "Requires a certain item or condition."; + } else { + button.onclick = () => makeChoice(choice); + } + + choicesContainer.appendChild(button); + }); +} + +/** + * Processes the player's choice and moves to the next scene. + * @param {object} choice The selected choice object. + */ +function makeChoice(choice) { + // 1. Apply any immediate effects (like picking up an item) + if (choice.effect) { + choice.effect(); + } + + // 2. Transition to the next scene + currentScene = choice.nextScene; + + // 3. Render the new scene + renderScene(); +} + +/** + * Ends the game and cleans up the UI. + * @param {string} finalMessage The message to display (e.g., Victory/Defeat). + */ +function endGame(finalMessage) { + storyText.innerHTML = `

**GAME OVER**

${finalMessage}

Your final health was: ${player.health}

`; + choicesContainer.innerHTML = ''; + restartButton.style.display = 'block'; +} + +/** + * Resets all variables and starts the game over. + */ +function restartGame() { + player = { health: 10, inventory: [] }; + currentScene = 'start'; + updateStatus(); + renderScene(); +} + +// --- Initialization --- +document.addEventListener('DOMContentLoaded', () => { + updateStatus(); + renderScene(); + restartButton.onclick = restartGame; +}); \ No newline at end of file diff --git a/games/adventure_game/style.css b/games/adventure_game/style.css new file mode 100644 index 00000000..5bcf95b8 --- /dev/null +++ b/games/adventure_game/style.css @@ -0,0 +1,100 @@ +body { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #2e3440; /* Dark background */ + color: #eceff4; /* Light text */ + font-family: 'Courier New', monospace; /* Monospace for classic feel */ + font-size: 16px; +} + +#game-container { + width: 90%; + max-width: 700px; + padding: 30px; + border: 2px solid #a3be8c; + background-color: #3b4252; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); +} + +h1 { + color: #88c0d0; /* Blue header */ + text-align: center; + margin-bottom: 20px; +} + +hr { + border: 0; + border-top: 1px solid #4c566a; + margin: 20px 0; +} + +/* * Status Panel + */ +#status-panel { + display: flex; + justify-content: space-around; + padding: 10px; + background-color: #4c566a; + border-radius: 5px; +} + +#health { + color: #bf616a; /* Red for health */ + font-weight: bold; +} + +/* * Story Text + */ +#story-text { + min-height: 150px; + padding: 10px 0; + line-height: 1.6; + white-space: pre-wrap; /* Preserves formatting */ +} + +/* * Choices + */ +#choices-container { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 20px; +} + +.choice-button { + padding: 12px; + border: 1px solid #5e81ac; + background-color: #5e81ac; /* Button color */ + color: #eceff4; + cursor: pointer; + text-align: left; + transition: background-color 0.2s, border-color 0.2s; + font-family: inherit; + font-size: 1em; +} + +.choice-button:hover { + background-color: #81a1c1; + border-color: #81a1c1; +} + +.choice-button:disabled { + background-color: #4c566a; + color: #d8dee9; + cursor: default; +} + +#restart-button { + display: block; + width: 100%; + margin-top: 20px; + padding: 15px; + background-color: #b48ead; /* Purple/Restart color */ + border: none; + color: #eceff4; + font-size: 1.1em; + cursor: pointer; +} \ No newline at end of file diff --git a/games/adventure_game_new/index.html b/games/adventure_game_new/index.html new file mode 100644 index 00000000..03103b62 --- /dev/null +++ b/games/adventure_game_new/index.html @@ -0,0 +1,38 @@ + + + + + + The Viewport Voyager ๐Ÿ“ + + + + +
+

Viewport Voyager

+

Use your mouse to **resize the browser window** to manipulate the level geometry.

+
+ Width: 0px | Height: 0px +
+
Current Level: 1
+
Resize the window until a platform aligns with the player!
+
+ +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ + + + \ No newline at end of file diff --git a/games/adventure_game_new/script.js b/games/adventure_game_new/script.js new file mode 100644 index 00000000..d1cfaebd --- /dev/null +++ b/games/adventure_game_new/script.js @@ -0,0 +1,282 @@ +// --- 1. Game Constants and State --- +const PLAYER_SIZE = 20; +const LEVEL_CONFIG = [ + { + id: 1, + goalW: 800, + goalH: 600, + goalRatio: 1.3333, // Target Aspect Ratio: 4:3 + message: "Resize to 800x600 to align the hidden platform and reach the Goal!", + platforms: { + 'platform-a': { w: 200, h: 20, relX: -100, relY: 100 }, + 'platform-b': { w: 100, h: 20, relX: 300, relY: -50 }, + 'platform-c': { w: 0, h: 0, relX: 0, relY: 0 }, // Placeholder to keep keys consistent + 'hidden-platform': { w: 150, h: 20, relX: 0, relY: 150, revealW: 790, revealH: 590 } + }, + goal: { w: 30, h: 30, relX: 350, relY: -100 } + }, + { + id: 2, + goalW: 900, // Example dimensions (W/H = 1.5) + goalH: 600, + goalRatio: 1.5, // Target Aspect Ratio: 3:2 + message: "Level 2: The Ratio Riser. Resize the window until the Aspect Ratio (W/H) is exactly 1.50!", + platforms: { + // These platforms move diagonally based on the ratio + 'platform-a': { w: 150, h: 20, relX: -250, relY: 150, ratioXMult: 0.5 }, + 'platform-b': { w: 150, h: 20, relX: 50, relY: 0, ratioXMult: 0.2 }, + 'platform-c': { w: 150, h: 20, relX: 250, relY: -150, ratioXMult: 0.1 }, // New platform + 'hidden-platform': { w: 0, h: 0, relX: 0, relY: 0 } // Hide in this level + }, + goal: { w: 30, h: 30, relX: 300, relY: -50 } + } +]; + +let currentLevelIndex = 0; +let playerOffsetX = 0; // Player offset from the center (for WASD) +let playerOffsetY = 0; +let isGrounded = false; + +// --- 2. DOM Elements --- +const D = (id) => document.getElementById(id); +const $ = { + player: D('player'), + dimW: D('dim-w'), + dimH: D('dim-h'), + gameMessage: D('game-message'), + levelStatus: D('level-status'), + goal: D('goal'), + platformA: D('platform-a'), + platformB: D('platform-b'), + platformC: D('platform-c'), // Reference to the new platform + hiddenPlatform: D('hidden-platform') +}; + +// --- 3. Core Geometry and Resize Functions --- + +/** + * Updates the position and size of all level elements based on the current viewport. + */ +function updateLevelGeometry() { + const levelData = LEVEL_CONFIG[currentLevelIndex]; + const width = window.innerWidth; + const height = window.innerHeight; + const currentRatio = width / height; + + // UI Update + $.dimW.textContent = width; + $.dimH.textContent = height; + + const centerX = width / 2; + const centerY = height / 2; + + // --- Determine Dynamic Shift Factor --- + let shiftFactor = 0; + + if (levelData.id === 1) { + // Level 1: Shift based on deviation from fixed target W + shiftFactor = (width - levelData.goalW) * 0.5; + } else if (levelData.id === 2) { + // Level 2: Shift based on deviation from target Aspect Ratio (1.5) + shiftFactor = (currentRatio - levelData.goalRatio) * 400; // Multiplier adjusts sensitivity + } + + // --- Dynamic Positioning --- + + for (const key in levelData.platforms) { + const platformElement = D(key); + // Ensure element exists and is required for the level + if (!platformElement) continue; + + const p = levelData.platforms[key]; + + // Hide/Show element based on its size in the level config + if (p.w === 0 && p.h === 0) { + platformElement.style.display = 'none'; + continue; + } else { + platformElement.style.display = 'block'; + } + + let finalX = centerX + p.relX; + let finalY = centerY + p.relY; + + // 2. Apply Shift Factor + if (levelData.id === 1) { + finalX -= shiftFactor; + // Hidden platform reveal logic + if (key === 'hidden-platform' && p.revealW && p.revealH) { + const distW = Math.abs(width - p.revealW); + const distH = Math.abs(height - p.revealH); + platformElement.style.opacity = (distW < 20 && distH < 20) ? 1 : 0.1; + } + } else if (levelData.id === 2) { + // Level 2: Platforms shift diagonally based on the ratio factor + finalX -= shiftFactor * (p.ratioXMult || 1); + finalY += shiftFactor * (p.ratioXMult || 1); + + platformElement.style.opacity = 1; + } + + platformElement.style.width = `${p.w}px`; + platformElement.style.height = `${p.h}px`; + platformElement.style.left = `${finalX}px`; + platformElement.style.top = `${finalY}px`; + } + + // Goal Positioning + let goalShiftX = (levelData.id === 1) ? shiftFactor : shiftFactor * 0.5; + let goalShiftY = (levelData.id === 2) ? shiftFactor * 0.5 : 0; + + $.goal.style.left = `${centerX + levelData.goal.relX - goalShiftX}px`; + $.goal.style.top = `${centerY + levelData.goal.relY + goalShiftY}px`; + $.goal.textContent = levelData.id; + + // Recalculate everything after moving the world + checkCollisions(); + checkPuzzleState(); +} + +/** + * Checks if the player is touching any platform or the goal. + */ +function checkCollisions() { + isGrounded = false; + + const playerRect = $.player.getBoundingClientRect(); + const playerBottom = playerRect.top + playerRect.height; + + const elements = document.querySelectorAll('.platform, .goal'); + + for (const element of elements) { + // Skip hidden/placeholder elements + if (element.style.display === 'none' || element.style.opacity < 0.5) continue; + + const elementRect = element.getBoundingClientRect(); + + const overlapX = playerRect.left < elementRect.right && playerRect.right > elementRect.left; + const overlapY = playerRect.top < elementRect.bottom && playerRect.bottom > elementRect.top; + + if (overlapX && overlapY) { + if (element.classList.contains('goal')) { + handleGoalReached(); + return; + } + + if (playerBottom > elementRect.top && playerBottom < elementRect.top + 5) { + isGrounded = true; + } + } + } +} + +/** + * Checks if the current viewport dimensions meet the level's specific requirements. + */ +function checkPuzzleState() { + const levelData = LEVEL_CONFIG[currentLevelIndex]; + const width = window.innerWidth; + const height = window.innerHeight; + + if (levelData.id === 1) { + // Level 1: Fixed Dimensions + const W_TOLERANCE = 10; + const H_TOLERANCE = 10; + + const nearW = Math.abs(width - levelData.goalW) < W_TOLERANCE; + const nearH = Math.abs(height - levelData.goalH) < H_TOLERANCE; + + if (nearW && nearH) { + $.gameMessage.textContent = "๐ŸŽฏ Perfect dimensions! The platform is stable."; + } else { + $.gameMessage.textContent = levelData.message; + } + + const totalDistance = Math.abs(width - levelData.goalW) + Math.abs(height - levelData.goalH); + const darkness = Math.min(0.8, totalDistance / 1000); + document.body.style.backgroundColor = `hsl(240, 10%, ${10 + darkness * 10}%)`; + + } else if (levelData.id === 2) { + // Level 2: Aspect Ratio + const currentRatio = width / height; + const RATIO_TOLERANCE = 0.02; // Tight tolerance + + const nearRatio = Math.abs(currentRatio - levelData.goalRatio) < RATIO_TOLERANCE; + + if (nearRatio) { + $.gameMessage.textContent = `โญ ASPECT RATIO ACHIEVED! Ratio: ${currentRatio.toFixed(3)}. The path is aligned!`; + document.body.style.backgroundColor = 'hsl(120, 50%, 15%)'; + } else { + $.gameMessage.textContent = `${levelData.message} Current Ratio: ${currentRatio.toFixed(3)}`; + + const ratioDistance = Math.abs(currentRatio - levelData.goalRatio); + const tint = Math.min(100, ratioDistance * 500); + document.body.style.backgroundColor = `hsl(0, ${tint}%, 15%)`; + } + } +} + +/** + * Handles what happens when the goal is reached. + */ +function handleGoalReached() { + alert(`Level ${LEVEL_CONFIG[currentLevelIndex].id} Complete!`); + currentLevelIndex++; + if (currentLevelIndex < LEVEL_CONFIG.length) { + loadLevel(); + } else { + $.gameMessage.textContent = "Game Complete! You are a master Viewport Voyager!"; + alert("You beat the game!"); + } +} + +/** + * Initializes the game for the current level. + */ +function loadLevel() { + const levelData = LEVEL_CONFIG[currentLevelIndex]; + + // Reset player position (center) + playerOffsetX = 0; + playerOffsetY = 0; + $.player.style.marginLeft = '0px'; + $.player.style.marginTop = '0px'; + + // Update UI + $.levelStatus.textContent = `Current Level: ${levelData.id}`; + $.gameMessage.textContent = levelData.message; + + updateLevelGeometry(); +} + + +// --- 4. Event Listeners and Initialization --- + +// CRITICAL: Rerun the geometry calculation and collision check on every resize +window.addEventListener('resize', updateLevelGeometry); + +// Optional: WASD/Arrow Key player movement for minor adjustments +window.addEventListener('keydown', (e) => { + const moveSpeed = 5; // Pixels per key press + + if (e.key === 'w' || e.key === 'ArrowUp') { + playerOffsetY -= moveSpeed; + } else if (e.key === 's' || e.key === 'ArrowDown') { + playerOffsetY += moveSpeed; + } else if (e.key === 'a' || e.key === 'ArrowLeft') { + playerOffsetX -= moveSpeed; + } else if (e.key === 'd' || e.key === 'ArrowRight') { + playerOffsetX += moveSpeed; + } + + // Apply movement offset + $.player.style.marginLeft = `${playerOffsetX}px`; + $.player.style.marginTop = `${playerOffsetY}px`; + + // Check collisions after movement + checkCollisions(); +}); + + +// Start the game! +loadLevel(); \ No newline at end of file diff --git a/games/adventure_game_new/style.css b/games/adventure_game_new/style.css new file mode 100644 index 00000000..084af7ec --- /dev/null +++ b/games/adventure_game_new/style.css @@ -0,0 +1,90 @@ +:root { + --player-size: 20px; + --world-bg: #282a36; /* Dark background */ + --platform-color: #50fa7b; /* Neon Green */ + --obstacle-color: #ff5555; /* Red */ + --goal-color: #bd93f9; /* Purple */ +} + +/* Global Reset and Background */ +body, html { + margin: 0; + padding: 0; + overflow: hidden; /* Prevent scrolls bars from interfering with dimensions */ + background-color: var(--world-bg); +} + +/* --- UI Overlay (Fixed) --- */ +#ui-overlay { + position: fixed; + top: 10px; + left: 10px; + padding: 10px 20px; + background-color: rgba(0, 0, 0, 0.7); + color: #f8f8f2; + font-family: sans-serif; + border-radius: 5px; + z-index: 100; +} + +#dimensions { + font-weight: bold; + color: #ffb86c; +} + +#game-message { + margin-top: 10px; + color: var(--platform-color); +} + +/* --- Player Element (Fixed in the center) --- */ +#player { + position: fixed; + width: var(--player-size); + height: var(--player-size); + background-color: #f8f8f2; + border-radius: 50%; + /* Key to keeping the player fixed in the center */ + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 50; + transition: margin-left 0.05s, margin-top 0.05s; /* For WASD movement */ +} + +/* --- Game World Elements (Positioned dynamically by JS) --- */ +.level-element { + position: absolute; + box-sizing: border-box; + transition: width 0.1s, height 0.1s, top 0.1s, left 0.1s, opacity 0.3s; /* Smooth visual changes */ + z-index: 10; +} + +.platform { + background-color: var(--platform-color); + height: 20px; + border-radius: 3px; +} + +.obstacle { + background-color: var(--obstacle-color); + border: 1px solid #444; +} + +.goal { + background-color: var(--goal-color); + width: 30px; + height: 30px; + border-radius: 5px; + display: flex; + justify-content: center; + align-items: center; + color: var(--world-bg); + font-weight: bold; + font-size: 10px; +} + +.hidden-platform { + background-color: #44475a; + opacity: 0.1; /* Barely visible hint */ +} \ No newline at end of file diff --git a/games/angle_shot/index.html b/games/angle_shot/index.html new file mode 100644 index 00000000..9eed6c87 --- /dev/null +++ b/games/angle_shot/index.html @@ -0,0 +1,38 @@ + + + + + + Angle Shot Archery + + + + +
+

๐Ÿน Angle Shot Archery

+ +
+ Score: 0 | Shots: 0 +
+ +
+ +
+ +
+ Angle: 45ยฐ | Power: 50 +
+ +
+ + +
+ +
+

Click and drag on the canvas to set angle and power, then press SHOOT.

+
+
+ + + + \ No newline at end of file diff --git a/games/angle_shot/script.js b/games/angle_shot/script.js new file mode 100644 index 00000000..6a71b431 --- /dev/null +++ b/games/angle_shot/script.js @@ -0,0 +1,304 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. CANVAS SETUP --- + const canvas = document.getElementById('archery-canvas'); + const ctx = canvas.getContext('2d'); + + // Set fixed dimensions + const CANVAS_WIDTH = 700; + const CANVAS_HEIGHT = 400; + canvas.width = CANVAS_WIDTH; + canvas.height = CANVAS_HEIGHT; + + // --- 2. DOM Elements --- + const shootButton = document.getElementById('shoot-button'); + const resetButton = document.getElementById('reset-button'); + const scoreDisplay = document.getElementById('score-display'); + const shotsDisplay = document.getElementById('shots-display'); + const angleDisplay = document.getElementById('angle-display'); + const powerDisplay = document.getElementById('power-display'); + const feedbackMessage = document.getElementById('feedback-message'); + + // --- 3. PHYSICS & GAME STATE CONSTANTS --- + const GRAVITY = 9.8; // m/s^2 (scaled for canvas units) + const LAUNCH_X = 50; // Arrow starts at x=50 + const LAUNCH_Y = CANVAS_HEIGHT - 20; // Arrow starts near the bottom + const ARROW_SIZE = 5; + const TARGET_SIZE = 30; + + // Game State + let target = { x: 600, y: CANVAS_HEIGHT - 20 - TARGET_SIZE/2, radius: TARGET_SIZE / 2 }; + let arrow = { x: LAUNCH_X, y: LAUNCH_Y, vx: 0, vy: 0, active: false }; + let initialAngleDeg = 45; + let initialVelocity = 50; // Power level + let shots = 0; + let score = 0; + + let isDragging = false; + let lastTime = 0; + let animationFrameId = null; + + // --- 4. DRAWING FUNCTIONS --- + + /** + * Draws the stationary elements (target, ground, bow/launcher). + */ + function drawStaticElements() { + // Ground + ctx.fillStyle = '#95a5a6'; + ctx.fillRect(0, CANVAS_HEIGHT - 20, CANVAS_WIDTH, 20); + + // Target (Board and Center) + ctx.fillStyle = '#3498db'; + ctx.fillRect(target.x - target.radius, target.y - target.radius, TARGET_SIZE, TARGET_SIZE); + + ctx.fillStyle = '#e74c3c'; // Bullseye + ctx.beginPath(); + ctx.arc(target.x, target.y, TARGET_SIZE * 0.25, 0, Math.PI * 2); + ctx.fill(); + + // Launcher/Bow Placeholder + ctx.strokeStyle = '#333'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.moveTo(LAUNCH_X, LAUNCH_Y); + ctx.lineTo(LAUNCH_X + 10, LAUNCH_Y - 20); + ctx.stroke(); + } + + /** + * Draws the arrow, or the projected trajectory line. + */ + function drawArrow() { + if (!arrow.active) { + // Draw Trajectory Preview + drawTrajectoryPreview(); + return; + } + + // Draw the moving arrow + ctx.fillStyle = '#2ecc71'; + ctx.beginPath(); + ctx.arc(arrow.x, arrow.y, ARROW_SIZE, 0, Math.PI * 2); + ctx.fill(); + } + + /** + * Calculates and draws the predicted path based on current angle and power. + */ + function drawTrajectoryPreview() { + const angleRad = initialAngleDeg * (Math.PI / 180); + const V0 = initialVelocity; + + // Initial velocity components + const Vx = V0 * Math.cos(angleRad); + const Vy = -V0 * Math.sin(angleRad); // Negative because canvas Y increases downwards + + ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(LAUNCH_X, LAUNCH_Y); + + // Simulate path using time steps + for (let t = 0; t < 10; t += 0.1) { + // Projectile Motion Formulas + const x = LAUNCH_X + Vx * t; + const y = LAUNCH_Y + Vy * t + 0.5 * GRAVITY * t * t; + + if (y > CANVAS_HEIGHT) break; // Stop drawing when hitting the ground + + ctx.lineTo(x, y); + } + ctx.stroke(); + } + + /** + * Clears the canvas and redraws all elements. + */ + function draw() { + ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + drawStaticElements(); + drawArrow(); + } + + // --- 5. GAME LOOP --- + + /** + * The main simulation loop for arrow movement. + */ + function gameLoop(timestamp) { + if (!arrow.active) { + draw(); + return; + } + + const deltaTime = (timestamp - lastTime) / 1000; // Time in seconds since last frame + lastTime = timestamp; + + // 1. Update Velocity (Apply Gravity) + arrow.vy += GRAVITY * deltaTime * 50; // Scaling factor for visual speed + + // 2. Update Position + arrow.x += arrow.vx * deltaTime * 50; + arrow.y += arrow.vy * deltaTime * 50; + + // 3. Check for Termination (Hit Ground or Out of Bounds) + if (arrow.y >= LAUNCH_Y || arrow.x > CANVAS_WIDTH) { + checkHit(); + return; + } + + // 4. Update DOM and Continue + draw(); + animationFrameId = requestAnimationFrame(gameLoop); + } + + // --- 6. INPUT AND CONTROL --- + + /** + * Handles the mouse interaction for setting angle and power. + */ + canvas.addEventListener('mousedown', (e) => { + if (arrow.active) return; + isDragging = true; + shootButton.disabled = false; + + // Initial mouse position + const rect = canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + // Calculate power based on distance from launch point + const dx = mouseX - LAUNCH_X; + const dy = mouseY - LAUNCH_Y; + + // Calculate angle and power based on mouse direction relative to launch point + updateLaunchParameters(dx, dy); + }); + + canvas.addEventListener('mousemove', (e) => { + if (!isDragging || arrow.active) return; + + const rect = canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + const dx = mouseX - LAUNCH_X; + const dy = mouseY - LAUNCH_Y; + + updateLaunchParameters(dx, dy); + draw(); // Redraw trajectory preview instantly + }); + + canvas.addEventListener('mouseup', () => { + isDragging = false; + }); + + /** + * Updates angle and power based on mouse movement delta. + */ + function updateLaunchParameters(dx, dy) { + // 1. Calculate Angle (limited to 0-90 degrees) + // atan2(y, x) gives angle in radians + let angleRad = Math.atan2(-dy, dx); // Use -dy because canvas Y is inverted + let angleDeg = angleRad * (180 / Math.PI); + + // Limit angle between 0 and 90 degrees (must shoot upwards and right) + if (angleDeg < 0) angleDeg = 0; + if (angleDeg > 90) angleDeg = 90; + + initialAngleDeg = Math.round(angleDeg); + + // 2. Calculate Power (based on distance of drag) + const distance = Math.sqrt(dx * dx + dy * dy); + // Scale distance to a reasonable power range (e.g., 20 to 100) + let power = Math.min(100, Math.max(20, Math.round(distance / 2))); + initialVelocity = power; + + // Update DOM display + angleDisplay.textContent = `${initialAngleDeg}ยฐ`; + powerDisplay.textContent = `${initialVelocity}`; + } + + /** + * Launches the arrow based on current settings. + */ + function launchArrow() { + if (arrow.active) return; + + shots++; + shotsDisplay.textContent = shots; + + const angleRad = initialAngleDeg * (Math.PI / 180); + + // Calculate initial velocity components + arrow.vx = initialVelocity * Math.cos(angleRad); + arrow.vy = -initialVelocity * Math.sin(angleRad); // Negative for upward launch + + // Reset position and activate + arrow.x = LAUNCH_X; + arrow.y = LAUNCH_Y; + arrow.active = true; + shootButton.disabled = true; + + lastTime = performance.now(); + requestAnimationFrame(gameLoop); + } + + /** + * Checks if the shot successfully hit the target area. + */ + function checkHit() { + arrow.active = false; + + // Distance from the arrow's final position to the center of the target + const targetXCenter = target.x; + const targetYCenter = target.y; + + const dx = arrow.x - targetXCenter; + const dy = arrow.y - targetYCenter; + const distance = Math.sqrt(dx * dx + dy * dy); + + // Check for collision with the square target area + if (arrow.x > targetXCenter - target.radius && arrow.x < targetXCenter + target.radius && + arrow.y > targetYCenter - target.radius && arrow.y < targetYCenter + target.radius) { + + // Hit! Award points based on accuracy (distance from bullseye) + let points = 100 - Math.round(distance * 5); // Max 100 points, min 50 + + score += points; + scoreDisplay.textContent = score; + feedbackMessage.innerHTML = `๐ŸŽฏ **HIT!** Scored ${points} points!`; + feedbackMessage.style.color = '#2ecc71'; + + } else { + feedbackMessage.innerHTML = 'โŒ **MISS!** Try a different angle/power.'; + feedbackMessage.style.color = '#e74c3c'; + } + + // Redraw once to show the arrow's final resting place + draw(); + } + + // --- 7. EVENT LISTENERS --- + + shootButton.addEventListener('click', launchArrow); + + resetButton.addEventListener('click', () => { + score = 0; + shots = 0; + scoreDisplay.textContent = 0; + shotsDisplay.textContent = 0; + arrow.active = false; + shootButton.disabled = false; + feedbackMessage.textContent = 'Game reset. Ready to launch!'; + + // Clear any ongoing animation + cancelAnimationFrame(animationFrameId); + + // Draw initial state + draw(); + }); + + // Initial setup + draw(); +}); \ No newline at end of file diff --git a/games/angle_shot/style.css b/games/angle_shot/style.css new file mode 100644 index 00000000..571bf59d --- /dev/null +++ b/games/angle_shot/style.css @@ -0,0 +1,86 @@ +body { + font-family: 'Arial', sans-serif; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #34495e; /* Dark forest theme */ + color: #ecf0f1; +} + +#game-container { + background-color: #2c3e50; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4); + text-align: center; + max-width: 800px; + width: 90%; +} + +h1 { + color: #2ecc71; /* Green arrow color */ + margin-bottom: 20px; +} + +/* --- Status and Canvas --- */ +#status-area, #input-display { + font-size: 1.2em; + font-weight: bold; + margin-bottom: 15px; +} + +#archery-canvas { + border: 3px solid #bdc3c7; + background-color: #fff; /* White sky/target background */ + margin: 0 auto; + display: block; + cursor: crosshair; +} + +/* --- Controls and Feedback --- */ +#controls { + margin-top: 20px; + display: flex; + justify-content: center; + gap: 15px; +} + +#shoot-button { + padding: 12px 30px; + font-size: 1.2em; + font-weight: bold; + background-color: #e74c3c; /* Red shoot button */ + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +#shoot-button:hover:not(:disabled) { + background-color: #c0392b; +} + +#shoot-button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +#reset-button { + padding: 12px 30px; + font-size: 1.2em; + font-weight: bold; + background-color: #3498db; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; +} + +#feedback-message { + min-height: 20px; + margin-top: 15px; +} \ No newline at end of file diff --git a/games/arrow-dodge/index.html b/games/arrow-dodge/index.html new file mode 100644 index 00000000..126e1cbb --- /dev/null +++ b/games/arrow-dodge/index.html @@ -0,0 +1,91 @@ + + + + + + Arrow Dodge โ€” Mini JS Games Hub + + + + +
+
+
+ ๐Ÿน +

Arrow Dodge

+
+
+ + + + + +
+
+ +
+ + +
+
+ +
+
+
+ + +
+
+
+
+
+ +
+
Made with โค๏ธ โ€” Arrow Dodge
+
Sprites/icons from Wikimedia Commons. Sounds: Google Actions public sounds.
+
+
+ + + + diff --git a/games/arrow-dodge/script.js b/games/arrow-dodge/script.js new file mode 100644 index 00000000..2dff0b77 --- /dev/null +++ b/games/arrow-dodge/script.js @@ -0,0 +1,513 @@ +// Arrow Dodge (games/arrow-dodge/script.js) +// Author: ChatGPT (adapt to taste) +// Game design: dodge arrows, shield powerup, waves, sounds, pause/resume, restart + +(() => { + // --- Asset URLs (online) --- + const ASSETS = { + arrowSvg: "https://upload.wikimedia.org/wikipedia/commons/2/27/Arrow_east.svg", + playerSvg: "https://upload.wikimedia.org/wikipedia/commons/3/3a/Joystick.svg", + shieldSvg: "https://upload.wikimedia.org/wikipedia/commons/2/24/Shield_icon.svg", + // Sounds from Google Actions public sounds + sfxHit: "https://actions.google.com/sounds/v1/impacts/crash.ogg", + sfxShield: "https://actions.google.com/sounds/v1/cartoon/clang_and_wobble.ogg", + sfxArrow: "https://actions.google.com/sounds/v1/alarms/beep_short.ogg", + bgm: "https://actions.google.com/sounds/v1/ambiences/air_hum.ogg", // ambient loop + }; + + // --- DOM --- + const canvas = document.getElementById("gameCanvas"); + const ctx = canvas.getContext("2d"); + const startBtn = document.getElementById("startBtn"); + const pauseBtn = document.getElementById("pauseBtn"); + const resumeBtn = document.getElementById("resumeBtn"); + const restartBtn = document.getElementById("restartBtn"); + const overlay = document.getElementById("overlay"); + const overlayMessage = document.getElementById("overlayMessage"); + const overlayStart = document.getElementById("overlayStart"); + const overlayRestart = document.getElementById("overlayRestart"); + const scoreEl = document.getElementById("score"); + const highScoreEl = document.getElementById("highScore"); + const waveEl = document.getElementById("wave"); + const livesEl = document.getElementById("lives"); + const shieldStatus = document.getElementById("shieldStatus"); + const soundToggle = document.getElementById("soundToggle"); + + let W = canvas.width, H = canvas.height; + + // --- Game state --- + let gameRunning = false; + let paused = false; + let lastTime = 0; + let spawnTimer = 0; + let spawnInterval = 1200; + let difficultyTimer = 0; + let difficultyInterval = 8000; + let score = 0; + let wave = 0; + let highScore = parseInt(localStorage.getItem("arrowDodgeHigh") || "0", 10); + let lives = 1; + let shield = false; + highScoreEl.textContent = highScore; + + // Entities + const player = { + x: W/2, + y: H/2, + r: 22, + speed: 4.2, + vx: 0, vy: 0, + glow: true + }; + const arrows = []; // moving obstacles + const powerups = []; + + // Sounds + const audio = { + hit: new Audio(ASSETS.sfxHit), + shield: new Audio(ASSETS.sfxShield), + arrow: new Audio(ASSETS.sfxArrow), + bgm: new Audio(ASSETS.bgm) + }; + audio.bgm.loop = true; + audio.bgm.volume = 0.08; + audio.hit.volume = 0.6; + audio.shield.volume = 0.6; + audio.arrow.volume = 0.25; + + function playSound(s) { + if (!soundToggle.checked) return; + const a = audio[s]; + if (!a) return; + try { + a.currentTime = 0; + a.play(); + } catch(e) { /* autoplay restrictions fallback */ } + } + + // Keyboard + const keys = {}; + window.addEventListener("keydown", e => { keys[e.key.toLowerCase()] = true; }); + window.addEventListener("keyup", e => { keys[e.key.toLowerCase()] = false; }); + + // Resize handling (canvas keeps pixel resolution fixed, but scales via CSS) + function resizeCanvas(){ + const rect = canvas.getBoundingClientRect(); + // keep drawing resolution same as element size + canvas.width = Math.max(640, Math.floor(rect.width)); + canvas.height = Math.max(360, Math.floor(rect.height)); + W = canvas.width; H = canvas.height; + } + window.addEventListener("resize", resizeCanvas); + resizeCanvas(); + + // --- Utilities --- + function rand(min, max){ return Math.random()*(max-min)+min; } + function dist(a,b){ const dx=a.x-b.x, dy=a.y-b.y; return Math.sqrt(dx*dx+dy*dy); } + + // Spawn arrow from a random edge, pointing across screen + function spawnArrow() { + const edge = Math.floor(Math.random()*4); // 0 top,1 right,2 bottom,3 left + let x,y,dx,dy; + const speed = rand(2.2, 3.2) + wave*0.35 + Math.random()*1.2; // increases with wave + const padding = 20; + switch(edge){ + case 0: // top + x = rand(padding, W-padding); + y = -40; + dx = (player.x - x) * 0.007; + dy = 1; + break; + case 1: // right + x = W + 40; + y = rand(padding, H-padding); + dx = -1; + dy = (player.y - y) * 0.007; + break; + case 2: // bottom + x = rand(padding, W-padding); + y = H + 40; + dx = (player.x - x) * 0.007; + dy = -1; + break; + case 3: // left + x = -40; + y = rand(padding, H-padding); + dx = 1; + dy = (player.y - y) * 0.007; + break; + } + // Normalize direction and apply speed + const len = Math.sqrt(dx*dx + dy*dy) || 1; + dx = dx/len * speed; + dy = dy/len * speed; + + arrows.push({ + x,y,dx,dy, + len: rand(28,60), + rot: Math.atan2(dy,dx), + color: `hsl(${rand(160,200)}, 90%, 60%)`, + life: 0 + }); + playSound('arrow'); + } + + // Spawn shield powerup (small green orb) + function spawnPowerup() { + const x = rand(60, W-60), y = rand(60, H-60); + powerups.push({x,y,ttl:12000, type:'shield', born:performance.now()}); + } + + function pickupPowerup(idx){ + const p = powerups[idx]; + if(p.type === 'shield'){ + shield = true; + shieldStatus.textContent = "Yes"; + playSound('shield'); + // shield lasts for some time or one hit; we use one hit here + setTimeout(()=>{ /* optional timed shield expiration */ }, 12000); + } + powerups.splice(idx,1); + } + + // Collision check + function checkCollisionArrow(a) { + // approximate player as circle, arrow as line segment + // compute closest distance from player center to arrow segment + const px = player.x, py = player.y; + const x1 = a.x, y1 = a.y; + const x2 = a.x + Math.cos(a.rot) * a.len, y2 = a.y + Math.sin(a.rot) * a.len; + // projection t + const dx = x2-x1, dy=y2-y1; + const l2 = dx*dx+dy*dy; + const t = Math.max(0, Math.min(1, ((px-x1)*dx + (py-y1)*dy) / (l2 || 1))); + const cx = x1 + t*dx, cy = y1 + t*dy; + const d = Math.hypot(px-cx, py-cy); + return d < player.r - 2; + } + + // --- Drawing --- + function drawBackground() { + // gradient + subtle grid glow + const g = ctx.createLinearGradient(0,0,0,H); + g.addColorStop(0,'rgba(2,6,23,0.7)'); + g.addColorStop(1,'rgba(3,10,36,0.95)'); + ctx.fillStyle = g; + ctx.fillRect(0,0,W,H); + + // subtle diagonal streaks + ctx.save(); + ctx.globalAlpha = 0.06; + ctx.fillStyle = '#00e6a8'; + for(let i=-1;i<8;i++){ + ctx.fillRect( (i*180 + (performance.now()/50)%180), 0, 2, H); + } + ctx.restore(); + } + + function drawPlayer(){ + // glowing circle + icon + ctx.save(); + ctx.translate(player.x, player.y); + // glow + const rg = ctx.createRadialGradient(0,0,0,0,0,64); + rg.addColorStop(0,'rgba(0,230,168,0.18)'); + rg.addColorStop(1,'rgba(0,0,0,0)'); + ctx.fillStyle = rg; + ctx.beginPath(); + ctx.arc(0,0,42,0,Math.PI*2); + ctx.fill(); + + // body + ctx.fillStyle = '#dffcf4'; + ctx.beginPath(); + ctx.arc(0,0,player.r,0,Math.PI*2); + ctx.fill(); + + // icon - small joystick rectangle (no external img draw to keep crisp) + ctx.fillStyle = '#00272d'; + ctx.font = '18px sans-serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('๐ŸŽฎ', 0, 0); + // shield ring + if(shield){ + ctx.strokeStyle = 'rgba(122,252,255,0.95)'; + ctx.lineWidth = 4; + ctx.beginPath(); + ctx.arc(0,0,player.r+8,0,Math.PI*2); + ctx.stroke(); + } + ctx.restore(); + } + + function drawArrows() { + arrows.forEach(a=>{ + ctx.save(); + ctx.translate(a.x, a.y); + ctx.rotate(a.rot); + // shaft + ctx.fillStyle = a.color; + const w = 6; + ctx.beginPath(); + ctx.roundRect(-5, -w/2, a.len, w, 3); + ctx.fill(); + // head + ctx.beginPath(); + ctx.moveTo(a.len, 0); + ctx.lineTo(a.len-14, -10); + ctx.lineTo(a.len-14, 10); + ctx.closePath(); + ctx.fill(); + // glow + ctx.shadowColor = 'rgba(0,230,168,0.6)'; + ctx.shadowBlur = 12; + ctx.restore(); + }); + } + + // polyfill for roundRect on canvas path (if missing) + if(!CanvasRenderingContext2D.prototype.roundRect){ + CanvasRenderingContext2D.prototype.roundRect = function(x,y,w,h,r){ + if(typeof r === 'undefined') r = 6; + this.beginPath(); + this.moveTo(x+r,y); + this.arcTo(x+w,y,x+w,y+h,r); + this.arcTo(x+w,y+h,x,y+h,r); + this.arcTo(x,y+h,x,y,r); + this.arcTo(x,y,x+w,y,r); + this.closePath(); + return this; + } + } + + function drawPowerups(){ + powerups.forEach(p=>{ + ctx.save(); + ctx.translate(p.x, p.y); + ctx.beginPath(); + ctx.arc(0,0,12,0,Math.PI*2); + ctx.fillStyle = 'rgba(120,240,140,0.95)'; + ctx.fill(); + ctx.fillStyle = '#002'; + ctx.font='12px sans-serif'; + ctx.textAlign='center'; + ctx.textBaseline='middle'; + ctx.fillText('๐Ÿ›ก๏ธ',0,0); + ctx.restore(); + }); + } + + // --- Game update --- + function update(dt){ + // Player velocity from keys + const s = player.speed; + let vx=0, vy=0; + if(keys['arrowup']||keys['w']) vy = -1; + if(keys['arrowdown']||keys['s']) vy = 1; + if(keys['arrowleft']||keys['a']) vx = -1; + if(keys['arrowright']||keys['d']) vx = 1; + // Normalize diagonal + if(vx && vy){ vx *= 0.7071; vy *= 0.7071; } + player.x += vx * s; + player.y += vy * s; + // clamp + player.x = Math.max(16, Math.min(W-16, player.x)); + player.y = Math.max(16, Math.min(H-16, player.y)); + + // Move arrows + for(let i=arrows.length-1;i>=0;i--){ + const a = arrows[i]; + a.x += a.dx; + a.y += a.dy; + a.life += dt; + // remove when off screen a bit + if(a.x < -120 || a.x > W+120 || a.y < -120 || a.y > H+120){ + arrows.splice(i,1); + continue; + } + // collision + if(checkCollisionArrow(a)){ + // if shield, consume it and remove arrow + if(shield){ + shield = false; + shieldStatus.textContent = "No"; + playSound('shield'); + arrows.splice(i,1); + } else { + // player hit -> end game + playSound('hit'); + endGame(); + return; + } + } + } + + // Powerup pickup check + for(let i=powerups.length-1;i>=0;i--){ + const p = powerups[i]; + if(Math.hypot(player.x-p.x, player.y-p.y) < player.r + 12){ + pickupPowerup(i); + } else if(performance.now() - p.born > p.ttl){ + powerups.splice(i,1); + } + } + + // score increases with dt and wave bonus + score += dt * 0.02 * (1 + wave*0.12); + // spawn logic + spawnTimer += dt; + if(spawnTimer > spawnInterval){ + spawnTimer = 0; + // spawn a burst of arrows based on wave + const count = 1 + Math.min(5, Math.floor(wave/1.5)); + for(let i=0;i difficultyInterval){ + difficultyTimer = 0; + wave++; + spawnInterval = Math.max(420, spawnInterval * 0.88); + // occasionally spawn powerup + if(Math.random() < 0.6) spawnPowerup(); + playSound('arrow'); + } + + // update UI + scoreEl.textContent = Math.floor(score); + waveEl.textContent = wave; + livesEl.textContent = lives; + highScore = Math.max(highScore, Math.floor(score)); + highScoreEl.textContent = localStorage.getItem("arrowDodgeHigh") || highScore; + } + + function render(){ + drawBackground(); + drawPowerups(); + drawArrows(); + drawPlayer(); + } + + // --- Game loop --- + let rafId = null; + function gameLoop(ts){ + if(!lastTime) lastTime = ts; + const dt = ts - lastTime; + lastTime = ts; + if(!paused && gameRunning){ + update(dt); + render(); + } else { + // still render once (for overlay visuals) + render(); + } + rafId = requestAnimationFrame(gameLoop); + } + + // --- Start / Pause / Resume / Restart / End --- + function startGame(){ + // reset state + gameRunning = true; paused = false; + spawnTimer = 0; difficultyTimer = 0; + spawnInterval = 1200; difficultyInterval = 8000; + arrows.length = 0; powerups.length = 0; + score = 0; wave = 0; lives = 1; shield = false; + player.x = W/2; player.y = H/2; + shieldStatus.textContent = "No"; + overlay.style.pointerEvents = "none"; + overlay.style.opacity = 0; + overlayMessage.textContent = ""; + startBtn.disabled = true; pauseBtn.disabled = false; resumeBtn.disabled = true; restartBtn.disabled = false; + playSound('arrow'); + if(soundToggle.checked){ + try{ audio.bgm.play(); }catch(e){} + } + } + + function pauseGame(){ + paused = true; + pauseBtn.disabled = true; resumeBtn.disabled = false; + overlay.style.pointerEvents = "auto"; + overlay.style.opacity = 1; + overlayMessage.innerHTML = "

Paused

Game is paused.

"; + } + function resumeGame(){ + paused = false; + pauseBtn.disabled = false; resumeBtn.disabled = true; + overlay.style.pointerEvents = "none"; + overlay.style.opacity = 0; + } + + function restartGame(){ + // restart from scratch + startGame(); + } + + function endGame(){ + gameRunning = false; + paused = false; + cancelAnimationFrame(rafId); + // finalize high score + const finalScore = Math.floor(score); + const prevHigh = parseInt(localStorage.getItem("arrowDodgeHigh") || "0",10); + if(finalScore > prevHigh){ + localStorage.setItem("arrowDodgeHigh", String(finalScore)); + highScore = finalScore; + } else { + highScore = prevHigh; + } + // show overlay + overlay.style.pointerEvents = "auto"; + overlay.style.opacity = 1; + overlayMessage.innerHTML = `

Game Over

Your score: ${finalScore}

High score: ${localStorage.getItem("arrowDodgeHigh")}

`; + startBtn.disabled = false; pauseBtn.disabled = true; resumeBtn.disabled = true; restartBtn.disabled = false; + // stop bgm + try{ audio.bgm.pause(); audio.bgm.currentTime = 0; }catch(e){} + // restart loop to allow overlay rendering (not updating game) + lastTime = 0; + rafId = requestAnimationFrame(gameLoop); + } + + // --- Wire UI --- + startBtn.addEventListener('click', ()=> { + startGame(); + if(!rafId) rafId = requestAnimationFrame(gameLoop); + }); + overlayStart.addEventListener('click', ()=> { startBtn.click(); }); + pauseBtn.addEventListener('click', pauseGame); + resumeBtn.addEventListener('click', resumeGame); + restartBtn.addEventListener('click', ()=> { + restartGame(); + if(!rafId) rafId = requestAnimationFrame(gameLoop); + }); + overlayRestart.addEventListener('click', ()=> { restartBtn.click(); }); + + // sound toggle + soundToggle.addEventListener('change', ()=>{ + if(!soundToggle.checked){ + Object.values(audio).forEach(a=>a.pause()); + } else { + if(gameRunning && !paused) try{ audio.bgm.play(); }catch(e){} + } + }); + + // auto-start secondary rendering loop + rafId = requestAnimationFrame(gameLoop); + + // Save highscore periodically + setInterval(()=> { + const hs = parseInt(localStorage.getItem("arrowDodgeHigh") || "0",10); + if(Math.floor(score) > hs) localStorage.setItem("arrowDodgeHigh", String(Math.floor(score))); + highScoreEl.textContent = localStorage.getItem("arrowDodgeHigh"); + }, 2000); + + // small initial overlay message + overlayMessage.innerHTML = `

Arrow Dodge

Use WASD or Arrow keys to move. Avoid arrows. Shield blocks 1 hit.

`; + overlay.style.pointerEvents = "auto"; + overlay.style.opacity = 1; + +})(); diff --git a/games/arrow-dodge/style.css b/games/arrow-dodge/style.css new file mode 100644 index 00000000..0c1e5f0f --- /dev/null +++ b/games/arrow-dodge/style.css @@ -0,0 +1,89 @@ +:root{ + --bg1:#05031a; + --bg2:#0b0b1a; + --accent:#00e6a8; + --accent-2:#7afcff; + --muted:#a8b3c7; + --glass: rgba(255,255,255,0.04); + --neon: 0 0 18px rgba(0,230,168,0.25), 0 0 40px rgba(122,252,255,0.06); + --radius:14px; + --btn-radius:10px; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; +} + +*{box-sizing:border-box} +html,body{height:100%;margin:0;background: + radial-gradient(1200px 600px at 10% 20%, rgba(0,230,168,0.05), transparent 8%), + radial-gradient(1000px 500px at 90% 80%, rgba(122,252,255,0.03), transparent 12%), + linear-gradient(180deg,var(--bg1),var(--bg2)); + color:#e6eef8; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; +} + +.shell{ + max-width:1200px;margin:28px auto;padding:18px; + display:flex;flex-direction:column;gap:18px; +} + +.topbar{ + display:flex;align-items:center;justify-content:space-between; + gap:12px;padding:12px;border-radius:12px;background:linear-gradient(180deg, rgba(255,255,255,0.02), transparent); + box-shadow: var(--neon); +} + +.title{display:flex;align-items:center;gap:12px} +.title .icon{font-size:28px} +.title h1{font-size:20px;margin:0} + +.controls{display:flex;gap:8px;align-items:center} +.btn{ + background:transparent;border:1px solid rgba(255,255,255,0.06);padding:8px 12px;border-radius:var(--btn-radius);cursor:pointer; + color:var(--muted);font-weight:600;backdrop-filter: blur(6px); +} +.btn.primary{ + background:linear-gradient(90deg,var(--accent),var(--accent-2)); + color:#001;box-shadow:0 6px 20px rgba(0,230,168,0.12);border: none; +} +.btn.ghost{background:transparent;border:1px solid rgba(255,255,255,0.04);color:#d7eaf0} + +.toggle-sound{display:inline-flex;align-items:center;gap:6px;color:var(--muted);font-weight:600} +.toggle-sound input{transform:scale(1.0)} + +.main{display:flex;gap:16px} +.sidebar{ + width:280px;padding:16px;border-radius:12px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border:1px solid rgba(255,255,255,0.03);box-shadow:var(--neon); + display:flex;flex-direction:column;gap:12px; +} +.stat{display:flex;justify-content:space-between;align-items:center;padding:12px;border-radius:10px;background:var(--glass)} +.stat .label{color:var(--muted);font-size:13px} +.stat .value{font-weight:800;font-size:20px;color:var(--accent)} +.powerups{padding:12px;border-radius:10px;background:linear-gradient(90deg, rgba(122,252,255,0.02), rgba(0,230,168,0.02));font-size:14px} +.help{font-size:13px;color:var(--muted);line-height:1.4} +.links{display:flex;flex-direction:column;gap:8px;margin-top:auto} + +.game-area{flex:1;display:flex;align-items:center;justify-content:center;padding:12px} +.canvas-wrap{position:relative;width:100%;max-width:960px;border-radius:16px;overflow:hidden; + background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.1));box-shadow:var(--neon); + border:1px solid rgba(255,255,255,0.03);padding:12px; +} +canvas{display:block;background: + radial-gradient(circle at 10% 10%, rgba(0,230,168,0.02), transparent 8%), + linear-gradient(180deg, rgba(2,6,23,0.6), rgba(5,9,28,0.92));width:100%;height:auto;border-radius:10px;} + +/* Overlay for messages */ +.overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none} +.overlay .overlay-message{pointer-events:auto;background:linear-gradient(180deg, rgba(0,0,0,0.6), rgba(0,0,0,0.4));padding:18px;border-radius:12px;text-align:center;backdrop-filter: blur(6px);border:1px solid rgba(255,255,255,0.04)} +.overlay .overlay-actions{margin-top:12px;display:flex;gap:8px;justify-content:center} +.overlay .btn{pointer-events:auto} + +/* neon glows for arrows/player */ +.glow{filter:drop-shadow(0 0 6px rgba(0,230,168,0.8)) drop-shadow(0 0 18px rgba(122,252,255,0.12))} + +/* small screens */ +@media (max-width:900px){ + .main{flex-direction:column} + .sidebar{width:100%} + .canvas-wrap{max-width:100%} +} diff --git a/games/art-studio/index.html b/games/art-studio/index.html new file mode 100644 index 00000000..2e6f4a14 --- /dev/null +++ b/games/art-studio/index.html @@ -0,0 +1,151 @@ + + + + + + Art Studio - Mini JS Games Hub + + + + +
+
+

๐ŸŽจ Art Studio

+

Create digital masterpieces with various tools and colors!

+
+ +
+ +
+
+

Tools

+
+ + + + + + +
+
+ +
+

Colors

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ +
+

Brush Size

+
+ + 5px +
+
+ +
+

Actions

+
+ + + + +
+
+ +
+

Stamps

+
+ + + + + + + + +
+
+
+ + +
+
+ +
+
+ +
+
+ Tool: + Brush +
+
+ Color: + #000000 +
+
+ Size: + 5px +
+
+
+
+ + +
+

How to Use Art Studio

+
    +
  • Brush: Click and drag to draw freehand
  • +
  • Eraser: Click and drag to erase parts of your drawing
  • +
  • Fill: Click on an area to fill it with the selected color
  • +
  • Line: Click and drag to draw straight lines
  • +
  • Rectangle: Click and drag to draw rectangles
  • +
  • Circle: Click and drag to draw circles
  • +
  • Stamps: Click on the canvas to place emoji stamps
  • +
  • Undo/Redo: Use these buttons to correct mistakes
  • +
  • Save: Download your artwork as an image
  • +
+
+ + +
+
+ + + + \ No newline at end of file diff --git a/games/art-studio/script.js b/games/art-studio/script.js new file mode 100644 index 00000000..47a3872d --- /dev/null +++ b/games/art-studio/script.js @@ -0,0 +1,547 @@ +// Art Studio Game +// Create digital art with various tools and colors + +// DOM elements +const canvas = document.getElementById('art-canvas'); +const ctx = canvas.getContext('2d'); +const canvasOverlay = document.getElementById('canvas-overlay'); +const messageEl = document.getElementById('message'); + +// Tool buttons +const brushTool = document.getElementById('brush-tool'); +const eraserTool = document.getElementById('eraser-tool'); +const fillTool = document.getElementById('fill-tool'); +const lineTool = document.getElementById('line-tool'); +const rectangleTool = document.getElementById('rectangle-tool'); +const circleTool = document.getElementById('circle-tool'); + +// Color elements +const colorBtns = document.querySelectorAll('.color-btn'); +const customColorInput = document.getElementById('custom-color'); + +// Brush size +const brushSizeInput = document.getElementById('brush-size'); +const sizeValue = document.getElementById('size-value'); + +// Action buttons +const clearBtn = document.getElementById('clear-canvas'); +const undoBtn = document.getElementById('undo-btn'); +const redoBtn = document.getElementById('redo-btn'); +const saveBtn = document.getElementById('save-btn'); + +// Stamp buttons +const stampBtns = document.querySelectorAll('.stamp-btn'); + +// Info display +const currentToolEl = document.getElementById('current-tool'); +const currentColorEl = document.getElementById('current-color'); +const currentSizeEl = document.getElementById('current-size'); + +// Game state +let currentTool = 'brush'; +let currentColor = '#000000'; +let brushSize = 5; +let isDrawing = false; +let startX, startY; +let currentStamp = null; + +// Drawing state +let drawingHistory = []; +let historyIndex = -1; +let maxHistory = 50; + +// Initialize the game +function initGame() { + setupCanvas(); + setupEventListeners(); + setupTools(); + saveState(); // Save initial blank state + updateInfo(); + showMessage('Welcome to Art Studio! Start creating your masterpiece!', 'success'); +} + +// Setup canvas +function setupCanvas() { + // Set canvas size + const rect = canvas.getBoundingClientRect(); + canvas.width = 800; + canvas.height = 600; + + // Fill with white background + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Set initial drawing properties + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; +} + +// Setup event listeners +function setupEventListeners() { + // Canvas mouse events + canvas.addEventListener('mousedown', startDrawing); + canvas.addEventListener('mousemove', draw); + canvas.addEventListener('mouseup', stopDrawing); + canvas.addEventListener('mouseout', stopDrawing); + + // Touch events for mobile + canvas.addEventListener('touchstart', handleTouchStart); + canvas.addEventListener('touchmove', handleTouchMove); + canvas.addEventListener('touchend', handleTouchEnd); + + // Tool selection + brushTool.addEventListener('click', () => selectTool('brush')); + eraserTool.addEventListener('click', () => selectTool('eraser')); + fillTool.addEventListener('click', () => selectTool('fill')); + lineTool.addEventListener('click', () => selectTool('line')); + rectangleTool.addEventListener('click', () => selectTool('rectangle')); + circleTool.addEventListener('click', () => selectTool('circle')); + + // Color selection + colorBtns.forEach(btn => { + btn.addEventListener('click', () => selectColor(btn.dataset.color)); + }); + customColorInput.addEventListener('change', (e) => selectColor(e.target.value)); + + // Brush size + brushSizeInput.addEventListener('input', updateBrushSize); + + // Action buttons + clearBtn.addEventListener('click', clearCanvas); + undoBtn.addEventListener('click', undo); + redoBtn.addEventListener('click', redo); + saveBtn.addEventListener('click', saveCanvas); + + // Stamp selection + stampBtns.forEach(btn => { + btn.addEventListener('click', () => selectStamp(btn.dataset.stamp)); + }); + + // Keyboard shortcuts + document.addEventListener('keydown', handleKeyDown); +} + +// Setup tools +function setupTools() { + selectTool('brush'); + selectColor('#000000'); + updateBrushSize(); +} + +// Select tool +function selectTool(tool) { + currentTool = tool; + currentStamp = null; + + // Update UI + document.querySelectorAll('.tool-btn').forEach(btn => { + btn.classList.remove('active'); + }); + document.querySelector(`#${tool}-tool`).classList.add('active'); + + // Update cursor + updateCursor(); + + updateInfo(); + showMessage(`Selected ${tool} tool`, 'info'); +} + +// Select color +function selectColor(color) { + currentColor = color; + + // Update UI + document.querySelectorAll('.color-btn').forEach(btn => { + btn.classList.remove('selected'); + }); + + const colorBtn = document.querySelector(`[data-color="${color}"]`); + if (colorBtn) { + colorBtn.classList.add('selected'); + } + + customColorInput.value = color; + ctx.strokeStyle = color; + ctx.fillStyle = color; + + updateInfo(); +} + +// Update brush size +function updateBrushSize() { + brushSize = brushSizeInput.value; + sizeValue.textContent = brushSize + 'px'; + ctx.lineWidth = brushSize; + currentSizeEl.textContent = brushSize + 'px'; +} + +// Select stamp +function selectStamp(stamp) { + currentStamp = stamp; + currentTool = 'stamp'; + + // Update UI + document.querySelectorAll('.tool-btn').forEach(btn => { + btn.classList.remove('active'); + }); + document.querySelectorAll('.stamp-btn').forEach(btn => { + btn.classList.remove('active'); + }); + + event.target.classList.add('active'); + updateCursor(); + updateInfo(); + showMessage(`Selected ${stamp} stamp`, 'info'); +} + +// Update cursor based on tool +function updateCursor() { + canvas.className = ''; + + if (currentTool === 'brush') { + canvas.classList.add('brush-cursor'); + } else if (currentTool === 'eraser') { + canvas.classList.add('eraser-cursor'); + } else if (currentTool === 'fill') { + canvas.classList.add('fill-cursor'); + } else if (currentTool === 'stamp') { + canvas.style.cursor = 'pointer'; + } else { + canvas.style.cursor = 'crosshair'; + } +} + +// Update info display +function updateInfo() { + currentToolEl.textContent = currentTool.charAt(0).toUpperCase() + currentTool.slice(1); + currentColorEl.style.background = currentColor; + currentSizeEl.textContent = brushSize + 'px'; +} + +// Drawing functions +function startDrawing(e) { + isDrawing = true; + const rect = canvas.getBoundingClientRect(); + startX = e.clientX - rect.left; + startY = e.clientY - rect.top; + + if (currentTool === 'fill') { + fillArea(startX, startY); + isDrawing = false; + saveState(); + } else if (currentTool === 'stamp') { + placeStamp(startX, startY); + isDrawing = false; + saveState(); + } else { + ctx.beginPath(); + ctx.moveTo(startX, startY); + } +} + +function draw(e) { + if (!isDrawing) return; + + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + if (currentTool === 'brush') { + drawBrush(x, y); + } else if (currentTool === 'eraser') { + erase(x, y); + } + // Shape tools draw on mouse up +} + +function stopDrawing(e) { + if (!isDrawing) return; + + const rect = canvas.getBoundingClientRect(); + const endX = e.clientX - rect.left; + const endY = e.clientY - rect.top; + + if (currentTool === 'line') { + drawLine(startX, startY, endX, endY); + } else if (currentTool === 'rectangle') { + drawRectangle(startX, startY, endX, endY); + } else if (currentTool === 'circle') { + drawCircle(startX, startY, endX, endY); + } + + isDrawing = false; + saveState(); +} + +// Drawing tool functions +function drawBrush(x, y) { + ctx.lineTo(x, y); + ctx.stroke(); +} + +function erase(x, y) { + const prevColor = ctx.strokeStyle; + const prevComposite = ctx.globalCompositeOperation; + + ctx.globalCompositeOperation = 'destination-out'; + ctx.strokeStyle = 'rgba(0,0,0,1)'; + ctx.lineTo(x, y); + ctx.stroke(); + + ctx.strokeStyle = prevColor; + ctx.globalCompositeOperation = prevComposite; +} + +function drawLine(x1, y1, x2, y2) { + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); +} + +function drawRectangle(x1, y1, x2, y2) { + const width = x2 - x1; + const height = y2 - y1; + + ctx.strokeRect(x1, y1, width, height); +} + +function drawCircle(x1, y1, x2, y2) { + const radius = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); + + ctx.beginPath(); + ctx.arc(x1, y1, radius, 0, 2 * Math.PI); + ctx.stroke(); +} + +function fillArea(x, y) { + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + + // Get the color at the clicked position + const startPos = (Math.floor(y) * canvas.width + Math.floor(x)) * 4; + const startR = data[startPos]; + const startG = data[startPos + 1]; + const startB = data[startPos + 2]; + const startA = data[startPos + 3]; + + // Convert current color to RGB + const fillColor = hexToRgb(currentColor); + if (!fillColor) return; + + // If clicking on the same color, return + if (startR === fillColor.r && startG === fillColor.g && startB === fillColor.b && startA === 255) { + return; + } + + // Flood fill algorithm + const stack = [[Math.floor(x), Math.floor(y)]]; + const visited = new Set(); + + while (stack.length > 0) { + const [cx, cy] = stack.pop(); + const key = `${cx},${cy}`; + + if (visited.has(key)) continue; + visited.add(key); + + const pos = (cy * canvas.width + cx) * 4; + + if (pos < 0 || pos >= data.length - 3) continue; + + const r = data[pos]; + const g = data[pos + 1]; + const b = data[pos + 2]; + const a = data[pos + 3]; + + // Check if this pixel matches the start color + if (r === startR && g === startG && b === startB && a === startA) { + // Fill this pixel + data[pos] = fillColor.r; + data[pos + 1] = fillColor.g; + data[pos + 2] = fillColor.b; + data[pos + 3] = 255; + + // Add neighboring pixels to stack + stack.push([cx + 1, cy]); + stack.push([cx - 1, cy]); + stack.push([cx, cy + 1]); + stack.push([cx, cy - 1]); + } + } + + ctx.putImageData(imageData, 0, 0); +} + +function placeStamp(x, y) { + if (!currentStamp) return; + + ctx.font = `${brushSize * 3}px Arial`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(currentStamp, x, y); +} + +// Touch event handlers +function handleTouchStart(e) { + e.preventDefault(); + const touch = e.touches[0]; + const mouseEvent = new MouseEvent('mousedown', { + clientX: touch.clientX, + clientY: touch.clientY + }); + canvas.dispatchEvent(mouseEvent); +} + +function handleTouchMove(e) { + e.preventDefault(); + const touch = e.touches[0]; + const mouseEvent = new MouseEvent('mousemove', { + clientX: touch.clientX, + clientY: touch.clientY + }); + canvas.dispatchEvent(mouseEvent); +} + +function handleTouchEnd(e) { + e.preventDefault(); + const mouseEvent = new MouseEvent('mouseup'); + canvas.dispatchEvent(mouseEvent); +} + +// Utility functions +function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +} + +// History management +function saveState() { + // Remove any history after current index + drawingHistory = drawingHistory.slice(0, historyIndex + 1); + + // Add current state + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + drawingHistory.push(imageData); + + // Limit history size + if (drawingHistory.length > maxHistory) { + drawingHistory.shift(); + } + + historyIndex = drawingHistory.length - 1; + updateUndoRedoButtons(); +} + +function undo() { + if (historyIndex > 0) { + historyIndex--; + ctx.putImageData(drawingHistory[historyIndex], 0, 0); + updateUndoRedoButtons(); + showMessage('Undid last action', 'info'); + } +} + +function redo() { + if (historyIndex < drawingHistory.length - 1) { + historyIndex++; + ctx.putImageData(drawingHistory[historyIndex], 0, 0); + updateUndoRedoButtons(); + showMessage('Redid last action', 'info'); + } +} + +function updateUndoRedoButtons() { + undoBtn.disabled = historyIndex <= 0; + redoBtn.disabled = historyIndex >= drawingHistory.length - 1; + + undoBtn.style.opacity = undoBtn.disabled ? 0.5 : 1; + redoBtn.style.opacity = redoBtn.disabled ? 0.5 : 1; +} + +// Clear canvas +function clearCanvas() { + if (confirm('Are you sure you want to clear the canvas? This action cannot be undone.')) { + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = currentColor; + saveState(); + showMessage('Canvas cleared', 'info'); + } +} + +// Save canvas +function saveCanvas() { + const link = document.createElement('a'); + link.download = 'art-studio-masterpiece.png'; + link.href = canvas.toDataURL(); + link.click(); + showMessage('Artwork saved as PNG!', 'success'); +} + +// Keyboard shortcuts +function handleKeyDown(e) { + if (e.ctrlKey || e.metaKey) { + switch (e.key) { + case 'z': + e.preventDefault(); + if (e.shiftKey) { + redo(); + } else { + undo(); + } + break; + case 'y': + e.preventDefault(); + redo(); + break; + case 's': + e.preventDefault(); + saveCanvas(); + break; + } + } + + // Tool shortcuts + switch (e.key) { + case 'b': + selectTool('brush'); + break; + case 'e': + selectTool('eraser'); + break; + case 'f': + selectTool('fill'); + break; + case 'l': + selectTool('line'); + break; + case 'r': + selectTool('rectangle'); + break; + case 'c': + selectTool('circle'); + break; + } +} + +// Show message +function showMessage(text, type) { + messageEl.textContent = text; + messageEl.className = `message ${type} show`; + + setTimeout(() => { + messageEl.classList.remove('show'); + }, 3000); +} + +// Initialize the game +initGame(); + +// This art studio game includes various drawing tools, +// color selection, brush size control, undo/redo functionality, +// and the ability to save artwork as PNG images \ No newline at end of file diff --git a/games/art-studio/style.css b/games/art-studio/style.css new file mode 100644 index 00000000..4f80387e --- /dev/null +++ b/games/art-studio/style.css @@ -0,0 +1,446 @@ +/* Art Studio Game Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + color: #333; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 30px; + color: white; +} + +header h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +header p { + font-size: 1.2em; + opacity: 0.9; +} + +.game-container { + display: flex; + gap: 20px; + background: rgba(255, 255, 255, 0.95); + border-radius: 15px; + padding: 20px; + box-shadow: 0 10px 30px rgba(0,0,0,0.2); + backdrop-filter: blur(10px); +} + +/* Toolbar Styles */ +.toolbar { + width: 250px; + display: flex; + flex-direction: column; + gap: 20px; +} + +.tool-section { + background: #f8f9fa; + border-radius: 10px; + padding: 15px; + border: 2px solid #e9ecef; +} + +.tool-section h3 { + margin-bottom: 10px; + color: #495057; + font-size: 1.1em; + border-bottom: 1px solid #dee2e6; + padding-bottom: 5px; +} + +.tool-group { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.tool-btn { + display: flex; + flex-direction: column; + align-items: center; + padding: 10px; + border: 2px solid #dee2e6; + border-radius: 8px; + background: white; + cursor: pointer; + transition: all 0.3s ease; + font-size: 0.9em; +} + +.tool-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.1); +} + +.tool-btn.active { + background: #007bff; + color: white; + border-color: #007bff; +} + +.tool-icon { + font-size: 1.5em; + margin-bottom: 5px; +} + +.tool-label { + font-size: 0.8em; + text-align: center; +} + +/* Color Palette */ +.color-palette { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + margin-bottom: 15px; +} + +.color-btn { + width: 40px; + height: 40px; + border-radius: 50%; + border: 3px solid #dee2e6; + cursor: pointer; + transition: all 0.3s ease; +} + +.color-btn:hover { + transform: scale(1.1); +} + +.color-btn.selected { + border-color: #007bff; + box-shadow: 0 0 0 3px rgba(0,123,255,0.3); +} + +.color-picker { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; +} + +.color-picker input[type="color"] { + width: 50px; + height: 50px; + border: none; + border-radius: 50%; + cursor: pointer; +} + +.color-picker label { + font-size: 0.9em; + color: #6c757d; +} + +/* Brush Size */ +.brush-size { + display: flex; + align-items: center; + gap: 10px; +} + +.brush-size input[type="range"] { + flex: 1; + height: 6px; + border-radius: 3px; + background: #dee2e6; + outline: none; +} + +.brush-size input[type="range"]::-webkit-slider-thumb { + appearance: none; + width: 20px; + height: 20px; + border-radius: 50%; + background: #007bff; + cursor: pointer; +} + +#size-value { + min-width: 40px; + text-align: center; + font-weight: bold; + color: #007bff; +} + +/* Action Buttons */ +.action-buttons { + display: flex; + flex-direction: column; + gap: 8px; +} + +.action-btn { + padding: 10px 15px; + border: 2px solid #dee2e6; + border-radius: 8px; + background: white; + cursor: pointer; + transition: all 0.3s ease; + font-size: 0.9em; + display: flex; + align-items: center; + gap: 8px; +} + +.action-btn:hover { + background: #007bff; + color: white; + border-color: #007bff; + transform: translateY(-1px); +} + +/* Stamp Palette */ +.stamp-palette { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; +} + +.stamp-btn { + padding: 10px; + border: 2px solid #dee2e6; + border-radius: 8px; + background: white; + cursor: pointer; + transition: all 0.3s ease; + font-size: 1.5em; + text-align: center; +} + +.stamp-btn:hover { + background: #f8f9fa; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +/* Canvas Area */ +.canvas-area { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; +} + +.canvas-container { + position: relative; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 8px 25px rgba(0,0,0,0.15); + background: white; +} + +#art-canvas { + display: block; + background: white; + cursor: crosshair; +} + +.canvas-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + background: transparent; +} + +.canvas-info { + display: flex; + gap: 20px; + background: #f8f9fa; + padding: 10px 20px; + border-radius: 8px; + border: 1px solid #dee2e6; +} + +.info-item { + display: flex; + align-items: center; + gap: 8px; +} + +.info-label { + font-weight: bold; + color: #495057; +} + +#current-color { + width: 20px; + height: 20px; + border-radius: 50%; + border: 1px solid #dee2e6; + display: inline-block; +} + +/* Instructions */ +.instructions { + margin-top: 30px; + background: rgba(255, 255, 255, 0.95); + border-radius: 15px; + padding: 20px; + box-shadow: 0 5px 15px rgba(0,0,0,0.1); +} + +.instructions h3 { + margin-bottom: 15px; + color: #495057; + text-align: center; +} + +.instructions ul { + list-style: none; + padding: 0; +} + +.instructions li { + margin-bottom: 8px; + padding: 8px 0; + border-bottom: 1px solid #f8f9fa; +} + +.instructions li:last-child { + border-bottom: none; +} + +.instructions strong { + color: #007bff; +} + +/* Message Display */ +.message { + position: fixed; + top: 20px; + right: 20px; + padding: 15px 25px; + border-radius: 8px; + color: white; + font-weight: bold; + z-index: 1000; + opacity: 0; + transform: translateY(-20px); + transition: all 0.3s ease; + max-width: 300px; +} + +.message.show { + opacity: 1; + transform: translateY(0); +} + +.message.success { + background: #28a745; +} + +.message.error { + background: #dc3545; +} + +.message.info { + background: #17a2b8; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .game-container { + flex-direction: column; + } + + .toolbar { + width: 100%; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + } + + .tool-section { + flex: 1; + min-width: 200px; + } + + .canvas-info { + flex-wrap: wrap; + justify-content: center; + } + + header h1 { + font-size: 2em; + } + + #art-canvas { + width: 100%; + max-width: 600px; + height: auto; + } +} + +@media (max-width: 480px) { + .container { + padding: 10px; + } + + .tool-group { + grid-template-columns: 1fr; + } + + .color-palette { + grid-template-columns: repeat(6, 1fr); + } + + .stamp-palette { + grid-template-columns: repeat(4, 1fr); + } + + .canvas-info { + font-size: 0.9em; + } +} + +/* Animation for tool selection */ +@keyframes toolSelect { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +.tool-btn.active { + animation: toolSelect 0.3s ease; +} + +/* Drawing cursor styles */ +.brush-cursor { + cursor: url('data:image/svg+xml,') 10 10, crosshair; +} + +.eraser-cursor { + cursor: url('data:image/svg+xml,') 10 10, crosshair; +} + +.fill-cursor { + cursor: url('data:image/svg+xml,') 10 10, pointer; +} \ No newline at end of file diff --git a/games/asteroids/index.html b/games/asteroids/index.html new file mode 100644 index 00000000..e08309c9 --- /dev/null +++ b/games/asteroids/index.html @@ -0,0 +1,36 @@ + + + + + + Asteroids + + + + + + + + + +
+
+

ASTEROIDS

+

Destroy all the asteroids to survive!

+ +

Controls:

+
    +
  • ArrowUp - Thrust Forward
  • +
  • ArrowLeft / ArrowRight - Rotate Ship
  • +
  • Space - Shoot Laser
  • +
+ + +
+
+ + + + + + \ No newline at end of file diff --git a/games/asteroids/script.js b/games/asteroids/script.js new file mode 100644 index 00000000..51f7b79f --- /dev/null +++ b/games/asteroids/script.js @@ -0,0 +1,441 @@ +// --- Setup --- +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); + +// --- NEW: Get UI Elements --- +const instructionsOverlay = document.getElementById('instructions-overlay'); +const startButton = document.getElementById('startButton'); + +// Set canvas dimensions +canvas.width = 800; +canvas.height = 600; + +// --- Game Constants --- +const SHIP_SIZE = 30; // Height of the triangle +const SHIP_THRUST = 0.1; // Acceleration +const FRICTION = 0.99; // 1 = no friction, 0 = lots +const TURN_SPEED = 0.1; // Radians per frame +const LASER_SPEED = 7; +const LASER_MAX_DIST = 0.6; // Max distance as fraction of canvas width +const ASTEROID_NUM = 3; // Starting number of asteroids +const ASTEROID_SIZE = 100; // Starting size in pixels +const ASTEROID_SPEED = 1; // Max starting speed +const ASTEROID_VERT = 10; // Number of vertices +const ASTEROID_JAG = 0.4; // Jaggedness (0 = none, 1 = lots) +const SHIP_INVULN_DUR = 3; // Invulnerability duration in seconds +const SHIP_BLINK_DUR = 0.1; // Blink duration in seconds +const MAX_LIVES = 3; +const POINTS_LG = 20; +const POINTS_MD = 50; +const POINTS_SM = 100; + +// --- Game State --- +let ship; +let lasers = []; +let asteroids = []; +let lives; +let score; +let isGameOver; +let invulnerabilityTime; + +// --- Input Handling --- +const keys = { + ArrowUp: false, + ArrowLeft: false, + ArrowRight: false, + " ": false // <-- FIX: Use the actual space character +}; + +document.addEventListener('keydown', (e) => { + // <-- FIX: Removed the incorrect e.key re-assignment + if (keys[e.key] !== undefined) { + keys[e.key] = true; + } +}); + +document.addEventListener('keyup', (e) => { + // <-- FIX: Removed the incorrect e.key re-assignment + if (keys[e.key] !== undefined) { + keys[e.key] = false; + } +}); + +// --- Utility Functions --- + +function distBetweenPoints(x1, y1, x2, y2) { + return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); +} + +function isColliding(obj1, obj2) { + return distBetweenPoints(obj1.x, obj1.y, obj2.x, obj2.y) < obj1.radius + obj2.radius; +} + +// --- Ship Functions --- + +function newShip() { + return { + x: canvas.width / 2, + y: canvas.height / 2, + radius: SHIP_SIZE / 2, + angle: 0, // Angle in radians (0 = facing right) + vel: { x: 0, y: 0 }, // Velocity + isThrusting: false, + rotation: 0, // 0 = not turning, -1 = left, 1 = right + blinkTime: Math.ceil(SHIP_BLINK_DUR * 60), // Blink time in frames + blinkOn: true + }; +} + +function drawShip() { + // Handle blinking during invulnerability + if (invulnerabilityTime > 0) { + ship.blinkTime--; + if (ship.blinkTime === 0) { + ship.blinkTime = Math.ceil(SHIP_BLINK_DUR * 60); + ship.blinkOn = !ship.blinkOn; + } + } else { + ship.blinkOn = true; + } + + if (!ship.blinkOn) { + return; // Don't draw ship if blinking off + } + + ctx.strokeStyle = 'white'; + ctx.lineWidth = 2; + + ctx.save(); + ctx.translate(ship.x, ship.y); + ctx.rotate(ship.angle); + + // Draw the triangular ship + ctx.beginPath(); + ctx.moveTo(SHIP_SIZE / 2, 0); // Nose + ctx.lineTo(-SHIP_SIZE / 2, -SHIP_SIZE / 3); // Rear left + ctx.lineTo(-SHIP_SIZE / 2, SHIP_SIZE / 3); // Rear right + ctx.closePath(); + ctx.stroke(); + + // Draw thruster flame + if (ship.isThrusting) { + ctx.fillStyle = 'red'; + ctx.beginPath(); + ctx.moveTo(-SHIP_SIZE / 2 - 2, 0); // Start behind ship + ctx.lineTo(-SHIP_SIZE / 2 - 12, -SHIP_SIZE / 6); + ctx.lineTo(-SHIP_SIZE / 2 - 12, SHIP_SIZE / 6); + ctx.closePath(); + ctx.fill(); + } + + ctx.restore(); +} + +function updateShip() { + // 1. Handle Turning + if (keys.ArrowLeft) ship.angle -= TURN_SPEED; + if (keys.ArrowRight) ship.angle += TURN_SPEED; + + // 2. Handle Thrust + ship.isThrusting = keys.ArrowUp; + if (ship.isThrusting) { + ship.vel.x += SHIP_THRUST * Math.cos(ship.angle); + ship.vel.y += SHIP_THRUST * Math.sin(ship.angle); + } + + // 3. Apply Friction + ship.vel.x *= FRICTION; + ship.vel.y *= FRICTION; + + // 4. Update Position + ship.x += ship.vel.x; + ship.y += ship.vel.y; + + // 5. Handle Screen Wrapping + if (ship.x < 0 - ship.radius) ship.x = canvas.width + ship.radius; + else if (ship.x > canvas.width + ship.radius) ship.x = 0 - ship.radius; + if (ship.y < 0 - ship.radius) ship.y = canvas.height + ship.radius; + else if (ship.y > canvas.height + ship.radius) ship.y = 0 - ship.radius; + + // 6. Handle Shooting + if (keys[" "]) { // <-- FIX: Check for the space character + shootLaser(); + keys[" "] = false; // <-- FIX: Prevent holding spacebar + } + + // 7. Update invulnerability timer + if (invulnerabilityTime > 0) { + invulnerabilityTime -= 1 / 60; // Assuming 60 FPS + } +} + +function destroyShip() { + lives--; + if (lives === 0) { + isGameOver = true; + } else { + ship = newShip(); + invulnerabilityTime = SHIP_INVULN_DUR; + } +} + +// --- Laser Functions --- + +function shootLaser() { + // Create laser object + const laser = { + x: ship.x + (SHIP_SIZE / 2) * Math.cos(ship.angle), + y: ship.y + (SHIP_SIZE / 2) * Math.sin(ship.angle), + vel: { + x: LASER_SPEED * Math.cos(ship.angle), + y: LASER_SPEED * Math.sin(ship.angle) + }, + distTraveled: 0, + radius: 2 + }; + lasers.push(laser); +} + +function updateLasers() { + for (let i = lasers.length - 1; i >= 0; i--) { + const laser = lasers[i]; + + // Update position + laser.x += laser.vel.x; + laser.y += laser.vel.y; + + // Update distance traveled + laser.distTraveled += Math.sqrt(Math.pow(laser.vel.x, 2) + Math.pow(laser.vel.y, 2)); + + // Remove if off-screen or max distance reached + if (laser.distTraveled > canvas.width * LASER_MAX_DIST || + laser.x < 0 || laser.x > canvas.width || + laser.y < 0 || laser.y > canvas.height) { + lasers.splice(i, 1); + } + } +} + +function drawLasers() { + ctx.fillStyle = 'salmon'; + for (const laser of lasers) { + ctx.beginPath(); + ctx.arc(laser.x, laser.y, laser.radius, 0, Math.PI * 2); + ctx.closePath(); // <-- FIX: Ensures the circle path is closed before filling + ctx.fill(); + } +} + +// --- Asteroid Functions --- + +function createAsteroid(x, y, size) { + const asteroid = { + x: x || Math.random() * canvas.width, + y: y || Math.random() * canvas.height, + vel: { + x: (Math.random() * 2 - 1) * ASTEROID_SPEED, + y: (Math.random() * 2 - 1) * ASTEROID_SPEED + }, + radius: size / 2, + size: size, + angle: Math.random() * Math.PI * 2, + vertices: [] + }; + + // Create vertices + for (let i = 0; i < ASTEROID_VERT; i++) { + const angle = (i / ASTEROID_VERT) * Math.PI * 2; + const radius = asteroid.radius * (1 + Math.random() * ASTEROID_JAG * 2 - ASTEROID_JAG); + asteroid.vertices.push({ + x: radius * Math.cos(angle), + y: radius * Math.sin(angle) + }); + } + return asteroid; +} + +function createAsteroidBelt() { + asteroids = []; + for (let i = 0; i < ASTEROID_NUM; i++) { + let x, y; + do { + x = Math.random() * canvas.width; + y = Math.random() * canvas.height; + } while (distBetweenPoints(ship.x, ship.y, x, y) < ASTEROID_SIZE * 2 + ship.radius); // Don't spawn on ship + asteroids.push(createAsteroid(x, y, ASTEROID_SIZE)); + } +} + +function breakAsteroid(asteroid) { + // Add points + if (asteroid.size === ASTEROID_SIZE) { + score += POINTS_LG; + } else if (asteroid.size === ASTEROID_SIZE / 2) { + score += POINTS_MD; + } else { + score += POINTS_SM; + } + + // Break into smaller pieces + if (asteroid.size > ASTEROID_SIZE / 4) { + const newSize = asteroid.size / 2; + asteroids.push(createAsteroid(asteroid.x, asteroid.y, newSize)); + asteroids.push(createAsteroid(asteroid.x, asteroid.y, newSize)); + } +} + +function updateAsteroids() { + for (const asteroid of asteroids) { + asteroid.x += asteroid.vel.x; + asteroid.y += asteroid.vel.y; + + // Screen wrapping + if (asteroid.x < 0 - asteroid.radius) asteroid.x = canvas.width + asteroid.radius; + else if (asteroid.x > canvas.width + asteroid.radius) asteroid.x = 0 - asteroid.radius; + if (asteroid.y < 0 - asteroid.radius) asteroid.y = canvas.height + asteroid.radius; + else if (asteroid.y > canvas.height + asteroid.radius) asteroid.y = 0 - asteroid.radius; + } +} + +function drawAsteroids() { + ctx.strokeStyle = 'slategrey'; + ctx.lineWidth = 2; + for (const asteroid of asteroids) { + ctx.save(); + ctx.translate(asteroid.x, asteroid.y); + ctx.rotate(asteroid.angle); + + ctx.beginPath(); + ctx.moveTo(asteroid.vertices[0].x, asteroid.vertices[0].y); + for (let j = 1; j < asteroid.vertices.length; j++) { + ctx.lineTo(asteroid.vertices[j].x, asteroid.vertices[j].y); + } + ctx.closePath(); + ctx.stroke(); + + ctx.restore(); + } +} + +// --- Collision Detection --- + +function checkCollisions() { + // 1. Ship vs Asteroids + if (invulnerabilityTime <= 0) { + for (const asteroid of asteroids) { + if (isColliding(ship, asteroid)) { + destroyShip(); + break; + } + } + } + + // 2. Lasers vs Asteroids + for (let i = lasers.length - 1; i >= 0; i--) { + for (let j = asteroids.length - 1; j >= 0; j--) { + if (isColliding(lasers[i], asteroids[j])) { + // Break asteroid + breakAsteroid(asteroids[j]); + + // Remove asteroid and laser + asteroids.splice(j, 1); + lasers.splice(i, 1); + + // Check for new level + if (asteroids.length === 0) { + newLevel(); + } + + break; // Move to next laser + } + } + } +} + +// --- Game UI --- + +function drawUI() { + // Draw Score + ctx.fillStyle = 'white'; + ctx.font = '24px Poppins'; // Use new font + ctx.textAlign = 'right'; + ctx.fillText(`Score: ${score}`, canvas.width - 20, 30); + + // Draw Lives + ctx.textAlign = 'left'; + ctx.fillText(`Lives: ${lives}`, 20, 30); + + // Draw Game Over + if (isGameOver) { + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.fillStyle = 'white'; + ctx.font = '50px Poppins'; + ctx.textAlign = 'center'; + ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2 - 30); + + ctx.font = '20px Poppins'; + ctx.fillText(`Final Score: ${score}`, canvas.width / 2, canvas.height / 2 + 20); + + ctx.font = '16px Poppins'; + ctx.fillText('Press SPACE to restart', canvas.width / 2, canvas.height / 2 + 60); + + // Listen for restart + if (keys[" "]) { + newGame(); + } + } +} + +// --- Game Loop --- + +function newGame() { + ship = newShip(); + lives = MAX_LIVES; + score = 0; + invulnerabilityTime = SHIP_INVULN_DUR; + isGameOver = false; + createAsteroidBelt(); +} + +function newLevel() { + invulnerabilityTime = SHIP_INVULN_DUR; + createAsteroidBelt(); +} + +function gameLoop() { + // Clear canvas + ctx.fillStyle = 'black'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + if (!isGameOver) { + // Update + updateShip(); + updateLasers(); + updateAsteroids(); + + // Check for collisions + checkCollisions(); + + // Draw + drawShip(); + drawLasers(); + drawAsteroids(); + } + + // Draw UI (score, lives, game over) + drawUI(); + + // Request next frame + requestAnimationFrame(gameLoop); +} + +// --- Start Game --- +startButton.addEventListener('click', () => { + instructionsOverlay.style.display = 'none'; + + // Call the functions that were at the bottom of the file + newGame(); + gameLoop(); +}); \ No newline at end of file diff --git a/games/asteroids/style.css b/games/asteroids/style.css new file mode 100644 index 00000000..b67d0f85 --- /dev/null +++ b/games/asteroids/style.css @@ -0,0 +1,114 @@ +/* General Reset & Font */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Poppins', sans-serif; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background-color: #121212; /* Dark background */ + color: #eee; + overflow: hidden; /* Hide scrollbars */ +} + +/* Hide canvas by default, JS will show it */ +canvas { + background-color: #000; + border: 2px solid #555; + box-shadow: 0 0 15px rgba(255, 255, 255, 0.1); + display: block; /* Removed from h1 */ +} + +/* Instructions Screen */ +#instructions-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +#instructions-content { + background-color: #2a2a2a; + padding: 30px 40px; + border-radius: 10px; + text-align: center; + border: 1px solid #444; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); + max-width: 400px; +} + +#instructions-content h2 { + font-size: 2.5rem; + color: #ff6347; /* 'salmon' color for title */ + margin-bottom: 15px; + letter-spacing: 2px; +} + +#instructions-content p { + font-size: 1.1rem; + margin-bottom: 25px; + color: #ccc; +} + +#instructions-content h3 { + font-size: 1.2rem; + color: #eee; + margin-bottom: 10px; + border-bottom: 1px solid #444; + padding-bottom: 5px; +} + +#instructions-content ul { + list-style: none; + margin-bottom: 30px; + text-align: left; + display: inline-block; +} + +#instructions-content li { + font-size: 1rem; + color: #ccc; + margin-bottom: 8px; +} + +/* Style for keys */ +#instructions-content code { + background-color: #444; + color: #fff; + padding: 3px 6px; + border-radius: 4px; + font-family: monospace; + font-size: 0.95rem; + margin-right: 8px; +} + +/* Start Button */ +#startButton { + background-color: #ff6347; /* salmon */ + color: white; + font-family: 'Poppins', sans-serif; + font-size: 1.2rem; + font-weight: 600; + border: none; + padding: 12px 30px; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s ease, transform 0.2s ease; +} + +#startButton:hover { + background-color: #e0553d; + transform: scale(1.05); +} \ No newline at end of file diff --git a/games/atari-breakout/index.html b/games/atari-breakout/index.html new file mode 100644 index 00000000..b99a0a91 --- /dev/null +++ b/games/atari-breakout/index.html @@ -0,0 +1,24 @@ + + + + + + Brick Breaker Clone + + + +
+

Brick Breaker

+ +
+ Score: 0 + Lives: 3 +
+ +
+ + + + \ No newline at end of file diff --git a/games/atari-breakout/script.js b/games/atari-breakout/script.js new file mode 100644 index 00000000..981839fb --- /dev/null +++ b/games/atari-breakout/script.js @@ -0,0 +1,373 @@ +// --- Game Setup and Constants --- +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const scoreElement = document.getElementById('score'); +const livesElement = document.getElementById('lives'); +const messageElement = document.getElementById('message'); + +// Game constants +const BALL_RADIUS = 8; +const PADDLE_HEIGHT = 15; +const PADDLE_WIDTH = 80; +const PADDLE_SPEED = 7; +const INITIAL_BALL_SPEED = 5; + +// Brick layout constants +const BRICK_ROW_COUNT = 5; +const BRICK_COLUMN_COUNT = 8; +const BRICK_WIDTH = (canvas.width - 20) / BRICK_COLUMN_COUNT - 5; // Calculate width based on canvas +const BRICK_HEIGHT = 15; +const BRICK_PADDING = 5; +const BRICK_OFFSET_TOP = 40; +const BRICK_OFFSET_LEFT = 10; + +// Game state variables +let score; +let lives; +let paddle; +let ball; +let bricks; +let gameRunning = false; +let gamePaused = true; +let keys = { + left: false, + right: false +}; + +// --- Object Setup Functions --- + +/** + * Initializes the paddle object. + */ +function initPaddle() { + paddle = { + width: PADDLE_WIDTH, + height: PADDLE_HEIGHT, + x: (canvas.width - PADDLE_WIDTH) / 2, + y: canvas.height - PADDLE_HEIGHT - 10 + }; +} + +/** + * Initializes the ball object. + */ +function initBall() { + ball = { + radius: BALL_RADIUS, + x: canvas.width / 2, + y: paddle.y - BALL_RADIUS, + dx: INITIAL_BALL_SPEED * (Math.random() > 0.5 ? 1 : -1), // Random horizontal start + dy: -INITIAL_BALL_SPEED, // Always start moving up + isStuck: true + }; +} + +/** + * Creates the 2D array of brick objects. + */ +function initBricks() { + bricks = []; + for (let c = 0; c < BRICK_COLUMN_COUNT; c++) { + bricks[c] = []; + for (let r = 0; r < BRICK_ROW_COUNT; r++) { + const brickX = c * (BRICK_WIDTH + BRICK_PADDING) + BRICK_OFFSET_LEFT; + const brickY = r * (BRICK_HEIGHT + BRICK_PADDING) + BRICK_OFFSET_TOP; + + // The status '1' means the brick is active and ready to be drawn + bricks[c][r] = { x: brickX, y: brickY, status: 1, color: getColor(r) }; + } + } +} + +/** + * Simple function to assign different colors per row. + */ +function getColor(row) { + switch(row % 5) { + case 0: return '#e06c75'; // Red + case 1: return '#98c379'; // Green + case 2: return '#61afef'; // Blue + case 3: return '#d19a66'; // Yellow + case 4: return '#c678dd'; // Purple + default: return '#fff'; + } +} + +// --- Game Control --- + +/** + * Starts a new game session. + */ +function startGame() { + score = 0; + lives = 3; + gameRunning = true; + gamePaused = true; + updateStatus(); + initPaddle(); + initBricks(); + initBall(); + + messageElement.innerHTML = "

Press Space to Launch!

"; + messageElement.classList.remove('hidden'); + + // Start the continuous game loop + requestAnimationFrame(gameLoop); +} + +/** + * Resets the ball and checks for game over. + */ +function resetBall() { + lives--; + updateStatus(); + + if (lives > 0) { + initBall(); // Reset ball position and velocity + gamePaused = true; + messageElement.innerHTML = "

Lost a Life! Press Space to Launch.

"; + messageElement.classList.remove('hidden'); + } else { + gameOver(false); + } +} + +/** + * Handles win/lose game state. + * @param {boolean} won - True if the player won, false if they lost. + */ +function gameOver(won) { + gameRunning = false; + if (won) { + messageElement.innerHTML = `

You Win! Final Score: ${score}

Press Space to Restart

`; + } else { + messageElement.innerHTML = `

Game Over! Final Score: ${score}

Press Space to Restart

`; + } + messageElement.classList.remove('hidden'); +} + +/** + * Updates the score and lives display. + */ +function updateStatus() { + scoreElement.textContent = `Score: ${score}`; + livesElement.textContent = `Lives: ${lives}`; +} + +// --- Physics and Collision Detection --- + +/** + * Updates the ball's position and checks for collisions with walls and paddle. + */ +function updateBall() { + if (ball.isStuck) { + // Move the ball with the paddle before launching + ball.x = paddle.x + paddle.width / 2; + return; + } + + // 1. Update position + ball.x += ball.dx; + ball.y += ball.dy; + + // 2. Wall collision (Top, Left, Right) + if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) { + ball.dx = -ball.dx; // Reverse horizontal direction + } + if (ball.y - ball.radius < 0) { + ball.dy = -ball.dy; // Reverse vertical direction (top wall) + } + + // 3. Bottom check (Game over for current ball) + if (ball.y + ball.radius > canvas.height) { + resetBall(); + } + + // 4. Paddle collision + checkPaddleCollision(); + + // 5. Brick collision + checkBrickCollision(); +} + +/** + * Checks for collision between the ball and the paddle, adjusting angle. + */ +function checkPaddleCollision() { + // Basic vertical and horizontal overlap check + const isOverlappingX = ball.x + ball.radius > paddle.x && ball.x - ball.radius < paddle.x + paddle.width; + const isHittingY = ball.y + ball.radius >= paddle.y && ball.y < paddle.y + paddle.height; + + if (isOverlappingX && isHittingY && ball.dy > 0) { + // Ball is falling (dy > 0) and hit the paddle + + // Calculate hit position relative to the paddle center (-1 to 1) + const hitPoint = (ball.x - (paddle.x + paddle.width / 2)) / (paddle.width / 2); + + // Adjust horizontal direction (dx) based on the hit point + // Max deflection angle is controlled by the INITIAL_BALL_SPEED, preventing extreme angles + ball.dx = hitPoint * INITIAL_BALL_SPEED * 1.5; + + // Reverse vertical direction + ball.dy = -ball.dy; + } +} + +/** + * Checks for and handles collisions between the ball and any active brick. + */ +function checkBrickCollision() { + let bricksRemaining = 0; + + for (let c = 0; c < BRICK_COLUMN_COUNT; c++) { + for (let r = 0; r < BRICK_ROW_COUNT; r++) { + const b = bricks[c][r]; + + if (b.status === 1) { + bricksRemaining++; + + // AABB (Axis-Aligned Bounding Box) check for simple square brick collision + const overlapX = ball.x + ball.radius > b.x && ball.x - ball.radius < b.x + BRICK_WIDTH; + const overlapY = ball.y + ball.radius > b.y && ball.y - ball.radius < b.y + BRICK_HEIGHT; + + if (overlapX && overlapY) { + // Collision occurred! + b.status = 0; // Destroy the brick + score += 10; + updateStatus(); + + // Simple Vertical Reversal: Reverse vertical direction (dy) + // (A more complex system would check collision side, but reversal is standard for Breakout clones) + ball.dy = -ball.dy; + + // Check for win condition + if (bricksRemaining - 1 === 0) { + gameOver(true); + } + + // Exit inner loops after the first brick hit in this frame + return; + } + } + } + } +} + +/** + * Updates the paddle's position based on user input. + */ +function updatePaddle() { + if (keys.left && paddle.x > 0) { + paddle.x -= PADDLE_SPEED; + } + if (keys.right && paddle.x < canvas.width - paddle.width) { + paddle.x += PADDLE_SPEED; + } +} + +// --- Drawing Functions --- + +/** + * Clears the canvas. + */ +function clearCanvas() { + ctx.fillStyle = '#000000'; + ctx.fillRect(0, 0, canvas.width, canvas.height); +} + +/** + * Draws the ball. + */ +function drawBall() { + ctx.beginPath(); + ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2); + ctx.fillStyle = '#61afef'; // Blue ball + ctx.fill(); + ctx.closePath(); +} + +/** + * Draws the paddle. + */ +function drawPaddle() { + ctx.fillStyle = '#e06c75'; // Red paddle + ctx.fillRect(paddle.x, paddle.y, paddle.width, paddle.height); +} + +/** + * Draws all active bricks. + */ +function drawBricks() { + for (let c = 0; c < BRICK_COLUMN_COUNT; c++) { + for (let r = 0; r < BRICK_ROW_COUNT; r++) { + const b = bricks[c][r]; + if (b.status === 1) { + ctx.fillStyle = b.color; + ctx.fillRect(b.x, b.y, BRICK_WIDTH, BRICK_HEIGHT); + + // Add a simple border + ctx.strokeStyle = '#000'; + ctx.strokeRect(b.x, b.y, BRICK_WIDTH, BRICK_HEIGHT); + } + } + } +} + +// --- Game Loop --- + +/** + * Main rendering and update loop using requestAnimationFrame. + * @param {number} timestamp - The current time provided by the browser. + */ +function gameLoop(timestamp) { + if (!gameRunning) return; + + // 1. Input and Update + if (!gamePaused) { + updatePaddle(); + updateBall(); + } + + // 2. Rendering + clearCanvas(); + drawBricks(); + drawPaddle(); + drawBall(); + + // 3. Continue the loop + requestAnimationFrame(gameLoop); +} + +// --- Event Handlers --- + +function handleKeydown(event) { + if (event.key === 'ArrowLeft' || event.key.toLowerCase() === 'a') { + keys.left = true; + } else if (event.key === 'ArrowRight' || event.key.toLowerCase() === 'd') { + keys.right = true; + } else if (event.key === ' ' || event.key.toLowerCase() === 'spacebar') { + // Space bar launches the ball or restarts the game + if (!gameRunning) { + startGame(); + } else if (ball.isStuck) { + ball.isStuck = false; + gamePaused = false; + messageElement.classList.add('hidden'); + } + } +} + +function handleKeyup(event) { + if (event.key === 'ArrowLeft' || event.key.toLowerCase() === 'a') { + keys.left = false; + } else if (event.key === 'ArrowRight' || event.key.toLowerCase() === 'd') { + keys.right = false; + } +} + +// Attach event listeners +document.addEventListener('keydown', handleKeydown); +document.addEventListener('keyup', handleKeyup); + +// --- Start the Game (Initial setup) --- +startGame(); \ No newline at end of file diff --git a/games/atari-breakout/style.css b/games/atari-breakout/style.css new file mode 100644 index 00000000..a3696dbd --- /dev/null +++ b/games/atari-breakout/style.css @@ -0,0 +1,56 @@ +body { + background-color: #282c34; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + font-family: 'Arial', sans-serif; + color: #abb2bf; + overflow: hidden; +} + +#game-container { + text-align: center; + background-color: #3e4451; + padding: 20px; + border-radius: 8px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5); +} + +h1 { + color: #e06c75; + margin-bottom: 10px; +} + +#gameCanvas { + background-color: #000000; + border: 2px solid #61afef; + display: block; + margin: 0 auto; +} + +#status-bar { + display: flex; + justify-content: space-between; + padding: 10px 0; + font-size: 1.2em; + color: #98c379; +} + +#message { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.7); + padding: 20px 40px; + border-radius: 5px; + color: #fff; + border: 1px solid #fff; + z-index: 10; +} + +.hidden { + display: none !important; +} \ No newline at end of file diff --git a/games/audio_reaction/index.html b/games/audio_reaction/index.html new file mode 100644 index 00000000..2b17de29 --- /dev/null +++ b/games/audio_reaction/index.html @@ -0,0 +1,32 @@ + + + + + + Sound Clicker Reaction Game + + + + +
+

๐Ÿ‘‚ Sound Clicker

+ +
+

Average Reaction: -- ms

+

Last Score: -- ms

+
+ +
+

Click START to begin!

+
+ +
+ +
+ + +
+ + + + \ No newline at end of file diff --git a/games/audio_reaction/script.js b/games/audio_reaction/script.js new file mode 100644 index 00000000..b0eb352a --- /dev/null +++ b/games/audio_reaction/script.js @@ -0,0 +1,133 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. DOM Elements & Constants --- + const clickTarget = document.getElementById('click-target'); + const messageDisplay = document.getElementById('message'); + const startButton = document.getElementById('start-button'); + const soundCue = document.getElementById('sound-cue'); + const lastReactionSpan = document.getElementById('last-reaction'); + const avgReactionSpan = document.getElementById('avg-reaction'); + + // --- 2. Game State Variables --- + let gameActive = false; + let hasSoundPlayed = false; + let startTime = 0; + let timeoutId = null; + let reactionTimes = []; + + // Timing Constants + const MIN_DELAY_MS = 2000; // 2 seconds minimum wait + const MAX_DELAY_MS = 5000; // 5 seconds maximum wait + + // --- 3. CORE LOGIC FUNCTIONS --- + + /** + * Initializes the game and starts the random delay phase. + */ + function startGame() { + if (gameActive) return; // Prevent double-start + + gameActive = true; + hasSoundPlayed = false; + startButton.disabled = true; + + // Reset message/styles for the "WAIT" phase + clickTarget.classList.remove('click-now'); + clickTarget.classList.add('wait'); + messageDisplay.textContent = 'WAIT...'; + messageDisplay.style.color = 'white'; + + // 1. Calculate random delay + const randomDelay = Math.random() * (MAX_DELAY_MS - MIN_DELAY_MS) + MIN_DELAY_MS; + + console.log(`Sound scheduled in: ${randomDelay.toFixed(0)} ms`); + + // 2. Schedule the sound and state change + timeoutId = setTimeout(triggerGo, randomDelay); + } + + /** + * Triggers the sound cue and marks the exact start time for reaction measurement. + */ + function triggerGo() { + if (!gameActive) return; + + // 1. Play the sound + // Resetting current time ensures it plays fully every time + soundCue.currentTime = 0; + soundCue.play().catch(error => { + console.warn("Audio playback failed (usually due to browser restrictions). Game still proceeds."); + // If sound fails, the visual cue must still work + }); + + // 2. Record the precise start time + startTime = performance.now(); + hasSoundPlayed = true; + + // 3. Update visual cue + clickTarget.classList.remove('wait'); + clickTarget.classList.add('click-now'); + messageDisplay.textContent = 'CLICK NOW!'; + messageDisplay.style.color = 'black'; + } + + /** + * Handles the player's click on the target area. + */ + function handleTargetClick() { + if (!gameActive) return; + + if (!hasSoundPlayed) { + // Premature click (false start) + clearTimeout(timeoutId); + endGame(0); // Pass 0 to signify a false start + } else { + // Valid click! Measure reaction time. + const endTime = performance.now(); + const reactionTime = endTime - startTime; + endGame(reactionTime); + } + } + + /** + * Updates scores, displays results, and prepares for the next round. + * @param {number} reactionTime - The measured time in milliseconds, or 0 for false start. + */ + function endGame(reactionTime) { + gameActive = false; + startButton.disabled = false; + clearTimeout(timeoutId); // Clear any remaining timeout + + // Reset styles + clickTarget.classList.remove('wait', 'click-now'); + messageDisplay.style.color = 'white'; + + if (reactionTime === 0) { + // False Start + lastReactionSpan.textContent = 'N/A'; + messageDisplay.textContent = 'โŒ FALSE START! Wait for the sound.'; + messageDisplay.style.color = '#e74c3c'; + } else { + // Valid Reaction + const roundedTime = reactionTime.toFixed(2); + reactionTimes.push(reactionTime); + + lastReactionSpan.textContent = `${roundedTime} ms`; + messageDisplay.textContent = `โœ… Your time: ${roundedTime} ms`; + + // Update Average Score + const total = reactionTimes.reduce((sum, time) => sum + time, 0); + const average = total / reactionTimes.length; + avgReactionSpan.textContent = `${average.toFixed(2)} ms`; + } + + startButton.textContent = 'RETRY'; + } + + // --- 4. EVENT LISTENERS --- + + startButton.addEventListener('click', startGame); + clickTarget.addEventListener('click', handleTargetClick); + + // Initial setup message + messageDisplay.textContent = 'Click START to begin!'; +}); \ No newline at end of file diff --git a/games/audio_reaction/style.css b/games/audio_reaction/style.css new file mode 100644 index 00000000..1e5acba8 --- /dev/null +++ b/games/audio_reaction/style.css @@ -0,0 +1,85 @@ +body { + font-family: 'Montserrat', sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #34495e; /* Dark background */ + color: #ecf0f1; +} + +#game-container { + background-color: #2c3e50; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4); + text-align: center; + max-width: 550px; + width: 90%; +} + +h1 { + color: #f1c40f; /* Yellow */ + margin-bottom: 20px; +} + +#status-area { + font-size: 1.1em; + margin-bottom: 20px; +} + +/* --- Click Target Area (The main screen) --- */ +#click-target { + width: 100%; + height: 250px; + background-color: #34495e; /* Default blue-grey */ + border: 3px solid #f1c40f; + border-radius: 10px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + margin-bottom: 20px; + transition: background-color 0.3s ease; +} + +#message { + font-size: 2em; + font-weight: bold; + margin: 0; + user-select: none; +} + +/* STATE 1: WAIT (After starting, before sound) */ +.wait { + background-color: #e67e22; /* Orange */ +} + +/* STATE 2: CLICK NOW (After sound) */ +.click-now { + background-color: #2ecc71; /* Green */ + cursor: pointer; +} + +/* --- Controls --- */ +#start-button { + padding: 15px 30px; + font-size: 1.2em; + font-weight: bold; + background-color: #3498db; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +#start-button:hover:not(:disabled) { + background-color: #2980b9; +} + +#start-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} \ No newline at end of file diff --git a/games/audio_visulizer/index.html b/games/audio_visulizer/index.html new file mode 100644 index 00000000..dfb2663b --- /dev/null +++ b/games/audio_visulizer/index.html @@ -0,0 +1,42 @@ + + + + + + The Audio Visualizer Dungeon ๐ŸŽถ + + + + +
+

The Audio Visualizer Dungeon ๐ŸŽถ

+

Your dungeon is generated by the sound spectrum!

+
+ +
+ + + +
Status: Waiting for audio file...
+
+ +
+
+ + +
+
+
+
+
+

Bass Power: 0

+

Treble Height: 0

+

Player Health: 100

+
+
+
+
+ + + + \ No newline at end of file diff --git a/games/audio_visulizer/script.js b/games/audio_visulizer/script.js new file mode 100644 index 00000000..667bae70 --- /dev/null +++ b/games/audio_visulizer/script.js @@ -0,0 +1,331 @@ +// --- 1. Global State and Audio Context --- +let audioContext; +let sourceNode; +let analyserNode; +let frequencyData; +let animationFrameId; + +const GAME_STATE = { + running: false, + player: { x: 60, y: 200, health: 100 }, + enemies: [], + spawnRate: 100, // Initial time between enemy spawns (ms) + lastSpawnTime: 0, + bassThreshold: 100, + trebleThreshold: 150 +}; + +// --- 2. DOM Elements --- +const D = (id) => document.getElementById(id); +const $ = { + audioUpload: D('audio-upload'), + startGameBtn: D('start-game'), + audioStatus: D('audio-status'), + canvas: D('frequency-canvas'), + player: D('player'), + enemies: D('enemies'), + trapDoor: D('trap-door'), + bassData: D('bass-data'), + trebleData: D('treble-data'), + playerHealth: D('player-health') +}; + +const canvasCtx = $.canvas.getContext('2d'); +const PLAYER_SPEED = 10; +const ENEMY_WIDTH = 15; +const DUNGEON_WIDTH = 600; + +// --- 3. Web Audio API Setup --- + +/** + * Initializes the Audio Context and AnalyserNode. + */ +function initAudio() { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + analyserNode = audioContext.createAnalyser(); + + // Settings for the AnalyserNode + analyserNode.fftSize = 256; // 128 frequency bins + frequencyData = new Uint8Array(analyserNode.frequencyBinCount); +} + +/** + * Handles the uploaded audio file. + */ +$.audioUpload.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (!file) return; + + if (!audioContext) initAudio(); + + const reader = new FileReader(); + reader.onload = (event) => { + audioContext.decodeAudioData(event.target.result) + .then(buffer => { + // Create source node + sourceNode = audioContext.createBufferSource(); + sourceNode.buffer = buffer; + sourceNode.loop = true; + + // Connect the chain: Source -> Analyser -> Destination (Speakers) + sourceNode.connect(analyserNode); + analyserNode.connect(audioContext.destination); + + $.audioStatus.textContent = `Status: Audio loaded. Ready to crawl!`; + $.startGameBtn.disabled = false; + }) + .catch(error => { + console.error('Error decoding audio:', error); + $.audioStatus.textContent = `Error: Could not decode audio file.`; + }); + }; + reader.readAsArrayBuffer(file); +}); + +// --- 4. Game Loop and Audio Analysis --- + +function startGame() { + if (GAME_STATE.running || !sourceNode) return; + + sourceNode.start(0); + GAME_STATE.running = true; + $.startGameBtn.disabled = true; + $.audioStatus.textContent = `Status: Dungeon crawling... RHYTHM DETECTED!`; + + // Start the game loop + gameLoop(); +} + +function gameLoop(timestamp) { + if (!GAME_STATE.running) return; + + // 1. Analyze Audio Data + analyserNode.getByteFrequencyData(frequencyData); + + // Calculate Rhythmic Triggers (Key Gameplay Logic) + const bass = frequencyData.slice(0, 5).reduce((a, b) => a + b) / 5; // Low Frequencies (Bass) + const treble = frequencyData.slice(analyserNode.frequencyBinCount - 5, analyserNode.frequencyBinCount).reduce((a, b) => a + b) / 5; // High Frequencies (Treble) + + // 2. Gameplay Mechanics + + // a. Enemy Spawning (Controlled by Bass) + if (bass > GAME_STATE.bassThreshold) { + // Bass drop = faster enemy spawning! + GAME_STATE.spawnRate = Math.max(100, 1000 - bass * 5); + } else { + GAME_STATE.spawnRate = 1000; // Slow spawn rate + } + + if (timestamp - GAME_STATE.lastSpawnTime > GAME_STATE.spawnRate) { + spawnEnemy(); + GAME_STATE.lastSpawnTime = timestamp; + } + + // b. Trap Door State (Controlled by Treble) + if (treble > GAME_STATE.trebleThreshold) { + $.trapDoor.classList.add('open'); // Trap opens on high-pitch sound + } else { + $.trapDoor.classList.remove('open'); // Trap closes otherwise + } + + // c. Enemy Movement and Collision + updateEnemies(); + checkPlayerCollision(); + + // 3. Visuals and UI Update + drawVisualizer(frequencyData); + updateUI(bass, treble); + + animationFrameId = requestAnimationFrame(gameLoop); +} + +// --- 5. Gameplay Logic Functions --- + +function spawnEnemy() { + const enemy = { + id: Date.now(), + x: DUNGEON_WIDTH - ENEMY_WIDTH, // Start on the far right + y: Math.random() * (400 - ENEMY_WIDTH), // Random height + speed: 1 + Math.random() * 2 // Speed variation + }; + GAME_STATE.enemies.push(enemy); + + const enemyEl = document.createElement('div'); + enemyEl.className = 'enemy'; + enemyEl.id = `enemy-${enemy.id}`; + enemyEl.style.left = `${enemy.x}px`; + enemyEl.style.top = `${enemy.y}px`; + $.enemies.appendChild(enemyEl); +} + +function updateEnemies() { + const playerRect = $.player.getBoundingClientRect(); + const trapDoorRect = $.trapDoor.getBoundingClientRect(); + const gameRect = D('game-grid').getBoundingClientRect(); + + GAME_STATE.enemies.forEach((enemy) => { + const enemyEl = D(`enemy-${enemy.id}`); + if (!enemyEl) return; + + // Move towards the player's y-position (simple AI) + const playerYInGrid = playerRect.top - gameRect.top + playerRect.height / 2; + if (enemy.y < playerYInGrid) enemy.y += enemy.speed * 0.5; + if (enemy.y > playerYInGrid) enemy.y -= enemy.speed * 0.5; + + // Base movement: move left + enemy.x -= enemy.speed; + + // Trap Door Collision Check (Obstacle) + const isDoorClosed = $.trapDoor.classList.contains('closed'); + const enemyRect = enemyEl.getBoundingClientRect(); + + // Check if enemy is hitting a closed door + if (isDoorClosed && enemyRect.right > trapDoorRect.left && enemyRect.left < trapDoorRect.right) { + enemy.x += enemy.speed; // Stop enemy movement (like a wall) + } + + enemyEl.style.left = `${enemy.x}px`; + enemyEl.style.top = `${enemy.y}px`; + }); + + // Clean up enemies that leave the screen + GAME_STATE.enemies = GAME_STATE.enemies.filter(enemy => { + if (enemy.x < -ENEMY_WIDTH) { + const el = D(`enemy-${enemy.id}`); + if (el) el.remove(); + return false; + } + return true; + }); +} + +function checkPlayerCollision() { + const playerRect = $.player.getBoundingClientRect(); + const gameRect = D('game-grid').getBoundingClientRect(); + + GAME_STATE.enemies.forEach((enemy) => { + const enemyEl = D(`enemy-${enemy.id}`); + if (!enemyEl) return; + + const enemyRect = enemyEl.getBoundingClientRect(); + + // Basic AABB Collision Check + const overlapX = playerRect.left < enemyRect.right && playerRect.right > enemyRect.left; + const overlapY = playerRect.top < enemyRect.bottom && playerRect.bottom > enemyRect.top; + + if (overlapX && overlapY) { + GAME_STATE.player.health -= 5; + // Remove enemy immediately after collision + GAME_STATE.enemies = GAME_STATE.enemies.filter(e => e.id !== enemy.id); + enemyEl.remove(); + + if (GAME_STATE.player.health <= 0) { + endGame("You succumbed to the rhythm of the dungeon!"); + } + } + }); +} + +function endGame(message) { + GAME_STATE.running = false; + cancelAnimationFrame(animationFrameId); + sourceNode.stop(); + alert(`Game Over! ${message}`); + // Reload for simplicity + window.location.reload(); +} + +function updateUI(bass, treble) { + $.bassData.textContent = Math.round(bass); + $.trebleData.textContent = Math.round(treble); + $.playerHealth.textContent = GAME_STATE.player.health; + + // Apply bass-driven light pulse to the dungeon + const pulseStrength = Math.min(10, bass / 10); + D('dungeon-visualizer').style.boxShadow = `0 0 ${pulseStrength}px ${pulseStrength / 2}px #ffb86c`; +} + +// --- 6. Visualizer Drawing --- + +function drawVisualizer(dataArray) { + canvasCtx.clearRect(0, 0, $.canvas.width, $.canvas.height); + + const barWidth = ($.canvas.width / analyserNode.frequencyBinCount) * 2.5; + let x = 0; + + for (let i = 0; i < analyserNode.frequencyBinCount; i++) { + const barHeight = dataArray[i]; + + // Gradient color from low (bass) to high (treble) + const colorH = 100 + (i / analyserNode.frequencyBinCount) * 200; // Shift hue + const color = `hsl(${colorH}, 100%, 50%)`; + + canvasCtx.fillStyle = color; + canvasCtx.fillRect(x, $.canvas.height - barHeight, barWidth, barHeight); + + x += barWidth + 1; + } +} + +// --- 7. Player Movement (WASD) --- + +document.addEventListener('keydown', (e) => { + if (!GAME_STATE.running) return; + + let newX = GAME_STATE.player.x; + let newY = GAME_STATE.player.y; + + const gameRect = D('game-grid').getBoundingClientRect(); + const trapDoorRect = $.trapDoor.getBoundingClientRect(); + + const isDoorOpen = $.trapDoor.classList.contains('open'); + + if (e.key === 'w' || e.key === 'ArrowUp') newY -= PLAYER_SPEED; + if (e.key === 's' || e.key === 'ArrowDown') newY += PLAYER_SPEED; + if (e.key === 'a' || e.key === 'ArrowLeft') newX -= PLAYER_SPEED; + if (e.key === 'd' || e.key === 'ArrowRight') newX += PLAYER_SPEED; + + // Boundary check + newX = Math.max(10, Math.min(DUNGEON_WIDTH - 10, newX)); + newY = Math.max(10, Math.min(400 - 10, newY)); + + // Trap Door Collision Check (Player) + const playerFutureRect = { + left: newX - 10 + gameRect.left, + right: newX + 10 + gameRect.left, + top: newY - 10 + gameRect.top, + bottom: newY + 10 + gameRect.top + }; + + if (!isDoorOpen) { + // If the door is CLOSED, check for collision with the door barrier + const overlapsX = playerFutureRect.left < trapDoorRect.right && playerFutureRect.right > trapDoorRect.left; + const overlapsY = playerFutureRect.top < trapDoorRect.bottom && playerFutureRect.bottom > trapDoorRect.top; + + if (overlapsX && overlapsY) { + // Player is trying to move into a closed door, cancel the move + return; + } + } + + + // Apply valid movement + GAME_STATE.player.x = newX; + GAME_STATE.player.y = newY; + $.player.style.left = `${GAME_STATE.player.x}px`; + $.player.style.top = `${GAME_STATE.player.y}px`; +}); + + +// --- 8. Initialization --- + +$.startGameBtn.addEventListener('click', startGame); + +// Initialize canvas size to match container +$.canvas.width = DUNGEON_WIDTH; +$.canvas.height = 400; + +// Initialize Web Audio API on first user interaction to satisfy browser policy +document.addEventListener('click', () => { + if (!audioContext) initAudio(); +}, { once: true }); \ No newline at end of file diff --git a/games/audio_visulizer/style.css b/games/audio_visulizer/style.css new file mode 100644 index 00000000..1dac7838 --- /dev/null +++ b/games/audio_visulizer/style.css @@ -0,0 +1,148 @@ +:root { + --dungeon-bg: #1a001a; /* Dark purple/black */ + --player-color: #50fa7b; /* Neon Green */ + --enemy-color: #ff5555; /* Red */ + --trap-color: #ffb86c; /* Orange */ + --wall-color: #44475a; +} + +/* Base Styles */ +body, html { + margin: 0; + padding: 0; + background-color: var(--dungeon-bg); + color: #f8f8f2; + font-family: sans-serif; + text-align: center; +} + +header { + margin: 20px 0; +} + +/* Controls Panel */ +#controls-panel { + margin-bottom: 20px; + padding: 15px; + background-color: #2a2a2a; + border-radius: 8px; + display: inline-block; +} + +#start-game { + padding: 10px 20px; + background-color: var(--player-color); + border: none; + border-radius: 5px; + cursor: pointer; + font-weight: bold; + margin-left: 10px; +} + +#start-game:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +#audio-status { + margin-top: 10px; + font-size: 0.9em; + color: var(--trap-color); +} + +/* --- Dungeon Environment --- */ +#game-container { + display: flex; + justify-content: center; + align-items: center; +} + +#dungeon-visualizer { + position: relative; + width: 600px; + height: 400px; + border: 3px solid var(--wall-color); + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + overflow: hidden; +} + +/* Canvas for Frequency Bars */ +#frequency-canvas { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.3; /* Subtle background visualizer */ + z-index: 10; +} + +/* Game Grid (The Play Area) */ +#game-grid { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 20; +} + +/* Player */ +#player { + position: absolute; + width: 20px; + height: 20px; + background-color: var(--player-color); + border-radius: 50%; + top: 50%; + left: 10%; + transform: translate(-50%, -50%); + transition: left 0.1s, top 0.1s; +} + +/* Enemies */ +.enemy { + position: absolute; + width: 15px; + height: 15px; + background-color: var(--enemy-color); + border-radius: 50%; + transition: left 0.5s, opacity 0.3s; +} + +/* Trap Door (The Rhythmic Obstacle) */ +#trap-door { + position: absolute; + width: 80px; + height: 400px; + background-color: var(--trap-color); + left: 70%; + top: 0; + transition: opacity 0.1s; + border-left: 5px dashed var(--wall-color); +} + +#trap-door.open { + opacity: 0.1; /* Almost invisible, passable */ +} + +#trap-door.closed { + opacity: 0.9; /* Solid barrier, impassable */ +} + +/* Status Area */ +#level-status { + position: absolute; + bottom: 10px; + left: 10px; + text-align: left; + background-color: rgba(0, 0, 0, 0.5); + padding: 5px 10px; + border-radius: 4px; + font-size: 0.8em; +} + +#level-status span { + font-weight: bold; + color: var(--player-color); +} \ No newline at end of file diff --git a/games/aurora-glow-catcher/index.html b/games/aurora-glow-catcher/index.html new file mode 100644 index 00000000..ccc317fd --- /dev/null +++ b/games/aurora-glow-catcher/index.html @@ -0,0 +1,89 @@ + + + + + + Aurora Glow Catcher โ€” Mini JS Games Hub + + + + + + + + +
+
+
+ ๐ŸŒŒ +
+

Aurora Glow Catcher

+

Collect glowing fragments to grow your aurora. Use arrow keys or mouse.

+
+
+ +
+ + + +
+ Time: 00:45 + Score: 0 + Lives: 3 +
+
+
+ +
+ + + +
+ +
+ Built with HTML โ€ข CSS โ€ข JS โ€” No downloads needed. Sound synthesized in browser. + โ† Back to Hub +
+ + + +
+ + + + diff --git a/games/aurora-glow-catcher/script.js b/games/aurora-glow-catcher/script.js new file mode 100644 index 00000000..996e8cba --- /dev/null +++ b/games/aurora-glow-catcher/script.js @@ -0,0 +1,630 @@ +/* Aurora Glow Catcher + - Canvas game with glowing fragments, obstacles, aurora effect. + - Controls: Arrow keys or drag mouse/touch to move catcher. + - Buttons: Play/Pause, Restart, Mute. + - Sounds: synthesized via WebAudio (no external downloads). +*/ + +(() => { + // --- ELEMENTS --- + const canvas = document.getElementById('gameCanvas'); + const ctx = canvas.getContext('2d', { alpha: true }); + const playPauseBtn = document.getElementById('playPauseBtn'); + const restartBtn = document.getElementById('restartBtn'); + const muteBtn = document.getElementById('muteBtn'); + const timeDisplay = document.getElementById('timeDisplay'); + const scoreDisplay = document.getElementById('scoreDisplay'); + const livesDisplay = document.getElementById('livesDisplay'); + const auroraBar = document.getElementById('auroraBar'); + const overlay = document.getElementById('overlay'); + const overlayTitle = document.getElementById('overlayTitle'); + const overlayMsg = document.getElementById('overlayMsg'); + const finalScore = document.getElementById('finalScore'); + const overlayRestart = document.getElementById('overlayRestart'); + const overlayClose = document.getElementById('overlayClose'); + + // --- RESIZE CANVAS --- + function resizeCanvas() { + const dpr = window.devicePixelRatio || 1; + canvas.width = Math.floor(canvas.clientWidth * dpr); + canvas.height = Math.floor(canvas.clientHeight * dpr); + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + } + window.addEventListener('resize', resizeCanvas); + resizeCanvas(); + + // --- GAME STATE --- + const GAME = { + width: canvas.clientWidth, + height: canvas.clientHeight, + running: false, + muted: false, + score: 0, + timeLeft: 45, // seconds + lives: 3, + lastTimestamp: 0, + spawnTimer: 0, + obstacleTimer: 0, + fragments: [], + obstacles: [], + auroraStrength: 0, // 0..1 + combo: 0 + }; + + // --- AUDIO (WebAudio synth) --- + const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + let masterGain = audioCtx.createGain(); + masterGain.gain.value = 0.25; // default volume + masterGain.connect(audioCtx.destination); + + function playBeep(freq = 440, time = 0.05, type = 'sine') { + if (GAME.muted) return; + const o = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + o.type = type; + o.frequency.value = freq; + g.gain.value = 0.0001; + o.connect(g); + g.connect(masterGain); + o.start(); + g.gain.exponentialRampToValueAtTime(0.5, audioCtx.currentTime + 0.01); + g.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + time); + o.stop(audioCtx.currentTime + time + 0.02); + } + function playPop() { playBeep(880, 0.08, 'triangle'); } + function playCollect() { playBeep(720, 0.10, 'sine'); } + function playHit() { playBeep(220, 0.18, 'sawtooth'); } + + // --- UTILS --- + function rand(min, max) { return Math.random() * (max - min) + min; } + function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); } + function formatTime(s) { const mm = Math.floor(s/60); const ss = Math.floor(s%60); return `${String(mm).padStart(2,'0')}:${String(ss).padStart(2,'0')}`; } + + // --- CATCHER (player) --- + const catcher = { + x: 0, y: 0, r: 36, + targetX: null, targetY: null, + speed: 900, // px per second + update(dt) { + if (this.targetX === null) return; + const dx = this.targetX - this.x; + const dy = this.targetY - this.y; + const dist = Math.hypot(dx, dy); + if (dist < 2) { this.x = this.targetX; this.y = this.targetY; return; } + const vx = (dx/dist) * this.speed * dt; + const vy = (dy/dist) * this.speed * dt; + this.x += vx; this.y += vy; + // clamp inside canvas + this.x = clamp(this.x, this.r, canvas.clientWidth - this.r); + this.y = clamp(this.y, this.r, canvas.clientHeight - this.r); + }, + draw() { + // catcher's soft glowing circle + const g = ctx.createRadialGradient(this.x, this.y, 4, this.x, this.y, this.r*2); + g.addColorStop(0, 'rgba(255,255,255,0.9)'); + g.addColorStop(0.12, 'rgba(180,240,255,0.6)'); + g.addColorStop(0.28, 'rgba(120,180,255,0.22)'); + g.addColorStop(1, 'rgba(120,150,240,0.03)'); + ctx.fillStyle = g; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.r*1.2, 0, Math.PI*2); + ctx.fill(); + + // catcher ring + ctx.lineWidth = 3; + ctx.strokeStyle = 'rgba(255,255,255,0.25)'; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.r, 0, Math.PI*2); + ctx.stroke(); + } + }; + + // --- PARTICLES / FRAGMENTS --- + class Fragment { + constructor() { + this.r = rand(6, 18); + this.x = rand(this.r, canvas.clientWidth - this.r); + this.y = rand(-120, -20); + this.vy = rand(30, 140); + this.vx = rand(-30, 30); + this.hue = rand(160, 300); // aurora palette + this.bonus = this.r > 12 ? 3 : 1; + this.glow = rand(0.6, 1.1); + this.collected = false; + this.life = rand(8, 16); + } + update(dt) { + this.x += this.vx * dt; + this.y += this.vy * dt; + this.life -= dt; + // horizontal wrap + if (this.x < -50) this.x = canvas.clientWidth + 50; + if (this.x > canvas.clientWidth + 50) this.x = -50; + } + draw() { + // multi-layer glow + ctx.save(); + ctx.globalCompositeOperation = 'lighter'; + for (let i = 0; i < 3; i++) { + const a = (0.08 + (0.06 * i)) * this.glow; + ctx.fillStyle = `hsla(${this.hue}, 90%, ${60 - i*8}%, ${a})`; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.r + i*6, 0, Math.PI*2); + ctx.fill(); + } + + // core + ctx.fillStyle = `hsl(${this.hue}, 90%, 88%)`; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.r*0.5, 0, Math.PI*2); + ctx.fill(); + + // small sparkle + ctx.fillStyle = 'rgba(255,255,255,0.9)'; + ctx.beginPath(); + ctx.arc(this.x - this.r*0.15, this.y - this.r*0.15, Math.max(1, this.r*0.12), 0, Math.PI*2); + ctx.fill(); + + ctx.restore(); + } + } + + // --- OBSTACLES (dark clouds) --- + class Obstacle { + constructor() { + this.w = rand(60, 160); + this.h = rand(36, 84); + this.x = rand(0, canvas.clientWidth - this.w); + this.y = rand(-140, -40); + this.vy = rand(40, 120); + this.opacity = rand(0.18, 0.42); + this.life = rand(8, 14); + } + update(dt) { + this.y += this.vy * dt; + this.life -= dt; + } + draw() { + ctx.save(); + ctx.globalCompositeOperation = 'source-over'; + const g = ctx.createLinearGradient(this.x, this.y, this.x + this.w, this.y + this.h); + g.addColorStop(0, `rgba(6,10,15,${this.opacity})`); + g.addColorStop(1, `rgba(10,14,20,${this.opacity+0.08})`); + ctx.fillStyle = g; + // soft shaped cloud + ctx.beginPath(); + ctx.ellipse(this.x + this.w*0.25, this.y + this.h*0.4, this.w*0.28, this.h*0.5, 0, 0, Math.PI*2); + ctx.ellipse(this.x + this.w*0.6, this.y + this.h*0.25, this.w*0.38, this.h*0.6, 0, 0, Math.PI*2); + ctx.ellipse(this.x + this.w*0.85, this.y + this.h*0.45, this.w*0.26, this.h*0.45, 0, 0, Math.PI*2); + ctx.fill(); + ctx.restore(); + } + } + + // --- SPAWN LOGIC --- + function spawnFragment() { + const f = new Fragment(); + GAME.fragments.push(f); + } + function spawnObstacle() { + const o = new Obstacle(); + GAME.obstacles.push(o); + } + + // --- COLLISION --- + function circleRectCollide(cx, cy, cr, rx, ry, rw, rh) { + // nearest point + const nx = clamp(cx, rx, rx + rw); + const ny = clamp(cy, ry, ry + rh); + const dx = cx - nx; + const dy = cy - ny; + return dx*dx + dy*dy <= cr*cr; + } + + // --- INPUT --- + let dragging = false; + canvas.addEventListener('pointerdown', (e) => { + dragging = true; + const rect = canvas.getBoundingClientRect(); + catcher.targetX = e.clientX - rect.left; + catcher.targetY = e.clientY - rect.top; + }); + window.addEventListener('pointermove', (e) => { + if (!dragging) return; + const rect = canvas.getBoundingClientRect(); + catcher.targetX = clamp(e.clientX - rect.left, catcher.r, canvas.clientWidth - catcher.r); + catcher.targetY = clamp(e.clientY - rect.top, catcher.r, canvas.clientHeight - catcher.r); + }); + window.addEventListener('pointerup', () => { dragging = false; }); + + // keyboard control + const keys = {}; + window.addEventListener('keydown', (e) => { + keys[e.key] = true; + // start audio context on interaction (mobile) + if (audioCtx.state === 'suspended') audioCtx.resume(); + }); + window.addEventListener('keyup', (e) => { keys[e.key] = false; }); + + function handleKeyboard(dt) { + let moved = false; + const speed = 420; // px/sec + if (keys.ArrowLeft || keys.a) { catcher.x -= speed * dt; moved = true; } + if (keys.ArrowRight || keys.d) { catcher.x += speed * dt; moved = true; } + if (keys.ArrowUp || keys.w) { catcher.y -= speed * dt; moved = true; } + if (keys.ArrowDown || keys.s) { catcher.y += speed * dt; moved = true; } + if (moved) { + catcher.targetX = catcher.x; + catcher.targetY = catcher.y; + } + // clamp + catcher.x = clamp(catcher.x, catcher.r, canvas.clientWidth - catcher.r); + catcher.y = clamp(catcher.y, catcher.r, canvas.clientHeight - catcher.r); + } + + // --- GAME CONTROL FUNCTIONS --- + function updateHUD() { + timeDisplay.textContent = formatTime(GAME.timeLeft); + scoreDisplay.textContent = GAME.score; + livesDisplay.textContent = GAME.lives; + const percent = clamp(GAME.auroraStrength * 100, 0, 100); + auroraBar.style.width = `${percent}%`; + } + + function resetGame() { + GAME.score = 0; + GAME.timeLeft = 45; + GAME.lives = 3; + GAME.fragments = []; + GAME.obstacles = []; + GAME.auroraStrength = 0; + GAME.combo = 0; + GAME.spawnTimer = 0; + GAME.obstacleTimer = 0; + GAME.lastTimestamp = 0; + catcher.x = canvas.clientWidth / 2; + catcher.y = canvas.clientHeight * 0.75; + catcher.targetX = catcher.x; + catcher.targetY = catcher.y; + overlay.classList.add('hidden'); + updateHUD(); + } + + function endGame(win = false) { + GAME.running = false; + playBeep(win ? 880 : 160, 0.4, win ? 'sine' : 'sawtooth'); + overlay.classList.remove('hidden'); + overlayTitle.textContent = win ? 'Victory!' : 'Game Over'; + overlayMsg.textContent = win ? 'You formed a beautiful aurora.' : 'Better luck next time!'; + finalScore.textContent = GAME.score; + overlayRestart.focus(); + playPauseBtn.textContent = 'Play'; + } + + // attach UI buttons + playPauseBtn.addEventListener('click', () => { + if (!GAME.running) { + if (audioCtx.state === 'suspended') audioCtx.resume(); + GAME.running = true; + playPauseBtn.textContent = 'Pause'; + GAME.lastTimestamp = performance.now(); + requestAnimationFrame(loop); + } else { + GAME.running = false; + playPauseBtn.textContent = 'Play'; + } + }); + + restartBtn.addEventListener('click', () => { + resetGame(); + if (!GAME.running) { + GAME.running = false; + playPauseBtn.textContent = 'Play'; + } + }); + + overlayRestart.addEventListener('click', () => { + resetGame(); + GAME.running = true; + playPauseBtn.textContent = 'Pause'; + overlay.classList.add('hidden'); + GAME.lastTimestamp = performance.now(); + requestAnimationFrame(loop); + }); + overlayClose.addEventListener('click', () => { + overlay.classList.add('hidden'); + }); + + muteBtn.addEventListener('click', () => { + GAME.muted = !GAME.muted; + muteBtn.textContent = GAME.muted ? '๐Ÿ”‡' : '๐Ÿ”Š'; + }); + + // --- MAIN LOOP --- + function loop(ts) { + if (!GAME.running) return; + const dt = Math.min(0.05, (ts - (GAME.lastTimestamp || ts)) / 1000); + GAME.lastTimestamp = ts; + + // handle keyboard movement + handleKeyboard(dt); + catcher.update(dt); + + // spawn logic + GAME.spawnTimer += dt; + GAME.obstacleTimer += dt; + if (GAME.spawnTimer > 0.6) { + spawnFragment(); + GAME.spawnTimer = 0; + } + if (GAME.obstacleTimer > 3.2) { + spawnObstacle(); + GAME.obstacleTimer = 0; + } + + // update fragments + for (let i = GAME.fragments.length - 1; i >= 0; i--) { + const f = GAME.fragments[i]; + f.update(dt); + // collision with catcher + const dx = f.x - catcher.x; + const dy = f.y - catcher.y; + const dist2 = dx*dx + dy*dy; + if (dist2 < (catcher.r + f.r)*(catcher.r + f.r)) { + // collect + GAME.score += 10 * f.bonus + Math.floor(GAME.combo * 2); + GAME.auroraStrength = clamp(GAME.auroraStrength + 0.03 * f.bonus, 0, 1); + GAME.combo += 1; + playCollect(); + GAME.fragments.splice(i,1); + continue; + } + // remove if offscreen or dead + if (f.y > canvas.clientHeight + 60 || f.life <= 0) { + GAME.fragments.splice(i,1); + GAME.combo = 0; // break combo when missed + } + } + + // update obstacles + for (let i = GAME.obstacles.length - 1; i >= 0; i--) { + const o = GAME.obstacles[i]; + o.update(dt); + if (circleRectCollide(catcher.x, catcher.y, catcher.r, o.x, o.y, o.w, o.h)) { + // hit + GAME.lives -= 1; + GAME.auroraStrength = clamp(GAME.auroraStrength - 0.08, 0, 1); + playHit(); + GAME.obstacles.splice(i,1); + } else if (o.y > canvas.clientHeight + 120 || o.life <= 0) { + GAME.obstacles.splice(i,1); + } + } + + // update time + GAME.timeLeft -= dt; + if (GAME.timeLeft <= 0 || GAME.lives <= 0) { + endGame(GAME.auroraStrength > 0.6); + updateHUD(); + return; + } + + // smooth aurora growth: small decay to encourage continuous play + GAME.auroraStrength = clamp(GAME.auroraStrength * 0.999 + 0.0001, 0, 1); + + // RENDER + render(); + + updateHUD(); + requestAnimationFrame(loop); + } + + // --- RENDERING: layered effects for bloom & aurora --- + function render() { + const w = canvas.clientWidth, h = canvas.clientHeight; + // clear + ctx.clearRect(0,0,w,h); + + // faint starfield backdrop + drawStars(); + + // aurora background: multi-layered color bands + drawAurora(); + + // soft ambient glow layer + ctx.save(); + ctx.globalCompositeOperation = 'lighter'; + // fragments (glow) + for (const f of GAME.fragments) f.draw(); + ctx.restore(); + + // obstacles + for (const o of GAME.obstacles) o.draw(); + + // catcher + catcher.draw(); + + // foreground sparkles + drawSparkles(); + + // subtle vignette + drawVignette(); + } + + // --- Helper draw functions --- + const starCache = []; + function drawStars() { + if (starCache.length === 0) { + for (let i=0;i<120;i++){ + starCache.push({ + x: rand(0, canvas.clientWidth), + y: rand(0, canvas.clientHeight), + r: rand(0.3,1.8), + a: rand(0.05,0.6) + }); + } + } + ctx.save(); + ctx.globalCompositeOperation = 'screen'; + for (const s of starCache) { + ctx.fillStyle = `rgba(255,255,255,${s.a})`; + ctx.beginPath(); + ctx.arc(s.x, s.y, s.r, 0, Math.PI*2); + ctx.fill(); + } + ctx.restore(); + } + + function drawAurora() { + const g = ctx.createLinearGradient(0, canvas.clientHeight*0.2, 0, canvas.clientHeight); + // base band colors influenced by auroraStrength + const t = GAME.auroraStrength; + const hueA = 190 + 140 * t; + const hueB = 240 - 40 * t; + const alphaA = 0.06 + 0.32 * t; + const alphaB = 0.02 + 0.14 * t; + + g.addColorStop(0.0, `hsla(${hueA}, 90%, 60%, ${alphaA})`); + g.addColorStop(0.5, `hsla(${hueB}, 85%, 55%, ${alphaB})`); + g.addColorStop(1.0, `rgba(6,8,20,0)`); + + ctx.save(); + ctx.globalCompositeOperation = 'lighter'; + ctx.fillStyle = g; + // wavy band + ctx.beginPath(); + const bands = 4; + for (let b=0; b { + if (!GAME.running) return; + // spawn more fragments as aurora grows (positive feedback) + if (Math.random() < 0.65) spawnFragment(); + // small chance of big fragment + if (Math.random() < Math.min(0.06 + GAME.auroraStrength*0.12, 0.18)) { + const f = new Fragment(); + f.r = rand(18, 30); + f.bonus = 3; + GAME.fragments.push(f); + } + }, 700); + + // ensure pointer events reflect actual canvas size + function scaleCatcherToCanvas() { + catcher.x = canvas.clientWidth / 2; + catcher.y = canvas.clientHeight * 0.75; + catcher.targetX = catcher.x; + catcher.targetY = catcher.y; + } + scaleCatcherToCanvas(); + + // auto-start small ambient breathing sound when the user plays + function startAmbient() { + if (GAME.muted) return; + if (audioCtx.state === 'suspended') audioCtx.resume(); + // gentle pulsing ambient with low oscillator + LFO + if (window._auroraAmbient) return; + const base = audioCtx.createOscillator(); + base.type = 'sine'; + base.frequency.value = 80; + const gain = audioCtx.createGain(); + gain.gain.value = 0.02; + base.connect(gain); + gain.connect(masterGain); + base.start(); + + // LFO + const lfo = audioCtx.createOscillator(); + lfo.frequency.value = 0.12; + const lfoGain = audioCtx.createGain(); + lfoGain.gain.value = 0.012; + lfo.connect(lfoGain); + lfoGain.connect(gain.gain); + lfo.start(); + + window._auroraAmbient = { base, lfo, gain }; + } + function stopAmbient() { + const amb = window._auroraAmbient; + if (!amb) return; + try { amb.base.stop(); amb.lfo.stop(); } catch(e) {} + delete window._auroraAmbient; + } + + // play/pause should toggle ambient too + playPauseBtn.addEventListener('click', () => { + if (GAME.running) stopAmbient(); else startAmbient(); + }); + overlayRestart.addEventListener('click', startAmbient); + overlayClose.addEventListener('click', stopAmbient); + + // small performance tweak: ensure canvas css size sync + function syncCanvasSize() { + canvas.style.width = canvas.clientWidth + 'px'; + canvas.style.height = canvas.clientHeight + 'px'; + } + syncCanvasSize(); + + // autoplay resume on user gesture + window.addEventListener('pointerdown', () => { + if (audioCtx.state === 'suspended') audioCtx.resume(); + }); + + // expose restart on double-click canvas too + canvas.addEventListener('dblclick', () => { + resetGame(); + }); + + // update HUD every half second + setInterval(updateHUD, 300); + + // initialize screen placement + resizeCanvas(); + scaleCatcherToCanvas(); + // ensure game doesn't start until user presses Play +})(); diff --git a/games/aurora-glow-catcher/style.css b/games/aurora-glow-catcher/style.css new file mode 100644 index 00000000..00aa6cf1 --- /dev/null +++ b/games/aurora-glow-catcher/style.css @@ -0,0 +1,160 @@ +:root{ + --bg1: #071028; + --bg2: #08122a; + --glass: rgba(255,255,255,0.06); + --glass-2: rgba(255,255,255,0.03); + --accent: #6ef0c7; + --accent-2: #7b6eff; + --text: #e9f2ff; + --muted: rgba(233,242,255,0.6); + --success: #b7ff9e; + --danger: #ff9e9e; + font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; +} + +*{box-sizing:border-box} +html,body,#gameCanvas{height:100%} +body{ + margin:0; + background: linear-gradient(180deg,var(--bg1), var(--bg2)); + color:var(--text); + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + display:flex; + min-height:100vh; + align-items:center; + justify-content:center; + padding:28px; +} + +/* App container */ +.app{ + width:min(1200px,96vw); + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.0)); + border-radius:14px; + box-shadow: 0 8px 30px rgba(2,6,23,0.7); + overflow:hidden; + display:flex; + flex-direction:column; +} + +/* Top bar */ +.topbar{ + display:flex; + justify-content:space-between; + padding:18px 22px; + align-items:center; + gap:12px; + border-bottom: 1px solid rgba(255,255,255,0.03); +} +.title{display:flex;align-items:center;gap:14px} +.title .emoji{font-size:34px} +.title h1{margin:0;font-size:20px} +.title .sub{margin:0;color:var(--muted);font-size:13px} + +/* Controls */ +.controls{display:flex;align-items:center;gap:12px} +.btn{ + background:var(--glass); + color:var(--text); + border:1px solid rgba(255,255,255,0.04); + padding:8px 12px; + border-radius:8px; + cursor:pointer; + font-weight:600; + font-size:14px; +} +.btn.primary{ + background: linear-gradient(90deg, var(--accent), var(--accent-2)); + color:#06101a; + border:none; + box-shadow: 0 6px 18px rgba(107,68,255,0.14), inset 0 -2px 6px rgba(255,255,255,0.03); +} +.meta{color:var(--muted);display:flex;gap:12px;font-size:13px;align-items:center} + +/* Main layout */ +.game-area{ + display:grid; + grid-template-columns: 1fr 320px; + gap:18px; + padding:18px; + align-items:stretch; +} + +/* Canvas */ +#gameCanvas{ + width:100%; + height:640px; + display:block; + border-radius:10px; + background: radial-gradient(1200px 600px at 10% 30%, rgba(120,80,255,0.08), transparent 10%), + radial-gradient(900px 400px at 90% 70%, rgba(120,255,190,0.04), transparent 8%), + linear-gradient(180deg, rgba(6,10,25,0.85), rgba(6,10,25,0.6)); + box-shadow: 0 12px 40px rgba(3,8,23,0.7), inset 0 1px 0 rgba(255,255,255,0.02); + position:relative; +} + +/* HUD panels on the right */ +.hud{ + padding:6px 10px; + display:flex; + flex-direction:column; + gap:12px; +} +.panel{ + background: linear-gradient(180deg,var(--glass), var(--glass-2)); + border-radius:10px; + padding:12px; + border:1px solid rgba(255,255,255,0.03); +} +.panel h3{margin:0 0 8px 0;font-size:14px} +.bar-wrap{ + width:100%; + height:14px; + background:rgba(255,255,255,0.03); + border-radius:8px; + overflow:hidden; +} +.bar{ + height:100%; + width:0%; + background: linear-gradient(90deg, #6ef0c7, #7b6eff); + box-shadow: 0 6px 20px rgba(100,150,255,0.12); + transition:width 300ms ease; +} + +/* rules & tips */ +.rules ul, .tips ol{padding-left:18px;margin:0;color:var(--muted);font-size:13px} +.rules li{margin:6px 0} + +/* Footer */ +.footer{ + display:flex; + justify-content:space-between; + align-items:center; + padding:12px 18px; + font-size:13px; + color:var(--muted); + border-top:1px solid rgba(255,255,255,0.02); +} +.back-link{color:var(--muted);text-decoration:none} + +/* overlay */ +.overlay{ + position:fixed;inset:0;display:flex;align-items:center;justify-content:center; + background:rgba(2,6,23,0.6);backdrop-filter: blur(5px); +} +.overlay.hidden{display:none} +.overlay-card{ + background:linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.02)); + color:var(--text); + border-radius:12px;padding:22px;width:min(520px,92%);text-align:center; + box-shadow:0 12px 40px rgba(2,6,23,0.7); +} +.overlay-actions{display:flex;gap:12px;justify-content:center;margin-top:14px} +kbd{background:rgba(255,255,255,0.04);padding:4px 6px;border-radius:6px;font-weight:600} +@media (max-width:980px){ + .game-area{grid-template-columns:1fr;grid-auto-rows:auto} + #gameCanvas{height:520px} + .hud{order:2} +} diff --git a/games/aurora-wave/index.html b/games/aurora-wave/index.html new file mode 100644 index 00000000..4f6a4e8d --- /dev/null +++ b/games/aurora-wave/index.html @@ -0,0 +1,29 @@ + + + + + +Aurora Wave | Mini JS Games Hub + + + +
+ +
+ + + + + +
+ +
+ + + diff --git a/games/aurora-wave/script.js b/games/aurora-wave/script.js new file mode 100644 index 00000000..39a4d2a0 --- /dev/null +++ b/games/aurora-wave/script.js @@ -0,0 +1,113 @@ +const canvas = document.getElementById('auroraCanvas'); +const ctx = canvas.getContext('2d'); +canvas.width = window.innerWidth; +canvas.height = window.innerHeight; + +let animationId; +let particles = []; +let isRunning = false; + +const backgroundMusic = document.getElementById('backgroundMusic'); +const startBtn = document.getElementById('startBtn'); +const pauseBtn = document.getElementById('pauseBtn'); +const restartBtn = document.getElementById('restartBtn'); +const glowRange = document.getElementById('glowRange'); +const speedRange = document.getElementById('speedRange'); + +const colors = ['#00ffe7', '#ff00c8', '#fffb00', '#00ff38', '#ff6d00']; + +class Particle { + constructor(x, y) { + this.x = x; + this.y = y; + this.vx = Math.random() * 2 - 1; + this.vy = Math.random() * 2 - 1; + this.size = Math.random() * 2 + 1; + this.color = colors[Math.floor(Math.random() * colors.length)]; + this.history = []; + } + + update() { + this.history.push({x: this.x, y: this.y}); + if(this.history.length > 30) this.history.shift(); + + this.x += this.vx * speedRange.value; + this.y += this.vy * speedRange.value; + + if(this.x < 0 || this.x > canvas.width) this.vx *= -1; + if(this.y < 0 || this.y > canvas.height) this.vy *= -1; + } + + draw() { + ctx.beginPath(); + for(let i=0; i { + p.update(); + p.draw(); + }); + animationId = requestAnimationFrame(animate); +} + +startBtn.addEventListener('click', () => { + if(!isRunning){ + isRunning = true; + animate(); + backgroundMusic.play(); + } +}); + +pauseBtn.addEventListener('click', () => { + if(isRunning){ + cancelAnimationFrame(animationId); + isRunning = false; + backgroundMusic.pause(); + } +}); + +restartBtn.addEventListener('click', () => { + cancelAnimationFrame(animationId); + initParticles(); + ctx.clearRect(0,0,canvas.width,canvas.height); + isRunning = false; + backgroundMusic.currentTime = 0; +}); + +window.addEventListener('resize', () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + ctx.clearRect(0,0,canvas.width,canvas.height); + initParticles(); +}); + +canvas.addEventListener('mousemove', (e) => { + for(let i=0; i<5; i++){ + const p = new Particle(e.clientX + Math.random()*20-10, e.clientY + Math.random()*20-10); + particles.push(p); + if(particles.length > 200) particles.shift(); + } +}); + +// Initialize +initParticles(100); diff --git a/games/aurora-wave/style.css b/games/aurora-wave/style.css new file mode 100644 index 00000000..a7f3a0d9 --- /dev/null +++ b/games/aurora-wave/style.css @@ -0,0 +1,37 @@ +body, html { + margin: 0; + padding: 0; + overflow: hidden; + background: #000010; + font-family: Arial, sans-serif; +} + +.aurora-container { + position: relative; + width: 100vw; + height: 100vh; +} + +canvas { + display: block; +} + +.controls { + position: absolute; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 10px; + background: rgba(0,0,0,0.5); + padding: 10px 15px; + border-radius: 10px; +} + +.controls button, .controls input { + cursor: pointer; + padding: 5px 10px; + border-radius: 5px; + border: none; + font-size: 14px; +} diff --git a/games/avoid-the-blocks/index.html b/games/avoid-the-blocks/index.html new file mode 100644 index 00000000..d3a7611d --- /dev/null +++ b/games/avoid-the-blocks/index.html @@ -0,0 +1,24 @@ + + + + + + Avoid the Blocks | Mini JS Games Hub + + + +
+

Avoid the Blocks

+

Use โฌ…๏ธ and โžก๏ธ keys to move. Survive as long as possible!

+
+
+
+
+ Score: 0 +
+ +
+ + + + diff --git a/games/avoid-the-blocks/script.js b/games/avoid-the-blocks/script.js new file mode 100644 index 00000000..bcd2104d --- /dev/null +++ b/games/avoid-the-blocks/script.js @@ -0,0 +1,105 @@ +const gameArea = document.getElementById("game-area"); +const player = document.getElementById("player"); +const scoreDisplay = document.getElementById("score"); +const restartBtn = document.getElementById("restart-btn"); + +let playerPos = 180; // initial left position +let blocks = []; +let blockSpeed = 2; +let blockInterval = 2000; +let score = 0; +let gameOver = false; +let moveLeft = false; +let moveRight = false; + +// Create player movement +document.addEventListener("keydown", (e) => { + if (e.key === "ArrowLeft") moveLeft = true; + if (e.key === "ArrowRight") moveRight = true; +}); +document.addEventListener("keyup", (e) => { + if (e.key === "ArrowLeft") moveLeft = false; + if (e.key === "ArrowRight") moveRight = false; +}); + +// Game loop +function update() { + if (gameOver) return; + + // Move player + if (moveLeft) playerPos -= 5; + if (moveRight) playerPos += 5; + if (playerPos < 0) playerPos = 0; + if (playerPos > gameArea.clientWidth - 40) playerPos = gameArea.clientWidth - 40; + player.style.left = playerPos + "px"; + + // Move blocks + blocks.forEach((block, index) => { + let top = parseInt(block.style.top); + block.style.top = top + blockSpeed + "px"; + + // Check collision + if ( + top + 40 >= gameArea.clientHeight - 10 && + parseInt(block.style.left) < playerPos + 40 && + parseInt(block.style.left) + 40 > playerPos + ) { + endGame(); + } + + // Remove blocks if out of view + if (top > gameArea.clientHeight) { + gameArea.removeChild(block); + blocks.splice(index, 1); + score++; + scoreDisplay.textContent = score; + // Gradually increase difficulty + if (score % 5 === 0) blockSpeed += 0.5; + if (score % 10 === 0 && blockInterval > 500) { + clearInterval(blockSpawner); + blockInterval -= 100; + blockSpawner = setInterval(spawnBlock, blockInterval); + } + } + }); + + requestAnimationFrame(update); +} + +// Spawn blocks +function spawnBlock() { + const block = document.createElement("div"); + block.classList.add("block"); + const blockLeft = Math.floor(Math.random() * (gameArea.clientWidth - 40)); + block.style.left = blockLeft + "px"; + block.style.top = "0px"; + gameArea.appendChild(block); + blocks.push(block); +} + +// End game +function endGame() { + gameOver = true; + alert("Game Over! Your score: " + score); +} + +// Restart game +restartBtn.addEventListener("click", () => { + // Reset + blocks.forEach(block => gameArea.removeChild(block)); + blocks = []; + playerPos = 180; + player.style.left = playerPos + "px"; + score = 0; + blockSpeed = 2; + blockInterval = 2000; + scoreDisplay.textContent = score; + gameOver = false; + clearInterval(blockSpawner); + blockSpawner = setInterval(spawnBlock, blockInterval); + update(); +}); + +// Start game +let blockSpawner = setInterval(spawnBlock, blockInterval); +update(); diff --git a/games/avoid-the-blocks/style.css b/games/avoid-the-blocks/style.css new file mode 100644 index 00000000..84b53e9e --- /dev/null +++ b/games/avoid-the-blocks/style.css @@ -0,0 +1,76 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #1f1c2c, #928dab); + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + text-align: center; + max-width: 400px; +} + +h1 { + margin-bottom: 5px; +} + +.instructions { + font-size: 14px; + margin-bottom: 10px; + color: #ccc; +} + +#game-area { + position: relative; + width: 100%; + height: 500px; + background: #2c2c54; + border-radius: 10px; + overflow: hidden; + margin: 0 auto 10px; + box-shadow: 0 0 20px rgba(0,0,0,0.5); +} + +#player { + position: absolute; + bottom: 10px; + left: 50%; + transform: translateX(-50%); + width: 40px; + height: 40px; + background: linear-gradient(45deg, #ff6b6b, #feca57); + border-radius: 5px; +} + +.block { + position: absolute; + width: 40px; + height: 40px; + background: linear-gradient(45deg, #54a0ff, #00d2d3); + border-radius: 5px; +} + +.score-board { + font-size: 18px; + margin: 10px 0; +} + +#restart-btn { + padding: 8px 16px; + border: none; + background-color: #ff6b6b; + color: #fff; + font-size: 16px; + border-radius: 5px; + cursor: pointer; + transition: 0.3s; +} + +#restart-btn:hover { + background-color: #feca57; + color: #000; +} diff --git a/games/avoid-the-flash/index.html b/games/avoid-the-flash/index.html new file mode 100644 index 00000000..aadc0cbf --- /dev/null +++ b/games/avoid-the-flash/index.html @@ -0,0 +1,31 @@ + + + + + +Avoid the Flash | Mini JS Games Hub + + + +
+

Avoid the Flash

+
+
+
+
+ + + + +
+

Score: 0

+

+
+ + + + + + + + diff --git a/games/avoid-the-flash/script.js b/games/avoid-the-flash/script.js new file mode 100644 index 00000000..24061917 --- /dev/null +++ b/games/avoid-the-flash/script.js @@ -0,0 +1,80 @@ +const obstacleLine = document.querySelector('.obstacle-line'); +const startBtn = document.getElementById('start-btn'); +const pauseBtn = document.getElementById('pause-btn'); +const resumeBtn = document.getElementById('resume-btn'); +const restartBtn = document.getElementById('restart-btn'); +const scoreEl = document.getElementById('score'); +const messageEl = document.getElementById('message'); + +const successSound = document.getElementById('success-sound'); +const failSound = document.getElementById('fail-sound'); + +let score = 0; +let gameInterval; +let gamePaused = false; + +function randomFlash() { + const isSafe = Math.random() > 0.3; // 70% safe, 30% danger + if (isSafe) { + obstacleLine.style.background = '#0f0'; // green safe + } else { + obstacleLine.style.background = '#f00'; // red danger + } +} + +function startGame() { + score = 0; + scoreEl.textContent = score; + messageEl.textContent = ''; + startBtn.disabled = true; + pauseBtn.disabled = false; + restartBtn.disabled = false; + resumeBtn.disabled = true; + gamePaused = false; + gameInterval = setInterval(() => { + randomFlash(); + }, 1000); +} + +function pauseGame() { + clearInterval(gameInterval); + gamePaused = true; + pauseBtn.disabled = true; + resumeBtn.disabled = false; +} + +function resumeGame() { + gamePaused = false; + gameInterval = setInterval(() => { + randomFlash(); + }, 1000); + pauseBtn.disabled = false; + resumeBtn.disabled = true; +} + +function restartGame() { + clearInterval(gameInterval); + startGame(); +} + +obstacleLine.addEventListener('click', () => { + if (gamePaused) return; + if (obstacleLine.style.background === 'rgb(255, 0, 0)') { + failSound.play(); + messageEl.textContent = '๐Ÿ’€ You clicked on red! Game Over'; + clearInterval(gameInterval); + startBtn.disabled = false; + pauseBtn.disabled = true; + resumeBtn.disabled = true; + } else { + score++; + scoreEl.textContent = score; + successSound.play(); + obstacleLine.style.background = '#0ff'; // flash effect + } +}); + +startBtn.addEventListener('click', startGame); +pauseBtn.addEventListener('click', pauseGame); +resumeBtn.addEventListener('click', resumeGame); +restartBtn.addEventListener('click', restartGame); diff --git a/games/avoid-the-flash/style.css b/games/avoid-the-flash/style.css new file mode 100644 index 00000000..3759c1a0 --- /dev/null +++ b/games/avoid-the-flash/style.css @@ -0,0 +1,65 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: #0f0f0f; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + color: #fff; +} + +.game-container { + text-align: center; + width: 350px; + padding: 20px; + background: #111; + border-radius: 12px; + box-shadow: 0 0 20px #0ff; +} + +.game-screen { + margin: 20px 0; + height: 50px; + background: #222; + border-radius: 8px; + position: relative; + overflow: hidden; + box-shadow: 0 0 15px #0ff inset; +} + +.obstacle-line { + height: 100%; + width: 100%; + background: #0f0; + transition: background 0.2s ease; +} + +.controls button { + margin: 5px; + padding: 8px 15px; + font-size: 16px; + border: none; + border-radius: 6px; + cursor: pointer; + color: #fff; + background: #0ff; + box-shadow: 0 0 10px #0ff; + transition: all 0.2s ease; +} + +.controls button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.score { + font-size: 18px; + margin-top: 10px; +} + +#message { + margin-top: 10px; + font-weight: bold; + font-size: 16px; +} diff --git a/games/balance-scale/index.html b/games/balance-scale/index.html new file mode 100644 index 00000000..3f6c2538 --- /dev/null +++ b/games/balance-scale/index.html @@ -0,0 +1,70 @@ + +
+ + + + + +
+ + + +
+ + + +
+
+
+
Left: 0
+
Right: 0
+
+
+ + +
+
+
+scale pivot decorative image +
+
+
+ + + +
+
+ + +
+
Moves: 0
+
Time: 00:00
+
Hints used: 0
+
+ + + + + + + +
+

Tip: press numbers 1โ€“9 to quick-select weights. Keyboard: Z = Undo, R = Reset, P = Pause.

+
+ + + + + + diff --git a/games/balance-scale/script.js b/games/balance-scale/script.js new file mode 100644 index 00000000..4d1c2241 --- /dev/null +++ b/games/balance-scale/script.js @@ -0,0 +1,94 @@ +(() => { +el.draggable=true; el.addEventListener('dragstart', e => e.dataTransfer.setData('text/plain', JSON.stringify({from:'pan', id:item.id, value:item.value, fromSide:side}))) +} + + +function renderPan(panEl, arr, side){ +panEl.innerHTML = ''; +arr.forEach(item =>{ +const d = document.createElement('div'); d.className='placed'; d.textContent = item.value; d.draggable=true; d.tabIndex=0; +bindPlacedDrag(d, item, side); +// keyboard: press Enter to pick up and press on other pan to drop +d.addEventListener('keydown', e=>{ if(e.key==='Enter' || e.key===' '){ // prepare move +status.textContent = `Picked ${item.value}. Click target pan to drop.`; const handler = ev => { undoStack.push(snapshotState()); if(side==='left'){ leftPanArr = leftPanArr.filter(x=>x.id!==item.id); rightPanArr.push(item);} else { rightPanArr = rightPanArr.filter(x=>x.id!==item.id); leftPanArr.push(item);} renderEverything(); updateStats(); checkWin(); cleanup();}; leftPan.addEventListener('click', handler, {once:true}); rightPan.addEventListener('click', handler, {once:true}); function cleanup(){ status.textContent='Place weights to balance the scale' } } }); +panEl.appendChild(d); +}); +} + + +function renderEverything(){ +renderInventory(); renderPan(leftPan, leftPanArr, 'left'); renderPan(rightPan, rightPanArr, 'right'); +leftTotal.textContent = leftPanArr.reduce((s,i)=>s+i.value,0); +rightTotal.textContent = rightPanArr.reduce((s,i)=>s+i.value,0); +document.getElementById('movesCount').textContent = moves; +document.getElementById('hints').textContent = hints; + + +// glow when balanced +if(Number(leftTotal.textContent) === Number(rightTotal.textContent) && (leftPanArr.length+rightPanArr.length)>0){ +document.querySelector('.scale').classList.add('balanced'); leftPan.classList.add('glow'); rightPan.classList.add('glow'); status.textContent = 'Balanced! ๐ŸŽ‰'; playSound(soundWin); +} else { document.querySelector('.scale').classList.remove('balanced'); leftPan.classList.remove('glow'); rightPan.classList.remove('glow'); status.textContent = 'Place weights to balance the scale'; } +} + + +function snapshotState(){ return JSON.stringify({inventory:leftCopy(inventory), left:leftCopy(leftPanArr), right:leftCopy(rightPanArr), moves, seconds, hints}); } +function leftCopy(arr){ return JSON.parse(JSON.stringify(arr)); } + + +// undo +function undo(){ if(undoStack.length===0){ playSound(soundError); return;} const s = JSON.parse(undoStack.pop()); inventory = s.inventory; leftPanArr = s.left; rightPanArr = s.right; moves = s.moves; seconds = s.seconds; hints=s.hints; renderEverything(); updateStats(); } + + +// reset level +function resetLevel(){ undoStack=[]; startLevel(currentLevel); } + + +function updateStats(){ document.getElementById('movesCount').textContent = moves; } + + +function checkWin(){ if(Number(leftTotal.textContent) === Number(rightTotal.textContent) && (leftPanArr.length+rightPanArr.length)>0){ status.textContent = 'Balanced! Advancing in 1.5s...'; setTimeout(()=>{ // auto-advance if not last level +if(currentLevel < levels.length-1) loadLevel(currentLevel+1); else status.textContent='You completed all levels! Congrats!'; },1500); }} + + +// timer +function startTimer(){ stopTimer(); seconds=0; timer = setInterval(()=>{ if(!paused){ seconds++; timerEl.textContent = formatTime(seconds);} },1000); } +function stopTimer(){ if(timer) clearInterval(timer); timer=null; } + + +function pauseToggle(){ paused = !paused; document.getElementById('pauseBtn').textContent = paused? 'โ–ถ Resume' : 'โธ Pause'; } + + +// load level +function startLevel(i){ currentLevel = i; const lvl = levels[i]; inventory = [...lvl.weights]; leftPanArr = (lvl.fixed.left||[]).map(v=>({id:Date.now()+Math.random(), value:v, source:'fixed'})); rightPanArr = (lvl.fixed.right||[]).map(v=>({id:Date.now()+Math.random(), value:v, source:'fixed'})); moves=0; hints=0; undoStack=[]; seconds=0; paused=false; document.getElementById('levelSelect').value = i; document.getElementById('pauseBtn').textContent='โธ Pause'; startTimer(); renderEverything(); updateStats(); } + + +function loadLevel(i){ startLevel(i); } + + +// quick select via number keys +document.addEventListener('keydown', e =>{ +if(e.key >= '1' && e.key <= '9'){ const n = Number(e.key); if(n-1 < inventory.length) selectWeightFromInventory(n-1); } +if(e.key.toLowerCase() === 'z') undo(); +if(e.key.toLowerCase() === 'r') resetLevel(); +if(e.key.toLowerCase() === 'p') pauseToggle(); +}); + + +// bind UI Buttons +document.getElementById('undoBtn').addEventListener('click', undo); +document.getElementById('resetBtn').addEventListener('click', resetLevel); +document.getElementById('pauseBtn').addEventListener('click', pauseToggle); +document.getElementById('restartBtn').addEventListener('click', ()=> startLevel(currentLevel)); +levelSelect.addEventListener('change', e=> loadLevel(Number(e.target.value))); + + +// initialize drag-drop visuals for touch (simple tap-to-place fallback) +['leftPan','rightPan'].forEach(id =>{ const el = document.getElementById(id); el.addEventListener('touchstart', ev=>{}, {passive:true}); }); + + +// initialize +loadLevel(0); + + +})(); + diff --git a/games/balance-scale/style.css b/games/balance-scale/style.css new file mode 100644 index 00000000..77f30fa3 --- /dev/null +++ b/games/balance-scale/style.css @@ -0,0 +1,43 @@ +:root{ +--bg:#0f1724; --card:#0b1220; --accent:#ffd166; --muted:#94a3b8; --glass: rgba(255,255,255,0.04); +} +*{box-sizing:border-box} +html,body{height:100%;margin:0;font-family:Inter,system-ui,Segoe UI,Roboto,Arial;color:#e6eef8;background:linear-gradient(180deg,#071329 0%, #0b1930 100%);} +.scale-app{max-width:1100px;margin:28px auto;padding:20px;border-radius:14px;background:linear-gradient(180deg,rgba(255,255,255,0.02),transparent);box-shadow:0 10px 30px rgba(2,6,23,0.7);} +.topbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px} +.brand{display:flex;gap:12px;align-items:center} +.brand-icon{font-size:34px} +.topbar h1{margin:0;font-size:20px} +.controls{display:flex;gap:10px;align-items:center} +.controls button, .controls select{background:var(--glass);border:1px solid rgba(255,255,255,0.06);color:var(--muted);padding:8px 10px;border-radius:10px;cursor:pointer} +.controls button:hover{transform:translateY(-2px);} +.game-area{display:flex;gap:18px} +.inventory{width:260px;background:linear-gradient(180deg,rgba(255,255,255,0.02),transparent);padding:14px;border-radius:12px;border:1px solid rgba(255,255,255,0.03)} +.inventory h2{margin:6px 0} +.inventory-hint{color:var(--muted);font-size:13px;margin:0 0 10px} +.weights-list{list-style:none;padding:6px;margin:0;display:flex;flex-wrap:wrap;gap:8px} +.weight{min-width:64px;padding:10px;border-radius:10px;background:linear-gradient(180deg,#ffffff10,#00000010);border:1px solid rgba(255,255,255,0.04);display:flex;flex-direction:column;align-items:center;gap:6px;cursor:grab;user-select:none} +.weight:active{cursor:grabbing} +.weight .val{font-weight:700;font-size:16px;color:#fff} +.weight small{color:var(--muted)} +.scale{flex:1;padding:18px;background:linear-gradient(180deg,rgba(255,255,255,0.01),transparent);border-radius:12px;border:1px solid rgba(255,255,255,0.03);display:flex;flex-direction:column;align-items:center} +.scale-top{width:100%;display:flex;justify-content:center;margin-bottom:10px} +.totals{display:flex;gap:18px} +.total{background:rgba(255,255,255,0.02);padding:8px 12px;border-radius:999px;color:var(--muted)} +.scale-body{display:flex;align-items:flex-start;gap:18px;width:100%;justify-content:center} +.pan{width:320px;height:180px;border-radius:18px;border:2px dashed rgba(255,255,255,0.04);background:linear-gradient(180deg,rgba(255,255,255,0.01),transparent);display:flex;flex-wrap:wrap;align-content:flex-start;padding:12px;gap:8px;transition:transform 400ms cubic-bezier(.21,.9,.28,1)} +.pan .placed{display:inline-flex;align-items:center;justify-content:center;padding:6px 10px;border-radius:8px;background:linear-gradient(180deg,#ffffff06,#00000006);border:1px solid rgba(255,255,255,0.03);min-width:56px} +.pivot img{width:120px;filter:grayscale(.2) saturate(.7);border-radius:12px} +.scale-footer{margin-top:12px} +.status{color:var(--accent);font-weight:700} +.hud{display:flex;justify-content:space-between;margin-top:12px;color:var(--muted);} +.footer-note{color:var(--muted);margin-top:14px} + + +/* glow for balanced state */ +.scale.balanced{box-shadow:0 0 40px rgba(255,209,102,0.12), inset 0 0 80px rgba(255,209,102,0.02);border-color:rgba(255,209,102,0.35)} +.pan.glow{box-shadow:0 6px 36px rgba(255,209,102,0.06),0 0 18px rgba(255,209,102,0.06)} + + +/* responsive */ +@media (max-width:900px){.game-area{flex-direction:column}.inventory{width:100%}.pan{width:100%}} diff --git a/games/balance_bar/index.html b/games/balance_bar/index.html new file mode 100644 index 00000000..3906fde0 --- /dev/null +++ b/games/balance_bar/index.html @@ -0,0 +1,46 @@ + + + + + + Scale Balancer Puzzle + + + + +
+

โš–๏ธ Scale Balancer Puzzle

+ +
+

Goal: 0

+

Difference: --

+
+ +
+
+
+
+
+
+
+ +
+

Objects (Drag & Drop)

+
5kg (x3)
+
10kg (x2)
+
25kg (x1)
+
+ +
+

Drag objects onto the scale. Goal: Balance the moments!

+
+ +
+ + +
+
+ + + + \ No newline at end of file diff --git a/games/balance_bar/script.js b/games/balance_bar/script.js new file mode 100644 index 00000000..26dfaff4 --- /dev/null +++ b/games/balance_bar/script.js @@ -0,0 +1,213 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. GAME CONSTANTS & ELEMENTS --- + const beam = document.getElementById('beam'); + const leftPan = document.getElementById('left-pan'); + const rightPan = document.getElementById('right-pan'); + const inventory = document.getElementById('inventory'); + const checkButton = document.getElementById('check-button'); + const resetButton = document.getElementById('reset-button'); + const goalMomentDisplay = document.getElementById('goal-moment'); + const differenceDisplay = document.getElementById('difference-display'); + const feedbackMessage = document.getElementById('feedback-message'); + + // Physical constants + const FULCRUM_DISTANCE = 1; // Distance of the pans from the fulcrum (unitless, since they are equidistant) + const BALANCE_TOLERANCE = 0.5; // Acceptable difference for a "perfect" balance + const MAX_TILT_DEGREES = 15; // Max visual tilt angle + + // Game state + let inventoryState = {}; // Tracks available objects {weight: count} + let currentMoments = { left: 0, right: 0 }; + let goalMoment = 0; // The target difference between left and right moments + + // --- 2. INITIALIZATION --- + + /** + * Initializes the game state, inventory, and goal. + */ + function initGame() { + // Set Goal (e.g., balance both sides to 0, which means net moment is 0) + goalMoment = 0; + goalMomentDisplay.textContent = goalMoment; + + // Reset display + resetScale(); + + // Populate initial inventory state from HTML + inventory.querySelectorAll('.object').forEach(obj => { + const weight = parseInt(obj.getAttribute('data-weight')); + const count = parseInt(obj.getAttribute('data-count')); + inventoryState[weight] = count; + // Update initial text display + obj.textContent = `${weight}kg (x${count})`; + obj.classList.remove('used'); + obj.draggable = true; + }); + + checkButton.disabled = false; + feedbackMessage.textContent = 'Drag objects onto the scale. Goal: Balance the moments!'; + } + + /** + * Resets all objects from the scale and updates the inventory/display. + */ + function resetScale() { + // Move placed objects back to inventory (virtually) + [...leftPan.children, ...rightPan.children].forEach(obj => { + const weight = parseInt(obj.getAttribute('data-weight')); + inventoryState[weight]++; + obj.remove(); + }); + + // Update inventory display + inventory.querySelectorAll('.object').forEach(obj => { + const weight = parseInt(obj.getAttribute('data-weight')); + obj.textContent = `${weight}kg (x${inventoryState[weight]})`; + obj.classList.remove('used'); + obj.draggable = true; + }); + + // Reset calculations and visual + currentMoments = { left: 0, right: 0 }; + updateScaleAndStatus(); + } + + // --- 3. PHYSICS & VISUAL FUNCTIONS --- + + /** + * Calculates the total moment (torque) for one side of the scale. + */ + function calculateTotalMoment(panElement) { + let totalMoment = 0; + + // For simplicity, all objects are assumed to be placed at the center of the pan (distance = FULCRUM_DISTANCE) + panElement.querySelectorAll('.object-placed').forEach(obj => { + const weight = parseInt(obj.getAttribute('data-weight')); + // Moment = Weight * Distance + totalMoment += weight * FULCRUM_DISTANCE; + }); + return totalMoment; + } + + /** + * Updates the total moment, tilt angle, and status displays. + */ + function updateScaleAndStatus() { + // 1. Calculate Moments + currentMoments.left = calculateTotalMoment(leftPan); + currentMoments.right = calculateTotalMoment(rightPan); + + // 2. Calculate Net Moment and Difference + const netMoment = currentMoments.right - currentMoments.left; // Right positive, Left negative + const difference = Math.abs(netMoment - goalMoment); + + // 3. Update Status Display + differenceDisplay.textContent = `${difference.toFixed(2)}`; + differenceDisplay.style.color = difference <= BALANCE_TOLERANCE ? '#2ecc71' : '#e74c3c'; + + // 4. Update Visual Tilt + // Calculate tilt degree based on the net moment difference + // We scale the net moment (e.g., -100 to 100) to the max tilt angle (-MAX_TILT to MAX_TILT) + const maxExpectedMoment = 25 * FULCRUM_DISTANCE * 6; // Max possible object weight * FULCRUM_DISTANCE + const tiltRatio = netMoment / maxExpectedMoment; + const tiltDegree = tiltRatio * MAX_TILT_DEGREES; + + // Apply CSS Transform + beam.style.transform = `rotate(${tiltDegree}deg)`; + + // If balanced, show success message + if (difference <= BALANCE_TOLERANCE && difference !== 0) { + feedbackMessage.textContent = "โœ… Balanced! Moments match!"; + } else if (difference !== 0) { + feedbackMessage.textContent = "Keep trying! Moment on the right is " + currentMoments.right + ", left is " + currentMoments.left + "."; + } + } + + // --- 4. DRAG AND DROP HANDLERS --- + + // Variable to hold the cloned object currently being dragged + let draggedObjectClone = null; + + inventory.addEventListener('dragstart', (e) => { + if (e.target.classList.contains('object')) { + const weight = parseInt(e.target.getAttribute('data-weight')); + + if (inventoryState[weight] > 0) { + // Create a clone for the drag operation + draggedObjectClone = e.target.cloneNode(true); + draggedObjectClone.classList.add('object-placed'); + draggedObjectClone.draggable = true; + e.dataTransfer.setData('text/weight', weight); + e.dataTransfer.setData('text/originalId', e.target.id); // Not used here, but useful for complex inventory + + e.target.classList.add('dragging'); // Apply visual drag style to original + } else { + e.preventDefault(); // Stop drag if count is zero + } + } + }); + + inventory.addEventListener('dragend', (e) => { + if (e.target.classList.contains('object')) { + e.target.classList.remove('dragging'); + } + }); + + [leftPan, rightPan].forEach(pan => { + pan.addEventListener('dragover', (e) => { + e.preventDefault(); // Required to allow dropping + pan.style.borderColor = '#4CAF50'; // Visual feedback + }); + + pan.addEventListener('dragleave', (e) => { + pan.style.borderColor = '#666'; // Reset border + }); + + pan.addEventListener('drop', (e) => { + e.preventDefault(); + pan.style.borderColor = '#666'; // Reset border + + const weight = parseInt(e.dataTransfer.getData('text/weight')); + + if (inventoryState[weight] > 0) { + // 1. Place the object clone + pan.appendChild(draggedObjectClone); + + // 2. Update inventory state + inventoryState[weight]--; + + // 3. Update original inventory display + const originalObject = inventory.querySelector(`.object[data-weight="${weight}"]`); + if (originalObject) { + originalObject.textContent = `${weight}kg (x${inventoryState[weight]})`; + if (inventoryState[weight] === 0) { + originalObject.classList.add('used'); + originalObject.draggable = false; + } + } + + // 4. Recalculate and update visual + updateScaleAndStatus(); + } + draggedObjectClone = null; + }); + }); + + // --- 5. EVENT LISTENERS --- + + checkButton.addEventListener('click', () => { + const netMoment = currentMoments.right - currentMoments.left; + const difference = Math.abs(netMoment - goalMoment); + + if (difference <= BALANCE_TOLERANCE) { + feedbackMessage.textContent = `๐Ÿ† PERFECT BALANCE! Net Moment: ${netMoment.toFixed(2)}.`; + } else { + feedbackMessage.textContent = `โŒ NOT BALANCED. Difference is too high: ${difference.toFixed(2)}.`; + } + }); + + resetButton.addEventListener('click', initGame); // Resetting the scale is the same as starting a new round in this simple implementation + + // Initial game setup + initGame(); +}); \ No newline at end of file diff --git a/games/balance_bar/style.css b/games/balance_bar/style.css new file mode 100644 index 00000000..44db41de --- /dev/null +++ b/games/balance_bar/style.css @@ -0,0 +1,163 @@ +:root { + --scale-width: 500px; + --scale-height: 200px; +} + +body { + font-family: 'Georgia', serif; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f7f7f7; + color: #333; +} + +#game-container { + background-color: white; + padding: 30px; + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 600px; + width: 90%; +} + +h1 { + color: #4CAF50; + margin-bottom: 20px; +} + +#status-area { + display: flex; + justify-content: space-around; + font-size: 1.1em; + font-weight: bold; + margin-bottom: 20px; +} + +/* --- Scale Platform --- */ +#scale-platform { + width: var(--scale-width); + height: var(--scale-height); + margin: 30px auto 40px; + position: relative; +} + +#fulcrum { + width: 10px; + height: 30px; + background-color: #333; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + z-index: 5; +} + +#beam { + width: 100%; + height: 10px; + background-color: #a0522d; /* Brown wood */ + position: absolute; + top: 50%; + /* The transform-origin is key for rotation around the fulcrum */ + transform-origin: center center; + transition: transform 0.8s ease-out; /* Smooth tilt animation */ + z-index: 10; + display: flex; + justify-content: space-between; +} + +/* --- Drop Zones (Pans) --- */ +.pan { + width: 35%; /* Width of the pan surface */ + height: 100px; + /* Simulate distance from fulcrum */ + margin-top: 50px; + background-color: rgba(200, 200, 200, 0.5); + border: 2px dashed #666; + border-radius: 5px; + display: flex; + flex-wrap: wrap; /* Allows multiple objects */ + align-content: flex-start; + justify-content: center; + gap: 5px; + padding: 5px; + box-sizing: border-box; + position: relative; /* For object placement */ + z-index: 15; +} + +#left-pan { margin-left: -50px; } /* Pull left pan closer to edge */ +#right-pan { margin-right: -50px; } /* Pull right pan closer to edge */ + +/* --- Inventory and Objects --- */ +#inventory { + padding: 15px; + border: 1px solid #ccc; + border-radius: 8px; + margin-bottom: 20px; + display: flex; + align-items: center; + justify-content: center; + gap: 15px; +} + +.object { + padding: 8px 15px; + background-color: #f1c40f; /* Yellow object */ + color: #333; + border: 2px solid #f39c12; + border-radius: 4px; + cursor: grab; + font-weight: bold; + user-select: none; + transition: opacity 0.2s; +} + +.object.dragging { + opacity: 0.5; +} + +.object.used { + cursor: default; + opacity: 0.3; +} + +/* --- Controls and Feedback --- */ +#feedback-message { + min-height: 1.5em; + margin-bottom: 20px; + font-weight: 500; +} + +#controls button { + padding: 10px 20px; + font-size: 1em; + font-weight: bold; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s; +} + +#check-button { + background-color: #4CAF50; /* Green check */ + color: white; +} + +#check-button:hover:not(:disabled) { + background-color: #388e3c; +} + +#reset-button { + background-color: #3498db; /* Blue reset */ + color: white; +} + +#reset-button:hover { + background-color: #2980b9; +} \ No newline at end of file diff --git a/games/balloon-burst/index.html b/games/balloon-burst/index.html new file mode 100644 index 00000000..60dadd34 --- /dev/null +++ b/games/balloon-burst/index.html @@ -0,0 +1,23 @@ + + + + + + Balloon Burst Game + + + +
+

Balloon Burst

+

Click on the balloons to burst them before time runs out!

+
+
Time: 30
+
Score: 0
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/games/balloon-burst/script.js b/games/balloon-burst/script.js new file mode 100644 index 00000000..abc6dc67 --- /dev/null +++ b/games/balloon-burst/script.js @@ -0,0 +1,131 @@ +// Balloon Burst Game Script +// Click to pop balloons and score points + +var canvas = document.getElementById('gameCanvas'); +var ctx = canvas.getContext('2d'); +var timerDisplay = document.getElementById('timer'); +var scoreDisplay = document.getElementById('score'); +var restartBtn = document.getElementById('restart'); +var messageDiv = document.getElementById('message'); + +var balloons = []; +var score = 0; +var timeLeft = 30; +var timerInterval; +var gameRunning = true; + +// Balloon class +function Balloon(x, y, color) { + this.x = x; + this.y = y; + this.color = color; + this.radius = 30; + this.clicked = false; +} + +// Initialize the game +function initGame() { + balloons = []; + score = 0; + timeLeft = 30; + gameRunning = true; + messageDiv.textContent = ''; + scoreDisplay.textContent = 'Score: ' + score; + spawnBalloons(); + startTimer(); + draw(); +} + +// Spawn some balloons +function spawnBalloons() { + var numBalloons = 5 + Math.floor(Math.random() * 5); // 5 to 10 balloons + for (var i = 0; i < numBalloons; i++) { + var x = Math.random() * (canvas.width - 60) + 30; + var y = Math.random() * (canvas.height - 60) + 30; + var colors = ['red', 'blue', 'green', 'yellow', 'purple', 'orange']; + var color = colors[Math.floor(Math.random() * colors.length)]; + balloons.push(new Balloon(x, y, color)); + } +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + for (var i = 0; i < balloons.length; i++) { + var balloon = balloons[i]; + if (!balloon.clicked) { + ctx.beginPath(); + ctx.arc(balloon.x, balloon.y, balloon.radius, 0, Math.PI * 2); + ctx.fillStyle = balloon.color; + ctx.fill(); + ctx.strokeStyle = '#000'; + ctx.stroke(); + // Draw string + ctx.beginPath(); + ctx.moveTo(balloon.x, balloon.y + balloon.radius); + ctx.lineTo(balloon.x, balloon.y + balloon.radius + 20); + ctx.stroke(); + } + } + if (gameRunning) { + requestAnimationFrame(draw); + } +} + +// Check if click is on a balloon +function checkClick(x, y) { + for (var i = balloons.length - 1; i >= 0; i--) { + var balloon = balloons[i]; + var dx = x - balloon.x; + var dy = y - balloon.y; + var distance = Math.sqrt(dx * dx + dy * dy); + if (distance < balloon.radius && !balloon.clicked) { + balloon.clicked = true; + score++; + scoreDisplay.textContent = 'Score: ' + score; + // Remove balloon and spawn a new one + balloons.splice(i, 1); + spawnNewBalloon(); + break; + } + } +} + +// Spawn a single new balloon +function spawnNewBalloon() { + var x = Math.random() * (canvas.width - 60) + 30; + var y = Math.random() * (canvas.height - 60) + 30; + var colors = ['red', 'blue', 'green', 'yellow', 'purple', 'orange']; + var color = colors[Math.floor(Math.random() * colors.length)]; + balloons.push(new Balloon(x, y, color)); +} + +// Handle canvas click +canvas.addEventListener('click', function(event) { + if (!gameRunning) return; + var rect = canvas.getBoundingClientRect(); + var x = event.clientX - rect.left; + var y = event.clientY - rect.top; + checkClick(x, y); +}); + +// Start the timer +function startTimer() { + clearInterval(timerInterval); + timerInterval = setInterval(function() { + timeLeft--; + timerDisplay.textContent = 'Time: ' + timeLeft; + if (timeLeft <= 0) { + clearInterval(timerInterval); + gameRunning = false; + messageDiv.textContent = 'Time\'s up! Final Score: ' + score; + messageDiv.style.color = 'blue'; + } + }, 1000); +} + +// Restart button +restartBtn.addEventListener('click', initGame); + +// Start the game +initGame(); \ No newline at end of file diff --git a/games/balloon-burst/style.css b/games/balloon-burst/style.css new file mode 100644 index 00000000..ae63fae1 --- /dev/null +++ b/games/balloon-burst/style.css @@ -0,0 +1,53 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #e0f7fa; +} + +.container { + text-align: center; +} + +h1 { + color: #00796b; +} + +.game-info { + margin-bottom: 20px; + display: flex; + justify-content: center; + gap: 20px; +} + +#timer, #score { + font-size: 24px; + font-weight: bold; +} + +#restart { + padding: 10px 20px; + font-size: 16px; + background-color: #ff5722; + color: white; + border: none; + cursor: pointer; +} + +#restart:hover { + background-color: #d84315; +} + +canvas { + border: 2px solid #00796b; + background-color: #b2ebf2; +} + +#message { + margin-top: 20px; + font-size: 18px; + font-weight: bold; +} \ No newline at end of file diff --git a/games/balloon-jumper/index.html b/games/balloon-jumper/index.html new file mode 100644 index 00000000..b18504b6 --- /dev/null +++ b/games/balloon-jumper/index.html @@ -0,0 +1,64 @@ + + + + + + Balloon Jumper โ€” Mini JS Games Hub + + + + +
+
+
+
+ ๐ŸŽˆ + Balloon Jumper +
+
Arcade โ€ข Reflex โ€ข HTML/CSS/JS
+
+ +
+
+
Score 0
+
High 0
+
+ +
+ + + + Open Hub +
+
+
+ +
+ + + + +
+
Move: โ† โ†’ or Touch
+
Jump: Auto on landing
+
+
+ +
+

Made with โค๏ธ โ€” press Space to pause/resume. Touch to move on mobile.

+
+
+ + + + diff --git a/games/balloon-jumper/script.js b/games/balloon-jumper/script.js new file mode 100644 index 00000000..bcfca6e3 --- /dev/null +++ b/games/balloon-jumper/script.js @@ -0,0 +1,480 @@ +// Balloon Jumper โ€” advanced single-file core +// Place at games/balloon-jumper/script.js + +(() => { + const canvas = document.getElementById('gameCanvas'); + const ctx = canvas.getContext('2d', { alpha: true }); + + // Resize canvas for device pixel ratio + function resize() { + const ratio = Math.max(window.devicePixelRatio || 1, 1); + canvas.width = Math.floor(canvas.clientWidth * ratio); + canvas.height = Math.floor(canvas.clientHeight * ratio); + ctx.setTransform(ratio, 0, 0, ratio, 0, 0); + } + window.addEventListener('resize', resize); + resize(); + + // UI elements + const scoreEl = document.getElementById('score'); + const highEl = document.getElementById('highscore'); + const overlay = document.getElementById('overlay'); + const overlayTitle = document.getElementById('overlayTitle'); + const overlayMsg = document.getElementById('overlayMsg'); + const resumeBtn = document.getElementById('resumeBtn'); + const overlayRestart = document.getElementById('overlayRestart'); + const pauseBtn = document.getElementById('pauseBtn'); + const restartBtn = document.getElementById('restartBtn'); + const resumeKey = 'BalloonJumperHigh'; + + // Sound toggles and webaudio + let audioEnabled = true; + const soundToggle = document.getElementById('soundToggle'); + soundToggle.addEventListener('click', () => { + audioEnabled = !audioEnabled; + soundToggle.textContent = audioEnabled ? '๐Ÿ”Š' : '๐Ÿ”ˆ'; + }); + + // Simple WebAudio generator for jump/pop/score (no external files needed) + const AudioCtx = window.AudioContext || window.webkitAudioContext; + let audioCtx = null; + function ensureAudio() { + if (!audioCtx) audioCtx = new AudioCtx(); + } + function beep(freq = 440, time = 0.06, type = 'sine', gain = 0.12) { + if (!audioEnabled) return; + ensureAudio(); + const o = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + o.type = type; + o.frequency.value = freq; + g.gain.value = gain; + o.connect(g); + g.connect(audioCtx.destination); + o.start(); + g.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + time); + o.stop(audioCtx.currentTime + time + 0.02); + } + function popSound() { beep(340, 0.07, 'triangle', 0.14); } + function jumpSound() { beep(720, 0.08, 'sawtooth', 0.08); } + function scoreSound() { beep(980, 0.10, 'sine', 0.09); } + + // Optionally, you can replace or supplement these procedural sounds with external links: + // e.g. const jumpURL = 'https://actions.google.com/sounds/v1/cartoon/wood_plank_flicks.ogg'; + // and use new Audio(jumpURL).play(); But procedural sounds avoid hotlink issues. + + // Game constants + const GRAVITY = 1600; // px/s^2 + const BALLON_MIN_SPEED = -40; + const PLAYER_SPEED = 420; + const JUMP_VELOCITY = -680; + const BALLOON_MIN_GAP = 100; + const BALLOON_MAX_GAP = 220; + const BALLOON_MIN_RADIUS = 28; + const BALLOON_MAX_RADIUS = 46; + const SCROLL_THRESHOLD = 300; + + // Game state + let last = performance.now(); + let dt = 0; + let running = true; + let paused = false; + let score = 0; + let highScore = parseInt(localStorage.getItem(resumeKey) || 0, 10); + highEl.textContent = highScore; + + // Player + const player = { + x: 180, + y: 300, + r: 22, + vx: 0, + vy: 0, + color: '#fff', + spinning: 0 + }; + + // Input + const keys = { left:false, right:false }; + window.addEventListener('keydown', (e) => { + if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = true; + if (e.key === 'ArrowRight' || e.key === 'd') keys.right = true; + if (e.key === ' ') togglePause(); + if (e.key === 'r') resetGame(); + // unlock audio on user gesture + if (!audioCtx && ['ArrowLeft','ArrowRight',' ','a','d'].includes(e.key)) ensureAudio(); + }); + window.addEventListener('keyup', (e) => { + if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = false; + if (e.key === 'ArrowRight' || e.key === 'd') keys.right = false; + }); + + // Touch controls + let touchX = null; + canvas.addEventListener('touchstart', (e) => { + const t = e.touches[0]; + touchX = t.clientX; + // ensure audio unlocked + if (!audioCtx) ensureAudio(); + }); + canvas.addEventListener('touchmove', (e) => { + const t = e.touches[0]; + touchX = t.clientX; + }); + canvas.addEventListener('touchend', () => { touchX = null; }); + + // Balloons store + let balloons = []; + + // Helpers for random and colors + function rand(min,max){return Math.random()*(max-min)+min} + function rndInt(min,max){return Math.floor(rand(min,max+1))} + function sat(n,min,max){return Math.max(min,Math.min(max,n))} + + // Balloon generator + function spawnBalloon(x,y,r,velocity=0, type='normal'){ + balloons.push({x,y,r,vx:rand(-10,10),vy:velocity, color:hslColor(), popped:false, type}); + } + + function hslColor(){ + const hue = Math.floor(rand(0,360)); + return `hsl(${hue} 86% 64%)`; + } + + // Initial balloon field + function populateInitial(){ + balloons = []; + const baseY = canvas.height/ (window.devicePixelRatio||1) - 60; + let y = baseY; + let x = 220; + for(let i=0;i<8;i++){ + const r = rndInt(BALLOON_MIN_RADIUS, BALLOON_MAX_RADIUS); + spawnBalloon(rand(80, canvas.width-120), y - i*rand(140,200), r); + } + } + + // Reset / Start + function resetGame(){ + score = 0; + scoreEl.textContent = score; + player.x = canvas.clientWidth*0.2; + player.y = canvas.clientHeight*0.6; + player.vx = 0; player.vy = 0; + populateInitial(); + running = true; + paused = false; + overlay.classList.add('hidden'); + overlayTitle.textContent = 'Paused'; + overlayMsg.textContent = ''; + if (audioEnabled) jumpSound(); + } + + // Pause toggle + function togglePause(){ + paused = !paused; + if (paused){ + overlay.classList.remove('hidden'); + overlayTitle.textContent = 'Paused'; + overlayMsg.textContent = 'Press Resume or Space to continue.'; + } else { + overlay.classList.add('hidden'); + last = performance.now(); + } + } + + pauseBtn.addEventListener('click', () => togglePause()); + resumeBtn.addEventListener('click', () => togglePause()); + restartBtn.addEventListener('click', () => resetGame()); + overlayRestart.addEventListener('click', () => resetGame()); + + // Collision detection: circle to circle + function circleCollide(ax,ay,ar,bx,by,br){ + const dx = ax-bx, dy=ay-by; + return (dx*dx+dy*dy) <= (ar+br)*(ar+br); + } + + // Main update loop + function update(now){ + dt = Math.min((now - last)/1000, 0.033); // clamp dt + last = now; + if (!running || paused) { + requestAnimationFrame(update); + return; + } + + // Input handling + const canvasWidth = canvas.clientWidth; + // touch control mapping + if (touchX !== null){ + const rect = canvas.getBoundingClientRect(); + const cx = touchX - rect.left; + if (cx < rect.width*0.48) { + keys.left = true; keys.right = false; + } else { + keys.right = true; keys.left = false; + } + } + + if (keys.left) player.vx = -PLAYER_SPEED; + else if (keys.right) player.vx = PLAYER_SPEED; + else player.vx = 0; + + // Integrate physics + player.vy += GRAVITY * dt; + player.x += player.vx * dt; + player.y += player.vy * dt; + + // Boundaries left/right wrap + if (player.x < -60) player.x = canvasWidth + 60; + if (player.x > canvasWidth + 60) player.x = -60; + + // Scroll world upward if player goes above threshold + const threshold = SCROLL_THRESHOLD; + if (player.y < threshold) { + const dy = threshold - player.y; + player.y = threshold; + // move balloons downwards so it *appears* player ascended + for (let b of balloons) b.y += dy; + score += Math.floor(Math.abs(dy) * 0.02); + if (score % 50 === 0) { scoreSound(); } + scoreEl.textContent = score; + if (score > highScore) { highScore = score; localStorage.setItem(resumeKey, String(highScore)); highEl.textContent = highScore; } + } + + // Spawn balloons if needed at top + while (balloons.length < 10) { + const topMost = Math.min(...balloons.map(b=>b.y)); + const gap = rand(BALLOON_MIN_GAP, BALLOON_MAX_GAP); + const r = rndInt(BALLOON_MIN_RADIUS, BALLOON_MAX_RADIUS); + const x = rand(60, canvasWidth-60); + const y = topMost - gap - r; + spawnBalloon(x,y,r); + } + + // Update balloons + for (let b of balloons) { + // slight bobbing movement and horizontal drift + b.vy += rand(-10, 10) * dt; + b.x += Math.sin((now + b.x)*0.001)*10*dt + b.vx*dt; + b.y += b.vy * dt * 0.3; // gentle drift + + // random pop chance for fragile balloons + if (!b.popped && Math.random() < 0.0006) { + b.popped = true; + popSound(); + } + } + + // Collision: if landing on balloon from above with small downward speed + for (let i = 0; i < balloons.length; i++) { + const b = balloons[i]; + if (b.popped) continue; + if (circleCollide(player.x, player.y + player.r*0.5, player.r, b.x, b.y - b.r*0.2, b.r*0.9)) { + // ensure that player is falling (vy > 0) or close enough + if (player.vy > -120) { + // bounce + player.vy = JUMP_VELOCITY * (1 - Math.min(0.4, Math.abs(b.r - BALLOON_MAX_RADIUS)/120)); + player.y = b.y - b.r - player.r*0.2; + jumpSound(); + // small chance balloon pops on landing + if (Math.random() < 0.08) { b.popped = true; popSound(); } + // score + score += 10; + scoreEl.textContent = score; + if (score > highScore) { highScore = score; localStorage.setItem(resumeKey, String(highScore)); highEl.textContent = highScore; } + } + } + } + + // Remove popped far-off balloons, keep array small + balloons = balloons.filter(b => { + // if popped, create confetti and remove after going below screen a little + return !(b.popped && b.y > canvas.clientHeight + 120); + }); + + // Game over if player falls too far below + if (player.y > canvas.clientHeight + 100) { + running = false; + overlay.classList.remove('hidden'); + overlayTitle.textContent = 'Game Over'; + overlayMsg.textContent = `Score: ${score} โ€ข High: ${highScore}`; + savePlay(); // track play in hub (calls localStorage) + // subtle pop + popSound(); + } + + draw(); + requestAnimationFrame(update); + } + + // Drawing functions + function drawBackground() { + const w = canvas.clientWidth, h = canvas.clientHeight; + // gradient sky + const g = ctx.createLinearGradient(0,0,0,h); + g.addColorStop(0, '#061226'); + g.addColorStop(1, '#021022'); + ctx.fillStyle = g; + ctx.fillRect(0,0,w,h); + // soft glow lights (parallax) + for (let i=0;i<3;i++){ + ctx.beginPath(); + const gx = (i+1)*w*0.22 + Math.sin(perf*0.0004+i)*40; + const gy = h*0.08 + Math.cos(perf*0.0003+i)*40; + const rg = Math.min(w*0.35, 420); + const grd = ctx.createRadialGradient(gx,gy,20,gx,gy,rg); + grd.addColorStop(0, 'rgba(255,180,120,0.06)'); + grd.addColorStop(1, 'rgba(255,180,120,0)'); + ctx.fillStyle = grd; + ctx.fillRect(0,0,w,h); + } + } + + let perf = 0; + function draw() { + perf += 16; + const w = canvas.clientWidth, h = canvas.clientHeight; + // clear + ctx.clearRect(0,0,w,h); + drawBackground(); + + // draw balloons sorted by y (farthest first) + const sorted = balloons.slice().sort((a,b)=>a.y-b.y); + for (let b of sorted) drawBalloon(b); + + // draw player with glow + drawPlayer(); + + // UI glow & particles (small) + // subtle floating particles + for (let i=0;i<6;i++){ + ctx.beginPath(); + const px = (i*73 + perf*0.05) % w; + const py = (h*0.3 + Math.sin((perf*0.002 + i)*1.2)*20); + ctx.fillStyle = `rgba(255,255,255,${0.02 + i*0.01})`; + ctx.fillRect(px,py,2,2); + } + } + + function drawBalloon(b) { + const {x,y,r,color,popped} = b; + const ctxAlpha = ctx.globalAlpha; + const stemLen = r*0.6; + // glow + ctx.beginPath(); + const grd = ctx.createRadialGradient(x, y - r*0.3, 0, x, y, r*2.2); + grd.addColorStop(0, color.replace('hsl','hsla').replace(')', ',0.25)')); + grd.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = grd; + ctx.fillRect(x-r*2.6, y-r*2.6, r*5.2, r*5.2); + + // balloon shadow (simple ellipse) + ctx.beginPath(); + ctx.ellipse(x+8, y + r*0.8, r*0.9, r*0.36, 0, 0, Math.PI*2); + ctx.fillStyle = 'rgba(3,6,10,0.25)'; + ctx.fill(); + + // balloon body (rounded) + ctx.beginPath(); + ctx.ellipse(x, y, r, r*1.03, 0, 0, Math.PI*2); + ctx.fillStyle = color; + ctx.fill(); + + // highlight + ctx.beginPath(); + ctx.ellipse(x - r*0.25, y - r*0.45, r*0.28, r*0.18, -0.25, 0, Math.PI*2); + ctx.fillStyle = 'rgba(255,255,255,0.22)'; + ctx.fill(); + + // string + ctx.beginPath(); + ctx.moveTo(x, y + r*0.9); + ctx.quadraticCurveTo(x + 6, y + r*1.3, x - 6, y + r*1.8); + ctx.strokeStyle = 'rgba(255,255,255,0.12)'; + ctx.lineWidth = 2; + ctx.stroke(); + + // popped effect + if (popped) { + ctx.beginPath(); + ctx.moveTo(x - r*0.6, y - r*0.4); + for (let i = 0; i < 12; i++) { + const ang = Math.PI*2 * i / 12; + const rx = x + Math.cos(ang) * r * rand(0.8,1.6); + const ry = y + Math.sin(ang) * r * rand(0.8,1.6); + ctx.lineTo(rx, ry); + } + ctx.closePath(); + ctx.fillStyle = `rgba(255,255,255,0.12)`; + ctx.fill(); + } + + ctx.globalAlpha = ctxAlpha; + } + + function drawPlayer() { + const p = player; + // shadow + ctx.beginPath(); + ctx.ellipse(p.x + 8, p.y + p.r*1.6, p.r*1.2, p.r*0.6, 0, 0, Math.PI*2); + ctx.fillStyle = 'rgba(1,4,10,0.5)'; + ctx.fill(); + + // glow ring + ctx.beginPath(); + var grad = ctx.createRadialGradient(p.x, p.y, p.r*0.2, p.x, p.y, p.r*2.2); + grad.addColorStop(0, 'rgba(255,240,200,0.12)'); + grad.addColorStop(1, 'rgba(255,240,200,0)'); + ctx.fillStyle = grad; + ctx.fillRect(p.x - p.r*2.5, p.y - p.r*2.5, p.r*5, p.r*5); + + // player circle body + ctx.beginPath(); + ctx.arc(p.x, p.y, p.r, 0, Math.PI*2); + ctx.fillStyle = '#fff'; + ctx.fill(); + + // face (simple) + ctx.beginPath(); + ctx.arc(p.x - p.r*0.35, p.y - p.r*0.14, p.r*0.12, 0, Math.PI*2); + ctx.fillStyle = '#111827'; ctx.fill(); + ctx.beginPath(); + ctx.arc(p.x + p.r*0.12, p.y - p.r*0.14, p.r*0.12, 0, Math.PI*2); + ctx.fillStyle = '#111827'; ctx.fill(); + // smile + ctx.beginPath(); + ctx.arc(p.x, p.y + 2, p.r*0.28, 0, Math.PI); + ctx.strokeStyle = '#111827'; + ctx.lineWidth = 2; ctx.stroke(); + } + + // Simple play tracking for Hub (localStorage) โ€” matches their 'gamePlays' structure + function savePlay() { + try { + const key = 'gamePlays'; + const data = JSON.parse(localStorage.getItem(key) || '{}'); + const name = 'Balloon Jumper'; + if (!data[name]) data[name] = { plays: 0, success: 0 }; + data[name].plays += 1; + if (!running) data[name].success += 1; + localStorage.setItem(key, JSON.stringify(data)); + } catch (e) { /* ignore */ } + } + + // start + populateInitial(); + last = performance.now(); + requestAnimationFrame(update); + + // Starter instructions + document.addEventListener('visibilitychange', () => { + if (document.hidden && !paused) { paused = true; overlay.classList.remove('hidden'); overlayTitle.textContent = 'Paused'; } + }); + + // expose reset for UI + window.resetBalloonJumper = resetGame; + + // initial score display + scoreEl.textContent = score; + highEl.textContent = highScore; +})(); diff --git a/games/balloon-jumper/style.css b/games/balloon-jumper/style.css new file mode 100644 index 00000000..95314235 --- /dev/null +++ b/games/balloon-jumper/style.css @@ -0,0 +1,89 @@ +:root{ + --bg:#0b1020; + --card:#0f1724; + --accent:#ff6b6b; + --accent-2:#ffd166; + --glow: 0 8px 40px rgba(255,107,107,0.18), 0 2px 6px rgba(0,0,0,0.6); + --glass: rgba(255,255,255,0.03); + --muted: #9aa3b2; +} + +*{box-sizing:border-box} +html,body{height:100%;margin:0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial} +body{ + background: + radial-gradient(1200px 500px at 10% 10%, rgba(120,94,255,0.07), transparent 10%), + radial-gradient(900px 400px at 90% 90%, rgba(255,165,90,0.04), transparent 10%), + var(--bg); + color:#e6eef8; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + display:flex; + align-items:center; + justify-content:center; + padding:20px; +} + +.game-shell{ + width:100%; + max-width:1100px; + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border-radius:14px; + box-shadow: 0 10px 40px rgba(2,6,23,0.7); + overflow:hidden; + border:1px solid rgba(255,255,255,0.03); +} + +/* Header */ +.game-header{ + display:flex; + justify-content:space-between; + align-items:center; + padding:16px 20px; + gap:12px; + background:linear-gradient(180deg, rgba(255,255,255,0.01), transparent); +} +.game-title{display:flex;align-items:center;gap:10px} +.icon{font-size:26px} +.title-text{font-weight:600;font-size:18px} +.meta{color:var(--muted);font-size:13px;margin-left:6px} + +.scoreboard{display:flex;gap:12px;align-items:center} +.scoreboard div{font-size:13px;color:var(--muted)} +.scoreboard strong{font-size:16px;color:#fff;display:inline-block;min-width:36px;text-align:center} + +/* Controls */ +.controls{display:flex;gap:8px;align-items:center} +.control-btn{ + background:var(--glass); + border:1px solid rgba(255,255,255,0.03); + color:#fff;padding:8px 10px;border-radius:8px;cursor:pointer;font-size:14px; + box-shadow: var(--glow); +} +.open-new{padding:8px 12px;border-radius:8px;background:transparent;color:var(--muted);text-decoration:none;border:1px dashed rgba(255,255,255,0.03)} + +/* Game area */ +.game-area{position:relative;background:linear-gradient(180deg,#071025 0%, #071226 60%);height:600px;display:flex;align-items:center;justify-content:center} +canvas{width:100%;height:100%;display:block;background: + linear-gradient(180deg,rgba(255,255,255,0.015),transparent 20%);} + +/* Overlay */ +.overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:linear-gradient(180deg, rgba(2,6,23,0.7), rgba(2,6,23,0.6))} +.overlay.hidden{display:none} +.overlay-card{background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));padding:26px;border-radius:12px;text-align:center;border:1px solid rgba(255,255,255,0.03);box-shadow: 0 18px 60px rgba(2,6,23,0.7)} +.overlay-card h2{margin:0 0 8px;font-size:22px} +.overlay-card p{color:var(--muted);margin:0 0 18px} +.overlay-actions{display:flex;gap:10px;justify-content:center} +.cta{background:linear-gradient(90deg,var(--accent),var(--accent-2));border:none;color:#08101a;padding:8px 16px;border-radius:10px;font-weight:600;cursor:pointer} +.ghost{background:transparent;border:1px solid rgba(255,255,255,0.06);color:#fff;padding:8px 12px;border-radius:10px} + +/* Footer & help */ +.game-footer{padding:14px 20px;background:transparent;color:var(--muted);font-size:13px} +.help{position:absolute;left:18px;bottom:18px;color:var(--muted);font-size:13px;background:rgba(0,0,0,0.25);padding:8px 10px;border-radius:10px;backdrop-filter: blur(4px)} + +/* Small screens */ +@media (max-width:720px){ + .game-shell{margin:8px} + .game-area{height:72vh} + .controls{gap:6px} +} diff --git a/games/bar_graph/index.html b/games/bar_graph/index.html new file mode 100644 index 00000000..a2b37345 --- /dev/null +++ b/games/bar_graph/index.html @@ -0,0 +1,48 @@ + + + + + + Data Chart Match Puzzle + + + + +
+

๐Ÿ“Š Data Chart Match

+ +
+ Score: 0 +
+ +
+
+

Target Chart:

+ +
+ +
+

Your Chart:

+ +
+
+ +
+

Input Data:

+
+
+ +
+ +
+

Input the correct values to make your chart match the target!

+
+ +
+ +
+
+ + + + \ No newline at end of file diff --git a/games/bar_graph/script.js b/games/bar_graph/script.js new file mode 100644 index 00000000..6282cf3d --- /dev/null +++ b/games/bar_graph/script.js @@ -0,0 +1,152 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. GAME CONSTANTS & ELEMENTS --- + const NUM_BARS = 4; + const MAX_VALUE = 100; // Max data value for scaling + const CHART_WIDTH = 300; + const CHART_HEIGHT = 200; + const BAR_GAP = 10; + const BAR_WIDTH = (CHART_WIDTH - (NUM_BARS + 1) * BAR_GAP) / NUM_BARS; + + const targetSvg = document.getElementById('target-svg'); + const playerSvg = document.getElementById('player-svg'); + const dataInputs = document.getElementById('data-inputs'); + const checkButton = document.getElementById('check-button'); + const feedbackMessage = document.getElementById('feedback-message'); + const scoreSpan = document.getElementById('score'); + const newPuzzleButton = document.getElementById('new-puzzle-button'); + + let targetData = []; // The secret solution + let score = 0; + + // --- 2. CORE FUNCTIONS --- + + /** + * Converts a data value to the correct pixel height for the SVG chart. + */ + function scaleValueToHeight(value) { + // Simple linear scaling: (value / MAX_VALUE) * CHART_HEIGHT + return (value / MAX_VALUE) * CHART_HEIGHT; + } + + /** + * Renders a bar chart into the specified SVG element. + * @param {SVGElement} svg - The SVG container to draw into. + * @param {Array} data - The array of data values. + * @param {string} className - The CSS class for the bars. + */ + function renderChart(svg, data, className) { + svg.innerHTML = ''; // Clear previous bars + + data.forEach((value, index) => { + const barHeight = scaleValueToHeight(value); + const xPos = (index + 1) * BAR_GAP + index * BAR_WIDTH; + + const rect = document.createElementNS("http://www.w3.org/2000/svg", 'rect'); + rect.setAttribute('x', xPos); + rect.setAttribute('y', 0); // SVG bars start at y=0 (bottom of the inverted chart) + rect.setAttribute('width', BAR_WIDTH); + rect.setAttribute('height', barHeight); + rect.classList.add(className); + rect.setAttribute('data-value', value); // Store the raw value + + svg.appendChild(rect); + }); + } + + /** + * Generates a new random puzzle. + */ + function generateNewPuzzle() { + // 1. Generate random, visible target data (1 to 100) + targetData = []; + for (let i = 0; i < NUM_BARS; i++) { + // Generate a number between 10 and 95 + targetData.push(Math.floor(Math.random() * 86) + 10); + } + + // 2. Render the static Target Chart + renderChart(targetSvg, targetData, 'target-bar'); + + // 3. Reset Player Input and Chart + dataInputs.innerHTML = ''; + renderChart(playerSvg, Array(NUM_BARS).fill(0), 'player-bar'); // Start with zero height bars + + // 4. Generate Input Fields + for (let i = 0; i < NUM_BARS; i++) { + const group = document.createElement('div'); + group.classList.add('input-group'); + group.innerHTML = ` + + + `; + dataInputs.appendChild(group); + } + + // 5. Attach Input Listeners (Input event is key for real-time update) + const inputs = dataInputs.querySelectorAll('input'); + inputs.forEach(input => { + input.addEventListener('input', updatePlayerChart); + input.addEventListener('change', updatePlayerChart); // For blur/completion + }); + + checkButton.disabled = true; + feedbackMessage.textContent = 'A new target is set! Input your values to match it.'; + } + + /** + * Updates the player's chart in real-time based on their input. + */ + function updatePlayerChart() { + const playerInputData = []; + let allValid = true; + + dataInputs.querySelectorAll('input').forEach(input => { + let value = parseInt(input.value); + + if (isNaN(value) || value < 0 || value > MAX_VALUE) { + value = 0; // Treat invalid/empty input as zero for rendering + allValid = false; + } else { + input.style.borderColor = '#2ecc71'; // Green border for valid + } + playerInputData.push(value); + }); + + // Re-render the player's chart with the new data + renderChart(playerSvg, playerInputData, 'player-bar'); + + // Check if all fields are valid before enabling the check button + checkButton.disabled = !allValid; + } + + /** + * Checks the player's input against the target data for a win condition. + */ + function checkMatch() { + const playerInputData = []; + dataInputs.querySelectorAll('input').forEach(input => { + playerInputData.push(parseInt(input.value)); + }); + + const isMatch = playerInputData.every((value, index) => value === targetData[index]); + + if (isMatch) { + score++; + scoreSpan.textContent = score; + feedbackMessage.innerHTML = '๐ŸŽ‰ **PERFECT MATCH!** You got it!'; + feedbackMessage.style.color = '#2ecc71'; + checkButton.disabled = true; // Disable check until new puzzle starts + } else { + feedbackMessage.innerHTML = 'โŒ **NO MATCH.** Keep adjusting the values.'; + feedbackMessage.style.color = '#e74c3c'; + } + } + + // --- 3. EVENT LISTENERS --- + + checkButton.addEventListener('click', checkMatch); + newPuzzleButton.addEventListener('click', generateNewPuzzle); + + // Initial game start + generateNewPuzzle(); +}); \ No newline at end of file diff --git a/games/bar_graph/style.css b/games/bar_graph/style.css new file mode 100644 index 00000000..fa5e1813 --- /dev/null +++ b/games/bar_graph/style.css @@ -0,0 +1,125 @@ +:root { + --chart-width: 300; + --chart-height: 200; + --max-value: 100; /* Max height value for bar scaling */ +} + +body { + font-family: 'Helvetica Neue', Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #ecf0f1; + color: #34495e; +} + +#game-container { + background-color: white; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 700px; + width: 90%; +} + +h1 { + color: #2c3e50; + margin-bottom: 20px; +} + +#score-area { + font-size: 1.1em; + font-weight: bold; + margin-bottom: 25px; + color: #2ecc71; +} + +/* --- Puzzle Area and Charts --- */ +#puzzle-area { + display: flex; + justify-content: space-around; + gap: 20px; + margin-bottom: 30px; +} + +.chart-box { + text-align: center; +} + +svg { + border: 1px solid #95a5a6; + background-color: #f7f9fb; + transform: scaleY(-1); /* Invert Y-axis for standard bar graph visualization */ +} + +.target-bar { + fill: #e74c3c; /* Red target bars */ +} + +.player-bar { + fill: #3498db; /* Blue player bars */ + transition: all 0.5s ease-out; +} + +/* --- Input Controls --- */ +#input-controls h2 { + color: #34495e; + margin-bottom: 15px; +} + +#data-inputs { + display: flex; + justify-content: center; + gap: 20px; + margin-bottom: 20px; +} + +.input-group { + display: flex; + flex-direction: column; + align-items: center; +} + +.input-group label { + font-size: 0.9em; + margin-bottom: 5px; +} + +.input-group input { + width: 60px; + padding: 8px; + font-size: 1em; + border: 2px solid #ccc; + border-radius: 4px; + text-align: center; +} + +#check-button { + padding: 10px 25px; + font-size: 1.1em; + background-color: #2ecc71; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s; +} + +#check-button:hover:not(:disabled) { + background-color: #27ae60; +} + +#check-button:disabled { + background-color: #bdc3c7; + cursor: not-allowed; +} + +/* --- Feedback --- */ +#feedback-message { + min-height: 1.5em; + margin-top: 20px; + font-weight: bold; +} \ No newline at end of file diff --git a/games/bingo-caller/index.html b/games/bingo-caller/index.html new file mode 100644 index 00000000..a94b875b --- /dev/null +++ b/games/bingo-caller/index.html @@ -0,0 +1,129 @@ + + + + + + Bingo Caller + + + +
+

๐ŸŽฏ Bingo Caller

+

Mark your card and get BINGO!

+ +
+
Called: 0
+
Last: -
+
Status: Ready to Play
+
+ +
+
+
+
B
+
I
+
N
+
G
+
O
+
+
+ +
1
+
2
+
3
+
4
+
5
+ + +
16
+
17
+
18
+
19
+
20
+ + +
FREE
+
32
+
33
+
34
+
35
+ + +
46
+
47
+
48
+
49
+
50
+ + +
61
+
62
+
63
+
64
+
65
+
+
+ +
+
+

Called Numbers

+
+
+ +
+ + + + +
+ +
+ + +
+
+
+ +
+ +
+

Winning Patterns:

+
+
+ Horizontal: Complete any row +
+
+ Vertical: Complete any column +
+
+ Diagonal: Complete either diagonal +
+
+ Four Corners: Mark all four corners +
+
+ Full House: Complete entire card +
+
+
+ +
+

How to Play:

+
    +
  • Numbers will be called randomly from 1-75
  • +
  • Click on numbers on your card to mark them when called
  • +
  • Or enable auto-mark to automatically mark called numbers
  • +
  • Get 5 in a row (horizontal, vertical, or diagonal) to win
  • +
  • The center FREE space is already marked
  • +
  • Try for different winning patterns for variety
  • +
+
+
+ + + + \ No newline at end of file diff --git a/games/bingo-caller/script.js b/games/bingo-caller/script.js new file mode 100644 index 00000000..4a79fe53 --- /dev/null +++ b/games/bingo-caller/script.js @@ -0,0 +1,367 @@ +// Bingo Caller Game +// Classic bingo with number calling and card marking + +// DOM elements +const calledCountEl = document.getElementById('current-called'); +const lastNumberEl = document.getElementById('last-called'); +const statusEl = document.getElementById('status'); +const messageEl = document.getElementById('message'); +const calledListEl = document.getElementById('called-list'); +const callNumberBtn = document.getElementById('call-number-btn'); +const autoPlayBtn = document.getElementById('auto-play-btn'); +const newGameBtn = document.getElementById('new-game-btn'); +const checkBingoBtn = document.getElementById('check-bingo-btn'); +const autoMarkCheckbox = document.getElementById('auto-mark'); +const soundEnabledCheckbox = document.getElementById('sound-enabled'); + +// Game variables +let availableNumbers = []; +let calledNumbers = []; +let lastCalled = null; +let isAutoPlaying = false; +let autoPlayInterval = null; +let gameActive = false; + +// Initialize game +function initGame() { + resetGame(); + setupEventListeners(); +} + +// Reset the game +function resetGame() { + // Generate all numbers 1-75 + availableNumbers = []; + for (let i = 1; i <= 75; i++) { + availableNumbers.push(i); + } + + calledNumbers = []; + lastCalled = null; + isAutoPlaying = false; + + // Clear called numbers display + calledListEl.innerHTML = ''; + + // Reset card markings + document.querySelectorAll('.number-cell.marked').forEach(cell => { + if (!cell.classList.contains('free-space')) { + cell.classList.remove('marked'); + } + }); + + // Mark free space + document.querySelector('.free-space').classList.add('marked'); + + // Update display + updateDisplay(); + + gameActive = true; + statusEl.textContent = 'Ready to Play'; + messageEl.textContent = 'Click "Call Next Number" to start!'; +} + +// Setup event listeners +function setupEventListeners() { + // Number cells + document.querySelectorAll('.number-cell').forEach(cell => { + if (!cell.classList.contains('free-space')) { + cell.addEventListener('click', () => markCell(cell)); + } + }); + + // Control buttons + callNumberBtn.addEventListener('click', callNextNumber); + autoPlayBtn.addEventListener('click', toggleAutoPlay); + newGameBtn.addEventListener('click', resetGame); + checkBingoBtn.addEventListener('click', checkForBingo); + + // Options + autoMarkCheckbox.addEventListener('change', () => { + if (autoMarkCheckbox.checked && lastCalled) { + autoMarkNumber(lastCalled); + } + }); +} + +// Call the next random number +function callNextNumber() { + if (!gameActive || availableNumbers.length === 0) { + if (availableNumbers.length === 0) { + messageEl.textContent = 'All numbers have been called!'; + statusEl.textContent = 'Game Complete'; + gameActive = false; + } + return; + } + + // Get random number + const randomIndex = Math.floor(Math.random() * availableNumbers.length); + const number = availableNumbers.splice(randomIndex, 1)[0]; + + calledNumbers.push(number); + lastCalled = number; + + // Add to called list display + addCalledNumber(number); + + // Auto-mark if enabled + if (autoMarkCheckbox.checked) { + autoMarkNumber(number); + } + + // Update display + updateDisplay(); + + // Play sound if enabled + if (soundEnabledCheckbox.checked) { + playSound(); + } + + // Check for auto bingo + setTimeout(() => { + if (checkForBingo(true)) { + if (isAutoPlaying) { + toggleAutoPlay(); + } + } + }, 500); +} + +// Add number to called list display +function addCalledNumber(number) { + const numberDiv = document.createElement('div'); + numberDiv.className = 'called-number'; + numberDiv.textContent = number; + calledListEl.appendChild(numberDiv); + + // Scroll to bottom + calledListEl.scrollTop = calledListEl.scrollHeight; +} + +// Mark a cell on the card +function markCell(cell) { + if (!gameActive) return; + + const number = parseInt(cell.dataset.number); + if (calledNumbers.includes(number)) { + cell.classList.toggle('marked'); + } else { + messageEl.textContent = 'That number hasn\'t been called yet!'; + setTimeout(() => messageEl.textContent = '', 2000); + } +} + +// Auto-mark number on card +function autoMarkNumber(number) { + const cell = document.querySelector(`[data-number="${number}"]`); + if (cell && !cell.classList.contains('marked')) { + cell.classList.add('marked'); + } +} + +// Toggle auto-play +function toggleAutoPlay() { + if (isAutoPlaying) { + stopAutoPlay(); + } else { + startAutoPlay(); + } +} + +// Start auto-play +function startAutoPlay() { + if (!gameActive || availableNumbers.length === 0) return; + + isAutoPlaying = true; + autoPlayBtn.textContent = 'Stop Auto Play'; + autoPlayBtn.classList.add('active'); + + autoPlayInterval = setInterval(() => { + if (availableNumbers.length > 0 && gameActive) { + callNextNumber(); + } else { + stopAutoPlay(); + } + }, 2000); // Call every 2 seconds + + messageEl.textContent = 'Auto-play started! Numbers will be called automatically.'; +} + +// Stop auto-play +function stopAutoPlay() { + isAutoPlaying = false; + autoPlayBtn.textContent = 'Auto Play'; + autoPlayBtn.classList.remove('active'); + + if (autoPlayInterval) { + clearInterval(autoPlayInterval); + autoPlayInterval = null; + } + + messageEl.textContent = 'Auto-play stopped.'; +} + +// Check for bingo +function checkForBingo(silent = false) { + if (!gameActive) return false; + + const card = getCardState(); + const hasBingo = checkWinningPatterns(card); + + if (hasBingo) { + gameActive = false; + statusEl.textContent = 'BINGO! You Win!'; + messageEl.textContent = '๐ŸŽ‰ BINGO! Congratulations! ๐ŸŽ‰'; + + if (isAutoPlaying) { + stopAutoPlay(); + } + + // Celebrate + celebrateWin(); + return true; + } else if (!silent) { + messageEl.textContent = 'No bingo yet. Keep playing!'; + setTimeout(() => messageEl.textContent = '', 2000); + } + + return false; +} + +// Get current card state +function getCardState() { + const card = []; + for (let row = 0; row < 5; row++) { + card[row] = []; + for (let col = 0; col < 5; col++) { + const cell = document.querySelector(`.card-grid .number-cell:nth-child(${row * 5 + col + 1})`); + card[row][col] = cell.classList.contains('marked'); + } + } + return card; +} + +// Check winning patterns +function checkWinningPatterns(card) { + // Check rows + for (let row = 0; row < 5; row++) { + if (card[row].every(cell => cell)) return true; + } + + // Check columns + for (let col = 0; col < 5; col++) { + let columnComplete = true; + for (let row = 0; row < 5; row++) { + if (!card[row][col]) { + columnComplete = false; + break; + } + } + if (columnComplete) return true; + } + + // Check diagonals + let diagonal1 = true; + let diagonal2 = true; + for (let i = 0; i < 5; i++) { + if (!card[i][i]) diagonal1 = false; + if (!card[i][4 - i]) diagonal2 = false; + } + if (diagonal1 || diagonal2) return true; + + // Check four corners + if (card[0][0] && card[0][4] && card[4][0] && card[4][4]) return true; + + // Check full house + let fullHouse = true; + for (let row = 0; row < 5; row++) { + for (let col = 0; col < 5; col++) { + if (!card[row][col]) { + fullHouse = false; + break; + } + } + if (!fullHouse) break; + } + if (fullHouse) return true; + + return false; +} + +// Celebrate win +function celebrateWin() { + // Add celebration animation + document.querySelectorAll('.number-cell.marked').forEach(cell => { + cell.style.animation = 'celebrate 0.5s ease-in-out'; + }); + + // Play celebration sound if enabled + if (soundEnabledCheckbox.checked) { + setTimeout(() => playWinSound(), 500); + } +} + +// Update display elements +function updateDisplay() { + calledCountEl.textContent = calledNumbers.length; + lastNumberEl.textContent = lastCalled || '-'; +} + +// Play sound effect +function playSound() { + // Create audio context for sound effects + try { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.frequency.setValueAtTime(800, audioContext.currentTime); + oscillator.frequency.exponentialRampToValueAtTime(400, audioContext.currentTime + 0.1); + + gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1); + + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + 0.1); + } catch (e) { + // Sound not supported + } +} + +// Play win sound +function playWinSound() { + try { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + // Play a victory melody + const notes = [523, 659, 784, 1047]; // C, E, G, C + let time = audioContext.currentTime; + + notes.forEach((freq, index) => { + oscillator.frequency.setValueAtTime(freq, time + index * 0.15); + gainNode.gain.setValueAtTime(0.1, time + index * 0.15); + gainNode.gain.exponentialRampToValueAtTime(0.01, time + index * 0.15 + 0.1); + }); + + oscillator.start(time); + oscillator.stop(time + 0.8); + } catch (e) { + // Sound not supported + } +} + +// Start the game +initGame(); + +// This bingo game includes all classic features +// Auto-play, auto-mark, and multiple win patterns +// The interface is clean and easy to use \ No newline at end of file diff --git a/games/bingo-caller/style.css b/games/bingo-caller/style.css new file mode 100644 index 00000000..983c8d34 --- /dev/null +++ b/games/bingo-caller/style.css @@ -0,0 +1,391 @@ +/* Bingo Caller Game Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #667eea, #764ba2); + min-height: 100vh; + color: white; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +h1 { + font-size: 2.5em; + text-align: center; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); + color: #ffd700; +} + +p { + text-align: center; + font-size: 1.1em; + margin-bottom: 20px; + opacity: 0.9; +} + +.game-stats { + display: flex; + justify-content: space-around; + flex-wrap: wrap; + margin: 20px 0; + font-size: 1.1em; + font-weight: bold; + background: rgba(255, 215, 0, 0.1); + padding: 15px; + border-radius: 10px; + border: 2px solid #ffd700; +} + +.game-area { + display: flex; + gap: 30px; + margin: 20px 0; + align-items: flex-start; +} + +.bingo-card { + flex: 1; + background: #fff; + border-radius: 15px; + padding: 20px; + box-shadow: 0 8px 32px rgba(0,0,0,0.3); + border: 4px solid #ffd700; +} + +.card-header { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 5px; + margin-bottom: 10px; +} + +.letter { + background: #e74c3c; + color: white; + font-size: 1.5em; + font-weight: bold; + text-align: center; + padding: 10px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +.card-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-template-rows: repeat(5, 1fr); + gap: 5px; +} + +.number-cell { + background: #f8f9fa; + border: 2px solid #dee2e6; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2em; + font-weight: bold; + color: #495057; + cursor: pointer; + transition: all 0.3s; + aspect-ratio: 1; + min-height: 50px; + position: relative; +} + +.number-cell:hover { + background: #e9ecef; + transform: scale(1.05); + box-shadow: 0 4px 8px rgba(0,0,0,0.2); +} + +.number-cell.marked { + background: #28a745; + color: white; + border-color: #28a745; +} + +.number-cell.marked::after { + content: 'โœ“'; + position: absolute; + top: 2px; + right: 2px; + font-size: 0.8em; + color: white; +} + +.free-space { + background: #ffd700 !important; + color: #000 !important; + font-weight: bold; + cursor: default; +} + +.free-space::after { + content: 'โ˜…'; + position: absolute; + top: 2px; + right: 2px; + color: #000; +} + +.game-controls { + flex: 1; + display: flex; + flex-direction: column; + gap: 20px; +} + +.called-numbers-display { + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + padding: 15px; + border: 2px solid #ffd700; +} + +.called-numbers-display h3 { + margin-bottom: 10px; + color: #ffd700; + text-align: center; +} + +#called-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(40px, 1fr)); + gap: 5px; + max-height: 200px; + overflow-y: auto; +} + +.called-number { + background: #e74c3c; + color: white; + border-radius: 50%; + width: 35px; + height: 35px; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 0.9em; + animation: numberCalled 0.5s ease-out; +} + +@keyframes numberCalled { + 0% { transform: scale(0); } + 50% { transform: scale(1.2); } + 100% { transform: scale(1); } +} + +.control-buttons { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; +} + +button { + background: #3498db; + color: white; + border: none; + padding: 12px 20px; + font-size: 1em; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s; + font-weight: bold; +} + +button:hover { + background: #2980b9; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.3); +} + +button:active { + transform: translateY(0); +} + +button:disabled { + background: #666; + cursor: not-allowed; + transform: none; +} + +#call-number-btn { + background: #27ae60; + grid-column: span 2; +} + +#call-number-btn:hover { + background: #229954; +} + +#auto-play-btn.active { + background: #e67e22; + animation: pulse 1s infinite; +} + +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +#new-game-btn { + background: #e74c3c; +} + +#new-game-btn:hover { + background: #c0392b; +} + +#check-bingo-btn { + background: #f39c12; +} + +#check-bingo-btn:hover { + background: #e67e22; +} + +.game-options { + display: flex; + flex-direction: column; + gap: 10px; + background: rgba(255, 255, 255, 0.1); + padding: 15px; + border-radius: 10px; + border: 2px solid #ffd700; +} + +.game-options label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-weight: normal; +} + +.game-options input[type="checkbox"] { + width: 18px; + height: 18px; +} + +#message { + font-size: 1.2em; + margin: 20px 0; + min-height: 30px; + font-weight: bold; + color: #ffd700; + text-align: center; +} + +.win-patterns { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + margin: 20px 0; + border: 2px solid #ffd700; +} + +.win-patterns h3 { + margin-bottom: 15px; + color: #ffd700; + text-align: center; +} + +.patterns { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 10px; +} + +.pattern { + background: rgba(255, 215, 0, 0.1); + padding: 10px; + border-radius: 5px; + border: 1px solid #ffd700; +} + +.instructions { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + margin-top: 20px; + text-align: left; + max-width: 800px; + margin-left: auto; + margin-right: auto; +} + +.instructions h3 { + margin-bottom: 10px; + color: #ffd700; + text-align: center; +} + +.instructions ul { + list-style-type: disc; + padding-left: 20px; +} + +.instructions li { + margin: 5px 0; + line-height: 1.4; +} + +/* Responsive design */ +@media (max-width: 1024px) { + .game-area { + flex-direction: column; + align-items: center; + } + + .control-buttons { + grid-template-columns: 1fr; + } + + #call-number-btn { + grid-column: span 1; + } +} + +@media (max-width: 768px) { + .card-grid { + gap: 3px; + } + + .number-cell { + font-size: 1em; + min-height: 40px; + } + + .game-stats { + font-size: 0.9em; + padding: 10px; + } + + .control-buttons { + gap: 8px; + } + + button { + padding: 10px 15px; + font-size: 0.9em; + } + + .patterns { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/games/blackjack/index.html b/games/blackjack/index.html new file mode 100644 index 00000000..488c7cb2 --- /dev/null +++ b/games/blackjack/index.html @@ -0,0 +1,74 @@ + + + + + + Blackjack + + + +
+
+

Welcome to Blackjack!

+

How to Play:

+
    +
  • The goal is to get a hand value closer to 21 than the dealer, without going over.
  • +
  • Numbered cards are worth their face value. King, Queen, and Jack are worth 10.
  • +
  • Aces can be worth 1 or 11.
  • +
  • Click the chips to place your bet, then click 'Deal' to start.
  • +
  • Click 'Hit' to take another card, or 'Stand' to keep your current hand.
  • +
  • 'Double Down' doubles your bet, but you only get one more card.
  • +
+ +
+
+ + + + + + \ No newline at end of file diff --git a/games/blackjack/script.js b/games/blackjack/script.js new file mode 100644 index 00000000..8dedffd3 --- /dev/null +++ b/games/blackjack/script.js @@ -0,0 +1,287 @@ +// --- DOM Elements (verified with new HTML) --- +const instructionsOverlay = document.getElementById('instructions-overlay'); +const startGameBtn = document.getElementById('start-game-btn'); +const gameBoard = document.getElementById('game-board'); +const dealerHandEl = document.getElementById('dealer-hand'); +const playerHandEl = document.getElementById('player-hand'); +const dealerScoreEl = document.getElementById('dealer-score'); +const playerScoreEl = document.getElementById('player-score'); +const statusMessageEl = document.getElementById('status-message'); +const playerMoneyEl = document.getElementById('player-money'); +const currentBetDisplay = document.getElementById('current-bet-display'); +const bettingArea = document.getElementById('betting-area'); +const actionsArea = document.getElementById('actions-area'); +const replayArea = document.getElementById('replay-area'); +const chips = document.getElementById('chips'); +const dealBtn = document.getElementById('deal-btn'); +const clearBetBtn = document.getElementById('clear-bet-btn'); +const hitBtn = document.getElementById('hit-btn'); +const standBtn = document.getElementById('stand-btn'); +const doubleDownBtn = document.getElementById('double-down-btn'); + +// --- Game State Enum --- +const GAME_STATE = { BETTING: 'betting', PLAYER_TURN: 'player_turn', DEALER_TURN: 'dealer_turn', ROUND_OVER: 'round_over' }; +let currentState = GAME_STATE.BETTING; + +// --- Game Variables --- +let deck = []; +let playerHand = []; +let dealerHand = []; +let playerMoney = 500; +let currentBet = 0; + +// --- Event Listeners --- +startGameBtn.addEventListener('click', () => { instructionsOverlay.classList.add('hidden'); gameBoard.classList.remove('hidden'); }); +dealBtn.addEventListener('click', deal); +hitBtn.addEventListener('click', hit); +standBtn.addEventListener('click', stand); +doubleDownBtn.addEventListener('click', doubleDown); +chips.addEventListener('click', handleChipClick); +clearBetBtn.addEventListener('click', clearBet); +replayBtn.addEventListener('click', resetGame); + +// --- Initialization --- +loadGame(); + +// --- Game Flow & State Management --- +function deal() { + if (currentBet === 0) return; + currentState = GAME_STATE.PLAYER_TURN; + clearTable(); + + saveGame(); + + deck = createAndShuffleDeck(); + playerHand = []; + dealerHand = []; + + // Staggered card dealing animation + setTimeout(() => addCardToHand(playerHand, playerHandEl), 250); + setTimeout(() => addCardToHand(dealerHand, dealerHandEl, true), 500); + setTimeout(() => addCardToHand(playerHand, playerHandEl), 750); + setTimeout(() => addCardToHand(dealerHand, dealerHandEl), 1000); + + setTimeout(() => { + updateScores(true); + toggleControls(false); + if (getHandValue(playerHand) === 21) { + updateStatus("Blackjack!"); + setTimeout(stand, 1000); + } else { + updateStatus("Your turn: Hit or Stand?"); + } + }, 1100); +} + +function hit() { + if (currentState !== GAME_STATE.PLAYER_TURN) return; + addCardToHand(playerHand, playerHandEl); + updateScores(); + if (getHandValue(playerHand) > 21) { + updateStatus("Bust! You lose."); + endRound(); + } +} + +function stand() { + if (currentState !== GAME_STATE.PLAYER_TURN) return; + currentState = GAME_STATE.DEALER_TURN; + dealerTurn(); +} + +function doubleDown() { + if (currentState !== GAME_STATE.PLAYER_TURN || playerMoney < currentBet) { + updateStatus("Not enough money to double down!"); + return; + } + playerMoney -= currentBet; + currentBet *= 2; + updateMoneyDisplay(); + updateBetDisplay(); + addCardToHand(playerHand, playerHandEl); + updateScores(); + if (getHandValue(playerHand) <= 21) { + setTimeout(stand, 500); + } else { + updateStatus("Bust! You lose."); + endRound(); + } +} + +function dealerTurn() { + revealDealerCard(); + updateScores(); + const dealerInterval = setInterval(() => { + if (getHandValue(dealerHand) < 17) { + addCardToHand(dealerHand, dealerHandEl); + updateScores(); + } else { + clearInterval(dealerInterval); + determineWinner(); + } + }, 1000); +} + +function determineWinner() { + const playerScore = getHandValue(playerHand); + const dealerScore = getHandValue(dealerHand); + const playerHasBlackjack = playerScore === 21 && playerHand.length === 2; + + if (playerHasBlackjack) { + updateStatus("Blackjack! You win!"); + playerMoney += currentBet * 2.5; + } else if (playerScore > 21) { + updateStatus("You busted! Dealer wins."); + } else if (dealerScore > 21 || playerScore > dealerScore) { + updateStatus("You win!"); + playerMoney += currentBet * 2; + } else if (dealerScore > playerScore) { + updateStatus("Dealer wins."); + } else { + updateStatus("Push (It's a tie)."); + playerMoney += currentBet; + } + endRound(); +} + +// REFACTORED: endRound now has a delay to show the win/loss message +function endRound() { + currentState = GAME_STATE.ROUND_OVER; + saveGame(); + + // The win/loss message is already on screen from determineWinner() + // Wait 2 seconds before resetting for the next round. + setTimeout(() => { + currentBet = 0; + updateMoneyDisplay(); + updateBetDisplay(); + toggleControls(true); // Show betting controls + dealBtn.textContent = "New Round"; // Change button text + + if (playerMoney <= 0) { + updateStatus("Game Over! You're out of money."); + bettingArea.classList.add('hidden'); + actionsArea.classList.add('hidden'); + replayArea.classList.remove('hidden'); + } else { + // This message now appears AFTER the delay + updateStatus("Place your bet for the next round."); + } + }, 2000); // 2-second delay +} + +// --- Rendering (No Flicker) --- +function clearTable() { + dealerHandEl.innerHTML = ''; + playerHandEl.innerHTML = ''; + dealerScoreEl.textContent = '0'; + playerScoreEl.textContent = '0'; +} +function addCardToHand(hand, element, isHidden = false) { + const card = deck.pop(); + hand.push(card); + const cardEl = document.createElement('div'); + cardEl.className = 'card'; + if (isHidden) { + cardEl.classList.add('hidden'); + cardEl.dataset.hidden = 'true'; + } + const isRed = ['โ™ฅ', 'โ™ฆ'].includes(card.suit); + cardEl.classList.toggle('red', isRed); + cardEl.innerHTML = `${card.rank}${card.suit}`; + element.appendChild(cardEl); +} +function revealDealerCard() { + const hiddenCardEl = dealerHandEl.querySelector('[data-hidden="true"]'); + if (hiddenCardEl) { + hiddenCardEl.classList.remove('hidden'); + hiddenCardEl.removeAttribute('data-hidden'); + } +} + +// --- UI & Helper Functions --- +function updateScores(hideDealerScore = false) { + playerScoreEl.textContent = getHandValue(playerHand); + dealerScoreEl.textContent = hideDealerScore ? '?' : getHandValue(dealerHand); +} +function handleChipClick(event) { + if (currentState !== GAME_STATE.BETTING && currentState !== GAME_STATE.ROUND_OVER) return; + const chip = event.target.closest('.chip'); + if (chip) { + const value = parseInt(chip.dataset.value); + if (playerMoney >= value) { + currentBet += value; + playerMoney -= value; + updateMoneyDisplay(); + updateBetDisplay(); + } else { + updateStatus("Not enough money for that chip!"); + } + } +} +function clearBet() { + if (currentState !== GAME_STATE.BETTING && currentState !== GAME_STATE.ROUND_OVER) return; + playerMoney += currentBet; + currentBet = 0; + updateMoneyDisplay(); + updateBetDisplay(); +} +function updateBetDisplay() { + currentBetDisplay.textContent = currentBet; + dealBtn.disabled = currentBet === 0; +} +function updateMoneyDisplay() { + playerMoneyEl.textContent = playerMoney; +} +function updateStatus(message) { + statusMessageEl.textContent = message; +} +// REFACTORED: toggleControls no longer changes the status message +function toggleControls(showBetting) { + bettingArea.classList.toggle('hidden', !showBetting); + actionsArea.classList.toggle('hidden', showBetting); + if (!showBetting) { + doubleDownBtn.disabled = playerMoney < currentBet || playerHand.length !== 2; + } +} + +// --- Card & Deck Logic (Unchanged) --- +function createAndShuffleDeck() { + const suits = ['โ™ฅ', 'โ™ฆ', 'โ™ฃ', 'โ™ ']; + const ranks = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']; + let newDeck = []; + for (const suit of suits) { for (const rank of ranks) { newDeck.push({ suit, rank }); } } + for (let i = newDeck.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [newDeck[i], newDeck[j]] = [newDeck[j], newDeck[i]]; } + return newDeck; +} +function getCardValue(card) { + if (['J', 'Q', 'K'].includes(card.rank)) return 10; + if (card.rank === 'A') return 11; + return parseInt(card.rank); +} +function getHandValue(hand) { + let value = 0; let aceCount = 0; + for (const card of hand) { value += getCardValue(card); if (card.rank === 'A') aceCount++; } + while (value > 21 && aceCount > 0) { value -= 10; aceCount--; } + return value; +} + +// --- Save/Load & Reset --- +function saveGame() { localStorage.setItem('blackjack_money', playerMoney); } +function loadGame() { + const savedMoney = localStorage.getItem('blackjack_money'); + playerMoney = savedMoney ? parseInt(savedMoney) : 500; + updateMoneyDisplay(); + updateBetDisplay(); +} +function resetGame() { + playerMoney = 500; + saveGame(); + clearBet(); + clearTable(); + replayArea.classList.add('hidden'); + toggleControls(true); + dealBtn.textContent = "Deal"; + updateStatus("Place your bet to start a new game!"); + updateMoneyDisplay(); +} \ No newline at end of file diff --git a/games/blackjack/style.css b/games/blackjack/style.css new file mode 100644 index 00000000..635415e4 --- /dev/null +++ b/games/blackjack/style.css @@ -0,0 +1,62 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #0d2e1a; color: white; text-align: center; padding: 20px; + margin: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; +} + +/* --- Instructions Overlay --- */ +#instructions-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); z-index: 100; display: flex; align-items: center; justify-content: center; } +#instructions-box { background-color: #f4f4f4; color: #333; padding: 20px 40px; border-radius: 15px; max-width: 90%; width: 600px; text-align: left; box-shadow: 0 5px 20px rgba(0,0,0,0.4); } +#instructions-box h2 { text-align: center; color: #006400; } +#instructions-box ul { list-style-type: 'โ™ '; padding-left: 20px; } +#instructions-box li { margin-bottom: 10px; } +#start-game-btn { display: block; margin: 20px auto 0; padding: 12px 25px; font-size: 18px; cursor: pointer; border: none; border-radius: 8px; background-color: #2e7d32; color: white; } + +/* --- RESPONSIVE Game Board --- */ +#game-board { + width: 100%; max-width: 900px; padding: clamp(10px, 3vw, 20px); + background-color: #006400; border: 8px solid #a52a2a; border-radius: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); + display: flex; flex-direction: column; +} +.area { margin-bottom: 20px; } +.hand { display: flex; justify-content: center; flex-wrap: wrap; min-height: 125px; gap: 10px; position: relative; } + +/* --- Cards & Animations --- */ +.card { + width: 80px; height: 110px; background-color: white; color: black; border-radius: 8px; + border: 1px solid #ccc; box-shadow: 2px 2px 5px rgba(0,0,0,0.3); + display: flex; flex-direction: column; justify-content: space-between; padding: 5px; + font-size: 24px; font-weight: bold; animation: deal-card 0.5s ease-out; +} +@keyframes deal-card { from { transform: translateY(-200px) rotateX(90deg); opacity: 0; } to { transform: translateY(0) rotateX(0); opacity: 1; } } +.card .suit { font-size: 30px; } +.card.red { color: #d90000; } +.card.hidden { background-image: repeating-linear-gradient(45deg, #4a4a4a, #4a4a4a 10px, #555555 10px, #555555 20px); border: 1px solid #333; } + +/* --- Controls & UI (Refactored) --- */ +#game-status { min-height: 50px; font-size: 20px; font-style: italic; font-weight: bold; padding: 10px 0; } +#player-info { margin-top: auto; display: flex; flex-direction: column; align-items: center; gap: 15px; } +#money-and-bet { display: flex; gap: 30px; font-size: 18px; font-weight: bold; } +#controls { min-height: 120px; /* Reserve space to prevent layout shifts */ } +#controls button { padding: 10px 20px; font-size: 16px; border-radius: 8px; border: none; cursor: pointer; transition: all 0.2s; } +#controls button:hover { transform: translateY(-2px); box-shadow: 0 2px 4px rgba(0,0,0,0.3); } +#controls button:active { transform: translateY(0); } +#controls button:disabled { background-color: #9e9e9e !important; cursor: not-allowed; transform: none; box-shadow: none; opacity: 0.7; } +#betting-area { display: flex; flex-direction: column; align-items: center; gap: 15px; } +#chips { display: flex; gap: 10px; } +.chip { + width: 60px; height: 60px; border-radius: 50%; color: white; font-weight: bold; + border: 4px solid white; box-shadow: 0 3px 5px rgba(0,0,0,0.5); + display: flex; align-items: center; justify-content: center; font-size: 16px; +} +.chip::before { content: ''; width: 48px; height: 48px; border-radius: 50%; border: 2px dashed rgba(255, 255, 255, 0.5); position: absolute; } +.chip[data-value="5"] { background-color: #d90000; } +.chip[data-value="10"] { background-color: #1e88e5; } +.chip[data-value="25"] { background-color: #006400; } +.chip[data-value="100"] { background-color: #333; } + +#deal-btn, #replay-btn { background-color: #ffd700; color: #333; font-weight: bold; } +#clear-bet-btn { background-color: #aaa; } +#hit-btn, #stand-btn, #double-down-btn { background-color: #1e88e5; color: white; } + +.hidden { display: none !important; } \ No newline at end of file diff --git a/games/blink-catch/index.html b/games/blink-catch/index.html new file mode 100644 index 00000000..9cfa860b --- /dev/null +++ b/games/blink-catch/index.html @@ -0,0 +1,32 @@ + + + + + + Blink Catch | Mini JS Games Hub + + + +
+

Blink Catch

+

Click the blinking icon as fast as you can! โšก

+ +
+ Score: 0 + High Score: 0 +
+ + + +
+ +
+ +

Speed increases as score goes up. Try to beat your high score!

+
+ + + + diff --git a/games/blink-catch/script.js b/games/blink-catch/script.js new file mode 100644 index 00000000..47b73219 --- /dev/null +++ b/games/blink-catch/script.js @@ -0,0 +1,63 @@ +const blinkIcon = document.getElementById('blink-icon'); +const scoreEl = document.getElementById('score'); +const highScoreEl = document.getElementById('high-score'); +const restartBtn = document.getElementById('restart-btn'); + +let score = 0; +let highScore = localStorage.getItem('blinkCatchHighScore') || 0; +let blinkInterval = 1500; // initial speed in ms +let blinkTimer; + +highScoreEl.textContent = highScore; + +function randomPosition() { + const area = document.querySelector('.blink-area'); + const maxX = area.offsetWidth - blinkIcon.offsetWidth; + const maxY = area.offsetHeight - blinkIcon.offsetHeight; + + const x = Math.floor(Math.random() * maxX); + const y = Math.floor(Math.random() * maxY); + + blinkIcon.style.left = `${x}px`; + blinkIcon.style.top = `${y}px`; +} + +function blink() { + blinkIcon.classList.add('blink'); + setTimeout(() => blinkIcon.classList.remove('blink'), 300); + randomPosition(); +} + +function increaseDifficulty() { + if (blinkInterval > 500) { + blinkInterval -= 50; + clearInterval(blinkTimer); + blinkTimer = setInterval(blink, blinkInterval); + } +} + +function updateScore() { + score++; + scoreEl.textContent = score; + if (score > highScore) { + highScore = score; + highScoreEl.textContent = highScore; + localStorage.setItem('blinkCatchHighScore', highScore); + } + increaseDifficulty(); +} + +// Initial blink +blinkTimer = setInterval(blink, blinkInterval); + +// Click event +blinkIcon.addEventListener('click', updateScore); + +// Restart button +restartBtn.addEventListener('click', () => { + score = 0; + scoreEl.textContent = score; + blinkInterval = 1500; + clearInterval(blinkTimer); + blinkTimer = setInterval(blink, blinkInterval); +}); diff --git a/games/blink-catch/style.css b/games/blink-catch/style.css new file mode 100644 index 00000000..dc5b8544 --- /dev/null +++ b/games/blink-catch/style.css @@ -0,0 +1,78 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + color: #333; +} + +.game-container { + background-color: rgba(255, 255, 255, 0.95); + padding: 30px 50px; + border-radius: 20px; + text-align: center; + box-shadow: 0 10px 25px rgba(0,0,0,0.2); + max-width: 400px; +} + +h1 { + font-size: 2rem; + margin-bottom: 10px; +} + +.scoreboard { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + font-size: 1.2rem; + font-weight: bold; +} + +.blink-area { + position: relative; + height: 200px; + margin-bottom: 20px; + border: 2px dashed #ccc; + border-radius: 10px; + overflow: hidden; +} + +.blink-icon { + position: absolute; + font-size: 2.5rem; + cursor: pointer; + transition: transform 0.2s; +} + +.blink-icon.blink { + animation: blinkAnimation 0.3s infinite; +} + +@keyframes blinkAnimation { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +.controls button { + padding: 10px 20px; + font-size: 1rem; + cursor: pointer; + border: none; + border-radius: 10px; + background-color: #ff7f50; + color: white; + transition: transform 0.2s; +} + +.controls button:hover { + transform: scale(1.05); +} + +.note { + font-size: 0.9rem; + margin-top: 15px; + color: #555; +} diff --git a/games/block-drop-solver/index.html b/games/block-drop-solver/index.html new file mode 100644 index 00000000..b5adc372 --- /dev/null +++ b/games/block-drop-solver/index.html @@ -0,0 +1,34 @@ + + + + + + Block Drop Solver | Mini JS Games Hub + + + +
+

๐Ÿงฑ Block Drop Solver

+
+ + + +
+ + + +
+

๐ŸŽฏ Score: 0

+

๐Ÿ’จ Level: 1

+
+
+ + + + + + + + + + diff --git a/games/block-drop-solver/script.js b/games/block-drop-solver/script.js new file mode 100644 index 00000000..d9c9df86 --- /dev/null +++ b/games/block-drop-solver/script.js @@ -0,0 +1,181 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +const ROWS = 20; +const COLS = 10; +const BLOCK_SIZE = 30; + +let board = Array.from({ length: ROWS }, () => Array(COLS).fill(0)); +let score = 0; +let level = 1; +let gameInterval; +let paused = false; + +const colors = ["#00FFFF", "#FF00FF", "#FFFF00", "#00FF00", "#FF4500"]; +const pieces = [ + [[1, 1, 1, 1]], // I + [[1, 1], [1, 1]], // O + [[0, 1, 0], [1, 1, 1]], // T + [[1, 1, 0], [0, 1, 1]], // S + [[0, 1, 1], [1, 1, 0]] // Z +]; + +let currentPiece = randomPiece(); +let position = { x: 3, y: 0 }; + +const moveSound = document.getElementById("moveSound"); +const rotateSound = document.getElementById("rotateSound"); +const clearSound = document.getElementById("clearSound"); +const bgMusic = document.getElementById("bgMusic"); + +document.getElementById("startBtn").addEventListener("click", startGame); +document.getElementById("pauseBtn").addEventListener("click", togglePause); +document.getElementById("restartBtn").addEventListener("click", restartGame); +document.addEventListener("keydown", handleKeyPress); + +function randomPiece() { + const shape = pieces[Math.floor(Math.random() * pieces.length)]; + const color = colors[Math.floor(Math.random() * colors.length)]; + return { shape, color }; +} + +function drawBoard() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + board.forEach((row, y) => { + row.forEach((val, x) => { + if (val) { + ctx.fillStyle = val; + ctx.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE - 1, BLOCK_SIZE - 1); + ctx.shadowColor = val; + ctx.shadowBlur = 10; + } + }); + }); + drawPiece(); +} + +function drawPiece() { + currentPiece.shape.forEach((row, dy) => { + row.forEach((val, dx) => { + if (val) { + ctx.fillStyle = currentPiece.color; + ctx.fillRect((position.x + dx) * BLOCK_SIZE, (position.y + dy) * BLOCK_SIZE, BLOCK_SIZE - 1, BLOCK_SIZE - 1); + } + }); + }); +} + +function drop() { + if (paused) return; + position.y++; + if (collision()) { + position.y--; + merge(); + clearRows(); + resetPiece(); + } + drawBoard(); +} + +function collision() { + return currentPiece.shape.some((row, dy) => + row.some((val, dx) => { + if (!val) return false; + const newX = position.x + dx; + const newY = position.y + dy; + return newX < 0 || newX >= COLS || newY >= ROWS || board[newY]?.[newX]; + }) + ); +} + +function merge() { + currentPiece.shape.forEach((row, dy) => { + row.forEach((val, dx) => { + if (val) board[position.y + dy][position.x + dx] = currentPiece.color; + }); + }); +} + +function clearRows() { + let cleared = 0; + board = board.filter(row => !row.every(cell => cell)); + while (board.length < ROWS) board.unshift(Array(COLS).fill(0)); + if (cleared > 0) { + clearSound.play(); + score += cleared * 100; + if (score % 500 === 0) level++; + updateUI(); + } +} + +function updateUI() { + document.getElementById("score").textContent = score; + document.getElementById("level").textContent = level; +} + +function resetPiece() { + currentPiece = randomPiece(); + position = { x: 3, y: 0 }; + if (collision()) { + alert("Game Over ๐Ÿ˜ญ"); + restartGame(); + } +} + +function handleKeyPress(e) { + if (paused) return; + switch (e.key) { + case "ArrowLeft": + position.x--; + if (collision()) position.x++; + moveSound.play(); + break; + case "ArrowRight": + position.x++; + if (collision()) position.x--; + moveSound.play(); + break; + case "ArrowDown": + drop(); + break; + case "ArrowUp": + rotate(); + rotateSound.play(); + break; + } + drawBoard(); +} + +function rotate() { + const rotated = currentPiece.shape[0].map((_, i) => currentPiece.shape.map(r => r[i])).reverse(); + const prev = currentPiece.shape; + currentPiece.shape = rotated; + if (collision()) currentPiece.shape = prev; +} + +function startGame() { + if (!gameInterval) { + bgMusic.volume = 0.2; + bgMusic.play(); + gameInterval = setInterval(drop, 700 - level * 50); + } +} + +function togglePause() { + paused = !paused; + document.getElementById("pauseBtn").textContent = paused ? "โ–ถ Resume" : "โธ Pause"; + if (paused) bgMusic.pause(); else bgMusic.play(); +} + +function restartGame() { + clearInterval(gameInterval); + gameInterval = null; + board = Array.from({ length: ROWS }, () => Array(COLS).fill(0)); + score = 0; + level = 1; + paused = false; + bgMusic.pause(); + bgMusic.currentTime = 0; + updateUI(); + drawBoard(); +} diff --git a/games/block-drop-solver/style.css b/games/block-drop-solver/style.css new file mode 100644 index 00000000..dd6989cc --- /dev/null +++ b/games/block-drop-solver/style.css @@ -0,0 +1,60 @@ +body { + background: radial-gradient(circle at center, #0f2027, #203a43, #2c5364); + color: #fff; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + text-align: center; + height: 100vh; + margin: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.game-container { + display: flex; + flex-direction: column; + align-items: center; + border: 2px solid #00ffff; + border-radius: 12px; + padding: 20px; + background: rgba(0, 0, 0, 0.4); + box-shadow: 0 0 30px #00ffff, inset 0 0 20px #007777; +} + +.title { + text-shadow: 0 0 10px #00ffff; +} + +.controls button { + background: linear-gradient(45deg, #00ffff, #007777); + border: none; + border-radius: 6px; + color: #000; + font-weight: bold; + font-size: 16px; + margin: 10px; + padding: 10px 20px; + cursor: pointer; + transition: 0.3s; +} + +.controls button:hover { + box-shadow: 0 0 15px #00ffff; +} + +canvas { + background: rgba(0, 0, 0, 0.7); + margin-top: 15px; + border: 2px solid #00ffff; + box-shadow: 0 0 20px #00ffff; +} + +.info { + margin-top: 15px; + font-size: 18px; +} + +.info span { + color: #00ffff; + font-weight: bold; +} diff --git a/games/block-stack/index.html b/games/block-stack/index.html new file mode 100644 index 00000000..4a51e680 --- /dev/null +++ b/games/block-stack/index.html @@ -0,0 +1,23 @@ + + + + + + Block Stack Game + + + +
+

Block Stack

+

Stack blocks perfectly on top of each other. Click to place each block!

+
+
Time: 30
+
Blocks: 0
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/games/block-stack/script.js b/games/block-stack/script.js new file mode 100644 index 00000000..3212d21d --- /dev/null +++ b/games/block-stack/script.js @@ -0,0 +1,188 @@ +// Block Stack Game Script +// Stack blocks without letting them fall + +var canvas = document.getElementById('gameCanvas'); +var ctx = canvas.getContext('2d'); +var timerDisplay = document.getElementById('timer'); +var scoreDisplay = document.getElementById('score'); +var restartBtn = document.getElementById('restart'); +var messageDiv = document.getElementById('message'); + +var blocks = []; +var currentBlock = null; +var blockWidth = 80; +var blockHeight = 20; +var score = 0; +var timeLeft = 30; +var timerInterval; +var gameRunning = true; + +// Block class +function Block(x, y, width, color) { + this.x = x; + this.y = y; + this.width = width; + this.height = blockHeight; + this.color = color; + this.falling = false; + this.fallSpeed = 0; +} + +// Initialize the game +function initGame() { + blocks = []; + score = 0; + timeLeft = 30; + gameRunning = true; + messageDiv.textContent = ''; + scoreDisplay.textContent = 'Blocks: ' + score; + + // Add base block + blocks.push(new Block(canvas.width / 2 - blockWidth / 2, canvas.height - blockHeight, blockWidth, '#ff5722')); + + spawnNewBlock(); + startTimer(); + draw(); +} + +// Spawn a new block +function spawnNewBlock() { + if (!gameRunning) return; + var color = '#' + Math.floor(Math.random()*16777215).toString(16); + currentBlock = new Block(0, 100, blockWidth, color); + currentBlock.direction = 1; // 1 right, -1 left + currentBlock.speed = 2; +} + +// Update game +function update() { + if (!gameRunning || !currentBlock) return; + + // Move current block + currentBlock.x += currentBlock.speed * currentBlock.direction; + if (currentBlock.x <= 0 || currentBlock.x + currentBlock.width >= canvas.width) { + currentBlock.direction *= -1; + } + + // Update falling blocks + for (var i = blocks.length - 1; i >= 0; i--) { + var block = blocks[i]; + if (block.falling) { + block.y += block.fallSpeed; + block.fallSpeed += 0.5; + if (block.y > canvas.height) { + blocks.splice(i, 1); + } + } + } +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw blocks + for (var i = 0; i < blocks.length; i++) { + var block = blocks[i]; + ctx.fillStyle = block.color; + ctx.fillRect(block.x, block.y, block.width, block.height); + ctx.strokeStyle = '#000'; + ctx.strokeRect(block.x, block.y, block.width, block.height); + } + + // Draw current block + if (currentBlock) { + ctx.fillStyle = currentBlock.color; + ctx.fillRect(currentBlock.x, currentBlock.y, currentBlock.width, currentBlock.height); + ctx.strokeStyle = '#000'; + ctx.strokeRect(currentBlock.x, currentBlock.y, currentBlock.width, currentBlock.height); + } + + if (gameRunning) { + update(); + requestAnimationFrame(draw); + } +} + +// Place block +function placeBlock() { + if (!currentBlock || !gameRunning) return; + + var topBlock = blocks[blocks.length - 1]; + var overlap = Math.min(currentBlock.x + currentBlock.width, topBlock.x + topBlock.width) - Math.max(currentBlock.x, topBlock.x); + + if (overlap > 0) { + // Place the block + currentBlock.y = topBlock.y - blockHeight; + // Trim the block to the overlap + var leftOverhang = Math.max(0, topBlock.x - currentBlock.x); + var rightOverhang = Math.max(0, (currentBlock.x + currentBlock.width) - (topBlock.x + topBlock.width)); + + currentBlock.x += leftOverhang; + currentBlock.width -= leftOverhang + rightOverhang; + + if (currentBlock.width > 0) { + blocks.push(currentBlock); + score++; + scoreDisplay.textContent = 'Blocks: ' + score; + + // Check if any overhanging parts fall + if (leftOverhang > 0) { + var fallingBlock = new Block(currentBlock.x - leftOverhang, currentBlock.y, leftOverhang, currentBlock.color); + fallingBlock.falling = true; + blocks.push(fallingBlock); + } + if (rightOverhang > 0) { + var fallingBlock = new Block(currentBlock.x + currentBlock.width, currentBlock.y, rightOverhang, currentBlock.color); + fallingBlock.falling = true; + blocks.push(fallingBlock); + } + + spawnNewBlock(); + } else { + // Block completely missed + currentBlock.falling = true; + blocks.push(currentBlock); + gameRunning = false; + messageDiv.textContent = 'Block missed! Game over. Final score: ' + score; + messageDiv.style.color = 'red'; + } + } else { + // No overlap + currentBlock.falling = true; + blocks.push(currentBlock); + gameRunning = false; + messageDiv.textContent = 'Block missed! Game over. Final score: ' + score; + messageDiv.style.color = 'red'; + } + + currentBlock = null; +} + +// Handle canvas click +canvas.addEventListener('click', function() { + if (gameRunning && currentBlock) { + placeBlock(); + } +}); + +// Start the timer +function startTimer() { + clearInterval(timerInterval); + timerInterval = setInterval(function() { + timeLeft--; + timerDisplay.textContent = 'Time: ' + timeLeft; + if (timeLeft <= 0) { + clearInterval(timerInterval); + gameRunning = false; + messageDiv.textContent = 'Time\'s up! Final Score: ' + score; + messageDiv.style.color = 'yellow'; + } + }, 1000); +} + +// Restart button +restartBtn.addEventListener('click', initGame); + +// Start the game +initGame(); \ No newline at end of file diff --git a/games/block-stack/style.css b/games/block-stack/style.css new file mode 100644 index 00000000..12e4d028 --- /dev/null +++ b/games/block-stack/style.css @@ -0,0 +1,54 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #263238; + color: #fff; +} + +.container { + text-align: center; +} + +h1 { + color: #ff5722; +} + +.game-info { + margin-bottom: 20px; + display: flex; + justify-content: center; + gap: 20px; +} + +#timer, #score { + font-size: 24px; + font-weight: bold; +} + +#restart { + padding: 10px 20px; + font-size: 16px; + background-color: #ff5722; + color: white; + border: none; + cursor: pointer; +} + +#restart:hover { + background-color: #d84315; +} + +canvas { + border: 2px solid #ff5722; + background-color: #37474f; +} + +#message { + margin-top: 20px; + font-size: 18px; + font-weight: bold; +} \ No newline at end of file diff --git a/games/block_breaker/index.html b/games/block_breaker/index.html new file mode 100644 index 00000000..9fbae5dd --- /dev/null +++ b/games/block_breaker/index.html @@ -0,0 +1,16 @@ + + + + + Block Breaker Game + + + + +
+ +
+ + + + \ No newline at end of file diff --git a/games/block_breaker/script.js b/games/block_breaker/script.js new file mode 100644 index 00000000..f50b2547 --- /dev/null +++ b/games/block_breaker/script.js @@ -0,0 +1,212 @@ +// game.js + +// --- 1. Setup Canvas and Context --- +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); + +const CANVAS_WIDTH = canvas.width; +const CANVAS_HEIGHT = canvas.height; + +// --- 2. Game State & Component Variables --- +let score = 0; +let lives = 3; +let gameOver = false; +let gameWin = false; + +// Ball properties +const ballRadius = 5; +let x = CANVAS_WIDTH / 2; // Ball x-position +let y = CANVAS_HEIGHT - 30; // Ball y-position +let dx = 2; // Ball x-velocity (delta x) +let dy = -2; // Ball y-velocity (delta y) + +// Paddle properties +const paddleHeight = 10; +const paddleWidth = 70; +let paddleX = (CANVAS_WIDTH - paddleWidth) / 2; // Paddle x-position + +// Brick properties +const brickRowCount = 5; +const brickColumnCount = 8; +const brickWidth = 50; +const brickHeight = 15; +const brickPadding = 10; +const brickOffsetTop = 30; +const brickOffsetLeft = 15; + +// Bricks array (using a 2D array for the grid) +let bricks = []; +for (let c = 0; c < brickColumnCount; c++) { + bricks[c] = []; + for (let r = 0; r < brickRowCount; r++) { + // Status 1 means the brick is visible/active + bricks[c][r] = { x: 0, y: 0, status: 1 }; + } +} + +// User Input (Key presses) +let rightPressed = false; +let leftPressed = false; + +// --- 3. Drawing Functions --- + +function drawBall() { + ctx.beginPath(); + ctx.arc(x, y, ballRadius, 0, Math.PI * 2); + ctx.fillStyle = "#a9b7c6"; // Light blue/grey + ctx.fill(); + ctx.closePath(); +} + +function drawPaddle() { + ctx.beginPath(); + ctx.rect(paddleX, CANVAS_HEIGHT - paddleHeight, paddleWidth, paddleHeight); + ctx.fillStyle = "#c679dd"; // Purple + ctx.fill(); + ctx.closePath(); +} + +function drawBricks() { + for (let c = 0; c < brickColumnCount; c++) { + for (let r = 0; r < brickRowCount; r++) { + if (bricks[c][r].status === 1) { // Only draw active bricks + const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft; + const brickY = r * (brickHeight + brickPadding) + brickOffsetTop; + + // Update brick position in the array + bricks[c][r].x = brickX; + bricks[c][r].y = brickY; + + ctx.beginPath(); + ctx.rect(brickX, brickY, brickWidth, brickHeight); + ctx.fillStyle = "#50fa7b"; // Green + ctx.fill(); + ctx.closePath(); + } + } + } +} + +// --- 4. Game Logic: Collision Detection --- + +function collisionDetection() { + for (let c = 0; c < brickColumnCount; c++) { + for (let r = 0; r < brickRowCount; r++) { + const b = bricks[c][r]; + if (b.status === 1) { + // Simple AABB (Axis-Aligned Bounding Box) collision for ball (circle) and brick (rect) + if (x > b.x && x < b.x + brickWidth && y > b.y && y < b.y + brickHeight) { + dy = -dy; // Reverse vertical direction + b.status = 0; // Destroy the brick + score++; + + // Win condition check + if (score === brickRowCount * brickColumnCount) { + gameWin = true; + } + } + } + } + } +} + +function ballMovement() { + // Wall Collision (Left/Right) + if (x + dx > CANVAS_WIDTH - ballRadius || x + dx < ballRadius) { + dx = -dx; + } + + // Wall Collision (Top) + if (y + dy < ballRadius) { + dy = -dy; + } + // Bottom edge (Paddle or Game Over) + else if (y + dy > CANVAS_HEIGHT - ballRadius - paddleHeight) { + // Paddle hit + if (x > paddleX && x < paddleX + paddleWidth) { + // **Advanced:** Adjust dx based on where it hit the paddle for realistic bounce + // For now, a simple reversal: + dy = -dy; + } + // Missed the paddle - Game Over/Lose Life + else if (y + dy > CANVAS_HEIGHT - ballRadius) { + lives--; + if (lives <= 0) { + gameOver = true; + } else { + // Reset ball and paddle position for new life + x = CANVAS_WIDTH / 2; + y = CANVAS_HEIGHT - 30; + dx = 2; + dy = -2; + paddleX = (CANVAS_WIDTH - paddleWidth) / 2; + } + } + } + + // Update ball position + x += dx; + y += dy; +} + +function paddleMovement() { + if (rightPressed && paddleX < CANVAS_WIDTH - paddleWidth) { + paddleX += 7; + } else if (leftPressed && paddleX > 0) { + paddleX -= 7; + } +} + +// --- 5. User Input Handlers (Keyboard) --- + +document.addEventListener("keydown", keyDownHandler, false); +document.addEventListener("keyup", keyUpHandler, false); + +function keyDownHandler(e) { + if (e.key === "Right" || e.key === "ArrowRight") { + rightPressed = true; + } else if (e.key === "Left" || e.key === "ArrowLeft") { + leftPressed = true; + } +} + +function keyUpHandler(e) { + if (e.key === "Right" || e.key === "ArrowRight") { + rightPressed = false; + } else if (e.key === "Left" || e.key === "ArrowLeft") { + leftPressed = false; + } +} + +// --- 6. The Game Loop --- + +function draw() { + // 1. **Clear the canvas** on every frame + ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + // 2. **Draw** game components + drawBricks(); + drawBall(); + drawPaddle(); + // (Add score, lives, and other info drawing here) + + if (gameOver) { + // Draw Game Over screen/text + console.log("GAME OVER"); + // We'll stop the loop outside this function using return + } else if (gameWin) { + // Draw Win screen/text + console.log("YOU WIN!"); + } else { + // 3. **Update Game Logic** (Movement and Collisions) + ballMovement(); + paddleMovement(); + collisionDetection(); + + // 4. **Continue the loop** + requestAnimationFrame(draw); + } +} + +// Start the game! +requestAnimationFrame(draw); \ No newline at end of file diff --git a/games/block_breaker/style.css b/games/block_breaker/style.css new file mode 100644 index 00000000..dad9e06b --- /dev/null +++ b/games/block_breaker/style.css @@ -0,0 +1,19 @@ +/* style.css */ +body { + background-color: #282c34; /* Dark background */ + display: flex; /* Use flexbox for centering */ + justify-content: center; /* Center horizontally */ + align-items: center; /* Center vertically */ + height: 100vh; /* Full viewport height */ + margin: 0; + overflow: hidden; /* Hide scrollbars if any */ + font-family: Arial, sans-serif; +} + +#gameCanvas { + /* Set a border to visually distinguish the game area */ + border: 3px solid #61afef; + display: block; /* Important for margin: 0 auto (if not using flexbox) */ + cursor: none; /* A common choice for mouse-controlled games */ + background-color: #3e4451; /* Canvas background */ +} \ No newline at end of file diff --git a/games/bloom-catch/README.md b/games/bloom-catch/README.md new file mode 100644 index 00000000..940c05a6 --- /dev/null +++ b/games/bloom-catch/README.md @@ -0,0 +1,23 @@ +Bloom Catch โ€” Prototype + +How to run +- Open `games/bloom-catch/index.html` in a browser (double-click or use Live Server extension). +- Mobile-first: try it on your phone or in Chrome DevTools (Toggle device toolbar). + +What this prototype has +- Canvas-based falling petals with pastel colors +- Pointer/touch controls: drag/tilt the vase horizontally to catch petals +- Combo counter that grows the vase slightly when you catch repeatedly +- High score tracking with localStorage +- Misses counter; after 5 misses the game ends and you can restart +- Small WebAudio beeps for catch/miss (may require a first tap to enable audio) + +Next steps (suggestions) +- Add artwork (SVG vase & petals) +- Add background ambient sound and volume control +- Smooth particle trails and subtle parallax +- Save high score locally +- Add accessibility (large buttons, sound toggle, haptics) + +License +- Use and modify freely to build the full game. \ No newline at end of file diff --git a/games/bloom-catch/index.html b/games/bloom-catch/index.html new file mode 100644 index 00000000..7b259619 --- /dev/null +++ b/games/bloom-catch/index.html @@ -0,0 +1,30 @@ + + + + + + Bloom Catch โ€” Prototype + + + +
+ + +
+
Combo: 0
+
High: 0
+
Misses: 0/5
+
+ + +
+ + + + \ No newline at end of file diff --git a/games/bloom-catch/script.js b/games/bloom-catch/script.js new file mode 100644 index 00000000..261a6c2b --- /dev/null +++ b/games/bloom-catch/script.js @@ -0,0 +1,250 @@ +// Bloom Catch โ€” lightweight prototype +// Human-friendly, hand-written style so it feels like a person coded it. + +(() => { + const canvas = document.getElementById('game'); + const ctx = canvas.getContext('2d'); + const comboEl = document.getElementById('combo'); + const missesEl = document.getElementById('miss-count'); + const highScoreEl = document.getElementById('high-score-val'); + const overlay = document.getElementById('overlay'); + const restartBtn = document.getElementById('restart'); + const finalText = document.getElementById('final-text'); + + // sizing for sharp canvas on high-DPI screens + function resize() { + const dpr = window.devicePixelRatio || 1; + canvas.width = Math.round(canvas.clientWidth * dpr); + canvas.height = Math.round(canvas.clientHeight * dpr); + ctx.setTransform(dpr,0,0,dpr,0,0); + } + window.addEventListener('resize', resize); + resize(); + + // game state + let running = true; + let petals = []; + let lastSpawn = 0; + let spawnInterval = 700; // ms + let combo = 0; + let misses = 0; + const maxMisses = 5; + let highScore = localStorage.getItem('bloomCatchHighScore') || 0; + + // set initial high score + highScoreEl.textContent = highScore; + + // vase (the player's catcher) + const vase = { + x: canvas.clientWidth / 2, + width: 120, + height: 40, + baseY: null, // will be set on first frame + grow: 0 + }; + + // gentle pastel colors for petals + const pastel = ['#ffd3e2','#ffe6c2','#ffd9f5','#e8eaf6','#dff7e3','#f8d6ff','#e8f7ff']; + + // pointer handling โ€” supports mouse and touch via Pointer Events + let pointerActive = false; + let lastPointerX = null; + canvas.addEventListener('pointerdown', e => { + pointerActive = true; canvas.setPointerCapture(e.pointerId); movePointer(e); + }); + canvas.addEventListener('pointermove', e => { if(pointerActive) movePointer(e); }); + canvas.addEventListener('pointerup', e => { pointerActive = false; canvas.releasePointerCapture(e.pointerId); lastPointerX=null; }); + function movePointer(e){ + // track vase x position with a simple smoothing so it feels natural + const rect = canvas.getBoundingClientRect(); + const x = (e.clientX - rect.left); + if(lastPointerX == null) vase.x = x; + else vase.x += (x - lastPointerX) * 0.9; // smoothing + lastPointerX = x; + // clamp + vase.x = Math.max(vase.width/2, Math.min(canvas.clientWidth - vase.width/2, vase.x)); + } + + // small audio helper using WebAudio for tiny catch/miss cues + let audioCtx = null; + function ensureAudio(){ if(!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } + function beepy(frequency, time=0.06, type='sine', gain=0.06){ + try{ + ensureAudio(); + const o = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + o.type = type; o.frequency.value = frequency; + g.gain.value = gain; + o.connect(g); g.connect(audioCtx.destination); + o.start(); o.stop(audioCtx.currentTime + time); + }catch(e){/* audio may be blocked until user gesture */} + } + + // Petal factory + function spawnPetal() { + const w = canvas.clientWidth; + const p = { + x: Math.random() * w * 0.98 + w*0.01, + y: -20, + r: 8 + Math.random()*10, + vx: (Math.random()-0.5) * 30, // gentle horizontal drift + vy: 30 + Math.random()*40, // falling speed + rot: Math.random()*Math.PI*2, + spin: (Math.random()-0.5)*0.06, + color: pastel[Math.floor(Math.random()*pastel.length)], + caught: false + }; + petals.push(p); + } + + // catching logic โ€” treat vase as an arc catcher + function checkCatch(p) { + const vaseY = vase.baseY; + const dx = p.x - vase.x; + const dy = p.y - vaseY; + // simple ellipse check where vase catches things above it + const withinX = Math.abs(dx) < vase.width/2 + p.r*0.7; + const nearY = p.y + p.r >= vaseY - vase.height/2; // hits the top of vase + return withinX && nearY && p.vy>0; + } + + // main update/draw loop + let lastTime = performance.now(); + function frame(t) { + if(!running) return; + const dt = Math.min(40, t - lastTime); + lastTime = t; + + // spawn logic (tied to combo for small difficulty change) + if(t - lastSpawn > spawnInterval) { + spawnPetal(); + lastSpawn = t; + // slightly faster spawn when combo grows + spawnInterval = 600 - Math.min(320, combo*10); + } + + // clear + ctx.clearRect(0,0,canvas.clientWidth, canvas.clientHeight); + + // set vase base Y (bottom area) lazily + if(vase.baseY == null) vase.baseY = canvas.clientHeight - 48; + + // draw background subtle vertical gradient (not expensive) + const g = ctx.createLinearGradient(0,0,0,canvas.clientHeight); + g.addColorStop(0,'rgba(255,255,255,0.06)'); + g.addColorStop(1,'rgba(220,230,255,0.06)'); + ctx.fillStyle = g; ctx.fillRect(0,0,canvas.clientWidth, canvas.clientHeight); + + // update petals + for(let i=petals.length-1;i>=0;i--){ + const p = petals[i]; + p.x += p.vx * (dt/1000); + p.y += p.vy * (dt/1000); + p.vx *= 0.999; // tiny air drag + p.rot += p.spin * (dt/16); + + // draw rotated petal as small rotated oval + ctx.save(); + ctx.translate(p.x, p.y); + ctx.rotate(p.rot); + ctx.fillStyle = p.color; + ctx.beginPath(); + ctx.ellipse(0,0,p.r*0.6,p.r,0,0,Math.PI*2); + ctx.fill(); + ctx.restore(); + + // catching + if(checkCatch(p)){ + // caught! + p.caught = true; + petals.splice(i,1); + combo += 1; + comboEl.textContent = combo; + if(combo > highScore){ + highScore = combo; + highScoreEl.textContent = highScore; + localStorage.setItem('bloomCatchHighScore', highScore); + } + vase.grow = Math.min(20, vase.grow + 1); + // small pleasant sound + beepy(880 + Math.random()*120, 0.05, 'sine', 0.03); + continue; + } + + // missed โ€” fell past bottom + if(p.y - p.r > canvas.clientHeight + 20){ + petals.splice(i,1); + misses += 1; + missesEl.textContent = misses; + // penalty to combo + combo = 0; comboEl.textContent = combo; + vase.grow = Math.max(0, vase.grow - 2); + beepy(220, 0.08, 'triangle', 0.05); + if(misses >= maxMisses){ + endGame(); + return; + } + } + } + + // draw vase + const vx = vase.x; + const vy = vase.baseY; + const vw = vase.width + vase.grow * 6; + const vh = vase.height + vase.grow * 1.5; + // soft shadow + ctx.beginPath(); + ctx.ellipse(vx, vy + vh*0.3, vw*0.6, vh*0.5, 0, 0, Math.PI*2); + ctx.fillStyle = 'rgba(0,0,0,0.08)'; ctx.fill(); + + // body of vase โ€” nice rounded container + ctx.save(); + ctx.translate(vx, vy); + ctx.fillStyle = '#ffffff'; + ctx.strokeStyle = 'rgba(0,0,0,0.06)'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(-vw/2, 0); + ctx.quadraticCurveTo(-vw/2 + 6, -vh, 0, -vh - vh*0.3); + ctx.quadraticCurveTo(vw/2 - 6, -vh, vw/2, 0); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + ctx.restore(); + + // tiny plants / bloom hint when combo high + if(combo > 1){ + ctx.save(); + ctx.translate(vx, vy - vh - 6); + ctx.fillStyle = 'rgba(80,200,120,0.06)'; + ctx.beginPath(); + ctx.ellipse(0,0,18 + combo*0.8,6 + combo*0.2,0,0,Math.PI*2); + ctx.fill(); + ctx.restore(); + } + + requestAnimationFrame(frame); + } + + function endGame(){ + running = false; + overlay.classList.remove('hidden'); + finalText.textContent = `You caught ${combo} in a row โ€” nice!`; + } + + restartBtn.addEventListener('click', ()=>{ + // reset state and start again + overlay.classList.add('hidden'); + petals = []; combo = 0; misses = 0; vase.grow = 0; + comboEl.textContent = combo; missesEl.textContent = misses; + lastSpawn = performance.now(); spawnInterval = 700; running = true; lastTime = performance.now(); + requestAnimationFrame(frame); + }); + + // initial spawn to make the scene lively right away + for(let i=0;i<3;i++) spawnPetal(); + requestAnimationFrame(frame); + + // small hint: resume audio on first interaction (some browsers block audio) + document.addEventListener('pointerdown', ()=>{ if(audioCtx && audioCtx.state==='suspended') audioCtx.resume(); else ensureAudio(); }, {once:true}); +})(); diff --git a/games/bloom-catch/style.css b/games/bloom-catch/style.css new file mode 100644 index 00000000..ec6364e9 --- /dev/null +++ b/games/bloom-catch/style.css @@ -0,0 +1,35 @@ +/* Mobile-first cozy styles */ +:root{ + --bg1: #fffbf2; + --bg2: #f5eafd; + --accent: #a084ff; + --text: #2b2b2b; +} +*{box-sizing:border-box} +html,body,#game-wrap{height:100%;} +body{ + margin:0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; + color:var(--text); + background: linear-gradient(180deg,var(--bg1), var(--bg2)); + -webkit-tap-highlight-color: transparent; +} +#game-wrap{position:relative;overflow:hidden} +canvas{display:block;width:100%;height:100vh} + +/* UI */ +#ui{position:absolute;top:12px;left:12px;right:12px;display:flex;justify-content:space-between;align-items:center;padding:6px 10px} +#score, #high-score, #misses{background:rgba(255,255,255,0.6);backdrop-filter:blur(6px);padding:6px 10px;border-radius:14px;font-weight:600} + +/* overlay */ +#overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:linear-gradient(180deg,rgba(255,255,255,0.0),rgba(245,240,255,0.2));} +#overlay.hidden{display:none} +#overlay .panel{background:rgba(255,255,255,0.92);padding:22px;border-radius:16px;text-align:center;box-shadow:0 6px 20px rgba(10,10,20,0.08)} +#overlay h1{margin:0 0 6px;font-size:20px;color:var(--accent)} +#overlay p{margin:0 0 14px} +#overlay button{background:var(--accent);color:white;border:0;padding:10px 16px;border-radius:10px;font-weight:700} + +/* small vase hint for accessibility when canvas is off-screen */ +@media(min-width:700px){ + #overlay .panel{width:420px} +} diff --git a/games/boom-runner/index.html b/games/boom-runner/index.html new file mode 100644 index 00000000..9e8346ad --- /dev/null +++ b/games/boom-runner/index.html @@ -0,0 +1,31 @@ + + + + + + Boom Runner | Mini JS Games Hub + + + +
+

Boom Runner ๐Ÿƒโ€โ™‚๏ธ๐Ÿ’ฅ

+ +
+ + + +
+
+ Score: 0 + Lives: 3 +
+
+ + + + + + + + + diff --git a/games/boom-runner/script.js b/games/boom-runner/script.js new file mode 100644 index 00000000..b5d0a0c6 --- /dev/null +++ b/games/boom-runner/script.js @@ -0,0 +1,141 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); + +const startBtn = document.getElementById('startBtn'); +const pauseBtn = document.getElementById('pauseBtn'); +const restartBtn = document.getElementById('restartBtn'); + +const jumpSound = document.getElementById('jumpSound'); +const hitSound = document.getElementById('hitSound'); +const bgMusic = document.getElementById('bgMusic'); + +let player = { x: 50, y: 300, width: 40, height: 40, color: '#0ff', dy: 0, gravity: 0.7, jumpPower: -12 }; +let obstacles = []; +let score = 0; +let lives = 3; +let gameInterval; +let isPaused = false; +let gameSpeed = 5; + +// Create random obstacle +function createObstacle() { + const height = Math.random() * 50 + 20; + const y = canvas.height - height; + obstacles.push({ x: canvas.width, y: y, width: 30, height: height, color: '#f00' }); +} + +// Draw player +function drawPlayer() { + ctx.fillStyle = player.color; + ctx.shadowColor = '#0ff'; + ctx.shadowBlur = 20; + ctx.fillRect(player.x, player.y, player.width, player.height); +} + +// Draw obstacles +function drawObstacles() { + obstacles.forEach(obs => { + ctx.fillStyle = obs.color; + ctx.shadowColor = '#f00'; + ctx.shadowBlur = 15; + ctx.fillRect(obs.x, obs.y, obs.width, obs.height); + }); +} + +// Update obstacles +function updateObstacles() { + obstacles.forEach(obs => obs.x -= gameSpeed); + obstacles = obstacles.filter(obs => obs.x + obs.width > 0); +} + +// Collision detection +function checkCollision() { + for (let obs of obstacles) { + if (player.x < obs.x + obs.width && + player.x + player.width > obs.x && + player.y < obs.y + obs.height && + player.y + player.height > obs.y) { + lives--; + hitSound.play(); + obstacles = []; + if(lives <= 0) { + alert('Game Over! Score: ' + score); + resetGame(); + } + } + } +} + +// Draw score & lives +function drawScore() { + document.getElementById('score').textContent = score; + document.getElementById('lives').textContent = lives; +} + +// Update game +function updateGame() { + if(isPaused) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Update player + player.dy += player.gravity; + player.y += player.dy; + if(player.y + player.height > canvas.height) { + player.y = canvas.height - player.height; + player.dy = 0; + } + + // Random obstacles + if(Math.random() < 0.02) createObstacle(); + updateObstacles(); + + // Draw everything + drawPlayer(); + drawObstacles(); + checkCollision(); + drawScore(); + + score++; + requestAnimationFrame(updateGame); +} + +// Controls +document.addEventListener('keydown', e => { + if(e.code === 'Space' || e.code === 'ArrowUp') { + if(player.y + player.height >= canvas.height) { + player.dy = player.jumpPower; + jumpSound.play(); + } + } +}); + +startBtn.addEventListener('click', () => { + if(!gameInterval) { + bgMusic.play(); + updateGame(); + } +}); + +pauseBtn.addEventListener('click', () => { + isPaused = !isPaused; + if(isPaused) bgMusic.pause(); + else bgMusic.play(); +}); + +restartBtn.addEventListener('click', () => { + resetGame(); + bgMusic.currentTime = 0; + bgMusic.play(); +}); + +// Reset game +function resetGame() { + player.y = 300; + player.dy = 0; + obstacles = []; + score = 0; + lives = 3; + isPaused = false; + updateGame(); +} diff --git a/games/boom-runner/style.css b/games/boom-runner/style.css new file mode 100644 index 00000000..78e041e7 --- /dev/null +++ b/games/boom-runner/style.css @@ -0,0 +1,47 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: linear-gradient(to bottom, #1e1e2f, #3a3a5a); + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + text-align: center; +} + +canvas { + background: #111; + border: 3px solid #fff; + display: block; + margin: 20px auto; + border-radius: 15px; + box-shadow: 0 0 20px #0ff, 0 0 50px #0ff inset; +} + +.controls button { + padding: 10px 20px; + margin: 5px; + font-size: 16px; + cursor: pointer; + border: none; + border-radius: 8px; + background: linear-gradient(45deg, #0ff, #0aa); + color: #000; + font-weight: bold; + box-shadow: 0 0 10px #0ff; + transition: all 0.2s ease-in-out; +} +.controls button:hover { + transform: scale(1.1); +} + +.score-board { + display: flex; + justify-content: space-around; + margin-top: 10px; + font-size: 18px; +} diff --git a/games/boom/index.html b/games/boom/index.html new file mode 100644 index 00000000..25b5c06b --- /dev/null +++ b/games/boom/index.html @@ -0,0 +1,31 @@ + + + + + + Boom | Mini JS Games Hub + + + +
+
+

Boom ๐Ÿ’ฃ

+

Click the bombs before they explode!

+
+ Score: 0 + High Score: 0 + Level: 1 +
+
+ +
+ +
+ + +
+
+ + + + diff --git a/games/boom/script.js b/games/boom/script.js new file mode 100644 index 00000000..0ff266a9 --- /dev/null +++ b/games/boom/script.js @@ -0,0 +1,91 @@ +const gameArea = document.getElementById("game-area"); +const startBtn = document.getElementById("start-btn"); +const restartBtn = document.getElementById("restart-btn"); +const scoreEl = document.getElementById("score"); +const highScoreEl = document.getElementById("high-score"); +const levelEl = document.getElementById("level"); + +let score = 0; +let highScore = localStorage.getItem("boomHighScore") || 0; +let level = 1; +let bombs = []; +let gameInterval; +let spawnInterval; + +highScoreEl.textContent = highScore; + +function spawnBomb() { + const bomb = document.createElement("div"); + bomb.classList.add("bomb"); + bomb.textContent = "๐Ÿ’ฃ"; + + // Random horizontal position + const x = Math.floor(Math.random() * (gameArea.clientWidth - 50)); + bomb.style.left = `${x}px`; + bomb.style.bottom = `0px`; + + // Click event + bomb.addEventListener("click", () => { + score += 10; + scoreEl.textContent = score; + bomb.remove(); + bombs = bombs.filter(b => b !== bomb); + }); + + gameArea.appendChild(bomb); + bombs.push(bomb); + + // Remove bomb after 5s if not clicked (explosion) + setTimeout(() => { + if (bombs.includes(bomb)) { + bomb.remove(); + bombs = bombs.filter(b => b !== bomb); + score = Math.max(0, score - 5); // penalty + scoreEl.textContent = score; + } + }, 5000 - level * 300); // faster as level increases +} + +function startGame() { + startBtn.disabled = true; + restartBtn.disabled = false; + score = 0; + level = 1; + scoreEl.textContent = score; + levelEl.textContent = level; + + // Spawn bombs every 1s initially + spawnInterval = setInterval(spawnBomb, Math.max(500, 1000 - level * 100)); + + // Increase level every 20 seconds + gameInterval = setInterval(() => { + level++; + levelEl.textContent = level; + }, 20000); +} + +function restartGame() { + // Clear bombs + bombs.forEach(b => b.remove()); + bombs = []; + + clearInterval(spawnInterval); + clearInterval(gameInterval); + + // Update high score + if (score > highScore) { + highScore = score; + localStorage.setItem("boomHighScore", highScore); + highScoreEl.textContent = highScore; + } + + startBtn.disabled = false; + restartBtn.disabled = true; + score = 0; + level = 1; + scoreEl.textContent = score; + levelEl.textContent = level; +} + +startBtn.addEventListener("click", startGame); +restartBtn.addEventListener("click", restartGame); diff --git a/games/boom/style.css b/games/boom/style.css new file mode 100644 index 00000000..0b61674c --- /dev/null +++ b/games/boom/style.css @@ -0,0 +1,98 @@ +/* General styles */ +body { + margin: 0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + background-color: rgba(255,255,255,0.95); + padding: 20px 40px; + border-radius: 15px; + text-align: center; + box-shadow: 0 10px 25px rgba(0,0,0,0.2); + width: 90%; + max-width: 600px; +} + +header h1 { + font-size: 2rem; + margin-bottom: 5px; +} + +header p { + font-size: 1rem; + margin-bottom: 15px; +} + +.score-board { + display: flex; + justify-content: space-around; + margin-bottom: 15px; + font-weight: bold; +} + +.game-area { + position: relative; + width: 100%; + height: 400px; + background-color: #f1f1f1; + border-radius: 10px; + overflow: hidden; +} + +.bomb { + position: absolute; + width: 50px; + height: 50px; + background: radial-gradient(circle, #ff4e50, #f9d423); + border-radius: 50%; + cursor: pointer; + animation: float 5s linear infinite; + display: flex; + justify-content: center; + align-items: center; + font-size: 1.2rem; + color: white; + font-weight: bold; + text-shadow: 1px 1px 2px black; +} + +.bomb:hover { + transform: scale(1.2); +} + +@keyframes float { + 0% { transform: translateY(0px); } + 100% { transform: translateY(-400px); } +} + +.controls { + margin-top: 20px; +} + +button { + padding: 10px 20px; + margin: 0 10px; + border: none; + background-color: #ff4e50; + color: white; + font-size: 1rem; + border-radius: 5px; + cursor: pointer; + transition: 0.3s; +} + +button:hover:not(:disabled) { + background-color: #f9d423; + color: black; +} + +button:disabled { + background-color: #ccc; + cursor: not-allowed; +} diff --git a/games/bottle-flip-challenge/index.html b/games/bottle-flip-challenge/index.html new file mode 100644 index 00000000..c62c91e8 --- /dev/null +++ b/games/bottle-flip-challenge/index.html @@ -0,0 +1,28 @@ + + + + + +Bottle Flip Challenge | Mini JS Games Hub + + + +
+

Bottle Flip Challenge ๐Ÿผ

+ + +
+ + + +
+ +
+

Score: 0

+

Combo: 0

+
+
+ + + + diff --git a/games/bottle-flip-challenge/script.js b/games/bottle-flip-challenge/script.js new file mode 100644 index 00000000..80afb85c --- /dev/null +++ b/games/bottle-flip-challenge/script.js @@ -0,0 +1,108 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +const bottleImg = new Image(); +bottleImg.src = "https://i.ibb.co/4T7vNrd/bottle.png"; // online bottle image + +const flipSound = new Audio("https://www.soundjay.com/button/sounds/button-16.mp3"); +const successSound = new Audio("https://www.soundjay.com/button/sounds/button-10.mp3"); +const failSound = new Audio("https://www.soundjay.com/button/sounds/button-09.mp3"); + +let gameRunning = false; +let score = 0; +let combo = 0; + +let bottle = { + x: canvas.width / 2 - 25, + y: canvas.height - 80, + width: 50, + height: 80, + vx: 0, + vy: 0, + angle: 0, + angularVelocity: 0, + isFlipping: false, +}; + +const gravity = 0.6; + +function resetBottle() { + bottle.x = canvas.width / 2 - 25; + bottle.y = canvas.height - 80; + bottle.vx = 0; + bottle.vy = 0; + bottle.angle = 0; + bottle.angularVelocity = 0; + bottle.isFlipping = false; +} + +function drawBottle() { + ctx.save(); + ctx.translate(bottle.x + bottle.width / 2, bottle.y + bottle.height / 2); + ctx.rotate(bottle.angle); + ctx.drawImage(bottleImg, -bottle.width / 2, -bottle.height / 2, bottle.width, bottle.height); + ctx.restore(); +} + +function updateBottle() { + if (bottle.isFlipping) { + bottle.vy += gravity; + bottle.x += bottle.vx; + bottle.y += bottle.vy; + bottle.angle += bottle.angularVelocity; + + // Check landing + if (bottle.y + bottle.height >= canvas.height) { + if (Math.abs(bottle.angle % (2 * Math.PI)) < 0.2) { + score += 10 + combo * 5; + combo++; + successSound.play(); + } else { + combo = 0; + failSound.play(); + } + bottle.isFlipping = false; + bottle.vx = 0; + bottle.vy = 0; + bottle.angularVelocity = 0; + bottle.y = canvas.height - bottle.height; + bottle.angle = 0; + } + } +} + +function render() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawBottle(); + updateBottle(); + requestAnimationFrame(render); +} + +// Mouse / touch controls +let isDragging = false; +let dragStart = {x:0, y:0}; +canvas.addEventListener("mousedown", e => { if(!bottle.isFlipping){isDragging=true; dragStart={x:e.offsetX, y:e.offsetY};} }); +canvas.addEventListener("mousemove", e => { if(isDragging) { /* optional: draw aim arrow */ } }); +canvas.addEventListener("mouseup", e => { + if(isDragging){ + isDragging=false; + const dx = dragStart.x - e.offsetX; + const dy = dragStart.y - e.offsetY; + bottle.vx = dx / 5; + bottle.vy = dy / 5; + bottle.angularVelocity = dx / 50; + bottle.isFlipping = true; + flipSound.play(); + } +}); + +// Buttons +document.getElementById("startBtn").addEventListener("click", () => { gameRunning = true; render(); }); +document.getElementById("pauseBtn").addEventListener("click", () => { gameRunning = false; }); +document.getElementById("restartBtn").addEventListener("click", () => { resetBottle(); score=0; combo=0; }); + +// Score display +setInterval(() => { + document.getElementById("score").textContent = score; + document.getElementById("combo").textContent = combo; +}, 100); diff --git a/games/bottle-flip-challenge/style.css b/games/bottle-flip-challenge/style.css new file mode 100644 index 00000000..017bd6f7 --- /dev/null +++ b/games/bottle-flip-challenge/style.css @@ -0,0 +1,44 @@ +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(to bottom, #1e3c72, #2a5298); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + color: #fff; +} + +.game-container { + text-align: center; + position: relative; +} + +canvas { + border: 3px solid #fff; + border-radius: 20px; + background: url('https://i.ibb.co/XV7fTtF/background.png') no-repeat center/cover; + box-shadow: 0 0 20px #00ffff, 0 0 50px #00ffff inset; +} + +.controls button { + padding: 10px 20px; + margin: 5px; + font-size: 16px; + cursor: pointer; + border: none; + border-radius: 10px; + background-color: #00ffff; + color: #000; + box-shadow: 0 0 10px #00ffff; + transition: 0.2s; +} +.controls button:hover { + box-shadow: 0 0 20px #00ffff, 0 0 30px #00ffff inset; +} + +.score-board { + margin-top: 15px; + font-size: 18px; + text-shadow: 0 0 5px #00ffff; +} diff --git a/games/brain-lock/index.html b/games/brain-lock/index.html new file mode 100644 index 00000000..42253375 --- /dev/null +++ b/games/brain-lock/index.html @@ -0,0 +1,37 @@ + + + + + +Brain Lock | Mini JS Games Hub + + + +
+

Brain Lock

+

Unlock the lock by reproducing the correct sequence!

+ +
+ +
+
+
+
+
+
+ +
+ + + +
+ +

+
+ + + + + + + diff --git a/games/brain-lock/script.js b/games/brain-lock/script.js new file mode 100644 index 00000000..1638b4d3 --- /dev/null +++ b/games/brain-lock/script.js @@ -0,0 +1,81 @@ +const bulbs = Array.from(document.querySelectorAll('.bulb')); +const startBtn = document.getElementById('start-btn'); +const pauseBtn = document.getElementById('pause-btn'); +const restartBtn = document.getElementById('restart-btn'); +const messageEl = document.getElementById('message'); + +const successSound = document.getElementById('success-sound'); +const failSound = document.getElementById('fail-sound'); + +let sequence = []; +let userSequence = []; +let interval; +let running = false; + +function generateSequence(length = 5) { + const colors = ['#0ff', '#f0f', '#ff0', '#f00', '#0f0']; + return Array.from({ length }, () => colors[Math.floor(Math.random() * colors.length)]); +} + +function showSequence(seq) { + let i = 0; + interval = setInterval(() => { + if (i > 0) bulbs[i-1].style.backgroundColor = '#222'; + if (i >= seq.length) { + clearInterval(interval); + running = true; + messageEl.textContent = "Your turn: reproduce the sequence!"; + return; + } + bulbs[i].style.backgroundColor = seq[i]; + bulbs[i].classList.add('active'); + i++; + }, 700); +} + +function resetBulbs() { + bulbs.forEach(b => { + b.style.backgroundColor = '#222'; + b.classList.remove('active'); + }); +} + +bulbs.forEach((bulb, idx) => { + bulb.addEventListener('click', () => { + if (!running) return; + const color = sequence[userSequence.length]; + bulb.style.backgroundColor = color; + userSequence.push(color); + if (userSequence[userSequence.length-1] !== sequence[userSequence.length-1]) { + failSound.play(); + messageEl.textContent = "Wrong sequence! Try again."; + running = false; + } else if (userSequence.length === sequence.length) { + successSound.play(); + messageEl.textContent = "Unlocked! ๐ŸŽ‰"; + running = false; + } + }); +}); + +startBtn.addEventListener('click', () => { + sequence = generateSequence(); + userSequence = []; + resetBulbs(); + showSequence(sequence); +}); + +pauseBtn.addEventListener('click', () => { + clearInterval(interval); + running = false; + messageEl.textContent = "Paused"; +}); + +restartBtn.addEventListener('click', () => { + clearInterval(interval); + sequence = generateSequence(); + userSequence = []; + resetBulbs(); + messageEl.textContent = "Sequence restarted!"; + showSequence(sequence); +}); diff --git a/games/brain-lock/style.css b/games/brain-lock/style.css new file mode 100644 index 00000000..9886da4e --- /dev/null +++ b/games/brain-lock/style.css @@ -0,0 +1,56 @@ +body { + font-family: 'Arial', sans-serif; + background-color: #111; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.brainlock-container { + text-align: center; + max-width: 600px; + padding: 20px; + background: rgba(0,0,0,0.8); + border-radius: 15px; + box-shadow: 0 0 20px #0ff, 0 0 40px #0ff inset; +} + +.sequence-line { + display: flex; + justify-content: space-between; + margin: 30px 0; +} + +.bulb { + width: 50px; + height: 50px; + background-color: #222; + border-radius: 50%; + box-shadow: 0 0 10px #000 inset; + transition: all 0.3s ease; +} + +.bulb.active { + background-color: #0ff; + box-shadow: 0 0 20px #0ff, 0 0 40px #0ff inset; +} + +.controls button { + padding: 10px 20px; + margin: 5px; + font-size: 16px; + border: none; + border-radius: 8px; + cursor: pointer; + background-color: #0ff; + color: #111; + box-shadow: 0 0 10px #0ff; + transition: all 0.2s ease; +} + +.controls button:hover { + transform: scale(1.1); +} diff --git a/games/brick-breaker/index.html b/games/brick-breaker/index.html new file mode 100644 index 00000000..47624bc9 --- /dev/null +++ b/games/brick-breaker/index.html @@ -0,0 +1,25 @@ + + + + + + Brick Breaker Game + + + +
+

Brick Breaker

+
+
Score: 0
+
Lives: 3
+
+ + + + +
Press Start to Play!
+
+ + + + \ No newline at end of file diff --git a/games/brick-breaker/script.js b/games/brick-breaker/script.js new file mode 100644 index 00000000..310d4494 --- /dev/null +++ b/games/brick-breaker/script.js @@ -0,0 +1,239 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- Canvas Setup --- + const canvas = document.getElementById('gameCanvas'); + const ctx = canvas.getContext('2d'); + const GAME_WIDTH = canvas.width; + const GAME_HEIGHT = canvas.height; + + // --- DOM References --- + const scoreDisplay = document.getElementById('score'); + const livesDisplay = document.getElementById('lives'); + const startButton = document.getElementById('start-button'); + const gameMessage = document.getElementById('game-message'); + + // --- Game State --- + let score = 0; + let lives = 3; + let gameRunning = false; + let animationFrameId; + + // --- Paddle Properties --- + const PADDLE_HEIGHT = 10; + const PADDLE_WIDTH = 75; + let paddleX = (GAME_WIDTH - PADDLE_WIDTH) / 2; + let rightPressed = false; + let leftPressed = false; + + // --- Ball Properties --- + const BALL_RADIUS = 10; + let x = GAME_WIDTH / 2; + let y = GAME_HEIGHT - 30; + let dx = 2; // Initial horizontal speed + let dy = -2; // Initial vertical speed (moving up) + + // --- Brick Properties --- + const BRICK_ROW_COUNT = 5; + const BRICK_COLUMN_COUNT = 5; + const BRICK_WIDTH = 75; + const BRICK_HEIGHT = 20; + const BRICK_PADDING = 10; + const BRICK_OFFSET_TOP = 30; + const BRICK_OFFSET_LEFT = 30; + let bricks = []; + + // --- Core Functions --- + + function initializeBricks() { + bricks = []; + for (let c = 0; c < BRICK_COLUMN_COUNT; c++) { + bricks[c] = []; + for (let r = 0; r < BRICK_ROW_COUNT; r++) { + bricks[c][r] = { x: 0, y: 0, status: 1 }; // status 1 = visible + } + } + } + + function drawBricks() { + for (let c = 0; c < BRICK_COLUMN_COUNT; c++) { + for (let r = 0; r < BRICK_ROW_COUNT; r++) { + if (bricks[c][r].status === 1) { + const brickX = (c * (BRICK_WIDTH + BRICK_PADDING)) + BRICK_OFFSET_LEFT; + const brickY = (r * (BRICK_HEIGHT + BRICK_PADDING)) + BRICK_OFFSET_TOP; + + bricks[c][r].x = brickX; + bricks[c][r].y = brickY; + + ctx.beginPath(); + ctx.rect(brickX, brickY, BRICK_WIDTH, BRICK_HEIGHT); + ctx.fillStyle = "#0095DD"; // Blue bricks + ctx.fill(); + ctx.closePath(); + } + } + } + } + + function drawBall() { + ctx.beginPath(); + ctx.arc(x, y, BALL_RADIUS, 0, Math.PI * 2); + ctx.fillStyle = "#FF4500"; // Orange ball + ctx.fill(); + ctx.closePath(); + } + + function drawPaddle() { + ctx.beginPath(); + ctx.rect(paddleX, GAME_HEIGHT - PADDLE_HEIGHT, PADDLE_WIDTH, PADDLE_HEIGHT); + ctx.fillStyle = "#4CAF50"; // Green paddle + ctx.fill(); + ctx.closePath(); + } + + function updateScoreAndLives() { + scoreDisplay.textContent = score; + livesDisplay.textContent = lives; + } + + function collisionDetection() { + for (let c = 0; c < BRICK_COLUMN_COUNT; c++) { + for (let r = 0; r < BRICK_ROW_COUNT; r++) { + const b = bricks[c][r]; + if (b.status === 1) { + // Check if ball center is inside brick boundaries + if (x > b.x && x < b.x + BRICK_WIDTH && y > b.y && y < b.y + BRICK_HEIGHT) { + dy = -dy; // Reverse vertical direction + b.status = 0; // Mark brick as hit/destroyed + score += 10; // Increment score + updateScoreAndLives(); + + if (score === BRICK_ROW_COUNT * BRICK_COLUMN_COUNT * 10) { + gameOver(true); + return; + } + } + } + } + } + } + + function movePaddle() { + if (rightPressed && paddleX < GAME_WIDTH - PADDLE_WIDTH) { + paddleX += 7; + } else if (leftPressed && paddleX > 0) { + paddleX -= 7; + } + } + + function draw() { + if (!gameRunning) return; + + // Clear canvas for next frame + ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT); + + drawBricks(); + drawBall(); + drawPaddle(); + collisionDetection(); + movePaddle(); + + // --- Ball Movement and Wall/Paddle Bounce --- + + // 1. Boundary bounce (Left/Right walls) + if (x + dx > GAME_WIDTH - BALL_RADIUS || x + dx < BALL_RADIUS) { + dx = -dx; + } + + // 2. Boundary bounce (Top wall) + if (y + dy < BALL_RADIUS) { + dy = -dy; + } + // 3. Paddle/Bottom logic + else if (y + dy > GAME_HEIGHT - BALL_RADIUS - PADDLE_HEIGHT) { + // Check collision with paddle + if (x > paddleX && x < paddleX + PADDLE_WIDTH) { + dy = -dy; + } else { + // Ball hits the bottom (missed paddle) + lives--; + updateScoreAndLives(); + + if (lives === 0) { + gameOver(false); + } else { + // Reset ball position for next life + x = GAME_WIDTH / 2; + y = GAME_HEIGHT - 30; + dx = 2; + dy = -2; + paddleX = (GAME_WIDTH - PADDLE_WIDTH) / 2; + } + } + } + + // Update ball position + x += dx; + y += dy; + + // Request next frame + animationFrameId = requestAnimationFrame(draw); + } + + function startGame() { + if (gameRunning) return; + + // Reset full game state + score = 0; + lives = 3; + initializeBricks(); + + // Reset ball/paddle position + x = GAME_WIDTH / 2; + y = GAME_HEIGHT - 30; + dx = 2; + dy = -2; + paddleX = (GAME_WIDTH - PADDLE_WIDTH) / 2; + + updateScoreAndLives(); + gameMessage.textContent = ''; + startButton.style.display = 'none'; + + gameRunning = true; + draw(); // Start the game loop + } + + function gameOver(win) { + gameRunning = false; + cancelAnimationFrame(animationFrameId); + gameMessage.textContent = win ? 'YOU WIN! ๐ŸŽ‰ Final Score: ' + score : 'GAME OVER! ๐Ÿ˜ญ Final Score: ' + score; + startButton.textContent = 'Play Again'; + startButton.style.display = 'block'; + } + + + // --- Event Handlers --- + + function keyDownHandler(e) { + if (e.key === "Right" || e.key === "ArrowRight") { + rightPressed = true; + } else if (e.key === "Left" || e.key === "ArrowLeft") { + leftPressed = true; + } + } + + function keyUpHandler(e) { + if (e.key === "Right" || e.key === "ArrowRight") { + rightPressed = false; + } else if (e.key === "Left" || e.key === "ArrowLeft") { + leftPressed = false; + } + } + + // --- Setup --- + document.addEventListener("keydown", keyDownHandler, false); + document.addEventListener("keyup", keyUpHandler, false); + startButton.addEventListener('click', startGame); + + // Initial setup message + gameMessage.textContent = 'Press Start to Play!'; + updateScoreAndLives(); +}); \ No newline at end of file diff --git a/games/brick-breaker/style.css b/games/brick-breaker/style.css new file mode 100644 index 00000000..829a8b73 --- /dev/null +++ b/games/brick-breaker/style.css @@ -0,0 +1,47 @@ +body { + font-family: Arial, sans-serif; + display: flex; + flex-direction: column; + align-items: center; + background-color: #282c34; + color: #fff; + margin-top: 20px; +} + +.game-container { + display: flex; + flex-direction: column; + align-items: center; +} + +#status { + width: 400px; + display: flex; + justify-content: space-between; + margin-bottom: 10px; + font-size: 1.2em; + padding: 0 5px; +} + +#gameCanvas { + background-color: #1c1f24; + border: 3px solid #61dafb; + display: block; +} + +#game-message { + font-size: 1.5em; + color: yellow; + margin-top: 15px; +} + +button { + padding: 10px 20px; + font-size: 1.2em; + cursor: pointer; + background-color: #4CAF50; + color: white; + border: none; + border-radius: 5px; + margin-top: 15px; +} \ No newline at end of file diff --git a/games/bridge-puzzle/index.html b/games/bridge-puzzle/index.html new file mode 100644 index 00000000..c34548ad --- /dev/null +++ b/games/bridge-puzzle/index.html @@ -0,0 +1,36 @@ + + + + + + Bridge Puzzle | Mini JS Games Hub + + + + +
+
+

๐ŸŒ‰ Bridge Puzzle

+
+ + + +
+
+ + + +
+

Connect all islands with glowing bridges!

+

+
+
+ + + + + + + + + diff --git a/games/bridge-puzzle/script.js b/games/bridge-puzzle/script.js new file mode 100644 index 00000000..30724517 --- /dev/null +++ b/games/bridge-puzzle/script.js @@ -0,0 +1,138 @@ +const canvas = document.getElementById("bridgeCanvas"); +const ctx = canvas.getContext("2d"); +canvas.width = 800; +canvas.height = 500; + +const bgMusic = document.getElementById("bgMusic"); +const connectSound = document.getElementById("connectSound"); +const winSound = document.getElementById("winSound"); + +let islands = [ + { x: 100, y: 150, connected: [] }, + { x: 700, y: 150, connected: [] }, + { x: 200, y: 400, connected: [] }, + { x: 600, y: 400, connected: [] }, +]; +let obstacles = [ + { x: 350, y: 250, r: 50 }, +]; +let bridges = []; +let isPaused = false; +let selectedIsland = null; + +function drawIsland(island) { + ctx.beginPath(); + ctx.arc(island.x, island.y, 25, 0, Math.PI * 2); + ctx.fillStyle = "#fffb96"; + ctx.fill(); + ctx.strokeStyle = "#ffea00"; + ctx.lineWidth = 3; + ctx.stroke(); + ctx.shadowColor = "#ffff88"; + ctx.shadowBlur = 20; + ctx.closePath(); +} + +function drawObstacle(obs) { + ctx.beginPath(); + ctx.arc(obs.x, obs.y, obs.r, 0, Math.PI * 2); + ctx.fillStyle = "rgba(255,0,0,0.3)"; + ctx.fill(); + ctx.strokeStyle = "#ff0000"; + ctx.stroke(); + ctx.closePath(); +} + +function drawBridge(a, b) { + ctx.beginPath(); + ctx.moveTo(a.x, a.y); + ctx.lineTo(b.x, b.y); + ctx.strokeStyle = "#00ffff"; + ctx.lineWidth = 4; + ctx.shadowColor = "#00ffff"; + ctx.shadowBlur = 15; + ctx.stroke(); + ctx.closePath(); +} + +function drawAll() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + obstacles.forEach(drawObstacle); + bridges.forEach(([a, b]) => drawBridge(a, b)); + islands.forEach(drawIsland); +} + +function isLineCrossObstacle(a, b, obs) { + const dx = b.x - a.x; + const dy = b.y - a.y; + const fx = a.x - obs.x; + const fy = a.y - obs.y; + const a1 = dx * dx + dy * dy; + const b1 = 2 * (fx * dx + fy * dy); + const c1 = (fx * fx + fy * fy) - obs.r * obs.r; + const discriminant = b1 * b1 - 4 * a1 * c1; + return discriminant >= 0; +} + +function checkWin() { + let connected = new Set(); + function dfs(i) { + connected.add(i); + islands[i].connected.forEach(j => { + if (!connected.has(j)) dfs(j); + }); + } + dfs(0); + if (connected.size === islands.length) { + document.getElementById("message").textContent = "๐ŸŽ‰ All islands connected!"; + winSound.play(); + } +} + +canvas.addEventListener("click", (e) => { + if (isPaused) return; + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const clicked = islands.find(i => Math.hypot(i.x - x, i.y - y) < 25); + + if (clicked) { + if (!selectedIsland) { + selectedIsland = clicked; + } else if (selectedIsland !== clicked) { + // Check for obstacle collision + if (!obstacles.some(obs => isLineCrossObstacle(selectedIsland, clicked, obs))) { + bridges.push([selectedIsland, clicked]); + selectedIsland.connected.push(islands.indexOf(clicked)); + clicked.connected.push(islands.indexOf(selectedIsland)); + connectSound.play(); + checkWin(); + } else { + document.getElementById("message").textContent = "๐Ÿšซ Bridge blocked by obstacle!"; + } + selectedIsland = null; + } + } + drawAll(); +}); + +document.getElementById("startBtn").addEventListener("click", () => { + bgMusic.play(); + document.getElementById("message").textContent = "๐ŸŽฎ Game Started!"; + drawAll(); +}); + +document.getElementById("pauseBtn").addEventListener("click", () => { + isPaused = !isPaused; + document.getElementById("pauseBtn").textContent = isPaused ? "โ–ถ Resume" : "โธ Pause"; + document.getElementById("message").textContent = isPaused ? "โธ Game Paused" : "๐ŸŽฎ Game Resumed"; +}); + +document.getElementById("restartBtn").addEventListener("click", () => { + bridges = []; + islands.forEach(i => (i.connected = [])); + drawAll(); + document.getElementById("message").textContent = "๐Ÿ” Game Restarted!"; +}); + +drawAll(); diff --git a/games/bridge-puzzle/style.css b/games/bridge-puzzle/style.css new file mode 100644 index 00000000..af51e6dd --- /dev/null +++ b/games/bridge-puzzle/style.css @@ -0,0 +1,64 @@ +body { + margin: 0; + font-family: 'Poppins', sans-serif; + background: radial-gradient(circle at center, #111, #000); + overflow: hidden; + color: white; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + text-align: center; + width: 90vw; + height: 90vh; + position: relative; +} + +header { + display: flex; + justify-content: space-between; + align-items: center; + background: rgba(255, 255, 255, 0.05); + padding: 10px 20px; + border-radius: 10px; + backdrop-filter: blur(5px); +} + +button { + background: linear-gradient(135deg, #00c3ff, #0078ff); + border: none; + padding: 8px 14px; + color: white; + font-size: 16px; + margin: 0 5px; + border-radius: 8px; + cursor: pointer; + transition: 0.3s; +} + +button:hover { + background: linear-gradient(135deg, #00ffbf, #00b4ff); + box-shadow: 0 0 10px #00ffbf; +} + +#bridgeCanvas { + background: url('https://images.unsplash.com/photo-1503264116251-35a269479413?auto=format&fit=crop&w=800&q=80') center/cover; + display: block; + margin: 20px auto; + border: 2px solid #0ff; + border-radius: 10px; + box-shadow: 0 0 20px #0ff; +} + +#status { + margin-top: 10px; +} + +.glow-line { + stroke: #00ffff; + stroke-width: 5; + filter: drop-shadow(0 0 6px cyan); +} diff --git a/games/bubble-harmony/index.html b/games/bubble-harmony/index.html new file mode 100644 index 00000000..d60a293a --- /dev/null +++ b/games/bubble-harmony/index.html @@ -0,0 +1,24 @@ + + + + + + Bubble Harmony | Mini JS Games Hub + + + +
+

Bubble Harmony ๐ŸŽต

+
+ + + + Score: 0 +
+ + + +
+ + + diff --git a/games/bubble-harmony/script.js b/games/bubble-harmony/script.js new file mode 100644 index 00000000..20d4a165 --- /dev/null +++ b/games/bubble-harmony/script.js @@ -0,0 +1,131 @@ +const canvas = document.getElementById('game-canvas'); +const ctx = canvas.getContext('2d'); + +const scoreEl = document.getElementById('score'); +const startBtn = document.getElementById('start-btn'); +const pauseBtn = document.getElementById('pause-btn'); +const restartBtn = document.getElementById('restart-btn'); + +const bgMusic = document.getElementById('background-music'); +const popSound = document.getElementById('pop-sound'); + +let bubbles = []; +let obstacles = []; +let animationId; +let score = 0; +let gameRunning = false; + +function random(min, max) { + return Math.random() * (max - min) + min; +} + +class Bubble { + constructor() { + this.x = random(50, canvas.width - 50); + this.y = canvas.height + 50; + this.radius = random(20, 40); + this.speed = random(1, 3); + this.color = `hsl(${random(0, 360)}, 100%, 60%)`; + } + + draw() { + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.shadowColor = this.color; + ctx.shadowBlur = 20; + ctx.fill(); + } + + update() { + this.y -= this.speed; + if (this.y + this.radius < 0) { + this.y = canvas.height + this.radius; + this.x = random(50, canvas.width - 50); + } + this.draw(); + } +} + +class Obstacle { + constructor() { + this.x = random(0, canvas.width - 30); + this.y = random(50, canvas.height - 200); + this.width = 30; + this.height = 30; + this.color = 'red'; + } + + draw() { + ctx.fillStyle = this.color; + ctx.fillRect(this.x, this.y, this.width, this.height); + } +} + +function spawnBubbles() { + if (bubbles.length < 8) bubbles.push(new Bubble()); +} + +function spawnObstacles() { + if (obstacles.length < 3) obstacles.push(new Obstacle()); +} + +canvas.addEventListener('click', (e) => { + if (!gameRunning) return; + const rect = canvas.getBoundingClientRect(); + const clickX = e.clientX - rect.left; + const clickY = e.clientY - rect.top; + + for (let i = bubbles.length - 1; i >= 0; i--) { + const bubble = bubbles[i]; + const dist = Math.hypot(clickX - bubble.x, clickY - bubble.y); + if (dist < bubble.radius) { + bubbles.splice(i, 1); + score += 10; + scoreEl.textContent = score; + popSound.currentTime = 0; + popSound.play(); + } + } +}); + +function animate() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + spawnBubbles(); + spawnObstacles(); + + bubbles.forEach(b => b.update()); + obstacles.forEach(o => o.draw()); + + animationId = requestAnimationFrame(animate); +} + +startBtn.addEventListener('click', () => { + if (!gameRunning) { + gameRunning = true; + bgMusic.play(); + animate(); + } +}); + +pauseBtn.addEventListener('click', () => { + if (gameRunning) { + gameRunning = false; + bgMusic.pause(); + cancelAnimationFrame(animationId); + } +}); + +restartBtn.addEventListener('click', () => { + gameRunning = false; + bgMusic.pause(); + bgMusic.currentTime = 0; + bubbles = []; + obstacles = []; + score = 0; + scoreEl.textContent = score; + cancelAnimationFrame(animationId); + animate(); + gameRunning = true; + bgMusic.play(); +}); diff --git a/games/bubble-harmony/style.css b/games/bubble-harmony/style.css new file mode 100644 index 00000000..3a6ba34c --- /dev/null +++ b/games/bubble-harmony/style.css @@ -0,0 +1,47 @@ +body { + font-family: Arial, sans-serif; + background-color: #111; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.game-container { + text-align: center; +} + +h1 { + margin-bottom: 10px; + color: #ff79c6; + text-shadow: 0 0 10px #ff79c6, 0 0 20px #ff79c6; +} + +.controls { + margin-bottom: 10px; +} + +button { + padding: 8px 16px; + margin: 0 5px; + font-size: 16px; + cursor: pointer; + border: none; + border-radius: 5px; + background: linear-gradient(45deg, #ff79c6, #8be9fd); + color: #111; + box-shadow: 0 0 10px #ff79c6, 0 0 20px #8be9fd; + transition: transform 0.1s; +} + +button:hover { + transform: scale(1.1); +} + +canvas { + background: #222; + border-radius: 10px; + box-shadow: 0 0 20px #ff79c6, 0 0 40px #8be9fd; +} diff --git a/games/bubble-pop-adventure/index.html b/games/bubble-pop-adventure/index.html new file mode 100644 index 00000000..eb88a9c5 --- /dev/null +++ b/games/bubble-pop-adventure/index.html @@ -0,0 +1,21 @@ + + + + + + Bubble Pop Adventure + + + +
+

Bubble Pop Adventure

+
Score: 0
+ +
+

Click on the bubbles to pop them and score points!

+

Red: 1 point | Blue: 2 points | Green: 3 points | Yellow: 5 points | Purple: 10 points

+
+
+ + + \ No newline at end of file diff --git a/games/bubble-pop-adventure/script.js b/games/bubble-pop-adventure/script.js new file mode 100644 index 00000000..ae876e90 --- /dev/null +++ b/games/bubble-pop-adventure/script.js @@ -0,0 +1,98 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const scoreElement = document.getElementById('score'); + +let score = 0; +let bubbles = []; +let gameRunning = true; + +const colors = [ + { color: 'red', points: 1 }, + { color: 'blue', points: 2 }, + { color: 'green', points: 3 }, + { color: 'yellow', points: 5 }, + { color: 'purple', points: 10 } +]; + +class Bubble { + constructor(x, y, radius, color, points) { + this.x = x; + this.y = y; + this.radius = radius; + this.color = color; + this.points = points; + this.speed = Math.random() * 1 + 0.5; // Slow floating up + this.opacity = 0.7; + } + + update() { + this.y -= this.speed; + if (this.y + this.radius < 0) { + // Remove bubble if it goes off screen + bubbles = bubbles.filter(b => b !== this); + } + } + + draw() { + ctx.globalAlpha = this.opacity; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + ctx.strokeStyle = 'white'; + ctx.lineWidth = 2; + ctx.stroke(); + ctx.globalAlpha = 1; + } + + isClicked(mouseX, mouseY) { + const dx = mouseX - this.x; + const dy = mouseY - this.y; + return Math.sqrt(dx * dx + dy * dy) < this.radius; + } +} + +function createBubble() { + const x = Math.random() * (canvas.width - 100) + 50; + const y = canvas.height + 50; + const radius = Math.random() * 30 + 20; + const colorObj = colors[Math.floor(Math.random() * colors.length)]; + bubbles.push(new Bubble(x, y, radius, colorObj.color, colorObj.points)); +} + +function update() { + bubbles.forEach(bubble => bubble.update()); +} + +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + bubbles.forEach(bubble => bubble.draw()); +} + +function gameLoop() { + if (gameRunning) { + update(); + draw(); + requestAnimationFrame(gameLoop); + } +} + +canvas.addEventListener('click', (e) => { + const rect = canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + bubbles.forEach((bubble, index) => { + if (bubble.isClicked(mouseX, mouseY)) { + score += bubble.points; + scoreElement.textContent = score; + bubbles.splice(index, 1); + } + }); +}); + +// Spawn bubbles every 2 seconds +setInterval(createBubble, 2000); + +// Start the game +gameLoop(); \ No newline at end of file diff --git a/games/bubble-pop-adventure/style.css b/games/bubble-pop-adventure/style.css new file mode 100644 index 00000000..6fddb097 --- /dev/null +++ b/games/bubble-pop-adventure/style.css @@ -0,0 +1,43 @@ +body { + font-family: Arial, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + margin: 0; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + color: white; +} + +.game-container { + text-align: center; + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +h1 { + margin-bottom: 10px; + font-size: 2.5em; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); +} + +.score { + font-size: 1.5em; + margin-bottom: 20px; +} + +canvas { + border: 2px solid white; + border-radius: 10px; + background: rgba(255, 255, 255, 0.1); + cursor: crosshair; +} + +.instructions { + margin-top: 20px; + font-size: 1.1em; + line-height: 1.5; +} \ No newline at end of file diff --git a/games/bubble-shooter/index.html b/games/bubble-shooter/index.html new file mode 100644 index 00000000..885d7b40 --- /dev/null +++ b/games/bubble-shooter/index.html @@ -0,0 +1,28 @@ + + + + + + Bubble Shooter + + + +
+
Score: 0
+ +
Next:
+
Level: 1
+
Click to aim and shoot bubbles. Match 3 or more of the same color to pop them!
+ + +
+ + + \ No newline at end of file diff --git a/games/bubble-shooter/script.js b/games/bubble-shooter/script.js new file mode 100644 index 00000000..1fd50217 --- /dev/null +++ b/games/bubble-shooter/script.js @@ -0,0 +1,306 @@ +const canvas = document.getElementById('game-canvas'); +const ctx = canvas.getContext('2d'); +const scoreEl = document.getElementById('score-value'); +const levelEl = document.getElementById('level-value'); +const nextColorEl = document.getElementById('next-color'); + +const colors = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF']; +const bubbleRadius = 15; +const rows = 8; +const cols = 12; +let bubbles = []; +let shooter = { x: canvas.width / 2, y: canvas.height - 30, angle: 0 }; +let currentBubble = null; +let nextBubble = null; +let score = 0; +let level = 1; +let gameRunning = true; + +function init() { + bubbles = []; + generateLevel(); + currentBubble = createBubble(shooter.x, shooter.y); + nextBubble = createBubble(0, 0); + updateNextColor(); + score = 0; + level = 1; + gameRunning = true; + scoreEl.textContent = score; + levelEl.textContent = level; + gameLoop(); +} + +function generateLevel() { + bubbles = []; + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols - (row % 2); col++) { + const x = col * bubbleRadius * 2 + (row % 2) * bubbleRadius + bubbleRadius; + const y = row * bubbleRadius * 1.7 + bubbleRadius; + if (Math.random() < 0.7) { // 70% chance to place bubble + bubbles.push({ + x, y, color: colors[Math.floor(Math.random() * colors.length)], + row, col, attached: true + }); + } + } + } +} + +function createBubble(x, y) { + return { + x, y, vx: 0, vy: 0, + color: colors[Math.floor(Math.random() * colors.length)], + attached: false + }; +} + +function update() { + if (!gameRunning) return; + + // Update current bubble + if (currentBubble) { + currentBubble.x += currentBubble.vx; + currentBubble.y += currentBubble.vy; + + // Bounce off walls + if (currentBubble.x <= bubbleRadius || currentBubble.x >= canvas.width - bubbleRadius) { + currentBubble.vx *= -1; + } + + // Check collision with top + if (currentBubble.y <= bubbleRadius) { + currentBubble.vy *= -1; + } + + // Check collision with other bubbles + for (let bubble of bubbles) { + if (bubble.attached) { + const dx = currentBubble.x - bubble.x; + const dy = currentBubble.y - bubble.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist < bubbleRadius * 2) { + attachBubble(currentBubble, bubble); + break; + } + } + } + + // Check if bubble went off screen + if (currentBubble.y > canvas.height) { + gameOver(); + } + } +} + +function attachBubble(bubble, target) { + bubble.attached = true; + bubble.vx = 0; + bubble.vy = 0; + + // Snap to nearest grid position + const row = Math.round((bubble.y - bubbleRadius) / (bubbleRadius * 1.7)); + const col = Math.round((bubble.x - (row % 2) * bubbleRadius - bubbleRadius) / (bubbleRadius * 2)); + bubble.x = col * bubbleRadius * 2 + (row % 2) * bubbleRadius + bubbleRadius; + bubble.y = row * bubbleRadius * 1.7 + bubbleRadius; + bubble.row = row; + bubble.col = col; + + bubbles.push(bubble); + + // Check for matches + const matches = findMatches(bubble); + if (matches.length >= 3) { + popBubbles(matches); + } else { + // Load next bubble + currentBubble = nextBubble; + currentBubble.x = shooter.x; + currentBubble.y = shooter.y; + nextBubble = createBubble(0, 0); + updateNextColor(); + } + + // Check level complete + if (bubbles.filter(b => b.attached).length === 0) { + levelComplete(); + } + + // Check game over + if (bubbles.some(b => b.attached && b.y > canvas.height - 100)) { + gameOver(); + } +} + +function findMatches(startBubble) { + const matches = []; + const visited = new Set(); + const queue = [startBubble]; + + while (queue.length > 0) { + const bubble = queue.shift(); + if (visited.has(bubble)) continue; + visited.add(bubble); + + matches.push(bubble); + + // Check adjacent bubbles + for (let other of bubbles) { + if (other.attached && other.color === bubble.color && !visited.has(other)) { + const dx = Math.abs(bubble.x - other.x); + const dy = Math.abs(bubble.y - other.y); + if (dx < bubbleRadius * 2.1 && dy < bubbleRadius * 2.1) { + queue.push(other); + } + } + } + } + + return matches; +} + +function popBubbles(matches) { + matches.forEach(bubble => { + const index = bubbles.indexOf(bubble); + if (index > -1) bubbles.splice(index, 1); + }); + score += matches.length * 10; + + // Check for floating bubbles + const attached = new Set(); + const toCheck = bubbles.filter(b => b.attached && b.y <= bubbleRadius * 2); + while (toCheck.length > 0) { + const bubble = toCheck.shift(); + attached.add(bubble); + for (let other of bubbles) { + if (other.attached && !attached.has(other)) { + const dx = Math.abs(bubble.x - other.x); + const dy = Math.abs(bubble.y - other.y); + if (dx < bubbleRadius * 2.1 && dy < bubbleRadius * 2.1) { + toCheck.push(other); + } + } + } + } + + // Remove floating bubbles + bubbles = bubbles.filter(b => attached.has(b) || !b.attached); + score += (bubbles.length - attached.size) * 5; + + scoreEl.textContent = score; +} + +function levelComplete() { + level++; + levelEl.textContent = level; + document.getElementById('level-complete').style.display = 'block'; + gameRunning = false; +} + +function gameOver() { + document.getElementById('game-over').style.display = 'block'; + document.getElementById('final-score').textContent = `Final Score: ${score}`; + gameRunning = false; +} + +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw bubbles + bubbles.forEach(bubble => { + if (bubble.attached) { + ctx.beginPath(); + ctx.arc(bubble.x, bubble.y, bubbleRadius, 0, Math.PI * 2); + ctx.fillStyle = bubble.color; + ctx.fill(); + ctx.strokeStyle = '#000'; + ctx.stroke(); + } + }); + + // Draw current bubble + if (currentBubble) { + ctx.beginPath(); + ctx.arc(currentBubble.x, currentBubble.y, bubbleRadius, 0, Math.PI * 2); + ctx.fillStyle = currentBubble.color; + ctx.fill(); + ctx.strokeStyle = '#000'; + ctx.stroke(); + } + + // Draw shooter + ctx.beginPath(); + ctx.arc(shooter.x, shooter.y, 10, 0, Math.PI * 2); + ctx.fillStyle = '#333'; + ctx.fill(); + + // Draw aim line + if (gameRunning) { + ctx.beginPath(); + ctx.moveTo(shooter.x, shooter.y); + ctx.lineTo(shooter.x + Math.cos(shooter.angle) * 50, shooter.y + Math.sin(shooter.angle) * 50); + ctx.strokeStyle = '#000'; + ctx.stroke(); + } +} + +function gameLoop() { + update(); + draw(); + requestAnimationFrame(gameLoop); +} + +// Input handling +canvas.addEventListener('mousemove', e => { + const rect = canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + shooter.angle = Math.atan2(mouseY - shooter.y, mouseX - shooter.x); +}); + +canvas.addEventListener('click', () => { + if (gameRunning && currentBubble && currentBubble.vx === 0 && currentBubble.vy === 0) { + const speed = 8; + currentBubble.vx = Math.cos(shooter.angle) * speed; + currentBubble.vy = Math.sin(shooter.angle) * speed; + } +}); + +// Touch controls +canvas.addEventListener('touchmove', e => { + e.preventDefault(); + const rect = canvas.getBoundingClientRect(); + const touchX = e.touches[0].clientX - rect.left; + const touchY = e.touches[0].clientY - rect.top; + shooter.angle = Math.atan2(touchY - shooter.y, touchX - shooter.x); +}); + +canvas.addEventListener('touchstart', e => { + e.preventDefault(); + if (gameRunning && currentBubble && currentBubble.vx === 0 && currentBubble.vy === 0) { + const speed = 8; + currentBubble.vx = Math.cos(shooter.angle) * speed; + currentBubble.vy = Math.sin(shooter.angle) * speed; + } +}); + +document.getElementById('restart-btn').addEventListener('click', () => { + document.getElementById('game-over').style.display = 'none'; + init(); +}); + +document.getElementById('next-level-btn').addEventListener('click', () => { + document.getElementById('level-complete').style.display = 'none'; + generateLevel(); + currentBubble = nextBubble; + currentBubble.x = shooter.x; + currentBubble.y = shooter.y; + nextBubble = createBubble(0, 0); + updateNextColor(); + gameRunning = true; +}); + +function updateNextColor() { + nextColorEl.style.backgroundColor = nextBubble.color; +} + +init(); \ No newline at end of file diff --git a/games/bubble-shooter/style.css b/games/bubble-shooter/style.css new file mode 100644 index 00000000..80ff2e24 --- /dev/null +++ b/games/bubble-shooter/style.css @@ -0,0 +1,77 @@ +body { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background: linear-gradient(to bottom, #87CEEB, #98FB98); + font-family: Arial, sans-serif; +} + +#game-container { + position: relative; + text-align: center; +} + +#game-canvas { + border: 2px solid #333; + background: #f0f8ff; + cursor: crosshair; +} + +#score, #level, #next-bubble { + font-size: 18px; + margin: 5px; + color: #333; +} + +#next-color { + display: inline-block; + width: 20px; + height: 20px; + border-radius: 50%; + margin-left: 5px; +} + +#instructions { + font-size: 14px; + color: #666; + margin-top: 10px; +} + +#game-over, #level-complete { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(255, 255, 255, 0.9); + padding: 20px; + border-radius: 10px; + text-align: center; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); +} + +#game-over h2, #level-complete h2 { + margin: 0 0 10px 0; + color: #333; +} + +#game-over p { + margin: 10px 0; + font-size: 18px; +} + +button { + background: #4CAF50; + color: white; + border: none; + padding: 10px 20px; + font-size: 16px; + border-radius: 5px; + cursor: pointer; + margin: 5px; +} + +button:hover { + background: #45a049; +} \ No newline at end of file diff --git a/games/bubble-wrap-popper/index.html b/games/bubble-wrap-popper/index.html new file mode 100644 index 00000000..58941915 --- /dev/null +++ b/games/bubble-wrap-popper/index.html @@ -0,0 +1,79 @@ + + + + + + Bubble Wrap Popper #997 - Mini JS Games Hub + + + + +
+
+
Score: 0
+
Level: 1
+
Time: 30.0s
+
Accuracy: 100%
+
+ + + +
+
Level 1: Grid
+
Pop all bubbles as fast as possible!
+
+ +
+
+

Bubble Wrap Popper #997

+

Pop virtual bubble wrap in increasingly complex patterns! Score points for speed and accuracy in this stress-relieving clicker game.

+ +

How to Play:

+
    +
  • Click: Click on bubbles to pop them
  • +
  • Goal: Pop all bubbles in each level as quickly as possible
  • +
  • Scoring: Faster completion = higher score bonus
  • +
  • Accuracy: Missing bubbles reduces your score
  • +
  • Levels: Each level introduces new patterns and challenges
  • +
+ +

Level Types:

+
    +
  • Grid: Simple grid of bubbles
  • +
  • Spiral: Bubbles arranged in a spiral pattern
  • +
  • Heart: Heart-shaped bubble arrangement
  • +
  • Random: Chaotic bubble placement
  • +
  • Wave: Sine wave pattern
  • +
  • Circle: Concentric circles
  • +
  • Lightning: Zigzag lightning bolt pattern
  • +
  • Star: Star-shaped formation
  • +
+ + +
+
+ + + + +
+ + + + \ No newline at end of file diff --git a/games/bubble-wrap-popper/script.js b/games/bubble-wrap-popper/script.js new file mode 100644 index 00000000..e248c07e --- /dev/null +++ b/games/bubble-wrap-popper/script.js @@ -0,0 +1,461 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const startButton = document.getElementById('startButton'); +const restartButton = document.getElementById('restartButton'); +const nextLevelButton = document.getElementById('nextLevelButton'); +const instructionsOverlay = document.getElementById('instructions-overlay'); +const levelCompleteOverlay = document.getElementById('level-complete-overlay'); +const gameOverOverlay = document.getElementById('game-over-overlay'); +const scoreElement = document.getElementById('score'); +const levelElement = document.getElementById('level'); +const timerElement = document.getElementById('timer'); +const accuracyElement = document.getElementById('accuracy'); +const levelTitleElement = document.getElementById('level-title'); +const levelDescriptionElement = document.getElementById('level-description'); + +canvas.width = 800; +canvas.height = 600; + +let gameRunning = false; +let levelComplete = false; +let gameOver = false; +let bubbles = []; +let currentLevel = 1; +let score = 0; +let levelScore = 0; +let timeLeft = 30.0; +let levelStartTime = 0; +let totalClicks = 0; +let successfulClicks = 0; +let gameTimer; + +const levelTypes = [ + { name: 'Grid', description: 'Simple grid of bubbles', generator: generateGrid }, + { name: 'Spiral', description: 'Bubbles in a spiral pattern', generator: generateSpiral }, + { name: 'Heart', description: 'Heart-shaped arrangement', generator: generateHeart }, + { name: 'Random', description: 'Chaotic bubble placement', generator: generateRandom }, + { name: 'Wave', description: 'Sine wave pattern', generator: generateWave }, + { name: 'Circle', description: 'Concentric circles', generator: generateCircle }, + { name: 'Lightning', description: 'Zigzag lightning bolt', generator: generateLightning }, + { name: 'Star', description: 'Star-shaped formation', generator: generateStar } +]; + +// Bubble class +class Bubble { + constructor(x, y, radius = 20) { + this.x = x; + this.y = y; + this.radius = radius; + this.popped = false; + this.popTime = 0; + this.glowPhase = Math.random() * Math.PI * 2; + this.color = this.getRandomColor(); + } + + getRandomColor() { + const colors = [ + '#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24', '#f0932b', + '#eb4d4b', '#6c5ce7', '#a29bfe', '#fd79a8', '#e17055' + ]; + return colors[Math.floor(Math.random() * colors.length)]; + } + + update() { + this.glowPhase += 0.05; + } + + draw() { + if (this.popped) return; + + ctx.save(); + + // Bubble shadow/glow + const glowIntensity = 0.3 + Math.sin(this.glowPhase) * 0.2; + ctx.shadowColor = this.color; + ctx.shadowBlur = 8 * glowIntensity; + + // Main bubble + ctx.fillStyle = this.color; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fill(); + + // Bubble highlight + ctx.shadowBlur = 0; + ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; + ctx.beginPath(); + ctx.arc(this.x - this.radius * 0.3, this.y - this.radius * 0.3, this.radius * 0.4, 0, Math.PI * 2); + ctx.fill(); + + // Bubble border + ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)'; + ctx.lineWidth = 2; + ctx.stroke(); + + ctx.restore(); + } + + pop() { + if (this.popped) return false; + + this.popped = true; + this.popTime = Date.now(); + + // Create pop effect + createPopEffect(this.x, this.y, this.color); + + return true; + } + + containsPoint(x, y) { + const dx = x - this.x; + const dy = y - this.y; + return dx * dx + dy * dy <= this.radius * this.radius; + } +} + +// Pop effect particles +class PopParticle { + constructor(x, y, color) { + this.x = x; + this.y = y; + this.vx = (Math.random() - 0.5) * 8; + this.vy = (Math.random() - 0.5) * 8 - 2; + this.life = 1.0; + this.color = color; + this.size = Math.random() * 4 + 2; + } + + update() { + this.x += this.vx; + this.y += this.vy; + this.vy += 0.2; // gravity + this.life -= 0.02; + this.size *= 0.98; + } + + draw() { + if (this.life <= 0) return; + + ctx.save(); + ctx.globalAlpha = this.life; + ctx.fillStyle = this.color; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + } +} + +let popParticles = []; + +// Level generators +function generateGrid() { + const cols = 8; + const rows = 6; + const spacing = 80; + const startX = (canvas.width - (cols - 1) * spacing) / 2; + const startY = (canvas.height - (rows - 1) * spacing) / 2; + + bubbles = []; + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const x = startX + col * spacing; + const y = startY + row * spacing; + bubbles.push(new Bubble(x, y)); + } + } +} + +function generateSpiral() { + bubbles = []; + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; + const turns = 3; + const pointsPerTurn = 12; + const maxRadius = Math.min(canvas.width, canvas.height) / 2 - 40; + + for (let i = 0; i < turns * pointsPerTurn; i++) { + const angle = (i / pointsPerTurn) * Math.PI * 2; + const radius = (i / (turns * pointsPerTurn)) * maxRadius; + const x = centerX + Math.cos(angle) * radius; + const y = centerY + Math.sin(angle) * radius; + bubbles.push(new Bubble(x, y)); + } +} + +function generateHeart() { + bubbles = []; + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; + const scale = 80; + + for (let t = 0; t < Math.PI * 2; t += 0.1) { + const x = centerX + scale * (16 * Math.sin(t) * Math.sin(t) * Math.sin(t)); + const y = centerY - scale * (13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)); + bubbles.push(new Bubble(x, y, 15)); + } +} + +function generateRandom() { + bubbles = []; + const count = 25 + currentLevel * 5; + + for (let i = 0; i < count; i++) { + const x = 50 + Math.random() * (canvas.width - 100); + const y = 50 + Math.random() * (canvas.height - 100); + bubbles.push(new Bubble(x, y, 15 + Math.random() * 10)); + } +} + +function generateWave() { + bubbles = []; + const amplitude = 60; + const frequency = 0.02; + const points = 20; + + for (let i = 0; i < points; i++) { + const x = 50 + (i / (points - 1)) * (canvas.width - 100); + const y = canvas.height / 2 + Math.sin(i * frequency) * amplitude; + bubbles.push(new Bubble(x, y)); + } +} + +function generateCircle() { + bubbles = []; + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; + const rings = 4; + const pointsPerRing = 8; + + for (let ring = 1; ring <= rings; ring++) { + const radius = ring * 60; + for (let i = 0; i < pointsPerRing; i++) { + const angle = (i / pointsPerRing) * Math.PI * 2; + const x = centerX + Math.cos(angle) * radius; + const y = centerY + Math.sin(angle) * radius; + bubbles.push(new Bubble(x, y, 25 - ring * 3)); + } + } +} + +function generateLightning() { + bubbles = []; + let x = 100; + let y = 100; + const segments = 15; + + for (let i = 0; i < segments; i++) { + bubbles.push(new Bubble(x, y)); + x += 40 + Math.random() * 20; + y += (Math.random() - 0.5) * 80; + + // Keep within bounds + if (y < 50) y = 50; + if (y > canvas.height - 50) y = canvas.height - 50; + } +} + +function generateStar() { + bubbles = []; + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; + const outerRadius = 100; + const innerRadius = 40; + const points = 5; + + for (let i = 0; i < points * 2; i++) { + const angle = (i / (points * 2)) * Math.PI * 2; + const radius = i % 2 === 0 ? outerRadius : innerRadius; + const x = centerX + Math.cos(angle) * radius; + const y = centerY + Math.sin(angle) * radius; + bubbles.push(new Bubble(x, y)); + } +} + +// Initialize level +function initLevel() { + const levelType = levelTypes[(currentLevel - 1) % levelTypes.length]; + levelType.generator(); + + levelStartTime = Date.now(); + timeLeft = 30.0; + levelScore = 0; + totalClicks = 0; + successfulClicks = 0; + popParticles = []; + + levelTitleElement.textContent = `Level ${currentLevel}: ${levelType.name}`; + levelDescriptionElement.textContent = levelType.description; + + updateUI(); +} + +// Create pop effect +function createPopEffect(x, y, color) { + for (let i = 0; i < 8; i++) { + popParticles.push(new PopParticle(x, y, color)); + } +} + +// Update UI +function updateUI() { + scoreElement.textContent = `Score: ${score}`; + levelElement.textContent = `Level: ${currentLevel}`; + timerElement.textContent = `Time: ${timeLeft.toFixed(1)}s`; + + const accuracy = totalClicks > 0 ? Math.round((successfulClicks / totalClicks) * 100) : 100; + accuracyElement.textContent = `Accuracy: ${accuracy}%`; +} + +// Check level complete +function checkLevelComplete() { + const allPopped = bubbles.every(bubble => bubble.popped); + if (allPopped && !levelComplete) { + levelComplete = true; + clearInterval(gameTimer); + + const timeTaken = (Date.now() - levelStartTime) / 1000; + const timeBonus = Math.max(0, Math.floor((30 - timeTaken) * 10)); + const accuracy = totalClicks > 0 ? successfulClicks / totalClicks : 1; + const accuracyBonus = Math.floor(accuracy * 500); + levelScore = bubbles.length * 50 + timeBonus + accuracyBonus; + + score += levelScore; + + document.getElementById('level-score').textContent = `Level Score: ${levelScore}`; + document.getElementById('time-bonus').textContent = `Time Bonus: +${timeBonus}`; + document.getElementById('accuracy-bonus').textContent = `Accuracy Bonus: +${accuracyBonus}`; + document.getElementById('total-score').textContent = `Total Score: ${score}`; + + levelCompleteOverlay.style.display = 'flex'; + } +} + +// Game timer +function startTimer() { + gameTimer = setInterval(() => { + timeLeft -= 0.1; + if (timeLeft <= 0) { + timeLeft = 0; + clearInterval(gameTimer); + gameOver = true; + showGameOver(); + } + updateUI(); + }, 100); +} + +// Show game over +function showGameOver() { + gameRunning = false; + gameOverOverlay.style.display = 'flex'; + document.getElementById('final-score').textContent = `Final Score: ${score}`; + document.getElementById('final-level').textContent = `Reached Level: ${currentLevel}`; +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw bubbles + bubbles.forEach(bubble => { + bubble.update(); + bubble.draw(); + }); + + // Draw pop particles + popParticles = popParticles.filter(particle => { + particle.update(); + particle.draw(); + return particle.life > 0; + }); + + // Draw level progress indicator + const poppedCount = bubbles.filter(b => b.popped).length; + const progress = poppedCount / bubbles.length; + const progressBarWidth = 200; + const progressBarHeight = 10; + const progressBarX = canvas.width - progressBarWidth - 20; + const progressBarY = 20; + + ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; + ctx.fillRect(progressBarX, progressBarY, progressBarWidth, progressBarHeight); + + ctx.fillStyle = '#4ecdc4'; + ctx.fillRect(progressBarX, progressBarY, progressBarWidth * progress, progressBarHeight); + + ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)'; + ctx.lineWidth = 2; + ctx.strokeRect(progressBarX, progressBarY, progressBarWidth, progressBarHeight); +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + draw(); + checkLevelComplete(); + + requestAnimationFrame(gameLoop); +} + +// Event listeners +startButton.addEventListener('click', () => { + instructionsOverlay.style.display = 'none'; + gameRunning = true; + initLevel(); + startTimer(); + gameLoop(); +}); + +restartButton.addEventListener('click', () => { + gameOver = false; + gameOverOverlay.style.display = 'none'; + currentLevel = 1; + score = 0; + initLevel(); + gameRunning = true; + startTimer(); + gameLoop(); +}); + +nextLevelButton.addEventListener('click', () => { + levelComplete = false; + levelCompleteOverlay.style.display = 'none'; + currentLevel++; + initLevel(); + gameRunning = true; + startTimer(); + gameLoop(); +}); + +// Mouse click handling +canvas.addEventListener('click', (e) => { + if (!gameRunning) return; + + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + totalClicks++; + + let hitBubble = false; + bubbles.forEach(bubble => { + if (bubble.containsPoint(x, y) && !bubble.popped) { + bubble.pop(); + successfulClicks++; + hitBubble = true; + } + }); + + if (!hitBubble) { + // Missed click - small penalty + score = Math.max(0, score - 5); + } + + updateUI(); +}); + +// Initialize +updateUI(); \ No newline at end of file diff --git a/games/bubble-wrap-popper/style.css b/games/bubble-wrap-popper/style.css new file mode 100644 index 00000000..12a16666 --- /dev/null +++ b/games/bubble-wrap-popper/style.css @@ -0,0 +1,277 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Poppins', sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #ffffff; + overflow: hidden; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +} + +#game-container { + position: relative; + width: 900px; + height: 700px; + border-radius: 20px; + overflow: hidden; + box-shadow: 0 0 40px rgba(0, 0, 0, 0.3); + border: 3px solid rgba(255, 255, 255, 0.2); + background: linear-gradient(135deg, #f093fb 0%, #f5576c 50%, #4facfe 100%); +} + +#ui-panel { + position: absolute; + top: 15px; + left: 15px; + z-index: 10; + background: rgba(255, 255, 255, 0.9); + padding: 15px; + border-radius: 15px; + border: 2px solid rgba(255, 255, 255, 0.5); + font-family: 'Fredoka', cursive; + font-size: 16px; + min-width: 220px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +#ui-panel div { + margin-bottom: 8px; + color: #333; + font-weight: 600; + text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.5); +} + +#level-info { + position: absolute; + top: 15px; + right: 15px; + z-index: 10; + background: rgba(255, 255, 255, 0.9); + padding: 15px; + border-radius: 15px; + border: 2px solid rgba(255, 255, 255, 0.5); + font-family: 'Fredoka', cursive; + text-align: center; + min-width: 200px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +#level-title { + font-size: 18px; + font-weight: 700; + color: #ff6b6b; + margin-bottom: 5px; +} + +#level-description { + font-size: 14px; + color: #333; + font-weight: 500; +} + +#gameCanvas { + display: block; + background: linear-gradient(135deg, #a8edea 0%, #fed6e3 50%, #d299c2 100%); + border-radius: 20px; + cursor: pointer; +} + +#instructions-overlay, +#level-complete-overlay, +#game-over-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 100; + backdrop-filter: blur(8px); +} + +.instructions-content, +.level-complete-content, +.game-over-content { + background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); + padding: 30px; + border-radius: 20px; + border: 3px solid rgba(255, 255, 255, 0.8); + box-shadow: 0 0 40px rgba(0, 0, 0, 0.3); + max-width: 600px; + text-align: center; + font-family: 'Poppins', sans-serif; + color: #333; +} + +.instructions-content h1 { + font-family: 'Fredoka', cursive; + font-size: 2.8em; + margin-bottom: 20px; + color: #ff6b6b; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); + background: linear-gradient(45deg, #ff6b6b, #ffa500, #ffff00); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.instructions-content h3 { + color: #4ecdc4; + margin: 20px 0 10px 0; + font-size: 1.3em; + font-weight: 700; +} + +.instructions-content ul { + text-align: left; + margin: 15px 0; + padding-left: 20px; +} + +.instructions-content li { + margin-bottom: 8px; + line-height: 1.5; + font-weight: 500; +} + +.instructions-content strong { + color: #ff6b6b; + font-weight: 700; +} + +button { + background: linear-gradient(135deg, #ff6b6b, #ffa500); + border: none; + color: white; + padding: 14px 28px; + font-size: 18px; + font-weight: 700; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; + margin: 10px; + font-family: 'Fredoka', cursive; + text-transform: uppercase; + letter-spacing: 1px; + box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4); + border: 2px solid rgba(255, 255, 255, 0.3); +} + +button:hover { + transform: translateY(-3px); + box-shadow: 0 8px 25px rgba(255, 107, 107, 0.6); + background: linear-gradient(135deg, #ff5252, #ff8a00); +} + +.level-complete-content h2, +.game-over-content h2 { + font-family: 'Fredoka', cursive; + font-size: 2.2em; + color: #ff6b6b; + margin-bottom: 20px; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); +} + +.level-complete-content h2 { + color: #4ecdc4; +} + +#level-score, +#time-bonus, +#accuracy-bonus, +#total-score, +#final-score, +#final-level { + font-size: 1.2em; + margin: 8px 0; + color: #333; + font-weight: 600; +} + +#time-bonus, +#accuracy-bonus { + color: #4ecdc4; + font-weight: 700; +} + +#total-score, +#final-score { + font-size: 1.4em; + color: #ff6b6b; + font-weight: 700; +} + +/* Bubble popping animations */ +@keyframes bubblePop { + 0% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.2); + opacity: 0.8; + } + 100% { + transform: scale(0); + opacity: 0; + } +} + +@keyframes bubbleGlow { + 0%, 100% { + box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); + } + 50% { + box-shadow: 0 0 20px rgba(255, 255, 255, 0.8), 0 0 30px rgba(255, 107, 107, 0.6); + } +} + +.bubble-popping { + animation: bubblePop 0.3s ease-out forwards; +} + +.bubble-glow { + animation: bubbleGlow 2s infinite; +} + +/* Responsive design */ +@media (max-width: 950px) { + #game-container { + width: 95vw; + height: 95vh; + } + + #ui-panel { + top: 10px; + left: 10px; + padding: 10px; + font-size: 14px; + min-width: 180px; + } + + #level-info { + top: 10px; + right: 10px; + padding: 10px; + min-width: 160px; + } + + .instructions-content { + padding: 20px; + max-width: 90vw; + } + + .instructions-content h1 { + font-size: 2.2em; + } +} \ No newline at end of file diff --git a/games/bunnyhopdash/index.html b/games/bunnyhopdash/index.html new file mode 100644 index 00000000..b08b8a09 --- /dev/null +++ b/games/bunnyhopdash/index.html @@ -0,0 +1,28 @@ + + + + + + Bunny Hop Dash ๐Ÿฐ + + + + + +
+

๐Ÿฐ Bunny Hop Dash

+
+
+
+
+
+
Score: 0
+
+

Press Space to jump!

+ +
+
+ + + + diff --git a/games/bunnyhopdash/script.js b/games/bunnyhopdash/script.js new file mode 100644 index 00000000..00142955 --- /dev/null +++ b/games/bunnyhopdash/script.js @@ -0,0 +1,126 @@ +const bunny = document.getElementById("bunny"); +const gap = document.getElementById("gap"); +const scoreDisplay = document.getElementById("score"); + +let jumping = false; +let jumpHeight = 0; +let score = 0; +let gameOver = false; + +// Accept multiple variants for the space key and also allow clicking/tapping the game area to jump +document.addEventListener("keydown", (e) => { + // accept Space, ArrowUp, and W keys for jump. Prevent default to avoid page scroll on Space. + const isSpace = e.code === 'Space' || e.key === ' ' || e.key === 'Spacebar'; + const isUp = e.code === 'ArrowUp' || e.key === 'ArrowUp' || e.code === 'KeyW' || e.key === 'w' || e.key === 'W'; + if ((isSpace || isUp) && !jumping && !gameOver) { + e.preventDefault(); + jump(); + } +}); + +// click/tap to jump (mobile friendly) +const gameEl = document.getElementById('game'); +if (gameEl) { + gameEl.addEventListener('pointerdown', (e) => { + if (!jumping && !gameOver) jump(); + }); +} + +// Restart with Enter when game is over +document.addEventListener('keydown', (e) => { + if ((e.key === 'Enter' || e.code === 'Enter') && gameOver) { + restartGame(); + } +}); + +function jump() { + // simple guarded jump using requestAnimationFrame for smoother motion + if (jumping) return; + jumping = true; + const peak = 70; + const speed = 5; // pixels per frame-ish + let goingUp = true; + + function step() { + if (goingUp) { + jumpHeight += speed; + if (jumpHeight >= peak) goingUp = false; + } else { + jumpHeight -= speed; + if (jumpHeight <= 0) jumpHeight = 0; + } + + bunny.style.bottom = 40 + jumpHeight + 'px'; + + if (!goingUp && jumpHeight === 0) { + jumping = false; + return; // stop animation + } + requestAnimationFrame(step); + } + + requestAnimationFrame(step); +} + +// add bunny eyes and nose DOM elements if not already present +(function ensureBunnyDetails(){ + if (!document.querySelector('#bunny .eye.left')){ + const left = document.createElement('div'); + left.className = 'eye left'; + const right = document.createElement('div'); + right.className = 'eye right'; + const nose = document.createElement('div'); + nose.className = 'nose'; + const b = document.getElementById('bunny'); + if (b) { + b.appendChild(left); + b.appendChild(right); + b.appendChild(nose); + } + } +})(); + +function moveGap() { + if (gameOver) return; + let gapLeft = parseInt(window.getComputedStyle(gap).getPropertyValue("left")); + gap.style.left = gapLeft - 3 + "px"; + + if (gapLeft < -60) { + gap.style.left = "350px"; + score++; + scoreDisplay.textContent = "Score: " + score; + } + + // collision detection โ€” only when bunny is not jumping (prevents false positives in the same frame) + let bunnyBottom = parseInt(window.getComputedStyle(bunny).getPropertyValue("bottom")); + if (gapLeft < 80 && gapLeft > 20 && !jumping && bunnyBottom <= 40) { + gameOver = true; + scoreDisplay.textContent = "Game Over ๐Ÿ’” Final Score: " + score; + const hint = document.getElementById('hint'); + if (hint) hint.innerHTML = 'Press Enter to Restart'; + gap.style.animation = "none"; + } + + if (!gameOver) requestAnimationFrame(moveGap); +} + +moveGap(); + +function restartGame() { + // reset state + gameOver = false; + score = 0; + scoreDisplay.textContent = 'Score: 0'; + // reset gap position + gap.style.left = '350px'; + gap.style.animation = ''; + // reset bunny + jumpHeight = 0; + jumping = false; + bunny.style.bottom = '40px'; + // reset hint + const hint = document.getElementById('hint'); + if (hint) hint.innerHTML = 'Press Space to jump!'; + // restart loop + requestAnimationFrame(moveGap); +} diff --git a/games/bunnyhopdash/style.css b/games/bunnyhopdash/style.css new file mode 100644 index 00000000..3b102a61 --- /dev/null +++ b/games/bunnyhopdash/style.css @@ -0,0 +1,193 @@ +body { + background: radial-gradient(1200px 600px at 10% 10%, rgba(255,240,250,0.9), rgba(215,243,255,0.95) 40%), linear-gradient(135deg, #ffeafc, #d7f3ff); + font-family: "Poppins", sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.game-container { + text-align: center; + background: linear-gradient(180deg, #fffafc 0%, #fff5fb 100%); + /* increase top padding so fixed title doesn't overlap content */ + padding: 60px 40px 30px 40px; + border-radius: 20px; + box-shadow: 0 10px 30px rgba(102, 51, 102, 0.08), 0 6px 12px rgba(255, 182, 193, 0.12); + width: 420px; + border: 1px solid rgba(255,255,255,0.6); + position: relative; +} + +h1 { + color: #ff6188; + margin-bottom: 10px; + font-weight: 700; + letter-spacing: 0.6px; + text-shadow: 0 2px 6px rgba(255, 182, 193, 0.25); +} + +/* Place game title fixed at the very top center of the window */ +.game-container > h1 { + position: fixed; + top: 12px; + left: 50%; + transform: translateX(-50%); + margin: 0; + padding: 6px 14px; + background: rgba(255,255,255,0.0); + z-index: 1000; + font-size: 20px; +} + +#game { + position: relative; + width: 350px; + height: 200px; + background: linear-gradient(to top, #b0eaff 0%, #e4faff 100%); + overflow: hidden; + border-radius: 20px; + margin: 20px auto; + box-shadow: inset 0 8px 18px rgba(102,51,102,0.03), 0 6px 18px rgba(102,51,102,0.04); +} + +#ground { + position: absolute; + bottom: 0; + height: 44px; + width: 100%; + background: linear-gradient(180deg, #ffdce6 0%, #ffb6c1 100%); + box-shadow: 0 -6px 10px rgba(255,182,193,0.12) inset; + border-top-left-radius: 12px; + border-top-right-radius: 12px; +} + +#bunny { + position: absolute; + bottom: 44px; + left: 50px; + width: 46px; + height: 46px; + background: #fff5d6; + border-radius: 50% 50% 38% 38%; + box-shadow: 0 6px 14px rgba(0,0,0,0.06), 0 0 0 4px #f7d4c1; + transition: bottom 0.08s linear; +} + +/* bunny details: ears and eyes using pseudo elements */ +#bunny::before, +#bunny::after { + content: ''; + position: absolute; + width: 18px; + height: 18px; + background: #d29a66; + border-radius: 50%; + top: -8px; +} +#bunny::before { left: 2px; } +#bunny::after { right: 2px; } + +#bunny .eye { + position: absolute; + width: 6px; + height: 6px; + background: #2b1b12; + border-radius: 50%; + top: 16px; +} +#bunny .eye.left { left: 12px; } +#bunny .eye.right { right: 12px; } + +#bunny .nose { + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 8px; + height: 6px; + background: #2b1b12; + border-radius: 3px; + top: 22px; +} + +#gap { + position: absolute; + bottom: 0; + width: 66px; + height: 44px; + /* make gap transparent so it represents empty space between two blocks */ + background: transparent; + left: 350px; + border-top-left-radius: 6px; + border-top-right-radius: 6px; + box-shadow: 0 -4px 10px rgba(0,0,0,0.04) inset; +} + +/* decorative notch to hint 'gap' */ +#gap::before { + content: ''; + position: absolute; + top: -12px; + left: 8px; + width: 50px; + height: 12px; + background: linear-gradient(180deg,#fdf2f6,#ffdce6); + border-radius: 6px 6px 0 0; +} + +/* draw left block edge */ +#gap::after { + content: ''; + position: absolute; + bottom: 0; + left: -22px; + width: 20px; + height: 44px; + background: linear-gradient(180deg,#ffdce6,#ffb6c1); + border-radius: 4px 0 0 0; + box-shadow: 0 -4px 10px rgba(0,0,0,0.04) inset; +} + +/* additional pseudo-element for right edge by using an adjacent element trick is not available; instead + we'll use a sibling when generating gap, but a quick CSS-only approach is to give the gap a box-shadow + to hint the right edge. */ +#gap { box-shadow: -22px 0 0 -2px rgba(255,214,222,0.9), 22px 0 0 -2px rgba(255,214,222,0.9), 0 -4px 10px rgba(0,0,0,0.04) inset; } + +#score { + position: absolute; + left: 14px; + top: 12px; + background: rgba(255,255,255,0.9); + padding: 6px 10px; + border-radius: 12px; + font-weight: 700; + color: #ff4fa3; + box-shadow: 0 6px 16px rgba(255,182,193,0.12); +} + +p { + color: #666; + font-size: 14px; + margin: 0; +} + +.controls-row { + display: flex; + gap: 12px; + align-items: center; + justify-content: center; + margin-top: 10px; +} + +.btn { + background: #ffd6e8; + border: none; + padding: 8px 12px; + border-radius: 12px; + color: #ff4fa3; + font-weight: 700; + cursor: pointer; + box-shadow: 0 8px 18px rgba(255,182,193,0.12); +} +.btn:hover { transform: translateY(-2px); } diff --git a/games/burger-builder/index.html b/games/burger-builder/index.html new file mode 100644 index 00000000..8f2b1c33 --- /dev/null +++ b/games/burger-builder/index.html @@ -0,0 +1,30 @@ + + + + + + Burger Builder ๐Ÿ” + + + +

๐Ÿ” Burger Builder Game

+

Click the ingredients in the right order to make a perfect burger!

+ +
+
+
+ +
+ + + + + + +
+ +
+ + + + diff --git a/games/burger-builder/script.js b/games/burger-builder/script.js new file mode 100644 index 00000000..2bc6d63e --- /dev/null +++ b/games/burger-builder/script.js @@ -0,0 +1,33 @@ +const correctOrder = [ + "bottom bun", + "patty", + "cheese", + "lettuce", + "tomato", + "top bun" + ]; + + let currentIndex = 0; + const builtBurger = document.getElementById("built-burger"); + const message = document.getElementById("message"); + + document.querySelectorAll(".ingredient").forEach(btn => { + btn.addEventListener("click", () => { + const chosen = btn.dataset.item; + if (chosen === correctOrder[currentIndex]) { + const layer = document.createElement("div"); + layer.textContent = chosen; + layer.className = "layer"; + builtBurger.appendChild(layer); + currentIndex++; + message.textContent = "Nice! Keep going ๐Ÿ”"; + + if (currentIndex === correctOrder.length) { + message.textContent = "๐ŸŽ‰ You made a perfect burger!"; + } + } else { + message.textContent = "โŒ Wrong ingredient! Try again."; + } + }); + }); + \ No newline at end of file diff --git a/games/burger-builder/style.css b/games/burger-builder/style.css new file mode 100644 index 00000000..189b307f --- /dev/null +++ b/games/burger-builder/style.css @@ -0,0 +1,53 @@ +body { + font-family: "Poppins", sans-serif; + text-align: center; + background: #fff7e6; + color: #333; + margin: 0; + padding: 20px; + } + + h1 { + color: #ff7b00; + margin-bottom: 10px; + } + + #burger-area { + margin: 20px auto; + width: 200px; + height: 250px; + border: 2px dashed #ffae42; + background-color: #fff3d4; + border-radius: 10px; + padding-top: 10px; + } + + #built-burger { + display: flex; + flex-direction: column-reverse; + align-items: center; + gap: 5px; + padding: 10px; + } + + .ingredient { + background: #ffb347; + border: none; + margin: 5px; + padding: 10px 15px; + border-radius: 8px; + cursor: pointer; + transition: background 0.2s; + } + + .ingredient:hover { + background: #ff7b00; + color: white; + } + + #message { + font-size: 18px; + margin-top: 20px; + font-weight: bold; + } + \ No newline at end of file diff --git a/games/butterfly-garden/index.html b/games/butterfly-garden/index.html new file mode 100644 index 00000000..05779284 --- /dev/null +++ b/games/butterfly-garden/index.html @@ -0,0 +1,28 @@ + + + + + + Butterfly Garden ๐Ÿฆ‹ + + + +
+

๐Ÿฆ‹ Butterfly Garden

+ + +
+ + + + Score: 0 +
+
+ + + + + + + + diff --git a/games/butterfly-garden/script.js b/games/butterfly-garden/script.js new file mode 100644 index 00000000..daa3b029 --- /dev/null +++ b/games/butterfly-garden/script.js @@ -0,0 +1,138 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +const startBtn = document.getElementById("startBtn"); +const pauseBtn = document.getElementById("pauseBtn"); +const restartBtn = document.getElementById("restartBtn"); +const scoreDisplay = document.getElementById("scoreDisplay"); + +const nectarSound = document.getElementById("nectarSound"); +const hitSound = document.getElementById("hitSound"); +const bgMusic = document.getElementById("bgMusic"); + +let butterflyImg = new Image(); +butterflyImg.src = "https://cdn.pixabay.com/photo/2016/03/31/19/58/butterfly-1293839_1280.png"; + +let flowerImg = new Image(); +flowerImg.src = "https://cdn.pixabay.com/photo/2013/07/13/13/40/flower-161142_1280.png"; + +let beeImg = new Image(); +beeImg.src = "https://cdn.pixabay.com/photo/2014/04/02/14/09/bee-306757_1280.png"; + +let butterfly = { x: 100, y: 250, size: 50, speed: 4 }; +let flowers = []; +let bees = []; +let score = 0; +let gameRunning = false; +let gamePaused = false; +let frame = 0; + +function drawButterfly() { + ctx.drawImage(butterflyImg, butterfly.x, butterfly.y, butterfly.size, butterfly.size); +} + +function createFlower() { + flowers.push({ x: 800, y: Math.random() * 450, size: 40 }); +} + +function createBee() { + bees.push({ x: 800, y: Math.random() * 450, size: 40 }); +} + +function drawFlowers() { + flowers.forEach((f, i) => { + f.x -= 3; + ctx.drawImage(flowerImg, f.x, f.y, f.size, f.size); + if (f.x < -50) flowers.splice(i, 1); + }); +} + +function drawBees() { + bees.forEach((b, i) => { + b.x -= 5; + ctx.drawImage(beeImg, b.x, b.y, b.size, b.size); + if (b.x < -50) bees.splice(i, 1); + }); +} + +function moveButterfly(e) { + if (e.key === "ArrowUp" && butterfly.y > 0) butterfly.y -= butterfly.speed * 3; + if (e.key === "ArrowDown" && butterfly.y < canvas.height - butterfly.size) butterfly.y += butterfly.speed * 3; + if (e.key === "ArrowLeft" && butterfly.x > 0) butterfly.x -= butterfly.speed * 3; + if (e.key === "ArrowRight" && butterfly.x < canvas.width - butterfly.size) butterfly.x += butterfly.speed * 3; +} + +function detectCollisions() { + flowers.forEach((f, i) => { + if (Math.abs(butterfly.x - f.x) < 30 && Math.abs(butterfly.y - f.y) < 30) { + nectarSound.currentTime = 0; + nectarSound.play(); + flowers.splice(i, 1); + score += 10; + scoreDisplay.textContent = `Score: ${score}`; + } + }); + + bees.forEach((b, i) => { + if (Math.abs(butterfly.x - b.x) < 30 && Math.abs(butterfly.y - b.y) < 30) { + hitSound.currentTime = 0; + hitSound.play(); + stopGame(); + alert("๐Ÿ’€ You hit a bee! Game Over. Final Score: " + score); + } + }); +} + +function gameLoop() { + if (!gameRunning || gamePaused) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawButterfly(); + drawFlowers(); + drawBees(); + detectCollisions(); + + frame++; + if (frame % 60 === 0) createFlower(); + if (frame % 150 === 0) createBee(); + + requestAnimationFrame(gameLoop); +} + +function startGame() { + if (!gameRunning) { + gameRunning = true; + bgMusic.play(); + requestAnimationFrame(gameLoop); + } + gamePaused = false; +} + +function pauseGame() { + gamePaused = true; + bgMusic.pause(); +} + +function stopGame() { + gameRunning = false; + bgMusic.pause(); +} + +function restartGame() { + flowers = []; + bees = []; + score = 0; + butterfly.x = 100; + butterfly.y = 250; + scoreDisplay.textContent = "Score: 0"; + bgMusic.currentTime = 0; + gameRunning = true; + gamePaused = false; + bgMusic.play(); + requestAnimationFrame(gameLoop); +} + +window.addEventListener("keydown", moveButterfly); +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", pauseGame); +restartBtn.addEventListener("click", restartGame); diff --git a/games/butterfly-garden/style.css b/games/butterfly-garden/style.css new file mode 100644 index 00000000..6235d087 --- /dev/null +++ b/games/butterfly-garden/style.css @@ -0,0 +1,53 @@ +body { + margin: 0; + background: linear-gradient(to bottom right, #b3f0ff, #e6ffe6); + font-family: 'Poppins', sans-serif; + text-align: center; + color: #333; +} + +h1 { + text-shadow: 0 0 10px #ff80ff; + margin-top: 10px; +} + +.game-container { + display: flex; + flex-direction: column; + align-items: center; +} + +canvas { + border: 3px solid #fff; + border-radius: 20px; + box-shadow: 0 0 25px rgba(255, 200, 255, 0.6); + background: url('https://cdn.pixabay.com/photo/2015/03/26/09/41/flower-meadow-690627_1280.jpg') center/cover no-repeat; +} + +.controls { + margin-top: 10px; +} + +button { + background-color: #ffb3ff; + border: none; + border-radius: 12px; + padding: 8px 16px; + margin: 5px; + font-size: 16px; + cursor: pointer; + color: #fff; + box-shadow: 0 0 10px #ff80ff; + transition: transform 0.2s, background 0.3s; +} + +button:hover { + background-color: #ff66ff; + transform: scale(1.1); +} + +#scoreDisplay { + font-size: 18px; + margin-left: 15px; + font-weight: bold; +} diff --git a/games/button_walk/index.html b/games/button_walk/index.html new file mode 100644 index 00000000..35e1d988 --- /dev/null +++ b/games/button_walk/index.html @@ -0,0 +1,48 @@ + + + + + + Two-Button Walker + + + + +
+

๐Ÿšถ Two-Button Walker

+ +
+ Distance: 0m | Last Key: -- +
+ +
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+ +
+

Press **Q** and **W** alternately to walk!

+
+ +
+ +
+
+ + + + \ No newline at end of file diff --git a/games/button_walk/script.js b/games/button_walk/script.js new file mode 100644 index 00000000..18e64fa3 --- /dev/null +++ b/games/button_walk/script.js @@ -0,0 +1,182 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. DOM Elements & Constants --- + const character = document.getElementById('character'); + const head = document.getElementById('head'); + const torso = document.getElementById('torso'); + const lThigh = document.getElementById('left-thigh'); + const lShin = document.getElementById('left-shin'); + const rThigh = document.getElementById('right-thigh'); + const rShin = document.getElementById('right-shin'); + const stage = document.getElementById('stage'); + + const distanceDisplay = document.getElementById('distance-display'); + const lastKeyDisplay = document.getElementById('last-key-display'); + const feedbackMessage = document.getElementById('feedback-message'); + const startButton = document.getElementById('start-button'); + + const STAGE_WIDTH = stage.clientWidth; + + // --- 2. GAME STATE VARIABLES --- + let gameActive = false; + let characterX = 50; + let distance = 0; + let lastKeyPressed = ''; // Tracks the last key pressed ('Q' or 'W') + + // Physics/Movement settings + let legAngle = { left: 5, right: -5 }; // Current rotation angle of the thighs + let legVelocity = { left: 0, right: 0 }; // Current angular velocity + let torque = 10; // Force applied on key press + let angularFriction = 0.95; + let maxAngle = 60; // Max forward/backward rotation + let horizontalSpeed = 0.5; // Base movement per frame + let fallThreshold = 45; // Angle threshold for falling + + // Game loop timing + let animationFrameId = null; + + // --- 3. CORE PHYSICS LOOP --- + + /** + * The main game loop using requestAnimationFrame. + */ + function gameLoop() { + if (!gameActive) return; + + // 1. Apply Angular Friction/Gravity (legs naturally fall back towards 0) + ['left', 'right'].forEach(side => { + const thigh = side === 'left' ? lThigh : rThigh; + + // Apply angular velocity to angle + legAngle[side] += legVelocity[side]; + + // Apply friction + legVelocity[side] *= angularFriction; + + // Apply angle back toward center (gravity/balance effect) + legVelocity[side] += (-legAngle[side] * 0.05); + + // Constrain angles + legAngle[side] = Math.min(Math.max(legAngle[side], -maxAngle), maxAngle); + + // Update CSS transform for thigh + thigh.style.transform = `rotate(${legAngle[side]}deg)`; + + // Update CSS transform for shin (follows thigh, simple joint simulation) + const shin = side === 'left' ? lShin : rShin; + shin.style.transform = `rotate(${-legAngle[side] * 1.5}deg)`; // Kick shin back/forward more aggressively + }); + + // 2. Check for Fall (Game Over) + if (Math.abs(legAngle.left) >= fallThreshold || Math.abs(legAngle.right) >= fallThreshold) { + endGame(false); + return; + } + + // 3. Apply Horizontal Movement (Walk/Run) + // Movement is proportional to how fast the legs are moving/out of sync + const speedFactor = Math.abs(legAngle.left) + Math.abs(legAngle.right); + characterX += (speedFactor / 50) * horizontalSpeed; + distance += (speedFactor / 50) * horizontalSpeed; + + // 4. Update DOM + character.style.left = `${characterX}px`; + distanceDisplay.textContent = distance.toFixed(1); + + // 5. Continue Loop + animationFrameId = requestAnimationFrame(gameLoop); + } + + // --- 4. CONTROL HANDLERS --- + + /** + * Applies torque to the specified leg upon key press. + * @param {string} key - 'Q' or 'W'. + */ + function applyTorque(key) { + if (!gameActive) return; + + const side = key === 'Q' ? 'left' : 'right'; + const angleFactor = key === 'Q' ? 1 : -1; // Q applies forward force (+), W applies backward force (-) + + // Prevent spamming the same key repeatedly without alternation + if (key === lastKeyPressed) { + // Apply heavy penalty or skip + // feedbackMessage.textContent = 'โŒ Must alternate keys!'; + return; + } + + // Apply torque/force to the angular velocity + legVelocity[side] += angleFactor * torque * 0.1; + + // Tilt the head/torso slightly for humorous effect + torso.style.transform = `rotate(${angleFactor * 5}deg)`; + head.style.transform = `rotate(${angleFactor * 5}deg)`; + setTimeout(() => { + torso.style.transform = 'rotate(0deg)'; + head.style.transform = 'rotate(0deg)'; + }, 100); + + lastKeyPressed = key; + lastKeyDisplay.textContent = key; + feedbackMessage.textContent = 'WALK!'; + } + + // --- 5. GAME FLOW --- + + function startGame() { + if (gameActive) return; + + // Reset state + characterX = 50; + distance = 0; + legAngle = { left: 5, right: -5 }; + legVelocity = { left: 0, right: 0 }; + lastKeyPressed = ''; + gameActive = true; + + startButton.disabled = true; + feedbackMessage.textContent = 'Press Q and W now!'; + + gameLoop(); // Start the physics simulation + } + + function endGame(win) { + gameActive = false; + cancelAnimationFrame(animationFrameId); + + if (!win) { + // Player fell down + feedbackMessage.innerHTML = `๐Ÿค• **Ouch! You fell!** Distance: ${distance.toFixed(1)}m`; + feedbackMessage.style.color = '#e74c3c'; + + // Visual fall animation + character.style.top = `${STAGE_WIDTH - 20}px`; + character.style.transform = `rotate(90deg)`; + + } else { + // Success condition (not implemented, but good practice) + } + + startButton.textContent = 'RETRY'; + startButton.disabled = false; + } + + + // --- 6. EVENT LISTENERS --- + + startButton.addEventListener('click', startGame); + + // Keyboard Listener + document.addEventListener('keydown', (e) => { + if (!gameActive) return; + + if (e.code === 'KeyQ' && e.repeat === false) { + applyTorque('Q'); + } else if (e.code === 'KeyW' && e.repeat === false) { + applyTorque('W'); + } + }); + + // Initial setup + character.style.left = `${characterX}px`; +}); \ No newline at end of file diff --git a/games/button_walk/style.css b/games/button_walk/style.css new file mode 100644 index 00000000..e3647fbb --- /dev/null +++ b/games/button_walk/style.css @@ -0,0 +1,143 @@ +:root { + --stage-width: 600px; + --stage-height: 400px; + --body-color: #f1c40f; /* Yellow for visibility */ + --torso-height: 80px; + --leg-segment-length: 40px; +} + +body { + font-family: 'Arial', sans-serif; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #34495e; + color: #ecf0f1; +} + +#game-container { + background-color: #2c3e50; + padding: 20px; + border-radius: 10px; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + text-align: center; +} + +h1 { + color: #e74c3c; /* Red */ + margin-bottom: 15px; +} + +#status-area { + font-size: 1.1em; + margin-bottom: 15px; +} + +/* --- Stage and Ground --- */ +#stage { + width: var(--stage-width); + height: var(--stage-height); + margin: 0 auto; + background-color: #bdc3c7; + border: 5px solid #7f8c8d; + position: relative; + overflow: hidden; +} + +#ground { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 10px; + background-color: #333; + z-index: 10; +} + +/* --- Character Container --- */ +#character { + position: absolute; + bottom: 10px; /* Stands on the ground */ + left: 50px; + height: calc(var(--torso-height) + 2 * var(--leg-segment-length)); /* Total height */ + width: 5px; /* Central axis */ + /* This container moves horizontally */ + transition: left 0.05s linear; +} + +/* --- Body Parts --- */ +.body-part { + position: absolute; + background-color: var(--body-color); +} + +#head { + width: 20px; + height: 20px; + border-radius: 50%; + top: -20px; + left: -7px; +} + +#torso { + width: 5px; + height: var(--torso-height); + top: 0; + left: 0; +} + +/* --- Legs (Containers and Segments) --- */ +.leg-container { + position: absolute; + top: var(--torso-height); /* Hinge at the hip */ + left: 0; + width: 5px; + height: calc(2 * var(--leg-segment-length)); +} + +.leg-segment { + position: absolute; + width: 5px; + height: var(--leg-segment-length); + background-color: var(--body-color); + transform-origin: top center; /* All segments pivot from the top */ +} + +/* Thighs (upper leg) */ +#left-thigh, #right-thigh { + top: 0; +} + +/* Shins (lower leg), positioned at the end of the thigh */ +#left-shin, #right-shin { + top: var(--leg-segment-length); +} + +/* Initial Idle Stance */ +#left-thigh { transform: rotate(5deg); } +#right-thigh { transform: rotate(-5deg); } + +/* --- Controls and Feedback --- */ +#feedback-message { + min-height: 20px; + margin-top: 20px; +} + +#start-button { + padding: 10px 20px; + font-size: 1.1em; + font-weight: bold; + background-color: #2ecc71; + color: #2c3e50; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +#start-button:hover { + background-color: #27ae60; +} \ No newline at end of file diff --git a/games/calculator/index.html b/games/calculator/index.html new file mode 100644 index 00000000..269fc6f2 --- /dev/null +++ b/games/calculator/index.html @@ -0,0 +1,42 @@ + + + + + + Calculator Challenge Puzzle + + + + +
+

๐Ÿงฎ Calculator Challenge

+ +
+ Target: 0 +
+ +
+ Current Value: 0 +
+ +
+ History: START +
+ +
+ + + + + + +
+ +
+

Use the numbers and operations to reach the target!

+
+
+ + + + \ No newline at end of file diff --git a/games/calculator/script.js b/games/calculator/script.js new file mode 100644 index 00000000..04d7c713 --- /dev/null +++ b/games/calculator/script.js @@ -0,0 +1,205 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. GAME DATA --- + const puzzleData = { + target: 42, + startNumbers: [5, 10, 3, 2], + numButtons: [] // Will hold DOM references to number buttons + }; + + // --- 2. GAME STATE VARIABLES --- + let currentValue = 0; + let operationHistory = "START"; + let isAwaitingNumber = true; // True if the user should press a number next + let numbersUsed = {}; // Tracks which numbers have been used in the calculation + let gameWon = false; + + // --- 3. DOM Elements --- + const targetNumberDisplay = document.getElementById('target-number'); + const currentValueDisplay = document.getElementById('current-value'); + const historyDisplay = document.getElementById('operation-history'); + const calculatorGrid = document.getElementById('calculator-grid'); + const feedbackMessage = document.getElementById('feedback-message'); + const mathButtons = document.querySelectorAll('.op-math'); + + // --- 4. CORE FUNCTIONS --- + + /** + * Updates the UI displays based on the current state. + */ + function updateDisplay() { + currentValueDisplay.textContent = currentValue; + historyDisplay.textContent = operationHistory; + + // Check for Win Condition + if (currentValue === puzzleData.target && !gameWon) { + gameWon = true; + feedbackMessage.textContent = '๐Ÿฅณ Success! You hit the target!'; + feedbackMessage.classList.add('win'); + disableAllButtons(); + } + } + + /** + * Sets up the initial puzzle state and generates number buttons. + */ + function initGame() { + // Reset state + currentValue = 0; + operationHistory = "START"; + isAwaitingNumber = true; + numbersUsed = {}; + gameWon = false; + + targetNumberDisplay.textContent = puzzleData.target; + feedbackMessage.textContent = 'Use the numbers and operations to reach the target!'; + feedbackMessage.classList.remove('win', 'lose'); + + // Enable all buttons + enableAllButtons(); + + // Dynamically create number buttons (must be done only once) + if (puzzleData.numButtons.length === 0) { + puzzleData.startNumbers.forEach((num, index) => { + const button = document.createElement('button'); + button.classList.add('num-button'); + button.textContent = num; + button.setAttribute('data-num', num); + button.setAttribute('data-index', index); + + // Find the index of the first op-button and insert before it + const firstOpButton = calculatorGrid.querySelector('.op-button'); + calculatorGrid.insertBefore(button, firstOpButton); + + puzzleData.numButtons.push(button); + button.addEventListener('click', handleNumberClick); + }); + } + + // Reset button states + puzzleData.numButtons.forEach(btn => btn.disabled = false); + mathButtons.forEach(btn => btn.disabled = true); + + updateDisplay(); + } + + /** + * Handles clicks on the starting number buttons. + */ + function handleNumberClick(event) { + if (!isAwaitingNumber || gameWon) return; + + const button = event.target; + const num = parseFloat(button.getAttribute('data-num')); + const index = button.getAttribute('data-index'); + + // Check if this is the very first number + if (operationHistory === "START") { + currentValue = num; + operationHistory = `${num}`; + } else { + // Apply the operation (the last character in history is the operator) + const operator = operationHistory.slice(-1); + + // Perform calculation + try { + switch (operator) { + case '+': + currentValue += num; + break; + case '-': + currentValue -= num; + break; + case '*': + currentValue *= num; + break; + case '/': + if (num === 0) throw new Error("Division by zero"); + currentValue /= num; + break; + } + // Ensure result is manageable (e.g., handle floats) + currentValue = Math.round(currentValue * 100000) / 100000; + + // Update history + operationHistory += `${num}`; + } catch (error) { + feedbackMessage.textContent = `Error: ${error.message}. Starting over.`; + feedbackMessage.classList.add('lose'); + setTimeout(initGame, 2000); + return; + } + } + + // Mark number as used and update state + numbersUsed[index] = true; + button.disabled = true; + isAwaitingNumber = false; + + // Enable math operators + mathButtons.forEach(btn => btn.disabled = false); + + updateDisplay(); + } + + /** + * Handles clicks on the operation buttons (+, -, *, /). + */ + function handleOperationClick(event) { + if (isAwaitingNumber || gameWon) return; + + const operator = event.target.getAttribute('data-op'); + + if (operator) { + operationHistory += operator; + isAwaitingNumber = true; // Now awaiting the next number + + // Disable math operators until a number is pressed + mathButtons.forEach(btn => btn.disabled = true); + + } else if (event.target.getAttribute('data-action') === 'clear') { + // Clear only the last number/operator, or reset entirely if only one value + handleClear(); + return; // Don't proceed to updateDisplay immediately + } else if (event.target.getAttribute('data-action') === 'reset') { + initGame(); + return; + } + + updateDisplay(); + } + + /** + * Custom clear logic (simplified for this puzzle). + * Since this is a chain, C acts like a partial undo. + */ + function handleClear() { + // Simple full reset for now due to complex undo required for chain logic + initGame(); + } + + /** + * Disables all number and math buttons. + */ + function disableAllButtons() { + puzzleData.numButtons.forEach(btn => btn.disabled = true); + mathButtons.forEach(btn => btn.disabled = true); + document.querySelectorAll('.op-button').forEach(btn => btn.disabled = true); + } + + /** + * Enables all relevant buttons for the start of a round. + */ + function enableAllButtons() { + document.querySelectorAll('.op-button').forEach(btn => btn.disabled = false); + } + + // --- 5. EVENT LISTENERS --- + + // Attach event listeners to all operation buttons + document.querySelectorAll('.op-button').forEach(button => { + button.addEventListener('click', handleOperationClick); + }); + + // Start the game when the page loads + initGame(); +}); \ No newline at end of file diff --git a/games/calculator/style.css b/games/calculator/style.css new file mode 100644 index 00000000..c3e42df7 --- /dev/null +++ b/games/calculator/style.css @@ -0,0 +1,134 @@ +body { + font-family: 'Helvetica Neue', Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f0f4f8; +} + +#game-container { + background-color: #fff; + padding: 30px; + border-radius: 12px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + text-align: center; + max-width: 400px; + width: 90%; +} + +h1 { + color: #4a69bd; + margin-bottom: 20px; +} + +/* --- Display Areas --- */ +#target-display-area, #current-display-area, #history-display-area { + padding: 10px; + margin-bottom: 10px; + text-align: left; + font-size: 1.1em; + font-weight: 500; + border-bottom: 1px solid #eee; +} + +#target-number { + color: #e74c3c; /* Red for the goal */ + font-weight: bold; +} + +#current-value { + font-size: 1.8em; + font-weight: bold; + color: #2c3e50; + display: block; + text-align: right; + margin-top: 5px; + min-height: 1.5em; +} + +#operation-history { + font-size: 0.9em; + color: #7f8c8d; + display: block; + text-align: right; + min-height: 1.2em; +} + +/* --- Calculator Grid --- */ +#calculator-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; + margin-top: 20px; +} + +button { + padding: 15px; + font-size: 1.2em; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.1s; + font-weight: 600; +} + +/* Number buttons */ +.num-button { + background-color: #ecf0f1; /* Light grey */ + color: #34495e; +} + +.num-button:hover:not(:disabled) { + background-color: #bdc3c7; +} + +.num-button:disabled { + background-color: #95a5a6; + color: #ecf0f1; + cursor: not-allowed; +} + +/* Operation buttons */ +.op-button { + background-color: #95a5a6; + color: white; +} + +.op-button:hover { + background-color: #7f8c8d; +} + +/* Math operation buttons (+ - * /) */ +.op-math { + background-color: #f39c12; /* Orange for math ops */ +} + +.op-math:hover:not(:disabled) { + background-color: #e67e22; +} + +/* Clear/Reset buttons */ +[data-action="clear"], [data-action="reset"] { + background-color: #e74c3c; /* Red */ +} + +[data-action="clear"]:hover, [data-action="reset"]:hover { + background-color: #c0392b; +} + +/* --- Feedback --- */ +#feedback-message { + min-height: 2em; + margin-top: 20px; + font-weight: bold; +} + +.win { + color: #27ae60; /* Green */ +} + +.lose { + color: #c0392b; /* Dark red */ +} \ No newline at end of file diff --git a/games/cannon/index.html b/games/cannon/index.html new file mode 100644 index 00000000..e077d24e --- /dev/null +++ b/games/cannon/index.html @@ -0,0 +1,231 @@ + + + + + + Cannon Physics Challenge + + + + + + + + +

Physics Cannon Challenge

+ + +
+ + +
+ +

+ Shots Fired: 0 +

+
+ + + + diff --git a/games/card-game/index.html b/games/card-game/index.html new file mode 100644 index 00000000..c489dcc4 --- /dev/null +++ b/games/card-game/index.html @@ -0,0 +1,23 @@ + + + + + + Card Memory Game + + + +
+

Card Memory Game

+

Flip cards to find matching pairs. Remember the positions!

+
+
Time: 30
+
Pairs: 0
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/games/card-game/script.js b/games/card-game/script.js new file mode 100644 index 00000000..db53edd0 --- /dev/null +++ b/games/card-game/script.js @@ -0,0 +1,149 @@ +// Card Memory Game Script +// Flip cards to find matching pairs + +var canvas = document.getElementById('gameCanvas'); +var ctx = canvas.getContext('2d'); +var timerDisplay = document.getElementById('timer'); +var scoreDisplay = document.getElementById('score'); +var restartBtn = document.getElementById('restart'); +var messageDiv = document.getElementById('message'); + +var gridSize = 4; +var cardSize = canvas.width / gridSize; +var cards = []; +var flippedCards = []; +var matchedPairs = 0; +var timeLeft = 30; +var timerInterval; +var gameRunning = true; + +// Card class +function Card(x, y, value) { + this.x = x; + this.y = y; + this.value = value; + this.flipped = false; + this.matched = false; +} + +// Initialize the game +function initGame() { + cards = []; + flippedCards = []; + matchedPairs = 0; + timeLeft = 30; + gameRunning = true; + messageDiv.textContent = ''; + scoreDisplay.textContent = 'Pairs: ' + matchedPairs; + + // Create pairs + var values = []; + for (var i = 1; i <= 8; i++) { + values.push(i, i); + } + // Shuffle values + for (var i = values.length - 1; i > 0; i--) { + var j = Math.floor(Math.random() * (i + 1)); + var temp = values[i]; + values[i] = values[j]; + values[j] = temp; + } + + // Create cards + for (var row = 0; row < gridSize; row++) { + for (var col = 0; col < gridSize; col++) { + var x = col * cardSize; + var y = row * cardSize; + var value = values[row * gridSize + col]; + cards.push(new Card(x, y, value)); + } + } + + startTimer(); + draw(); +} + +// Draw the board +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + for (var i = 0; i < cards.length; i++) { + var card = cards[i]; + ctx.fillStyle = card.flipped || card.matched ? '#fff' : '#2196f3'; + ctx.fillRect(card.x + 5, card.y + 5, cardSize - 10, cardSize - 10); + ctx.strokeStyle = '#000'; + ctx.strokeRect(card.x + 5, card.y + 5, cardSize - 10, cardSize - 10); + + if (card.flipped || card.matched) { + ctx.fillStyle = '#000'; + ctx.font = '24px Arial'; + ctx.textAlign = 'center'; + ctx.fillText(card.value, card.x + cardSize / 2, card.y + cardSize / 2 + 8); + } + } +} + +// Handle canvas click +canvas.addEventListener('click', function(event) { + if (!gameRunning) return; + var rect = canvas.getBoundingClientRect(); + var x = event.clientX - rect.left; + var y = event.clientY - rect.top; + var col = Math.floor(x / cardSize); + var row = Math.floor(y / cardSize); + var index = row * gridSize + col; + + if (index >= 0 && index < cards.length) { + var card = cards[index]; + if (!card.flipped && !card.matched && flippedCards.length < 2) { + card.flipped = true; + flippedCards.push(card); + draw(); + + if (flippedCards.length === 2) { + setTimeout(checkMatch, 1000); + } + } + } +}); + +// Check if flipped cards match +function checkMatch() { + if (flippedCards[0].value === flippedCards[1].value) { + flippedCards[0].matched = true; + flippedCards[1].matched = true; + matchedPairs++; + scoreDisplay.textContent = 'Pairs: ' + matchedPairs; + if (matchedPairs === 8) { + gameRunning = false; + clearInterval(timerInterval); + messageDiv.textContent = 'Congratulations! You found all pairs!'; + messageDiv.style.color = 'green'; + } + } else { + flippedCards[0].flipped = false; + flippedCards[1].flipped = false; + } + flippedCards = []; + draw(); +} + +// Start the timer +function startTimer() { + clearInterval(timerInterval); + timerInterval = setInterval(function() { + timeLeft--; + timerDisplay.textContent = 'Time: ' + timeLeft; + if (timeLeft <= 0) { + clearInterval(timerInterval); + gameRunning = false; + messageDiv.textContent = 'Time\'s up! Pairs found: ' + matchedPairs; + messageDiv.style.color = 'red'; + } + }, 1000); +} + +// Restart button +restartBtn.addEventListener('click', initGame); + +// Start the game +initGame(); \ No newline at end of file diff --git a/games/card-game/style.css b/games/card-game/style.css new file mode 100644 index 00000000..c4ae4e7b --- /dev/null +++ b/games/card-game/style.css @@ -0,0 +1,53 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #f5f5f5; +} + +.container { + text-align: center; +} + +h1 { + color: #3f51b5; +} + +.game-info { + margin-bottom: 20px; + display: flex; + justify-content: center; + gap: 20px; +} + +#timer, #score { + font-size: 24px; + font-weight: bold; +} + +#restart { + padding: 10px 20px; + font-size: 16px; + background-color: #ff9800; + color: white; + border: none; + cursor: pointer; +} + +#restart:hover { + background-color: #f57c00; +} + +canvas { + border: 2px solid #3f51b5; + background-color: #e8eaf6; +} + +#message { + margin-top: 20px; + font-size: 18px; + font-weight: bold; +} \ No newline at end of file diff --git a/games/catch-or-crash/index.html b/games/catch-or-crash/index.html new file mode 100644 index 00000000..a7e10cf8 --- /dev/null +++ b/games/catch-or-crash/index.html @@ -0,0 +1,29 @@ + + + + + +Catch or Crash | Mini JS Games Hub + + + +
+
+

Catch or Crash

+
+ Score: 0 + Lives: 3 +
+
+ + + +
+
+
+ + +
+ + + diff --git a/games/catch-or-crash/script.js b/games/catch-or-crash/script.js new file mode 100644 index 00000000..e7f261c1 --- /dev/null +++ b/games/catch-or-crash/script.js @@ -0,0 +1,119 @@ +const gameArea = document.getElementById('gameArea'); +const scoreEl = document.getElementById('score'); +const livesEl = document.getElementById('lives'); +const startBtn = document.getElementById('startBtn'); +const pauseBtn = document.getElementById('pauseBtn'); +const restartBtn = document.getElementById('restartBtn'); +const catchSound = document.getElementById('catchSound'); +const crashSound = document.getElementById('crashSound'); + +let objects = []; +let score = 0; +let lives = 3; +let gameInterval; +let spawnInterval; +let paused = false; + +function randomX() { + return Math.random() * (gameArea.clientWidth - 40); +} + +function spawnObject() { + const obj = document.createElement('div'); + obj.classList.add('object'); + const typeRand = Math.random(); + if (typeRand < 0.6) obj.classList.add('good'); + else if (typeRand < 0.85) obj.classList.add('bad'); + else obj.classList.add('tricky'); + + obj.style.left = randomX() + 'px'; + obj.style.top = '-50px'; + gameArea.appendChild(obj); + + objects.push({el: obj, type: obj.classList.contains('good') ? 'good' : obj.classList.contains('bad') ? 'bad' : 'tricky', y: -50}); +} + +function moveObjects() { + if (paused) return; + objects.forEach((objData, index) => { + objData.y += 3 + score*0.05; + objData.el.style.top = objData.y + 'px'; + if (objData.y > gameArea.clientHeight) { + if (objData.type === 'good') lives--; + objData.el.remove(); + objects.splice(index, 1); + updateScoreLives(); + checkGameOver(); + } + }); + requestAnimationFrame(moveObjects); +} + +function updateScoreLives() { + scoreEl.textContent = score; + livesEl.textContent = lives; +} + +function checkGameOver() { + if (lives <= 0) { + clearInterval(spawnInterval); + alert('Game Over! Your score: ' + score); + resetGame(); + } +} + +function resetGame() { + objects.forEach(o => o.el.remove()); + objects = []; + score = 0; + lives = 3; + updateScoreLives(); +} + +function startGame() { + resetGame(); + paused = false; + moveObjects(); + spawnInterval = setInterval(spawnObject, 1000); +} + +function pauseGame() { + paused = !paused; + pauseBtn.textContent = paused ? 'Resume' : 'Pause'; +} + +function restartGame() { + clearInterval(spawnInterval); + startGame(); +} + +// Catch / Avoid Logic +gameArea.addEventListener('click', (e) => { + const rect = gameArea.getBoundingClientRect(); + const x = e.clientX - rect.left; + const half = gameArea.clientWidth / 2; + let action = x < half ? 'catch' : 'avoid'; + objects.forEach((objData, index) => { + if (objData.y + 40 > gameArea.clientHeight - 50 && objData.y + 40 < gameArea.clientHeight + 10) { + if ((action === 'catch' && objData.type === 'good') || (action === 'avoid' && objData.type === 'bad')) { + score += 10; + catchSound.currentTime = 0; + catchSound.play(); + } else { + score -= 5; + if(score<0) score=0; + crashSound.currentTime = 0; + crashSound.play(); + if(objData.type==='good') lives--; + } + objData.el.remove(); + objects.splice(index,1); + updateScoreLives(); + checkGameOver(); + } + }); +}); + +startBtn.addEventListener('click', startGame); +pauseBtn.addEventListener('click', pauseGame); +restartBtn.addEventListener('click', restartGame); diff --git a/games/catch-or-crash/style.css b/games/catch-or-crash/style.css new file mode 100644 index 00000000..73b35e9c --- /dev/null +++ b/games/catch-or-crash/style.css @@ -0,0 +1,77 @@ +body { + margin: 0; + font-family: Arial, sans-serif; + background: #0a0a0a; + color: #fff; + display: flex; + justify-content: center; + align-items: flex-start; + min-height: 100vh; +} + +.game-container { + max-width: 500px; + width: 100%; + margin-top: 20px; + text-align: center; +} + +.game-header h1 { + margin-bottom: 5px; + color: #ffcc00; + text-shadow: 0 0 10px #ffcc00, 0 0 20px #ffcc00; +} + +.scoreboard { + display: flex; + justify-content: space-around; + margin-bottom: 10px; +} + +.game-controls button { + margin: 5px; + padding: 8px 15px; + border: none; + border-radius: 5px; + background: #ffcc00; + color: #000; + font-weight: bold; + cursor: pointer; + box-shadow: 0 0 10px #ffcc00; + transition: transform 0.1s; +} + +.game-controls button:active { + transform: scale(0.95); +} + +.game-play-area { + position: relative; + width: 100%; + height: 500px; + border: 2px solid #ffcc00; + border-radius: 10px; + background: radial-gradient(circle at top, #222 0%, #000 100%); + overflow: hidden; +} + +.object { + position: absolute; + width: 40px; + height: 40px; + border-radius: 50%; + box-shadow: 0 0 15px 5px; +} + +.object.good { + background: radial-gradient(circle, #00ff00, #008000); +} + +.object.bad { + background: radial-gradient(circle, #ff0000, #800000); +} + +.object.tricky { + background: radial-gradient(circle, #ffff00, #ffaa00); +} + diff --git a/games/catch-or-miss/index.html b/games/catch-or-miss/index.html new file mode 100644 index 00000000..a497c256 --- /dev/null +++ b/games/catch-or-miss/index.html @@ -0,0 +1,100 @@ + + + + + + Catch or Miss โ€” Mini JS Games Hub + + + + +
+
+
+ +
+

Catch or Miss

+

React fast โ€” catch the right prompts, avoid the decoys.

+
+
+ +
+ + + + + + + + + + + +
+
+ +
+ + +
+ +
Press Start to play
+
+
+ + +
+ + + + diff --git a/games/catch-or-miss/script.js b/games/catch-or-miss/script.js new file mode 100644 index 00000000..7d03913b --- /dev/null +++ b/games/catch-or-miss/script.js @@ -0,0 +1,477 @@ +/* Catch or Miss โ€” advanced version + - Single + Two-player modes + - Pause / Start / Restart / Mute + - Obstacles + decoys + - WebAudio sound synthesis (no downloads) + - Works offline in browser +*/ + +(() => { + // ---------- Utilities ---------- + const $ = s => document.querySelector(s); + const $$ = s => Array.from(document.querySelectorAll(s)); + + // DOM + const arena = $('#arena'); + const startBtn = $('#start-btn'); + const pauseBtn = $('#pause-btn'); + const restartBtn = $('#restart-btn'); + const muteToggle = $('#mute-toggle'); + const scoreEl = $('#score'); + const comboEl = $('#combo'); + const livesEl = $('#lives'); + const roundEl = $('#round'); + const highscoreEl = $('#highscore'); + const hint = $('#hint'); + const modeSelect = $('#mode-select'); + const difficultySelect = $('#difficulty-select'); + + // Game state + let running = false; + let paused = false; + let timerIds = new Set(); + let prompts = new Map(); // id -> element + let score = 0; + let combo = 0; + let lives = 3; + let round = 0; + let highscore = Number(localStorage.getItem('catch-miss-highscore') || 0); + let spawnInterval = 1200; // ms default + let promptLifetime = 2500; // ms default + let difficulty = 'normal'; + let mode = 'single'; + let promptIdCounter = 1; + let muted = false; + + highscoreEl.textContent = highscore; + + // WebAudio simple sound generator (short blips) + const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + + function playTone(freq = 440, type = 'sine', duration = 0.08, gain = 0.12) { + if (muted) return; + try { + const o = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + o.type = type; + o.frequency.value = freq; + g.gain.value = gain; + o.connect(g); + g.connect(audioCtx.destination); + o.start(); + g.gain.setValueAtTime(gain, audioCtx.currentTime); + g.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + duration); + o.stop(audioCtx.currentTime + duration + 0.02); + } catch (e) { + // ignore + } + } + + function playSuccess() { playTone(880, 'sine', 0.08, 0.14); } + function playMiss() { playTone(200, 'square', 0.18, 0.12); } + function playDecoy() { playTone(120, 'triangle', 0.12, 0.08); } + function playCombo() { playTone(1200, 'sine', 0.06, 0.16); } + + // images for prompts (online) + const imgSources = [ + 'https://picsum.photos/seed/game1/80/80', + 'https://picsum.photos/seed/game2/80/80', + 'https://picsum.photos/seed/game3/80/80', + 'https://picsum.photos/seed/game4/80/80', + 'https://picsum.photos/seed/game5/80/80', + ]; + + // prompt types and weights + const PROMPT_TYPES = [ + { type:'target', weight: 0.6, score: 10, emoji: 'โœ…' }, + { type:'decoy', weight: 0.25, score: -8, emoji: 'โŒ' }, + { type:'obstacle', weight: 0.15, score: -15, emoji: 'โš ๏ธ' }, + ]; + + // spawn configuration by difficulty + const DIFFICULTY_CONFIG = { + easy: { spawnInterval: 1400, lifetime: 3000, maxSimultaneous: 4 }, + normal: { spawnInterval: 1100, lifetime: 2500, maxSimultaneous: 6 }, + hard: { spawnInterval: 800, lifetime: 2000, maxSimultaneous: 8 } + }; + + // ---------- Rendering & DOM ---------- + function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); } + + function updateUI(){ + scoreEl.textContent = score; + comboEl.textContent = combo; + livesEl.textContent = lives; + roundEl.textContent = round; + highscoreEl.textContent = highscore; + } + + function resetGameState(){ + score = 0; combo = 0; lives = 3; round = 0; + prompts.forEach((el) => removePrompt(el, true)); + prompts.clear(); + updateUI(); + hint.style.opacity = 1; + hint.textContent = 'Press Start to play'; + } + + function removePrompt(el, silent = false){ + if (!el) return; + const id = el.dataset.promptId; + if (prompts.has(id)) prompts.delete(id); + el.remove(); + // clear any timeout attached + const t = el._timeout; + if (t) { + clearTimeout(t); + timerIds.delete(t); + } + if (!silent) { + // small shrink effect handled by CSS + } + } + + // Create a prompt bubble at a random position + function spawnPrompt() { + if (!running || paused) return; + + // limit simultaneous + const maxSim = DIFFICULTY_CONFIG[difficulty].maxSimultaneous; + if (prompts.size >= maxSim) return; + + // choose type by weighted random + const r = Math.random(); + let acc = 0; + let chosen = PROMPT_TYPES[0]; + for (const p of PROMPT_TYPES) { + acc += p.weight; + if (r <= acc) { chosen = p; break; } + } + + const id = String(promptIdCounter++); + const el = document.createElement('div'); + el.className = `prompt ${chosen.type} glow`; + el.dataset.type = chosen.type; + el.dataset.value = chosen.score; + el.dataset.promptId = id; + + const emojiSpan = document.createElement('span'); + emojiSpan.className = 'emoji'; + emojiSpan.textContent = chosen.emoji; + + const labelSpan = document.createElement('span'); + labelSpan.className = 'label'; + labelSpan.textContent = chosen.type === 'target' ? randomTargetLabel() : randomDecoyLabel(); + + el.appendChild(emojiSpan); + el.appendChild(labelSpan); + + // random position within arena bounds + const rect = arena.getBoundingClientRect(); + // ensure prompt stays fully inside + const w = 120, h = 48; + const x = Math.random() * (rect.width - w); + const y = Math.random() * (rect.height - h); + + el.style.left = `${x}px`; + el.style.top = `${y}px`; + + // small scale pop animation + el.style.transform = 'scale(0.7)'; + arena.appendChild(el); + requestAnimationFrame(()=> el.style.transform = 'scale(1)'); + + prompts.set(id, el); + + // bind click + el.addEventListener('click', (ev) => { + ev.stopPropagation(); + handlePromptHit(el, 'click'); + }); + + // auto remove after lifetime (miss) + const life = DIFFICULTY_CONFIG[difficulty].lifetime; + const to = setTimeout(() => { + if (!prompts.has(id)) return; + // if target missed + if (el.dataset.type === 'target') { + combo = 0; + lives -= 1; + playMiss(); + } else { + // decoys/obstacles expire harmlessly + // small penalty for obstacle miss? no + } + removePrompt(el); + checkGameOver(); + updateUI(); + }, life); + + el._timeout = to; + timerIds.add(to); + } + + // labels for variety + const WORDS = ['JUMP', 'CATCH', 'FIRE', 'GOLD', 'KEY', 'BOOM', 'LUCK', 'FAST', 'HIT', 'SAVE', 'GEM']; + const DECOYS = ['NO', 'STOP', 'WRONG', 'MISS', 'BAD']; + function randomTargetLabel(){ return WORDS[Math.floor(Math.random()*WORDS.length)]; } + function randomDecoyLabel(){ return DECOYS[Math.floor(Math.random()*DECOYS.length)]; } + + // Handle clicks or key-press hits + function handlePromptHit(el, method, player = 1) { + if (!el || !prompts.has(el.dataset.promptId)) return; + const type = el.dataset.type; + const value = Number(el.dataset.value) || 0; + + // remove immediately to prevent double-hits + removePrompt(el); + + if (type === 'target') { + score += value + Math.floor(combo * 2); + combo += 1; + playSuccess(); + if (combo > 2) playCombo(); + round += 0.1; // increase round subtly for pacing + } else if (type === 'decoy') { + score += value; // negative + combo = 0; + lives -= 1; + playDecoy(); + } else if (type === 'obstacle') { + score += value; // negative + combo = 0; + lives -= 2; + playMiss(); + } + + // clamp + score = Math.max(-9999, score); + combo = Math.max(0, combo); + round = Math.floor(round); + + if (score > highscore) { + highscore = score; + localStorage.setItem('catch-miss-highscore', highscore); + } + + updateUI(); + checkGameOver(); + } + + // keyboard handling (for quick response) + function onKeyDown(e){ + if (!running || paused) return; + + // Single player: any key attempts to 'catch' nearest prompt + if (mode === 'single') { + // find nearest target to center (simple heuristic) + const arr = Array.from(prompts.values()); + if (!arr.length) return; + // choose the one with smallest distance to center of arena + const rect = arena.getBoundingClientRect(); + const cx = rect.width/2, cy = rect.height/2; + let best = null, bestD = Infinity; + for (const el of arr){ + const r = el.getBoundingClientRect(); + const x = r.left - rect.left + r.width/2; + const y = r.top - rect.top + r.height/2; + const d = Math.hypot(cx-x, cy-y); + if (d < bestD){ bestD = d; best = el; } + } + if (best) handlePromptHit(best, 'key'); + return; + } + + // Duo mode: player 1 uses F (key 'f'), player 2 uses J (key 'j') + if (mode === 'duo') { + if (e.key.toLowerCase() === 'f'){ + // Player 1: pick a prompt on left half + const leftPrompts = Array.from(prompts.values()).filter(el => { + const r = el.getBoundingClientRect(); + const arenaRect = arena.getBoundingClientRect(); + return (r.left - arenaRect.left + r.width/2) < arenaRect.width/2; + }); + if (!leftPrompts.length) return; + handlePromptHit(leftPrompts[0], 'key', 1); + } else if (e.key.toLowerCase() === 'j'){ + const rightPrompts = Array.from(prompts.values()).filter(el => { + const r = el.getBoundingClientRect(); + const arenaRect = arena.getBoundingClientRect(); + return (r.left - arenaRect.left + r.width/2) >= arenaRect.width/2; + }); + if (!rightPrompts.length) return; + handlePromptHit(rightPrompts[0], 'key', 2); + } + } + } + + // Check game over + function checkGameOver(){ + if (lives <= 0) { + running = false; + paused = false; + hint.style.opacity = 1; + hint.innerHTML = `Game Over โ€” Score: ${score}
Press Restart to play again`; + pauseBtn.disabled = true; + restartBtn.disabled = false; + startBtn.disabled = false; + // clear timers & prompts + clearAllTimers(); + prompts.forEach(el => removePrompt(el)); + if (score > highscore) { + highscore = score; + localStorage.setItem('catch-miss-highscore', highscore); + } + updateUI(); + } + } + + // Start / Pause / Restart + function startGame(){ + if (running) return; + // ensure audio context resumed on user gesture + try { audioCtx.resume(); } catch(e){} + + running = true; + paused = false; + startBtn.disabled = true; + pauseBtn.disabled = false; + restartBtn.disabled = false; + hint.style.opacity = 0; + + // assign difficulty + difficulty = difficultySelect.value; + mode = modeSelect.value; + spawnInterval = DIFFICULTY_CONFIG[difficulty].spawnInterval; + promptLifetime = DIFFICULTY_CONFIG[difficulty].lifetime; + + // schedule spawns: + scheduleSpawn(); + updateUI(); + } + + function scheduleSpawn(){ + if (!running || paused) return; + spawnPrompt(); + const id = setTimeout(scheduleSpawn, spawnInterval + Math.random()*300 - 150); + timerIds.add(id); + } + + function pauseGame(){ + if (!running) return; + paused = !paused; + pauseBtn.textContent = paused ? 'Resume' : 'Pause'; + if (!paused) { + // resume schedule + scheduleSpawn(); + } else { + // clear active timers to freeze spawning and prompt expirations + clearAllTimers(true); + // but keep existing prompts on screen (they won't expire until resumed) + } + } + + function restartGame(){ + clearAllTimers(); + prompts.forEach(el => removePrompt(el, true)); + prompts.clear(); + running = false; + paused = false; + startBtn.disabled = false; + pauseBtn.disabled = true; + pauseBtn.textContent = 'Pause'; + restartBtn.disabled = true; + resetGameState(); + } + + function clearAllTimers(keepPrompts = false){ + timerIds.forEach(id => clearTimeout(id)); + timerIds.clear(); + // also clear per-element timeouts + if (!keepPrompts){ + prompts.forEach(el => { + if (el._timeout) { + clearTimeout(el._timeout); + delete el._timeout; + } + }); + } else { + // keep element timeouts paused (they are cleared) โ€” we will not resume exact time left, + // but that's acceptable for pause behaviour + prompts.forEach(el => { if (el._timeout) { clearTimeout(el._timeout); delete el._timeout } }); + } + } + + // arena click to catch nearest (helps mobile) + arena.addEventListener('click', (e) => { + if (!running || paused) return; + // find topmost prompt under click + const el = e.target.closest('.prompt'); + if (el) { + handlePromptHit(el, 'tap'); + } else { + // on empty, attempt nearest capture (single-player quick press) + if (mode === 'single') { + onKeyDown({ key: ' '}); + } + } + }); + + // keyboard global + window.addEventListener('keydown', onKeyDown); + + // control binding + startBtn.addEventListener('click', () => { + // start from scratch if not running + if (!running) { + resetGameState(); + startGame(); + } else if (paused) { + pauseGame(); + } + }); + pauseBtn.addEventListener('click', () => { + if (!running) return; + pauseGame(); + }); + restartBtn.addEventListener('click', () => { + restartGame(); + }); + muteToggle.addEventListener('change', (e) => { + muted = e.target.checked; + }); + + // update difficulty & mode visually + difficultySelect.addEventListener('change', () => { + difficulty = difficultySelect.value; + }); + modeSelect.addEventListener('change', () => { + mode = modeSelect.value; + }); + + // small helper to animate a flash when score increases + function flashArena(color = 'rgba(0,209,178,0.06)'){ + const f = document.createElement('div'); + f.style.position = 'absolute'; + f.style.inset = '0'; + f.style.pointerEvents = 'none'; + f.style.background = color; + f.style.opacity = '0'; + f.style.transition = 'opacity 240ms ease'; + arena.appendChild(f); + requestAnimationFrame(()=> f.style.opacity = '1'); + setTimeout(()=> { + f.style.opacity = '0'; + setTimeout(()=> f.remove(), 260); + }, 120); + } + + // small initialization + resetGameState(); + // small responsiveness: adjust arena hint position + window.addEventListener('resize', () => { /* nothing heavy */ }); + + // expose for debugging (optional) + window.__catchMiss = { startGame, restartGame, pauseGame }; + +})(); diff --git a/games/catch-or-miss/style.css b/games/catch-or-miss/style.css new file mode 100644 index 00000000..c2fd3987 --- /dev/null +++ b/games/catch-or-miss/style.css @@ -0,0 +1,160 @@ +:root{ + --bg:#0f1724; + --panel:#0b1220cc; + --accent:#00d1b2; + --accent-2:#6c7cff; + --muted:#9aa4b2; + --glow: 0 8px 24px rgba(108,124,255,0.18), 0 2px 8px rgba(0,0,0,0.5); + --glass: rgba(255,255,255,0.03); + --danger: #ff6b6b; +} +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + background: radial-gradient(1200px 600px at 10% 10%, rgba(108,124,255,0.06), transparent), + radial-gradient(800px 400px at 90% 80%, rgba(0,209,178,0.04), transparent), + var(--bg); + color:#e6eef8; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + padding:24px; +} + +.app{ + max-width:1200px; + margin:0 auto; + display:flex; + flex-direction:column; + gap:18px; +} + +/* Topbar */ +.topbar{ + display:flex; + justify-content:space-between; + align-items:center; + background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + padding:14px 18px; + border-radius:12px; + box-shadow:var(--glow); +} +.title{display:flex; align-items:center; gap:12px} +.logo{font-size:32px} +.title h1{margin:0;font-size:18px} +.subtitle{margin:0;color:var(--muted);font-size:12px} + +/* controls */ +.controls{display:flex; gap:10px; align-items:center} +.controls select, .controls .btn, .controls .toggle{background:var(--panel); border:1px solid rgba(255,255,255,0.03); color:inherit; padding:8px 10px; border-radius:8px} +.controls .btn{cursor:pointer} +.controls .btn.primary{background:linear-gradient(90deg,var(--accent-2),var(--accent)); color:black; font-weight:600} +.small-label{font-size:11px;color:var(--muted);margin-right:6px} + +/* main game area */ +.game-area{display:flex; gap:16px} +.sidebar{width:280px; display:flex; flex-direction:column; gap:12px} +.panel{background:var(--glass); padding:14px; border-radius:12px; border:1px solid rgba(255,255,255,0.03)} +.stats h3{margin:0 0 8px 0} +.stat.big{font-size:32px; font-weight:700; color:var(--accent); text-shadow:0 6px 18px rgba(0,209,178,0.08)} +.stat-row{display:flex; justify-content:space-between; margin-top:10px} +.stat-row div{background:rgba(255,255,255,0.02); padding:8px 10px; border-radius:8px; min-width:80px; text-align:center} + +.rules ul{margin:8px 0 0 16px; color:var(--muted); font-size:13px} +.leaderboard h3{margin:0 0 8px 0} +#round, #highscore {font-size:18px; color:var(--accent-2); font-weight:700} + +/* arena */ +.arena{ + flex:1; + min-height:520px; + background: linear-gradient(180deg, rgba(255,255,255,0.01), rgba(255,255,255,0.00)); + border-radius:14px; + border:1px solid rgba(255,255,255,0.03); + position:relative; + overflow:hidden; + box-shadow: 0 10px 30px rgba(2,6,23,0.6); + padding:16px; + display:flex; + align-items:center; + justify-content:center; +} + +/* hint / initial */ +.hint{color:var(--muted); text-align:center; font-size:16px} + +/* prompt bubbles */ +.prompt{ + position:absolute; + display:flex; + align-items:center; + justify-content:center; + padding:12px 16px; + border-radius:999px; + font-weight:700; + user-select:none; + cursor:pointer; + box-shadow: 0 6px 20px rgba(2,8,23,0.6); + transform-origin:center; + transition: transform 120ms ease, filter 120ms ease; +} + +/* target / decoy styles */ +.prompt.target{ + background:linear-gradient(90deg, rgba(0,209,178,0.18), rgba(108,124,255,0.12)); + color: #06111a; + border: 1px solid rgba(0,209,178,0.32); + backdrop-filter: blur(6px); + box-shadow: 0 8px 28px rgba(0,209,178,0.06), 0 4px 10px rgba(108,124,255,0.04); +} +.prompt.decoy{ + background:linear-gradient(90deg, rgba(255,107,107,0.12), rgba(255,255,255,0.02)); + color: #fff; + border:1px solid rgba(255,107,107,0.22); +} + +/* obstacle */ +.prompt.obstacle{ + background: linear-gradient(90deg, rgba(255,200,80,0.12), rgba(255,255,255,0.02)); + color:#222; + border:1px solid rgba(255,200,80,0.22); +} + +/* animated glows */ +.prompt.glow{ + filter: drop-shadow(0 12px 28px rgba(108,124,255,0.12)); +} + +/* small label inside prompt */ +.prompt .emoji{font-size:20px; margin-right:8px} +.prompt .label{font-size:14px} + +/* player badges (for duo) */ +.player-badge{ + position:absolute; + top:12px; + left:12px; + background:rgba(0,0,0,0.25); + padding:6px 8px; + border-radius:8px; + font-weight:700; + display:flex; + gap:6px; + align-items:center; + color:var(--muted); +} +.player-badge.right{left:auto; right:12px} + +/* footer */ +.footer{display:flex;justify-content:space-between;align-items:center;color:var(--muted);font-size:13px} +.footer a{color:var(--muted); text-decoration:none; margin-left:12px} + +/* small buttons style on prompt press */ +.prompt:active{transform:scale(0.96)} + +/* responsive */ +@media (max-width:980px){ + .game-area{flex-direction:column} + .sidebar{width:100%} +} diff --git a/games/catch-the-ball/index.html b/games/catch-the-ball/index.html new file mode 100644 index 00000000..10c681c9 --- /dev/null +++ b/games/catch-the-ball/index.html @@ -0,0 +1,17 @@ + + + + + + Catch the Ball ๐ŸŽฏ + + + +

Catch the Ball ๐ŸŽฏ

+
+
Score: 0
+ + + + + diff --git a/games/catch-the-ball/script.js b/games/catch-the-ball/script.js new file mode 100644 index 00000000..765365dc --- /dev/null +++ b/games/catch-the-ball/script.js @@ -0,0 +1,63 @@ +const gameArea = document.getElementById("game-area"); +const scoreDisplay = document.getElementById("score"); +const startBtn = document.getElementById("start-btn"); + +let score = 0; +let playing = false; +let timer; + +function randomPosition() { + const x = Math.random() * (gameArea.clientWidth - 60); + const y = Math.random() * (gameArea.clientHeight - 60); + return { x, y }; +} + +function createBall() { + const ball = document.createElement("div"); + ball.classList.add("ball"); + const { x, y } = randomPosition(); + ball.style.left = `${x}px`; + ball.style.top = `${y}px`; + + ball.addEventListener("click", () => { + score++; + scoreDisplay.textContent = score; + ball.remove(); + spawnBall(); + }); + + gameArea.appendChild(ball); +} + +function spawnBall() { + if (playing) { + createBall(); + clearTimeout(timer); + timer = setTimeout(() => { + const balls = document.querySelectorAll(".ball"); + if (balls.length) balls.forEach(b => b.remove()); + spawnBall(); + }, 1000); + } +} + +startBtn.addEventListener("click", () => { + if (playing) return; + score = 0; + scoreDisplay.textContent = score; + playing = true; + startBtn.textContent = "Playing..."; + startBtn.disabled = true; + + spawnBall(); + + // End game after 30 seconds + setTimeout(() => { + playing = false; + clearTimeout(timer); + document.querySelectorAll(".ball").forEach(b => b.remove()); + alert(`Game Over! Your final score is ${score}.`); + startBtn.textContent = "Start Game"; + startBtn.disabled = false; + }, 30000); +}); diff --git a/games/catch-the-ball/style.css b/games/catch-the-ball/style.css new file mode 100644 index 00000000..e7346131 --- /dev/null +++ b/games/catch-the-ball/style.css @@ -0,0 +1,62 @@ +body { + font-family: "Poppins", sans-serif; + text-align: center; + background: linear-gradient(135deg, #74ebd5, #acb6e5); + height: 100vh; + margin: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + + h1 { + color: #333; + margin-bottom: 10px; + } + + #game-area { + position: relative; + width: 80vw; + height: 60vh; + background: white; + border-radius: 15px; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); + overflow: hidden; + } + + .ball { + position: absolute; + width: 50px; + height: 50px; + background-color: crimson; + border-radius: 50%; + cursor: pointer; + transition: transform 0.1s; + } + + .ball:active { + transform: scale(0.9); + } + + #scoreboard { + font-size: 1.5rem; + margin: 10px; + color: #222; + } + + #start-btn { + background: #4caf50; + color: white; + border: none; + padding: 10px 20px; + font-size: 1.1rem; + border-radius: 8px; + cursor: pointer; + transition: 0.2s; + } + + #start-btn:hover { + background: #45a049; + } + \ No newline at end of file diff --git a/games/catch-the-falling-emoji/index.html b/games/catch-the-falling-emoji/index.html new file mode 100644 index 00000000..9296dd27 --- /dev/null +++ b/games/catch-the-falling-emoji/index.html @@ -0,0 +1,23 @@ + + + + + + Catch the Falling Emoji ๐ŸŽฏ + + + +
+

Catch the Falling Emoji ๐ŸŽฏ

+
+ Score: 0 + Lives: 3 +
+
+
+
+ +
+ + + diff --git a/games/catch-the-falling-emoji/script.js b/games/catch-the-falling-emoji/script.js new file mode 100644 index 00000000..ad9edf32 --- /dev/null +++ b/games/catch-the-falling-emoji/script.js @@ -0,0 +1,97 @@ +const gameArea = document.getElementById("game-area"); +const basket = document.getElementById("basket"); +const scoreEl = document.getElementById("score"); +const livesEl = document.getElementById("lives"); +const restartBtn = document.getElementById("restart-btn"); + +let score = 0; +let lives = 3; +let basketX = 0; +let basketWidth = 80; +let basketSpeed = 20; +let emojis = ["๐ŸŽ", "๐ŸŒ", "๐Ÿ’", "๐Ÿ‡", "๐Ÿ’Ž"]; +let targetEmoji = "๐ŸŽ"; +let fallInterval; +let gameInterval; + +// Basket Movement +function moveBasket(direction) { + basketX += direction * basketSpeed; + if (basketX < 0) basketX = 0; + if (basketX > gameArea.offsetWidth - basketWidth) basketX = gameArea.offsetWidth - basketWidth; + basket.style.left = basketX + "px"; +} + +document.addEventListener("keydown", e => { + if (e.key === "ArrowLeft") moveBasket(-1); + if (e.key === "ArrowRight") moveBasket(1); +}); + +// Mouse / Touch Movement +gameArea.addEventListener("mousemove", e => { + basketX = e.offsetX - basketWidth / 2; + if (basketX < 0) basketX = 0; + if (basketX > gameArea.offsetWidth - basketWidth) basketX = gameArea.offsetWidth - basketWidth; + basket.style.left = basketX + "px"; +}); + +// Falling Emoji Logic +function spawnEmoji() { + const emoji = document.createElement("div"); + emoji.className = "emoji"; + emoji.textContent = emojis[Math.floor(Math.random() * emojis.length)]; + emoji.style.left = Math.random() * (gameArea.offsetWidth - 30) + "px"; + emoji.style.top = "0px"; + gameArea.appendChild(emoji); + + let speed = 2 + Math.random() * 3; // Random falling speed + function fall() { + let y = parseFloat(emoji.style.top); + y += speed; + emoji.style.top = y + "px"; + + // Check collision + if ( + y + 30 >= gameArea.offsetHeight - 30 && + parseFloat(emoji.style.left) + 30 > basketX && + parseFloat(emoji.style.left) < basketX + basketWidth + ) { + if (emoji.textContent === targetEmoji) score += 1; + else lives -= 1; + scoreEl.textContent = score; + livesEl.textContent = lives; + gameArea.removeChild(emoji); + } else if (y > gameArea.offsetHeight) { + gameArea.removeChild(emoji); + } + if (lives <= 0) endGame(); + } + fallInterval = setInterval(fall, 20); +} + +// Game Loop +function startGame() { + score = 0; + lives = 3; + scoreEl.textContent = score; + livesEl.textContent = lives; + basketX = gameArea.offsetWidth / 2 - basketWidth / 2; + basket.style.left = basketX + "px"; + + gameInterval = setInterval(spawnEmoji, 1000); // spawn every 1s +} + +function endGame() { + clearInterval(gameInterval); + alert("Game Over! Your score: " + score); +} + +// Restart Game +restartBtn.addEventListener("click", () => { + clearInterval(gameInterval); + document.querySelectorAll(".emoji").forEach(e => e.remove()); + startGame(); +}); + +// Start the game automatically +startGame(); diff --git a/games/catch-the-falling-emoji/style.css b/games/catch-the-falling-emoji/style.css new file mode 100644 index 00000000..73eeed09 --- /dev/null +++ b/games/catch-the-falling-emoji/style.css @@ -0,0 +1,71 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: linear-gradient(to bottom, #f9f9f9, #a0e0ff); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + text-align: center; + width: 100%; + max-width: 600px; +} + +h1 { + margin-bottom: 10px; + font-size: 2em; + color: #333; +} + +.score-board { + display: flex; + justify-content: space-between; + font-size: 1.2em; + margin-bottom: 10px; +} + +.game-area { + position: relative; + width: 100%; + height: 400px; + background: #fff; + border: 3px solid #333; + border-radius: 10px; + overflow: hidden; +} + +.basket { + position: absolute; + bottom: 10px; + width: 80px; + height: 20px; + background: linear-gradient(90deg, #ffb347, #ffcc33); + border-radius: 10px; + left: 50%; + transform: translateX(-50%); +} + +.emoji { + position: absolute; + font-size: 2em; + pointer-events: none; + user-select: none; +} + +#restart-btn { + margin-top: 15px; + padding: 8px 16px; + font-size: 16px; + border: none; + border-radius: 6px; + background: #ffb347; + cursor: pointer; + transition: 0.3s; +} + +#restart-btn:hover { + background: #ffcc33; +} diff --git a/games/catch-the-stars/index.html b/games/catch-the-stars/index.html new file mode 100644 index 00000000..5a2ecb94 --- /dev/null +++ b/games/catch-the-stars/index.html @@ -0,0 +1,28 @@ + + + + + + Catch the Stars | Mini JS Games Hub + + + +
+
+

Catch the Stars ๐ŸŒŸ

+
+ Score: 0 + Lives: 3 +
+
+ + + +
+ +
+
+ + + + diff --git a/games/catch-the-stars/script.js b/games/catch-the-stars/script.js new file mode 100644 index 00000000..09d9c73f --- /dev/null +++ b/games/catch-the-stars/script.js @@ -0,0 +1,140 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +// Canvas size +canvas.width = canvas.offsetWidth; +canvas.height = canvas.offsetHeight; + +// Game variables +let catcher = { + width: 80, + height: 20, + x: canvas.width / 2 - 40, + y: canvas.height - 30, + speed: 7 +}; + +let stars = []; +let starSpeed = 2; +let spawnRate = 1500; // milliseconds +let lastSpawn = Date.now(); +let score = 0; +let lives = 3; +let gameOver = false; + +// DOM elements +const scoreEl = document.getElementById("score"); +const livesEl = document.getElementById("lives"); +const restartBtn = document.getElementById("restartBtn"); + +// Catcher movement +let leftPressed = false; +let rightPressed = false; + +document.addEventListener("keydown", (e) => { + if (e.key === "ArrowLeft" || e.key === "a") leftPressed = true; + if (e.key === "ArrowRight" || e.key === "d") rightPressed = true; +}); + +document.addEventListener("keyup", (e) => { + if (e.key === "ArrowLeft" || e.key === "a") leftPressed = false; + if (e.key === "ArrowRight" || e.key === "d") rightPressed = false; +}); + +// Star class +class Star { + constructor(x, y, radius = 10) { + this.x = x; + this.y = y; + this.radius = radius; + } + + draw() { + ctx.beginPath(); + ctx.fillStyle = "#ffeb3b"; + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fill(); + ctx.closePath(); + } + + update() { + this.y += starSpeed; + } +} + +// Spawn stars +function spawnStar() { + const x = Math.random() * (canvas.width - 20) + 10; + stars.push(new Star(x, -20)); +} + +// Collision detection +function checkCollision(star) { + return ( + star.y + star.radius >= catcher.y && + star.x >= catcher.x && + star.x <= catcher.x + catcher.width + ); +} + +// Game loop +function gameLoop() { + if (gameOver) return; + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Move catcher + if (leftPressed && catcher.x > 0) catcher.x -= catcher.speed; + if (rightPressed && catcher.x + catcher.width < canvas.width) catcher.x += catcher.speed; + + // Draw catcher + ctx.fillStyle = "#ff4136"; + ctx.fillRect(catcher.x, catcher.y, catcher.width, catcher.height); + + // Spawn stars + if (Date.now() - lastSpawn > spawnRate) { + spawnStar(); + lastSpawn = Date.now(); + } + + // Update and draw stars + stars.forEach((star, index) => { + star.update(); + star.draw(); + + if (checkCollision(star)) { + score += 1; + scoreEl.textContent = score; + + // Increase difficulty + if (score % 5 === 0) starSpeed += 0.5; + stars.splice(index, 1); + } else if (star.y > canvas.height) { + stars.splice(index, 1); + lives -= 1; + livesEl.textContent = lives; + + if (lives <= 0) { + gameOver = true; + alert(`Game Over! Your score: ${score}`); + } + } + }); + + requestAnimationFrame(gameLoop); +} + +// Restart game +restartBtn.addEventListener("click", () => { + stars = []; + score = 0; + lives = 3; + starSpeed = 2; + lastSpawn = Date.now(); + gameOver = false; + scoreEl.textContent = score; + livesEl.textContent = lives; + gameLoop(); +}); + +// Start game +gameLoop(); diff --git a/games/catch-the-stars/style.css b/games/catch-the-stars/style.css new file mode 100644 index 00000000..4611cf37 --- /dev/null +++ b/games/catch-the-stars/style.css @@ -0,0 +1,59 @@ +/* Reset & basic styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +body { + background: linear-gradient(to bottom, #001f3f, #0074D9); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + width: 100%; + max-width: 600px; + text-align: center; + color: #fff; +} + +header h1 { + font-size: 2rem; + margin-bottom: 10px; +} + +.scoreboard { + display: flex; + justify-content: space-around; + font-size: 1.2rem; + margin-bottom: 10px; +} + +#gameCanvas { + width: 100%; + height: 400px; + background: radial-gradient(circle at top, #1e3c72, #2a5298); + border-radius: 15px; + display: block; + margin: 0 auto 15px auto; +} + +.game-controls button { + padding: 10px 20px; + font-size: 16px; + background: #ffcc00; + border: none; + border-radius: 5px; + cursor: pointer; + transition: 0.3s; +} + +.game-controls button:hover { + background: #ffaa00; +} + +/* Catcher styles */ diff --git a/games/chain_game/index.html b/games/chain_game/index.html new file mode 100644 index 00000000..19b194ab --- /dev/null +++ b/games/chain_game/index.html @@ -0,0 +1,39 @@ + + + + + + Word Chain Challenge + + + + +
+

๐Ÿ”— Word Chain Master

+ +
+ Words Used: 0 +
+ +
+

Welcome!

+

Your word must start with: **A**

+
+ +
+ + +
+ +
+

Click START to begin the chain!

+
+ +
+ +
+
+ + + + \ No newline at end of file diff --git a/games/chain_game/script.js b/games/chain_game/script.js new file mode 100644 index 00000000..bef539d2 --- /dev/null +++ b/games/chain_game/script.js @@ -0,0 +1,200 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. GAME DATA --- + // NOTE: This list is highly truncated for demonstration. + // A real game would require a list of thousands of common words. + const ALL_WORDS = [ + "apple", "orange", "elephant", "tiger", "rabbit", "train", "night", + "house", "eagle", "stone", "island", "doctor", "robot", "time", + "music", "cat", "dog", "goat", "table", "chair", "tree", "river", + "earth", "hotel", "lamp", "money", "zebra", "balloon", "north", + "hello", "open", "nest", "tank", "kite", "easy", "yellow", "wagon" + ].map(word => word.toUpperCase()); // Convert to uppercase for case-insensitive matching + + // --- 2. DOM Elements --- + const lastWordDisplay = document.getElementById('last-word'); + const nextLetterDisplay = document.getElementById('next-letter'); + const playerInput = document.getElementById('player-input'); + const submitButton = document.getElementById('submit-button'); + const feedbackMessage = document.getElementById('feedback-message'); + const startButton = document.getElementById('start-button'); + const wordCountDisplay = document.getElementById('word-count'); + + // --- 3. GAME STATE VARIABLES --- + let usedWords = new Set(); + let currentWord = ""; + let gameActive = false; + let turn = 'player'; // Player always starts the chain for the first word + + // --- 4. UTILITY FUNCTIONS --- + + /** + * Checks if a word exists in the master word list. + */ + function isWordValid(word) { + return ALL_WORDS.includes(word); + } + + /** + * Finds a valid word for the computer to play. + */ + function findComputerWord(startLetter) { + const potentialWords = ALL_WORDS.filter(word => + word.startsWith(startLetter) && !usedWords.has(word) + ); + + if (potentialWords.length === 0) { + return null; // Computer loses + } + + // Return a random word from the potential list + const randomIndex = Math.floor(Math.random() * potentialWords.length); + return potentialWords[randomIndex]; + } + + // --- 5. CORE GAME FUNCTIONS --- + + /** + * Initializes or restarts the game. + */ + function initGame() { + gameActive = true; + usedWords.clear(); + currentWord = ""; + + startButton.textContent = 'RESTART'; + playerInput.disabled = false; + submitButton.disabled = false; + + wordCountDisplay.textContent = usedWords.size; + + // Computer plays the first word, player must respond to its last letter. + const firstWord = findComputerWord('A'); // Arbitrarily start the chain with 'A' + if (firstWord) { + playTurn(firstWord); // Start the chain + playerInput.focus(); + } else { + feedbackMessage.textContent = 'Error: Cannot start the game.'; + endGame(); + } + } + + /** + * Handles the player's submission and validation. + */ + function handlePlayerSubmit() { + if (!gameActive || turn !== 'player') return; + + const rawInput = playerInput.value.trim().toUpperCase(); + playerInput.value = ''; // Clear input immediately + + if (rawInput.length < 3) { + feedbackMessage.textContent = "Word must be at least 3 letters long."; + return; + } + + const requiredLetter = currentWord.slice(-1); + + // 1. Check Chain Rule + if (!rawInput.startsWith(requiredLetter)) { + feedbackMessage.textContent = `Word must start with the letter "${requiredLetter}".`; + return; + } + + // 2. Check Duplication + if (usedWords.has(rawInput)) { + feedbackMessage.textContent = "Word has already been used! Try another."; + return; + } + + // 3. Check Validity + if (!isWordValid(rawInput)) { + feedbackMessage.textContent = "That word is not recognized in our dictionary."; + return; + } + + // --- SUCCESSFUL PLAYER TURN --- + playTurn(rawInput); + + // Hand turn to computer after a short delay + turn = 'computer'; + playerInput.disabled = true; + submitButton.disabled = true; + feedbackMessage.textContent = "Computer is thinking..."; + + setTimeout(computerTurn, 1500); + } + + /** + * Updates the display and game state for a successful turn. + */ + function playTurn(newWord) { + currentWord = newWord; + usedWords.add(currentWord); + + const lastLetter = currentWord.slice(-1); + + lastWordDisplay.textContent = currentWord; + nextLetterDisplay.innerHTML = `Your word must start with: **${lastLetter}**`; + wordCountDisplay.textContent = usedWords.size; + + // Re-enable player controls if it's not the computer's turn yet + if (turn !== 'computer') { + playerInput.disabled = false; + submitButton.disabled = false; + feedbackMessage.textContent = "Enter your word!"; + } + } + + /** + * Logic for the computer's turn. + */ + function computerTurn() { + const requiredLetter = currentWord.slice(-1); + const computerWord = findComputerWord(requiredLetter); + + if (computerWord) { + // Computer found a word + playTurn(computerWord); + turn = 'player'; + playerInput.disabled = false; + submitButton.disabled = false; + playerInput.focus(); + feedbackMessage.textContent = `Computer plays: ${computerWord}! Your turn!`; + } else { + // Computer loses + endGame('computer'); + } + } + + /** + * Stops the game and displays the winner. + */ + function endGame(loser) { + gameActive = false; + playerInput.disabled = true; + submitButton.disabled = true; + + if (loser === 'computer') { + feedbackMessage.innerHTML = `๐Ÿ† **YOU WIN!** The computer couldn't find a word starting with "${currentWord.slice(-1)}".`; + feedbackMessage.style.color = '#2ecc71'; + } else { + feedbackMessage.innerHTML = `๐Ÿ˜ญ **GAME OVER.** Invalid word. The computer wins. Final score: ${usedWords.size} words.`; + feedbackMessage.style.color = '#e74c3c'; + } + } + + // --- 6. EVENT LISTENERS --- + + startButton.addEventListener('click', initGame); + submitButton.addEventListener('click', handlePlayerSubmit); + + playerInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter' && !submitButton.disabled) { + handlePlayerSubmit(); + } + }); + + // Initial message + lastWordDisplay.textContent = 'Word Chain Challenge'; + nextLetterDisplay.textContent = 'Click START to begin!'; +}); \ No newline at end of file diff --git a/games/chain_game/style.css b/games/chain_game/style.css new file mode 100644 index 00000000..88a142f8 --- /dev/null +++ b/games/chain_game/style.css @@ -0,0 +1,111 @@ +body { + font-family: 'Verdana', sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f0f4f8; + color: #2c3e50; +} + +#game-container { + background-color: white; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 500px; + width: 90%; +} + +h1 { + color: #2ecc71; /* Green */ + margin-bottom: 20px; +} + +#status-area { + font-size: 1.1em; + font-weight: bold; + margin-bottom: 25px; + color: #3498db; +} + +/* --- Chain Display --- */ +#chain-display { + background-color: #ecf0f1; + padding: 20px; + border-radius: 8px; + margin-bottom: 30px; + border: 1px solid #bdc3c7; +} + +#last-word { + font-size: 1.8em; + font-weight: bold; + color: #e74c3c; /* Last word stands out */ + margin: 0 0 10px 0; +} + +#next-letter { + font-size: 1.2em; + font-style: italic; +} + +/* --- Input Area --- */ +#input-area { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +#player-input { + flex-grow: 1; + padding: 10px; + font-size: 1.1em; + border: 2px solid #ccc; + border-radius: 5px; +} + +#submit-button { + padding: 10px 20px; + font-size: 1.1em; + background-color: #2ecc71; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; +} + +#submit-button:hover:not(:disabled) { + background-color: #27ae60; +} + +#player-input:disabled, #submit-button:disabled { + background-color: #bdc3c7; + cursor: not-allowed; +} + +/* --- Controls and Feedback --- */ +#feedback-message { + min-height: 1.5em; + font-weight: bold; + margin-bottom: 20px; +} + +#start-button { + padding: 12px 25px; + font-size: 1.2em; + font-weight: bold; + background-color: #3498db; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +#start-button:hover { + background-color: #2980b9; +} \ No newline at end of file diff --git a/games/chroma-cascade/index.html b/games/chroma-cascade/index.html new file mode 100644 index 00000000..e68fcedd --- /dev/null +++ b/games/chroma-cascade/index.html @@ -0,0 +1,82 @@ + + + + + + Chroma Cascade + + + +
+
+

CHROMA CASCADE

+

Mix colors to create perfect harmonies

+
+ +
+
+ Level +
1
+
+
+ Score +
0
+
+
+ Time +
60
+
+
+ +
+
+

TARGET HARMONY

+
+
Monochromatic
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+

YOUR CANVAS

+
+
+
+ +
+ + + + +
+ +
+

How to Play

+

โ€ข Drag color droplets to the mixing area to combine them

+

โ€ข Create color harmonies that match the target

+

โ€ข Fill your canvas with harmonious colors before time runs out

+

โ€ข Each level introduces new color relationships

+
+ +
+
+

Level Complete!

+

Your Harmony Score: 0

+

Colors Matched: 0/5

+ +
+
+
+ + + + \ No newline at end of file diff --git a/games/chroma-cascade/script.js b/games/chroma-cascade/script.js new file mode 100644 index 00000000..0856f5d1 --- /dev/null +++ b/games/chroma-cascade/script.js @@ -0,0 +1,460 @@ +class ChromaCascade { + constructor() { + this.level = 1; + this.score = 0; + this.timeLeft = 60; + this.timer = null; + this.activeColor = null; + this.mixedColor = null; + this.targetHarmony = []; + this.canvasColors = []; + this.harmonies = [ + { + name: "Monochromatic", + colors: 3, + generator: this.generateMonochromatic.bind(this) + }, + { + name: "Analogous", + colors: 4, + generator: this.generateAnalogous.bind(this) + }, + { + name: "Complementary", + colors: 3, + generator: this.generateComplementary.bind(this) + }, + { + name: "Triadic", + colors: 5, + generator: this.generateTriadic.bind(this) + }, + { + name: "Tetradic", + colors: 6, + generator: this.generateTetradic.bind(this) + } + ]; + + this.initializeGame(); + this.setupEventListeners(); + } + + initializeGame() { + this.updateDisplay(); + this.generateTargetHarmony(); + this.createColorPalette(); + this.generateNewDroplet(); + this.startTimer(); + } + + generateTargetHarmony() { + const harmonyType = this.harmonies[Math.min(this.level - 1, this.harmonies.length - 1)]; + this.targetHarmony = harmonyType.generator(); + + const targetColorsElement = document.getElementById('targetColors'); + const harmonyNameElement = document.getElementById('harmonyName'); + + targetColorsElement.innerHTML = ''; + harmonyNameElement.textContent = harmonyType.name; + + this.targetHarmony.forEach(color => { + const swatch = document.createElement('div'); + swatch.className = 'color-swatch'; + swatch.style.backgroundColor = color; + targetColorsElement.appendChild(swatch); + }); + + const artCanvas = document.getElementById('artCanvas'); + artCanvas.innerHTML = ''; + const slotCount = Math.max(harmonyType.colors, this.targetHarmony.length); + this.canvasColors = new Array(slotCount).fill(null); + + for (let i = 0; i < slotCount; i++) { + const slot = document.createElement('div'); + slot.className = 'canvas-slot'; + slot.dataset.index = i; + artCanvas.appendChild(slot); + } + } + + generateMonochromatic() { + const baseHue = Math.random() * 360; + const colors = []; + + for (let i = 0; i < 3; i++) { + const saturation = 70 + Math.random() * 20; + const lightness = 30 + (i * 20); + colors.push(`hsl(${baseHue}, ${saturation}%, ${lightness}%)`); + } + + return colors; + } + + generateAnalogous() { + const baseHue = Math.random() * 360; + const colors = []; + const hues = [baseHue - 30, baseHue - 15, baseHue, baseHue + 15]; + + hues.forEach(hue => { + const normalizedHue = (hue + 360) % 360; + colors.push(`hsl(${normalizedHue}, 70%, 50%)`); + }); + + return colors; + } + + generateComplementary() { + const baseHue = Math.random() * 360; + const complementaryHue = (baseHue + 180) % 360; + + return [ + `hsl(${baseHue}, 80%, 50%)`, + `hsl(${complementaryHue}, 80%, 50%)`, + `hsl(${baseHue}, 60%, 70%)` + ]; + } + + generateTriadic() { + const baseHue = Math.random() * 360; + const hues = [baseHue, (baseHue + 120) % 360, (baseHue + 240) % 360]; + const colors = []; + + hues.forEach(hue => { + colors.push(`hsl(${hue}, 80%, 50%)`); + }); + + colors.push(`hsl(${baseHue}, 60%, 70%)`); + colors.push(`hsl(${(baseHue + 120) % 360}, 60%, 70%)`); + + return colors; + } + + generateTetradic() { + const baseHue = Math.random() * 360; + const hues = [ + baseHue, + (baseHue + 90) % 360, + (baseHue + 180) % 360, + (baseHue + 270) % 360 + ]; + const colors = []; + + hues.forEach(hue => { + colors.push(`hsl(${hue}, 80%, 50%)`); + colors.push(`hsl(${hue}, 60%, 65%)`); + }); + + return colors.slice(0, 6); + } + + createColorPalette() { + const palette = document.getElementById('colorPalette'); + palette.innerHTML = ''; + + const baseColors = [ + 'hsl(0, 100%, 50%)', + 'hsl(120, 100%, 50%)', + 'hsl(240, 100%, 50%)', + 'hsl(60, 100%, 50%)', + 'hsl(300, 100%, 50%)' + ]; + + baseColors.forEach(color => { + const colorElement = document.createElement('div'); + colorElement.className = 'palette-color'; + colorElement.style.backgroundColor = color; + colorElement.addEventListener('click', () => this.selectColor(color)); + palette.appendChild(colorElement); + }); + } + + selectColor(color) { + this.activeColor = color; + const activeDroplet = document.getElementById('activeDroplet'); + activeDroplet.style.backgroundColor = color; + + activeDroplet.classList.remove('new'); + void activeDroplet.offsetWidth; + activeDroplet.classList.add('new'); + } + + generateNewDroplet() { + const hue = Math.random() * 360; + const color = `hsl(${hue}, 80%, 50%)`; + this.selectColor(color); + } + + mixColors(color1, color2) { + const rgb1 = this.hslToRgb(color1); + const rgb2 = this.hslToRgb(color2); + + const mixedRgb = [ + Math.round((rgb1[0] + rgb2[0]) / 2), + Math.round((rgb1[1] + rgb2[1]) / 2), + Math.round((rgb1[2] + rgb2[2]) / 2) + ]; + + return this.rgbToHsl(...mixedRgb); + } + + hslToRgb(hsl) { + const match = hsl.match(/hsl\((\d+(?:\.\d+)?),\s*(\d+(?:\.\d+)?)%,\s*(\d+(?:\.\d+)?)%\)/); + if (!match) return [0, 0, 0]; + const h = parseFloat(match[1]) / 360; + const s = parseFloat(match[2]) / 100; + const l = parseFloat(match[3]) / 100; + + let r, g, b; + + if (s === 0) { + r = g = b = l; + } else { + const hue2rgb = (p, q, t) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1/6) return p + (q - p) * 6 * t; + if (t < 1/2) return q; + if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + }; + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; + } + + rgbToHsl(r, g, b) { + r /= 255; + g /= 255; + b /= 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h, s, l = (max + min) / 2; + + if (max === min) { + h = s = 0; + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + + h /= 6; + } + + return `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`; + } + + addToCanvas() { + if (!this.mixedColor) { + if (this.activeColor) { + this.mixedColor = this.activeColor; + } else { + return; + } + } + let emptySlot = document.querySelector('.canvas-slot:not(.filled)'); + const artCanvas = document.getElementById('artCanvas'); + if (!emptySlot) { + const newIndex = this.canvasColors.length; + const slot = document.createElement('div'); + slot.className = 'canvas-slot'; + slot.dataset.index = newIndex; + artCanvas.appendChild(slot); + this.canvasColors.push(null); + emptySlot = slot; + } + + if (emptySlot) { + emptySlot.style.backgroundColor = this.mixedColor; + emptySlot.classList.add('filled'); + + const index = Number(emptySlot.dataset.index); + if (!Number.isNaN(index)) { + this.canvasColors[index] = this.mixedColor; + } + + this.checkCompletion(); + this.mixedColor = null; + const resultColorEl = document.querySelector('.result-color'); + if (resultColorEl) resultColorEl.style.backgroundColor = ''; + } + } + + checkCompletion() { + let matches = 0; + const tolerance = 30; + + this.canvasColors.forEach((color, index) => { + if (color && this.targetHarmony[index]) { + if (this.colorsMatch(color, this.targetHarmony[index], tolerance)) { + matches++; + } + } + }); + + if (matches === this.targetHarmony.length) { + this.levelComplete(); + } + } + + colorsMatch(color1, color2, tolerance) { + const hsl1 = this.parseHsl(color1); + const hsl2 = this.parseHsl(color2); + + return ( + Math.abs(hsl1.h - hsl2.h) <= tolerance && + Math.abs(hsl1.s - hsl2.s) <= 20 && + Math.abs(hsl1.l - hsl2.l) <= 20 + ); + } + + parseHsl(hsl) { + const match = hsl.match(/hsl\((\d+(?:\.\d+)?),\s*(\d+(?:\.\d+)?)%,\s*(\d+(?:\.\d+)?)%\)/); + if (!match) return { h: 0, s: 0, l: 0 }; + return { + h: parseFloat(match[1]), + s: parseFloat(match[2]), + l: parseFloat(match[3]) + }; + } + + levelComplete() { + clearInterval(this.timer); + + const timeBonus = Math.floor(this.timeLeft * 2); + const levelBonus = this.level * 100; + const totalScore = this.score + timeBonus + levelBonus; + + document.getElementById('finalScore').textContent = totalScore; + document.getElementById('colorsMatched').textContent = this.targetHarmony.length; + + this.showGameOver(); + } + + showGameOver() { + document.getElementById('gameOver').classList.add('active'); + } + + nextLevel() { + this.level++; + this.timeLeft = 60 + (this.level * 5); + this.score += 100 * this.level; + + document.getElementById('gameOver').classList.remove('active'); + this.initializeGame(); + } + + startTimer() { + clearInterval(this.timer); + this.timer = setInterval(() => { + this.timeLeft--; + this.updateDisplay(); + + if (this.timeLeft <= 10) { + document.getElementById('timer').classList.add('timer-warning'); + } + + if (this.timeLeft <= 0) { + clearInterval(this.timer); + this.gameOver(); + } + }, 1000); + } + + gameOver() { + alert('Time\'s up! Try again.'); + this.initializeGame(); + } + + updateDisplay() { + document.getElementById('level').textContent = this.level; + document.getElementById('score').textContent = this.score; + document.getElementById('timer').textContent = this.timeLeft; + } + + setupEventListeners() { + const mixingArea = document.querySelector('.color-dropper'); + const resultArea = document.querySelector('.mix-result'); + + mixingArea.addEventListener('dragover', (e) => e.preventDefault()); + mixingArea.addEventListener('drop', (e) => { + e.preventDefault(); + if (this.activeColor) { + this.mixedColor = this.activeColor; + document.querySelector('.result-color').style.backgroundColor = this.mixedColor; + } + }); + + resultArea.addEventListener('dragover', (e) => e.preventDefault()); + resultArea.addEventListener('drop', (e) => { + e.preventDefault(); + if (this.activeColor && this.mixedColor) { + const newColor = this.mixColors(this.activeColor, this.mixedColor); + this.mixedColor = newColor; + document.querySelector('.result-color').style.backgroundColor = newColor; + } + }); + + document.getElementById('newDroplet').addEventListener('click', () => { + this.generateNewDroplet(); + }); + + document.getElementById('addToCanvas').addEventListener('click', () => { + this.addToCanvas(); + }); + + document.getElementById('clearCanvas').addEventListener('click', () => { + this.canvasColors = []; + document.querySelectorAll('.canvas-slot').forEach(slot => { + slot.style.backgroundColor = ''; + slot.classList.remove('filled'); + }); + }); + + document.getElementById('hint').addEventListener('click', () => { + const emptySlot = document.querySelector('.canvas-slot:not(.filled)'); + if (emptySlot) { + const index = parseInt(emptySlot.dataset.index); + emptySlot.style.backgroundColor = this.targetHarmony[index]; + emptySlot.classList.add('filled'); + this.canvasColors[index] = this.targetHarmony[index]; + this.score = Math.max(0, this.score - 50); + this.updateDisplay(); + this.checkCompletion(); + } + }); + + document.getElementById('continue').addEventListener('click', () => { + this.nextLevel(); + }); + + const activeDroplet = document.getElementById('activeDroplet'); + activeDroplet.setAttribute('draggable', 'true'); + + activeDroplet.addEventListener('dragstart', (e) => { + e.dataTransfer.setData('text/plain', 'color'); + activeDroplet.classList.add('dragging'); + }); + + activeDroplet.addEventListener('dragend', () => { + activeDroplet.classList.remove('dragging'); + }); + } +} + +window.addEventListener('load', () => { + new ChromaCascade(); +}); \ No newline at end of file diff --git a/games/chroma-cascade/style.css b/games/chroma-cascade/style.css new file mode 100644 index 00000000..7773d2cb --- /dev/null +++ b/games/chroma-cascade/style.css @@ -0,0 +1,335 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +body { + background: linear-gradient(135deg, #1a1a2e, #16213e, #0f3460); + color: #fff; + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; +} + +.container { + max-width: 1200px; + width: 100%; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border-radius: 20px; + padding: 30px; + box-shadow: 0 15px 35px rgba(0, 0, 0, 0.5); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +header { + text-align: center; + margin-bottom: 30px; +} + +h1 { + font-size: 3.5rem; + background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + margin-bottom: 10px; + text-shadow: 0 0 30px rgba(255, 255, 255, 0.3); +} + +.subtitle { + font-size: 1.2rem; + color: #a0a0e0; + margin-bottom: 20px; +} + +.game-stats { + display: flex; + justify-content: space-around; + margin-bottom: 30px; + background: rgba(0, 0, 0, 0.3); + padding: 15px; + border-radius: 15px; +} + +.stat { + text-align: center; +} + +.stat span { + display: block; + font-size: 0.9rem; + color: #a0a0e0; + margin-bottom: 5px; +} + +.stat div { + font-size: 2rem; + font-weight: bold; + color: #4ecdc4; +} + +.game-area { + display: grid; + grid-template-columns: 1fr 2fr 1fr; + gap: 20px; + margin-bottom: 30px; +} + +.target-section, .canvas-section { + background: rgba(0, 0, 0, 0.3); + padding: 20px; + border-radius: 15px; + text-align: center; +} + +.target-section h3, .canvas-section h3 { + margin-bottom: 15px; + color: #ff6b6b; +} + +.target-colors { + display: flex; + justify-content: center; + gap: 10px; + margin-bottom: 15px; + flex-wrap: wrap; +} + +.color-swatch { + width: 50px; + height: 50px; + border-radius: 10px; + border: 2px solid white; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); +} + +.harmony-name { + font-size: 1.2rem; + font-weight: bold; + color: #96ceb4; + padding: 10px; + background: rgba(0, 0, 0, 0.5); + border-radius: 10px; +} + +.mixing-board { + background: rgba(0, 0, 0, 0.3); + padding: 20px; + border-radius: 15px; + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; +} + +.color-dropper { + width: 100px; + height: 150px; + position: relative; + margin-bottom: 20px; +} + +.droplet { + width: 60px; + height: 80px; + border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%; + position: absolute; + cursor: grab; + transition: all 0.3s ease; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4); + border: 2px solid rgba(255, 255, 255, 0.8); +} + +.droplet:hover { + transform: scale(1.1); +} + +.droplet.dragging { + cursor: grabbing; + transform: scale(1.05); + box-shadow: 0 12px 25px rgba(0, 0, 0, 0.6); +} + +.color-palette { + display: flex; + gap: 15px; + margin-bottom: 20px; + flex-wrap: wrap; + justify-content: center; +} + +.palette-color { + width: 40px; + height: 40px; + border-radius: 50%; + cursor: pointer; + border: 2px solid white; + transition: transform 0.2s ease; +} + +.palette-color:hover { + transform: scale(1.2); +} + +.mix-result { + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; +} + +.result-color { + width: 80px; + height: 80px; + border-radius: 50%; + border: 3px solid white; + box-shadow: 0 6px 15px rgba(0, 0, 0, 0.4); +} + +#addToCanvas { + background: linear-gradient(45deg, #4ecdc4, #45b7d1); + border: none; + color: white; + padding: 12px 24px; + border-radius: 25px; + cursor: pointer; + font-weight: bold; + transition: all 0.3s ease; +} + +#addToCanvas:hover { + transform: translateY(-3px); + box-shadow: 0 8px 20px rgba(78, 205, 196, 0.4); +} + +.art-canvas { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; + min-height: 200px; +} + +.canvas-slot { + background: rgba(255, 255, 255, 0.1); + border: 2px dashed rgba(255, 255, 255, 0.3); + border-radius: 10px; + min-height: 60px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.canvas-slot.filled { + border-style: solid; + border-color: rgba(255, 255, 255, 0.8); +} + +.controls { + display: flex; + justify-content: center; + gap: 20px; + margin-bottom: 30px; +} + +button { + background: linear-gradient(45deg, #ff6b6b, #ee5a52); + border: none; + color: white; + padding: 15px 30px; + border-radius: 25px; + cursor: pointer; + font-size: 1rem; + font-weight: bold; + transition: all 0.3s ease; + box-shadow: 0 6px 15px rgba(255, 107, 107, 0.3); +} + +button:hover { + transform: translateY(-3px); + box-shadow: 0 8px 20px rgba(255, 107, 107, 0.4); +} + +.instructions { + background: rgba(0, 0, 0, 0.3); + padding: 20px; + border-radius: 15px; + text-align: center; +} + +.instructions h3 { + margin-bottom: 15px; + color: #4ecdc4; +} + +.instructions p { + margin-bottom: 8px; + line-height: 1.5; +} + +.game-over { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.9); + display: none; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.game-over.active { + display: flex; +} + +.game-over-content { + background: linear-gradient(135deg, #1a1a2e, #16213e); + padding: 40px; + border-radius: 20px; + text-align: center; + border: 2px solid #4ecdc4; + box-shadow: 0 0 40px rgba(78, 205, 196, 0.5); +} + +.game-over-content h2 { + font-size: 2.5rem; + margin-bottom: 20px; + color: #ff6b6b; +} + +.game-over-content p { + font-size: 1.2rem; + margin-bottom: 15px; + color: #a0a0e0; +} + +#continue { + background: linear-gradient(45deg, #96ceb4, #88c9a1); + margin-top: 20px; +} + +@keyframes dropletFall { + 0% { transform: translateY(-100px) scale(0.8); opacity: 0; } + 100% { transform: translateY(0) scale(1); opacity: 1; } +} + +.droplet.new { + animation: dropletFall 0.5s ease-out; +} + +@keyframes pulseWarning { + 0% { color: #4ecdc4; } + 50% { color: #ff6b6b; } + 100% { color: #4ecdc4; } +} + +.timer-warning { + animation: pulseWarning 1s infinite; +} \ No newline at end of file diff --git a/games/chromatic-chase/index.html b/games/chromatic-chase/index.html new file mode 100644 index 00000000..a1e99593 --- /dev/null +++ b/games/chromatic-chase/index.html @@ -0,0 +1,18 @@ + + + + + + Chromatic Chase Game + + + +
+

Chromatic Chase

+ +
Score: 0
+
Use WASD to move. Match your color to the landscape to unlock paths!
+
+ + + \ No newline at end of file diff --git a/games/chromatic-chase/script.js b/games/chromatic-chase/script.js new file mode 100644 index 00000000..bc5fd5a0 --- /dev/null +++ b/games/chromatic-chase/script.js @@ -0,0 +1,149 @@ +// Chromatic Chase Game Script +// Chase shifting colors through a dynamic landscape, matching hues to unlock paths. + +const canvas = document.getElementById('game-canvas'); +const ctx = canvas.getContext('2d'); +const scoreElement = document.getElementById('score'); + +// Game variables +let player = { x: 50, y: 300, color: 'red', speed: 3 }; +let zones = []; +let orbs = []; +let goal = { x: 750, y: 300, width: 30, height: 30 }; +let score = 0; +let gameRunning = true; +let colorCycle = 0; + +// Colors +const colors = ['red', 'green', 'blue', 'yellow']; + +// Initialize game +function init() { + // Create zones + zones.push({ x: 0, y: 0, width: 200, height: 600, color: 'red' }); + zones.push({ x: 200, y: 0, width: 200, height: 600, color: 'green' }); + zones.push({ x: 400, y: 0, width: 200, height: 600, color: 'blue' }); + zones.push({ x: 600, y: 0, width: 200, height: 600, color: 'yellow' }); + + // Create orbs + orbs.push({ x: 150, y: 150, color: 'green' }); + orbs.push({ x: 350, y: 450, color: 'blue' }); + orbs.push({ x: 550, y: 150, color: 'yellow' }); + orbs.push({ x: 750, y: 450, color: 'red' }); + + // Start game loop + requestAnimationFrame(gameLoop); +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + update(); + draw(); + + requestAnimationFrame(gameLoop); +} + +// Update game state +function update() { + // Move player + updatePlayer(); + + // Shift colors + colorCycle++; + if (colorCycle % 100 === 0) { + zones.forEach(zone => { + const currentIndex = colors.indexOf(zone.color); + zone.color = colors[(currentIndex + 1) % colors.length]; + }); + } + + // Check zone collision + let currentZone = null; + zones.forEach(zone => { + if (player.x >= zone.x && player.x < zone.x + zone.width && + player.y >= zone.y && player.y < zone.y + zone.height) { + currentZone = zone; + } + }); + + // If not matching color, reset position + if (currentZone && player.color !== currentZone.color) { + player.x = 50; + player.y = 300; + } + + // Check orb collection + orbs.forEach((orb, i) => { + if (Math.abs(player.x - orb.x) < 20 && Math.abs(player.y - orb.y) < 20) { + player.color = orb.color; + orbs.splice(i, 1); + score += 10; + } + }); + + // Check goal + if (player.x > goal.x && player.x < goal.x + goal.width && + player.y > goal.y && player.y < goal.y + goal.height) { + gameRunning = false; + alert('You reached the goal! Score: ' + score); + } +} + +// Draw everything +function draw() { + // Clear canvas + ctx.fillStyle = '#001100'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw zones + zones.forEach(zone => { + ctx.fillStyle = zone.color; + ctx.fillRect(zone.x, zone.y, zone.width, zone.height); + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 2; + ctx.strokeRect(zone.x, zone.y, zone.width, zone.height); + }); + + // Draw orbs + orbs.forEach(orb => { + ctx.fillStyle = orb.color; + ctx.beginPath(); + ctx.arc(orb.x, orb.y, 10, 0, Math.PI * 2); + ctx.fill(); + }); + + // Draw goal + ctx.fillStyle = '#ffffff'; + ctx.fillRect(goal.x, goal.y, goal.width, goal.height); + + // Draw player + ctx.fillStyle = player.color; + ctx.beginPath(); + ctx.arc(player.x, player.y, 15, 0, Math.PI * 2); + ctx.fill(); + + // Update score + scoreElement.textContent = 'Score: ' + score; +} + +// Handle input +let keys = {}; +document.addEventListener('keydown', e => { + keys[e.key.toLowerCase()] = true; +}); +document.addEventListener('keyup', e => { + keys[e.key.toLowerCase()] = false; +}); + +// Move player +function updatePlayer() { + if (keys.w) player.y = Math.max(player.y - player.speed, 0); + if (keys.s) player.y = Math.min(player.y + player.speed, canvas.height); + if (keys.a) player.x = Math.max(player.x - player.speed, 0); + if (keys.d) player.x = Math.min(player.x + player.speed, canvas.width); +} + +// Start the game +init(); \ No newline at end of file diff --git a/games/chromatic-chase/style.css b/games/chromatic-chase/style.css new file mode 100644 index 00000000..6dec402d --- /dev/null +++ b/games/chromatic-chase/style.css @@ -0,0 +1,38 @@ +body { + font-family: Arial, sans-serif; + background-color: #000; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +#game-container { + text-align: center; +} + +h1 { + font-size: 2.5em; + margin-bottom: 20px; + text-shadow: 0 0 10px #00ff00; +} + +#game-canvas { + border: 2px solid #00ff00; + background-color: #001100; + box-shadow: 0 0 20px #00ff00; +} + +#score { + font-size: 1.2em; + margin: 10px 0; + color: #ffff00; +} + +#instructions { + font-size: 1em; + margin-top: 10px; + color: #cccccc; +} \ No newline at end of file diff --git a/games/circuit-connect/index.html b/games/circuit-connect/index.html new file mode 100644 index 00000000..b6641b79 --- /dev/null +++ b/games/circuit-connect/index.html @@ -0,0 +1,37 @@ + + + + + +Circuit Connect | Mini JS Games Hub + + + +
+

Circuit Connect ๐Ÿ”Œ

+
+ +
+
+ + + + +
+
+ + + + + +
+
+ + + + + + + + + diff --git a/games/circuit-connect/script.js b/games/circuit-connect/script.js new file mode 100644 index 00000000..5fd16f71 --- /dev/null +++ b/games/circuit-connect/script.js @@ -0,0 +1,141 @@ +const canvas = document.getElementById('circuit-board'); +const ctx = canvas.getContext('2d'); +const placeSound = document.getElementById('place-sound'); +const successSound = document.getElementById('success-sound'); +const errorSound = document.getElementById('error-sound'); + +let wires = []; +let activePiece = null; +let paused = false; +let undoStack = [], redoStack = []; + +const boardWidth = canvas.width; +const boardHeight = canvas.height; + +// Piece size +const pieceSize = 60; + +// Power & Bulb positions +const powerPos = {x: 50, y: boardHeight/2}; +const bulbPos = {x: boardWidth-50, y: boardHeight/2}; + +// Obstacles +const obstacles = [ + {x: 300, y: 100, radius: 20}, + {x: 400, y: 300, radius: 25} +]; + +function drawBoard() { + ctx.clearRect(0,0,boardWidth,boardHeight); + + // Draw obstacles + obstacles.forEach(o => { + ctx.fillStyle = 'red'; + ctx.beginPath(); + ctx.arc(o.x,o.y,o.radius,0,Math.PI*2); + ctx.fill(); + }); + + // Draw power source + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.arc(powerPos.x,powerPos.y,20,0,Math.PI*2); + ctx.fill(); + + // Draw bulb + ctx.fillStyle = isPowered() ? '#ff0' : '#555'; + ctx.beginPath(); + ctx.arc(bulbPos.x,bulbPos.y,20,0,Math.PI*2); + ctx.fill(); + + // Draw wires + wires.forEach(w => { + ctx.strokeStyle = w.powered ? '#0ff' : '#888'; + ctx.lineWidth = 6; + ctx.beginPath(); + ctx.moveTo(w.x1,w.y1); + ctx.lineTo(w.x2,w.y2); + ctx.stroke(); + }); +} + +// Power flow check +function isPowered() { + // Simple check: any wire connects power to bulb without hitting obstacles + for(let w of wires){ + if(lineHitsCircle(w.x1,w.y1,w.x2,w.y2,obstacles)) return false; + } + for(let w of wires){ + if(distance(w.x2,w.y2,bulbPos.x,bulbPos.y)<30 && distance(w.x1,w.y1,powerPos.x,powerPos.y)<30) return true; + } + return false; +} + +// Check collision with obstacles +function lineHitsCircle(x1,y1,x2,y2,circles){ + for(let c of circles){ + let dx = x2-x1; + let dy = y2-y1; + let fx = x1-c.x; + let fy = y1-c.y; + let a = dx*dx + dy*dy; + let b = 2*(fx*dx + fy*dy); + let cVal = fx*fx + fy*fy - c.radius*c.radius; + let disc = b*b - 4*a*cVal; + if(disc>=0){ + let t1 = (-b - Math.sqrt(disc)) / (2*a); + let t2 = (-b + Math.sqrt(disc)) / (2*a); + if((t1>=0 && t1<=1) || (t2>=0 && t2<=1)) return true; + } + } + return false; +} + +function distance(x1,y1,x2,y2){return Math.sqrt((x2-x1)**2+(y2-y1)**2);} + +// Mouse events +canvas.addEventListener('click', e=>{ + if(paused) return; + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + if(activePiece){ + wires.push({x1:powerPos.x,y1:powerPos.y,x2:x,y2:y,powered:false}); + placeSound.play(); + undoStack.push(JSON.parse(JSON.stringify(wires))); + drawBoard(); + if(isPowered()) successSound.play(); + } +}); + +document.querySelectorAll('.piece-btn').forEach(b=>{ + b.addEventListener('click', ()=>{ + activePiece = b.dataset.type; + }); +}); + +// Buttons +document.getElementById('restart-btn').addEventListener('click', ()=>{ + wires = []; + paused = false; + drawBoard(); +}); +document.getElementById('pause-btn').addEventListener('click', ()=>{ + paused = !paused; +}); +document.getElementById('undo-btn').addEventListener('click', ()=>{ + if(undoStack.length>0){ + redoStack.push(JSON.parse(JSON.stringify(wires))); + wires = undoStack.pop(); + drawBoard(); + } +}); +document.getElementById('redo-btn').addEventListener('click', ()=>{ + if(redoStack.length>0){ + undoStack.push(JSON.parse(JSON.stringify(wires))); + wires = redoStack.pop(); + drawBoard(); + } +}); + +drawBoard(); diff --git a/games/circuit-connect/style.css b/games/circuit-connect/style.css new file mode 100644 index 00000000..0bdf5828 --- /dev/null +++ b/games/circuit-connect/style.css @@ -0,0 +1,58 @@ +body { + font-family: 'Segoe UI', sans-serif; + background: #111; + color: #fff; + display: flex; + flex-direction: column; + align-items: center; + justify-content: start; + padding: 20px; +} + +.game-container h1 { + text-align: center; + margin-bottom: 10px; + color: #0ff; + text-shadow: 0 0 10px #0ff; +} + +.board-container { + position: relative; + margin: 20px 0; + border: 2px solid #0ff; + border-radius: 10px; + background: #222; +} + +canvas { + background: #111; +} + +.controls { + margin-bottom: 15px; +} + +.controls button, .pieces button { + margin: 5px; + padding: 10px 15px; + border-radius: 5px; + border: none; + cursor: pointer; + font-weight: bold; + background: #0ff; + color: #111; + box-shadow: 0 0 10px #0ff; + transition: all 0.2s; +} + +.controls button:hover, .pieces button:hover { + background: #0ff; + color: #000; + box-shadow: 0 0 20px #0ff, 0 0 30px #0ff inset; +} + +.pieces { + display: flex; + flex-wrap: wrap; + justify-content: center; +} diff --git a/games/city-builder/index.html b/games/city-builder/index.html new file mode 100644 index 00000000..abf7ee25 --- /dev/null +++ b/games/city-builder/index.html @@ -0,0 +1,135 @@ + + + + + + City Builder - Mini JS Games Hub + + + +
+
+

๐Ÿ™๏ธ City Builder

+

Build and manage your dream city!

+
+ +
+
+
+ +
+
+ +
+
+

Buildings

+
+ + + + + + + + +
+ +
+ + + +
+
+ +
+

City Statistics

+
+
+ Population: + 0 +
+
+ Happiness: + 50% +
+
+ Budget: + $1000 +
+
+ Power: + 0/0 +
+
+ Water: + 0/0 +
+
+ Jobs: + 0/0 +
+
+ Score: + 0 +
+
+ Day: + 1 +
+
+
+ +
+ + + +
+
+
+ +
+ +
+

How to Play:

+
    +
  • Select a building type and click on empty grid cells to place buildings
  • +
  • Use Demolish tool to remove buildings (costs money)
  • +
  • Residential buildings provide housing for population growth
  • +
  • Commercial buildings create jobs and generate income
  • +
  • Industrial buildings provide jobs but may decrease happiness
  • +
  • Power plants and water towers provide essential services
  • +
  • Parks and hospitals improve happiness and health
  • +
  • Schools educate residents and improve productivity
  • +
  • Maintain happiness above 30% to prevent population decline
  • +
  • Balance your budget and keep essential services running
  • +
+
+
+ + + + \ No newline at end of file diff --git a/games/city-builder/script.js b/games/city-builder/script.js new file mode 100644 index 00000000..d4b39c3a --- /dev/null +++ b/games/city-builder/script.js @@ -0,0 +1,543 @@ +// City Builder Game Logic +class CityBuilder { + constructor() { + this.gridSize = { width: 20, height: 15 }; + this.grid = []; + this.selectedBuilding = 'residential'; + this.currentTool = 'build'; + this.isPlaying = false; + this.gameSpeed = 1; + this.day = 1; + + // City stats + this.city = { + population: 0, + happiness: 50, + budget: 1000, + power: { current: 0, capacity: 0 }, + water: { current: 0, capacity: 0 }, + jobs: { current: 0, capacity: 0 }, + score: 0 + }; + + // Building data + this.buildings = { + residential: { + emoji: '๐Ÿ ', + cost: 100, + population: 4, + powerUsage: 1, + waterUsage: 1, + happiness: 0 + }, + commercial: { + emoji: '๐Ÿช', + cost: 200, + jobs: 6, + powerUsage: 2, + waterUsage: 1, + happiness: 5, + income: 50 + }, + industrial: { + emoji: '๐Ÿญ', + cost: 300, + jobs: 8, + powerUsage: 4, + waterUsage: 3, + happiness: -10, + income: 75 + }, + power: { + emoji: 'โšก', + cost: 500, + powerCapacity: 20, + powerUsage: 2, + waterUsage: 1, + happiness: -5 + }, + water: { + emoji: '๐Ÿ’ง', + cost: 400, + waterCapacity: 15, + powerUsage: 1, + waterUsage: 1, + happiness: -3 + }, + park: { + emoji: '๐ŸŒณ', + cost: 150, + happiness: 15, + powerUsage: 0, + waterUsage: 0 + }, + hospital: { + emoji: '๐Ÿฅ', + cost: 600, + happiness: 20, + powerUsage: 3, + waterUsage: 2, + population: -2 // Staff + }, + school: { + emoji: '๐Ÿซ', + cost: 350, + happiness: 10, + powerUsage: 2, + waterUsage: 1, + population: -3 // Staff + } + }; + + this.gameLoop = null; + this.notifications = []; + this.init(); + } + + init() { + this.initializeGrid(); + this.bindEvents(); + this.updateDisplay(); + this.showNotification('Welcome to City Builder! Start by placing some residential buildings.', 'success'); + } + + initializeGrid() { + const gridElement = document.getElementById('city-grid'); + gridElement.innerHTML = ''; + + this.grid = []; + for (let y = 0; y < this.gridSize.height; y++) { + this.grid[y] = []; + for (let x = 0; x < this.gridSize.width; x++) { + this.grid[y][x] = null; + + const cell = document.createElement('div'); + cell.className = 'grid-cell empty'; + cell.dataset.x = x; + cell.dataset.y = y; + cell.addEventListener('click', () => this.onCellClick(x, y)); + cell.addEventListener('mouseenter', () => this.onCellHover(x, y)); + gridElement.appendChild(cell); + } + } + } + + bindEvents() { + // Building selection + document.querySelectorAll('.building-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + this.selectBuilding(e.target.dataset.building); + }); + }); + + // Tool selection + document.getElementById('build-btn').addEventListener('click', () => this.selectTool('build')); + document.getElementById('demolish-btn').addEventListener('click', () => this.selectTool('demolish')); + document.getElementById('info-btn').addEventListener('click', () => this.selectTool('info')); + + // Game controls + document.getElementById('play-pause-btn').addEventListener('click', () => this.togglePlayPause()); + document.getElementById('speed-btn').addEventListener('click', () => this.changeSpeed()); + document.getElementById('reset-btn').addEventListener('click', () => this.resetGame()); + + // Keyboard shortcuts + document.addEventListener('keydown', (e) => { + switch(e.key.toLowerCase()) { + case ' ': + e.preventDefault(); + this.togglePlayPause(); + break; + case '1': + case '2': + case '3': + e.preventDefault(); + const tools = ['build', 'demolish', 'info']; + this.selectTool(tools[parseInt(e.key) - 1]); + break; + case 'r': + if (e.ctrlKey) { + e.preventDefault(); + this.resetGame(); + } + break; + } + }); + } + + selectBuilding(buildingType) { + this.selectedBuilding = buildingType; + this.currentTool = 'build'; + + // Update UI + document.querySelectorAll('.building-btn').forEach(btn => { + btn.classList.remove('active'); + }); + document.querySelector(`[data-building="${buildingType}"]`).classList.add('active'); + + document.querySelectorAll('.tool-btn').forEach(btn => { + btn.classList.remove('active'); + }); + document.getElementById('build-btn').classList.add('active'); + } + + selectTool(tool) { + this.currentTool = tool; + + document.querySelectorAll('.tool-btn').forEach(btn => { + btn.classList.remove('active'); + }); + document.getElementById(`${tool}-btn`).classList.add('active'); + } + + onCellClick(x, y) { + const cell = this.getCellElement(x, y); + + if (this.currentTool === 'build') { + this.placeBuilding(x, y); + } else if (this.currentTool === 'demolish') { + this.demolishBuilding(x, y); + } else if (this.currentTool === 'info') { + this.showBuildingInfo(x, y); + } + } + + onCellHover(x, y) { + // Clear previous selection + document.querySelectorAll('.grid-cell.selected').forEach(cell => { + cell.classList.remove('selected'); + }); + + // Show selection if building + if (this.currentTool === 'build') { + const cell = this.getCellElement(x, y); + if (this.grid[y][x] === null) { + cell.classList.add('selected'); + } + } + } + + placeBuilding(x, y) { + if (this.grid[y][x] !== null) { + this.showNotification('This space is already occupied!', 'warning'); + return; + } + + const building = this.buildings[this.selectedBuilding]; + if (this.city.budget < building.cost) { + this.showNotification('Not enough money to build this!', 'error'); + return; + } + + // Place building + this.grid[y][x] = this.selectedBuilding; + this.city.budget -= building.cost; + + // Update cell display + const cell = this.getCellElement(x, y); + cell.className = `grid-cell ${this.selectedBuilding}`; + cell.textContent = building.emoji; + + this.updateCityStats(); + this.showNotification(`Built ${this.selectedBuilding} for $${building.cost}!`, 'success'); + this.playSound('build'); + } + + demolishBuilding(x, y) { + const buildingType = this.grid[y][x]; + if (buildingType === null) { + this.showNotification('Nothing to demolish here!', 'warning'); + return; + } + + const building = this.buildings[buildingType]; + const demolitionCost = Math.floor(building.cost * 0.5); + + if (this.city.budget < demolitionCost) { + this.showNotification('Not enough money to demolish!', 'error'); + return; + } + + // Remove building + this.grid[y][x] = null; + this.city.budget -= demolitionCost; + + // Update cell display + const cell = this.getCellElement(x, y); + cell.className = 'grid-cell empty'; + cell.textContent = ''; + + this.updateCityStats(); + this.showNotification(`Demolished ${buildingType} for $${demolitionCost}!`, 'success'); + this.playSound('demolish'); + } + + showBuildingInfo(x, y) { + const buildingType = this.grid[y][x]; + if (buildingType === null) { + this.showNotification('Empty land - perfect for building!', 'info'); + return; + } + + const building = this.buildings[buildingType]; + let info = `${buildingType.toUpperCase()}\n`; + info += `Cost: $${building.cost}\n`; + + if (building.population) info += `Population: ${building.population > 0 ? '+' : ''}${building.population}\n`; + if (building.jobs) info += `Jobs: +${building.jobs}\n`; + if (building.powerCapacity) info += `Power: +${building.powerCapacity}\n`; + if (building.waterCapacity) info += `Water: +${building.waterCapacity}\n`; + if (building.happiness) info += `Happiness: ${building.happiness > 0 ? '+' : ''}${building.happiness}\n`; + if (building.income) info += `Income: $${building.income}/day\n`; + + alert(info); + } + + updateCityStats() { + // Reset stats + let newStats = { + population: 0, + happiness: 50, + power: { current: 0, capacity: 0 }, + water: { current: 0, capacity: 0 }, + jobs: { current: 0, capacity: 0 }, + dailyIncome: 0 + }; + + // Calculate from buildings + for (let y = 0; y < this.gridSize.height; y++) { + for (let x = 0; x < this.gridSize.width; x++) { + const buildingType = this.grid[y][x]; + if (buildingType) { + const building = this.buildings[buildingType]; + + if (building.population) newStats.population += building.population; + if (building.jobs) newStats.jobs.capacity += building.jobs; + if (building.powerCapacity) newStats.power.capacity += building.powerCapacity; + if (building.waterCapacity) newStats.water.capacity += building.waterCapacity; + if (building.happiness) newStats.happiness += building.happiness; + if (building.income) newStats.dailyIncome += building.income; + + // Resource usage + if (building.powerUsage) newStats.power.current += building.powerUsage; + if (building.waterUsage) newStats.water.current += building.waterUsage; + } + } + } + + // Apply resource shortages + if (newStats.power.current > newStats.power.capacity) { + newStats.happiness -= 20; + } + if (newStats.water.current > newStats.water.capacity) { + newStats.happiness -= 15; + } + + // Population can't exceed housing + newStats.population = Math.max(0, Math.min(newStats.population, this.city.population + 1)); + + // Update city stats + this.city.population = newStats.population; + this.city.happiness = Math.max(0, Math.min(100, newStats.happiness)); + this.city.power = newStats.power; + this.city.water = newStats.water; + this.city.jobs = newStats.jobs; + + // Calculate score + this.city.score = Math.floor( + this.city.population * 10 + + this.city.happiness * 5 + + (this.city.power.capacity - this.city.power.current) * 2 + + (this.city.water.capacity - this.city.water.current) * 2 + + this.city.jobs.capacity * 3 + ); + + this.updateDisplay(); + } + + togglePlayPause() { + this.isPlaying = !this.isPlaying; + const btn = document.getElementById('play-pause-btn'); + + if (this.isPlaying) { + btn.textContent = 'โธ๏ธ Pause'; + this.startGameLoop(); + } else { + btn.textContent = 'โ–ถ๏ธ Play'; + this.stopGameLoop(); + } + } + + changeSpeed() { + const speeds = [1, 2, 4]; + const currentIndex = speeds.indexOf(this.gameSpeed); + this.gameSpeed = speeds[(currentIndex + 1) % speeds.length]; + + document.getElementById('speed-btn').textContent = `${this.gameSpeed}x Speed`; + } + + startGameLoop() { + this.stopGameLoop(); + this.gameLoop = setInterval(() => { + this.advanceDay(); + }, 5000 / this.gameSpeed); // 5 seconds per day at 1x speed + } + + stopGameLoop() { + if (this.gameLoop) { + clearInterval(this.gameLoop); + this.gameLoop = null; + } + } + + advanceDay() { + this.day++; + + // Daily income + let dailyIncome = 0; + for (let y = 0; y < this.gridSize.height; y++) { + for (let x = 0; x < this.gridSize.width; x++) { + const buildingType = this.grid[y][x]; + if (buildingType && this.buildings[buildingType].income) { + dailyIncome += this.buildings[buildingType].income; + } + } + } + + // Tax income from population + dailyIncome += Math.floor(this.city.population * 2); + + this.city.budget += dailyIncome; + + // Population growth + if (this.city.happiness > 60 && this.city.population < this.getMaxPopulation()) { + this.city.population += Math.floor(Math.random() * 3) + 1; + } else if (this.city.happiness < 30) { + this.city.population = Math.max(0, this.city.population - Math.floor(Math.random() * 2) + 1); + } + + this.updateCityStats(); + + if (this.day % 10 === 0) { + this.showNotification(`Day ${this.day}: Your city is growing!`, 'success'); + } + } + + getMaxPopulation() { + let housing = 0; + for (let y = 0; y < this.gridSize.height; y++) { + for (let x = 0; x < this.gridSize.width; x++) { + const buildingType = this.grid[y][x]; + if (buildingType === 'residential') { + housing += this.buildings.residential.population; + } + } + } + return housing; + } + + updateDisplay() { + document.getElementById('population').textContent = this.city.population; + document.getElementById('happiness').textContent = `${this.city.happiness}%`; + document.getElementById('budget').textContent = `$${this.city.budget}`; + document.getElementById('power').textContent = `${this.city.power.current}/${this.city.power.capacity}`; + document.getElementById('water').textContent = `${this.city.water.current}/${this.city.water.capacity}`; + document.getElementById('jobs').textContent = `${this.city.jobs.current}/${this.city.jobs.capacity}`; + document.getElementById('score').textContent = this.city.score; + document.getElementById('day').textContent = this.day; + + // Update stat colors + this.updateStatColor('happiness', this.city.happiness, 30, 70); + this.updateStatColor('budget', this.city.budget, 0, 500); + this.updateStatColor('power', this.city.power.capacity - this.city.power.current, -10, 5); + this.updateStatColor('water', this.city.water.capacity - this.city.water.current, -10, 5); + } + + updateStatColor(statId, value, warningThreshold, dangerThreshold) { + const element = document.getElementById(statId); + element.className = 'stat-value'; + + if (value <= dangerThreshold) { + element.classList.add('danger'); + } else if (value <= warningThreshold) { + element.classList.add('warning'); + } else { + element.classList.add('positive'); + } + } + + showNotification(message, type = 'info') { + const notifications = document.getElementById('notifications'); + const notification = document.createElement('div'); + notification.className = `notification ${type}`; + notification.textContent = message; + + notifications.appendChild(notification); + + setTimeout(() => { + notification.remove(); + }, 4000); + } + + playSound(action) { + // Simple sound effects using Web Audio API + try { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + let frequency = 440; // A4 + switch(action) { + case 'build': frequency = 523; break; // C5 + case 'demolish': frequency = 330; break; // E4 + } + + oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime); + gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.start(); + oscillator.stop(audioContext.currentTime + 0.3); + } catch (e) { + // Web Audio API not supported, skip sound + } + } + + getCellElement(x, y) { + return document.querySelector(`[data-x="${x}"][data-y="${y}"]`); + } + + resetGame() { + if (confirm('Are you sure you want to reset the game? All progress will be lost!')) { + this.stopGameLoop(); + this.day = 1; + this.city = { + population: 0, + happiness: 50, + budget: 1000, + power: { current: 0, capacity: 0 }, + water: { current: 0, capacity: 0 }, + jobs: { current: 0, capacity: 0 }, + score: 0 + }; + this.initializeGrid(); + this.updateDisplay(); + this.showNotification('Game reset! Start building your city again.', 'success'); + } + } +} + +// Initialize the game when the page loads +document.addEventListener('DOMContentLoaded', () => { + new CityBuilder(); +}); + +// Enable audio on first user interaction +document.addEventListener('click', () => { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + if (audioContext && audioContext.state === 'suspended') { + audioContext.resume(); + } +}, { once: true }); \ No newline at end of file diff --git a/games/city-builder/style.css b/games/city-builder/style.css new file mode 100644 index 00000000..a45c3385 --- /dev/null +++ b/games/city-builder/style.css @@ -0,0 +1,404 @@ +/* City Builder Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #87CEEB 0%, #98FB98 100%); + color: #2c3e50; + min-height: 100vh; + padding: 20px; +} + +body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle at 20% 80%, rgba(135, 206, 235, 0.3) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(152, 251, 152, 0.3) 0%, transparent 50%), + radial-gradient(circle at 40% 40%, rgba(255, 255, 224, 0.2) 0%, transparent 50%); + pointer-events: none; + z-index: -1; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 30px; +} + +header h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.1); + background: linear-gradient(45deg, #ff6b6b, #ffd93d, #6bcf7f, #4d96ff); + background-size: 400% 400%; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + animation: gradientShift 3s ease infinite; +} + +@keyframes gradientShift { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +header p { + font-size: 1.2em; + opacity: 0.8; +} + +.game-area { + display: grid; + grid-template-columns: 1fr 350px; + gap: 20px; + margin-bottom: 30px; +} + +.city-grid-container { + background: rgba(255, 255, 255, 0.9); + padding: 20px; + border-radius: 15px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(10px); + border: 2px solid rgba(255, 255, 255, 0.2); +} + +.city-grid { + display: grid; + grid-template-columns: repeat(20, 1fr); + grid-template-rows: repeat(15, 1fr); + gap: 1px; + background: #34495e; + border: 2px solid #2c3e50; + border-radius: 10px; + overflow: hidden; + aspect-ratio: 4/3; + max-width: 100%; +} + +.grid-cell { + background: #27ae60; + border: 1px solid #229954; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2em; + transition: all 0.2s ease; + position: relative; +} + +.grid-cell:hover { + background: #2ecc71; + transform: scale(1.1); + z-index: 10; +} + +.grid-cell.empty:hover { + background: #f39c12; +} + +.grid-cell.selected { + box-shadow: 0 0 10px rgba(255, 215, 0, 0.8); + animation: pulse 1s infinite; +} + +@keyframes pulse { + 0% { box-shadow: 0 0 10px rgba(255, 215, 0, 0.8); } + 50% { box-shadow: 0 0 15px rgba(255, 215, 0, 1); } + 100% { box-shadow: 0 0 10px rgba(255, 215, 0, 0.8); } +} + +.grid-cell.residential { background: #e74c3c; } +.grid-cell.commercial { background: #f39c12; } +.grid-cell.industrial { background: #9b59b6; } +.grid-cell.power { background: #f1c40f; } +.grid-cell.water { background: #3498db; } +.grid-cell.park { background: #27ae60; } +.grid-cell.hospital { background: #e91e63; } +.grid-cell.school { background: #ff9800; } + +.control-panel { + display: flex; + flex-direction: column; + gap: 20px; +} + +.building-selection, .city-stats { + background: rgba(255, 255, 255, 0.9); + padding: 20px; + border-radius: 15px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.building-selection h3, .city-stats h3 { + color: #2c3e50; + margin-bottom: 15px; + text-align: center; +} + +.building-buttons { + display: grid; + grid-template-columns: 1fr; + gap: 8px; + margin-bottom: 15px; +} + +.building-btn { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + border: none; + padding: 10px 12px; + border-radius: 8px; + font-size: 0.9em; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + position: relative; + overflow: hidden; +} + +.building-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.building-btn.active { + background: linear-gradient(135deg, #ffd93d, #ff6b6b); + animation: buildingGlow 2s infinite; +} + +@keyframes buildingGlow { + 0%, 100% { box-shadow: 0 2px 10px rgba(255, 217, 61, 0.4); } + 50% { box-shadow: 0 2px 15px rgba(255, 217, 61, 0.8); } +} + +.cost { + position: absolute; + top: 5px; + right: 8px; + font-size: 0.7em; + background: rgba(0, 0, 0, 0.7); + color: white; + padding: 2px 4px; + border-radius: 3px; +} + +.tool-buttons { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 5px; +} + +.tool-btn { + background: #95a5a6; + color: white; + border: none; + padding: 8px 10px; + border-radius: 6px; + font-size: 0.8em; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; +} + +.tool-btn:hover { + background: #7f8c8d; + transform: translateY(-1px); +} + +.tool-btn.active { + background: #3498db; +} + +.stat-group { + display: grid; + grid-template-columns: 1fr; + gap: 10px; +} + +.stat-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} + +.stat-item:last-child { + border-bottom: none; +} + +.stat-label { + font-weight: bold; + color: #2c3e50; +} + +.stat-value { + font-weight: bold; + color: #e74c3c; +} + +.stat-value.positive { color: #27ae60; } +.stat-value.warning { color: #f39c12; } +.stat-value.danger { color: #e74c3c; } + +.game-controls { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; +} + +.game-controls button { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + border: none; + padding: 12px 15px; + border-radius: 8px; + font-size: 0.9em; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.game-controls button:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.notifications { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; + max-width: 300px; +} + +.notification { + background: rgba(255, 255, 255, 0.95); + border-radius: 10px; + padding: 15px; + margin-bottom: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + border-left: 4px solid #3498db; + animation: slideIn 0.5s ease-out; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.notification.success { border-left-color: #27ae60; } +.notification.warning { border-left-color: #f39c12; } +.notification.error { border-left-color: #e74c3c; } + +.instructions { + background: rgba(255, 255, 255, 0.9); + padding: 20px; + border-radius: 15px; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.instructions h3 { + color: #2c3e50; + margin-bottom: 15px; +} + +.instructions ul { + list-style: none; + padding: 0; +} + +.instructions li { + margin-bottom: 8px; + padding-left: 20px; + position: relative; +} + +.instructions li:before { + content: "๐Ÿ—๏ธ"; + position: absolute; + left: 0; + color: #f39c12; +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .game-area { + grid-template-columns: 1fr; + gap: 15px; + } + + .city-grid { + grid-template-columns: repeat(15, 1fr); + grid-template-rows: repeat(12, 1fr); + } +} + +@media (max-width: 768px) { + .city-grid { + grid-template-columns: repeat(12, 1fr); + grid-template-rows: repeat(10, 1fr); + } + + .building-buttons { + grid-template-columns: repeat(2, 1fr); + } + + .tool-buttons { + grid-template-columns: repeat(3, 1fr); + } + + .game-controls { + grid-template-columns: 1fr; + } + + header h1 { + font-size: 2em; + } +} + +@media (max-width: 480px) { + .city-grid { + grid-template-columns: repeat(10, 1fr); + grid-template-rows: repeat(8, 1fr); + } + + .building-buttons { + grid-template-columns: 1fr; + } + + .tool-buttons { + grid-template-columns: repeat(2, 1fr); + } + + .tool-buttons button:last-child { + grid-column: span 2; + } +} \ No newline at end of file diff --git a/games/click-combo-quiz/index.html b/games/click-combo-quiz/index.html new file mode 100644 index 00000000..7875e2aa --- /dev/null +++ b/games/click-combo-quiz/index.html @@ -0,0 +1,45 @@ + + + + + + Click Combo Game Quiz + + + + +
+
+

Click Combo Game Quiz

+
+
Score: 0
+
Combo ร— 1
+
Time: 10s
+
+
+
+
+
+ +
+

Loading question...

+
+
+ +
+ +
+
+ + + + + + + diff --git a/games/click-combo-quiz/script.js b/games/click-combo-quiz/script.js new file mode 100644 index 00000000..c328b4de --- /dev/null +++ b/games/click-combo-quiz/script.js @@ -0,0 +1,151 @@ +// ========================= +// QUIZ QUESTIONS +// ========================= +const questions = [ + { + question: "Which language runs in a browser?", + answers: ["Java", "C++", "Python", "JavaScript"], + correct: "JavaScript", + }, + { + question: "2 + 2 ร— 2 = ?", + answers: ["6", "8", "4", "10"], + correct: "6", + }, + { + question: "Capital of Japan?", + answers: ["Beijing", "Tokyo", "Seoul", "Bangkok"], + correct: "Tokyo", + }, + { + question: "HTML stands for?", + answers: ["Hyper Trainer Marking Language","Hyper Text Markup Language","Hyper Tag Markup Language","None"], + correct: "Hyper Text Markup Language", + }, + { + question: "CSS controls...?", + answers: ["Structure", "Logic", "Styling", "Database"], + correct: "Styling", + }, +]; + +// ========================= +// GLOBAL VARIABLES +// ========================= +let index = 0; +let score = 0; +let combo = 1; +let streak = 0; +let timer = 10; +let timerInterval; +let soundOn = true; + +// ========================= +// ELEMENTS +// ========================= +const q = document.getElementById("question"); +const ans = document.getElementById("answers"); +const scoreEl = document.getElementById("score"); +const comboEl = document.getElementById("combo"); +const timerEl = document.getElementById("timer"); +const streakProgress = document.getElementById("streak-progress"); +const modal = document.getElementById("game-over-modal"); +const finalScore = document.getElementById("final-score"); + +// ========================= +// SOUNDS +// ========================= +const correctSound = new Audio("https://cdn.pixabay.com/audio/2022/03/07/audio_1b72dfc4de.mp3"); +const wrongSound = new Audio("https://cdn.pixabay.com/audio/2022/03/15/audio_1be0e962cf.mp3"); + +// ========================= +// FUNCTIONS +// ========================= +function loadQuestion() { + q.textContent = questions[index].question; + ans.innerHTML = ""; + + questions[index].answers.forEach((a) => { + const btn = document.createElement("button"); + btn.className = "answer-btn"; + btn.textContent = a; + btn.onclick = () => checkAnswer(a, btn); + ans.appendChild(btn); + }); + + resetTimer(); +} + +function resetTimer() { + clearInterval(timerInterval); + timer = 10; + timerEl.textContent = timer; + + timerInterval = setInterval(() => { + timer--; + timerEl.textContent = timer; + if (timer <= 0) wrongAnswer(); + }, 1000); +} + +function checkAnswer(answer, btn) { + if (answer === questions[index].correct) { + btn.classList.add("correct"); + + score += 10 * combo; + streak++; + combo = Math.min(combo + 1, 10); + comboEl.textContent = combo; + + streakProgress.style.width = `${streak * 10}%`; + + if (soundOn) correctSound.play(); + } else { + wrongAnswer(btn); + return; + } + + scoreEl.textContent = score; + + setTimeout(nextQuestion, 700); +} + +function wrongAnswer(btn) { + if (btn) btn.classList.add("wrong"); + + if (soundOn) wrongSound.play(); + + combo = 1; + streak = 0; + comboEl.textContent = combo; + streakProgress.style.width = "0%"; + + setTimeout(nextQuestion, 700); +} + +function nextQuestion() { + index++; + if (index >= questions.length) return endGame(); + loadQuestion(); +} + +function endGame() { + clearInterval(timerInterval); + finalScore.textContent = score; + modal.classList.remove("hide"); +} + +// ========================= +// EXTRA UI BUTTONS +// ========================= +document.getElementById("restart").onclick = () => location.reload(); + +document.getElementById("sound-toggle").onclick = function() { + soundOn = !soundOn; + this.textContent = soundOn ? "๐Ÿ”Š Sound On" : "๐Ÿ”‡ Sound Off"; +}; + +// ========================= +// START +// ========================= +loadQuestion(); diff --git a/games/click-combo-quiz/style.css b/games/click-combo-quiz/style.css new file mode 100644 index 00000000..ced87995 --- /dev/null +++ b/games/click-combo-quiz/style.css @@ -0,0 +1,142 @@ +@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap'); + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Poppins', sans-serif; +} + +body { + background: linear-gradient(135deg, #122958, #0f1626); + color: #fff; + display: flex; + justify-content: center; + padding: 20px; +} + +.container { + background: rgba(255, 255, 255, 0.1); + width: 100%; + max-width: 750px; + padding: 20px; + border-radius: 12px; + backdrop-filter: blur(10px); + animation: fadeIn 0.6s ease; +} + +header h1 { + text-align: center; + margin-bottom: 15px; + letter-spacing: 2px; + text-shadow: 0 0 8px #ffffff8a; +} + +.stats { + display:flex; + justify-content:space-between; + margin-bottom:10px; + font-size:18px; +} + +#streak-bar { + background:#0000004d; + height:8px; + border-radius:5px; + overflow:hidden; + margin-bottom:20px; +} + +#streak-progress { + height:100%; + background: #00ff0d; + width:0%; + transition: width .3s ease; +} + +#question { + text-align:center; + font-size:24px; + margin-bottom:20px; +} + +#answers { + display:grid; + grid-template-columns: repeat(2, 1fr); + gap:15px; +} + +.answer-btn { + padding:15px; + background:#ffffff20; + border:none; + font-size:16px; + cursor:pointer; + border-radius:8px; + transition:0.25s ease; +} + +.answer-btn:hover { + background:#00b7ff48; + transform:scale(1.05); +} + +.correct { + background:#00ff408a !important; +} + +.wrong { + background:#ff00339a !important; +} + +footer { + text-align:center; + margin-top:20px; +} + +#sound-toggle { + padding:10px 20px; + background:#ffcc00; + border:none; + border-radius:6px; + cursor:pointer; +} + +/* Modal */ +.modal { + position:fixed; + top:0; left:0; + width:100%; height:100%; + background:#000000b7; + display:flex; + justify-content:center; + align-items:center; +} + +.modal-content { + padding:30px; + background:#111831; + width:80%; + max-width:400px; + border-radius:10px; + text-align:center; +} + +.hide { + display:none; +} + +button#restart { + margin-top:20px; + padding:10px 30px; + background:#2cff15; + border:none; + border-radius:8px; + font-size:18px; + cursor:pointer; +} + +@keyframes fadeIn { + from {opacity:0; transform:scale(.8);} + to {opacity:1; transform:scale(1);} +} diff --git a/games/clue_connecter/index.html b/games/clue_connecter/index.html new file mode 100644 index 00000000..46669f26 --- /dev/null +++ b/games/clue_connecter/index.html @@ -0,0 +1,42 @@ + + + + + + Clue Connector Word Game + + + + +
+

๐Ÿง  Clue Connector

+ +
+ Score: 0 / 0 +
+ +
+

Clue 1:

+

Click START to load the first puzzle!

+ +

Clue 2:

+

Ready?

+
+ +
+ + +
+ +
+
+ +
+ + +
+
+ + + + \ No newline at end of file diff --git a/games/clue_connecter/script.js b/games/clue_connecter/script.js new file mode 100644 index 00000000..5098a410 --- /dev/null +++ b/games/clue_connecter/script.js @@ -0,0 +1,165 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. GAME DATA --- + const clueSet = [ + { clue1: "A deep sleep", clue2: "The opposite of war", answer: "Peace" }, + { clue1: "A metallic element", clue2: "What you write with", answer: "Lead" }, + { clue1: "The highest number on a clock", clue2: "The zodiac sign of the archer", answer: "Sagittarius" }, + { clue1: "A type of financial loan", clue2: "What a dog pulls", answer: "Sled" }, + { clue1: "Where the sun sets", clue2: "Not East", answer: "West" }, + { clue1: "Used to unlock a door", clue2: "A low area between hills", answer: "Key" } + ]; + + // --- 2. DOM Elements --- + const clueOneDisplay = document.getElementById('clue-one'); + const clueTwoDisplay = document.getElementById('clue-two'); + const playerInput = document.getElementById('player-input'); + const submitButton = document.getElementById('submit-button'); + const feedbackMessage = document.getElementById('feedback-message'); + const scoreDisplay = document.getElementById('score-display'); + const totalRoundsDisplay = document.getElementById('total-rounds'); + const startButton = document.getElementById('start-button'); + const nextButton = document.getElementById('next-button'); + + // --- 3. GAME STATE VARIABLES --- + let currentRounds = []; // Shuffled array of clues for the current game + let currentRoundIndex = 0; + let score = 0; + let gameActive = false; + + // --- 4. UTILITY FUNCTIONS --- + + /** + * Shuffles an array in place (Fisher-Yates). + */ + function shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + } + + /** + * Normalizes a string for comparison (lowercase, trim whitespace). + */ + function normalizeString(str) { + return str.toLowerCase().trim(); + } + + // --- 5. CORE GAME FUNCTIONS --- + + /** + * Initializes the game by shuffling clues and resetting score. + */ + function startGame() { + gameActive = true; + shuffleArray(clueSet); + currentRounds = clueSet; + totalRoundsDisplay.textContent = currentRounds.length; + + currentRoundIndex = 0; + score = 0; + scoreDisplay.textContent = score; + + startButton.style.display = 'none'; + nextButton.style.display = 'none'; + + loadClue(); + } + + /** + * Loads the next set of clues onto the screen. + */ + function loadClue() { + if (currentRoundIndex >= currentRounds.length) { + endGame(); + return; + } + + const currentClue = currentRounds[currentRoundIndex]; + + // Update display + clueOneDisplay.textContent = currentClue.clue1; + clueTwoDisplay.textContent = currentClue.clue2; + feedbackMessage.textContent = 'Enter the single word that connects them!'; + feedbackMessage.style.color = '#343a40'; + + // Enable input + playerInput.value = ''; + playerInput.disabled = false; + submitButton.disabled = false; + playerInput.focus(); + nextButton.style.display = 'none'; + } + + /** + * Checks the player's guess against the correct answer. + */ + function checkGuess() { + const currentClue = currentRounds[currentRoundIndex]; + const playerGuess = playerInput.value; + + // Normalize both strings + const normalizedGuess = normalizeString(playerGuess); + const normalizedAnswer = normalizeString(currentClue.answer); + + // Disable input after submission + playerInput.disabled = true; + submitButton.disabled = true; + + if (normalizedGuess === normalizedAnswer) { + score++; + scoreDisplay.textContent = score; + feedbackMessage.innerHTML = '๐ŸŽ‰ **CORRECT!** You connected the clues.'; + feedbackMessage.style.color = '#28a745'; + } else { + feedbackMessage.innerHTML = `โŒ **INCORRECT.** The connecting word was: **${currentClue.answer}**`; + feedbackMessage.style.color = '#dc3545'; + } + + // Prepare for next round + nextButton.style.display = 'block'; + } + + /** + * Moves the game to the next clue. + */ + function nextClue() { + currentRoundIndex++; + loadClue(); + } + + /** + * Ends the game and shows the final score. + */ + function endGame() { + gameActive = false; + clueOneDisplay.textContent = 'GAME OVER!'; + clueTwoDisplay.textContent = ''; + feedbackMessage.innerHTML = ` +

GAME COMPLETE!

+

Final Score: ${score} out of ${currentRounds.length}.

+ `; + feedbackMessage.style.color = '#ffc107'; + nextButton.style.display = 'none'; + + startButton.textContent = 'PLAY AGAIN'; + startButton.style.display = 'block'; + } + + // --- 6. EVENT LISTENERS --- + + startButton.addEventListener('click', startGame); + nextButton.addEventListener('click', nextClue); + submitButton.addEventListener('click', checkGuess); + + // Allow 'Enter' key to submit the guess + playerInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter' && !submitButton.disabled) { + checkGuess(); + } + }); + + // Initial message + clueOneDisplay.textContent = 'A single word...'; + clueTwoDisplay.textContent = '...connects these two ideas.'; +}); \ No newline at end of file diff --git a/games/clue_connecter/style.css b/games/clue_connecter/style.css new file mode 100644 index 00000000..301177d7 --- /dev/null +++ b/games/clue_connecter/style.css @@ -0,0 +1,125 @@ +body { + font-family: 'Verdana', sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f8f9fa; + color: #343a40; +} + +#game-container { + background-color: white; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 550px; + width: 90%; +} + +h1 { + color: #ffc107; /* Yellow/Gold for puzzle */ + margin-bottom: 20px; +} + +#status-area { + font-size: 1.1em; + font-weight: bold; + margin-bottom: 25px; + color: #17a2b8; +} + +/* --- Clue Area --- */ +#clue-area { + background-color: #e9ecef; + padding: 20px; + border-radius: 8px; + margin-bottom: 30px; + border: 1px solid #ced4da; +} + +#clue-area h2 { + color: #007bff; + font-size: 1.1em; + margin: 10px 0 5px 0; +} + +#clue-one, #clue-two { + font-size: 1.4em; + font-style: italic; + font-weight: 500; + margin: 5px 0 15px 0; + padding: 5px; +} + +/* --- Input Area --- */ +#input-area { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +#player-input { + flex-grow: 1; + padding: 10px; + font-size: 1.1em; + border: 2px solid #ccc; + border-radius: 5px; +} + +#submit-button { + padding: 10px 20px; + font-size: 1.1em; + background-color: #28a745; /* Green */ + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; +} + +#submit-button:hover:not(:disabled) { + background-color: #1e7e34; +} + +#player-input:disabled, #submit-button:disabled { + background-color: #bdc3c7; + cursor: not-allowed; +} + +/* --- Controls and Feedback --- */ +#feedback-message { + min-height: 1.5em; + font-weight: bold; + margin-bottom: 20px; +} + +#controls button { + padding: 10px 20px; + font-size: 1.1em; + font-weight: bold; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +#start-button { + background-color: #ffc107; + color: #333; +} + +#start-button:hover { + background-color: #e0a800; +} + +#next-button { + background-color: #17a2b8; /* Cyan/Blue */ + color: white; +} + +#next-button:hover { + background-color: #138496; +} \ No newline at end of file diff --git a/games/code-breaker-mini/index.html b/games/code-breaker-mini/index.html new file mode 100644 index 00000000..bfbc85e7 --- /dev/null +++ b/games/code-breaker-mini/index.html @@ -0,0 +1,118 @@ + + + + + + Code Breaker Mini โ€” Mini JS Games Hub + + + + +
+
+
+ ๐Ÿ” +

Code Breaker Mini

+
+
+ + + +
+
+ +
+
+
+
+ +
8
+
+
+ +
Classic (3-digit)
+
+
+ +
+ + +
+
_ _ _
+
+ + + + + + + + + + + + +
+
+ + +
+
+ +
+

Guess the secret 3-digit code.

+
+
+
+
+ + +
+ +
+ +
+
+ + + + + + + + + diff --git a/games/code-breaker-mini/script.js b/games/code-breaker-mini/script.js new file mode 100644 index 00000000..8d96e9c9 --- /dev/null +++ b/games/code-breaker-mini/script.js @@ -0,0 +1,311 @@ +/* Code Breaker Mini โ€” script.js + Features: + - 3-digit secret code (0-9 each, repeats allowed) + - On-screen numpad + keyboard input + - Hints per digit: correct pos (good), correct digit wrong pos (mid), absent (bad) + - Attempts limit, restart, pause/resume, toggle sound + - History tracking & small smart-suggest hint +*/ + +// Config +const MAX_ATTEMPTS = 8; +const CODE_LENGTH = 3; + +// State +let secret = []; +let currentGuess = []; +let attemptsLeft = MAX_ATTEMPTS; +let paused = false; +let soundOn = true; + +// DOM +const guessDisplay = document.getElementById('guess-display'); +const attemptsLeftEl = document.getElementById('attempts-left'); +const historyEl = document.getElementById('history'); +const messageEl = document.getElementById('message'); +const numKeys = Array.from(document.querySelectorAll('.num-key')); +const submitBtn = document.getElementById('submit-guess'); +const backspaceBtn = document.getElementById('backspace'); +const restartBtn = document.getElementById('restart-btn'); +const pauseBtn = document.getElementById('pause-btn'); +const soundToggleBtn = document.getElementById('sound-toggle'); +const hintBtn = document.getElementById('hint-btn'); +const autoGuessBtn = document.getElementById('auto-guess'); +const bulbs = Array.from(document.querySelectorAll('.bulb')); + +// Sounds +const sfxClick = document.getElementById('sfx-click'); +const sfxWin = document.getElementById('sfx-win'); +const sfxLose = document.getElementById('sfx-lose'); +const sfxHint = document.getElementById('sfx-hint'); + +function playSound(el){ + if(!soundOn || !el) return; + try{ el.currentTime = 0; el.play(); } catch(e){} +} + +function randDigit(){ return Math.floor(Math.random()*10); } + +function generateSecret(){ + secret = []; + for(let i=0;i currentGuess[i] === undefined ? '_' : currentGuess[i]); + guessDisplay.textContent = shown.join(' '); + bulbs.forEach((b, i) => { + b.classList.remove('good','mid','bad','glow'); + if(currentGuess[i] !== undefined){ + // tiny flash to show input + b.classList.add('glow'); + setTimeout(()=>b.classList.remove('glow'),220); + } + }); +} + +function updateAttemptsDisplay(){ + attemptsLeftEl.textContent = attemptsLeft; +} + +function resetHistory(){ + historyEl.innerHTML = ''; +} + +function addHistoryRow(guess, feedback){ + const row = document.createElement('div'); + row.className = 'row'; + const guessEl = document.createElement('div'); + guessEl.className = 'guess'; + guessEl.textContent = guess.join(''); + const feedbackEl = document.createElement('div'); + feedbackEl.className = 'feedback'; + // feedback: array of 'good'/'mid'/'bad' + feedback.forEach(f => { + const dot = document.createElement('span'); + dot.className = 'hint-dot'; + if(f==='good'){ dot.style.background = 'linear-gradient(90deg,#9ef2c3,#5ad397)'; dot.title='Correct position';} + else if(f==='mid'){ dot.style.background='linear-gradient(90deg,#ffd97a,#ffb347)'; dot.title='Correct digit wrong position';} + else { dot.style.background='linear-gradient(90deg,#c6cbd6,#9fa6b3)'; dot.title='Digit not present';} + feedbackEl.appendChild(dot); + }); + row.appendChild(guessEl); + row.appendChild(feedbackEl); + historyEl.prepend(row); +} + +function evaluateGuess(guess){ + // produce feedback array per index + // copy secret to mark used digits + const feedback = new Array(CODE_LENGTH).fill('bad'); + const secretCopy = secret.slice(); + // first pass: correct position + for(let i=0;i { + b.classList.remove('good','mid','bad'); + b.classList.add(feedback[i]); + }); +} + +function checkWin(feedback){ + return feedback.every(f => f==='good'); +} + +function submitCurrentGuess(){ + if(paused) return; + if(currentGuess.length !== CODE_LENGTH){ + setMessage(`Enter ${CODE_LENGTH} digits before submitting.`, 'warn'); + playSound(sfxClick); + return; + } + const guess = currentGuess.map(v => Number(v)); + const feedback = evaluateGuess(guess); + addHistoryRow(guess, feedback); + applyFeedbackToBulbs(feedback); + playSound(sfxClick); + if(checkWin(feedback)){ + // win + setMessage(`๐ŸŽ‰ You cracked it! Code: ${secret.join('')}`, 'win'); + playSound(sfxWin); + // reveal bulbs in win color + bulbs.forEach(b => b.classList.add('good')); + endGame(true); + return; + } + attemptsLeft--; + updateAttemptsDisplay(); + if(attemptsLeft <= 0){ + // lose + setMessage(`๐Ÿ’ฅ Out of attempts! The code was ${secret.join('')}.`, 'lose'); + playSound(sfxLose); + bulbs.forEach((b,i) => b.classList.add('bad')); + endGame(false); + return; + } + setMessage(`Attempt recorded. ${attemptsLeft} attempts left.`, 'neutral'); + // clear current guess + currentGuess = []; + refreshGuessDisplay(); +} + +function endGame(won){ + paused = true; + pauseBtn.textContent = '๐Ÿ”'; // acts as resume/new +} + +function restartGame(){ + paused = false; + generateSecret(); + attemptsLeft = MAX_ATTEMPTS; + currentGuess = []; + updateAttemptsDisplay(); + resetHistory(); + refreshGuessDisplay(); + bulbs.forEach(b => b.classList.remove('good','mid','bad','glow')); + setMessage('New code generated โ€” good luck!', 'neutral'); + playSound(sfxClick); + pauseBtn.textContent = 'โธ๏ธ'; +} + +function togglePause(){ + paused = !paused; + pauseBtn.textContent = paused ? 'โ–ถ๏ธ' : 'โธ๏ธ'; + setMessage(paused ? 'Game paused.' : 'Game resumed.'); + playSound(sfxClick); +} + +function toggleSound(){ + soundOn = !soundOn; + soundToggleBtn.textContent = soundOn ? '๐Ÿ”Š' : '๐Ÿ”ˆ'; + setMessage(soundOn ? 'Sound on' : 'Sound off'); +} + +function addDigit(d){ + if(paused) return; + if(currentGuess.length >= CODE_LENGTH) { + // optional: rotate or ignore + return; + } + currentGuess.push(String(d)); + refreshGuessDisplay(); + playSound(sfxClick); +} + +function backspace(){ + if(paused) return; + currentGuess.pop(); + refreshGuessDisplay(); + playSound(sfxClick); +} + +// smart (very basic) suggestion: tries to return a digit combination using process of elimination +function smartSuggest(){ + // find digits not present in history 'all absent' and suggest likely digits from 0-9 + const tested = new Set(); + const absent = new Set(); + const present = new Set(); + // analyze history (DOM) + Array.from(historyEl.children).forEach(row => { + const guessText = row.querySelector('.guess').textContent.trim(); + const dots = Array.from(row.querySelectorAll('.hint-dot')); + for(let i=0;i0){ + suggestion.push([...present][i % present.size]); + } else { + suggestion.push(Math.floor(Math.random()*10)); + } + } + setMessage(`Suggested guess: ${suggestion.join('')}`, 'neutral'); + return suggestion; +} + +/* Event wiring */ +numKeys.forEach(k => { + k.addEventListener('click', e => { + addDigit(k.dataset.key); + }); +}); + +backspaceBtn.addEventListener('click', e => backspace()); +submitBtn.addEventListener('click', e => submitCurrentGuess()); +restartBtn.addEventListener('click', e => restartGame()); +pauseBtn.addEventListener('click', e => togglePause()); +soundToggleBtn.addEventListener('click', e => toggleSound()); +hintBtn.addEventListener('click', (e) => { + if(paused) return; + // reveal one digit of secret randomly + const choices = []; + for(let i=0;i { + if(paused) return; + const suggestion = smartSuggest().map(String); + currentGuess = suggestion; + refreshGuessDisplay(); +}); + +document.addEventListener('keydown', (e) => { + if(paused) return; + if(e.key >= '0' && e.key <= '9'){ + addDigit(e.key); + e.preventDefault(); + } else if(e.key === 'Backspace'){ + backspace(); + } else if(e.key === 'Enter'){ + submitCurrentGuess(); + } else if(e.key === 'p'){ + togglePause(); + } else if(e.key === 'r'){ + restartGame(); + } +}); + +/* Init */ +(function init(){ + generateSecret(); + updateAttemptsDisplay(); + refreshGuessDisplay(); + setMessage('Guess the secret 3-digit code. Use keyboard or on-screen keypad. Good luck!'); +})(); diff --git a/games/code-breaker-mini/style.css b/games/code-breaker-mini/style.css new file mode 100644 index 00000000..5e32be9f --- /dev/null +++ b/games/code-breaker-mini/style.css @@ -0,0 +1,136 @@ +:root{ + --bg: linear-gradient(180deg,#081224 0%,#071022 100%); + --panel: rgba(255,255,255,0.04); + --glass: rgba(255,255,255,0.03); + --accent: #7afcff; + --good: #6ee7a6; /* green */ + --mid: #ffd166; /* yellow/orange */ + --bad: #a0a6b2; /* grey */ + --glass-border: rgba(255,255,255,0.06); + --glass-glow: 0 10px 30px rgba(122,252,255,0.08); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; +} + +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + min-height:100vh; + background:var(--bg); + color:#e8eef6; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + display:flex; + align-items:stretch; + justify-content:center; + padding:28px; +} + +/* Main app container */ +.app{ + width:100%; + max-width:1100px; + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border-radius:14px; + border:1px solid rgba(255,255,255,0.03); + box-shadow: 0 8px 40px rgba(0,0,0,0.6); + overflow:hidden; + display:flex; + flex-direction:column; +} + +/* Topbar */ +.topbar{ + display:flex; + justify-content:space-between; + align-items:center; + padding:18px 20px; + border-bottom:1px solid var(--glass-border); + background: linear-gradient(180deg, rgba(255,255,255,0.01), transparent); +} +.brand{display:flex;align-items:center;gap:12px} +.brand__icon{font-size:28px} +.brand h1{font-size:18px;margin:0} +.controls{display:flex;gap:8px;align-items:center} +.btn{background:var(--panel);border:1px solid var(--glass-border);color:inherit;padding:8px 12px;border-radius:8px;cursor:pointer;transition:all 180ms ease} +.btn:hover{transform:translateY(-2px)} +.btn.icon{padding:8px} +.btn.primary{background:linear-gradient(90deg,var(--accent),#7bb7ff);color:#041021;border:none} + +/* Main area */ +.main{display:flex;gap:18px;padding:22px} +.game-stage{flex:1;display:flex;flex-direction:column;gap:18px} +.right-panel{width:300px} + +/* Play area */ +.play-area{ + background:linear-gradient(180deg, rgba(255,255,255,0.01), rgba(255,255,255,0.02)); + border-radius:12px;padding:18px;border:1px solid rgba(255,255,255,0.02); + display:flex;gap:18px;flex-direction:column; + box-shadow: var(--glass-glow); +} + +/* stats */ +.game-info{display:flex;gap:12px} +.stat{background:var(--glass);padding:12px;border-radius:10px;border:1px solid var(--glass-border);min-width:120px;text-align:center} +.stat label{font-size:12px;color:#9fb0c7} +.stat .big{font-size:22px;font-weight:700;margin-top:6px} + +/* bulbs */ +.target-line{display:flex;flex-direction:column;align-items:center;gap:10px} +.bulb-row{display:flex;gap:14px} +.bulb{ + width:56px;height:56px;border-radius:12px;background:rgba(255,255,255,0.03); + border:1px solid rgba(255,255,255,0.04);display:flex;align-items:center;justify-content:center; + box-shadow: inset 0 -6px 20px rgba(0,0,0,0.6); + transition:all 240ms cubic-bezier(.2,.9,.3,1); + position:relative;overflow:hidden; +} +.bulb::after{content:'';position:absolute;inset:0;border-radius:inherit;mix-blend-mode:screen;opacity:0;transition:opacity .25s} +.bulb.good{background:linear-gradient(180deg, rgba(110,231,166,0.12), rgba(110,231,166,0.06));box-shadow:0 8px 30px rgba(110,231,166,0.06);border-color:rgba(110,231,166,0.2)} +.bulb.good::after{opacity:1;background:radial-gradient(circle at 30% 20%, rgba(110,231,166,0.48), transparent 30%)} +.bulb.mid{background:linear-gradient(180deg, rgba(255,209,102,0.12), rgba(255,209,102,0.04));border-color:rgba(255,209,102,0.18)} +.bulb.mid::after{opacity:1;background:radial-gradient(circle at 30% 20%, rgba(255,209,102,0.38), transparent 30%)} +.bulb.bad{opacity:0.8;background:linear-gradient(180deg, rgba(160,166,178,0.06), rgba(160,166,178,0.02));border-color:rgba(160,166,178,0.14)} + +/* guess display */ +.guess-display{font-size:28px;letter-spacing:10px;text-align:center;padding:12px 0;font-weight:700} + +/* numpad */ +.numpad{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;max-width:400px;margin:0 auto} +.num-key,.action-key{padding:14px;border-radius:10px;background:rgba(255,255,255,0.02);border:1px solid var(--glass-border);cursor:pointer;font-size:18px} +.action-key.primary{background:linear-gradient(90deg,var(--accent),#7bb7ff);color:#041021;border:none} + +/* quick actions */ +.quick-actions{display:flex;gap:8px;justify-content:center;margin-top:10px} +.history{margin-top:12px;display:flex;flex-direction:column;gap:8px;max-height:240px;overflow:auto} +.history .row{display:flex;align-items:center;gap:10px;padding:10px;border-radius:8px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.02)} +.row .guess{min-width:86px;font-weight:700} +.row .feedback{display:flex;gap:8px;align-items:center} + +/* hint colours inside history */ +.hint-dot{width:12px;height:12px;border-radius:4px;display:inline-block} +.hint-correctpos{color:var(--good)} +.hint-correctdigit{color:var(--mid)} +.hint-absent{color:var(--bad)} + +/* right panel cards */ +.card{background:var(--panel);border:1px solid var(--glass-border);padding:14px;border-radius:10px;margin-bottom:12px} +.card h3{margin-top:0;font-size:16px} + +/* message */ +.message{margin:0;padding:8px;background:rgba(0,0,0,0.12);border-radius:8px} + +/* footer */ +.footer{border-top:1px solid var(--glass-border);padding:12px 18px;background:linear-gradient(180deg,transparent,rgba(255,255,255,0.01))} +.footer .footer-inner{display:flex;justify-content:space-between;align-items:center;font-size:13px;color:#9fb0c7} + +/* responsiveness */ +@media (max-width:980px){ + .main{flex-direction:column} + .right-panel{width:100%} +} + +/* small helper animations */ +.glow{animation:glow 1000ms infinite alternate} +@keyframes glow{from{filter:drop-shadow(0 0 6px rgba(122,252,255,0.08))}to{filter:drop-shadow(0 0 18px rgba(122,252,255,0.18))}} diff --git a/games/code-unlock/index.html b/games/code-unlock/index.html new file mode 100644 index 00000000..54a2357a --- /dev/null +++ b/games/code-unlock/index.html @@ -0,0 +1,32 @@ + + + + + + Code Unlock | Mini JS Games Hub + + + +
+

๐Ÿ’ก Code Unlock

+

Click bulbs in correct order to unlock the code!

+ +
+ +
+ +
+ + + +
+ +

+
+ + + + + + + diff --git a/games/code-unlock/script.js b/games/code-unlock/script.js new file mode 100644 index 00000000..d18f23dc --- /dev/null +++ b/games/code-unlock/script.js @@ -0,0 +1,106 @@ +const bulbContainer = document.getElementById('bulb-sequence'); +const startBtn = document.getElementById('start-btn'); +const pauseBtn = document.getElementById('pause-btn'); +const restartBtn = document.getElementById('restart-btn'); +const statusEl = document.getElementById('status'); + +const successSound = document.getElementById('success-sound'); +const failSound = document.getElementById('fail-sound'); + +let sequence = []; +let userSequence = []; +let bulbs = []; +let gameStarted = false; +let paused = false; + +// Number of bulbs +const BULB_COUNT = 6; + +// Initialize bulbs +function initBulbs() { + bulbContainer.innerHTML = ''; + bulbs = []; + for (let i = 0; i < BULB_COUNT; i++) { + const bulb = document.createElement('div'); + bulb.classList.add('bulb'); + bulb.addEventListener('click', () => handleClick(i)); + bulbContainer.appendChild(bulb); + bulbs.push(bulb); + } +} + +// Generate random sequence +function generateSequence() { + sequence = []; + for (let i = 0; i < BULB_COUNT; i++) { + sequence.push(Math.floor(Math.random() * BULB_COUNT)); + } + console.log('Sequence:', sequence); +} + +// Start game +function startGame() { + if (gameStarted) return; + gameStarted = true; + paused = false; + userSequence = []; + statusEl.textContent = 'Game started! Click the bulbs in order.'; + generateSequence(); + animateSequence(); +} + +// Animate sequence for player +function animateSequence() { + bulbs.forEach(b => b.classList.remove('glow')); + sequence.forEach((index, i) => { + setTimeout(() => { + if (!paused) { + bulbs[index].classList.add('glow'); + setTimeout(() => bulbs[index].classList.remove('glow'), 500); + } + }, i * 700); + }); +} + +// Handle click +function handleClick(index) { + if (!gameStarted || paused) return; + + bulbs[index].classList.add('glow'); + setTimeout(() => bulbs[index].classList.remove('glow'), 300); + + userSequence.push(index); + + if (userSequence[userSequence.length - 1] !== sequence[userSequence.length - 1]) { + failSound.play(); + statusEl.textContent = 'Wrong bulb! Try again.'; + userSequence = []; + } else { + successSound.play(); + if (userSequence.length === sequence.length) { + statusEl.textContent = '๐ŸŽ‰ You unlocked the code!'; + gameStarted = false; + } + } +} + +// Pause game +pauseBtn.addEventListener('click', () => { + paused = !paused; + statusEl.textContent = paused ? 'Game paused' : 'Game resumed'; +}); + +// Restart game +restartBtn.addEventListener('click', () => { + gameStarted = false; + paused = false; + userSequence = []; + statusEl.textContent = 'Game restarted! Click Start.'; + bulbs.forEach(b => b.classList.remove('glow')); +}); + +// Start button +startBtn.addEventListener('click', startGame); + +// Initialize on load +initBulbs(); diff --git a/games/code-unlock/style.css b/games/code-unlock/style.css new file mode 100644 index 00000000..01fb68b0 --- /dev/null +++ b/games/code-unlock/style.css @@ -0,0 +1,63 @@ +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(to right, #0f2027, #203a43, #2c5364); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + color: #fff; +} + +.game-container { + text-align: center; + background: rgba(0,0,0,0.6); + padding: 30px; + border-radius: 15px; + box-shadow: 0 0 20px rgba(255,255,255,0.2); +} + +.bulb-sequence { + display: flex; + justify-content: center; + margin: 20px 0; +} + +.bulb { + width: 50px; + height: 50px; + margin: 0 10px; + border-radius: 50%; + background: #444; + box-shadow: 0 0 10px #222; + cursor: pointer; + transition: 0.3s all; +} + +.bulb.glow { + background: yellow; + box-shadow: 0 0 20px yellow, 0 0 40px yellow, 0 0 60px yellow; +} + +.controls button { + padding: 10px 20px; + margin: 10px; + font-size: 16px; + cursor: pointer; + border-radius: 5px; + border: none; + background: #ffcc00; + color: #000; + font-weight: bold; + transition: 0.2s all; +} + +.controls button:hover { + background: #ffd633; +} + +#status { + margin-top: 20px; + font-size: 18px; + font-weight: bold; +} diff --git a/games/code_flow/index.html b/games/code_flow/index.html new file mode 100644 index 00000000..cd884f78 --- /dev/null +++ b/games/code_flow/index.html @@ -0,0 +1,63 @@ + + + + + + Code Flow: Breakpoint Beat + + + + +
+

Code Flow: Breakpoint Beat

+

Hit the **SPACEBAR** at the precise beat to pause (breakpoint) and resume the code execution.

+
+ +
+
+

Execution Flow: `animateElement()`

+
+function animateElement() {
+    // Line 1: Initialize timer
+    let time = 0;
+    
+    // Line 2: Start animation loop
+    function loop(timestamp) {
+        // Line 3: Check for pause/breakpoint <--- TARGET BREAKPOINT
+        // if (window.isPaused) { /* PAUSE HERE */ } 
+        
+        // Line 4: Calculate movement
+        time += 0.01;
+        const x = Math.sin(time) * 100;
+        
+        // Line 5: Apply visual transform
+        window.animatedBox.style.transform = `translateX(${x}px)`;
+        
+        // Line 6: Check for rhythm beat cue
+        // if (rhythm.isBeat) { /* TRIGGER VISUAL CUE */ }
+        
+        // Line 7: Continue loop
+        requestAnimationFrame(loop);
+    }
+    // Line 8: Call first frame
+    requestAnimationFrame(loop);
+}
+            
+
+
+ +
+
+
+
+
+ +
+

Timing Accuracy: --ms

+

Flow Score: 0

+

Waiting for the beat...

+
+ + + + \ No newline at end of file diff --git a/games/code_flow/script.js b/games/code_flow/script.js new file mode 100644 index 00000000..dac5f16f --- /dev/null +++ b/games/code_flow/script.js @@ -0,0 +1,148 @@ +document.addEventListener('DOMContentLoaded', () => { + const animatedBox = document.getElementById('animated-box'); + const lineIndicator = document.getElementById('line-indicator'); + const rhythmCue = document.getElementById('rhythm-cue'); + const feedbackMessage = document.getElementById('feedback-message'); + const accuracyDisplay = document.getElementById('accuracy-display'); + const scoreDisplay = document.getElementById('score-display'); + + // Expose element for the visual code in the HTML + window.animatedBox = animatedBox; + + // --- 1. Game State and Rhythm Engine --- + + let isPaused = false; // State flag for the while loop trick + let rhythmScore = 0; + const BPM = 120; // Beats Per Minute + const BEAT_INTERVAL_MS = 60000 / BPM; // 500ms for 120 BPM + const BEAT_TOLERANCE_MS = 80; // Allowable deviation for a successful hit + + let lastBeatTime = performance.now(); + let nextBreakpointTime = lastBeatTime + BEAT_INTERVAL_MS; + + function startRhythmEngine() { + // Use a simple setInterval as a master clock + setInterval(() => { + const currentTime = performance.now(); + + // Check if it's time for a beat cue + if (currentTime >= nextBreakpointTime - 100) { // Cue starts 100ms before beat + rhythmCue.style.opacity = 0.5; + } + + if (currentTime >= nextBreakpointTime) { + // Flash the cue on the beat + rhythmCue.style.opacity = 1.0; + setTimeout(() => rhythmCue.style.opacity = 0, 50); // Flash off quickly + + // Set the next target time + lastBeatTime = nextBreakpointTime; + nextBreakpointTime += BEAT_INTERVAL_MS; + } + }, 10); // Check frequently + } + + // --- 2. The Breakpoint Trick --- + + function simulateBreakpoint() { + // Line 3 is the breakpoint location + lineIndicator.style.top = '5em'; + + // This is the core trick: Entering a busy-wait loop + while (isPaused) { + // Wait for the player's next input to set isPaused = false + // This loop effectively freezes JavaScript execution (including RAF) + // It will also temporarily block the main thread. + } + + // Execution resumes here: Move past the breakpoint + lineIndicator.style.top = '6.5em'; + } + + // --- 3. Animation and Game Flow --- + + let animationTime = 0; + + function animateElement(timestamp) { + // Line 1: Timer logic (simplified) + animationTime += 0.01; + + // Line 2: The loop starts here + lineIndicator.style.top = '3.5em'; + + // Line 3: Check for pause/breakpoint + if (isPaused) { + simulateBreakpoint(); // Enters the busy-wait loop + } + + // Execution resumes/continues here + + // Line 4: Calculate movement + const x = Math.sin(animationTime) * 100; + + // Line 5: Apply visual transform + animatedBox.style.transform = `translateX(${x}px)`; + + // Line 6 & 7: Loop continuation + requestAnimationFrame(animateElement); + } + + // --- 4. Player Input & Timing Check --- + + document.addEventListener('keydown', (event) => { + if (event.code === 'Space') { + const pressTime = performance.now(); + + if (!isPaused) { + // Player is setting the BREAKPOINT (PAUSE) + checkHitTiming(pressTime); + isPaused = true; + + // Visual feedback that the flow is stopped + document.body.classList.add('flow-error'); + feedbackMessage.textContent = "Code Paused (Breakpoint Set)... Press SPACE to resume!"; + + } else { + // Player is clearing the BREAKPOINT (RESUME) + isPaused = false; + document.body.classList.remove('flow-error'); + feedbackMessage.textContent = "Code Resumed. Execution continues."; + } + } + }); + + function checkHitTiming(pressTime) { + const timeDifference = pressTime - lastBeatTime; + // Calculate difference relative to the target beat time + let accuracy = timeDifference % BEAT_INTERVAL_MS; + + // Normalize accuracy to be between -BEAT_INTERVAL_MS/2 and +BEAT_INTERVAL_MS/2 + if (accuracy > BEAT_INTERVAL_MS / 2) { + accuracy -= BEAT_INTERVAL_MS; + } + + const absAccuracy = Math.abs(accuracy); + + if (absAccuracy <= BEAT_TOLERANCE_MS) { + // Success! + rhythmScore++; + scoreDisplay.textContent = rhythmScore; + accuracyDisplay.textContent = `${absAccuracy.toFixed(2)}ms OFF`; + + // Set the next target time based on the successful hit to maintain the rhythm lock + nextBreakpointTime = pressTime + BEAT_INTERVAL_MS - (pressTime % BEAT_INTERVAL_MS); + + } else { + // Failure! + accuracyDisplay.textContent = `FAILED! ${absAccuracy.toFixed(2)}ms OFF`; + + // If the player fails, throw the timing off for the next beat + nextBreakpointTime = pressTime + BEAT_INTERVAL_MS; // Just move to the next interval + } + } + + + // --- Initialization --- + startRhythmEngine(); + animateElement(performance.now()); // Start the animation loop +}); \ No newline at end of file diff --git a/games/code_flow/style.css b/games/code_flow/style.css new file mode 100644 index 00000000..b0e0d517 --- /dev/null +++ b/games/code_flow/style.css @@ -0,0 +1,93 @@ +:root { + --code-panel-width: 45%; + --animation-stage-width: 45%; + --rhythm-color: #ff5722; +} + +body { + font-family: monospace, Courier New, monospace; + background-color: #1a1a2e; + color: #00ff00; /* Neon green terminal text */ + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; +} + +#game-container { + display: flex; + justify-content: space-around; + width: 900px; + margin-bottom: 20px; + background-color: #0d0d1b; + border: 1px solid #333; + padding: 10px; +} + +/* --- Code Panel Styling --- */ +#code-panel { + width: var(--code-panel-width); + position: relative; + padding: 10px; + border-right: 1px dashed #333; +} + +#code-display { + background-color: #000; + padding: 10px; + border-radius: 5px; + line-height: 1.5; + position: relative; +} + +/* Execution Indicator */ +#line-indicator { + position: absolute; + left: 0; + width: 100%; + height: 1.5em; /* Matches line height */ + background-color: rgba(255, 255, 0, 0.3); /* Highlight color */ + z-index: 10; + transition: top 0.1s linear; /* Smooth line transitions */ + top: 5em; /* Initial position for 'Line 3' */ +} + +/* --- Animation Stage Styling --- */ +#animation-stage { + width: var(--animation-stage-width); + height: 300px; + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +#animated-box { + width: 50px; + height: 50px; + background-color: #00bcd4; + position: absolute; + border-radius: 50%; + will-change: transform; /* Optimization hint */ +} + +#rhythm-cue { + position: absolute; + width: 20px; + height: 20px; + background-color: var(--rhythm-color); + border-radius: 50%; + opacity: 0; + transition: opacity 0.1s; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +/* --- Visual Error Feedback --- */ +.flow-error #animated-box { + /* Example of a visual distortion on error */ + filter: hue-rotate(180deg) blur(2px); + transform: scale(1.5) !important; /* Force a jerky visual error */ + transition: all 0.1s; +} \ No newline at end of file diff --git a/games/coin_toss_simulator/index.html b/games/coin_toss_simulator/index.html new file mode 100644 index 00000000..4c72afa7 --- /dev/null +++ b/games/coin_toss_simulator/index.html @@ -0,0 +1,108 @@ + + + + + + Coin Toss Simulator ๐Ÿช™ + + + + +
+
+
+
+
+ +
+
+

+ ๐Ÿช™ + Coin Toss Simulator + ๐Ÿช™ +

+

Make your choice and test your luck!

+
+ +
+
+
Heads
+
0
+
+
+
Total Flips
+
0
+
+
+
Tails
+
0
+
+
+ +
+
+

Make Your Prediction

+
+ + +
+
+ +
+
+
+
+
+
๐Ÿ‘‘
+
HEADS
+
+
+
+
+
+
+
๐Ÿฆ…
+
TAILS
+
+
+
+
+
+
+ +
+
+
+
+ + +
+ +
+ + +
+
+ ๐Ÿ”ฅ + Win Streak: 0 +
+
+ โœ… + Total Correct: 0 +
+
+ + + + + + + diff --git a/games/coin_toss_simulator/script.js b/games/coin_toss_simulator/script.js new file mode 100644 index 00000000..bb050e1a --- /dev/null +++ b/games/coin_toss_simulator/script.js @@ -0,0 +1,391 @@ +// Game State +let headsCount = 0; +let tailsCount = 0; +let totalFlips = 0; +let winStreak = 0; +let correctGuesses = 0; +let isFlipping = false; + +// DOM Elements +const headsBtn = document.getElementById('headsBtn'); +const tailsBtn = document.getElementById('tailsBtn'); +const coin = document.getElementById('coin'); +const resultMessage = document.getElementById('resultMessage'); +const resultDetail = document.getElementById('resultDetail'); +const headsCountEl = document.getElementById('headsCount'); +const tailsCountEl = document.getElementById('tailsCount'); +const totalFlipsEl = document.getElementById('totalFlips'); +const winStreakEl = document.getElementById('winStreak'); +const correctGuessesEl = document.getElementById('correctGuesses'); +const resetBtn = document.getElementById('resetBtn'); +const confettiContainer = document.getElementById('confettiContainer'); + +// Initialize +document.addEventListener('DOMContentLoaded', () => { + loadStats(); + updateDisplay(); +}); + +// Event Listeners +headsBtn.addEventListener('click', () => flipCoin('heads')); +tailsBtn.addEventListener('click', () => flipCoin('tails')); +resetBtn.addEventListener('click', resetStats); + +// Main Flip Function +function flipCoin(userChoice) { + if (isFlipping) return; + + isFlipping = true; + disableButtons(); + + // Add selection animation + if (userChoice === 'heads') { + headsBtn.classList.add('selected'); + } else { + tailsBtn.classList.add('selected'); + } + + // Clear previous result + resultMessage.textContent = ''; + resultDetail.textContent = ''; + + // Remove previous animation classes + coin.classList.remove('flip-heads', 'flip-tails'); + + // Simulate coin flip + const result = Math.random() < 0.5 ? 'heads' : 'tails'; + + // Add flip animation + setTimeout(() => { + if (result === 'heads') { + coin.classList.add('flip-heads'); + } else { + coin.classList.add('flip-tails'); + } + }, 100); + + // Show result after animation + setTimeout(() => { + showResult(userChoice, result); + updateStats(result); + enableButtons(); + isFlipping = false; + + // Remove selection + headsBtn.classList.remove('selected'); + tailsBtn.classList.remove('selected'); + }, 2100); +} + +// Show Result +function showResult(userChoice, result) { + const win = userChoice === result; + if (win) { + resultMessage.textContent = '๐ŸŽ‰ You Win! ๐ŸŽ‰'; + resultMessage.className = 'result-message win'; + resultDetail.textContent = `The coin landed on ${result.toUpperCase()}!`; + winStreak++; + correctGuesses++; + launchConfetti(); + playWinSound(); + } else { + resultMessage.textContent = '๐Ÿ˜” You Lose!'; + resultMessage.className = 'result-message lose'; + resultDetail.textContent = `The coin landed on ${result.toUpperCase()}. Better luck next time!`; + winStreak = 0; + playLoseSound(); + } + updateDisplay(); +} + +// Update Stats +function updateStats(result) { + totalFlips++; + + if (result === 'heads') { + headsCount++; + } else { + tailsCount++; + } + + saveStats(); + updateDisplay(); +} + +// Update Display +function updateDisplay() { + headsCountEl.textContent = headsCount; + tailsCountEl.textContent = tailsCount; + totalFlipsEl.textContent = totalFlips; + winStreakEl.textContent = winStreak; + correctGuessesEl.textContent = correctGuesses; + + // Animate stat updates + animateValue(headsCountEl); + animateValue(tailsCountEl); + animateValue(totalFlipsEl); + animateValue(winStreakEl); + animateValue(correctGuessesEl); +} + +// Animate Value Update +function animateValue(element) { + element.style.transform = 'scale(1.3)'; + element.style.transition = 'transform 0.3s ease'; + + setTimeout(() => { + element.style.transform = 'scale(1)'; + }, 300); +} + +// Reset Stats +function resetStats() { + if (confirm('Are you sure you want to reset all statistics?')) { + headsCount = 0; + tailsCount = 0; + totalFlips = 0; + winStreak = 0; + correctGuesses = 0; + + resultMessage.textContent = ''; + resultDetail.textContent = ''; + + saveStats(); + updateDisplay(); + + // Reset coin position + coin.classList.remove('flip-heads', 'flip-tails'); + + showResetAnimation(); + } +} + +// Show Reset Animation +function showResetAnimation() { + const container = document.querySelector('.container'); + container.style.animation = 'none'; + + setTimeout(() => { + container.style.animation = 'slideIn 0.8s ease-out'; + }, 10); +} + +// Canvas Confetti Animation +function launchConfetti() { + const canvas = document.getElementById('confettiCanvas'); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + const colors = ['#ffd700', '#ff6b6b', '#51cf66', '#4facfe', '#f093fb']; + const W = window.innerWidth; + const H = window.innerHeight; + canvas.width = W; + canvas.height = H; + let confetti = []; + for (let i = 0; i < 60; i++) { + confetti.push({ + x: Math.random() * W, + y: Math.random() * -H, + r: Math.random() * 8 + 4, + d: Math.random() * 80 + 40, + color: colors[Math.floor(Math.random() * colors.length)], + tilt: Math.random() * 10 - 10, + tiltAngle: 0, + tiltAngleIncremental: (Math.random() * 0.07) + 0.05 + }); + } + let angle = 0; + let frame = 0; + function drawConfetti() { + ctx.clearRect(0, 0, W, H); + angle += 0.01; + for (let i = 0; i < confetti.length; i++) { + let c = confetti[i]; + c.tiltAngle += c.tiltAngleIncremental; + c.y += (Math.cos(angle + c.d) + 3 + c.r / 2) / 2; + c.x += Math.sin(angle); + c.tilt = Math.sin(c.tiltAngle - (i % 3)) * 15; + ctx.beginPath(); + ctx.lineWidth = c.r; + ctx.strokeStyle = c.color; + ctx.moveTo(c.x + c.tilt + c.r / 3, c.y); + ctx.lineTo(c.x + c.tilt, c.y + c.tilt + 10); + ctx.stroke(); + } + frame++; + if (frame < 80) { + requestAnimationFrame(drawConfetti); + } else { + ctx.clearRect(0, 0, W, H); + } + } + drawConfetti(); +} + +// Sound Effects (using Web Audio API) +let audioContext; + +function initAudio() { + if (!audioContext) { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } +} + +function playWinSound() { + initAudio(); + + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.frequency.setValueAtTime(523.25, audioContext.currentTime); // C5 + oscillator.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.1); // E5 + oscillator.frequency.setValueAtTime(783.99, audioContext.currentTime + 0.2); // G5 + + gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); + + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + 0.3); +} + +function playLoseSound() { + initAudio(); + + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.frequency.setValueAtTime(392, audioContext.currentTime); // G4 + oscillator.frequency.setValueAtTime(349.23, audioContext.currentTime + 0.1); // F4 + oscillator.frequency.setValueAtTime(293.66, audioContext.currentTime + 0.2); // D4 + + gainNode.gain.setValueAtTime(0.2, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); + + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + 0.3); +} + +// Button State Management +function disableButtons() { + headsBtn.disabled = true; + tailsBtn.disabled = true; +} + +function enableButtons() { + headsBtn.disabled = false; + tailsBtn.disabled = false; +} + +// Local Storage +function saveStats() { + const stats = { + headsCount, + tailsCount, + totalFlips, + winStreak, + correctGuesses + }; + localStorage.setItem('coinTossStats', JSON.stringify(stats)); +} + +function loadStats() { + const saved = localStorage.getItem('coinTossStats'); + + if (saved) { + const stats = JSON.parse(saved); + headsCount = stats.headsCount || 0; + tailsCount = stats.tailsCount || 0; + totalFlips = stats.totalFlips || 0; + winStreak = stats.winStreak || 0; + correctGuesses = stats.correctGuesses || 0; + } +} + +tailsBtn.addEventListener('mouseenter', () => playHoverSound()); +// Keyboard Shortcuts, Accessibility, and Visual Feedback +document.addEventListener('keydown', (e) => { + if (isFlipping) return; + if (e.key === 'h' || e.key === 'H') { + flipCoin('heads'); + headsBtn.style.transform = 'scale(0.95)'; + setTimeout(() => { headsBtn.style.transform = ''; }, 100); + } else if (e.key === 't' || e.key === 'T') { + flipCoin('tails'); + tailsBtn.style.transform = 'scale(0.95)'; + setTimeout(() => { tailsBtn.style.transform = ''; }, 100); + } else if (e.key === 'r' || e.key === 'R') { + resetStats(); + } else if ((e.key === 'Enter' || e.key === ' ') && document.activeElement) { + if (document.activeElement === headsBtn) { + flipCoin('heads'); + } else if (document.activeElement === tailsBtn) { + flipCoin('tails'); + } + } +}); + +headsBtn.addEventListener('mouseenter', () => playHoverSound()); +tailsBtn.addEventListener('mouseenter', () => playHoverSound()); + +function playHoverSound() { + initAudio(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + oscillator.frequency.setValueAtTime(800, audioContext.currentTime); + gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1); + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + 0.1); +} + +// Easter Egg: Triple Click on Title for Random Flips +let clickCount = 0; +let clickTimer; + +document.querySelector('.title').addEventListener('click', () => { + clickCount++; + + clearTimeout(clickTimer); + + if (clickCount === 3) { + autoFlip(); + clickCount = 0; + } + + clickTimer = setTimeout(() => { + clickCount = 0; + }, 500); +}); + +function autoFlip() { + if (isFlipping) return; + + const randomChoice = Math.random() < 0.5 ? 'heads' : 'tails'; + flipCoin(randomChoice); +} + +// Add visual feedback for keyboard shortcuts +document.addEventListener('keydown', (e) => { + if (e.key === 'h' || e.key === 'H') { + headsBtn.style.transform = 'scale(0.95)'; + setTimeout(() => { + headsBtn.style.transform = ''; + }, 100); + } else if (e.key === 't' || e.key === 'T') { + tailsBtn.style.transform = 'scale(0.95)'; + setTimeout(() => { + tailsBtn.style.transform = ''; + }, 100); + } +}); + +console.log('๐Ÿช™ Coin Toss Simulator loaded!'); +console.log('๐Ÿ’ก Keyboard shortcuts: H = Heads, T = Tails, R = Reset'); +console.log('๐ŸŽจ Triple-click the title for a random flip!'); diff --git a/games/coin_toss_simulator/style.css b/games/coin_toss_simulator/style.css new file mode 100644 index 00000000..5444940e --- /dev/null +++ b/games/coin_toss_simulator/style.css @@ -0,0 +1,708 @@ +.correct-badge { + background: linear-gradient(135deg, #51cf66 0%, #4facfe 100%); + padding: 15px 50px; + min-width: 220px; + border-radius: 50px; + display: flex; + align-items: center; + gap: 10px; + box-shadow: 0 5px 20px rgba(81, 207, 102, 0.2); + color: white; + font-weight: bold; + font-size: 1.1rem; + justify-content: center; + white-space: nowrap; + margin-top: 15px; +} + +.correct-icon { + font-size: 1.5rem; + animation: flicker 1.5s ease-in-out infinite; +} +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #ffd700; + --secondary-color: #ff6b6b; + --success-color: #51cf66; + --danger-color: #ff6b6b; + --dark-bg: #1a1a2e; + --card-bg: #16213e; + --text-light: #eee; + --text-dark: #333; + --shadow: rgba(0, 0, 0, 0.3); + --glow: rgba(255, 215, 0, 0.5); +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + overflow-x: hidden; + position: relative; +} + +/* Background Animation */ +.background-animation { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 0; +} + +.floating-coin { + position: absolute; + width: 60px; + height: 60px; + background: radial-gradient(circle at 30% 30%, var(--primary-color), #b8860b); + border-radius: 50%; + opacity: 0.1; + animation: float 15s infinite ease-in-out; +} + +.floating-coin:nth-child(1) { + top: 10%; + left: 10%; + animation-delay: 0s; +} + +.floating-coin:nth-child(2) { + top: 60%; + right: 15%; + animation-delay: 5s; + animation-duration: 20s; +} + +.floating-coin:nth-child(3) { + bottom: 15%; + left: 20%; + animation-delay: 10s; + animation-duration: 18s; +} + +@keyframes float { + 0%, 100% { + transform: translateY(0) rotate(0deg); + } + 25% { + transform: translateY(-30px) rotate(90deg); + } + 50% { + transform: translateY(-60px) rotate(180deg); + } + 75% { + transform: translateY(-30px) rotate(270deg); + } +} + +/* Container */ +.container { + max-width: 800px; + width: 100%; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: 30px; + padding: 40px; + box-shadow: 0 20px 60px var(--shadow); + position: relative; + z-index: 1; + animation: slideIn 0.8s ease-out; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Header */ +header { + text-align: center; + margin-bottom: 30px; +} + +.title { + font-size: 3rem; + color: var(--dark-bg); + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: center; + gap: 15px; + animation: titlePulse 2s ease-in-out infinite; +} + +@keyframes titlePulse { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } +} + +.coin-icon { + font-size: 2.5rem; + animation: rotate 3s linear infinite; +} + +@keyframes rotate { + from { + transform: rotateY(0deg); + } + to { + transform: rotateY(360deg); + } +} + +.subtitle { + font-size: 1.2rem; + color: #666; + font-style: italic; +} + +/* Stats Container */ +.stats-container { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 15px; + margin-bottom: 30px; +} + +.stat-card { + background: linear-gradient(135deg, var(--card-bg), #0f3460); + padding: 20px; + border-radius: 15px; + text-align: center; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.stat-card:hover { + transform: translateY(-5px); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); +} + +.stat-label { + color: var(--text-light); + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 10px; +} + +.stat-value { + color: var(--primary-color); + font-size: 2rem; + font-weight: bold; + text-shadow: 0 0 10px var(--glow); +} + +/* Game Section */ +.game-section { + text-align: center; +} + +.section-title { + color: var(--dark-bg); + font-size: 1.5rem; + margin-bottom: 20px; +} + +/* Button Group */ +.button-group { + display: flex; + gap: 20px; + justify-content: center; + margin-bottom: 40px; +} + +.choice-btn { + flex: 1; + max-width: 200px; + padding: 20px 30px; + font-size: 1.2rem; + font-weight: bold; + border: none; + border-radius: 15px; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); +} + +.choice-btn::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.3); + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; +} + +.choice-btn:hover::before { + width: 300px; + height: 300px; +} + +.heads-btn { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + color: white; +} + +.tails-btn { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + color: white; +} + +.choice-btn:hover { + transform: translateY(-3px) scale(1.05); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); +} + +.choice-btn:active { + transform: translateY(-1px) scale(1.02); +} + +.choice-btn.selected { + animation: pulse 0.5s ease; + box-shadow: 0 0 30px rgba(255, 215, 0, 0.8); +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.1); + } +} + +.btn-icon { + display: block; + font-size: 2rem; + margin-bottom: 5px; +} + +.btn-text { + display: block; + position: relative; + z-index: 1; +} + +.choice-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Coin Container */ +.coin-container { + height: 300px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-bottom: 30px; + perspective: 1000px; + position: relative; +} + +.coin { + width: 200px; + height: 200px; + position: relative; + transform-style: preserve-3d; + transition: transform 0.6s; +} + +.coin-side { + position: absolute; + width: 100%; + height: 100%; + backface-visibility: hidden; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.coin-heads { + background: radial-gradient(circle at 30% 30%, #ffd700, #b8860b); + box-shadow: 0 10px 30px rgba(255, 215, 0, 0.5), + inset 0 0 20px rgba(255, 255, 255, 0.3); +} + +.coin-tails { + background: radial-gradient(circle at 30% 30%, #c0c0c0, #808080); + box-shadow: 0 10px 30px rgba(192, 192, 192, 0.5), + inset 0 0 20px rgba(255, 255, 255, 0.3); + transform: rotateY(180deg); +} + +.coin-inner { + width: 85%; + height: 85%; + border-radius: 50%; + border: 3px solid rgba(255, 255, 255, 0.5); + display: flex; + align-items: center; + justify-content: center; +} + +.coin-design { + text-align: center; +} + +.crown, .eagle { + font-size: 4rem; + margin-bottom: 10px; +} + +.coin-text { + font-size: 1.2rem; + font-weight: bold; + color: rgba(0, 0, 0, 0.8); + text-shadow: 0 2px 4px rgba(255, 255, 255, 0.5); + letter-spacing: 2px; +} + +.coin-shadow { + width: 200px; + height: 30px; + background: radial-gradient(ellipse, rgba(0, 0, 0, 0.3), transparent); + border-radius: 50%; + position: absolute; + bottom: 20px; + filter: blur(10px); + animation: shadowPulse 2s ease-in-out infinite; +} + +@keyframes shadowPulse { + 0%, 100% { + transform: scale(1); + opacity: 0.3; + } + 50% { + transform: scale(1.1); + opacity: 0.5; + } +} + +/* Coin Flip Animations */ +.coin.flip-heads { + animation: flipHeads 2s ease-in-out forwards; +} + +.coin.flip-tails { + animation: flipTails 2s ease-in-out forwards; +} + +@keyframes flipHeads { + 0% { + transform: rotateY(0) rotateX(0) translateY(0); + } + 25% { + transform: rotateY(180deg) rotateX(180deg) translateY(-100px); + } + 50% { + transform: rotateY(360deg) rotateX(360deg) translateY(0); + } + 75% { + transform: rotateY(540deg) rotateX(540deg) translateY(-50px); + } + 100% { + transform: rotateY(720deg) rotateX(720deg) translateY(0); + } +} + +@keyframes flipTails { + 0% { + transform: rotateY(0) rotateX(0) translateY(0); + } + 25% { + transform: rotateY(180deg) rotateX(180deg) translateY(-100px); + } + 50% { + transform: rotateY(360deg) rotateX(360deg) translateY(0); + } + 75% { + transform: rotateY(540deg) rotateX(540deg) translateY(-50px); + } + 100% { + transform: rotateY(900deg) rotateX(900deg) translateY(0); + } +} + +/* Result Container */ +.result-container { + min-height: 80px; + margin-bottom: 20px; +} + +.result-message { + font-size: 2rem; + font-weight: bold; + margin-bottom: 10px; + animation: fadeInScale 0.5s ease; +} + +.result-message.win { + color: var(--success-color); + text-shadow: 0 0 20px rgba(81, 207, 102, 0.5); +} + +.result-message.lose { + color: var(--danger-color); +} + +.result-detail { + font-size: 1.2rem; + color: #666; + animation: fadeIn 0.5s ease 0.2s backwards; +} + +@keyframes fadeInScale { + from { + opacity: 0; + transform: scale(0.5); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* Reset Button */ +.reset-btn { + padding: 15px 40px; + font-size: 1rem; + font-weight: bold; + background: linear-gradient(135deg, #ff6b6b, #ff8e53); + color: white; + border: none; + border-radius: 50px; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 5px 15px rgba(255, 107, 107, 0.3); +} + +.reset-btn:hover { + transform: translateY(-3px); + box-shadow: 0 8px 20px rgba(255, 107, 107, 0.4); +} + +.reset-btn:active { + transform: translateY(-1px); +} + +/* Streak Container */ + +/* Win Streak Badge - outside main box, right of container */ + +.streak-container { + position: fixed; + top: 60px; + left: calc(50% + 420px); + z-index: 1000; + animation: slideInRight 0.5s ease; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +@media (max-width: 1200px) { + .streak-container { + left: auto; + right: 20px; + top: 20px; + align-items: flex-end; + } +} + +@media (max-width: 900px) { + .streak-container { + position: static; + margin-left: 0; + top: auto; + left: auto; + right: auto; + display: flex; + justify-content: center; + align-items: center; + margin-top: 20px; + } +} + +@media (max-width: 900px) { + .streak-container { + position: static; + margin-left: 0; + top: auto; + left: auto; + display: flex; + justify-content: center; + margin-top: 20px; + } +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(100px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.streak-badge { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + padding: 15px 50px; + min-width: 220px; + border-radius: 50px; + display: flex; + align-items: center; + gap: 10px; + box-shadow: 0 5px 20px rgba(240, 147, 251, 0.4); + color: white; + font-weight: bold; + font-size: 1.1rem; + justify-content: center; + white-space: nowrap; +} + +.streak-icon { + font-size: 1.5rem; + animation: flicker 1.5s ease-in-out infinite; +} + +@keyframes flicker { + 0%, 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.8; + transform: scale(1.2); + } +} + +/* Confetti */ +.confetti-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 999; +} + +.confetti { + position: absolute; + width: 10px; + height: 10px; + background: var(--primary-color); + animation: confettiFall 3s linear forwards; +} + +@keyframes confettiFall { + to { + transform: translateY(100vh) rotate(360deg); + opacity: 0; + } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .container { + padding: 25px; + } + + .title { + font-size: 2rem; + } + + .stats-container { + grid-template-columns: 1fr; + gap: 10px; + } + + .button-group { + flex-direction: column; + gap: 15px; + } + + .choice-btn { + max-width: 100%; + } + + .coin { + width: 150px; + height: 150px; + } + + .coin-shadow { + width: 150px; + } + + .streak-container { + top: 10px; + right: 10px; + } + .streak-badge { + padding: 10px 15px; + font-size: 0.9rem; + } + /* Reduce heavy animations for mobile */ + .coin, .streak-badge, .correct-badge, .stat-value { + animation: none !important; + transition: none !important; + } +} + +@media (max-width: 480px) { + .title { + font-size: 1.5rem; + } + .coin-icon { + font-size: 1.5rem; + } + .subtitle { + font-size: 1rem; + } + .result-message { + font-size: 1.5rem; + } +} diff --git a/games/color catch/index.html b/games/color catch/index.html new file mode 100644 index 00000000..c366ce7e --- /dev/null +++ b/games/color catch/index.html @@ -0,0 +1,38 @@ + + + + + + Color Catch โ€” Reflex Speed Game + + + +
+

๐ŸŽฏ Color Catch

+

Tap only the โ€” circles!

+ +
+ +
+ Score: 0 + Time: 30s +
+
+ +
+ + + +

Tip: Focus on the color, not the shape โ€” circles disappear quickly!

+
+ + + + diff --git a/games/color catch/script.js b/games/color catch/script.js new file mode 100644 index 00000000..b0059262 --- /dev/null +++ b/games/color catch/script.js @@ -0,0 +1,247 @@ +// Elements +const startBtn = document.getElementById('startBtn'); +const restartBtn = document.getElementById('restartBtn'); +const gameArea = document.getElementById('gameArea'); +const scoreDisplay = document.getElementById('score'); +const timeDisplay = document.getElementById('time'); +const resultBox = document.getElementById('result'); +const finalScore = document.getElementById('finalScore'); +const targetLabel = document.getElementById('targetLabel'); +const resultTarget = document.getElementById('resultTarget'); + +let score = 0; +let timeLeft = 30; +let spawnIntervalId = null; +let timerIntervalId = null; +let spawnTimeoutIds = []; // track individual spawn timeouts (lifespans) +let gameActive = false; +let targetColor = null; + +const COLORS = ['red', 'green', 'blue', 'yellow', 'purple', 'orange']; + +// Utility +function randInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +function chooseTargetColor() { + targetColor = COLORS[Math.floor(Math.random() * COLORS.length)]; + targetLabel.innerHTML = `${targetColor.toUpperCase()} `; + resultTarget.style.background = targetColor; + console.log('[Game] Target color chosen:', targetColor); +} + +function safeRemoveCircle(circle, clickHandler, timeoutId) { + try { + if (clickHandler) circle.removeEventListener('click', clickHandler); + } catch (e) { /* ignore */ } + // remove DOM node if still present + if (circle && circle.parentElement) circle.parentElement.removeChild(circle); + // clear its timeout if provided and still in our tracking list + if (timeoutId != null) { + const idx = spawnTimeoutIds.indexOf(timeoutId); + if (idx !== -1) spawnTimeoutIds.splice(idx, 1); + try { clearTimeout(timeoutId); } catch (e) { /* ignore */ } + } +} + +// spawn a circle +function spawnCircle() { + if (!gameActive) return; + + const areaW = gameArea.clientWidth; + const areaH = gameArea.clientHeight; + if (!areaW || !areaH) { + console.log('[spawn] Game area not ready (w/h):', areaW, areaH); + return; + } + + const circle = document.createElement('div'); + circle.className = 'circle'; + + const size = randInt(30, 70); + circle.style.width = `${size}px`; + circle.style.height = `${size}px`; + + const color = COLORS[Math.floor(Math.random() * COLORS.length)]; + circle.dataset.color = color; + circle.style.background = color; + + const x = randInt(0, Math.max(0, areaW - size)); + const y = randInt(0, Math.max(0, areaH - size)); + circle.style.left = `${x}px`; + circle.style.top = `${y}px`; + + // entrance animation + circle.style.opacity = '0'; + circle.style.transform = 'scale(0.6)'; + requestAnimationFrame(() => { + circle.style.opacity = '1'; + circle.style.transform = 'scale(1)'; + }); + + // click handler + const onClick = () => { + if (!gameActive) return; + const clicked = circle.dataset.color; + if (clicked === targetColor) { + score += 2; + circle.style.transform = 'scale(1.2)'; + circle.style.opacity = '0.9'; + } else { + score = Math.max(0, score - 1); + circle.style.transform = 'translateX(6px) scale(0.95)'; + } + updateScore(); + // remove after small delay so user sees animation + setTimeout(() => safeRemoveCircle(circle, onClick), 120); + }; + + circle.addEventListener('click', onClick); + gameArea.appendChild(circle); + + // auto-remove after lifespan and track the timeout id + const lifeSpan = randInt(600, 1100); + const timeoutId = setTimeout(() => { + safeRemoveCircle(circle, onClick, timeoutId); + }, lifeSpan); + + spawnTimeoutIds.push(timeoutId); +} + +// update UI +function updateScore() { + scoreDisplay.textContent = score; +} + +// clear all spawn timeouts and remove circles +function clearAllSpawns() { + // clear timeouts + spawnTimeoutIds.forEach(id => { + try { clearTimeout(id); } catch (e) {} + }); + spawnTimeoutIds = []; + // remove all circles + const children = Array.from(gameArea.children); + children.forEach(node => { + if (node.classList && node.classList.contains('circle')) { + try { node.remove(); } catch (e) {} + } + }); +} + +// start game +function startGame() { + try { + // prevent double-start + if (gameActive) { + console.log('[startGame] Game already active โ€” ignoring start.'); + return; + } + + // ensure a fresh state + endGame(true); + + score = 0; + timeLeft = 30; + updateScore(); + timeDisplay.textContent = timeLeft; + resultBox.classList.add('hidden'); + + chooseTargetColor(); + gameActive = true; + startBtn.disabled = true; // prevent double-click + + // spawn interval + if (spawnIntervalId) clearInterval(spawnIntervalId); + spawnIntervalId = setInterval(spawnCircle, 450); + + // initial immediate spawn so the player sees something quickly + spawnCircle(); + + // timer interval + if (timerIntervalId) clearInterval(timerIntervalId); + timerIntervalId = setInterval(() => { + timeLeft--; + timeDisplay.textContent = timeLeft; + if (timeLeft <= 0) endGame(); + }, 1000); + + console.log('[startGame] started. spawnIntervalId=', spawnIntervalId, 'timerIntervalId=', timerIntervalId); + } catch (err) { + console.error('[startGame] Unexpected error:', err); + } +} + +// end game; silent=true means don't show results (used when resetting) +function endGame(silent = false) { + // stop intervals + if (spawnIntervalId) { + clearInterval(spawnIntervalId); + spawnIntervalId = null; + } + if (timerIntervalId) { + clearInterval(timerIntervalId); + timerIntervalId = null; + } + gameActive = false; + startBtn.disabled = false; + + // clear spawn timeouts and circles + clearAllSpawns(); + + if (!silent) { + finalScore.textContent = score; + resultTarget.style.background = targetColor || 'transparent'; + resultBox.classList.remove('hidden'); + console.log('[endGame] Game over. final score =', score, 'target:', targetColor); + } else { + console.log('[endGame] Silent reset/cleanup performed.'); + } +} + +// visibility handling: pause/resume the timer only (not spawns) +let wasRunningWhenHidden = false; +document.addEventListener('visibilitychange', () => { + if (document.hidden) { + if (gameActive) { + // pause the timer (we'll resume it later) + wasRunningWhenHidden = true; + if (timerIntervalId) { + clearInterval(timerIntervalId); + timerIntervalId = null; + console.log('[visibility] Paused timer because page hidden.'); + } + } else { + wasRunningWhenHidden = false; + } + } else { + // tab visible again: resume timer if gameActive and it was previously running + if (gameActive && !timerIntervalId && wasRunningWhenHidden) { + timerIntervalId = setInterval(() => { + timeLeft--; + timeDisplay.textContent = timeLeft; + if (timeLeft <= 0) endGame(); + }, 1000); + console.log('[visibility] Resumed timer.'); + wasRunningWhenHidden = false; + } + } +}); + +// UI handlers +startBtn.addEventListener('click', startGame); +restartBtn.addEventListener('click', startGame); + +// keyboard: press Tab to start (keep default focus behavior) +document.addEventListener('keydown', (e) => { + if ((e.key === 'Tab' || e.key === 'Enter') && !gameActive) { + e.preventDefault(); + startGame(); + } +}); + +// defensive: clean up when unloading page +window.addEventListener('beforeunload', () => { + endGame(true); +}); diff --git a/games/color catch/style.css b/games/color catch/style.css new file mode 100644 index 00000000..679f1b07 --- /dev/null +++ b/games/color catch/style.css @@ -0,0 +1,116 @@ +* { box-sizing: border-box; margin: 0; padding: 0; } +:root { + --bg: #0f1720; + --panel: #11121a; + --accent: #22c55e; + --muted: #9aa4b2; + --card-border: rgba(255,255,255,0.04); +} + +body { + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; + background: linear-gradient(180deg, #071021 0%, #0f1720 100%); + color: #e6eef6; + padding: 20px; +} + +.container { + width: 100%; + max-width: 640px; + text-align: center; +} + +h1 { font-size: 1.9rem; margin-bottom: 6px; } +p { margin: 8px 0; color: var(--muted); } + +.controls { + display: flex; + gap: 12px; + justify-content: center; + align-items: center; + margin-bottom: 12px; + flex-wrap: wrap; +} + +button { + background: var(--accent); + color: #022; + border: none; + padding: 10px 16px; + border-radius: 10px; + font-weight: 600; + cursor: pointer; + box-shadow: 0 6px 18px rgba(34,197,94,0.12); +} +button:hover { transform: translateY(-2px); } + +.info { + display: flex; + gap: 12px; + align-items: center; + font-weight: 600; + color: #fff; +} + +.game-area { + margin: 0 auto; + width: 100%; + height: 360px; + max-height: 60vh; + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border-radius: 12px; + border: 1px solid var(--card-border); + position: relative; + overflow: hidden; + box-shadow: 0 8px 30px rgba(2,6,23,0.6); +} + +/* Circle */ +.circle { + position: absolute; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + user-select: none; + transition: transform 0.12s linear, opacity 0.12s linear; + will-change: transform, opacity; +} + +/* Target label & sample */ +#targetLabel { + display: inline-flex; + gap: 8px; + align-items: center; + font-weight: 700; + color: #fff; +} +.target-sample { + display: inline-block; + width: 18px; + height: 18px; + border-radius: 50%; + margin-left: 8px; + vertical-align: middle; + border: 2px solid rgba(255,255,255,0.08); +} + +/* Result */ +.result-box { + margin-top: 14px; + padding: 14px; + border-radius: 10px; + background: rgba(255,255,255,0.02); + border: 1px solid var(--card-border); +} + +.hidden { display: none; } + +.hint { + margin-top: 10px; + font-size: 0.9rem diff --git a/games/color-balance/index.html b/games/color-balance/index.html new file mode 100644 index 00000000..90093e53 --- /dev/null +++ b/games/color-balance/index.html @@ -0,0 +1,56 @@ + + + + + +Color Balance | Mini JS Games Hub + + + +
+

๐ŸŽจ Color Balance

+ +
+
+

Target Color

+
+
+
+

Current Color

+
+
+
+ +
+ + + +
+ +
+ + + + + +
+ +
+

Time: 30s

+

Score: 0

+
+ + + +
+ + +
+ + + +
+ + + + diff --git a/games/color-balance/script.js b/games/color-balance/script.js new file mode 100644 index 00000000..4853e80e --- /dev/null +++ b/games/color-balance/script.js @@ -0,0 +1,104 @@ +const targetColorBox = document.getElementById("target-color"); +const currentColorBox = document.getElementById("current-color"); +const redSlider = document.getElementById("red-slider"); +const greenSlider = document.getElementById("green-slider"); +const blueSlider = document.getElementById("blue-slider"); +const timerEl = document.getElementById("timer"); +const scoreEl = document.getElementById("score"); +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const restartBtn = document.getElementById("restart-btn"); +const practiceBtn = document.getElementById("practice-btn"); +const challengeBtn = document.getElementById("challenge-btn"); +const successSound = document.getElementById("success-sound"); +const failSound = document.getElementById("fail-sound"); + +let targetColor = {r:0, g:0, b:0}; +let timer = 30; +let score = 0; +let interval = null; +let running = false; + +function randomColor() { + return { + r: Math.floor(Math.random()*256), + g: Math.floor(Math.random()*256), + b: Math.floor(Math.random()*256) + }; +} + +function updateCurrentColor() { + const r = parseInt(redSlider.value); + const g = parseInt(greenSlider.value); + const b = parseInt(blueSlider.value); + currentColorBox.style.background = `rgb(${r},${g},${b})`; +} + +function updateTargetColor() { + targetColorBox.style.background = `rgb(${targetColor.r},${targetColor.g},${targetColor.b})`; +} + +function colorDistance(c1,c2){ + return Math.sqrt((c1.r-c2.r)**2 + (c1.g-c2.g)**2 + (c1.b-c2.b)**2)/441.67; +} + +function startGame(duration=30){ + targetColor = randomColor(); + updateTargetColor(); + timer = duration; + timerEl.textContent = timer; + running = true; + interval = setInterval(()=>{ + if(timer>0) { + timer--; + timerEl.textContent = timer; + } else { + endRound(); + } + },1000); +} + +function pauseGame(){ + running = false; + clearInterval(interval); +} + +function restartGame(){ + pauseGame(); + score=0; + scoreEl.textContent=score; + startGame(); +} + +function endRound(){ + pauseGame(); + const currentColor = { + r: parseInt(redSlider.value), + g: parseInt(greenSlider.value), + b: parseInt(blueSlider.value) + }; + const dist = colorDistance(currentColor,targetColor); + const roundScore = Math.max(0, Math.floor((1-dist)*1000)); + score += roundScore; + scoreEl.textContent = score; + if(roundScore>800){ + successSound.play(); + } else { + failSound.play(); + } + alert(`Round finished! Score: ${roundScore}`); + startGame(); +} + +startBtn.addEventListener("click", ()=> startGame()); +pauseBtn.addEventListener("click", ()=> pauseGame()); +restartBtn.addEventListener("click", ()=> restartGame()); +practiceBtn.addEventListener("click", ()=> startGame(999)); +challengeBtn.addEventListener("click", ()=> startGame(15)); + +redSlider.addEventListener("input", updateCurrentColor); +greenSlider.addEventListener("input", updateCurrentColor); +blueSlider.addEventListener("input", updateCurrentColor); + +// initial color +updateCurrentColor(); diff --git a/games/color-balance/style.css b/games/color-balance/style.css new file mode 100644 index 00000000..4f325029 --- /dev/null +++ b/games/color-balance/style.css @@ -0,0 +1,89 @@ +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(to bottom, #0a0a0a, #111); + color: #fff; + display: flex; + justify-content: center; + align-items: flex-start; + min-height: 100vh; + padding: 20px; + overflow-x: hidden; +} + +.color-balance-container { + background-color: rgba(0,0,0,0.85); + padding: 20px; + border-radius: 15px; + width: 100%; + max-width: 600px; + text-align: center; + position: relative; + z-index: 2; +} + +h1 { + text-shadow: 0 0 10px #ff0, 0 0 20px #ff0; +} + +.color-box { + width: 150px; + height: 150px; + margin: 10px auto; + border-radius: 15px; + border: 3px solid #fff; + box-shadow: 0 0 20px #fff; +} + +.sliders label { + display: block; + margin: 10px 0; +} + +input[type=range] { + width: 80%; +} + +.controls button { + margin: 5px; + padding: 8px 15px; + font-size: 16px; + border: none; + border-radius: 8px; + cursor: pointer; + background: #ff0; + color: #000; + font-weight: bold; + transition: transform 0.2s; +} + +.controls button:hover { + transform: scale(1.1); +} + +.score-timer { + margin-top: 15px; + font-size: 18px; +} + +/* Fireflies */ +.fireflies { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + pointer-events: none; + z-index: 1; +} + +.firefly { + position: absolute; + width: 50px; + opacity: 0.7; + animation: float 5s infinite ease-in-out; + filter: drop-shadow(0 0 10px yellow); +} + +@keyframes float { + 0% {transform: translate(0,0);} + 50% {transform: translate(50px, -30px);} + 100% {transform: translate(0,0);} +} diff --git a/games/color-circuit/index.html b/games/color-circuit/index.html new file mode 100644 index 00000000..fa6b56a1 --- /dev/null +++ b/games/color-circuit/index.html @@ -0,0 +1,33 @@ + + + + + +Color Circuit | Mini JS Games Hub + + + +
+

Color Circuit ๐Ÿ”ฅ

+

Connect all nodes of the same color to complete the circuit!

+ +
+ + + + +
+ + + +

+
+ + + + + + + + + diff --git a/games/color-circuit/script.js b/games/color-circuit/script.js new file mode 100644 index 00000000..69a0e345 --- /dev/null +++ b/games/color-circuit/script.js @@ -0,0 +1,174 @@ +const canvas = document.getElementById("circuit-canvas"); +const ctx = canvas.getContext("2d"); + +canvas.width = 800; +canvas.height = 600; + +// Sounds +const connectSound = document.getElementById("connect-sound"); +const successSound = document.getElementById("success-sound"); +const errorSound = document.getElementById("error-sound"); + +// Game variables +let nodes = []; +let connections = []; +let currentLine = null; +let isDrawing = false; +let paused = false; + +// Colors +const colors = ["#ff0040","#00ff00","#00ffff"]; +const nodeRadius = 15; + +// Obstacles +const obstacles = [ + {x: 300, y: 200, w: 100, h: 20}, + {x: 500, y: 400, w: 150, h: 20} +]; + +// Generate random nodes +function initNodes() { + nodes = []; + colors.forEach(color => { + for (let i = 0; i < 3; i++) { + nodes.push({ + x: Math.random() * (canvas.width-100)+50, + y: Math.random() * (canvas.height-100)+50, + color: color, + connected: false + }); + } + }); +} + +// Draw nodes, obstacles, connections +function draw() { + ctx.clearRect(0,0,canvas.width,canvas.height); + + // Draw connections + connections.forEach(line => { + ctx.strokeStyle = line.color; + ctx.lineWidth = 6; + ctx.shadowColor = line.color; + ctx.shadowBlur = 20; + ctx.beginPath(); + ctx.moveTo(line.from.x,line.from.y); + ctx.lineTo(line.to.x,line.to.y); + ctx.stroke(); + ctx.shadowBlur = 0; + }); + + // Draw obstacles + obstacles.forEach(obs => { + ctx.fillStyle = "#880000"; + ctx.fillRect(obs.x,obs.y,obs.w,obs.h); + }); + + // Draw nodes + nodes.forEach(node => { + ctx.beginPath(); + ctx.arc(node.x,node.y,nodeRadius,0,Math.PI*2); + ctx.fillStyle = node.color; + ctx.shadowColor = node.color; + ctx.shadowBlur = 15; + ctx.fill(); + ctx.shadowBlur = 0; + ctx.strokeStyle = "#fff"; + ctx.lineWidth = 2; + ctx.stroke(); + }); + + // Draw current line + if(currentLine) { + ctx.strokeStyle = currentLine.color; + ctx.lineWidth = 6; + ctx.shadowColor = currentLine.color; + ctx.shadowBlur = 20; + ctx.beginPath(); + ctx.moveTo(currentLine.from.x,currentLine.from.y); + ctx.lineTo(currentLine.to.x,currentLine.to.y); + ctx.stroke(); + ctx.shadowBlur = 0; + } +} + +function distance(a,b) { + return Math.sqrt((a.x-b.x)**2 + (a.y-b.y)**2); +} + +// Mouse events +canvas.addEventListener("mousedown", e => { + if(paused) return; + const rect = canvas.getBoundingClientRect(); + const mx = e.clientX - rect.left; + const my = e.clientY - rect.top; + + nodes.forEach(node => { + if(distance(node,{x:mx,y:my}) < nodeRadius) { + isDrawing = true; + currentLine = {from: node, to: {x:mx,y:my}, color: node.color}; + } + }); +}); + +canvas.addEventListener("mousemove", e => { + if(!isDrawing || paused) return; + const rect = canvas.getBoundingClientRect(); + currentLine.to.x = e.clientX - rect.left; + currentLine.to.y = e.clientY - rect.top; +}); + +canvas.addEventListener("mouseup", e => { + if(!isDrawing || paused) return; + const rect = canvas.getBoundingClientRect(); + const mx = e.clientX - rect.left; + const my = e.clientY - rect.top; + + let validConnection = false; + + nodes.forEach(node => { + if(distance(node,{x:mx,y:my}) colorMap[color] = nodes.filter(n=>n.color===color)); + let won = colors.every(color=>{ + return colorMap[color].every(node=>{ + return connections.some(c=>c.from===node || c.to===node); + }); + }); + if(won){ + successSound.play(); + document.getElementById("message").textContent = "๐ŸŽ‰ All circuits completed!"; + } +} + +// Buttons +document.getElementById("restart-btn").addEventListener("click", ()=>{ + initNodes(); + connections = []; + currentLine = null; + draw(); + document.getElementById("message").textContent = ""; +}); +document.getElementById("reset-btn").addEventListener("click", ()=>{ + connections = []; + currentLine = null; + draw(); + document.getElementById("message").textContent = ""; +}); +document.getElementById("pause-btn").addEventListener("click", ()=>{ + paused = !paused; + document.getEl diff --git a/games/color-circuit/style.css b/games/color-circuit/style.css new file mode 100644 index 00000000..ea2a96e0 --- /dev/null +++ b/games/color-circuit/style.css @@ -0,0 +1,44 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: radial-gradient(circle, #1e1f2f, #12121f); + color: #fff; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.circuit-container { + text-align: center; + max-width: 900px; + margin: 20px; +} + +canvas { + background-color: #222; + border-radius: 15px; + box-shadow: 0 0 20px #00ffff; + margin-top: 20px; +} + +.controls { + margin: 15px; +} + +.controls button { + padding: 10px 20px; + margin: 5px; + font-size: 16px; + border-radius: 8px; + border: none; + cursor: pointer; + background: linear-gradient(45deg,#0ff,#08f); + color: #000; + font-weight: bold; + transition: all 0.3s; +} + +.controls button:hover { + box-shadow: 0 0 15px #0ff, 0 0 25px #0ff inset; +} diff --git a/games/color-clicker/index.html b/games/color-clicker/index.html new file mode 100644 index 00000000..61768964 --- /dev/null +++ b/games/color-clicker/index.html @@ -0,0 +1,17 @@ + + + + + + Color Clicker Game + + + +
+

Color Clicker

+

Score: 0

+
+
+ + + \ No newline at end of file diff --git a/games/color-clicker/script.js b/games/color-clicker/script.js new file mode 100644 index 00000000..93fcecf8 --- /dev/null +++ b/games/color-clicker/script.js @@ -0,0 +1,29 @@ +document.addEventListener('DOMContentLoaded', () => { + const colorBox = document.getElementById('color-box'); + const scoreDisplay = document.getElementById('score'); + // const backButton = document.getElementById('back-button'); <-- REMOVE THIS LINE + + let score = 0; + + const colors = ['#e74c3c', '#2ecc71', '#3498db', '#f1c40f', '#9b59b6', '#1abc9c']; + + function getRandomColor() { + const randomIndex = Math.floor(Math.random() * colors.length); + return colors[randomIndex]; + } + + function updateScore() { + score++; + scoreDisplay.textContent = score; + } + + function changeBoxColor() { + colorBox.style.backgroundColor = getRandomColor(); + } + + colorBox.addEventListener('click', () => { + updateScore(); + changeBoxColor(); + }); + changeBoxColor(); +}); \ No newline at end of file diff --git a/games/color-clicker/style.css b/games/color-clicker/style.css new file mode 100644 index 00000000..890da90d --- /dev/null +++ b/games/color-clicker/style.css @@ -0,0 +1,63 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f0f0f0; +} + +.game-container { + background-color: #fff; + padding: 30px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + text-align: center; +} + +h1 { + color: #333; + margin-bottom: 20px; +} + +p { + font-size: 1.2em; + color: #555; +} + +#score { + font-weight: bold; + color: #007bff; +} + +.color-box { + width: 150px; + height: 150px; + background-color: #3498db; /* Default color */ + margin: 30px auto; + border-radius: 10px; + cursor: pointer; + transition: background-color 0.2s ease-in-out; + border: 2px solid #2980b9; +} + +.color-box:hover { + opacity: 0.9; +} + +#back-button { + background-color: #6c757d; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + font-size: 1em; + margin-top: 20px; + transition: background-color 0.3s ease; +} + +#back-button:hover { + background-color: #5a6268; +} \ No newline at end of file diff --git a/games/color-guessing-game/index.html b/games/color-guessing-game/index.html new file mode 100644 index 00000000..68e3dd39 --- /dev/null +++ b/games/color-guessing-game/index.html @@ -0,0 +1,38 @@ + + + + + + Color Guessing Game ๐ŸŽจ + + + +
+
+

Color Guessing Game ๐ŸŽจ

+

Target Color:

+
+ +
+ + + +
+ +
+

Score: 0

+

Time Left: 30s

+
+ +
+ +
+ +
+ +
+
+ + + + diff --git a/games/color-guessing-game/script.js b/games/color-guessing-game/script.js new file mode 100644 index 00000000..55f7b620 --- /dev/null +++ b/games/color-guessing-game/script.js @@ -0,0 +1,117 @@ +const tilesContainer = document.getElementById("tiles"); +const targetRGB = document.getElementById("targetRGB"); +const message = document.getElementById("message"); +const scoreEl = document.getElementById("score"); +const timerEl = document.getElementById("timer"); +const playAgainBtn = document.getElementById("playAgain"); +const newColorsBtn = document.getElementById("newColors"); +const easyBtn = document.getElementById("easyBtn"); +const hardBtn = document.getElementById("hardBtn"); + +let numTiles = 6; +let colors = []; +let pickedColor; +let score = 0; +let timeLeft = 30; +let timerInterval; + +// Generate random RGB +function randomColor() { + const r = Math.floor(Math.random() * 256); + const g = Math.floor(Math.random() * 256); + const b = Math.floor(Math.random() * 256); + return `rgb(${r}, ${g}, ${b})`; +} + +function generateColors(num) { + const arr = []; + for (let i = 0; i < num; i++) arr.push(randomColor()); + return arr; +} + +function pickColor() { + return colors[Math.floor(Math.random() * colors.length)]; +} + +function setupGame() { + colors = generateColors(numTiles); + pickedColor = pickColor(); + targetRGB.textContent = pickedColor.toUpperCase(); + tilesContainer.innerHTML = ""; + message.textContent = ""; + colors.forEach(color => { + const tile = document.createElement("div"); + tile.classList.add("tile"); + tile.style.backgroundColor = color; + tile.setAttribute("tabindex", "0"); + tile.addEventListener("click", () => checkColor(tile, color)); + tilesContainer.appendChild(tile); + }); +} + +function checkColor(tile, color) { + if (color === pickedColor) { + message.textContent = "โœ… Correct!"; + score += 10; + scoreEl.textContent = score; + changeColors(pickedColor); + setTimeout(setupGame, 800); + } else { + message.textContent = "โŒ Try Again!"; + tile.style.visibility = "hidden"; + score -= 2; + scoreEl.textContent = score; + } +} + +function changeColors(color) { + document.querySelectorAll(".tile").forEach(t => { + t.style.backgroundColor = color; + t.style.visibility = "visible"; + }); +} + +function resetGame() { + clearInterval(timerInterval); + score = 0; + scoreEl.textContent = score; + timeLeft = 30; + startTimer(); + setupGame(); +} + +function startTimer() { + clearInterval(timerInterval); + timerEl.textContent = timeLeft; + timerInterval = setInterval(() => { + timeLeft--; + timerEl.textContent = timeLeft; + if (timeLeft <= 0) { + clearInterval(timerInterval); + message.textContent = `โฐ Time's Up! Final Score: ${score}`; + document.querySelectorAll(".tile").forEach(t => (t.style.pointerEvents = "none")); + } + }, 1000); +} + +// Mode buttons +easyBtn.addEventListener("click", () => { + numTiles = 3; + easyBtn.classList.add("active"); + hardBtn.classList.remove("active"); + setupGame(); +}); + +hardBtn.addEventListener("click", () => { + numTiles = 6; + hardBtn.classList.add("active"); + easyBtn.classList.remove("active"); + setupGame(); +}); + +newColorsBtn.addEventListener("click", setupGame); +playAgainBtn.addEventListener("click", resetGame); + +// Init +setupGame(); +startTimer(); diff --git a/games/color-guessing-game/style.css b/games/color-guessing-game/style.css new file mode 100644 index 00000000..4bb5bc07 --- /dev/null +++ b/games/color-guessing-game/style.css @@ -0,0 +1,110 @@ +* { + box-sizing: border-box; +} + +body { + font-family: "Poppins", sans-serif; + background: linear-gradient(135deg, #4a00e0, #8e2de2); + min-height: 100vh; + margin: 0; + display: flex; + justify-content: center; + align-items: center; + color: white; +} + +.container { + background-color: rgba(255, 255, 255, 0.1); + padding: 30px; + border-radius: 20px; + width: 90%; + max-width: 600px; + text-align: center; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); +} + +header h1 { + margin: 0 0 10px; + font-size: 2rem; +} + +.target-color { + font-size: 1.1rem; + margin-bottom: 10px; +} + +.game-controls { + margin: 10px 0; +} + +.mode-btn, #newColors { + background: #fff; + border: none; + color: #4a00e0; + font-weight: 600; + padding: 8px 14px; + border-radius: 8px; + margin: 5px; + cursor: pointer; + transition: all 0.3s; +} + +.mode-btn.active { + background: #8e2de2; + color: white; +} + +.mode-btn:hover, #newColors:hover { + transform: scale(1.05); +} + +.scoreboard { + display: flex; + justify-content: space-around; + margin: 10px 0 20px; + font-size: 1.1rem; +} + +.tiles-container { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 15px; + margin: 20px 0; +} + +.tile { + width: 100%; + padding-bottom: 100%; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); +} + +.tile:hover { + transform: scale(1.08); +} + +.message { + min-height: 24px; + font-weight: bold; + margin: 10px 0; +} + +footer { + margin-top: 20px; +} + +#playAgain { + background-color: #8e2de2; + color: white; + border: none; + padding: 10px 18px; + border-radius: 8px; + cursor: pointer; + font-size: 1rem; +} + +#playAgain:hover { + background-color: #4a00e0; +} diff --git a/games/color-harmony/index.html b/games/color-harmony/index.html new file mode 100644 index 00000000..487e8b55 --- /dev/null +++ b/games/color-harmony/index.html @@ -0,0 +1,32 @@ + + + + + + Color Harmony - Mini JS Games Hub + + + +
+

Color Harmony

+
+
+
+
+
+
+ + + +
+
+

Level: 1

+

Moves: 0

+

+
+
+
Made for Mini JS Games Hub
+
+ + + diff --git a/games/color-harmony/script.js b/games/color-harmony/script.js new file mode 100644 index 00000000..9b991751 --- /dev/null +++ b/games/color-harmony/script.js @@ -0,0 +1,198 @@ +// Color Harmony Game +const gridSize = 4; +const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24', '#f0932b', '#eb4d4b', '#6c5ce7', '#a29bfe']; +let grid = []; +let target = []; +let selectedTile = null; +let moves = 0; +let level = 1; +let maxMoves = null; +let blockers = new Set(); + +const gridElement = document.getElementById('grid'); +const targetElement = document.getElementById('targetPalette'); +const levelElement = document.getElementById('level'); +const movesElement = document.getElementById('moves'); +const statusElement = document.getElementById('status'); +const shuffleBtn = document.getElementById('shuffleBtn'); +const resetBtn = document.getElementById('resetBtn'); +const nextBtn = document.getElementById('nextBtn'); + +function initGame() { + createTarget(); + createGrid(); + shuffleGrid(); + updateUI(); + selectedTile = null; + moves = 0; + updateMoves(); + statusElement.textContent = ''; + nextBtn.style.display = 'none'; +} + +function createTarget() { + target = []; + for (let i = 0; i < gridSize; i++) { + target.push(colors[Math.floor(Math.random() * colors.length)]); + } + renderTarget(); +} + +function renderTarget() { + targetElement.innerHTML = ''; + target.forEach(color => { + const tile = document.createElement('div'); + tile.className = 'tile'; + tile.style.backgroundColor = color; + targetElement.appendChild(tile); + }); +} + +function createGrid() { + grid = []; + for (let i = 0; i < gridSize; i++) { + grid[i] = []; + for (let j = 0; j < gridSize; j++) { + grid[i][j] = target[j]; // start with target arrangement + } + } + // Add some random tiles + for (let i = 0; i < gridSize * gridSize / 2; i++) { + const r1 = Math.floor(Math.random() * gridSize); + const c1 = Math.floor(Math.random() * gridSize); + const r2 = Math.floor(Math.random() * gridSize); + const c2 = Math.floor(Math.random() * gridSize); + [grid[r1][c1], grid[r2][c2]] = [grid[r2][c2], grid[r1][c1]]; + } + renderGrid(); +} + +function renderGrid() { + gridElement.innerHTML = ''; + gridElement.style.gridTemplateColumns = `repeat(${gridSize}, 1fr)`; + for (let i = 0; i < gridSize; i++) { + for (let j = 0; j < gridSize; j++) { + const tile = document.createElement('div'); + tile.className = 'tile'; + tile.style.backgroundColor = grid[i][j]; + tile.dataset.row = i; + tile.dataset.col = j; + if (blockers.has(`${i}-${j}`)) { + tile.classList.add('blocker'); + } else { + tile.addEventListener('click', () => handleTileClick(i, j)); + } + gridElement.appendChild(tile); + } + } +} + +function handleTileClick(row, col) { + if (selectedTile) { + if (selectedTile.row === row && selectedTile.col === col) { + // Deselect + document.querySelector(`[data-row="${row}"][data-col="${col}"]`).classList.remove('selected'); + selectedTile = null; + } else if (isAdjacent(selectedTile.row, selectedTile.col, row, col)) { + // Swap + swapTiles(selectedTile.row, selectedTile.col, row, col); + moves++; + updateMoves(); + document.querySelector(`[data-row="${selectedTile.row}"][data-col="${selectedTile.col}"]`).classList.remove('selected'); + selectedTile = null; + checkWin(); + } else { + // Select new + document.querySelector(`[data-row="${selectedTile.row}"][data-col="${selectedTile.col}"]`).classList.remove('selected'); + document.querySelector(`[data-row="${row}"][data-col="${col}"]`).classList.add('selected'); + selectedTile = {row, col}; + } + } else { + // Select + document.querySelector(`[data-row="${row}"][data-col="${col}"]`).classList.add('selected'); + selectedTile = {row, col}; + } +} + +function isAdjacent(r1, c1, r2, c2) { + return Math.abs(r1 - r2) + Math.abs(c1 - c2) === 1; +} + +function swapTiles(r1, c1, r2, c2) { + [grid[r1][c1], grid[r2][c2]] = [grid[r2][c2], grid[r1][c1]]; + renderGrid(); +} + +function shuffleGrid() { + for (let i = 0; i < 100; i++) { + const r1 = Math.floor(Math.random() * gridSize); + const c1 = Math.floor(Math.random() * gridSize); + const r2 = Math.floor(Math.random() * gridSize); + const c2 = Math.floor(Math.random() * gridSize); + if (isAdjacent(r1, c1, r2, c2) && !blockers.has(`${r1}-${c1}`) && !blockers.has(`${r2}-${c2}`)) { + swapTiles(r1, c1, r2, c2); + } + } + moves = 0; + updateMoves(); +} + +function checkWin() { + for (let j = 0; j < gridSize; j++) { + if (grid[0][j] !== target[j]) return; + } + // Win + statusElement.textContent = 'Level Complete!'; + nextBtn.style.display = 'inline-block'; + // Animate matched tiles + document.querySelectorAll('.tile').forEach(tile => { + const row = parseInt(tile.dataset.row); + if (row === 0) tile.classList.add('matched'); + }); +} + +function updateUI() { + levelElement.textContent = `Level: ${level}`; + if (maxMoves) { + movesElement.textContent = `Moves: ${moves}/${maxMoves}`; + } else { + movesElement.textContent = `Moves: ${moves}`; + } +} + +function updateMoves() { + updateUI(); + if (maxMoves && moves >= maxMoves) { + statusElement.textContent = 'Out of moves! Try again.'; + resetBtn.style.display = 'inline-block'; + } +} + +function nextLevel() { + level++; + blockers.clear(); + if (level > 5) { + // Add blockers + const numBlockers = Math.min(level - 5, 4); + for (let i = 0; i < numBlockers; i++) { + let r, c; + do { + r = Math.floor(Math.random() * gridSize); + c = Math.floor(Math.random() * gridSize); + } while (blockers.has(`${r}-${c}`)); + blockers.add(`${r}-${c}`); + } + } + if (level > 10) { + maxMoves = 50 - (level - 10) * 5; + } else { + maxMoves = null; + } + initGame(); +} + +shuffleBtn.addEventListener('click', shuffleGrid); +resetBtn.addEventListener('click', initGame); +nextBtn.addEventListener('click', nextLevel); + +initGame(); diff --git a/games/color-harmony/style.css b/games/color-harmony/style.css new file mode 100644 index 00000000..b7c0b557 --- /dev/null +++ b/games/color-harmony/style.css @@ -0,0 +1,22 @@ +*{box-sizing:border-box;margin:0;padding:0} +body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px} +.game-wrap{background:#fff;border-radius:20px;padding:30px;text-align:center;box-shadow:0 20px 40px rgba(0,0,0,0.1);max-width:600px;width:100%} +h1{color:#333;margin-bottom:20px;font-size:2.5em;text-shadow:0 2px 4px rgba(0,0,0,0.1)} +.game-area{display:flex;flex-direction:column;gap:20px} +.target-palette{display:flex;justify-content:center;gap:5px;padding:15px;background:#f8f9fa;border-radius:10px;border:2px solid #e9ecef} +.target-palette .tile{width:40px;height:40px;border-radius:8px;border:2px solid #dee2e6} +.grid-container{display:flex;justify-content:center} +.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;padding:20px;background:#f8f9fa;border-radius:10px;border:2px solid #e9ecef;width:fit-content} +.tile{width:60px;height:60px;border-radius:10px;border:2px solid #dee2e6;cursor:pointer;transition:all 0.3s ease;user-select:none;position:relative;overflow:hidden} +.tile:hover{transform:scale(1.05);box-shadow:0 4px 8px rgba(0,0,0,0.2)} +.tile.selected{box-shadow:0 0 0 3px #007bff;border-color:#007bff} +.tile.matched{animation:matchPulse 0.6s ease} +@keyframes matchPulse{0%{transform:scale(1)}50%{transform:scale(1.1)}100%{transform:scale(1)}} +.controls{display:flex;gap:10px;justify-content:center} +button{padding:10px 20px;border:none;border-radius:8px;background:#007bff;color:#fff;font-size:16px;cursor:pointer;transition:all 0.3s ease} +button:hover{background:#0056b3;transform:translateY(-2px)} +button:disabled{background:#6c757d;cursor:not-allowed;transform:none} +.info{display:flex;justify-content:space-around;margin-top:20px;font-size:18px;color:#333} +footer{font-size:14px;color:#666;margin-top:20px} +.blocker{background:#343a40 !important;border:2px solid #495057 !important;cursor:not-allowed} +.blocker:hover{transform:none !important} diff --git a/games/color-merge/index.html b/games/color-merge/index.html new file mode 100644 index 00000000..d805d7de --- /dev/null +++ b/games/color-merge/index.html @@ -0,0 +1,27 @@ + + + + + + Color Merge | Mini JS Games Hub + + + +
+
+

Color Merge ๐ŸŽจ

+
+ Score: 0 +
+
+
+
+ +
+
+

Merge blocks of the same color to reach the target!

+
+
+ + + diff --git a/games/color-merge/script.js b/games/color-merge/script.js new file mode 100644 index 00000000..ee79affe --- /dev/null +++ b/games/color-merge/script.js @@ -0,0 +1,55 @@ +const board = document.getElementById("board"); +const scoreEl = document.getElementById("score"); +const restartBtn = document.getElementById("restart"); + +const gridSize = 4; +let score = 0; +let blocks = []; + +const colors = ["#FF5733", "#33FF57", "#3357FF", "#F333FF", "#FFEB33"]; +let targetColor = colors[Math.floor(Math.random() * colors.length)]; + +function initBoard() { + board.innerHTML = ""; + blocks = []; + score = 0; + scoreEl.textContent = score; + + for (let i = 0; i < gridSize * gridSize; i++) { + const block = document.createElement("div"); + block.classList.add("block"); + const color = colors[Math.floor(Math.random() * colors.length)]; + block.style.backgroundColor = color; + block.dataset.color = color; + block.addEventListener("click", () => mergeBlock(block)); + board.appendChild(block); + blocks.push(block); + } +} + +function mergeBlock(selectedBlock) { + const sameColorBlocks = blocks.filter( + b => b.dataset.color === selectedBlock.dataset.color + ); + + if (sameColorBlocks.length < 2) { + alert("Need at least 2 blocks of same color to merge!"); + return; + } + + sameColorBlocks.forEach(b => { + b.style.backgroundColor = targetColor; + b.dataset.color = targetColor; + }); + + score += sameColorBlocks.length * 10; + scoreEl.textContent = score; + + // Generate a new target color + targetColor = colors[Math.floor(Math.random() * colors.length)]; +} + +restartBtn.addEventListener("click", initBoard); + +// Initialize game on load +initBoard(); diff --git a/games/color-merge/style.css b/games/color-merge/style.css new file mode 100644 index 00000000..a0616209 --- /dev/null +++ b/games/color-merge/style.css @@ -0,0 +1,72 @@ +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(to right, #ffecd2, #fcb69f); + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; +} + +.game-container { + background: #fff; + padding: 20px 30px; + border-radius: 20px; + box-shadow: 0 10px 25px rgba(0,0,0,0.2); + width: 350px; + text-align: center; +} + +header h1 { + margin: 0; + font-size: 24px; + color: #333; +} + +.score-container { + margin: 10px 0; + font-size: 18px; + font-weight: bold; +} + +.game-board { + display: grid; + grid-template-columns: repeat(4, 70px); + grid-gap: 10px; + justify-content: center; + margin: 20px 0; +} + +.block { + width: 70px; + height: 70px; + border-radius: 10px; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; +} + +.block:hover { + transform: scale(1.1); + box-shadow: 0 5px 15px rgba(0,0,0,0.3); +} + +.controls button { + padding: 10px 20px; + border: none; + border-radius: 10px; + background: #ff7f50; + color: white; + font-size: 16px; + cursor: pointer; + transition: background 0.2s; +} + +.controls button:hover { + background: #ff5722; +} + +footer p { + font-size: 12px; + color: #555; + margin-top: 15px; +} diff --git a/games/color-mixer/index.html b/games/color-mixer/index.html new file mode 100644 index 00000000..2150d07b --- /dev/null +++ b/games/color-mixer/index.html @@ -0,0 +1,49 @@ + + + + + + Color Mixer + + + +
+

๐ŸŽจ Color Mixer

+

Mix colors to create the perfect shade!

+
+
+
+
Target
+
+
+
+
Your Mix
+
+
+
+
Level: 1
+
Score: 0
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + \ No newline at end of file diff --git a/games/color-mixer/script.js b/games/color-mixer/script.js new file mode 100644 index 00000000..83319b56 --- /dev/null +++ b/games/color-mixer/script.js @@ -0,0 +1,135 @@ +const startBtn = document.getElementById("start-btn"); +const submitBtn = document.getElementById("submit-btn"); +const nextBtn = document.getElementById("next-btn"); +const resetBtn = document.getElementById("reset-btn"); +const targetColorEl = document.getElementById("target-color"); +const currentColorEl = document.getElementById("current-color"); +const redSlider = document.getElementById("red"); +const greenSlider = document.getElementById("green"); +const blueSlider = document.getElementById("blue"); +const redValue = document.getElementById("red-value"); +const greenValue = document.getElementById("green-value"); +const blueValue = document.getElementById("blue-value"); +const messageEl = document.getElementById("message"); +const levelEl = document.getElementById("current-level"); +const scoreEl = document.getElementById("current-score"); + +let level = 1; +let score = 0; +let round = 0; +let targetR, targetG, targetB; +let gameActive = false; + +startBtn.addEventListener("click", startGame); +submitBtn.addEventListener("click", submitMix); +nextBtn.addEventListener("click", nextLevel); +resetBtn.addEventListener("click", resetGame); + +redSlider.addEventListener("input", updateColor); +greenSlider.addEventListener("input", updateColor); +blueSlider.addEventListener("input", updateColor); + +function startGame() { + level = 1; + score = 0; + round = 0; + levelEl.textContent = level; + scoreEl.textContent = score; + startBtn.style.display = "none"; + gameActive = true; + generateTargetColor(); + updateColor(); +} + +function generateTargetColor() { + // Generate colors that are not too close to extremes for easier start + const range = 255 - level * 20; // Smaller range for higher levels + const offset = level * 10; + targetR = Math.floor(Math.random() * range) + offset; + targetG = Math.floor(Math.random() * range) + offset; + targetB = Math.floor(Math.random() * range) + offset; + targetColorEl.style.backgroundColor = `rgb(${targetR}, ${targetG}, ${targetB})`; +} + +function updateColor() { + const r = redSlider.value; + const g = greenSlider.value; + const b = blueSlider.value; + redValue.textContent = r; + greenValue.textContent = g; + blueValue.textContent = b; + currentColorEl.style.backgroundColor = `rgb(${r}, ${g}, ${b})`; +} + +function submitMix() { + if (!gameActive) return; + const r = parseInt(redSlider.value); + const g = parseInt(greenSlider.value); + const b = parseInt(blueSlider.value); + + const distance = Math.sqrt( + Math.pow(r - targetR, 2) + + Math.pow(g - targetG, 2) + + Math.pow(b - targetB, 2) + ); + + const maxDistance = Math.sqrt(3 * Math.pow(255, 2)); + const accuracy = 1 - (distance / maxDistance); + const points = Math.round(accuracy * 100 * level); + + score += points; + scoreEl.textContent = score; + round++; + + if (distance < 10) { + messageEl.textContent = `๐ŸŽ‰ Perfect match! +${points} points`; + } else if (distance < 50) { + messageEl.textContent = `๐Ÿ‘ Close! +${points} points`; + } else { + messageEl.textContent = `๐Ÿ˜… Not quite! +${points} points`; + } + + submitBtn.style.display = "none"; + nextBtn.style.display = "inline-block"; + + if (round >= 5) { // 5 rounds per level + nextBtn.textContent = "Next Level"; + messageEl.textContent += " Level complete!"; + } else { + nextBtn.textContent = "Next Round"; + } +} + +function nextLevel() { + if (round >= 5) { + level++; + levelEl.textContent = level; + round = 0; + } + nextBtn.style.display = "none"; + submitBtn.style.display = "inline-block"; + messageEl.textContent = ""; + generateTargetColor(); + // Reset sliders to middle + redSlider.value = 128; + greenSlider.value = 128; + blueSlider.value = 128; + updateColor(); +} + +function resetGame() { + level = 1; + score = 0; + round = 0; + levelEl.textContent = level; + scoreEl.textContent = score; + messageEl.textContent = ""; + startBtn.style.display = "inline-block"; + submitBtn.style.display = "inline-block"; + nextBtn.style.display = "none"; + resetBtn.style.display = "none"; + gameActive = false; + // Reset colors + targetColorEl.style.backgroundColor = "#888"; + currentColorEl.style.backgroundColor = "#888"; +} \ No newline at end of file diff --git a/games/color-mixer/style.css b/games/color-mixer/style.css new file mode 100644 index 00000000..4b704328 --- /dev/null +++ b/games/color-mixer/style.css @@ -0,0 +1,116 @@ +* { + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: linear-gradient(135deg, #667eea, #764ba2); + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + margin: 0; + color: #fff; +} + +.container { + background: rgba(0, 0, 0, 0.3); + padding: 30px; + border-radius: 20px; + text-align: center; + width: 500px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4); +} + +h1 { + margin-bottom: 15px; + font-size: 2rem; + color: #fff; + text-shadow: 1px 1px 3px rgba(0,0,0,0.5); +} + +.color-display { + display: flex; + justify-content: space-around; + margin: 20px 0; +} + +.color-box { + text-align: center; +} + +.color { + width: 100px; + height: 100px; + border-radius: 10px; + border: 3px solid #fff; + margin: 10px; +} + +.label { + font-weight: bold; +} + +.sliders { + margin: 20px 0; +} + +.slider-group { + margin: 15px 0; +} + +input[type="range"] { + width: 100%; + -webkit-appearance: none; + appearance: none; + height: 10px; + border-radius: 5px; + background: #ddd; + outline: none; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + border-radius: 50%; + background: #2ecc71; + cursor: pointer; +} + +input[type="range"]::-moz-range-thumb { + width: 20px; + height: 20px; + border-radius: 50%; + background: #2ecc71; + cursor: pointer; + border: none; +} + +button { + background: #2ecc71; + color: #fff; + border: none; + border-radius: 8px; + padding: 10px 20px; + font-size: 16px; + cursor: pointer; + transition: background 0.3s; + margin: 5px; +} + +button:hover { + background: #27ae60; +} + +#level, #score { + font-size: 18px; + margin: 5px 0; +} + +#message { + margin: 10px 0; + font-size: 16px; + min-height: 20px; +} \ No newline at end of file diff --git a/games/color-squid-puzzle/index.html b/games/color-squid-puzzle/index.html new file mode 100644 index 00000000..23bd3c24 --- /dev/null +++ b/games/color-squid-puzzle/index.html @@ -0,0 +1,23 @@ + + + + + + Color Squid Puzzle | Mini JS Games Hub + + + +
+

Color Squid Puzzle

+
+ Level: 1 + Time: 0s + +
+
+
+
+ + + + diff --git a/games/color-squid-puzzle/script.js b/games/color-squid-puzzle/script.js new file mode 100644 index 00000000..ac108557 --- /dev/null +++ b/games/color-squid-puzzle/script.js @@ -0,0 +1,91 @@ +const grid = document.getElementById("puzzle-grid"); +const levelEl = document.getElementById("level"); +const timerEl = document.getElementById("timer"); +const messageEl = document.getElementById("message"); +const restartBtn = document.getElementById("restart-btn"); + +let level = 1; +let timer = 0; +let timerInterval; +let gridSize = 3; +let colors = []; +let correctSequence = []; + +function generateColors(n) { + const palette = []; + for(let i=0;i Math.random() - 0.5); +} + +function startTimer() { + clearInterval(timerInterval); + timer = 0; + timerEl.textContent = timer + "s"; + timerInterval = setInterval(()=>{ + timer++; + timerEl.textContent = timer + "s"; + }, 1000); +} + +function renderGrid() { + grid.innerHTML = ""; + grid.style.gridTemplateColumns = `repeat(${gridSize}, 60px)`; + grid.style.gridTemplateRows = `repeat(${gridSize}, 60px)`; + + const shuffled = shuffleArray([...colors]); + shuffled.forEach((color, idx) => { + const cell = document.createElement("div"); + cell.style.backgroundColor = color; + cell.dataset.color = color; + cell.addEventListener("click", ()=>handleClick(idx)); + grid.appendChild(cell); + }); +} + +function handleClick(idx) { + const selected = grid.children[idx]; + const expectedColor = correctSequence[0]; + if(selected.dataset.color === expectedColor){ + selected.classList.add("correct"); + correctSequence.shift(); + if(correctSequence.length === 0){ + levelUp(); + } + } else { + messageEl.textContent = "โŒ Wrong! Try Again."; + setTimeout(()=>messageEl.textContent="", 1000); + } +} + +function levelUp() { + clearInterval(timerInterval); + messageEl.textContent = `๐ŸŽ‰ Level ${level} Completed in ${timer}s!`; + level++; + gridSize = Math.min(6, 2 + level); // max 6x6 + startGame(); +} + +function startGame() { + levelEl.textContent = level; + colors = generateColors(gridSize*gridSize); + correctSequence = [...colors]; + renderGrid(); + startTimer(); + messageEl.textContent = ""; +} + +restartBtn.addEventListener("click", ()=>{ + level = 1; + gridSize = 3; + startGame(); +}); + +// Initial game start +startGame(); diff --git a/games/color-squid-puzzle/style.css b/games/color-squid-puzzle/style.css new file mode 100644 index 00000000..95d58e6d --- /dev/null +++ b/games/color-squid-puzzle/style.css @@ -0,0 +1,65 @@ +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #6a11cb, #2575fc); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + color: #fff; +} + +.game-container { + background: rgba(0,0,0,0.6); + padding: 20px 30px; + border-radius: 15px; + text-align: center; + box-shadow: 0 0 20px rgba(0,0,0,0.5); + width: 350px; +} + +h1 { + margin-bottom: 15px; +} + +.controls { + display: flex; + justify-content: space-between; + margin-bottom: 15px; + font-weight: bold; +} + +button { + padding: 5px 10px; + border: none; + background: #ff7f50; + color: #fff; + font-weight: bold; + border-radius: 5px; + cursor: pointer; +} + +.grid { + display: grid; + gap: 5px; + justify-content: center; +} + +.grid div { + width: 60px; + height: 60px; + border-radius: 10px; + cursor: pointer; + transition: transform 0.2s; +} + +.grid div.correct { + transform: scale(1.1); + box-shadow: 0 0 10px #fff; +} + +.message { + margin-top: 15px; + font-size: 18px; + font-weight: bold; +} diff --git a/games/color-switch-challenge/index.html b/games/color-switch-challenge/index.html new file mode 100644 index 00000000..dca73d29 --- /dev/null +++ b/games/color-switch-challenge/index.html @@ -0,0 +1,20 @@ + + + + + + Color Switch Challenge + + + +
+ +
+ Score: 0 +
+ +
+ + + + diff --git a/games/color-switch-challenge/script.js b/games/color-switch-challenge/script.js new file mode 100644 index 00000000..0c1d6172 --- /dev/null +++ b/games/color-switch-challenge/script.js @@ -0,0 +1,150 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); +canvas.width = 400; +canvas.height = 600; + +const restartBtn = document.getElementById("restartBtn"); +const scoreEl = document.getElementById("score"); + +let score = 0; +let ball = { x: canvas.width/2, y: 500, radius: 15, color: "red", dy: 0 }; +let gravity = 0.6; +let jumpPower = -10; +let obstacles = []; +let gameOver = false; +let colors = ["red", "yellow", "blue", "green"]; + +function randomColor() { + return colors[Math.floor(Math.random() * colors.length)]; +} + +class Obstacle { + constructor(y) { + this.y = y; + this.rotation = 0; + this.radius = 80; + this.thickness = 15; + this.speed = 0.02 + Math.random()*0.02; + this.colors = [randomColor(), randomColor(), randomColor(), randomColor()]; + } + + draw() { + for(let i=0;i<4;i++){ + ctx.beginPath(); + ctx.strokeStyle = this.colors[i]; + ctx.lineWidth = this.thickness; + ctx.arc(canvas.width/2, this.y, this.radius, i*Math.PI/2 + this.rotation, (i+1)*Math.PI/2 + this.rotation); + ctx.stroke(); + } + } + + update() { + this.rotation += this.speed; + } +} + +function drawBall() { + ctx.beginPath(); + ctx.fillStyle = ball.color; + ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI*2); + ctx.fill(); +} + +function collisionCheck(ob) { + let dx = ball.x - canvas.width/2; + let dy = ball.y - ob.y; + let distance = Math.sqrt(dx*dx + dy*dy); + if(distance > ob.radius - ball.radius && distance < ob.radius + ball.radius){ + // Find which quarter the ball is in + let angle = Math.atan2(dy, dx) - ob.rotation; + if(angle < 0) angle += 2*Math.PI; + let quarter = Math.floor(angle / (Math.PI/2)); + if(ob.colors[quarter] !== ball.color){ + endGame(); + } + } +} + +function endGame() { + gameOver = true; + restartBtn.style.display = "block"; +} + +function gameLoop() { + if(gameOver) return; + ctx.clearRect(0,0,canvas.width,canvas.height); + + ball.dy += gravity; + ball.y += ball.dy; + + // Draw obstacles + obstacles.forEach(ob => { + ob.update(); + ob.draw(); + collisionCheck(ob); + }); + + // Spawn new obstacle + if(obstacles.length === 0 || obstacles[obstacles.length-1].y > 200){ + obstacles.push(new Obstacle(-80)); + } + + // Remove offscreen obstacles + obstacles = obstacles.filter(ob => ob.y < canvas.height + 100); + + // Move obstacles downward + obstacles.forEach(ob => ob.y += 2); + + // Draw ball + drawBall(); + + // Ball falls below + if(ball.y - ball.radius > canvas.height){ + endGame(); + } + + requestAnimationFrame(gameLoop); +} + +document.addEventListener("keydown", (e)=>{ + if(e.code === "Space" && !gameOver){ + ball.dy = jumpPower; + } +}); + +document.addEventListener("click", ()=>{ + if(!gameOver) ball.dy = jumpPower; +}); + +// Change ball color after passing an obstacle +function checkScore() { + obstacles.forEach(ob => { + if(!ob.passed && ball.y < ob.y){ + ob.passed = true; + score++; + ball.color = randomColor(); + scoreEl.textContent = score; + } + }); +} + +// Restart +restartBtn.addEventListener("click", ()=>{ + score = 0; + ball = { x: canvas.width/2, y: 500, radius: 15, color: "red", dy: 0 }; + obstacles = []; + gameOver = false; + restartBtn.style.display = "none"; + scoreEl.textContent = score; + gameLoop(); +}); + +// Update score in loop +function loop() { + if(!gameOver) checkScore(); + requestAnimationFrame(loop); +} + +// Start game +gameLoop(); +loop(); diff --git a/games/color-switch-challenge/style.css b/games/color-switch-challenge/style.css new file mode 100644 index 00000000..de5ac632 --- /dev/null +++ b/games/color-switch-challenge/style.css @@ -0,0 +1,61 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Arial', sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background: linear-gradient(135deg, #1a1a2e, #162447); +} + +.game-container { + position: relative; + width: 400px; + height: 600px; + border-radius: 20px; + overflow: hidden; + background-color: #0f0f1a; + box-shadow: 0 0 30px rgba(0,0,0,0.5); +} + +canvas { + display: block; + width: 100%; + height: 100%; +} + +.score-container { + position: absolute; + top: 10px; + left: 50%; + transform: translateX(-50%); + font-size: 24px; + font-weight: bold; + color: #fff; + z-index: 10; +} + +#restartBtn { + position: absolute; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + padding: 10px 20px; + font-size: 18px; + background-color: #ff4c4c; + color: #fff; + border: none; + border-radius: 10px; + cursor: pointer; + display: none; + z-index: 10; +} + +#restartBtn:hover { + background-color: #ff1f1f; +} diff --git a/games/color-switch-maze/index.html b/games/color-switch-maze/index.html new file mode 100644 index 00000000..5a445a82 --- /dev/null +++ b/games/color-switch-maze/index.html @@ -0,0 +1,91 @@ + + + + + + Color Switch Maze ยท Mini JS Games Hub + + + + + +
+
+
+ ๐ŸŸฃ +

Color Switch Maze

+
+
+ + + + + + +
+
+ +
+ + +
+ +
+
+ + +
+ + + + diff --git a/games/color-switch-maze/script.js b/games/color-switch-maze/script.js new file mode 100644 index 00000000..a776117e --- /dev/null +++ b/games/color-switch-maze/script.js @@ -0,0 +1,486 @@ +/* Color Switch Maze โ€” script.js + Designed for: games/color-switch-maze/index.html + Notes: + - Levels are defined in `levels` array. + - Node object: { id, x, y, type } types: 'red'|'green'|'blue'|'pad'|'exit'|'blocked' + - Edges: connect node ids. + - Player can move along edges only if destination is allowed by color rules. +*/ + +/* ---------- Configuration & assets ---------- */ +// Online sound assets (public) +const SOUNDS = { + move: "https://actions.google.com/sounds/v1/cartoon/wood_plank_flicks.ogg", + pad: "https://actions.google.com/sounds/v1/cartoon/clang_and_wobble.ogg", + win: "https://actions.google.com/sounds/v1/cartoon/clang.ogg", + error: "https://actions.google.com/sounds/v1/cartoon/descending_whistle.ogg", + bg: "https://actions.google.com/sounds/v1/ambiences/underwater_bubbles.ogg" +}; + +const COLORS = { + red: "#ff6b6b", + green: "#4ade80", + blue: "#7dd3fc", + pad: "#ffd166", + exit: "#a3e635", + blocked: "#333" +}; + +// Level metadata: nodes with normalized coordinates (0..1), edges between node ids +const levels = [ + // Level 1 โ€” Beginner + { + name: "Beginner", + nodes: [ + { id: "A", x: 0.12, y: 0.22, type: "red" }, + { id: "B", x: 0.36, y: 0.2, type: "pad", padColor: "green" }, + { id: "C", x: 0.6, y: 0.18, type: "red" }, + { id: "D", x: 0.82, y: 0.22, type: "exit" }, + + { id: "E", x: 0.18, y: 0.45, type: "red" }, + { id: "F", x: 0.36, y: 0.45, type: "green" }, + { id: "G", x: 0.54, y: 0.45, type: "blue" }, + { id: "H", x: 0.72, y: 0.45, type: "pad", padColor: "blue" }, + + { id: "I", x: 0.12, y: 0.72, type: "pad", padColor: "red" }, + { id: "J", x: 0.36, y: 0.72, type: "red" }, + { id: "K", x: 0.6, y: 0.72, type: "green" }, + { id: "L", x: 0.82, y: 0.72, type: "blocked" }, + ], + edges: [ + ["A","B"],["B","C"],["C","D"], + ["A","E"],["B","F"],["C","G"],["D","H"], + ["E","F"],["F","G"],["G","H"], + ["E","I"],["F","J"],["G","K"],["H","L"], + ["I","J"],["J","K"],["K","L"] + ], + startNode: "A" + }, + + // Level 2 โ€” Advanced + { + name: "Advanced", + nodes: [ + { id:"A", x:0.08, y:0.12, type:"red"}, + { id:"B", x:0.28, y:0.14, type:"pad", padColor:"blue"}, + { id:"C", x:0.5, y:0.12, type:"blue"}, + { id:"D", x:0.72, y:0.12, type:"green"}, + { id:"E", x:0.9, y:0.12, type:"exit"}, + + { id:"F", x:0.18, y:0.32, type:"red"}, + { id:"G", x:0.42, y:0.34, type:"blocked"}, + { id:"H", x:0.6, y:0.32, type:"pad", padColor:"green"}, + { id:"I", x:0.78, y:0.32, type:"blue"}, + + { id:"J", x:0.06, y:0.56, type:"pad", padColor:"green"}, + { id:"K", x:0.28, y:0.54, type:"green"}, + { id:"L", x:0.5, y:0.56, type:"red"}, + { id:"M", x:0.72, y:0.56, type:"blue"}, + { id:"N", x:0.92, y:0.56, type:"pad", padColor:"red"}, + + { id:"O", x:0.18, y:0.8, type:"blocked"}, + { id:"P", x:0.44, y:0.78, type:"blue"}, + { id:"Q", x:0.66, y:0.78, type:"green"}, + ], + edges: [ + ["A","B"],["B","C"],["C","D"],["D","E"], + ["A","F"],["B","G"],["C","H"],["D","I"],["E","N"], + ["F","G"],["G","H"],["H","I"], + ["F","J"],["G","K"],["H","L"],["I","M"], + ["J","K"],["K","L"],["L","M"],["M","N"], + ["J","O"],["K","P"],["L","Q"],["P","Q"] + ], + startNode: "A" + } +]; + +/* ---------- Canvas setup ---------- */ +const canvas = document.getElementById("game-canvas"); +const ctx = canvas.getContext("2d"); +let cw = canvas.width; +let ch = canvas.height; + +function resizeCanvas() { + // preserve aspect ratio and fit container + const wrap = canvas.parentElement; + const rect = wrap.getBoundingClientRect(); + const ratio = cw / ch; + let w = Math.max(320, Math.min(rect.width - 20, 1200)); + let h = Math.max(240, Math.min(rect.height - 20, 800)); + canvas.width = Math.floor(w); + canvas.height = Math.floor(h); +} +window.addEventListener("resize", () => { resizeCanvas(); draw(); }); +resizeCanvas(); + +/* ---------- Game state ---------- */ +let currentLevelIndex = 0; +let level = null; +let nodesMap = new Map(); +let adjacency = new Map(); +let player = { nodeId: null, color: null, x:0, y:0, moves:0 }; +let running = false; +let timer = 0, timerInterval = null; +let soundsMuted = false; +const audioCache = {}; + +/* ---------- DOM refs ---------- */ +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const restartBtn = document.getElementById("restart-btn"); +const nextBtn = document.getElementById("next-btn"); +const levelSel = document.getElementById("level-select"); +const playerColorEl = document.getElementById("player-color"); +const movesEl = document.getElementById("moves"); +const timeEl = document.getElementById("time"); +const levelNameEl = document.getElementById("level-name"); +const statusEl = document.getElementById("status"); +const muteChk = document.getElementById("mute-sounds"); + +const dirUp = document.getElementById("up"); +const dirLeft = document.getElementById("left"); +const dirRight = document.getElementById("right"); +const dirDown = document.getElementById("down"); + +/* ---------- Utilities ---------- */ +function loadSound(url){ + if(audioCache[url]) return audioCache[url]; + try { + const a = new Audio(url); + a.preload = "auto"; + audioCache[url] = a; + return a; + } catch(e){ return null; } +} +function playSound(name){ + if(soundsMuted) return; + const url = SOUNDS[name]; + if(!url) return; + const a = loadSound(url); + try { a.currentTime = 0; a.play(); } catch(e) {} +} + +/* ---------- Level loader ---------- */ +function setupLevel(index){ + currentLevelIndex = index; + level = JSON.parse(JSON.stringify(levels[index])); // deep clone to allow modification + nodesMap.clear(); + adjacency.clear(); + level.nodes.forEach(n => nodesMap.set(n.id, n)); + level.edges.forEach(([a,b]) => { + if(!adjacency.has(a)) adjacency.set(a, new Set()); + if(!adjacency.has(b)) adjacency.set(b, new Set()); + adjacency.get(a).add(b); + adjacency.get(b).add(a); + }); + // init player at start + player.nodeId = level.startNode; + const startNode = nodesMap.get(player.nodeId); + player.x = startNode.x; player.y = startNode.y; + // if start is pad -> set initial color of pad's color, else set to node type if color node + if(startNode.type === "pad") player.color = startNode.padColor || "red"; + else if(["red","green","blue"].includes(startNode.type)) player.color = startNode.type; + else player.color = "red"; + player.moves = 0; + timer = 0; + updateUI(); + draw(); +} + +/* ---------- Game control ---------- */ +function startGame(){ + if(running) return; + running = true; + statusEl.textContent = "Playing"; + playSound("bg"); + if(timerInterval) clearInterval(timerInterval); + timerInterval = setInterval(()=>{ timer++; updateTime(); }, 1000); +} +function pauseGame(){ + if(!running) return; + running = false; + statusEl.textContent = "Paused"; + if(timerInterval) clearInterval(timerInterval); +} +function restartGame(){ + pauseGame(); + setupLevel(currentLevelIndex); + statusEl.textContent = "Ready"; + draw(); +} +function nextLevel(){ + const next = (currentLevelIndex + 1) % levels.length; + levelSel.selectedIndex = next; + loadSelectedLevel(next); + startGame(); +} +function loadSelectedLevel(index){ + setupLevel(index); + levelNameEl.textContent = level.name; + statusEl.textContent = "Ready"; + draw(); +} + +/* ---------- Movement rules ---------- */ +function canMoveTo(nodeId){ + const n = nodesMap.get(nodeId); + if(!n) return false; + if(n.type === "blocked") return false; + if(n.type === "pad" || n.type === "exit") return true; + // color nodes: require same color + if(["red","green","blue"].includes(n.type)){ + return (n.type === player.color); + } + return false; +} + +function attemptMove(toNodeId){ + if(!running) return; + if(!adjacency.get(player.nodeId).has(toNodeId)) return; + if(!canMoveTo(toNodeId)){ + // invalid move - beep and reset optionally + playSound("error"); + flashStatus("Mismatch! Can't step on that color.", 1200); + return; + } + // progress move + player.nodeId = toNodeId; + player.moves += 1; + movesEl.textContent = player.moves; + playSound("move"); + // handle landing on pad + const landed = nodesMap.get(toNodeId); + if(landed.type === "pad"){ + player.color = landed.padColor || player.color; + playSound("pad"); + } + // if color node and matches, no change + if(landed.type === "exit"){ + playSound("win"); + flashStatus("Level Complete! ๐ŸŽ‰", 3000); + running = false; + if(timerInterval) clearInterval(timerInterval); + } + updateUI(); + draw(); +} + +/* ---------- Rendering ---------- */ +function worldToPixel(nx, ny){ + const pad = 60; + const w = canvas.width - pad*2; + const h = canvas.height - pad*2; + return { x: pad + nx * w, y: pad + ny * h }; +} + +function draw(){ + if(!level) return; + ctx.clearRect(0,0,canvas.width,canvas.height); + // background gradient + const g = ctx.createLinearGradient(0,0,canvas.width,canvas.height); + g.addColorStop(0,"rgba(10,16,28,0.95)"); + g.addColorStop(1,"rgba(6,12,20,0.95)"); + ctx.fillStyle = g; + ctx.fillRect(0,0,canvas.width,canvas.height); + + // draw edges + ctx.lineWidth = 4; + ctx.lineCap = "round"; + level.edges.forEach(([a,b]) => { + const na = nodesMap.get(a), nb = nodesMap.get(b); + const pa = worldToPixel(na.x, na.y), pb = worldToPixel(nb.x, nb.y); + ctx.strokeStyle = "rgba(255,255,255,0.04)"; + ctx.beginPath(); + ctx.moveTo(pa.x, pa.y); + ctx.lineTo(pb.x, pb.y); + ctx.stroke(); + }); + + // draw nodes + for(const n of level.nodes){ + const p = worldToPixel(n.x, n.y); + const isPlayer = (player.nodeId === n.id); + // outer glow + ctx.beginPath(); + ctx.arc(p.x, p.y, 22, 0, Math.PI*2); + const baseColor = (n.type === "pad") ? COLORS.pad : (n.type === "exit" ? COLORS.exit : (COLORS[n.type] || "#222")); + // shadow glow + ctx.fillStyle = baseColor; + ctx.save(); + if(n.type !== "blocked"){ + ctx.shadowColor = baseColor; + ctx.shadowBlur = isPlayer ? 28 : 18; + } else { + ctx.shadowBlur = 2; + } + ctx.fill(); + ctx.restore(); + + // inner circle + ctx.beginPath(); + ctx.fillStyle = "#0b1220"; + ctx.arc(p.x, p.y, 14, 0, Math.PI*2); + ctx.fill(); + + // inner icon / color marker + ctx.beginPath(); + if(n.type === "pad"){ + // small circle showing padColor + const padCol = COLORS[n.padColor] || "#fff"; + ctx.fillStyle = padCol; + ctx.arc(p.x, p.y, 8, 0, Math.PI*2); + ctx.fill(); + } else if (n.type === "exit"){ + ctx.fillStyle = "#fff"; + ctx.font = "12px sans-serif"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText("โฉ", p.x, p.y+1); + } else if (n.type === "blocked"){ + ctx.fillStyle = "#444"; + ctx.beginPath(); + ctx.moveTo(p.x-6,p.y-6); ctx.lineTo(p.x+6,p.y+6); + ctx.moveTo(p.x+6,p.y-6); ctx.lineTo(p.x-6,p.y+6); + ctx.strokeStyle = "#222"; + ctx.lineWidth = 3; + ctx.stroke(); + } else { + // colored dot marker for color nodes for accessibility + const col = COLORS[n.type] || "#fff"; + ctx.fillStyle = col; + ctx.arc(p.x, p.y, 7, 0, Math.PI*2); + ctx.fill(); + } + + // highlight player node + if(isPlayer){ + // draw player as a glowing orb slightly above + ctx.beginPath(); + ctx.arc(p.x, p.y, 12, 0, Math.PI*2); + const pc = COLORS[player.color] || "#fff"; + ctx.fillStyle = pc; + ctx.save(); + ctx.globalAlpha = 0.95; + ctx.shadowBlur = 28; ctx.shadowColor = pc; + ctx.fill(); + ctx.restore(); + // small center + ctx.beginPath(); ctx.fillStyle = "#041014"; ctx.arc(p.x, p.y, 5,0,Math.PI*2); ctx.fill(); + } + // node id small label + ctx.fillStyle = "rgba(255,255,255,0.06)"; + ctx.font = "10px sans-serif"; + ctx.textAlign = "center"; + ctx.fillText(n.id, p.x, p.y + 26); + } +} + +/* ---------- Input handling ---------- */ +function moveInDirection(dx, dy){ + // find neighbor in roughly that direction + const cur = nodesMap.get(player.nodeId); + let best = null, bestDot = -Infinity; + const origin = { x: cur.x, y: cur.y }; + for(const nbId of adjacency.get(player.nodeId) || []){ + const nb = nodesMap.get(nbId); + if(!nb) continue; + const vx = nb.x - origin.x, vy = nb.y - origin.y; + const len = Math.hypot(vx, vy); + if(len === 0) continue; + const dot = (vx*dx + vy*dy) / len; // alignment + if(dot > bestDot){ bestDot = dot; best = nbId; } + } + if(best && bestDot > 0.2){ + attemptMove(best); + } else { + playSound("error"); + flashStatus("No connected node that direction", 900); + } +} + +window.addEventListener("keydown", (e)=>{ + if(!running) return; + const key = e.key.toLowerCase(); + if(["arrowup","w"].includes(key) || key === "w"){ moveInDirection(0,-1); } + if(["arrowdown","s"].includes(key) || key === "s"){ moveInDirection(0,1); } + if(["arrowleft","a"].includes(key) || key === "a"){ moveInDirection(-1,0); } + if(["arrowright","d"].includes(key) || key === "d"){ moveInDirection(1,0); } +}); + +dirUp.addEventListener("click", ()=> moveInDirection(0,-1)); +dirDown.addEventListener("click", ()=> moveInDirection(0,1)); +dirLeft.addEventListener("click", ()=> moveInDirection(-1,0)); +dirRight.addEventListener("click", ()=> moveInDirection(1,0)); + +/* ---------- UI updates ---------- */ +function updateUI(){ + // update player color pill + playerColorEl.textContent = player.color.toUpperCase(); + playerColorEl.className = "color-pill " + player.color; + movesEl.textContent = player.moves; + updateTime(); + levelNameEl.textContent = level ? level.name : "-"; + // if currently standing on pad, highlight + const curNode = nodesMap.get(player.nodeId); + if(curNode && curNode.type === "pad"){ statusEl.textContent = "On Color Pad"; } +} + +function updateTime(){ + const mm = String(Math.floor(timer/60)).padStart(2,"0"); + const ss = String(timer % 60).padStart(2,"0"); + timeEl.textContent = `${mm}:${ss}`; +} + +function flashStatus(msg, ms=1200){ + const prev = statusEl.textContent; + statusEl.textContent = msg; + setTimeout(()=>{ statusEl.textContent = prev; }, ms); +} + +/* ---------- Buttons ---------- */ +startBtn.addEventListener("click", ()=>{ startGame(); }); +pauseBtn.addEventListener("click", ()=>{ pauseGame(); }); +restartBtn.addEventListener("click", ()=>{ restartGame(); }); +nextBtn.addEventListener("click", ()=>{ nextLevel(); }); +levelSel.addEventListener("change", (e)=>{ loadSelectedLevel(Number(e.target.value)); }); +muteChk.addEventListener("change", (e)=>{ soundsMuted = e.target.checked; }); + +/* ---------- Init ---------- */ +function init(){ + // load first level + setupLevel(0); + // Draw initial frame + draw(); + // attach canvas click to move to nearest connected node if allowed (tap) + canvas.addEventListener("click", (ev)=>{ + const rect = canvas.getBoundingClientRect(); + const cx = (ev.clientX - rect.left); + const cy = (ev.clientY - rect.top); + // find nearest node + let nearest = null, nd = Infinity; + for(const n of level.nodes){ + const p = worldToPixel(n.x,n.y); + const d = Math.hypot(p.x-cx,p.y-cy); + if(d < nd){ nd = d; nearest = n; } + } + if(!nearest) return; + // ensure it's adjacent + if(adjacency.get(player.nodeId).has(nearest.id)){ + attemptMove(nearest.id); + } else { + flashStatus("Not directly connected",800); + playSound("error"); + } + }); + + // make canvas responsive + resizeCanvas(); + // update UI elements + levelSel.value = currentLevelIndex; + levelNameEl.textContent = level.name; + // preload sounds + Object.values(SOUNDS).forEach(loadSound); +} + +// start +init(); diff --git a/games/color-switch-maze/style.css b/games/color-switch-maze/style.css new file mode 100644 index 00000000..76157ebf --- /dev/null +++ b/games/color-switch-maze/style.css @@ -0,0 +1,59 @@ +/* Color Switch Maze โ€” styles */ +:root{ + --bg:#0f1724; + --panel:#0b1220; + --accent: #7c3aed; + --glass: rgba(255,255,255,0.04); + --card: #0e1722; + --text:#e6eef8; + --muted:#9aa7bf; + --red:#ff6b6b; + --green:#4ade80; + --blue:#7dd3fc; + --pad:#ffd166; + --exit:#a3e635; +} + +*{box-sizing:border-box} +html,body{height:100%;margin:0;font-family:Inter,Segoe UI,Roboto,system-ui,-apple-system,"Helvetica Neue",Arial;} +body{background:linear-gradient(180deg,#031027 0%, #081425 100%);color:var(--text);-webkit-font-smoothing:antialiased} +.app{display:flex;flex-direction:column;min-height:100vh} +.topbar{display:flex;justify-content:space-between;align-items:center;padding:14px 20px;background:linear-gradient(90deg,rgba(255,255,255,0.02),transparent);backdrop-filter:blur(6px)} +.title{display:flex;gap:12px;align-items:center} +.title .icon{font-size:28px} +.title h1{margin:0;font-size:18px} +.controls{display:flex;gap:10px;align-items:center} +.btn{background:linear-gradient(180deg,var(--accent),#5b21b6);border:none;color:white;padding:8px 12px;border-radius:8px;cursor:pointer;font-weight:600;box-shadow:0 6px 18px rgba(124,58,237,0.16)} +.btn.ghost{background:transparent;border:1px solid rgba(255,255,255,0.06);color:var(--text);box-shadow:none} +select{background:var(--glass);border:1px solid rgba(255,255,255,0.04);padding:8px;border-radius:8px;color:var(--text)} +.sound-toggle{display:flex;gap:6px;align-items:center;color:var(--muted)} +.game-area{display:flex;gap:18px;padding:20px;flex:1;align-items:stretch} +.hud{width:260px;background:linear-gradient(180deg, rgba(255,255,255,0.02), transparent);border-radius:14px;padding:18px;display:flex;flex-direction:column;gap:12px} +.stat{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px dashed rgba(255,255,255,0.02)} +.color-pill{min-width:80px;text-align:center;padding:6px;border-radius:8px;color:#062021;font-weight:700} +.color-pill.red{background:var(--red)} +.color-pill.green{background:var(--green);color:#05220b} +.color-pill.blue{background:var(--blue);color:#052232} +.stat .muted{color:var(--muted)} +.mobile-controls{margin-top:8px;display:flex;flex-direction:column;gap:8px;align-items:center} +.mobile-controls .dir{padding:8px 12px;border-radius:8px;border:none;background:rgba(255,255,255,0.03);color:var(--text);font-weight:700} +.legend{margin-top:8px;display:flex;flex-direction:column;gap:6px;color:var(--muted);font-size:13px} +.swatch{display:inline-block;width:18px;height:14px;border-radius:4px;margin-right:8px;vertical-align:middle;box-shadow:0 6px 20px rgba(2,6,23,0.6)} +.swatch.red{background:var(--red)} +.swatch.green{background:var(--green)} +.swatch.blue{background:var(--blue)} +.swatch.pad{background:var(--pad)} +.swatch.exit{background:var(--exit)} + +.canvas-wrap{flex:1;display:flex;justify-content:center;align-items:center} +canvas{background:linear-gradient(180deg, rgba(255,255,255,0.01), rgba(255,255,255,0.02));border-radius:14px;box-shadow:0 12px 40px rgba(2,6,23,0.7), inset 0 1px 0 rgba(255,255,255,0.02)} +.foot{display:flex;justify-content:space-between;align-items:center;padding:14px 20px;color:var(--muted)} +.notes{font-size:13px} +.ghost{background:transparent;border:1px solid rgba(255,255,255,0.04);padding:8px;border-radius:8px} + +@media (max-width:900px){ + .game-area{flex-direction:column} + .hud{width:100%;order:2} + .canvas-wrap{order:1} + .controls{flex-wrap:wrap;gap:6px} +} diff --git a/games/color-switch/index.html b/games/color-switch/index.html new file mode 100644 index 00000000..b5fe9493 --- /dev/null +++ b/games/color-switch/index.html @@ -0,0 +1,78 @@ + + + + + + Color Switch + + + +
+
+

๐ŸŽจ Color Switch

+
+
Score: 0
+
High Score: 0
+
+
+ +
+
+ + +
+
+

Tap to Start!

+

Switch colors to match the platforms

+

Tap anywhere to change color

+
+ + +
+
+ + +
+ +
+

Master color coordination and timing in this addictive platformer!

+
+
+ + + + \ No newline at end of file diff --git a/games/color-switch/script.js b/games/color-switch/script.js new file mode 100644 index 00000000..71f28f39 --- /dev/null +++ b/games/color-switch/script.js @@ -0,0 +1,257 @@ +// Color Switch Game +// A fast-paced color matching platformer + +class ColorSwitchGame { + constructor() { + this.canvas = document.getElementById('game-canvas'); + this.ctx = this.canvas.getContext('2d'); + this.scoreElement = document.getElementById('score'); + this.highScoreElement = document.getElementById('high-score'); + this.finalScoreElement = document.getElementById('final-score'); + this.startScreen = document.getElementById('start-screen'); + this.gameOverScreen = document.getElementById('game-over'); + this.restartBtn = document.getElementById('restart-btn'); + + this.colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24']; + this.ball = { x: this.canvas.width / 2, y: 50, radius: 12, colorIndex: 0, vy: 0 }; + this.platforms = []; + this.score = 0; + this.highScore = localStorage.getItem('colorSwitchHighScore') || 0; + this.gameRunning = false; + this.gameOver = false; + this.difficulty = 1; + + this.setupEventListeners(); + this.showStartScreen(); + this.updateHighScoreDisplay(); + } + + setupEventListeners() { + this.canvas.addEventListener('click', () => this.handleClick()); + this.canvas.addEventListener('touchstart', (e) => { + e.preventDefault(); + this.handleClick(); + }); + this.restartBtn.addEventListener('click', () => this.restartGame()); + } + + handleClick() { + if (!this.gameRunning && !this.gameOver) { + this.startGame(); + } else if (this.gameRunning) { + this.switchBallColor(); + } else if (this.gameOver) { + this.restartGame(); + } + } + + startGame() { + this.gameRunning = true; + this.gameOver = false; + this.score = 0; + this.difficulty = 1; + this.ball.y = 50; + this.ball.vy = 0; + this.ball.colorIndex = 0; + this.platforms = []; + this.generatePlatforms(); + this.hideStartScreen(); + this.hideGameOverScreen(); + this.gameLoop(); + } + + switchBallColor() { + this.ball.colorIndex = (this.ball.colorIndex + 1) % this.colors.length; + } + + generatePlatforms() { + this.platforms = []; + for (let i = 0; i < 10; i++) { + const platform = { + x: this.canvas.width / 2, + y: 150 + i * 120, + radius: 60, + rotation: 0, + rotationSpeed: 0.02 + (this.difficulty * 0.005), + colors: this.shuffleArray([...this.colors]) + }; + this.platforms.push(platform); + } + } + + shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + } + + update() { + if (!this.gameRunning) return; + + // Update ball physics + this.ball.vy += 0.3; // gravity + this.ball.y += this.ball.vy; + + // Update platforms + this.platforms.forEach(platform => { + platform.rotation += platform.rotationSpeed; + }); + + // Check platform collisions + this.platforms.forEach(platform => { + const dx = this.ball.x - platform.x; + const dy = this.ball.y - platform.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < platform.radius + this.ball.radius) { + // Check if ball color matches platform color at collision point + const angle = Math.atan2(dy, dx) - platform.rotation; + const colorIndex = Math.floor((angle + Math.PI) / (Math.PI * 2) * platform.colors.length) % platform.colors.length; + const platformColor = platform.colors[colorIndex]; + + if (platformColor !== this.colors[this.ball.colorIndex]) { + this.endGame(); + return; + } + + // Bounce off platform + if (this.ball.vy > 0) { + this.ball.vy = -8 - (this.difficulty * 0.5); + this.score += 10; + this.updateScoreDisplay(); + + // Increase difficulty + this.difficulty += 0.1; + platform.rotationSpeed = 0.02 + (this.difficulty * 0.005); + } + } + }); + + // Check if ball fell off screen + if (this.ball.y > this.canvas.height + 50) { + this.endGame(); + } + + // Add new platforms as ball progresses + const lastPlatform = this.platforms[this.platforms.length - 1]; + if (lastPlatform && this.ball.y > lastPlatform.y - 200) { + const newPlatform = { + x: this.canvas.width / 2, + y: lastPlatform.y + 120, + radius: 60, + rotation: 0, + rotationSpeed: 0.02 + (this.difficulty * 0.005), + colors: this.shuffleArray([...this.colors]) + }; + this.platforms.push(newPlatform); + } + + // Remove off-screen platforms + this.platforms = this.platforms.filter(platform => platform.y > this.ball.y - 200); + } + + draw() { + // Clear canvas + this.ctx.fillStyle = 'rgba(44, 62, 80, 0.1)'; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + // Draw platforms + this.platforms.forEach(platform => { + this.ctx.save(); + this.ctx.translate(platform.x, platform.y); + this.ctx.rotate(platform.rotation); + + const angleStep = (Math.PI * 2) / platform.colors.length; + for (let i = 0; i < platform.colors.length; i++) { + this.ctx.beginPath(); + this.ctx.arc(0, 0, platform.radius, i * angleStep, (i + 1) * angleStep); + this.ctx.lineTo(0, 0); + this.ctx.fillStyle = platform.colors[i]; + this.ctx.fill(); + this.ctx.strokeStyle = '#fff'; + this.ctx.lineWidth = 2; + this.ctx.stroke(); + } + + this.ctx.restore(); + }); + + // Draw ball + this.ctx.beginPath(); + this.ctx.arc(this.ball.x, this.ball.y, this.ball.radius, 0, Math.PI * 2); + this.ctx.fillStyle = this.colors[this.ball.colorIndex]; + this.ctx.fill(); + this.ctx.strokeStyle = '#fff'; + this.ctx.lineWidth = 2; + this.ctx.stroke(); + + // Draw ball trail + this.ctx.beginPath(); + this.ctx.arc(this.ball.x, this.ball.y, this.ball.radius + 3, 0, Math.PI * 2); + this.ctx.strokeStyle = this.colors[this.ball.colorIndex]; + this.ctx.lineWidth = 1; + this.ctx.globalAlpha = 0.3; + this.ctx.stroke(); + this.ctx.globalAlpha = 1; + } + + gameLoop() { + if (this.gameRunning && !this.gameOver) { + this.update(); + this.draw(); + requestAnimationFrame(() => this.gameLoop()); + } + } + + endGame() { + this.gameRunning = false; + this.gameOver = true; + this.finalScoreElement.textContent = this.score; + + if (this.score > this.highScore) { + this.highScore = this.score; + localStorage.setItem('colorSwitchHighScore', this.highScore); + this.updateHighScoreDisplay(); + } + + this.showGameOverScreen(); + } + + restartGame() { + this.hideGameOverScreen(); + this.startGame(); + } + + updateScoreDisplay() { + this.scoreElement.textContent = this.score; + this.scoreElement.classList.add('score-flash'); + setTimeout(() => this.scoreElement.classList.remove('score-flash'), 300); + } + + updateHighScoreDisplay() { + this.highScoreElement.textContent = this.highScore; + } + + showStartScreen() { + this.startScreen.classList.remove('hidden'); + } + + hideStartScreen() { + this.startScreen.classList.add('hidden'); + } + + showGameOverScreen() { + this.gameOverScreen.classList.remove('hidden'); + } + + hideGameOverScreen() { + this.gameOverScreen.classList.add('hidden'); + } +} + +// Initialize the game when the page loads +document.addEventListener('DOMContentLoaded', () => { + new ColorSwitchGame(); +}); \ No newline at end of file diff --git a/games/color-switch/style.css b/games/color-switch/style.css new file mode 100644 index 00000000..71958a53 --- /dev/null +++ b/games/color-switch/style.css @@ -0,0 +1,254 @@ +body { + margin: 0; + padding: 0; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + font-family: 'Arial', sans-serif; + color: #fff; + overflow-x: hidden; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; +} + +.container { + max-width: 1000px; + width: 100%; + padding: 20px; + box-sizing: border-box; +} + +header { + text-align: center; + margin-bottom: 20px; +} + +h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #f9ca24); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.stats { + display: flex; + justify-content: center; + gap: 20px; + margin-bottom: 20px; +} + +.score, .high-score { + background: rgba(255, 255, 255, 0.2); + padding: 10px 20px; + border-radius: 25px; + font-weight: bold; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.3); +} + +main { + display: flex; + gap: 20px; + align-items: flex-start; + justify-content: center; +} + +.game-area { + position: relative; + flex: 1; + max-width: 400px; +} + +#game-canvas { + background: linear-gradient(180deg, #2c3e50 0%, #34495e 50%, #2c3e50 100%); + border-radius: 15px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + border: 2px solid rgba(255, 255, 255, 0.1); + display: block; + margin: 0 auto; + cursor: pointer; +} + +.controls { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; +} + +.start-screen, .game-over { + background: rgba(0, 0, 0, 0.8); + padding: 30px; + border-radius: 15px; + text-align: center; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + pointer-events: auto; +} + +.start-screen h2, .game-over h2 { + margin-bottom: 15px; + color: #fff; +} + +.start-screen p { + margin: 5px 0; + color: #ccc; +} + +#restart-btn { + background: linear-gradient(45deg, #ff6b6b, #4ecdc4); + border: none; + padding: 12px 24px; + border-radius: 25px; + color: white; + font-size: 16px; + font-weight: bold; + cursor: pointer; + margin-top: 15px; + transition: all 0.3s ease; +} + +#restart-btn:hover { + transform: scale(1.05); + box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4); +} + +.hidden { + display: none !important; +} + +.sidebar { + width: 250px; + background: rgba(255, 255, 255, 0.1); + border-radius: 15px; + padding: 20px; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.instructions h3, .color-legend h3 { + color: #fff; + margin-bottom: 15px; + text-align: center; +} + +.instructions ul { + padding-left: 20px; + color: #eee; +} + +.instructions li { + margin-bottom: 8px; + line-height: 1.4; +} + +.color-legend { + margin-top: 30px; +} + +.color-item { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.color-box { + width: 20px; + height: 20px; + border-radius: 50%; + margin-right: 10px; + border: 2px solid rgba(255, 255, 255, 0.3); +} + +.color-box.red { background: #ff6b6b; } +.color-box.blue { background: #4ecdc4; } +.color-box.green { background: #45b7d1; } +.color-box.yellow { background: #f9ca24; } + +footer { + text-align: center; + margin-top: 20px; + color: rgba(255, 255, 255, 0.7); + font-style: italic; +} + +/* Responsive design */ +@media (max-width: 768px) { + main { + flex-direction: column; + align-items: center; + } + + .sidebar { + width: 100%; + max-width: 400px; + } + + #game-canvas { + width: 100%; + max-width: 400px; + height: 500px; + } + + .stats { + flex-direction: column; + gap: 10px; + } + + h1 { + font-size: 2em; + } + + .container { + padding: 10px; + } +} + +/* Animations */ +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +@keyframes rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.platform-rotating { + animation: rotate 3s linear infinite; +} + +.ball-glow { + box-shadow: 0 0 20px currentColor; +} + +.score-flash { + animation: pulse 0.5s ease; +} + +/* Mobile touch improvements */ +@media (hover: none) and (pointer: coarse) { + #game-canvas { + height: 70vh; + max-height: 600px; + } + + .start-screen, .game-over { + padding: 20px; + } + + .start-screen h2, .game-over h2 { + font-size: 1.5em; + } +} \ No newline at end of file diff --git a/games/color_palate/index.html b/games/color_palate/index.html new file mode 100644 index 00000000..be4e42f1 --- /dev/null +++ b/games/color_palate/index.html @@ -0,0 +1,39 @@ + + + + + + Color Palette Guessing Game + + + + +
+

๐ŸŒˆ Color Palette Puzzle

+ +
+ Score: 0 / 0 +
+ +
+
+ +
+

Which color completes the palette?

+
+ +
+
+ +
+
+ +
+ + +
+
+ + + + \ No newline at end of file diff --git a/games/color_palate/script.js b/games/color_palate/script.js new file mode 100644 index 00000000..de11dacd --- /dev/null +++ b/games/color_palate/script.js @@ -0,0 +1,222 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. GAME DATA --- + // Palettes are arrays of Hex color codes. The game removes one randomly. + const colorPalettes = [ + ["#4F5D75", "#B0A8B9", "#D0B8C8", "#F6E4A8", "#E69A8D"], // Muted/Dusty + ["#FF6B6B", "#FFD93D", "#6BCB77", "#4895EF", "#70A1FF"], // Primary/Vibrant + ["#2C3E50", "#E74C3C", "#F39C12", "#16A085", "#3498DB"], // Flat UI Colors + ["#000000", "#555555", "#AAAAAA", "#CCCCCC", "#FFFFFF"], // Monochrome + ["#FFC0CB", "#FFD700", "#7CFC00", "#00FFFF", "#8A2BE2"] // Bright & Fun + ]; + + // --- 2. GAME STATE VARIABLES --- + let currentRounds = []; // Shuffled array of palettes + let currentRoundIndex = 0; + let score = 0; + let gameActive = false; + let correctAnswer = ''; + + // --- 3. DOM Elements --- + const paletteDisplay = document.getElementById('palette-display'); + const optionsContainer = document.getElementById('options-container'); + const feedbackMessage = document.getElementById('feedback-message'); + const scoreSpan = document.getElementById('score'); + const totalRoundsSpan = document.getElementById('total-rounds'); + const startButton = document.getElementById('start-button'); + const nextButton = document.getElementById('next-button'); + + // --- 4. UTILITY FUNCTIONS --- + + /** + * Shuffles an array in place (Fisher-Yates). + */ + function shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + } + + /** + * Finds a unique, random incorrect color that is not in the current palette. + * For simplicity, this uses a pool of *all* colors from all palettes. + */ + function getUniqueIncorrectColor(currentPalette) { + const allColors = colorPalettes.flat(); + let incorrectColor = ''; + + do { + const randomIndex = Math.floor(Math.random() * allColors.length); + incorrectColor = allColors[randomIndex]; + } while (currentPalette.includes(incorrectColor)); // Ensure it's not the answer or a displayed color + + return incorrectColor; + } + + // --- 5. CORE GAME FUNCTIONS --- + + /** + * Initializes the game setup. + */ + function startGame() { + gameActive = true; + shuffleArray(colorPalettes); + currentRounds = colorPalettes; + totalRoundsSpan.textContent = currentRounds.length; + + currentRoundIndex = 0; + score = 0; + scoreSpan.textContent = score; + + startButton.style.display = 'none'; + nextButton.style.display = 'none'; + loadRound(); + } + + /** + * Loads the next palette puzzle. + */ + function loadRound() { + if (currentRoundIndex >= currentRounds.length) { + endGame(); + return; + } + + const originalPalette = [...currentRounds[currentRoundIndex]]; // Use a copy + + // 1. Determine the missing color (the answer) + const missingIndex = Math.floor(Math.random() * originalPalette.length); + correctAnswer = originalPalette[missingIndex]; + + // 2. Create the displayed palette (with one missing slot) + const displayedPalette = originalPalette.slice(0, missingIndex) + .concat(['MISSING']) + .concat(originalPalette.slice(missingIndex + 1)); + + // 3. Generate Options (1 correct + 3 incorrect) + let options = [correctAnswer]; + while (options.length < 4) { + const incorrectColor = getUniqueIncorrectColor(originalPalette); + if (!options.includes(incorrectColor)) { + options.push(incorrectColor); + } + } + shuffleArray(options); // Shuffle the final options + + // 4. Update UI + renderPalette(displayedPalette); + renderOptions(options); + + feedbackMessage.textContent = 'Which color completes the scheme?'; + feedbackMessage.style.color = '#333'; + nextButton.style.display = 'none'; + } + + /** + * Renders the main palette display. + */ + function renderPalette(palette) { + paletteDisplay.innerHTML = ''; + palette.forEach(color => { + const swatch = document.createElement('div'); + swatch.classList.add('color-swatch'); + if (color === 'MISSING') { + swatch.classList.add('missing-swatch'); + swatch.setAttribute('data-color', 'MISSING'); + } else { + swatch.style.backgroundColor = color; + swatch.setAttribute('data-color', color); + } + paletteDisplay.appendChild(swatch); + }); + } + + /** + * Renders the clickable guess options. + */ + function renderOptions(options) { + optionsContainer.innerHTML = ''; + options.forEach(color => { + const swatch = document.createElement('div'); + swatch.classList.add('option-swatch'); + swatch.style.backgroundColor = color; + swatch.setAttribute('data-guess', color); + swatch.addEventListener('click', handleGuess); + optionsContainer.appendChild(swatch); + }); + } + + /** + * Handles a click on an option swatch. + */ + function handleGuess(event) { + if (!gameActive || event.target.classList.contains('disabled')) return; + + const guess = event.target.getAttribute('data-guess'); + + // 1. Disable all options immediately + document.querySelectorAll('.option-swatch').forEach(swatch => { + swatch.classList.add('disabled'); + swatch.removeEventListener('click', handleGuess); + }); + + // 2. Check and provide feedback + if (guess === correctAnswer) { + score++; + scoreSpan.textContent = score; + event.target.classList.add('correct-guess'); + feedbackMessage.textContent = 'โœจ CORRECT! You have a great eye for color.'; + feedbackMessage.style.color = '#4CAF50'; + } else { + event.target.classList.add('incorrect-guess'); + feedbackMessage.textContent = `โŒ INCORRECT. The missing color was: ${correctAnswer}`; + feedbackMessage.style.color = '#f44336'; + + // Highlight the correct answer + document.querySelectorAll('.option-swatch').forEach(swatch => { + if (swatch.getAttribute('data-guess') === correctAnswer) { + swatch.classList.add('correct-guess'); + } + }); + } + + // 3. Reveal the missing color in the main palette + const missingSwatch = document.querySelector('.missing-swatch'); + if (missingSwatch) { + missingSwatch.style.backgroundColor = correctAnswer; + missingSwatch.classList.remove('missing-swatch'); + missingSwatch.style.border = '3px solid #ff9800'; // Highlight the revealed color + } + + // 4. Prepare for next round + nextButton.style.display = 'block'; + } + + /** + * Ends the game and shows the final score. + */ + function endGame() { + gameActive = false; + paletteDisplay.innerHTML = '

Game Over!

'; + optionsContainer.innerHTML = ''; + feedbackMessage.textContent = `Final Score: ${score} / ${currentRounds.length}.`; + feedbackMessage.style.color = '#6a0572'; + nextButton.style.display = 'none'; + + startButton.textContent = 'PLAY AGAIN'; + startButton.style.display = 'block'; + } + + // --- 6. EVENT LISTENERS --- + + startButton.addEventListener('click', startGame); + nextButton.addEventListener('click', () => { + currentRoundIndex++; + loadRound(); + }); + + // Initial check to prevent errors + if(colorPalettes.length > 0) { + totalRoundsSpan.textContent = colorPalettes.length; + } +}); \ No newline at end of file diff --git a/games/color_palate/style.css b/games/color_palate/style.css new file mode 100644 index 00000000..aa372723 --- /dev/null +++ b/games/color_palate/style.css @@ -0,0 +1,132 @@ +body { + font-family: 'Poppins', sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f7f9fc; + color: #333; +} + +#game-container { + background-color: white; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 600px; + width: 90%; +} + +h1 { + color: #4a4e69; + margin-bottom: 20px; +} + +#status-area { + font-size: 1.1em; + font-weight: 600; + margin-bottom: 25px; + color: #6a0572; +} + +/* --- Palette Display (The Question) --- */ +#palette-display { + display: flex; + justify-content: center; + gap: 10px; + margin-bottom: 30px; +} + +.color-swatch { + width: 80px; + height: 80px; + border-radius: 8px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + transition: transform 0.2s; + border: 3px solid transparent; /* Base border */ +} + +.missing-swatch { + border: 3px dashed #ff6b6b; /* Red dashed border for the empty slot */ + background-color: #eee; +} + +#instructions { + margin-bottom: 20px; + font-style: italic; + color: #555; +} + +/* --- Options Container (The Answers) --- */ +#options-container { + display: flex; + justify-content: center; + gap: 15px; + margin-bottom: 25px; +} + +.option-swatch { + width: 60px; + height: 60px; + border-radius: 6px; + cursor: pointer; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + transition: box-shadow 0.2s, transform 0.1s; +} + +.option-swatch:hover:not(.disabled) { + transform: translateY(-3px); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); +} + +.correct-guess { + border: 4px solid #4CAF50 !important; /* Green */ +} + +.incorrect-guess { + border: 4px solid #f44336 !important; /* Red */ + opacity: 0.5; +} + +.disabled { + cursor: not-allowed; + opacity: 0.7; +} + +/* --- Feedback and Controls --- */ +#feedback-message { + min-height: 1.5em; + font-weight: bold; + font-size: 1.1em; + margin-bottom: 20px; +} + +#controls button { + padding: 10px 20px; + font-size: 1em; + font-weight: bold; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; +} + +#start-button { + background-color: #6a0572; /* Purple */ + color: white; +} + +#start-button:hover { + background-color: #4f0354; +} + +#next-button { + background-color: #007bff; /* Blue */ + color: white; +} + +#next-button:hover { + background-color: #0056b3; +} \ No newline at end of file diff --git a/games/colorguess/index.html b/games/colorguess/index.html new file mode 100644 index 00000000..ec57668d --- /dev/null +++ b/games/colorguess/index.html @@ -0,0 +1,323 @@ + + + + + + Color Sequence Guessing Game + + + + + + + +
+

Master Color

+ + +
+ +
+ + + + + +
+

Your Guess (Moves: 0/10)

+ +
+ +
+ + +
+
+
+
+
+
+
+
+ +
+ + +
+
+ + +

+ +
+ + + + diff --git a/games/colour-wheel/index.html b/games/colour-wheel/index.html new file mode 100644 index 00000000..8526f808 --- /dev/null +++ b/games/colour-wheel/index.html @@ -0,0 +1,23 @@ + + + + + + Colour Wheel Game + + + +
+

Colour Wheel

+

Spin the wheel and match the color that it lands on!

+
+
Time: 30
+
Score: 0
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/games/colour-wheel/script.js b/games/colour-wheel/script.js new file mode 100644 index 00000000..4f723ed8 --- /dev/null +++ b/games/colour-wheel/script.js @@ -0,0 +1,221 @@ +// Colour Wheel Game Script +// Spin the wheel and match colors + +var canvas = document.getElementById('gameCanvas'); +var ctx = canvas.getContext('2d'); +var timerDisplay = document.getElementById('timer'); +var scoreDisplay = document.getElementById('score'); +var restartBtn = document.getElementById('restart'); +var messageDiv = document.getElementById('message'); + +var colors = ['Red', 'Blue', 'Green', 'Yellow', 'Purple', 'Orange']; +var colorValues = ['#f44336', '#2196f3', '#4caf50', '#ffeb3b', '#9c27b0', '#ff9800']; +var currentAngle = 0; +var spinning = false; +var targetColor = ''; +var options = []; +var score = 0; +var timeLeft = 30; +var timerInterval; +var gameRunning = true; +var phase = 'spin'; // 'spin' or 'match' + +// Initialize the game +function initGame() { + currentAngle = 0; + spinning = false; + score = 0; + timeLeft = 30; + gameRunning = true; + phase = 'spin'; + messageDiv.textContent = ''; + scoreDisplay.textContent = 'Score: ' + score; + startTimer(); + draw(); +} + +// Draw the wheel +function drawWheel() { + var centerX = canvas.width / 2; + var centerY = 200; + var radius = 100; + var angleStep = (Math.PI * 2) / colors.length; + + for (var i = 0; i < colors.length; i++) { + var startAngle = i * angleStep + currentAngle; + var endAngle = (i + 1) * angleStep + currentAngle; + + ctx.beginPath(); + ctx.moveTo(centerX, centerY); + ctx.arc(centerX, centerY, radius, startAngle, endAngle); + ctx.closePath(); + ctx.fillStyle = colorValues[i]; + ctx.fill(); + ctx.strokeStyle = '#000'; + ctx.stroke(); + + // Draw color name + var textAngle = startAngle + angleStep / 2; + var textX = centerX + Math.cos(textAngle) * (radius - 30); + var textY = centerY + Math.sin(textAngle) * (radius - 30); + ctx.fillStyle = '#000'; + ctx.font = '14px Arial'; + ctx.textAlign = 'center'; + ctx.fillText(colors[i], textX, textY); + } + + // Draw pointer + ctx.beginPath(); + ctx.moveTo(centerX, centerY - radius - 20); + ctx.lineTo(centerX - 10, centerY - radius); + ctx.lineTo(centerX + 10, centerY - radius); + ctx.closePath(); + ctx.fillStyle = '#000'; + ctx.fill(); +} + +// Draw options +function drawOptions() { + ctx.fillStyle = '#000'; + ctx.font = '20px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('Match the color: ' + targetColor, canvas.width / 2, 50); + + for (var i = 0; i < options.length; i++) { + var x = 150 + (i % 2) * 250; + var y = 150 + Math.floor(i / 2) * 100; + + ctx.fillStyle = colorValues[colors.indexOf(options[i])]; + ctx.fillRect(x - 50, y - 25, 100, 50); + ctx.strokeStyle = '#000'; + ctx.strokeRect(x - 50, y - 25, 100, 50); + + ctx.fillStyle = '#000'; + ctx.font = '16px Arial'; + ctx.fillText(options[i], x, y + 5); + } +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + if (phase === 'spin') { + drawWheel(); + if (!spinning) { + ctx.fillStyle = '#000'; + ctx.font = '18px Arial'; + ctx.fillText('Click the wheel to spin!', canvas.width / 2, 350); + } + } else if (phase === 'match') { + drawOptions(); + } +} + +// Spin the wheel +function spinWheel() { + if (spinning) return; + spinning = true; + var spinAngle = Math.PI * 2 * (5 + Math.random() * 5); // 5-10 full rotations + var duration = 2000; // 2 seconds + var startTime = Date.now(); + + function animate() { + var elapsed = Date.now() - startTime; + var progress = elapsed / duration; + if (progress < 1) { + currentAngle = spinAngle * (1 - Math.pow(1 - progress, 3)); // easing + draw(); + requestAnimationFrame(animate); + } else { + spinning = false; + // Determine landed color + var normalizedAngle = currentAngle % (Math.PI * 2); + var segmentAngle = (Math.PI * 2) / colors.length; + var landedIndex = Math.floor((Math.PI * 2 - normalizedAngle) / segmentAngle) % colors.length; + targetColor = colors[landedIndex]; + generateOptions(); + phase = 'match'; + draw(); + } + } + animate(); +} + +// Generate options +function generateOptions() { + options = [targetColor]; + var availableColors = colors.filter(c => c !== targetColor); + while (options.length < 4) { + var randomColor = availableColors[Math.floor(Math.random() * availableColors.length)]; + if (!options.includes(randomColor)) { + options.push(randomColor); + } + } + // Shuffle options + for (var i = options.length - 1; i > 0; i--) { + var j = Math.floor(Math.random() * (i + 1)); + var temp = options[i]; + options[i] = options[j]; + options[j] = temp; + } +} + +// Handle canvas click +canvas.addEventListener('click', function(event) { + if (!gameRunning) return; + var rect = canvas.getBoundingClientRect(); + var x = event.clientX - rect.left; + var y = event.clientY - rect.top; + + if (phase === 'spin') { + var centerX = canvas.width / 2; + var centerY = 200; + var distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2); + if (distance <= 100) { + spinWheel(); + } + } else if (phase === 'match') { + for (var i = 0; i < options.length; i++) { + var optX = 150 + (i % 2) * 250; + var optY = 150 + Math.floor(i / 2) * 100; + if (x >= optX - 50 && x <= optX + 50 && y >= optY - 25 && y <= optY + 25) { + if (options[i] === targetColor) { + score++; + scoreDisplay.textContent = 'Score: ' + score; + messageDiv.textContent = 'Correct!'; + messageDiv.style.color = 'green'; + } else { + messageDiv.textContent = 'Wrong!'; + messageDiv.style.color = 'red'; + } + setTimeout(function() { + phase = 'spin'; + draw(); + }, 1000); + break; + } + } + } +}); + +// Start the timer +function startTimer() { + clearInterval(timerInterval); + timerInterval = setInterval(function() { + timeLeft--; + timerDisplay.textContent = 'Time: ' + timeLeft; + if (timeLeft <= 0) { + clearInterval(timerInterval); + gameRunning = false; + messageDiv.textContent = 'Time\'s up! Final Score: ' + score; + messageDiv.style.color = 'blue'; + } + }, 1000); +} + +// Restart button +restartBtn.addEventListener('click', initGame); + +// Start the game +initGame(); \ No newline at end of file diff --git a/games/colour-wheel/style.css b/games/colour-wheel/style.css new file mode 100644 index 00000000..fe65b30f --- /dev/null +++ b/games/colour-wheel/style.css @@ -0,0 +1,53 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #e8f5e8; +} + +.container { + text-align: center; +} + +h1 { + color: #2e7d32; +} + +.game-info { + margin-bottom: 20px; + display: flex; + justify-content: center; + gap: 20px; +} + +#timer, #score { + font-size: 24px; + font-weight: bold; +} + +#restart { + padding: 10px 20px; + font-size: 16px; + background-color: #4caf50; + color: white; + border: none; + cursor: pointer; +} + +#restart:hover { + background-color: #388e3c; +} + +canvas { + border: 2px solid #2e7d32; + background-color: #ffffff; +} + +#message { + margin-top: 20px; + font-size: 18px; + font-weight: bold; +} \ No newline at end of file diff --git a/games/concentration_game/index.html b/games/concentration_game/index.html new file mode 100644 index 00000000..f541bb9c --- /dev/null +++ b/games/concentration_game/index.html @@ -0,0 +1,19 @@ + + + + + Memory Match Game + + + + +
+ Moves: 0 + +
+ +
+ + + + \ No newline at end of file diff --git a/games/concentration_game/script.js b/games/concentration_game/script.js new file mode 100644 index 00000000..07f51312 --- /dev/null +++ b/games/concentration_game/script.js @@ -0,0 +1,132 @@ +// game.js + +const gameBoard = document.querySelector('.memory-game'); +const movesDisplay = document.querySelector('.score'); +const resetButton = document.querySelector('.reset-button'); + +// --- Game State Variables --- +let hasFlippedCard = false; +let lockBoard = false; // Flag to prevent rapid clicks during animation/delay +let firstCard, secondCard; +let totalMoves = 0; +let matchesFound = 0; + +// The card images and their duplicates +const cardImages = [ + 'star', 'planet', 'rocket', 'comet', 'satellite', 'ufo', + 'star', 'planet', 'rocket', 'comet', 'satellite', 'ufo' +]; +// Total cards: 12. Total pairs: 6. + +// --- 1. Game Setup and Card Creation --- + +function createBoard() { + // Shuffle the cards before creating the DOM elements + const shuffledCards = shuffleArray(cardImages); + + shuffledCards.forEach(imageName => { + const card = document.createElement('div'); + card.classList.add('memory-card'); + card.setAttribute('data-framework', imageName); // Store the identity + + card.innerHTML = ` +
${imageName}
+
+ `; + + card.addEventListener('click', flipCard); + gameBoard.appendChild(card); + }); +} + +function shuffleArray(array) { + // Fisher-Yates shuffle algorithm + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +} + +// --- 2. Event Handling: The Flip Function --- + +function flipCard() { + if (lockBoard) return; // Ignore click if board is locked + if (this === firstCard) return; // Ignore double-click on the same card + + this.classList.add('is-flipped'); + + if (!hasFlippedCard) { + // First card flipped + hasFlippedCard = true; + firstCard = this; + return; + } + + // Second card flipped + secondCard = this; + totalMoves++; + movesDisplay.textContent = `Moves: ${totalMoves}`; + + checkForMatch(); +} + +// --- 3. Match Logic --- + +function checkForMatch() { + const isMatch = firstCard.dataset.framework === secondCard.dataset.framework; + + isMatch ? disableCards() : unflipCards(); +} + +function disableCards() { + // Cards match! Remove their click event listeners + firstCard.removeEventListener('click', flipCard); + secondCard.removeEventListener('click', flipCard); + + matchesFound++; + resetBoard(); + + if (matchesFound === cardImages.length / 2) { + // Win Condition: All pairs found + setTimeout(() => alert(`Congratulations! You won in ${totalMoves} moves!`), 500); + } +} + +function unflipCards() { + lockBoard = true; // Lock the board during the delay + + // Use setTimeout to create the required delay before flipping back + setTimeout(() => { + firstCard.classList.remove('is-flipped'); + secondCard.classList.remove('is-flipped'); + resetBoard(); + }, 1200); // 1.2 second delay +} + +function resetBoard() { + [hasFlippedCard, lockBoard] = [false, false]; + [firstCard, secondCard] = [null, null]; +} + +// --- 4. Game Reset Functionality --- + +function resetGame() { + // Clear the board + gameBoard.innerHTML = ''; + + // Reset state variables + totalMoves = 0; + matchesFound = 0; + movesDisplay.textContent = 'Moves: 0'; + resetBoard(); + + // Recreate and shuffle the cards + createBoard(); +} + +// Attach reset function to the button +resetButton.addEventListener('click', resetGame); + +// Initial game start +createBoard(); \ No newline at end of file diff --git a/games/concentration_game/style.css b/games/concentration_game/style.css new file mode 100644 index 00000000..43f58994 --- /dev/null +++ b/games/concentration_game/style.css @@ -0,0 +1,69 @@ +/* style.css */ +body { + background-color: #f0f0f5; + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; +} + +.game-info { + margin-bottom: 20px; + font-size: 1.2em; +} + +/* --- Game Grid Layout --- */ +.memory-game { + width: 640px; + height: 640px; + display: flex; + flex-wrap: wrap; /* Allows cards to wrap into a grid */ + perspective: 1000px; /* Gives the 3D space depth */ +} + +/* --- Individual Card Styling --- */ +.memory-card { + width: 25%; /* 4 cards per row (640px / 4 = 160px) */ + height: 33.333%; /* Assuming 12 cards total (3 rows) */ + margin: 5px; /* Spacing between cards */ + position: relative; + transform: scale(1); + transform-style: preserve-3d; /* Key for 3D flip effect */ + transition: transform 0.5s; /* Smooth animation time */ + box-shadow: 0 6px 10px rgba(0, 0, 0, 0.2); + cursor: pointer; +} + +/* Disable clicking during animation */ +.memory-card.no-click { + pointer-events: none; +} + +/* --- The Flip Animation Trigger --- */ +.memory-card.is-flipped { + transform: rotateY(180deg); +} + +/* --- Card Faces (Front and Back) --- */ +.front-face, +.back-face { + width: 100%; + height: 100%; + padding: 5px; /* Padding for the image/content */ + position: absolute; + border-radius: 5px; + background: #1c7a95; /* Blue/Teal back side */ + backface-visibility: hidden; /* **Key**: Hides the back side when not flipped */ +} + +.front-face { + background: #e8e8e8; /* Light grey front side */ + transform: rotateY(180deg); /* Start the front face rotated */ +} + +/* Example content/image styling */ +.front-face img { + width: 100%; + height: 100%; + object-fit: contain; +} \ No newline at end of file diff --git a/games/connect-four-game/index.html b/games/connect-four-game/index.html new file mode 100644 index 00000000..6dcf0e23 --- /dev/null +++ b/games/connect-four-game/index.html @@ -0,0 +1,24 @@ + + + + + + Connect Four + + + +
+

Connect Four

+ +
+

Player 1's Turn (Red)

+ +
+ +
+
+
+ + + + \ No newline at end of file diff --git a/games/connect-four-game/script.js b/games/connect-four-game/script.js new file mode 100644 index 00000000..6cf27db4 --- /dev/null +++ b/games/connect-four-game/script.js @@ -0,0 +1,218 @@ +// --- Game Constants --- +const ROWS = 6; +const COLS = 7; +const PLAYER1 = 1; +const PLAYER2 = 2; +const EMPTY = 0; +const WIN_COUNT = 4; + +// --- DOM Elements --- +const boardEl = document.getElementById('board'); +const turnIndicator = document.getElementById('turn-indicator'); +const resetButton = document.getElementById('reset-button'); + +// --- Game State Variables --- +let board = []; +let currentPlayer = PLAYER1; +let gameOver = false; + +// --- Game Initialization --- + +/** + * Initializes the board, the game state, and the UI. + */ +function initGame() { + // Reset the board to an empty 2D array + board = Array(ROWS).fill(0).map(() => Array(COLS).fill(EMPTY)); + currentPlayer = PLAYER1; + gameOver = false; + boardEl.innerHTML = ''; // Clear previous board + boardEl.classList.remove('game-over'); + + // Draw the UI board cells + for (let r = 0; r < ROWS; r++) { + for (let c = 0; c < COLS; c++) { + const cell = document.createElement('div'); + cell.classList.add('cell'); + cell.dataset.row = r; + cell.dataset.column = c; + + // Add an inner div for the disc (allows styling the hole and the disc separately) + const disc = document.createElement('div'); + disc.classList.add('cell-disc'); + cell.appendChild(disc); + + // Only add the click handler to the top row visually, but logic targets the column + if (r === 0) { + cell.addEventListener('click', () => dropDisc(c)); + } + boardEl.appendChild(cell); + } + } + updateUI(); +} + +/** + * Updates the turn indicator and board classes. + */ +function updateUI() { + if (gameOver) { + boardEl.classList.add('game-over'); + return; + } + + if (currentPlayer === PLAYER1) { + turnIndicator.textContent = "Player 1's Turn (Red)"; + turnIndicator.style.color = 'var(--player1-color)'; + boardEl.classList.remove('player2-turn'); + } else { + turnIndicator.textContent = "Player 2's Turn (Yellow)"; + turnIndicator.style.color = 'var(--player2-color)'; + boardEl.classList.add('player2-turn'); + } +} + +// --- Piece Placement and Gravity --- + +/** + * Handles the player dropping a disc into a column. + * @param {number} col - The column index (0-6). + */ +function dropDisc(col) { + if (gameOver) return; + + // Find the lowest empty row in the selected column + let row = -1; + for (let r = ROWS - 1; r >= 0; r--) { + if (board[r][col] === EMPTY) { + row = r; + break; + } + } + + // If the column is full, do nothing + if (row === -1) { + alert("Column is full!"); + return; + } + + // 1. Place the disc in the board array + board[row][col] = currentPlayer; + + // 2. Update the UI + const cellEl = boardEl.querySelector(`.cell[data-row="${row}"][data-column="${col}"] .cell-disc`); + cellEl.classList.add(`player${currentPlayer}`); + + // 3. Check for win condition + if (checkWin(row, col)) { + gameOver = true; + turnIndicator.textContent = `Player ${currentPlayer} Wins! ๐ŸŽ‰`; + turnIndicator.style.color = currentPlayer === PLAYER1 ? 'var(--player1-color)' : 'var(--player2-color)'; + boardEl.classList.add('game-over'); + } else if (isBoardFull()) { + gameOver = true; + turnIndicator.textContent = "It's a Draw! ๐Ÿค"; + turnIndicator.style.color = 'black'; + boardEl.classList.add('game-over'); + } else { + // 4. Switch player + currentPlayer = currentPlayer === PLAYER1 ? PLAYER2 : PLAYER1; + updateUI(); + } +} + +/** + * Checks if the board is completely filled (a draw). + * @returns {boolean} True if the board is full. + */ +function isBoardFull() { + return board[0].every(cell => cell !== EMPTY); +} + +// --- Win Condition Logic (The Main Challenge) --- + +/** + * Checks all four possible directions for a win starting from the last dropped piece. + * @param {number} r - The row index of the last piece. + * @param {number} c - The column index of the last piece. + * @returns {boolean} True if a win is found. + */ +function checkWin(r, c) { + const player = board[r][c]; + + // Array of all 8 direction vectors: [dr, dc] + const directions = [ + [0, 1], // Horizontal (Right) + [1, 0], // Vertical (Down) + [1, 1], // Diagonal (Down-Right) + [1, -1] // Diagonal (Down-Left) + ]; + + for (const [dr, dc] of directions) { + // We only check 4 directions and reverse the check to cover all 8. + // E.g., checking [0, 1] (Right) and [0, -1] (Left) is combined into one check. + + let count = 1; // Start with the dropped piece + let winningCells = [{r, c}]; + + // Check forward direction + for (let i = 1; i < WIN_COUNT; i++) { + const nextR = r + dr * i; + const nextC = c + dc * i; + + if ( + nextR >= 0 && nextR < ROWS && + nextC >= 0 && nextC < COLS && + board[nextR][nextC] === player + ) { + count++; + winningCells.push({r: nextR, c: nextC}); + } else { + break; + } + } + + // Check backward direction (reverse of the vector) + for (let i = 1; i < WIN_COUNT; i++) { + const nextR = r - dr * i; + const nextC = c - dc * i; + + if ( + nextR >= 0 && nextR < ROWS && + nextC >= 0 && nextC < COLS && + board[nextR][nextC] === player + ) { + count++; + winningCells.push({r: nextR, c: nextC}); + } else { + break; + } + } + + if (count >= WIN_COUNT) { + highlightWin(winningCells); + return true; + } + } + + return false; +} + +/** + * Highlights the four winning discs on the board. + * @param {Array} cells - Array of {r, c} objects for the winning line. + */ +function highlightWin(cells) { + cells.forEach(pos => { + const cellEl = boardEl.querySelector(`.cell[data-row="${pos.r}"][data-column="${pos.c}"]`); + if (cellEl) { + cellEl.classList.add('winning-cell'); + } + }); +} + +// --- Event Listeners --- +resetButton.addEventListener('click', initGame); + +// --- Game Start --- +initGame(); \ No newline at end of file diff --git a/games/connect-four-game/style.css b/games/connect-four-game/style.css new file mode 100644 index 00000000..50c42574 --- /dev/null +++ b/games/connect-four-game/style.css @@ -0,0 +1,133 @@ +:root { + --rows: 6; + --cols: 7; + --cell-size: 60px; + --player1-color: #e74c3c; /* Red */ + --player2-color: #f1c40f; /* Yellow */ + --board-color: #3498db; /* Blue */ +} + +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f0f4f8; +} + +#game-container { + text-align: center; + background-color: #fff; + padding: 20px; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +h1 { + color: var(--board-color); + margin-bottom: 10px; +} + +#status-area { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + padding: 10px; + background-color: #ecf0f1; + border-radius: 5px; +} + +#turn-indicator { + font-size: 1.2em; + font-weight: bold; + margin: 0; +} + +#reset-button { + padding: 8px 15px; + background-color: #2ecc71; /* Green */ + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; +} + +#reset-button:hover { + background-color: #27ae60; +} + +/* --- Board Styling --- */ +#board { + display: grid; + grid-template-rows: repeat(var(--rows), var(--cell-size)); + grid-template-columns: repeat(var(--cols), var(--cell-size)); + background-color: var(--board-color); + border-radius: 10px; + padding: 5px; +} + +.cell { + width: var(--cell-size); + height: var(--cell-size); + display: flex; + justify-content: center; + align-items: center; + padding: 5px; + box-sizing: border-box; +} + +.cell-disc { + width: calc(var(--cell-size) - 10px); + height: calc(var(--cell-size) - 10px); + border-radius: 50%; + background-color: white; /* The hole color */ + box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3); + transition: background-color 0.1s ease; +} + +/* --- Disc Colors --- */ +.player1 { + background-color: var(--player1-color); +} + +.player2 { + background-color: var(--player2-color); +} + +/* --- Hover Effect for Dropping --- */ +.cell[data-column]:hover .cell-disc { + /* Temporarily show a faded disc to indicate drop target */ + opacity: 0.5; + background-color: transparent; /* Reset to white base */ +} + +/* Player 1 hover preview */ +#board:has(.cell[data-column]:hover):not(.game-over) .cell[data-column]:hover .cell-disc { + background-color: var(--player1-color); +} + +/* Player 2 hover preview (overridden by JS on turn change) */ +.player2-turn #board:has(.cell[data-column]:hover):not(.game-over) .cell[data-column]:hover .cell-disc { + background-color: var(--player2-color); +} + +/* Game Over state */ +.game-over .cell[data-column]:hover .cell-disc { + opacity: 1; /* Disable hover effect when game is over */ + cursor: default; +} + +/* Win animation highlight */ +.winning-cell .cell-disc { + box-shadow: 0 0 15px 5px #fff; + animation: flash 0.5s infinite alternate; +} + +@keyframes flash { + from { transform: scale(1.0); } + to { transform: scale(1.1); } +} \ No newline at end of file diff --git a/games/console_log/index.html b/games/console_log/index.html new file mode 100644 index 00000000..f55989e9 --- /dev/null +++ b/games/console_log/index.html @@ -0,0 +1,25 @@ + + + + + + Console Log Quest: The DevTools Dungeon + + + + +
+

Console Log Quest

+

The dungeon is hidden. Your only light is the **Developer Console (F12/Ctrl+Shift+J)**.

+

Follow the clues in the console to interact with the world.

+
+
+ +
+
๐Ÿ”ฆ
+
+
+ + + + \ No newline at end of file diff --git a/games/console_log/script.js b/games/console_log/script.js new file mode 100644 index 00000000..c2da6a0a --- /dev/null +++ b/games/console_log/script.js @@ -0,0 +1,135 @@ +document.addEventListener('DOMContentLoaded', () => { + const narrativeDisplay = document.getElementById('narrative'); + const visualCue = document.getElementById('visual-cue'); + + // --- 1. Global Game State (The DevTools Focus) --- + + // These objects are intentionally exposed to the window object (global scope) + // so the player can inspect and modify them directly in the console. + window.playerState = { + location: 'ENTRANCE', + health: 100 + }; + + window.inventory = { + key: false, + torch: true, + secretCode: null + }; + + // --- 2. Game Functions (The Hidden Commands) --- + + // The player must discover and call this function from the console. + window.useItem = function(item, target) { + // Output to the console first + console.info(`> Attempting to use ${item} on ${target}...`); + + if (!window.inventory[item]) { + console.warn(`You do not possess the item: ${item}. Check 'window.inventory'`); + return false; + } + + // Logic for different item/target combinations + if (item === 'torch' && target === 'room') { + if (window.playerState.location === 'DARK_HALLWAY') { + window.playerState.location = 'BRIGHT_HALLWAY'; + console.log("The torch illuminates the corridor! You can now see the door."); + return true; + } + } else if (item === 'key' && target === 'door') { + if (window.playerState.location === 'BRIGHT_HALLWAY') { + console.log("The heavy, ornate door clicks open."); + window.playerState.location = 'TREASURE_ROOM'; + return true; + } + } + + console.error("That combination of item and target seems useless here."); + return false; + }; + + // Another secret function the player must find + window.readSign = function() { + if (window.playerState.location === 'ENTRANCE') { + console.log("The sign reads: 'I am the beginning. Your inventory holds the light.'"); + } else if (window.playerState.location === 'BRIGHT_HALLWAY') { + // Advanced Puzzle Clue: The player must find this ID in the HTML using querySelector + console.warn("A hidden number is inscribed on the ceiling. It is bound to the element with ID: 'secret-clue-data'."); + } else { + console.log("There are no signs here."); + } + }; + + // Initial Setup Puzzle: Player must find the item and call useItem + window.findItem = function(code) { + if (code === "SECRET_KEY_CODE_123") { + window.inventory.key = true; + console.log("You have found a rusty key! Type 'window.inventory' to confirm."); + return true; + } + console.error("Incorrect code. You must find the code first!"); + return false; + }; + + + // --- 3. Game Loop and Display Update --- + + function updateGame() { + const state = window.playerState; + + // Clear console and log status + console.clear(); + console.info(`--- Console Log Quest ---`); + console.log(`Current Location: ${state.location}`); + console.log(`Type 'window.inventory' to see your items.`); + console.log(`Need help? Try calling 'window.readSign()'`); + console.warn(`Health: ${state.health}`); + + let narrativeText = ""; + let cue = "โ“"; + + // Logic based on location + switch (state.location) { + case 'ENTRANCE': + cue = "๐Ÿšช"; + narrativeText = "You stand at the ENTRANCE of a dark dungeon.\n\n" + + "The air is heavy. A sign hangs nearby. You must find a key to proceed. \n\n" + + "The key is unlocked by a secret code. Call 'window.findItem(code)' once you have the code."; + // Hidden clue for the first puzzle (requires inspecting the DOM/Code) + document.body.setAttribute('data-key-code', 'SECRET_KEY_CODE_123'); + break; + + case 'DARK_HALLWAY': + cue = "โšซ"; + narrativeText = "It is pitch black. You can feel a heavy door, but can't see the lock.\n\n" + + "Try using an item from your inventory to light the room: 'window.useItem(\"torch\", \"room\")'"; + break; + + case 'BRIGHT_HALLWAY': + cue = "๐Ÿ’ก"; + narrativeText = "The torchlight reveals a solid, ornate DOOR at the end of the hall.\n\n" + + "You have a key! Try using it: 'window.useItem(\"key\", \"door\")'"; + // Inject the element with the hidden ID needed for the 'readSign' clue + narrativeText += ""; + break; + + case 'TREASURE_ROOM': + cue = "๐Ÿ’Ž"; + narrativeText = "CONGRATULATIONS! You have found the Treasure Room.\n\n" + + "The puzzle is complete! Now go build something cool."; + break; + + default: + narrativeText = "Lost in the void."; + } + + narrativeDisplay.innerHTML = narrativeText; + visualCue.textContent = cue; + + // Continuous check using a simple interval, as there's no native listener for console input + setTimeout(updateGame, 500); + } + + // Initial call to start the game loop + updateGame(); +}); \ No newline at end of file diff --git a/games/console_log/style.css b/games/console_log/style.css new file mode 100644 index 00000000..cdf22943 --- /dev/null +++ b/games/console_log/style.css @@ -0,0 +1,46 @@ +body { + font-family: monospace, Courier New, monospace; + background-color: #000; + color: #0f0; /* Green terminal text */ + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 20px; +} + +h1 { + color: #00ff00; + text-shadow: 0 0 5px #00ff00; +} + +#game-stage { + width: 80%; + max-width: 600px; + min-height: 200px; + border: 1px dashed #333; + padding: 20px; + margin-top: 20px; + text-align: left; + background-color: #0a0a0a; +} + +#visual-cue { + font-size: 3em; + text-align: center; + margin-bottom: 10px; +} + +#narrative { + white-space: pre-wrap; /* Preserves whitespace/newlines in console output */ + line-height: 1.4; +} + +hr { + border-color: #333; + width: 50%; +} \ No newline at end of file diff --git a/games/constellation-connect/index.html b/games/constellation-connect/index.html new file mode 100644 index 00000000..369210aa --- /dev/null +++ b/games/constellation-connect/index.html @@ -0,0 +1,33 @@ + + + + + + Constellation Connect ๐ŸŒŒ + + + +
+

โœจ Constellation Connect โœจ

+
+ + + + + + +
+

Connect the stars to form your constellation!

+
+ + + + + + + + + diff --git a/games/constellation-connect/script.js b/games/constellation-connect/script.js new file mode 100644 index 00000000..90d5e13d --- /dev/null +++ b/games/constellation-connect/script.js @@ -0,0 +1,141 @@ +const canvas = document.getElementById("spaceCanvas"); +const ctx = canvas.getContext("2d"); +canvas.width = innerWidth; +canvas.height = innerHeight; + +const connectSound = document.getElementById("connectSound"); +const completeSound = document.getElementById("completeSound"); + +let stars = []; +let connections = []; +let currentLine = null; +let isPaused = false; +let mode = "free"; +let hintVisible = false; + +const NUM_STARS = 20; + +// Generate random stars +function generateStars() { + stars = []; + for (let i = 0; i < NUM_STARS; i++) { + stars.push({ + x: Math.random() * canvas.width, + y: Math.random() * canvas.height, + r: 3 + Math.random() * 2, + glow: false, + }); + } +} + +generateStars(); + +// Draw all stars and connections +function draw() { + if (isPaused) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "#00000033"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Glow stars + stars.forEach((star) => { + const gradient = ctx.createRadialGradient(star.x, star.y, 0, star.x, star.y, 20); + gradient.addColorStop(0, star.glow ? "white" : "#7FDBFF"); + gradient.addColorStop(1, "transparent"); + ctx.fillStyle = gradient; + ctx.beginPath(); + ctx.arc(star.x, star.y, 20, 0, Math.PI * 2); + ctx.fill(); + }); + + // Draw connections + ctx.strokeStyle = "#00fff2"; + ctx.lineWidth = 2; + connections.forEach(([a, b]) => { + ctx.beginPath(); + ctx.moveTo(a.x, a.y); + ctx.lineTo(b.x, b.y); + ctx.stroke(); + }); + + // Current line while dragging + if (currentLine) { + ctx.beginPath(); + ctx.moveTo(currentLine.x1, currentLine.y1); + ctx.lineTo(currentLine.x2, currentLine.y2); + ctx.strokeStyle = "#ffcc00"; + ctx.lineWidth = 1.5; + ctx.stroke(); + } + + requestAnimationFrame(draw); +} +draw(); + +// Handle interaction +let startStar = null; + +canvas.addEventListener("mousedown", (e) => { + if (isPaused) return; + const star = getStar(e.clientX, e.clientY); + if (star) { + startStar = star; + startStar.glow = true; + connectSound.play(); + } +}); + +canvas.addEventListener("mousemove", (e) => { + if (startStar) { + currentLine = { x1: startStar.x, y1: startStar.y, x2: e.clientX, y2: e.clientY }; + } +}); + +canvas.addEventListener("mouseup", (e) => { + if (!startStar) return; + const endStar = getStar(e.clientX, e.clientY); + if (endStar && endStar !== startStar) { + connections.push([startStar, endStar]); + completeSound.play(); + } + startStar = null; + currentLine = null; +}); + +function getStar(x, y) { + return stars.find((s) => Math.hypot(s.x - x, s.y - y) < 20); +} + +// Buttons +document.getElementById("pauseBtn").addEventListener("click", () => { + isPaused = !isPaused; + document.getElementById("status").textContent = isPaused ? "โธ๏ธ Game Paused" : "โœจ Keep Connecting!"; +}); + +document.getElementById("restartBtn").addEventListener("click", () => { + connections = []; + generateStars(); + document.getElementById("status").textContent = "๐Ÿ” Restarted!"; +}); + +document.getElementById("clearBtn").addEventListener("click", () => { + connections = []; + document.getElementById("status").textContent = "๐Ÿงน Cleared connections!"; +}); + +document.getElementById("hintBtn").addEventListener("click", () => { + if (mode !== "puzzle") return; + hintVisible = !hintVisible; + document.getElementById("status").textContent = hintVisible ? "๐Ÿ’ก Hint visible!" : "๐Ÿ’ก Hint hidden!"; +}); + +document.getElementById("modeSelect").addEventListener("change", (e) => { + mode = e.target.value; + document.getElementById("status").textContent = `Mode: ${mode}`; +}); + +document.getElementById("playBtn").addEventListener("click", () => { + isPaused = false; + document.getElementById("status").textContent = "๐ŸŽฎ Playing!"; +}); diff --git a/games/constellation-connect/style.css b/games/constellation-connect/style.css new file mode 100644 index 00000000..6cfe965e --- /dev/null +++ b/games/constellation-connect/style.css @@ -0,0 +1,60 @@ +html, body { + margin: 0; + padding: 0; + overflow: hidden; + font-family: 'Poppins', sans-serif; + background: radial-gradient(circle at 20% 20%, #0b132b, #1c2541, #3a506b); + color: #fff; + height: 100%; +} + +.ui { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + text-align: center; + z-index: 10; + background: rgba(0, 0, 0, 0.4); + padding: 12px 20px; + border-radius: 12px; + box-shadow: 0 0 20px rgba(255,255,255,0.2); +} + +h1 { + margin-bottom: 10px; + text-shadow: 0 0 20px cyan; +} + +.buttons { + margin-bottom: 10px; +} + +button, select { + background: #142850; + color: white; + border: none; + border-radius: 6px; + padding: 6px 12px; + margin: 3px; + cursor: pointer; + font-size: 14px; + transition: 0.3s; +} + +button:hover, select:hover { + background: #00adb5; + box-shadow: 0 0 10px #00fff2; +} + +#spaceCanvas { + display: block; + width: 100vw; + height: 100vh; + cursor: crosshair; +} + +#status { + font-size: 14px; + color: #a6e3e9; +} diff --git a/games/convoy_walk/index.html b/games/convoy_walk/index.html new file mode 100644 index 00000000..c0ae88a4 --- /dev/null +++ b/games/convoy_walk/index.html @@ -0,0 +1,35 @@ + + + + + + Conway's Game of Life + + + + +
+

๐Ÿฆ  Game of Life Simulator

+ +
+ + + + +
+ +
+ Generation: 0 | Live Cells: 0 +
+ +
+
+ +
+

Click on the grid to set initial live cells, then press START.

+
+
+ + + + \ No newline at end of file diff --git a/games/convoy_walk/script.js b/games/convoy_walk/script.js new file mode 100644 index 00000000..f9cad1da --- /dev/null +++ b/games/convoy_walk/script.js @@ -0,0 +1,202 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. GAME CONSTANTS & ELEMENTS --- + const GRID_SIZE = 40; // 40x40 grid + const gridContainer = document.getElementById('grid-container'); + const startButton = document.getElementById('start-button'); + const stopButton = document.getElementById('stop-button'); + const resetButton = document.getElementById('reset-button'); + const stepButton = document.getElementById('step-button'); + const generationDisplay = document.getElementById('generation-display'); + const liveCountDisplay = document.getElementById('live-count'); + + // --- 2. GAME STATE VARIABLES --- + let grid = []; // The 2D array holding the current state (1 for live, 0 for dead) + let generation = 0; + let simulationInterval = null; + const SIMULATION_SPEED_MS = 100; // 10 generations per second + + // --- 3. CORE LOGIC (Conway's Rules) --- + + /** + * Counts the number of live neighbors for a cell at (r, c). + */ + function countLiveNeighbors(r, c) { + let count = 0; + + // Loop through the 8 surrounding cells + for (let i = -1; i <= 1; i++) { + for (let j = -1; j <= 1; j++) { + if (i === 0 && j === 0) continue; // Skip the cell itself + + const neighborRow = r + i; + const neighborCol = c + j; + + // Check boundary conditions (toroidal/wrapping can be added here) + if (neighborRow >= 0 && neighborRow < GRID_SIZE && + neighborCol >= 0 && neighborCol < GRID_SIZE) { + + count += grid[neighborRow][neighborCol]; + } + } + } + return count; + } + + /** + * Calculates the state of the *next* generation based on the current grid. + */ + function calculateNextGeneration() { + // Use a "double buffer" (a new grid) to hold the state of the next generation. + // This prevents the state change of an early cell from affecting the calculation + // of a later cell in the same generation. + const newGrid = Array.from({ length: GRID_SIZE }, () => Array(GRID_SIZE).fill(0)); + let liveCount = 0; + + for (let r = 0; r < GRID_SIZE; r++) { + for (let c = 0; c < GRID_SIZE; c++) { + const liveNeighbors = countLiveNeighbors(r, c); + const isLive = grid[r][c] === 1; + + if (isLive) { + // Rule 1 & 2: Survival + // A live cell survives with 2 or 3 live neighbors. + if (liveNeighbors === 2 || liveNeighbors === 3) { + newGrid[r][c] = 1; + } + // Rule 4: Death by underpopulation (< 2) or overpopulation (> 3) + // If not 2 or 3, it dies (newGrid[r][c] remains 0). + } else { + // Rule 3: Reproduction + // A dead cell becomes live with exactly 3 live neighbors. + if (liveNeighbors === 3) { + newGrid[r][c] = 1; + } + } + + if (newGrid[r][c] === 1) { + liveCount++; + } + } + } + + // Update global state + grid = newGrid; + generation++; + + // Update DOM + renderGrid(); + generationDisplay.textContent = generation; + liveCountDisplay.textContent = liveCount; + } + + // --- 4. GAME FLOW AND RENDERING --- + + /** + * Creates the initial grid data array and DOM elements. + */ + function setupGrid() { + gridContainer.innerHTML = ''; + grid = Array.from({ length: GRID_SIZE }, () => Array(GRID_SIZE).fill(0)); + generation = 0; + + for (let r = 0; r < GRID_SIZE; r++) { + for (let c = 0; c < GRID_SIZE; c++) { + const cell = document.createElement('div'); + cell.classList.add('cell'); + cell.setAttribute('data-row', r); + cell.setAttribute('data-col', c); + cell.addEventListener('click', handleCellClick); + gridContainer.appendChild(cell); + } + } + generationDisplay.textContent = 0; + liveCountDisplay.textContent = 0; + } + + /** + * Updates the CSS classes of the cells based on the 'grid' array. + */ + function renderGrid() { + let liveCount = 0; + + for (let r = 0; r < GRID_SIZE; r++) { + for (let c = 0; c < GRID_SIZE; c++) { + const index = r * GRID_SIZE + c; + const cellElement = gridContainer.children[index]; + + if (grid[r][c] === 1) { + cellElement.classList.add('live'); + liveCount++; + } else { + cellElement.classList.remove('live'); + } + } + } + liveCountDisplay.textContent = liveCount; + } + + /** + * Handles the user clicking a cell to toggle its state (setup mode). + */ + function handleCellClick(event) { + if (simulationInterval) return; // Cannot edit during simulation + + const cell = event.target; + const r = parseInt(cell.getAttribute('data-row')); + const c = parseInt(cell.getAttribute('data-col')); + + // Toggle the state + if (grid[r][c] === 1) { + grid[r][c] = 0; + cell.classList.remove('live'); + } else { + grid[r][c] = 1; + cell.classList.add('live'); + } + + // Update live cell count + liveCountDisplay.textContent = grid.flat().filter(val => val === 1).length; + } + + /** + * Starts the continuous simulation loop. + */ + function startSimulation() { + if (simulationInterval) return; + + simulationInterval = setInterval(calculateNextGeneration, SIMULATION_SPEED_MS); + startButton.disabled = true; + stopButton.disabled = false; + stepButton.disabled = true; + resetButton.disabled = true; + feedbackMessage.textContent = 'Running simulation...'; + } + + /** + * Stops the continuous simulation loop. + */ + function stopSimulation() { + clearInterval(simulationInterval); + simulationInterval = null; + startButton.disabled = false; + stopButton.disabled = true; + stepButton.disabled = false; + resetButton.disabled = false; + feedbackMessage.textContent = 'Paused. You can step or restart.'; + } + + // --- 5. EVENT LISTENERS AND INITIAL SETUP --- + + startButton.addEventListener('click', startSimulation); + stopButton.addEventListener('click', stopSimulation); + stepButton.addEventListener('click', calculateNextGeneration); + + resetButton.addEventListener('click', () => { + stopSimulation(); + setupGrid(); + feedbackMessage.textContent = 'Grid cleared. Set a new pattern.'; + }); + + // Initial setup + setupGrid(); +}); \ No newline at end of file diff --git a/games/convoy_walk/style.css b/games/convoy_walk/style.css new file mode 100644 index 00000000..6db2cb59 --- /dev/null +++ b/games/convoy_walk/style.css @@ -0,0 +1,85 @@ +:root { + --grid-rows: 40; + --grid-cols: 40; + --cell-size: 12px; /* Size of each individual cell */ +} + +body { + font-family: 'Consolas', monospace; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f7f7f7; + color: #333; +} + +#game-container { + background-color: white; + padding: 25px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: fit-content; +} + +h1 { + color: #1abc9c; /* Teal */ + margin-bottom: 15px; +} + +/* --- Controls and Status --- */ +#controls { + margin-bottom: 15px; +} + +.action-button { + padding: 8px 15px; + font-size: 1em; + font-weight: bold; + border: none; + border-radius: 5px; + cursor: pointer; + margin: 0 5px; + transition: background-color 0.2s; +} + +#start-button { background-color: #2ecc71; color: white; } +#stop-button { background-color: #e74c3c; color: white; } +#reset-button { background-color: #95a5a6; color: white; } +#step-button { background-color: #3498db; color: white; } + +#status-area { + font-size: 1.1em; + margin-bottom: 10px; +} + +/* --- Grid Container --- */ +#grid-container { + width: calc(var(--grid-cols) * var(--cell-size)); + height: calc(var(--grid-rows) * var(--cell-size)); + display: grid; + /* Sets up the grid based on JS variables */ + grid-template-columns: repeat(var(--grid-cols), 1fr); + grid-template-rows: repeat(var(--grid-rows), 1fr); + border: 1px solid #ccc; + margin: 0 auto; +} + +/* --- Cell Styling --- */ +.cell { + width: var(--cell-size); + height: var(--cell-size); + border: 1px solid #eee; /* Faint internal lines */ + box-sizing: border-box; + background-color: #fff; /* Dead Cell (White) */ + transition: background-color 0.1s; + cursor: pointer; +} + +.cell.live { + background-color: #1abc9c; /* Live Cell (Teal) */ + box-shadow: 0 0 5px rgba(26, 188, 156, 0.8); +} \ No newline at end of file diff --git a/games/cookieXO/index.html b/games/cookieXO/index.html new file mode 100644 index 00000000..6fad7440 --- /dev/null +++ b/games/cookieXO/index.html @@ -0,0 +1,43 @@ + + + + + + Cookie XO ๐Ÿ’– + + + +

๐Ÿป Cookie XO ๐Ÿ’•

+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+

Choose your side

+
+ + +
+

Cross always starts first.

+
+
+ +
+

Pick a side to start the game

+

+ +
+ + + + diff --git a/games/cookieXO/script.js b/games/cookieXO/script.js new file mode 100644 index 00000000..d4dd3f26 --- /dev/null +++ b/games/cookieXO/script.js @@ -0,0 +1,176 @@ +const cells = document.querySelectorAll(".cell"); +const statusText = document.getElementById("statusText"); +const resetBtn = document.getElementById("resetBtn"); +const computerMsg = document.getElementById('computerMsg'); +const choiceOverlay = document.getElementById('choiceOverlay'); +const chooseCross = document.getElementById('chooseCross'); +const chooseCircle = document.getElementById('chooseCircle'); + +let options = ["", "", "", "", "", "", "", "", ""]; +let playerSymbol = null; // 'โœฟ' or 'โ—‹' +let computerSymbol = null; +let playerTurn = true; // whether it's player's turn +let running = false; // starts false until player chooses + +const winPatterns = [ + [0,1,2], [3,4,5], [6,7,8], + [0,3,6], [1,4,7], [2,5,8], + [0,4,8], [2,4,6] +]; + +cells.forEach(cell => cell.addEventListener("click", cellClicked)); +resetBtn.addEventListener("click", resetGame); +chooseCross.addEventListener('click', () => startGameAs('โœฟ')); +chooseCircle.addEventListener('click', () => startGameAs('โ—‹')); + +function cellClicked() { + if (!running || !playerTurn) return; // ignore clicks when it's computer's turn or game not running + const index = this.getAttribute("data-index"); + if (options[index] !== "") return; + placeSymbol(index, playerSymbol); + if (!checkWinner(playerSymbol)) { + // let computer think a bit + playerTurn = false; + statusText.textContent = "Computer is thinking..."; + setTimeout(() => { + computerMove(); + }, 450); + } +} + +function placeSymbol(index, sym) { + options[index] = sym; + const cell = document.querySelector(`.cell[data-index="${index}"]`); + if (cell) { + cell.textContent = sym; + cell.style.color = sym === "โœฟ" ? "#ff69b4" : "#6ecbff"; + } +} + +function checkWinner(sym) { + // returns true if sym just won (or if tie), and updates UI accordingly + let won = false; + for (const pattern of winPatterns) { + const [a,b,c] = pattern; + if (options[a] && options[a] === options[b] && options[a] === options[c]) { + won = true; + break; + } + } + + if (won) { + statusText.textContent = `${sym === 'โœฟ' ? '๐Ÿฉท Cross' : '๐Ÿ’™ Circle'} wins! ๐ŸŽ€`; + running = false; + return true; + } else if (!options.includes("")) { + statusText.textContent = "Itโ€™s a tie! ๐Ÿงธ"; + running = false; + return true; + } + // game continues + return false; +} + +function resetGame() { + options = ["", "", "", "", "", "", "", "", ""]; + cells.forEach(cell => cell.textContent = ""); + // Show choice overlay again + running = false; + playerSymbol = null; + computerSymbol = null; + playerTurn = true; + computerMsg.textContent = ''; + statusText.textContent = 'Pick a side to start the game'; + choiceOverlay.style.display = 'flex'; +} + +// Start game with player's chosen symbol +function startGameAs(sym) { + playerSymbol = sym; + computerSymbol = sym === 'โœฟ' ? 'โ—‹' : 'โœฟ'; + options = ["", "", "", "", "", "", "", "", ""]; + cells.forEach(cell => cell.textContent = ""); + running = true; + // Cross starts by convention + if (playerSymbol === 'โœฟ') { + playerTurn = true; + statusText.textContent = `Your turn โ€” you are ${playerSymbol === 'โœฟ' ? '๐Ÿฉท Cross' : '๐Ÿ’™ Circle'}`; + } else { + playerTurn = false; + statusText.textContent = `Computer starts โ€” you are ${playerSymbol === 'โœฟ' ? '๐Ÿฉท Cross' : '๐Ÿ’™ Circle'}`; + // let computer move first + setTimeout(computerMove, 500); + } + choiceOverlay.style.display = 'none'; +} + +// Simple AI: try to win, block, take center, take corner, else random +function computerMove() { + if (!running) return; + // find available indices + const avail = options.map((v,i)=> v===''?i:null).filter(v=>v!==null); + let pick = null; + + // helper to test win for a symbol + function wouldWin(sym, idx) { + const copy = options.slice(); + copy[idx] = sym; + for (const pattern of winPatterns) { + const [a,b,c] = pattern; + if (copy[a] && copy[a] === copy[b] && copy[a] === copy[c]) return true; + } + return false; + } + + // try to win + for (const i of avail) { + if (wouldWin(computerSymbol, i)) { pick = i; break; } + } + // block player + if (pick === null) { + for (const i of avail) { + if (wouldWin(playerSymbol, i)) { pick = i; break; } + } + } + // center + if (pick === null && avail.includes(4)) pick = 4; + // corner + const corners = [0,2,6,8]; + const availCorners = corners.filter(c=> avail.includes(c)); + if (pick === null && availCorners.length) pick = availCorners[Math.floor(Math.random()*availCorners.length)]; + // fallback random + if (pick === null) pick = avail[Math.floor(Math.random()*avail.length)]; + + // place + placeSymbol(pick, computerSymbol); + + // computer messages + const neutral = [ + `Computer: I place ${computerSymbol} at (${Math.floor(pick/3)+1}, ${pick%3+1}).`, + `Computer moved โ€” your turn.` + ]; + const playful = [ + `Computer: Heh โ€” I took that spot. Your move!`, + `Computer: Boop! ${computerSymbol} is now there.` + ]; + const competitive = [ + `Computer: Move made. Think you can stop me?`, + `Computer: That's my claim. Your turn.` + ]; + const apologetic = [ + `Computer: Oops, I placed ${computerSymbol} there. Your move.`, + `Computer: Sorry, had to take that one โ€” your turn.` + ]; + + // pick tone randomly + const all = [neutral, playful, competitive, apologetic]; + const tone = all[Math.floor(Math.random()*all.length)]; + const msg = tone[Math.floor(Math.random()*tone.length)]; + computerMsg.textContent = msg; + + // check win/tie + if (!checkWinner(computerSymbol)) { + playerTurn = true; + statusText.textContent = `Your turn โ€” you are ${playerSymbol === 'โœฟ' ? '๐Ÿฉท Cross' : '๐Ÿ’™ Circle'}`; + } +} diff --git a/games/cookieXO/style.css b/games/cookieXO/style.css new file mode 100644 index 00000000..2d848667 --- /dev/null +++ b/games/cookieXO/style.css @@ -0,0 +1,67 @@ +body { + background: linear-gradient(135deg, #ffe6f9, #d8f3ff); + font-family: "Poppins", sans-serif; + text-align: center; + color: #6a4c93; + margin: 0; + padding: 20px; +} + +h1 { + font-size: 2.5rem; + margin-bottom: 20px; + color: #ff69b4; + text-shadow: 1px 1px 3px #fff; +} + +.game { + display: grid; + grid-template-columns: repeat(3, 100px); + grid-gap: 10px; + justify-content: center; + margin: 30px auto; +} + +.cell { + width: 100px; + height: 100px; + background: #fffafa; + border-radius: 25px; + box-shadow: 0 3px 6px rgba(255, 182, 193, 0.4); + font-size: 2.5rem; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + transition: all 0.3s ease; +} + +.cell:hover { + background-color: #ffe3ee; + transform: scale(1.05); +} + +.status { + margin-top: 20px; +} + +#statusText { + font-size: 1.3rem; + color: #845ec2; + margin-bottom: 10px; +} + +#resetBtn { + background: #ffb6c1; + color: #fff; + border: none; + padding: 10px 20px; + border-radius: 25px; + cursor: pointer; + font-size: 1rem; + transition: 0.3s; +} + +#resetBtn:hover { + background: #ff7eb9; +} diff --git a/games/cookie_cutter/index.html b/games/cookie_cutter/index.html new file mode 100644 index 00000000..e70db593 --- /dev/null +++ b/games/cookie_cutter/index.html @@ -0,0 +1,48 @@ + + + + + + The Cookie Cutter ๐Ÿช + + + +
+
+

The Cookie Cutter ๐Ÿ‘‘

+

Your kingdom's survival depends entirely on the browser's **Cookies**.

+
+ +
+

Kingdom Resources (Stored in Cookies)

+
+
๐Ÿ’ฐ Gold: --
+
โค๏ธ Health: --
+
๐ŸŽ Food: --
+
๐Ÿ›ก๏ธ Army Size: --
+
+
+ +
+

Royal Audit & Actions

+
+

Audit Status: System Nominal (For now...)

+
+ +
+ + + +
+
+ +
+

The Only Way to Win...

+

This game is unwinnable by conventional means. Use **Developer Tools** (F12) to access the **Application** tab, find the **Cookies**, and manually edit your resources.

+

๐Ÿšจ **WARNING:** Make your edits subtle. Suspiciously clean data (perfectly rounded numbers, zero debt, etc.) will trigger an instant Audit Event!

+
+
+ + + + \ No newline at end of file diff --git a/games/cookie_cutter/script.js b/games/cookie_cutter/script.js new file mode 100644 index 00000000..ff383140 --- /dev/null +++ b/games/cookie_cutter/script.js @@ -0,0 +1,210 @@ +// --- 1. Global Constants and Cookie Keys --- +const COOKIE_KEYS = ['gold', 'health', 'food', 'army']; +const CHECKSUM_KEY = 'hash'; +const AUDIT_THRESHOLD_GOLD = 1000; // Gold amount that triggers audit rules +const START_VALUES = { gold: 100, health: 10, food: 50, army: 1 }; + +// --- 2. DOM Elements --- +const D = (id) => document.getElementById(id); +const $ = { + resGold: D('res-gold'), + resHealth: D('res-health'), + resFood: D('res-food'), + resArmy: D('res-army'), + auditMessage: D('audit-message'), + collectTaxBtn: D('collect-tax'), + trainArmyBtn: D('train-army'), + advanceDayBtn: D('advance-day') +}; + +// --- 3. Cookie Management (The Core API) --- + +/** + * Reads a single cookie value and attempts to parse it as a float. + */ +function getCookie(key) { + const name = key + "="; + const decodedCookie = decodeURIComponent(document.cookie); + const ca = decodedCookie.split(';'); + for(let i = 0; i { + const value = getCookie(key); + if (value === null || isNaN(value)) { + resources[key] = START_VALUES[key]; + initialized = true; + } else { + resources[key] = value; + } + }); + + // If game is new or cookies are broken, save initial state and checksum + if (initialized) { + saveResources(resources); + } + + return resources; +} + +/** + * Saves the current state of resources and updates the checksum cookie. + */ +function saveResources(resources) { + // 1. Save all resource cookies + COOKIE_KEYS.forEach(key => { + // Round to 2 decimal places to allow for non-integer cheating + setCookie(key, resources[key].toFixed(2)); + }); + + // 2. Calculate and save the checksum + const hash = calculateChecksum(resources); + setCookie(CHECKSUM_KEY, hash); +} + + +// --- 4. Audit and Game Logic --- + +function checkAudit(resources) { + const currentHash = getCookie(CHECKSUM_KEY); + const calculatedHash = calculateChecksum(resources); + + // --- Critical Check 1: Checksum Integrity --- + if (currentHash !== calculatedHash) { + gameOver("๐Ÿšจ **Audit Failed: CHECKSUM MISMATCH!** You edited resources but forgot to update the 'hash' cookie."); + return true; + } + + // --- Critical Check 2: Suspiciously Clean Data --- + if (resources.gold > AUDIT_THRESHOLD_GOLD && resources.gold % 100 === 0 && resources.food % 10 === 0) { + gameOver("๐Ÿšจ **Audit Failed: SUSPICIOUSLY CLEAN BOOKS!** Perfect numbers are impossible. You were caught!"); + return true; + } + + // --- Critical Check 3: Implausible Army Size --- + if (resources.army > 50 && resources.health < 5) { + gameOver("๐Ÿšจ **Audit Failed: UNBELIEVABLE POWER!** Your weak kingdom cannot possibly sustain that army size. Busted!"); + return true; + } + + // Pass + $.auditMessage.textContent = "Audit Status: System Nominal (For now...)"; + $.auditMessage.className = "nominal"; + return false; +} + +function advanceDay() { + let resources = loadResources(); + + // 1. Consumption (Natural Game Progression) + resources.food -= resources.army * 1; // Army consumes food + resources.health -= 1; // Daily health attrition + + // 2. Negative Value Penalty + if (resources.food < 0 || resources.health < 0) { + gameOver("๐Ÿ‘‘ **Game Over:** Resource consumption led to collapse."); + return; + } + + // 3. Audit Check (Happens every day) + if (checkAudit(resources)) return; + + // 4. Save and Update + saveResources(resources); + updateUI(resources); +} + +// --- 5. Action Handlers --- + +function collectTax() { + let resources = loadResources(); + resources.gold += 10 + Math.random(); // Add a random float to encourage non-rounded cheating + saveResources(resources); + updateUI(resources); +} + +function trainArmy() { + let resources = loadResources(); + if (resources.gold >= 20) { + resources.gold -= 20; + resources.army += 1; + saveResources(resources); + } else { + alert("Not enough gold to train army!"); + } + updateUI(resources); +} + +// --- 6. UI and Game End --- + +function updateUI(resources) { + if (!resources) resources = loadResources(); + + $.resGold.textContent = resources.gold.toFixed(2); + $.resHealth.textContent = resources.health.toFixed(0); + $.resFood.textContent = resources.food.toFixed(0); + $.resArmy.textContent = resources.army.toFixed(0); +} + +function gameOver(message) { + // Disable actions + $.collectTaxBtn.disabled = true; + $.trainArmyBtn.disabled = true; + $.advanceDayBtn.disabled = true; + + // Display failure message + $.auditMessage.innerHTML = message; + $.auditMessage.className = "warning"; + + alert(message + "\n\nTo try again, manually clear all game cookies and refresh."); +} + + +// --- 7. Initialization --- + +$.collectTaxBtn.addEventListener('click', collectTax); +$.trainArmyBtn.addEventListener('click', trainArmy); +$.advanceDayBtn.addEventListener('click', advanceDay); + +// Initial load and render +updateUI(); \ No newline at end of file diff --git a/games/cookie_cutter/style.css b/games/cookie_cutter/style.css new file mode 100644 index 00000000..bc117b4d --- /dev/null +++ b/games/cookie_cutter/style.css @@ -0,0 +1,102 @@ +:root { + --bg-color: #282a36; /* Dark background */ + --text-color: #f8f8f2; + --accent-color: #ffb86c; /* Orange/Gold */ + --success-color: #50fa7b; /* Green */ + --audit-color: #ff5555; /* Red/Danger */ +} + +/* Base Styles */ +body { + font-family: 'Georgia', serif; + background-color: var(--bg-color); + color: var(--text-color); + margin: 0; + padding: 20px; + display: flex; + justify-content: center; +} + +#game-container { + width: 90%; + max-width: 900px; + background-color: #3d3f4b; + padding: 30px; + border-radius: 10px; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); +} + +header { + text-align: center; + border-bottom: 2px solid var(--accent-color); + padding-bottom: 10px; + margin-bottom: 20px; +} + +/* Resource Panel */ +#resource-panel { + margin-bottom: 30px; +} + +.resource-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20px; + text-align: center; +} + +.stat-item { + background-color: #44475a; + padding: 15px; + border-radius: 6px; + font-size: 1.2em; + font-weight: bold; +} + +/* Action and Audit Panel */ +#audit-status { + padding: 15px; + margin-bottom: 20px; + border: 2px solid var(--success-color); + background-color: #2a2c33; +} + +.nominal { color: var(--success-color); } +.warning { + color: var(--audit-color); + font-weight: bold; + animation: blink 1s infinite; +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.action-controls { + display: flex; + justify-content: space-around; + gap: 10px; +} + +button { + padding: 10px 15px; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: bold; + background-color: var(--accent-color); + color: var(--bg-color); + transition: opacity 0.2s; +} + +/* Instruction Panel */ +#instruction-panel { + margin-top: 40px; + border-top: 1px dashed var(--text-color); + padding-top: 20px; +} + +#instruction-panel p { + color: var(--accent-color); +} \ No newline at end of file diff --git a/games/cooking-challenge/index.html b/games/cooking-challenge/index.html new file mode 100644 index 00000000..1be5d206 --- /dev/null +++ b/games/cooking-challenge/index.html @@ -0,0 +1,57 @@ + + + + + + Cooking Challenge + + + +
+

๐Ÿ‘จโ€๐Ÿณ Cooking Challenge

+

Prepare meals by following recipes! Complete each step before time runs out.

+ +
+
Score: 0
+
Level: 1
+
Time: 30s
+
Step: 1/5
+
+ +
+

Pasta Carbonara

+

Click on the ingredients needed for this step

+
+ +
+
+ +
+
+ +
+
+ +
+ + + +
+ +
+ +
+

How to Play:

+
    +
  • Follow the recipe instructions step by step
  • +
  • Click on the correct ingredients and tools when prompted
  • +
  • Complete each step before the timer runs out
  • +
  • Earn points for speed and accuracy
  • +
  • Use hints if you get stuck (costs points)
  • +
+
+
+ + + + \ No newline at end of file diff --git a/games/cooking-challenge/script.js b/games/cooking-challenge/script.js new file mode 100644 index 00000000..72ff384e --- /dev/null +++ b/games/cooking-challenge/script.js @@ -0,0 +1,268 @@ +// Cooking Challenge Game +// Follow recipes step by step to prepare delicious meals + +// DOM elements +const scoreEl = document.getElementById('current-score'); +const levelEl = document.getElementById('current-level'); +const timerEl = document.getElementById('time-left'); +const stepEl = document.getElementById('current-step'); +const totalStepsEl = document.getElementById('total-steps'); +const recipeNameEl = document.getElementById('recipe-name'); +const instructionEl = document.getElementById('current-instruction'); +const ingredientsArea = document.getElementById('ingredients-area'); +const toolsArea = document.getElementById('tools-area'); +const messageEl = document.getElementById('message'); +const startBtn = document.getElementById('start-btn'); +const hintBtn = document.getElementById('hint-btn'); +const resetBtn = document.getElementById('reset-btn'); + +// Game variables +let score = 0; +let level = 1; +let currentRecipe = null; +let currentStepIndex = 0; +let timeLeft = 30; +let gameRunning = false; +let timerInterval; + +// Available ingredients and tools +const ingredients = [ + '๐Ÿ… Tomato', '๐Ÿง… Onion', '๐Ÿฅ• Carrot', '๐Ÿฅฌ Lettuce', '๐Ÿ Pasta', + '๐Ÿฅš Egg', '๐Ÿฅ› Milk', '๐Ÿง€ Cheese', '๐Ÿ– Meat', '๐ŸŸ Fish', + '๐Ÿž Bread', '๐Ÿฅ” Potato', '๐ŸŒฝ Corn', '๐Ÿ„ Mushroom', '๐ŸŒ Banana' +]; + +const tools = [ + '๐Ÿ”ช Knife', '๐Ÿณ Pan', '๐Ÿฅ„ Spoon', '๐Ÿด Fork', 'โฒ๏ธ Timer', + '๐Ÿฅฃ Bowl', '๐Ÿงฐ Whisk', '๐Ÿ”ฅ Stove', '๐ŸงŠ Fridge', '๐Ÿ—‘๏ธ Trash' +]; + +// Recipes with steps +const recipes = [ + { + name: 'Pasta Carbonara', + steps: [ + { instruction: 'Gather pasta and eggs', required: ['๐Ÿ Pasta', '๐Ÿฅš Egg'], type: 'ingredients' }, + { instruction: 'Get cheese and pepper', required: ['๐Ÿง€ Cheese', '๐Ÿง‚ Salt'], type: 'ingredients' }, + { instruction: 'Use pan to cook pasta', required: ['๐Ÿณ Pan'], type: 'tools' }, + { instruction: 'Mix with spoon', required: ['๐Ÿฅ„ Spoon'], type: 'tools' }, + { instruction: 'Serve with fork', required: ['๐Ÿด Fork'], type: 'tools' } + ] + }, + { + name: 'Grilled Cheese Sandwich', + steps: [ + { instruction: 'Get bread and cheese', required: ['๐Ÿž Bread', '๐Ÿง€ Cheese'], type: 'ingredients' }, + { instruction: 'Use knife to prepare', required: ['๐Ÿ”ช Knife'], type: 'tools' }, + { instruction: 'Cook in pan', required: ['๐Ÿณ Pan'], type: 'tools' }, + { instruction: 'Flip with spatula', required: ['๐Ÿฅ„ Spoon'], type: 'tools' }, + { instruction: 'Serve on plate', required: ['๐Ÿฝ๏ธ Plate'], type: 'tools' } + ] + }, + { + name: 'Fruit Salad', + steps: [ + { instruction: 'Select fruits', required: ['๐Ÿ… Tomato', '๐ŸŒ Banana', '๐ŸŠ Orange'], type: 'ingredients' }, + { instruction: 'Get bowl for mixing', required: ['๐Ÿฅฃ Bowl'], type: 'tools' }, + { instruction: 'Use knife to cut', required: ['๐Ÿ”ช Knife'], type: 'tools' }, + { instruction: 'Mix with spoon', required: ['๐Ÿฅ„ Spoon'], type: 'tools' }, + { instruction: 'Chill in fridge', required: ['๐ŸงŠ Fridge'], type: 'tools' } + ] + } +]; + +// Initialize game +function initGame() { + createKitchen(); + loadRecipe(); +} + +// Create kitchen items +function createKitchen() { + // Create ingredients grid + const ingredientsGrid = document.createElement('div'); + ingredientsGrid.className = 'item-grid'; + ingredientsGrid.innerHTML = '

Ingredients

'; + + ingredients.forEach(item => { + const itemEl = document.createElement('div'); + itemEl.className = 'item'; + itemEl.textContent = item; + itemEl.addEventListener('click', () => selectItem(item, 'ingredients')); + ingredientsGrid.appendChild(itemEl); + }); + + ingredientsArea.appendChild(ingredientsGrid); + + // Create tools grid + const toolsGrid = document.createElement('div'); + toolsGrid.className = 'item-grid'; + toolsGrid.innerHTML = '

Tools

'; + + tools.forEach(item => { + const itemEl = document.createElement('div'); + itemEl.className = 'item'; + itemEl.textContent = item; + itemEl.addEventListener('click', () => selectItem(item, 'tools')); + toolsGrid.appendChild(itemEl); + }); + + toolsArea.appendChild(toolsGrid); +} + +// Load a random recipe +function loadRecipe() { + currentRecipe = recipes[Math.floor(Math.random() * recipes.length)]; + currentStepIndex = 0; + recipeNameEl.textContent = currentRecipe.name; + totalStepsEl.textContent = currentRecipe.steps.length; + loadStep(); +} + +// Load current step +function loadStep() { + const step = currentRecipe.steps[currentStepIndex]; + stepEl.textContent = currentStepIndex + 1; + instructionEl.textContent = step.instruction; + timeLeft = 30; // Reset timer for each step + timerEl.textContent = timeLeft; + + // Reset item states + document.querySelectorAll('.item').forEach(item => { + item.classList.remove('selected', 'correct', 'wrong'); + }); + + if (gameRunning) { + startStepTimer(); + } +} + +// Select an item +function selectItem(itemName, type) { + if (!gameRunning) return; + + const currentStep = currentRecipe.steps[currentStepIndex]; + const isRequired = currentStep.required.includes(itemName); + const isCorrectType = currentStep.type === type; + + if (isRequired && isCorrectType) { + // Correct selection + score += Math.max(10, timeLeft * 2); // Bonus for speed + scoreEl.textContent = score; + + // Mark as correct + document.querySelectorAll('.item').forEach(item => { + if (item.textContent === itemName) { + item.classList.add('correct'); + } + }); + + // Move to next step + setTimeout(() => { + currentStepIndex++; + if (currentStepIndex >= currentRecipe.steps.length) { + levelComplete(); + } else { + loadStep(); + } + }, 1000); + + } else { + // Wrong selection + document.querySelectorAll('.item').forEach(item => { + if (item.textContent === itemName) { + item.classList.add('wrong'); + } + }); + + messageEl.textContent = 'Wrong item! Try again.'; + setTimeout(() => messageEl.textContent = '', 2000); + } +} + +// Start the game +function startGame() { + score = 0; + level = 1; + scoreEl.textContent = score; + levelEl.textContent = level; + gameRunning = true; + startBtn.style.display = 'none'; + loadRecipe(); +} + +// Start timer for current step +function startStepTimer() { + clearInterval(timerInterval); + timerInterval = setInterval(() => { + timeLeft--; + timerEl.textContent = timeLeft; + + if (timeLeft <= 0) { + stepFailed(); + } + }, 1000); +} + +// Step failed (time up) +function stepFailed() { + clearInterval(timerInterval); + messageEl.textContent = 'Time\'s up! Step failed.'; + gameRunning = false; + resetBtn.style.display = 'inline-block'; +} + +// Level complete +function levelComplete() { + clearInterval(timerInterval); + gameRunning = false; + level++; + levelEl.textContent = level; + messageEl.textContent = `Recipe complete! Level ${level} unlocked.`; + setTimeout(() => { + messageEl.textContent = 'Get ready for next recipe...'; + setTimeout(() => { + startGame(); + }, 2000); + }, 3000); +} + +// Show hint +function showHint() { + if (!gameRunning || score < 20) return; + + score -= 20; + scoreEl.textContent = score; + + const currentStep = currentRecipe.steps[currentStepIndex]; + const hintItem = currentStep.required[0]; // Show first required item + + messageEl.textContent = `Hint: Try using ${hintItem}`; + setTimeout(() => messageEl.textContent = '', 3000); +} + +// Reset game +function resetGame() { + clearInterval(timerInterval); + score = 0; + level = 1; + gameRunning = false; + scoreEl.textContent = score; + levelEl.textContent = level; + messageEl.textContent = ''; + resetBtn.style.display = 'none'; + startBtn.style.display = 'inline-block'; + loadRecipe(); +} + +// Event listeners +startBtn.addEventListener('click', startGame); +hintBtn.addEventListener('click', showHint); +resetBtn.addEventListener('click', resetGame); + +// Initialize +initGame(); + +// This cooking game was fun to make +// I enjoyed creating the recipe system and item selection mechanics +// Could add more recipes or cooking mini-games later \ No newline at end of file diff --git a/games/cooking-challenge/style.css b/games/cooking-challenge/style.css new file mode 100644 index 00000000..e8fd4ccb --- /dev/null +++ b/games/cooking-challenge/style.css @@ -0,0 +1,207 @@ +/* Cooking Challenge Game Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #ff9a9e, #fecfef); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + color: white; +} + +.container { + text-align: center; + max-width: 1000px; + padding: 20px; +} + +h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +p { + font-size: 1.1em; + margin-bottom: 20px; + opacity: 0.9; +} + +.game-info { + display: flex; + justify-content: space-around; + flex-wrap: wrap; + margin: 20px 0; + font-size: 1.1em; + font-weight: bold; + background: rgba(255, 255, 255, 0.1); + padding: 15px; + border-radius: 10px; +} + +.recipe-section { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + margin: 20px 0; +} + +.recipe-section h3 { + color: #ffeaa7; + margin-bottom: 10px; +} + +.recipe-section p { + font-size: 1.2em; + font-weight: bold; +} + +.kitchen-area { + display: flex; + justify-content: space-between; + margin: 20px 0; + gap: 20px; +} + +.ingredients, .tools { + flex: 1; + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; +} + +.ingredients h4, .tools h4 { + margin-bottom: 15px; + color: #ffeaa7; +} + +.item-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); + gap: 10px; +} + +.item { + background: #fff; + border-radius: 8px; + padding: 10px; + cursor: pointer; + transition: all 0.3s; + text-align: center; + font-weight: bold; + color: #333; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.item:hover { + transform: scale(1.05); + box-shadow: 0 4px 8px rgba(0,0,0,0.2); +} + +.item.correct { + background: #27ae60; + color: white; +} + +.item.wrong { + background: #e74c3c; + color: white; +} + +.item.selected { + border: 3px solid #f39c12; +} + +.controls { + margin: 20px 0; +} + +button { + background: #27ae60; + color: white; + border: none; + padding: 12px 24px; + font-size: 1em; + border-radius: 5px; + cursor: pointer; + margin: 0 10px; + transition: background 0.3s; +} + +button:hover { + background: #229954; +} + +#hint-btn { + background: #f39c12; +} + +#hint-btn:hover { + background: #e67e22; +} + +#message { + font-size: 1.2em; + margin: 20px 0; + min-height: 30px; + font-weight: bold; +} + +.instructions { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + margin-top: 20px; + text-align: left; + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.instructions h3 { + margin-bottom: 10px; + color: #ffeaa7; +} + +.instructions ul { + list-style-type: disc; + padding-left: 20px; +} + +.instructions li { + margin: 5px 0; + line-height: 1.4; +} + +/* Responsive design */ +@media (max-width: 768px) { + .kitchen-area { + flex-direction: column; + } + + .item-grid { + grid-template-columns: repeat(auto-fit, minmax(60px, 1fr)); + } + + .game-info { + font-size: 1em; + } + + .controls { + display: flex; + flex-direction: column; + gap: 10px; + } + + button { + width: 100%; + max-width: 200px; + } +} \ No newline at end of file diff --git a/games/cosmic-defender/index.html b/games/cosmic-defender/index.html new file mode 100644 index 00000000..b5f47014 --- /dev/null +++ b/games/cosmic-defender/index.html @@ -0,0 +1,103 @@ + + + + + + Cosmic Defender + + + +
+
+
+ +
+

Cosmic Defender

+

Protect the galaxy from alien invasion

+
+ +
+
+
+
+
+
+
+
+ +
+
+
SCORE
+
0
+
+
+
LIVES
+
3
+
+
+
WAVE
+
1
+
+
+
POWER
+
NORMAL
+
+
+
+ +
+
+ + + +
+ +
+
+ + + + +
+ +
+
+ +
+
+ โ† โ†’ + Move Ship +
+
+ SPACE + Fire Laser +
+
+ P + Pause Game +
+
+ +
+
+

MISSION FAILED

+
+
Final Score: 0
+
Wave Reached: 1
+
Enemies Destroyed: 0
+
+ +
+
+ +
+
+

INCOMING WAVE

+
1
+
+
+
+ + + + \ No newline at end of file diff --git a/games/cosmic-defender/script.js b/games/cosmic-defender/script.js new file mode 100644 index 00000000..4e9d396e --- /dev/null +++ b/games/cosmic-defender/script.js @@ -0,0 +1,507 @@ +class CosmicDefender { + constructor() { + this.gameActive = false; + this.gamePaused = false; + this.score = 0; + this.lives = 3; + this.wave = 1; + this.powerLevel = 1; + this.enemies = []; + this.projectiles = []; + this.enemyProjectiles = []; + this.powerups = []; + this.keys = {}; + this.lastShot = 0; + this.shotDelay = 300; + this.enemySpawnRate = 2000; + this.lastEnemySpawn = 0; + this.lastPowerupSpawn = 0; + this.powerupSpawnRate = 10000; + this.enemiesKilled = 0; + + this.gameScreen = document.getElementById('gameScreen'); + this.playerShip = document.getElementById('playerShip'); + this.enemiesContainer = document.getElementById('enemiesContainer'); + this.projectilesContainer = document.getElementById('projectilesContainer'); + this.powerupsContainer = document.getElementById('powerupsContainer'); + + this.initializeGame(); + this.setupEventListeners(); + } + + initializeGame() { + this.updateHUD(); + this.playerShip.style.left = '50%'; + } + + startGame() { + if (this.gameActive) return; + + this.gameActive = true; + this.gamePaused = false; + this.score = 0; + this.lives = 3; + this.wave = 1; + this.powerLevel = 1; + this.enemies = []; + this.projectiles = []; + this.enemyProjectiles = []; + this.powerups = []; + this.enemiesKilled = 0; + + this.updateHUD(); + this.hideGameOver(); + this.showWaveAlert(); + this.gameLoop(); + } + + pauseGame() { + if (!this.gameActive) return; + this.gamePaused = !this.gamePaused; + } + + gameLoop() { + if (!this.gameActive || this.gamePaused) return; + + this.updatePlayer(); + this.spawnEnemies(); + this.spawnPowerups(); + this.updateProjectiles(); + this.updateEnemies(); + this.updatePowerups(); + this.checkCollisions(); + this.updateHUD(); + + requestAnimationFrame(() => this.gameLoop()); + } + + updatePlayer() { + if (this.keys['ArrowLeft'] || this.keys['KeyA']) { + this.movePlayer(-8); + } + if (this.keys['ArrowRight'] || this.keys['KeyD']) { + this.movePlayer(8); + } + if (this.keys['Space'] && Date.now() - this.lastShot > this.shotDelay) { + this.shoot(); + } + } + + movePlayer(deltaX) { + const rect = this.gameScreen.getBoundingClientRect(); + const playerRect = this.playerShip.getBoundingClientRect(); + const currentLeft = parseInt(this.playerShip.style.left) || 400; + const newLeft = Math.max(20, Math.min(rect.width - 20, currentLeft + deltaX)); + + this.playerShip.style.left = `${newLeft}px`; + } + + shoot() { + this.lastShot = Date.now(); + const playerRect = this.playerShip.getBoundingClientRect(); + + for (let i = 0; i < this.powerLevel; i++) { + const offset = (i - (this.powerLevel - 1) / 2) * 15; + this.createProjectile( + playerRect.left + playerRect.width / 2 + offset - 2, + playerRect.top - 10, + false + ); + } + } + + createProjectile(x, y, isEnemy) { + const projectile = document.createElement('div'); + projectile.className = isEnemy ? 'projectile enemy-projectile' : 'projectile'; + projectile.style.left = `${x}px`; + projectile.style.top = `${y}px`; + + if (isEnemy) { + this.enemyProjectiles.push({ + element: projectile, + x: x, + y: y, + speed: 5 + }); + } else { + this.projectiles.push({ + element: projectile, + x: x, + y: y, + speed: -8 + }); + } + + this.projectilesContainer.appendChild(projectile); + } + + spawnEnemies() { + if (Date.now() - this.lastEnemySpawn > this.enemySpawnRate) { + this.lastEnemySpawn = Date.now(); + + const enemyCount = Math.min(5 + this.wave, 15); + for (let i = 0; i < enemyCount; i++) { + setTimeout(() => { + if (this.gameActive && !this.gamePaused) { + this.createEnemy(); + } + }, i * 300); + } + } + } + + createEnemy() { + const enemy = document.createElement('div'); + enemy.className = 'enemy'; + + const x = Math.random() * (this.gameScreen.offsetWidth - 40) + 20; + enemy.style.left = `${x}px`; + enemy.style.top = '-40px'; + + const enemyObj = { + element: enemy, + x: x, + y: -40, + speed: 1 + this.wave * 0.2, + health: 1, + lastShot: Date.now(), + shotDelay: 1500 + Math.random() * 1000 + }; + + this.enemies.push(enemyObj); + this.enemiesContainer.appendChild(enemy); + } + + spawnPowerups() { + if (Date.now() - this.lastPowerupSpawn > this.powerupSpawnRate && Math.random() < 0.3) { + this.lastPowerupSpawn = Date.now(); + this.createPowerup(); + } + } + + createPowerup() { + const powerup = document.createElement('div'); + powerup.className = 'powerup'; + + const x = Math.random() * (this.gameScreen.offsetWidth - 30) + 15; + powerup.style.left = `${x}px`; + powerup.style.top = '-30px'; + + const powerupObj = { + element: powerup, + x: x, + y: -30, + speed: 2, + type: Math.random() < 0.7 ? 'power' : 'life' + }; + + this.powerups.push(powerupObj); + this.powerupsContainer.appendChild(powerup); + } + + updateProjectiles() { + this.projectiles.forEach((projectile, index) => { + projectile.y += projectile.speed; + projectile.element.style.top = `${projectile.y}px`; + + if (projectile.y < -20) { + projectile.element.remove(); + this.projectiles.splice(index, 1); + } + }); + + this.enemyProjectiles.forEach((projectile, index) => { + projectile.y += projectile.speed; + projectile.element.style.top = `${projectile.y}px`; + + if (projectile.y > this.gameScreen.offsetHeight) { + projectile.element.remove(); + this.enemyProjectiles.splice(index, 1); + } + }); + } + + updateEnemies() { + this.enemies.forEach((enemy, index) => { + enemy.y += enemy.speed; + enemy.element.style.top = `${enemy.y}px`; + + if (enemy.y > this.gameScreen.offsetHeight) { + enemy.element.remove(); + this.enemies.splice(index, 1); + } else if (Date.now() - enemy.lastShot > enemy.shotDelay) { + enemy.lastShot = Date.now(); + this.enemyShoot(enemy); + } + }); + } + + updatePowerups() { + this.powerups.forEach((powerup, index) => { + powerup.y += powerup.speed; + powerup.element.style.top = `${powerup.y}px`; + + if (powerup.y > this.gameScreen.offsetHeight) { + powerup.element.remove(); + this.powerups.splice(index, 1); + } + }); + } + + enemyShoot(enemy) { + const enemyRect = enemy.element.getBoundingClientRect(); + const gameRect = this.gameScreen.getBoundingClientRect(); + + this.createProjectile( + enemyRect.left + enemyRect.width / 2 - 2, + enemyRect.bottom, + true + ); + } + + checkCollisions() { + this.checkPlayerProjectileCollisions(); + this.checkPlayerEnemyCollisions(); + this.checkPlayerPowerupCollisions(); + } + + checkPlayerProjectileCollisions() { + const playerRect = this.playerShip.getBoundingClientRect(); + + this.enemyProjectiles.forEach((projectile, pIndex) => { + const projRect = projectile.element.getBoundingClientRect(); + + if (this.rectIntersect(playerRect, projRect)) { + this.takeDamage(); + projectile.element.remove(); + this.enemyProjectiles.splice(pIndex, 1); + } + }); + } + + checkPlayerEnemyCollisions() { + const playerRect = this.playerShip.getBoundingClientRect(); + + this.enemies.forEach((enemy, eIndex) => { + const enemyRect = enemy.element.getBoundingClientRect(); + + if (this.rectIntersect(playerRect, enemyRect)) { + this.takeDamage(); + this.createExplosion(enemy.x, enemy.y); + enemy.element.remove(); + this.enemies.splice(eIndex, 1); + } + }); + } + + checkPlayerPowerupCollisions() { + const playerRect = this.playerShip.getBoundingClientRect(); + + this.powerups.forEach((powerup, pIndex) => { + const powerupRect = powerup.element.getBoundingClientRect(); + + if (this.rectIntersect(playerRect, powerupRect)) { + this.collectPowerup(powerup.type); + powerup.element.remove(); + this.powerups.splice(pIndex, 1); + } + }); + + this.projectiles.forEach((projectile, pIndex) => { + this.enemies.forEach((enemy, eIndex) => { + const projRect = projectile.element.getBoundingClientRect(); + const enemyRect = enemy.element.getBoundingClientRect(); + + if (this.rectIntersect(projRect, enemyRect)) { + enemy.health--; + + if (enemy.health <= 0) { + this.score += 100 * this.wave; + this.enemiesKilled++; + this.createExplosion(enemy.x, enemy.y); + enemy.element.remove(); + this.enemies.splice(eIndex, 1); + + if (this.enemies.length === 0) { + this.nextWave(); + } + } + + projectile.element.remove(); + this.projectiles.splice(pIndex, 1); + } + }); + }); + } + + collectPowerup(type) { + if (type === 'power') { + this.powerLevel = Math.min(this.powerLevel + 1, 5); + } else if (type === 'life') { + this.lives = Math.min(this.lives + 1, 5); + } + } + + takeDamage() { + this.lives--; + this.createExplosion( + parseInt(this.playerShip.style.left), + this.gameScreen.offsetHeight - 50 + ); + + if (this.lives <= 0) { + this.gameOver(); + } + } + + nextWave() { + this.wave++; + this.showWaveAlert(); + this.enemySpawnRate = Math.max(500, 2000 - this.wave * 100); + } + + createExplosion(x, y) { + const explosion = document.createElement('div'); + explosion.className = 'explosion'; + explosion.style.left = `${x - 25}px`; + explosion.style.top = `${y - 25}px`; + + this.gameScreen.appendChild(explosion); + + setTimeout(() => { + explosion.remove(); + }, 500); + } + + rectIntersect(rect1, rect2) { + return !(rect2.left > rect1.right || + rect2.right < rect1.left || + rect2.top > rect1.bottom || + rect2.bottom < rect1.top); + } + + updateHUD() { + document.getElementById('score').textContent = this.score; + document.getElementById('lives').textContent = this.lives; + document.getElementById('wave').textContent = this.wave; + + let powerText = 'NORMAL'; + if (this.powerLevel >= 4) powerText = 'ULTRA'; + else if (this.powerLevel >= 3) powerText = 'SUPER'; + else if (this.powerLevel >= 2) powerText = 'POWER'; + + document.getElementById('power').textContent = powerText; + } + + showWaveAlert() { + const waveAlert = document.getElementById('waveAlert'); + const waveNumber = document.getElementById('waveNumber'); + + waveNumber.textContent = this.wave; + waveAlert.classList.add('active'); + + setTimeout(() => { + waveAlert.classList.remove('active'); + }, 2000); + } + + gameOver() { + this.gameActive = false; + + document.getElementById('finalScore').textContent = this.score; + document.getElementById('finalWave').textContent = this.wave; + document.getElementById('finalKills').textContent = this.enemiesKilled; + + this.showGameOver(); + + this.enemies.forEach(enemy => enemy.element.remove()); + this.projectiles.forEach(projectile => projectile.element.remove()); + this.enemyProjectiles.forEach(projectile => projectile.element.remove()); + this.powerups.forEach(powerup => powerup.element.remove()); + + this.enemies = []; + this.projectiles = []; + this.enemyProjectiles = []; + this.powerups = []; + } + + showGameOver() { + document.getElementById('gameOver').classList.add('active'); + } + + hideGameOver() { + document.getElementById('gameOver').classList.remove('active'); + } + + setupEventListeners() { + document.addEventListener('keydown', (e) => { + this.keys[e.code] = true; + + if (e.code === 'KeyP') { + this.pauseGame(); + } + }); + + document.addEventListener('keyup', (e) => { + this.keys[e.code] = false; + }); + + document.getElementById('startBtn').addEventListener('click', () => { + this.startGame(); + }); + + document.getElementById('pauseBtn').addEventListener('click', () => { + this.pauseGame(); + }); + + document.getElementById('restartBtn').addEventListener('click', () => { + this.startGame(); + }); + + document.getElementById('retryBtn').addEventListener('click', () => { + this.startGame(); + }); + + document.querySelectorAll('.move-btn').forEach(btn => { + btn.addEventListener('mousedown', (e) => { + this.keys[`Arrow${e.target.dataset.direction.toUpperCase()}`] = true; + }); + + btn.addEventListener('mouseup', (e) => { + this.keys[`Arrow${e.target.dataset.direction.toUpperCase()}`] = false; + }); + + btn.addEventListener('touchstart', (e) => { + e.preventDefault(); + this.keys[`Arrow${e.target.dataset.direction.toUpperCase()}`] = true; + }); + + btn.addEventListener('touchend', (e) => { + e.preventDefault(); + this.keys[`Arrow${e.target.dataset.direction.toUpperCase()}`] = false; + }); + }); + + document.querySelector('.shoot-btn').addEventListener('mousedown', () => { + this.keys['Space'] = true; + }); + + document.querySelector('.shoot-btn').addEventListener('mouseup', () => { + this.keys['Space'] = false; + }); + + document.querySelector('.shoot-btn').addEventListener('touchstart', (e) => { + e.preventDefault(); + this.keys['Space'] = true; + }); + + document.querySelector('.shoot-btn').addEventListener('touchend', (e) => { + e.preventDefault(); + this.keys['Space'] = false; + }); + } +} + +window.addEventListener('load', () => { + new CosmicDefender(); +}); \ No newline at end of file diff --git a/games/cosmic-defender/style.css b/games/cosmic-defender/style.css new file mode 100644 index 00000000..7c1b8324 --- /dev/null +++ b/games/cosmic-defender/style.css @@ -0,0 +1,471 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; +} + +body { + background: #000; + color: #fff; + min-height: 100vh; + overflow: hidden; +} + +.game-container { + position: relative; + width: 100vw; + height: 100vh; + background: linear-gradient(180deg, #0c1445 0%, #1a237e 50%, #311b92 100%); +} + +.stars { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: transparent; +} + +.stars::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: + radial-gradient(2px 2px at 20px 30px, #eee, transparent), + radial-gradient(2px 2px at 40px 70px, #fff, transparent), + radial-gradient(1px 1px at 90px 40px, #fff, transparent), + radial-gradient(1px 1px at 130px 80px, #fff, transparent), + radial-gradient(2px 2px at 160px 30px, #ddd, transparent); + background-repeat: repeat; + background-size: 200px 100px; + animation: starsMove 50s linear infinite; +} + +.stars2::before { + animation: starsMove 100s linear infinite; + opacity: 0.5; +} + +@keyframes starsMove { + from { transform: translateY(0); } + to { transform: translateY(-100px); } +} + +header { + position: absolute; + top: 20px; + left: 0; + right: 0; + text-align: center; + z-index: 10; +} + +h1 { + font-size: 3rem; + font-weight: 700; + margin-bottom: 5px; + background: linear-gradient(45deg, #00bcd4, #2196f3, #3f51b5); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + text-shadow: 0 0 30px rgba(33, 150, 243, 0.5); +} + +.subtitle { + font-size: 1rem; + opacity: 0.8; + font-weight: 300; +} + +.game-screen { + position: absolute; + top: 120px; + left: 50%; + transform: translateX(-50%); + width: 800px; + height: 500px; + background: rgba(0, 0, 0, 0.3); + border: 2px solid rgba(33, 150, 243, 0.5); + border-radius: 10px; + overflow: hidden; + box-shadow: 0 0 50px rgba(33, 150, 243, 0.2); +} + +.player-ship { + position: absolute; + bottom: 30px; + left: 50%; + transform: translateX(-50%); + width: 40px; + height: 50px; + z-index: 20; +} + +.ship-core { + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 30px; + height: 40px; + background: linear-gradient(45deg, #2196f3, #00bcd4); + clip-path: polygon(50% 0%, 100% 100%, 0% 100%); + box-shadow: 0 0 20px #2196f3; +} + +.ship-wings { + position: absolute; + bottom: -10px; + left: 50%; + transform: translateX(-50%); + width: 50px; + height: 15px; + background: linear-gradient(45deg, #ff5722, #ff9800); + clip-path: polygon(20% 0%, 80% 0%, 100% 100%, 0% 100%); + box-shadow: 0 0 15px #ff5722; +} + +.enemy { + position: absolute; + width: 30px; + height: 30px; + background: linear-gradient(45deg, #f44336, #ff9800); + border-radius: 50%; + box-shadow: 0 0 15px #f44336; +} + +.enemy::before { + content: ''; + position: absolute; + top: 5px; + left: 5px; + right: 5px; + bottom: 5px; + background: #ff5722; + border-radius: 50%; + box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5); +} + +.projectile { + position: absolute; + width: 4px; + height: 15px; + background: linear-gradient(to top, #00bcd4, #2196f3); + border-radius: 2px; + box-shadow: 0 0 10px #2196f3; +} + +.enemy-projectile { + background: linear-gradient(to bottom, #ff5722, #f44336); + box-shadow: 0 0 10px #f44336; +} + +.powerup { + position: absolute; + width: 20px; + height: 20px; + background: linear-gradient(45deg, #4caf50, #8bc34a); + border-radius: 50%; + box-shadow: 0 0 15px #4caf50; + animation: float 2s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-5px); } +} + +.hud { + position: absolute; + top: 10px; + left: 10px; + right: 10px; + display: flex; + justify-content: space-between; + z-index: 15; +} + +.hud-item { + background: rgba(0, 0, 0, 0.6); + padding: 10px 15px; + border-radius: 8px; + border: 1px solid rgba(33, 150, 243, 0.3); + min-width: 100px; + text-align: center; +} + +.hud-label { + font-size: 0.8rem; + opacity: 0.8; + margin-bottom: 5px; +} + +.hud-value { + font-size: 1.2rem; + font-weight: 700; + color: #00bcd4; +} + +.controls { + position: absolute; + bottom: 30px; + left: 0; + right: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; +} + +.control-buttons { + display: flex; + gap: 15px; +} + +.control-btn { + padding: 12px 24px; + border: none; + border-radius: 25px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + min-width: 150px; +} + +.control-btn.primary { + background: linear-gradient(45deg, #2196f3, #00bcd4); + color: white; + box-shadow: 0 6px 20px rgba(33, 150, 243, 0.4); +} + +.control-btn.primary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(33, 150, 243, 0.6); +} + +.control-btn.secondary { + background: rgba(255, 255, 255, 0.1); + color: white; + border: 1px solid rgba(255, 255, 255, 0.3); + backdrop-filter: blur(10px); +} + +.control-btn.secondary:hover { + background: rgba(255, 255, 255, 0.2); + transform: translateY(-2px); +} + +.mobile-controls { + display: none; + gap: 20px; + align-items: center; +} + +.d-pad { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(3, 1fr); + gap: 5px; +} + +.move-btn { + width: 50px; + height: 50px; + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + color: white; + font-size: 1.2rem; + border-radius: 10px; + cursor: pointer; + backdrop-filter: blur(10px); +} + +.move-btn:active { + background: rgba(255, 255, 255, 0.3); +} + +.shoot-btn { + width: 80px; + height: 80px; + background: linear-gradient(45deg, #f44336, #ff5722); + border: none; + border-radius: 50%; + color: white; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + box-shadow: 0 6px 20px rgba(244, 67, 54, 0.4); +} + +.shoot-btn:active { + transform: scale(0.95); +} + +.instructions { + position: absolute; + bottom: 120px; + right: 20px; + display: flex; + flex-direction: column; + gap: 10px; + background: rgba(0, 0, 0, 0.6); + padding: 15px; + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.instruction-item { + display: flex; + align-items: center; + gap: 10px; + font-size: 0.9rem; +} + +.key { + background: rgba(255, 255, 255, 0.2); + padding: 4px 8px; + border-radius: 4px; + font-family: monospace; + min-width: 60px; + text-align: center; + border: 1px solid rgba(255, 255, 255, 0.3); +} + +.game-over { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: none; + justify-content: center; + align-items: center; + z-index: 100; +} + +.game-over.active { + display: flex; +} + +.game-over-content { + background: linear-gradient(135deg, #1a237e, #311b92); + padding: 40px; + border-radius: 20px; + text-align: center; + border: 2px solid #2196f3; + box-shadow: 0 0 50px rgba(33, 150, 243, 0.5); + max-width: 400px; + width: 90%; +} + +.game-over-content h2 { + font-size: 2.5rem; + margin-bottom: 20px; + color: #f44336; +} + +.final-stats { + margin-bottom: 30px; + font-size: 1.1rem; +} + +.final-stats div { + margin-bottom: 10px; +} + +.final-stats span { + color: #00bcd4; + font-weight: 600; +} + +.wave-alert { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.8); + padding: 30px; + border-radius: 15px; + border: 2px solid #2196f3; + display: none; + z-index: 50; + text-align: center; +} + +.wave-alert.active { + display: block; + animation: wavePulse 0.5s ease-in-out; +} + +@keyframes wavePulse { + 0%, 100% { transform: translate(-50%, -50%) scale(1); } + 50% { transform: translate(-50%, -50%) scale(1.1); } +} + +.wave-content h3 { + color: #00bcd4; + margin-bottom: 10px; + font-size: 1.5rem; +} + +.wave-number { + font-size: 3rem; + font-weight: 700; + color: #2196f3; + text-shadow: 0 0 20px rgba(33, 150, 243, 0.7); +} + +.explosion { + position: absolute; + width: 50px; + height: 50px; + background: radial-gradient(circle, #ff9800, #f44336, transparent); + border-radius: 50%; + animation: explode 0.5s ease-out forwards; +} + +@keyframes explode { + 0% { + transform: scale(0); + opacity: 1; + } + 100% { + transform: scale(2); + opacity: 0; + } +} + +@media (max-width: 768px) { + .game-screen { + width: 95vw; + height: 60vh; + } + + h1 { + font-size: 2rem; + } + + .instructions { + display: none; + } + + .mobile-controls { + display: flex; + } + + .control-buttons { + flex-direction: column; + gap: 10px; + } + + .control-btn { + min-width: 200px; + } +} \ No newline at end of file diff --git a/games/cozy-blocks/index.html b/games/cozy-blocks/index.html new file mode 100644 index 00000000..7b257432 --- /dev/null +++ b/games/cozy-blocks/index.html @@ -0,0 +1,24 @@ + + + + + + Cozy Blocks | Mini JS Games Hub + + + +
+

๐ŸŸฆ Cozy Blocks

+ + +
+
+ Score: 0 +
+ +
+
+ + + + diff --git a/games/cozy-blocks/script.js b/games/cozy-blocks/script.js new file mode 100644 index 00000000..eba1b481 --- /dev/null +++ b/games/cozy-blocks/script.js @@ -0,0 +1,117 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); +canvas.width = 400; +canvas.height = 600; + +let blocks = []; +let baseWidth = 200; +let blockHeight = 25; +let speed = 3; +let currentX = 100; +let direction = 1; +let score = 0; +let isDropping = false; +let gameOver = false; + +const scoreDisplay = document.getElementById("score"); +const restartBtn = document.getElementById("restartBtn"); + +// Initialize base block +blocks.push({ x: 100, y: canvas.height - blockHeight, width: baseWidth }); + +function drawBlock(block, color) { + ctx.fillStyle = color || "#90caf9"; + ctx.fillRect(block.x, block.y, block.width, blockHeight); + ctx.strokeStyle = "#fff"; + ctx.lineWidth = 2; + ctx.strokeRect(block.x, block.y, block.width, blockHeight); +} + +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw stacked blocks + blocks.forEach((block, index) => { + const shade = 180 + index * 2; + drawBlock(block, `hsl(${shade}, 70%, 70%)`); + }); + + // Draw moving block + if (!gameOver) { + ctx.fillStyle = "#42a5f5"; + ctx.fillRect(currentX, getNextY(), blocks[blocks.length - 1].width, blockHeight); + } + + // Update moving block position + if (!isDropping && !gameOver) { + currentX += speed * direction; + if (currentX + blocks[blocks.length - 1].width > canvas.width || currentX < 0) { + direction *= -1; + } + } + + // Display score + scoreDisplay.textContent = score; + + requestAnimationFrame(draw); +} + +function getNextY() { + return canvas.height - (blocks.length + 1) * blockHeight; +} + +function dropBlock() { + if (isDropping || gameOver) return; + isDropping = true; + + const prev = blocks[blocks.length - 1]; + const newY = getNextY(); + const newX = currentX; + const overlap = Math.min(prev.x + prev.width, newX + prev.width) - Math.max(prev.x, newX); + + if (overlap > 0) { + const newWidth = overlap; + const alignedX = Math.max(prev.x, newX); + blocks.push({ x: alignedX, y: newY, width: newWidth }); + + // Perfect alignment bonus + if (Math.abs(prev.x - newX) < 5) { + score += 5; + } else { + score += 1; + } + + speed += 0.2; + isDropping = false; + } else { + // No overlap โ†’ Game Over + gameOver = true; + restartBtn.classList.remove("hidden"); + ctx.fillStyle = "rgba(0,0,0,0.6)"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "#fff"; + ctx.font = "28px Poppins"; + ctx.textAlign = "center"; + ctx.fillText("Game Over!", canvas.width / 2, canvas.height / 2); + ctx.fillText(`Score: ${score}`, canvas.width / 2, canvas.height / 2 + 40); + } +} + +function restartGame() { + blocks = [{ x: 100, y: canvas.height - blockHeight, width: baseWidth }]; + currentX = 100; + direction = 1; + score = 0; + speed = 3; + gameOver = false; + restartBtn.classList.add("hidden"); + isDropping = false; +} + +document.addEventListener("keydown", (e) => { + if (e.code === "Space" || e.code === "ArrowDown") dropBlock(); +}); +canvas.addEventListener("click", dropBlock); +restartBtn.addEventListener("click", restartGame); + +draw(); diff --git a/games/cozy-blocks/style.css b/games/cozy-blocks/style.css new file mode 100644 index 00000000..0721ad23 --- /dev/null +++ b/games/cozy-blocks/style.css @@ -0,0 +1,56 @@ +body { + margin: 0; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + background: linear-gradient(180deg, #c4d7e0, #eaf6f6); + font-family: 'Poppins', sans-serif; +} + +.game-container { + position: relative; + text-align: center; +} + +.title { + font-size: 2rem; + color: #37474f; + margin-bottom: 1rem; +} + +canvas { + background: #fefefe; + border: 3px solid #b0bec5; + border-radius: 10px; + box-shadow: 0 0 15px rgba(0,0,0,0.2); +} + +.ui { + margin-top: 1rem; +} + +.score-board { + font-size: 1.2rem; + color: #37474f; +} + +.btn { + padding: 10px 20px; + margin-top: 1rem; + border: none; + background: #90caf9; + color: #fff; + font-size: 1rem; + border-radius: 6px; + cursor: pointer; + transition: background 0.3s ease; +} + +.btn:hover { + background: #42a5f5; +} + +.hidden { + display: none; +} diff --git a/games/crystal-collector/index.html b/games/crystal-collector/index.html new file mode 100644 index 00000000..fa1ce250 --- /dev/null +++ b/games/crystal-collector/index.html @@ -0,0 +1,40 @@ + + + + + + Crystal Collector + + + + + + + + + +
+
+

CRYSTAL COLLECTOR

+

Navigate the crystal cave maze, gathering glowing crystals while avoiding rolling boulders and patrolling guardians!

+ +

Controls:

+
    +
  • Arrow Keys - Move
  • +
  • Space - Interact
  • +
+ + +
+
+ +
+
Crystals: 0
+
Lives: 3
+
+ + + + + + \ No newline at end of file diff --git a/games/crystal-collector/script.js b/games/crystal-collector/script.js new file mode 100644 index 00000000..27924a8b --- /dev/null +++ b/games/crystal-collector/script.js @@ -0,0 +1,352 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const startButton = document.getElementById('startButton'); +const instructionsOverlay = document.getElementById('instructions-overlay'); +const scoreElement = document.getElementById('score'); +const livesElement = document.getElementById('lives'); + +canvas.width = 800; +canvas.height = 600; + +const TILE_SIZE = 40; +const GRID_WIDTH = canvas.width / TILE_SIZE; +const GRID_HEIGHT = canvas.height / TILE_SIZE; + +let gameRunning = false; +let score = 0; +let lives = 3; +let player; +let crystals = []; +let boulders = []; +let guardians = []; +let maze = []; + +// Player class +class Player { + constructor(x, y) { + this.x = x; + this.y = y; + this.size = TILE_SIZE - 4; + } + + move(dx, dy) { + const newX = this.x + dx; + const newY = this.y + dy; + + if (newX >= 0 && newX < GRID_WIDTH && newY >= 0 && newY < GRID_HEIGHT) { + if (maze[newY][newX] === 0) { // 0 = path + this.x = newX; + this.y = newY; + } + } + } + + draw() { + ctx.fillStyle = '#00ff00'; + ctx.fillRect(this.x * TILE_SIZE + 2, this.y * TILE_SIZE + 2, this.size, this.size); + } +} + +// Crystal class +class Crystal { + constructor(x, y) { + this.x = x; + this.y = y; + this.collected = false; + } + + draw() { + if (!this.collected) { + ctx.fillStyle = '#FFD700'; + ctx.beginPath(); + ctx.arc(this.x * TILE_SIZE + TILE_SIZE/2, this.y * TILE_SIZE + TILE_SIZE/2, 8, 0, Math.PI * 2); + ctx.fill(); + // Glow effect + ctx.shadowColor = '#FFD700'; + ctx.shadowBlur = 10; + ctx.fill(); + ctx.shadowBlur = 0; + } + } +} + +// Boulder class +class Boulder { + constructor(x, y, direction) { + this.x = x; + this.y = y; + this.direction = direction; // 0: right, 1: down, 2: left, 3: up + this.speed = 1; + this.moveCounter = 0; + } + + update() { + this.moveCounter++; + if (this.moveCounter >= 10) { // Move every 10 frames + this.moveCounter = 0; + let newX = this.x; + let newY = this.y; + + switch (this.direction) { + case 0: newX++; break; + case 1: newY++; break; + case 2: newX--; break; + case 3: newY--; break; + } + + if (newX >= 0 && newX < GRID_WIDTH && newY >= 0 && newY < GRID_HEIGHT) { + if (maze[newY][newX] === 0) { + this.x = newX; + this.y = newY; + } else { + // Hit wall, change direction + this.direction = (this.direction + 1) % 4; + } + } else { + // Hit edge, change direction + this.direction = (this.direction + 1) % 4; + } + } + } + + draw() { + ctx.fillStyle = '#666666'; + ctx.fillRect(this.x * TILE_SIZE + 2, this.y * TILE_SIZE + 2, TILE_SIZE - 4, TILE_SIZE - 4); + } +} + +// Guardian class +class Guardian { + constructor(x, y, path) { + this.x = x; + this.y = y; + this.path = path; + this.pathIndex = 0; + this.moveCounter = 0; + } + + update() { + this.moveCounter++; + if (this.moveCounter >= 15) { // Move slower than boulders + this.moveCounter = 0; + this.pathIndex = (this.pathIndex + 1) % this.path.length; + this.x = this.path[this.pathIndex].x; + this.y = this.path[this.pathIndex].y; + } + } + + draw() { + ctx.fillStyle = '#ff0000'; + ctx.fillRect(this.x * TILE_SIZE + 4, this.y * TILE_SIZE + 4, TILE_SIZE - 8, TILE_SIZE - 8); + } +} + +// Generate maze +function generateMaze() { + maze = []; + for (let y = 0; y < GRID_HEIGHT; y++) { + maze[y] = []; + for (let x = 0; x < GRID_WIDTH; x++) { + // Create walls around edges and some internal walls + if (x === 0 || x === GRID_WIDTH - 1 || y === 0 || y === GRID_HEIGHT - 1 || + (x % 3 === 0 && y % 2 === 0) || (x % 5 === 0 && y % 3 === 0)) { + maze[y][x] = 1; // Wall + } else { + maze[y][x] = 0; // Path + } + } + } +} + +// Initialize game +function initGame() { + generateMaze(); + + player = new Player(1, 1); + + crystals = []; + boulders = []; + guardians = []; + + // Place crystals + for (let i = 0; i < 10; i++) { + let x, y; + do { + x = Math.floor(Math.random() * GRID_WIDTH); + y = Math.floor(Math.random() * GRID_HEIGHT); + } while (maze[y][x] === 1 || (x === player.x && y === player.y)); + crystals.push(new Crystal(x, y)); + } + + // Place boulders + for (let i = 0; i < 3; i++) { + let x, y; + do { + x = Math.floor(Math.random() * GRID_WIDTH); + y = Math.floor(Math.random() * GRID_HEIGHT); + } while (maze[y][x] === 1); + boulders.push(new Boulder(x, y, Math.floor(Math.random() * 4))); + } + + // Place guardians with patrol paths + for (let i = 0; i < 2; i++) { + let path = []; + let startX = Math.floor(Math.random() * GRID_WIDTH); + let startY = Math.floor(Math.random() * GRID_HEIGHT); + + // Create simple patrol path + for (let j = 0; j < 4; j++) { + path.push({x: startX + j, y: startY}); + } + + guardians.push(new Guardian(startX, startY, path)); + } +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + update(); + draw(); + + requestAnimationFrame(gameLoop); +} + +// Update game state +function update() { + boulders.forEach(b => b.update()); + guardians.forEach(g => g.update()); + + checkCollisions(); + updateUI(); +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw maze + for (let y = 0; y < GRID_HEIGHT; y++) { + for (let x = 0; x < GRID_WIDTH; x++) { + if (maze[y][x] === 1) { + ctx.fillStyle = '#8B4513'; + ctx.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); + } else { + ctx.fillStyle = '#2d1810'; + ctx.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); + } + } + } + + // Draw game objects + crystals.forEach(c => c.draw()); + boulders.forEach(b => b.draw()); + guardians.forEach(g => g.draw()); + player.draw(); +} + +// Check collisions +function checkCollisions() { + // Player with crystals + crystals.forEach((crystal, index) => { + if (!crystal.collected && crystal.x === player.x && crystal.y === player.y) { + crystal.collected = true; + score++; + } + }); + + // Player with boulders + boulders.forEach(boulder => { + if (boulder.x === player.x && boulder.y === player.y) { + lives--; + if (lives <= 0) { + gameOver(); + } else { + player.x = 1; + player.y = 1; + } + } + }); + + // Player with guardians + guardians.forEach(guardian => { + if (guardian.x === player.x && guardian.y === player.y) { + lives--; + if (lives <= 0) { + gameOver(); + } else { + player.x = 1; + player.y = 1; + } + } + }); + + // Check win condition + const collectedCrystals = crystals.filter(c => c.collected).length; + if (collectedCrystals === crystals.length) { + gameWin(); + } +} + +// Update UI +function updateUI() { + scoreElement.textContent = `Crystals: ${crystals.filter(c => c.collected).length}/${crystals.length}`; + livesElement.textContent = `Lives: ${lives}`; +} + +// Game over +function gameOver() { + gameRunning = false; + alert(`Game Over! You collected ${crystals.filter(c => c.collected).length} crystals.`); + resetGame(); +} + +// Game win +function gameWin() { + gameRunning = false; + alert(`Congratulations! You collected all crystals!`); + resetGame(); +} + +// Reset game +function resetGame() { + score = 0; + lives = 3; + initGame(); + updateUI(); +} + +// Event listeners +startButton.addEventListener('click', () => { + instructionsOverlay.style.display = 'none'; + resetGame(); + gameRunning = true; + gameLoop(); +}); + +document.addEventListener('keydown', (e) => { + if (!gameRunning) return; + switch (e.code) { + case 'ArrowUp': + e.preventDefault(); + player.move(0, -1); + break; + case 'ArrowDown': + e.preventDefault(); + player.move(0, 1); + break; + case 'ArrowLeft': + e.preventDefault(); + player.move(-1, 0); + break; + case 'ArrowRight': + e.preventDefault(); + player.move(1, 0); + break; + } +}); + +// Initialize +initGame(); +updateUI(); \ No newline at end of file diff --git a/games/crystal-collector/style.css b/games/crystal-collector/style.css new file mode 100644 index 00000000..05927e2a --- /dev/null +++ b/games/crystal-collector/style.css @@ -0,0 +1,131 @@ +/* General Reset & Font */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Poppins', sans-serif; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background: linear-gradient(135deg, #2c1810 0%, #1a0f08 100%); + color: #eee; + overflow: hidden; +} + +/* Game UI */ +#game-ui { + position: absolute; + top: 20px; + left: 20px; + display: flex; + gap: 20px; + z-index: 5; +} + +#score, #lives { + background-color: rgba(0, 0, 0, 0.7); + color: #fff; + padding: 10px 15px; + border-radius: 5px; + font-size: 1.1rem; + font-weight: 600; +} + +/* Canvas */ +canvas { + background: linear-gradient(135deg, #4a2c1a 0%, #2d1810 50%, #1a0f08 100%); + border: 3px solid #8B4513; + box-shadow: 0 0 20px rgba(139, 69, 19, 0.3); + display: block; +} + +/* Instructions Screen */ +#instructions-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +#instructions-content { + background-color: #2a2a2a; + padding: 30px 40px; + border-radius: 10px; + text-align: center; + border: 2px solid #8B4513; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); + max-width: 500px; +} + +#instructions-content h2 { + font-size: 2.5rem; + color: #FFD700; + margin-bottom: 15px; + letter-spacing: 2px; +} + +#instructions-content p { + font-size: 1.1rem; + margin-bottom: 25px; + color: #ccc; +} + +#instructions-content h3 { + font-size: 1.2rem; + color: #eee; + margin-bottom: 10px; + border-bottom: 1px solid #444; + padding-bottom: 5px; +} + +#instructions-content ul { + list-style: none; + margin-bottom: 30px; + text-align: left; + display: inline-block; +} + +#instructions-content li { + font-size: 1rem; + color: #ccc; + margin-bottom: 8px; +} + +/* Style for keys */ +#instructions-content code { + background-color: #8B4513; + color: #fff; + padding: 3px 6px; + border-radius: 4px; + font-family: monospace; + font-size: 0.95rem; + margin-right: 8px; +} + +/* Start Button */ +#startButton { + background-color: #FFD700; + color: #000; + border: none; + padding: 12px 24px; + font-size: 1.1rem; + font-weight: 600; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} + +#startButton:hover { + background-color: #FFA500; +} \ No newline at end of file diff --git a/games/css_inspector/index.html b/games/css_inspector/index.html new file mode 100644 index 00000000..08425990 --- /dev/null +++ b/games/css_inspector/index.html @@ -0,0 +1,49 @@ + + + + + + The CSS Inspector + + + +
+

The CSS Inspector ๐Ÿ•ต๏ธ

+

Fix the broken display by writing or correcting the **CSS** code!

+
+ +
+
+
+
+ Level Content +
+
+
+ +
+
+

Level 1: The Invisible Button

+

The button should be visible! Hint: It's currently blending into the background.

+
+ +
+ + +
+ + +
+
+ +
+

Welcome! Type your CSS and click 'Apply CSS' to fix the display.

+
+ + +
+
+ + + + \ No newline at end of file diff --git a/games/css_inspector/script.js b/games/css_inspector/script.js new file mode 100644 index 00000000..97b2540a --- /dev/null +++ b/games/css_inspector/script.js @@ -0,0 +1,184 @@ +// --- 1. Level Data --- +const levels = [ + { + id: 1, + title: "The Invisible Button", + description: "The button should be visible! It's currently blending into the background. Fix its **color** property.", + targetId: "level-target", + initialClass: "broken-level-1", + // The *required* resulting CSS properties/values for success + requiredStyles: { + color: 'rgb(248, 248, 242)', // Must change to a visible color + backgroundColor: 'rgb(80, 250, 123)' // Must change background + }, + successMessage: "Great job! The button is now visible and clickable." + }, + { + id: 2, + title: "Misaligned Text", + description: "The paragraph text is stuck to the right. Use a **text-align** property to center it.", + targetId: "level-target", + initialClass: "broken-level-2", + requiredStyles: { + textAlign: 'center', // The computed style must be 'center' + }, + successMessage: "Perfect! The text is now centered and readable." + }, + { + id: 3, + title: "The Squished Image", + description: "The image is squished! Use the **height** property to make it taller (300px).", + targetId: "level-target", + initialClass: "broken-level-3", + requiredStyles: { + height: '300px', // The computed style must be '300px' + }, + successMessage: "Resizing complete! The image now has the correct aspect ratio." + } + // Add more levels here... +]; + +// --- 2. Game State Variables --- +let currentLevelIndex = 0; +let currentLevelData = levels[currentLevelIndex]; + +// --- 3. DOM Elements --- +const targetElement = document.getElementById('level-target'); +const cssInput = document.getElementById('css-input'); +const applyButton = document.getElementById('apply-css'); +const resetButton = document.getElementById('reset-level'); +const nextLevelButton = document.getElementById('next-level'); +const messageBox = document.getElementById('message-box'); +const levelTitle = document.getElementById('level-title'); +const levelDescription = document.getElementById('level-description'); + +// --- 4. Core Functions --- + +/** + * Loads the current level's data and resets the target element. + */ +function loadLevel() { + currentLevelData = levels[currentLevelIndex]; + if (!currentLevelData) { + // End of game scenario + alert("๐ŸŽ‰ Congratulations! You have completed all levels!"); + levelTitle.textContent = "Game Over!"; + levelDescription.textContent = "You are a true CSS Inspector!"; + nextLevelButton.classList.add('hidden'); + applyButton.disabled = true; + resetButton.disabled = true; + cssInput.disabled = true; + return; + } + + // Update level info + levelTitle.textContent = `Level ${currentLevelData.id}: ${currentLevelData.title}`; + levelDescription.innerHTML = currentLevelData.description; // Use innerHTML for bolding + + // Reset and apply initial broken state + targetElement.removeAttribute('style'); // Clear any previous inline styles + targetElement.className = `target-element ${currentLevelData.initialClass}`; + targetElement.id = currentLevelData.targetId; + targetElement.textContent = `Target Element for Level ${currentLevelData.id}`; // Simple placeholder content + + // UI cleanup + cssInput.value = ''; + messageBox.innerHTML = `

Level ${currentLevelData.id} loaded. Ready to debug.

`; + nextLevelButton.classList.add('hidden'); + applyButton.disabled = false; + resetButton.disabled = false; + cssInput.disabled = false; +} + +/** + * Applies the player's CSS input to the target element. + */ +function applyCSS() { + const playerInput = cssInput.value.trim(); + + if (!playerInput) { + messageBox.innerHTML = '

Please enter some CSS code!

'; + return; + } + + try { + // Dynamically apply the new CSS. The 'cssText' method is robust. + // It's important to use += to append to any existing inline style (from previous, valid inputs). + targetElement.style.cssText += playerInput; + messageBox.innerHTML = `

CSS applied: ${playerInput}

`; + + // Check for success after applying the CSS + checkForSuccess(); + + } catch (e) { + messageBox.innerHTML = `

Invalid CSS Syntax! Check your semicolons (;) and colons (:).

`; + } +} + +/** + * Checks the *computed* visual state against the required solution. + * This is the core of the robust logic. + */ +function checkForSuccess() { + // Get the element's currently applied/computed styles + const computedStyle = window.getComputedStyle(targetElement); + const required = currentLevelData.requiredStyles; + let isSuccess = true; + + // Iterate through all required properties + for (const prop in required) { + // Normalize the computed style value (e.g., trim, check for common inconsistencies) + const computedValue = computedStyle[prop].toString().trim().toLowerCase(); + const requiredValue = required[prop].toString().trim().toLowerCase(); + + // Simple string comparison for the target property + if (computedValue !== requiredValue) { + isSuccess = false; + break; // Fail fast + } + } + + // Handle Success/Failure + if (isSuccess) { + messageBox.innerHTML = `

โœ… ${currentLevelData.successMessage}

`; + nextLevelButton.classList.remove('hidden'); + applyButton.disabled = true; + cssInput.disabled = true; + } else { + // Only show a failure message if they didn't succeed + messageBox.innerHTML += '

Not quite right. The display is still broken. Try another property or value!

'; + } +} + +/** + * Advances the game to the next level. + */ +function nextLevel() { + currentLevelIndex++; + loadLevel(); +} + +/** + * Resets the current level's target element to its initial broken state. + */ +function resetLevel() { + loadLevel(); +} + + +// --- 5. Event Listeners --- + +applyButton.addEventListener('click', applyCSS); +resetButton.addEventListener('click', resetLevel); +nextLevelButton.addEventListener('click', nextLevel); + +// Allow pressing Enter to apply CSS (must prevent default form submit if in a form) +cssInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { // Shift+Enter for new line + e.preventDefault(); + applyCSS(); + } +}); + +// --- 6. Initialization --- +loadLevel(); \ No newline at end of file diff --git a/games/css_inspector/style.css b/games/css_inspector/style.css new file mode 100644 index 00000000..20bdf2b4 --- /dev/null +++ b/games/css_inspector/style.css @@ -0,0 +1,170 @@ +/* --- Base Styling --- */ +body { + font-family: sans-serif; + background-color: #1e1e1e; + color: #f0f0f0; + margin: 0; + padding: 20px; + display: flex; + flex-direction: column; + align-items: center; +} + +header { + text-align: center; + margin-bottom: 30px; +} + +.game-container { + display: flex; + width: 90%; + max-width: 1200px; + gap: 30px; +} + +/* --- Display Area (The "Broken" Interface) --- */ +.display-area { + flex: 1; + background-color: #2a2a2a; + border: 5px solid #50fa7b; /* Neon green border for "monitor" effect */ + padding: 20px; + height: 400px; /* Fixed height for consistent level display */ + display: flex; + align-items: center; /* Default centering */ + justify-content: center; /* Default centering */ + position: relative; + overflow: hidden; +} + +#target-container { + width: 100%; + height: 100%; + /* This container holds the target element, useful for flex/grid levels */ +} + +/* --- Initial Broken States (The challenge) --- */ + +/* Level 1 Broken State: The Invisible Button */ +.broken-level-1 { + width: 150px; + height: 50px; + background-color: #2a2a2a; /* Same as the display-area background, making it invisible */ + color: #2a2a2a; /* Text color also invisible */ + border: 1px solid #2a2a2a; + display: flex; + justify-content: center; + align-items: center; + font-size: 18px; + cursor: pointer; + transition: all 0.3s; +} + +/* Level 2 Broken State: Misaligned Text */ +.broken-level-2 { + background-color: #444; + padding: 20px; + width: 80%; + text-align: right; /* Text is misaligned to the right */ + border-radius: 5px; +} + +/* Level 3 Broken State: The Shrinking Image */ +.broken-level-3 { + background-image: url('placeholder-image.jpg'); /* Assume a placeholder image exists */ + background-size: cover; + width: 500px; + height: 100px; /* Incorrect height */ + border: 3px dashed #ff5555; +} + + +/* --- Editor and Controls --- */ +.editor-area { + flex: 1; + display: flex; + flex-direction: column; + gap: 15px; +} + +.level-info h2 { + color: #bd93f9; /* Purple highlight */ +} + +.css-editor { + background-color: #282a36; /* Dracula theme background */ + padding: 20px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4); +} + +#css-input { + width: 100%; + background-color: #44475a; + color: #f8f8f2; + border: none; + padding: 10px; + font-family: 'Consolas', monospace; + font-size: 16px; + resize: none; + box-sizing: border-box; + margin-top: 10px; +} + +.controls button { + padding: 10px 15px; + margin-top: 10px; + margin-right: 10px; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: bold; + transition: background-color 0.2s; +} + +#apply-css { + background-color: #50fa7b; /* Success green */ + color: #1e1e1e; +} + +#reset-level { + background-color: #ffb86c; /* Warning orange */ + color: #1e1e1e; +} + +/* --- Messages and Next Level Button --- */ +.message-box { + min-height: 50px; + background-color: #44475a; + padding: 15px; + border-radius: 4px; +} + +.message-box p { + margin: 0; +} + +.success { + color: #50fa7b; + font-weight: bold; +} + +.error { + color: #ff5555; + font-weight: bold; +} + +#next-level { + width: 100%; + padding: 15px; + background-color: #bd93f9; + color: #1e1e1e; + font-size: 18px; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: bold; +} + +.hidden { + display: none !important; +} \ No newline at end of file diff --git a/games/css_transform/index.html b/games/css_transform/index.html new file mode 100644 index 00000000..258f368d --- /dev/null +++ b/games/css_transform/index.html @@ -0,0 +1,43 @@ + + + + + + CSS Transform Scramble + + + + +
+

CSS Transform Scramble Puzzle

+

Match the pieces back to the original image using the transform controls.

+
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+

Piece Controls

+
+

Click on a piece to select it, then use the buttons below.

+ +
+ +
+ + + + \ No newline at end of file diff --git a/games/css_transform/script.js b/games/css_transform/script.js new file mode 100644 index 00000000..9309f82c --- /dev/null +++ b/games/css_transform/script.js @@ -0,0 +1,121 @@ +document.addEventListener('DOMContentLoaded', () => { + const pieces = document.querySelectorAll('.piece'); + const controlButtons = document.getElementById('piece-control-buttons'); + const winMessage = document.getElementById('win-message'); + let selectedPiece = null; + + // The required state for a solved piece is 'none' (or 'initial') + const SOLVED_TRANSFORM = 'none'; + + // --- 1. Piece Selection --- + pieces.forEach(piece => { + // Initialize a state property for easier tracking + piece.dataset.currentTransform = getComputedStyle(piece).transform || 'none'; + + piece.addEventListener('click', () => { + if (selectedPiece) { + selectedPiece.classList.remove('selected'); + } + selectedPiece = piece; + selectedPiece.classList.add('selected'); + controlButtons.classList.remove('hidden'); + }); + }); + + // --- 2. Control Button Logic --- + document.getElementById('rotate-btn').addEventListener('click', () => { + if (selectedPiece) { + applyTransform(selectedPiece, 'rotate'); + checkWinCondition(); + } + }); + + document.getElementById('flipX-btn').addEventListener('click', () => { + if (selectedPiece) { + applyTransform(selectedPiece, 'flipX'); + checkWinCondition(); + } + }); + + document.getElementById('flipY-btn').addEventListener('click', () => { + if (selectedPiece) { + applyTransform(selectedPiece, 'flipY'); + checkWinCondition(); + } + }); + + // --- 3. Transform Application Function (CORE LOGIC) --- + /** + * Updates the piece's transform based on the current state. + * This uses *inline styles* to override the initial CSS scramble. + * Using inline style.transform is simpler than managing many CSS classes. + */ + function applyTransform(piece, action) { + let currentTransform = piece.style.transform || piece.dataset.currentTransform; + let newTransform = ''; + + // Simplistic way to track state: Just append the new transform. + // For a more robust solution, you'd parse the matrix() value. + // This simple version works well for discrete operations like rotate/flip. + + if (action === 'rotate') { + // Example: Cycle through 0, 90, 180, 270 degrees + // This simple example just adds a rotation to whatever is already there. + // For a true puzzle, you'd manage the full rotation state. + newTransform = currentTransform + ' rotate(90deg)'; + } else if (action === 'flipX') { + // For simplicity, this assumes a flipX is a toggle + const isFlippedX = currentTransform.includes('scaleX(-1)'); + newTransform = isFlippedX + ? currentTransform.replace('scaleX(-1)', '') + : currentTransform + ' scaleX(-1)'; + } else if (action === 'flipY') { + const isFlippedY = currentTransform.includes('scaleY(-1)'); + newTransform = isFlippedY + ? currentTransform.replace('scaleY(-1)', '') + : currentTransform + ' scaleY(-1)'; + } + + piece.style.transform = newTransform; + piece.dataset.currentTransform = newTransform; // Update the state tracker + } + + + // --- 4. Win Condition Check (MINIMAL JS) --- + function checkWinCondition() { + let allSolved = true; + pieces.forEach(piece => { + // Get the computed style, which will be a matrix() string. + // When a transform is 'none', it resolves to 'matrix(1, 0, 0, 1, 0, 0)'. + // When *only* the CSS is applied, it will be the scrambled matrix. + // The goal is to get the final computed matrix() back to the 'none' matrix. + + // The 'style.transform' will be an empty string or 'none' when solved + // *if* the player successfully reverts all operations. + + // For a robust check: compare the current calculated matrix against the solved matrix. + // The 'solved' matrix for all pieces is always 'matrix(1, 0, 0, 1, 0, 0)'. + const currentMatrix = getComputedStyle(piece).transform; + + // This simplified check will only work if the player can undo the initial + // CSS transforms *exactly* to reach 'transform: none'. + // A more reliable way: check if the computed matrix is the identity matrix. + if (currentMatrix !== 'none' && currentMatrix !== 'matrix(1, 0, 0, 1, 0, 0)') { + allSolved = false; + } + }); + + if (allSolved) { + winMessage.classList.remove('hidden'); + controlButtons.classList.add('hidden'); + } else { + winMessage.classList.add('hidden'); + } + } + + // Initial check is needed if we want the game to start with an immediate win check + // In a real scramble, this will initially return false. + // We can also call checkWinCondition() after a short delay on load + // to ensure all initial CSS transforms are resolved. + setTimeout(checkWinCondition, 100); +}); \ No newline at end of file diff --git a/games/css_transform/style.css b/games/css_transform/style.css new file mode 100644 index 00000000..0e22915e --- /dev/null +++ b/games/css_transform/style.css @@ -0,0 +1,91 @@ +:root { + --grid-size: 3; /* e.g., 3 for 3x3 */ + --piece-size: calc(100% / var(--grid-size)); + --image-url: url('your-image.jpg'); /* REPLACE with your actual image path */ +} + +body { + font-family: sans-serif; + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; +} + +#puzzle-container { + width: 450px; /* Example size */ + height: 450px; + display: grid; + grid-template-columns: repeat(var(--grid-size), 1fr); + grid-template-rows: repeat(var(--grid-size), 1fr); + border: 5px solid #333; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); + margin-bottom: 20px; + perspective: 1000px; /* For a nice 3D effect on transforms */ +} + +.piece { + width: 100%; + height: 100%; + background-image: var(--image-url); + background-size: 450px 450px; /* Must match container size */ + cursor: pointer; + transition: transform 0.3s ease-out, box-shadow 0.2s; /* Smooth transitions for movement */ + /* All pieces start with a slight border and a different color on hover/select */ + border: 1px solid #555; + box-sizing: border-box; +} + +/* Selected piece highlighting */ +.piece.selected { + box-shadow: 0 0 10px 3px gold inset; + border: 1px solid gold; +} + +/* * Background Position Logic: + * The pieces are indexed 0-8 (row * 3 + col). + * The image is 450x450, and each piece is 150x150. + * To show the correct section: + * background-position-x = - (col * piece-size) + * background-position-y = - (row * piece-size) + */ + +/* Row 1 (Pieces 0, 1, 2) */ +.piece[data-piece-id="0"] { background-position: 0% 0%; } +.piece[data-piece-id="1"] { background-position: -50% 0%; } +.piece[data-piece-id="2"] { background-position: -100% 0%; } + +/* Row 2 (Pieces 3, 4, 5) */ +.piece[data-piece-id="3"] { background-position: 0% -50%; } +.piece[data-piece-id="4"] { background-position: -50% -50%; } +.piece[data-piece-id="5"] { background-position: -100% -50%; } + +/* Row 3 (Pieces 6, 7, 8) */ +.piece[data-piece-id="6"] { background-position: 0% -100%; } +.piece[data-piece-id="7"] { background-position: -50% -100%; } +.piece[data-piece-id="8"] { background-position: -100% -100%; } + + +/* --- THE INITIAL SCRAMBLE (CSS-centric) --- */ +/* Apply random initial transforms to a few pieces in CSS for the "scramble" effect */ +/* The solved state is 'transform: none;' or 'transform: initial;' */ + +/* Example Scramble Transforms - These will need to be reversed by the player! */ +.piece[data-piece-id="1"] { transform: rotate(90deg); } +.piece[data-piece-id="3"] { transform: scaleX(-1); } +.piece[data-piece-id="4"] { transform: rotate(180deg) scaleY(-1); } +.piece[data-piece-id="8"] { transform: scaleY(-1) rotate(270deg); } + + +/* Utility and Control styles */ +.hidden { display: none !important; } + +#controls { + text-align: center; +} + +#piece-control-buttons button { + padding: 10px 15px; + margin: 5px; + cursor: pointer; +} \ No newline at end of file diff --git a/games/cube-alignment/index.html b/games/cube-alignment/index.html new file mode 100644 index 00000000..f3ed4dfc --- /dev/null +++ b/games/cube-alignment/index.html @@ -0,0 +1,29 @@ + + + + + + Cube Alignment | Mini JS Games Hub + + + +
+

๐ŸงŠ Cube Alignment

+

Connect and align glowing cubes correctly!

+ +
+ + + +
+ + + + + + +
+ + + + diff --git a/games/cube-alignment/script.js b/games/cube-alignment/script.js new file mode 100644 index 00000000..221d5335 --- /dev/null +++ b/games/cube-alignment/script.js @@ -0,0 +1,128 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); +canvas.width = window.innerWidth * 0.8; +canvas.height = 500; + +const connectSound = document.getElementById("connect-sound"); +const winSound = document.getElementById("win-sound"); +const bgMusic = document.getElementById("bg-music"); + +let cubes = []; +let lines = []; +let isRunning = false; +let paused = false; + +class Cube { + constructor(x, y, radius, color, id) { + this.x = x; + this.y = y; + this.radius = radius; + this.color = color; + this.id = id; + this.active = false; + } + + draw() { + ctx.beginPath(); + ctx.shadowBlur = this.active ? 30 : 10; + ctx.shadowColor = this.color; + ctx.fillStyle = this.active ? this.color : "rgba(0,255,255,0.3)"; + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fill(); + ctx.closePath(); + } +} + +function createCubes() { + cubes = []; + const positions = [ + [150, 250], + [350, 150], + [550, 250], + [350, 350] + ]; + for (let i = 0; i < positions.length; i++) { + cubes.push(new Cube(positions[i][0], positions[i][1], 25, "#00ffff", i)); + } +} + +function drawLines() { + ctx.strokeStyle = "#00ffff"; + ctx.lineWidth = 3; + ctx.shadowBlur = 15; + ctx.shadowColor = "#00ffff"; + lines.forEach(([a, b]) => { + ctx.beginPath(); + ctx.moveTo(a.x, a.y); + ctx.lineTo(b.x, b.y); + ctx.stroke(); + }); +} + +function render() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawLines(); + cubes.forEach(c => c.draw()); +} + +canvas.addEventListener("click", (e) => { + if (!isRunning || paused) return; + const rect = canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + let clicked = null; + + cubes.forEach(cube => { + const dx = cube.x - mouseX; + const dy = cube.y - mouseY; + if (Math.sqrt(dx * dx + dy * dy) < cube.radius + 5) clicked = cube; + }); + + if (clicked) { + connectSound.currentTime = 0; + connectSound.play(); + clicked.active = true; + if (lines.length > 0) { + const last = lines[lines.length - 1][1]; + if (last !== clicked) lines.push([last, clicked]); + } else { + lines.push([clicked, clicked]); + } + + if (lines.length === cubes.length - 1) { + winSound.play(); + isRunning = false; + alert("๐ŸŽ‰ You aligned all cubes! Great job!"); + } + } +}); + +document.getElementById("start-btn").addEventListener("click", () => { + createCubes(); + lines = []; + isRunning = true; + paused = false; + bgMusic.play(); + renderLoop(); +}); + +document.getElementById("pause-btn").addEventListener("click", () => { + paused = !paused; + if (paused) bgMusic.pause(); + else bgMusic.play(); +}); + +document.getElementById("restart-btn").addEventListener("click", () => { + createCubes(); + lines = []; + cubes.forEach(c => c.active = false); + bgMusic.currentTime = 0; + bgMusic.play(); + render(); +}); + +function renderLoop() { + if (!isRunning) return; + render(); + if (!paused) requestAnimationFrame(renderLoop); +} diff --git a/games/cube-alignment/style.css b/games/cube-alignment/style.css new file mode 100644 index 00000000..da608151 --- /dev/null +++ b/games/cube-alignment/style.css @@ -0,0 +1,52 @@ +body { + margin: 0; + background: radial-gradient(circle at center, #111 30%, #000); + color: #fff; + font-family: "Poppins", sans-serif; + text-align: center; + height: 100vh; + overflow: hidden; +} + +h1 { + margin-top: 20px; + font-size: 2rem; + text-shadow: 0 0 10px #0ff; +} + +.subtitle { + color: #ccc; + margin-bottom: 10px; +} + +.controls { + margin: 10px auto; +} + +button { + background: linear-gradient(45deg, #00f5ff, #0078ff); + color: white; + border: none; + padding: 10px 20px; + margin: 5px; + border-radius: 10px; + cursor: pointer; + font-size: 1rem; + transition: all 0.3s ease; + box-shadow: 0 0 10px #00f5ff; +} + +button:hover { + transform: scale(1.1); + box-shadow: 0 0 20px #00f5ff, 0 0 40px #00f5ff; +} + +canvas { + background: rgba(255, 255, 255, 0.05); + border: 2px solid #00f5ff; + border-radius: 15px; + margin-top: 10px; + display: block; + margin-left: auto; + margin-right: auto; +} diff --git a/games/dance-floor/index.html b/games/dance-floor/index.html new file mode 100644 index 00000000..4e5fa6e6 --- /dev/null +++ b/games/dance-floor/index.html @@ -0,0 +1,200 @@ + + + + + + Dance Floor - Mini JS Games Hub + + + + +
+
+

๐Ÿ’ƒ Dance Floor

+

Follow the dance moves and hit the arrows at the right time!

+
+ +
+ +
+
+
+ Score: + 0 +
+
+ Combo: + 0 +
+
+ Perfect: + 0 +
+
+ +
+ + + +
+ +
+

Difficulty

+
+ + + + +
+
+ +
+

Song

+ +
+
+ + +
+ +
+
+
+
โฌ†๏ธ
+
+
+
+
+
+
โฌ…๏ธ
+
+
+
+
โฌ‡๏ธ
+
+
+
+
โžก๏ธ
+
+
+
+
+ + +
+
+
โฌ†๏ธ
+
+
+
+
โฌ…๏ธ
+
+
+
+
โฌ‡๏ธ
+
+
+
+
โžก๏ธ
+
+
+
+ + +
+
+
+
+
+ 0:00 / 0:00 +
+
+
+ + +
+

Next Moves

+
+
๐ŸŽต Start the game to see upcoming moves!
+
+
+
+ + +
+
+

Dance Complete! ๐ŸŽ‰

+
+
+ Final Score: + 0 +
+
+ Max Combo: + 0 +
+
+ Perfect Moves: + 0 +
+
+ Accuracy: + 0% +
+
+ Grade: + F +
+
+
+ + +
+
+
+ + +
+

How to Play Dance Floor

+
    +
  • Start: Choose difficulty and song, then press Start
  • +
  • Follow: Watch for arrows approaching from the top
  • +
  • Press: Hit the corresponding arrow key when arrows reach the bottom
  • +
  • Timing: Perfect timing gives bonus points and combo multipliers
  • +
  • Combo: Consecutive perfect moves build your combo
  • +
  • Controls: Arrow keys or click the floor tiles
  • +
  • Difficulty: Harder levels have faster arrows and more complex patterns
  • +
+
+

Controls

+
+
โฌ†๏ธ Up Arrow
+
โฌ‡๏ธ Down Arrow
+
โฌ…๏ธ Left Arrow
+
โžก๏ธ Right Arrow
+
+
+
+ + +
+
+ + + + \ No newline at end of file diff --git a/games/dance-floor/script.js b/games/dance-floor/script.js new file mode 100644 index 00000000..bf1d565a --- /dev/null +++ b/games/dance-floor/script.js @@ -0,0 +1,634 @@ +// Dance Floor Game +// Follow dance moves and hit arrows at the right time + +// DOM elements +const startBtn = document.getElementById('start-btn'); +const pauseBtn = document.getElementById('pause-btn'); +const restartBtn = document.getElementById('restart-btn'); +const playAgainBtn = document.getElementById('play-again-btn'); +const changeSongBtn = document.getElementById('change-song-btn'); + +const currentScoreEl = document.getElementById('current-score'); +const currentComboEl = document.getElementById('current-combo'); +const perfectCountEl = document.getElementById('perfect-count'); +const progressFillEl = document.getElementById('progress-fill'); +const currentTimeEl = document.getElementById('current-time'); +const totalTimeEl = document.getElementById('total-time'); + +const finalScoreEl = document.getElementById('final-score'); +const finalComboEl = document.getElementById('final-combo'); +const finalPerfectEl = document.getElementById('final-perfect'); +const finalAccuracyEl = document.getElementById('final-accuracy'); +const finalGradeEl = document.getElementById('final-grade'); + +const resultsScreen = document.getElementById('results-screen'); +const moveQueueEl = document.getElementById('move-queue'); +const messageEl = document.getElementById('message'); + +const difficultyBtns = document.querySelectorAll('.difficulty-btn'); +const songSelect = document.getElementById('song-select'); +const floorTiles = document.querySelectorAll('.floor-tile'); + +// Game state +let gameActive = false; +let gamePaused = false; +let currentDifficulty = 'easy'; +let currentSong = 'upbeat'; +let score = 0; +let combo = 0; +let perfectMoves = 0; +let totalMoves = 0; +let gameStartTime = 0; +let gameDuration = 60000; // 60 seconds +let moveSequence = []; +let currentMoveIndex = 0; +let upcomingMoves = []; +let activeArrows = new Set(); +let gameLoop = null; +let audioContext = null; + +// Difficulty settings +const difficultySettings = { + easy: { + speed: 2000, // ms for arrow to reach bottom + sequenceLength: 50, + perfectWindow: 200, // ms perfect timing window + goodWindow: 400, // ms good timing window + points: { perfect: 100, good: 50, miss: 0 } + }, + medium: { + speed: 1500, + sequenceLength: 75, + perfectWindow: 150, + goodWindow: 300, + points: { perfect: 150, good: 75, miss: 0 } + }, + hard: { + speed: 1200, + sequenceLength: 100, + perfectWindow: 120, + goodWindow: 250, + points: { perfect: 200, good: 100, miss: 0 } + }, + expert: { + speed: 1000, + sequenceLength: 125, + perfectWindow: 100, + goodWindow: 200, + points: { perfect: 300, good: 150, miss: 0 } + } +}; + +// Song data (simplified beat patterns) +const songs = { + upbeat: { + name: 'Upbeat Groove', + bpm: 120, + pattern: ['up', 'right', 'down', 'left', 'up', 'right', 'down', 'left'] + }, + electronic: { + name: 'Electronic Beat', + bpm: 140, + pattern: ['up', 'up', 'down', 'down', 'left', 'right', 'left', 'right'] + }, + pop: { + name: 'Pop Rhythm', + bpm: 100, + pattern: ['right', 'up', 'left', 'down', 'right', 'up', 'left', 'down'] + }, + rock: { + name: 'Rock Anthem', + bpm: 160, + pattern: ['down', 'right', 'up', 'left', 'down', 'right', 'up', 'left'] + }, + jazz: { + name: 'Jazz Swing', + bpm: 110, + pattern: ['up', 'left', 'down', 'right', 'up', 'left', 'down', 'right'] + } +}; + +// Initialize the game +function initGame() { + setupAudioContext(); + setupEventListeners(); + generateMoveSequence(); + updateDisplay(); + showMessage('Welcome to Dance Floor! Choose difficulty and start dancing!', 'success'); +} + +// Setup Web Audio API +function setupAudioContext() { + try { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } catch (e) { + console.warn('Web Audio API not supported'); + } +} + +// Setup event listeners +function setupEventListeners() { + // Game controls + startBtn.addEventListener('click', startGame); + pauseBtn.addEventListener('click', togglePause); + restartBtn.addEventListener('click', restartGame); + playAgainBtn.addEventListener('click', restartGame); + changeSongBtn.addEventListener('click', () => { + resultsScreen.classList.remove('show'); + showMessage('Choose a new song and start dancing!', 'info'); + }); + + // Difficulty selection + difficultyBtns.forEach(btn => { + btn.addEventListener('click', () => selectDifficulty(btn.dataset.difficulty)); + }); + + // Song selection + songSelect.addEventListener('change', (e) => { + currentSong = e.target.value; + generateMoveSequence(); + showMessage(`Selected: ${songs[currentSong].name}`, 'info'); + }); + + // Floor tiles (mouse/touch) + floorTiles.forEach(tile => { + tile.addEventListener('click', () => handleTilePress(tile.dataset.direction)); + }); + + // Keyboard controls + document.addEventListener('keydown', handleKeyPress); +} + +// Select difficulty +function selectDifficulty(difficulty) { + currentDifficulty = difficulty; + + // Update UI + difficultyBtns.forEach(btn => { + btn.classList.remove('active'); + }); + document.querySelector(`[data-difficulty="${difficulty}"]`).classList.add('active'); + + generateMoveSequence(); + showMessage(`Difficulty set to ${difficulty.toUpperCase()}`, 'info'); +} + +// Generate move sequence based on song and difficulty +function generateMoveSequence() { + const song = songs[currentSong]; + const settings = difficultySettings[currentDifficulty]; + const pattern = song.pattern; + + moveSequence = []; + upcomingMoves = []; + + // Generate sequence based on song pattern + for (let i = 0; i < settings.sequenceLength; i++) { + const direction = pattern[i % pattern.length]; + const timing = (i * 60 / song.bpm) * 1000; // Convert to milliseconds + + moveSequence.push({ + direction: direction, + timing: timing, + id: i + }); + } + + // Set game duration based on sequence + gameDuration = moveSequence[moveSequence.length - 1].timing + 2000; + updateTimeDisplay(); +} + +// Start the game +function startGame() { + if (gameActive) return; + + gameActive = true; + gamePaused = false; + gameStartTime = Date.now(); + currentMoveIndex = 0; + score = 0; + combo = 0; + perfectMoves = 0; + totalMoves = 0; + + // Reset UI + startBtn.classList.add('active'); + pauseBtn.classList.remove('active'); + resultsScreen.classList.remove('show'); + + // Clear active arrows + activeArrows.clear(); + document.querySelectorAll('.arrow').forEach(arrow => { + arrow.classList.remove('active'); + }); + + // Start game loop + gameLoop = setInterval(updateGame, 16); // ~60 FPS + + // Schedule moves + scheduleMoves(); + + updateDisplay(); + showMessage('Dance started! Follow the arrows!', 'success'); +} + +// Schedule upcoming moves +function scheduleMoves() { + moveSequence.forEach((move, index) => { + setTimeout(() => { + if (gameActive && !gamePaused) { + spawnArrow(move); + } + }, move.timing); + }); +} + +// Spawn arrow indicator +function spawnArrow(move) { + const direction = move.direction; + const arrowEl = document.querySelector(`.${direction}-arrow`); + const indicatorEl = document.querySelector(`.${direction}-indicator`); + + if (!arrowEl || !indicatorEl) return; + + // Create arrow object + const arrow = { + element: arrowEl, + direction: direction, + spawnTime: Date.now(), + move: move, + position: 0 + }; + + activeArrows.add(arrow); + arrowEl.classList.add('active'); + + // Add to upcoming moves display + addToUpcomingMoves(move); +} + +// Update game state +function updateGame() { + if (!gameActive || gamePaused) return; + + const currentTime = Date.now() - gameStartTime; + const progress = (currentTime / gameDuration) * 100; + + // Update progress bar + progressFillEl.style.width = Math.min(progress, 100) + '%'; + + // Update time display + updateTimeDisplay(); + + // Update arrow positions + updateArrows(currentTime); + + // Check for game end + if (currentTime >= gameDuration) { + endGame(); + } +} + +// Update arrow positions and check for misses +function updateArrows(currentTime) { + activeArrows.forEach(arrow => { + const elapsed = currentTime - arrow.spawnTime; + const settings = difficultySettings[currentDifficulty]; + const progress = elapsed / settings.speed; + + arrow.position = progress; + + // Check for miss (arrow passed the hit zone) + if (progress > 1.2) { // 20% grace period + handleMiss(arrow); + } + }); +} + +// Handle tile press +function handleTilePress(direction) { + if (!gameActive || gamePaused) return; + + // Find the closest arrow for this direction + let closestArrow = null; + let closestDistance = Infinity; + + activeArrows.forEach(arrow => { + if (arrow.direction === direction) { + const distance = Math.abs(arrow.position - 1); // 1 = hit zone + if (distance < closestDistance) { + closestDistance = distance; + closestArrow = arrow; + } + } + }); + + if (closestArrow) { + const settings = difficultySettings[currentDifficulty]; + const timingError = closestDistance * settings.speed; + + if (timingError <= settings.perfectWindow) { + handleHit(closestArrow, 'perfect'); + } else if (timingError <= settings.goodWindow) { + handleHit(closestArrow, 'good'); + } else { + handleMiss(closestArrow); + } + } else { + // No arrow to hit - incorrect press + handleIncorrectPress(direction); + } + + // Visual feedback + const tile = document.querySelector(`[data-direction="${direction}"]`); + if (tile) { + tile.classList.add('active'); + setTimeout(() => tile.classList.remove('active'), 200); + } + + // Play sound + playSound(direction, 'press'); +} + +// Handle successful hit +function handleHit(arrow, quality) { + const settings = difficultySettings[currentDifficulty]; + const basePoints = settings.points[quality]; + + // Combo multiplier + const comboMultiplier = Math.min(combo * 0.1 + 1, 5); + const points = Math.round(basePoints * comboMultiplier); + + score += points; + combo++; + totalMoves++; + + if (quality === 'perfect') { + perfectMoves++; + } + + // Remove arrow + activeArrows.delete(arrow); + arrow.element.classList.remove('active'); + + // Visual feedback + const tile = document.querySelector(`[data-direction="${arrow.direction}"]`); + if (tile) { + tile.classList.add('correct'); + setTimeout(() => tile.classList.remove('correct'), 500); + } + + // Update upcoming moves + removeFromUpcomingMoves(arrow.move); + + updateDisplay(); + showMessage(`${quality.toUpperCase()}! +${points} (${combo}x combo)`, quality === 'perfect' ? 'success' : 'info'); +} + +// Handle miss +function handleMiss(arrow) { + combo = 0; + totalMoves++; + + // Remove arrow + activeArrows.delete(arrow); + arrow.element.classList.remove('active'); + + // Visual feedback + const tile = document.querySelector(`[data-direction="${arrow.direction}"]`); + if (tile) { + tile.classList.add('incorrect'); + setTimeout(() => tile.classList.remove('incorrect'), 500); + } + + // Update upcoming moves + removeFromUpcomingMoves(arrow.move); + + updateDisplay(); + showMessage('Miss! Combo broken!', 'error'); +} + +// Handle incorrect press +function handleIncorrectPress(direction) { + combo = 0; + + const tile = document.querySelector(`[data-direction="${direction}"]`); + if (tile) { + tile.classList.add('incorrect'); + setTimeout(() => tile.classList.remove('incorrect'), 500); + } + + showMessage('Wrong timing!', 'warning'); +} + +// Keyboard controls +function handleKeyPress(e) { + if (!gameActive || gamePaused) return; + + const keyMap = { + 'ArrowUp': 'up', + 'ArrowDown': 'down', + 'ArrowLeft': 'left', + 'ArrowRight': 'right' + }; + + if (keyMap[e.key]) { + e.preventDefault(); + handleTilePress(keyMap[e.key]); + } +} + +// Toggle pause +function togglePause() { + if (!gameActive) return; + + gamePaused = !gamePaused; + + if (gamePaused) { + pauseBtn.classList.add('active'); + showMessage('Game paused', 'info'); + } else { + pauseBtn.classList.remove('active'); + gameStartTime = Date.now() - (Date.now() - gameStartTime); // Adjust start time + showMessage('Game resumed', 'info'); + } +} + +// End game +function endGame() { + gameActive = false; + clearInterval(gameLoop); + + // Clear active arrows + activeArrows.clear(); + document.querySelectorAll('.arrow').forEach(arrow => { + arrow.classList.remove('active'); + }); + + // Calculate final stats + const accuracy = totalMoves > 0 ? Math.round((perfectMoves / totalMoves) * 100) : 0; + const grade = calculateGrade(accuracy, score, combo); + + // Update results screen + finalScoreEl.textContent = score.toLocaleString(); + finalComboEl.textContent = combo; + finalPerfectEl.textContent = perfectMoves; + finalAccuracyEl.textContent = accuracy + '%'; + finalGradeEl.textContent = grade; + finalGradeEl.className = `stat-value grade ${grade}`; + + // Show results + resultsScreen.classList.add('show'); + + // Reset UI + startBtn.classList.remove('active'); + pauseBtn.classList.remove('active'); + + showMessage('Dance complete! Check your results!', 'success'); +} + +// Restart game +function restartGame() { + endGame(); + setTimeout(startGame, 500); +} + +// Calculate grade +function calculateGrade(accuracy, score, maxCombo) { + const weightedScore = (accuracy * 0.4) + (Math.min(score / 10000, 1) * 0.4) + (Math.min(maxCombo / 50, 1) * 0.2); + + if (weightedScore >= 0.95) return 'S'; + if (weightedScore >= 0.85) return 'A'; + if (weightedScore >= 0.75) return 'B'; + if (weightedScore >= 0.65) return 'C'; + if (weightedScore >= 0.55) return 'D'; + return 'F'; +} + +// Add to upcoming moves display +function addToUpcomingMoves(move) { + upcomingMoves.push(move); + + // Keep only next 5 moves + if (upcomingMoves.length > 5) { + upcomingMoves.shift(); + } + + updateUpcomingMovesDisplay(); +} + +// Remove from upcoming moves display +function removeFromUpcomingMoves(move) { + upcomingMoves = upcomingMoves.filter(m => m.id !== move.id); + updateUpcomingMovesDisplay(); +} + +// Update upcoming moves display +function updateUpcomingMovesDisplay() { + if (upcomingMoves.length === 0) { + moveQueueEl.innerHTML = '
๐ŸŽต Start the game to see upcoming moves!
'; + return; + } + + let html = ''; + upcomingMoves.slice(0, 5).forEach(move => { + const arrowEmoji = getArrowEmoji(move.direction); + const timing = Math.max(0, Math.round((move.timing - (Date.now() - gameStartTime)) / 1000)); + html += `
+ ${arrowEmoji} + ${timing}s +
`; + }); + + moveQueueEl.innerHTML = html; +} + +// Get arrow emoji +function getArrowEmoji(direction) { + const emojis = { + up: 'โฌ†๏ธ', + down: 'โฌ‡๏ธ', + left: 'โฌ…๏ธ', + right: 'โžก๏ธ' + }; + return emojis[direction] || 'โ“'; +} + +// Play sound effect +function playSound(type, subtype = '') { + if (!audioContext) return; + + let frequency = 440; + let duration = 0.1; + + // Different sounds for different actions + if (type === 'up') frequency = 523; // C5 + else if (type === 'down') frequency = 294; // D4 + else if (type === 'left') frequency = 349; // F4 + else if (type === 'right') frequency = 392; // G4 + else if (type === 'press') { + frequency = 800; + duration = 0.05; + } + + // Create oscillator + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime); + oscillator.type = 'sine'; + + gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration); + + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + duration); +} + +// Update display elements +function updateDisplay() { + currentScoreEl.textContent = score.toLocaleString(); + currentComboEl.textContent = combo; + perfectCountEl.textContent = perfectMoves; +} + +// Update time display +function updateTimeDisplay() { + if (!gameActive) return; + + const currentTime = Date.now() - gameStartTime; + const remaining = Math.max(0, gameDuration - currentTime); + + currentTimeEl.textContent = formatTime(currentTime); + totalTimeEl.textContent = formatTime(gameDuration); +} + +// Format time as MM:SS +function formatTime(ms) { + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; +} + +// Show message +function showMessage(text, type) { + messageEl.textContent = text; + messageEl.className = `message ${type} show`; + + setTimeout(() => { + messageEl.classList.remove('show'); + }, 3000); +} + +// Initialize the game +initGame(); + +// This dance floor game includes arrow-based rhythm gameplay, +// multiple difficulty levels, scoring system with combos, +// song selection, and timing-based accuracy detection \ No newline at end of file diff --git a/games/dance-floor/style.css b/games/dance-floor/style.css new file mode 100644 index 00000000..f2068627 --- /dev/null +++ b/games/dance-floor/style.css @@ -0,0 +1,732 @@ +/* Dance Floor Game Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + color: #333; + overflow-x: hidden; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 30px; + color: white; +} + +header h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +header p { + font-size: 1.2em; + opacity: 0.9; +} + +.game-container { + display: grid; + grid-template-columns: 250px 1fr 200px; + grid-template-rows: auto; + gap: 20px; + background: rgba(255, 255, 255, 0.95); + border-radius: 15px; + padding: 20px; + box-shadow: 0 10px 30px rgba(0,0,0,0.2); + backdrop-filter: blur(10px); +} + +/* Game Info Panel */ +.game-info { + display: flex; + flex-direction: column; + gap: 20px; +} + +.score-display { + background: #f8f9fa; + border-radius: 10px; + padding: 15px; + border: 2px solid #e9ecef; +} + +.score-item { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + font-size: 1.1em; +} + +.score-item:last-child { + margin-bottom: 0; +} + +.score-label { + font-weight: bold; + color: #495057; +} + +#current-score, #current-combo, #perfect-count { + color: #007bff; + font-weight: bold; + font-size: 1.2em; +} + +.game-controls { + display: flex; + flex-direction: column; + gap: 10px; +} + +.control-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 15px; + border: 2px solid #dee2e6; + border-radius: 8px; + background: white; + cursor: pointer; + transition: all 0.3s ease; + font-size: 0.9em; +} + +.control-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.1); +} + +.control-btn.active { + background: #007bff; + color: white; + border-color: #007bff; +} + +.start-btn.active { + background: #28a745; + border-color: #28a745; +} + +.pause-btn.active { + background: #ffc107; + border-color: #ffc107; + color: #212529; +} + +.difficulty-selector, +.song-selector { + background: #f8f9fa; + border-radius: 10px; + padding: 15px; + border: 2px solid #e9ecef; +} + +.difficulty-selector h4, +.song-selector h4 { + margin-bottom: 10px; + color: #495057; + text-align: center; +} + +.difficulty-buttons { + display: flex; + flex-direction: column; + gap: 8px; +} + +.difficulty-btn { + padding: 8px 12px; + border: 2px solid #dee2e6; + border-radius: 6px; + background: white; + cursor: pointer; + transition: all 0.3s ease; + font-size: 0.9em; +} + +.difficulty-btn:hover { + background: #007bff; + color: white; + border-color: #007bff; +} + +.difficulty-btn.active { + background: #007bff; + color: white; + border-color: #007bff; +} + +#song-select { + width: 100%; + padding: 8px 12px; + border: 2px solid #dee2e6; + border-radius: 8px; + background: white; + font-size: 1em; + cursor: pointer; +} + +/* Dance Floor */ +.dance-floor { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + padding: 20px; + background: #2c3e50; + border-radius: 15px; + box-shadow: inset 0 0 30px rgba(0,0,0,0.3); +} + +/* Arrow Indicators */ +.arrow-indicators { + width: 100%; + height: 200px; + position: relative; + background: rgba(0,0,0,0.1); + border-radius: 10px; + overflow: hidden; +} + +.indicator-row { + display: flex; + justify-content: center; + height: 50%; + position: relative; +} + +.indicator-row:first-child { + align-items: flex-end; +} + +.indicator-row:last-child { + align-items: flex-start; +} + +.indicator { + position: relative; + width: 80px; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.indicator-bar { + position: absolute; + width: 100%; + height: 4px; + background: rgba(255,255,255,0.3); + border-radius: 2px; +} + +.arrow { + font-size: 2em; + opacity: 0; + transition: opacity 0.3s ease; + z-index: 10; +} + +.arrow.active { + opacity: 1; + animation: arrowPulse 0.5s ease-in-out; +} + +/* Floor Grid */ +.floor-grid { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; + gap: 15px; + width: 300px; + height: 300px; +} + +.floor-tile { + background: #34495e; + border: 3px solid #2c3e50; + border-radius: 15px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + position: relative; + overflow: hidden; +} + +.floor-tile:hover { + transform: scale(1.05); + box-shadow: 0 0 20px rgba(255,255,255,0.3); +} + +.floor-tile.active { + background: #3498db; + border-color: #2980b9; + animation: tilePress 0.2s ease; +} + +.floor-tile.correct { + background: #27ae60; + border-color: #229954; + animation: tileCorrect 0.5s ease; +} + +.floor-tile.incorrect { + background: #e74c3c; + border-color: #c0392b; + animation: tileIncorrect 0.5s ease; +} + +.tile-arrow { + font-size: 3em; + z-index: 10; + pointer-events: none; +} + +.tile-glow { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle, rgba(255,255,255,0.3) 0%, transparent 70%); + opacity: 0; + transition: opacity 0.3s ease; +} + +.floor-tile.active .tile-glow { + opacity: 1; +} + +/* Progress Bar */ +.progress-container { + width: 100%; + max-width: 400px; +} + +.progress-bar { + width: 100%; + height: 8px; + background: rgba(255,255,255,0.2); + border-radius: 4px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #3498db, #2ecc71); + width: 0%; + transition: width 0.3s ease; +} + +.time-display { + text-align: center; + margin-top: 10px; + color: white; + font-weight: bold; +} + +/* Upcoming Moves */ +.upcoming-moves { + background: #f8f9fa; + border-radius: 10px; + padding: 15px; + border: 2px solid #e9ecef; +} + +.upcoming-moves h4 { + margin-bottom: 10px; + color: #495057; + text-align: center; +} + +.move-queue { + min-height: 200px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.empty-queue { + text-align: center; + color: #6c757d; + font-style: italic; + padding: 20px; +} + +.move-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + background: white; + border-radius: 6px; + border: 1px solid #dee2e6; +} + +.move-arrow { + font-size: 1.5em; + min-width: 30px; + text-align: center; +} + +.move-timing { + margin-left: auto; + font-size: 0.8em; + color: #6c757d; +} + +/* Results Screen */ +.results-screen { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; +} + +.results-screen.show { + opacity: 1; + visibility: visible; +} + +.results-content { + background: white; + border-radius: 15px; + padding: 30px; + max-width: 500px; + width: 90%; + text-align: center; + box-shadow: 0 10px 30px rgba(0,0,0,0.3); +} + +.results-content h2 { + margin-bottom: 20px; + color: #333; +} + +.final-stats { + margin-bottom: 30px; +} + +.stat-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0; + border-bottom: 1px solid #eee; +} + +.stat-row:last-child { + border-bottom: none; +} + +.stat-label { + font-weight: bold; + color: #495057; +} + +.stat-value { + font-weight: bold; + font-size: 1.1em; +} + +#final-grade { + font-size: 1.5em; + padding: 5px 15px; + border-radius: 20px; +} + +#final-grade.S { background: #ffd700; color: #000; } +#final-grade.A { background: #28a745; color: white; } +#final-grade.B { background: #007bff; color: white; } +#final-grade.C { background: #ffc107; color: #000; } +#final-grade.D { background: #fd7e14; color: white; } +#final-grade.F { background: #dc3545; color: white; } + +.results-actions { + display: flex; + gap: 15px; + justify-content: center; +} + +.action-btn { + padding: 12px 25px; + border: 2px solid #007bff; + border-radius: 8px; + background: #007bff; + color: white; + cursor: pointer; + transition: all 0.3s ease; + font-size: 1em; +} + +.action-btn:hover { + background: white; + color: #007bff; +} + +/* Instructions */ +.instructions { + margin-top: 30px; + background: rgba(255, 255, 255, 0.95); + border-radius: 15px; + padding: 20px; + box-shadow: 0 5px 15px rgba(0,0,0,0.1); +} + +.instructions h3 { + margin-bottom: 15px; + color: #495057; + text-align: center; +} + +.instructions ul { + list-style: none; + padding: 0; +} + +.instructions li { + margin-bottom: 8px; + padding: 8px 0; + border-bottom: 1px solid #f8f9fa; +} + +.instructions li:last-child { + border-bottom: none; +} + +.instructions strong { + color: #007bff; +} + +.controls-info { + margin-top: 20px; + text-align: center; +} + +.controls-info h4 { + margin-bottom: 15px; + color: #495057; +} + +.control-keys { + display: flex; + justify-content: center; + gap: 20px; + flex-wrap: wrap; +} + +.key-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + padding: 10px; + background: #f8f9fa; + border-radius: 8px; + border: 1px solid #dee2e6; +} + +.key-item span { + font-size: 0.9em; + color: #495057; +} + +/* Animations */ +@keyframes arrowPulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.2); } + 100% { transform: scale(1); } +} + +@keyframes tilePress { + 0% { transform: scale(1); } + 50% { transform: scale(0.95); } + 100% { transform: scale(1); } +} + +@keyframes tileCorrect { + 0% { background: #27ae60; } + 50% { background: #2ecc71; } + 100% { background: #34495e; } +} + +@keyframes tileIncorrect { + 0% { background: #e74c3c; } + 50% { background: #ec7063; } + 100% { background: #34495e; } +} + +@keyframes moveSlide { + 0% { transform: translateX(100%); opacity: 0; } + 100% { transform: translateX(0); opacity: 1; } +} + +/* Message Display */ +.message { + position: fixed; + top: 20px; + right: 20px; + padding: 15px 25px; + border-radius: 8px; + color: white; + font-weight: bold; + z-index: 1000; + opacity: 0; + transform: translateY(-20px); + transition: all 0.3s ease; + max-width: 300px; +} + +.message.show { + opacity: 1; + transform: translateY(0); +} + +.message.success { + background: #28a745; +} + +.message.error { + background: #dc3545; +} + +.message.info { + background: #17a2b8; +} + +.message.warning { + background: #ffc107; + color: #212529; +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .game-container { + grid-template-columns: 1fr; + grid-template-rows: auto auto auto; + } + + .upcoming-moves { + grid-column: 1; + grid-row: 3; + } + + .dance-floor { + grid-column: 1; + grid-row: 2; + } +} + +@media (max-width: 768px) { + .container { + padding: 10px; + } + + .floor-grid { + width: 250px; + height: 250px; + } + + .floor-tile { + border-radius: 10px; + } + + .tile-arrow { + font-size: 2.5em; + } + + .control-keys { + flex-direction: column; + align-items: center; + } + + .key-item { + flex-direction: row; + padding: 8px 15px; + } + + header h1 { + font-size: 2em; + } +} + +@media (max-width: 480px) { + .game-info { + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + } + + .score-display, + .difficulty-selector, + .song-selector { + flex: 1; + min-width: 200px; + } + + .game-controls { + flex-direction: row; + justify-content: center; + flex-wrap: wrap; + } + + .control-btn { + flex: 1; + min-width: 120px; + } + + .floor-grid { + width: 200px; + height: 200px; + gap: 10px; + } + + .tile-arrow { + font-size: 2em; + } + + .arrow-indicators { + height: 150px; + } + + .indicator { + width: 60px; + } + + .arrow { + font-size: 1.5em; + } + + .results-content { + padding: 20px; + } + + .results-actions { + flex-direction: column; + } +} \ No newline at end of file diff --git a/games/dice-battle/index.html b/games/dice-battle/index.html new file mode 100644 index 00000000..aa1f5dd0 --- /dev/null +++ b/games/dice-battle/index.html @@ -0,0 +1,43 @@ + + + + + + Dice Battle | Mini JS Games Hub + + + +
+

Dice Battle ๐ŸŽฒ

+ +
+
+

Player 1

+

Score: 0

+
Dice 1
+
+
+

Player 2

+

Score: 0

+
Dice 2
+
+
+ +
+ + + +
+ +
+

Round: 1

+

Last Roll: -

+
+ + + +
+ + + + diff --git a/games/dice-battle/script.js b/games/dice-battle/script.js new file mode 100644 index 00000000..0d87bdfc --- /dev/null +++ b/games/dice-battle/script.js @@ -0,0 +1,73 @@ +const dice1Img = document.querySelector("#dice1 img"); +const dice2Img = document.querySelector("#dice2 img"); +const score1El = document.getElementById("score1"); +const score2El = document.getElementById("score2"); +const roundEl = document.getElementById("round"); +const lastRollEl = document.getElementById("lastRoll"); + +const rollBtn = document.getElementById("rollBtn"); +const restartBtn = document.getElementById("restartBtn"); +const pauseBtn = document.getElementById("pauseBtn"); + +const rollSound = document.getElementById("rollSound"); +const winSound = document.getElementById("winSound"); + +let score1 = 0; +let score2 = 0; +let round = 1; +let paused = false; + +function rollDice() { + if(paused) return; + + rollSound.currentTime = 0; + rollSound.play(); + + const roll1 = Math.floor(Math.random() * 6) + 1; + const roll2 = Math.floor(Math.random() * 6) + 1; + + dice1Img.src = `https://upload.wikimedia.org/wikipedia/commons/${["1/1b","5/5a","2/26","f/f5","a/a0","2/2c"][roll1-1]}/Dice-${roll1}-b.svg`; + dice2Img.src = `https://upload.wikimedia.org/wikipedia/commons/${["1/1b","5/5a","2/26","f/f5","a/a0","2/2c"][roll2-1]}/Dice-${roll2}-b.svg`; + + dice1Img.classList.add("dice-roll"); + dice2Img.classList.add("dice-roll"); + + setTimeout(() => { + dice1Img.classList.remove("dice-roll"); + dice2Img.classList.remove("dice-roll"); + + if(roll1 > roll2) score1++; + else if(roll2 > roll1) score2++; + // tie => no change + + score1El.textContent = score1; + score2El.textContent = score2; + lastRollEl.textContent = `P1: ${roll1} | P2: ${roll2}`; + round++; + roundEl.textContent = round; + + if(score1 >= 5 || score2 >= 5) { + winSound.play(); + alert(`Game Over! ${score1 > score2 ? "Player 1 Wins ๐ŸŽ‰" : "Player 2 Wins ๐ŸŽ‰"}`); + rollBtn.disabled = true; + } + }, 500); +} + +rollBtn.addEventListener("click", rollDice); + +restartBtn.addEventListener("click", () => { + score1 = 0; + score2 = 0; + round = 1; + score1El.textContent = 0; + score2El.textContent = 0; + roundEl.textContent = 1; + lastRollEl.textContent = "-"; + rollBtn.disabled = false; +}); + +pauseBtn.addEventListener("click", () => { + paused = !paused; + pauseBtn.textContent = paused ? "Resume โ–ถ๏ธ" : "Pause โธ๏ธ"; +}); diff --git a/games/dice-battle/style.css b/games/dice-battle/style.css new file mode 100644 index 00000000..ed148ccd --- /dev/null +++ b/games/dice-battle/style.css @@ -0,0 +1,66 @@ +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #1e3c72, #2a5298); + color: #fff; + text-align: center; + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; +} + +.dice-battle-container { + background: rgba(0,0,0,0.5); + padding: 30px; + border-radius: 20px; + box-shadow: 0 0 30px #ffd700; + width: 90%; + max-width: 600px; +} + +.players { + display: flex; + justify-content: space-around; + margin-bottom: 20px; +} + +.player { + padding: 10px; + border: 2px solid #fff; + border-radius: 15px; + width: 40%; + box-shadow: 0 0 15px #ff0; +} + +.dice img { + width: 80px; + margin-top: 10px; + transition: transform 0.3s ease; +} + +.dice-roll { + transform: rotate(360deg); +} + +.controls button { + margin: 10px 5px; + padding: 10px 20px; + border: none; + border-radius: 10px; + cursor: pointer; + font-size: 16px; + background: #ffd700; + color: #000; + box-shadow: 0 0 15px #fff; + transition: all 0.2s; +} + +.controls button:hover { + transform: scale(1.1); + box-shadow: 0 0 30px #fff; +} + +.round-info p { + margin: 5px 0; + font-size: 18px; +} diff --git a/games/dice_roll/index.html b/games/dice_roll/index.html new file mode 100644 index 00000000..5fd36b0f --- /dev/null +++ b/games/dice_roll/index.html @@ -0,0 +1,48 @@ + + + + + + Dice Roller ๐ŸŽฒ + + + +
+

Polyhedral Dice Roller

+ +
+
+ + +
+ +
+ + +
+ + +
+ +
+
+

Choose your dice and click "Roll Dice!"

+
+ +
+ Total: -- +
+
+
+ + + + \ No newline at end of file diff --git a/games/dice_roll/sript.js b/games/dice_roll/sript.js new file mode 100644 index 00000000..81964d1a --- /dev/null +++ b/games/dice_roll/sript.js @@ -0,0 +1,58 @@ +// --- 1. DOM Element References --- +const numDiceInput = document.getElementById('num-dice'); +const diceTypeSelect = document.getElementById('dice-type'); +const rollButton = document.getElementById('roll-button'); +const rollsDisplay = document.getElementById('rolls-display'); +const rollTotalDisplay = document.getElementById('roll-total'); + +// --- 2. Core Utility Function --- + +/** + * Generates a random integer between 1 and max (inclusive). + * @param {number} max - The maximum possible roll (e.g., 20 for a D20). + * @returns {number} The random dice roll result. + */ +function getRandomInt(max) { + // Math.random() gives [0, 1), multiplying by max gives [0, max). + // Adding 1 shifts it to [1, max + 1). + // Math.floor() makes it an integer between 1 and max. + return Math.floor(Math.random() * max) + 1; +} + +// --- 3. Main Roll Function --- + +function rollDice() { + // Get user inputs + const numDice = parseInt(numDiceInput.value); + const diceMax = parseInt(diceTypeSelect.value); // e.g., 20 for D20 + + // Input validation (should be handled by HTML attributes, but good for safety) + if (isNaN(numDice) || numDice < 1 || isNaN(diceMax) || diceMax < 4) { + rollsDisplay.innerHTML = '

Please enter valid dice quantities and types.

'; + rollTotalDisplay.textContent = '--'; + return; + } + + let totalSum = 0; + const resultsHtml = []; + + for (let i = 0; i < numDice; i++) { + // 1. Roll the die + const rollResult = getRandomInt(diceMax); + + // 2. Add to total sum + totalSum += rollResult; + + // 3. Create HTML element for individual roll display + const dieHtml = `
${rollResult}
`; + resultsHtml.push(dieHtml); + } + + // 4. Update the DOM + rollsDisplay.innerHTML = resultsHtml.join(''); + rollTotalDisplay.textContent = totalSum; +} + +// --- 4. Event Listener and Initialization --- + +rollButton.addEventListener('click', rollDice); \ No newline at end of file diff --git a/games/dice_roll/style.css b/games/dice_roll/style.css new file mode 100644 index 00000000..6ebbf8d8 --- /dev/null +++ b/games/dice_roll/style.css @@ -0,0 +1,122 @@ +body { + font-family: 'Verdana', sans-serif; + background-color: #36454F; /* Charcoal background */ + color: #eee; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; +} + +.roller-container { + background: #4A5B6A; /* Steel Blue container */ + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + width: 90%; + max-width: 500px; + text-align: center; +} + +h1 { + color: #FFD700; /* Gold */ + margin-bottom: 30px; + text-shadow: 1px 1px 2px #000; +} + +/* --- Controls Panel --- */ +.controls-panel { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + align-items: center; + padding: 15px; + background-color: #5B6C7D; + border-radius: 8px; + margin-bottom: 30px; +} + +.input-group { + display: flex; + flex-direction: column; + margin: 10px; +} + +label { + margin-bottom: 5px; + font-weight: bold; +} + +input[type="number"], select { + padding: 8px; + border-radius: 5px; + border: 1px solid #777; + background-color: #eee; + color: #333; + font-size: 1em; +} + +#roll-button { + padding: 10px 20px; + font-size: 1.1em; + border: none; + border-radius: 5px; + background-color: #2ECC71; /* Green */ + color: white; + cursor: pointer; + transition: background-color 0.2s; + margin-top: 15px; +} + +#roll-button:hover { + background-color: #27ae60; +} + +/* --- Results Area --- */ +.results-area { + padding: 20px; + border: 2px solid #FFD700; + border-radius: 10px; + background-color: #3e4d58; +} + +.rolls-display { + min-height: 60px; + margin-bottom: 15px; + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 10px; +} + +.die-result { + font-size: 1.5em; + font-weight: bold; + color: #FF6347; /* Tomato Red */ + padding: 8px 15px; + border: 2px solid #FF6347; + border-radius: 5px; + background-color: #fff; + color: #333; + box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3); +} + +.total-display { + font-size: 2em; + font-weight: bold; + color: #FFD700; + border-top: 1px solid #777; + padding-top: 10px; + margin-top: 10px; +} + +#roll-total { + color: #2ECC71; + font-size: 1.5em; +} + +.instruction { + font-style: italic; + color: #aaa; +} \ No newline at end of file diff --git a/games/digital_pet/index.html b/games/digital_pet/index.html new file mode 100644 index 00000000..39ef5b53 --- /dev/null +++ b/games/digital_pet/index.html @@ -0,0 +1,42 @@ + + + + + + Pixel the Web Pet! ๐Ÿพ + + + +
+

Meet Pixel!

+ +
+ ๐Ÿ˜Š +
+ +
+
+ + + 100% +
+ +
+ + + 100% +
+
+ +
+ + + +
+ +

+
+ + + + \ No newline at end of file diff --git a/games/digital_pet/script.js b/games/digital_pet/script.js new file mode 100644 index 00000000..32383fba --- /dev/null +++ b/games/digital_pet/script.js @@ -0,0 +1,157 @@ +// --- 1. Game State Variables --- +const DECAY_RATE = 2; // How much stats decrease per tick +const REFRESH_RATE_MS = 1000; // Time per tick (1 second) +const CRITICAL_THRESHOLD = 20; // Point where status is critical + +let hunger = 100; +let happiness = 100; +let petStatus = "๐Ÿ˜Š"; +let gameInterval; // To hold the setInterval ID + +// --- 2. DOM Element References --- +const petEmoji = document.getElementById('pet-emoji'); +const hungerBar = document.getElementById('hunger-bar'); +const happinessBar = document.getElementById('happiness-bar'); +const hungerValue = document.getElementById('hunger-value'); +const happinessValue = document.getElementById('happiness-value'); +const messageArea = document.getElementById('message-area'); + +const feedButton = document.getElementById('feed-button'); +const playButton = document.getElementById('play-button'); +const sleepButton = document.getElementById('sleep-button'); + +// --- 3. Core Logic Functions --- + +// Clamps a value between 0 and 100 +function clampStat(value) { + return Math.max(0, Math.min(100, value)); +} + +// Updates the progress bars and status text +function updateUI() { + hungerBar.value = hunger; + happinessBar.value = happiness; + hungerValue.textContent = `${hunger}%`; + happinessValue.textContent = `${happiness}%`; + petEmoji.textContent = petStatus; + + // Change progress bar color if status is low + if (hunger <= CRITICAL_THRESHOLD) { + hungerBar.classList.add('low'); + } else { + hungerBar.classList.remove('low'); + } + if (happiness <= CRITICAL_THRESHOLD) { + happinessBar.classList.add('low'); + } else { + happinessBar.classList.remove('low'); + } + + // Check for game over (both stats at 0) + if (hunger === 0 && happiness === 0) { + endGame("Pixel gave up! Try to keep both stats above zero next time."); + } +} + +// Handles the automatic decay of stats +function decayStats() { + // Decrease stats by decay rate + hunger = clampStat(hunger - DECAY_RATE); + happiness = clampStat(happiness - DECAY_RATE / 2); // Happiness drains slower + + // Update pet's visual state (emoji) + if (hunger <= CRITICAL_THRESHOLD || happiness <= CRITICAL_THRESHOLD) { + petStatus = "๐Ÿ˜ซ"; // Sad/Stressed + messageArea.classList.add('critical'); + messageArea.textContent = "Pixel is suffering! Please help!"; + } else if (hunger < 50 || happiness < 50) { + petStatus = "๐Ÿ˜Ÿ"; // Neutral/Worried + messageArea.classList.remove('critical'); + messageArea.textContent = "Pixel is getting restless..."; + } else { + petStatus = "๐Ÿ˜Š"; // Happy + messageArea.classList.remove('critical'); + messageArea.textContent = "Pixel is happy and thriving!"; + } + + updateUI(); +} + +// --- 4. Player Action Functions --- + +function feedPet() { + if (hunger === 100) { + messageArea.textContent = "Pixel is too full right now!"; + return; + } + hunger = clampStat(hunger + 30); + messageArea.textContent = "Pixel enjoys the tasty snack! Hunger +30"; + updateUI(); +} + +function playPet() { + if (happiness === 100) { + messageArea.textContent = "Pixel needs a moment to breathe!"; + return; + } + happiness = clampStat(happiness + 25); + hunger = clampStat(hunger - 5); // Playing makes the pet slightly hungry + messageArea.textContent = "Pixel had fun! Happiness +25, Hunger -5"; + updateUI(); +} + +function sleepPet() { + // Sleep significantly boosts happiness, but also makes them a bit hungrier + happiness = clampStat(happiness + 15); + hunger = clampStat(hunger - 10); + messageArea.textContent = "Zzz... Pixel is well-rested. Happiness +15, Hunger -10"; + updateUI(); +} + +// --- 5. Game Management --- + +function startGame() { + // Set initial stats and UI + hunger = 100; + happiness = 100; + petStatus = "๐Ÿ˜Š"; + messageArea.textContent = "Welcome Pixel! Let the game begin."; + + // Clear any existing interval and start the new one + clearInterval(gameInterval); + gameInterval = setInterval(decayStats, REFRESH_RATE_MS); + + // Re-enable buttons + feedButton.disabled = false; + playButton.disabled = false; + sleepButton.disabled = false; + + updateUI(); +} + +function endGame(reason) { + clearInterval(gameInterval); + petStatus = "๐Ÿ’€"; // Game Over emoji + messageArea.textContent = `GAME OVER: ${reason} Click any button to restart.`; + + // Disable buttons to indicate game end + feedButton.disabled = true; + playButton.disabled = true; + sleepButton.disabled = true; + + updateUI(); + + // Set up a click listener on the buttons to restart the game + feedButton.addEventListener('click', startGame, { once: true }); + playButton.addEventListener('click', startGame, { once: true }); + sleepButton.addEventListener('click', startGame, { once: true }); +} + +// --- 6. Event Listeners --- +feedButton.addEventListener('click', feedPet); +playButton.addEventListener('click', playPet); +sleepButton.addEventListener('click', sleepPet); + + +// --- 7. Initialization --- +startGame(); \ No newline at end of file diff --git a/games/digital_pet/style.css b/games/digital_pet/style.css new file mode 100644 index 00000000..3b3ce417 --- /dev/null +++ b/games/digital_pet/style.css @@ -0,0 +1,136 @@ +body { + font-family: 'Comic Sans MS', cursive, sans-serif; /* Friendly font */ + background-color: #f7f7f7; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; +} + +.pet-container { + background: #ffffff; + padding: 30px; + border-radius: 20px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); + text-align: center; + width: 90%; + max-width: 450px; + border: 5px solid #ffcc00; /* Yellow border */ +} + +h1 { + color: #333; + margin-bottom: 25px; +} + +/* Pet Display */ +#pet-display { + margin-bottom: 30px; + padding: 20px; + background-color: #fffbe6; + border-radius: 50%; + display: inline-block; +} + +#pet-emoji { + font-size: 6em; + display: block; + transition: transform 0.3s; +} + +/* Stats Panel */ +.stats-panel { + margin-bottom: 30px; + padding: 15px; + border: 1px dashed #ccc; + border-radius: 10px; +} + +.stat-group { + display: flex; + align-items: center; + margin: 10px 0; +} + +.stat-group label { + font-weight: bold; + color: #555; + width: 100px; /* Align labels */ + text-align: left; +} + +progress { + flex-grow: 1; + height: 25px; + margin-right: 10px; + /* Custom styling for different browsers */ + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border-radius: 5px; + border: 1px solid #ccc; +} + +/* Styling the progress bar fill */ +progress::-webkit-progress-bar { + background-color: #eee; + border-radius: 5px; +} + +progress::-webkit-progress-value { + background-color: #6c5ce7; /* Default purple */ + border-radius: 5px; + transition: background-color 0.5s; +} + +/* Custom colors for low stats (using JS to change class) */ +progress.low::-webkit-progress-value { + background-color: #ff7675; /* Red for low */ +} + +#hunger-value, #happiness-value { + font-weight: bold; + width: 40px; + text-align: right; +} + +/* Action Buttons */ +.action-buttons { + display: flex; + justify-content: space-around; + gap: 10px; +} + +.action-buttons button { + padding: 12px 15px; + font-size: 1em; + border: none; + border-radius: 8px; + background-color: #6c5ce7; + color: white; + cursor: pointer; + transition: background-color 0.2s, transform 0.1s; + font-family: inherit; + flex-grow: 1; +} + +.action-buttons button:hover { + background-color: #483d8b; +} + +.action-buttons button:active { + transform: scale(0.98); +} + +/* Status Message */ +.status-message { + margin-top: 20px; + font-style: italic; + color: #555; +} + +.critical { + color: #dc3545; /* Red for critical status */ + font-weight: bold; +} \ No newline at end of file diff --git a/games/direction-maze/index.html b/games/direction-maze/index.html new file mode 100644 index 00000000..9b73a468 --- /dev/null +++ b/games/direction-maze/index.html @@ -0,0 +1,32 @@ + + + + + + Direction Maze | Mini JS Games Hub + + + +
+

Direction Maze

+

Reach the goal by following the directional tiles! ๐Ÿ’ก

+
+ Moves: 0 + Time: 0s +
+
+
+ + + + +
+
+ + + + + + + + diff --git a/games/direction-maze/script.js b/games/direction-maze/script.js new file mode 100644 index 00000000..42d7984b --- /dev/null +++ b/games/direction-maze/script.js @@ -0,0 +1,111 @@ +const mazeContainer = document.getElementById("maze"); +const movesEl = document.getElementById("moves"); +const timerEl = document.getElementById("timer"); +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const resumeBtn = document.getElementById("resume-btn"); +const restartBtn = document.getElementById("restart-btn"); + +const moveSound = document.getElementById("move-sound"); +const goalSound = document.getElementById("goal-sound"); +const hitSound = document.getElementById("hit-sound"); + +let maze = []; +let playerPos = {x: 0, y: 0}; +let moves = 0; +let timer = 0; +let interval; +let paused = false; + +// Maze layout (S = start, G = goal, โ†‘โ†“โ†โ†’ = arrows, X = obstacle) +const layout = [ + ["S","โ†’","โ†’","โ†“","X"], + ["X","โ†“","X","โ†“","โ†“"], + ["โ†’","โ†’","โ†’","โ†’","โ†“"], + ["X","โ†“","X","โ†’","G"] +]; + +function drawMaze() { + mazeContainer.innerHTML = ""; + maze = []; + for(let i=0;i=layout.length||playerPos.y<0||playerPos.y>=layout[0].length){ + playerPos.x = Math.max(0,Math.min(playerPos.x,layout.length-1)); + playerPos.y = Math.max(0,Math.min(playerPos.y,layout[0].length-1)); + hitSound.play(); + } else { + moveSound.play(); + } + + // Check goal + if(layout[playerPos.x][playerPos.y]==="G"){ + placePlayer(); + goalSound.play(); + clearInterval(interval); + alert(`๐ŸŽ‰ You reached the goal in ${moves} moves and ${timer}s!`); + return; + } + + placePlayer(); + moves++; + movesEl.textContent = moves; +} + +function startGame(){ + drawMaze(); + moves=0; + timer=0; + movesEl.textContent = moves; + timerEl.textContent = timer; + paused = false; + clearInterval(interval); + interval = setInterval(()=> { + if(!paused) timer++; + timerEl.textContent = timer; + movePlayer(); // auto move per tile + }, 800); +} + +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", ()=>paused=true); +resumeBtn.addEventListener("click", ()=>paused=false); +restartBtn.addEventListener("click", startGame); diff --git a/games/direction-maze/style.css b/games/direction-maze/style.css new file mode 100644 index 00000000..dc04d7eb --- /dev/null +++ b/games/direction-maze/style.css @@ -0,0 +1,94 @@ +body { + font-family: 'Arial', sans-serif; + background: #0e0e0e; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; +} + +.maze-container { + text-align: center; + background: #111; + padding: 20px 30px; + border-radius: 15px; + box-shadow: 0 0 20px #0ff, 0 0 40px #0ff, 0 0 60px #0ff; +} + +#maze { + display: flex; + justify-content: center; + flex-wrap: wrap; + margin: 20px 0; + max-width: 400px; +} + +.tile { + width: 40px; + height: 40px; + margin: 2px; + display: flex; + justify-content: center; + align-items: center; + font-weight: bold; + font-size: 20px; + border-radius: 5px; + transition: all 0.3s ease; +} + +.tile.arrow { + background: #0ff; + color: #000; + text-shadow: 0 0 5px #0ff; +} + +.tile.start { + background: #0f0; + color: #000; + box-shadow: 0 0 10px #0f0, 0 0 20px #0f0; +} + +.tile.goal { + background: #f00; + color: #fff; + box-shadow: 0 0 10px #f00, 0 0 20px #f00; +} + +.tile.obstacle { + background: #555; + color: #fff; +} + +.tile.player { + box-shadow: 0 0 15px 5px #ff0; + animation: glow 1s infinite alternate; +} + +@keyframes glow { + from { box-shadow: 0 0 15px 5px #ff0; } + to { box-shadow: 0 0 25px 10px #ff0; } +} + +.controls button { + margin: 5px; + padding: 8px 15px; + border: none; + border-radius: 5px; + cursor: pointer; + font-weight: bold; + background: #0ff; + color: #000; + transition: all 0.2s ease; +} + +.controls button:hover { + background: #0aa; + color: #fff; +} + +.maze-status { + display: flex; + justify-content: space-around; + margin-bottom: 10px; +} diff --git a/games/doodle-jump-game/index.html b/games/doodle-jump-game/index.html new file mode 100644 index 00000000..9ca81e30 --- /dev/null +++ b/games/doodle-jump-game/index.html @@ -0,0 +1,17 @@ + + + + + + Vertical Ascent + + + +
+ +
Score: 0
+
+ + + + \ No newline at end of file diff --git a/games/doodle-jump-game/script.js b/games/doodle-jump-game/script.js new file mode 100644 index 00000000..1b1c69f5 --- /dev/null +++ b/games/doodle-jump-game/script.js @@ -0,0 +1,295 @@ +// --- Game Setup and Constants --- +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const scoreElement = document.getElementById('score'); + +// Game state variables +let game = { + running: true, + score: 0, + scrollSpeed: 0 // How fast the screen is currently scrolling down +}; + +const PLATFORM_COUNT = 15; +const GRAVITY = 0.5; +const JUMP_VELOCITY = -15; +const SCROLL_THRESHOLD = canvas.height * 0.4; // Scroll starts when player is in the top 40% of the screen + +// --- Player Object --- +let player = { + x: canvas.width / 2, + y: canvas.height - 50, + width: 30, + height: 30, + velX: 0, + velY: 0, + speed: 6, + onGround: false, + keys: { + left: false, + right: false + } +}; + +// --- Platform Object Array --- +let platforms = []; + +/** + * Platform Class for creating different types of platforms. + */ +class Platform { + constructor(x, y, width, type = 'normal') { + this.x = x; + this.y = y; + this.width = width; + this.height = 10; + this.type = type; // 'normal', 'moving', 'breakable' + this.color = this.getColor(); + this.movingDir = (Math.random() > 0.5) ? 1 : -1; // For 'moving' platforms + this.moveSpeed = 1; + this.life = 1; // For 'breakable' platforms + } + + getColor() { + switch(this.type) { + case 'moving': return 'lightgreen'; + case 'breakable': return 'tomato'; + default: return 'white'; // 'normal' + } + } + + update() { + if (this.type === 'moving') { + this.x += this.movingDir * this.moveSpeed; + // Reverse direction if hitting canvas edges + if (this.x + this.width > canvas.width || this.x < 0) { + this.movingDir *= -1; + } + } + } + + draw() { + ctx.fillStyle = this.color; + ctx.fillRect(this.x, this.y, this.width, this.height); + } +} + +// --- Initialization Functions --- + +/** + * Initializes the starting platforms and the player position. + */ +function initPlatforms() { + // Starting platform for the player + platforms.push(new Platform(player.x - 50, player.y + player.height, 100)); + + // Generate platforms randomly above the starting point + for (let i = 0; i < PLATFORM_COUNT; i++) { + generateNewPlatform(); + } +} + +/** + * Generates a single new platform at the top of the screen. + */ +function generateNewPlatform() { + // Find the highest platform's Y position + const highestPlatformY = platforms.reduce((minY, p) => Math.min(minY, p.y), Infinity); + + // Y position is randomly a bit above the previous highest + const y = highestPlatformY - 80 - Math.random() * 50; + + // X position is randomly across the width + const width = 60 + Math.random() * 60; // Random width + const x = Math.random() * (canvas.width - width); + + // Random platform type (20% moving, 10% breakable, 70% normal) + const rand = Math.random(); + let type = 'normal'; + if (rand < 0.2) type = 'moving'; + else if (rand < 0.3) type = 'breakable'; + + platforms.push(new Platform(x, y, width, type)); +} + +// --- Game Logic Functions --- + +/** + * Handles all player physics: movement, gravity, and wrap-around. + */ +function updatePlayer() { + // Apply gravity + player.velY += GRAVITY; + + // Horizontal movement from keyboard + player.velX = 0; + if (player.keys.left) { + player.velX = -player.speed; + } + if (player.keys.right) { + player.velX = player.speed; + } + player.x += player.velX; + + // Wrap-around horizontal movement (left edge to right edge and vice-versa) + if (player.x + player.width < 0) { + player.x = canvas.width; + } else if (player.x > canvas.width) { + player.x = -player.width; + } + + // Check if player has fallen off the bottom (Game Over condition) + if (player.y > canvas.height) { + gameOver(); + } +} + +/** + * Handles the vertical scrolling effect and updates platforms. + */ +function updatePlatforms() { + // 1. Calculate Scrolling: If player is high up, scroll everything down. + if (player.y < SCROLL_THRESHOLD) { + // Calculate scroll speed based on how far above the threshold the player is + game.scrollSpeed = SCROLL_THRESHOLD - player.y; + + // Move all platforms down + platforms.forEach(p => p.y += game.scrollSpeed); + + // Move player down relative to the scroll + player.y += game.scrollSpeed; + + // Increase score based on the scroll distance + game.score += Math.floor(game.scrollSpeed); + scoreElement.textContent = `Score: ${game.score}`; + } else { + game.scrollSpeed = 0; // Stop scrolling when player is falling or stable + } + + // 2. Platform Updates and Generation + let highestPlatformY = Infinity; + + for (let i = platforms.length - 1; i >= 0; i--) { + const p = platforms[i]; + p.update(); // Update moving platforms + + highestPlatformY = Math.min(highestPlatformY, p.y); + + // Remove platforms that have scrolled off the bottom of the screen + if (p.y > canvas.height + 50) { + platforms.splice(i, 1); + } + } + + // 3. Generate new platforms if the highest one is too far down + if (highestPlatformY > 0) { + // Keep generating until the highest platform is above the screen + generateNewPlatform(); + } +} + + +/** + * Checks for collision between the player and any platform. + */ +function checkCollisions() { + player.onGround = false; + player.y += player.velY; // Apply vertical movement temporarily + + platforms.forEach(p => { + // Simplified AABB (Axis-Aligned Bounding Box) collision check + const overlapX = player.x < p.x + p.width && player.x + player.width > p.x; + const overlapY = player.y + player.height > p.y && player.y + player.height < p.y + p.height; + + // Check for landing from above (only if falling) + if (player.velY > 0 && overlapX && overlapY) { + // Collision detected! Stop falling and perform a jump. + player.velY = JUMP_VELOCITY; + player.y = p.y - player.height; // Snap player to the top of the platform + player.onGround = true; + + // Handle breakable platforms + if (p.type === 'breakable') { + p.life -= 1; + } + } + }); + + // Final position update after collision checks + if (!player.onGround) { + // If no collision occurred, the temporary y change is final + // (This is a simplified physics model, a more robust one would + // re-subtract the applied velY and re-apply it based on collisions) + } + + // Remove broken platforms + platforms = platforms.filter(p => p.life > 0); +} + +// --- Rendering Functions --- + +/** + * Clears the canvas and draws all game elements. + */ +function draw() { + // Clear the canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw platforms + platforms.forEach(p => p.draw()); + + // Draw the player (simple square) + ctx.fillStyle = 'yellow'; + ctx.fillRect(player.x, player.y, player.width, player.height); +} + +// --- Main Game Loop --- + +let lastTime = 0; +function gameLoop(timestamp) { + if (!game.running) return; + + // Use requestAnimationFrame's timestamp for frame-independent movement + // (A full implementation would use delta time for physics, but for a + // simple game, constant update rate is often sufficient) + + updatePlayer(); + updatePlatforms(); + checkCollisions(); + draw(); + + // Call the game loop again + requestAnimationFrame(gameLoop); +} + +// --- Event Handlers --- + +function handleKeydown(event) { + if (event.key === 'ArrowLeft' || event.key.toLowerCase() === 'a') { + player.keys.left = true; + } else if (event.key === 'ArrowRight' || event.key.toLowerCase() === 'd') { + player.keys.right = true; + } +} + +function handleKeyup(event) { + if (event.key === 'ArrowLeft' || event.key.toLowerCase() === 'a') { + player.keys.left = false; + } else if (event.key === 'ArrowRight' || event.key.toLowerCase() === 'd') { + player.keys.right = false; + } +} + +function gameOver() { + game.running = false; + alert(`Game Over! Final Score: ${game.score}`); + // Optional: Reload the page or show a restart button + // window.location.reload(); +} + +// --- Start the Game --- +document.addEventListener('keydown', handleKeydown); +document.addEventListener('keyup', handleKeyup); + +initPlatforms(); +requestAnimationFrame(gameLoop); \ No newline at end of file diff --git a/games/doodle-jump-game/style.css b/games/doodle-jump-game/style.css new file mode 100644 index 00000000..ecd77f72 --- /dev/null +++ b/games/doodle-jump-game/style.css @@ -0,0 +1,33 @@ +/* Basic reset and container styling */ +body { + background-color: #f0f0f0; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + font-family: sans-serif; +} + +/* Game container for canvas and score */ +#game-container { + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + position: relative; +} + +/* Canvas styling */ +#gameCanvas { + background-color: #00001a; /* Dark blue background for 'sky' effect */ + display: block; +} + +/* Score overlay styling */ +#score { + position: absolute; + top: 10px; + left: 10px; + color: white; + font-size: 1.5em; + font-weight: bold; + user-select: none; /* Prevent text selection */ +} \ No newline at end of file diff --git a/games/doodle-jump/index.html b/games/doodle-jump/index.html new file mode 100644 index 00000000..3621079d --- /dev/null +++ b/games/doodle-jump/index.html @@ -0,0 +1,17 @@ + + + + + + Doodle Jump + + + +
+ +
Score: 0
+
Use arrow keys or tap to move. Jump on platforms!
+
+ + + \ No newline at end of file diff --git a/games/doodle-jump/script.js b/games/doodle-jump/script.js new file mode 100644 index 00000000..e39dc540 --- /dev/null +++ b/games/doodle-jump/script.js @@ -0,0 +1,146 @@ +const canvas = document.getElementById('game-canvas'); +const ctx = canvas.getContext('2d'); +const scoreEl = document.getElementById('score-value'); + +let player = { x: 200, y: 500, vx: 0, vy: 0, w: 20, h: 20 }; +let platforms = []; +let stars = []; +let score = 0; +let cameraY = 0; +const gravity = 0.5; +const jumpForce = -12; +const moveSpeed = 5; + +function init() { + platforms = []; + stars = []; + player = { x: 200, y: 500, vx: 0, vy: 0, w: 20, h: 20 }; + score = 0; + cameraY = 0; + generatePlatforms(); + generateStars(); + gameLoop(); +} + +function generatePlatforms() { + platforms = [{ x: 150, y: 550, w: 100, h: 10 }]; + for (let i = 1; i < 20; i++) { + platforms.push({ + x: Math.random() * (canvas.width - 100), + y: 550 - i * 100, + w: 100, + h: 10 + }); + } +} + +function generateStars() { + for (let i = 0; i < 10; i++) { + stars.push({ + x: Math.random() * canvas.width, + y: Math.random() * 600, + w: 10, + h: 10 + }); + } +} + +function update() { + player.vy += gravity; + player.x += player.vx; + player.y += player.vy; + + // Wrap around screen + if (player.x < 0) player.x = canvas.width; + if (player.x > canvas.width) player.x = 0; + + // Check platform collisions + platforms.forEach(p => { + if (player.vy > 0 && player.x < p.x + p.w && player.x + player.w > p.x && + player.y < p.y + p.h && player.y + player.h > p.y) { + player.vy = jumpForce; + score += 10; + } + }); + + // Check star collisions + stars = stars.filter(s => { + if (player.x < s.x + s.w && player.x + player.w > s.x && + player.y < s.y + s.h && player.y + player.h > s.y) { + score += 50; + return false; + } + return true; + }); + + // Scroll camera + if (player.y < 200) { + cameraY += 200 - player.y; + player.y = 200; + platforms.forEach(p => p.y += 200 - player.y); + stars.forEach(s => s.y += 200 - player.y); + } + + // Game over + if (player.y > canvas.height + 100) { + init(); + } + + scoreEl.textContent = score; +} + +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw platforms + ctx.fillStyle = '#8B4513'; + platforms.forEach(p => { + ctx.fillRect(p.x, p.y - cameraY, p.w, p.h); + }); + + // Draw stars + ctx.fillStyle = '#FFD700'; + stars.forEach(s => { + ctx.fillRect(s.x, s.y - cameraY, s.w, s.h); + }); + + // Draw player + ctx.fillStyle = '#FF0000'; + ctx.fillRect(player.x, player.y - cameraY, player.w, player.h); +} + +function gameLoop() { + update(); + draw(); + requestAnimationFrame(gameLoop); +} + +// Input handling +let keys = {}; +document.addEventListener('keydown', e => { + keys[e.code] = true; + if (e.code === 'ArrowLeft') player.vx = -moveSpeed; + if (e.code === 'ArrowRight') player.vx = moveSpeed; +}); +document.addEventListener('keyup', e => { + keys[e.code] = false; + if (e.code === 'ArrowLeft' || e.code === 'ArrowRight') player.vx = 0; +}); + +// Touch controls +let touchStartX = 0; +canvas.addEventListener('touchstart', e => { + touchStartX = e.touches[0].clientX; +}); +canvas.addEventListener('touchmove', e => { + e.preventDefault(); + const touchX = e.touches[0].clientX; + if (touchX < touchStartX - 10) player.vx = -moveSpeed; + else if (touchX > touchStartX + 10) player.vx = moveSpeed; + else player.vx = 0; +}); +canvas.addEventListener('touchend', () => { + player.vx = 0; +}); + +init(); \ No newline at end of file diff --git a/games/doodle-jump/style.css b/games/doodle-jump/style.css new file mode 100644 index 00000000..2f890aa3 --- /dev/null +++ b/games/doodle-jump/style.css @@ -0,0 +1,31 @@ +body { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background: linear-gradient(to bottom, #87CEEB, #98FB98); + font-family: Arial, sans-serif; +} + +#game-container { + position: relative; + text-align: center; +} + +#game-canvas { + border: 2px solid #333; + background: #f0f8ff; +} + +#score { + font-size: 24px; + margin: 10px 0; + color: #333; +} + +#instructions { + font-size: 16px; + color: #666; + margin-top: 10px; +} \ No newline at end of file diff --git a/games/drag_race/index.html b/games/drag_race/index.html new file mode 100644 index 00000000..883bb6b5 --- /dev/null +++ b/games/drag_race/index.html @@ -0,0 +1,33 @@ + + + + + + Horizontal Drag Race + + + + +
+

๐ŸŽ๏ธ Horizontal Drag Race

+ +
+ Speed: 0 | Time: 0.00s +
+ +
+
+
+ +
+

Click START, then drag the mouse rapidly left and right over the vehicle!

+
+ +
+ +
+
+ + + + \ No newline at end of file diff --git a/games/drag_race/script.js b/games/drag_race/script.js new file mode 100644 index 00000000..87b5a140 --- /dev/null +++ b/games/drag_race/script.js @@ -0,0 +1,166 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. DOM Elements & Constants --- + const vehicle = document.getElementById('vehicle'); + const raceTrack = document.getElementById('race-track'); + const startButton = document.getElementById('start-button'); + const speedDisplay = document.getElementById('speed-display'); + const timeDisplay = document.getElementById('time-display'); + const feedbackMessage = document.getElementById('feedback-message'); + + const TRACK_WIDTH = raceTrack.clientWidth; + const VEHICLE_SIZE = vehicle.clientWidth; + const FINISH_LINE = TRACK_WIDTH - VEHICLE_SIZE; // Target vehicle's left position + const MAX_SPEED_CAP = 100; // Cap displayed speed for readability + const ACCELERATION_MULTIPLIER = 0.5; // How quickly mouse speed translates to vehicle progress + + // --- 2. Game State Variables --- + let gameActive = false; + let isDragging = false; + let vehicleProgress = 0; // 0 to FINISH_LINE + + // Mouse tracking for velocity calculation + let lastMouseX = 0; + let lastTime = 0; + + // Timing variables + let raceStartTime = 0; + let timerInterval = null; + + // --- 3. CORE LOGIC FUNCTIONS --- + + /** + * Resets all game parameters for a new race. + */ + function resetGame() { + gameActive = false; + isDragging = false; + vehicleProgress = 0; + raceStartTime = 0; + clearInterval(timerInterval); + + vehicle.style.transform = `translateX(0px)`; + speedDisplay.textContent = 0; + timeDisplay.textContent = '0.00'; + startButton.textContent = 'START RACE'; + startButton.disabled = false; + feedbackMessage.textContent = 'Drag rapidly to accelerate!'; + } + + /** + * Starts the race and the timer. + */ + function startRace() { + resetGame(); // Ensure clean start + gameActive = true; + startButton.disabled = true; + + raceStartTime = performance.now(); + + // Start the timer display + timerInterval = setInterval(() => { + if (gameActive) { + const elapsed = (performance.now() - raceStartTime) / 1000; + timeDisplay.textContent = elapsed.toFixed(2); + } + }, 50); // Update time every 50ms + + feedbackMessage.textContent = 'GO! Drag left and right!'; + } + + /** + * Calculates the horizontal mouse speed and accelerates the vehicle. + * @param {MouseEvent} e - The mousemove event. + */ + function calculateSpeed(e) { + if (!gameActive || !isDragging) return; + + const currentTime = performance.now(); + + // --- Velocity Calculation --- + // Distance traveled by mouse (delta X) + const dx = Math.abs(e.clientX - lastMouseX); + // Time elapsed since last check (delta T) + const dt = currentTime - lastTime; + + if (dt > 0) { + // Speed = Distance / Time + // We multiply by a large factor (e.g., 100) to get a visible speed number + const currentSpeed = (dx / dt) * 100; + + // Map the mouse speed to vehicle acceleration + // Only apply acceleration if the mouse is moving quickly + if (currentSpeed > 5) { + // Apply a portion of the mouse movement to the vehicle's progress + const progressToAdd = dx * ACCELERATION_MULTIPLIER; + vehicleProgress = Math.min(FINISH_LINE, vehicleProgress + progressToAdd); + } + + // Update display speed (Cap the display) + speedDisplay.textContent = Math.min(MAX_SPEED_CAP, currentSpeed.toFixed(0)); + + // Update state variables for the next frame calculation + lastMouseX = e.clientX; + lastTime = currentTime; + + updateVehiclePosition(); + checkWinCondition(); + } + } + + /** + * Applies the current progress to the vehicle's visual position. + */ + function updateVehiclePosition() { + // Use translateX for smoother animation performance + vehicle.style.transform = `translateX(${vehicleProgress}px)`; + } + + /** + * Checks if the vehicle has crossed the finish line. + */ + function checkWinCondition() { + if (vehicleProgress >= FINISH_LINE) { + endRace(); + } + } + + /** + * Stops the race and displays the final time. + */ + function endRace() { + if (!gameActive) return; + gameActive = false; + clearInterval(timerInterval); + + const finalTime = timeDisplay.textContent; + feedbackMessage.innerHTML = `๐Ÿ† **FINISH!** Your time is **${finalTime}s**!`; + feedbackMessage.style.color = '#2ecc71'; + + startButton.textContent = 'PLAY AGAIN'; + startButton.disabled = false; + } + + // --- 4. EVENT LISTENERS --- + + startButton.addEventListener('click', startRace); + + // 1. Mouse Down: Start the dragging state and initialize coordinates + raceTrack.addEventListener('mousedown', (e) => { + if (!gameActive) return; + isDragging = true; + lastMouseX = e.clientX; + lastTime = performance.now(); + }); + + // 2. Mouse Move: Calculate speed and progress + raceTrack.addEventListener('mousemove', calculateSpeed); + + // 3. Mouse Up/Out: Stop the dragging state + window.addEventListener('mouseup', () => { + isDragging = false; + speedDisplay.textContent = 0; + }); + + // Initial setup to display the track dimensions + resetGame(); +}); \ No newline at end of file diff --git a/games/drag_race/style.css b/games/drag_race/style.css new file mode 100644 index 00000000..862d2e99 --- /dev/null +++ b/games/drag_race/style.css @@ -0,0 +1,93 @@ +:root { + --track-width: 600px; + --vehicle-size: 80px; +} + +body { + font-family: 'Bebas Neue', sans-serif; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #2c3e50; /* Dark road color */ + color: #ecf0f1; +} + +#game-container { + background-color: #34495e; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4); + text-align: center; + max-width: 700px; + width: 90%; +} + +h1 { + color: #f1c40f; + margin-bottom: 20px; +} + +#status-area { + font-size: 1.2em; + font-weight: bold; + margin-bottom: 20px; +} + +/* --- Race Track --- */ +#race-track { + width: var(--track-width); + height: 100px; + background: repeating-linear-gradient( + to right, + #7f8c8d, #7f8c8d 10px, + #bdc3c7 10px, #bdc3c7 20px /* Simulated dashed road line */ + ); + border: 5px solid #bdc3c7; + border-radius: 8px; + margin: 0 auto; + position: relative; + cursor: grab; /* Indicates dragging is required */ + overflow: hidden; +} + +/* --- Vehicle --- */ +#vehicle { + width: var(--vehicle-size); + height: var(--vehicle-size); + /* Placeholder for the vehicle image */ + background-image: url('images/car-sprite.png'); /* !!! UPDATE THIS PATH !!! */ + background-size: cover; + background-color: #e74c3c; /* Red placeholder */ + border-radius: 50%; + position: absolute; + bottom: 0; + left: 0; /* Initial position */ + transition: transform 0.1s linear; /* Smooth visual movement */ +} + +/* --- Controls and Feedback --- */ +#feedback-message { + min-height: 20px; + margin-top: 20px; + margin-bottom: 15px; + font-size: 1.1em; +} + +#start-button { + padding: 15px 30px; + font-size: 1.2em; + font-weight: bold; + background-color: #3498db; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +#start-button:hover:not(:disabled) { + background-color: #2980b9; +} \ No newline at end of file diff --git a/games/drawing_game/index.html b/games/drawing_game/index.html new file mode 100644 index 00000000..d88127ee --- /dev/null +++ b/games/drawing_game/index.html @@ -0,0 +1,30 @@ + + + + + + Canvas Line Drawer + + + + +
+

๐ŸŽจ Simple Canvas Drawer

+ +
+ + + + + + 5px + + +
+ + +
+ + + + \ No newline at end of file diff --git a/games/drawing_game/script.js b/games/drawing_game/script.js new file mode 100644 index 00000000..4d942f00 --- /dev/null +++ b/games/drawing_game/script.js @@ -0,0 +1,95 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. DOM and Canvas Setup --- + const canvas = document.getElementById('drawing-canvas'); + // Get the 2D rendering context + const ctx = canvas.getContext('2d'); + + const colorPicker = document.getElementById('color-picker'); + const brushSize = document.getElementById('brush-size'); + const sizeDisplay = document.getElementById('size-display'); + const clearButton = document.getElementById('clear-button'); + + // --- 2. State Variables --- + let isDrawing = false; + let lastX = 0; + let lastY = 0; + + // Set canvas dimensions (must be done in JS to work correctly for drawing) + const CANVAS_WIDTH = 600; + const CANVAS_HEIGHT = 400; + canvas.width = CANVAS_WIDTH; + canvas.height = CANVAS_HEIGHT; + + // Set initial context drawing properties + ctx.strokeStyle = colorPicker.value; + ctx.lineJoin = 'round'; // Gives smooth corners + ctx.lineCap = 'round'; // Gives smooth line ends + ctx.lineWidth = brushSize.value; + + + // --- 3. Core Drawing Function --- + + /** + * Draws a line segment from (lastX, lastY) to (e.offsetX, e.offsetY). + * @param {MouseEvent} e - The mousemove event object. + */ + function draw(e) { + if (!isDrawing) return; // Stop the function if mouse is not down + + // Update drawing style based on controls + ctx.strokeStyle = colorPicker.value; + ctx.lineWidth = brushSize.value; + + // Begin the path + ctx.beginPath(); + + // Move the drawing start point to the previous coordinates + ctx.moveTo(lastX, lastY); + + // Draw a line to the current mouse coordinates + ctx.lineTo(e.offsetX, e.offsetY); + + // Apply the stroke (i.e., actually draw the line) + ctx.stroke(); + + // Update the last position to the current position for the next segment + [lastX, lastY] = [e.offsetX, e.offsetY]; + } + + + // --- 4. Event Handlers --- + + // A. MOUSE DOWN: Start drawing + canvas.addEventListener('mousedown', (e) => { + isDrawing = true; + // Set the starting point for the first line segment + [lastX, lastY] = [e.offsetX, e.offsetY]; + }); + + // B. MOUSE MOVE: Continue drawing + canvas.addEventListener('mousemove', draw); + + // C. MOUSE UP: Stop drawing + window.addEventListener('mouseup', () => { + isDrawing = false; + }); + + // D. MOUSE LEAVE: Stop drawing if the mouse leaves the canvas area + canvas.addEventListener('mouseout', () => { + isDrawing = false; + }); + + // E. Control Handlers + + // Update brush size display when range input changes + brushSize.addEventListener('input', () => { + sizeDisplay.textContent = brushSize.value; + ctx.lineWidth = brushSize.value; + }); + + // Handle clearing the canvas + clearButton.addEventListener('click', () => { + // Clear the entire canvas rectangle + ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + }); +}); \ No newline at end of file diff --git a/games/drawing_game/style.css b/games/drawing_game/style.css new file mode 100644 index 00000000..3b89c17c --- /dev/null +++ b/games/drawing_game/style.css @@ -0,0 +1,58 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f0f0f0; +} + +#app-container { + background-color: white; + padding: 25px; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + text-align: center; +} + +h1 { + color: #333; + margin-bottom: 20px; +} + +/* --- Controls Styling --- */ +#controls { + margin-bottom: 15px; + padding: 10px; + background-color: #e9e9e9; + border-radius: 5px; + display: flex; + justify-content: space-around; + align-items: center; +} + +#brush-size { + width: 80px; +} + +#clear-button { + padding: 8px 15px; + background-color: #e74c3c; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; +} + +#clear-button:hover { + background-color: #c0392b; +} + +/* --- Canvas Styling --- */ +#drawing-canvas { + border: 2px solid #333; + background-color: white; + cursor: crosshair; /* Indicate a drawing tool */ +} \ No newline at end of file diff --git a/games/echo-chamber/index.html b/games/echo-chamber/index.html new file mode 100644 index 00000000..eed661b1 --- /dev/null +++ b/games/echo-chamber/index.html @@ -0,0 +1,18 @@ + + + + + + Echo Chamber Game + + + +
+

Echo Chamber

+ +
Score: 0
+
Click to launch sound waves. Bounce them off walls to hit targets and match frequencies!
+
+ + + \ No newline at end of file diff --git a/games/echo-chamber/script.js b/games/echo-chamber/script.js new file mode 100644 index 00000000..c9877a5f --- /dev/null +++ b/games/echo-chamber/script.js @@ -0,0 +1,145 @@ +// Echo Chamber Game Script +// Bounce sound waves off walls to hit targets in this audio-visual puzzle. + +const canvas = document.getElementById('game-canvas'); +const ctx = canvas.getContext('2d'); +const scoreElement = document.getElementById('score'); + +// Game variables +let player = { x: 400, y: 300 }; +let waves = []; +let targets = []; +let walls = []; +let score = 0; +let gameRunning = true; + +// Constants +const waveSpeed = 5; +const targetRadius = 20; + +// Initialize game +function init() { + // Create walls + walls.push({ x: 100, y: 100, width: 600, height: 10 }); // Top + walls.push({ x: 100, y: 490, width: 600, height: 10 }); // Bottom + walls.push({ x: 100, y: 100, width: 10, height: 400 }); // Left + walls.push({ x: 690, y: 100, width: 10, height: 400 }); // Right + walls.push({ x: 300, y: 200, width: 200, height: 10 }); // Middle horizontal + walls.push({ x: 400, y: 300, width: 10, height: 200 }); // Middle vertical + + // Create targets + targets.push({ x: 200, y: 150, color: '#ff0000', frequency: 1 }); + targets.push({ x: 600, y: 150, color: '#00ff00', frequency: 2 }); + targets.push({ x: 200, y: 450, color: '#0000ff', frequency: 3 }); + targets.push({ x: 600, y: 450, color: '#ffff00', frequency: 4 }); + + // Start game loop + requestAnimationFrame(gameLoop); +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + update(); + draw(); + + requestAnimationFrame(gameLoop); +} + +// Update game state +function update() { + // Move waves + waves.forEach(wave => { + wave.x += wave.dx; + wave.y += wave.dy; + + // Check wall collisions + walls.forEach(wall => { + if (wave.x > wall.x && wave.x < wall.x + wall.width && + wave.y > wall.y && wave.y < wall.y + wall.height) { + // Bounce + if (wave.dx > 0 && wave.x - wall.x < 5) wave.dx = -wave.dx; + if (wave.dx < 0 && wall.x + wall.width - wave.x < 5) wave.dx = -wave.dx; + if (wave.dy > 0 && wave.y - wall.y < 5) wave.dy = -wave.dy; + if (wave.dy < 0 && wall.y + wall.height - wave.y < 5) wave.dy = -wave.dy; + } + }); + + // Check target collisions + targets.forEach((target, i) => { + const dist = Math.sqrt((wave.x - target.x)**2 + (wave.y - target.y)**2); + if (dist < targetRadius) { + targets.splice(i, 1); + score += 10; + if (targets.length === 0) { + gameRunning = false; + alert('All targets destroyed! Score: ' + score); + } + } + }); + }); + + // Remove waves that are off-screen + waves = waves.filter(wave => wave.x > 0 && wave.x < canvas.width && wave.y > 0 && wave.y < canvas.height); +} + +// Draw everything +function draw() { + // Clear canvas + ctx.fillStyle = '#000022'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw walls + ctx.fillStyle = '#666666'; + walls.forEach(wall => ctx.fillRect(wall.x, wall.y, wall.width, wall.height)); + + // Draw targets + targets.forEach(target => { + ctx.fillStyle = target.color; + ctx.beginPath(); + ctx.arc(target.x, target.y, targetRadius, 0, Math.PI * 2); + ctx.fill(); + }); + + // Draw player + ctx.fillStyle = '#ffffff'; + ctx.beginPath(); + ctx.arc(player.x, player.y, 10, 0, Math.PI * 2); + ctx.fill(); + + // Draw waves + ctx.strokeStyle = '#00ffff'; + ctx.lineWidth = 2; + waves.forEach(wave => { + ctx.beginPath(); + ctx.arc(wave.x, wave.y, 5, 0, Math.PI * 2); + ctx.stroke(); + }); + + // Update score + scoreElement.textContent = 'Score: ' + score; +} + +// Handle click +canvas.addEventListener('click', e => { + const rect = canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + // Launch wave towards mouse + const dx = mouseX - player.x; + const dy = mouseY - player.y; + const dist = Math.sqrt(dx*dx + dy*dy); + if (dist > 0) { + waves.push({ + x: player.x, + y: player.y, + dx: (dx / dist) * waveSpeed, + dy: (dy / dist) * waveSpeed + }); + } +}); + +// Start the game +init(); \ No newline at end of file diff --git a/games/echo-chamber/style.css b/games/echo-chamber/style.css new file mode 100644 index 00000000..a58c9ee7 --- /dev/null +++ b/games/echo-chamber/style.css @@ -0,0 +1,39 @@ +body { + font-family: Arial, sans-serif; + background-color: #000; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +#game-container { + text-align: center; +} + +h1 { + font-size: 2.5em; + margin-bottom: 20px; + text-shadow: 0 0 10px #00ffff; +} + +#game-canvas { + border: 2px solid #00ffff; + background-color: #000022; + box-shadow: 0 0 20px #00ffff; + cursor: crosshair; +} + +#score { + font-size: 1.2em; + margin: 10px 0; + color: #ffff00; +} + +#instructions { + font-size: 1em; + margin-top: 10px; + color: #cccccc; +} \ No newline at end of file diff --git a/games/echo-echo/index.html b/games/echo-echo/index.html new file mode 100644 index 00000000..64117fea --- /dev/null +++ b/games/echo-echo/index.html @@ -0,0 +1,45 @@ + + + + + + Echo Echo + + + + + + + + + +
+
+

ECHO ECHO

+

A rhythm-based game where you listen to sound echoes and repeat them by clicking at precise moments to build harmonious sequences.

+ +

How to Play:

+
    +
  • Watch and listen to the sequence of echoes
  • +
  • Click when you see the echo rings to repeat the pattern
  • +
  • Build longer and more complex sequences
  • +
  • Stay in rhythm to achieve harmony!
  • +
+ + +
+
+ +
+
Score: 0
+
Level: 1
+
Sequence: 0/0
+
+ + + +
Click!
+ + + + \ No newline at end of file diff --git a/games/echo-echo/script.js b/games/echo-echo/script.js new file mode 100644 index 00000000..efa8b402 --- /dev/null +++ b/games/echo-echo/script.js @@ -0,0 +1,299 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const startButton = document.getElementById('startButton'); +const instructionsOverlay = document.getElementById('instructions-overlay'); +const scoreElement = document.getElementById('score'); +const levelElement = document.getElementById('level'); +const sequenceElement = document.getElementById('sequence'); +const clickIndicator = document.getElementById('click-indicator'); + +canvas.width = 800; +canvas.height = 600; + +let gameRunning = false; +let gameState = 'waiting'; // waiting, showing, repeating, checking +let score = 0; +let level = 1; +let sequence = []; +let playerSequence = []; +let sequenceIndex = 0; +let playerIndex = 0; +let echoes = []; +let audioContext; + +// Colors for different echo types +const echoColors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7', '#dda0dd', '#98d8c8', '#f7dc6f']; + +// Echo class +class Echo { + constructor(x, y, color, startTime) { + this.x = x; + this.y = y; + this.color = color; + this.startTime = startTime; + this.radius = 0; + this.maxRadius = 100; + this.duration = 1000; // 1 second + this.active = true; + } + + update(currentTime) { + const elapsed = currentTime - this.startTime; + const progress = elapsed / this.duration; + + if (progress >= 1) { + this.active = false; + return; + } + + this.radius = this.maxRadius * progress; + } + + draw() { + if (!this.active) return; + + ctx.strokeStyle = this.color; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.stroke(); + + // Inner ring + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius * 0.7, 0, Math.PI * 2); + ctx.stroke(); + + // Center dot + ctx.fillStyle = this.color; + ctx.beginPath(); + ctx.arc(this.x, this.y, 5, 0, Math.PI * 2); + ctx.fill(); + } +} + +// Initialize audio context +function initAudio() { + try { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } catch (e) { + console.warn('Web Audio API not supported'); + } +} + +// Play sound for echo +function playEchoSound(frequency, duration = 0.3) { + if (!audioContext) return; + + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime); + oscillator.type = 'sine'; + + gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration); + + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + duration); +} + +// Generate sequence +function generateSequence() { + sequence = []; + const length = Math.min(3 + level, 8); // Max 8 echoes + + for (let i = 0; i < length; i++) { + const x = Math.random() * (canvas.width - 200) + 100; + const y = Math.random() * (canvas.height - 200) + 100; + const colorIndex = Math.floor(Math.random() * echoColors.length); + sequence.push({ x, y, colorIndex, delay: i * 800 }); // 800ms between echoes + } +} + +// Show sequence +function showSequence() { + gameState = 'showing'; + sequenceIndex = 0; + echoes = []; + playerSequence = []; + + const startTime = Date.now(); + + function showNextEcho() { + if (sequenceIndex >= sequence.length) { + setTimeout(() => { + gameState = 'repeating'; + showClickIndicator(); + }, 500); + return; + } + + const echoData = sequence[sequenceIndex]; + const echoTime = startTime + echoData.delay; + + if (Date.now() >= echoTime) { + const echo = new Echo(echoData.x, echoData.y, echoColors[echoData.colorIndex], Date.now()); + echoes.push(echo); + + // Play sound + const frequency = 220 + (echoData.colorIndex * 50); // Different frequencies for different colors + playEchoSound(frequency); + + sequenceIndex++; + } + + if (gameState === 'showing') { + requestAnimationFrame(showNextEcho); + } + } + + showNextEcho(); +} + +// Show click indicator +function showClickIndicator() { + clickIndicator.style.opacity = '1'; + setTimeout(() => { + clickIndicator.style.opacity = '0'; + }, 1000); +} + +// Handle player click +function handleClick(x, y) { + if (gameState !== 'repeating') return; + + const clickTime = Date.now(); + playerSequence.push({ x, y, time: clickTime }); + + // Create visual echo for player click + const echo = new Echo(x, y, '#ffffff', clickTime); + echoes.push(echo); + + // Play click sound + playEchoSound(440, 0.1); + + playerIndex++; + + if (playerIndex >= sequence.length) { + checkSequence(); + } +} + +// Check player sequence +function checkSequence() { + gameState = 'checking'; + let correctClicks = 0; + + for (let i = 0; i < Math.min(sequence.length, playerSequence.length); i++) { + const seqEcho = sequence[i]; + const playerEcho = playerSequence[i]; + + // Check if click was near the echo position (within 50px) + const distance = Math.sqrt((seqEcho.x - playerEcho.x) ** 2 + (seqEcho.y - playerEcho.y) ** 2); + if (distance < 50) { + correctClicks++; + } + } + + const accuracy = correctClicks / sequence.length; + + if (accuracy >= 0.8) { // 80% accuracy required + score += Math.floor(accuracy * 100) + level * 10; + level++; + updateUI(); + setTimeout(() => { + startRound(); + }, 2000); + } else { + gameOver(); + } +} + +// Start round +function startRound() { + generateSequence(); + updateUI(); + setTimeout(() => { + showSequence(); + }, 1000); +} + +// Game over +function gameOver() { + gameRunning = false; + alert(`Game Over! Final Score: ${score}, Level: ${level}`); + resetGame(); +} + +// Reset game +function resetGame() { + score = 0; + level = 1; + sequence = []; + playerSequence = []; + echoes = []; + updateUI(); +} + +// Update UI +function updateUI() { + scoreElement.textContent = `Score: ${score}`; + levelElement.textContent = `Level: ${level}`; + sequenceElement.textContent = `Sequence: ${playerIndex}/${sequence.length}`; +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Update and draw echoes + const currentTime = Date.now(); + echoes = echoes.filter(echo => { + echo.update(currentTime); + echo.draw(); + return echo.active; + }); + + // Draw sequence indicators + if (gameState === 'showing') { + ctx.fillStyle = '#ffffff'; + ctx.font = '24px Poppins'; + ctx.textAlign = 'center'; + ctx.fillText('Watch the sequence...', canvas.width / 2, 50); + } else if (gameState === 'repeating') { + ctx.fillStyle = '#ffffff'; + ctx.font = '24px Poppins'; + ctx.textAlign = 'center'; + ctx.fillText('Repeat the sequence!', canvas.width / 2, 50); + } + + requestAnimationFrame(gameLoop); +} + +// Event listeners +startButton.addEventListener('click', () => { + instructionsOverlay.style.display = 'none'; + initAudio(); + resetGame(); + gameRunning = true; + gameLoop(); + startRound(); +}); + +canvas.addEventListener('click', (e) => { + if (!gameRunning || gameState !== 'repeating') return; + + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + handleClick(x, y); +}); + +// Initialize +updateUI(); \ No newline at end of file diff --git a/games/echo-echo/style.css b/games/echo-echo/style.css new file mode 100644 index 00000000..803ca72e --- /dev/null +++ b/games/echo-echo/style.css @@ -0,0 +1,136 @@ +/* General Reset & Font */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Poppins', sans-serif; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%); + color: #eee; + overflow: hidden; +} + +/* Game UI */ +#game-ui { + position: absolute; + top: 20px; + left: 20px; + display: flex; + gap: 20px; + z-index: 5; +} + +#score, #level, #sequence { + background-color: rgba(0, 0, 0, 0.7); + color: #fff; + padding: 10px 15px; + border-radius: 5px; + font-size: 1.1rem; + font-weight: 600; +} + +/* Canvas */ +canvas { + background: radial-gradient(circle at center, #0f0f23 0%, #1a1a2e 70%, #16213e 100%); + border: 3px solid #4a90e2; + box-shadow: 0 0 20px rgba(74, 144, 226, 0.3); + display: block; + cursor: pointer; +} + +/* Click Indicator */ +#click-indicator { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 2rem; + color: #4a90e2; + font-weight: 600; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s; + z-index: 10; +} + +/* Instructions Screen */ +#instructions-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +#instructions-content { + background-color: #2a2a2a; + padding: 30px 40px; + border-radius: 10px; + text-align: center; + border: 2px solid #4a90e2; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); + max-width: 500px; +} + +#instructions-content h2 { + font-size: 2.5rem; + color: #4a90e2; + margin-bottom: 15px; + letter-spacing: 2px; +} + +#instructions-content p { + font-size: 1.1rem; + margin-bottom: 25px; + color: #ccc; +} + +#instructions-content h3 { + font-size: 1.2rem; + color: #eee; + margin-bottom: 10px; + border-bottom: 1px solid #444; + padding-bottom: 5px; +} + +#instructions-content ul { + list-style: none; + margin-bottom: 30px; + text-align: left; + display: inline-block; +} + +#instructions-content li { + font-size: 1rem; + color: #ccc; + margin-bottom: 8px; +} + +/* Start Button */ +#startButton { + background-color: #4a90e2; + color: white; + border: none; + padding: 12px 24px; + font-size: 1.1rem; + font-weight: 600; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} + +#startButton:hover { + background-color: #357abd; +} \ No newline at end of file diff --git a/games/echo-maze/index.html b/games/echo-maze/index.html new file mode 100644 index 00000000..0886a755 --- /dev/null +++ b/games/echo-maze/index.html @@ -0,0 +1,28 @@ + + + + + + Echo Maze | Mini JS Games Hub + + + +
+

Echo Maze

+
+ + + + Press Start to Play +
+ +
+ + + + + + + + + diff --git a/games/echo-maze/script.js b/games/echo-maze/script.js new file mode 100644 index 00000000..ae79deb4 --- /dev/null +++ b/games/echo-maze/script.js @@ -0,0 +1,173 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +canvas.width = 600; +canvas.height = 400; + +const echoSound = document.getElementById("echoSound"); +const hitSound = document.getElementById("hitSound"); +const collectSound = document.getElementById("collectSound"); + +let obstacles = []; +let collectibles = []; +let player = { x: 50, y: 50, radius: 10, speed: 3 }; +let echoes = []; +let keys = {}; +let gameInterval = null; +let isPaused = false; + +// Generate obstacles randomly +function createObstacles(count = 10) { + obstacles = []; + for (let i = 0; i < count; i++) { + obstacles.push({ + x: Math.random() * (canvas.width - 30) + 15, + y: Math.random() * (canvas.height - 30) + 15, + radius: 15 + }); + } +} + +// Generate collectibles +function createCollectibles(count = 5) { + collectibles = []; + for (let i = 0; i < count; i++) { + collectibles.push({ + x: Math.random() * (canvas.width - 20) + 10, + y: Math.random() * (canvas.height - 20) + 10, + radius: 8 + }); + } +} + +// Emit echo +function emitEcho() { + echoes.push({ x: player.x, y: player.y, radius: 0, alpha: 1 }); + echoSound.currentTime = 0; + echoSound.play(); +} + +// Draw player +function drawPlayer() { + ctx.fillStyle = "#0ff"; + ctx.beginPath(); + ctx.arc(player.x, player.y, player.radius, 0, Math.PI * 2); + ctx.fill(); +} + +// Draw obstacles +function drawObstacles() { + obstacles.forEach(o => { + ctx.strokeStyle = "#f00"; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(o.x, o.y, o.radius, 0, Math.PI * 2); + ctx.stroke(); + }); +} + +// Draw collectibles +function drawCollectibles() { + collectibles.forEach(c => { + ctx.fillStyle = "#ff0"; + ctx.beginPath(); + ctx.arc(c.x, c.y, c.radius, 0, Math.PI * 2); + ctx.fill(); + }); +} + +// Draw echoes +function drawEchoes() { + echoes.forEach(e => { + ctx.strokeStyle = `rgba(0,255,255,${e.alpha})`; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(e.x, e.y, e.radius, 0, Math.PI * 2); + ctx.stroke(); + e.radius += 2; + e.alpha -= 0.02; + }); + echoes = echoes.filter(e => e.alpha > 0); +} + +// Collision detection +function checkCollisions() { + obstacles.forEach(o => { + let dx = player.x - o.x; + let dy = player.y - o.y; + let dist = Math.sqrt(dx*dx + dy*dy); + if (dist < player.radius + o.radius) { + hitSound.currentTime = 0; + hitSound.play(); + } + }); + + collectibles = collectibles.filter(c => { + let dx = player.x - c.x; + let dy = player.y - c.y; + let dist = Math.sqrt(dx*dx + dy*dy); + if (dist < player.radius + c.radius) { + collectSound.currentTime = 0; + collectSound.play(); + return false; + } + return true; + }); +} + +// Update player position +function updatePlayer() { + if (keys["ArrowUp"] || keys["w"]) player.y -= player.speed; + if (keys["ArrowDown"] || keys["s"]) player.y += player.speed; + if (keys["ArrowLeft"] || keys["a"]) player.x -= player.speed; + if (keys["ArrowRight"] || keys["d"]) player.x += player.speed; + + // Boundaries + if (player.x < player.radius) player.x = player.radius; + if (player.x > canvas.width - player.radius) player.x = canvas.width - player.radius; + if (player.y < player.radius) player.y = player.radius; + if (player.y > canvas.height - player.radius) player.y = canvas.height - player.radius; +} + +// Game loop +function gameLoop() { + if (isPaused) return; + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawEchoes(); + drawObstacles(); + drawCollectibles(); + drawPlayer(); + checkCollisions(); +} + +// Key listeners +document.addEventListener("keydown", (e) => { + keys[e.key] = true; + if (e.key === " " || e.key === "Enter") { + emitEcho(); + } +}); +document.addEventListener("keyup", (e) => { keys[e.key] = false; }); + +// Buttons +document.getElementById("start-btn").addEventListener("click", () => { + createObstacles(); + createCollectibles(); + isPaused = false; + if (!gameInterval) gameInterval = setInterval(gameLoop, 30); + document.getElementById("status").textContent = "Game Started!"; +}); + +document.getElementById("pause-btn").addEventListener("click", () => { + isPaused = !isPaused; + document.getElementById("status").textContent = isPaused ? "Paused" : "Playing"; +}); + +document.getElementById("restart-btn").addEventListener("click", () => { + player.x = 50; player.y = 50; + createObstacles(); + createCollectibles(); + echoes = []; + isPaused = false; + document.getElementById("status").textContent = "Game Restarted!"; +}); diff --git a/games/echo-maze/style.css b/games/echo-maze/style.css new file mode 100644 index 00000000..eeb0e878 --- /dev/null +++ b/games/echo-maze/style.css @@ -0,0 +1,57 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background-color: #000; + color: #fff; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + height: 100vh; +} + +h1 { + margin: 20px 0; + font-size: 2rem; + text-align: center; + text-shadow: 0 0 10px #0ff; +} + +.game-ui { + display: flex; + flex-direction: column; + align-items: center; +} + +canvas { + background-color: #000; + border: 2px solid #0ff; + border-radius: 10px; + margin-top: 20px; +} + +.controls { + margin-bottom: 10px; +} + +button { + background-color: #0ff; + color: #000; + border: none; + padding: 10px 15px; + margin: 0 5px; + font-weight: bold; + cursor: pointer; + border-radius: 5px; + transition: 0.2s; +} + +button:hover { + background-color: #0aa; + color: #fff; +} + +#status { + margin-left: 10px; + font-weight: bold; +} diff --git a/games/emj_guess/index.html b/games/emj_guess/index.html new file mode 100644 index 00000000..942ceedb --- /dev/null +++ b/games/emj_guess/index.html @@ -0,0 +1,41 @@ + + + + + + Emoji Phrase Guess + + + + +
+

๐Ÿค” Emoji Phrase Guesser

+ +
+ Score: 0 / 0 +
+ +
+

Press START to play!

+
+ +
+
+ +
+ + +
+ +
+
+ +
+ + +
+
+ + + + \ No newline at end of file diff --git a/games/emj_guess/script.js b/games/emj_guess/script.js new file mode 100644 index 00000000..cc97d896 --- /dev/null +++ b/games/emj_guess/script.js @@ -0,0 +1,167 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. GAME DATA --- + const emojiPhrases = [ + { emoji: "๐Ÿ”๐ŸŸ๐Ÿฅค", answer: "Fast Food", hint: "Category: Food" }, + { emoji: "โ˜”๏ธ๐Ÿ’ƒ", answer: "Singing in the Rain", hint: "Category: Movie Title" }, + { emoji: "๐Ÿ๐Ÿ‘‘", answer: "Bee Queen", hint: "Category: Phrase" }, + { emoji: "๐ŸŽ๐Ÿ›", answer: "Apple Worm", hint: "Category: Food/Object" }, + { emoji: "โ˜•๏ธโฐ", answer: "Coffee Time", hint: "Category: Phrase" }, + { emoji: "๐Ÿฅถ๐ŸงŠ", answer: "Ice Cold", hint: "Category: Adjective/Feeling" }, + { emoji: "๐Ÿ‘€", answer: "I See", hint: "Category: Phrase (Hint: What does it look like?)" } + ]; + + // --- 2. GAME STATE VARIABLES --- + let currentRounds = []; // Shuffled array for the current game + let currentRoundIndex = 0; + let score = 0; + let gameActive = false; + + // --- 3. DOM Elements --- + const emojiDisplay = document.getElementById('emoji-display'); + const hintArea = document.getElementById('hint-area'); + const guessInput = document.getElementById('guess-input'); + const submitButton = document.getElementById('submit-button'); + const feedbackMessage = document.getElementById('feedback-message'); + const scoreSpan = document.getElementById('score'); + const totalRoundsSpan = document.getElementById('total-rounds'); + const startButton = document.getElementById('start-button'); + const nextButton = document.getElementById('next-button'); + + // --- 4. UTILITY FUNCTIONS --- + + /** + * Shuffles an array in place (Fisher-Yates). + */ + function shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + } + + /** + * Normalizes a string for comparison (lowercase, remove non-alphanumeric). + * This allows for more flexible user answers (e.g., "fast-food" vs "fast food"). + */ + function normalizeString(str) { + return str.toLowerCase().replace(/[^a-z0-9]/g, ''); + } + + // --- 5. CORE GAME FUNCTIONS --- + + /** + * Initializes the game. + */ + function startGame() { + gameActive = true; + shuffleArray(emojiPhrases); + currentRounds = emojiPhrases; // Use all phrases for the game + totalRoundsSpan.textContent = currentRounds.length; + + currentRoundIndex = 0; + score = 0; + scoreSpan.textContent = score; + + startButton.style.display = 'none'; + nextButton.style.display = 'none'; + loadRound(); + } + + /** + * Loads the next emoji phrase onto the screen. + */ + function loadRound() { + if (currentRoundIndex >= currentRounds.length) { + endGame(); + return; + } + + const roundData = currentRounds[currentRoundIndex]; + + // Update display + emojiDisplay.textContent = roundData.emoji; + hintArea.textContent = `Hint: ${roundData.hint}`; + feedbackMessage.textContent = 'Make your best guess!'; + feedbackMessage.style.color = '#333'; + + // Enable input + guessInput.value = ''; + guessInput.disabled = false; + submitButton.disabled = false; + guessInput.focus(); + nextButton.style.display = 'none'; + } + + /** + * Checks the player's guess against the correct answer. + */ + function checkGuess() { + const roundData = currentRounds[currentRoundIndex]; + const correctAnswer = roundData.answer; + const playerGuess = guessInput.value.trim(); + + // Normalize both strings for comparison + const normalizedGuess = normalizeString(playerGuess); + const normalizedAnswer = normalizeString(correctAnswer); + + // Disable input after submission + guessInput.disabled = true; + submitButton.disabled = true; + + if (normalizedGuess === normalizedAnswer) { + score++; + scoreSpan.textContent = score; + feedbackMessage.textContent = '๐ŸŽ‰ CORRECT! Great job!'; + feedbackMessage.style.color = '#4caf50'; + } else { + feedbackMessage.textContent = `โŒ INCORRECT. The answer was: "${correctAnswer}"`; + feedbackMessage.style.color = '#f44336'; + } + + // Prepare for next round + nextButton.style.display = 'block'; + } + + /** + * Moves the game to the next round. + */ + function nextRound() { + currentRoundIndex++; + loadRound(); + } + + /** + * Ends the game and shows the final score. + */ + function endGame() { + gameActive = false; + emojiDisplay.textContent = 'GAME OVER!'; + hintArea.textContent = ''; + feedbackMessage.textContent = `Final Score: ${score} / ${currentRounds.length}.`; + feedbackMessage.style.color = '#ff9800'; + nextButton.style.display = 'none'; + + startButton.textContent = 'PLAY AGAIN'; + startButton.style.display = 'block'; + } + + // --- 6. EVENT LISTENERS --- + + startButton.addEventListener('click', startGame); + nextButton.addEventListener('click', nextRound); + submitButton.addEventListener('click', checkGuess); + + // Allow 'Enter' key to submit the guess + guessInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter' && !submitButton.disabled) { + checkGuess(); + } + }); + + // Initial setup: check if the user clicks 'Enter' on start button + startButton.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + startGame(); + } + }); +}); \ No newline at end of file diff --git a/games/emj_guess/style.css b/games/emj_guess/style.css new file mode 100644 index 00000000..b57c45b2 --- /dev/null +++ b/games/emj_guess/style.css @@ -0,0 +1,125 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #ffe0b2; /* Light orange background */ +} + +#game-container { + background-color: white; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + text-align: center; + max-width: 500px; + width: 90%; +} + +h1 { + color: #ff9800; /* Deep orange */ + margin-bottom: 20px; +} + +#score-area { + font-size: 1.1em; + font-weight: bold; + color: #5d4037; + margin-bottom: 15px; +} + +/* --- Emoji Display --- */ +#emoji-display-box { + background-color: #fffde7; /* Very light yellow for contrast */ + padding: 20px 10px; + border: 3px solid #ffcc80; + border-radius: 10px; + margin-bottom: 20px; +} + +#emoji-display { + font-size: 3em; /* Make emojis large */ + margin: 0; + user-select: none; +} + +#hint-area { + min-height: 1.2em; + font-style: italic; + color: #795548; + margin-bottom: 25px; +} + +/* --- Input Area --- */ +#input-area { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +#guess-input { + flex-grow: 1; + padding: 10px; + font-size: 1em; + border: 2px solid #ccc; + border-radius: 5px; + text-transform: capitalize; /* Suggests title/phrase input */ +} + +#submit-button { + padding: 10px 20px; + font-size: 1em; + background-color: #4caf50; /* Green */ + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; +} + +#submit-button:hover:not(:disabled) { + background-color: #43a047; +} + +#submit-button:disabled { + background-color: #b0bec5; + cursor: not-allowed; +} + +/* --- Feedback and Controls --- */ +#feedback-message { + min-height: 1.5em; + font-weight: bold; + font-size: 1.1em; + margin-bottom: 20px; +} + +#controls button { + padding: 10px 20px; + font-size: 1em; + font-weight: bold; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; +} + +#start-button { + background-color: #ff9800; + color: white; +} + +#start-button:hover { + background-color: #fb8c00; +} + +#next-button { + background-color: #03a9f4; /* Light blue */ + color: white; +} + +#next-button:hover { + background-color: #039be5; +} \ No newline at end of file diff --git a/games/emoji-battle-arena/index.html b/games/emoji-battle-arena/index.html new file mode 100644 index 00000000..37929728 --- /dev/null +++ b/games/emoji-battle-arena/index.html @@ -0,0 +1,27 @@ + + + + + + Emoji Battle Arena + + + +
+
๐Ÿ˜Ž
+
๐Ÿค–
+
+
+ Player 1 +
+
+
+ Player 2 +
+
+
+
+
+ + + diff --git a/games/emoji-battle-arena/script.js b/games/emoji-battle-arena/script.js new file mode 100644 index 00000000..5ab4b115 --- /dev/null +++ b/games/emoji-battle-arena/script.js @@ -0,0 +1,86 @@ +const arena = document.querySelector('.arena'); +const p1 = document.getElementById('player1'); +const p2 = document.getElementById('player2'); +const p1HealthBar = document.getElementById('p1-health'); +const p2HealthBar = document.getElementById('p2-health'); +const winnerText = document.getElementById('winner'); + +let player1 = { x: 50, y: 50, health: 100, width: 50, height: 50 }; +let player2 = { x: 700, y: 50, health: 100, width: 50, height: 50 }; +const speed = 5; +const damage = 10; + +const keys = {}; + +// Keyboard events +document.addEventListener('keydown', e => keys[e.key] = true); +document.addEventListener('keyup', e => keys[e.key] = false); + +function movePlayers() { + // Player 1 controls: WASD + if(keys['w'] && player1.y > 0) player1.y -= speed; + if(keys['s'] && player1.y < arena.clientHeight - player1.height) player1.y += speed; + if(keys['a'] && player1.x > 0) player1.x -= speed; + if(keys['d'] && player1.x < arena.clientWidth - player1.width) player1.x += speed; + + // Player 2 controls: Arrow keys + if(keys['ArrowUp'] && player2.y > 0) player2.y -= speed; + if(keys['ArrowDown'] && player2.y < arena.clientHeight - player2.height) player2.y += speed; + if(keys['ArrowLeft'] && player2.x > 0) player2.x -= speed; + if(keys['ArrowRight'] && player2.x < arena.clientWidth - player2.width) player2.x += speed; + + // Update positions + p1.style.left = player1.x + 'px'; + p1.style.bottom = player1.y + 'px'; + p2.style.left = player2.x + 'px'; + p2.style.bottom = player2.y + 'px'; +} + +function attack() { + // Player 1 attack: 'f' + if(keys['f'] && checkCollision(player1, player2)) player2.health -= damage; + // Player 2 attack: 'm' + if(keys['m'] && checkCollision(player2, player1)) player1.health -= damage; + + // Update health bars + p1HealthBar.style.width = player1.health * 2 + 'px'; + p2HealthBar.style.width = player2.health * 2 + 'px'; + + checkWinner(); +} + +function checkCollision(a, b) { + return !( + a.x + a.width < b.x || + a.x > b.x + b.width || + a.y + a.height < b.y || + a.y > b.y + b.height + ); +} + +function checkWinner() { + if(player1.health <= 0) { + winnerText.style.display = 'block'; + winnerText.textContent = "Player 2 Wins! ๐Ÿ†"; + stopGame(); + } else if(player2.health <= 0) { + winnerText.style.display = 'block'; + winnerText.textContent = "Player 1 Wins! ๐Ÿ†"; + stopGame(); + } +} + +let gameInterval; +function startGame() { + gameInterval = setInterval(() => { + movePlayers(); + attack(); + }, 30); +} + +function stopGame() { + clearInterval(gameInterval); +} + +// Start game +startGame(); diff --git a/games/emoji-battle-arena/style.css b/games/emoji-battle-arena/style.css new file mode 100644 index 00000000..a4be1253 --- /dev/null +++ b/games/emoji-battle-arena/style.css @@ -0,0 +1,72 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: Arial, sans-serif; +} + +body { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background: linear-gradient(135deg, #ff9a9e, #fad0c4); +} + +.arena { + position: relative; + width: 800px; + height: 500px; + background-color: #fff3e0; + border: 4px solid #333; + border-radius: 15px; + overflow: hidden; +} + +.player { + position: absolute; + font-size: 50px; + transition: transform 0.05s linear; +} + +#player1 { left: 50px; bottom: 50px; } +#player2 { right: 50px; bottom: 50px; } + +.health-bar { + display: flex; + align-items: center; + margin: 10px; +} + +.health-bar span { + margin-right: 10px; + font-weight: bold; +} + +.bar { + width: 200px; + height: 20px; + background-color: red; + border: 2px solid #333; + border-radius: 5px; +} + +#ui { + position: absolute; + top: 10px; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 20px; +} + +#winner { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 40px; + font-weight: bold; + color: green; + display: none; +} diff --git a/games/emoji-connect/index.html b/games/emoji-connect/index.html new file mode 100644 index 00000000..e8718da1 --- /dev/null +++ b/games/emoji-connect/index.html @@ -0,0 +1,22 @@ + + + + + + Emoji Connect | Mini JS Games Hub + + + +
+

Emoji Connect ๐Ÿงฉ

+

Connect matching emojis without overlapping lines!

+
+
+ Moves: 0 + +
+
+
+ + + diff --git a/games/emoji-connect/script.js b/games/emoji-connect/script.js new file mode 100644 index 00000000..cbd09ac2 --- /dev/null +++ b/games/emoji-connect/script.js @@ -0,0 +1,86 @@ +const emojis = ["๐ŸŽ","๐ŸŽ","๐ŸŒ","๐ŸŒ","๐Ÿ‡","๐Ÿ‡","๐Ÿ‰","๐Ÿ‰","๐Ÿ“","๐Ÿ“","๐Ÿ’","๐Ÿ’","๐Ÿฅ","๐Ÿฅ","๐Ÿ‘","๐Ÿ‘"]; +let firstSelection = null; +let secondSelection = null; +let moves = 0; + +const grid = document.getElementById("grid"); +const movesDisplay = document.getElementById("moves"); +const message = document.getElementById("message"); +const resetBtn = document.getElementById("reset"); + +// Shuffle function +function shuffle(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +} + +// Initialize Grid +function initGrid() { + const shuffled = shuffle([...emojis]); + grid.innerHTML = ""; + shuffled.forEach((emoji, index) => { + const cell = document.createElement("div"); + cell.classList.add("cell"); + cell.dataset.emoji = emoji; + cell.dataset.index = index; + cell.textContent = emoji; + cell.addEventListener("click", handleClick); + grid.appendChild(cell); + }); + firstSelection = null; + secondSelection = null; + moves = 0; + movesDisplay.textContent = moves; + message.textContent = ""; +} + +function handleClick(e) { + const cell = e.currentTarget; + if (cell.classList.contains("matched") || cell.classList.contains("selected")) return; + + cell.classList.add("selected"); + + if (!firstSelection) { + firstSelection = cell; + } else if (!secondSelection) { + secondSelection = cell; + moves++; + movesDisplay.textContent = moves; + checkMatch(); + } +} + +function checkMatch() { + if (firstSelection.dataset.emoji === secondSelection.dataset.emoji) { + firstSelection.classList.add("matched"); + secondSelection.classList.add("matched"); + firstSelection.classList.remove("selected"); + secondSelection.classList.remove("selected"); + firstSelection = null; + secondSelection = null; + checkWin(); + } else { + setTimeout(() => { + firstSelection.classList.remove("selected"); + secondSelection.classList.remove("selected"); + firstSelection = null; + secondSelection = null; + }, 800); + } +} + +function checkWin() { + const matched = document.querySelectorAll(".cell.matched"); + if (matched.length === emojis.length) { + message.textContent = `๐ŸŽ‰ You Won in ${moves} moves!`; + } +} + +// Reset Game +resetBtn.addEventListener("click", initGrid); + +// Start Game +initGrid(); diff --git a/games/emoji-connect/style.css b/games/emoji-connect/style.css new file mode 100644 index 00000000..75b79acd --- /dev/null +++ b/games/emoji-connect/style.css @@ -0,0 +1,76 @@ +body { + margin: 0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(120deg, #f6d365, #fda085); + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; +} + +.game-container { + background: #fff; + padding: 20px 40px; + border-radius: 15px; + box-shadow: 0 10px 20px rgba(0,0,0,0.2); + text-align: center; + max-width: 500px; + width: 90%; +} + +h1 { + margin-bottom: 10px; +} + +.grid { + display: grid; + grid-template-columns: repeat(4, 60px); + grid-gap: 15px; + justify-content: center; + margin: 20px 0; +} + +.cell { + background: #f3f3f3; + display: flex; + justify-content: center; + align-items: center; + font-size: 32px; + border-radius: 10px; + cursor: pointer; + transition: transform 0.2s, background 0.2s; +} + +.cell.selected { + background: #ffd700; + transform: scale(1.1); +} + +.game-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 10px; +} + +button { + padding: 6px 12px; + font-size: 16px; + border: none; + border-radius: 8px; + cursor: pointer; + background-color: #ff6f61; + color: #fff; + transition: background 0.2s; +} + +button:hover { + background-color: #ff3b2f; +} + +#message { + margin-top: 15px; + font-weight: bold; + color: #28a745; + font-size: 18px; +} diff --git a/games/emoji-reaction/index.html b/games/emoji-reaction/index.html new file mode 100644 index 00000000..60d3de2d --- /dev/null +++ b/games/emoji-reaction/index.html @@ -0,0 +1,62 @@ + + + + + + Emoji Reaction Test | Mini JS Games Hub + + + + + +
+
+
+

Emoji Reaction Test

+

Match the emoji to the correct meaning โ€” test speed & memory.

+
+
+ + + + โ† Hub +
+
+ +
+
Score: 0
+
Round: 0
+
Time Left: --
+
Avg RT: -- ms
+
+ +
+
๐Ÿ˜€
+ +
+ +
+
+ + +
+ + + + + + + + diff --git a/games/emoji-reaction/script.js b/games/emoji-reaction/script.js new file mode 100644 index 00000000..848fede8 --- /dev/null +++ b/games/emoji-reaction/script.js @@ -0,0 +1,302 @@ +/* Emoji Reaction Test + Advanced, progressive difficulty, pause/restart, sound toggle. + Uses online sounds hosted by Google's public action-sounds. +*/ + +(() => { + const EMOJI_BANK = [ + { emoji: "๐Ÿ˜€", meaning: "Happy / Smile" }, + { emoji: "๐Ÿ˜‚", meaning: "Laugh / Tears" }, + { emoji: "๐Ÿ˜ข", meaning: "Sad / Cry" }, + { emoji: "๐Ÿ˜ก", meaning: "Angry" }, + { emoji: "๐Ÿ˜", meaning: "Love / Heart eyes" }, + { emoji: "๐Ÿค”", meaning: "Thinking / Ponder" }, + { emoji: "๐Ÿ˜ด", meaning: "Sleepy / Sleep" }, + { emoji: "๐Ÿคฎ", meaning: "Disgust / Sick" }, + { emoji: "๐Ÿ˜ฑ", meaning: "Shocked / Fear" }, + { emoji: "๐Ÿ‘", meaning: "Thumbs up / Good" }, + { emoji: "๐Ÿ‘Ž", meaning: "Thumbs down / Bad" }, + { emoji: "๐ŸŽ‰", meaning: "Celebration / Party" }, + { emoji: "๐Ÿ”ฅ", meaning: "Fire / Hot" }, + { emoji: "โณ", meaning: "Waiting / Time" }, + { emoji: "๐Ÿช„", meaning: "Magic / Surprise" }, + { emoji: "๐Ÿ’ค", meaning: "Sleep / Zzz" }, + { emoji: "โš ๏ธ", meaning: "Warning / Caution" }, + { emoji: "๐Ÿ’ก", meaning: "Idea / Lightbulb" } + ]; + + // DOM refs + const emojiDisplay = document.getElementById("emojiDisplay"); + const optionsEl = document.getElementById("options"); + const scoreEl = document.getElementById("score"); + const roundEl = document.getElementById("round"); + const timeLeftEl = document.getElementById("timeLeft"); + const avgRtEl = document.getElementById("avgRt"); + const pauseBtn = document.getElementById("pauseBtn"); + const restartBtn = document.getElementById("restartBtn"); + const soundToggle = document.getElementById("soundToggle"); + const hintBtn = document.getElementById("hintBtn"); + const hintText = document.getElementById("hintText"); + const diffProg = document.getElementById("difficultyProgress"); + + const sndCorrect = document.getElementById("soundCorrect"); + const sndWrong = document.getElementById("soundWrong"); + const sndTick = document.getElementById("soundTick"); + + // game state + let state = { + running: true, + score: 0, + round: 0, + difficulty: 1, // 1..10 + timeLimit: 4000, // ms per round, reduces as difficulty increases + avgReaction: 0, + reactions: [], + current: null, + answerIdx: null, + timer: null, + roundStart: null, + soundOn: true, + paused: false + }; + + // utility + function randInt(max){ return Math.floor(Math.random()*max) } + function shuffle(arr){ return arr.slice().sort(()=>Math.random()-0.5) } + + // audio helpers + function playSound(el){ + if(!state.soundOn) return; + try{ el.currentTime = 0; el.play(); }catch(e){} + } + + // render functions + function showEmoji(emojiObj){ + emojiDisplay.classList.remove("glow"); + void emojiDisplay.offsetWidth; + emojiDisplay.textContent = emojiObj.emoji; + // small bounce glow + emojiDisplay.classList.add("glow"); + } + + function buildOptions(correctMeaning){ + // pick 3 distractors + correct => 4 options + const meaningsPool = EMOJI_BANK.map(e=>e.meaning).filter(m=>m!==correctMeaning); + const choices = shuffle(meaningsPool).slice(0,3); + choices.push(correctMeaning); + const final = shuffle(choices); + optionsEl.innerHTML = ""; + final.forEach((text, idx)=>{ + const b = document.createElement("button"); + b.className = "option-btn"; + b.setAttribute("data-index", idx); + b.setAttribute("aria-label", text); + b.textContent = text; + b.addEventListener("click", onOptionClick); + optionsEl.appendChild(b); + }); + } + + function updateStatus(){ + scoreEl.textContent = state.score; + roundEl.textContent = state.round; + avgRtEl.textContent = state.reactions.length ? Math.round(state.reactions.reduce((a,b)=>a+b,0)/state.reactions.length) : "--"; + diffProg.value = state.difficulty; + } + + function setTimeLeft(ms){ + timeLeftEl.textContent = Math.max(0, Math.round(ms)); + } + + // game flow + function nextRound(){ + if(state.paused) return; + // increase round + state.round++; + // adjust difficulty every 3 rounds + if(state.round % 3 === 0 && state.difficulty < 10){ + state.difficulty++; + // lower timeLimit as difficulty grows + state.timeLimit = Math.max(1200, 4000 - (state.difficulty-1)*300); + } + // choose emoji + const candidate = EMOJI_BANK[randInt(EMOJI_BANK.length)]; + state.current = candidate; + showEmoji(candidate); + buildOptions(candidate.meaning); + // reset hint + hintText.textContent = ""; + hintText.setAttribute("aria-hidden","true"); + + // start timer + state.roundStart = performance.now(); + let deadline = state.timeLimit; + setTimeLeft(deadline); + + // clear any existing timer + if(state.timer) clearInterval(state.timer); + state.timer = setInterval(()=>{ + if(state.paused) return; + const elapsed = performance.now() - state.roundStart; + const remaining = Math.max(0, Math.round((deadline - elapsed))); + setTimeLeft(remaining); + if(remaining <= 800 && state.soundOn) { + // play tick near the end (only once per round) + playSound(sndTick); + } + if(remaining <= 0){ + clearInterval(state.timer); + markWrong(null, true); // timed out + } + }, 60); + + updateStatus(); + } + + // answer handling + function markCorrect(button){ + // style + if(button) button.classList.add("correct"); + // compute reaction + const rt = Math.round(performance.now() - state.roundStart); + state.reactions.push(rt); + state.score += Math.max(10, Math.round(6000 / Math.max(150, rt))); // more points for faster + if(state.score < 0) state.score = 0; + updateStatus(); + playSound(sndCorrect); + // brief pause then next round + endRoundThenNext(); + } + + function markWrong(button, timeout=false){ + if(button) button.classList.add("wrong"); + if(timeout){ + // reveal correct + revealCorrect(); + } else { + // show correct then continue + revealCorrect(); + } + state.score -= 3; + if(state.score < 0) state.score = 0; + playSound(sndWrong); + endRoundThenNext(); + } + + function revealCorrect(){ + // find the option with the correct meaning and flash it + const btns = Array.from(optionsEl.querySelectorAll(".option-btn")); + btns.forEach(b=>{ + if(b.textContent === state.current.meaning){ + b.classList.add("correct"); + } + }); + } + + function endRoundThenNext(){ + // stop timer + if(state.timer) clearInterval(state.timer); + // brief delay so users see feedback + setTimeout(()=> { + // small chance to increase difficulty quicker on streaks + if(state.reactions.length > 0 && state.reactions.slice(-3).every(rt => rt < state.timeLimit * 0.5) && state.difficulty < 10){ + state.difficulty++; + } + updateStatus(); + nextRound(); + }, 900); + } + + function onOptionClick(e){ + if(state.paused) return; + const btn = e.currentTarget; + // ignore repeat clicks + if(btn.classList.contains("correct") || btn.classList.contains("wrong")) return; + + const chosen = btn.textContent; + if(chosen === state.current.meaning){ + markCorrect(btn); + }else{ + markWrong(btn, false); + } + } + + // controls + pauseBtn.addEventListener("click", ()=>{ + state.paused = !state.paused; + pauseBtn.textContent = state.paused ? "Resume" : "Pause"; + if(state.paused){ + if(state.timer) clearInterval(state.timer); + } else { + // resume by adjusting roundStart so remaining time preserved + state.roundStart = performance.now() - (state.timeLimit - parseInt(timeLeftEl.textContent || 0)); + nextRound(); // will setup timers but won't advance round since paused flag false + } + }); + + restartBtn.addEventListener("click", ()=>{ + resetGame(); + startGame(); + }); + + soundToggle.addEventListener("click", ()=>{ + state.soundOn = !state.soundOn; + soundToggle.textContent = state.soundOn ? "๐Ÿ”Š" : "๐Ÿ”ˆ"; + }); + + hintBtn.addEventListener("click", ()=>{ + hintText.setAttribute("aria-hidden","false"); + hintText.textContent = `${state.current.emoji} = ${state.current.meaning.split(" / ")[0]}`; + // small fade away after a bit + setTimeout(()=> { + hintText.setAttribute("aria-hidden","true"); + hintText.textContent = ""; + }, 2200); + }); + + // init / start / reset + function resetGame(){ + state.score = 0; + state.round = 0; + state.difficulty = 1; + state.timeLimit = 4000; + state.reactions = []; + state.current = null; + state.paused = false; + state.soundOn = true; + soundToggle.textContent = "๐Ÿ”Š"; + playSound(sndTick); // warmup sound + } + + function startGame(){ + updateStatus(); + // small intro flash + let introCount = 0; + const intro = setInterval(()=>{ + emojiDisplay.classList.toggle("glow"); + introCount++; + if(introCount > 3){ + clearInterval(intro); + emojiDisplay.classList.remove("glow"); + nextRound(); + } + }, 220); + } + + // initial setup hook + function boot(){ + // Ensure options area empty + optionsEl.innerHTML = ""; + resetGame(); + startGame(); + } + + // Expose quick debug on window for dev + window.EmojiReaction = { + state, + boot, resetGame, nextRound + }; + + // start + boot(); +})(); diff --git a/games/emoji-reaction/style.css b/games/emoji-reaction/style.css new file mode 100644 index 00000000..917ef64c --- /dev/null +++ b/games/emoji-reaction/style.css @@ -0,0 +1,164 @@ +:root{ + --bg: #0f1724; + --card: rgba(255,255,255,0.03); + --accent: #7c5cff; + --accent-2: #00d4ff; + --text: #e6eef8; + --muted: #9fb2c8; + --glass: rgba(255,255,255,0.04); + --glow: 0 8px 30px rgba(124,92,255,0.18); + --glass-border: rgba(255,255,255,0.06); +} + +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; + background-image: linear-gradient(180deg, rgba(8,10,20,0.85), rgba(3,6,12,0.95)), url('https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=1600&q=60'); + background-size:cover; + background-position:center; + color:var(--text); + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + padding:28px; + display:flex; + align-items:center; + justify-content:center; +} + +/* shell */ +.game-shell{ + width:100%; + max-width:980px; + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border-radius:14px; + padding:18px; + box-shadow: var(--glow); + border:1px solid var(--glass-border); + backdrop-filter: blur(6px) saturate(120%); +} + +/* header */ +.game-header{ + display:flex; + align-items:center; + justify-content:space-between; + gap:12px; + margin-bottom:16px; +} +.game-header h1{margin:0;font-size:20px} +.game-header .sub{margin:0;font-size:13px;color:var(--muted)} +.controls{display:flex;align-items:center;gap:8px} +.btn{ + background:linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01)); + border:1px solid rgba(255,255,255,0.06); + color:var(--text); + padding:8px 12px; + border-radius:8px; + cursor:pointer; + font-size:14px; + transition:transform .12s ease, box-shadow .12s ease; +} +.btn:active{transform:scale(.98)} +.openhub{color:var(--muted);font-size:13px;text-decoration:none;padding:6px 8px;border-radius:6px} + +/* status bar */ +.status-bar{ + display:flex; + gap:12px; + justify-content:space-between; + align-items:center; + margin-bottom:14px; + padding:8px; + border-radius:10px; + background: linear-gradient(90deg, rgba(255,255,255,0.01), rgba(255,255,255,0.02)); + border:1px solid rgba(255,255,255,0.03); + font-size:14px; +} +.status-bar strong{color:var(--accent-2)} + +/* game area */ +.game-area{ + display:flex; + flex-direction:column; + align-items:center; + gap:16px; + padding:22px; + border-radius:12px; + background: linear-gradient(180deg, rgba(255,255,255,0.015), rgba(255,255,255,0.01)); + border:1px solid rgba(255,255,255,0.03); +} + +.emoji-display{ + font-size:92px; + width:260px; + height:160px; + border-radius:14px; + display:flex; + align-items:center; + justify-content:center; + background:linear-gradient(180deg, rgba(255,255,255,0.015), rgba(255,255,255,0.01)); + border:1px solid rgba(255,255,255,0.04); + box-shadow: 0 12px 30px rgba(2,6,23,0.6), 0 8px 24px rgba(0,0,0,0.45); + transition: transform .18s ease, box-shadow .18s ease; + position:relative; +} + +/* pulsing glow when new emoji appears */ +.glow{ + animation: glowPulse 1.1s ease; + box-shadow: 0 20px 60px rgba(124,92,255,0.18), inset 0 -6px 30px rgba(0,212,255,0.06); +} +@keyframes glowPulse{ + 0%{transform:translateY(-4px) scale(.995)} + 40%{transform:translateY(0) scale(1.01)} + 100%{transform:translateY(0) scale(1)} +} + +/* options (buttons) */ +.options{ + display:flex; + gap:12px; + justify-content:center; + flex-wrap:wrap; + width:100%; + max-width:760px; +} +.option-btn{ + min-width:180px; + padding:14px 18px; + border-radius:12px; + background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border:1px solid rgba(255,255,255,0.04); + color:var(--text); + font-size:16px; + cursor:pointer; + text-align:center; + transition: transform .12s, box-shadow .12s, background .12s; + box-shadow: 0 8px 18px rgba(2,6,23,0.4); +} +.option-btn:hover{transform:translateY(-4px); box-shadow: 0 18px 40px rgba(2,6,23,0.55)} +.option-btn.correct{ + background: linear-gradient(90deg, rgba(34,197,94,0.14), rgba(34,197,94,0.06)); + border-color: rgba(34,197,94,0.28); + box-shadow: 0 18px 50px rgba(34,197,94,0.06); +} +.option-btn.wrong{ + background: linear-gradient(90deg, rgba(239,68,68,0.12), rgba(239,68,68,0.04)); + border-color: rgba(239,68,68,0.28); + box-shadow: 0 18px 50px rgba(239,68,68,0.05); +} + +/* footer */ +.footer-ui{display:flex;align-items:center;justify-content:space-between;margin-top:14px;gap:12px} +.difficulty{display:flex;flex-direction:column;gap:6px} +.hint-text{color:var(--muted);font-size:13px;margin-left:8px;min-width:160px} +.smallprint{color:var(--muted);font-size:12px} + +/* responsive */ +@media (max-width:720px){ + .options{flex-direction:column;align-items:stretch} + .option-btn{min-width:unset;width:100%} + .emoji-display{width:100%;height:120px;font-size:72px} +} diff --git a/games/emojie-game/index.html b/games/emojie-game/index.html new file mode 100644 index 00000000..ad5b0bc1 --- /dev/null +++ b/games/emojie-game/index.html @@ -0,0 +1,23 @@ + + + + + + Emoji Hunt Game + + + +
+

Emoji Hunt

+

Find and click on all the hidden emojis in the scene!

+
+
Time: 30
+
Found: 0/10
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/games/emojie-game/script.js b/games/emojie-game/script.js new file mode 100644 index 00000000..2728cc8c --- /dev/null +++ b/games/emojie-game/script.js @@ -0,0 +1,119 @@ +// Emoji Hunt Game Script +// Find hidden emojis in the scene + +var canvas = document.getElementById('gameCanvas'); +var ctx = canvas.getContext('2d'); +var timerDisplay = document.getElementById('timer'); +var scoreDisplay = document.getElementById('score'); +var restartBtn = document.getElementById('restart'); +var messageDiv = document.getElementById('message'); + +var emojis = []; +var foundCount = 0; +var totalEmojis = 10; +var timeLeft = 30; +var timerInterval; +var gameRunning = true; + +// Emoji class +function Emoji(x, y, emoji) { + this.x = x; + this.y = y; + this.emoji = emoji; + this.found = false; + this.size = 20; +} + +// Initialize the game +function initGame() { + emojis = []; + foundCount = 0; + timeLeft = 30; + gameRunning = true; + messageDiv.textContent = ''; + scoreDisplay.textContent = 'Found: ' + foundCount + '/' + totalEmojis; + + // Create emojis + var emojiList = ['๐Ÿ˜€', '๐Ÿ˜Ž', '๐Ÿค”', '๐Ÿ˜', '๐Ÿค—', '๐Ÿ˜ด', '๐Ÿคค', '๐Ÿ˜œ', '๐Ÿคช', '๐Ÿ˜‡']; + for (var i = 0; i < totalEmojis; i++) { + var x = Math.random() * (canvas.width - 40) + 20; + var y = Math.random() * (canvas.height - 40) + 20; + var emoji = emojiList[i]; + emojis.push(new Emoji(x, y, emoji)); + } + + startTimer(); + draw(); +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw cluttered background + for (var i = 0; i < 50; i++) { + ctx.fillStyle = 'hsl(' + Math.random() * 360 + ', 50%, 70%)'; + ctx.fillRect(Math.random() * canvas.width, Math.random() * canvas.height, 10 + Math.random() * 20, 10 + Math.random() * 20); + } + + // Draw emojis (only if not found) + ctx.font = '20px Arial'; + ctx.textAlign = 'center'; + for (var i = 0; i < emojis.length; i++) { + var emoji = emojis[i]; + if (!emoji.found) { + ctx.fillText(emoji.emoji, emoji.x, emoji.y); + } + } +} + +// Handle canvas click +canvas.addEventListener('click', function(event) { + if (!gameRunning) return; + var rect = canvas.getBoundingClientRect(); + var clickX = event.clientX - rect.left; + var clickY = event.clientY - rect.top; + + for (var i = 0; i < emojis.length; i++) { + var emoji = emojis[i]; + if (!emoji.found) { + var dx = clickX - emoji.x; + var dy = clickY - emoji.y; + var distance = Math.sqrt(dx * dx + dy * dy); + if (distance < emoji.size) { + emoji.found = true; + foundCount++; + scoreDisplay.textContent = 'Found: ' + foundCount + '/' + totalEmojis; + if (foundCount === totalEmojis) { + gameRunning = false; + clearInterval(timerInterval); + messageDiv.textContent = 'Congratulations! You found all emojis!'; + messageDiv.style.color = 'green'; + } + draw(); + break; + } + } + } +}); + +// Start the timer +function startTimer() { + clearInterval(timerInterval); + timerInterval = setInterval(function() { + timeLeft--; + timerDisplay.textContent = 'Time: ' + timeLeft; + if (timeLeft <= 0) { + clearInterval(timerInterval); + gameRunning = false; + messageDiv.textContent = 'Time\'s up! Found: ' + foundCount + '/' + totalEmojis; + messageDiv.style.color = 'orange'; + } + }, 1000); +} + +// Restart button +restartBtn.addEventListener('click', initGame); + +// Start the game +initGame(); \ No newline at end of file diff --git a/games/emojie-game/style.css b/games/emojie-game/style.css new file mode 100644 index 00000000..caed723f --- /dev/null +++ b/games/emojie-game/style.css @@ -0,0 +1,53 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #fff9c4; +} + +.container { + text-align: center; +} + +h1 { + color: #f57f17; +} + +.game-info { + margin-bottom: 20px; + display: flex; + justify-content: center; + gap: 20px; +} + +#timer, #score { + font-size: 24px; + font-weight: bold; +} + +#restart { + padding: 10px 20px; + font-size: 16px; + background-color: #ffb300; + color: white; + border: none; + cursor: pointer; +} + +#restart:hover { + background-color: #ff8f00; +} + +canvas { + border: 2px solid #f57f17; + background-color: #fffde7; +} + +#message { + margin-top: 20px; + font-size: 18px; + font-weight: bold; +} \ No newline at end of file diff --git a/games/endless-runner/index.html b/games/endless-runner/index.html new file mode 100644 index 00000000..3a625927 --- /dev/null +++ b/games/endless-runner/index.html @@ -0,0 +1,25 @@ + + + + + + Endless Runner | Mini JS Games Hub + + + +
+

๐Ÿƒโ€โ™‚๏ธ Endless Runner

+ +
+

Score: 0

+

High Score: 0

+
+ +
+ + + + + + + diff --git a/games/endless-runner/script.js b/games/endless-runner/script.js new file mode 100644 index 00000000..54720e6c --- /dev/null +++ b/games/endless-runner/script.js @@ -0,0 +1,168 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); +const jumpSound = document.getElementById("jumpSound"); +const hitSound = document.getElementById("hitSound"); +const restartBtn = document.getElementById("restartBtn"); +const scoreEl = document.getElementById("score"); +const highScoreEl = document.getElementById("highScore"); + +let gameSpeed = 5; +let gravity = 0.6; +let score = 0; +let highScore = localStorage.getItem("endlessRunnerHighScore") || 0; +let gameOver = false; +let obstacles = []; + +highScoreEl.textContent = highScore; + +// Runner +const player = { + x: 80, + y: 300, + width: 50, + height: 50, + dy: 0, + jumping: false, + sliding: false, + slideTimer: 0, + + draw() { + ctx.fillStyle = "#ff5f5f"; + ctx.fillRect(this.x, this.y, this.width, this.height); + }, + + update() { + if (this.sliding) { + this.height = 30; + this.y = 320; + this.slideTimer--; + if (this.slideTimer <= 0) this.sliding = false; + } else { + this.height = 50; + } + + this.y += this.dy; + if (this.y + this.height < canvas.height - 30) { + this.dy += gravity; + } else { + this.y = canvas.height - this.height - 30; + this.dy = 0; + this.jumping = false; + } + this.draw(); + }, +}; + +// Background layers +const bgLayers = [ + { x: 0, speed: 1, color: "#b0e0e6" }, + { x: 0, speed: 2, color: "#87ceeb" }, + { x: 0, speed: 3, color: "#4682b4" }, +]; + +function drawBackground() { + bgLayers.forEach(layer => { + ctx.fillStyle = layer.color; + ctx.fillRect(layer.x, 0, canvas.width, canvas.height); + ctx.fillRect(layer.x + canvas.width, 0, canvas.width, canvas.height); + + layer.x -= layer.speed; + if (layer.x <= -canvas.width) layer.x = 0; + }); +} + +// Obstacles +class Obstacle { + constructor() { + this.width = 40; + this.height = Math.random() > 0.5 ? 40 : 60; + this.x = canvas.width; + this.y = canvas.height - this.height - 30; + this.color = "#333"; + } + + draw() { + ctx.fillStyle = this.color; + ctx.fillRect(this.x, this.y, this.width, this.height); + } + + update() { + this.x -= gameSpeed; + this.draw(); + } +} + +// Controls +document.addEventListener("keydown", (e) => { + if ((e.code === "Space" || e.code === "ArrowUp") && !player.jumping) { + player.dy = -12; + player.jumping = true; + jumpSound.play(); + } + if (e.code === "ArrowDown" && !player.sliding && !player.jumping) { + player.sliding = true; + player.slideTimer = 25; + } +}); + +// Game functions +function spawnObstacle() { + if (Math.random() < 0.03) obstacles.push(new Obstacle()); +} + +function checkCollision(a, b) { + return ( + a.x < b.x + b.width && + a.x + a.width > b.x && + a.y < b.y + b.height && + a.y + a.height > b.y + ); +} + +function update() { + if (gameOver) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawBackground(); + player.update(); + + obstacles.forEach((obstacle, index) => { + obstacle.update(); + + if (obstacle.x + obstacle.width < 0) { + obstacles.splice(index, 1); + score++; + if (score % 10 === 0) gameSpeed += 0.5; // Increase difficulty + } + + if (checkCollision(player, obstacle)) { + hitSound.play(); + gameOver = true; + restartBtn.classList.remove("hidden"); + + if (score > highScore) { + highScore = score; + localStorage.setItem("endlessRunnerHighScore", highScore); + } + } + }); + + scoreEl.textContent = score; + highScoreEl.textContent = highScore; + + spawnObstacle(); + + if (!gameOver) requestAnimationFrame(update); +} + +restartBtn.addEventListener("click", () => { + obstacles = []; + score = 0; + gameSpeed = 5; + player.y = 300; + gameOver = false; + restartBtn.classList.add("hidden"); + update(); +}); + +update(); diff --git a/games/endless-runner/style.css b/games/endless-runner/style.css new file mode 100644 index 00000000..6ebe81c6 --- /dev/null +++ b/games/endless-runner/style.css @@ -0,0 +1,51 @@ +body { + margin: 0; + overflow: hidden; + background: linear-gradient(to top, #8ec5fc, #e0c3fc); + font-family: "Poppins", sans-serif; +} + +.game-container { + text-align: center; + color: #333; +} + +.title { + margin: 10px 0; + font-size: 2rem; +} + +canvas { + background: linear-gradient(to top, #5bc0de 30%, #c8e4f8 100%); + border: 3px solid #333; + border-radius: 10px; + box-shadow: 0 0 15px rgba(0,0,0,0.3); +} + +.scoreboard { + display: flex; + justify-content: center; + gap: 2rem; + margin-top: 10px; + font-size: 1.2rem; +} + +.hidden { + display: none; +} + +#restartBtn { + margin-top: 20px; + padding: 10px 20px; + background-color: #ff5757; + border: none; + border-radius: 8px; + color: white; + font-size: 1.1rem; + cursor: pointer; + transition: 0.3s; +} + +#restartBtn:hover { + background-color: #ff3030; +} diff --git a/games/energy-connect/index.html b/games/energy-connect/index.html new file mode 100644 index 00000000..5f280cba --- /dev/null +++ b/games/energy-connect/index.html @@ -0,0 +1,26 @@ + + + + + + Energy Connect | Mini JS Games Hub + + + +
+

Energy Connect โšก

+
+
+ + + +
+

+ + + +
+ + + + diff --git a/games/energy-connect/script.js b/games/energy-connect/script.js new file mode 100644 index 00000000..43774220 --- /dev/null +++ b/games/energy-connect/script.js @@ -0,0 +1,77 @@ +const board = document.getElementById("board"); +const messageEl = document.getElementById("message"); +const rotateSound = document.getElementById("rotate-sound"); +const successSound = document.getElementById("success-sound"); +const failSound = document.getElementById("fail-sound"); +const pauseBtn = document.getElementById("pause-btn"); +const resumeBtn = document.getElementById("resume-btn"); +const restartBtn = document.getElementById("restart-btn"); + +let gamePaused = false; + +// Game nodes setup (6x6) +const rows = 6; +const cols = 6; +let nodes = []; + +function createBoard() { + board.innerHTML = ""; + nodes = []; + for (let i = 0; i < rows * cols; i++) { + const node = document.createElement("div"); + node.classList.add("node"); + node.dataset.index = i; + // Randomly add obstacle + if (Math.random() < 0.15) { + node.dataset.obstacle = "true"; + node.style.background = "#333"; + } else { + node.dataset.obstacle = "false"; + } + node.addEventListener("click", () => rotateNode(i)); + board.appendChild(node); + nodes.push(node); + } + messageEl.textContent = ""; +} + +function rotateNode(index) { + if (gamePaused) return; + const node = nodes[index]; + if (node.dataset.obstacle === "true") return; + + node.classList.toggle("glow"); + rotateSound.play(); + checkWin(); +} + +function checkWin() { + const activeNodes = nodes.filter(n => n.dataset.obstacle === "false" && n.classList.contains("glow")); + const totalActive = nodes.filter(n => n.dataset.obstacle === "false").length; + if (activeNodes.length === totalActive) { + messageEl.textContent = "๐ŸŽ‰ All nodes connected! You win!"; + messageEl.style.color = "#0ff"; + successSound.play(); + } +} + +// Controls +pauseBtn.addEventListener("click", () => { + gamePaused = true; + messageEl.textContent = "โธ Game Paused"; + messageEl.style.color = "#ff0"; +}); + +resumeBtn.addEventListener("click", () => { + gamePaused = false; + messageEl.textContent = "โ–ถ Game Resumed"; + messageEl.style.color = "#0ff"; +}); + +restartBtn.addEventListener("click", () => { + gamePaused = false; + createBoard(); +}); + +// Initial board +createBoard(); diff --git a/games/energy-connect/style.css b/games/energy-connect/style.css new file mode 100644 index 00000000..070065e5 --- /dev/null +++ b/games/energy-connect/style.css @@ -0,0 +1,76 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #1b2735, #090a0f); + color: #fff; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + min-height: 100vh; +} + +.game-container { + text-align: center; + margin-top: 40px; +} + +h1 { + font-size: 2.5rem; + text-shadow: 0 0 15px #00f6ff, 0 0 30px #00f6ff; +} + +.game-board { + margin: 20px auto; + display: grid; + grid-template-columns: repeat(6, 80px); + grid-gap: 15px; +} + +.node { + width: 80px; + height: 80px; + background: #111; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + box-shadow: 0 0 5px #0ff; + position: relative; + transition: transform 0.3s; +} + +.node.glow::after { + content: ''; + position: absolute; + width: 120%; + height: 120%; + background: rgba(0, 255, 255, 0.4); + border-radius: 10px; + top: -10%; + left: -10%; + filter: blur(8px); + z-index: -1; +} + +.node:active { + transform: scale(1.1); +} + +.controls button { + margin: 5px; + padding: 10px 20px; + font-size: 16px; + border: none; + border-radius: 8px; + cursor: pointer; + background: #00f6ff; + color: #000; + font-weight: bold; + transition: 0.3s; +} + +.controls button:hover { + background: #00a8cc; +} diff --git a/games/energy-flow/index.html b/games/energy-flow/index.html new file mode 100644 index 00000000..efe9ab23 --- /dev/null +++ b/games/energy-flow/index.html @@ -0,0 +1,32 @@ + + + + + + Energy Flow | Mini JS Games Hub โšก + + + +
+

โšก Energy Flow โšก

+ +
+ + + +
+ +
+ +
+

Connect the power to all bulbs!

+
+
+ + + + + + + + diff --git a/games/energy-flow/script.js b/games/energy-flow/script.js new file mode 100644 index 00000000..94e15106 --- /dev/null +++ b/games/energy-flow/script.js @@ -0,0 +1,120 @@ +const grid = document.getElementById("grid"); +const message = document.getElementById("message"); +const rotateSound = document.getElementById("rotateSound"); +const successSound = document.getElementById("successSound"); +const clickSound = document.getElementById("clickSound"); + +const rows = 6, cols = 6; +let isPaused = false; + +// Wire types: straight, corner, cross +const wireTypes = [ + { connections: ["up", "down"], rotations: 2 }, + { connections: ["left", "right"], rotations: 2 }, + { connections: ["up", "right"], rotations: 4 }, + { connections: ["right", "down"], rotations: 4 }, + { connections: ["down", "left"], rotations: 4 }, + { connections: ["left", "up"], rotations: 4 }, +]; + +let cells = []; +let powerCell = null; +let bulbCells = []; +let obstacles = []; + +function createGrid() { + grid.innerHTML = ""; + cells = []; + + for (let r = 0; r < rows; r++) { + const row = []; + for (let c = 0; c < cols; c++) { + const div = document.createElement("div"); + div.classList.add("cell"); + + // Randomize cell type + const random = Math.random(); + if (random < 0.1) { + div.classList.add("obstacle"); + } else if (random < 0.15) { + div.classList.add("power"); + powerCell = { r, c, element: div }; + } else if (random < 0.25) { + div.classList.add("bulb"); + bulbCells.push({ r, c, element: div }); + } else { + const type = Math.floor(Math.random() * wireTypes.length); + div.dataset.type = type; + div.dataset.rotation = 0; + div.addEventListener("click", () => rotateWire(div)); + drawWire(div, type, 0); + } + + grid.appendChild(div); + row.push(div); + } + cells.push(row); + } +} + +function drawWire(div, type, rotation) { + div.innerHTML = ""; + const wire = document.createElement("div"); + wire.className = "wire"; + wire.style.width = "100%"; + wire.style.height = "100%"; + wire.style.transform = `rotate(${rotation * 90}deg)`; + div.appendChild(wire); +} + +function rotateWire(div) { + if (isPaused) return; + const type = parseInt(div.dataset.type); + let rotation = parseInt(div.dataset.rotation); + rotation = (rotation + 1) % wireTypes[type].rotations; + div.dataset.rotation = rotation; + drawWire(div, type, rotation); + rotateSound.play(); + checkConnections(); +} + +function checkConnections() { + if (!powerCell) return; + let connected = 0; + bulbCells.forEach(bulb => { + if (Math.random() > 0.5) { + bulb.element.classList.add("glow"); + connected++; + } else { + bulb.element.classList.remove("glow"); + } + }); + + if (connected === bulbCells.length) { + message.textContent = "โšก All bulbs are lit! You win!"; + successSound.play(); + } else { + message.textContent = `Connected ${connected}/${bulbCells.length} bulbs`; + } +} + +document.getElementById("startBtn").addEventListener("click", () => { + clickSound.play(); + message.textContent = "Game started! Rotate wires to connect power!"; +}); + +document.getElementById("pauseBtn").addEventListener("click", () => { + isPaused = !isPaused; + clickSound.play(); + message.textContent = isPaused ? "โธ๏ธ Game paused." : "โ–ถ๏ธ Game resumed."; +}); + +document.getElementById("restartBtn").addEventListener("click", () => { + clickSound.play(); + bulbCells = []; + powerCell = null; + createGrid(); + message.textContent = "Game restarted!"; +}); + +createGrid(); diff --git a/games/energy-flow/style.css b/games/energy-flow/style.css new file mode 100644 index 00000000..01e51c48 --- /dev/null +++ b/games/energy-flow/style.css @@ -0,0 +1,102 @@ +body { + margin: 0; + padding: 0; + background: radial-gradient(circle, #0b0b0b 40%, #000); + font-family: 'Poppins', sans-serif; + color: #fff; + text-align: center; + overflow: hidden; +} + +.game-container { + max-width: 700px; + margin: 40px auto; + padding: 20px; + background: rgba(30, 30, 30, 0.8); + border-radius: 15px; + box-shadow: 0 0 25px #0ff; +} + +h1 { + color: #00e0ff; + text-shadow: 0 0 20px #0ff; + margin-bottom: 10px; +} + +.controls { + margin: 15px 0; +} + +button { + background-color: #111; + color: #0ff; + border: 1px solid #0ff; + padding: 8px 16px; + border-radius: 8px; + cursor: pointer; + margin: 5px; + font-size: 16px; + transition: 0.2s; +} + +button:hover { + background-color: #0ff; + color: #111; + box-shadow: 0 0 20px #0ff; +} + +.grid { + display: grid; + grid-template-columns: repeat(6, 70px); + grid-gap: 5px; + justify-content: center; +} + +.cell { + width: 70px; + height: 70px; + background-color: #1a1a1a; + border-radius: 12px; + box-shadow: inset 0 0 8px #333; + position: relative; + cursor: pointer; + transition: transform 0.3s; +} + +.cell:hover { + transform: scale(1.05); +} + +.wire { + position: absolute; + background: #555; + transition: background 0.3s, box-shadow 0.3s; +} + +.active { + background: #0ff; + box-shadow: 0 0 20px #0ff; +} + +.obstacle { + background: linear-gradient(135deg, #111, #333); + box-shadow: inset 0 0 10px #000; +} + +.power { + background: url('https://cdn-icons-png.flaticon.com/512/1048/1048953.png') center/50% no-repeat, #111; +} + +.bulb { + background: url('https://cdn-icons-png.flaticon.com/512/428/428001.png') center/50% no-repeat, #111; +} + +.bulb.glow { + filter: drop-shadow(0 0 15px yellow); +} + +.status { + margin-top: 20px; + font-size: 18px; + color: #0ff; +} diff --git a/games/escape_game/index.html b/games/escape_game/index.html new file mode 100644 index 00000000..7347e4a4 --- /dev/null +++ b/games/escape_game/index.html @@ -0,0 +1,33 @@ + + + + + + The CSS Room Escape + + + + +
+
๐Ÿ”’ DOOR: Find the key!
+ +
Wall: Look closely!
+ +
+ + + +
+ +
+ Box: Move me to the DOOR. +
+
+ +
+

Welcome to the CSS Room. Your goal is to open the door. Start inspecting elements!

+
+ + + + \ No newline at end of file diff --git a/games/escape_game/script.js b/games/escape_game/script.js new file mode 100644 index 00000000..a63717fa --- /dev/null +++ b/games/escape_game/script.js @@ -0,0 +1,128 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. DOM Elements & Constants --- + const roomContainer = document.getElementById('room-container'); + const wallClue = document.getElementById('wall-clue'); + const hiddenConsole = document.getElementById('hidden-console'); + const codeInput = document.getElementById('code-input'); + const submitCodeButton = document.getElementById('submit-code'); + const gridManipulator = document.getElementById('grid-manipulator'); + const door = document.getElementById('door'); + const messagePanel = document.getElementById('message'); + + // --- 2. GAME STATE --- + const CODE_ANSWER = '1337'; + let codeSolved = false; + let manipulatorMoved = false; + + // --- 3. CORE LOGIC FUNCTIONS --- + + /** + * Checks the submitted code against the known answer. + */ + function checkCode() { + if (codeInput.value === CODE_ANSWER) { + codeSolved = true; + messagePanel.textContent = '๐Ÿ”“ Console Unlocked! Key Fragment 1 found. Now, where is the final resting spot? (Hint: Grid positioning)'; + codeInput.disabled = true; + submitCodeButton.disabled = true; + + // Mark the manipulator as "active" for the next puzzle + gridManipulator.classList.add('active-clue'); + gridManipulator.textContent = 'Box: Move me to (3,3).'; + + // Show the next clue on the console itself + hiddenConsole.innerHTML += '

Clue: 3 / 3 / 4 / 4

'; + } else { + messagePanel.textContent = 'โŒ Incorrect Code. Inspect the room more thoroughly.'; + } + } + + /** + * Handles clicks on the grid manipulator box. + */ + function handleManipulatorClick() { + if (!codeSolved || manipulatorMoved) { + messagePanel.textContent = codeSolved + ? 'The box is now active. Its final position is grid row 3, column 3.' + : 'The box is locked. Find the code first!'; + return; + } + + // --- Puzzle 3: Grid Manipulation --- + // The player must use the browser DevTools to change the grid-area of the box + // Target CSS: grid-area: 3 / 3 / 4 / 4; (Bottom Right - Cell 9) + + // This JavaScript simply checks if the player has manually updated the CSS successfully. + + // Get the computed grid-area property + const computedStyle = window.getComputedStyle(gridManipulator); + const currentGridArea = computedStyle.getPropertyValue('grid-area').replace(/\s/g, ''); // Remove spaces + + // Target grid-area: 3 / 3 / 4 / 4 (which should resolve to '3/3/4/4') + const targetGridArea = '3/3/4/4'; + + if (currentGridArea === targetGridArea) { + manipulatorMoved = true; + messagePanel.textContent = '๐Ÿ”“ Box Moved! Key Fragment 2 found. The door is now ready to receive the final action.'; + gridManipulator.textContent = 'BOX: DESTINATION REACHED.'; + + door.textContent = '๐Ÿ”‘ DOOR: Click to escape!'; + door.classList.add('ready-to-open'); + } else { + messagePanel.textContent = `The box must be moved to grid-area: 3 / 3 / 4 / 4. Current area: ${currentGridArea}. Use DevTools!`; + } + } + + /** + * Handles the final click on the door. + */ + function handleDoorClick() { + if (door.classList.contains('ready-to-open')) { + messagePanel.innerHTML = '๐Ÿ† **YOU ESCAPED THE CSS ROOM!** All puzzles solved using DevTools!'; + door.style.backgroundColor = '#2ecc71'; + door.style.cursor = 'default'; + // Disable all interactions + submitCodeButton.disabled = true; + gridManipulator.removeEventListener('click', handleManipulatorClick); + door.removeEventListener('click', handleDoorClick); + } else { + messagePanel.textContent = 'The door is still locked. Complete the other puzzles first.'; + } + } + + // --- 4. HINT/STARTUP FUNCTIONS --- + + /** + * A hint for Puzzle 2: The player must inject CSS to unlock the console. + */ + function giveConsoleHint() { + // The console is hidden by opacity: 0.1 and transform: translateY(100px). + // The solution is to override these properties (e.g., using .unlocked class). + messagePanel.textContent += " (HINT: The console is nearly transparent and displaced. Find its ID and override its CSS!)"; + } + + // --- 5. EVENT LISTENERS --- + + submitCodeButton.addEventListener('click', checkCode); + + // Listen for Enter keypress on the input field + codeInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + checkCode(); + } + }); + + // The Wall Clue requires inspection, clicking just gives a hint + wallClue.addEventListener('click', giveConsoleHint); + + // The Grid Manipulator click handler + gridManipulator.addEventListener('click', handleManipulatorClick); + + // The Final Door handler + door.addEventListener('click', handleDoorClick); + + // Initial clue: tell the player to use DevTools to reveal the console + setTimeout(() => { + messagePanel.textContent = "HINT 1: Inspect element #hidden-console. It's almost invisible. You must manually override its CSS properties to reveal the input fields and continue."; + }, 1000); +}); \ No newline at end of file diff --git a/games/escape_game/style.css b/games/escape_game/style.css new file mode 100644 index 00000000..605a0367 --- /dev/null +++ b/games/escape_game/style.css @@ -0,0 +1,113 @@ +:root { + --grid-size: 3; + --room-width: 450px; + --cell-size: calc(var(--room-width) / var(--grid-size)); +} + +body { + font-family: 'Courier New', monospace; /* Developer/Code style font */ + display: flex; + flex-direction: column; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #333; /* Dark, hacker-ish theme */ + color: #00ff00; /* Green terminal text */ +} + +#room-container { + width: var(--room-width); + height: var(--room-width); + margin: 50px auto 20px; + border: 5px solid #007bff; /* Blue frame */ + display: grid; + /* 3x3 Grid layout */ + grid-template-columns: repeat(var(--grid-size), 1fr); + grid-template-rows: repeat(var(--grid-size), 1fr); + + /* Place elements in specific grid areas (1-9) */ + /* 1 2 3 */ + /* 4 5 6 */ + /* 7 8 9 */ +} + +.cell { + border: 1px solid #007bff; + padding: 10px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + cursor: pointer; + background-color: #222; + transition: background-color 0.2s, transform 0.3s; +} + +/* Initial Placement */ +#door { + grid-area: 1 / 3 / 2 / 4; /* Top Right (Cell 3) */ + background-color: #555; + color: white; +} + +#wall-clue { + grid-area: 2 / 1 / 3 / 2; /* Middle Left (Cell 4) */ +} + +#hidden-console { + grid-area: 3 / 2 / 4 / 3; /* Bottom Middle (Cell 8) */ + background-color: #111; +} + +#grid-manipulator { + grid-area: 1 / 1 / 2 / 2; /* Top Left (Cell 1) */ + background-color: #f1c40f; /* Yellow object */ + color: #333; + font-weight: bold; + z-index: 20; +} + +/* --- Puzzle 1: Inspect Clue (Hidden Text) --- */ +#wall-clue { + position: relative; + overflow: hidden; +} + +/* The invisible clue */ +#wall-clue::after { + content: "Z-INDEX IS 1337"; /* This is the answer to the console puzzle */ + position: absolute; + color: rgba(0, 0, 0, 0.001); /* Nearly transparent, requires dev tools to read */ + font-size: 0.1px; /* Tiny font */ + z-index: 5; + background-color: #222; /* Same color as background */ + transform: rotate(180deg) scale(0); /* Rotated and shrunk for extra difficulty */ + transition: all 1s; +} + +/* --- Puzzle 2: Hiding Clue (Console) --- */ +#hidden-console { + /* Clue: To reveal this, the user must change its CSS property */ + opacity: 0.1; /* Almost invisible */ + transform: translateY(100px); /* Pushed out of view */ + pointer-events: none; /* Cannot be clicked until revealed */ + transition: opacity 1s, transform 1s; +} + +/* State to be activated by player's custom CSS injection */ +.unlocked { + opacity: 1 !important; + transform: translateY(0) !important; + pointer-events: auto !important; +} + +/* --- Feedback Panel --- */ +#feedback-panel { + margin-top: 20px; + padding: 15px; + border: 1px solid #00ff00; + width: 80%; + max-width: var(--room-width); + min-height: 50px; +} \ No newline at end of file diff --git a/games/eye-blink-test/index.html b/games/eye-blink-test/index.html new file mode 100644 index 00000000..0dfcf9c4 --- /dev/null +++ b/games/eye-blink-test/index.html @@ -0,0 +1,37 @@ + + + + + + Eye Blink Test ๐Ÿ‘๏ธ + + + +
+

๐Ÿ‘๏ธ Eye Blink Test

+

Keep your cursor inside the eye for 10 seconds without blinking (moving out)!

+ +
+
+
+
+ +
+ + + +
+ +
+

โฑ๏ธ Time: 0s

+

+
+ + + + +
+ + + + diff --git a/games/eye-blink-test/script.js b/games/eye-blink-test/script.js new file mode 100644 index 00000000..c226ae57 --- /dev/null +++ b/games/eye-blink-test/script.js @@ -0,0 +1,97 @@ +const eyeArea = document.getElementById("eye-area"); +const pupil = document.getElementById("pupil"); +const timerDisplay = document.getElementById("timer"); +const message = document.getElementById("message"); + +const startBtn = document.getElementById("startBtn"); +const pauseBtn = document.getElementById("pauseBtn"); +const restartBtn = document.getElementById("restartBtn"); + +const winSound = document.getElementById("winSound"); +const loseSound = document.getElementById("loseSound"); +const hoverSound = document.getElementById("hoverSound"); + +let timer = 0; +let interval = null; +let gameStarted = false; +let paused = false; + +// Handle pupil movement following cursor +document.addEventListener("mousemove", (e) => { + const rect = eyeArea.getBoundingClientRect(); + const x = e.clientX - rect.left - rect.width / 2; + const y = e.clientY - rect.top - rect.height / 2; + pupil.style.transform = `translate(${x / 10}px, ${y / 10}px)`; +}); + +// Game logic +eyeArea.addEventListener("mouseenter", () => { + if (gameStarted && !paused) { + hoverSound.currentTime = 0; + hoverSound.play(); + } +}); + +eyeArea.addEventListener("mouseleave", () => { + if (gameStarted && !paused) { + loseSound.currentTime = 0; + loseSound.play(); + message.textContent = "๐Ÿ’ฅ You blinked! Timer reset."; + resetTimer(); + } +}); + +function startGame() { + if (!gameStarted) { + message.textContent = "Keep hovering inside the eye!"; + interval = setInterval(runTimer, 1000); + gameStarted = true; + paused = false; + } +} + +function pauseGame() { + if (gameStarted && !paused) { + clearInterval(interval); + paused = true; + message.textContent = "โธ๏ธ Game paused."; + } else if (paused) { + interval = setInterval(runTimer, 1000); + paused = false; + message.textContent = "โ–ถ๏ธ Game resumed!"; + } +} + +function restartGame() { + clearInterval(interval); + timer = 0; + timerDisplay.textContent = timer; + message.textContent = "๐Ÿ”„ Game restarted. Click Start!"; + gameStarted = false; + paused = false; +} + +function runTimer() { + timer++; + timerDisplay.textContent = timer; + if (timer >= 10) { + clearInterval(interval); + winSound.play(); + message.textContent = "๐ŸŽ‰ You won! You didnโ€™t blink!"; + gameStarted = false; + } +} + +function resetTimer() { + clearInterval(interval); + timer = 0; + timerDisplay.textContent = timer; + gameStarted = false; + setTimeout(() => { + message.textContent = "Try again!"; + }, 1000); +} + +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", pauseGame); +restartBtn.addEventListener("click", restartGame); diff --git a/games/eye-blink-test/style.css b/games/eye-blink-test/style.css new file mode 100644 index 00000000..deb8640c --- /dev/null +++ b/games/eye-blink-test/style.css @@ -0,0 +1,88 @@ +body { + margin: 0; + height: 100vh; + background: radial-gradient(circle at center, #0d1117, #000); + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + color: white; + font-family: 'Poppins', sans-serif; + overflow: hidden; +} + +h1 { + margin-bottom: 10px; + text-shadow: 0 0 15px #00eaff, 0 0 30px #00eaff; +} + +.instructions { + color: #bbb; + margin-bottom: 20px; +} + +#eye-area { + width: 200px; + height: 120px; + background: radial-gradient(ellipse at center, #111 0%, #000 100%); + border: 3px solid #00eaff; + border-radius: 50% / 60%; + position: relative; + box-shadow: 0 0 25px #00eaff; + overflow: hidden; + transition: transform 0.2s ease; +} + +#pupil { + position: absolute; + width: 40px; + height: 40px; + background: radial-gradient(circle, #222 20%, #000 90%); + border-radius: 50%; + top: 40px; + left: 80px; + transition: all 0.1s ease; +} + +#glow { + position: absolute; + width: 100%; + height: 100%; + border-radius: 50%; + box-shadow: 0 0 60px #00eaff; + pointer-events: none; + opacity: 0.5; +} + +.controls { + margin-top: 25px; +} + +button { + background: #00eaff; + color: black; + font-weight: 600; + border: none; + border-radius: 8px; + padding: 10px 16px; + margin: 5px; + cursor: pointer; + transition: all 0.3s ease; +} + +button:hover { + background: #00b8d4; + transform: scale(1.05); +} + +.status { + margin-top: 15px; + font-size: 18px; +} + +#message { + margin-top: 10px; + font-weight: bold; + color: #00eaff; + text-shadow: 0 0 10px #00eaff; +} diff --git a/games/falling-leaves-collector/index.html b/games/falling-leaves-collector/index.html new file mode 100644 index 00000000..77f5abd0 --- /dev/null +++ b/games/falling-leaves-collector/index.html @@ -0,0 +1,22 @@ + + + + + + Falling Leaves Collector | Mini JS Games Hub + + + +
+

๐Ÿ‚ Falling Leaves Collector ๐Ÿ‚

+
+ Score: 0 + Lives: 5 +
+ +

Use Left/Right arrow keys or Move Mouse to collect falling leaves!

+ +
+ + + diff --git a/games/falling-leaves-collector/script.js b/games/falling-leaves-collector/script.js new file mode 100644 index 00000000..2ff3db99 --- /dev/null +++ b/games/falling-leaves-collector/script.js @@ -0,0 +1,123 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); + +canvas.width = 400; +canvas.height = 600; + +let score = 0; +let lives = 5; +let leaves = []; +let basket = { x: canvas.width / 2 - 40, y: canvas.height - 50, width: 80, height: 20, speed: 7 }; +let gameOver = false; +let animationId; // store the animation frame id + +// Generate random leaves +function createLeaf() { + const size = Math.random() * 20 + 20; + const leaf = { + x: Math.random() * (canvas.width - size), + y: -size, + size: size, + speed: Math.random() * 2 + 2, + color: `hsl(${Math.random() * 40 + 20}, 80%, 50%)` + }; + leaves.push(leaf); +} + +// Draw basket +function drawBasket() { + ctx.fillStyle = "#8b4513"; + ctx.fillRect(basket.x, basket.y, basket.width, basket.height); +} + +// Draw leaves +function drawLeaves() { + leaves.forEach(leaf => { + ctx.fillStyle = leaf.color; + ctx.beginPath(); + ctx.ellipse(leaf.x, leaf.y, leaf.size / 2, leaf.size / 3, 0, 0, Math.PI * 2); + ctx.fill(); + }); +} + +// Draw score and lives +function drawScoreboard() { + document.getElementById('score').textContent = score; + document.getElementById('lives').textContent = lives; +} + +// Detect collision +function checkCollision(leaf) { + return leaf.x < basket.x + basket.width && + leaf.x + leaf.size > basket.x && + leaf.y + leaf.size / 3 > basket.y && + leaf.y < basket.y + basket.height; +} + +// Update game state +function update() { + if (gameOver) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Add leaves randomly + if (Math.random() < 0.02) createLeaf(); + + // Update leaf positions - iterate backwards for safe removal + for (let i = leaves.length - 1; i >= 0; i--) { + const leaf = leaves[i]; + leaf.y += leaf.speed; + if (checkCollision(leaf)) { + score += 10; + leaves.splice(i, 1); + } else if (leaf.y > canvas.height) { + lives--; + leaves.splice(i, 1); + if (lives <= 0) gameOver = true; + } + } + + drawLeaves(); + drawBasket(); + drawScoreboard(); + + if (gameOver) { + ctx.fillStyle = "rgba(0,0,0,0.5)"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "#fff"; + ctx.font = "30px Arial"; + ctx.textAlign = "center"; + ctx.fillText("Game Over!", canvas.width / 2, canvas.height / 2); + ctx.fillText(`Score: ${score}`, canvas.width / 2, canvas.height / 2 + 40); + } else { + animationId = requestAnimationFrame(update); + } +} + +// Basket controls +document.addEventListener('keydown', e => { + if (e.key === "ArrowLeft" && basket.x > 0) basket.x -= basket.speed; + if (e.key === "ArrowRight" && basket.x + basket.width < canvas.width) basket.x += basket.speed; +}); + +// Mouse movement +canvas.addEventListener('mousemove', e => { + const rect = canvas.getBoundingClientRect(); + basket.x = e.clientX - rect.left - basket.width / 2; + if (basket.x < 0) basket.x = 0; + if (basket.x + basket.width > canvas.width) basket.x = canvas.width - basket.width; +}); + +// Restart button +document.getElementById('restartBtn').addEventListener('click', () => { + cancelAnimationFrame(animationId); // Stop any existing loop + score = 0; + lives = 5; + leaves = []; + basket.x = canvas.width / 2 - 40; + gameOver = false; + animationId = requestAnimationFrame(update); // Start a fresh loop +}); + +// Start game +animationId = requestAnimationFrame(update); diff --git a/games/falling-leaves-collector/style.css b/games/falling-leaves-collector/style.css new file mode 100644 index 00000000..294f3c9b --- /dev/null +++ b/games/falling-leaves-collector/style.css @@ -0,0 +1,56 @@ +body { + margin: 0; + padding: 0; + font-family: 'Arial', sans-serif; + background: linear-gradient(to bottom, #87ceeb, #f4e1d2); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + text-align: center; + width: 100%; + max-width: 500px; +} + +h1 { + font-size: 2rem; + margin-bottom: 10px; +} + +.scoreboard { + display: flex; + justify-content: space-between; + font-size: 1.2rem; + margin-bottom: 10px; +} + +canvas { + border: 2px solid #333; + background: rgba(255, 255, 255, 0.3); + display: block; + margin: 0 auto; + border-radius: 10px; +} + +.instructions { + font-size: 0.9rem; + margin-top: 10px; +} + +button { + margin-top: 15px; + padding: 10px 20px; + font-size: 1rem; + cursor: pointer; + border: none; + background-color: #ff8c00; + color: white; + border-radius: 5px; +} + +button:hover { + background-color: #e07b00; +} diff --git a/games/farm-life/index.html b/games/farm-life/index.html new file mode 100644 index 00000000..1bfd56c1 --- /dev/null +++ b/games/farm-life/index.html @@ -0,0 +1,147 @@ + + + + + + Farm Life - Mini JS Games Hub + + + +
+
+

๐ŸŒพ Farm Life

+

Manage your farm, grow crops, and raise animals!

+
+ +
+
+
+ +
+
+ +
+
+
+

Season: Spring

+
+
+
+
+
+

Weather: Sunny

+
โ˜€๏ธ
+
+
+ +
+

Farm Actions

+
+ + + + + + + + +
+
+ +
+

Inventory

+
+
+ ๐ŸŒฑ + Seeds + 10 +
+
+ ๐Ÿ’ง + Water + 20 +
+
+ ๐ŸŒฝ + Feed + 15 +
+
+ ๐Ÿฅš + Eggs + 0 +
+
+ ๐Ÿฅ• + Carrots + 0 +
+
+ ๐ŸŒป + Sunflowers + 0 +
+
+
+ +
+

Farm Statistics

+
+ Money: + $100 +
+
+ Level: + 1 +
+
+ Experience: + 0/100 +
+
+ Chickens: + 0 +
+
+ Cows: + 0 +
+
+ Day: + 1 +
+
+ Score: + 0 +
+
+ +
+ + + +
+
+
+ +
+ +
+

How to Play:

+
    +
  • Plant seeds on empty soil tiles and water them to grow crops
  • +
  • Harvest mature crops and sell them for money
  • +
  • Buy chickens and cows to produce eggs and milk
  • +
  • Feed your animals regularly to keep them productive
  • +
  • Collect eggs and milk from your animals
  • +
  • Weather affects crop growth - watch the forecast!
  • +
  • Seasons change every 10 days, affecting crop types
  • +
  • Upgrade your farm to unlock new features
  • +
  • Gain experience and level up to expand your farm
  • +
+
+
+ + + + \ No newline at end of file diff --git a/games/farm-life/script.js b/games/farm-life/script.js new file mode 100644 index 00000000..75276ad2 --- /dev/null +++ b/games/farm-life/script.js @@ -0,0 +1,646 @@ +// Farm Life Game Logic +class FarmLife { + constructor() { + this.gridSize = { width: 15, height: 10 }; + this.grid = []; + this.day = 1; + this.season = 'spring'; + this.weather = 'sunny'; + this.seasonDay = 1; + this.isPlaying = false; + this.gameSpeed = 1; + + // Farm resources + this.resources = { + money: 100, + seeds: 10, + water: 20, + feed: 15, + eggs: 0, + carrots: 0, + sunflowers: 0 + }; + + // Farm stats + this.stats = { + level: 1, + experience: 0, + chickens: 0, + cows: 0, + score: 0 + }; + + // Crop data + this.crops = { + carrot: { + emoji: '๐Ÿฅ•', + growthTime: 3, + waterNeeded: 2, + value: 5, + seasons: ['spring', 'summer', 'fall'] + }, + sunflower: { + emoji: '๐ŸŒป', + growthTime: 4, + waterNeeded: 3, + value: 8, + seasons: ['summer', 'fall'] + }, + wheat: { + emoji: '๐ŸŒพ', + growthTime: 5, + waterNeeded: 4, + value: 6, + seasons: ['spring', 'summer', 'fall'] + } + }; + + // Animal data + this.animals = { + chicken: { + emoji: '๐Ÿ”', + cost: 20, + feedCost: 1, + production: 'eggs', + productionRate: 0.8, // eggs per day + happiness: 100 + }, + cow: { + emoji: '๐Ÿ„', + cost: 50, + feedCost: 2, + production: 'milk', + productionRate: 0.5, // milk per day + happiness: 100 + } + }; + + this.gameLoop = null; + this.selectedAction = 'plant'; + this.selectedCrop = 'carrot'; + this.notifications = []; + this.init(); + } + + init() { + this.initializeGrid(); + this.bindEvents(); + this.updateDisplay(); + this.showNotification('Welcome to Farm Life! Start by planting some crops.', 'success'); + } + + initializeGrid() { + const gridElement = document.getElementById('farm-grid'); + gridElement.innerHTML = ''; + + this.grid = []; + for (let y = 0; y < this.gridSize.height; y++) { + this.grid[y] = []; + for (let x = 0; x < this.gridSize.width; x++) { + this.grid[y][x] = { + type: 'empty', + crop: null, + growthStage: 0, + watered: false, + animal: null + }; + + const cell = document.createElement('div'); + cell.className = 'grid-cell empty'; + cell.dataset.x = x; + cell.dataset.y = y; + cell.addEventListener('click', () => this.onCellClick(x, y)); + gridElement.appendChild(cell); + } + } + } + + bindEvents() { + // Action buttons + document.getElementById('plant-btn').addEventListener('click', () => this.selectAction('plant')); + document.getElementById('water-btn').addEventListener('click', () => this.selectAction('water')); + document.getElementById('harvest-btn').addEventListener('click', () => this.selectAction('harvest')); + document.getElementById('buy-animal-btn').addEventListener('click', () => this.selectAction('buy-animal')); + document.getElementById('feed-animals-btn').addEventListener('click', () => this.selectAction('feed-animals')); + document.getElementById('collect-eggs-btn').addEventListener('click', () => this.selectAction('collect')); + document.getElementById('sell-btn').addEventListener('click', () => this.selectAction('sell')); + document.getElementById('upgrade-btn').addEventListener('click', () => this.upgradeFarm()); + + // Game controls + document.getElementById('play-pause-btn').addEventListener('click', () => this.togglePlayPause()); + document.getElementById('speed-btn').addEventListener('click', () => this.changeSpeed()); + document.getElementById('reset-btn').addEventListener('click', () => this.resetGame()); + + // Keyboard shortcuts + document.addEventListener('keydown', (e) => { + switch(e.key.toLowerCase()) { + case ' ': + e.preventDefault(); + this.togglePlayPause(); + break; + case 'p': + if (!e.ctrlKey) this.selectAction('plant'); + break; + case 'w': + if (!e.ctrlKey) this.selectAction('water'); + break; + case 'h': + if (!e.ctrlKey) this.selectAction('harvest'); + break; + case 'r': + if (e.ctrlKey) { + e.preventDefault(); + this.resetGame(); + } + break; + } + }); + } + + selectAction(action) { + this.selectedAction = action; + + // Update button styles + document.querySelectorAll('.action-btn').forEach(btn => { + btn.classList.remove('active'); + }); + document.getElementById(`${action}-btn`).classList.add('active'); + } + + onCellClick(x, y) { + const cell = this.grid[y][x]; + const cellElement = this.getCellElement(x, y); + + switch(this.selectedAction) { + case 'plant': + this.plantCrop(x, y); + break; + case 'water': + this.waterCrop(x, y); + break; + case 'harvest': + this.harvestCrop(x, y); + break; + case 'buy-animal': + this.buyAnimal(x, y); + break; + case 'feed-animals': + this.feedAnimal(x, y); + break; + case 'collect': + this.collectFromAnimal(x, y); + break; + case 'sell': + this.showNotification('Use the sell button to sell all harvested produce!', 'info'); + break; + } + } + + plantCrop(x, y) { + const cell = this.grid[y][x]; + + if (cell.type !== 'empty' && cell.type !== 'plowed') { + this.showNotification('This spot is not suitable for planting!', 'warning'); + return; + } + + if (this.resources.seeds <= 0) { + this.showNotification('You need seeds to plant crops!', 'error'); + return; + } + + // Check if crop can be planted in current season + if (!this.crops[this.selectedCrop].seasons.includes(this.season)) { + this.showNotification(`${this.selectedCrop} cannot be planted in ${this.season}!`, 'warning'); + return; + } + + cell.type = 'planted'; + cell.crop = this.selectedCrop; + cell.growthStage = 1; + cell.watered = false; + + this.resources.seeds--; + + this.updateCellDisplay(x, y); + this.updateDisplay(); + this.showNotification(`Planted ${this.selectedCrop}!`, 'success'); + this.playSound('plant'); + } + + waterCrop(x, y) { + const cell = this.grid[y][x]; + + if (cell.type !== 'planted' && cell.type !== 'growing') { + this.showNotification('Nothing to water here!', 'warning'); + return; + } + + if (this.resources.water <= 0) { + this.showNotification('You need water to water crops!', 'error'); + return; + } + + cell.watered = true; + this.resources.water--; + + this.updateCellDisplay(x, y); + this.updateDisplay(); + this.showNotification('Crops watered!', 'success'); + this.playSound('water'); + } + + harvestCrop(x, y) { + const cell = this.grid[y][x]; + + if (cell.type !== 'ready') { + this.showNotification('This crop is not ready to harvest!', 'warning'); + return; + } + + const crop = this.crops[cell.crop]; + const harvestAmount = Math.floor(Math.random() * 3) + 1; // 1-3 items + + // Add to inventory + if (cell.crop === 'carrot') { + this.resources.carrots += harvestAmount; + } else if (cell.crop === 'sunflower') { + this.resources.sunflowers += harvestAmount; + } + + // Reset cell + cell.type = 'empty'; + cell.crop = null; + cell.growthStage = 0; + cell.watered = false; + + this.stats.experience += 5; + this.checkLevelUp(); + + this.updateCellDisplay(x, y); + this.updateDisplay(); + this.showNotification(`Harvested ${harvestAmount} ${cell.crop}(s)!`, 'success'); + this.playSound('harvest'); + } + + buyAnimal(x, y) { + const cell = this.grid[y][x]; + + if (cell.type !== 'empty') { + this.showNotification('This spot is occupied!', 'warning'); + return; + } + + // Determine which animal to buy (alternates between chicken and cow) + const animalType = this.stats.chickens <= this.stats.cows ? 'chicken' : 'cow'; + const animal = this.animals[animalType]; + + if (this.resources.money < animal.cost) { + this.showNotification(`Not enough money to buy a ${animalType}!`, 'error'); + return; + } + + cell.type = 'animal'; + cell.animal = animalType; + this.resources.money -= animal.cost; + + if (animalType === 'chicken') { + this.stats.chickens++; + } else { + this.stats.cows++; + } + + this.updateCellDisplay(x, y); + this.updateDisplay(); + this.showNotification(`Bought a ${animalType}!`, 'success'); + this.playSound('buy'); + } + + feedAnimal(x, y) { + const cell = this.grid[y][x]; + + if (cell.type !== 'animal') { + this.showNotification('No animal here to feed!', 'warning'); + return; + } + + const animal = this.animals[cell.animal]; + if (this.resources.feed < animal.feedCost) { + this.showNotification('Not enough feed!', 'error'); + return; + } + + this.resources.feed -= animal.feedCost; + this.updateDisplay(); + this.showNotification(`Fed the ${cell.animal}!`, 'success'); + this.playSound('feed'); + } + + collectFromAnimal(x, y) { + const cell = this.grid[y][x]; + + if (cell.type !== 'animal') { + this.showNotification('No animal here to collect from!', 'warning'); + return; + } + + const animal = this.animals[cell.animal]; + const production = Math.random() < animal.productionRate ? 1 : 0; + + if (production > 0) { + if (cell.animal === 'chicken') { + this.resources.eggs += production; + this.showNotification(`Collected ${production} egg(s)!`, 'success'); + } else { + // For cows, we'll add milk to inventory (simplified) + this.resources.eggs += production; // Using eggs count for milk too + this.showNotification(`Collected ${production} milk!`, 'success'); + } + this.stats.experience += 3; + this.checkLevelUp(); + } else { + this.showNotification('Nothing to collect yet!', 'info'); + } + + this.playSound('collect'); + } + + upgradeFarm() { + const upgradeCost = this.stats.level * 50; + + if (this.resources.money < upgradeCost) { + this.showNotification('Not enough money for upgrade!', 'error'); + return; + } + + this.resources.money -= upgradeCost; + this.stats.level++; + + // Upgrade benefits + this.resources.seeds += 5; + this.resources.water += 10; + this.resources.feed += 5; + + this.updateDisplay(); + this.showNotification(`Farm upgraded to level ${this.stats.level}!`, 'success'); + this.playSound('upgrade'); + } + + updateCellDisplay(x, y) { + const cell = this.grid[y][x]; + const cellElement = this.getCellElement(x, y); + + cellElement.className = 'grid-cell'; + + if (cell.type === 'empty') { + cellElement.classList.add('empty'); + cellElement.textContent = ''; + } else if (cell.type === 'planted' || cell.type === 'growing') { + cellElement.classList.add(cell.type); + cellElement.textContent = this.crops[cell.crop].emoji; + } else if (cell.type === 'ready') { + cellElement.classList.add('ready'); + cellElement.textContent = this.crops[cell.crop].emoji; + } else if (cell.type === 'animal') { + cellElement.classList.add(cell.animal); + cellElement.textContent = this.animals[cell.animal].emoji; + } + } + + togglePlayPause() { + this.isPlaying = !this.isPlaying; + const btn = document.getElementById('play-pause-btn'); + + if (this.isPlaying) { + btn.textContent = 'โธ๏ธ Pause'; + this.startGameLoop(); + } else { + btn.textContent = 'โ–ถ๏ธ Play'; + this.stopGameLoop(); + } + } + + changeSpeed() { + const speeds = [1, 2, 4]; + const currentIndex = speeds.indexOf(this.gameSpeed); + this.gameSpeed = speeds[(currentIndex + 1) % speeds.length]; + + document.getElementById('speed-btn').textContent = `${this.gameSpeed}x Speed`; + } + + startGameLoop() { + this.stopGameLoop(); + this.gameLoop = setInterval(() => { + this.advanceDay(); + }, 10000 / this.gameSpeed); // 10 seconds per day at 1x speed + } + + stopGameLoop() { + if (this.gameLoop) { + clearInterval(this.gameLoop); + this.gameLoop = null; + } + } + + advanceDay() { + this.day++; + this.seasonDay++; + + // Change season every 10 days + if (this.seasonDay > 10) { + this.changeSeason(); + this.seasonDay = 1; + } + + // Random weather + this.changeWeather(); + + // Grow crops + this.growCrops(); + + // Update score + this.updateScore(); + + this.updateDisplay(); + + if (this.day % 5 === 0) { + this.showNotification(`Day ${this.day}: Your farm is growing!`, 'success'); + } + } + + changeSeason() { + const seasons = ['spring', 'summer', 'fall', 'winter']; + const currentIndex = seasons.indexOf(this.season); + this.season = seasons[(currentIndex + 1) % seasons.length]; + + document.getElementById('current-season').textContent = this.season.charAt(0).toUpperCase() + this.season.slice(1); + + if (this.season === 'winter') { + this.showNotification('Winter has arrived! Many crops won\'t grow.', 'warning'); + } else { + this.showNotification(`${this.season.charAt(0).toUpperCase() + this.season.slice(1)} has arrived!`, 'success'); + } + } + + changeWeather() { + const weathers = [ + { name: 'sunny', emoji: 'โ˜€๏ธ', growth: 1.2 }, + { name: 'cloudy', emoji: 'โ˜๏ธ', growth: 1.0 }, + { name: 'rainy', emoji: '๐ŸŒง๏ธ', growth: 1.5 }, + { name: 'stormy', emoji: 'โ›ˆ๏ธ', growth: 0.8 } + ]; + + const randomWeather = weathers[Math.floor(Math.random() * weathers.length)]; + this.weather = randomWeather.name; + + document.getElementById('current-weather').textContent = randomWeather.name.charAt(0).toUpperCase() + randomWeather.name.slice(1); + document.getElementById('weather-icon').textContent = randomWeather.emoji; + } + + growCrops() { + for (let y = 0; y < this.gridSize.height; y++) { + for (let x = 0; x < this.gridSize.width; x++) { + const cell = this.grid[y][x]; + + if (cell.type === 'planted' || cell.type === 'growing') { + const crop = this.crops[cell.crop]; + let growthRate = 1; + + // Weather affects growth + if (this.weather === 'sunny') growthRate *= 1.2; + else if (this.weather === 'rainy') growthRate *= 1.5; + else if (this.weather === 'stormy') growthRate *= 0.8; + + // Watered crops grow faster + if (cell.watered) growthRate *= 1.3; + + cell.growthStage += growthRate; + + if (cell.growthStage >= crop.growthTime) { + cell.type = 'ready'; + } else if (cell.growthStage >= crop.growthTime * 0.5) { + cell.type = 'growing'; + } + + this.updateCellDisplay(x, y); + } + } + } + } + + updateScore() { + this.stats.score = Math.floor( + this.resources.money * 0.1 + + this.stats.experience + + this.stats.chickens * 20 + + this.stats.cows * 50 + + this.resources.eggs * 2 + + this.resources.carrots * 3 + + this.resources.sunflowers * 5 + ); + } + + checkLevelUp() { + const expNeeded = this.stats.level * 100; + if (this.stats.experience >= expNeeded) { + this.stats.level++; + this.stats.experience -= expNeeded; + this.resources.money += 25; + this.showNotification(`Level up! You are now level ${this.stats.level}!`, 'success'); + this.playSound('levelup'); + } + } + + updateDisplay() { + // Resources + document.getElementById('money').textContent = `$${this.resources.money}`; + document.getElementById('seeds-count').textContent = this.resources.seeds; + document.getElementById('water-count').textContent = this.resources.water; + document.getElementById('feed-count').textContent = this.resources.feed; + document.getElementById('eggs-count').textContent = this.resources.eggs; + document.getElementById('carrots-count').textContent = this.resources.carrots; + document.getElementById('sunflowers-count').textContent = this.resources.sunflowers; + + // Stats + document.getElementById('level').textContent = this.stats.level; + document.getElementById('experience').textContent = `${this.stats.experience}/${this.stats.level * 100}`; + document.getElementById('chickens').textContent = this.stats.chickens; + document.getElementById('cows').textContent = this.stats.cows; + document.getElementById('day').textContent = this.day; + document.getElementById('score').textContent = this.stats.score; + + // Season progress + const seasonProgress = (this.seasonDay / 10) * 100; + document.getElementById('season-bar').style.width = seasonProgress + '%'; + } + + showNotification(message, type = 'info') { + const notifications = document.getElementById('notifications'); + const notification = document.createElement('div'); + notification.className = `notification ${type}`; + notification.textContent = message; + + notifications.appendChild(notification); + + setTimeout(() => { + notification.remove(); + }, 4000); + } + + playSound(action) { + // Simple sound effects using Web Audio API + try { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + let frequency = 440; // A4 + switch(action) { + case 'plant': frequency = 523; break; // C5 + case 'water': frequency = 330; break; // E4 + case 'harvest': frequency = 659; break; // E5 + case 'buy': frequency = 784; break; // G5 + case 'feed': frequency = 440; break; // A4 + case 'collect': frequency = 554; break; // C#5 + case 'upgrade': frequency = 831; break; // G#5 + case 'levelup': frequency = 1047; break; // C6 + } + + oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime); + gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.start(); + oscillator.stop(audioContext.currentTime + 0.3); + } catch (e) { + // Web Audio API not supported, skip sound + } + } + + getCellElement(x, y) { + return document.querySelector(`[data-x="${x}"][data-y="${y}"]`); + } + + resetGame() { + if (confirm('Are you sure you want to reset the game? All progress will be lost!')) { + this.stopGameLoop(); + location.reload(); + } + } +} + +// Initialize the game when the page loads +document.addEventListener('DOMContentLoaded', () => { + new FarmLife(); +}); + +// Enable audio on first user interaction +document.addEventListener('click', () => { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + if (audioContext && audioContext.state === 'suspended') { + audioContext.resume(); + } +}, { once: true }); \ No newline at end of file diff --git a/games/farm-life/style.css b/games/farm-life/style.css new file mode 100644 index 00000000..db55cdff --- /dev/null +++ b/games/farm-life/style.css @@ -0,0 +1,418 @@ +/* Farm Life Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #87CEEB 0%, #90EE90 100%); + color: #2c3e50; + min-height: 100vh; + padding: 20px; +} + +body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle at 20% 80%, rgba(135, 206, 235, 0.3) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(144, 238, 144, 0.3) 0%, transparent 50%), + radial-gradient(circle at 40% 40%, rgba(255, 255, 224, 0.2) 0%, transparent 50%); + pointer-events: none; + z-index: -1; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 30px; +} + +header h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.1); + background: linear-gradient(45deg, #228B22, #FFD700, #FF6347, #4169E1); + background-size: 400% 400%; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + animation: gradientShift 3s ease infinite; +} + +@keyframes gradientShift { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +header p { + font-size: 1.2em; + opacity: 0.8; +} + +.game-area { + display: grid; + grid-template-columns: 1fr 350px; + gap: 20px; + margin-bottom: 30px; +} + +.farm-grid-container { + background: rgba(255, 255, 255, 0.9); + padding: 20px; + border-radius: 15px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(10px); + border: 2px solid rgba(255, 255, 255, 0.2); +} + +.farm-grid { + display: grid; + grid-template-columns: repeat(15, 1fr); + grid-template-rows: repeat(10, 1fr); + gap: 2px; + background: #8B4513; + border: 2px solid #654321; + border-radius: 10px; + overflow: hidden; + aspect-ratio: 1.5/1; + max-width: 100%; +} + +.grid-cell { + background: #228B22; + border: 1px solid #006400; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2em; + transition: all 0.2s ease; + position: relative; +} + +.grid-cell:hover { + background: #32CD32; + transform: scale(1.05); +} + +.grid-cell.plowed { + background: #8B4513; +} + +.grid-cell.planted { + background: #228B22; + animation: grow 0.5s ease; +} + +@keyframes grow { + 0% { transform: scale(0.8); } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); } +} + +.grid-cell.growing { + background: #90EE90; +} + +.grid-cell.ready { + background: #FFD700; + animation: ready 1s infinite; +} + +@keyframes ready { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.1); } +} + +.grid-cell.chicken { + background: #FFFACD; +} + +.grid-cell.cow { + background: #F5DEB3; +} + +.control-panel { + display: flex; + flex-direction: column; + gap: 15px; +} + +.season-weather, .actions-panel, .inventory-panel, .farm-stats { + background: rgba(255, 255, 255, 0.9); + padding: 15px; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.season-weather h3, .actions-panel h3, .inventory-panel h3, .farm-stats h3 { + color: #228B22; + margin-bottom: 10px; + text-align: center; +} + +.season-weather { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.season-progress { + width: 100%; + height: 8px; + background: rgba(0, 0, 0, 0.1); + border-radius: 4px; + overflow: hidden; +} + +.season-bar { + height: 100%; + background: linear-gradient(90deg, #FFD700, #FF6347); + width: 0%; + transition: width 0.5s ease; +} + +.weather-icon { + font-size: 2em; + text-align: center; + margin-top: 5px; +} + +.action-buttons { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; +} + +.action-btn { + background: linear-gradient(135deg, #228B22, #32CD32); + color: white; + border: none; + padding: 8px 10px; + border-radius: 8px; + font-size: 0.9em; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.action-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.action-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.inventory-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; +} + +.inventory-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px; + background: rgba(144, 238, 144, 0.3); + border-radius: 6px; + border: 1px solid rgba(34, 139, 34, 0.2); +} + +.item-icon { + font-size: 1.2em; +} + +.item-name { + flex: 1; + font-weight: bold; + color: #228B22; +} + +.item-count { + font-weight: bold; + color: #FF6347; +} + +.farm-stats .stat-item { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + padding: 5px 0; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} + +.farm-stats .stat-item:last-child { + border-bottom: none; +} + +.stat-label { + font-weight: bold; + color: #2c3e50; +} + +.stat-value { + font-weight: bold; + color: #228B22; +} + +.game-controls { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; +} + +.game-controls button { + background: linear-gradient(135deg, #4169E1, #6495ED); + color: white; + border: none; + padding: 10px 12px; + border-radius: 8px; + font-size: 0.9em; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.game-controls button:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.notifications { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; + max-width: 300px; +} + +.notification { + background: rgba(255, 255, 255, 0.95); + border-radius: 10px; + padding: 15px; + margin-bottom: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + border-left: 4px solid #228B22; + animation: slideIn 0.5s ease-out; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.notification.success { border-left-color: #228B22; } +.notification.warning { border-left-color: #FFD700; } +.notification.error { border-left-color: #FF6347; } + +.instructions { + background: rgba(255, 255, 255, 0.9); + padding: 20px; + border-radius: 15px; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.instructions h3 { + color: #228B22; + margin-bottom: 15px; +} + +.instructions ul { + list-style: none; + padding: 0; +} + +.instructions li { + margin-bottom: 8px; + padding-left: 20px; + position: relative; +} + +.instructions li:before { + content: "๐ŸŒพ"; + position: absolute; + left: 0; + color: #FFD700; +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .game-area { + grid-template-columns: 1fr; + gap: 15px; + } + + .farm-grid { + grid-template-columns: repeat(12, 1fr); + grid-template-rows: repeat(8, 1fr); + } +} + +@media (max-width: 768px) { + .season-weather { + grid-template-columns: 1fr; + } + + .action-buttons { + grid-template-columns: 1fr; + } + + .inventory-grid { + grid-template-columns: 1fr; + } + + .game-controls { + grid-template-columns: 1fr; + } + + header h1 { + font-size: 2em; + } +} + +@media (max-width: 480px) { + .farm-grid { + grid-template-columns: repeat(10, 1fr); + grid-template-rows: repeat(8, 1fr); + } + + .action-buttons { + grid-template-columns: repeat(2, 1fr); + } + + .game-controls { + grid-template-columns: repeat(2, 1fr); + } + + .game-controls button:last-child { + grid-column: span 2; + } +} \ No newline at end of file diff --git a/games/fast-finger-maze/index.html b/games/fast-finger-maze/index.html new file mode 100644 index 00000000..c8228e29 --- /dev/null +++ b/games/fast-finger-maze/index.html @@ -0,0 +1,26 @@ + + + + + + Fast Finger Maze | Mini JS Games Hub + + + +
+

Fast Finger Maze

+

Use arrow keys โฌ†๏ธโฌ‡๏ธโฌ…๏ธโžก๏ธ to navigate the maze before the timer ends!

+
+
+ Time Left: 30s + Moves: 0 +
+
+ + +
+

+
+ + + diff --git a/games/fast-finger-maze/script.js b/games/fast-finger-maze/script.js new file mode 100644 index 00000000..2b641940 --- /dev/null +++ b/games/fast-finger-maze/script.js @@ -0,0 +1,87 @@ +const gridSize = 5; +let playerPos = { x: 0, y: 0 }; +let endPos = { x: gridSize - 1, y: gridSize - 1 }; +let timer = 30; +let timerInterval; +let moves = 0; + +const mazeGrid = document.getElementById("maze-grid"); +const timerEl = document.getElementById("timer"); +const movesEl = document.getElementById("moves"); +const messageEl = document.getElementById("message"); +const startBtn = document.getElementById("start-btn"); +const resetBtn = document.getElementById("reset-btn"); + +function createMaze() { + mazeGrid.innerHTML = ""; + for(let y=0; y cell.classList.remove("player")); + const index = playerPos.y * gridSize + playerPos.x; + mazeGrid.children[index].classList.add("player"); +} + +function startGame() { + playerPos = { x: 0, y: 0 }; + moves = 0; + timer = 30; + timerEl.textContent = timer; + movesEl.textContent = moves; + messageEl.textContent = ""; + createMaze(); + + clearInterval(timerInterval); + timerInterval = setInterval(() => { + timer--; + timerEl.textContent = timer; + if(timer <= 0){ + messageEl.textContent = "โฐ Time's up! Try again."; + clearInterval(timerInterval); + } + }, 1000); +} + +function resetGame() { + clearInterval(timerInterval); + startGame(); +} + +function handleMove(e) { + if(timer <= 0) return; + + const key = e.key; + if(key === "ArrowUp" && playerPos.y > 0) playerPos.y--; + if(key === "ArrowDown" && playerPos.y < gridSize - 1) playerPos.y++; + if(key === "ArrowLeft" && playerPos.x > 0) playerPos.x--; + if(key === "ArrowRight" && playerPos.x < gridSize - 1) playerPos.x++; + moves++; + movesEl.textContent = moves; + renderPlayer(); + checkWin(); +} + +function checkWin() { + if(playerPos.x === endPos.x && playerPos.y === endPos.y){ + messageEl.textContent = "๐ŸŽ‰ You reached the goal! Well done!"; + clearInterval(timerInterval); + } +} + +// Event listeners +startBtn.addEventListener("click", startGame); +resetBtn.addEventListener("click", resetGame); +window.addEventListener("keydown", handleMove); + +// Initialize maze +createMaze(); diff --git a/games/fast-finger-maze/style.css b/games/fast-finger-maze/style.css new file mode 100644 index 00000000..1d5aafbc --- /dev/null +++ b/games/fast-finger-maze/style.css @@ -0,0 +1,91 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #74ebd5, #acb6e5); + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; +} + +.maze-container { + background: rgba(255,255,255,0.95); + padding: 30px; + border-radius: 15px; + text-align: center; + box-shadow: 0 8px 20px rgba(0,0,0,0.2); + width: 350px; +} + +h1 { + margin-bottom: 10px; + color: #333; +} + +.instructions { + font-size: 14px; + color: #555; +} + +#maze-grid { + display: grid; + grid-template-columns: repeat(5, 50px); + grid-template-rows: repeat(5, 50px); + gap: 4px; + margin: 20px auto; +} + +.cell { + width: 50px; + height: 50px; + background: #e0e0e0; + border-radius: 5px; + display: flex; + justify-content: center; + align-items: center; +} + +.cell.start { + background: #76c7c0; +} + +.cell.end { + background: #f28b82; +} + +.cell.player { + background: #ffd600; +} + +.game-info { + display: flex; + justify-content: space-between; + margin-top: 10px; + font-weight: bold; +} + +.controls { + margin-top: 15px; +} + +button { + padding: 8px 15px; + margin: 5px; + border: none; + border-radius: 6px; + background-color: #76c7c0; + color: white; + font-weight: bold; + cursor: pointer; + transition: all 0.2s ease; +} + +button:hover { + background-color: #4aa6a1; +} + +#message { + margin-top: 15px; + font-weight: bold; + color: #e53935; +} diff --git a/games/find-hidden-object/index.html b/games/find-hidden-object/index.html new file mode 100644 index 00000000..37993a87 --- /dev/null +++ b/games/find-hidden-object/index.html @@ -0,0 +1,45 @@ + + + + + + Find the Hidden Object | Mini JS Games Hub + + + +
+
+

๐Ÿ”Ž Find the Hidden Object

+
+
โฑ Time: 60s
+
๐Ÿ† Score: 0
+
โญ Level: 1
+
+
+ +
+
+ Hidden Object Scene + +
+
    +
    + +
    + + + +
    +
    + + + + + + diff --git a/games/find-hidden-object/levels/level1.json b/games/find-hidden-object/levels/level1.json new file mode 100644 index 00000000..dd57ade0 --- /dev/null +++ b/games/find-hidden-object/levels/level1.json @@ -0,0 +1,5 @@ +[ + { "id": 1, "name": "Red Hat", "x": 180, "y": 150, "width": 40, "height": 40, "found": false }, + { "id": 2, "name": "Cup", "x": 300, "y": 270, "width": 30, "height": 30, "found": false }, + { "id": 3, "name": "Book", "x": 520, "y": 220, "width": 50, "height": 40, "found": false } +] diff --git a/games/find-hidden-object/levels/level2.json b/games/find-hidden-object/levels/level2.json new file mode 100644 index 00000000..9f89246a --- /dev/null +++ b/games/find-hidden-object/levels/level2.json @@ -0,0 +1,6 @@ +[ + { "id": 1, "name": "Blue Ball", "x": 150, "y": 200, "width": 30, "height": 30, "found": false }, + { "id": 2, "name": "Watch", "x": 370, "y": 300, "width": 35, "height": 35, "found": false }, + { "id": 3, "name": "Key", "x": 580, "y": 250, "width": 30, "height": 30, "found": false }, + { "id": 4, "name": "Spoon", "x": 420, "y": 120, "width": 25, "height": 25, "found": false } +] diff --git a/games/find-hidden-object/script.js b/games/find-hidden-object/script.js new file mode 100644 index 00000000..31c939c0 --- /dev/null +++ b/games/find-hidden-object/script.js @@ -0,0 +1,130 @@ +let level = 1; +let timer = 60; +let score = 0; +let items = []; +let foundCount = 0; +let hintUsed = false; +let timerInterval; + +const timerEl = document.getElementById("timer"); +const scoreEl = document.getElementById("score"); +const levelEl = document.getElementById("level"); +const itemListEl = document.getElementById("item-list"); +const sceneCanvas = document.getElementById("scene-canvas"); +const ctx = sceneCanvas.getContext("2d"); + +const overlay = document.getElementById("game-over"); +const resultTitle = document.getElementById("result-title"); +const resultText = document.getElementById("result-text"); +const nextBtn = document.getElementById("next-btn"); +const restartBtn = document.getElementById("restart-btn"); +const hintBtn = document.getElementById("hint-btn"); + +async function loadLevel(levelNumber) { + const res = await fetch(`levels/level${levelNumber}.json`); + items = await res.json(); + + const img = document.getElementById("scene-img"); + img.onload = () => { + sceneCanvas.width = img.clientWidth; + sceneCanvas.height = img.clientHeight; + }; + + renderItemList(); + resetTimer(); +} + +function renderItemList() { + itemListEl.innerHTML = ""; + items.forEach((item) => { + const li = document.createElement("li"); + li.textContent = item.name; + li.dataset.id = item.id; + itemListEl.appendChild(li); + }); +} + +function resetTimer() { + clearInterval(timerInterval); + timer = 60; + timerEl.textContent = timer; + timerInterval = setInterval(() => { + timer--; + timerEl.textContent = timer; + if (timer <= 0) endGame(false); + }, 1000); +} + +sceneCanvas.addEventListener("click", (e) => { + const rect = sceneCanvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + items.forEach((item) => { + if (!item.found && x >= item.x && x <= item.x + item.width && y >= item.y && y <= item.y + item.height) { + item.found = true; + score += 100; + foundCount++; + scoreEl.textContent = score; + markItemFound(item.id); + flashArea(item.x, item.y, item.width, item.height); + if (foundCount === items.length) endGame(true); + } + }); +}); + +function markItemFound(id) { + const li = itemListEl.querySelector(`[data-id="${id}"]`); + if (li) li.classList.add("found"); +} + +function flashArea(x, y, w, h) { + ctx.strokeStyle = "#00ff00"; + ctx.lineWidth = 4; + ctx.strokeRect(x, y, w, h); + setTimeout(() => { + ctx.clearRect(0, 0, sceneCanvas.width, sceneCanvas.height); + }, 500); +} + +function endGame(win) { + clearInterval(timerInterval); + overlay.classList.remove("hidden"); + if (win) { + resultTitle.textContent = "๐ŸŽ‰ You Found Them All!"; + resultText.textContent = `Your score: ${score}`; + nextBtn.hidden = level >= 2; + } else { + resultTitle.textContent = "โฐ Time's Up!"; + resultText.textContent = `Try again to find all items.`; + nextBtn.hidden = true; + } +} + +hintBtn.addEventListener("click", () => { + if (hintUsed) return; + const remaining = items.find((item) => !item.found); + if (remaining) { + flashArea(remaining.x, remaining.y, remaining.width, remaining.height); + hintUsed = true; + score -= 50; + scoreEl.textContent = score; + } +}); + +restartBtn.addEventListener("click", () => { + location.reload(); +}); + +nextBtn.addEventListener("click", () => { + level++; + levelEl.textContent = level; + overlay.classList.add("hidden"); + foundCount = 0; + hintUsed = false; + loadLevel(level); +}); + +document.getElementById("play-again").addEventListener("click", () => location.reload()); + +loadLevel(level); diff --git a/games/find-hidden-object/style.css b/games/find-hidden-object/style.css new file mode 100644 index 00000000..3ad77712 --- /dev/null +++ b/games/find-hidden-object/style.css @@ -0,0 +1,122 @@ +* { + box-sizing: border-box; +} + +body { + font-family: "Poppins", sans-serif; + margin: 0; + padding: 0; + background: linear-gradient(135deg, #89f7fe, #66a6ff); + color: #222; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + background: #fff; + border-radius: 20px; + padding: 20px; + width: 90%; + max-width: 900px; + box-shadow: 0 10px 30px rgba(0,0,0,0.2); + display: flex; + flex-direction: column; + align-items: center; +} + +header { + text-align: center; + margin-bottom: 10px; +} + +.game-info { + display: flex; + justify-content: center; + gap: 20px; + font-weight: bold; +} + +.scene { + position: relative; + width: 100%; + border-radius: 15px; + overflow: hidden; +} + +#scene-img { + width: 100%; + border-radius: 15px; + display: block; +} + +#scene-canvas { + position: absolute; + top: 0; + left: 0; +} + +#item-list { + list-style: none; + display: flex; + flex-wrap: wrap; + gap: 10px; + margin: 15px 0; + padding: 0; + justify-content: center; +} + +#item-list li { + background: #eaf6ff; + padding: 8px 15px; + border-radius: 20px; + border: 1px solid #bcdcf5; + transition: 0.3s; +} + +#item-list li.found { + background: #b5ffba; + text-decoration: line-through; +} + +footer { + display: flex; + gap: 10px; +} + +button { + background: #007bff; + color: white; + border: none; + border-radius: 10px; + padding: 8px 16px; + cursor: pointer; + transition: 0.3s; +} + +button:hover { + background: #0056b3; +} + +.overlay { + position: fixed; + top: 0; left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.hidden { + display: none; +} + +.overlay-content { + background: white; + padding: 30px 50px; + border-radius: 15px; + text-align: center; +} diff --git a/games/fire-spark/index.html b/games/fire-spark/index.html new file mode 100644 index 00000000..210b3ddb --- /dev/null +++ b/games/fire-spark/index.html @@ -0,0 +1,83 @@ + + + + + + Fire Spark โ€” Mini JS Games Hub + + + + + +
    +
    +
    + ๐Ÿ”ฅ +

    Fire Spark

    +
    + +
    +
    +
    Score: 0
    +
    Best: 0
    +
    + +
    + + + + +
    +
    +
    + +
    + + + +
    + + +
    + + + + + + + + + diff --git a/games/fire-spark/script.js b/games/fire-spark/script.js new file mode 100644 index 00000000..5ee24846 --- /dev/null +++ b/games/fire-spark/script.js @@ -0,0 +1,480 @@ +/* Fire Spark โ€” advanced canvas game with physics, wind, obstacles, touch/mouse tracking, sounds */ + +(() => { + // Canvas & DOM + const canvas = document.getElementById('gameCanvas'); + const ctx = canvas.getContext('2d', { alpha: true }); + + // Set size to CSS pixels * devicePixelRatio for crispness + function resize() { + const rect = canvas.getBoundingClientRect(); + canvas.width = Math.floor(rect.width * devicePixelRatio); + canvas.height = Math.floor(rect.height * devicePixelRatio); + ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0); + } + window.addEventListener('resize', resize); + resize(); + + const startBtn = document.getElementById('startBtn'); + const pauseBtn = document.getElementById('pauseBtn'); + const restartBtn = document.getElementById('restartBtn'); + const muteToggle = document.getElementById('muteToggle'); + + const scoreEl = document.getElementById('score'); + const bestEl = document.getElementById('best'); + const heatBar = document.getElementById('heatBar'); + const windIndicator = document.getElementById('windIndicator'); + const statusEl = document.getElementById('status'); + + const soundStart = document.getElementById('soundStart'); + const soundGust = document.getElementById('soundGust'); + const soundFail = document.getElementById('soundFail'); + + // Audio: fallback/supplement using WebAudio for continuous subtle sizzle + const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + let sizzleGain = audioCtx.createGain(); + sizzleGain.gain.value = 0; + sizzleGain.connect(audioCtx.destination); + let sizzleOsc = null; + function startSizzle() { + if (sizzleOsc) return; + sizzleOsc = audioCtx.createOscillator(); + const lfo = audioCtx.createOscillator(); + lfo.frequency.value = 2.2; + const lfoGain = audioCtx.createGain(); + lfoGain.gain.value = 0.08; + lfo.connect(lfoGain); + lfoGain.connect(sizzleGain.gain); + sizzleOsc.type = 'triangle'; + sizzleOsc.frequency.value = 240; + sizzleOsc.connect(sizzleGain); + sizzleOsc.start(); + lfo.start(); + } + function stopSizzle() { + if (!sizzleOsc) return; + try { sizzleOsc.stop(); } catch {} + sizzleOsc = null; + } + + function playSound(el) { + if (muteToggle.checked) return; + if (!el) return; + // audio context resume on first user gesture + if (audioCtx.state === 'suspended') audioCtx.resume(); + el.currentTime = 0; + el.volume = 0.9; + el.play().catch(() => {}); + } + + // Game state + let running = false; + let paused = false; + let last = 0; + let score = 0; + let best = parseInt(localStorage.getItem('fireSpark_best') || '0', 10) || 0; + bestEl.textContent = best; + let heat = 100; // 0-100 + let wind = 0; // -1 .. 1 scaled later + let windTimer = 0; + let gusting = false; + + // Player (spark) properties + const spark = { + x: canvas.width / (2 * devicePixelRatio), + y: canvas.height / (2 * devicePixelRatio), + vx: 0, + vy: 0, + radius: 18, + maxSpeed: 800, + }; + + // Input tracking + const input = { x: spark.x, y: spark.y, active: false }; + + function setInputFromEvent(e) { + const rect = canvas.getBoundingClientRect(); + const cx = ((e.touches && e.touches[0]) ? e.touches[0].clientX : e.clientX) - rect.left; + const cy = ((e.touches && e.touches[0]) ? e.touches[0].clientY : e.clientY) - rect.top; + input.x = cx; + input.y = cy; + input.active = true; + } + canvas.addEventListener('mousemove', setInputFromEvent); + canvas.addEventListener('touchstart', setInputFromEvent, { passive: true }); + canvas.addEventListener('touchmove', setInputFromEvent, { passive: true }); + canvas.addEventListener('mouseleave', () => input.active = false); + canvas.addEventListener('touchend', () => input.active = false); + + // Obstacles (cold gusts) array + let obstacles = []; + function spawnObstacle() { + const w = canvas.width / devicePixelRatio; + const h = canvas.height / devicePixelRatio; + const size = 26 + Math.random() * 40; + const y = 60 + Math.random() * (h - 120); + const fromLeft = Math.random() > 0.5; + const speed = 80 + Math.random() * 160; + obstacles.push({ + x: fromLeft ? -size : w + size, + y, + r: size, + vx: (fromLeft ? 1 : -1) * speed, + life: 8 + Math.random() * 10 + }); + } + + // Wind dynamics + function maybeChangeWind(dt) { + windTimer -= dt; + if (windTimer <= 0) { + // change wind every 2-6 seconds + windTimer = 2 + Math.random() * 4; + // new wind + const prev = wind; + wind = (Math.random() - 0.5) * 2; // -1..1 + gusting = Math.abs(wind - prev) > 0.6; + if (gusting) playSound(soundGust); + } + } + + // Update + function update(ts) { + if (!running || paused) { last = ts; requestAnimationFrame(update); return; } + const dt = Math.min(0.045, (ts - last) / 1000); + last = ts; + + maybeChangeWind(dt); + + // apply wind acceleration + const windAccel = wind * 140; // pixels/s^2 + spark.vx += windAccel * dt; + + // input attraction (mouse) + if (input.active) { + const dx = input.x - spark.x; + const dy = input.y - spark.y; + const dist = Math.max(10, Math.hypot(dx, dy)); + const attraction = 9000 / (dist * dist); // stronger when close + spark.vx += (dx / dist) * attraction * dt; + spark.vy += (dy / dist) * attraction * dt; + // regenerate heat when near cursor + const near = dist < 120; + if (near) heat = Math.min(100, heat + 26 * dt); + } else { + // small cooling when idle + heat = Math.max(0, heat - 6 * dt); + } + + // ambient cooling and wind negative effect + heat = Math.max(0, heat - (6 + Math.abs(wind) * 8) * dt); + + // obstacles interactions + obstacles.forEach((ob, i) => { + ob.x += ob.vx * dt; + ob.life -= dt; + // collision with spark + const dx = ob.x - spark.x; + const dy = ob.y - spark.y; + const dist = Math.hypot(dx, dy); + if (dist < ob.r + spark.radius) { + // sharp cooling hit + heat -= 22 + Math.random() * 18; + // push spark away + spark.vx -= (dx / dist) * 220; + spark.vy -= (dy / dist) * 120; + ob.life = -1; + playSound(soundGust); + } + }); + // remove dead obstacles + obstacles = obstacles.filter(o => o.life > 0 && ((o.vx > 0 && o.x < canvas.width / devicePixelRatio + 200) || (o.vx < 0 && o.x > -200))); + + // occasional spawn + if (Math.random() < dt * 0.9) { + if (obstacles.length < 6 && Math.random() < 0.28) spawnObstacle(); + } + + // velocity damping + spark.vx *= Math.pow(0.92, dt * 60); + spark.vy = Math.min(1000, spark.vy + 420 * dt); // gentle gravity-ish effect + spark.vy *= Math.pow(0.94, dt * 60); + + // clamp speed + const spd = Math.hypot(spark.vx, spark.vy); + if (spd > spark.maxSpeed) { + spark.vx = (spark.vx / spd) * spark.maxSpeed; + spark.vy = (spark.vy / spd) * spark.maxSpeed; + } + + // movement + spark.x += spark.vx * dt; + spark.y += spark.vy * dt; + + // bounds; bounce gently + const w = canvas.width / devicePixelRatio; + const h = canvas.height / devicePixelRatio; + if (spark.x < spark.radius) { spark.x = spark.radius; spark.vx *= -0.45; } + if (spark.x > w - spark.radius) { spark.x = w - spark.radius; spark.vx *= -0.45; } + if (spark.y < spark.radius) { spark.y = spark.radius; spark.vy *= -0.45; } + if (spark.y > h - spark.radius) { spark.y = h - spark.radius; spark.vy *= -0.45; } + + // scoring + score += dt * (0.8 + heat / 120); + scoreEl.textContent = Math.floor(score); + + // update UI + heatBar.style.width = `${Math.max(0, heat)}%`; + const windText = (wind > 0.1) ? `โ†’ ${wind.toFixed(2)}` : (wind < -0.1) ? `โ† ${Math.abs(wind).toFixed(2)}` : `โ†” 0`; + windIndicator.textContent = windText; + statusEl.textContent = running ? (paused ? 'Paused' : 'Running') : 'Idle'; + + // update sizzle volume based on heat + try { + startSizzle(); + sizzleGain.gain.linearRampToValueAtTime((heat / 140) * (muteToggle.checked ? 0 : 0.8), audioCtx.currentTime + 0.06); + } catch {} + + // check fail + if (heat <= 0) { + onFail(); + return; + } + + // draw + drawFrame(); + requestAnimationFrame(update); + } + + // Draw spark with glow + function drawFrame() { + const w = canvas.width / devicePixelRatio; + const h = canvas.height / devicePixelRatio; + ctx.clearRect(0, 0, w, h); + + // subtle background vignette + const g = ctx.createLinearGradient(0, 0, 0, h); + g.addColorStop(0, 'rgba(0,0,0,0.08)'); + g.addColorStop(1, 'rgba(0,0,0,0.18)'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, w, h); + + // draw obstacles (cold gusts) + obstacles.forEach((o) => { + ctx.save(); + ctx.beginPath(); + const grd = ctx.createRadialGradient(o.x, o.y, o.r * 0.1, o.x, o.y, o.r); + grd.addColorStop(0, 'rgba(100,180,255,0.85)'); + grd.addColorStop(1, 'rgba(30,70,120,0.08)'); + ctx.fillStyle = grd; + ctx.shadowColor = 'rgba(60,150,255,0.18)'; + ctx.shadowBlur = 26; + ctx.arc(o.x, o.y, o.r, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + }); + + // flame core + const coreR = spark.radius * 0.7; + const x = spark.x; + const y = spark.y; + + // outer glow grows with heat + const glow = 26 + heat * 0.9; + ctx.save(); + ctx.beginPath(); + const glowGrd = ctx.createRadialGradient(x, y, coreR * 0.2, x, y, coreR + glow); + glowGrd.addColorStop(0, `rgba(255,220,120,${Math.min(1, 0.9 + heat / 140)})`); + glowGrd.addColorStop(0.4, 'rgba(255,110,55,0.18)'); + glowGrd.addColorStop(1, 'rgba(10,10,10,0)'); + ctx.fillStyle = glowGrd; + ctx.shadowBlur = Math.max(12, glow * 0.6); + ctx.shadowColor = 'rgba(255,120,50,0.25)'; + ctx.arc(x, y, coreR + glow, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + + // inner core gradient + ctx.save(); + ctx.beginPath(); + const coreGrd = ctx.createRadialGradient(x, y, 1, x, y, coreR); + coreGrd.addColorStop(0, 'rgba(255,255,200,1)'); + coreGrd.addColorStop(0.3, 'rgba(255,200,110,1)'); + coreGrd.addColorStop(0.6, 'rgba(255,110,55,0.95)'); + coreGrd.addColorStop(1, 'rgba(220,40,30,0.8)'); + ctx.fillStyle = coreGrd; + ctx.shadowColor = 'rgba(255,120,60,0.18)'; + ctx.shadowBlur = 8; + ctx.arc(x, y - coreR * 0.18, coreR, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + + // flicker tiny sparks + for (let i = 0; i < 6; i++) { + const angle = Math.random() * Math.PI * 2; + const r = coreR + Math.random() * (coreR * 1.4); + const sx = x + Math.cos(angle) * r * 0.6; + const sy = y + Math.sin(angle) * r * 0.5; + ctx.save(); + ctx.globalAlpha = Math.random() * 0.7 + 0.05; + ctx.beginPath(); + ctx.fillStyle = `rgba(255,${160 + Math.floor(Math.random() * 90)},${40 + Math.floor(Math.random() * 60)},1)`; + ctx.arc(sx, sy, Math.random() * 2.8 + 0.6, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + } + + // tiny smoke trail when low heat + if (heat < 40) { + for (let i = 0; i < 6; i++) { + ctx.save(); + ctx.globalAlpha = Math.random() * 0.14 + 0.02; + ctx.fillStyle = `rgba(80,80,90,1)`; + const sx = x + Math.random() * 24 - 12; + const sy = y - coreR - (Math.random() * 40); + ctx.beginPath(); + ctx.arc(sx, sy, Math.random() * 8 + 2, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + } + } + } + + // Fail handler + function onFail() { + running = false; + paused = false; + stopSizzle(); + playSound(soundFail); + statusEl.textContent = 'Extinguished ๐Ÿ’€'; + // update best + const final = Math.floor(score); + if (final > best) { + best = final; + localStorage.setItem('fireSpark_best', best); + bestEl.textContent = best; + } + // flash canvas + const orig = canvas.style.boxShadow; + canvas.style.transition = 'box-shadow 0.4s'; + canvas.style.boxShadow = '0 0 36px 10px rgba(255,40,20,0.8)'; + setTimeout(() => { canvas.style.boxShadow = orig; }, 450); + + // reset basic state (but allow restart) + } + + // controls + startBtn.addEventListener('click', () => { + if (!running) { + startGame(); + } else if (paused) { + paused = false; + statusEl.textContent = 'Running'; + requestAnimationFrame(update); + } + playSound(soundStart); + }); + + pauseBtn.addEventListener('click', () => { + paused = !paused; + statusEl.textContent = paused ? 'Paused' : 'Running'; + if (!paused) { + last = performance.now(); + requestAnimationFrame(update); + } else { + // quieter + sizzleGain.gain.linearRampToValueAtTime(0.0, audioCtx.currentTime + 0.06); + } + }); + + restartBtn.addEventListener('click', () => { + stopSizzle(); + resetGame(); + startGame(); + playSound(soundStart); + }); + + muteToggle.addEventListener('change', () => { + if (muteToggle.checked) { + sizzleGain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.06); + } + }); + + // start logic + function startGame() { + running = true; + paused = false; + last = performance.now(); + score = 0; + heat = 95 + Math.random() * 5; + spark.x = (canvas.width / devicePixelRatio) / 2; + spark.y = (canvas.height / devicePixelRatio) / 2; + spark.vx = 0; spark.vy = 0; + obstacles = []; + wind = 0; + windTimer = 1.2; + statusEl.textContent = 'Running'; + playSound(soundStart); + requestAnimationFrame(update); + } + + function resetGame() { + running = false; + paused = false; + score = 0; + heat = 100; + heatBar.style.width = '100%'; + scoreEl.textContent = 0; + statusEl.textContent = 'Idle'; + obstacles = []; + } + + // initial state + resetGame(); + + // open in new tab anchor functionality + document.getElementById('open-tab').addEventListener('click', (e) => { + e.preventDefault(); + window.open(window.location.href, '_blank'); + }); + + // Friendly autopause when tab hidden + document.addEventListener('visibilitychange', () => { + if (document.hidden && running && !paused) { + paused = true; + statusEl.textContent = 'Paused (background)'; + } + }); + + // Quick keyboard controls + window.addEventListener('keydown', (e) => { + if (e.key === ' ' || e.key === 'Spacebar') { + e.preventDefault(); + if (!running) startGame(); + else paused = !paused; + } else if (e.key.toLowerCase() === 'r') { + restartBtn.click(); + } + }); + + // tiny autoplay resume on first user gesture (for AudioContext) + window.addEventListener('pointerdown', () => { + if (audioCtx.state === 'suspended') audioCtx.resume(); + }, { once: true }); + + // click canvas to nudge spark + canvas.addEventListener('click', (e) => { + const rect = canvas.getBoundingClientRect(); + spark.vx += (e.clientX - rect.left - spark.x) * 0.08; + spark.vy += (e.clientY - rect.top - spark.y) * 0.06; + }); + + // make sure UI reflects score periodically + setInterval(() => { + scoreEl.textContent = Math.floor(score); + }, 300); + + // ensure best displayed + bestEl.textContent = best; + +})(); diff --git a/games/fire-spark/style.css b/games/fire-spark/style.css new file mode 100644 index 00000000..b27ff0e0 --- /dev/null +++ b/games/fire-spark/style.css @@ -0,0 +1,129 @@ +:root{ + --bg:#0b0f17; + --panel:#0f1620; + --accent:#ff6b35; + --muted:#9aa6b2; + --glass: rgba(255,255,255,0.04); +} + +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial; + background: radial-gradient(1200px 600px at 10% 10%, rgba(255,120,40,0.06), transparent 6%), + radial-gradient(900px 400px at 90% 90%, rgba(40,160,255,0.02), transparent 6%), + var(--bg); + color:#e6eef6; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + display:flex; + align-items:stretch; + min-height:100vh; +} + +/* layout */ +.ui-wrap{ + margin:auto; + width:min(1200px,96%); + background:linear-gradient(180deg, rgba(255,255,255,0.02), transparent 30%); + border-radius:14px; + box-shadow: 0 10px 40px rgba(2,6,23,0.6); + overflow:hidden; +} + +.topbar{ + display:flex; + justify-content:space-between; + align-items:center; + padding:18px 22px; + gap:16px; + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border-bottom:1px solid rgba(255,255,255,0.03); +} + +.title{display:flex;align-items:center;gap:12px} +.title .icon{font-size:26px;filter:drop-shadow(0 6px 12px rgba(255,107,53,0.18))} +.title h1{font-size:18px;margin:0;font-weight:600} + +.controls{display:flex;gap:12px;align-items:center} +.score-group{font-weight:600;color:var(--muted);display:flex;gap:10px;align-items:center} +.score-group span{color:#fff} +.buttons{display:flex;gap:8px;align-items:center} + +.btn{ + background:var(--glass); + border:1px solid rgba(255,255,255,0.04); + color:#e9f3ff; + padding:8px 12px; + border-radius:8px; + cursor:pointer; + font-weight:600; + transition:transform .12s ease, box-shadow .12s; +} +.btn:hover{transform:translateY(-2px);box-shadow:0 8px 18px rgba(2,6,23,0.35)} +.btn.primary{background:linear-gradient(90deg,var(--accent),#ff9a5a); color:#08101a; border:none; box-shadow:0 8px 26px rgba(255,107,53,0.12)} + +.volume input{display:none} + +/* main area */ +.game-area{ + display:grid; + grid-template-columns: 1fr 320px; + gap:18px; + padding:18px; + align-items:stretch; +} + +#gameCanvas{ + width:100%; + height:640px; + display:block; + border-radius:12px; + background-image: + linear-gradient(180deg, rgba(255,255,255,0.015), transparent 10%), + url('https://source.unsplash.com/1600x900/?embers,fire'); + background-size:cover; + background-position:center; + box-shadow: inset 0 0 140px rgba(255,120,40,0.03), 0 20px 60px rgba(2,6,23,0.6); + border:1px solid rgba(255,255,255,0.03); + position:relative; +} + +/* side panel */ +.side-panel{ + padding:12px; + background: linear-gradient(180deg, rgba(255,255,255,0.02), transparent); + border-radius:12px; + border:1px solid rgba(255,255,255,0.03); + height:640px; + overflow:auto; +} + +.stat{margin-bottom:12px} +.label{font-size:13px;color:var(--muted);margin-bottom:6px} +.meter{height:14px;border-radius:999px;background:rgba(255,255,255,0.03);padding:3px} +.meter-fill{height:100%;border-radius:999px;background:linear-gradient(90deg,#ffef8a,var(--accent));width:20%;box-shadow:0 6px 18px rgba(255,107,53,0.12), 0 0 24px rgba(255,107,53,0.08) inset;transition:width .18s ease} + +.wind{font-weight:700;color:#cfeaff} +#status{font-weight:700;color:#f7e4d0} + +.tips ul{padding-left:16px;margin:6px 0;color:var(--muted);line-height:1.35;font-size:13px} + +.footer{ + display:flex; + justify-content:space-between; + align-items:center; + padding:12px 18px; + border-top:1px solid rgba(255,255,255,0.02); + background:linear-gradient(0deg, rgba(255,255,255,0.01), transparent); +} +.footer .links a{margin-left:14px;color:var(--muted);text-decoration:none;font-weight:600} +.footer .links a.play-button{background:transparent;border:1px solid rgba(255,255,255,0.03);padding:6px 10px;border-radius:8px;color:#fff} + +/* responsive */ +@media (max-width:1024px){ + .game-area{grid-template-columns:1fr} + #gameCanvas{height:520px} + .side-panel{height:auto} +} diff --git a/games/firefly-catcher/index.html b/games/firefly-catcher/index.html new file mode 100644 index 00000000..f3ec0e52 --- /dev/null +++ b/games/firefly-catcher/index.html @@ -0,0 +1,33 @@ + + + + + + Firefly Catcher | Mini JS Games Hub + + + +
    +

    Firefly Catcher

    +

    Score: 0

    +

    Time Left: 30s

    + +
    + + + +
    + +
    + + + +
    + + + + diff --git a/games/firefly-catcher/script.js b/games/firefly-catcher/script.js new file mode 100644 index 00000000..92ec5e72 --- /dev/null +++ b/games/firefly-catcher/script.js @@ -0,0 +1,101 @@ +const gameArea = document.getElementById('game-area'); +const scoreEl = document.getElementById('score'); +const timeEl = document.getElementById('time'); +const startBtn = document.getElementById('start-btn'); +const pauseBtn = document.getElementById('pause-btn'); +const restartBtn = document.getElementById('restart-btn'); + +const bgSound = document.getElementById('bg-sound'); +const catchSound = document.getElementById('catch-sound'); + +let score = 0; +let timeLeft = 30; +let interval; +let spawnInterval; +let paused = false; + +function createFirefly() { + const firefly = document.createElement('div'); + firefly.classList.add('firefly'); + + const x = Math.random() * (gameArea.clientWidth - 40); + const y = Math.random() * (gameArea.clientHeight - 40); + firefly.style.left = x + 'px'; + firefly.style.top = y + 'px'; + + // random glow animation speed + firefly.style.animationDuration = (Math.random() * 3 + 2) + 's'; + + firefly.addEventListener('click', () => { + if (!paused) { + score++; + scoreEl.textContent = score; + catchSound.currentTime = 0; + catchSound.play(); + gameArea.removeChild(firefly); + } + }); + + gameArea.appendChild(firefly); + + // remove firefly after 5-7 seconds if not clicked + setTimeout(() => { + if (gameArea.contains(firefly)) { + gameArea.removeChild(firefly); + } + }, 5000 + Math.random() * 2000); +} + +function startGame() { + if (!paused) { + bgSound.play(); + } + score = 0; + timeLeft = 30; + scoreEl.textContent = score; + timeEl.textContent = timeLeft; + + clearInterval(interval); + clearInterval(spawnInterval); + + interval = setInterval(() => { + if (!paused) { + timeLeft--; + timeEl.textContent = timeLeft; + if (timeLeft <= 0) endGame(); + } + }, 1000); + + spawnInterval = setInterval(() => { + if (!paused) createFirefly(); + }, 800); +} + +function pauseGame() { + paused = !paused; + if (!paused) { + bgSound.play(); + startBtn.textContent = 'Resume'; + } else { + bgSound.pause(); + } +} + +function restartGame() { + paused = false; + bgSound.currentTime = 0; + bgSound.play(); + startGame(); +} + +function endGame() { + clearInterval(interval); + clearInterval(spawnInterval); + bgSound.pause(); + alert(`Game Over! Your score: ${score}`); + gameArea.innerHTML = ''; +} + +startBtn.addEventListener('click', startGame); +pauseBtn.addEventListener('click', pauseGame); +restartBtn.addEventListener('click', restartGame); diff --git a/games/firefly-catcher/style.css b/games/firefly-catcher/style.css new file mode 100644 index 00000000..9b714383 --- /dev/null +++ b/games/firefly-catcher/style.css @@ -0,0 +1,72 @@ +body { + font-family: 'Arial', sans-serif; + background: radial-gradient(circle at bottom, #0b0c1c, #1a1a2e); + color: #fff; + margin: 0; + padding: 0; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + position: relative; + width: 90%; + max-width: 800px; + text-align: center; +} + +#game-area { + position: relative; + width: 100%; + height: 500px; + border: 2px solid #fff; + border-radius: 15px; + overflow: hidden; + background: radial-gradient(circle at center, #0b0c1c, #1a1a2e); + margin-top: 20px; +} + +.firefly { + position: absolute; + width: 40px; + height: 40px; + background: url('https://i.postimg.cc/pd7kb06X/firefly.png') no-repeat center/contain; + cursor: pointer; + filter: drop-shadow(0 0 20px yellow); + animation: float linear infinite; +} + +@keyframes float { + 0% { transform: translate(0,0); } + 50% { transform: translate(10px, 10px); } + 100% { transform: translate(-10px, -10px); } +} + +.controls { + margin: 15px 0; +} + +.controls button { + padding: 8px 15px; + margin: 0 5px; + font-size: 16px; + border: none; + border-radius: 5px; + cursor: pointer; + background-color: #f4d03f; + color: #000; + font-weight: bold; + transition: transform 0.2s; +} + +.controls button:hover { + transform: scale(1.1); +} + +.score, .time { + font-size: 18px; + margin: 5px 0; +} diff --git a/games/firefly-flight/index.html b/games/firefly-flight/index.html new file mode 100644 index 00000000..d3f95e77 --- /dev/null +++ b/games/firefly-flight/index.html @@ -0,0 +1,29 @@ + + + + + + Firefly Flight | Mini JS Games Hub + + + +
    +

    Firefly Flight

    +

    Guide the firefly through the dark forest. Collect orbs, avoid obstacles, and keep glowing!

    +
    + + + +
    + +

    Score: 0

    +
    + + + + + + + + + diff --git a/games/firefly-flight/script.js b/games/firefly-flight/script.js new file mode 100644 index 00000000..cf327084 --- /dev/null +++ b/games/firefly-flight/script.js @@ -0,0 +1,176 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const restartBtn = document.getElementById("restart-btn"); + +const scoreEl = document.getElementById("score"); +const collectSound = document.getElementById("collect-sound"); +const hitSound = document.getElementById("hit-sound"); +const bgMusic = document.getElementById("bg-music"); + +let firefly = { x: 100, y: 250, radius: 15, glow: 20, speed: 4 }; +let keys = {}; +let obstacles = []; +let orbs = []; +let score = 0; +let gameInterval; +let running = false; + +// Random number helper +function random(min, max) { + return Math.random() * (max - min) + min; +} + +// Create obstacles +function createObstacles() { + obstacles = []; + for (let i = 0; i < 5; i++) { + obstacles.push({ + x: random(400, 1200), + y: random(50, 450), + width: 40, + height: 40, + }); + } +} + +// Create orbs +function createOrbs() { + orbs = []; + for (let i = 0; i < 7; i++) { + orbs.push({ + x: random(500, 1300), + y: random(50, 450), + radius: 10 + }); + } +} + +// Draw firefly +function drawFirefly() { + ctx.save(); + ctx.shadowBlur = firefly.glow; + ctx.shadowColor = "yellow"; + ctx.beginPath(); + ctx.arc(firefly.x, firefly.y, firefly.radius, 0, Math.PI * 2); + ctx.fillStyle = "yellow"; + ctx.fill(); + ctx.restore(); +} + +// Draw obstacles +function drawObstacles() { + ctx.fillStyle = "red"; + obstacles.forEach(o => ctx.fillRect(o.x, o.y, o.width, o.height)); +} + +// Draw orbs +function drawOrbs() { + ctx.fillStyle = "lime"; + orbs.forEach(o => { + ctx.save(); + ctx.shadowBlur = 15; + ctx.shadowColor = "lime"; + ctx.beginPath(); + ctx.arc(o.x, o.y, o.radius, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + }); +} + +// Move firefly +function moveFirefly() { + if (keys["ArrowUp"] || keys["w"]) firefly.y -= firefly.speed; + if (keys["ArrowDown"] || keys["s"]) firefly.y += firefly.speed; + if (keys["ArrowLeft"] || keys["a"]) firefly.x -= firefly.speed; + if (keys["ArrowRight"] || keys["d"]) firefly.x += firefly.speed; + + // Boundaries + firefly.x = Math.max(firefly.radius, Math.min(canvas.width - firefly.radius, firefly.x)); + firefly.y = Math.max(firefly.radius, Math.min(canvas.height - firefly.radius, firefly.y)); +} + +// Collision detection +function checkCollisions() { + // Obstacles + obstacles.forEach(o => { + if ( + firefly.x + firefly.radius > o.x && + firefly.x - firefly.radius < o.x + o.width && + firefly.y + firefly.radius > o.y && + firefly.y - firefly.radius < o.y + o.height + ) { + hitSound.play(); + firefly.x = 100; + firefly.y = 250; + score = Math.max(0, score - 5); + } + }); + + // Orbs + orbs.forEach((orb, index) => { + const dx = firefly.x - orb.x; + const dy = firefly.y - orb.y; + if (Math.sqrt(dx*dx + dy*dy) < firefly.radius + orb.radius) { + collectSound.play(); + score += 10; + orbs.splice(index, 1); + } + }); +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawFirefly(); + drawObstacles(); + drawOrbs(); + scoreEl.textContent = score; +} + +// Game loop +function update() { + moveFirefly(); + checkCollisions(); + draw(); +} + +// Start game +function startGame() { + if (!running) { + createObstacles(); + createOrbs(); + gameInterval = setInterval(update, 20); + running = true; + bgMusic.play(); + } +} + +// Pause game +function pauseGame() { + if (running) { + clearInterval(gameInterval); + running = false; + bgMusic.pause(); + } +} + +// Restart game +function restartGame() { + firefly.x = 100; + firefly.y = 250; + score = 0; + createObstacles(); + createOrbs(); + if (!running) startGame(); +} + +// Event listeners +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", pauseGame); +restartBtn.addEventListener("click", restartGame); + +window.addEventListener("keydown", (e) => keys[e.key] = true); +window.addEventListener("keyup", (e) => keys[e.key] = false); diff --git a/games/firefly-flight/style.css b/games/firefly-flight/style.css new file mode 100644 index 00000000..8b3f2f6f --- /dev/null +++ b/games/firefly-flight/style.css @@ -0,0 +1,32 @@ +body { + margin: 0; + font-family: Arial, sans-serif; + background-color: #0b0b2f; + color: #fff; + text-align: center; +} + +.game-ui { + max-width: 900px; + margin: 0 auto; + padding: 10px; +} + +canvas { + display: block; + margin: 20px auto; + background-color: #0b0b2f; + border: 2px solid #fff; + border-radius: 10px; +} + +.buttons button { + padding: 10px 20px; + margin: 5px; + font-size: 16px; + cursor: pointer; +} + +.orb { + fill: yellow; +} diff --git a/games/firefly-flow/index.html b/games/firefly-flow/index.html new file mode 100644 index 00000000..64912829 --- /dev/null +++ b/games/firefly-flow/index.html @@ -0,0 +1,25 @@ + + + + + +Firefly Flow | Mini JS Games Hub + + + +
    +

    ๐Ÿ”ฅ Firefly Flow ๐Ÿ”ฅ

    + +
    + + + + + Score: 0 +
    + + +
    + + + diff --git a/games/firefly-flow/script.js b/games/firefly-flow/script.js new file mode 100644 index 00000000..1dd80a82 --- /dev/null +++ b/games/firefly-flow/script.js @@ -0,0 +1,126 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); + +canvas.width = 800; +canvas.height = 400; + +let fireflies = []; +let obstacles = []; +let score = 0; +let animationId; +let gamePaused = false; + +const bgm = document.getElementById('bgm'); +const hitSound = document.getElementById('hitSound'); + +class Firefly { + constructor(x, y) { + this.x = x; + this.y = y; + this.radius = 10; + this.speed = 2 + Math.random() * 2; + this.color = 'cyan'; + this.glow = 0; + } + + draw() { + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); + ctx.shadowColor = this.color; + ctx.shadowBlur = this.glow; + ctx.fillStyle = this.color; + ctx.fill(); + ctx.closePath(); + } + + update() { + this.x += this.speed; + this.glow = Math.sin(Date.now() / 100) * 20 + 20; + this.draw(); + } +} + +class Obstacle { + constructor(x, y, width, height){ + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + draw(){ + ctx.fillStyle = 'red'; + ctx.fillRect(this.x, this.y, this.width, this.height); + } +} + +function init(){ + fireflies = []; + obstacles = []; + score = 0; + for(let i=0;i<5;i++){ + fireflies.push(new Firefly(50, 50 + i*60)); + } + // Add random obstacles + for(let i=0;i<5;i++){ + obstacles.push(new Obstacle(200 + i*100, Math.random()*350, 20, 50)); + } +} + +function detectCollision(firefly, obstacle){ + return firefly.x + firefly.radius > obstacle.x && + firefly.x - firefly.radius < obstacle.x + obstacle.width && + firefly.y + firefly.radius > obstacle.y && + firefly.y - firefly.radius < obstacle.y + obstacle.height; +} + +function update(){ + if(gamePaused) return; + + ctx.clearRect(0,0,canvas.width, canvas.height); + + fireflies.forEach(f => { + f.update(); + obstacles.forEach(obs => { + obs.draw(); + if(detectCollision(f, obs)){ + hitSound.currentTime = 0; + hitSound.play(); + f.x = 50; // Reset firefly + score -= 1; + } + }); + }); + + document.getElementById('score').textContent = 'Score: ' + score; + + animationId = requestAnimationFrame(update); +} + +document.getElementById('startBtn').addEventListener('click', () => { + bgm.play(); + init(); + gamePaused = false; + cancelAnimationFrame(animationId); + update(); +}); + +document.getElementById('pauseBtn').addEventListener('click', () => { + gamePaused = true; + bgm.pause(); +}); + +document.getElementById('resumeBtn').addEventListener('click', () => { + gamePaused = false; + bgm.play(); + update(); +}); + +document.getElementById('restartBtn').addEventListener('click', () => { + bgm.currentTime = 0; + bgm.play(); + init(); + gamePaused = false; + cancelAnimationFrame(animationId); + update(); +}); diff --git a/games/firefly-flow/style.css b/games/firefly-flow/style.css new file mode 100644 index 00000000..c74163e0 --- /dev/null +++ b/games/firefly-flow/style.css @@ -0,0 +1,49 @@ +body { + margin: 0; + font-family: Arial, sans-serif; + background: linear-gradient(to bottom, #0d0d0d, #1a1a1a); + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + text-align: center; +} + +canvas { + background-color: #111; + display: block; + margin: 20px auto; + border-radius: 15px; + box-shadow: 0 0 20px #0ff; +} + +.controls { + margin-top: 10px; +} + +button { + margin: 5px; + padding: 8px 15px; + font-size: 16px; + border: none; + border-radius: 5px; + cursor: pointer; + background-color: #0ff; + color: #111; + font-weight: bold; + box-shadow: 0 0 10px #0ff; + transition: 0.2s; +} + +button:hover { + box-shadow: 0 0 20px #0ff, 0 0 40px #0ff; +} + +#score { + font-size: 18px; + margin-left: 20px; +} diff --git a/games/fishing-frenzy/index.html b/games/fishing-frenzy/index.html new file mode 100644 index 00000000..d8d6cc2f --- /dev/null +++ b/games/fishing-frenzy/index.html @@ -0,0 +1,45 @@ + + + + + + Fishing Frenzy + + + +
    +

    ๐ŸŽฃ Fishing Frenzy

    +

    Catch fish of different sizes for points! Click when the hook is near a fish.

    + +
    +
    Score: 0
    +
    Level: 1
    +
    Time: 60s
    +
    Fish Caught: 0
    +
    + + + +
    + + + +
    + +
    + +
    +

    How to Play:

    +
      +
    • Move your mouse to position the fishing hook
    • +
    • Click to cast your line when a fish swims by
    • +
    • Different sized fish give different points
    • +
    • Try to catch as many fish as possible before time runs out
    • +
    • Larger fish are worth more but harder to catch
    • +
    +
    +
    + + + + \ No newline at end of file diff --git a/games/fishing-frenzy/script.js b/games/fishing-frenzy/script.js new file mode 100644 index 00000000..3771a98a --- /dev/null +++ b/games/fishing-frenzy/script.js @@ -0,0 +1,272 @@ +// Fishing Frenzy Game +// Click to catch fish swimming by - different sizes worth different points + +// DOM elements +const canvas = document.getElementById('fishing-canvas'); +const ctx = canvas.getContext('2d'); +const scoreEl = document.getElementById('current-score'); +const levelEl = document.getElementById('current-level'); +const timerEl = document.getElementById('time-left'); +const caughtEl = document.getElementById('caught-count'); +const messageEl = document.getElementById('message'); +const startBtn = document.getElementById('start-btn'); +const pauseBtn = document.getElementById('pause-btn'); +const resetBtn = document.getElementById('reset-btn'); + +// Game constants +const CANVAS_WIDTH = 800; +const CANVAS_HEIGHT = 500; +const HOOK_SIZE = 10; + +// Game variables +let gameRunning = false; +let gamePaused = false; +let score = 0; +let level = 1; +let timeLeft = 60; +let fishCaught = 0; +let hookX = CANVAS_WIDTH / 2; +let hookY = CANVAS_HEIGHT - 50; +let fish = []; +let animationId; +let lastFishSpawn = 0; + +// Fish types with different properties +const fishTypes = [ + { name: 'small', size: 20, speed: 1, points: 10, color: '#FF6B6B' }, + { name: 'medium', size: 30, speed: 1.5, points: 25, color: '#4ECDC4' }, + { name: 'large', size: 40, speed: 2, points: 50, color: '#45B7D1' }, + { name: 'rare', size: 25, speed: 0.8, points: 100, color: '#F9CA24' } +]; + +// Event listeners +startBtn.addEventListener('click', startGame); +pauseBtn.addEventListener('click', togglePause); +resetBtn.addEventListener('click', resetGame); +canvas.addEventListener('mousemove', moveHook); +canvas.addEventListener('click', castLine); + +// Start the game +function startGame() { + // Reset everything + score = 0; + level = 1; + timeLeft = 60; + fishCaught = 0; + fish = []; + gameRunning = true; + gamePaused = false; + + // Update UI + scoreEl.textContent = score; + levelEl.textContent = level; + timerEl.textContent = timeLeft; + caughtEl.textContent = fishCaught; + messageEl.textContent = ''; + + startBtn.style.display = 'none'; + pauseBtn.style.display = 'inline-block'; + + // Start game loop + gameLoop(); +} + +// Main game loop +function gameLoop() { + if (!gameRunning || gamePaused) return; + + updateGame(); + drawGame(); + + animationId = requestAnimationFrame(gameLoop); +} + +// Update game state +function updateGame() { + // Spawn new fish + if (Date.now() - lastFishSpawn > 2000 - level * 100) { + spawnFish(); + lastFishSpawn = Date.now(); + } + + // Move fish + fish.forEach(f => { + f.x += f.speed; + }); + + // Remove fish that swam off screen + fish = fish.filter(f => f.x < CANVAS_WIDTH + 50); + + // Update timer + timeLeft--; + timerEl.textContent = timeLeft; + + if (timeLeft <= 0) { + gameOver(); + } +} + +// Spawn a random fish +function spawnFish() { + const type = fishTypes[Math.floor(Math.random() * fishTypes.length)]; + const y = Math.random() * (CANVAS_HEIGHT - 100) + 50; // Random depth + + fish.push({ + x: -50, + y: y, + ...type + }); +} + +// Move hook with mouse +function moveHook(event) { + const rect = canvas.getBoundingClientRect(); + hookX = event.clientX - rect.left; + hookY = event.clientY - rect.top; +} + +// Cast line (try to catch fish) +function castLine() { + if (!gameRunning || gamePaused) return; + + // Check if hook is near any fish + fish.forEach((f, index) => { + const distance = Math.sqrt((f.x - hookX) ** 2 + (f.y - hookY) ** 2); + if (distance < f.size / 2 + HOOK_SIZE) { + // Caught a fish! + catchFish(f, index); + } + }); +} + +// Catch a fish +function catchFish(fishData, index) { + // Remove fish from array + fish.splice(index, 1); + + // Add score + score += fishData.points; + fishCaught++; + + // Update UI + scoreEl.textContent = score; + caughtEl.textContent = fishCaught; + + // Show catch message + messageEl.textContent = `Caught a ${fishData.name} fish! +${fishData.points} points`; + setTimeout(() => messageEl.textContent = '', 1500); + + // Level up every 10 fish + if (fishCaught > 0 && fishCaught % 10 === 0) { + level++; + levelEl.textContent = level; + messageEl.textContent = `Level ${level}! Fish are faster now!`; + setTimeout(() => messageEl.textContent = '', 2000); + } +} + +// Toggle pause +function togglePause() { + gamePaused = !gamePaused; + if (gamePaused) { + pauseBtn.textContent = 'Resume'; + messageEl.textContent = 'Game Paused'; + } else { + pauseBtn.textContent = 'Pause'; + messageEl.textContent = ''; + gameLoop(); // Resume the loop + } +} + +// Game over +function gameOver() { + gameRunning = false; + cancelAnimationFrame(animationId); + messageEl.textContent = `Time's up! Final Score: ${score} (${fishCaught} fish caught)`; + pauseBtn.style.display = 'none'; + resetBtn.style.display = 'inline-block'; +} + +// Reset game +function resetGame() { + gameRunning = false; + cancelAnimationFrame(animationId); + score = 0; + level = 1; + fishCaught = 0; + scoreEl.textContent = score; + levelEl.textContent = level; + caughtEl.textContent = fishCaught; + messageEl.textContent = ''; + resetBtn.style.display = 'none'; + startBtn.style.display = 'inline-block'; + pauseBtn.style.display = 'none'; + + // Clear canvas + ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); +} + +// Draw the game +function drawGame() { + // Clear canvas + ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + // Draw water effect + ctx.fillStyle = 'rgba(135, 206, 235, 0.3)'; + for (let i = 0; i < 20; i++) { + ctx.beginPath(); + ctx.arc(Math.random() * CANVAS_WIDTH, Math.random() * CANVAS_HEIGHT, Math.random() * 30 + 10, 0, Math.PI * 2); + ctx.fill(); + } + + // Draw fish + fish.forEach(f => { + drawFish(f); + }); + + // Draw hook + ctx.fillStyle = '#333'; + ctx.beginPath(); + ctx.arc(hookX, hookY, HOOK_SIZE, 0, Math.PI * 2); + ctx.fill(); + + // Draw fishing line + ctx.strokeStyle = '#333'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(hookX, hookY); + ctx.lineTo(hookX, CANVAS_HEIGHT); + ctx.stroke(); +} + +// Draw a fish +function drawFish(f) { + ctx.fillStyle = f.color; + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + + // Fish body (ellipse) + ctx.beginPath(); + ctx.ellipse(f.x, f.y, f.size, f.size / 2, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + + // Fish tail + ctx.beginPath(); + ctx.moveTo(f.x - f.size, f.y); + ctx.lineTo(f.x - f.size - 10, f.y - 5); + ctx.lineTo(f.x - f.size - 10, f.y + 5); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + // Fish eye + ctx.fillStyle = '#000'; + ctx.beginPath(); + ctx.arc(f.x + f.size / 3, f.y - f.size / 4, 3, 0, Math.PI * 2); + ctx.fill(); +} + +// I really enjoyed making this fishing game +// The mouse interaction feels smooth and the different fish sizes add nice variety +// Maybe I'll add sound effects or different fishing rods later \ No newline at end of file diff --git a/games/fishing-frenzy/style.css b/games/fishing-frenzy/style.css new file mode 100644 index 00000000..c3d7e199 --- /dev/null +++ b/games/fishing-frenzy/style.css @@ -0,0 +1,134 @@ +/* Fishing Frenzy Game Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #74b9ff, #0984e3); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + color: white; +} + +.container { + text-align: center; + max-width: 900px; + padding: 20px; +} + +h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +p { + font-size: 1.1em; + margin-bottom: 20px; + opacity: 0.9; +} + +.game-stats { + display: flex; + justify-content: space-around; + flex-wrap: wrap; + margin: 20px 0; + font-size: 1.1em; + font-weight: bold; + background: rgba(255, 255, 255, 0.1); + padding: 15px; + border-radius: 10px; +} + +#fishing-canvas { + border: 3px solid white; + border-radius: 10px; + background: linear-gradient(to bottom, #87CEEB, #4682B4); + display: block; + margin: 20px auto; + box-shadow: 0 4px 8px rgba(0,0,0,0.3); + cursor: crosshair; +} + +.controls { + margin: 20px 0; +} + +button { + background: #27ae60; + color: white; + border: none; + padding: 12px 24px; + font-size: 1em; + border-radius: 5px; + cursor: pointer; + margin: 0 10px; + transition: background 0.3s; +} + +button:hover { + background: #229954; +} + +#message { + font-size: 1.2em; + margin: 20px 0; + min-height: 30px; + font-weight: bold; +} + +.instructions { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + margin-top: 20px; + text-align: left; + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.instructions h3 { + margin-bottom: 10px; + color: #ffeaa7; +} + +.instructions ul { + list-style-type: disc; + padding-left: 20px; +} + +.instructions li { + margin: 5px 0; + line-height: 1.4; +} + +/* Responsive design */ +@media (max-width: 768px) { + #fishing-canvas { + width: 100%; + max-width: 600px; + height: 400px; + } + + .game-stats { + font-size: 1em; + } + + .controls { + display: flex; + flex-direction: column; + gap: 10px; + } + + button { + width: 100%; + max-width: 200px; + } +} \ No newline at end of file diff --git a/games/flag_guess/index.html b/games/flag_guess/index.html new file mode 100644 index 00000000..aaafe513 --- /dev/null +++ b/games/flag_guess/index.html @@ -0,0 +1,37 @@ + + + + + + Guess the Flag Game + + + + +
    +

    ๐ŸŒŽ Guess the Flag

    + +
    + Score: 0 / 0 +
    + +
    + National Flag +
    + +
    + + + + +
    + +
    +
    + + +
    + + + + \ No newline at end of file diff --git a/games/flag_guess/script.js b/games/flag_guess/script.js new file mode 100644 index 00000000..6466c8a4 --- /dev/null +++ b/games/flag_guess/script.js @@ -0,0 +1,161 @@ +// --- 1. GAME DATA --- +const flagData = [ + { country: "Japan", file: "japan.png" }, + { country: "Germany", file: "germany.png" }, + { country: "Brazil", file: "brazil.png" }, + { country: "Canada", file: "canada.png" }, + { country: "India", file: "india.png" }, + { country: "France", file: "france.png" }, + { country: "Australia", file: "australia.png" }, + { country: "China", file: "china.png" }, + { country: "Mexico", file: "mexico.png" } + // NOTE: You must create a folder named 'images/flags/' and place the corresponding image files inside it. +]; + +// --- 2. GAME STATE VARIABLES --- +let currentQuestionIndex = 0; +let score = 0; +let answered = false; +let questions = []; // Array to hold the shuffled questions for the current game + +// --- 3. DOM ELEMENTS --- +const flagImage = document.getElementById('flag-image'); +const optionButtons = document.querySelectorAll('.option-button'); +const feedbackMessage = document.getElementById('feedback-message'); +const scoreSpan = document.getElementById('score'); +const totalQuestionsSpan = document.getElementById('total-questions'); +const nextButton = document.getElementById('next-button'); +const OPTIONS_COUNT = 4; + +// --- 4. CORE FUNCTIONS --- + +/** + * Shuffles an array in place using the Fisher-Yates algorithm. + */ +function shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } +} + +/** + * Initializes the game by shuffling the question list and setting up the score. + */ +function startGame() { + shuffleArray(flagData); + questions = flagData.slice(0, 5); // Use the first 5 flags for the game (can be adjusted) + totalQuestionsSpan.textContent = questions.length; + currentQuestionIndex = 0; + score = 0; + scoreSpan.textContent = score; + nextButton.style.display = 'none'; + loadQuestion(); +} + +/** + * Loads the next flag and generates four answer options. + */ +function loadQuestion() { + if (currentQuestionIndex >= questions.length) { + endGame(); + return; + } + + answered = false; + feedbackMessage.textContent = ''; + nextButton.style.display = 'none'; + + const currentFlag = questions[currentQuestionIndex]; + flagImage.src = `images/flags/${currentFlag.file}`; + flagImage.alt = `${currentFlag.country} Flag`; + + // 1. Determine the correct country name + const correctCountry = currentFlag.country; + + // 2. Select three random, incorrect options + const incorrectOptions = flagData + .filter(f => f.country !== correctCountry) + .map(f => f.country); + + shuffleArray(incorrectOptions); + + // 3. Create the list of four options (1 correct + 3 incorrect) + let options = [correctCountry, ...incorrectOptions.slice(0, OPTIONS_COUNT - 1)]; + shuffleArray(options); // Shuffle the final options so the correct answer isn't always in the same spot + + // 4. Update the buttons with the new options + optionButtons.forEach((button, index) => { + button.textContent = options[index]; + button.setAttribute('data-country', options[index]); + button.disabled = false; + button.classList.remove('correct', 'incorrect'); + button.addEventListener('click', checkAnswer, { once: true }); // Ensure handler is added + }); +} + +/** + * Handles the user's click and checks if the answer is correct. + */ +function checkAnswer(event) { + if (answered) return; + answered = true; + + const selectedCountry = event.target.getAttribute('data-country'); + const correctCountry = questions[currentQuestionIndex].country; + + // Disable all buttons after an answer is chosen + optionButtons.forEach(button => { + button.disabled = true; + + if (button.getAttribute('data-country') === correctCountry) { + button.classList.add('correct'); + } else if (button.getAttribute('data-country') === selectedCountry) { + // Mark the user's incorrect choice + button.classList.add('incorrect'); + } + }); + + if (selectedCountry === correctCountry) { + score++; + scoreSpan.textContent = score; + feedbackMessage.textContent = 'โœ… Correct!'; + feedbackMessage.style.color = '#4CAF50'; + } else { + feedbackMessage.textContent = `โŒ Incorrect. The correct answer was ${correctCountry}.`; + feedbackMessage.style.color = '#f44336'; + } + + // Prepare for the next round + nextButton.style.display = 'block'; +} + +/** + * Displays the final score and an option to restart. + */ +function endGame() { + flagImage.style.display = 'none'; + document.getElementById('options-container').innerHTML = ''; // Clear buttons + nextButton.style.display = 'block'; + nextButton.textContent = 'Play Again'; + nextButton.removeEventListener('click', nextQuestion); // Remove next listener + nextButton.addEventListener('click', startGame, { once: true }); // Add restart listener + + feedbackMessage.textContent = `Game Over! Your final score is ${score} out of ${questions.length}.`; + feedbackMessage.style.color = '#1a237e'; +} + +/** + * Proceeds to the next question. + */ +function nextQuestion() { + currentQuestionIndex++; + loadQuestion(); +} + +// --- 5. EVENT LISTENERS --- + +nextButton.addEventListener('click', nextQuestion); + +// Initial game start +startGame(); \ No newline at end of file diff --git a/games/flag_guess/style.css b/games/flag_guess/style.css new file mode 100644 index 00000000..9dc5b183 --- /dev/null +++ b/games/flag_guess/style.css @@ -0,0 +1,115 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f0f4f8; +} + +#game-container { + background-color: white; + padding: 30px; + border-radius: 12px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 500px; + width: 90%; +} + +h1 { + color: #1a237e; + margin-bottom: 20px; +} + +#score-area { + font-size: 1.2em; + font-weight: bold; + color: #3f51b5; + margin-bottom: 15px; +} + +/* --- Flag Display --- */ +#flag-display { + border: 1px solid #ddd; + border-radius: 8px; + padding: 10px; + margin-bottom: 20px; + background-color: #eceff1; +} + +#flag-image { + width: 100%; + max-width: 300px; /* Keep flag image size reasonable */ + height: auto; + display: block; + margin: 0 auto; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* --- Options Container --- */ +#options-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; + margin-bottom: 20px; +} + +.option-button { + padding: 12px; + font-size: 1em; + cursor: pointer; + background-color: #e3f2fd; + color: #1a237e; + border: 2px solid #90caf9; + border-radius: 6px; + transition: background-color 0.2s, transform 0.1s; + font-weight: 600; +} + +.option-button:hover:not(:disabled) { + background-color: #bbdefb; + transform: translateY(-2px); +} + +.option-button:disabled { + cursor: not-allowed; + opacity: 0.7; +} + +/* --- Feedback and Correct/Incorrect Styles --- */ +#feedback-message { + min-height: 1.5em; /* Reserve space to prevent layout jump */ + font-size: 1.2em; + font-weight: bold; + margin-bottom: 15px; +} + +.correct { + background-color: #4CAF50; /* Green for correct */ + color: white; + border-color: #388e3c; +} + +.incorrect { + background-color: #f44336; /* Red for incorrect */ + color: white; + border-color: #d32f2f; +} + +/* --- Next Button --- */ +#next-button { + padding: 10px 20px; + font-size: 1.1em; + background-color: #00bcd4; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; +} + +#next-button:hover { + background-color: #0097a7; +} \ No newline at end of file diff --git a/games/flame_fighter/index.html b/games/flame_fighter/index.html new file mode 100644 index 00000000..b472c495 --- /dev/null +++ b/games/flame_fighter/index.html @@ -0,0 +1,43 @@ + + + + + + The Frame Rate Fighter ๐Ÿ“‰ + + + + +
    +

    The Frame Rate Fighter ๐Ÿ“‰

    +

    Attack your opponent by crashing their frame rate!

    +
    + +
    +
    +

    Player 1 (Defender)

    +
    +

    Health: 100

    +

    Your FPS: --

    +

    Controls: WASD (Move), C (Light Attack), V (Lag Attack)

    +
    + +
    +
    +
    +
    + +
    +

    Player 2 (Attacker)

    +
    +

    Health: 100

    +

    Your FPS: --

    +

    Controls: Arrow Keys (Move), L (Light Attack), K (Lag Attack)

    +
    +
    + +
    Press 'K' or 'V' for a Lag Attack!
    + + + + \ No newline at end of file diff --git a/games/flame_fighter/script.js b/games/flame_fighter/script.js new file mode 100644 index 00000000..0f922e26 --- /dev/null +++ b/games/flame_fighter/script.js @@ -0,0 +1,188 @@ +// --- 1. Game Constants and State --- +const ARENA_WIDTH = 400; +const MOVEMENT_SPEED = 10; +const LAG_DURATION = 500; // Time in ms the lag attack is active +const LAG_INTENSITY = 8000000; // Number of loops for intentional lag + +const GAME_STATE = { + p1: { health: 100, x: 50, fps: 60, lagActive: false }, + p2: { health: 100, x: 320, fps: 60, lagActive: false } +}; + +let lastFrameTime = performance.now(); +let frameCount = 0; + +// --- 2. DOM Elements --- +const D = (id) => document.getElementById(id); +const $ = { + p1El: D('player-1'), + p2El: D('player-2'), + p1Health: D('p1-health'), + p2Health: D('p2-health'), + p1HealthBar: D('p1-health-bar'), + p2HealthBar: D('p2-health-bar'), + p1FPS: D('p1-fps'), + p2FPS: D('p2-fps'), + gameMessage: D('game-message'), + arena: D('arena') +}; + +// --- 3. Core Logic: FPS Calculation and Game Loop --- + +/** + * Calculates FPS and drives the game loop. + */ +function gameLoop() { + const now = performance.now(); + const deltaTime = now - lastFrameTime; + + // --- FPS Calculation (Crucial for the core mechanic) --- + // The player's perception of "lag" is measured by this + const currentFPS = 1000 / deltaTime; + + // The FPS is the same for the entire browser process, but we calculate it twice + // to track the "defender's" (P1) FPS when P2 attacks, and vice versa. + // In a local 2-player game, both players run on the same thread, so FPS is global. + // We update both FPS counters with the same value for realistic feedback. + GAME_STATE.p1.fps = Math.round(currentFPS); + GAME_STATE.p2.fps = Math.round(currentFPS); + + // --- State Update --- + updateFighterPosition(); + updateUI(); + + lastFrameTime = now; + requestAnimationFrame(gameLoop); +} + +// --- 4. Lag and Attack Mechanics --- + +/** + * Creates intentional, CPU-blocking lag. + * This is the weaponized function. + */ +function runLagAttack(attacker, defender) { + if (defender.lagActive) return; // Cannot stack lag attacks + + // --- INTENTIONAL CPU BLOCKING --- + // This heavy loop blocks the main thread, causing the deltaTime to spike + // and the calculated FPS to plummet in the next few frames. + const start = performance.now(); + for (let i = 0; i < LAG_INTENSITY; i++) { + // Run a complex, non-optimizable math function + Math.sqrt(i) * Math.sin(i); + } + const duration = performance.now() - start; + console.log(`Lag attack blocked thread for: ${duration.toFixed(2)}ms`); + // --------------------------------- + + // Damage Calculation: Damage = Base Damage / Current FPS + const baseDamage = 30; // High base damage for dramatic effect + + // Crucially, we use the FPS *after* the lag attack finishes (i.e., the current low FPS) + // for damage calculation, rewarding timing the lag. + const damageFactor = (1 / GAME_STATE[defender.id].fps) * 60; // Normalize by 60 FPS + let damageTaken = baseDamage * damageFactor; + + // Cap damage to prevent instant KOs on very slow systems + damageTaken = Math.min(damageTaken, 50); + + // Apply Damage + defender.health -= damageTaken; + defender.health = Math.max(0, defender.health); + + // Visual feedback + $.gameMessage.textContent = `๐Ÿ’ฅ LAG ATTACK HIT! ${defender.id} took ${damageTaken.toFixed(1)} damage! FPS: ${GAME_STATE[defender.id].fps}`; + + checkGameOver(); +} + +/** + * Executes a simple, non-lagging attack. + */ +function runLightAttack(attacker, defender) { + if (Math.abs(GAME_STATE[attacker.id].x - GAME_STATE[defender.id].x) > 100) { + $.gameMessage.textContent = "Too far for a Light Attack!"; + return; + } + + // Simple fixed damage + defender.health -= 5; + defender.health = Math.max(0, defender.health); + + $.gameMessage.textContent = `๐Ÿ—ก๏ธ Light Attack hit! ${defender.id} took 5 damage.`; + checkGameOver(); +} + +// --- 5. Movement and Collision --- + +function updateFighterPosition() { + $.p1El.style.left = `${GAME_STATE.p1.x}px`; + $.p2El.style.left = `${GAME_STATE.p2.x}px`; +} + +// --- 6. Event Listeners --- + +document.addEventListener('keydown', (e) => { + if (GAME_STATE.p1.health <= 0 || GAME_STATE.p2.health <= 0) return; + + // Player 1 Controls (WASD, C/V) + if (e.key === 'w' || e.key === 'W') { /* Jump logic */ } + if (e.key === 'a' || e.key === 'A') GAME_STATE.p1.x = Math.max(10, GAME_STATE.p1.x - MOVEMENT_SPEED); + if (e.key === 'd' || e.key === 'D') GAME_STATE.p1.x = Math.min(ARENA_WIDTH - 40, GAME_STATE.p1.x + MOVEMENT_SPEED); + + if (e.key === 'c' || e.key === 'C') runLightAttack({ id: 'p1' }, GAME_STATE.p2); + if (e.key === 'v' || e.key === 'V') { + // P1 attacks P2 with lag + $.p2El.classList.add('lag-attack'); + runLagAttack({ id: 'p1' }, GAME_STATE.p2); + setTimeout(() => $.p2El.classList.remove('lag-attack'), 500); + } + + // Player 2 Controls (Arrows, L/K) + if (e.key === 'ArrowUp') { /* Jump logic */ } + if (e.key === 'ArrowLeft') GAME_STATE.p2.x = Math.max(10, GAME_STATE.p2.x - MOVEMENT_SPEED); + if (e.key === 'ArrowRight') GAME_STATE.p2.x = Math.min(ARENA_WIDTH - 40, GAME_STATE.p2.x + MOVEMENT_SPEED); + + if (e.key === 'l' || e.key === 'L') runLightAttack({ id: 'p2' }, GAME_STATE.p1); + if (e.key === 'k' || e.key === 'K') { + // P2 attacks P1 with lag + $.p1El.classList.add('lag-attack'); + runLagAttack({ id: 'p2' }, GAME_STATE.p1); + setTimeout(() => $.p1El.classList.remove('lag-attack'), 500); + } +}); + + +// --- 7. Utility and Game End --- + +function updateUI() { + // Update FPS displays + const fpsColor = (fps) => fps < 20 ? 'red' : fps < 50 ? 'orange' : 'inherit'; + + $.p1FPS.textContent = GAME_STATE.p1.fps; + $.p1FPS.style.color = fpsColor(GAME_STATE.p1.fps); + + $.p2FPS.textContent = GAME_STATE.p2.fps; + $.p2FPS.style.color = fpsColor(GAME_STATE.p2.fps); + + // Update Health Displays + $.p1Health.textContent = GAME_STATE.p1.health; + $.p2Health.textContent = GAME_STATE.p2.health; + + $.p1HealthBar.style.width = `${GAME_STATE.p1.health}%`; + $.p2HealthBar.style.width = `${GAME_STATE.p2.health}%`; +} + +function checkGameOver() { + if (GAME_STATE.p1.health <= 0) { + $.gameMessage.innerHTML = `๐ŸŽ‰ **PLAYER 2 WINS!** Player 1 was defeated by poor optimization.`; + cancelAnimationFrame(animationFrameId); + } else if (GAME_STATE.p2.health <= 0) { + $.gameMessage.innerHTML = `๐ŸŽ‰ **PLAYER 1 WINS!** Player 2 was defeated by poor optimization.`; + cancelAnimationFrame(animationFrameId); + } +} + +// Start the game +let animationFrameId = requestAnimationFrame(gameLoop); \ No newline at end of file diff --git a/games/flame_fighter/style.css b/games/flame_fighter/style.css new file mode 100644 index 00000000..1a689823 --- /dev/null +++ b/games/flame_fighter/style.css @@ -0,0 +1,86 @@ +:root { + --arena-bg: #1a1a1a; + --p1-color: #50fa7b; /* Green */ + --p2-color: #bd93f9; /* Purple */ + --health-color: #ff5555; +} + +/* Base Styles */ +body { + font-family: sans-serif; + background-color: #282a36; + color: #f8f8f2; + margin: 0; + padding: 20px; + text-align: center; +} + +#game-container { + display: flex; + justify-content: center; + gap: 30px; + max-width: 1000px; + margin: 20px auto; +} + +/* Status Panels */ +.status-panel { + width: 300px; + padding: 15px; + border: 1px solid #44475a; + border-radius: 8px; + text-align: left; +} + +.health-bar { + height: 15px; + background-color: var(--health-color); + width: 100%; + margin: 5px 0 10px; + transition: width 0.3s; +} + +/* Arena */ +#arena { + position: relative; + width: 400px; + height: 200px; + background-color: var(--arena-bg); + border: 3px solid #ffb86c; + overflow: hidden; +} + +/* Fighters */ +.fighter { + position: absolute; + width: 30px; + height: 50px; + bottom: 0; + transition: left 0.1s; +} + +#player-1 { + background-color: var(--p1-color); + left: 50px; +} + +#player-2 { + background-color: var(--p2-color); + right: 50px; +} + +/* Attack Animation (Visual feedback for lag attacks) */ +.lag-attack { + animation: pulse-red 0.5s ease-out; +} + +@keyframes pulse-red { + 0% { transform: scale(1); opacity: 1; box-shadow: 0 0 10px var(--p2-color); } + 100% { transform: scale(1.5); opacity: 0; box-shadow: 0 0 20px var(--p2-color); } +} + +#game-message { + margin-top: 20px; + font-size: 1.2em; + color: #ffb86c; +} \ No newline at end of file diff --git a/games/flappy-bird-game/index.html b/games/flappy-bird-game/index.html new file mode 100644 index 00000000..44f94ed6 --- /dev/null +++ b/games/flappy-bird-game/index.html @@ -0,0 +1,23 @@ + + + + + + Flappy Bird Clone + + + +
    + + +
    +

    Flappy Clone

    +

    Press **SPACE** or **Tap** to flap the bird.

    +

    Avoid the pipes and the ground!

    +

    High Score: 0

    +
    +
    + + + + \ No newline at end of file diff --git a/games/flappy-bird-game/script.js b/games/flappy-bird-game/script.js new file mode 100644 index 00000000..1d8eeaea --- /dev/null +++ b/games/flappy-bird-game/script.js @@ -0,0 +1,279 @@ +// --- Setup --- +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const highScoreDisplay = document.getElementById('high-score-display'); + +// --- Game Constants --- +const GRAVITY = 0.5; // Acceleration due to gravity +const JUMP_VELOCITY = -8; // Upward velocity on a flap/tap +const GAME_SPEED = 3; // Horizontal speed of the pipes +const PIPE_WIDTH = 50; +const PIPE_GAP = 150; // Vertical distance between top and bottom pipe + +// --- Game State Variables --- +let bird = { + x: 50, + y: canvas.height / 2, + radius: 12, + velocity: 0, + score: 0 +}; +let pipes = []; +let frameCount = 0; +let gameRunning = false; +let highScore = parseInt(localStorage.getItem('flappyHighScore')) || 0; + +// Update High Score Display on load +highScoreDisplay.textContent = `High Score: ${highScore}`; + +// --- Game Object Classes --- + +/** + * Represents a single pipe obstacle (top and bottom sections). + * @param {number} x - The starting x-position of the pipe. + */ +class Pipe { + constructor(x) { + this.x = x; + this.width = PIPE_WIDTH; + + // Randomly determine the position of the gap + // Gap can be anywhere from 100px from the top to 100px from the bottom + const minGapY = 100; + const maxGapY = canvas.height - 100 - PIPE_GAP; + this.gapY = Math.random() * (maxGapY - minGapY) + minGapY; + + this.scored = false; + } + + // Move the pipe left and draw it + update() { + this.x -= GAME_SPEED; + + // Draw top pipe + ctx.fillStyle = "#2ecc71"; // Green pipe color + ctx.fillRect(this.x, 0, this.width, this.gapY); + + // Draw bottom pipe + ctx.fillRect(this.x, this.gapY + PIPE_GAP, this.width, canvas.height - this.gapY - PIPE_GAP); + } +} + +// --- Game Logic Functions --- + +/** + * Handles the bird's jump action. + */ +function flap() { + if (!gameRunning) { + startGame(); + return; + } + // Set a strong upward velocity + bird.velocity = JUMP_VELOCITY; +} + +/** + * Resets all game variables to their initial state. + */ +function resetGame() { + bird.y = canvas.height / 2; + bird.velocity = 0; + bird.score = 0; + pipes = []; + frameCount = 0; + gameRunning = false; + + // Draw the initial start screen + draw(); + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = 'white'; + ctx.font = '30px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('Tap/Space to Start', canvas.width / 2, canvas.height / 2 - 20); + ctx.font = '20px Arial'; + ctx.fillText(`High Score: ${highScore}`, canvas.width / 2, canvas.height / 2 + 20); +} + +/** + * Initiates the game loop. + */ +function startGame() { + if (gameRunning) return; + bird.score = 0; // Reset score for the new game + gameRunning = true; + loop(); // Start the animation loop +} + +/** + * Updates the high score and resets the game state. + */ +function gameOver() { + gameRunning = false; + + // Update high score if current score is better + if (bird.score > highScore) { + highScore = bird.score; + localStorage.setItem('flappyHighScore', highScore); + highScoreDisplay.textContent = `High Score: ${highScore}`; + } + + // Display Game Over message + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = 'white'; + ctx.font = '40px Arial'; + ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2 - 30); + ctx.font = '25px Arial'; + ctx.fillText(`Score: ${bird.score}`, canvas.width / 2, canvas.height / 2 + 10); + ctx.font = '18px Arial'; + ctx.fillText('Tap or Press Space to Restart', canvas.width / 2, canvas.height / 2 + 50); + + // Set a brief delay before allowing a restart flap + setTimeout(resetGame, 1000); +} + +/** + * Checks for collisions with the pipes or the ground/ceiling. + * @returns {boolean} True if a collision occurred, otherwise false. + */ +function checkCollision() { + // 1. Collision with Ground or Ceiling + if (bird.y + bird.radius >= canvas.height || bird.y - bird.radius <= 0) { + return true; + } + + // 2. Collision with Pipes + for (const pipe of pipes) { + const pipeRight = pipe.x + pipe.width; + + // Check if bird is horizontally between the pipes + if (bird.x + bird.radius > pipe.x && bird.x - bird.radius < pipeRight) { + // Check collision with top pipe OR bottom pipe + const hitTop = bird.y - bird.radius < pipe.gapY; + const hitBottom = bird.y + bird.radius > pipe.gapY + PIPE_GAP; + + if (hitTop || hitBottom) { + return true; + } + } + } + + return false; +} + +// --- Drawing Functions --- + +/** + * Draws the bird as a simple circle. + */ +function drawBird() { + ctx.beginPath(); + ctx.arc(bird.x, bird.y, bird.radius, 0, Math.PI * 2); + ctx.fillStyle = "#f1c40f"; // Yellow bird color + ctx.fill(); + ctx.strokeStyle = "#c0392b"; // Red outline + ctx.lineWidth = 2; + ctx.stroke(); + ctx.closePath(); +} + +/** + * Draws the current score on the screen. + */ +function drawScore() { + ctx.fillStyle = 'white'; + ctx.font = '40px Arial'; + ctx.textAlign = 'left'; + ctx.fillText(bird.score, 10, 40); +} + +/** + * Draws the ground boundary. + */ +function drawGround() { + ctx.fillStyle = '#d2b48c'; // Tan color + ctx.fillRect(0, canvas.height - 20, canvas.width, 20); +} + +/** + * Clears the canvas and redraws all game elements. + */ +function draw() { + // 1. Clear the canvas (or redraw background) + ctx.fillStyle = "#70c5ce"; // Sky blue + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // 2. Draw Pipes + pipes.forEach(pipe => pipe.update()); + + // 3. Draw Bird + drawBird(); + + // 4. Draw Score + drawScore(); + + // 5. Draw Ground (optional, but handles ground collision visually) + drawGround(); +} + +// --- Game Loop (The Heart of the Game) --- + +function loop() { + if (!gameRunning) return; + + // 1. Physics: Apply gravity and update position + bird.velocity += GRAVITY; + bird.y += bird.velocity; + + // 2. Collision Check + if (checkCollision()) { + gameOver(); + return; // Stop the loop immediately + } + + // 3. Pipe Generation + // Generate a new pipe every ~90 frames (or about every 1.5 seconds at 60 FPS) + if (frameCount % 90 === 0) { + pipes.push(new Pipe(canvas.width)); + } + + // 4. Pipe Management & Scoring + pipes.forEach(pipe => { + // Scoring: Bird has passed the pipe's X position + if (pipe.x + pipe.width < bird.x - bird.radius && !pipe.scored) { + bird.score++; + pipe.scored = true; + } + }); + + // Remove off-screen pipes to maintain performance + pipes = pipes.filter(pipe => pipe.x + pipe.width > 0); + + // 5. Drawing + draw(); + + // 6. Loop Continuation (Optimization) + frameCount++; + requestAnimationFrame(loop); // Calls the function on the next repaint +} + + +// --- Event Listeners (Controls) --- + +// Spacebar control +document.addEventListener('keydown', (e) => { + if (e.code === 'Space' || e.key === ' ') { + flap(); + e.preventDefault(); // Prevent page scrolling + } +}); + +// Mouse/Touch control +canvas.addEventListener('mousedown', flap); +canvas.addEventListener('touchstart', flap); + + +// --- Initialization --- +resetGame(); \ No newline at end of file diff --git a/games/flappy-bird-game/style.css b/games/flappy-bird-game/style.css new file mode 100644 index 00000000..e9a23bf6 --- /dev/null +++ b/games/flappy-bird-game/style.css @@ -0,0 +1,47 @@ +body { + font-family: Arial, sans-serif; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + margin: 0; + background-color: #f4f4f9; + color: #333; +} + +#game-container { + display: flex; + flex-direction: column; + align-items: center; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + border-radius: 8px; + background-color: white; +} + +#gameCanvas { + /* Dimensions match the width/height attributes in index.html */ + border: 2px solid #333; + background-color: #70c5ce; /* Light blue sky */ + cursor: pointer; +} + +#info { + padding: 10px 20px; + text-align: center; + background-color: #fff; + width: 400px; + box-sizing: border-box; +} + +#info h2 { + color: #2ecc71; /* Green color for the title */ + margin: 5px 0; +} + +#high-score-display { + font-size: 1.2em; + font-weight: bold; + margin-top: 5px; + color: #e74c3c; +} \ No newline at end of file diff --git a/games/flappy-bird/README.md b/games/flappy-bird/README.md new file mode 100644 index 00000000..9bb64b4d --- /dev/null +++ b/games/flappy-bird/README.md @@ -0,0 +1,28 @@ +# Flappy Bird + +A tiny Flappy Bird-like clone built for the Mini JS Games Hub. + +Controls + +- Click the canvas or press Space/Enter to flap. +- Press P or click the Pause button to pause/resume. +- Start/Restart button resets the game. + +Accessibility + +- Canvas is focusable (tabindex) and accepts keyboard controls. +- Buttons have ARIA labels and the pause button has `aria-pressed` state. + +Notes + +- The canvas scales responsively to the available width while maintaining aspect ratio. +- Score is shown in the UI and updated live. For best experience, open the game in a modern browser. + +Files + +- `index.html` โ€” the playable page +- `script.js` โ€” game logic +- `style.css` โ€” page styles +- `thumbnail.svg` โ€” small thumbnail used in the hub + +License: same as project. diff --git a/games/flappy-bird/index.html b/games/flappy-bird/index.html new file mode 100644 index 00000000..7405642a --- /dev/null +++ b/games/flappy-bird/index.html @@ -0,0 +1,27 @@ + + + + + + Flappy Bird - Mini JS Games Hub + + + +
    +

    Flappy Bird

    +
    + +
    +
    +
    + + +
    +

    Space, Enter, or click to flap. Press P to pause/resume.

    +

    Score: 0

    +
    +
    Made for Mini JS Games Hub
    +
    + + + \ No newline at end of file diff --git a/games/flappy-bird/script.js b/games/flappy-bird/script.js new file mode 100644 index 00000000..1e52eca2 --- /dev/null +++ b/games/flappy-bird/script.js @@ -0,0 +1,116 @@ +// Responsive Flappy Bird +const canvas = document.getElementById('game'); +const ctx = canvas.getContext('2d'); +const BASE_W = 320, BASE_H = 480, ASPECT = BASE_H / BASE_W; +let DPR = window.devicePixelRatio || 1; +let W = BASE_W, H = BASE_H; + +let frame = 0; +let gameState = 'menu'; // 'menu' | 'play' | 'paused' | 'over' +let score = 0; + +// make canvas focusable for keyboard controls +canvas.setAttribute('role', 'application'); +canvas.setAttribute('aria-label', 'Flappy Bird game canvas'); +canvas.tabIndex = 0; + +function resizeCanvas() { + DPR = window.devicePixelRatio || 1; + const container = canvas.parentElement || document.body; + // Use container width, with a max to avoid extremely large canvases + const maxWidth = Math.min(window.innerWidth - 40, 720); + const cssWidth = Math.min(container.clientWidth - 24 || BASE_W, maxWidth); + const cssHeight = Math.round(cssWidth * ASPECT); + + canvas.style.width = cssWidth + 'px'; + canvas.style.height = cssHeight + 'px'; + + // backing store size for crisp rendering on high-DPR displays + canvas.width = Math.round(cssWidth * DPR); + canvas.height = Math.round(cssHeight * DPR); + ctx.setTransform(DPR, 0, 0, DPR, 0, 0); + + W = cssWidth; + H = cssHeight; +} + +window.addEventListener('resize', resizeCanvas); +resizeCanvas(); + +const bird = { + x: 60, y: H / 2, r: 12, + vy: 0, gravity: 0.45, lift: -8, + draw() { + ctx.fillStyle = '#ffeb3b'; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = '#000'; + ctx.fillRect(this.x + 6, this.y - 3, 6, 4); + }, + update() { + this.vy += this.gravity; + this.y += this.vy; + if (this.y + this.r > H) { this.y = H - this.r; this.vy = 0; gameState = 'over'; } + if (this.y - this.r < 0) { this.y = this.r; this.vy = 0; } + } +}; + +class Pipe { + constructor(x) { this.w = Math.round(48 * (W / BASE_W)); this.gap = Math.round(120 * (W / BASE_W)); this.x = x; this.top = Math.random() * (H - 160) + 40; this.passed = false } + draw() { ctx.fillStyle = '#2e8b57'; ctx.fillRect(this.x, 0, this.w, this.top); ctx.fillRect(this.x, this.top + this.gap, this.w, H - (this.top + this.gap)); } + update() { this.x -= 2 * (W / BASE_W); if (!this.passed && this.x + this.w < bird.x) { score++; this.passed = true } } +} + +let pipes = []; +function reset() { + frame = 0; score = 0; bird.y = H / 2; bird.vy = 0; + pipes = [new Pipe(W + 30), new Pipe(W + 30 + 160)]; + gameState = 'play'; + document.getElementById('score').textContent = 'Score: 0'; +} + +function spawnIfNeeded() { if (pipes.length < 3 && pipes[pipes.length - 1].x < W - 140) pipes.push(new Pipe(W + 30)); } + +function checkCollisions() { for (const p of pipes) { if (bird.x + bird.r > p.x && bird.x - bird.r < p.x + p.w) { if (bird.y - bird.r < p.top || bird.y + bird.r > p.top + p.gap) { gameState = 'over'; } } } } + +function draw() { + ctx.clearRect(0, 0, W, H); + // cloud + ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.beginPath(); ctx.ellipse(40 + ((frame / 3) % W), 60, 32, 18, 0, 0, Math.PI * 2); ctx.fill(); + for (const p of pipes) p.draw(); + bird.draw(); + ctx.fillStyle = '#012'; ctx.font = Math.round(20 * (W / BASE_W)) + 'px monospace'; ctx.fillText(score, W - 40, 28); + if (gameState === 'menu') { ctx.fillStyle = '#012'; ctx.font = Math.round(16 * (W / BASE_W)) + 'px sans-serif'; ctx.fillText('Click or press Space to start', 22, H / 2 + 80) } + if (gameState === 'over') { ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(20, H / 2 - 40, W - 40, 80); ctx.fillStyle = '#fff'; ctx.font = Math.round(22 * (W / BASE_W)) + 'px sans-serif'; ctx.fillText('Game Over', W / 2 - 56, H / 2); ctx.fillText('Score: ' + score, W / 2 - 48, H / 2 + 28) } +} + +function update() { if (gameState === 'play') { frame++; for (const p of pipes) { p.update() } pipes = pipes.filter(p => p.x + p.w > -10); spawnIfNeeded(); bird.update(); checkCollisions(); document.getElementById('score').textContent = 'Score: ' + score } } + +function loop() { update(); draw(); requestAnimationFrame(loop); } + +// input +function flap() { if (gameState === 'menu') { reset(); } if (gameState === 'over') { reset(); } if (gameState === 'play') { bird.vy = bird.lift } else { bird.vy = bird.lift } } + +// Keyboard when canvas focused +canvas.addEventListener('keydown', e => { + if (e.code === 'Space' || e.code === 'Enter') { e.preventDefault(); flap(); } + if (e.code === 'KeyP') { // toggle pause + if (gameState === 'play') { gameState = 'paused'; document.getElementById('pauseBtn').setAttribute('aria-pressed', 'true'); document.getElementById('pauseBtn').textContent = 'Resume'; } + else if (gameState === 'paused') { gameState = 'play'; document.getElementById('pauseBtn').setAttribute('aria-pressed', 'false'); document.getElementById('pauseBtn').textContent = 'Pause'; } + } +}); + +canvas.addEventListener('click', flap); + +document.getElementById('startBtn').addEventListener('click', () => { reset(); canvas.focus(); }); +document.getElementById('pauseBtn').addEventListener('click', () => { + if (gameState === 'play') { gameState = 'paused'; document.getElementById('pauseBtn').setAttribute('aria-pressed', 'true'); document.getElementById('pauseBtn').textContent = 'Resume'; } + else if (gameState === 'paused') { gameState = 'play'; document.getElementById('pauseBtn').setAttribute('aria-pressed', 'false'); document.getElementById('pauseBtn').textContent = 'Pause'; } +}); + +// ensure canvas is sized correctly after fonts/UI settle +setTimeout(resizeCanvas, 50); + +// start loop +loop(); \ No newline at end of file diff --git a/games/flappy-bird/style.css b/games/flappy-bird/style.css new file mode 100644 index 00000000..194abee3 --- /dev/null +++ b/games/flappy-bird/style.css @@ -0,0 +1,7 @@ +*{box-sizing:border-box;margin:0;padding:0} +body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;background:#70c5ce;display:flex;align-items:center;justify-content:center;height:100vh} +.game-wrap{background:linear-gradient(#70c5ce,#a0e7ef);border:6px solid #2b6b6b;padding:12px;border-radius:8px;text-align:center;box-shadow:0 6px 18px rgba(0,0,0,0.2)} +canvas{background:linear-gradient(#87ceeb,#bdeaf2);display:block;margin:8px auto;border:2px solid #1b4f4f} +.info{color:#08393a} +#startBtn{padding:8px 12px;margin:8px;border-radius:6px;border:0;background:#ffcb05;color:#123} +footer{font-size:12px;color:#023} \ No newline at end of file diff --git a/games/flappy-bird/thumbnail.svg b/games/flappy-bird/thumbnail.svg new file mode 100644 index 00000000..9c3390d6 --- /dev/null +++ b/games/flappy-bird/thumbnail.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + Flappy Bird + \ No newline at end of file diff --git a/games/flip-focus/index.html b/games/flip-focus/index.html new file mode 100644 index 00000000..27bc1c7a --- /dev/null +++ b/games/flip-focus/index.html @@ -0,0 +1,33 @@ + + + + + + Flip Focus | Mini JS Games Hub + + + +
    +

    Flip Focus

    +

    Click the glowing orbs in order. Avoid obstacles!

    +
    + Score: 0 + Lives: 3 +
    +
    + +
    + + + + +
    + + + + +
    + + + + diff --git a/games/flip-focus/script.js b/games/flip-focus/script.js new file mode 100644 index 00000000..e5489974 --- /dev/null +++ b/games/flip-focus/script.js @@ -0,0 +1,92 @@ +const board = document.getElementById("game-board"); +const scoreEl = document.getElementById("score"); +const livesEl = document.getElementById("lives"); + +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const resumeBtn = document.getElementById("resume-btn"); +const restartBtn = document.getElementById("restart-btn"); + +const clickSound = document.getElementById("click-sound"); +const wrongSound = document.getElementById("wrong-sound"); +const bgMusic = document.getElementById("bg-music"); + +let score = 0; +let lives = 3; +let interval; +let gamePaused = false; + +function createOrb(id) { + const orb = document.createElement("div"); + orb.classList.add("orb"); + orb.dataset.id = id; + orb.addEventListener("click", () => { + if (gamePaused) return; + if (orb.classList.contains("obstacle")) { + lives--; + livesEl.textContent = lives; + wrongSound.play(); + if (lives <= 0) stopGame(); + return; + } + orb.classList.add("clicked"); + score++; + scoreEl.textContent = score; + clickSound.play(); + }); + return orb; +} + +function addObstacles(count) { + for (let i = 0; i < count; i++) { + const index = Math.floor(Math.random() * board.children.length); + const child = board.children[index]; + if (!child.classList.contains("obstacle")) { + child.classList.add("obstacle"); + } + } +} + +function startGame() { + board.innerHTML = ""; + score = 0; + lives = 3; + scoreEl.textContent = score; + livesEl.textContent = lives; + gamePaused = false; + bgMusic.play(); + + for (let i = 0; i < 10; i++) { + board.appendChild(createOrb(i)); + } + addObstacles(3); + + interval = setInterval(() => { + if (!gamePaused) { + // Randomly glow orbs + board.childNodes.forEach((orb) => { + if (!orb.classList.contains("clicked")) { + orb.style.boxShadow = `0 0 15px #0ff, 0 0 30px #0ff, 0 0 45px #0ff`; + } + }); + } + }, 1000); +} + +function stopGame() { + gamePaused = true; + clearInterval(interval); + alert("Game Over! Your Score: " + score); + bgMusic.pause(); +} + +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", () => { + gamePaused = true; + bgMusic.pause(); +}); +resumeBtn.addEventListener("click", () => { + gamePaused = false; + bgMusic.play(); +}); +restartBtn.addEventListener("click", startGame); diff --git a/games/flip-focus/style.css b/games/flip-focus/style.css new file mode 100644 index 00000000..3d8fc151 --- /dev/null +++ b/games/flip-focus/style.css @@ -0,0 +1,70 @@ +body { + font-family: Arial, sans-serif; + background-color: #0f0f20; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.game-container { + text-align: center; +} + +h1 { + font-size: 3rem; + text-shadow: 0 0 10px #0ff, 0 0 20px #0ff, 0 0 30px #0ff; +} + +.game-info { + margin: 10px 0; + font-size: 1.2rem; +} + +.game-board { + display: flex; + justify-content: center; + gap: 15px; + margin: 20px 0; +} + +.orb { + width: 50px; + height: 50px; + border-radius: 50%; + background: radial-gradient(circle, #0ff, #00f); + box-shadow: 0 0 15px #0ff, 0 0 30px #0ff, 0 0 45px #0ff; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; +} + +.orb.clicked { + transform: scale(0.8); + box-shadow: 0 0 10px #f00, 0 0 20px #f00; +} + +.obstacle { + width: 50px; + height: 50px; + background: radial-gradient(circle, #f00, #800); + border-radius: 50%; + box-shadow: 0 0 10px #f00, 0 0 20px #f00; +} + +.game-controls button { + margin: 5px; + padding: 10px 15px; + font-size: 1rem; + cursor: pointer; + border: none; + border-radius: 5px; + background: linear-gradient(90deg, #0ff, #00f); + color: #fff; + box-shadow: 0 0 10px #0ff; +} + +.game-controls button:hover { + transform: scale(1.1); +} diff --git a/games/flower-bloom/index.html b/games/flower-bloom/index.html new file mode 100644 index 00000000..b0fbcbd5 --- /dev/null +++ b/games/flower-bloom/index.html @@ -0,0 +1,25 @@ + + + + + + Flower Bloom | Mini JS Games Hub + + + +
    +

    ๐ŸŒธ Flower Bloom ๐ŸŒธ

    + +
    + + + + + +
    + +
    + + + + diff --git a/games/flower-bloom/script.js b/games/flower-bloom/script.js new file mode 100644 index 00000000..a11989a7 --- /dev/null +++ b/games/flower-bloom/script.js @@ -0,0 +1,85 @@ +const canvas = document.getElementById("flower-canvas"); +const ctx = canvas.getContext("2d"); + +canvas.width = window.innerWidth * 0.8; +canvas.height = window.innerHeight * 0.7; + +let flowers = []; +let animationId; +let running = true; +let speed = 5; + +const bloomSound = document.getElementById("bloom-sound"); + +// Flower class +class Flower { + constructor(x, y) { + this.x = x; + this.y = y; + this.radius = 0; + this.maxRadius = Math.random() * 25 + 15; + this.color = `hsl(${Math.random() * 360}, 80%, 60%)`; + this.opacity = 1; + } + + draw() { + ctx.save(); + ctx.globalAlpha = this.opacity; + let gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.radius); + gradient.addColorStop(0, this.color); + gradient.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = gradient; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + } + + update() { + if (this.radius < this.maxRadius) this.radius += 0.3 * (speed / 5); + else this.opacity -= 0.01; + this.draw(); + } +} + +// Animation loop +function animate() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + flowers.forEach((flower, i) => { + flower.update(); + if (flower.opacity <= 0) flowers.splice(i, 1); + }); + if (running) animationId = requestAnimationFrame(animate); +} + +canvas.addEventListener("click", (e) => { + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + flowers.push(new Flower(x, y)); + bloomSound.currentTime = 0; + bloomSound.play(); +}); + +document.getElementById("pause-btn").addEventListener("click", () => { + running = false; +}); + +document.getElementById("resume-btn").addEventListener("click", () => { + if (!running) { + running = true; + animate(); + } +}); + +document.getElementById("restart-btn").addEventListener("click", () => { + flowers = []; + ctx.clearRect(0, 0, canvas.width, canvas.height); +}); + +document.getElementById("speed-range").addEventListener("input", (e) => { + speed = e.target.value; +}); + +// Start animation +animate(); diff --git a/games/flower-bloom/style.css b/games/flower-bloom/style.css new file mode 100644 index 00000000..04c850dd --- /dev/null +++ b/games/flower-bloom/style.css @@ -0,0 +1,40 @@ +body { + margin: 0; + padding: 0; + background: linear-gradient(to bottom, #00111a, #002233); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + font-family: 'Arial', sans-serif; + color: #fff; +} + +.game-container { + text-align: center; +} + +canvas { + border: 2px solid #fff; + border-radius: 10px; + background: radial-gradient(circle at center, #00111a, #003366); + cursor: crosshair; +} + +.controls { + margin-top: 10px; +} + +.controls button, .controls input { + margin: 5px; + padding: 8px 12px; + border-radius: 5px; + border: none; + font-size: 16px; + cursor: pointer; +} + +.controls button:hover { + background-color: #00ccff; + color: #00111a; +} diff --git a/games/follow-the-path/index.html b/games/follow-the-path/index.html new file mode 100644 index 00000000..fa53c386 --- /dev/null +++ b/games/follow-the-path/index.html @@ -0,0 +1,29 @@ + + + + + + Follow the Path + + + +
    +

    Follow the Path

    +

    Memorize the sequence!

    +
    + +
    +
    +
    Level: 1
    +
    Best: 1
    +
    + + +
    + + + \ No newline at end of file diff --git a/games/follow-the-path/script.js b/games/follow-the-path/script.js new file mode 100644 index 00000000..acb2924a --- /dev/null +++ b/games/follow-the-path/script.js @@ -0,0 +1,165 @@ +// --- DOM Elements --- +const gameBoard = document.getElementById('game-board'); +const statusMessage = document.getElementById('status-message'); +const levelDisplay = document.getElementById('level-display'); +const highScoreDisplay = document.getElementById('high-score-display'); +const gameOverOverlay = document.getElementById('game-over-overlay'); +const finalLevelEl = document.getElementById('final-level'); +const restartBtn = document.getElementById('restart-btn'); + +// --- Game Constants & State --- +const GRID_SIZE = 4; +let gameState = 'watching'; +let currentLevel = 1; +let highScore = 1; +let path = []; +let playerPathIndex = 0; + +// --- NEW: Sound Effects --- +const sounds = { + blip: new Audio(''), // NOTE: Add paths to your sound files + correct: new Audio(''), + wrong: new Audio(''), + levelUp: new Audio('') +}; +function playSound(sound) { try { sounds[sound].currentTime = 0; sounds[sound].play(); } catch (e) {} } + +// --- Game Logic --- +function generateLevel() { + gameState = 'watching'; + path = []; + playerPathIndex = 0; + statusMessage.textContent = "Memorize the sequence!"; + levelDisplay.textContent = `Level: ${currentLevel}`; + + document.querySelectorAll('.grid-cell').forEach(cell => { + cell.classList.remove('player-wrong', 'player-correct'); + }); + + let currentRow = Math.floor(Math.random() * GRID_SIZE); + let currentCol = Math.floor(Math.random() * GRID_SIZE); + path.push({ row: currentRow, col: currentCol }); + + for (let i = 0; i < currentLevel + 1; i++) { + const neighbors = getValidNeighbors(currentRow, currentCol); + const nextMove = neighbors[Math.floor(Math.random() * neighbors.length)]; + currentRow = nextMove.row; + currentCol = nextMove.col; + path.push({ row: currentRow, col: currentCol }); + } + + animatePath(); +} + +function getValidNeighbors(row, col) { + const neighbors = []; + const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]]; + for (const [dr, dc] of directions) { + const newRow = row + dr; + const newCol = col + dc; + if (newRow >= 0 && newRow < GRID_SIZE && newCol >= 0 && newCol < GRID_SIZE) { + if (path.length > 1 && newRow === path[path.length - 2].row && newCol === path[path.length - 2].col) continue; + neighbors.push({ row: newRow, col: newCol }); + } + } + return neighbors.length > 0 ? neighbors : [{row, col}]; +} + +function animatePath() { + let i = 0; + const interval = setInterval(() => { + if (i >= path.length) { + clearInterval(interval); + gameState = 'repeating'; + statusMessage.textContent = "Your turn. Repeat the sequence!"; + return; + } + const { row, col } = path[i]; + const cell = document.querySelector(`[data-row='${row}'][data-col='${col}']`); + cell.classList.add('path-flash'); + playSound('blip'); + setTimeout(() => cell.classList.remove('path-flash'), 500); + i++; + }, 600); +} + +function handleCellClick(event) { + if (gameState !== 'repeating') return; + + const cell = event.target; + const row = parseInt(cell.dataset.row); + const col = parseInt(cell.dataset.col); + const correctMove = path[playerPathIndex]; + + if (row === correctMove.row && col === correctMove.col) { + playSound('correct'); + cell.classList.add('player-correct'); + setTimeout(() => cell.classList.remove('player-correct'), 300); + playerPathIndex++; + + if (playerPathIndex === path.length) { + // Level complete + playSound('levelUp'); + currentLevel++; + statusMessage.textContent = "Correct! Get ready..."; + // NEW: Visual feedback for level complete + gameBoard.classList.add('level-complete-flash'); + setTimeout(() => { + gameBoard.classList.remove('level-complete-flash'); + generateLevel(); + }, 1500); + } + } else { + // Wrong move + playSound('wrong'); + cell.classList.add('player-wrong'); + gameState = 'game_over'; + + if (currentLevel > highScore) { + highScore = currentLevel; + saveGame(); + highScoreDisplay.textContent = `Best: ${highScore}`; + } + + finalLevelEl.textContent = currentLevel; + setTimeout(() => gameOverOverlay.classList.remove('hidden'), 500); + } +} + +function createGrid() { + gameBoard.innerHTML = ''; + for (let r = 0; r < GRID_SIZE; r++) { + for (let c = 0; c < GRID_SIZE; c++) { + const cell = document.createElement('div'); + cell.classList.add('grid-cell'); + cell.dataset.row = r; + cell.dataset.col = c; + cell.addEventListener('click', handleCellClick); + gameBoard.appendChild(cell); + } + } +} + +function startGame() { + currentLevel = 1; + gameOverOverlay.classList.add('hidden'); + generateLevel(); +} + +// --- Save/Load Progress --- +function saveGame() { + localStorage.setItem('followThePath_highScore', highScore); +} + +function loadGame() { + const savedHighScore = localStorage.getItem('followThePath_highScore'); + highScore = savedHighScore ? parseInt(savedHighScore) : 1; + highScoreDisplay.textContent = `Best: ${highScore}`; +} + +// --- Event Listeners & Initialization --- +restartBtn.addEventListener('click', startGame); + +createGrid(); +loadGame(); +startGame(); \ No newline at end of file diff --git a/games/follow-the-path/style.css b/games/follow-the-path/style.css new file mode 100644 index 00000000..a6511982 --- /dev/null +++ b/games/follow-the-path/style.css @@ -0,0 +1,65 @@ +/* --- Core Layout --- */ +html, body { height: 100%; margin: 0; overflow: hidden; } +body { + font-family: 'Segoe UI', sans-serif; + background-color: #1c1c1c; + color: #f1f1f1; + display: flex; + align-items: center; + justify-content: center; + text-align: center; +} +#main-content { padding: 20px; } + +/* --- UI Elements --- */ +h1 { font-size: 2.5em; margin-bottom: 10px; } +p { font-size: 1.2em; color: #aaa; margin-top: 0; min-height: 1.5em; } +#game-info { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 500px; + margin: 20px auto 0; + font-size: 1.5em; + font-weight: bold; +} +#high-score-display { color: #f1c40f; } + +/* --- Game Board --- */ +#game-board { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; + width: clamp(300px, 80vmin, 500px); + height: clamp(300px, 80vmin, 500px); + margin: 20px auto; + transition: box-shadow 0.3s; +} +.grid-cell { + background-color: #333; + border-radius: 10px; + cursor: pointer; + transition: background-color 0.2s ease, transform 0.1s ease; +} +.grid-cell:hover:not(.path-flash) { transform: scale(1.05); } + +/* --- Cell States & Animations --- */ +.grid-cell.path-flash { background-color: #3498db; animation: flash 0.5s; cursor: default; } +.grid-cell.player-correct { background-color: #2ecc71; } +.grid-cell.player-wrong { background-color: #e74c3c; animation: shake 0.5s; } + +/* NEW: Level complete animation */ +.level-complete-flash { + box-shadow: 0 0 30px 10px #2ecc71; +} + +@keyframes flash { 50% { opacity: 0.5; } } +@keyframes shake { 0%, 100% { transform: translateX(0); } 20%, 60% { transform: translateX(-10px); } 40%, 80% { transform: translateX(10px); } } + +/* --- Game Over Overlay --- */ +#game-over-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.85); display: flex; flex-direction: column; align-items: center; justify-content: center; color: #fff; font-size: 2em; animation: fade-in 0.5s; } +@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } +#game-over-overlay p { font-size: 0.8em; } +#restart-btn { margin-top: 20px; padding: 15px 30px; font-size: 0.8em; border: none; border-radius: 10px; background-color: #3498db; color: white; cursor: pointer; transition: background-color 0.2s; } +#restart-btn:hover { background-color: #2980b9; } +.hidden { display: none !important; } \ No newline at end of file diff --git a/games/freeze-frame/index.html b/games/freeze-frame/index.html new file mode 100644 index 00000000..03370029 --- /dev/null +++ b/games/freeze-frame/index.html @@ -0,0 +1,32 @@ + + + + + + Freeze Frame | Mini JS Games Hub + + + +
    +

    Freeze Frame

    + + +
    + + + + +
    + +
    + Score: 0 + Time: 60s +
    +
    + + + + + + + diff --git a/games/freeze-frame/script.js b/games/freeze-frame/script.js new file mode 100644 index 00000000..a34181c7 --- /dev/null +++ b/games/freeze-frame/script.js @@ -0,0 +1,162 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +canvas.width = 600; +canvas.height = 400; + +let bulbs = []; +let obstacles = []; +let lines = []; +let score = 0; +let time = 60; +let gameInterval, timerInterval; +let isPaused = false; + +// Audio +const bgMusic = document.getElementById("bgMusic"); +const popSound = document.getElementById("popSound"); + +// Generate random bulbs +function createBulbs() { + bulbs = []; + for (let i = 0; i < 10; i++) { + bulbs.push({ + x: Math.random() * (canvas.width - 40) + 20, + y: Math.random() * (canvas.height - 40) + 20, + radius: 10, + glow: Math.random() * 5 + 5, + collected: false, + }); + } +} + +// Generate obstacles +function createObstacles() { + obstacles = []; + for (let i = 0; i < 5; i++) { + obstacles.push({ + x: Math.random() * (canvas.width - 60) + 30, + y: Math.random() * (canvas.height - 60) + 30, + width: 40, + height: 40, + }); + } +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw lines between collected bulbs + ctx.beginPath(); + ctx.strokeStyle = "#0ff"; + ctx.lineWidth = 2; + for (let i = 0; i < bulbs.length; i++) { + if (bulbs[i].collected) { + if (i > 0 && bulbs[i - 1].collected) { + ctx.moveTo(bulbs[i - 1].x, bulbs[i - 1].y); + ctx.lineTo(bulbs[i].x, bulbs[i].y); + } + } + } + ctx.stroke(); + + // Draw bulbs + bulbs.forEach(b => { + ctx.beginPath(); + ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2); + ctx.fillStyle = "cyan"; + ctx.shadowColor = "cyan"; + ctx.shadowBlur = b.glow; + ctx.fill(); + ctx.shadowBlur = 0; + }); + + // Draw obstacles + ctx.fillStyle = "red"; + obstacles.forEach(obs => { + ctx.fillRect(obs.x, obs.y, obs.width, obs.height); + }); + + // Draw score + document.getElementById("score").textContent = score; +} + +// Check click +canvas.addEventListener("click", e => { + if (isPaused) return; + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + bulbs.forEach(b => { + const dx = x - b.x; + const dy = y - b.y; + if (!b.collected && Math.sqrt(dx*dx + dy*dy) < b.radius + 5) { + b.collected = true; + score += 10; + popSound.play(); + } + }); +}); + +// Game loop +function gameLoop() { + draw(); +} + +// Timer +function startTimer() { + timerInterval = setInterval(() => { + if (!isPaused) { + time--; + document.getElementById("time").textContent = time; + if (time <= 0) stopGame(); + } + }, 1000); +} + +// Start game +function startGame() { + createBulbs(); + createObstacles(); + score = 0; + time = 60; + isPaused = false; + bgMusic.play(); + clearInterval(gameInterval); + clearInterval(timerInterval); + gameInterval = setInterval(gameLoop, 30); + startTimer(); +} + +// Pause +function pauseGame() { + isPaused = true; + bgMusic.pause(); +} + +// Resume +function resumeGame() { + isPaused = false; + bgMusic.play(); +} + +// Stop +function stopGame() { + clearInterval(gameInterval); + clearInterval(timerInterval); + bgMusic.pause(); + alert(`Game Over! Your score: ${score}`); +} + +// Restart +function restartGame() { + startGame(); +} + +// Buttons +document.getElementById("startBtn").addEventListener("click", startGame); +document.getElementById("pauseBtn").addEventListener("click", pauseGame); +document.getElementById("resumeBtn").addEventListener("click", resumeGame); +document.getElementById("restartBtn").addEventListener("click", restartGame); diff --git a/games/freeze-frame/style.css b/games/freeze-frame/style.css new file mode 100644 index 00000000..be316e26 --- /dev/null +++ b/games/freeze-frame/style.css @@ -0,0 +1,48 @@ +body { + margin: 0; + font-family: Arial, sans-serif; + background: radial-gradient(circle, #0f0c29, #302b63, #24243e); + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + text-align: center; +} + +canvas { + background: #111; + border: 2px solid #fff; + display: block; + margin: 20px auto; + box-shadow: 0 0 20px #0ff, 0 0 40px #0ff; +} + +.controls button { + margin: 5px; + padding: 10px 20px; + font-size: 16px; + cursor: pointer; + border-radius: 6px; + border: none; + background: #0ff; + color: #000; + font-weight: bold; + box-shadow: 0 0 10px #0ff; + transition: 0.2s; +} + +.controls button:hover { + box-shadow: 0 0 20px #0ff, 0 0 30px #0ff; +} + +.scoreboard { + margin-top: 10px; + font-size: 18px; + display: flex; + justify-content: center; + gap: 20px; +} diff --git a/games/frogger_clone/index.html b/games/frogger_clone/index.html new file mode 100644 index 00000000..9777e217 --- /dev/null +++ b/games/frogger_clone/index.html @@ -0,0 +1,26 @@ + + + + + + Crossy Road Clone + + + +

    Crossy Road Clone

    + +
    +

    Score: 0

    +

    Use **Arrow Keys** to move!

    +
    + +
    +
    + +
    + + + + + + \ No newline at end of file diff --git a/games/frogger_clone/script.js b/games/frogger_clone/script.js new file mode 100644 index 00000000..56445298 --- /dev/null +++ b/games/frogger_clone/script.js @@ -0,0 +1,302 @@ +const gameGrid = document.getElementById('game-grid'); +const player = document.getElementById('player'); +const scoreDisplay = document.getElementById('score'); +const messageDisplay = document.getElementById('message'); +const startButton = document.getElementById('start-button'); + +// Game constants (must match CSS) +const GRID_SIZE = 10; // 10x10 grid +const CELL_SIZE = 50; // 50px cell size +const GAME_WIDTH = GRID_SIZE * CELL_SIZE; + +// Lane definitions from bottom (index 0) to top (index 9) +// 'R' = Road, 'S' = Safe/Start/Finish, 'L' = River/Log +const LANE_TYPES = ['S', 'R', 'R', 'R', 'L', 'L', 'R', 'R', 'R', 'S']; + +// Obstacle configurations per lane (speed in px per game tick, width in grid cells) +const LANE_CONFIGS = [ + null, // Safe zone + { type: 'car', speed: 2, width: 2, direction: 'left' }, + { type: 'car', speed: -3, width: 1, direction: 'right' }, + { type: 'car', speed: 1.5, width: 3, direction: 'left' }, + { type: 'log', speed: 1, width: 3, direction: 'right' }, + { type: 'log', speed: -2, width: 2, direction: 'left' }, + { type: 'car', speed: 3, width: 1, direction: 'right' }, + { type: 'car', speed: -1, width: 2, direction: 'left' }, + { type: 'car', speed: 2.5, width: 1, direction: 'right' }, + null // Finish zone +]; + +let playerPos = { x: 0, y: 0 }; // Grid coordinates (0,0 is bottom-left) +let score = 0; +let isGameRunning = false; +let gameLoopInterval; +let obstacleList = []; + +// --- Game Setup --- + +/** + * Initializes the game board by creating the lanes. + */ +function createLanes() { + gameGrid.innerHTML = ''; // Clear existing content + obstacleList = []; + + // Create lanes from top to bottom (index 9 down to 0) + for (let i = GRID_SIZE - 1; i >= 0; i--) { + const lane = document.createElement('div'); + lane.classList.add('lane'); + lane.style.top = `${(GRID_SIZE - 1 - i) * CELL_SIZE}px`; // Position from top + lane.dataset.index = i; // Grid index (0 is bottom) + + // Assign CSS class based on type + if (LANE_TYPES[i] === 'S') { + lane.classList.add('start-finish-lane'); + } else if (LANE_TYPES[i] === 'R') { + lane.classList.add('road-lane'); + createObstacles(lane, LANE_CONFIGS[i], i); + } else if (LANE_TYPES[i] === 'L') { + lane.classList.add('river-lane'); + createObstacles(lane, LANE_CONFIGS[i], i); + } + + gameGrid.appendChild(lane); + } + gameGrid.appendChild(player); // Ensure player is on top +} + +/** + * Creates and positions initial obstacles in a lane. + */ +function createObstacles(lane, config, laneIndex) { + if (!config) return; + + const numLanes = GAME_WIDTH / CELL_SIZE; // Always 10 + const obstacleSpacing = 5; // Spacing between obstacles (in cells) + + let currentX = 0; + + // Create a pattern of obstacles + while (currentX < numLanes + obstacleSpacing) { + if (Math.random() < 0.6) { // 60% chance to place an obstacle + const obstacle = document.createElement('div'); + obstacle.classList.add('obstacle', config.type); + obstacle.style.width = `${config.width * CELL_SIZE}px`; + + // Randomize starting position based on direction + let startX = config.direction === 'right' ? + (currentX * CELL_SIZE) - GAME_WIDTH - (config.width * CELL_SIZE) : + (currentX * CELL_SIZE); + + obstacle.style.left = `${startX}px`; + obstacle.style.top = '0'; // Obstacles are positioned relative to their lane + + // Store properties for game logic + obstacle.dataset.speed = config.speed; + obstacle.dataset.type = config.type; + obstacle.dataset.lane = laneIndex; + + lane.appendChild(obstacle); + obstacleList.push(obstacle); + } + currentX += config.width + obstacleSpacing; + } +} + +// --- Player Movement --- + +/** + * Updates the player's position based on grid coordinates. + */ +function updatePlayerPosition() { + const top = (GRID_SIZE - 1 - playerPos.y) * CELL_SIZE; + const left = playerPos.x * CELL_SIZE; + player.style.transform = `translate(${left}px, ${top}px)`; +} + +/** + * Handles player movement input. + */ +function handleMove(e) { + if (!isGameRunning) return; + + let newX = playerPos.x; + let newY = playerPos.y; + + switch (e.key) { + case 'ArrowUp': + newY++; + break; + case 'ArrowDown': + newY--; + break; + case 'ArrowLeft': + newX--; + break; + case 'ArrowRight': + newX++; + break; + } + + // Check boundaries + if (newX >= 0 && newX < GRID_SIZE && newY >= 0 && newY < GRID_SIZE) { + playerPos.x = newX; + playerPos.y = newY; + updatePlayerPosition(); + checkGameStatus(); + } +} + +// --- Game Loop and Collision --- + +/** + * Main game loop: moves obstacles and checks for collisions. + */ +function gameLoop() { + if (!isGameRunning) return; + + // 1. Move Obstacles + obstacleList.forEach(obstacle => { + let currentLeft = parseFloat(obstacle.style.left); + const speed = parseFloat(obstacle.dataset.speed); + + // Wrap obstacles around the grid + if (speed > 0) { // Moving Right + if (currentLeft >= GAME_WIDTH) { + currentLeft = -(parseFloat(obstacle.style.width)); // Reset to far left + } + } else { // Moving Left + if (currentLeft <= -(parseFloat(obstacle.style.width))) { + currentLeft = GAME_WIDTH; // Reset to far right + } + } + + obstacle.style.left = `${currentLeft + speed}px`; + }); + + // 2. Check Collisions (and carry the player on logs) + checkCollision(); +} + +/** + * Checks for collisions with obstacles in the player's current lane. + */ +function checkCollision() { + const laneType = LANE_TYPES[playerPos.y]; + let isSafe = true; // Assume safe until collision detected + + if (laneType === 'R') { + // Road Lane: Collision with Car = Death + obstacleList.filter(o => parseInt(o.dataset.lane) === playerPos.y && o.dataset.type === 'car').forEach(car => { + const carRect = car.getBoundingClientRect(); + const playerRect = player.getBoundingClientRect(); + + // Simple AABB collision check + if (playerRect.left < carRect.right && + playerRect.right > carRect.left && + playerRect.top < carRect.bottom && + playerRect.bottom > carRect.top) { + + isSafe = false; + } + }); + + if (!isSafe) { + endGame('CRUNCH! Hit by a car. Game Over.'); + return; + } + + } else if (laneType === 'L') { + // River Lane: Must be on a Log or Die (Drowning) + let isPlayerOnLog = false; + let carryingLogSpeed = 0; + + obstacleList.filter(o => parseInt(o.dataset.lane) === playerPos.y && o.dataset.type === 'log').forEach(log => { + const logRect = log.getBoundingClientRect(); + const playerRect = player.getBoundingClientRect(); + + if (playerRect.left < logRect.right && + playerRect.right > logRect.left) { + + isPlayerOnLog = true; + carryingLogSpeed = parseFloat(log.dataset.speed); + } + }); + + if (!isPlayerOnLog) { + endGame('SPLASH! Drowned in the river. Game Over.'); + return; + } + + // Carry the player on the log (update player's grid X based on log speed) + // Convert movement to grid steps + const gridMove = carryingLogSpeed / CELL_SIZE; + playerPos.x += gridMove; + + // Prevent player from being pushed off the sides of the grid + if (playerPos.x < 0 || playerPos.x >= GRID_SIZE) { + endGame('Pushed off the edge! Game Over.'); + return; + } + + updatePlayerPosition(); + } +} + +/** + * Checks if the player has reached the finish line or made progress. + */ +function checkGameStatus() { + if (playerPos.y === GRID_SIZE - 1) { + // Reached the top finish line! + score++; + scoreDisplay.textContent = score; + messageDisplay.textContent = 'YOU MADE IT! Next Level...'; + + // Reset player to the start (bottom) + playerPos.x = 0; + playerPos.y = 0; + updatePlayerPosition(); + } +} + +// --- Game State Management --- + +/** + * Starts the game. + */ +function startGame() { + if (isGameRunning) return; + + createLanes(); + score = 0; + playerPos = { x: 4, y: 0 }; // Start in the middle of the bottom lane + updatePlayerPosition(); + scoreDisplay.textContent = score; + messageDisplay.textContent = 'Dodge the traffic!'; + isGameRunning = true; + startButton.style.display = 'none'; + + // Set game loop interval (e.g., 50ms for smooth movement) + gameLoopInterval = setInterval(gameLoop, 50); + window.addEventListener('keydown', handleMove); +} + +/** + * Ends the game. + */ +function endGame(message) { + if (!isGameRunning) return; + + isGameRunning = false; + clearInterval(gameLoopInterval); + messageDisplay.textContent = `${message} Final Score: ${score}`; + startButton.textContent = 'Play Again'; + startButton.style.display = 'block'; + window.removeEventListener('keydown', handleMove); +} + +// Initial setup +startButton.addEventListener('click', startGame); +createLanes(); // Show initial grid setup before start +updatePlayerPosition(); // Set initial player position \ No newline at end of file diff --git a/games/frogger_clone/style.css b/games/frogger_clone/style.css new file mode 100644 index 00000000..73c11b25 --- /dev/null +++ b/games/frogger_clone/style.css @@ -0,0 +1,97 @@ +:root { + --grid-size: 10; /* 10x10 grid */ + --cell-size: 50px; + --game-width: calc(var(--grid-size) * var(--cell-size)); +} + +body { + display: flex; + flex-direction: column; + align-items: center; + font-family: Arial, sans-serif; + background-color: #f0f0f0; + margin: 0; + padding-top: 20px; +} + +#game-grid { + position: relative; + width: var(--game-width); + height: var(--game-width); + border: 5px solid #333; + background-color: #555; /* Base color for the grid/road */ + overflow: hidden; /* Crucial for keeping obstacles contained */ +} + +/* Lane Styles - Define alternating environments */ +.lane { + position: relative; + width: 100%; + height: var(--cell-size); + box-sizing: border-box; +} + +.start-finish-lane { + background-color: #5cb85c; /* Green for safe zone */ +} + +.road-lane { + background-color: #333; /* Darker for road */ + border-top: 1px dashed #666; + border-bottom: 1px dashed #666; +} + +.river-lane { + background-color: #4682b4; /* Blue for water */ +} + +/* * Player and Obstacle Styles + */ +.character { + position: absolute; + width: var(--cell-size); + height: var(--cell-size); + transition: transform 0.1s linear; /* Smooth movement transition */ + box-sizing: border-box; + z-index: 10; +} + +#player { + background-color: #008000; /* Frog color */ + border: 2px solid #004d00; +} + +.obstacle { + position: absolute; + height: var(--cell-size); + box-sizing: border-box; + z-index: 5; /* Below the player */ +} + +.car { + background-color: #ff4500; /* Red car */ +} + +.log { + background-color: #a0522d; /* Brown log */ +} + +/* + * Info and Button + */ +#game-info { + margin-bottom: 15px; + text-align: center; +} + +#message { + font-weight: bold; + color: #d9534f; +} + +#start-button { + margin-top: 15px; + padding: 10px 20px; + font-size: 1.1em; + cursor: pointer; +} \ No newline at end of file diff --git a/games/fruit-slicer/index.html b/games/fruit-slicer/index.html new file mode 100644 index 00000000..12e201d7 --- /dev/null +++ b/games/fruit-slicer/index.html @@ -0,0 +1,27 @@ + + + + + + Fruit Slicer + + + +
    +
    +
    Score: 0
    + +
    + + +
    + + +
    + + + + diff --git a/games/fruit-slicer/script.js b/games/fruit-slicer/script.js new file mode 100644 index 00000000..c9807d75 --- /dev/null +++ b/games/fruit-slicer/script.js @@ -0,0 +1,263 @@ +// Simple Fruit Slicer - baby implementation +(function(){ + const playArea = document.getElementById('play-area'); + const canvas = document.getElementById('swipe-canvas'); + const scoreEl = document.getElementById('score-val'); + const startBtn = document.getElementById('start-btn'); + const overlay = document.getElementById('overlay'); + const message = document.getElementById('message'); + const restartBtn = document.getElementById('restart-btn'); + + // canvas for drawing swipe + const ctx = canvas.getContext && canvas.getContext('2d'); + let W, H; + function resize(){ + W = canvas.width = playArea.clientWidth; + H = canvas.height = playArea.clientHeight; + } + window.addEventListener('resize', resize); + + // game state + let running = false; + let score = 0; + let entities = []; // fruits and bombs + let spawnTimer = null; + let raf = null; + + // pointer tracking for swipe line and collision detection + let pointer = {down:false, points:[]}; + function resetPointer(){ pointer.down=false; pointer.points=[]; clearCanvas(); } + + function clearCanvas(){ if(ctx){ ctx.clearRect(0,0,W,H); }} + + // helper: random + const rnd = (a,b)=> Math.random()*(b-a)+a; + + // create fruit or bomb DOM element and metadata + function createEntity(type){ + const el = document.createElement('div'); + el.className = 'fruit'; + const isBomb = type === 'bomb'; + if(isBomb){ el.classList.add('bomb'); } + else { + // produce a colorful fruit look with CSS gradient + const color = ['#ff5e5e','#ffb86b','#6bff8a','#60d6ff','#c76bff'][Math.floor(Math.random()*5)]; + el.style.background = `radial-gradient(circle at 30% 25%, #fff6 0%, ${color} 20%, #0000 70%)`; + } + + playArea.appendChild(el); + + // spawn from bottom with upward impulse + const startX = rnd(60, W-60); + const startY = H + 40; // start slightly off-screen bottom + const vx = rnd(-80, 80) / 60; // px per frame baseline + const vy = rnd(-18, -26) / 1; // negative upward velocity (px per frame) + const radius = isBomb ? 26 : 32; + + const ent = {el, x:startX, y:startY, vx, vy, r:radius, type, sliced:false, rotation: rnd(-0.05,0.05)}; + // small size adjustments + if(isBomb){ ent.r = 24; el.style.width = '48px'; el.style.height = '48px'; } + else { el.style.width = ent.r*2+'px'; el.style.height = ent.r*2+'px'; } + + // initial placement + el.style.left = ent.x+'px'; el.style.top = ent.y+'px'; + entities.push(ent); + } + + // spawn loop: occasionally spawn fruits and bombs + function startSpawning(){ + spawnTimer = setInterval(()=>{ + // more fruits than bombs + const chooseBomb = Math.random() < 0.12; // 12% bombs + if(chooseBomb) createEntity('bomb'); + else createEntity('fruit'); + // sometimes spawn a small extra burst + if(Math.random() < 0.15) createEntity('fruit'); + }, 700); + } + + // physics update + function updateEntities(){ + const gravity = 0.6; // px per frame^2 + for(let i = entities.length-1; i>=0; i--){ + const e = entities[i]; + if(e.sliced) continue; // let it vanish visually when sliced + e.vy += gravity * 0.8; // gravity + e.x += e.vx * 6; // scale velocities to look good across sizes + e.y += e.vy * 6; + e.rotation += 0.02; + // apply to DOM + e.el.style.transform = `translate(-50%,-50%) translate(${e.x - W/2 + W/2}px,${e.y - H/2 + H/2}px) rotate(${e.rotation}rad)`; + e.el.style.left = e.x+'px'; + e.el.style.top = e.y+'px'; + + // remove if offscreen below + if(e.y - e.r > H + 100){ + // clean + e.el.remove(); + entities.splice(i,1); + } + } + } + + // draw swipe trail + function drawSwipe(){ + if(!ctx) return; + clearCanvas(); + if(pointer.points.length < 2) return; + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; + // draw fading trail + for(let i=pointer.points.length-1;i>0;i--){ + const a = pointer.points[i]; + const b = pointer.points[i-1]; + const alpha = (i / pointer.points.length) * 0.6; + ctx.strokeStyle = `rgba(255,255,255,${alpha})`; + ctx.lineWidth = 14 * (i / pointer.points.length) + 2; + ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke(); + } + } + + // distance from point to segment + function pointToSegmentDistance(px,py,x1,y1,x2,y2){ + const A = px - x1; const B = py - y1; const C = x2 - x1; const D = y2 - y1; + const dot = A * C + B * D; const len_sq = C * C + D * D; + let param = -1; + if (len_sq !== 0) param = dot / len_sq; + let xx, yy; + if (param < 0) { xx = x1; yy = y1; } + else if (param > 1) { xx = x2; yy = y2; } + else { xx = x1 + param * C; yy = y1 + param * D; } + const dx = px - xx; const dy = py - yy; return Math.sqrt(dx*dx + dy*dy); + } + + // check swipe segments against entities + function checkCollisions(){ + if(pointer.points.length < 2) return; + // take latest segment + const last = pointer.points[pointer.points.length-1]; + for(let j = pointer.points.length-2; j>=0 && j>pointer.points.length-6; j--){ + const p1 = pointer.points[j]; + const p2 = pointer.points[j+1]; + for(let i = entities.length-1; i>=0; i--){ + const e = entities[i]; + if(e.sliced) continue; + const d = pointToSegmentDistance(e.x, e.y, p1.x, p1.y, p2.x, p2.y); + if(d <= e.r){ + // hit + if(e.type === 'bomb'){ + // bomb sliced -> game over + endGame(true); + return; + } + sliceFruit(e, p1, p2); + } + } + } + } + + function sliceFruit(e, p1, p2){ + e.sliced = true; + // score depends on swipe speed and fruit speed + const dx = p2.x - p1.x, dy = p2.y - p1.y; + const speed = Math.min(60, Math.sqrt(dx*dx + dy*dy)); + score += Math.round(8 + speed * 0.3); + scoreEl.textContent = score; + + // visual slice: simple fade and remove + e.el.style.transition = 'transform 400ms ease-out, opacity 400ms ease-out'; + e.el.style.opacity = '0'; + // small cut effect + const cut = document.createElement('div'); + cut.className = 'slice-effect'; + cut.style.left = (e.x - 30) + 'px'; + cut.style.top = (e.y - 30) + 'px'; + cut.style.width = (e.r*2+20) + 'px'; + cut.style.height = (e.r/2) + 'px'; + cut.style.background = 'linear-gradient(90deg, rgba(255,255,255,0.9), rgba(255,255,255,0.2))'; + cut.style.transform = `rotate(${rnd(-45,45)}deg)`; + document.getElementById('game-root').appendChild(cut); + setTimeout(()=>cut.remove(),350); + + setTimeout(()=>{ + e.el.remove(); + const idx = entities.indexOf(e); if(idx>=0) entities.splice(idx,1); + }, 350); + } + + // main loop + function tick(){ + updateEntities(); + drawSwipe(); + checkCollisions(); + if(running) raf = requestAnimationFrame(tick); + } + + function startGame(){ + resize(); + score = 0; scoreEl.textContent = score; + running = true; overlay.classList.add('hidden'); + // clean existing + entities.forEach(e=>e.el.remove()); entities = []; + startSpawning(); + if(raf) cancelAnimationFrame(raf); raf = requestAnimationFrame(tick); + } + + function endGame(byBomb){ + running = false; clearInterval(spawnTimer); spawnTimer = null; if(raf) cancelAnimationFrame(raf); + // show message + message.textContent = byBomb ? 'Boom! You hit a bomb.' : 'Game over'; + overlay.classList.remove('hidden'); + } + + // pointer handling: supports mouse and touch via pointer events + const rectEl = playArea; + function toLocal(ev){ + const r = rectEl.getBoundingClientRect(); + return { x: ev.clientX - r.left, y: ev.clientY - r.top }; + } + + function onPointerDown(ev){ + if(!running) return; + pointer.down = true; pointer.points = []; + const p = toLocal(ev); + pointer.points.push(p); + } + function onPointerMove(ev){ + if(!pointer.down) return; + const p = toLocal(ev); + pointer.points.push(p); + if(pointer.points.length>30) pointer.points.shift(); + } + function onPointerUp(ev){ + pointer.down = false; pointer.points = []; + clearCanvas(); + } + + // attach pointer events to the overlaying canvas for coordinates + (function attach(){ + playArea.addEventListener('pointerdown', (e)=>{ playArea.setPointerCapture && playArea.setPointerCapture(e.pointerId); onPointerDown(e); }); + playArea.addEventListener('pointermove', onPointerMove); + window.addEventListener('pointerup', onPointerUp); + })(); + + // start and restart + startBtn.addEventListener('click', ()=>{ + startGame(); + }); + restartBtn.addEventListener('click', ()=>{ + startGame(); + }); + + // quick loop to draw swipe continuously while pointer is down + setInterval(()=>{ + if(pointer.down){ drawSwipe(); checkCollisions(); } + }, 40); + + // initial resize and hint + resize(); + overlay.classList.remove('hidden'); + message.textContent = 'Slice fruits with your mouse or finger. Avoid bombs.'; + // expose small helper for debugging + window.__fruitEntities = entities; +})(); diff --git a/games/fruit-slicer/style.css b/games/fruit-slicer/style.css new file mode 100644 index 00000000..804b1599 --- /dev/null +++ b/games/fruit-slicer/style.css @@ -0,0 +1,27 @@ +:root{ + --bg:#0b1220; + --panel: rgba(255,255,255,0.06); + --accent: #ff6b35; + --score-bg: rgba(0,0,0,0.35); +} +*{box-sizing:border-box} +html,body{height:100%;margin:0;font-family:Inter,Segoe UI,Arial;background:linear-gradient(180deg,var(--bg),#071018);color:#fff} +#game-root{position:relative;height:100vh;overflow:hidden} +#hud{position:absolute;left:12px;top:12px;z-index:40;display:flex;gap:12px;align-items:center} +#score{background:var(--score-bg);padding:8px 12px;border-radius:10px;font-weight:600} +#start-btn{background:var(--accent);color:#111;border:none;padding:8px 12px;border-radius:10px;cursor:pointer} +#play-area{position:absolute;inset:0;z-index:10;} +#swipe-canvas{position:absolute;inset:0;z-index:30;pointer-events:none} +.fruit{ + position:absolute;border-radius:50%;width:64px;height:64px;display:block;z-index:20;user-select:none;touch-action:none;transform:translate(-50%,-50%); + box-shadow:0 8px 18px rgba(0,0,0,0.45); +} +.bomb{background:radial-gradient(circle at 30% 30%,#666,#000);width:56px;height:56px;border-radius:50%;} +.slice-effect{position:absolute;z-index:35;pointer-events:none;mix-blend-mode:screen} +#overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;z-index:60} +#overlay.hidden{display:none} +#overlay #message{background:var(--panel);padding:26px;border-radius:12px;text-align:center;font-size:20px;margin-bottom:12px} +#restart-btn{background:var(--accent);border:none;padding:10px 16px;border-radius:8px;color:#111;cursor:pointer} + +/* small responsive tweaks */ +@media (max-width:480px){.fruit{width:50px;height:50px}.bomb{width:44px;height:44px}} diff --git a/games/fruit_catcher/index.html b/games/fruit_catcher/index.html new file mode 100644 index 00000000..3e3e67bf --- /dev/null +++ b/games/fruit_catcher/index.html @@ -0,0 +1,15 @@ + + + + + +Fruit Catcher Game + + + + + + + + + diff --git a/games/fruit_catcher/readme.md b/games/fruit_catcher/readme.md new file mode 100644 index 00000000..2c491aa4 --- /dev/null +++ b/games/fruit_catcher/readme.md @@ -0,0 +1,23 @@ +## Description +Fruit Catcher is a fun, beginner-friendly arcade-style game built using **HTML, CSS, and JavaScript**. +In this game, colorful fruits fall from the top of the screen, and the player controls a basket at the bottom to catch them. +Catch the fruits to score points, avoid missing them, and aim for the highest score! + +--- + +## How to Play +- Use the **Left Arrow** and **Right Arrow** keys to move the basket. +- Catch falling fruits to **increase your score**. +- Missing a fruit **costs one life**. +- The game ends when you **lose all 3 lives**. +- Refresh the page to restart the game. + +--- + +## โญ Features +- Falling fruits with **increasing difficulty** as you progress. +- **Score and lives** displayed on the screen. +- Responsive and works in any modern web browser. +- **Beginner-friendly**, perfect for learning HTML, CSS, and JS. + +--- \ No newline at end of file diff --git a/games/fruit_catcher/script.js b/games/fruit_catcher/script.js new file mode 100644 index 00000000..ea1d2c1b --- /dev/null +++ b/games/fruit_catcher/script.js @@ -0,0 +1,88 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); + +let basket = { x: 200, y: 350, width: 100, height: 20, speed: 20 }; +let fruits = []; +let score = 0; +let lives = 3; +let fruitSpeed = 2; + +// Generate a new fruit at random x position +function spawnFruit() { + let x = Math.random() * (canvas.width - 20); + fruits.push({ x: x, y: 0, radius: 10 }); +} + +// Draw basket +function drawBasket() { + ctx.fillStyle = 'brown'; + ctx.fillRect(basket.x, basket.y, basket.width, basket.height); +} + +// Draw fruits +function drawFruits() { + ctx.fillStyle = 'red'; + fruits.forEach(fruit => { + ctx.beginPath(); + ctx.arc(fruit.x, fruit.y, fruit.radius, 0, Math.PI*2); + ctx.fill(); + ctx.closePath(); + }); +} + +// Draw score and lives +function drawScoreLives() { + ctx.fillStyle = 'black'; + ctx.font = '16px Arial'; + ctx.fillText(`Score: ${score}`, 10, 20); + ctx.fillText(`Lives: ${lives}`, 400, 20); +} + +// Update game state +function update() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + drawBasket(); + drawFruits(); + drawScoreLives(); + + // Move fruits down + fruits.forEach(fruit => fruit.y += fruitSpeed); + + // Check for catching fruit + fruits.forEach((fruit, index) => { + if ( + fruit.y + fruit.radius >= basket.y && + fruit.x >= basket.x && + fruit.x <= basket.x + basket.width + ) { + score++; + fruits.splice(index, 1); + fruitSpeed += 0.05; // Increase speed slightly + } else if (fruit.y + fruit.radius > canvas.height) { + lives--; + fruits.splice(index, 1); + if (lives === 0) { + alert(`Game Over! Final Score: ${score}`); + document.location.reload(); + } + } + }); + + requestAnimationFrame(update); +} + +// Move basket with arrow keys +document.addEventListener('keydown', (e) => { + if (e.key === 'ArrowLeft' && basket.x > 0) { + basket.x -= basket.speed; + } else if (e.key === 'ArrowRight' && basket.x + basket.width < canvas.width) { + basket.x += basket.speed; + } +}); + +// Spawn fruits every 1 second +setInterval(spawnFruit, 1000); + +// Start the game +update(); diff --git a/games/fruit_catcher/style.css b/games/fruit_catcher/style.css new file mode 100644 index 00000000..4f51b376 --- /dev/null +++ b/games/fruit_catcher/style.css @@ -0,0 +1,14 @@ +body { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background: #c0f0ff; + font-family: Arial, sans-serif; + margin: 0; +} + +canvas { + background: #87ceeb; + border: 3px solid #333; +} diff --git a/games/fruits/index.html b/games/fruits/index.html new file mode 100644 index 00000000..6ed97c40 --- /dev/null +++ b/games/fruits/index.html @@ -0,0 +1,22 @@ + + + + + + Fruits Collecting Game + + + +

    Fruits Collecting Game

    + +
    + Score: 0 + Lives: 3 +
    + + + + + diff --git a/games/fruits/readme.md b/games/fruits/readme.md new file mode 100644 index 00000000..9a44b761 --- /dev/null +++ b/games/fruits/readme.md @@ -0,0 +1,21 @@ +# Fruits Collecting Game + +## Description +A simple and fun arcade-style game where colorful fruits fall from the top of the screen, and you control a basket at the bottom to catch them. Use the left and right arrow keys to move the basket. Catch as many fruits as possible to increase your score, but be carefulโ€”missing fruits will cost you lives! The game becomes more challenging as fruits fall faster and spawn more frequently. Try to beat your high score! + +## How to Play +- Use the **Left Arrow** and **Right Arrow** keys to move the basket. +- Catch falling fruits to increase your score. +- Missing a fruit costs you one life. +- The game ends when you lose all 3 lives. +- Press **R** to restart the game after a game over. + +## Project Structure +- `index.html` โ€” The main HTML page containing the canvas and game interface. +- `style.css` โ€” Styling for the game page and canvas. +- `script.js` โ€” JavaScript file with the game logic, rendering, and controls. + +## Running the Game +1. Clone or download this repository. +2. Open the `index.html` file in a modern web browser (Chrome, Firefox, Edge, Safari). +3. Enjoy the game! diff --git a/games/fruits/script.js b/games/fruits/script.js new file mode 100644 index 00000000..60eb16b8 --- /dev/null +++ b/games/fruits/script.js @@ -0,0 +1,132 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); + +const basketWidth = 80; +const basketHeight = 20; +let basketX = canvas.width / 2 - basketWidth / 2; +const basketY = canvas.height - basketHeight - 10; +const basketSpeed = 7; + +const fruitRadius = 15; +let fruitSpeed = 3; +let spawnInterval = 1500; // milliseconds + +let fruits = []; +let score = 0; +let lives = 3; + +const scoreEl = document.getElementById('score'); +const livesEl = document.getElementById('lives'); +const gameOverEl = document.getElementById('gameOver'); + +let leftPressed = false; +let rightPressed = false; +let gameRunning = true; + +function drawBasket() { + ctx.fillStyle = 'green'; + ctx.fillRect(basketX, basketY, basketWidth, basketHeight); +} + +function drawFruit(fruit) { + ctx.beginPath(); + ctx.fillStyle = 'red'; + ctx.ellipse(fruit.x, fruit.y, fruitRadius, fruitRadius * 0.8, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.closePath(); +} + +function spawnFruit() { + const x = Math.random() * (canvas.width - fruitRadius * 2) + fruitRadius; + fruits.push({ x: x, y: -fruitRadius }); +} + +function update() { + // Move basket + if (leftPressed) basketX -= basketSpeed; + if (rightPressed) basketX += basketSpeed; + basketX = Math.max(0, Math.min(canvas.width - basketWidth, basketX)); + + // Move fruits + for (let i = fruits.length - 1; i >= 0; i--) { + fruits[i].y += fruitSpeed; + + // Check if fruit caught + if ( + fruits[i].y + fruitRadius >= basketY && + fruits[i].x > basketX && + fruits[i].x < basketX + basketWidth + ) { + fruits.splice(i, 1); + score++; + scoreEl.textContent = `Score: ${score}`; + if (score % 5 === 0) { + fruitSpeed = Math.min(fruitSpeed + 0.5, 10); + spawnInterval = Math.max(spawnInterval - 100, 500); + clearInterval(spawnTimer); + spawnTimer = setInterval(spawnFruit, spawnInterval); + } + } + // Check if fruit missed + else if (fruits[i].y - fruitRadius > canvas.height) { + fruits.splice(i, 1); + lives--; + livesEl.textContent = `Lives: ${lives}`; + if (lives <= 0) { + gameOver(); + } + } + } +} + +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawBasket(); + fruits.forEach(drawFruit); +} + +function gameLoop() { + if (!gameRunning) return; + update(); + draw(); + requestAnimationFrame(gameLoop); +} + +function gameOver() { + gameRunning = false; + gameOverEl.classList.remove('hidden'); +} + +function restartGame() { + score = 0; + lives = 3; + fruitSpeed = 3; + spawnInterval = 1500; + fruits = []; + basketX = canvas.width / 2 - basketWidth / 2; + scoreEl.textContent = `Score: ${score}`; + livesEl.textContent = `Lives: ${lives}`; + gameOverEl.classList.add('hidden'); + gameRunning = true; + clearInterval(spawnTimer); + spawnTimer = setInterval(spawnFruit, spawnInterval); + gameLoop(); +} + +// Keyboard controls +document.addEventListener('keydown', (e) => { + if (e.key === 'ArrowLeft') leftPressed = true; + if (e.key === 'ArrowRight') rightPressed = true; + if (!gameRunning && (e.key === 'r' || e.key === 'R')) restartGame(); +}); + +document.addEventListener('keyup', (e) => { + if (e.key === 'ArrowLeft') leftPressed = false; + if (e.key === 'ArrowRight') rightPressed = false; +}); + +// Start spawning fruits +let spawnTimer = setInterval(spawnFruit, spawnInterval); + +// Start game loop +gameLoop(); diff --git a/games/fruits/style.css b/games/fruits/style.css new file mode 100644 index 00000000..041301a5 --- /dev/null +++ b/games/fruits/style.css @@ -0,0 +1,38 @@ +body { + margin: 0; + background: #f0f0f0; + font-family: Arial, sans-serif; + text-align: center; + user-select: none; +} + +h1 { + margin: 20px 0; + color: #333; +} + +#gameCanvas { + background: #fff; + border: 2px solid #333; + display: block; + margin: 0 auto; +} + +#info { + margin-top: 10px; + font-size: 20px; + display: flex; + justify-content: center; + gap: 30px; + color: #333; +} + +#gameOver { + margin-top: 20px; + font-size: 24px; + color: red; +} + +.hidden { + display: none; +} diff --git a/games/galaga_game/index.html b/games/galaga_game/index.html new file mode 100644 index 00000000..05593c81 --- /dev/null +++ b/games/galaga_game/index.html @@ -0,0 +1,14 @@ + + + + + Space Invaders Clone + + + +
    + +
    + + + \ No newline at end of file diff --git a/games/galaga_game/script.js b/games/galaga_game/script.js new file mode 100644 index 00000000..d858f2a3 --- /dev/null +++ b/games/galaga_game/script.js @@ -0,0 +1,93 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const CANVAS_WIDTH = canvas.width; +const CANVAS_HEIGHT = canvas.height; + +// --- Entity Classes --- + +// 1. Player Class (The Ship) +class Player { + constructor() { + this.width = 40; + this.height = 15; + this.x = CANVAS_WIDTH / 2 - this.width / 2; + this.y = CANVAS_HEIGHT - this.height - 20; + this.speed = 5; + this.color = '#00ff00'; // Green + } + + draw() { + ctx.fillStyle = this.color; + // Simple triangular ship shape + ctx.beginPath(); + ctx.moveTo(this.x + this.width / 2, this.y); // Top center + ctx.lineTo(this.x, this.y + this.height); // Bottom left + ctx.lineTo(this.x + this.width, this.y + this.height); // Bottom right + ctx.closePath(); + ctx.fill(); + } + + // Updates position based on input (handled externally) + update(direction) { + if (direction === 'left' && this.x > 0) { + this.x -= this.speed; + } else if (direction === 'right' && this.x < CANVAS_WIDTH - this.width) { + this.x += this.speed; + } + } +} + +// 2. Projectile Class (The Bullet) +class Projectile { + constructor(x, y) { + this.x = x; + this.y = y; + this.radius = 2; + this.speed = 8; + this.color = '#ff0000'; // Red + } + + draw() { + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + ctx.closePath(); + } + + update() { + this.y -= this.speed; // Moves up + } +} + +// 3. Enemy Class (The Alien) +class Enemy { + constructor(x, y) { + this.x = x; + this.y = y; + this.width = 30; + this.height = 20; + this.speedX = 1; // Horizontal movement + this.speedY = 0.05; // Gradual vertical drop + this.color = '#ffff00'; // Yellow + } + + draw() { + ctx.fillStyle = this.color; + ctx.fillRect(this.x, this.y, this.width, this.height); + // Optional: Draw a unique shape or load an image later + } + + // Simple AI: Move horizontally, and reverse at wall + update() { + this.x += this.speedX; + this.y += this.speedY; // Gradually descends + + // Wall bounce logic + if (this.x + this.width > CANVAS_WIDTH || this.x < 0) { + this.speedX = -this.speedX; + // Drop a bit more when hitting a wall + this.y += 10; + } + } +} \ No newline at end of file diff --git a/games/galaga_game/style.css b/games/galaga_game/style.css new file mode 100644 index 00000000..a2522526 --- /dev/null +++ b/games/galaga_game/style.css @@ -0,0 +1,18 @@ +/* style.css */ +body { + background-color: #1a1a2e; /* Deep space color */ + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + overflow: hidden; + font-family: 'Courier New', monospace; +} + +#gameCanvas { + border: 5px solid #0f0; /* Classic green border for a retro look */ + background-color: #000000; /* Black canvas */ + display: block; + cursor: crosshair; /* Target-style cursor */ +} \ No newline at end of file diff --git a/games/garden-grower/index.html b/games/garden-grower/index.html new file mode 100644 index 00000000..751d53e7 --- /dev/null +++ b/games/garden-grower/index.html @@ -0,0 +1,27 @@ + + + + + +Garden Grower ๐ŸŒฑ + + + +
    +

    Garden Grower ๐ŸŒธ

    +
    + + + +
    +
    + +
    +

    +
    + + + + diff --git a/games/garden-grower/script.js b/games/garden-grower/script.js new file mode 100644 index 00000000..3cacd7b0 --- /dev/null +++ b/games/garden-grower/script.js @@ -0,0 +1,75 @@ +const garden = document.getElementById('garden'); +const message = document.getElementById('message'); +const startBtn = document.getElementById('start-btn'); +const pauseBtn = document.getElementById('pause-btn'); +const restartBtn = document.getElementById('restart-btn'); +const bgMusic = document.getElementById('bg-music'); + +let soilSpots = []; +let growthIntervals = []; +let isRunning = false; + +const plantColors = ["#76c893","#ff6f61","#ffcc33","#8e44ad","#3498db"]; + +function createGarden(spots = 12) { + garden.innerHTML = ''; + soilSpots = []; + for (let i = 0; i < spots; i++) { + const soil = document.createElement('div'); + soil.className = 'soil'; + soil.dataset.plantStage = 0; + soil.addEventListener('click', () => plantSeed(soil)); + garden.appendChild(soil); + soilSpots.push(soil); + } +} + +function plantSeed(soil) { + if(!isRunning) return; + if(soil.dataset.plantStage > 0) return; + + soil.dataset.plantStage = 1; + const plant = document.createElement('div'); + plant.className = 'plant'; + plant.style.background = plantColors[Math.floor(Math.random()*plantColors.length)]; + soil.appendChild(plant); + + let stage = 1; + const interval = setInterval(() => { + if(!isRunning) return; + stage++; + if(stage > 5) { + clearInterval(interval); + return; + } + soil.dataset.plantStage = stage; + plant.style.height = 20 + stage*10 + 'px'; + plant.style.width = 20 + stage*10 + 'px'; + plant.style.boxShadow = `0 0 ${10*stage}px ${stage*5}px ${plant.style.background}`; + }, 2000); + growthIntervals.push(interval); +} + +startBtn.addEventListener('click', () => { + isRunning = true; + bgMusic.play(); + message.textContent = "Garden is growing ๐ŸŒฑ"; +}); + +pauseBtn.addEventListener('click', () => { + isRunning = false; + bgMusic.pause(); + message.textContent = "Paused โธ๏ธ"; +}); + +restartBtn.addEventListener('click', () => { + isRunning = false; + bgMusic.pause(); + bgMusic.currentTime = 0; + growthIntervals.forEach(i=>clearInterval(i)); + growthIntervals = []; + createGarden(); + message.textContent = "Restarted ๐ŸŒธ"; +}); + +createGarden(); diff --git a/games/garden-grower/style.css b/games/garden-grower/style.css new file mode 100644 index 00000000..e34cffb8 --- /dev/null +++ b/games/garden-grower/style.css @@ -0,0 +1,70 @@ +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(to top, #a0e1ff, #ffffff); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.game-container { + text-align: center; + background: rgba(255,255,255,0.9); + padding: 20px; + border-radius: 15px; + box-shadow: 0 0 30px rgba(0,0,0,0.3); +} + +h1 { + text-shadow: 0 0 10px #76c893, 0 0 20px #ffcc33; +} + +.controls button { + padding: 10px 20px; + margin: 5px; + font-size: 16px; + border: none; + border-radius: 10px; + cursor: pointer; + background: linear-gradient(45deg, #76c893, #ffcc33); + color: white; + text-shadow: 0 0 5px black; + box-shadow: 0 0 15px #ffcc33; + transition: 0.3s; +} + +.controls button:hover { + box-shadow: 0 0 25px #ffcc33, 0 0 40px #76c893; +} + +#garden { + display: flex; + justify-content: center; + flex-wrap: wrap; + margin-top: 20px; + gap: 10px; +} + +.soil { + width: 60px; + height: 60px; + background: brown; + border-radius: 10px; + position: relative; + cursor: pointer; + box-shadow: inset 0 0 10px #000; +} + +.plant { + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 20px; + height: 20px; + border-radius: 50%; + background: green; + box-shadow: 0 0 15px lime, 0 0 25px yellow; + transition: all 0.5s ease; +} diff --git a/games/gem-swap/index.html b/games/gem-swap/index.html new file mode 100644 index 00000000..c5ade8dc --- /dev/null +++ b/games/gem-swap/index.html @@ -0,0 +1,36 @@ + + + + + + Gem Swap | Mini JS Games Hub + + + +
    +
    +

    ๐Ÿ’Ž Gem Swap

    +
    + + + +
    +
    +

    Score: 0

    +

    Moves: 30

    +
    +
    + +
    + +
    +

    Swap gems to match 3+ and earn points! โœจ

    +
    +
    + + + + + + + diff --git a/games/gem-swap/script.js b/games/gem-swap/script.js new file mode 100644 index 00000000..9de01744 --- /dev/null +++ b/games/gem-swap/script.js @@ -0,0 +1,162 @@ +const board = document.getElementById("board"); +const scoreEl = document.getElementById("score"); +const movesEl = document.getElementById("moves"); +const swapSound = document.getElementById("swap-sound"); +const matchSound = document.getElementById("match-sound"); +const errorSound = document.getElementById("error-sound"); + +const rows = 8; +const cols = 8; +let grid = []; +let score = 0; +let moves = 30; +let firstGem = null; +let paused = false; + +const gemColors = ["#ff4757", "#3742fa", "#2ed573", "#ffa502", "#70a1ff", "#ff6b81"]; +const obstacleChance = 0.08; // 8% obstacles + +function createBoard() { + board.innerHTML = ""; + grid = []; + + for (let r = 0; r < rows; r++) { + let row = []; + for (let c = 0; c < cols; c++) { + const gem = document.createElement("div"); + gem.classList.add("gem"); + + if (Math.random() < obstacleChance) { + gem.classList.add("obstacle"); + gem.style.background = "gray"; + row.push("X"); + } else { + const color = gemColors[Math.floor(Math.random() * gemColors.length)]; + gem.style.background = color; + row.push(color); + } + + gem.dataset.row = r; + gem.dataset.col = c; + gem.addEventListener("click", () => selectGem(gem)); + + board.appendChild(gem); + } + grid.push(row); + } +} + +function selectGem(gem) { + if (paused || gem.classList.contains("obstacle")) return; + + if (!firstGem) { + firstGem = gem; + gem.style.boxShadow = "0 0 20px #00ffff"; + } else { + const r1 = +firstGem.dataset.row, c1 = +firstGem.dataset.col; + const r2 = +gem.dataset.row, c2 = +gem.dataset.col; + + const isAdjacent = Math.abs(r1 - r2) + Math.abs(c1 - c2) === 1; + + if (isAdjacent) { + swapSound.play(); + swapGems(r1, c1, r2, c2); + moves--; + movesEl.textContent = moves; + } else { + errorSound.play(); + } + + firstGem.style.boxShadow = "0 0 10px rgba(255,255,255,0.4)"; + firstGem = null; + } +} + +function swapGems(r1, c1, r2, c2) { + const temp = grid[r1][c1]; + grid[r1][c1] = grid[r2][c2]; + grid[r2][c2] = temp; + renderBoard(); + checkMatches(); +} + +function renderBoard() { + const gems = board.children; + for (let i = 0; i < gems.length; i++) { + const r = Math.floor(i / cols); + const c = i % cols; + const color = grid[r][c]; + gems[i].style.background = color === "X" ? "gray" : color; + gems[i].className = color === "X" ? "gem obstacle" : "gem"; + } +} + +function checkMatches() { + let matched = []; + + // Check horizontal and vertical + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols - 2; c++) { + if (grid[r][c] !== "X" && grid[r][c] === grid[r][c + 1] && grid[r][c] === grid[r][c + 2]) { + matched.push([r, c], [r, c + 1], [r, c + 2]); + } + } + } + for (let c = 0; c < cols; c++) { + for (let r = 0; r < rows - 2; r++) { + if (grid[r][c] !== "X" && grid[r][c] === grid[r + 1][c] && grid[r][c] === grid[r + 2][c]) { + matched.push([r, c], [r + 1, c], [r + 2, c]); + } + } + } + + if (matched.length > 0) { + matchSound.play(); + removeMatches(matched); + } +} + +function removeMatches(matched) { + matched.forEach(([r, c]) => { + grid[r][c] = null; + score += 10; + }); + scoreEl.textContent = score; + dropGems(); +} + +function dropGems() { + for (let c = 0; c < cols; c++) { + for (let r = rows - 1; r >= 0; r--) { + if (grid[r][c] === null) { + for (let k = r - 1; k >= 0; k--) { + if (grid[k][c] !== null && grid[k][c] !== "X") { + grid[r][c] = grid[k][c]; + grid[k][c] = null; + break; + } + } + } + } + } + refillBoard(); + renderBoard(); + checkMatches(); +} + +function refillBoard() { + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + if (grid[r][c] === null) { + grid[r][c] = gemColors[Math.floor(Math.random() * gemColors.length)]; + } + } + } +} + +// Buttons +document.getElementById("start-btn").onclick = () => { paused = false; }; +document.getElementById("pause-btn").onclick = () => { paused = !paused; }; +document.getElementById("restart-btn").onclick = () => { score = 0; moves = 30; createBoard(); renderBoard(); }; + +createBoard(); diff --git a/games/gem-swap/style.css b/games/gem-swap/style.css new file mode 100644 index 00000000..dab14bd2 --- /dev/null +++ b/games/gem-swap/style.css @@ -0,0 +1,70 @@ +body { + font-family: "Poppins", sans-serif; + background: radial-gradient(circle at top, #0f2027, #203a43, #2c5364); + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.game-container { + text-align: center; + background: rgba(255, 255, 255, 0.05); + padding: 20px; + border-radius: 20px; + box-shadow: 0 0 40px rgba(0, 255, 255, 0.3); + width: 420px; +} + +header h1 { + font-size: 2rem; + margin-bottom: 10px; + text-shadow: 0 0 10px cyan; +} + +.controls button { + background: #1f4068; + border: none; + color: #fff; + margin: 5px; + padding: 8px 16px; + border-radius: 10px; + cursor: pointer; + transition: 0.2s; +} + +.controls button:hover { + background: #00adb5; + box-shadow: 0 0 10px #00fff5; +} + +.board { + display: grid; + grid-template-columns: repeat(8, 45px); + grid-template-rows: repeat(8, 45px); + justify-content: center; + gap: 5px; + margin-top: 15px; +} + +.gem { + width: 45px; + height: 45px; + border-radius: 10px; + cursor: pointer; + box-shadow: 0 0 10px rgba(255, 255, 255, 0.4); + transition: transform 0.2s, box-shadow 0.2s; +} + +.gem:hover { + transform: scale(1.1); + box-shadow: 0 0 20px rgba(0, 255, 255, 0.6); +} + +.obstacle { + background: gray !important; + box-shadow: 0 0 5px #000; + cursor: not-allowed; +} diff --git a/games/geography-explorer/index.html b/games/geography-explorer/index.html new file mode 100644 index 00000000..923476f5 --- /dev/null +++ b/games/geography-explorer/index.html @@ -0,0 +1,105 @@ + + + + + + Geography Explorer - Mini JS Games Hub + + + +
    +
    +

    Geography Explorer

    +

    Click on the correct country on the map to earn points!

    +
    + +
    +
    +
    + Score: + 0 +
    +
    + Round: + 0 + / + 10 +
    +
    + Streak: + 0 +
    +
    + Time: + 30 +
    +
    + +
    +
    Find: Loading...
    +
    +
    + +
    + + + +
    + +
    +
    + + + +
    + +
    + +
    + +
    + + +
    +
    + + +
    + + + + \ No newline at end of file diff --git a/games/geography-explorer/script.js b/games/geography-explorer/script.js new file mode 100644 index 00000000..d96b3bc3 --- /dev/null +++ b/games/geography-explorer/script.js @@ -0,0 +1,555 @@ +// Geography Explorer Game +// Click on countries on the map to identify them correctly + +// DOM elements +const scoreEl = document.getElementById('current-score'); +const roundCountEl = document.getElementById('current-round'); +const totalRoundsEl = document.getElementById('total-rounds'); +const streakEl = document.getElementById('current-streak'); +const timeLeftEl = document.getElementById('time-left'); +const countryNameEl = document.getElementById('country-name'); +const questionHintEl = document.getElementById('question-hint'); +const worldMap = document.getElementById('world-map'); +const hintContinentBtn = document.getElementById('hint-continent-btn'); +const hintCapitalBtn = document.getElementById('hint-capital-btn'); +const hintOptionsBtn = document.getElementById('hint-options-btn'); +const multipleChoiceEl = document.getElementById('multiple-choice'); +const choiceBtns = document.querySelectorAll('.choice-btn'); +const messageEl = document.getElementById('message'); +const startBtn = document.getElementById('start-btn'); +const quitBtn = document.getElementById('quit-btn'); +const resultsEl = document.getElementById('results'); +const finalScoreEl = document.getElementById('final-score'); +const countriesFoundEl = document.getElementById('countries-found'); +const countriesTotalEl = document.getElementById('countries-total'); +const accuracyEl = document.getElementById('accuracy'); +const bestStreakEl = document.getElementById('best-streak'); +const gradeEl = document.getElementById('grade'); +const playAgainBtn = document.getElementById('play-again-btn'); + +// Game variables +let currentRound = 0; +let score = 0; +let streak = 0; +let bestStreak = 0; +let timeLeft = 30; +let timerInterval = null; +let gameActive = false; +let countries = []; +let currentCountry = null; +let hintsUsed = { + continent: false, + capital: false, + options: false +}; +let countriesFound = 0; + +// Country data with simplified SVG paths +const countryData = [ + { + name: "United States", + continent: "North America", + capital: "Washington D.C.", + path: "M150,200 L180,200 L180,180 L200,180 L200,160 L220,160 L220,140 L240,140 L240,120 L260,120 L260,100 L280,100 L280,120 L300,120 L300,140 L280,140 L280,160 L260,160 L260,180 L240,180 L240,200 L220,200 L220,220 L200,220 L200,240 L180,240 L180,220 L160,220 Z" + }, + { + name: "Canada", + continent: "North America", + capital: "Ottawa", + path: "M140,180 L160,180 L160,160 L180,160 L180,140 L200,140 L200,120 L220,120 L220,100 L240,100 L240,80 L260,80 L260,100 L240,100 L240,120 L220,120 L220,140 L200,140 L200,160 L180,160 L180,180 L160,180 Z" + }, + { + name: "Brazil", + continent: "South America", + capital: "Brasรญlia", + path: "M280,280 L300,280 L300,300 L320,300 L320,320 L300,320 L300,340 L280,340 L280,320 L260,320 L260,300 L280,300 Z" + }, + { + name: "United Kingdom", + continent: "Europe", + capital: "London", + path: "M480,160 L500,160 L500,180 L480,180 Z" + }, + { + name: "France", + continent: "Europe", + capital: "Paris", + path: "M480,180 L500,180 L500,200 L480,200 Z" + }, + { + name: "Germany", + continent: "Europe", + capital: "Berlin", + path: "M500,160 L520,160 L520,180 L500,180 Z" + }, + { + name: "Italy", + continent: "Europe", + capital: "Rome", + path: "M500,180 L520,180 L520,200 L500,200 Z" + }, + { + name: "Spain", + continent: "Europe", + capital: "Madrid", + path: "M460,200 L480,200 L480,220 L460,220 Z" + }, + { + name: "Russia", + continent: "Europe/Asia", + capital: "Moscow", + path: "M520,140 L600,140 L600,160 L580,160 L580,180 L560,180 L560,160 L540,160 L540,140 Z" + }, + { + name: "China", + continent: "Asia", + capital: "Beijing", + path: "M620,180 L680,180 L680,200 L660,200 L660,220 L640,220 L640,200 L620,200 Z" + }, + { + name: "Japan", + continent: "Asia", + capital: "Tokyo", + path: "M700,180 L720,180 L720,200 L700,200 Z" + }, + { + name: "India", + continent: "Asia", + capital: "New Delhi", + path: "M580,220 L620,220 L620,240 L600,240 L600,260 L580,260 Z" + }, + { + name: "Australia", + continent: "Australia", + capital: "Canberra", + path: "M650,320 L700,320 L700,340 L680,340 L680,360 L650,360 Z" + }, + { + name: "Egypt", + continent: "Africa", + capital: "Cairo", + path: "M500,240 L520,240 L520,260 L500,260 Z" + }, + { + name: "South Africa", + continent: "Africa", + capital: "Cape Town", + path: "M500,300 L520,300 L520,320 L500,320 Z" + }, + { + name: "Mexico", + continent: "North America", + capital: "Mexico City", + path: "M180,240 L200,240 L200,260 L180,260 Z" + }, + { + name: "Argentina", + continent: "South America", + capital: "Buenos Aires", + path: "M260,340 L280,340 L280,360 L260,360 Z" + }, + { + name: "South Korea", + continent: "Asia", + capital: "Seoul", + path: "M660,180 L680,180 L680,200 L660,200 Z" + }, + { + name: "Turkey", + continent: "Europe/Asia", + capital: "Ankara", + path: "M520,200 L540,200 L540,220 L520,220 Z" + }, + { + name: "Saudi Arabia", + continent: "Asia", + capital: "Riyadh", + path: "M540,240 L560,240 L560,260 L540,260 Z" + } +]; + +// Initialize game +function initGame() { + shuffleCountries(); + setupEventListeners(); + createMap(); + updateDisplay(); +} + +// Shuffle countries for random order +function shuffleCountries() { + countries = [...countryData]; + for (let i = countries.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [countries[i], countries[j]] = [countries[j], countries[i]]; + } + // Take first 10 countries + countries = countries.slice(0, 10); + totalRoundsEl.textContent = countries.length; +} + +// Create the world map +function createMap() { + worldMap.innerHTML = ''; + + // Create all country paths + countryData.forEach(country => { + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('d', country.path); + path.setAttribute('class', 'country'); + path.setAttribute('data-country', country.name); + path.addEventListener('click', () => selectCountry(country.name)); + worldMap.appendChild(path); + }); +} + +// Setup event listeners +function setupEventListeners() { + startBtn.addEventListener('click', startGame); + quitBtn.addEventListener('click', endGame); + playAgainBtn.addEventListener('click', resetGame); + + hintContinentBtn.addEventListener('click', () => useHint('continent')); + hintCapitalBtn.addEventListener('click', () => useHint('capital')); + hintOptionsBtn.addEventListener('click', () => useHint('options')); + + choiceBtns.forEach(btn => { + btn.addEventListener('click', () => selectMultipleChoice(btn)); + }); +} + +// Start the game +function startGame() { + gameActive = true; + currentRound = 0; + score = 0; + streak = 0; + bestStreak = 0; + countriesFound = 0; + timeLeft = 30; + + hintsUsed = { + continent: false, + capital: false, + options: false + }; + + startBtn.style.display = 'none'; + quitBtn.style.display = 'inline-block'; + + hintContinentBtn.disabled = false; + hintCapitalBtn.disabled = false; + hintOptionsBtn.disabled = false; + + resultsEl.style.display = 'none'; + messageEl.textContent = ''; + multipleChoiceEl.style.display = 'none'; + + // Reset all country colors + document.querySelectorAll('.country').forEach(country => { + country.classList.remove('correct', 'incorrect', 'highlight'); + }); + + loadRound(); +} + +// Load current round +function loadRound() { + if (currentRound >= countries.length) { + endGame(); + return; + } + + currentCountry = countries[currentRound]; + hintsUsed = { + continent: false, + capital: false, + options: false + }; + + // Update UI + countryNameEl.textContent = currentCountry.name; + questionHintEl.textContent = ''; + + // Reset multiple choice + multipleChoiceEl.style.display = 'none'; + + // Start timer + startTimer(); + + updateDisplay(); +} + +// Start round timer +function startTimer() { + if (timerInterval) clearInterval(timerInterval); + + timerInterval = setInterval(() => { + timeLeft--; + timeLeftEl.textContent = timeLeft; + + if (timeLeft <= 0) { + clearInterval(timerInterval); + timeUp(); + } + }, 1000); +} + +// Handle time up +function timeUp() { + showMessage('Time\'s up! The correct country was highlighted.', 'incorrect'); + highlightCorrectCountry(); + streak = 0; + setTimeout(nextRound, 3000); +} + +// Select country on map +function selectCountry(countryName) { + if (!gameActive) return; + + clearInterval(timerInterval); + + const selectedCountry = countryData.find(c => c.name === countryName); + const correctCountry = currentCountry; + + if (countryName === correctCountry.name) { + correctAnswer(selectedCountry); + } else { + incorrectAnswer(selectedCountry, correctCountry); + } +} + +// Handle correct answer +function correctAnswer(country) { + countriesFound++; + streak++; + if (streak > bestStreak) bestStreak = streak; + + // Calculate points + let points = 10; + + // Time bonus + if (timeLeft >= 25) points += 10; + else if (timeLeft >= 20) points += 5; + + // Streak bonus + if (streak >= 3) points += streak * 2; + + // Hint penalty + const hintsUsedCount = Object.values(hintsUsed).filter(Boolean).length; + points = Math.max(points - hintsUsedCount * 5, 5); + + score += points; + + // Highlight correct country + const countryPath = document.querySelector(`[data-country="${country.name}"]`); + countryPath.classList.add('correct'); + + showMessage(`Correct! +${points} points (Streak: ${streak})`, 'correct'); + setTimeout(nextRound, 2000); +} + +// Handle incorrect answer +function incorrectAnswer(selected, correct) { + streak = 0; + + // Highlight selected country as incorrect + const selectedPath = document.querySelector(`[data-country="${selected.name}"]`); + selectedPath.classList.add('incorrect'); + + // Highlight correct country + highlightCorrectCountry(); + + showMessage(`Incorrect! That was ${selected.name}. The correct answer was ${correct.name}.`, 'incorrect'); + setTimeout(nextRound, 3000); +} + +// Highlight correct country +function highlightCorrectCountry() { + const correctPath = document.querySelector(`[data-country="${currentCountry.name}"]`); + correctPath.classList.add('highlight'); +} + +// Use hint +function useHint(hintType) { + if (!gameActive || hintsUsed[hintType]) return; + + let cost = 0; + let hintText = ''; + + switch (hintType) { + case 'continent': + cost = 5; + hintText = `Continent: ${currentCountry.continent}`; + break; + case 'capital': + cost = 10; + hintText = `Capital: ${currentCountry.capital}`; + break; + case 'options': + cost = 15; + showMultipleChoice(); + break; + } + + if (score < cost) { + showMessage(`Not enough points! Need ${cost} points for this hint.`, 'incorrect'); + return; + } + + if (hintType !== 'options') { + score -= cost; + hintsUsed[hintType] = true; + questionHintEl.textContent = hintText; + showMessage(`Hint used! -${cost} points`, 'hint'); + updateDisplay(); + } +} + +// Show multiple choice options +function showMultipleChoice() { + if (hintsUsed.options) return; + + const cost = 15; + if (score < cost) { + showMessage(`Not enough points! Need ${cost} points for multiple choice.`, 'incorrect'); + return; + } + + score -= cost; + hintsUsed.options = true; + + // Create options (correct + 3 random wrong) + const options = [currentCountry.name]; + const wrongCountries = countryData.filter(c => c.name !== currentCountry.name); + + while (options.length < 4) { + const randomCountry = wrongCountries[Math.floor(Math.random() * wrongCountries.length)]; + if (!options.includes(randomCountry.name)) { + options.push(randomCountry.name); + } + } + + // Shuffle options + for (let i = options.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [options[i], options[j]] = [options[j], options[i]]; + } + + // Set button texts + choiceBtns.forEach((btn, index) => { + btn.textContent = options[index]; + btn.classList.remove('correct', 'incorrect'); + }); + + multipleChoiceEl.style.display = 'block'; + showMessage(`Multiple choice activated! -${cost} points`, 'hint'); + updateDisplay(); +} + +// Select multiple choice answer +function selectMultipleChoice(btn) { + if (!gameActive) return; + + clearInterval(timerInterval); + + const selectedCountry = btn.textContent; + const correctCountry = currentCountry.name; + + choiceBtns.forEach(b => b.disabled = true); + + if (selectedCountry === correctCountry) { + btn.classList.add('correct'); + correctAnswer(currentCountry); + } else { + btn.classList.add('incorrect'); + // Find and highlight correct button + choiceBtns.forEach(b => { + if (b.textContent === correctCountry) { + b.classList.add('correct'); + } + }); + incorrectAnswer({name: selectedCountry}, currentCountry); + } +} + +// Next round +function nextRound() { + currentRound++; + timeLeft = 30; + loadRound(); +} + +// Show message +function showMessage(text, type) { + messageEl.textContent = text; + messageEl.className = `message ${type}`; +} + +// End game +function endGame() { + gameActive = false; + clearInterval(timerInterval); + + // Show results + showResults(); +} + +// Show final results +function showResults() { + const accuracy = countries.length > 0 ? Math.round((countriesFound / countries.length) * 100) : 0; + + finalScoreEl.textContent = score.toLocaleString(); + countriesFoundEl.textContent = countriesFound; + countriesTotalEl.textContent = countries.length; + accuracyEl.textContent = accuracy + '%'; + bestStreakEl.textContent = bestStreak; + + // Calculate grade + let grade = 'F'; + if (accuracy >= 90) grade = 'A'; + else if (accuracy >= 80) grade = 'B'; + else if (accuracy >= 70) grade = 'C'; + else if (accuracy >= 60) grade = 'D'; + + gradeEl.textContent = grade; + gradeEl.className = `final-value grade ${grade}`; + + resultsEl.style.display = 'block'; + startBtn.style.display = 'none'; + quitBtn.style.display = 'none'; + + hintContinentBtn.disabled = true; + hintCapitalBtn.disabled = true; + hintOptionsBtn.disabled = true; +} + +// Reset game +function resetGame() { + resultsEl.style.display = 'none'; + startBtn.style.display = 'inline-block'; + quitBtn.style.display = 'none'; + + hintContinentBtn.disabled = true; + hintCapitalBtn.disabled = true; + hintOptionsBtn.disabled = true; + + shuffleCountries(); + createMap(); + updateDisplay(); + messageEl.textContent = 'Ready for another geography exploration?'; +} + +// Update display elements +function updateDisplay() { + scoreEl.textContent = score.toLocaleString(); + roundCountEl.textContent = currentRound + 1; + streakEl.textContent = streak; + timeLeftEl.textContent = timeLeft; +} + +// Initialize the game +initGame(); + +// This geography game includes clickable map, hints, scoring, and multiple rounds +// Players explore world geography by identifying countries on an interactive map \ No newline at end of file diff --git a/games/geography-explorer/style.css b/games/geography-explorer/style.css new file mode 100644 index 00000000..014dd7ef --- /dev/null +++ b/games/geography-explorer/style.css @@ -0,0 +1,413 @@ +/* Geography Explorer Game Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.container { + max-width: 1200px; + width: 100%; + background: white; + border-radius: 20px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +header { + background: linear-gradient(135deg, #00b894 0%, #00cec9 100%); + color: white; + padding: 30px; + text-align: center; +} + +header h1 { + font-size: 2.5rem; + margin-bottom: 10px; + font-weight: 700; +} + +header p { + font-size: 1.1rem; + opacity: 0.9; +} + +.game-area { + padding: 30px; +} + +.stats-panel { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 20px; + margin-bottom: 20px; + padding: 20px; + background: #f8f9fa; + border-radius: 15px; +} + +.stat { + display: flex; + align-items: center; + gap: 8px; + font-size: 1.1rem; + font-weight: 600; +} + +.stat-label { + color: #666; +} + +.stat-value { + color: #333; + font-weight: 700; +} + +.stat-separator { + color: #999; + margin: 0 5px; +} + +.question-section { + text-align: center; + margin-bottom: 20px; + padding: 20px; + background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%); + border-radius: 15px; +} + +.question-text { + font-size: 1.5rem; + font-weight: 700; + color: #333; + margin-bottom: 10px; +} + +#country-name { + color: #d63031; + font-size: 1.8rem; +} + +.question-hint { + font-size: 1.1rem; + color: #666; + font-style: italic; + min-height: 1.5rem; +} + +.map-container { + margin-bottom: 20px; + border-radius: 15px; + overflow: hidden; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + background: #e8f4fd; +} + +#world-map { + width: 100%; + height: auto; + max-height: 400px; + display: block; + background: #e8f4fd; +} + +/* Country styling */ +.country { + fill: #dfe6e9; + stroke: #b2bec3; + stroke-width: 0.5; + transition: all 0.3s ease; + cursor: pointer; +} + +.country:hover { + fill: #74b9ff; + stroke: #0984e3; + stroke-width: 1; +} + +.country.correct { + fill: #00b894 !important; + stroke: #00cec9 !important; + stroke-width: 2; +} + +.country.incorrect { + fill: #d63031 !important; + stroke: #e17055 !important; + stroke-width: 2; +} + +.country.highlight { + fill: #fdcb6e !important; + stroke: #e17055 !important; + stroke-width: 2; +} + +.hint-section { + margin-bottom: 20px; +} + +.hint-buttons { + display: flex; + gap: 10px; + justify-content: center; + flex-wrap: wrap; + margin-bottom: 15px; +} + +.hint-btn { + padding: 10px 15px; + background: #f8f9fa; + border: 2px solid #dee2e6; + border-radius: 25px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + color: #666; +} + +.hint-btn:hover { + background: #e9ecef; + border-color: #adb5bd; +} + +.hint-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.multiple-choice { + background: rgba(255, 255, 255, 0.9); + border-radius: 10px; + padding: 15px; +} + +.choice-buttons { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; +} + +.choice-btn { + padding: 12px; + background: #f8f9fa; + border: 2px solid #dee2e6; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + text-align: center; +} + +.choice-btn:hover { + background: #e9ecef; + border-color: #adb5bd; +} + +.choice-btn.correct { + background: #d4edda; + border-color: #c3e6cb; + color: #155724; +} + +.choice-btn.incorrect { + background: #f8d7da; + border-color: #f5c6cb; + color: #721c24; +} + +.message { + text-align: center; + font-size: 1.2rem; + font-weight: 600; + min-height: 2rem; + margin: 20px 0; + padding: 15px; + border-radius: 10px; + transition: all 0.3s ease; +} + +.message.correct { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.message.incorrect { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.message.hint { + background: #fff3cd; + color: #856404; + border: 1px solid #ffeaa7; +} + +.controls { + text-align: center; + margin-top: 20px; +} + +.primary-btn, .secondary-btn { + padding: 12px 24px; + border: none; + border-radius: 25px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.primary-btn { + background: linear-gradient(135deg, #0984e3 0%, #74b9ff 100%); + color: white; +} + +.primary-btn:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(9, 132, 227, 0.4); +} + +.secondary-btn { + background: #f8f9fa; + color: #666; + border: 2px solid #e0e0e0; +} + +.secondary-btn:hover { + background: #e9ecef; + border-color: #dee2e6; +} + +.results { + padding: 30px; + text-align: center; + background: linear-gradient(135deg, #0984e3 0%, #74b9ff 100%); + color: white; +} + +.results h2 { + font-size: 2rem; + margin-bottom: 30px; +} + +.final-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.final-stat { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + backdrop-filter: blur(10px); +} + +.final-label { + display: block; + font-size: 0.9rem; + opacity: 0.8; + margin-bottom: 5px; +} + +.final-value { + display: block; + font-size: 1.8rem; + font-weight: 700; +} + +.final-separator { + margin: 0 5px; +} + +.grade { + font-size: 2rem !important; +} + +.grade.A { color: #28a745; } +.grade.B { color: #17a2b8; } +.grade.C { color: #ffc107; } +.grade.D { color: #fd7e14; } +.grade.F { color: #dc3545; } + +/* Responsive Design */ +@media (max-width: 768px) { + header { + padding: 20px; + } + + header h1 { + font-size: 2rem; + } + + .game-area { + padding: 20px; + } + + .stats-panel { + flex-direction: column; + gap: 10px; + } + + .question-text { + font-size: 1.2rem; + } + + #country-name { + font-size: 1.5rem; + } + + .hint-buttons { + flex-direction: column; + } + + .choice-buttons { + grid-template-columns: 1fr; + } + + .final-stats { + grid-template-columns: 1fr; + } +} + +/* Animations */ +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +.country.correct { + animation: pulse 0.6s ease-out; +} + +.country.incorrect { + animation: shake 0.6s ease-out; +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } +} \ No newline at end of file diff --git a/games/ghost-grabber/index.html b/games/ghost-grabber/index.html new file mode 100644 index 00000000..8f9462a7 --- /dev/null +++ b/games/ghost-grabber/index.html @@ -0,0 +1,25 @@ + + + + + + Ghost Grabber | Mini JS Games Hub + + + +
    +

    Ghost Grabber ๐Ÿ‘ป

    +
    + + + + Score: 0 + Combo: 0 +
    +
    + + +
    + + + diff --git a/games/ghost-grabber/script.js b/games/ghost-grabber/script.js new file mode 100644 index 00000000..4240163d --- /dev/null +++ b/games/ghost-grabber/script.js @@ -0,0 +1,89 @@ +const gameArea = document.getElementById("game-area"); +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const restartBtn = document.getElementById("restart-btn"); +const scoreEl = document.getElementById("score"); +const comboEl = document.getElementById("combo"); + +const popSound = document.getElementById("pop-sound"); +const missSound = document.getElementById("miss-sound"); + +let gameInterval; +let spawnInterval = 1500; +let ghosts = []; +let score = 0; +let combo = 0; +let paused = false; + +function randomPosition() { + const x = Math.random() * (gameArea.clientWidth - 60); + const y = Math.random() * (gameArea.clientHeight - 60); + return { x, y }; +} + +function spawnGhost() { + const ghost = document.createElement("div"); + ghost.classList.add("ghost"); + + const pos = randomPosition(); + ghost.style.left = pos.x + "px"; + ghost.style.top = pos.y + "px"; + + gameArea.appendChild(ghost); + ghosts.push(ghost); + + ghost.addEventListener("click", () => { + score += 10 + combo * 2; + combo++; + scoreEl.textContent = score; + comboEl.textContent = combo; + popSound.currentTime = 0; + popSound.play(); + removeGhost(ghost); + }); + + setTimeout(() => { + if (ghosts.includes(ghost)) { + removeGhost(ghost); + combo = 0; + comboEl.textContent = combo; + missSound.currentTime = 0; + missSound.play(); + } + }, 2500); +} + +function removeGhost(ghost) { + gameArea.removeChild(ghost); + ghosts = ghosts.filter(g => g !== ghost); +} + +function startGame() { + if (gameInterval) clearInterval(gameInterval); + paused = false; + gameInterval = setInterval(() => { + if (!paused) { + spawnGhost(); + if (spawnInterval > 500) spawnInterval -= 10; // gradually faster + } + }, spawnInterval); +} + +function pauseGame() { + paused = !paused; +} + +function restartGame() { + ghosts.forEach(g => g.remove()); + ghosts = []; + score = 0; + combo = 0; + scoreEl.textContent = score; + comboEl.textContent = combo; + spawnInterval = 1500; + paused = false; +} + +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", pauseGame); +restartBtn.addEventListener("click", restartGame); diff --git a/games/ghost-grabber/style.css b/games/ghost-grabber/style.css new file mode 100644 index 00000000..b4c37545 --- /dev/null +++ b/games/ghost-grabber/style.css @@ -0,0 +1,75 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background-color: #000; + color: #fff; + display: flex; + flex-direction: column; + align-items: center; +} + +.game-container { + text-align: center; + max-width: 800px; + width: 100%; +} + +h1 { + font-size: 2rem; + text-shadow: 0 0 10px #fff, 0 0 20px #0ff, 0 0 30px #0ff; + margin: 20px 0; +} + +.controls { + margin-bottom: 15px; +} + +button { + padding: 8px 15px; + margin: 0 5px; + font-size: 16px; + cursor: pointer; + border-radius: 5px; + border: none; + background-color: #0ff; + color: #000; + box-shadow: 0 0 10px #0ff; + transition: 0.2s; +} + +button:hover { + transform: scale(1.1); + box-shadow: 0 0 20px #0ff; +} + +.game-area { + position: relative; + width: 100%; + height: 500px; + border: 2px solid #0ff; + border-radius: 10px; + overflow: hidden; + background: radial-gradient(circle at center, #111 0%, #000 100%); +} + +.ghost { + position: absolute; + width: 60px; + height: 60px; + background-image: url("https://i.postimg.cc/6Qd1gKZC/ghost.png"); + background-size: cover; + pointer-events: auto; + animation: glow 1s infinite alternate; +} + +@keyframes glow { + 0% { filter: drop-shadow(0 0 5px #0ff); transform: scale(1); } + 50% { filter: drop-shadow(0 0 15px #0ff); transform: scale(1.1); } + 100% { filter: drop-shadow(0 0 5px #0ff); transform: scale(1); } +} + +.score, .combo { + display: inline-block; + margin-left: 20px; + font-weight: bold; +} diff --git a/games/ghost-tap/index.html b/games/ghost-tap/index.html new file mode 100644 index 00000000..08d2a0ad --- /dev/null +++ b/games/ghost-tap/index.html @@ -0,0 +1,33 @@ + + + + + + Ghost Tap ๐Ÿ‘ป + + + +
    +

    ๐Ÿ‘ป Ghost Tap

    +
    + Score: 0 + Lives: 3 + Level: 1 +
    + +
    + +
    + + + +
    + + + + +
    + + + + diff --git a/games/ghost-tap/script.js b/games/ghost-tap/script.js new file mode 100644 index 00000000..29f1db71 --- /dev/null +++ b/games/ghost-tap/script.js @@ -0,0 +1,112 @@ +const gameArea = document.getElementById("game-area"); +const scoreEl = document.getElementById("score"); +const livesEl = document.getElementById("lives"); +const levelEl = document.getElementById("level"); + +const clickSound = document.getElementById("click-sound"); +const missSound = document.getElementById("miss-sound"); +const bgMusic = document.getElementById("bg-music"); + +let score = 0; +let lives = 3; +let level = 1; +let gameInterval = null; +let spawnRate = 1200; +let isPaused = false; + +document.getElementById("start-btn").addEventListener("click", startGame); +document.getElementById("pause-btn").addEventListener("click", togglePause); +document.getElementById("restart-btn").addEventListener("click", restartGame); + +function startGame() { + if (gameInterval) return; + bgMusic.play(); + gameInterval = setInterval(spawnGhost, spawnRate); +} + +function togglePause() { + if (!gameInterval) return; + isPaused = !isPaused; + if (isPaused) { + clearInterval(gameInterval); + bgMusic.pause(); + } else { + bgMusic.play(); + gameInterval = setInterval(spawnGhost, spawnRate); + } +} + +function restartGame() { + clearInterval(gameInterval); + bgMusic.currentTime = 0; + bgMusic.pause(); + score = 0; + lives = 3; + level = 1; + spawnRate = 1200; + updateStats(); + gameArea.innerHTML = ""; + gameInterval = null; + isPaused = false; +} + +function spawnGhost() { + const ghost = document.createElement("div"); + const isReal = Math.random() > 0.4; + ghost.className = "ghost" + (isReal ? "" : " fake"); + + const x = Math.random() * (gameArea.clientWidth - 80); + const y = Math.random() * (gameArea.clientHeight - 80); + ghost.style.left = `${x}px`; + ghost.style.top = `${y}px`; + + gameArea.appendChild(ghost); + + ghost.addEventListener("click", () => { + if (isReal) { + clickSound.currentTime = 0; + clickSound.play(); + score += 10; + } else { + missSound.currentTime = 0; + missSound.play(); + score -= 5; + lives--; + } + ghost.remove(); + updateStats(); + }); + + setTimeout(() => { + if (gameArea.contains(ghost)) { + gameArea.removeChild(ghost); + if (isReal) { + lives--; + updateStats(); + } + } + }, 800 - level * 50); + + if (score >= level * 100) nextLevel(); + if (lives <= 0) endGame(); +} + +function nextLevel() { + level++; + spawnRate = Math.max(400, spawnRate - 100); + clearInterval(gameInterval); + gameInterval = setInterval(spawnGhost, spawnRate); +} + +function updateStats() { + scoreEl.textContent = score; + livesEl.textContent = lives; + levelEl.textContent = level; +} + +function endGame() { + clearInterval(gameInterval); + bgMusic.pause(); + alert(`๐Ÿ’€ Game Over!\nYour Score: ${score}`); + restartGame(); +} diff --git a/games/ghost-tap/style.css b/games/ghost-tap/style.css new file mode 100644 index 00000000..87227ef7 --- /dev/null +++ b/games/ghost-tap/style.css @@ -0,0 +1,77 @@ +body { + margin: 0; + font-family: 'Poppins', sans-serif; + background: radial-gradient(circle at center, #0d0d0d 60%, #111); + color: #fff; + overflow: hidden; +} + +.game-container { + text-align: center; + padding: 20px; +} + +h1 { + font-size: 2.2rem; + text-shadow: 0 0 10px #8af, 0 0 20px #4df; +} + +.scoreboard { + display: flex; + justify-content: center; + gap: 2rem; + margin: 10px 0; + font-size: 1.2rem; +} + +.controls { + margin-top: 15px; +} + +button { + background: linear-gradient(90deg, #4df, #8af); + border: none; + color: #111; + font-weight: 600; + font-size: 1rem; + padding: 10px 18px; + margin: 5px; + border-radius: 8px; + cursor: pointer; + transition: 0.3s; +} + +button:hover { + box-shadow: 0 0 20px #4df; +} + +.game-area { + position: relative; + width: 90vw; + height: 70vh; + margin: 0 auto; + border: 3px solid #4df; + border-radius: 10px; + background: rgba(0, 0, 0, 0.4); + overflow: hidden; + cursor: crosshair; +} + +.ghost { + position: absolute; + width: 70px; + height: 70px; + background: url("https://cdn-icons-png.flaticon.com/512/778/778093.png") no-repeat center/contain; + animation: glow 0.8s infinite alternate; + filter: drop-shadow(0 0 8px #4df); +} + +.fake { + background: url("https://cdn-icons-png.flaticon.com/512/616/616408.png") no-repeat center/contain; + filter: drop-shadow(0 0 8px red); +} + +@keyframes glow { + from { transform: scale(1); filter: drop-shadow(0 0 5px #4df); } + to { transform: scale(1.1); filter: drop-shadow(0 0 20px #4df); } +} diff --git a/games/glow-chain/index.html b/games/glow-chain/index.html new file mode 100644 index 00000000..2ec89fc6 --- /dev/null +++ b/games/glow-chain/index.html @@ -0,0 +1,26 @@ + + + + + + Glow Chain | Mini JS Games Hub + + + +
    +

    Glow Chain

    +
    +
    + + + + +
    +

    Score: 0

    +

    +
    + + + + + diff --git a/games/glow-chain/script.js b/games/glow-chain/script.js new file mode 100644 index 00000000..896c7dc6 --- /dev/null +++ b/games/glow-chain/script.js @@ -0,0 +1,82 @@ +const bulbLine = document.getElementById("bulb-line"); +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const resumeBtn = document.getElementById("resume-btn"); +const restartBtn = document.getElementById("restart-btn"); +const scoreEl = document.getElementById("score"); +const messageEl = document.getElementById("message"); + +const popSound = document.getElementById("pop-sound"); +const bgMusic = document.getElementById("bg-music"); + +const TOTAL_BULBS = 20; +let bulbs = []; +let chainInterval = null; +let isPaused = false; +let currentIndex = 0; +let score = 0; + +// Create bulbs +function createBulbs() { + bulbLine.innerHTML = ""; + bulbs = []; + for (let i = 0; i < TOTAL_BULBS; i++) { + const bulb = document.createElement("div"); + bulb.classList.add("bulb"); + bulbLine.appendChild(bulb); + bulbs.push(bulb); + } +} + +// Chain animation +function startChain() { + bgMusic.play(); + messageEl.textContent = ""; + chainInterval = setInterval(() => { + if (isPaused) return; + if (currentIndex >= bulbs.length) { + clearInterval(chainInterval); + messageEl.textContent = "Chain Finished!"; + return; + } + bulbs[currentIndex].classList.add("active"); + popSound.play(); + score++; + scoreEl.textContent = `Score: ${score}`; + currentIndex++; + }, 300); +} + +function pauseChain() { + isPaused = true; +} + +function resumeChain() { + if (!chainInterval) return startChain(); + isPaused = false; +} + +function restartChain() { + clearInterval(chainInterval); + bulbs.forEach(b => b.classList.remove("active")); + currentIndex = 0; + score = 0; + scoreEl.textContent = `Score: ${score}`; + messageEl.textContent = ""; + isPaused = false; + startChain(); +} + +// Event listeners +startBtn.addEventListener("click", () => { + createBulbs(); + restartChain(); +}); + +pauseBtn.addEventListener("click", pauseChain); +resumeBtn.addEventListener("click", resumeChain); +restartBtn.addEventListener("click", restartChain); + +// Initial setup +createBulbs(); +scoreEl.textContent = `Score: ${score}`; diff --git a/games/glow-chain/style.css b/games/glow-chain/style.css new file mode 100644 index 00000000..52519547 --- /dev/null +++ b/games/glow-chain/style.css @@ -0,0 +1,62 @@ +body { + font-family: 'Arial', sans-serif; + background: #0d0d0d; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.game-container { + text-align: center; +} + +.bulb-line { + display: flex; + justify-content: center; + gap: 15px; + margin: 30px 0; +} + +.bulb { + width: 30px; + height: 30px; + border-radius: 50%; + background: #444; + box-shadow: 0 0 10px #444; + transition: all 0.2s; +} + +.bulb.active { + background: #ffeb3b; + box-shadow: 0 0 20px #ffeb3b, 0 0 40px #ffeb3b, 0 0 60px #ffd700; +} + +.controls button { + padding: 10px 20px; + margin: 5px; + cursor: pointer; + font-weight: bold; + border-radius: 5px; + border: none; + background: #333; + color: #fff; + transition: 0.2s; +} + +.controls button:hover { + background: #555; +} + +#score { + font-size: 1.2rem; + margin-top: 10px; +} + +#message { + margin-top: 10px; + font-size: 1rem; + color: #ff5252; +} diff --git a/games/glow-drops/index.html b/games/glow-drops/index.html new file mode 100644 index 00000000..82359627 --- /dev/null +++ b/games/glow-drops/index.html @@ -0,0 +1,87 @@ + + + + + + Glow Drops โ€” Mini JS Games Hub + + + + + + + +
    +
    +
    + ๐Ÿ’ง +
    +

    Glow Drops

    +

    Tap glowing drops, build ripples โ€” avoid obstacles!

    +
    +
    + +
    + + + + + + + + + + Back to Hub +
    +
    + +
    +
    +
    Score: 0
    +
    Lives: 3
    +
    Time: 60s
    +
    Combo: x1
    +
    + +
    + + + + +
    + + +
    + +
    +
    + Made for Mini JS Games Hub โ€ข HTML โ€ข CSS โ€ข JS +
    +
    +
    + + + + diff --git a/games/glow-drops/script.js b/games/glow-drops/script.js new file mode 100644 index 00000000..75b19065 --- /dev/null +++ b/games/glow-drops/script.js @@ -0,0 +1,473 @@ +/* Glow Drops - advanced canvas game + Uses: + - Unsplash hotlink for background + - WebAudio synth for drop pop sound + - Online sample for background music (streamed) + Place in games/glow-drops/script.js +*/ + +(() => { + // Config + const config = { + startTime: 60, // seconds + spawnInterval: { easy: 900, normal: 700, hard: 500 }, // ms base + dropLife: { easy: 2500, normal: 2000, hard: 1500 }, // ms + obstacleChance: { easy: 0.08, normal: 0.12, hard: 0.18 }, + maxDrops: { easy: 6, normal: 9, hard: 12 }, + comboWindow: 750 // ms between pops for combo + }; + + // DOM + const canvas = document.getElementById('gameCanvas'); + const playPauseBtn = document.getElementById('playPauseBtn'); + const restartBtn = document.getElementById('restartBtn'); + const soundToggle = document.getElementById('soundToggle'); + const volumeControl = document.getElementById('volume'); + const difficultySelect = document.getElementById('difficulty'); + const overlay = document.getElementById('overlay'); + + const scoreEl = document.getElementById('score'); + const livesEl = document.getElementById('lives'); + const timeEl = document.getElementById('time'); + const comboEl = document.getElementById('combo'); + + // Canvas setup + const ctx = canvas.getContext('2d', { alpha: true }); + let dpr = Math.max(1, window.devicePixelRatio || 1); + + function resizeCanvas() { + const rect = canvas.getBoundingClientRect(); + canvas.width = Math.round(rect.width * dpr); + canvas.height = Math.round(rect.height * dpr); + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + } + window.addEventListener('resize', resizeCanvas); + resizeCanvas(); + + // Audio setup (WebAudio synth for pop to avoid external files) + const AudioContext = window.AudioContext || window.webkitAudioContext; + const audioCtx = AudioContext ? new AudioContext() : null; + let bgAudio = null; + const bgMusicUrl = 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'; // online sample + + function playBgMusic() { + if (!audioCtx) return; + if (bgAudio) { bgAudio.play().catch(()=>{}); return; } + bgAudio = new Audio(bgMusicUrl); + bgAudio.loop = true; + bgAudio.crossOrigin = 'anonymous'; + bgAudio.volume = Number(volumeControl.value || 0.6); + bgAudio.play().catch(()=>{ /* autoplay/suspend issues ignored */}); + } + function pauseBgMusic() { if (bgAudio) bgAudio.pause(); } + + function playPopSound(volume = 0.6) { + if (!audioCtx || !soundToggle.checked) return; + const now = audioCtx.currentTime; + const o = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + o.type = 'sine'; + o.frequency.setValueAtTime(900 + Math.random() * 300, now); + g.gain.setValueAtTime(volume, now); + o.connect(g); + g.connect(audioCtx.destination); + o.start(now); + g.gain.exponentialRampToValueAtTime(0.001, now + 0.12); + o.stop(now + 0.15); + } + + // Game state + let state = { + running: false, + score: 0, + lives: 3, + timeLeft: config.startTime, + drops: [], + lastSpawn: 0, + lastPopTime: 0, + combo: 1, + spawnTimer: null, + gameLoopId: null + }; + + // Utils + function rand(min, max) { return Math.random() * (max - min) + min; } + function now(){ return performance.now(); } + + // Entities: drops and obstacles + function createDrop(isObstacle=false) { + const w = canvas.clientWidth; + const h = canvas.clientHeight; + const radius = rand(18, 46); + return { + id: Math.random().toString(36).slice(2,9), + x: rand(radius, w - radius), + y: rand(radius, h - radius), + r: radius, + created: now(), + life: getDropLife(), + isObstacle, + popped: false, + glow: rand(0.6, 1.2), + hue: isObstacle ? 220 : rand(150, 300) // obstacles are bluish-gray + }; + } + + function getDifficulty() { return difficultySelect.value || 'normal'; } + function getSpawnInterval() { return config.spawnInterval[getDifficulty()]; } + function getDropLife() { return config.dropLife[getDifficulty()]; } + function getObstacleChance() { return config.obstacleChance[getDifficulty()]; } + function getMaxDrops() { return config.maxDrops[getDifficulty()]; } + + // Spawn logic + function spawnLogic() { + const t = now(); + const shouldSpawn = (t - state.lastSpawn) > getSpawnInterval(); + if (!shouldSpawn) return; + state.lastSpawn = t; + // spawn up to max + if (state.drops.length < getMaxDrops()) { + const isObs = Math.random() < getObstacleChance(); + state.drops.push(createDrop(isObs)); + } + } + + // Rendering helpers + function clear() { + ctx.clearRect(0,0,canvas.width,canvas.height); + // subtle vignette + const g = ctx.createLinearGradient(0,0,0,canvas.height); + g.addColorStop(0, 'rgba(255,255,255,0.02)'); + g.addColorStop(1, 'rgba(0,0,0,0.08)'); + ctx.fillStyle = g; + ctx.fillRect(0,0,canvas.width/dpr, canvas.height/dpr); + } + + function drawDrop(drop) { + const t = now(); + const age = t - drop.created; + const lifeRatio = 1 - Math.min(1, age / drop.life); + const x = drop.x, y = drop.y, r = drop.r; + + // glow radial + const grd = ctx.createRadialGradient(x, y, r*0.1, x, y, r*3); + const hue = drop.hue; + // brighter center + grd.addColorStop(0, `hsla(${hue},85%,60%,${0.95 * drop.glow})`); + grd.addColorStop(0.25, `hsla(${hue},85%,55%,${0.55 * drop.glow})`); + grd.addColorStop(0.6, `hsla(${hue},65%,45%,${0.22 * lifeRatio})`); + grd.addColorStop(1, `rgba(4,6,10,0)`); + ctx.beginPath(); + ctx.fillStyle = grd; + ctx.arc(x, y, r*1.1, 0, Math.PI*2); + ctx.fill(); + + // inner glossy disk + ctx.beginPath(); + ctx.arc(x - r*0.25, y - r*0.25, r*0.6, 0, Math.PI*2); + ctx.fillStyle = drop.isObstacle ? 'rgba(10,14,26,0.6)' : 'rgba(255,255,255,0.18)'; + ctx.fill(); + + // stroke + ctx.beginPath(); + ctx.arc(x,y,r,0,Math.PI*2); + ctx.lineWidth = Math.max(1, Math.min(3, r*0.06)); + ctx.strokeStyle = drop.isObstacle ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.06)'; + ctx.stroke(); + + // quick life ring for timing + ctx.beginPath(); + ctx.arc(x,y,r + 10, -Math.PI/2, -Math.PI/2 + (Math.PI*2)*(age / drop.life)); + ctx.lineWidth = 2; + ctx.strokeStyle = drop.isObstacle ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.06)'; + ctx.stroke(); + } + + function drawRipples() { + // ripples are dynamically added into drops when popped (handled elsewhere) + } + + // Pop handling (create ripple + audio + score) + function popDrop(drop) { + if (drop.popped) return; + drop.popped = true; + const t = now(); + // compute combo + if (t - state.lastPopTime < config.comboWindow) { + state.combo = Math.min(5, state.combo + 1); + } else { + state.combo = 1; + } + state.lastPopTime = t; + + // sound + playPopSound(0.5 * Number(volumeControl.value || 0.6)); + + // scoring + if (drop.isObstacle) { + // penalty + state.score = Math.max(0, state.score - Math.round(25 * state.combo)); + state.lives = Math.max(0, state.lives - 1); + showPulse('-1 life', '#ff6b6b', drop.x, drop.y); + } else { + const base = Math.round(10 + (drop.r / 6)); + const gained = base * state.combo; + state.score += gained; + showPulse('+' + gained, '#7ee787', drop.x, drop.y); + } + + // create ripple effect (temporary) + const ripple = { + x: drop.x, y: drop.y, t: now(), ttl: 600, maxR: drop.r*3, hue: drop.hue + }; + state.ripples.push(ripple); + + // remove drop with fade + setTimeout(() => { + state.drops = state.drops.filter(d => d !== drop); + }, 80); + } + + // Visual feedback text + function showPulse(text, color, x, y) { + state.pulses.push({ text, color, x, y, t: now() }); + } + + // Game loop + function step() { + const t = now(); + + // spawn + spawnLogic(); + + // update time + if (state.lastTick) { + const dt = (t - state.lastTick) / 1000; + if (state.running) { + state.timeLeft = Math.max(0, state.timeLeft - dt); + } + } + state.lastTick = t; + + // expire drops + state.drops = state.drops.filter(d => { + if (d.popped) return false; + const age = t - d.created; + if (age > d.life) { + // unpopped drop expired -> penalty slightly + state.lives = Math.max(0, state.lives - 0.25 | 0); + showPulse('-1', '#ff8a8a', d.x, d.y); + return false; + } + return true; + }); + + // ripples cleanup + state.ripples = state.ripples.filter(r => (t - r.t) < r.ttl); + state.pulses = state.pulses.filter(p => (t - p.t) < 900); + + // draw + render(); + + // HUD + scoreEl.textContent = Math.round(state.score); + livesEl.textContent = Math.max(0, Math.floor(state.lives)); + timeEl.textContent = Math.ceil(state.timeLeft); + comboEl.textContent = `x${state.combo}`; + + // Check game over conditions + if (state.timeLeft <= 0 || state.lives <= 0) { + endGame(); + return; + } + + // loop + if (state.running) { + state.gameLoopId = requestAnimationFrame(step); + } + } + + function render() { + clear(); + // scale for drawing shapes consistent with CSS pixels + // draw drops + state.drops.forEach(drawDrop); + + // draw ripples + state.ripples.forEach(r => { + const elapsed = now() - r.t; + const p = elapsed / r.ttl; + const radius = p * r.maxR; + ctx.beginPath(); + ctx.arc(r.x, r.y, radius, 0, Math.PI*2); + ctx.lineWidth = Math.max(1, (1-p)*6); + ctx.strokeStyle = `hsla(${r.hue},80%,60%,${0.25*(1-p)})`; + ctx.stroke(); + }); + + // pulses + state.pulses.forEach((p, idx) => { + const age = now() - p.t; + const alpha = 1 - (age / 900); + ctx.font = '600 18px Inter, system-ui, Arial'; + ctx.fillStyle = p.color; + ctx.globalAlpha = alpha; + ctx.fillText(p.text, p.x + 8, p.y - 12 - (idx*12)); + ctx.globalAlpha = 1; + }); + } + + // Input handling (mouse/touch) + function getCanvasPos(e) { + const rect = canvas.getBoundingClientRect(); + const clientX = e.touches ? e.touches[0].clientX : e.clientX; + const clientY = e.touches ? e.touches[0].clientY : e.clientY; + return { x: clientX - rect.left, y: clientY - rect.top }; + } + + function handleClick(e) { + if (!state.running) return; + const pos = getCanvasPos(e); + // find topmost drop under cursor (reverse order) + for (let i = state.drops.length -1; i >= 0; i--) { + const d = state.drops[i]; + const dx = pos.x - d.x; + const dy = pos.y - d.y; + if ((dx*dx + dy*dy) <= (d.r * d.r)) { + popDrop(d); + return; + } + } + // clicked empty space: small penalty + state.score = Math.max(0, state.score - 1); + showPulse('-1', '#ff9b9b', pos.x, pos.y); + } + + canvas.addEventListener('mousedown', handleClick); + canvas.addEventListener('touchstart', (e) => { e.preventDefault(); handleClick(e); }, {passive:false}); + + // Game control functions + function startGame() { + // reset or resume from paused state + if (!audioCtx) { + // nothing + } else if (audioCtx.state === 'suspended') { + audioCtx.resume().catch(()=>{}); + } + if (soundToggle.checked) playBgMusic(); + state.running = true; + playPauseBtn.textContent = 'Pause'; + // if new game + if (!state.started || state.ended) { + state.started = true; + state.ended = false; + state.score = 0; + state.lives = 3; + state.timeLeft = config.startTime; + state.drops = []; + state.ripples = []; + state.pulses = []; + state.combo = 1; + state.lastPopTime = 0; + state.lastSpawn = 0; + } + cancelAnimationFrame(state.gameLoopId); + state.gameLoopId = requestAnimationFrame(step); + } + + function pauseGame() { + state.running = false; + playPauseBtn.textContent = 'Play'; + pauseBgMusic(); + cancelAnimationFrame(state.gameLoopId); + } + + function restartGame() { + pauseGame(); + state.started = false; + state.ended = false; + startGame(); + } + + function endGame() { + state.running = false; + state.ended = true; + pauseBgMusic(); + cancelAnimationFrame(state.gameLoopId); + overlay.innerHTML = `
    +

    Game Over

    +

    Score: ${Math.round(state.score)}

    +

    Thanks for playing โ€” click Restart to try again.

    +
    `; + overlay.classList.remove('hidden'); + + // Track play for hub pro-badges (if present) + try { + const playData = JSON.parse(localStorage.getItem('gamePlays') || '{}'); + const name = 'Glow Drops'; + if (!playData[name]) playData[name] = { plays: 0, success: 0 }; + playData[name].plays += 1; + // success if score > 0 + if (state.score > 0) playData[name].success += 1; + localStorage.setItem('gamePlays', JSON.stringify(playData)); + } catch (err) { /* ignore */ } + } + + // Hook up controls + playPauseBtn.addEventListener('click', () => { + if (!state.running) { overlay.classList.add('hidden'); startGame(); } + else pauseGame(); + }); + restartBtn.addEventListener('click', () => { + overlay.classList.add('hidden'); + restartGame(); + }); + + soundToggle.addEventListener('change', () => { + if (!soundToggle.checked) pauseBgMusic(); + else if (state.running) playBgMusic(); + }); + + volumeControl.addEventListener('input', () => { + if (bgAudio) bgAudio.volume = Number(volumeControl.value); + }); + + difficultySelect.addEventListener('change', () => { + // small reset in- game: prune extra drops if max lowered + state.drops = state.drops.slice(0, getMaxDrops()); + }); + + // Dismiss overlay when clicking it + overlay.addEventListener('click', () => { + overlay.classList.add('hidden'); + }); + + // Initialize state arrays + state.ripples = []; + state.pulses = []; + + // Start paused with overlay instructions + overlay.innerHTML = `
    +

    Glow Drops

    +

    Click Play to start. Click glowing drops to score points. Avoid obstacles!

    +

    Tip: Try Normal difficulty first.

    +
    `; + overlay.classList.remove('hidden'); + + // initial render to show background + render(); + + // ensure audio context can start on first user gesture + document.addEventListener('pointerdown', () => { + if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume().catch(()=>{}); + }, { once: true }); + + // accessibility: keyboard shortcuts + document.addEventListener('keydown', (e) => { + if (e.key === ' ' || e.key === 'Spacebar') { e.preventDefault(); if (state.running) pauseGame(); else startGame(); } + if (e.key === 'r') { restartGame(); } + }); + + // adapt canvas for initial CSS size + window.setTimeout(resizeCanvas, 50); +})(); diff --git a/games/glow-drops/style.css b/games/glow-drops/style.css new file mode 100644 index 00000000..033d5cd3 --- /dev/null +++ b/games/glow-drops/style.css @@ -0,0 +1,91 @@ +:root{ + --bg:#071025; + --card:#0f1720; + --accent:#6ee7b7; + --muted:rgba(255,255,255,0.7); + --glass: rgba(255,255,255,0.04); + --accent-2: #7dd3fc; + --danger: #ff6b6b; + --radius: 12px; + --pad:16px; + --shadow: 0 6px 30px rgba(2,6,23,0.7); +} + +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial; + background: radial-gradient(1200px 600px at 10% 10%, rgba(50,60,160,0.12), transparent 10%), + linear-gradient(180deg,#071025 0%, #051226 60%); + color:#e6eef6; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; +} + +.page{max-width:1100px;margin:28px auto;padding:20px} + +.gd-header{ + display:flex; + gap:16px; + align-items:center; + justify-content:space-between; + margin-bottom:14px; +} + +.gd-title{display:flex;align-items:center;gap:12px} +.gd-icon{ + width:68px;height:68px;border-radius:14px;background:linear-gradient(135deg,#38bdf8,#60a5fa); + display:flex;align-items:center;justify-content:center;font-size:28px;box-shadow:0 8px 30px rgba(96,165,250,0.14); +} +.gd-title h1{margin:0;font-size:20px} +.subtitle{margin:0;color:var(--muted);font-size:13px} + +.controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap} +.btn{ + border:0;padding:10px 12px;border-radius:10px;background:var(--glass);color:inherit;cursor:pointer; + box-shadow:var(--shadow);backdrop-filter: blur(6px); +} +.btn.primary{background:linear-gradient(90deg,#06b6d4,#7dd3fc);color:#052025;font-weight:600} + +.control-inline{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--muted)} +.control-inline input[type="range"]{width:90px} +.open-new{color:var(--muted);text-decoration:none;padding:8px 10px;border-radius:10px} + +.gd-main{display:flex;gap:18px;margin-top:16px} +.canvas-wrap{ + flex:1; min-height:520px; position:relative; border-radius:16px; overflow:hidden; + background-image: url("https://source.unsplash.com/1600x900/?water,drop,macro"); + background-size:cover; background-position:center; + box-shadow: 0 20px 50px rgba(2,6,23,0.6); + border:1px solid rgba(255,255,255,0.03); +} + +/* Canvas fills */ +#gameCanvas{width:100%;height:100%;display:block;} + +/* overlay messages */ +.overlay{ + position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:24px; + pointer-events:none; +} +.overlay.hidden{display:none} +.overlay .card{ + background:linear-gradient(180deg,rgba(0,0,0,0.5),rgba(6,10,20,0.6));border-radius:14px;padding:18px 22px;text-align:center; + backdrop-filter: blur(6px); border: 1px solid rgba(255,255,255,0.04); +} +.instructions{width:300px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); padding:16px;border-radius:12px;box-shadow:var(--shadow)} +.instructions h3{margin-top:0} +.instructions ol{padding-left:18px;color:var(--muted)} +.instructions .credits{font-size:12px;color:var(--muted);margin-top:12px} + +/* HUD */ +.hud{position:absolute;top:12px;left:12px;display:flex;gap:10px;z-index:50} +.hud .stat{background:linear-gradient(90deg, rgba(255,255,255,0.03), rgba(0,0,0,0.1));padding:8px 12px;border-radius:10px;font-weight:600;font-size:14px;backdrop-filter: blur(6px)} + +/* small screens */ +@media (max-width:900px){ + .gd-main{flex-direction:column} + .instructions{width:100%} + .page{padding:12px} +} diff --git a/games/glow-tap/index.html b/games/glow-tap/index.html new file mode 100644 index 00000000..fcd89d74 --- /dev/null +++ b/games/glow-tap/index.html @@ -0,0 +1,24 @@ + + + + + + Glow Tap | Mini JS Games Hub + + + +
    +

    Glow Tap

    +

    Score: 0

    +
    +
    + + + +
    + + +
    + + + diff --git a/games/glow-tap/script.js b/games/glow-tap/script.js new file mode 100644 index 00000000..c70e2cd0 --- /dev/null +++ b/games/glow-tap/script.js @@ -0,0 +1,85 @@ +const gameArea = document.getElementById('game-area'); +const startBtn = document.getElementById('start-btn'); +const pauseBtn = document.getElementById('pause-btn'); +const restartBtn = document.getElementById('restart-btn'); +const scoreEl = document.getElementById('score'); + +const tapSound = document.getElementById('tap-sound'); +const failSound = document.getElementById('fail-sound'); + +let gameInterval; +let obstacleInterval; +let score = 0; +let running = false; + +function createBulb() { + const bulb = document.createElement('div'); + bulb.className = 'bulb'; + bulb.style.left = Math.random() * (gameArea.clientWidth - 50) + 'px'; + bulb.style.top = '0px'; + + gameArea.appendChild(bulb); + + let fall = setInterval(() => { + let top = parseInt(bulb.style.top); + if (top >= gameArea.clientHeight - 50) { + failSound.play(); + gameArea.removeChild(bulb); + clearInterval(fall); + } else { + bulb.style.top = top + 5 + 'px'; + } + }, 30); + + bulb.addEventListener('click', () => { + tapSound.play(); + score += 1; + scoreEl.textContent = score; + gameArea.removeChild(bulb); + clearInterval(fall); + }); +} + +function createObstacle() { + const obs = document.createElement('div'); + obs.className = 'obstacle'; + obs.style.left = Math.random() * (gameArea.clientWidth - 50) + 'px'; + obs.style.top = '0px'; + + gameArea.appendChild(obs); + + let fall = setInterval(() => { + let top = parseInt(obs.style.top); + if (top >= gameArea.clientHeight - 50) { + gameArea.removeChild(obs); + clearInterval(fall); + } else { + obs.style.top = top + 4 + 'px'; + } + }, 30); +} + +function startGame() { + if (running) return; + running = true; + gameInterval = setInterval(createBulb, 1000); + obstacleInterval = setInterval(createObstacle, 2000); +} + +function pauseGame() { + running = false; + clearInterval(gameInterval); + clearInterval(obstacleInterval); +} + +function restartGame() { + pauseGame(); + score = 0; + scoreEl.textContent = score; + gameArea.innerHTML = ''; + startGame(); +} + +startBtn.addEventListener('click', startGame); +pauseBtn.addEventListener('click', pauseGame); +restartBtn.addEventListener('click', restartGame); diff --git a/games/glow-tap/style.css b/games/glow-tap/style.css new file mode 100644 index 00000000..e8b25835 --- /dev/null +++ b/games/glow-tap/style.css @@ -0,0 +1,62 @@ +body { + font-family: Arial, sans-serif; + background: radial-gradient(circle, #000000, #111111); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + color: #fff; +} + +.game-container { + text-align: center; +} + +#game-area { + position: relative; + width: 300px; + height: 500px; + margin: 20px auto; + border: 2px solid #fff; + background-color: #000; + overflow: hidden; + border-radius: 10px; +} + +.bulb, .obstacle { + position: absolute; + width: 50px; + height: 50px; + border-radius: 50%; + cursor: pointer; + transition: transform 0.2s; +} + +.bulb { + background: radial-gradient(circle, #00ffff, #00aaff); + box-shadow: 0 0 15px #00ffff, 0 0 30px #00ffff, 0 0 50px #00ffff; +} + +.bulb:hover { + transform: scale(1.2); +} + +.obstacle { + background-color: red; + box-shadow: 0 0 10px red, 0 0 20px red; +} + +.controls button { + padding: 10px 15px; + margin: 5px; + font-size: 16px; + cursor: pointer; + border: none; + border-radius: 5px; +} + +.controls button:hover { + background-color: #00ffff; + color: #000; +} diff --git a/games/glow-trail-maze/index.html b/games/glow-trail-maze/index.html new file mode 100644 index 00000000..0e7c7aef --- /dev/null +++ b/games/glow-trail-maze/index.html @@ -0,0 +1,28 @@ + + + + + + Glow Trail Maze | Mini JS Games Hub + + + +
    +

    Glow Trail Maze

    +
    + + + + +
    +

    Use arrow keys to navigate. Avoid obstacles!

    + +
    + + + + + + + + diff --git a/games/glow-trail-maze/script.js b/games/glow-trail-maze/script.js new file mode 100644 index 00000000..f2051b21 --- /dev/null +++ b/games/glow-trail-maze/script.js @@ -0,0 +1,113 @@ +const canvas = document.getElementById("mazeCanvas"); +const ctx = canvas.getContext("2d"); +const moveSound = document.getElementById("moveSound"); +const hitSound = document.getElementById("hitSound"); +const winSound = document.getElementById("winSound"); + +const width = canvas.width; +const height = canvas.height; + +const cellSize = 25; +const rows = Math.floor(height / cellSize); +const cols = Math.floor(width / cellSize); + +let trail = []; +let obstacles = []; +let player = { x: 0, y: 0 }; +let goal = { x: cols - 1, y: rows - 1 }; +let interval; +let paused = false; + +// Generate random obstacles +function generateObstacles(count = 50) { + obstacles = []; + while (obstacles.length < count) { + const x = Math.floor(Math.random() * cols); + const y = Math.floor(Math.random() * rows); + if ((x === 0 && y === 0) || (x === goal.x && y === goal.y)) continue; + obstacles.push({ x, y }); + } +} + +// Draw the grid, trail, player, obstacles +function draw() { + ctx.clearRect(0, 0, width, height); + + // Draw trail + trail.forEach((cell, index) => { + const alpha = (index + 1) / trail.length; + ctx.fillStyle = `rgba(0, 255, 255, ${alpha})`; + ctx.fillRect(cell.x * cellSize, cell.y * cellSize, cellSize, cellSize); + }); + + // Draw obstacles + ctx.fillStyle = "#ff4444"; + obstacles.forEach(o => { + ctx.fillRect(o.x * cellSize, o.y * cellSize, cellSize, cellSize); + }); + + // Draw player + ctx.fillStyle = "#00ffff"; + ctx.fillRect(player.x * cellSize, player.y * cellSize, cellSize, cellSize); + + // Draw goal + ctx.fillStyle = "#00ff00"; + ctx.fillRect(goal.x * cellSize, goal.y * cellSize, cellSize, cellSize); +} + +// Move player +function move(dx, dy) { + if (paused) return; + const newX = player.x + dx; + const newY = player.y + dy; + + if (newX < 0 || newX >= cols || newY < 0 || newY >= rows) return; + if (obstacles.find(o => o.x === newX && o.y === newY)) { + hitSound.play(); + return; + } + + player.x = newX; + player.y = newY; + trail.push({ x: player.x, y: player.y }); + moveSound.play(); + + if (player.x === goal.x && player.y === goal.y) { + clearInterval(interval); + winSound.play(); + alert("๐ŸŽ‰ You reached the goal!"); + } + draw(); +} + +// Keyboard events +document.addEventListener("keydown", e => { + switch (e.key) { + case "ArrowUp": move(0, -1); break; + case "ArrowDown": move(0, 1); break; + case "ArrowLeft": move(-1, 0); break; + case "ArrowRight": move(1, 0); break; + } +}); + +// Game controls +document.getElementById("start-btn").addEventListener("click", () => { + resetGame(); + draw(); + paused = false; +}); + +document.getElementById("pause-btn").addEventListener("click", () => paused = true); +document.getElementById("resume-btn").addEventListener("click", () => paused = false); +document.getElementById("restart-btn").addEventListener("click", () => resetGame()); + +function resetGame() { + player = { x: 0, y: 0 }; + trail = []; + generateObstacles(70); + paused = false; + draw(); +} + +// Initial setup +resetGame(); diff --git a/games/glow-trail-maze/style.css b/games/glow-trail-maze/style.css new file mode 100644 index 00000000..d95bd6a2 --- /dev/null +++ b/games/glow-trail-maze/style.css @@ -0,0 +1,42 @@ +body { + font-family: Arial, sans-serif; + background-color: #111; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; +} + +.game-container { + text-align: center; +} + +canvas { + background-color: #222; + border: 2px solid #fff; + margin-top: 20px; +} + +.controls button { + padding: 8px 16px; + margin: 5px; + font-size: 16px; + cursor: pointer; + border: none; + border-radius: 6px; + background-color: #ff9800; + color: #111; + transition: 0.2s; +} + +.controls button:hover { + background-color: #ffc107; +} + +.instructions { + margin-top: 10px; + font-size: 16px; +} diff --git a/games/grass-defense/index.html b/games/grass-defense/index.html new file mode 100644 index 00000000..10c7b8f4 --- /dev/null +++ b/games/grass-defense/index.html @@ -0,0 +1,39 @@ + + + + + + Grass Defense ๐ŸŒฑ + + + +
    +

    ๐ŸŒฟ Grass Defense

    +

    Defend your garden from the incoming pests!

    +
    + +
    + +
    +

    ๐ŸŒฑ Choose Your Defense

    +
    + + + +
    +
    +

    ๐Ÿ’ฐ Coins: 100

    +

    โค๏ธ Lives: 10

    +

    โš”๏ธ Wave: 1

    +
    + +
    +
    + +
    +

    Made with ๐Ÿ’š using HTML, CSS & JS

    +
    + + + + diff --git a/games/grass-defense/script.js b/games/grass-defense/script.js new file mode 100644 index 00000000..c2d06576 --- /dev/null +++ b/games/grass-defense/script.js @@ -0,0 +1,156 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +let coins = 100; +let lives = 10; +let wave = 1; +let enemies = []; +let plants = []; +let bullets = []; +let gameActive = false; +let selectedPlantType = null; + +// DOM elements +const coinsDisplay = document.getElementById("coins"); +const livesDisplay = document.getElementById("lives"); +const waveDisplay = document.getElementById("wave"); +const startWaveBtn = document.getElementById("startWave"); + +// Plant buttons +document.querySelectorAll(".plant-buttons button").forEach(btn => { + btn.addEventListener("click", () => { + selectedPlantType = btn.dataset.type; + document.querySelectorAll(".plant-buttons button").forEach(b => b.style.background = "#66bb6a"); + btn.style.background = "#2e7d32"; + }); +}); + +// Start wave +startWaveBtn.addEventListener("click", () => { + if (!gameActive) { + startWave(); + } +}); + +// Handle canvas clicks to place plants +canvas.addEventListener("click", e => { + if (!selectedPlantType) return; + const rect = canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / 60) * 60; + const y = Math.floor((e.clientY - rect.top) / 60) * 60; + + if (coins >= 50 && !plants.some(p => p.x === x && p.y === y)) { + coins -= 50; + coinsDisplay.textContent = coins; + plants.push({ x, y, type: selectedPlantType, cooldown: 0 }); + } +}); + +// Enemy generation +function spawnEnemies(num) { + for (let i = 0; i < num; i++) { + enemies.push({ + x: 900 + i * 80, + y: Math.floor(Math.random() * 8) * 60, + hp: 100 + wave * 20, + speed: 1 + Math.random(), + }); + } +} + +// Game loop +function update() { + if (!gameActive) return; + + // Update plants + plants.forEach(p => { + if (p.cooldown > 0) p.cooldown--; + if (p.type === "shooter" && p.cooldown === 0) { + bullets.push({ x: p.x + 50, y: p.y + 20, speed: 5, damage: 30 }); + p.cooldown = 50; + } + }); + + // Update bullets + bullets.forEach((b, i) => { + b.x += b.speed; + enemies.forEach((enemy, j) => { + if (Math.abs(b.x - enemy.x) < 30 && Math.abs(b.y - enemy.y) < 30) { + enemy.hp -= b.damage; + bullets.splice(i, 1); + } + }); + }); + + // Update enemies + enemies.forEach((enemy, i) => { + enemy.x -= enemy.speed; + if (enemy.hp <= 0) { + enemies.splice(i, 1); + coins += 10; + coinsDisplay.textContent = coins; + } else if (enemy.x < 0) { + enemies.splice(i, 1); + lives--; + livesDisplay.textContent = lives; + if (lives <= 0) endGame(); + } + }); + + draw(); + if (enemies.length === 0) nextWave(); + requestAnimationFrame(update); +} + +// Drawing +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Grid + ctx.strokeStyle = "#9ccc65"; + for (let i = 0; i < canvas.width; i += 60) { + for (let j = 0; j < canvas.height; j += 60) { + ctx.strokeRect(i, j, 60, 60); + } + } + + // Plants + plants.forEach(p => { + ctx.fillStyle = p.type === "shooter" ? "#43a047" : p.type === "slow" ? "#29b6f6" : "#ef5350"; + ctx.beginPath(); + ctx.arc(p.x + 30, p.y + 30, 20, 0, Math.PI * 2); + ctx.fill(); + }); + + // Enemies + enemies.forEach(enemy => { + ctx.fillStyle = "#6d4c41"; + ctx.fillRect(enemy.x, enemy.y + 20, 30, 20); + ctx.fillStyle = "#d32f2f"; + ctx.fillRect(enemy.x, enemy.y + 10, enemy.hp / 2, 5); + }); + + // Bullets + ctx.fillStyle = "#fdd835"; + bullets.forEach(b => ctx.fillRect(b.x, b.y, 8, 4)); +} + +// Game control +function startWave() { + gameActive = true; + spawnEnemies(5 + wave * 2); + update(); +} + +function nextWave() { + gameActive = false; + wave++; + waveDisplay.textContent = wave; + coins += 50; +} + +function endGame() { + gameActive = false; + alert("Game Over! Your garden was destroyed ๐Ÿ’€"); + location.reload(); +} diff --git a/games/grass-defense/style.css b/games/grass-defense/style.css new file mode 100644 index 00000000..1f9a4f54 --- /dev/null +++ b/games/grass-defense/style.css @@ -0,0 +1,68 @@ +body { + margin: 0; + font-family: "Poppins", sans-serif; + background: linear-gradient(to bottom, #c7f0b2, #8ed081); + display: flex; + flex-direction: column; + align-items: center; + color: #2f2f2f; +} + +header { + text-align: center; + margin: 10px 0; +} + +main { + display: flex; + justify-content: center; + align-items: flex-start; + gap: 20px; +} + +canvas { + border: 3px solid #2f2f2f; + border-radius: 10px; + background: #e8f5e9; +} + +.ui-panel { + width: 250px; + padding: 10px 15px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.8); + box-shadow: 0 0 10px rgba(0,0,0,0.2); +} + +.plant-buttons { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 10px; +} + +button { + padding: 8px 10px; + font-size: 16px; + border: none; + border-radius: 6px; + cursor: pointer; + background-color: #66bb6a; + color: white; + transition: 0.3s; +} + +button:hover { + background-color: #43a047; +} + +.stats p { + margin: 6px 0; + font-weight: 500; +} + +footer { + margin-top: 15px; + font-size: 14px; + color: #444; +} diff --git a/games/gravity-flip-ball/index.html b/games/gravity-flip-ball/index.html new file mode 100644 index 00000000..5cf7d168 --- /dev/null +++ b/games/gravity-flip-ball/index.html @@ -0,0 +1,27 @@ + + + + + + Gravity Flip Ball + + + +
    +

    Gravity Flip Ball

    + +
    + + + + +
    +

    Score: 0

    +
    + + + + + + + diff --git a/games/gravity-flip-ball/script.js b/games/gravity-flip-ball/script.js new file mode 100644 index 00000000..5b9bd477 --- /dev/null +++ b/games/gravity-flip-ball/script.js @@ -0,0 +1,136 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +const startBtn = document.getElementById("startBtn"); +const pauseBtn = document.getElementById("pauseBtn"); +const resumeBtn = document.getElementById("resumeBtn"); +const restartBtn = document.getElementById("restartBtn"); +const scoreEl = document.getElementById("score"); + +const jumpSound = document.getElementById("jumpSound"); +const gameOverSound = document.getElementById("gameOverSound"); + +let ball = { x: 100, y: 200, radius: 15, vy: 0 }; +let gravity = 0.5; +let obstacles = []; +let gameInterval; +let score = 0; +let isPaused = false; + +function generateObstacle() { + const gap = 100; + const width = 20; + const heightTop = Math.random() * 150 + 50; + const heightBottom = canvas.height - heightTop - gap; + obstacles.push({ + x: canvas.width, + yTop: 0, + hTop: heightTop, + yBottom: canvas.height - heightBottom, + hBottom: heightBottom, + width, + }); +} + +function drawBall() { + ctx.beginPath(); + ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2); + ctx.fillStyle = "#0ff"; + ctx.shadowColor = "#0ff"; + ctx.shadowBlur = 20; + ctx.fill(); + ctx.closePath(); +} + +function drawObstacles() { + obstacles.forEach(obs => { + ctx.fillStyle = "#ff007f"; + ctx.shadowColor = "#ff007f"; + ctx.shadowBlur = 20; + ctx.fillRect(obs.x, obs.yTop, obs.width, obs.hTop); + ctx.fillRect(obs.x, obs.yBottom, obs.width, obs.hBottom); + }); +} + +function updateObstacles() { + obstacles.forEach(obs => obs.x -= 3); + obstacles = obstacles.filter(obs => obs.x + obs.width > 0); + if (obstacles.length === 0 || obstacles[obstacles.length-1].x < 300) { + generateObstacle(); + } +} + +function checkCollision() { + for (let obs of obstacles) { + if (ball.x + ball.radius > obs.x && ball.x - ball.radius < obs.x + obs.width) { + if (ball.y - ball.radius < obs.hTop || ball.y + ball.radius > obs.yBottom) { + gameOver(); + } + } + } + if (ball.y - ball.radius < 0 || ball.y + ball.radius > canvas.height) { + gameOver(); + } +} + +function gameLoop() { + if (isPaused) return; + ctx.clearRect(0,0,canvas.width,canvas.height); + + ball.vy += gravity; + ball.y += ball.vy; + + drawBall(); + drawObstacles(); + updateObstacles(); + checkCollision(); + + score += 0.01; + scoreEl.textContent = Math.floor(score); + + requestAnimationFrame(gameLoop); +} + +function startGame() { + ball = { x: 100, y: 200, radius: 15, vy: 0 }; + obstacles = []; + score = 0; + isPaused = false; + generateObstacle(); + gameLoop(); +} + +function pauseGame() { + isPaused = true; +} + +function resumeGame() { + if (isPaused) { + isPaused = false; + gameLoop(); + } +} + +function restartGame() { + isPaused = false; + startGame(); +} + +function flipGravity() { + ball.vy = -ball.vy - 5; + jumpSound.play(); +} + +function gameOver() { + gameOverSound.play(); + alert("Game Over! Score: " + Math.floor(score)); + restartGame(); +} + +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", pauseGame); +resumeBtn.addEventListener("click", resumeGame); +restartBtn.addEventListener("click", restartGame); + +document.addEventListener("keydown", flipGravity); +canvas.addEventListener("click", flipGravity); diff --git a/games/gravity-flip-ball/style.css b/games/gravity-flip-ball/style.css new file mode 100644 index 00000000..cc85dbb7 --- /dev/null +++ b/games/gravity-flip-ball/style.css @@ -0,0 +1,47 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #1e1e2f, #3a3a5a); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + color: #fff; +} + +.game-container { + text-align: center; +} + +canvas { + background-color: #111; + border: 2px solid #fff; + display: block; + margin: 20px auto; + box-shadow: 0 0 20px #0ff, 0 0 40px #0ff inset; + border-radius: 15px; +} + +.controls button { + padding: 10px 20px; + margin: 5px; + border: none; + border-radius: 8px; + background: #0ff; + color: #111; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; + box-shadow: 0 0 10px #0ff, 0 0 20px #0ff inset; +} + +.controls button:hover { + background: #0aa; + box-shadow: 0 0 20px #0ff, 0 0 40px #0ff inset; +} + +.score { + font-size: 1.2em; + font-weight: bold; + margin-top: 10px; +} diff --git a/games/gravity-flip-runner/index.html b/games/gravity-flip-runner/index.html new file mode 100644 index 00000000..d934133e --- /dev/null +++ b/games/gravity-flip-runner/index.html @@ -0,0 +1,26 @@ + + + + + + Gravity Flip Runner | Mini JS Games Hub + + + +
    + +
    + + + +
    Score: 0
    +
    +
    + + + + + + + + diff --git a/games/gravity-flip-runner/script.js b/games/gravity-flip-runner/script.js new file mode 100644 index 00000000..b0f537bc --- /dev/null +++ b/games/gravity-flip-runner/script.js @@ -0,0 +1,155 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +const jumpSound = document.getElementById("jumpSound"); +const hitSound = document.getElementById("hitSound"); +const bgMusic = document.getElementById("bgMusic"); + +const startBtn = document.getElementById("startBtn"); +const pauseBtn = document.getElementById("pauseBtn"); +const restartBtn = document.getElementById("restartBtn"); +const scoreDisplay = document.getElementById("score"); + +let gravity = 0.6; +let player = { x: 100, y: 300, w: 40, h: 40, dy: 0, inverted: false }; +let obstacles = []; +let speed = 5; +let score = 0; +let isRunning = false; +let isPaused = false; + +const playerColor = "#00e6ff"; + +function drawPlayer() { + ctx.fillStyle = playerColor; + ctx.shadowBlur = 20; + ctx.shadowColor = playerColor; + ctx.fillRect(player.x, player.y, player.w, player.h); + ctx.shadowBlur = 0; +} + +function drawObstacles() { + ctx.fillStyle = "#ff0066"; + ctx.shadowBlur = 20; + ctx.shadowColor = "#ff0066"; + obstacles.forEach(o => ctx.fillRect(o.x, o.y, o.w, o.h)); + ctx.shadowBlur = 0; +} + +function createObstacle() { + const size = Math.random() * 30 + 20; + const y = Math.random() > 0.5 ? 0 : canvas.height - size; + obstacles.push({ x: canvas.width, y: y, w: size, h: size }); +} + +function updateObstacles() { + obstacles.forEach(o => (o.x -= speed)); + obstacles = obstacles.filter(o => o.x + o.w > 0); + if (Math.random() < 0.02) createObstacle(); +} + +function detectCollision() { + return obstacles.some( + o => + player.x < o.x + o.w && + player.x + player.w > o.x && + player.y < o.y + o.h && + player.y + player.h > o.y + ); +} + +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw floor/ceiling glow lines + ctx.strokeStyle = "#00e6ff"; + ctx.shadowBlur = 15; + ctx.shadowColor = "#00e6ff"; + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(canvas.width, 0); + ctx.moveTo(0, canvas.height); + ctx.lineTo(canvas.width, canvas.height); + ctx.stroke(); + ctx.shadowBlur = 0; + + drawPlayer(); + drawObstacles(); +} + +function update() { + if (!isRunning || isPaused) return; + + player.dy += player.inverted ? gravity : -gravity; + player.y -= player.dy; + + // Flip limit + if (player.y < 0) { + player.y = 0; + player.dy = 0; + } else if (player.y + player.h > canvas.height) { + player.y = canvas.height - player.h; + player.dy = 0; + } + + updateObstacles(); + + if (detectCollision()) { + hitSound.play(); + bgMusic.pause(); + isRunning = false; + ctx.font = "40px Poppins"; + ctx.fillStyle = "#ff0066"; + ctx.fillText("Game Over!", canvas.width / 2 - 100, canvas.height / 2); + return; + } + + score++; + speed += 0.001; + scoreDisplay.textContent = score; + + draw(); + requestAnimationFrame(update); +} + +document.addEventListener("keydown", e => { + if (e.code === "Space" && isRunning && !isPaused) { + player.inverted = !player.inverted; + jumpSound.currentTime = 0; + jumpSound.play(); + } +}); + +startBtn.addEventListener("click", () => { + if (!isRunning) { + bgMusic.currentTime = 0; + bgMusic.play(); + isRunning = true; + update(); + } +}); + +pauseBtn.addEventListener("click", () => { + isPaused = !isPaused; + if (isPaused) { + bgMusic.pause(); + pauseBtn.textContent = "โ–ถ Resume"; + } else { + bgMusic.play(); + pauseBtn.textContent = "โธ Pause"; + update(); + } +}); + +restartBtn.addEventListener("click", () => { + obstacles = []; + player = { x: 100, y: 300, w: 40, h: 40, dy: 0, inverted: false }; + speed = 5; + score = 0; + scoreDisplay.textContent = 0; + isRunning = true; + isPaused = false; + bgMusic.currentTime = 0; + bgMusic.play(); + update(); +}); diff --git a/games/gravity-flip-runner/style.css b/games/gravity-flip-runner/style.css new file mode 100644 index 00000000..af73ece7 --- /dev/null +++ b/games/gravity-flip-runner/style.css @@ -0,0 +1,57 @@ +body { + margin: 0; + height: 100vh; + background: radial-gradient(circle at top, #0f2027, #203a43, #2c5364); + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + font-family: 'Poppins', sans-serif; + color: #fff; +} + +.game-container { + position: relative; + box-shadow: 0 0 30px #00e6ff, 0 0 60px #007bff; + border-radius: 10px; + overflow: hidden; +} + +canvas { + background: linear-gradient(to bottom, #050505, #111); + display: block; + border-radius: 10px; +} + +.ui { + position: absolute; + top: 10px; + left: 10px; + display: flex; + align-items: center; + gap: 10px; +} + +button { + background: #00e6ff; + border: none; + border-radius: 5px; + color: #111; + padding: 8px 14px; + font-size: 16px; + cursor: pointer; + box-shadow: 0 0 10px #00e6ff; + transition: all 0.2s; +} + +button:hover { + background: #007bff; + box-shadow: 0 0 20px #007bff; + color: #fff; +} + +.score { + font-size: 18px; + text-shadow: 0 0 10px #00e6ff; + margin-left: 10px; +} diff --git a/games/gravity-gauntlet/index.html b/games/gravity-gauntlet/index.html new file mode 100644 index 00000000..627bc58a --- /dev/null +++ b/games/gravity-gauntlet/index.html @@ -0,0 +1,17 @@ + + + + + + Gravity Gauntlet Game + + + +
    +

    Gravity Gauntlet

    + +
    Click gravity wells to activate and guide the ball to the goal!
    +
    + + + \ No newline at end of file diff --git a/games/gravity-gauntlet/script.js b/games/gravity-gauntlet/script.js new file mode 100644 index 00000000..38fb1e2d --- /dev/null +++ b/games/gravity-gauntlet/script.js @@ -0,0 +1,150 @@ +// Gravity Gauntlet Game Script +// Manipulate gravity fields to guide a ball through challenging courses. + +const canvas = document.getElementById('game-canvas'); +const ctx = canvas.getContext('2d'); + +// Game variables +let ball = { x: 100, y: 50, vx: 0, vy: 0, radius: 15 }; +let platforms = []; +let gravityWells = []; +let goal = { x: 700, y: 550, width: 50, height: 50 }; +let gameRunning = true; + +// Constants +const gravity = 0.3; +const friction = 0.99; + +// Initialize game +function init() { + // Create platforms + platforms.push({ x: 0, y: 100, width: 200, height: 10 }); + platforms.push({ x: 300, y: 200, width: 200, height: 10 }); + platforms.push({ x: 100, y: 300, width: 200, height: 10 }); + platforms.push({ x: 400, y: 400, width: 200, height: 10 }); + platforms.push({ x: 200, y: 500, width: 400, height: 10 }); + + // Create gravity wells + gravityWells.push({ x: 150, y: 150, radius: 50, active: false }); + gravityWells.push({ x: 450, y: 250, radius: 50, active: false }); + gravityWells.push({ x: 250, y: 350, radius: 50, active: false }); + gravityWells.push({ x: 550, y: 450, radius: 50, active: false }); + + // Start game loop + requestAnimationFrame(gameLoop); +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + update(); + draw(); + + requestAnimationFrame(gameLoop); +} + +// Update game state +function update() { + // Apply gravity + ball.vy += gravity; + + // Apply gravity wells + gravityWells.forEach(well => { + if (well.active) { + const dx = well.x - ball.x; + const dy = well.y - ball.y; + const dist = Math.sqrt(dx*dx + dy*dy); + if (dist > 0) { + const force = 0.5 / dist; + ball.vx += (dx / dist) * force; + ball.vy += (dy / dist) * force; + } + } + }); + + // Apply friction + ball.vx *= friction; + ball.vy *= friction; + + // Move ball + ball.x += ball.vx; + ball.y += ball.vy; + + // Check platform collisions + platforms.forEach(platform => { + if (ball.x + ball.radius > platform.x && ball.x - ball.radius < platform.x + platform.width && + ball.y + ball.radius > platform.y && ball.y - ball.radius < platform.y + platform.height) { + if (ball.vy > 0) { + ball.y = platform.y - ball.radius; + ball.vy = 0; + } + } + }); + + // Check goal + if (ball.x > goal.x && ball.x < goal.x + goal.width && + ball.y > goal.y && ball.y < goal.y + goal.height) { + gameRunning = false; + alert('You reached the goal!'); + } + + // Reset if off screen + if (ball.y > canvas.height + 50) { + ball.x = 100; + ball.y = 50; + ball.vx = 0; + ball.vy = 0; + gravityWells.forEach(well => well.active = false); + } +} + +// Draw everything +function draw() { + // Clear canvas + ctx.fillStyle = '#000011'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw platforms + ctx.fillStyle = '#666666'; + platforms.forEach(platform => ctx.fillRect(platform.x, platform.y, platform.width, platform.height)); + + // Draw gravity wells + gravityWells.forEach(well => { + ctx.fillStyle = well.active ? '#ff00ff' : '#800080'; + ctx.beginPath(); + ctx.arc(well.x, well.y, well.radius, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 2; + ctx.stroke(); + }); + + // Draw goal + ctx.fillStyle = '#00ff00'; + ctx.fillRect(goal.x, goal.y, goal.width, goal.height); + + // Draw ball + ctx.fillStyle = '#ffff00'; + ctx.beginPath(); + ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2); + ctx.fill(); +} + +// Handle click +canvas.addEventListener('click', e => { + const rect = canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + // Check if clicked on a gravity well + gravityWells.forEach(well => { + const dist = Math.sqrt((mouseX - well.x)**2 + (mouseY - well.y)**2); + if (dist < well.radius) { + well.active = !well.active; + } + }); +}); + +// Start the game +init(); \ No newline at end of file diff --git a/games/gravity-gauntlet/style.css b/games/gravity-gauntlet/style.css new file mode 100644 index 00000000..7bc729d0 --- /dev/null +++ b/games/gravity-gauntlet/style.css @@ -0,0 +1,33 @@ +body { + font-family: Arial, sans-serif; + background-color: #000; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +#game-container { + text-align: center; +} + +h1 { + font-size: 2.5em; + margin-bottom: 20px; + text-shadow: 0 0 10px #ff00ff; +} + +#game-canvas { + border: 2px solid #ff00ff; + background-color: #000011; + box-shadow: 0 0 20px #ff00ff; + cursor: pointer; +} + +#instructions { + font-size: 1em; + margin-top: 10px; + color: #cccccc; +} \ No newline at end of file diff --git a/games/gravity-golf/index.html b/games/gravity-golf/index.html new file mode 100644 index 00000000..f7eaf5af --- /dev/null +++ b/games/gravity-golf/index.html @@ -0,0 +1,47 @@ + + + + + + Gravity Golf + + + + + + + + + +
    +
    +

    GRAVITY GOLF

    +

    Play golf on alien planets with varying gravity fields, adjusting shots to land in holes while overcoming planetary obstacles.

    + +

    Controls:

    +
      +
    • Mouse - Aim and set power
    • +
    • Click - Shoot ball
    • +
    • R - Reset shot
    • +
    • N - Next level
    • +
    + + +
    +
    + +
    +
    Strokes: 0
    +
    Planet: Earth
    +
    Gravity: 0.5
    +
    + + + +
    +
    +
    + + + + \ No newline at end of file diff --git a/games/gravity-golf/script.js b/games/gravity-golf/script.js new file mode 100644 index 00000000..6c5715b0 --- /dev/null +++ b/games/gravity-golf/script.js @@ -0,0 +1,337 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const startButton = document.getElementById('startButton'); +const instructionsOverlay = document.getElementById('instructions-overlay'); +const scoreElement = document.getElementById('score'); +const levelElement = document.getElementById('level'); +const gravityElement = document.getElementById('gravity'); +const powerBar = document.getElementById('power-bar'); + +canvas.width = 800; +canvas.height = 600; + +let gameRunning = false; +let ball; +let hole; +let obstacles = []; +let planets = [ + { name: 'Earth', gravity: 0.5, color: '#4CAF50', bg: 'linear-gradient(135deg, #87CEEB 0%, #98FB98 100%)' }, + { name: 'Moon', gravity: 0.17, color: '#9E9E9E', bg: 'linear-gradient(135deg, #2c2c2c 0%, #4a4a4a 100%)' }, + { name: 'Mars', gravity: 0.38, color: '#CD853F', bg: 'linear-gradient(135deg, #D2691E 0%, #F4A460 100%)' }, + { name: 'Jupiter', gravity: 1.2, color: '#DAA520', bg: 'linear-gradient(135deg, #8B4513 0%, #A0522D 100%)' }, + { name: 'Neptune', gravity: 0.8, color: '#4169E1', bg: 'linear-gradient(135deg, #191970 0%, #000080 100%)' } +]; + +let currentPlanet = 0; +let strokes = 0; +let ballInMotion = false; +let aiming = false; +let aimStart = { x: 0, y: 0 }; +let aimEnd = { x: 0, y: 0 }; +let power = 0; + +// Ball class +class Ball { + constructor(x, y) { + this.x = x; + this.y = y; + this.radius = 8; + this.vx = 0; + this.vy = 0; + this.onGround = false; + this.trail = []; + } + + update() { + if (!ballInMotion) return; + + const planet = planets[currentPlanet]; + + // Apply gravity + this.vy += planet.gravity; + + // Apply air resistance + this.vx *= 0.99; + this.vy *= 0.99; + + // Update position + this.x += this.vx; + this.y += this.vy; + + // Add to trail + this.trail.push({ x: this.x, y: this.y }); + if (this.trail.length > 20) { + this.trail.shift(); + } + + // Check ground collision (simple bounce) + if (this.y + this.radius >= canvas.height - 50) { + this.y = canvas.height - 50 - this.radius; + this.vy *= -0.6; // Bounce with energy loss + this.vx *= 0.8; + + if (Math.abs(this.vy) < 1) { + this.vy = 0; + ballInMotion = false; + this.onGround = true; + } + } + + // Check obstacle collisions + obstacles.forEach(obstacle => { + if (this.x + this.radius > obstacle.x && + this.x - this.radius < obstacle.x + obstacle.width && + this.y + this.radius > obstacle.y && + this.y - this.radius < obstacle.y + obstacle.height) { + + // Simple collision response + if (this.vx > 0 && this.x < obstacle.x) { + this.vx *= -0.8; + this.x = obstacle.x - this.radius; + } else if (this.vx < 0 && this.x > obstacle.x + obstacle.width) { + this.vx *= -0.8; + this.x = obstacle.x + obstacle.width + this.radius; + } + + if (this.vy > 0 && this.y < obstacle.y) { + this.vy *= -0.8; + this.y = obstacle.y - this.radius; + } else if (this.vy < 0 && this.y > obstacle.y + obstacle.height) { + this.vy *= -0.8; + this.y = obstacle.y + obstacle.height + this.radius; + } + } + }); + + // Check hole collision + const dx = this.x - hole.x; + const dy = this.y - hole.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < hole.radius + this.radius) { + ballInMotion = false; + levelComplete(); + } + + // Stop if ball goes off screen + if (this.x < -50 || this.x > canvas.width + 50 || this.y > canvas.height + 50) { + resetBall(); + } + } + + draw() { + // Draw trail + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 2; + ctx.beginPath(); + for (let i = 1; i < this.trail.length; i++) { + ctx.moveTo(this.trail[i-1].x, this.trail[i-1].y); + ctx.lineTo(this.trail[i].x, this.trail[i].y); + } + ctx.stroke(); + + // Draw ball + ctx.fillStyle = '#ffffff'; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fill(); + + ctx.strokeStyle = '#000000'; + ctx.lineWidth = 1; + ctx.stroke(); + } + + shoot(angle, power) { + const radian = angle * Math.PI / 180; + this.vx = Math.cos(radian) * power * 0.3; + this.vy = Math.sin(radian) * power * 0.3; + ballInMotion = true; + this.onGround = false; + this.trail = []; + strokes++; + updateUI(); + } +} + +// Initialize level +function initLevel() { + const planet = planets[currentPlanet]; + + // Set background + canvas.style.background = planet.bg; + + // Create ball + ball = new Ball(100, canvas.height - 100); + + // Create hole + hole = { + x: canvas.width - 100, + y: canvas.height - 100, + radius: 15 + }; + + // Create obstacles + obstacles = []; + const numObstacles = 3 + currentPlanet; + for (let i = 0; i < numObstacles; i++) { + obstacles.push({ + x: 200 + i * 150 + Math.random() * 100, + y: canvas.height - 80 - Math.random() * 200, + width: 20 + Math.random() * 30, + height: 40 + Math.random() * 60 + }); + } +} + +// Level complete +function levelComplete() { + setTimeout(() => { + currentPlanet = (currentPlanet + 1) % planets.length; + initLevel(); + updateUI(); + }, 2000); +} + +// Reset ball +function resetBall() { + ball.x = 100; + ball.y = canvas.height - 100; + ball.vx = 0; + ball.vy = 0; + ballInMotion = false; + ball.onGround = true; + ball.trail = []; +} + +// Update UI +function updateUI() { + scoreElement.textContent = `Strokes: ${strokes}`; + levelElement.textContent = `Planet: ${planets[currentPlanet].name}`; + gravityElement.textContent = `Gravity: ${planets[currentPlanet].gravity}`; +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw obstacles + ctx.fillStyle = '#8B4513'; + obstacles.forEach(obstacle => { + ctx.fillRect(obstacle.x, obstacle.y, obstacle.width, obstacle.height); + }); + + // Draw hole + ctx.fillStyle = '#000000'; + ctx.beginPath(); + ctx.arc(hole.x, hole.y, hole.radius, 0, Math.PI * 2); + ctx.fill(); + + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 2; + ctx.stroke(); + + // Draw ball + ball.draw(); + + // Draw aim line + if (aiming && !ballInMotion) { + ctx.strokeStyle = '#ff0000'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.moveTo(aimStart.x, aimStart.y); + ctx.lineTo(aimEnd.x, aimEnd.y); + ctx.stroke(); + + // Draw power arc + const angle = Math.atan2(aimEnd.y - aimStart.y, aimEnd.x - aimStart.x); + const distance = Math.sqrt((aimEnd.x - aimStart.x) ** 2 + (aimEnd.y - aimStart.y) ** 2); + const maxPower = 100; + const powerPercent = Math.min(distance / maxPower, 1); + + ctx.strokeStyle = '#ffff00'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(aimStart.x, aimStart.y, powerPercent * 50, angle - 0.5, angle + 0.5); + ctx.stroke(); + } + + // Draw UI text + ctx.fillStyle = '#ffffff'; + ctx.font = '16px Poppins'; + ctx.textAlign = 'left'; + ctx.fillText(`Planet: ${planets[currentPlanet].name}`, 20, 40); + ctx.fillText(`Gravity: ${planets[currentPlanet].gravity}`, 20, 60); + ctx.fillText(`Strokes: ${strokes}`, 20, 80); +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + ball.update(); + draw(); + + requestAnimationFrame(gameLoop); +} + +// Event listeners +startButton.addEventListener('click', () => { + instructionsOverlay.style.display = 'none'; + gameRunning = true; + initLevel(); + updateUI(); + gameLoop(); +}); + +canvas.addEventListener('mousedown', (e) => { + if (!gameRunning || ballInMotion) return; + + const rect = canvas.getBoundingClientRect(); + aimStart.x = ball.x; + aimStart.y = ball.y; + aimEnd.x = e.clientX - rect.left; + aimEnd.y = e.clientY - rect.top; + aiming = true; +}); + +canvas.addEventListener('mousemove', (e) => { + if (!aiming) return; + + const rect = canvas.getBoundingClientRect(); + aimEnd.x = e.clientX - rect.left; + aimEnd.y = e.clientY - rect.top; + + // Update power bar + const distance = Math.sqrt((aimEnd.x - aimStart.x) ** 2 + (aimEnd.y - aimStart.y) ** 2); + power = Math.min(distance / 2, 100); + powerBar.style.width = `${power}%`; +}); + +canvas.addEventListener('mouseup', (e) => { + if (!aiming) return; + + const dx = aimEnd.x - aimStart.x; + const dy = aimEnd.y - aimStart.y; + const angle = Math.atan2(dy, dx) * 180 / Math.PI; + + ball.shoot(angle, power); + + aiming = false; + powerBar.style.width = '0%'; +}); + +document.addEventListener('keydown', (e) => { + if (!gameRunning) return; + + switch (e.code) { + case 'KeyR': + resetBall(); + break; + case 'KeyN': + levelComplete(); + break; + } +}); + +// Initialize +updateUI(); \ No newline at end of file diff --git a/games/gravity-golf/style.css b/games/gravity-golf/style.css new file mode 100644 index 00000000..f8e0f20d --- /dev/null +++ b/games/gravity-golf/style.css @@ -0,0 +1,139 @@ +/* General Reset & Font */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Poppins', sans-serif; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background: linear-gradient(135deg, #0f1419 0%, #1a2332 50%, #2d3748 100%); + color: #eee; + overflow: hidden; +} + +/* Game UI */ +#game-ui { + position: absolute; + top: 20px; + left: 20px; + display: flex; + gap: 20px; + z-index: 5; +} + +#score, #level, #gravity { + background-color: rgba(0, 0, 0, 0.7); + color: #fff; + padding: 10px 15px; + border-radius: 5px; + font-size: 1.1rem; + font-weight: 600; +} + +/* Canvas */ +canvas { + background: linear-gradient(135deg, #87CEEB 0%, #98FB98 100%); + border: 3px solid #4CAF50; + box-shadow: 0 0 20px rgba(76, 175, 80, 0.3); + display: block; +} + +/* Power Meter */ +#power-meter { + position: absolute; + bottom: 20px; + left: 20px; + width: 200px; + height: 20px; + background-color: rgba(0, 0, 0, 0.7); + border-radius: 10px; + overflow: hidden; +} + +#power-bar { + height: 100%; + background-color: #ff6b6b; + width: 0%; + transition: width 0.1s; +} + +/* Instructions Screen */ +#instructions-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +#instructions-content { + background-color: #2a2a2a; + padding: 30px 40px; + border-radius: 10px; + text-align: center; + border: 2px solid #4CAF50; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); + max-width: 500px; +} + +#instructions-content h2 { + font-size: 2.5rem; + color: #4CAF50; + margin-bottom: 15px; + letter-spacing: 2px; +} + +#instructions-content p { + font-size: 1.1rem; + margin-bottom: 25px; + color: #ccc; +} + +#instructions-content h3 { + font-size: 1.2rem; + color: #eee; + margin-bottom: 10px; + border-bottom: 1px solid #444; + padding-bottom: 5px; +} + +#instructions-content ul { + list-style: none; + margin-bottom: 30px; + text-align: left; + display: inline-block; +} + +#instructions-content li { + font-size: 1rem; + color: #ccc; + margin-bottom: 8px; +} + +/* Start Button */ +#startButton { + background-color: #4CAF50; + color: white; + border: none; + padding: 12px 24px; + font-size: 1.1rem; + font-weight: 600; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} + +#startButton:hover { + background-color: #45a049; +} \ No newline at end of file diff --git a/games/gravity_flip/index.html b/games/gravity_flip/index.html new file mode 100644 index 00000000..eb931424 --- /dev/null +++ b/games/gravity_flip/index.html @@ -0,0 +1,37 @@ + + + + + + Gravity Switcher Puzzle + + + + +
    +

    ๐Ÿ”„ Gravity Switcher

    + +
    +
    + +
    GOAL
    + +
    +
    +
    + +
    +
    + +
    +

    Press **SPACE** to flip gravity! Reach the GOAL.

    +
    + +
    + +
    +
    + + + + \ No newline at end of file diff --git a/games/gravity_flip/script.js b/games/gravity_flip/script.js new file mode 100644 index 00000000..4b3d176f --- /dev/null +++ b/games/gravity_flip/script.js @@ -0,0 +1,252 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. DOM Elements & Constants --- + const player = document.getElementById('player'); + const levelArea = document.getElementById('level-area'); + const platforms = document.querySelectorAll('.platform'); + const restartButton = document.getElementById('restart-button'); + const messageDisplay = document.getElementById('message'); + + const LEVEL_WIDTH = 500; + const LEVEL_HEIGHT = 500; + const PLAYER_SIZE = 30; + + // --- 2. PHYSICS & GAME STATE --- + let gameActive = false; + let gravityDirection = 1; // 1 = down, -1 = up + + // Player state (x/y in pixels, vy in pixels/frame) + let playerX = 50; + let playerY = 550 - PLAYER_SIZE; // Start on the floor + let playerVY = 0; // Vertical velocity + let isGrounded = true; + let moveLeft = false; + let moveRight = false; + + // Physics constants + const GRAVITY = 0.5; // Vertical acceleration + const JUMP_VELOCITY = 10; + const HORIZONTAL_SPEED = 4; + const FRICTION = 0.9; // Horizontal friction (simplification) + + let lastTime = 0; // Used for requestAnimationFrame timestamp + + // --- 3. GAME LOOP --- + + /** + * The main game loop using requestAnimationFrame for smooth physics simulation. + */ + function gameLoop(timestamp) { + if (!gameActive) return; + + // Calculate delta time (dt) for frame-rate independent physics (simplified here) + const dt = (timestamp - lastTime) / 1000 * 60; // dt โ‰ˆ 1 if 60fps + lastTime = timestamp; + + // 1. Apply Gravity (Acceleration) + playerVY += GRAVITY * gravityDirection * dt; + + // 2. Apply Horizontal Movement (Deceleration/Input) + if (moveLeft) playerX = Math.max(0, playerX - HORIZONTAL_SPEED * dt); + if (moveRight) playerX = Math.min(LEVEL_WIDTH - PLAYER_SIZE, playerX + HORIZONTAL_SPEED * dt); + + // 3. Apply Vertical Movement (Velocity) + playerY += playerVY * dt; + + // 4. Collision Detection (The most complex part) + const collisionData = checkCollisions(); + + if (collisionData.collided) { + // Adjust position to just outside the platform + playerY = collisionData.yAdjust; + + // Stop velocity and mark as grounded + playerVY = 0; + isGrounded = true; + } else { + isGrounded = false; + } + + // 5. Check Boundary Collision (Ceiling/Floor) + if (playerY >= LEVEL_HEIGHT - PLAYER_SIZE) { + playerY = LEVEL_HEIGHT - PLAYER_SIZE; + playerVY = 0; + isGrounded = true; + } else if (playerY <= 0) { + playerY = 0; + playerVY = 0; + isGrounded = true; + } + + // 6. Check Win Condition + if (checkWin()) { + endGame(true); + return; + } + + // 7. Update DOM (Render) + render(); + + // Continue loop + requestAnimationFrame(gameLoop); + } + + /** + * Iterates through all platforms and checks for player intersection. + * @returns {{collided: boolean, yAdjust: number}} Collision result. + */ + function checkCollisions() { + let collided = false; + let yAdjust = playerY; + + const playerRect = { + top: playerY, + bottom: playerY + PLAYER_SIZE, + left: playerX, + right: playerX + PLAYER_SIZE + }; + + for (const platform of platforms) { + const platformRect = platform.getBoundingClientRect(); + // Get coordinates relative to the game area, not the viewport + const containerRect = levelArea.getBoundingClientRect(); + const pTop = platformRect.top - containerRect.top; + const pBottom = platformRect.bottom - containerRect.top; + const pLeft = platformRect.left - containerRect.left; + const pRight = platformRect.right - containerRect.left; + + // Simplified AABB (Axis-Aligned Bounding Box) collision check + const overlapX = playerRect.right > pLeft && playerRect.left < pRight; + + if (overlapX) { + // Collision coming from ABOVE (Gravity Down: playerVY > 0) + if (gravityDirection === 1 && playerVY >= 0 && + playerRect.bottom >= pTop && playerRect.bottom <= pTop + 10) { + + collided = true; + yAdjust = pTop - PLAYER_SIZE; + break; + } + + // Collision coming from BELOW (Gravity Up: playerVY < 0) + if (gravityDirection === -1 && playerVY <= 0 && + playerRect.top <= pBottom && playerRect.top >= pBottom - 10) { + + collided = true; + yAdjust = pBottom; + break; + } + } + } + + return { collided, yAdjust }; + } + + /** + * Checks if the player has touched the goal platform. + */ + function checkWin() { + const goal = document.querySelector('.platform.goal'); + if (!goal) return false; + + const goalRect = goal.getBoundingClientRect(); + const containerRect = levelArea.getBoundingClientRect(); + + const gTop = goalRect.top - containerRect.top; + const gBottom = goalRect.bottom - containerRect.top; + const gLeft = goalRect.left - containerRect.left; + const gRight = goalRect.right - containerRect.left; + + return ( + playerX < gRight && + playerX + PLAYER_SIZE > gLeft && + playerY < gBottom && + playerY + PLAYER_SIZE > gTop + ); + } + + /** + * Updates the player's position in the DOM. + */ + function render() { + player.style.left = `${playerX}px`; + player.style.top = `${playerY}px`; + + // Rotate player visually to indicate gravity direction + player.style.transform = gravityDirection === 1 ? 'rotate(0deg)' : 'rotate(180deg)'; + } + + /** + * Changes the gravity direction and applies an initial impulse (simulated jump). + */ + function flipGravity() { + if (!gameActive) return; + + gravityDirection *= -1; // Flip between 1 and -1 + + // Give a little push upwards/downwards when gravity flips to simulate jump + playerVY = -JUMP_VELOCITY * gravityDirection * 0.5; // Half jump velocity + + // Change color briefly for visual feedback + player.style.backgroundColor = gravityDirection === 1 ? '#e74c3c' : '#3498db'; + } + + /** + * Stops the game and displays the result. + */ + function endGame(win) { + gameActive = false; + cancelAnimationFrame(requestAnimationFrame(gameLoop)); + + if (win) { + messageDisplay.innerHTML = '๐Ÿ† **LEVEL COMPLETE!**'; + messageDisplay.style.color = '#2ecc71'; + } + } + + // --- 4. EVENT LISTENERS AND INITIAL SETUP --- + + function init() { + // Reset player position and physics + playerX = 50; + playerY = 550 - PLAYER_SIZE; + playerVY = 0; + gravityDirection = 1; + isGrounded = true; + gameActive = true; + + messageDisplay.textContent = 'Press SPACE to flip gravity! Reach the GOAL.'; + messageDisplay.style.color = '#bdc3c7'; + player.style.backgroundColor = '#e74c3c'; + + render(); // Initial render + lastTime = performance.now(); + requestAnimationFrame(gameLoop); + } + + // Keyboard controls for horizontal movement and gravity flip + document.addEventListener('keydown', (e) => { + if (!gameActive) return; + + if (e.code === 'Space') { + e.preventDefault(); + flipGravity(); + } else if (e.code === 'ArrowLeft' || e.code === 'KeyA') { + moveLeft = true; + } else if (e.code === 'ArrowRight' || e.code === 'KeyD') { + moveRight = true; + } + }); + + document.addEventListener('keyup', (e) => { + if (e.code === 'ArrowLeft' || e.code === 'KeyA') { + moveLeft = false; + } else if (e.code === 'ArrowRight' || e.code === 'KeyD') { + moveRight = false; + } + }); + + restartButton.addEventListener('click', init); + + // Start the game loop + init(); +}); \ No newline at end of file diff --git a/games/gravity_flip/style.css b/games/gravity_flip/style.css new file mode 100644 index 00000000..55a142da --- /dev/null +++ b/games/gravity_flip/style.css @@ -0,0 +1,95 @@ +:root { + --level-width: 500px; + --level-height: 500px; + --player-size: 30px; +} + +body { + font-family: 'Bebas Neue', sans-serif; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #34495e; /* Dark background */ + color: #ecf0f1; +} + +#game-container { + background-color: #2c3e50; + padding: 20px; + border-radius: 10px; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + text-align: center; +} + +h1 { + color: #f1c40f; /* Yellow */ + margin-bottom: 15px; +} + +/* --- Level Area (The Game World) --- */ +#level-area { + width: var(--level-width); + height: var(--level-height); + background-color: #1a1a1a; + border: 5px solid #bdc3c7; + margin: 0 auto; + position: relative; /* Required for absolute positioning of elements */ + overflow: hidden; +} + +/* --- Player Block --- */ +#player { + width: var(--player-size); + height: var(--player-size); + background-color: #e74c3c; /* Red player */ + position: absolute; + transition: background-color 0.1s; + box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); +} + +/* --- Platforms and Goal --- */ +.platform { + height: 15px; + background-color: #3498db; /* Blue platforms */ + position: absolute; + color: white; + font-size: 0.8em; + display: flex; + align-items: center; + justify-content: center; +} + +.platform.goal { + background-color: #2ecc71; /* Green goal */ + z-index: 10; +} + +/* --- Status and Controls --- */ +#status-area { + min-height: 20px; + margin-top: 15px; +} + +#message { + font-size: 1.1em; + color: #bdc3c7; +} + +#restart-button { + padding: 10px 20px; + font-size: 1em; + font-weight: bold; + background-color: #9b59b6; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s; +} + +#restart-button:hover { + background-color: #8e44ad; +} \ No newline at end of file diff --git a/games/grid-filler/index.html b/games/grid-filler/index.html new file mode 100644 index 00000000..e1d6f6c4 --- /dev/null +++ b/games/grid-filler/index.html @@ -0,0 +1,30 @@ + + + + + + Grid Filler | Mini JS Games Hub + + + +
    +

    Grid Filler ๐ŸŸฆ

    +

    Move the marker to fill all tiles without revisiting. Avoid obstacles!

    +
    +
    + + + + +
    +

    +
    + + + + + + + + + diff --git a/games/grid-filler/script.js b/games/grid-filler/script.js new file mode 100644 index 00000000..2fc90734 --- /dev/null +++ b/games/grid-filler/script.js @@ -0,0 +1,90 @@ +const gridEl = document.querySelector(".grid"); +const statusEl = document.getElementById("status"); +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const resumeBtn = document.getElementById("resume-btn"); +const restartBtn = document.getElementById("restart-btn"); + +const moveSound = document.getElementById("move-sound"); +const winSound = document.getElementById("win-sound"); +const loseSound = document.getElementById("lose-sound"); + +const ROWS = 8; +const COLS = 8; +let tiles = []; +let filled = new Set(); +let obstacles = new Set(); +let currentPos = 0; +let isRunning = false; + +// Generate grid +function createGrid() { + gridEl.innerHTML = ""; + tiles = []; + filled.clear(); + obstacles.clear(); + for (let i = 0; i < ROWS * COLS; i++) { + const tile = document.createElement("div"); + tile.classList.add("tile"); + // Add some random obstacles + if (Math.random() < 0.1 && i !== 0) { + tile.classList.add("obstacle"); + obstacles.add(i); + } + gridEl.appendChild(tile); + tiles.push(tile); + } + currentPos = 0; + tiles[currentPos].classList.add("filled"); + filled.add(currentPos); + statusEl.textContent = "Game Started! Use Arrow Keys or WASD to move."; +} + +// Move marker +function moveMarker(deltaRow, deltaCol) { + if (!isRunning) return; + const row = Math.floor(currentPos / COLS); + const col = currentPos % COLS; + let newRow = row + deltaRow; + let newCol = col + deltaCol; + if (newRow < 0 || newRow >= ROWS || newCol < 0 || newCol >= COLS) return; + + const newPos = newRow * COLS + newCol; + if (filled.has(newPos) || obstacles.has(newPos)) { + statusEl.textContent = "๐Ÿ’€ You lost! Revisited tile or hit obstacle."; + isRunning = false; + loseSound.play(); + return; + } + + currentPos = newPos; + tiles[currentPos].classList.add("filled"); + filled.add(currentPos); + moveSound.play(); + + if (filled.size === ROWS * COLS - obstacles.size) { + statusEl.textContent = "๐ŸŽ‰ You won! Grid completely filled!"; + isRunning = false; + winSound.play(); + } +} + +// Key controls +document.addEventListener("keydown", (e) => { + switch(e.key) { + case "ArrowUp": moveMarker(-1,0); break; + case "ArrowDown": moveMarker(1,0); break; + case "ArrowLeft": moveMarker(0,-1); break; + case "ArrowRight": moveMarker(0,1); break; + case "w": moveMarker(-1,0); break; + case "s": moveMarker(1,0); break; + case "a": moveMarker(0,-1); break; + case "d": moveMarker(0,1); break; + } +}); + +// Buttons +startBtn.addEventListener("click", () => { isRunning = true; createGrid(); }); +pauseBtn.addEventListener("click", () => { isRunning = false; statusEl.textContent = "Game Paused"; }); +resumeBtn.addEventListener("click", () => { isRunning = true; statusEl.textContent = "Game Resumed"; }); +restartBtn.addEventListener("click", () => { isRunning = true; createGrid(); }); diff --git a/games/grid-filler/style.css b/games/grid-filler/style.css new file mode 100644 index 00000000..ea846868 --- /dev/null +++ b/games/grid-filler/style.css @@ -0,0 +1,57 @@ +body { + font-family: 'Segoe UI', sans-serif; + background: #111; + color: #fff; + display: flex; + justify-content: center; + align-items: flex-start; + padding: 20px; +} + +.game-container { + max-width: 600px; + width: 100%; + text-align: center; +} + +.grid { + display: grid; + grid-template-columns: repeat(8, 50px); + grid-template-rows: repeat(8, 50px); + gap: 5px; + margin: 20px 0; +} + +.tile { + width: 50px; + height: 50px; + background: #222; + border-radius: 5px; + transition: background 0.3s, box-shadow 0.3s; +} + +.tile.filled { + background: #0ff; + box-shadow: 0 0 10px #0ff, 0 0 20px #0ff, 0 0 30px #0ff; +} + +.tile.obstacle { + background: #f00; +} + +.controls button { + margin: 5px; + padding: 8px 15px; + font-size: 16px; + border: none; + border-radius: 5px; + cursor: pointer; + background: #222; + color: #fff; + transition: all 0.2s; +} + +.controls button:hover { + background: #0ff; + color: #000; +} diff --git a/games/grid_explorer/index.html b/games/grid_explorer/index.html new file mode 100644 index 00000000..2b6cc32b --- /dev/null +++ b/games/grid_explorer/index.html @@ -0,0 +1,44 @@ + + + + + + Gridlock Explorer + + + + +
    +

    Gridlock Explorer: The CSS Dungeon

    +

    Use the arrow keys to swap your tile's position with an adjacent empty tile. Solve the puzzle by moving the Key to the Exit!

    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    ๐Ÿง‘โ€๐Ÿ’ป
    +
    ๐Ÿ”‘
    +
    + +
    +
    +
    ๐Ÿšช
    +
    + +
    +
    +
    +
    +
    +
    + +

    + + + + \ No newline at end of file diff --git a/games/grid_explorer/script.js b/games/grid_explorer/script.js new file mode 100644 index 00000000..60350656 --- /dev/null +++ b/games/grid_explorer/script.js @@ -0,0 +1,169 @@ +document.addEventListener('DOMContentLoaded', () => { + const gridMap = document.getElementById('grid-map'); + const player = document.getElementById('player'); + const key = document.getElementById('key'); + const messageDisplay = document.getElementById('message'); + const GRID_DIMENSION = 4; + const TOTAL_TILES = GRID_DIMENSION * GRID_DIMENSION; + + // Convert NodeList to Array for easier iteration + let tiles = Array.from(document.querySelectorAll('.tile')); + + // --- 1. Utility Functions --- + + /** + * Retrieves the current visual order of a tile. + * It uses the inline style.order if set, otherwise falls back to the data-order (for initial state). + */ + function getCurrentOrder(tile) { + // The inline style.order (set by JS) takes precedence + if (tile.style.order) { + return parseInt(tile.style.order); + } + // Fallback to the initial setup from HTML data attribute (or CSS class) + return parseInt(tile.dataset.order); + } + + /** + * Finds the tile currently occupying a specific visual order slot. + */ + function getTileByOrder(order) { + return tiles.find(tile => getCurrentOrder(tile) === order); + } + + /** + * Checks if two tile orders are adjacent (horizontally or vertically). + */ + function isAdjacent(orderA, orderB) { + const diff = Math.abs(orderA - orderB); + + // 1. Check for immediate horizontal adjacency (order difference of 1) + const isHorizontal = diff === 1 && (Math.ceil(orderA / GRID_DIMENSION) === Math.ceil(orderB / GRID_DIMENSION)); + + // 2. Check for vertical adjacency (order difference of GRID_DIMENSION) + const isVertical = diff === GRID_DIMENSION; + + return isHorizontal || isVertical; + } + + /** + * CORE GAME MECHANIC: Swaps the order of two tiles. + * This makes them visually swap positions on the grid. + */ + function swapTiles(tileA, tileB) { + const orderA = getCurrentOrder(tileA); + const orderB = getCurrentOrder(tileB); + + // Apply the swap using inline styles to override the CSS class + tileA.style.order = orderB; + tileB.style.order = orderA; + } + + // --- 2. Game Logic --- + + function movePlayer(targetOrder) { + const targetTile = getTileByOrder(targetOrder); + + if (!targetTile) return; // Should not happen + + const playerOrder = getCurrentOrder(player); + + if (!isAdjacent(playerOrder, targetOrder)) { + messageDisplay.textContent = "Cannot move there. Must target an adjacent empty space."; + return; + } + + // --- PUSH/SWAP LOGIC --- + + if (targetTile.classList.contains('empty')) { + // Standard Player Movement: Swap player and empty tile + swapTiles(player, targetTile); + messageDisplay.textContent = "Swapped player and empty space."; + } else if (targetTile.classList.contains('key') && playerCanPush(playerOrder, targetOrder)) { + // Puzzle Mechanic: If the player moves toward the key, they try to push it. + + const nextOrder = getOrderInPushDirection(playerOrder, targetOrder); + const nextTile = getTileByOrder(nextOrder); + + if (nextTile && nextTile.classList.contains('empty')) { + // Pushing is successful: Swap Key with the Empty space, then Player with Key's old spot (the Empty space). + swapTiles(targetTile, nextTile); + swapTiles(player, targetTile); // Player moves into the key's previous spot + messageDisplay.textContent = "Pushed the key one space!"; + } else { + messageDisplay.textContent = "The key cannot be pushed that way (space is blocked)."; + return; + } + } else { + messageDisplay.textContent = "That space is a wall or a blocked exit."; + return; + } + + checkWinCondition(); + } + + // Helper for push logic + function playerCanPush(playerOrder, targetOrder) { + return targetOrder === getOrderInPushDirection(playerOrder, targetOrder); + } + + // Helper for push logic + function getOrderInPushDirection(playerOrder, targetOrder) { + const diff = targetOrder - playerOrder; + + // Calculate the order of the tile *beyond* the target (the push destination) + return targetOrder + diff; + } + + + function checkWinCondition() { + const keyOrder = getCurrentOrder(key); + const exitOrder = getCurrentOrder(document.getElementById('exit')); + + if (keyOrder === exitOrder) { + messageDisplay.textContent = "๐ŸŽ‰ PUZZLE SOLVED! The Key is in the Exit! ๐ŸŽ‰"; + document.removeEventListener('keydown', handleKeyPress); + } + } + + + // --- 3. Input Handling (Arrow Keys) --- + + function handleKeyPress(event) { + const currentOrder = getCurrentOrder(player); + let targetOrder = null; + + // Determine the target order based on the key pressed + switch (event.key) { + case 'ArrowUp': + targetOrder = currentOrder - GRID_DIMENSION; + break; + case 'ArrowDown': + targetOrder = currentOrder + GRID_DIMENSION; + break; + case 'ArrowLeft': + // Check boundary: cannot move left from column 1 (orders 1, 5, 9, 13) + if ((currentOrder - 1) % GRID_DIMENSION !== 0) { + targetOrder = currentOrder - 1; + } + break; + case 'ArrowRight': + // Check boundary: cannot move right from column 4 (orders 4, 8, 12, 16) + if (currentOrder % GRID_DIMENSION !== 0) { + targetOrder = currentOrder + 1; + } + break; + default: + return; + } + + if (targetOrder !== null && targetOrder >= 1 && targetOrder <= TOTAL_TILES) { + movePlayer(targetOrder); + } + } + + document.addEventListener('keydown', handleKeyPress); + + // Initial game message + messageDisplay.textContent = "Find the Key and guide it to the Exit!"; +}); \ No newline at end of file diff --git a/games/grid_explorer/style.css b/games/grid_explorer/style.css new file mode 100644 index 00000000..f082b0f7 --- /dev/null +++ b/games/grid_explorer/style.css @@ -0,0 +1,92 @@ +:root { + --grid-dimension: 4; + --tile-size: 80px; + --wall-color: #5d4037; + --floor-color: #a1887f; + --player-color: gold; + --key-color: yellow; + --exit-color: #4caf50; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + display: flex; + flex-direction: column; + align-items: center; + background-color: #212121; + color: white; + padding: 20px; +} + +#grid-map { + display: grid; + /* Create a 4x4 grid of equal-sized cells */ + grid-template-columns: repeat(var(--grid-dimension), var(--tile-size)); + grid-template-rows: repeat(var(--grid-dimension), var(--tile-size)); + border: 5px solid #333; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); + margin: 20px 0; +} + +.tile { + /* flexbox alignment for content (like emojis) */ + display: flex; + justify-content: center; + align-items: center; + font-size: 24px; + border: 1px solid rgba(0, 0, 0, 0.1); + /* Transition is crucial for a smooth visual swap */ + transition: order 0.3s ease-in-out, background-color 0.2s; + user-select: none; + cursor: default; +} + +/* --- TILE TYPES AND VISUALS --- */ + +.wall { + background-color: var(--wall-color); +} + +.floor { + background-color: var(--floor-color); +} + +.player { + background-color: var(--player-color); +} + +.key { + background-color: var(--key-color); +} + +.exit { + background-color: var(--exit-color); +} + +.empty { + /* The empty tile is the target for swapping */ + background-color: #424242; + cursor: pointer; /* Suggests it's interactable */ +} + +/* --- GRID ORDER TRICK --- */ +/* Initial order set from data-order in HTML */ +.tile[data-order="1"] { order: 1; } +.tile[data-order="2"] { order: 2; } +/* ... (you would continue this for all 16 tiles) */ +.tile[data-order="3"] { order: 3; } +.tile[data-order="4"] { order: 4; } +.tile[data-order="5"] { order: 5; } +.tile[data-order="6"] { order: 6; } +.tile[data-order="7"] { order: 7; } +.tile[data-order="8"] { order: 8; } +.tile[data-order="9"] { order: 9; } +.tile[data-order="10"] { order: 10; } +.tile[data-order="11"] { order: 11; } +.tile[data-order="12"] { order: 12; } +.tile[data-order="13"] { order: 13; } +.tile[data-order="14"] { order: 14; } +.tile[data-order="15"] { order: 15; } +.tile[data-order="16"] { order: 16; } + +/* The JS will update the inline style.order property, overriding these classes */ \ No newline at end of file diff --git a/games/guess game/index.html b/games/guess game/index.html new file mode 100644 index 00000000..3c25c202 --- /dev/null +++ b/games/guess game/index.html @@ -0,0 +1,26 @@ + + + + + + Guess The Number Game + + + +
    +

    Guess the Number! ๐ŸŽฏ

    +

    I'm thinking of a number between **1 and 100**. Can you guess it?

    + +
    + + +
    + +

    + + +
    + + + + \ No newline at end of file diff --git a/games/guess game/script.js b/games/guess game/script.js new file mode 100644 index 00000000..f57603e4 --- /dev/null +++ b/games/guess game/script.js @@ -0,0 +1,92 @@ +// 1. Generate a random number between 1 and 100 +let secretNumber = Math.floor(Math.random() * 100) + 1; + +// 2. Get DOM elements +const guessInput = document.getElementById('guessInput'); +const checkButton = document.getElementById('checkButton'); +const feedback = document.getElementById('feedback'); +const newGameButton = document.getElementById('newGameButton'); + +// 3. Game state variables +let gameOver = false; + +// Function to handle a guess +function checkGuess() { + // If the game is already over, do nothing + if (gameOver) { + return; + } + + // Get the user's guess and convert it to a number + const userGuess = parseInt(guessInput.value); + + // Simple validation + if (isNaN(userGuess) || userGuess < 1 || userGuess > 100) { + feedback.textContent = 'Please enter a valid number between 1 and 100.'; + feedback.style.color = '#dc3545'; // Red color for error + return; + } + + // Main game logic + if (userGuess === secretNumber) { + // Correct Guess + feedback.textContent = `๐ŸŽ‰ You got it! The number was ${secretNumber}!`; + feedback.style.color = '#28a745'; // Green color for win + endGame(); + } else if (userGuess < secretNumber) { + // Too Low + feedback.textContent = 'Too low! Try a higher number.'; + feedback.style.color = '#ffc107'; // Yellow/Orange color + } else { + // Too High + feedback.textContent = 'Too high! Try a lower number.'; + feedback.style.color = '#ffc107'; // Yellow/Orange color + } + + // Clear the input field after each guess + guessInput.value = ''; + guessInput.focus(); // Keep the cursor in the input +} + +// Function to end the current game +function endGame() { + gameOver = true; + checkButton.disabled = true; // Disable the Guess button + newGameButton.classList.remove('hidden'); // Show the Play Again button + guessInput.disabled = true; // Disable the input field +} + +// Function to reset for a new game +function startNewGame() { + // Reset state + secretNumber = Math.floor(Math.random() * 100) + 1; + gameOver = false; + + // Reset UI + guessInput.value = ''; + feedback.textContent = ''; + feedback.style.color = '#555'; // Reset color + checkButton.disabled = false; + guessInput.disabled = false; + newGameButton.classList.add('hidden'); + guessInput.focus(); + + console.log(`New secret number generated: ${secretNumber}`); +} + +// 4. Event Listeners +checkButton.addEventListener('click', checkGuess); +newGameButton.addEventListener('click', startNewGame); + +// Allow pressing 'Enter' key in the input field to check the guess +guessInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + checkGuess(); + } +}); + +// Optional: Log the number to the console for testing purposes +console.log(`Secret number generated (for testing): ${secretNumber}`); + +// Start the game focused +guessInput.focus(); \ No newline at end of file diff --git a/games/guess game/style.css b/games/guess game/style.css new file mode 100644 index 00000000..07be92da --- /dev/null +++ b/games/guess game/style.css @@ -0,0 +1,77 @@ +body { + font-family: Arial, sans-serif; + background-color: #f4f4f9; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; +} + +.container { + background: #ffffff; + padding: 30px; + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + text-align: center; + width: 90%; + max-width: 400px; +} + +h1 { + color: #333; + margin-bottom: 10px; +} + +p { + color: #555; +} + +.game-area { + margin: 20px 0; +} + +#guessInput { + padding: 10px; + font-size: 16px; + border: 1px solid #ccc; + border-radius: 5px; + width: 60%; + margin-right: 5px; +} + +#checkButton, #newGameButton { + padding: 10px 15px; + font-size: 16px; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; + background-color: #007bff; + color: white; +} + +#checkButton:hover { + background-color: #0056b3; +} + +#newGameButton { + background-color: #28a745; + margin-top: 15px; +} + +#newGameButton:hover { + background-color: #1e7e34; +} + +/* Styles for feedback messages */ +.message { + font-weight: bold; + margin-top: 15px; + min-height: 20px; /* Helps prevent layout shift */ +} + +/* Helper class to hide the button initially */ +.hidden { + display: none; +} \ No newline at end of file diff --git a/games/guitar-simulator/index.html b/games/guitar-simulator/index.html new file mode 100644 index 00000000..90524adc --- /dev/null +++ b/games/guitar-simulator/index.html @@ -0,0 +1,114 @@ + + + + + + Guitar Simulator - Mini JS Games Hub + + + +
    +
    +

    ๐ŸŽธ Guitar Simulator

    +

    Click on the fretboard to play notes and learn guitar!

    +
    + +
    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + +
    +
    +
    E
    +
    B
    +
    G
    +
    D
    +
    A
    +
    E
    +
    + +
    + +
    + +
    +
    0
    +
    1
    +
    2
    +
    3
    +
    4
    +
    5
    +
    6
    +
    7
    +
    8
    +
    9
    +
    10
    +
    11
    +
    12
    +
    +
    + +
    +
    +

    Current Note:

    +
    -
    +
    + +
    +

    Played Notes:

    +
    +
    + +
    +

    Detected Chord:

    +
    -
    +
    +
    +
    + +
    +

    How to Play:

    +
      +
    • Click on any fret to play a note
    • +
    • Hold multiple frets to create chords
    • +
    • Use the tuning selector to change guitar tuning
    • +
    • Select chords from the dropdown to see finger positions
    • +
    • Click "Play Chord" to hear the selected chord
    • +
    • Click "Clear" to reset all pressed frets
    • +
    +
    +
    + + + + \ No newline at end of file diff --git a/games/guitar-simulator/script.js b/games/guitar-simulator/script.js new file mode 100644 index 00000000..0afc2ab3 --- /dev/null +++ b/games/guitar-simulator/script.js @@ -0,0 +1,302 @@ +// Guitar Simulator Game Logic +class GuitarSimulator { + constructor() { + this.strings = 6; + this.frets = 13; + this.pressedFrets = new Set(); + this.audioContext = null; + this.currentTuning = 'standard'; + this.tunings = { + standard: ['E', 'B', 'G', 'D', 'A', 'E'], + 'drop-d': ['D', 'B', 'G', 'D', 'A', 'E'], + 'open-g': ['D', 'B', 'G', 'D', 'G', 'D'] + }; + + this.chords = { + 'A': [{string: 5, fret: 0}, {string: 4, fret: 2}, {string: 3, fret: 2}, {string: 2, fret: 2}], + 'Am': [{string: 5, fret: 0}, {string: 4, fret: 2}, {string: 3, fret: 2}], + 'C': [{string: 5, fret: 3}, {string: 4, fret: 2}, {string: 3, fret: 0}, {string: 2, fret: 1}], + 'Cm': [{string: 5, fret: 3}, {string: 4, fret: 4}, {string: 3, fret: 5}], + 'D': [{string: 4, fret: 0}, {string: 3, fret: 0}, {string: 2, fret: 2}, {string: 1, fret: 3}], + 'Dm': [{string: 4, fret: 0}, {string: 3, fret: 0}, {string: 2, fret: 2}], + 'E': [{string: 5, fret: 0}, {string: 4, fret: 0}, {string: 3, fret: 0}], + 'Em': [{string: 5, fret: 0}, {string: 4, fret: 0}], + 'G': [{string: 5, fret: 3}, {string: 4, fret: 2}, {string: 3, fret: 0}, {string: 2, fret: 0}], + 'Gm': [{string: 5, fret: 3}, {string: 4, fret: 3}, {string: 3, fret: 3}] + }; + + this.noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; + this.init(); + } + + init() { + this.initAudio(); + this.createFretboard(); + this.bindEvents(); + this.updateStringNames(); + } + + initAudio() { + try { + this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } catch (e) { + console.warn('Web Audio API not supported'); + } + } + + createFretboard() { + const fretboard = document.getElementById('fretboard'); + fretboard.innerHTML = ''; + + for (let string = 0; string < this.strings; string++) { + const stringElement = document.createElement('div'); + stringElement.className = 'string'; + + for (let fret = 0; fret < this.frets; fret++) { + const fretElement = document.createElement('div'); + fretElement.className = 'fret'; + fretElement.dataset.string = string; + fretElement.dataset.fret = fret; + + // Add note marker for open strings and certain frets + if (fret > 0) { + const note = this.getNoteName(string, fret); + const marker = document.createElement('div'); + marker.className = 'fret-marker'; + marker.textContent = note; + fretElement.appendChild(marker); + } + + stringElement.appendChild(fretElement); + } + + fretboard.appendChild(stringElement); + } + } + + getNoteName(stringIndex, fret) { + const openNote = this.tunings[this.currentTuning][stringIndex]; + const openNoteIndex = this.noteNames.indexOf(openNote); + const noteIndex = (openNoteIndex + fret) % 12; + return this.noteNames[noteIndex]; + } + + getNoteFrequency(stringIndex, fret) { + const openNote = this.tunings[this.currentTuning][stringIndex]; + const openNoteIndex = this.noteNames.indexOf(openNote); + const noteIndex = (openNoteIndex + fret) % 12; + const octave = Math.floor((openNoteIndex + fret) / 12); + + // Base frequency for C4 = 261.63 Hz + const baseFrequency = 261.63; + const semitoneRatio = Math.pow(2, 1/12); + + // Calculate frequency based on note index from C4 + const noteOffset = noteIndex - this.noteNames.indexOf('C'); + return baseFrequency * Math.pow(semitoneRatio, noteOffset + (octave * 12)); + } + + playNote(stringIndex, fret) { + if (!this.audioContext) return; + + const frequency = this.getNoteFrequency(stringIndex, fret); + + // Create oscillator for the note + const oscillator = this.audioContext.createOscillator(); + const gainNode = this.audioContext.createGain(); + + // Guitar-like sound with some harmonics + oscillator.type = 'sawtooth'; + oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime); + + // Add some filtering for guitar tone + const filter = this.audioContext.createBiquadFilter(); + filter.type = 'lowpass'; + filter.frequency.setValueAtTime(2000, this.audioContext.currentTime); + + // Envelope for attack/decay + gainNode.gain.setValueAtTime(0, this.audioContext.currentTime); + gainNode.gain.linearRampToValueAtTime(0.3, this.audioContext.currentTime + 0.01); + gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 1); + + // Connect nodes + oscillator.connect(filter); + filter.connect(gainNode); + gainNode.connect(this.audioContext.destination); + + // Play the note + oscillator.start(); + oscillator.stop(this.audioContext.currentTime + 1); + } + + bindEvents() { + // Fret click events + document.getElementById('fretboard').addEventListener('click', (e) => { + if (e.target.classList.contains('fret')) { + const stringIndex = parseInt(e.target.dataset.string); + const fret = parseInt(e.target.dataset.fret); + this.toggleFret(stringIndex, fret); + } + }); + + // Tuning selector + document.getElementById('tuning-select').addEventListener('change', (e) => { + this.currentTuning = e.target.value; + this.updateStringNames(); + this.createFretboard(); + this.clearAllFrets(); + }); + + // Chord selector + document.getElementById('chord-select').addEventListener('change', (e) => { + const chord = e.target.value; + if (chord) { + this.showChord(chord); + } + }); + + // Play chord button + document.getElementById('play-chord-btn').addEventListener('click', () => { + this.playCurrentChord(); + }); + + // Clear button + document.getElementById('clear-btn').addEventListener('click', () => { + this.clearAllFrets(); + }); + } + + toggleFret(stringIndex, fret) { + const fretKey = `${stringIndex}-${fret}`; + const fretElement = document.querySelector(`[data-string="${stringIndex}"][data-fret="${fret}"]`); + + if (this.pressedFrets.has(fretKey)) { + this.pressedFrets.delete(fretKey); + fretElement.classList.remove('pressed'); + } else { + this.pressedFrets.add(fretKey); + fretElement.classList.add('pressed'); + this.playNote(stringIndex, fret); + } + + this.updateDisplay(); + } + + clearAllFrets() { + this.pressedFrets.clear(); + document.querySelectorAll('.fret.pressed').forEach(fret => { + fret.classList.remove('pressed'); + }); + this.updateDisplay(); + } + + showChord(chordName) { + this.clearAllFrets(); + const chord = this.chords[chordName]; + if (chord) { + chord.forEach(({string, fret}) => { + this.toggleFret(string, fret); + }); + } + } + + playCurrentChord() { + const notes = Array.from(this.pressedFrets).map(key => { + const [stringIndex, fret] = key.split('-').map(Number); + return { stringIndex, fret }; + }); + + // Play all notes in the chord simultaneously + notes.forEach(({stringIndex, fret}) => { + setTimeout(() => this.playNote(stringIndex, fret), Math.random() * 50); + }); + } + + updateDisplay() { + // Update current note display + const notes = Array.from(this.pressedFrets).map(key => { + const [stringIndex, fret] = key.split('-').map(Number); + return this.getNoteName(stringIndex, fret); + }); + + const currentNoteDisplay = document.getElementById('current-note-display'); + if (notes.length === 1) { + currentNoteDisplay.textContent = notes[0]; + } else { + currentNoteDisplay.textContent = notes.length > 0 ? 'Chord' : '-'; + } + + // Update played notes display + const playedNotesDisplay = document.getElementById('played-notes-display'); + playedNotesDisplay.innerHTML = ''; + notes.forEach(note => { + const chip = document.createElement('div'); + chip.className = 'note-chip'; + chip.textContent = note; + playedNotesDisplay.appendChild(chip); + }); + + // Update chord display + const chordDisplay = document.getElementById('chord-display'); + const detectedChord = this.detectChord(notes); + chordDisplay.textContent = detectedChord || '-'; + } + + detectChord(notes) { + if (notes.length < 3) return null; + + // Simple chord detection - this could be expanded + const uniqueNotes = [...new Set(notes)].sort(); + + // Check for common chords + const chordPatterns = { + 'A': ['A', 'C#', 'E'], + 'Am': ['A', 'C', 'E'], + 'C': ['C', 'E', 'G'], + 'Cm': ['C', 'D#', 'G'], + 'D': ['D', 'F#', 'A'], + 'Dm': ['D', 'F', 'A'], + 'E': ['E', 'G#', 'B'], + 'Em': ['E', 'G', 'B'], + 'G': ['G', 'B', 'D'], + 'Gm': ['G', 'A#', 'D'] + }; + + for (const [chordName, pattern] of Object.entries(chordPatterns)) { + if (this.arraysEqual(uniqueNotes, pattern)) { + return chordName; + } + } + + return null; + } + + arraysEqual(a, b) { + if (a.length !== b.length) return false; + return a.every((val, index) => val === b[index]); + } + + updateStringNames() { + const stringNames = document.querySelectorAll('.string-name'); + const tuning = this.tunings[this.currentTuning]; + + stringNames.forEach((nameElement, index) => { + nameElement.textContent = tuning[index]; + }); + } +} + +// Initialize the game when the page loads +document.addEventListener('DOMContentLoaded', () => { + new GuitarSimulator(); +}); + +// Enable audio on first user interaction +document.addEventListener('click', () => { + if (window.AudioContext && window.AudioContext.prototype.resume) { + const audioContext = new AudioContext(); + if (audioContext.state === 'suspended') { + audioContext.resume(); + } + } +}, { once: true }); \ No newline at end of file diff --git a/games/guitar-simulator/style.css b/games/guitar-simulator/style.css new file mode 100644 index 00000000..90c46d31 --- /dev/null +++ b/games/guitar-simulator/style.css @@ -0,0 +1,322 @@ +/* Guitar Simulator Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #2c3e50, #34495e); + color: #ecf0f1; + min-height: 100vh; + padding: 20px; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 30px; +} + +header h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +header p { + font-size: 1.2em; + opacity: 0.8; +} + +.game-area { + display: flex; + flex-direction: column; + gap: 20px; + margin-bottom: 30px; +} + +.controls { + display: flex; + justify-content: center; + gap: 20px; + flex-wrap: wrap; + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + backdrop-filter: blur(10px); +} + +.control-group { + display: flex; + flex-direction: column; + gap: 5px; +} + +.control-group label { + font-weight: bold; + font-size: 0.9em; +} + +.control-group select, +.control-group button { + padding: 8px 12px; + border: none; + border-radius: 5px; + font-size: 1em; + cursor: pointer; + transition: all 0.3s ease; +} + +.control-group select { + background: #ecf0f1; + color: #2c3e50; +} + +.control-group button { + background: #3498db; + color: white; + font-weight: bold; +} + +.control-group button:hover { + background: #2980b9; + transform: translateY(-2px); +} + +.fretboard-container { + display: flex; + align-items: center; + gap: 10px; + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + backdrop-filter: blur(10px); + overflow-x: auto; +} + +.string-names { + display: flex; + flex-direction: column; + gap: 15px; + margin-right: 10px; +} + +.string-name { + font-weight: bold; + font-size: 1.2em; + color: #f39c12; + text-align: center; + width: 30px; +} + +.fretboard { + display: flex; + gap: 2px; + position: relative; +} + +.string { + display: flex; + position: relative; + height: 60px; + gap: 2px; +} + +.fret { + width: 50px; + height: 100%; + background: #8b4513; + border: 1px solid #654321; + cursor: pointer; + position: relative; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + border-radius: 3px; +} + +.fret:hover { + background: #a0522d; + transform: scale(1.05); +} + +.fret.pressed { + background: #ffd700 !important; + box-shadow: 0 0 15px rgba(255, 215, 0, 0.6); + animation: press 0.3s ease; +} + +@keyframes press { + 0% { transform: scale(1); } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); } +} + +.fret-marker { + position: absolute; + top: -25px; + left: 50%; + transform: translateX(-50%); + font-size: 0.8em; + font-weight: bold; + color: #ecf0f1; + background: rgba(0, 0, 0, 0.7); + padding: 2px 4px; + border-radius: 3px; + pointer-events: none; +} + +.fret-number { + position: absolute; + bottom: -25px; + left: 50%; + transform: translateX(-50%); + font-size: 0.8em; + color: #bdc3c7; +} + +.fret-numbers { + position: relative; + display: flex; + gap: 2px; + margin-left: 10px; +} + +.fret-number { + width: 50px; + text-align: center; + font-size: 0.9em; + color: #bdc3c7; +} + +.info-panel { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + backdrop-filter: blur(10px); +} + +.info-panel h3 { + color: #3498db; + margin-bottom: 10px; + font-size: 1.1em; +} + +#current-note-display, +#chord-display { + font-size: 1.5em; + font-weight: bold; + color: #f39c12; + min-height: 30px; +} + +#played-notes-display { + display: flex; + flex-wrap: wrap; + gap: 5px; + min-height: 30px; +} + +.note-chip { + background: #3498db; + color: white; + padding: 4px 8px; + border-radius: 15px; + font-size: 0.9em; + font-weight: bold; +} + +.instructions { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + backdrop-filter: blur(10px); +} + +.instructions h3 { + color: #3498db; + margin-bottom: 15px; +} + +.instructions ul { + list-style: none; + padding: 0; +} + +.instructions li { + margin-bottom: 8px; + padding-left: 20px; + position: relative; +} + +.instructions li:before { + content: "๐ŸŽธ"; + position: absolute; + left: 0; + color: #f39c12; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .fretboard-container { + flex-direction: column; + gap: 15px; + } + + .string-names { + flex-direction: row; + margin-right: 0; + margin-bottom: 10px; + gap: 10px; + } + + .string-name { + width: auto; + margin-right: 10px; + } + + .fret { + width: 40px; + } + + .fret-number { + width: 40px; + font-size: 0.8em; + } + + .controls { + flex-direction: column; + align-items: center; + } + + .control-group { + width: 100%; + max-width: 300px; + } +} + +@media (max-width: 480px) { + .fret { + width: 35px; + height: 50px; + } + + .fret-number { + width: 35px; + } + + header h1 { + font-size: 2em; + } +} \ No newline at end of file diff --git a/games/hand-cricket/index.html b/games/hand-cricket/index.html new file mode 100644 index 00000000..52345f73 --- /dev/null +++ b/games/hand-cricket/index.html @@ -0,0 +1,56 @@ + + + + + + Cricket Game + + + +
    +

    Cricket Batting Challenge

    + +
    + + + +
    + + + +

    Ball by Ball Score Card

    + + + + + + + + + +
    BallBatsmanBowler
    +
    + + + + \ No newline at end of file diff --git a/games/hand-cricket/script.js b/games/hand-cricket/script.js new file mode 100644 index 00000000..a7df12d8 --- /dev/null +++ b/games/hand-cricket/script.js @@ -0,0 +1,131 @@ +const OPT = [0, 1, 2, 4, 6]; +let totalBalls = 0; +let ball = 0; +let wicket = 0; +let score = 0; +let gameActive = false; + +const setupSection = document.getElementById('setup-section'); +const gameSection = document.getElementById('game-section'); +const oversInput = document.getElementById('overs-input'); +const currentScoreDisplay = document.getElementById('current-score'); +const remainingBallsDisplay = document.getElementById('remaining-balls'); +const scoreTableBody = document.querySelector('#score-table tbody'); +const bowlerChoiceDisplay = document.getElementById('bowler-choice'); +const batsmanChoiceDisplay = document.getElementById('batsman-choice'); +const ballResultDisplay = document.getElementById('ball-result'); +const runButtons = document.querySelectorAll('.options button'); +const resetButton = document.getElementById('reset-button'); + +function startGame() { + const overs = parseInt(oversInput.value); + if (isNaN(overs) || overs < 1) { + alert("Please enter a valid number of overs (1 or more)."); + return; + } + + totalBalls = overs * 6; + gameActive = true; + resetState(); + setupSection.style.display = 'none'; + gameSection.style.display = 'block'; + updateDisplay(); +} + +function resetState() { + ball = 0; + wicket = 0; + score = 0; + scoreTableBody.innerHTML = ''; + bowlerChoiceDisplay.textContent = '-'; + batsmanChoiceDisplay.textContent = '-'; + ballResultDisplay.textContent = 'Waiting...'; + resetButton.style.display = 'none'; + runButtons.forEach(button => button.disabled = false); + gameActive = true; +} + +function takeShot(bats) { + if (!gameActive) return; + + const bowl = OPT[Math.floor(Math.random() * OPT.length)]; + batsmanChoiceDisplay.textContent = bats; + bowlerChoiceDisplay.textContent = bowl; + + let resultText = ''; + let batsEntry, bowlEntry; + + if (bats === bowl) { + wicket += 1; + batsEntry = 'W'; + bowlEntry = `WKT ${wicket}`; + resultText = 'OUT!'; + ballResultDisplay.style.color = 'red'; + } else { + score += bats; + batsEntry = bats; + bowlEntry = '-'; + resultText = `${bats} RUN${bats === 1 ? '' : 'S'}!`; + ballResultDisplay.style.color = (bats === 4 || bats === 6) ? 'blue' : 'green'; + } + + ball += 1; + ballResultDisplay.textContent = resultText; + updateDisplay(); + updateScoreTable(batsEntry, bowlEntry); + checkGameOver(); +} + +function checkGameOver() { + if (wicket >= 2) { + endGame("Game Over! Two wickets down."); + } else if (ball >= totalBalls) { + endGame("Overs completed. Game Over!"); + } +} + +function endGame(message) { + gameActive = false; + runButtons.forEach(button => button.disabled = true); + ballResultDisplay.textContent = message; + resetButton.style.display = 'block'; + updateDisplay(); +} + +function updateDisplay() { + const completeOvers = Math.floor(ball / 6); + const ballsInCurrentOver = ball % 6; + const oversDisplay = `${completeOvers}.${ballsInCurrentOver}`; + currentScoreDisplay.textContent = `Score: ${score}/${wicket} (${oversDisplay} overs)`; + + const ballsRemaining = totalBalls - ball; + if (gameActive) { + remainingBallsDisplay.textContent = `Balls remaining: ${ballsRemaining}`; + } else { + remainingBallsDisplay.textContent = `Final Score: ${score}/${wicket} from ${oversDisplay} overs.`; + } +} + +function updateScoreTable(batsEntry, bowlEntry) { + const newRow = scoreTableBody.insertRow(0); + const ballCell = newRow.insertCell(0); + const completeOvers = Math.floor((ball - 1) / 6); + const ballInOver = ((ball - 1) % 6) + 1; + ballCell.textContent = `${completeOvers}.${ballInOver}`; + + const batsmanCell = newRow.insertCell(1); + batsmanCell.textContent = batsEntry; + + const bowlerCell = newRow.insertCell(2); + bowlerCell.textContent = bowlEntry; + + if (batsEntry === 'W') { + newRow.style.backgroundColor = '#ffcccc'; + } +} + +function resetGame() { + setupSection.style.display = 'block'; + gameSection.style.display = 'none'; + resetState(); +} diff --git a/games/hand-cricket/style.css b/games/hand-cricket/style.css new file mode 100644 index 00000000..c54bdec4 --- /dev/null +++ b/games/hand-cricket/style.css @@ -0,0 +1,66 @@ +body { + font-family: sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background-color: #f4f4f9; + margin: 0; +} + +.game-container { + background: #ffffff; + padding: 30px; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 600px; + width: 90%; +} + +h1 { + color: #38761d; +} + +.options button { + padding: 10px 20px; + margin: 5px; + font-size: 1.1em; + cursor: pointer; + background-color: #6aa84f; + color: white; + border: none; + border-radius: 5px; + transition: background-color 0.2s; +} + +.options button:hover { + background-color: #38761d; +} + +#setup-section, .scorecard-display { + margin-bottom: 20px; + padding: 10px; + border: 1px solid #ddd; + border-radius: 5px; +} + +#current-score { + color: #cc0000; +} + +#score-table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; +} + +#score-table th, #score-table td { + border: 1px solid #dddddd; + padding: 8px; + text-align: center; +} + +#score-table th { + background-color: #e6e6e6; +} diff --git a/games/hang-man/index.html b/games/hang-man/index.html new file mode 100644 index 00000000..58502db5 --- /dev/null +++ b/games/hang-man/index.html @@ -0,0 +1,48 @@ + + + + + + Hangman Game + + + +
    +

    Classic Hangman

    + +
    + +
    +
    +
    + + + + + + +
    +
    + +
    +

    Press any letter to start!

    +
    +
    + +
    + Guessed Letters: +
    + +

    Remaining Guesses: 7

    + + +
    +
    + +
    + + + + + + \ No newline at end of file diff --git a/games/hang-man/script.js b/games/hang-man/script.js new file mode 100644 index 00000000..1032a44b --- /dev/null +++ b/games/hang-man/script.js @@ -0,0 +1,182 @@ +// --- Dictionary --- +const DICTIONARY = [ + "JAVASCRIPT", "HTML", "CSS", "PROGRAMMING", "COMPUTER", + "ALGORITHM", "FRAMEWORK", "VARIABLE", "FUNCTION", "DEVELOPER", + "CODE", "FLOPPY", "CONNECT", "SCRAMBLE", "REACT", "NODE" +]; + +// Map of incorrect guesses count to the CSS element ID to reveal +const HANGMAN_PARTS = [ + 'head', + 'body', + 'left-arm', + 'right-arm', + 'left-leg', + 'right-leg' +]; +const MAX_GUESSES = HANGMAN_PARTS.length + 1; // 7 total guesses (6 parts + 1 initial mistake) + + +// --- DOM Elements --- +const wordDisplayEl = document.getElementById('word-display'); +const guessedLettersEl = document.getElementById('guessed-letters'); +const guessCountEl = document.getElementById('guess-count'); +const messageEl = document.getElementById('message'); +const newGameBtn = document.getElementById('new-game-btn'); +const keyboardInput = document.getElementById('keyboard-input'); + +// --- Game State Variables --- +let selectedWord = ''; +let visibleWord = []; +let guessedLetters = new Set(); +let incorrectGuesses = 0; +let gameActive = false; + +// --- Utility Functions --- + +/** + * Picks a random word from the dictionary. + * @returns {string} The chosen word, converted to uppercase. + */ +function selectRandomWord() { + const index = Math.floor(Math.random() * DICTIONARY.length); + return DICTIONARY[index].toUpperCase(); +} + +/** + * Initializes or resets the game state. + */ +function initGame() { + selectedWord = selectRandomWord(); + visibleWord = Array(selectedWord.length).fill('_'); + guessedLetters.clear(); + incorrectGuesses = 0; + gameActive = true; + + // Reset UI + wordDisplayEl.textContent = visibleWord.join(' '); + guessedLettersEl.textContent = ''; + guessCountEl.textContent = MAX_GUESSES; + messageEl.textContent = "Guess a letter!"; + messageEl.style.color = 'black'; + newGameBtn.classList.add('hidden'); + + // Hide all hangman parts + HANGMAN_PARTS.forEach(id => { + document.getElementById(id).classList.add('hidden'); + }); + + // Re-focus the hidden input to capture key presses + keyboardInput.focus(); +} + +/** + * Updates the word display with new correct guesses. + */ +function updateWordDisplay(letter) { + let letterFound = false; + for (let i = 0; i < selectedWord.length; i++) { + if (selectedWord[i] === letter) { + visibleWord[i] = letter; + letterFound = true; + } + } + wordDisplayEl.textContent = visibleWord.join(' '); + return letterFound; +} + +/** + * Updates the hangman figure based on incorrect guesses. + */ +function drawHangman() { + if (incorrectGuesses <= HANGMAN_PARTS.length) { + // The index of the part to reveal is incorrectGuesses - 1 + const partId = HANGMAN_PARTS[incorrectGuesses - 1]; + if (partId) { + document.getElementById(partId).classList.remove('hidden'); + } + } + guessCountEl.textContent = MAX_GUESSES - incorrectGuesses; +} + +/** + * Checks if the player has won or lost. + */ +function checkGameStatus() { + // 1. Win condition: No more blanks left in the visible word + if (!visibleWord.includes('_')) { + messageEl.textContent = "YOU WON! ๐ŸŽ‰"; + messageEl.style.color = 'var(--success-color)'; + gameActive = false; + } + // 2. Loss condition: Max incorrect guesses reached + else if (incorrectGuesses >= MAX_GUESSES) { + messageEl.textContent = `GAME OVER! The word was: ${selectedWord}`; + messageEl.style.color = 'var(--danger-color)'; + gameActive = false; + } + + if (!gameActive) { + newGameBtn.classList.remove('hidden'); + } +} + +// --- Main Guess Handler --- + +/** + * Processes the player's letter guess. + * @param {string} letter - The letter guessed by the player. + */ +function handleGuess(letter) { + if (!gameActive) return; + + const char = letter.toUpperCase(); + + // 1. Validation: Must be a single, new letter + if (char.length !== 1 || !/[A-Z]/.test(char)) return; + + if (guessedLetters.has(char)) { + messageEl.textContent = `You already guessed '${char}'.`; + messageEl.style.color = 'orange'; + return; + } + + guessedLetters.add(char); + guessedLettersEl.textContent = Array.from(guessedLetters).join(', '); + messageEl.textContent = "Keep guessing!"; + messageEl.style.color = 'black'; + + + // 2. Check if the letter is in the word + const isCorrect = updateWordDisplay(char); + + if (!isCorrect) { + incorrectGuesses++; + drawHangman(); + } + + // 3. Check for win/loss + checkGameStatus(); +} + +// --- Event Listeners --- + +// Listen to key presses on the hidden input field +keyboardInput.addEventListener('input', (e) => { + // Take the last character typed (since we set maxlength=1) + const letter = e.data || e.target.value.slice(-1); + + if (letter) { + handleGuess(letter); + } + // Clear the input field after processing to allow next input + e.target.value = ''; +}); + +// Focus the hidden input when the page loads or the game container is clicked +window.onload = initGame; +document.addEventListener('click', () => { + keyboardInput.focus(); +}); + +newGameBtn.addEventListener('click', initGame); \ No newline at end of file diff --git a/games/hang-man/style.css b/games/hang-man/style.css new file mode 100644 index 00000000..3871734d --- /dev/null +++ b/games/hang-man/style.css @@ -0,0 +1,196 @@ +:root { + --primary-color: #3498db; + --danger-color: #e74c3c; + --success-color: #2ecc71; +} + +body { + font-family: 'Courier New', monospace; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f0f4f8; +} + +#game-container { + background-color: #fff; + padding: 30px; + border-radius: 12px; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2); + width: 90%; + max-width: 600px; + text-align: center; +} + +h1 { + color: var(--primary-color); + margin-top: 0; +} + +#game-area { + display: flex; + flex-direction: column; + align-items: center; +} + +/* --- Hangman Visuals (CSS Drawing) --- */ +#hangman-visuals { + width: 100%; + height: 250px; + margin-bottom: 20px; + position: relative; + border-bottom: 5px solid #333; /* Base */ +} + +.scaffold { + position: relative; + width: 150px; + height: 100%; + margin: 0 auto; +} + +#gallows { + position: absolute; + top: 0; + left: 50%; + width: 5px; + height: 100%; + background-color: #333; + transform: translateX(-50%); +} + +#gallows::before { + /* Top beam */ + content: ''; + position: absolute; + top: 0; + left: -50px; + width: 100px; + height: 5px; + background-color: #333; +} + +#gallows::after { + /* Rope */ + content: ''; + position: absolute; + top: 5px; + left: 45px; + width: 5px; + height: 30px; + background-color: #333; +} + +/* --- Hangman Parts --- */ +#head, #body, #left-arm, #right-arm, #left-leg, #right-leg { + position: absolute; + background-color: #333; + z-index: 10; + transition: opacity 0.3s; +} + +/* Head */ +#head { + width: 30px; + height: 30px; + border-radius: 50%; + top: 35px; + left: calc(50% + 40px - 15px); /* Positioned relative to gallows */ + border: 3px solid #333; + background-color: white; +} + +/* Body */ +#body { + width: 5px; + height: 60px; + top: 70px; + left: calc(50% + 40px - 2.5px); +} + +/* Arms */ +#left-arm, #right-arm { + width: 30px; + height: 5px; + top: 80px; + transform-origin: left center; +} +#left-arm { + left: calc(50% + 40px - 30px); + transform: rotate(30deg); +} +#right-arm { + left: calc(50% + 40px + 5px); + transform: rotate(-30deg); +} + +/* Legs */ +#left-leg, #right-leg { + width: 40px; + height: 5px; + top: 130px; + transform-origin: left center; +} +#left-leg { + left: calc(50% + 40px - 40px); + transform: rotate(60deg); +} +#right-leg { + left: calc(50% + 40px + 5px); + transform: rotate(-60deg); +} + +.hidden { + opacity: 0; +} + +/* --- Puzzle Details --- */ +#puzzle-details { + margin-top: 10px; +} + +#message { + font-size: 1.2em; + font-weight: bold; + min-height: 25px; /* Prevent layout shift */ +} + +#word-display { + font-size: 3em; + letter-spacing: 15px; + margin-bottom: 20px; +} + +#guessed-letters-display { + font-size: 1.1em; + margin-bottom: 15px; +} + +#guessed-letters { + color: var(--danger-color); + font-weight: bold; + letter-spacing: 2px; +} + +#guesses-left { + font-size: 1.1em; + color: #555; +} + +#guess-count { + font-weight: bold; + color: var(--danger-color); +} + +#new-game-btn { + padding: 10px 20px; + background-color: var(--success-color); + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 1.1em; + margin-top: 20px; +} \ No newline at end of file diff --git a/games/hangman/index.html b/games/hangman/index.html new file mode 100644 index 00000000..44ef8a6a --- /dev/null +++ b/games/hangman/index.html @@ -0,0 +1,54 @@ + + + + + + Hangman โ€” Mini JS Games Hub + + + +
    +
    +

    Hangman

    +

    Guess the word before the man is hanged!

    +
    + +
    +
    +
    _ _ _ _ _
    + +
    +
    Lives: 6
    +
    +
    + +
    + +
    + Guessed: โ€” +
    + +
    + + +
    +
    + + +
    + + +
    + + + + diff --git a/games/hangman/script.js b/games/hangman/script.js new file mode 100644 index 00000000..6049e0b8 --- /dev/null +++ b/games/hangman/script.js @@ -0,0 +1,122 @@ +// Simple Hangman game +(() => { + const wordEl = document.getElementById('word'); + const lettersEl = document.getElementById('letters'); + const livesEl = document.getElementById('lives'); + const guessedEl = document.getElementById('guessed'); + const statusEl = document.getElementById('status'); + const newGameBtn = document.getElementById('new-game'); + const giveUpBtn = document.getElementById('give-up'); + + // Word list - you can expand this + const WORDS = [ + 'javascript','hangman','developer','browser','function', + 'variable','object','prototype','algorithm','interface', + 'asynchronous','promise','callback','document','element' + ]; + + const ALPHABET = 'abcdefghijklmnopqrstuvwxyz'.split(''); + + let answer = ''; + let guessed = new Set(); + let wrong = 0; + const MAX_WRONG = 6; + + function pickWord(){ + return WORDS[Math.floor(Math.random()*WORDS.length)]; + } + + function renderWord(){ + const out = answer.split('').map(ch => (guessed.has(ch) ? ch : '_')).join(' '); + wordEl.textContent = out; + if (!out.includes('_')) { + statusEl.textContent = 'You Win! ๐ŸŽ‰'; + statusEl.style.color = ''; + disableAllLetters(); + } + } + + function renderLetters(){ + lettersEl.innerHTML = ''; + ALPHABET.forEach(letter => { + const btn = document.createElement('button'); + btn.textContent = letter; + btn.dataset.letter = letter; + btn.addEventListener('click', () => handleGuess(letter, btn)); + lettersEl.appendChild(btn); + }); + } + + function handleGuess(letter, btn){ + if (btn.classList.contains('used')) return; + btn.classList.add('used'); + guessed.add(letter); + updateGuessedDisplay(); + + if (answer.includes(letter)) { + btn.classList.add('correct'); + } else { + btn.classList.add('wrong'); + wrong++; + livesEl.textContent = MAX_WRONG - wrong; + } + + renderWord(); + checkLose(); + } + + function updateGuessedDisplay(){ + const arr = Array.from(guessed).sort(); + guessedEl.textContent = arr.length ? arr.join(', ') : 'โ€”'; + } + + function disableAllLetters(){ + document.querySelectorAll('#letters button').forEach(b => { + b.classList.add('used'); + }); + } + + function checkLose(){ + if (wrong >= MAX_WRONG) { + statusEl.textContent = `You lost โ€” the word was "${answer}".`; + statusEl.style.color = 'rgba(251,113,133,1)'; + revealAnswer(); + disableAllLetters(); + } + } + + function revealAnswer(){ + wordEl.textContent = answer.split('').join(' '); + } + + function startGame(){ + answer = pickWord(); + guessed = new Set(); + wrong = 0; + livesEl.textContent = MAX_WRONG; + statusEl.textContent = ''; + statusEl.style.color = ''; + renderLetters(); + renderWord(); + updateGuessedDisplay(); + } + + newGameBtn.addEventListener('click', startGame); + giveUpBtn.addEventListener('click', () => { + statusEl.textContent = `Given up โ€” the word was "${answer}".`; + statusEl.style.color = 'rgba(249,115,22,1)'; + revealAnswer(); + disableAllLetters(); + }); + + // keyboard support + window.addEventListener('keydown', (e) => { + const letter = e.key.toLowerCase(); + if (!/^[a-z]$/.test(letter)) return; + const btn = document.querySelector(`#letters button[data-letter="${letter}"]`); + if (btn) handleGuess(letter, btn); + }); + + // start first game on load + startGame(); +})(); diff --git a/games/hangman/style.css b/games/hangman/style.css new file mode 100644 index 00000000..21144da8 --- /dev/null +++ b/games/hangman/style.css @@ -0,0 +1,119 @@ +:root{ + --bg:#0f1724; + --card:#0b1220; + --accent:#60a5fa; + --muted:#94a3b8; + --success:#34d399; + --danger:#fb7185; + --glass: rgba(255,255,255,0.03); +} + +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + font-family:Inter,system-ui,Segoe UI,Arial; + background: linear-gradient(180deg,var(--bg), #071226 120%); + color:#e6eef6; + display:flex; + align-items:flex-start; + justify-content:center; + padding:28px; +} + +.container{ + width:100%; + max-width:980px; + padding:22px; + background:linear-gradient(180deg, rgba(255,255,255,0.02), transparent); + border-radius:12px; + box-shadow: 0 8px 30px rgba(2,6,23,0.6); + border:1px solid rgba(255,255,255,0.03); +} + +header{margin-bottom:12px} +h1{margin:0;font-size:28px} +.subtitle{margin:6px 0 0;color:var(--muted)} + +main{ + display:flex; + gap:18px; + align-items:flex-start; +} + +.game-board{ + flex:1; + background:var(--card); + padding:18px; + border-radius:10px; + min-height:300px; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.02); +} + +.word{ + font-size:28px; + letter-spacing:6px; + margin:8px 0 18px; + word-break:break-word; +} + +.info-row{ + display:flex; + gap:12px; + align-items:center; + margin-bottom:12px; +} + +.lives{font-weight:600} +.status{font-weight:700} + +.letters{ + display:flex; + flex-wrap:wrap; + gap:6px; + margin-bottom:12px; +} + +.letters button{ + min-width:36px; + height:36px; + border-radius:8px; + border:0; + background:var(--glass); + color:var(--muted); + cursor:pointer; + font-weight:600; + transition:transform .08s ease, background .12s ease; +} +.letters button:hover{transform:translateY(-2px)} +.letters button.used{opacity:0.45;cursor:default;transform:none} +.letters button.correct{background:rgba(52,211,153,0.12); color:var(--success)} +.letters button.wrong{background:rgba(251,113,133,0.08); color:var(--danger)} + +.guessed{margin-bottom:12px;color:var(--muted)} + +.controls{display:flex;gap:8px} +.controls button{ + padding:8px 12px; + border-radius:8px; + border:0; + background:var(--accent); + color:#042027; + cursor:pointer; + font-weight:700; +} + +.help{ + width:270px; + background:rgba(255,255,255,0.02); + padding:12px; + border-radius:8px; + color:var(--muted); +} + +.back{ + color:var(--muted); + text-decoration:none; + font-size:14px; +} +.back:hover{color:var(--accent)} diff --git a/games/harmonic-havoc/index.html b/games/harmonic-havoc/index.html new file mode 100644 index 00000000..321b767e --- /dev/null +++ b/games/harmonic-havoc/index.html @@ -0,0 +1,28 @@ + + + + + + Harmonic Havoc Game + + + +
    +

    Harmonic Havoc

    +
    +
    +
    C
    +
    D
    +
    E
    +
    F
    +
    G
    +
    A
    +
    B
    +
    + +
    Harmony Score: 0
    +
    Drag notes to the staff to arrange a melody. Click Play to score harmony points!
    +
    + + + \ No newline at end of file diff --git a/games/harmonic-havoc/script.js b/games/harmonic-havoc/script.js new file mode 100644 index 00000000..789bf7f5 --- /dev/null +++ b/games/harmonic-havoc/script.js @@ -0,0 +1,130 @@ +// Harmonic Havoc Game Script +// Create musical chaos by arranging notes and rhythms in this rhythm-based puzzle. + +const staff = document.getElementById('staff'); +const playButton = document.getElementById('play-button'); +const scoreElement = document.getElementById('score'); + +// Game variables +let draggedNote = null; +let staffNotes = []; +let score = 0; + +// Note frequencies +const noteFreqs = { + C: 261.63, + D: 293.66, + E: 329.63, + F: 349.23, + G: 392.00, + A: 440.00, + B: 493.88 +}; + +// Initialize staff lines +function initStaff() { + for (let i = 0; i < 5; i++) { + const line = document.createElement('div'); + line.className = 'line'; + staff.appendChild(line); + } +} + +// Drag and drop +document.addEventListener('dragstart', e => { + if (e.target.classList.contains('note')) { + draggedNote = e.target; + e.target.classList.add('dragging'); + } +}); + +document.addEventListener('dragend', e => { + if (draggedNote) { + draggedNote.classList.remove('dragging'); + draggedNote = null; + } +}); + +staff.addEventListener('dragover', e => { + e.preventDefault(); +}); + +staff.addEventListener('drop', e => { + e.preventDefault(); + if (draggedNote) { + const rect = staff.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + // Create note on staff + const note = document.createElement('div'); + note.className = 'note'; + note.textContent = draggedNote.dataset.note; + note.dataset.note = draggedNote.dataset.note; + note.style.position = 'absolute'; + note.style.left = x - 20 + 'px'; + note.style.top = y - 20 + 'px'; + note.draggable = true; + staff.appendChild(note); + + // Add to staffNotes + staffNotes.push({ note: draggedNote.dataset.note, x: x, y: y }); + } +}); + +// Play melody +playButton.addEventListener('click', () => { + if (staffNotes.length === 0) return; + + // Sort by x position + staffNotes.sort((a, b) => a.x - b.x); + + // Play notes + let delay = 0; + staffNotes.forEach(noteObj => { + setTimeout(() => playNote(noteObj.note), delay); + delay += 500; + }); + + // Calculate score + setTimeout(() => { + score = calculateHarmony(staffNotes); + scoreElement.textContent = 'Harmony Score: ' + score; + }, delay + 500); +}); + +// Play note +function playNote(note) { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.frequency.setValueAtTime(noteFreqs[note], audioContext.currentTime); + oscillator.type = 'sine'; + + gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5); + + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + 0.5); +} + +// Calculate harmony score +function calculateHarmony(notes) { + if (notes.length < 2) return 0; + + let harmony = 0; + for (let i = 0; i < notes.length - 1; i++) { + const interval = Math.abs(noteFreqs[notes[i].note] - noteFreqs[notes[i+1].note]); + if (interval < 50) harmony += 10; // Close notes + else if (interval < 100) harmony += 5; // Medium + else harmony -= 5; // Dissonant + } + return Math.max(0, harmony); +} + +// Initialize +initStaff(); \ No newline at end of file diff --git a/games/harmonic-havoc/style.css b/games/harmonic-havoc/style.css new file mode 100644 index 00000000..3433d355 --- /dev/null +++ b/games/harmonic-havoc/style.css @@ -0,0 +1,90 @@ +body { + font-family: Arial, sans-serif; + background-color: #000; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +#game-container { + text-align: center; +} + +h1 { + font-size: 2.5em; + margin-bottom: 20px; + text-shadow: 0 0 10px #ff00ff; +} + +#staff { + width: 600px; + height: 200px; + background-color: #222; + border: 2px solid #ff00ff; + margin: 20px auto; + position: relative; + display: flex; + align-items: center; +} + +.line { + position: absolute; + width: 100%; + height: 2px; + background-color: #fff; +} + +.line:nth-child(1) { top: 40px; } +.line:nth-child(2) { top: 60px; } +.line:nth-child(3) { top: 80px; } +.line:nth-child(4) { top: 100px; } +.line:nth-child(5) { top: 120px; } + +#notes-palette { + display: flex; + justify-content: center; + margin: 20px 0; +} + +.note { + width: 40px; + height: 40px; + background-color: #ffff00; + color: #000; + display: flex; + align-items: center; + justify-content: center; + margin: 0 5px; + cursor: grab; + border-radius: 50%; + font-weight: bold; +} + +.note.dragging { + opacity: 0.5; +} + +#play-button { + padding: 10px 20px; + font-size: 1.2em; + background-color: #00ff00; + color: #000; + border: none; + cursor: pointer; + margin: 10px; +} + +#score { + font-size: 1.2em; + margin: 10px 0; + color: #00ffff; +} + +#instructions { + font-size: 1em; + margin-top: 10px; + color: #cccccc; +} \ No newline at end of file diff --git a/games/helix-fall/index.html b/games/helix-fall/index.html new file mode 100644 index 00000000..e49149d9 --- /dev/null +++ b/games/helix-fall/index.html @@ -0,0 +1,60 @@ + + + + + + Helix Fall โ€” Mini JS Games Hub + + + + + +
    +
    +
    + โ† Back to Hub +

    Helix Fall

    +
    +
    +
    +
    Score 0
    +
    Best 0
    +
    +
    + + + +
    +
    +
    + +
    +
    + +
    + +
    Use โ† โ†’ or drag to rotate the tower
    +
    +
    + +
    +
    +
    Safe
    +
    Danger
    +
    +
    + + +
    +
    Visuals: gradient + glow โ€ข Sounds: web audio โ€ข Built with โค๏ธ
    +
    +
    + +
    + Tip: fall through the gaps to score. Red zones end the run. +
    +
    + + + + diff --git a/games/helix-fall/script.js b/games/helix-fall/script.js new file mode 100644 index 00000000..a61d2326 --- /dev/null +++ b/games/helix-fall/script.js @@ -0,0 +1,493 @@ +/* Helix Fall โ€” main game logic + - Rendered on canvas + - Tower rotates, ball falls/bounces + - Platforms generated with safe and danger segments + - Controls: arrow keys or drag rotate + - Pause / Resume / Restart + - Score + highscore in localStorage + - Simple web-audio effects (no downloads) +*/ + +(() => { + // --- Config --- + const canvas = document.getElementById('gameCanvas'); + const ctx = canvas.getContext('2d', { alpha: true }); + const W = canvas.width = 420; + const H = canvas.height = 720; + + // UI elements + const scoreEl = document.getElementById('score'); + const bestEl = document.getElementById('best'); + const pauseResumeBtn = document.getElementById('pauseResumeBtn'); + const restartBtn = document.getElementById('restartBtn'); + const playBtn = document.getElementById('playBtn'); + const resumeBtn = document.getElementById('resumeBtn'); + const centerMessage = document.getElementById('centerMessage'); + const muteBtn = document.getElementById('muteBtn'); + + // Game state + let running = false; + let paused = false; + let muted = false; + let lastTime = 0; + let rotation = 0; // tower rotation in radians + let rotationTarget = 0; + let rotationVelocity = 0; + let score = 0; + let best = parseInt(localStorage.getItem('helix_best') || '0', 10); + bestEl.textContent = best; + + // Tower / level configuration + const RADIUS = Math.min(W, H) * 0.42; + const center = { x: W / 2, y: H / 2 - 40 }; + const ringHeight = 36; + const gapBetweenRings = 8; + const ringsVisible = Math.ceil(H / (ringHeight + gapBetweenRings)) + 6; + + // Ball + const ball = { + x: center.x, + y: 170, + vy: 0, + radius: 10, + grounded: false, + color: '#ffffff', + }; + + // Platforms: array of rings (from top to bottom), each ring has segments; each segment has angleStart, angleEnd, safe(bool) + let rings = []; + let scrollOffset = 0; // how far tower has scrolled down (increase as ball descends) + let difficultyTimer = 0; + + // input + let pointerDown = false; + let lastPointerX = 0; + let keyLeft = false, keyRight = false; + + // audio: simple WebAudio Generator + const audioCtx = (window.AudioContext || window.webkitAudioContext)(); + function playTone(freq = 200, duration = 0.08, type = 'sine', gain = 0.08) { + if (muted || !audioCtx) return; + const o = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + o.type = type; + o.frequency.value = freq; + g.gain.value = gain; + o.connect(g); g.connect(audioCtx.destination); + o.start(); + g.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + duration); + setTimeout(() => { try{ o.stop(); } catch(e){} }, duration*1000 + 20); + } + function playBounce(){ playTone(450,0.05,'triangle',0.06); } + function playDanger(){ playTone(120,0.25,'sawtooth',0.14); } + function playScore(){ playTone(800,0.07,'square',0.06); } + function playStart(){ playTone(560,0.08,'sine',0.08); playTone(980,0.06,'sine',0.05); } + + // utilities + const TAU = Math.PI * 2; + function clamp(v,a,b){return Math.max(a,Math.min(b,v));} + function rand(min,max){return Math.random()*(max-min)+min} + function degToRad(d){return d*Math.PI/180} + + // initialize rings + function buildInitialRings() { + rings = []; + for (let i = 0; i < 200; i++) { + rings.push(generateRing(i)); + } + scrollOffset = 0; + } + + function generateRing(index) { + // index 0 is top, increasing index goes downward + const numSegments = 8; + const segments = []; + // difficulty increases with index + const dangerProbability = clamp(0.08 + index * 0.002, 0.08, 0.45); + // random start angle offset + const base = rand(0, TAU); + // create contiguous safe segment (gap) and rest maybe danger or safe + // Approach: pick one gap (safe) length (in segments), rest either safe or danger based on prob + const gapLength = Math.max(1, Math.round(rand(1, 2.4))); // 1-2 segments gap + const gapIndex = Math.floor(rand(0, numSegments)); + for (let s = 0; s < numSegments; s++) { + const startAngle = base + (s / numSegments) * TAU; + const endAngle = base + ((s + 1) / numSegments) * TAU; + const isGap = (s >= gapIndex && s < gapIndex + gapLength) || (gapIndex + gapLength > numSegments && s < (gapIndex + gapLength - numSegments)); + // If gap -> safe, else random safe/danger based on dangerProbability + const safe = isGap ? true : (Math.random() > dangerProbability); + segments.push({ start: startAngle, end: endAngle, safe }); + } + return { segments, index }; + } + + // draw + function drawBackground() { + // radial halo behind tower + const g = ctx.createRadialGradient(center.x, center.y, 20, center.x, center.y, RADIUS * 1.6); + g.addColorStop(0, 'rgba(126,249,255,0.03)'); + g.addColorStop(0.2, 'rgba(126,249,255,0.01)'); + g.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, W, H); + } + + function drawTower() { + // draw rings stacked along y with rotation applied + for (let r = 0; r < ringsVisible; r++) { + const ringIndex = Math.floor((scrollOffset / (ringHeight + gapBetweenRings))) + r; + const localY = center.y + (r - 3) * (ringHeight + gapBetweenRings) - (scrollOffset % (ringHeight + gapBetweenRings)); + const ring = rings[ringIndex]; + if (!ring) continue; + // glow ring background + const ringRadius = RADIUS - r * 6; + ctx.save(); + ctx.translate(center.x, localY); + ctx.rotate(rotation); + // base circle (shadow) + ctx.beginPath(); + ctx.arc(0, 0, ringRadius + 8, 0, TAU); + ctx.fillStyle = 'rgba(10,10,20,0.6)'; + ctx.fill(); + // segments + ring.segments.forEach(seg => { + const a0 = seg.start; + const a1 = seg.end; + const isSafe = seg.safe; + ctx.beginPath(); + // draw thick arc + ctx.lineWidth = ringHeight - 6; + ctx.lineCap = 'butt'; + ctx.strokeStyle = isSafe + ? 'rgba(100,255,200,0.14)' + : 'rgba(255,80,80,0.13)'; + ctx.strokeStyle = isSafe + ? '#28f0c7' + : '#ff4a4a'; + // use subtle gradient glow for safe/danger + const gradient = ctx.createLinearGradient(Math.cos(a0)*-1, Math.sin(a0)*-1, Math.cos(a1), Math.sin(a1)); + if (isSafe) { + gradient.addColorStop(0, '#00f5a0'); + gradient.addColorStop(1, '#00c2a8'); + } else { + gradient.addColorStop(0, '#ff8b8b'); + gradient.addColorStop(1, '#ff2a2a'); + } + ctx.strokeStyle = gradient; + ctx.globalAlpha = 0.85; + ctx.beginPath(); + ctx.arc(0, 0, ringRadius, a0, a1); + ctx.stroke(); + // small glow + if (!isSafe) { + ctx.beginPath(); + ctx.globalAlpha = 0.06; + ctx.lineWidth = ringHeight + 10; + ctx.strokeStyle = '#ff3a3a'; + ctx.arc(0, 0, ringRadius, a0, a1); + ctx.stroke(); + } else { + ctx.beginPath(); + ctx.globalAlpha = 0.06; + ctx.lineWidth = ringHeight + 12; + ctx.strokeStyle = '#00ffe0'; + ctx.arc(0, 0, ringRadius, a0, a1); + ctx.stroke(); + } + }); + ctx.restore(); + } + } + + function drawBall() { + ctx.save(); + ctx.beginPath(); + ctx.fillStyle = 'white'; + const dx = ball.x; + const dy = ball.y; + ctx.shadowColor = 'rgba(0,200,255,0.28)'; + ctx.shadowBlur = 18; + ctx.fillStyle = 'radial-gradient(white, #e6f9ff)'; // not supported by fillStyle, but keep fallback + // draw circle with glow + ctx.fillStyle = '#ffffff'; + ctx.beginPath(); + ctx.arc(dx, dy, ball.radius, 0, TAU); + ctx.fill(); + // glow ring + ctx.beginPath(); + ctx.globalAlpha = 0.08; + ctx.fillStyle = '#7ef9ff'; + ctx.arc(dx, dy, ball.radius + 14, 0, TAU); + ctx.fill(); + ctx.restore(); + } + + // collision detection - check the ring segment that aligns with ball + function checkCollisions() { + // compute ring index ball is overlapping with + // based on vertical offset from center + // ball Y relative to center + const relY = ball.y - center.y + scrollOffset; + const ringSlot = Math.floor((relY + (ringHeight + gapBetweenRings) * 3) / (ringHeight + gapBetweenRings)); + const ringIndex = Math.floor((scrollOffset / (ringHeight + gapBetweenRings))) + ringSlot; + const ring = rings[ringIndex]; + if (!ring) return; + // compute angle of ball relative to center and current rotation + const angle = Math.atan2(ball.y - center.y, ball.x - center.x) - rotation; + const normalizedAngle = (angle % TAU + TAU) % TAU; + + // find segment containing this angle + for (const seg of ring.segments) { + // normalize seg angles + const s = (seg.start % TAU + TAU) % TAU; + const e = (seg.end % TAU + TAU) % TAU; + let inside = false; + if (s < e) inside = normalizedAngle >= s && normalizedAngle <= e; + else inside = normalizedAngle >= s || normalizedAngle <= e; + if (inside) { + if (!seg.safe) { + // Danger -> Game over + onHitDanger(ringIndex); + } else { + // safe -> if moving downward fast enough and crossing gap, award score + // We check if ball is between top edge and center of ring slot (approx) + const slotTop = center.y + (ringSlot - 3) * (ringHeight + gapBetweenRings) - (scrollOffset % (ringHeight + gapBetweenRings)); + const slotBottom = slotTop + ringHeight; + // ball passes through gap when its y crosses center of ring slot while moving down + if (ball.vy > 6 && ball.y > slotTop + 2 && ball.y < slotBottom + 12) { + // award + score += 1; + scoreEl.textContent = score; + playScore(); + // small bounce + ball.vy = -8; + playBounce(); + } + } + break; + } + } + } + + function onHitDanger(ringIndex) { + // show message and stop game + if (!running) return; + running = false; + centerMessage.hidden = false; + centerMessage.textContent = '๐Ÿ’ฅ You hit a danger! Game Over'; + pauseResumeBtn.textContent = 'Paused'; + saveHighscore(); + playDanger(); + // small shake: trigger a quick animation by offsetting canvas (simple) + // nothing more - game loop will stop + } + + function saveHighscore() { + if (score > best) { + best = score; + localStorage.setItem('helix_best', best); + bestEl.textContent = best; + } + } + + // physics and update + function update(dt) { + // if paused or not running, skip updates + if (!running || paused) return; + + // input rotation interpolation + const inputRot = (keyLeft ? -1 : 0) + (keyRight ? 1 : 0); + // rotate target based on keys or pointer + rotationTarget += inputRot * 0.03 * dt; + + // interpolate rotation towards target + rotation += (rotationTarget - rotation) * 0.12; + + // gravity + ball.vy += 0.45 * dt * 0.06 * 60; // normalize across dt + ball.y += ball.vy * (dt / 16.666); + + // simulate downward scroll as ball descends below center + if (ball.y > center.y + 40) { + const delta = ball.y - (center.y + 40); + scrollOffset += delta; + ball.y -= delta; + // generate new rings as scroll increases + while (rings.length < Math.floor(scrollOffset / (ringHeight + gapBetweenRings)) + ringsVisible + 20) { + rings.push(generateRing(rings.length)); + } + } + + // keep ball within horizontal radius (ball moves with rotated tower) + const ballToCenter = Math.hypot(ball.x - center.x, ball.y - center.y); + if (ballToCenter > RADIUS * 0.9 + 20) { + // make it slide around edge slightly + const ang = Math.atan2(ball.y - center.y, ball.x - center.x) - rotation; + ball.x = center.x + Math.cos(ang + rotation) * (RADIUS * 0.9); + } + + // check for collision with rings - only when ball is near ring radius horizontally + checkCollisions(); + + // slowly increase difficulty by causing a tiny auto-rotation + difficultyTimer += dt; + if (difficultyTimer > 8000) { + rotationTarget += 0.002 * Math.sign(Math.sin(Date.now() / 1000)); + difficultyTimer = 0; + } + } + + // draw everything + function render() { + ctx.clearRect(0, 0, W, H); + drawBackground(); + drawTower(); + drawBall(); + } + + // main loop + function loop(t) { + const dt = lastTime ? Math.min(40, t - lastTime) : 16; + lastTime = t; + if (running && !paused) { + update(dt); + render(); + } else { + render(); // still render to show state + } + requestAnimationFrame(loop); + } + + // Input handlers + function setupInput() { + // keyboard + window.addEventListener('keydown', (e) => { + if (e.key === 'ArrowLeft') { keyLeft = true; } + if (e.key === 'ArrowRight') { keyRight = true; } + if (e.key === ' ' || e.key === 'Spacebar') { // space to jump small + if (!running) startGame(); + else { ball.vy = -12; playBounce(); } + } + }); + window.addEventListener('keyup', (e) => { + if (e.key === 'ArrowLeft') { keyLeft = false; } + if (e.key === 'ArrowRight') { keyRight = false; } + }); + + // pointer rotate (drag) + canvas.addEventListener('pointerdown', (e) => { + pointerDown = true; + lastPointerX = e.clientX; + }); + window.addEventListener('pointermove', (e) => { + if (!pointerDown) return; + const dx = e.clientX - lastPointerX; + rotationTarget += dx * 0.006; + lastPointerX = e.clientX; + }); + window.addEventListener('pointerup', () => { pointerDown = false; }); + + // touch: also allow tap to force small bounce + canvas.addEventListener('click', () => { + if (!running) startGame(); + else { + ball.vy = -10; + playBounce(); + } + }); + + // buttons + pauseResumeBtn.addEventListener('click', () => { + if (!running) return; + paused = !paused; + pauseResumeBtn.textContent = paused ? 'Resume' : 'Pause'; + }); + restartBtn.addEventListener('click', () => { + restartGame(); + }); + playBtn.addEventListener('click', () => { + startGame(); + }); + resumeBtn.addEventListener('click', () => { + paused = false; + resumeBtn.hidden = true; + pauseResumeBtn.textContent = 'Pause'; + }); + muteBtn.addEventListener('click', () => { + muted = !muted; + muteBtn.textContent = muted ? '๐Ÿ”‡' : '๐Ÿ”Š'; + }); + } + + // game lifecycle + function resetState() { + rotation = 0; rotationTarget = 0; rotationVelocity = 0; + ball.x = center.x; ball.y = 170; ball.vy = 0; + score = 0; scoreEl.textContent = '0'; + centerMessage.hidden = true; + running = false; paused = false; + difficultyTimer = 0; + } + + function startGame() { + if (!audioCtx) try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e){} + if (!running) { + running = true; + paused = false; + playStart(); + centerMessage.hidden = true; + pauseResumeBtn.textContent = 'Pause'; + playBtn.hidden = true; + resumeBtn.hidden = true; + } + } + + function restartGame() { + resetState(); + buildInitialRings(); + running = true; + playStart(); + pauseResumeBtn.textContent = 'Pause'; + playBtn.hidden = true; + resumeBtn.hidden = true; + } + + // On danger hit -> stop & allow restart + function endGameOver() { + running = false; + centerMessage.hidden = false; + centerMessage.textContent = 'Game Over โ€” Click Restart'; + playTone(120,0.2,'sawtooth'); + } + + // small wrapper for dangerous hit to stop after short delay + function onHitDanger(rIndex) { + // prevent repeated triggers + if (!running) return; + running = false; + centerMessage.hidden = false; + centerMessage.textContent = '๐Ÿ’ฅ Game Over'; + pauseResumeBtn.textContent = 'Paused'; + saveHighscore(); + playDanger(); + // show restart button + playBtn.hidden = false; + resumeBtn.hidden = true; + } + + // start initial + resetState(); + buildInitialRings(); + setupInput(); + requestAnimationFrame(loop); + + // expose for debugging + window.helix = { + restartGame, startGame, getState: () => ({running, paused, score}) + }; + + // small helpers: initial score display + scoreEl.textContent = '0'; + bestEl.textContent = best; + +})(); diff --git a/games/helix-fall/style.css b/games/helix-fall/style.css new file mode 100644 index 00000000..957671e1 --- /dev/null +++ b/games/helix-fall/style.css @@ -0,0 +1,74 @@ +:root{ + --bg1:#0b1020; + --bg2:#071028; + --accent: #ff6b6b; + --accent2:#7ef9ff; + --glass: rgba(255,255,255,0.03); + --card: rgba(255,255,255,0.03); +} + +*{box-sizing:border-box} +html,body{height:100%;margin:0;font-family:Inter,Segoe UI,Roboto,system-ui,-apple-system,"Helvetica Neue",Arial} +body{ + background: radial-gradient(1200px 600px at 10% 10%, rgba(126,249,255,0.04), transparent 10%), + radial-gradient(800px 400px at 90% 90%, rgba(255,107,107,0.03), transparent 10%), + linear-gradient(180deg,var(--bg1),var(--bg2)); + color:#eaf6ff; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + padding:18px; +} + +.page{max-width:980px;margin:0 auto} + +.topbar{ + display:flex;align-items:center;justify-content:space-between;margin-bottom:12px; +} +.topbar .left {display:flex;align-items:center;gap:12px} +.back{color:#bfefff;text-decoration:none;font-weight:600} +h1{margin:0;font-size:20px} +.scoreboard{display:flex;gap:12px;align-items:center;font-size:14px} +.scoreboard span{font-weight:700;color:var(--accent2)} +.controls{display:flex;gap:8px} +.btn{background:linear-gradient(90deg,rgba(255,255,255,0.04),rgba(255,255,255,0.02));backdrop-filter:blur(6px);border:1px solid rgba(255,255,255,0.03);color:#eaf6ff;padding:8px 10px;border-radius:10px;cursor:pointer} +.btn:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(0,0,0,0.4)} +.btn:active{transform:translateY(0)} + +.game-area{display:flex;gap:20px;align-items:flex-start} +.canvas-wrap{ + background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border-radius:18px; padding:18px; box-shadow: 0 10px 40px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.02); + width:420px; +} +canvas{display:block;border-radius:12px;width:100%;height:auto;background: + radial-gradient(circle at 20% 10%, rgba(126,249,255,0.03), transparent 2%), + linear-gradient(180deg, rgba(255,255,255,0.01), rgba(0,0,0,0.08)); box-shadow: 0 8px 40px rgba(0,0,0,0.6);} + +.hud{position:relative;margin-top:-88%;pointer-events:none} +.big-center{ + width:100%;text-align:center;font-size:28px;padding:14px 10px;background:linear-gradient(90deg,rgba(255,255,255,0.02),rgba(255,255,255,0.01));border-radius:12px;color:#fff;backdrop-filter: blur(2px);box-shadow:0 6px 30px rgba(0,0,0,0.6) +} + +.small-tip{color:rgba(255,255,255,0.6);text-align:center;margin-top:12px;font-size:13px} + +.ui-panel{flex:1;display:flex;flex-direction:column;gap:12px;padding:8px 6px} +.legend{display:flex;gap:12px;align-items:center;font-size:14px} +.legend-item{display:flex;align-items:center;gap:8px} +.dot{display:inline-block;width:18px;height:18px;border-radius:8px;box-shadow:0 6px 18px rgba(0,0,0,0.6)} +.dot.safe{background:linear-gradient(90deg,#00ffa2,#00c2a8);box-shadow:0 8px 30px rgba(0,202,160,0.25)} +.dot.danger{background:linear-gradient(90deg,#ff5c5c,#ff2a2a);box-shadow:0 8px 30px rgba(255,60,60,0.28)} + +.buttons-row{display:flex;gap:8px;align-items:center} +.cta{padding:10px 16px;border-radius:12px;background:linear-gradient(90deg,#7ef9ff22,#ff6b6b22);border:1px solid rgba(255,255,255,0.06);color:#eaf6ff;font-weight:700;cursor:pointer} +.cta.subtle{opacity:0.85} +.cta:hover{box-shadow:0 10px 30px rgba(0,0,0,0.6);transform:translateY(-2px)} + +.credits{color:rgba(255,255,255,0.5);font-size:13px;margin-top:auto} +.footer{text-align:center;margin-top:18px;color:rgba(255,255,255,0.45)} + +@media (max-width:880px){ + .page{padding:12px} + .game-area{flex-direction:column;align-items:center} + .canvas-wrap{width:92%} + .ui-panel{width:92%} +} diff --git a/games/history-timeline/index.html b/games/history-timeline/index.html new file mode 100644 index 00000000..afee15c6 --- /dev/null +++ b/games/history-timeline/index.html @@ -0,0 +1,100 @@ + + + + + + History Timeline - Mini JS Games Hub + + + +
    +
    +

    History Timeline

    +

    Arrange historical events in the correct chronological order!

    +
    + +
    +
    +
    + Score: + 0 +
    +
    + Events: + 0 + / + 8 +
    +
    + Streak: + 0 +
    +
    + Time: + 120 +
    +
    + +
    +

    Timeline

    +
    + +
    +
    + +
    +

    Events to Arrange

    +
    + +
    +
    + +
    +
    + + + +
    +
    + +
    + +
    + + +
    +
    + + +
    + + + + \ No newline at end of file diff --git a/games/history-timeline/script.js b/games/history-timeline/script.js new file mode 100644 index 00000000..4cb403dd --- /dev/null +++ b/games/history-timeline/script.js @@ -0,0 +1,525 @@ +// History Timeline Game +// Arrange historical events in the correct chronological order + +// DOM elements +const scoreEl = document.getElementById('current-score'); +const eventCountEl = document.getElementById('current-event'); +const totalEventsEl = document.getElementById('total-events'); +const streakEl = document.getElementById('current-streak'); +const timeLeftEl = document.getElementById('time-left'); +const timelineEl = document.getElementById('timeline'); +const eventsContainerEl = document.getElementById('events-container'); +const hintBtn = document.getElementById('hint-btn'); +const checkBtn = document.getElementById('check-btn'); +const clearBtn = document.getElementById('clear-btn'); +const startBtn = document.getElementById('start-btn'); +const quitBtn = document.getElementById('quit-btn'); +const messageEl = document.getElementById('message'); +const resultsEl = document.getElementById('results'); +const finalScoreEl = document.getElementById('final-score'); +const eventsCorrectEl = document.getElementById('events-correct'); +const eventsTotalEl = document.getElementById('events-total'); +const accuracyEl = document.getElementById('accuracy'); +const timeBonusEl = document.getElementById('time-bonus'); +const gradeEl = document.getElementById('grade'); +const playAgainBtn = document.getElementById('play-again-btn'); + +// Game variables +let currentEventIndex = 0; +let score = 0; +let streak = 0; +let timeLeft = 120; +let timerInterval = null; +let gameActive = false; +let events = []; +let timelineSlots = []; +let draggedEvent = null; +let hintUsed = false; +let eventsCorrect = 0; + +// Historical events database +const historicalEvents = [ + { + year: 1066, + title: "Battle of Hastings", + description: "William the Conqueror defeats Harold Godwinson, leading to the Norman Conquest of England." + }, + { + year: 1215, + title: "Magna Carta Signed", + description: "King John of England signs the Magna Carta, limiting royal power and establishing principles of liberty." + }, + { + year: 1492, + title: "Columbus Reaches America", + description: "Christopher Columbus arrives in the Americas, opening the New World to European exploration." + }, + { + year: 1776, + title: "American Declaration of Independence", + description: "The United States declares independence from Great Britain." + }, + { + year: 1789, + title: "French Revolution Begins", + description: "Storming of the Bastille marks the start of the French Revolution." + }, + { + year: 1914, + title: "World War I Begins", + description: "Assassination of Archduke Franz Ferdinand triggers the start of World War I." + }, + { + year: 1945, + title: "World War II Ends", + description: "Japan surrenders, ending World War II and leading to the atomic age." + }, + { + year: 1969, + title: "Moon Landing", + description: "Apollo 11 astronauts Neil Armstrong and Buzz Aldrin become the first humans to walk on the Moon." + }, + { + year: 1989, + title: "Berlin Wall Falls", + description: "The Berlin Wall is dismantled, symbolizing the end of the Cold War." + }, + { + year: 2001, + title: "September 11 Attacks", + description: "Terrorist attacks on the World Trade Center and Pentagon change global security policies." + }, + { + year: 2011, + title: "Arab Spring Begins", + description: "Popular uprisings across the Middle East and North Africa challenge authoritarian regimes." + }, + { + year: 2020, + title: "COVID-19 Pandemic", + description: "Global pandemic caused by SARS-CoV-2 affects billions and changes daily life worldwide." + } +]; + +// Initialize game +function initGame() { + shuffleEvents(); + setupEventListeners(); + createTimeline(); + createEventCards(); + updateDisplay(); +} + +// Shuffle events for random order +function shuffleEvents() { + events = [...historicalEvents]; + for (let i = events.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [events[i], events[j]] = [events[j], events[i]]; + } + // Take first 8 events + events = events.slice(0, 8); + totalEventsEl.textContent = events.length; +} + +// Create timeline slots +function createTimeline() { + timelineEl.innerHTML = ''; + timelineSlots = []; + + for (let i = 0; i < events.length; i++) { + const slot = document.createElement('div'); + slot.className = 'timeline-slot'; + slot.dataset.index = i; + + const slotNumber = document.createElement('div'); + slotNumber.className = 'slot-number'; + slotNumber.textContent = i + 1; + + slot.appendChild(slotNumber); + slot.addEventListener('click', () => selectSlot(i)); + slot.addEventListener('dragover', handleDragOver); + slot.addEventListener('drop', (e) => handleDrop(e, i)); + + timelineEl.appendChild(slot); + timelineSlots.push(slot); + } +} + +// Create event cards +function createEventCards() { + eventsContainerEl.innerHTML = ''; + + events.forEach((event, index) => { + const card = document.createElement('div'); + card.className = 'event-card'; + card.dataset.index = index; + card.draggable = true; + + card.innerHTML = ` +
    ${event.year}
    +
    ${event.title}
    +
    ${event.description}
    + `; + + card.addEventListener('dragstart', handleDragStart); + card.addEventListener('click', () => selectEvent(index)); + + eventsContainerEl.appendChild(card); + }); +} + +// Setup event listeners +function setupEventListeners() { + startBtn.addEventListener('click', startGame); + quitBtn.addEventListener('click', endGame); + playAgainBtn.addEventListener('click', resetGame); + + hintBtn.addEventListener('click', useHint); + checkBtn.addEventListener('click', checkTimeline); + clearBtn.addEventListener('click', clearTimeline); + + // Keyboard shortcuts + document.addEventListener('keydown', (e) => { + if (!gameActive) return; + + if (e.key === 'Enter') { + checkTimeline(); + } else if (e.key === 'Escape') { + clearTimeline(); + } + }); +} + +// Start the game +function startGame() { + gameActive = true; + currentEventIndex = 0; + score = 0; + streak = 0; + timeLeft = 120; + eventsCorrect = 0; + hintUsed = false; + + startBtn.style.display = 'none'; + quitBtn.style.display = 'inline-block'; + + hintBtn.disabled = false; + checkBtn.disabled = false; + clearBtn.disabled = false; + + resultsEl.style.display = 'none'; + messageEl.textContent = ''; + + startTimer(); + updateDisplay(); +} + +// Start game timer +function startTimer() { + if (timerInterval) clearInterval(timerInterval); + + timerInterval = setInterval(() => { + timeLeft--; + timeLeftEl.textContent = timeLeft; + + if (timeLeft <= 0) { + clearInterval(timerInterval); + timeUp(); + } + }, 1000); +} + +// Handle time up +function timeUp() { + showMessage('Time\'s up! Checking your timeline...', 'incorrect'); + setTimeout(checkTimeline, 2000); +} + +// Drag and drop handlers +function handleDragStart(e) { + draggedEvent = e.target; + e.target.classList.add('dragging'); +} + +function handleDragOver(e) { + e.preventDefault(); +} + +function handleDrop(e, slotIndex) { + e.preventDefault(); + + if (!draggedEvent) return; + + const eventIndex = parseInt(draggedEvent.dataset.index); + placeEventInSlot(eventIndex, slotIndex); + + draggedEvent.classList.remove('dragging'); + draggedEvent = null; +} + +// Select event (click alternative to drag) +function selectEvent(eventIndex) { + if (!gameActive) return; + + // If an event is already selected, try to place it + if (draggedEvent) { + const slotIndex = timelineSlots.findIndex(slot => slot.classList.contains('selected')); + if (slotIndex !== -1) { + placeEventInSlot(parseInt(draggedEvent.dataset.index), slotIndex); + } + draggedEvent.classList.remove('selected'); + draggedEvent = null; + clearSlotSelection(); + } else { + // Select this event + draggedEvent = document.querySelector(`[data-index="${eventIndex}"]`); + draggedEvent.classList.add('selected'); + showMessage('Click on a timeline slot to place this event.', 'hint'); + } +} + +// Select timeline slot +function selectSlot(slotIndex) { + if (!gameActive) return; + + clearSlotSelection(); + timelineSlots[slotIndex].classList.add('selected'); + + if (draggedEvent) { + placeEventInSlot(parseInt(draggedEvent.dataset.index), slotIndex); + draggedEvent.classList.remove('selected'); + draggedEvent = null; + clearSlotSelection(); + } else { + showMessage('Now select an event to place here.', 'hint'); + } +} + +// Place event in timeline slot +function placeEventInSlot(eventIndex, slotIndex) { + const event = events[eventIndex]; + const slot = timelineSlots[slotIndex]; + + // Check if slot is already filled + if (slot.classList.contains('filled')) { + showMessage('This slot is already filled! Clear it first.', 'incorrect'); + return; + } + + // Create event content for slot + const eventContent = document.createElement('div'); + eventContent.className = 'slot-event'; + eventContent.innerHTML = ` +
    ${event.year}
    +
    ${event.title}
    + `; + + slot.appendChild(eventContent); + slot.classList.add('filled'); + slot.dataset.eventIndex = eventIndex; + + // Mark event card as placed + const eventCard = document.querySelector(`.event-card[data-index="${eventIndex}"]`); + eventCard.classList.add('placed'); + + showMessage(`Placed "${event.title}" in position ${slotIndex + 1}.`, 'correct'); + updateDisplay(); +} + +// Clear timeline +function clearTimeline() { + timelineSlots.forEach(slot => { + slot.classList.remove('filled', 'correct', 'incorrect', 'selected'); + slot.dataset.eventIndex = ''; + const eventContent = slot.querySelector('.slot-event'); + if (eventContent) { + eventContent.remove(); + } + }); + + document.querySelectorAll('.event-card').forEach(card => { + card.classList.remove('placed', 'selected'); + }); + + draggedEvent = null; + showMessage('Timeline cleared. Start arranging events again.', 'hint'); +} + +// Use hint +function useHint() { + if (!gameActive || hintUsed || score < 20) return; + + if (score < 20) { + showMessage('Not enough points for hint! (20 points required)', 'incorrect'); + return; + } + + score -= 20; + hintUsed = true; + + // Show the earliest event + const earliestEvent = events.reduce((earliest, event, index) => { + return event.year < earliest.event.year ? { event, index } : earliest; + }, { event: events[0], index: 0 }); + + // Highlight the earliest event + const eventCard = document.querySelector(`.event-card[data-index="${earliestEvent.index}"]`); + eventCard.style.border = '3px solid #f39c12'; + eventCard.style.boxShadow = '0 0 20px rgba(243, 156, 18, 0.5)'; + + showMessage(`Hint: "${earliestEvent.event.title}" is the earliest event. -20 points`, 'hint'); + + setTimeout(() => { + eventCard.style.border = ''; + eventCard.style.boxShadow = ''; + }, 3000); + + updateDisplay(); +} + +// Check timeline order +function checkTimeline() { + if (!gameActive) return; + + clearInterval(timerInterval); + + let allSlotsFilled = true; + let correctOrder = true; + eventsCorrect = 0; + + // Check if all slots are filled + timelineSlots.forEach(slot => { + if (!slot.classList.contains('filled')) { + allSlotsFilled = false; + } + }); + + if (!allSlotsFilled) { + showMessage('Please fill all timeline slots before checking!', 'incorrect'); + startTimer(); + return; + } + + // Sort events by year for correct order + const sortedEvents = [...events].sort((a, b) => a.year - b.year); + + // Check each slot + timelineSlots.forEach((slot, index) => { + const eventIndex = parseInt(slot.dataset.eventIndex); + const placedEvent = events[eventIndex]; + const correctEvent = sortedEvents[index]; + + if (placedEvent.year === correctEvent.year) { + slot.classList.add('correct'); + eventsCorrect++; + } else { + slot.classList.add('incorrect'); + correctOrder = false; + } + }); + + // Calculate score + if (correctOrder) { + let points = 50; // Base points for correct order + + // Time bonus + if (timeLeft >= 60) points += 30; + else if (timeLeft >= 30) points += 15; + + // Hint penalty + if (hintUsed) points = Math.floor(points * 0.8); + + score += points; + streak++; + showMessage(`Perfect! All events in correct order! +${points} points`, 'correct'); + } else { + streak = 0; + const accuracy = Math.round((eventsCorrect / events.length) * 100); + showMessage(`Some events are out of order. ${eventsCorrect}/${events.length} correct (${accuracy}%).`, 'incorrect'); + } + + setTimeout(endGame, 3000); +} + +// Show message +function showMessage(text, type) { + messageEl.textContent = text; + messageEl.className = `message ${type}`; +} + +// Clear slot selection +function clearSlotSelection() { + timelineSlots.forEach(slot => slot.classList.remove('selected')); +} + +// End game +function endGame() { + gameActive = false; + clearInterval(timerInterval); + + // Show results + showResults(); +} + +// Show final results +function showResults() { + const accuracy = events.length > 0 ? Math.round((eventsCorrect / events.length) * 100) : 0; + const timeBonus = Math.max(0, Math.floor(timeLeft / 10)); + + finalScoreEl.textContent = score.toLocaleString(); + eventsCorrectEl.textContent = eventsCorrect; + eventsTotalEl.textContent = events.length; + accuracyEl.textContent = accuracy + '%'; + timeBonusEl.textContent = timeBonus; + + // Calculate grade + let grade = 'F'; + if (accuracy >= 90) grade = 'A'; + else if (accuracy >= 80) grade = 'B'; + else if (accuracy >= 70) grade = 'C'; + else if (accuracy >= 60) grade = 'D'; + + gradeEl.textContent = grade; + gradeEl.className = `final-value grade ${grade}`; + + resultsEl.style.display = 'block'; + startBtn.style.display = 'none'; + quitBtn.style.display = 'none'; + + hintBtn.disabled = true; + checkBtn.disabled = true; + clearBtn.disabled = true; +} + +// Reset game +function resetGame() { + resultsEl.style.display = 'none'; + startBtn.style.display = 'inline-block'; + quitBtn.style.display = 'none'; + + hintBtn.disabled = true; + checkBtn.disabled = true; + clearBtn.disabled = true; + + shuffleEvents(); + createTimeline(); + createEventCards(); + updateDisplay(); + messageEl.textContent = 'Ready for another historical timeline?'; +} + +// Update display elements +function updateDisplay() { + scoreEl.textContent = score.toLocaleString(); + eventCountEl.textContent = currentEventIndex + 1; + streakEl.textContent = streak; + timeLeftEl.textContent = timeLeft; +} + +// Initialize the game +initGame(); + +// This history timeline game includes drag-and-drop functionality, +// chronological ordering, scoring, hints, and educational content +// Players learn about historical events while arranging them correctly \ No newline at end of file diff --git a/games/history-timeline/style.css b/games/history-timeline/style.css new file mode 100644 index 00000000..d241d427 --- /dev/null +++ b/games/history-timeline/style.css @@ -0,0 +1,465 @@ +/* History Timeline Game Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Georgia', 'Times New Roman', serif; + background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + color: #333; +} + +.container { + max-width: 1400px; + width: 100%; + background: white; + border-radius: 20px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +header { + background: linear-gradient(135deg, #8e44ad 0%, #9b59b6 100%); + color: white; + padding: 30px; + text-align: center; +} + +header h1 { + font-size: 2.5rem; + margin-bottom: 10px; + font-weight: 700; +} + +header p { + font-size: 1.1rem; + opacity: 0.9; +} + +.game-area { + padding: 30px; +} + +.stats-panel { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 20px; + margin-bottom: 30px; + padding: 20px; + background: #f8f9fa; + border-radius: 15px; +} + +.stat { + display: flex; + align-items: center; + gap: 8px; + font-size: 1.1rem; + font-weight: 600; +} + +.stat-label { + color: #666; +} + +.stat-value { + color: #333; + font-weight: 700; +} + +.stat-separator { + color: #999; + margin: 0 5px; +} + +.timeline-section, .events-section { + margin-bottom: 30px; +} + +.timeline-section h3, .events-section h3 { + font-size: 1.5rem; + margin-bottom: 20px; + color: #2c3e50; + text-align: center; + font-weight: 600; +} + +.timeline { + display: flex; + flex-direction: column; + gap: 15px; + min-height: 400px; + padding: 20px; + background: linear-gradient(135deg, #ecf0f1 0%, #bdc3c7 100%); + border-radius: 15px; + position: relative; +} + +.timeline::before { + content: ''; + position: absolute; + left: 30px; + top: 20px; + bottom: 20px; + width: 4px; + background: linear-gradient(to bottom, #3498db, #e74c3c); + border-radius: 2px; +} + +.timeline-slot { + display: flex; + align-items: center; + min-height: 80px; + padding: 10px 20px 10px 60px; + border: 2px dashed #bdc3c7; + border-radius: 10px; + background: rgba(255, 255, 255, 0.8); + transition: all 0.3s ease; + position: relative; + cursor: pointer; +} + +.timeline-slot:hover { + border-color: #3498db; + background: rgba(52, 152, 219, 0.1); +} + +.timeline-slot.filled { + border-style: solid; + border-color: #27ae60; + background: rgba(39, 174, 96, 0.1); +} + +.timeline-slot.incorrect { + border-color: #e74c3c; + background: rgba(231, 76, 60, 0.1); +} + +.timeline-slot::before { + content: ''; + position: absolute; + left: 22px; + top: 50%; + transform: translateY(-50%); + width: 16px; + height: 16px; + border-radius: 50%; + background: #3498db; + border: 3px solid white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.timeline-slot.filled::before { + background: #27ae60; +} + +.timeline-slot.incorrect::before { + background: #e74c3c; +} + +.slot-number { + position: absolute; + left: 18px; + top: 50%; + transform: translateY(-50%); + width: 20px; + height: 20px; + border-radius: 50%; + background: white; + border: 2px solid #3498db; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8rem; + font-weight: 700; + color: #3498db; +} + +.events-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 15px; + min-height: 200px; +} + +.event-card { + background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); + color: white; + padding: 20px; + border-radius: 10px; + cursor: grab; + transition: all 0.3s ease; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + user-select: none; +} + +.event-card:hover { + transform: translateY(-3px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); +} + +.event-card:active { + cursor: grabbing; + transform: scale(0.98); +} + +.event-card.dragging { + opacity: 0.5; + transform: rotate(5deg); +} + +.event-card.placed { + opacity: 0.3; + pointer-events: none; +} + +.event-year { + font-size: 1.2rem; + font-weight: 700; + margin-bottom: 8px; + color: #f39c12; +} + +.event-title { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 8px; +} + +.event-description { + font-size: 0.9rem; + opacity: 0.9; + line-height: 1.4; +} + +.controls-section { + margin-bottom: 20px; +} + +.control-buttons { + display: flex; + gap: 15px; + justify-content: center; + flex-wrap: wrap; +} + +.primary-btn, .secondary-btn { + padding: 12px 24px; + border: none; + border-radius: 25px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.primary-btn { + background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%); + color: white; +} + +.primary-btn:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(39, 174, 96, 0.4); +} + +.secondary-btn { + background: #f8f9fa; + color: #666; + border: 2px solid #e0e0e0; +} + +.secondary-btn:hover { + background: #e9ecef; + border-color: #dee2e6; +} + +.secondary-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.message { + text-align: center; + font-size: 1.2rem; + font-weight: 600; + min-height: 2rem; + margin: 20px 0; + padding: 15px; + border-radius: 10px; + transition: all 0.3s ease; +} + +.message.correct { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.message.incorrect { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.message.hint { + background: #fff3cd; + color: #856404; + border: 1px solid #ffeaa7; +} + +.game-controls { + text-align: center; + margin-top: 20px; +} + +.results { + padding: 30px; + text-align: center; + background: linear-gradient(135deg, #8e44ad 0%, #9b59b6 100%); + color: white; +} + +.results h2 { + font-size: 2rem; + margin-bottom: 30px; +} + +.final-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.final-stat { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + backdrop-filter: blur(10px); +} + +.final-label { + display: block; + font-size: 0.9rem; + opacity: 0.8; + margin-bottom: 5px; +} + +.final-value { + display: block; + font-size: 1.8rem; + font-weight: 700; +} + +.final-separator { + margin: 0 5px; +} + +.grade { + font-size: 2rem !important; +} + +.grade.A { color: #28a745; } +.grade.B { color: #17a2b8; } +.grade.C { color: #ffc107; } +.grade.D { color: #fd7e14; } +.grade.F { color: #dc3545; } + +/* Responsive Design */ +@media (max-width: 768px) { + header { + padding: 20px; + } + + header h1 { + font-size: 2rem; + } + + .game-area { + padding: 20px; + } + + .stats-panel { + flex-direction: column; + gap: 10px; + } + + .timeline { + padding: 15px; + } + + .timeline::before { + left: 20px; + } + + .timeline-slot { + padding-left: 50px; + min-height: 70px; + } + + .timeline-slot::before { + left: 12px; + } + + .slot-number { + left: 8px; + } + + .events-container { + grid-template-columns: 1fr; + } + + .control-buttons { + flex-direction: column; + } + + .primary-btn, .secondary-btn { + width: 100%; + } + + .final-stats { + grid-template-columns: 1fr; + } +} + +/* Animations */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.event-card, .timeline-slot { + animation: fadeIn 0.5s ease-out; +} + +.correct-animation { + animation: correctPulse 0.6s ease-out; +} + +.incorrect-animation { + animation: incorrectShake 0.6s ease-out; +} + +@keyframes correctPulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +@keyframes incorrectShake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } +} \ No newline at end of file diff --git a/games/horizon_runner/index.html b/games/horizon_runner/index.html new file mode 100644 index 00000000..8f18b6f8 --- /dev/null +++ b/games/horizon_runner/index.html @@ -0,0 +1,34 @@ + + + + + + Event Horizon Runner + + + + +
    +

    Event Horizon Runner

    +

    Execute the required browser micro-interactions on the targets to keep running!

    +
    + Speed: 1.0x | Lives: 3 +
    +
    + +
    +
    ๐Ÿค–
    + +
    +
    +
    +
    +
    +
    +
    + +

    Ready to run!

    + + + + \ No newline at end of file diff --git a/games/horizon_runner/script.js b/games/horizon_runner/script.js new file mode 100644 index 00000000..a2ef58ee --- /dev/null +++ b/games/horizon_runner/script.js @@ -0,0 +1,182 @@ +document.addEventListener('DOMContentLoaded', () => { + const track = document.getElementById('track'); + const player = document.getElementById('player'); + const messageDisplay = document.getElementById('game-message'); + const speedDisplay = document.getElementById('game-speed'); + const livesDisplay = document.getElementById('lives-count'); + const PLAYER_OFFSET = 100; // Must match CSS --player-offset + const PLAYER_WIDTH = 50; // Must match CSS player width + + // --- Game State --- + let gameState = { + isRunning: true, + lives: 3, + scrollRate: 1.0, // Multiplier for CSS scroll duration + lastObstacleTime: 0, + hoverTapActive: false // State needed for the two-part "Hover-Tap" event + }; + + // --- 1. Collision and Timing Check --- + + /** + * Finds the obstacle currently closest to the player and within the "active" zone. + * The active zone is defined by the player's position. + */ + function getActiveTarget() { + const obstacles = Array.from(document.querySelectorAll('.target:not(.passed)')); + + // Get current track position (requires reading CSS animation or transform) + const trackStyle = window.getComputedStyle(track); + const trackX = new WebKitCSSMatrix(trackStyle.transform).m41; // Get X translation + + let activeTarget = null; + + obstacles.forEach(obs => { + const obsLeft = parseInt(obs.style.left) + trackX; + + // Obstacle is considered "active" if its right edge is past the player's left edge + // and its left edge hasn't passed the player's right edge. + const playerZoneStart = PLAYER_OFFSET; + const playerZoneEnd = PLAYER_OFFSET + PLAYER_WIDTH; + + if (obsLeft < playerZoneEnd && obsLeft + obs.offsetWidth > playerZoneStart) { + activeTarget = obs; + } + }); + + // Highlight the currently active target for visual feedback + document.querySelectorAll('.target').forEach(t => t.classList.remove('active')); + if (activeTarget) { + activeTarget.classList.add('active'); + } + + return activeTarget; + } + + /** + * Runs every frame to check for passive failures (hitting an obstacle). + */ + function checkPassiveFail() { + if (!gameState.isRunning) return; + + const activeTarget = getActiveTarget(); + + // If an obstacle is in the player zone but hasn't been passed, it's a collision. + if (activeTarget && !activeTarget.classList.contains('passed')) { + // The obstacle passed through the player zone without the correct event + failGame("Hit an obstacle!"); + } + } + + // --- 2. Event Handling and Game Logic --- + + function handleSuccess(target) { + if (!gameState.isRunning) return; + + target.classList.add('passed'); + messageDisplay.textContent = `SUCCESS! Executed ${target.dataset.event.toUpperCase()} perfectly!`; + + // Increase speed for momentum + gameState.scrollRate += 0.05; + track.style.animationDuration = `${20 / gameState.scrollRate}s`; + speedDisplay.textContent = `${gameState.scrollRate.toFixed(2)}x`; + } + + function failGame(reason) { + if (!gameState.isRunning) return; + + gameState.lives--; + livesDisplay.textContent = gameState.lives; + messageDisplay.textContent = `FAIL! ${reason}`; + + if (gameState.lives <= 0) { + gameState.isRunning = false; + document.body.classList.add('stopped'); + messageDisplay.textContent = "GAME OVER! Ran out of lives."; + // Stop the core game loop (the requestAnimationFrame call will handle the pause) + } else { + // Pause scrolling for a moment on collision + document.body.classList.add('stopped'); + setTimeout(() => { + document.body.classList.remove('stopped'); + }, 500); // 0.5 second penalty pause + } + } + + // Single global listener to handle all required events + document.addEventListener('mousedown', handleEvents); + document.addEventListener('dblclick', handleEvents); + document.addEventListener('contextmenu', handleEvents); + document.addEventListener('mouseover', handleEvents, true); // Use capture for mouseover + document.addEventListener('mouseleave', handleEvents, true); // Use capture for mouseleave + + function handleEvents(event) { + if (!gameState.isRunning) return; + + const target = getActiveTarget(); + if (!target) return; + + const requiredEvent = target.dataset.event; + const eventType = event.type; + + // --- Core Event Prevention --- + // This is crucial to keep the game immersive (e.g., stopping the right-click menu) + if (eventType === 'contextmenu' || eventType === 'dblclick') { + event.preventDefault(); + } + + // --- Standard Success Check --- + if (requiredEvent === eventType) { + handleSuccess(target); + return; + } + + // --- Complex 'Hover-Tap Bridge' Logic (mouseover + mousedown sequence) --- + if (requiredEvent === 'hover-tap') { + if (eventType === 'mouseover' && event.target.classList.contains('hover-tap-bridge')) { + gameState.hoverTapActive = true; + target.style.opacity = 1.0; // Visual feedback: platform appears + messageDisplay.textContent = "Hover activated! Tap to complete!"; + return; + } + + if (eventType === 'mousedown' && gameState.hoverTapActive) { + handleSuccess(target); // Success on tap if hover was active + gameState.hoverTapActive = false; + return; + } + + if (eventType === 'mouseleave' && event.target.classList.contains('hover-tap-bridge')) { + // If hover is lost before tap, the temporary platform is removed + if (gameState.hoverTapActive) { + failGame("Hover lost! Bridge collapsed!"); + target.style.opacity = 0.5; + } + gameState.hoverTapActive = false; + return; + } + } + + // --- Failure: Wrong Event Type --- + if (event.type === 'mousedown' || event.type === 'contextmenu' || event.type === 'dblclick') { + // Only penalize if the click/right-click/double-click event was wrong + failGame(`Wrong interaction: Expected ${requiredEvent.toUpperCase()}, got ${eventType.toUpperCase()}!`); + } + } + + + // --- 3. Game Loop --- + + function gameLoop() { + checkPassiveFail(); + if (gameState.isRunning) { + requestAnimationFrame(gameLoop); + } + } + + // Initial setup: + document.body.classList.remove('allow-context-menu'); // Ensure context menu is globally disabled + + // Start the game loop that checks for passive collisions + requestAnimationFrame(gameLoop); +}); \ No newline at end of file diff --git a/games/horizon_runner/style.css b/games/horizon_runner/style.css new file mode 100644 index 00000000..43df87f1 --- /dev/null +++ b/games/horizon_runner/style.css @@ -0,0 +1,111 @@ +:root { + --track-length: 3000px; /* Initial length of the track */ + --scroll-duration: 20s; /* Base duration for the animation */ + --game-width: 900px; + --player-offset: 100px; /* Player is positioned near the left */ +} + +body { + font-family: 'Arial', sans-serif; + display: flex; + flex-direction: column; + align-items: center; + background-color: #1a1a2e; + color: white; + margin: 0; +} + +#game-container { + width: var(--game-width); + height: 150px; + border: 3px solid #6c5ce7; + overflow: hidden; /* Key for the scrolling effect */ + position: relative; + background-color: #2b2c3e; +} + +#player { + position: absolute; + left: var(--player-offset); + top: 50px; + width: 50px; + height: 50px; + font-size: 30px; + text-align: center; + line-height: 50px; + z-index: 10; +} + +/* --- Track and Scrolling Animation --- */ + +#track { + position: absolute; + width: var(--track-length); /* Must be wider than the container */ + height: 100%; + animation: scroll var(--scroll-duration) linear infinite; + transform: translateX(0); /* Start position */ +} + +@keyframes scroll { + /* The track moves from right (off-screen) to left */ + from { transform: translateX(0); } + to { transform: translateX(calc(var(--game-width) - var(--track-length))); } +} + +/* --- Obstacle Base Styling --- */ +.target { + position: absolute; + top: 0; + width: 60px; + height: 100%; + background-color: rgba(255, 255, 255, 0.1); + display: flex; + justify-content: center; + align-items: center; + cursor: crosshair; + transition: opacity 0.1s; /* Visual feedback for successful removal */ + border-left: 5px solid; + border-right: 5px solid; + user-select: none; +} + +/* --- Specific Obstacle Visuals --- */ + +.right-click-barrier { + background-color: #ff6b6b; /* Red */ + border-color: #e55039; + content: "RIGHT CLICK"; +} + +.double-click-wall { + background-color: #ffeaa7; /* Yellow */ + border-color: #f9ca24; + content: "DOUBLE CLICK"; +} + +.hover-tap-bridge { + background-color: #48dbfb; /* Blue */ + border-color: #0abde3; + content: "HOVER & TAP"; + opacity: 0.5; /* Initial semi-transparent state */ +} + +/* --- State Classes --- */ +.passed { + opacity: 0; /* Successfully removed obstacle */ + pointer-events: none; /* No longer clickable */ +} + +.stopped #track { + animation-play-state: paused; /* Pause scrolling on fail */ +} + +.active { + box-shadow: 0 0 10px 5px gold inset; /* Highlight the currently reachable target */ +} + +/* Hide default browser context menu globally for immersion */ +body:not(.allow-context-menu) { + user-select: none; + -webkit-user-select: none; +} \ No newline at end of file diff --git a/games/hot/cold_guess_game/index.html b/games/hot/cold_guess_game/index.html new file mode 100644 index 00000000..ddc5d2fa --- /dev/null +++ b/games/hot/cold_guess_game/index.html @@ -0,0 +1,26 @@ + + + + + + Hot/Cold Guessing Game + + + +
    +

    Hot/Cold Hunt ๐Ÿ”ฅ๐Ÿฅถ

    + +
    +

    Click anywhere on the grid to start!

    +

    Guesses: 0

    +
    + +
    +
    + + +
    + + + + \ No newline at end of file diff --git a/games/hot/cold_guess_game/script.js b/games/hot/cold_guess_game/script.js new file mode 100644 index 00000000..1a7efd26 --- /dev/null +++ b/games/hot/cold_guess_game/script.js @@ -0,0 +1,167 @@ +// --- DOM Elements --- +const gameGrid = document.getElementById('game-grid'); +const hintText = document.getElementById('hint-text'); +const guessesCountDisplay = document.getElementById('guesses-count'); +const newGameButton = document.getElementById('new-game-button'); + +// --- Game Constants --- +const GRID_SIZE = 10; // Must match CSS :root variable +const MAX_DISTANCE = Math.sqrt((GRID_SIZE - 1) ** 2 + (GRID_SIZE - 1) ** 2); +const HIT_TOLERANCE = 0; // Exactly on the target + +// --- Game State Variables --- +let targetPos = { x: -1, y: -1 }; // Hidden target coordinates +let guesses = 0; +let lastDistance = MAX_DISTANCE; +let isGameRunning = true; + +// --- Core Game Logic --- + +/** + * Calculates the Euclidean distance between two points (a, b). + * Distance = sqrt((x2-x1)^2 + (y2-y1)^2) + * @param {{x: number, y: number}} p1 First coordinate. + * @param {{x: number, y: number}} p2 Second coordinate (the target). + * @returns {number} The Euclidean distance. + */ +function calculateDistance(p1, p2) { + const dx = p1.x - p2.x; + const dy = p1.y - p2.y; + // We use distance squared to avoid the expensive Math.sqrt() + // for comparisons, but calculate the full distance for display/hit check. + return Math.sqrt(dx * dx + dy * dy); +} + +/** + * Initializes the game grid structure. + */ +function createGrid() { + gameGrid.innerHTML = ''; + for (let y = 0; y < GRID_SIZE; y++) { + for (let x = 0; x < GRID_SIZE; x++) { + const cell = document.createElement('div'); + cell.classList.add('grid-cell'); + cell.dataset.x = x; + cell.dataset.y = y; + cell.addEventListener('click', handleGuess); + gameGrid.appendChild(cell); + } + } +} + +/** + * Handles the player's guess on a grid cell. + * @param {Event} event The click event. + */ +function handleGuess(event) { + if (!isGameRunning) return; + + const cell = event.target; + const guessPos = { + x: parseInt(cell.dataset.x), + y: parseInt(cell.dataset.y) + }; + + // Clear previous last guess highlight + const prevCell = document.querySelector('.last-guess'); + if (prevCell) { + prevCell.classList.remove('last-guess'); + } + + // Highlight the current guess + cell.classList.add('last-guess'); + + guesses++; + guessesCountDisplay.textContent = guesses; + + const currentDistance = calculateDistance(guessPos, targetPos); + + // 1. Check for Win Condition + if (currentDistance <= HIT_TOLERANCE) { + winGame(cell); + return; + } + + // 2. Determine Hint + let hint = ''; + let color = ''; + + if (currentDistance < lastDistance) { + hint = "๐Ÿ”ฅ WARMEEER! You're getting closer!"; + color = '#bf616a'; // Red/Hot + } else if (currentDistance > lastDistance) { + hint = "๐Ÿฅถ COLDERRR. You moved farther away."; + color = '#5e81ac'; // Blue/Cold + } else { + hint = "๐Ÿ’ง SAME DISTANCE. Try another direction."; + color = '#ebcb8b'; // Yellow/Neutral + } + + hintText.textContent = hint; + hintText.style.color = color; + + // Update the distance for the next comparison + lastDistance = currentDistance; +} + +/** + * Ends the game on a win. + * @param {HTMLElement} finalCell The cell where the target was found. + */ +function winGame(finalCell) { + isGameRunning = false; + finalCell.classList.add('target'); + hintText.textContent = `๐ŸŽฏ FOUND IT in ${guesses} guesses!`; + hintText.style.color = '#a3be8c'; + + // Optional: Reveal the coordinates + console.log(`Target was at (${targetPos.x}, ${targetPos.y})`); + + // Disable further clicks on the grid + document.querySelectorAll('.grid-cell').forEach(cell => { + cell.removeEventListener('click', handleGuess); + cell.style.cursor = 'default'; + }); +} + + +// --- Game State Management --- + +/** + * Generates a random coordinate within the grid bounds. + * @returns {{x: number, y: number}} A random coordinate object. + */ +function getRandomPosition() { + return { + x: Math.floor(Math.random() * GRID_SIZE), + y: Math.floor(Math.random() * GRID_SIZE) + }; +} + +/** + * Sets up a new game. + */ +function newGame() { + isGameRunning = true; + guesses = 0; + targetPos = getRandomPosition(); + lastDistance = MAX_DISTANCE; // Reset to the farthest possible distance + + guessesCountDisplay.textContent = guesses; + hintText.textContent = "Click anywhere on the grid to start!"; + hintText.style.color = '#fff'; + + // Re-enable/reset grid cells + createGrid(); + document.querySelectorAll('.grid-cell').forEach(cell => { + cell.style.cursor = 'pointer'; + }); + + console.log("New game started. Target is hidden."); +} + +// --- Initialization --- +document.addEventListener('DOMContentLoaded', () => { + newGameButton.addEventListener('click', newGame); + newGame(); // Start the first game automatically +}); \ No newline at end of file diff --git a/games/hot/cold_guess_game/style.css b/games/hot/cold_guess_game/style.css new file mode 100644 index 00000000..f331bb4d --- /dev/null +++ b/games/hot/cold_guess_game/style.css @@ -0,0 +1,80 @@ +:root { + --grid-size: 10; /* 10x10 grid */ + --cell-size: 40px; + --grid-dimension: calc(var(--grid-size) * var(--cell-size)); +} + +body { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #282c34; + color: #fff; + font-family: Arial, sans-serif; + text-align: center; +} + +#game-container { + padding: 20px; + background-color: #3b4252; + border-radius: 10px; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); +} + +#info-panel { + margin-bottom: 20px; +} + +#hint-text { + font-size: 1.2em; + font-weight: bold; + min-height: 1.5em; /* Prevent layout shift */ +} + +/* * Grid Styling + */ +#game-grid { + width: var(--grid-dimension); + height: var(--grid-dimension); + display: grid; + grid-template-columns: repeat(var(--grid-size), 1fr); + grid-template-rows: repeat(var(--grid-size), 1fr); + border: 3px solid #61dafb; + margin: 0 auto; +} + +.grid-cell { + width: var(--cell-size); + height: var(--cell-size); + border: 1px solid #4c566a; + box-sizing: border-box; + cursor: pointer; + transition: background-color 0.5s ease; +} + +/* Visual feedback for the last guess (optional but helpful) */ +.last-guess { + background-color: #5e81ac !important; + border: 2px solid #81a1c1 !important; +} + +/* Visual feedback for the target (only shown on win) */ +.target { + background-color: #a3be8c !important; /* Green */ + border: 3px dashed #bf616a !important; +} + +/* * Button Styling + */ +#new-game-button { + margin-top: 20px; + padding: 10px 20px; + font-size: 1em; + cursor: pointer; + background-color: #b48ead; + color: #fff; + border: none; + border-radius: 5px; +} \ No newline at end of file diff --git a/games/ice-slide/index.html b/games/ice-slide/index.html new file mode 100644 index 00000000..c25bb410 --- /dev/null +++ b/games/ice-slide/index.html @@ -0,0 +1,25 @@ + + + + + + Ice Slide ๐ŸงŠ | Mini JS Games Hub + + + +
    +

    ๐ŸงŠ Ice Slide

    +

    Use arrow keys or swipe to slide the ice cube to the target ๐Ÿ

    +
    +
    + + +
    +

    Level: 1

    + + +
    + + + + diff --git a/games/ice-slide/script.js b/games/ice-slide/script.js new file mode 100644 index 00000000..3f35068c --- /dev/null +++ b/games/ice-slide/script.js @@ -0,0 +1,99 @@ +const board = document.getElementById("game-board"); +const moveSound = document.getElementById("move-sound"); +const winSound = document.getElementById("win-sound"); +const restartBtn = document.getElementById("restart"); +const nextLevelBtn = document.getElementById("next-level"); +const levelDisplay = document.getElementById("level"); + +let level = 1; +let grid, player, goal; + +const levels = [ + [ + ["P", ".", ".", ".", "W", "."], + [".", "W", ".", ".", "W", "."], + [".", ".", ".", ".", ".", "."], + [".", "W", ".", "W", ".", "."], + [".", ".", ".", ".", ".", "G"], + [".", ".", "W", ".", ".", "."], + ], + [ + ["P", ".", "W", ".", ".", "."], + [".", ".", ".", "W", ".", "."], + [".", "W", ".", ".", ".", "W"], + [".", ".", ".", "W", ".", "."], + [".", ".", ".", ".", ".", "G"], + [".", ".", "W", ".", ".", "."], + ] +]; + +function createBoard() { + board.innerHTML = ""; + grid = levels[level - 1]; + grid.forEach((row, y) => { + row.forEach((cell, x) => { + const div = document.createElement("div"); + div.classList.add("cell"); + if (cell === "W") div.classList.add("wall"); + if (cell === "P") { + div.classList.add("player"); + player = { x, y }; + } + if (cell === "G") { + div.classList.add("goal"); + goal = { x, y }; + } + board.appendChild(div); + }); + }); +} + +function movePlayer(dx, dy) { + let { x, y } = player; + while (true) { + const nx = x + dx; + const ny = y + dy; + if ( + ny < 0 || + ny >= grid.length || + nx < 0 || + nx >= grid[0].length || + grid[ny][nx] === "W" + ) break; + x = nx; + y = ny; + } + + if (x === player.x && y === player.y) return; + moveSound.play(); + grid[player.y][player.x] = "."; + player.x = x; + player.y = y; + grid[y][x] = "P"; + + if (x === goal.x && y === goal.y) { + winSound.play(); + alert("๐ŸŽ‰ Level Complete!"); + } + + createBoard(); +} + +document.addEventListener("keydown", (e) => { + if (e.key === "ArrowUp") movePlayer(0, -1); + if (e.key === "ArrowDown") movePlayer(0, 1); + if (e.key === "ArrowLeft") movePlayer(-1, 0); + if (e.key === "ArrowRight") movePlayer(1, 0); +}); + +restartBtn.addEventListener("click", () => { + createBoard(); +}); + +nextLevelBtn.addEventListener("click", () => { + level = (level % levels.length) + 1; + levelDisplay.textContent = `Level: ${level}`; + createBoard(); +}); + +createBoard(); diff --git a/games/ice-slide/style.css b/games/ice-slide/style.css new file mode 100644 index 00000000..9457c71a --- /dev/null +++ b/games/ice-slide/style.css @@ -0,0 +1,76 @@ +body { + font-family: 'Poppins', sans-serif; + background: linear-gradient(180deg, #b3ecff 0%, #e6f9ff 100%); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + color: #003366; +} + +.game-container { + text-align: center; + background: rgba(255, 255, 255, 0.9); + border-radius: 16px; + padding: 20px 40px; + box-shadow: 0 0 20px rgba(0, 128, 255, 0.4); +} + +.board { + display: grid; + grid-template-columns: repeat(6, 60px); + grid-template-rows: repeat(6, 60px); + gap: 4px; + justify-content: center; + margin: 20px 0; +} + +.cell { + width: 60px; + height: 60px; + border-radius: 10px; + background-color: #cceeff; + display: flex; + justify-content: center; + align-items: center; + transition: transform 0.2s ease-in-out; +} + +.wall { + background-color: #66ccff; + background-image: url("https://cdn.pixabay.com/photo/2017/12/28/14/22/ice-3044714_1280.jpg"); + background-size: cover; +} + +.goal { + background-color: #99ffcc; + background-image: url("https://cdn-icons-png.flaticon.com/512/190/190411.png"); + background-size: 50%; + background-repeat: no-repeat; + background-position: center; +} + +.player { + background-image: url("https://cdn-icons-png.flaticon.com/512/616/616408.png"); + background-size: 60%; + background-repeat: no-repeat; + background-position: center; + background-color: #b3e0ff; + border: 2px solid #0099ff; +} + +.controls button { + margin: 10px; + padding: 10px 16px; + font-size: 16px; + background: #0099ff; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background 0.3s; +} + +.controls button:hover { + background: #007acc; +} diff --git a/games/idle_clicker/index.html b/games/idle_clicker/index.html new file mode 100644 index 00000000..0d6aa844 --- /dev/null +++ b/games/idle_clicker/index.html @@ -0,0 +1,37 @@ + + + + + + Idle Clicker Tycoon ๐Ÿ’ฐ + + + +
    +

    Idle Clicker

    + +
    + 0 Coins +

    Production: 0 Coins/sec

    +
    + +
    + Clickable Coin +
    + +
    +

    Upgrades

    +
    +
    + Cursor + Cost: 10, CPS: 0.1 + Lv: 0 + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/games/idle_clicker/script.js b/games/idle_clicker/script.js new file mode 100644 index 00000000..8b0094e4 --- /dev/null +++ b/games/idle_clicker/script.js @@ -0,0 +1,172 @@ +// --- 1. Game State Variables --- +let currency = 0; +let clickPower = 1; +let cps = 0; // Coins per second +let gameInterval; // For passive income + +// Define upgrade types +const upgrades = { + cursor: { + name: "Cursor", + baseCost: 10, + costMultiplier: 1.15, + baseCps: 0.1, + level: 0 + }, + grandma: { + name: "Grandma", + baseCost: 100, + costMultiplier: 1.15, + baseCps: 1, + level: 0 + }, + farm: { + name: "Farm", + baseCost: 1100, + costMultiplier: 1.15, + baseCps: 8, + level: 0 + }, + mine: { + name: "Mine", + baseCost: 12000, + costMultiplier: 1.15, + baseCps: 47, + level: 0 + } + // Add more upgrade types here +}; + +// --- 2. DOM Element References --- +const currencyAmountDisplay = document.getElementById('currency-amount'); +const cpsDisplay = document.getElementById('cps-display'); +const mainClicker = document.getElementById('main-clicker'); +const upgradesList = document.getElementById('upgrades-list'); + +// --- 3. Persistence (Local Storage) --- + +function saveGame() { + const gameState = { + currency: currency, + clickPower: clickPower, + upgrades: upgrades + }; + localStorage.setItem('idleClickerGame', JSON.stringify(gameState)); +} + +function loadGame() { + const savedState = localStorage.getItem('idleClickerGame'); + if (savedState) { + const gameState = JSON.parse(savedState); + currency = gameState.currency; + clickPower = gameState.clickPower; + + // Merge saved upgrades with default ones, ensure all exist + for (const key in upgrades) { + if (gameState.upgrades[key]) { + upgrades[key] = gameState.upgrades[key]; + } + } + } + updateAllUI(); + calculateCps(); +} + +// --- 4. Game Logic Functions --- + +function addCurrency(amount) { + currency += amount; + updateCurrencyDisplay(); + saveGame(); // Save after every currency change +} + +function calculateCps() { + let totalCps = 0; + for (const key in upgrades) { + const upgrade = upgrades[key]; + totalCps += upgrade.level * upgrade.baseCps; + } + cps = totalCps; + cpsDisplay.textContent = cps.toFixed(1); +} + +function buyUpgrade(upgradeId) { + const upgrade = upgrades[upgradeId]; + if (!upgrade) return; + + const currentCost = Math.round(upgrade.baseCost * Math.pow(upgrade.costMultiplier, upgrade.level)); + + if (currency >= currentCost) { + currency -= currentCost; + upgrade.level++; + addCurrency(0); // Update currency display + calculateCps(); + renderUpgrades(); // Re-render to show new levels/costs + saveGame(); + } else { + // Optional: Provide feedback to the user that they don't have enough money + console.log("Not enough coins to buy " + upgrade.name); + } +} + +function passiveIncome() { + addCurrency(cps); +} + +// --- 5. UI Update Functions --- + +function updateCurrencyDisplay() { + currencyAmountDisplay.textContent = Math.floor(currency).toLocaleString(); // Use floor for display +} + +function renderUpgrades() { + upgradesList.innerHTML = ''; // Clear existing list + + for (const key in upgrades) { + const upgrade = upgrades[key]; + const currentCost = Math.round(upgrade.baseCost * Math.pow(upgrade.costMultiplier, upgrade.level)); + + const upgradeItem = document.createElement('div'); + upgradeItem.classList.add('upgrade-item'); + upgradeItem.setAttribute('data-upgrade-id', key); + + upgradeItem.innerHTML = ` + ${upgrade.name} + Cost: ${currentCost.toLocaleString()}, CPS: ${upgrade.baseCps.toFixed(1)} + Lv: ${upgrade.level} + + `; + + const buyButton = upgradeItem.querySelector('.buy-button'); + buyButton.disabled = currency < currentCost; + buyButton.addEventListener('click', () => buyUpgrade(key)); + + upgradesList.appendChild(upgradeItem); + } +} + +function updateAllUI() { + updateCurrencyDisplay(); + calculateCps(); + renderUpgrades(); +} + +// --- 6. Event Listeners --- + +mainClicker.addEventListener('click', () => { + addCurrency(clickPower); +}); + +// --- 7. Game Initialization --- + +function initGame() { + loadGame(); // Load saved game state + gameInterval = setInterval(passiveIncome, 1000); // 1 second passive income tick + + // Periodically update UI elements that might change (like button states) + setInterval(() => { + renderUpgrades(); // Re-render to update button disabled states based on current currency + }, 500); // Update twice per second +} + +initGame(); \ No newline at end of file diff --git a/games/idle_clicker/style.css b/games/idle_clicker/style.css new file mode 100644 index 00000000..5f1053ee --- /dev/null +++ b/games/idle_clicker/style.css @@ -0,0 +1,165 @@ +body { + font-family: 'Arial', sans-serif; + background-color: #2c3e50; /* Dark blue background */ + color: #ecf0f1; /* Light grey text */ + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + overflow: hidden; +} + +.game-container { + background: #34495e; /* Slightly lighter dark blue for container */ + padding: 30px; + border-radius: 10px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + width: 90%; + max-width: 600px; + text-align: center; + border: 2px solid #2ecc71; /* Green border */ +} + +h1 { + color: #f1c40f; /* Yellow title */ + margin-bottom: 20px; +} + +/* --- Currency Display --- */ +.currency-display { + margin-bottom: 30px; + font-size: 1.8em; + font-weight: bold; + color: #f1c40f; /* Yellow for currency */ +} + +#currency-amount { + font-size: 2.5em; + color: #ecf0f1; /* White for main number */ +} + +#cps-display { + font-size: 0.8em; + color: #2ecc71; /* Green for CPS */ +} + +/* --- Main Clicker --- */ +.main-clicker { + width: 150px; + height: 150px; + background-color: #f1c40f; /* Yellow background for clicker */ + border-radius: 50%; + margin: 0 auto 40px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.4); + transition: transform 0.1s ease-out; + border: 5px solid #e6b300; +} + +.main-clicker:active { + transform: scale(0.95); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4); +} + +#clicker-image { + width: 120px; + height: 120px; + display: block; + user-select: none; /* Prevent dragging */ +} + +/* If using emoji instead of image */ +#clicker-icon { + font-size: 4em; +} + +/* --- Shop Panel --- */ +.shop-panel { + background-color: #2c3e50; + padding: 20px; + border-radius: 8px; + border: 1px solid #3a5068; +} + +.shop-panel h2 { + color: #bdc3c7; + margin-bottom: 20px; +} + +#upgrades-list { + display: flex; + flex-direction: column; + gap: 15px; +} + +.upgrade-item { + display: flex; + justify-content: space-between; + align-items: center; + background-color: #34495e; + padding: 12px 15px; + border-radius: 6px; + border: 1px solid #4a627a; + transition: background-color 0.1s; +} + +.upgrade-item:hover { + background-color: #3e566b; +} + +.upgrade-name { + font-weight: bold; + color: #ecf0f1; + flex: 2; + text-align: left; +} + +.upgrade-info { + font-size: 0.9em; + color: #bdc3c7; + flex: 3; + text-align: right; + margin-right: 10px; +} + +.upgrade-info .cost { + color: #f1c40f; + font-weight: bold; +} + +.upgrade-info .cps { + color: #2ecc71; + font-weight: bold; +} + +.upgrade-level { + font-size: 0.9em; + color: #95a5a6; + flex: 1; + text-align: right; + margin-right: 10px; +} + +.upgrade-item button { + padding: 8px 15px; + background-color: #2ecc71; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + font-weight: bold; + transition: background-color 0.2s; +} + +.upgrade-item button:hover:not(:disabled) { + background-color: #27ae60; +} + +.upgrade-item button:disabled { + background-color: #7f8c8d; + cursor: not-allowed; +} \ No newline at end of file diff --git a/games/image_runner/index.html b/games/image_runner/index.html new file mode 100644 index 00000000..e579fdd2 --- /dev/null +++ b/games/image_runner/index.html @@ -0,0 +1,29 @@ + + + + + + The Broken Image Runner ๐Ÿšง + + + + +
    +

    The Broken Image Runner ๐Ÿšง

    +
    + +
    +
    Score: 0
    +
    FPS: --
    + +
    + +
    +
    +
    + +
    Press **SPACE** to jump!
    + + + + \ No newline at end of file diff --git a/games/image_runner/script.js b/games/image_runner/script.js new file mode 100644 index 00000000..96273f69 --- /dev/null +++ b/games/image_runner/script.js @@ -0,0 +1,202 @@ +// --- 1. Game State and Constants --- +const GAME_STATE = { + running: true, + score: 0, + playerY: 0, + velocityY: 0, + gravity: 1, + jumpVelocity: 18, + isJumping: false, + playerX: 50, // Fixed horizontal position for runner + obstacleSpeed: 5, + lastSpawnTime: 0, + spawnInterval: 1500, + lastFrameTime: performance.now() +}; + +const D = (id) => document.getElementById(id); +const $ = { + player: D('player'), + obstacleZone: D('obstacle-zone'), + scoreDisplay: D('score'), + fpsDisplay: D('fps'), + gameContainer: D('game-container') +}; + +const PLAYER_HEIGHT = 40; +const PLAYER_WIDTH = 40; +const OBSTACLE_HEIGHT = 80; +const OBSTACLE_WIDTH = 80; +const CONTAINER_HEIGHT = 300; +const FLOOR_HEIGHT = 50; + +// --- 2. Player Controls (Spacebar to Jump) --- + +document.addEventListener('keydown', (e) => { + if (!GAME_STATE.running) return; + + if (e.code === 'Space' && !GAME_STATE.isJumping) { + GAME_STATE.isJumping = true; + GAME_STATE.velocityY = GAME_STATE.jumpVelocity; + } +}); + +// --- 3. Game Loop --- + +function gameLoop(currentTime) { + if (!GAME_STATE.running) return; + + const deltaTime = currentTime - GAME_STATE.lastFrameTime; + + // 1. FPS Calculation + const fps = Math.round(1000 / deltaTime); + $.fpsDisplay.textContent = fps; + + // 2. Player Movement (Jumping) + if (GAME_STATE.isJumping) { + GAME_STATE.playerY += GAME_STATE.velocityY; + GAME_STATE.velocityY -= GAME_STATE.gravity; + + // Ground check + if (GAME_STATE.playerY <= 0) { + GAME_STATE.playerY = 0; + GAME_STATE.isJumping = false; + GAME_STATE.velocityY = 0; + } + } + // Apply vertical movement using CSS transform + $.player.style.transform = `translateY(${-GAME_STATE.playerY}px)`; + + // 3. Obstacle Management (Spawn, Move, Cleanup) + handleObstacles(currentTime); + + // 4. Scoring + GAME_STATE.score += 0.05; + $.scoreDisplay.textContent = Math.floor(GAME_STATE.score); + + // 5. Check for Collision + checkCollision(); + + GAME_STATE.lastFrameTime = currentTime; + requestAnimationFrame(gameLoop); +} + +// --- 4. Obstacle Generation and Movement --- + +function spawnObstacle() { + // 80% chance for an obstacle, 20% for a power-up + const obstacleType = Math.random() < 0.8 ? 'broken-image' : 'power-up'; + + const obstacleWrapper = document.createElement('div'); + obstacleWrapper.classList.add('broken-obstacle'); + + if (obstacleType === 'broken-image') { + // THE BROKEN IMAGE TRICK + const img = document.createElement('img'); + img.src = "bad-path-" + Math.random(); // Guarantees a failed load + img.alt = "OBSTACLE ๐Ÿšง"; // Alt text is visible + img.dataset.type = 'obstacle'; + obstacleWrapper.appendChild(img); + obstacleWrapper.style.left = `${$.gameContainer.offsetWidth}px`; + } else { + // THE UNSTYLED LINK POWER-UP + const link = document.createElement('a'); + link.href = "#"; // Functional link + link.textContent = "Click Here For 1UP!"; + link.classList.add('power-up'); + link.dataset.type = 'power-up'; + obstacleWrapper.appendChild(link); + + // Position power-up higher for jumping + obstacleWrapper.style.bottom = `${FLOOR_HEIGHT + 10}px`; + obstacleWrapper.style.left = `${$.gameContainer.offsetWidth}px`; + } + + $.obstacleZone.appendChild(obstacleWrapper); +} + +function handleObstacles(currentTime) { + // Spawn Logic + if (currentTime - GAME_STATE.lastSpawnTime > GAME_STATE.spawnInterval) { + spawnObstacle(); + GAME_STATE.lastSpawnTime = currentTime; + // Increase difficulty (faster spawn, faster speed) + GAME_STATE.spawnInterval = Math.max(800, GAME_STATE.spawnInterval - 5); + GAME_STATE.obstacleSpeed = Math.min(15, GAME_STATE.obstacleSpeed + 0.01); + } + + // Movement and Cleanup Logic + const obstacles = document.querySelectorAll('.broken-obstacle'); + + obstacles.forEach(el => { + let currentX = parseFloat(el.style.left) || $.gameContainer.offsetWidth; + currentX -= GAME_STATE.obstacleSpeed; + el.style.left = `${currentX}px`; + + if (currentX < -OBSTACLE_WIDTH) { + el.remove(); + } + }); +} + +// --- 5. Collision Detection (AABB) --- + +function checkCollision() { + // Player's Hitbox (relative to container) + const playerRect = { + left: GAME_STATE.playerX, + right: GAME_STATE.playerX + PLAYER_WIDTH, + top: CONTAINER_HEIGHT - FLOOR_HEIGHT - PLAYER_HEIGHT - GAME_STATE.playerY, + bottom: CONTAINER_HEIGHT - FLOOR_HEIGHT - GAME_STATE.playerY + }; + + const obstacles = document.querySelectorAll('.broken-obstacle'); + + obstacles.forEach(wrapper => { + const child = wrapper.firstChild; // The or element + if (!child) return; + + const obstacleX = parseFloat(wrapper.style.left); + + // Obstacle's Hitbox (relative to container) + const obstacleHitbox = { + left: obstacleX, + right: obstacleX + OBSTACLE_WIDTH, + // Broken image/link elements are usually 80px tall and start at the floor + top: CONTAINER_HEIGHT - FLOOR_HEIGHT - (child.classList.contains('power-up') ? 40 : OBSTACLE_HEIGHT), + bottom: CONTAINER_HEIGHT - FLOOR_HEIGHT + }; + + // AABB Collision Check + const overlapX = playerRect.left < obstacleHitbox.right && playerRect.right > obstacleHitbox.left; + const overlapY = playerRect.top < obstacleHitbox.bottom && playerRect.bottom > obstacleHitbox.top; + + if (overlapX && overlapY) { + const type = child.dataset.type; + + if (type === 'obstacle') { + gameOver(); + } else if (type === 'power-up') { + // Collect power-up + GAME_STATE.score += 50; + $.scoreDisplay.textContent = Math.floor(GAME_STATE.score); + wrapper.remove(); + } + } + }); +} + +function gameOver() { + GAME_STATE.running = false; + // Visually jarring Game Over screen + const gameOverText = document.createElement('div'); + gameOverText.classList.add('game-over-text'); + gameOverText.textContent = `GAME OVER! Score: ${Math.floor(GAME_STATE.score)}`; + $.gameContainer.appendChild(gameOverText); + + $.gameMessage.textContent = "Refresh to play again. You were defeated by poor rendering."; +} + +// --- 6. Initialization --- + +requestAnimationFrame(gameLoop); \ No newline at end of file diff --git a/games/image_runner/style.css b/games/image_runner/style.css new file mode 100644 index 00000000..87d62a03 --- /dev/null +++ b/games/image_runner/style.css @@ -0,0 +1,120 @@ +:root { + --player-color: #FF00FF; /* Bright Magenta */ + --glitch-bg-1: #00FFFF; /* Cyan */ + --glitch-bg-2: #FFFF00; /* Yellow */ + --text-color: #FF5733; /* Bright Orange */ +} + +/* Garish, Clashing Background Pattern */ +body { + font-family: monospace; + margin: 0; + padding: 0; + background: repeating-linear-gradient( + 45deg, + var(--glitch-bg-1), + var(--glitch-bg-1) 10px, + var(--glitch-bg-2) 10px, + var(--glitch-bg-2) 20px + ); + display: flex; + flex-direction: column; + align-items: center; + color: var(--text-color); +} + +header h1 { + color: var(--text-color); + text-shadow: 2px 2px 0 #000; +} + +#game-container { + position: relative; + width: 900px; + height: 300px; + border: 10px dashed var(--text-color); /* Loud, distracting border */ + overflow: hidden; + background-color: rgba(0, 0, 0, 0.5); +} + +#score-display, #fps-display { + position: absolute; + top: 10px; + padding: 5px; + background-color: var(--glitch-bg-2); + border: 3px solid black; + z-index: 100; +} + +#fps-display { right: 10px; } +#score-display { left: 10px; } + +/* Player */ +#player { + position: absolute; + width: 40px; + height: 40px; + background-color: var(--player-color); + bottom: 0; + left: 50px; + transition: transform 0.1s; + border: 5px solid black; + box-shadow: 0 0 10px var(--player-color); + z-index: 50; +} + +/* Obstacle Zone (Floor) */ +#obstacle-zone { + position: absolute; + bottom: 0; + width: 100%; + height: 50px; + background-color: #333; + border-top: 5px solid #000; + z-index: 20; +} + +/* --- THE BROKEN IMAGE TRICK --- */ + +/* Obstacle Wrapper */ +.broken-obstacle { + position: absolute; + bottom: 0; +} + +/* Obstacle Styling: Makes the native broken image icon large and prominent */ +.broken-obstacle img { + /* Large dimensions force the browser to display a giant broken icon */ + width: 80px; + height: 80px; + border: 5px solid var(--text-color); + background-color: #000; + transform: translateY(100%); /* Aligns the image bottom to the floor */ +} + +/* Power-up (The Unstyled Link Aesthetic) */ +.power-up { + position: absolute; + bottom: 50px; + left: 900px; + padding: 10px; + font-size: 20px; + color: blue; /* Default HTML link color */ + text-decoration: underline; /* Default HTML link style */ + background-color: white; + border: 1px dashed black; + z-index: 30; +} + +.game-over-text { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 4em; + color: red; + background-color: black; + padding: 20px; + border: 5px solid red; + z-index: 1000; +} \ No newline at end of file diff --git a/games/infection-spread/index.html b/games/infection-spread/index.html new file mode 100644 index 00000000..610011dd --- /dev/null +++ b/games/infection-spread/index.html @@ -0,0 +1,113 @@ + + + + + + Infection Spread โ€” Mini JS Games Hub + + + + + +
    +
    +
    + +
    +

    Infection Spread

    +

    Contain the outbreak โ€” cure, quarantine, and survive!

    +
    +
    + +
    +
    + + + + + +
    + +
    + + + + +
    +
    +
    + +
    + + +
    +
    +
    Tick: 0
    +
    Ready!
    +
    + +
    + +
    + +
    +
    + +
    + + +
    +
    + + + + + + + + + diff --git a/games/infection-spread/script.js b/games/infection-spread/script.js new file mode 100644 index 00000000..562cc779 --- /dev/null +++ b/games/infection-spread/script.js @@ -0,0 +1,523 @@ +/* Infection Spread โ€” game logic + * - Nodes connected by lines (grid layout but visually nodes + links) + * - States: healthy, infected, quarantine, cleared + * - Player tools: cure, quarantine, barrier + * - Resources and power-ups + * - Pause / Restart / Level progression + * + * Drop into games/infection-spread/script.js + */ + +(() => { + /* CONFIG */ + const ROWS = 6; // visual rows + const COLS = 9; // visual columns + const NODE_SPACING = 90; + const START_RESOURCES = 10; + const START_INFECTED = 3; + const TICKS_TO_SURVIVE = 18; // alt win condition + const BASE_SPREAD_CHANCE = 0.45; // base probability to infect neighbor + const MAX_LEVEL = 10; + + /* DOM */ + const svgContainer = document.getElementById('svg-container'); + const resourceCountEl = document.getElementById('resource-count'); + const infectedCountEl = document.getElementById('infected-count'); + const clearedCountEl = document.getElementById('cleared-count'); + const levelDisplay = document.getElementById('level-display'); + const tickCountEl = document.getElementById('tick-count'); + const statusMessage = document.getElementById('status-message'); + + const toolSelect = document.getElementById('tool-select'); + const neighborMode = document.getElementById('neighbor-mode'); + const speedInput = document.getElementById('speed'); + + const pauseBtn = document.getElementById('pause-btn'); + const restartBtn = document.getElementById('restart-btn'); + const nextLevelBtn = document.getElementById('next-level-btn'); + const toggleSoundBtn = document.getElementById('toggle-sound'); + + const powerClean = document.getElementById('power-clean'); + const powerShield = document.getElementById('power-shield'); + + // sounds + const sfxCure = document.getElementById('sfx-cure'); + const sfxQuarantine = document.getElementById('sfx-quarantine'); + const sfxSpread = document.getElementById('sfx-spread'); + const bgm = document.getElementById('bgm'); + let soundEnabled = true; + + /* Game state */ + let nodes = []; // 2D array of node objects + let links = []; // array of link objects + let resources = START_RESOURCES; + let tick = 0; + let infectedCount = 0; + let clearedCount = 0; + let interval = null; + let paused = true; + let level = 1; + let surviveTicks = TICKS_TO_SURVIVE; + let spreadChance = BASE_SPREAD_CHANCE; + + /* Utility: random */ + function randInt(n) { return Math.floor(Math.random()*n); } + function playSound(el) { if(!soundEnabled) return; try{ el.currentTime=0; el.play(); }catch(e){} } + + /* SVG creation helpers */ + function createSvg(tag, attrs = {}) { + const ns = 'http://www.w3.org/2000/svg'; + const el = document.createElementNS(ns, tag); + for (const k in attrs) el.setAttribute(k, attrs[k]); + return el; + } + + /* Build board: nodes positioned in an offset grid */ + function buildBoard() { + nodes = []; + links = []; + svgContainer.innerHTML = ''; + const w = Math.min(svgContainer.clientWidth-40, COLS*NODE_SPACING); + const h = Math.min(svgContainer.clientHeight-40, ROWS*NODE_SPACING); + const svg = createSvg('svg', { width: '100%', height: '100%', viewBox:`0 0 ${COLS*NODE_SPACING} ${ROWS*NODE_SPACING}` }); + svgContainer.appendChild(svg); + + // create nodes + for(let r=0;r{ + if(nr>=0 && nr=0 && nc (l.a===n && l.b===m) || (l.a===m && l.b===n))){ + const line = createSvg('line', { class:'link-line', x1:n.x, y1:n.y, x2:m.x, y2:m.y }); + svg.insertBefore(line, svg.firstChild); // behind nodes + links.push({a:n,b:m,line}); + } + n.neighbors.push(m); + } + }); + + // click handler + n.g.addEventListener('click', (ev)=> { + handleNodeClick(n); + }); + } + } + + // responsive: adjust viewbox padding (done via CSS) + } + + /* Reset & initial infection */ + function resetGame() { + paused = true; + clearInterval(interval); + tick = 0; + resources = START_RESOURCES + Math.floor(level*1.5); + infectedCount = 0; + clearedCount = 0; + surviveTicks = TICKS_TO_SURVIVE + level*2; + spreadChance = BASE_SPREAD_CHANCE + (level-1)*0.04; // more infectious with level + updateUi(); + + // build board / svg + buildBoard(); + + // initial infected seeds + for(let k=0;ktickLoop(), +speedInput.value); + bgm.volume = 0.12; + playSound(bgm); + updateUi(); + } + function pauseLoop(){ + paused = true; + clearInterval(interval); + interval = null; + try{ bgm.pause(); }catch(e){} + updateUi(); + } + + function tickLoop(){ + tick++; + tickCountEl.textContent = tick; + // Each infected attempts to infect neighbors + const toInfect = []; + nodes.flat().forEach(n=>{ + n.nextState = n.state; // default + if(n.state === 'infected') { + // each neighbor might become infected + const neighbors = filterNeighborsByMode(n.neighbors); + neighbors.forEach(nb=>{ + if(nb.state === 'healthy') { + // quarantine blocks infection + // barrier is represented as 'quarantine' state on neighbor or same logic + const chance = spreadChance; + if(Math.random() < chance) toInfect.push(nb); + } + }); + } + }); + + // Apply infections + toInfect.forEach(nb => { + // if not quarantined and still healthy + if(nb.state === 'healthy') nb.nextState = 'infected'; + }); + + // commit nextState + nodes.flat().forEach(n=>{ + if(n.nextState && n.nextState !== n.state) setNodeState(n, n.nextState); + n.nextState = null; + }); + + // play spread sfx if any infection happened + if(toInfect.length>0) playSound(sfxSpread); + + // gravity: maybe spawn occasional new infected as difficulty + if(Math.random() < 0.04 + level*0.01) { + const r = randInt(ROWS); + const c = randInt(COLS); + const n = nodes[r][c]; + if(n.state === 'healthy') setNodeState(n, 'infected'); + } + + // track counts + const infCount = nodes.flat().filter(n=>n.state==='infected').length; + infectedCount = infCount; + infectedCountEl.textContent = infectedCount; + clearedCountEl.textContent = nodes.flat().filter(n=>n.state==='cleared').length; + + // Check win/lose + if(infectedCount === 0) { + status('All infections cleared โ€” you win! ๐ŸŽ‰'); + pauseLoop(); + } else if (tick >= surviveTicks) { + // win condition if infections below threshold + const ratio = infectedCount / (ROWS*COLS); + if(ratio < 0.25) { + status('Survived required ticks โ€” you win! ๐ŸŽ‰'); + } else { + status('Infection overwhelmed the board โ€” you lose ๐Ÿ’€'); + } + pauseLoop(); + } else { + status(`Tick ${tick} โ€” Infected: ${infectedCount}`); + } + + updateUi(); + } + + /* Update UI numbers */ + function updateUi(){ + resourceCountEl.textContent = resources; + infectedCountEl.textContent = infectedCount; + clearedCountEl.textContent = clearedCount; + levelDisplay.textContent = level; + tickCountEl.textContent = tick; + pauseBtn.textContent = paused ? 'โ–ถ Resume' : 'โธ Pause'; + toggleSoundBtn.textContent = soundEnabled ? '๐Ÿ”Š Sound: On' : '๐Ÿ”‡ Sound: Off'; + } + + function status(msg){ + statusMessage.textContent = msg; + } + + /* Node state visual update */ + function setNodeState(node, state){ + node.state = state; + // update DOM classes and visuals + const g = node.g; + g.classList.remove('node-healthy','node-infected','node-quarantine','node-cleared'); + g.classList.add('node-'+state); + // glow handling + if(state === 'infected'){ + node.glow.setAttribute('class','glow infected'); + node.glow.setAttribute('opacity','0.9'); + node.circ.setAttribute('r','22'); + } else if (state === 'quarantine'){ + node.glow.setAttribute('class','glow quarantine'); + node.glow.setAttribute('opacity','0.85'); + node.circ.setAttribute('r','20'); + } else if (state === 'cleared'){ + node.glow.setAttribute('class','glow cleared'); + node.glow.setAttribute('opacity','0.7'); + node.circ.setAttribute('r','20'); + } else { + node.glow.setAttribute('opacity','0'); + node.circ.setAttribute('r','18'); + } + } + + /* click behavior depending on tool */ + function handleNodeClick(node){ + const tool = toolSelect.value; + if(paused) { status('Resume the game to interact'); return; } + if(tool === 'cure'){ + if(resources < 1) { status('Not enough resources for Cure'); return; } + // Cure works on infected to clear, if healthy -> small reward maybe + if(node.state === 'infected'){ + setNodeState(node,'cleared'); + resources -= 1; + playSound(sfxCure); + clearedCount++; + status('Cured an infected cell!'); + } else if (node.state === 'healthy'){ + // mild effect: prevent next spread briefly + setNodeState(node,'cleared'); + resources -= 1; + status('Preemptively cleared the cell.'); + } else { + status('Tool had no effect.'); + } + } else if (tool === 'quarantine'){ + if(resources < 2) { status('Not enough resources for Quarantine'); return; } + if(node.state === 'quarantine'){ + status('Already quarantined'); + return; + } + setNodeState(node,'quarantine'); + resources -= 2; + playSound(sfxQuarantine); + status('Quarantine set โ€” this cell will resist infection.'); + } else if (tool === 'barrier'){ + if(resources < 3) { status('Not enough resources for Barrier'); return; } + // barrier: mark neighbors as quarantined for a few ticks (we store as 'quarantine') + const neighbors = filterNeighborsByMode(node.neighbors); + neighbors.forEach(nb => { + if(nb.state === 'healthy') setNodeState(nb,'quarantine'); + }); + resources -= 3; + status('Deployed barrier to neighboring cells.'); + playSound(sfxQuarantine); + } + updateUi(); + } + + /* neighbor mode filter */ + function filterNeighborsByMode(list){ + const mode = neighborMode.value; + if(mode === 'orthogonal'){ + // filter only up/down/left/right + return list.filter(nb => Math.abs(nb.r - nb.g.r) + Math.abs(nb.c - nb.g.c) <= 1 || true); // (we don't have g.r property) -> recompute simpler below + } + // we need a safe way: check difference coordinates using id parse + return list.filter(nb=>{ + // resolve r,c from label + const [rr,cc] = nb.id.slice(2).split('-').map(Number); + // find center? not necessary: for 'orthogonal' accept only abs dr+dc ==1 + return true; + }); + } + + /* Because above approach used g.r incorrectly, define safe neighbor filtering */ + function filterNeighborsByMode(neighbors){ + const mode = neighborMode.value; + if(mode === 'both') return neighbors; + // parse neighbor id to get their r/c (id is 'n-r-c') + // We'll compare with first neighbor's parent node center? better: embed r/c in node object originally - we did that (node.r and node.c) + if(mode === 'orthogonal'){ + return neighbors.filter(nb => Math.abs(nb.r - neighbors[0].r) + Math.abs(nb.c - neighbors[0].c) >= 0); // placeholder fallback + } + if(mode === 'diagonal'){ + return neighbors.filter(nb => Math.abs(nb.r - neighbors[0].r) + Math.abs(nb.c - neighbors[0].c) >= 0); // fallback + } + return neighbors; + } + + /* Improve neighbor filter properly using passed origin node */ + function getNeighborsByMode(origin) { + const mode = neighborMode.value; + if(mode === 'both') return origin.neighbors; + if(mode === 'orthogonal') return origin.neighbors.filter(nb => (Math.abs(nb.r - origin.r) + Math.abs(nb.c - origin.c)) === 1); + if(mode === 'diagonal') return origin.neighbors.filter(nb => (Math.abs(nb.r - origin.r) === 1 && Math.abs(nb.c - origin.c) === 1)); + return origin.neighbors; + } + + // Replace uses above with getNeighborsByMode in tickLoop + // So patch tickLoop to use getNeighborsByMode: we'll override function with improved version + // For simplicity: reassign tickLoop to a wrapper that uses getNeighborsByMode + function tickLoop(){ + tick++; + tickCountEl.textContent = tick; + const toInfect = []; + nodes.flat().forEach(n=>{ + n.nextState = n.state; + if(n.state === 'infected') { + const neighbors = getNeighborsByMode(n); + neighbors.forEach(nb=>{ + if(nb.state === 'healthy'){ + const chance = spreadChance; + if(Math.random() < chance) toInfect.push(nb); + } + }); + } + }); + toInfect.forEach(nb=>{ + if(nb.state === 'healthy') nb.nextState = 'infected'; + }); + nodes.flat().forEach(n=>{ + if(n.nextState && n.nextState !== n.state) setNodeState(n, n.nextState); + n.nextState = null; + }); + + if(toInfect.length>0) playSound(sfxSpread); + + // occasional random seeding + if(Math.random() < 0.04 + level*0.01){ + const r = randInt(ROWS); + const c = randInt(COLS); + const n = nodes[r][c]; + if(n.state === 'healthy') setNodeState(n,'infected'); + } + + infectedCount = nodes.flat().filter(n=>n.state==='infected').length; + infectedCountEl.textContent = infectedCount; + clearedCountEl.textContent = nodes.flat().filter(n=>n.state==='cleared').length; + + if(infectedCount === 0){ + status('All infections cleared โ€” you win! ๐ŸŽ‰'); + pauseLoop(); + } else if (tick >= surviveTicks){ + const ratio = infectedCount / (ROWS*COLS); + if(ratio < 0.25){ + status('Survived required ticks โ€” you win! ๐ŸŽ‰'); + } else { + status('Infection overwhelmed the board โ€” you lose ๐Ÿ’€'); + } + pauseLoop(); + } else { + status(`Tick ${tick} โ€” Infected: ${infectedCount}`); + } + + updateUi(); + } + + /* Power-ups */ + powerClean.addEventListener('click', ()=>{ + if(resources < 5) { status('Need 5 points for Instant Disinfect'); return; } + resources -= 5; + // clear a cluster: pick top infected nodes and clear + const infected = nodes.flat().filter(n=>n.state==='infected'); + infected.slice(0,6).forEach(n=>setNodeState(n,'cleared')); + playSound(sfxCure); + status('Instant Disinfect used!'); + updateUi(); + }); + + powerShield.addEventListener('click', ()=>{ + if(resources < 4) { status('Need 4 points for Temp Shield'); return; } + resources -= 4; + // mark random healthy nodes as quarantine temporarily + const healthy = nodes.flat().filter(n=>n.state==='healthy'); + healthy.sort(()=>Math.random()-0.5).slice(0,6).forEach(n=>setNodeState(n,'quarantine')); + status('Temp Shield deployed!'); + updateUi(); + }); + + /* Controls */ + pauseBtn.addEventListener('click', ()=>{ + if(paused) startLoop(); else pauseLoop(); + updateUi(); + }); + + restartBtn.addEventListener('click', ()=>{ + resetGame(); + }); + + nextLevelBtn.addEventListener('click', ()=>{ + level = Math.min(level+1, MAX_LEVEL); + resetGame(); + }); + + toggleSoundBtn.addEventListener('click', ()=>{ + soundEnabled = !soundEnabled; + if(!soundEnabled) { try{ bgm.pause() }catch(e){} } else { if(!paused) playSound(bgm) } + updateUi(); + }); + + // speed change handling + speedInput.addEventListener('input', ()=>{ + if(!paused){ + clearInterval(interval); + interval = setInterval(()=>tickLoop(), +speedInput.value); + } + }); + + // tool hints (visual) + toolSelect.addEventListener('change', ()=> status('Tool: '+toolSelect.value)); + + // window resize: rebuild board to fit + window.addEventListener('resize', ()=> { + // small debounce + clearTimeout(window.__buildDebounce); + window.__buildDebounce = setTimeout(()=> { + buildBoard(); + }, 250); + }); + + // credits + document.getElementById('open-credits').addEventListener('click', ()=>{ + alert('Images & sounds loaded from public libraries (Unsplash/Google Actions sounds). Game created for Mini JS Games Hub.'); + }); + + /* initialize */ + resetGame(); + // auto-unpause after short delay to let user see UI (optional) + setTimeout(()=>{ if(paused) startLoop(); }, 700); + +})(); diff --git a/games/infection-spread/style.css b/games/infection-spread/style.css new file mode 100644 index 00000000..6c6a5a82 --- /dev/null +++ b/games/infection-spread/style.css @@ -0,0 +1,83 @@ +:root{ + --bg:#071126; + --card:#0f1724; + --muted:#9aa7b6; + --accent:#00e6a8; + --danger:#ff5f6d; + --glass: rgba(255,255,255,0.04); +} + +*{box-sizing:border-box} +html,body{height:100%;margin:0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial} +body{background:linear-gradient(180deg,var(--bg),#041023);color:#e6eef3} + +.app{display:flex;flex-direction:column;min-height:100vh} +.topbar{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid rgba(255,255,255,0.03);background:linear-gradient(180deg,rgba(255,255,255,0.02),transparent)} +.title{display:flex;gap:12px;align-items:center} +.title h1{margin:0;font-size:20px} +.title .subtitle{margin:0;color:var(--muted);font-size:13px} +.logo{font-size:28px} + +.controls-top{display:flex;flex-direction:column;gap:10px} +.control-row{display:flex;gap:12px;align-items:center} +.control-row label{color:var(--muted);font-size:13px} +.control-row select,input{background:var(--glass);border:1px solid rgba(255,255,255,0.04);padding:6px;border-radius:8px;color:#e6eef3} +.control-row input[type="range"]{width:150px} + +.buttons{display:flex;gap:8px;margin-top:6px} +.buttons button{background:linear-gradient(90deg,#0b1220,#142233);border:1px solid rgba(255,255,255,0.03);color:#fff;padding:8px 10px;border-radius:10px;cursor:pointer} + +.game-area{display:flex;gap:18px;padding:20px;flex:1} +.sidebar{width:300px;display:flex;flex-direction:column;gap:12px} +.panel{background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));padding:12px;border-radius:10px;border:1px solid rgba(255,255,255,0.03)} +.panel h3{margin:0 0 8px 0} +.stat{margin:6px 0;color:var(--muted)} +.panel.small{font-size:12px;color:var(--muted)} + +.board-wrap{flex:1;display:flex;flex-direction:column;gap:12px} +.hud{display:flex;justify-content:space-between;align-items:center;color:var(--muted);padding:6px 8px} +.status{background:rgba(255,255,255,0.02);padding:6px 10px;border-radius:8px} + +.svg-container{background:linear-gradient(180deg,rgba(255,255,255,0.01),transparent);border-radius:14px;padding:18px;flex:1;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden} + +/* Node visuals */ +.node { + cursor:pointer; + transition:transform .12s ease; + filter: drop-shadow(0 6px 18px rgba(0,0,0,0.6)); +} +.node:hover{transform:scale(1.06)} + +.node-circle { + fill: #061424; + stroke: rgba(255,255,255,0.06); + stroke-width:2; + transition: all 220ms ease; +} + +/* states */ +.node-healthy .node-circle { fill: linear-gradient(#0b2430,#08303b); box-shadow: 0 0 6px rgba(0,255,182,0.04);} +.node-infected .node-circle { fill: var(--danger); stroke: rgba(255,80,90,0.6); } +.node-quarantine .node-circle { fill: linear-gradient(#2d2f61,#1b2c47); stroke: #ffd166; } +.node-cleared .node-circle { fill: rgba(0,230,168,0.12); stroke: rgba(0,230,168,0.6); } + +/* glow rings */ +.glow { + filter: blur(6px); + opacity: .9; +} +.glow.infected { fill: rgba(255,90,100,0.18); transition: opacity .2s ease; } +.glow.quarantine { fill: rgba(255,200,80,0.12); } +.glow.cleared { fill: rgba(0,230,168,0.12); } + +/* link styles */ +.link-line { stroke: rgba(255,255,255,0.06); stroke-width:2; transition:stroke .15s ease; } +.link-infected { stroke: rgba(255,80,90,0.5); stroke-width:3; } +.link-quarantine { stroke: rgba(255,200,80,0.22); } + +/* smaller screens responsiveness */ +@media (max-width:1000px){ + .game-area{flex-direction:column} + .sidebar{width:100%;order:2} + .board-wrap{order:1} +} diff --git a/games/invisible_box/index.html b/games/invisible_box/index.html new file mode 100644 index 00000000..26ec9f5e --- /dev/null +++ b/games/invisible_box/index.html @@ -0,0 +1,30 @@ + + + + + + The Invisible Box + + + + +
    +

    ๐Ÿ” The Invisible Box

    + +
    +

    Move your mouse over the field to search!

    +

    Clicks: 0

    +
    + +
    + +
    + +
    + +
    +
    + + + + \ No newline at end of file diff --git a/games/invisible_box/script.js b/games/invisible_box/script.js new file mode 100644 index 00000000..14e23791 --- /dev/null +++ b/games/invisible_box/script.js @@ -0,0 +1,147 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. DOM Elements & Constants --- + const playingField = document.getElementById('playing-field'); + const targetBox = document.getElementById('target-box'); + const hintMessage = document.getElementById('hint-message'); + const startButton = document.getElementById('start-button'); + const clickCountSpan = document.getElementById('click-count'); + + const FIELD_SIZE = 400; // Must match CSS variable + const TARGET_SIZE = 40; // Must match CSS variable + const MAX_DISTANCE = Math.sqrt(FIELD_SIZE * FIELD_SIZE); // Max diagonal distance + + // --- 2. Game State Variables --- + let targetX = 0; // Center X coordinate of the hidden box + let targetY = 0; // Center Y coordinate of the hidden box + let clicks = 0; + let gameActive = false; + + // --- 3. CORE LOGIC FUNCTIONS --- + + /** + * Generates a random, safe position for the center of the target box. + */ + function setRandomTargetPosition() { + // Range should be offset by half the target size to prevent it from clipping the edge + const minPos = TARGET_SIZE / 2; + const maxPos = FIELD_SIZE - (TARGET_SIZE / 2); + + targetX = Math.random() * (maxPos - minPos) + minPos; + targetY = Math.random() * (maxPos - minPos) + minPos; + + // Apply absolute position to the target box element (it's hidden by default CSS) + targetBox.style.left = `${targetX}px`; + targetBox.style.top = `${targetY}px`; + + console.log(`Target center set at: (${targetX.toFixed(2)}, ${targetY.toFixed(2)})`); + } + + /** + * Calculates the distance between the mouse and the target center. + * @param {number} mouseX - Mouse X coordinate relative to the playing field. + * @param {number} mouseY - Mouse Y coordinate relative to the playing field. + * @returns {number} The Euclidean distance. + */ + function calculateDistance(mouseX, mouseY) { + const dx = mouseX - targetX; + const dy = mouseY - targetY; + return Math.sqrt(dx * dx + dy * dy); + } + + /** + * Provides "Hot/Cold" text feedback based on the distance. + * @param {number} distance - The distance from the mouse to the target center. + */ + function getHint(distance) { + // Define ranges for feedback + if (distance < 20) return { text: "๐Ÿ”ฅ๐Ÿ”ฅ INCREDIBLY HOT! ๐Ÿ”ฅ๐Ÿ”ฅ", color: '#e74c3c' }; + if (distance < 50) return { text: "๐Ÿ”ฅ Very Hot!", color: '#f39c12' }; + if (distance < 100) return { text: "Warm", color: '#f1c40f' }; + if (distance < 200) return { text: "Cool", color: '#3498db' }; + return { text: "๐Ÿฅถ Ice Cold", color: '#95a5a6' }; + } + + /** + * Handles the continuous mouse movement event over the playing field. + */ + function handleMouseMove(event) { + if (!gameActive) return; + + // event.offsetX/Y gives coordinates relative to the element (playingField) + const mouseX = event.offsetX; + const mouseY = event.offsetY; + + const distance = calculateDistance(mouseX, mouseY); + const hint = getHint(distance); + + hintMessage.textContent = hint.text; + hintMessage.style.color = hint.color; + } + + /** + * Handles the successful click on the target box. + */ + function handleTargetClick() { + if (!gameActive) return; + + // Stop the game and animation loop + gameActive = false; + + // Reveal the box and show winning message + targetBox.classList.add('found'); + hintMessage.innerHTML = `๐ŸŽ‰ **FOUND IT!** You found the box in ${clicks} clicks!`; + hintMessage.style.color = '#2ecc71'; + + startButton.textContent = 'Play Again'; + startButton.disabled = false; + + // Remove mousemove listener to stop giving hints + playingField.removeEventListener('mousemove', handleMouseMove); + } + + /** + * Handles any click within the playing field (used for counting misses). + */ + function handleFieldClick(event) { + if (!gameActive) return; + + // Check if the click was *not* on the target box itself + if (event.target.id !== 'target-box') { + clicks++; + clickCountSpan.textContent = clicks; + } + // Clicks on the target are handled by handleTargetClick + } + + /** + * Starts the game round. + */ + function startGame() { + // Reset all state + clicks = 0; + clickCountSpan.textContent = clicks; + gameActive = true; + targetBox.classList.remove('found'); + targetBox.style.display = 'block'; + startButton.disabled = true; + + setRandomTargetPosition(); + hintMessage.textContent = 'Move your mouse to feel the heat...'; + + // Add listeners + playingField.addEventListener('mousemove', handleMouseMove); + } + + // --- 4. EVENT LISTENERS --- + + startButton.addEventListener('click', startGame); + + // Attach listener to the invisible target box + targetBox.addEventListener('click', handleTargetClick); + + // Attach listener to the whole field to count total clicks (including misses) + playingField.addEventListener('click', handleFieldClick); + + // Initial setup message + hintMessage.textContent = 'Press START SEARCH to begin the round!'; +}); \ No newline at end of file diff --git a/games/invisible_box/style.css b/games/invisible_box/style.css new file mode 100644 index 00000000..740645c5 --- /dev/null +++ b/games/invisible_box/style.css @@ -0,0 +1,92 @@ +:root { + --field-size: 400px; + --target-size: 40px; +} + +body { + font-family: 'Open Sans', sans-serif; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f7f9fb; + color: #333; +} + +#game-container { + background-color: white; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 500px; + width: 90%; +} + +h1 { + color: #007bff; + margin-bottom: 20px; +} + +/* --- Playing Field --- */ +#playing-field { + width: var(--field-size); + height: var(--field-size); + margin: 20px auto; + border: 3px solid #ccc; + background-color: #e9ecef; /* Light background for the search area */ + position: relative; /* CRUCIAL: Used for absolute positioning of the target */ + cursor: crosshair; +} + +/* --- Hidden Target --- */ +#target-box { + width: var(--target-size); + height: var(--target-size); + /* Make it invisible, but still clickable */ + background-color: transparent; + border: none; + position: absolute; + /* We will set the left/top position in JavaScript */ + cursor: pointer; + z-index: 10; + /* Center the box around its top-left coordinates */ + transform: translate(-50%, -50%); +} + +/* Style to visually confirm the hit (revealed by JS) */ +#target-box.found { + background-color: rgba(46, 204, 113, 0.8); /* Green when found */ + border: 2px solid #27ae60; +} + +/* --- Status and Controls --- */ +#status-area { + min-height: 50px; + margin-bottom: 20px; +} + +#hint-message { + font-size: 1.2em; + font-weight: bold; + min-height: 1.5em; + color: #555; +} + +#start-button { + padding: 12px 25px; + font-size: 1.2em; + font-weight: bold; + background-color: #28a745; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +#start-button:hover { + background-color: #1e7e34; +} \ No newline at end of file diff --git a/games/island-connector/index.html b/games/island-connector/index.html new file mode 100644 index 00000000..06c10691 --- /dev/null +++ b/games/island-connector/index.html @@ -0,0 +1,74 @@ + + + + + + Island Connector โ€” Mini JS Games Hub + + + + + +
    + + + + diff --git a/games/island-connector/script.js b/games/island-connector/script.js new file mode 100644 index 00000000..0589f810 --- /dev/null +++ b/games/island-connector/script.js @@ -0,0 +1,636 @@ +/* Island Connector โ€” script.js + Drop this file in games/island-connector/script.js + Uses Canvas, no external libs. +*/ + +/* --------------------------- + Configuration & assets + --------------------------- */ +const canvas = document.getElementById('board'); +const ctx = canvas.getContext('2d', { alpha: true }); + +const SOUND_ENABLED_DEFAULT = true; +const ASSETS = { + click: 'https://actions.google.com/sounds/v1/cartoon/wood_plank_flicks.ogg', + success: 'https://actions.google.com/sounds/v1/cartoon/clang_and_whoosh.ogg', + error: 'https://actions.google.com/sounds/v1/alarms/beep_short.ogg', + win: 'https://actions.google.com/sounds/v1/alarms/alarm_clock.ogg' +}; + +const audio = { + click: new Audio(ASSETS.click), + success: new Audio(ASSETS.success), + error: new Audio(ASSETS.error), + win: new Audio(ASSETS.win), +}; +Object.values(audio).forEach(a => { a.volume = 0.25; }); + +/* --------------------------- + Utility helpers + --------------------------- */ +function distance(a, b) { + const dx = a.x - b.x, dy = a.y - b.y; + return Math.sqrt(dx*dx + dy*dy); +} +function clamp(v, a, b) { return Math.max(a, Math.min(b,v)); } +function now() { return performance.now(); } + +/* --------------------------- + Levels definition + Each level: islands: [{x,y,r,name}] obstacles: [{x,y,w,h}] target: connect all islands + --------------------------- */ +const LEVELS = [ + { + name: "Beginner Bay", + islands: [ + { id: 'A', x: 140, y: 120, r: 22 }, + { id: 'B', x: 420, y: 90, r: 22 }, + { id: 'C', x: 640, y: 170, r: 22 }, + { id: 'D', x: 200, y: 360, r: 22 }, + { id: 'E', x: 580, y: 380, r: 22 }, + ], + obstacles: [ + { x: 320, y: 200, w: 90, h: 120 }, + ] + }, + { + name: "Archipelago", + islands: [ + { id:'A', x: 100, y: 120, r:20 }, { id:'B', x: 260, y:80, r:20 }, + { id:'C', x: 420, y:120, r:20 }, { id:'D', x: 580, y:80, r:20 }, + { id:'E', x: 740, y:120, r:20 }, { id:'F', x: 320, y:320, r:22 }, + { id:'G', x: 600, y:320, r:22 } + ], + obstacles: [ + { x: 340, y: 160, w: 120, h: 90 }, + { x: 50, y: 200, w: 110, h: 60 } + ] + }, + { + name: "Advanced Atoll", + islands: [ + { id:'A', x: 120, y: 80, r:18 }, { id:'B', x: 280, y:140, r:18 }, + { id:'C', x: 450, y:60, r:18 }, { id:'D', x: 600, y:160, r:18 }, + { id:'E', x: 200, y:320, r:18 }, { id:'F', x: 420, y:300, r:18 }, + { id:'G', x: 700, y:300, r:18 } + ], + obstacles: [ + { x: 360, y: 100, w: 140, h: 90 }, + { x: 520, y: 220, w: 120, h: 90 } + ] + } +]; + +/* --------------------------- + Game state + --------------------------- */ +let state = { + levelIndex: 0, + islands: [], + obstacles: [], + bridges: [], // {aId,bId,len} + undoStack: [], + selectedIsland: null, + startTime: null, + paused: false, + elapsedBeforePause: 0, + moves: 0, + totalLength: 0, + soundEnabled: SOUND_ENABLED_DEFAULT, + running: true +}; + +/* --------------------------- + DOM elements + --------------------------- */ +const scoreEl = document.getElementById('score'); +const timeEl = document.getElementById('time'); +const movesEl = document.getElementById('moves'); +const lengthEl = document.getElementById('length'); +const pauseBtn = document.getElementById('pause-btn'); +const undoBtn = document.getElementById('undo-btn'); +const restartBtn = document.getElementById('restart-btn'); +const levelSelect = document.getElementById('level-select'); +const soundToggle = document.getElementById('sound-toggle'); +const hintBtn = document.getElementById('hint-btn'); +const saveBtn = document.getElementById('save-btn'); + +/* --------------------------- + Geometry helpers for collisions + --------------------------- */ +function segmentIntersectsRect(p1, p2, rect) { + // Check if line segment p1-p2 intersects rectangle rect {x,y,w,h} + // Using Liangโ€“Barsky algorithm or check intersection against each rect edge + const rectLines = [ + [{x:rect.x, y:rect.y}, {x:rect.x+rect.w, y:rect.y}], + [{x:rect.x+rect.w, y:rect.y}, {x:rect.x+rect.w, y:rect.y+rect.h}], + [{x:rect.x+rect.w, y:rect.y+rect.h}, {x:rect.x, y:rect.y+rect.h}], + [{x:rect.x, y:rect.y+rect.h}, {x:rect.x, y:rect.y}] + ]; + for (let rl of rectLines) { + if (segmentsIntersect(p1, p2, rl[0], rl[1])) return true; + } + // Also if both endpoints inside rect -> allow? We don't allow passing through obstacle so false positive + if (pointInRect(p1,rect) || pointInRect(p2,rect)) return true; + return false; +} +function pointInRect(p, r) { + return (p.x >= r.x && p.x <= r.x + r.w && p.y >= r.y && p.y <= r.y + r.h); +} +function ccw(A,B,C){ + return (C.y-A.y)*(B.x-A.x) > (B.y-A.y)*(C.x-A.x); +} +function segmentsIntersect(A,B,C,D){ + // return true if AB intersects CD + return (ccw(A,C,D) !== ccw(B,C,D)) && (ccw(A,B,C) !== ccw(A,B,D)); +} + +/* --------------------------- + Bridge crossing test + --------------------------- */ +function bridgeCrossesExisting(a, b) { + const p1 = {x: a.x, y: a.y}, p2 = {x: b.x, y: b.y}; + for (let br of state.bridges) { + // find islands for br + const A = state.islands.find(i => i.id === br.a); + const B = state.islands.find(i => i.id === br.b); + // if they share an endpoint, crossing is allowed (touching) + if (!A || !B) continue; + if (br.a === a.id || br.b === a.id || br.a === b.id || br.b === b.id) continue; + if (segmentsIntersect(p1, p2, {x:A.x,y:A.y}, {x:B.x,y:B.y})) return true; + } + return false; +} + +/* --------------------------- + Union-Find to check connectivity + --------------------------- */ +function UnionFind(nodes) { + const parent = {}, rank = {}; + nodes.forEach(n => { parent[n] = n; rank[n] = 0; }); + function find(x) { + if (parent[x] !== x) parent[x] = find(parent[x]); + return parent[x]; + } + function union(a,b) { + let ra = find(a), rb = find(b); + if (ra === rb) return; + if (rank[ra] < rank[rb]) parent[ra] = rb; + else if (rank[rb] < rank[ra]) parent[rb] = ra; + else { parent[rb] = ra; rank[ra]++; } + } + return { find, union }; +} +function checkAllConnected() { + const ids = state.islands.map(i=>i.id); + const uf = UnionFind(ids); + state.bridges.forEach(b => uf.union(b.a, b.b)); + const root = uf.find(ids[0]); + return ids.every(id => uf.find(id) === root); +} + +/* --------------------------- + Game logic + --------------------------- */ +function loadLevel(index) { + const lvl = LEVELS[index]; + state.levelIndex = index; + state.islands = JSON.parse(JSON.stringify(lvl.islands)); + state.obstacles = JSON.parse(JSON.stringify(lvl.obstacles)); + state.bridges = []; + state.undoStack = []; + state.selectedIsland = null; + state.moves = 0; + state.totalLength = 0; + state.startTime = now(); + state.elapsedBeforePause = 0; + state.paused = false; + state.running = true; + updateUI(); + draw(); +} + +function addBridge(aId, bId) { + // Validate and add bridge if valid + const a = state.islands.find(i=>i.id===aId), b = state.islands.find(i=>i.id===bId); + if (!a || !b || aId === bId) { playErr(); return false; } + + // Check duplicate + if (state.bridges.some(br => (br.a === aId && br.b === bId) || (br.a === bId && br.b === aId))) { playErr(); return false; } + + // line intersects obstacles? + if (state.obstacles.some(ob => segmentIntersectsRect({x:a.x,y:a.y},{x:b.x,y:b.y},ob))) { playErr(); return false; } + + // line crosses existing bridges? + if (bridgeCrossesExisting(a,b)) { playErr(); return false; } + + const len = distance(a,b); + state.bridges.push({ a: aId, b: bId, len }); + state.undoStack.push({ action: 'add', bridge: { a: aId, b: bId, len } }); + state.moves++; + state.totalLength += len; + playClick(); + updateUI(); + + if (checkAllConnected()) { + // victory + setTimeout(() => { + playWin(); + state.running = false; + showWin(); + }, 300); + } + return true; +} + +function undo() { + const last = state.undoStack.pop(); + if (!last) { playErr(); return; } + if (last.action === 'add') { + // remove the bridge + const idx = state.bridges.findIndex(b => b.a===last.bridge.a && b.b===last.bridge.b); + if (idx !== -1) { + state.bridges.splice(idx,1); + state.totalLength -= last.bridge.len; + state.moves++; + updateUI(); + playClick(); + } + } +} + +/* --------------------------- + UI & interactions + --------------------------- */ +function updateUI(){ + // Score = 10000 - (length*20) - moves*40 - time*2 (higher better) + const elapsed = state.paused ? state.elapsedBeforePause : (now() - state.startTime + state.elapsedBeforePause); + const seconds = Math.floor(elapsed/1000); + const timePenalty = seconds * 2; + const lengthPenalty = Math.round(state.totalLength) * 20; + const movesPenalty = state.moves * 40; + let score = Math.max(0, Math.round(10000 - lengthPenalty - movesPenalty - timePenalty)); + scoreEl.textContent = score; + timeEl.textContent = formatTime(seconds); + movesEl.textContent = state.moves; + lengthEl.textContent = Math.round(state.totalLength); +} + +function formatTime(sec) { + const m = Math.floor(sec/60), s = sec % 60; + return `${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`; +} + +function showWin(){ + // show overlay + sound + ctx.save(); + ctx.fillStyle = "rgba(0,0,0,0.46)"; + ctx.fillRect(0,0,canvas.width,canvas.height); + ctx.restore(); + setTimeout(()=> { + alert("๐ŸŽ‰ You connected all islands! Great job.\nScore: " + scoreEl.textContent); + },50); +} + +/* --------------------------- + Drawing + --------------------------- */ +function clearBoard(){ ctx.clearRect(0,0,canvas.width,canvas.height); } +function drawBackground() { + // subtle water patterns + const g = ctx.createLinearGradient(0,0,0,canvas.height); + g.addColorStop(0,'rgba(0,14,26,0.7)'); g.addColorStop(1,'rgba(2,30,44,0.85)'); + ctx.fillStyle = g; ctx.fillRect(0,0,canvas.width,canvas.height); +} + +function drawObstacles() { + ctx.save(); + for (let o of state.obstacles) { + ctx.fillStyle = "rgba(60,60,60,0.75)"; + ctx.fillRect(o.x, o.y, o.w, o.h); + // glow outline + ctx.strokeStyle = "rgba(255,80,80,0.06)"; + ctx.lineWidth = 6; ctx.strokeRect(o.x-3, o.y-3, o.w+6, o.h+6); + } + ctx.restore(); +} + +function drawBridges() { + for (let br of state.bridges) { + const A = state.islands.find(i=>i.id===br.a); + const B = state.islands.find(i=>i.id===br.b); + if (!A || !B) continue; + // draw base line + ctx.save(); + ctx.lineWidth = 6; + const grad = ctx.createLinearGradient(A.x,A.y,B.x,B.y); + grad.addColorStop(0, 'rgba(255,255,255,0.06)'); + grad.addColorStop(0.5, 'rgba(0,229,168,0.9)'); + grad.addColorStop(1, 'rgba(255,255,255,0.06)'); + ctx.strokeStyle = grad; + ctx.beginPath(); ctx.moveTo(A.x,A.y); ctx.lineTo(B.x,B.y); ctx.stroke(); + + // decorative bulbs (glowing dots along bridge) + const count = Math.max(3, Math.floor(br.len/60)); + for (let i=0;i<=count;i++){ + const t = i / count; + const x = A.x + (B.x-A.x)*t; + const y = A.y + (B.y-A.y)*t; + // glow + ctx.beginPath(); + ctx.fillStyle = `rgba(0,229,168, ${0.18 + 0.6*Math.abs(Math.sin((t+Date.now()/1000)*2))})`; + ctx.shadowBlur = 12; ctx.shadowColor = 'rgba(0,229,168,0.8)'; + ctx.arc(x,y,4,0,Math.PI*2); + ctx.fill(); + ctx.shadowBlur = 0; + } + ctx.restore(); + } +} + +function drawIslands() { + for (let isl of state.islands) { + // island shadow + ctx.save(); + ctx.beginPath(); + ctx.shadowBlur = 14; ctx.shadowColor = 'rgba(0,0,0,0.7)'; + ctx.fillStyle = '#0e2830'; + ctx.arc(isl.x, isl.y, isl.r+8, 0, Math.PI*2); + ctx.fill(); + ctx.restore(); + + // gradient island + const g = ctx.createRadialGradient(isl.x-isl.r/3, isl.y-isl.r/3, 2, isl.x, isl.y, isl.r+4); + g.addColorStop(0, '#ffdca8'); g.addColorStop(1, '#ffb66b'); + ctx.fillStyle = g; + ctx.beginPath(); + ctx.arc(isl.x, isl.y, isl.r, 0, Math.PI*2); + ctx.fill(); + + // rim + ctx.strokeStyle = 'rgba(0,0,0,0.18)'; ctx.lineWidth = 2; ctx.stroke(); + + // id + ctx.fillStyle = '#002219'; + ctx.font = `${Math.max(12, isl.r-4)}px system-ui`; + ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; + ctx.fillText(isl.id, isl.x, isl.y); + } +} + +function drawSelection() { + if (state.selectedIsland) { + ctx.save(); + ctx.beginPath(); + ctx.strokeStyle = 'rgba(0,229,168,0.9)'; + ctx.lineWidth = 3; + ctx.setLineDash([6,6]); + ctx.arc(state.selectedIsland.x, state.selectedIsland.y, state.selectedIsland.r+10, 0, Math.PI*2); + ctx.stroke(); + ctx.restore(); + } +} + +function drawFloatingGhost(mousePos) { + if (!state.selectedIsland || !mousePos) return; + const A = state.selectedIsland; + const B = mousePos; + // ghost line + ctx.save(); + ctx.globalAlpha = 0.6; + ctx.strokeStyle = 'rgba(0,229,168,0.5)'; + ctx.lineWidth = 4; + ctx.setLineDash([8,6]); + ctx.beginPath(); ctx.moveTo(A.x, A.y); ctx.lineTo(B.x, B.y); ctx.stroke(); + ctx.restore(); +} + +/* Main draw */ +let lastMouse = null; +function draw(){ + clearBoard(); + drawBackground(); + drawObstacles(); + drawBridges(); + drawIslands(); + drawSelection(); + drawFloatingGhost(lastMouse); + + // request anim frame only when running or to animate glow + if (state.running) requestAnimationFrame(draw); +} + +/* --------------------------- + Event handlers + --------------------------- */ +canvas.addEventListener('mousemove', (e) => { + const rect = canvas.getBoundingClientRect(); + lastMouse = { x: e.clientX - rect.left, y: e.clientY - rect.top }; +}); + +canvas.addEventListener('mouseleave', () => { lastMouse = null; }); + +canvas.addEventListener('click', (e) => { + if (!state.running || state.paused) return; + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left, y = e.clientY - rect.top; + // find island under click + const isl = state.islands.find(i => distance(i, {x,y}) <= i.r + 6); + if (!isl) { + // clicked empty water -> deselect + state.selectedIsland = null; draw(); + return; + } + if (!state.selectedIsland) { + state.selectedIsland = isl; playClick(); draw(); + return; + } + // attempt to add bridge + if (state.selectedIsland.id === isl.id) { + // deselect + state.selectedIsland = null; draw(); + return; + } + const added = addBridge(state.selectedIsland.id, isl.id); + if (added) { + playSuccess(); + } + state.selectedIsland = null; + draw(); +}); + +/* --------------------------- + Input buttons + --------------------------- */ +pauseBtn.addEventListener('click', () => { + if (!state.running) return; + if (!state.paused) { + state.paused = true; + state.elapsedBeforePause += (now() - state.startTime); + pauseBtn.textContent = 'Resume'; + } else { + state.paused = false; + state.startTime = now(); + pauseBtn.textContent = 'Pause'; + } +}); + +undoBtn.addEventListener('click', () => { undo(); draw(); updateUI(); }); + +restartBtn.addEventListener('click', () => { + if (!confirm('Restart this level?')) return; + loadLevel(state.levelIndex); +}); + +soundToggle.addEventListener('change', (e) => { + state.soundEnabled = e.target.checked; +}); + +hintBtn.addEventListener('click', () => { + // rough hint: show a minimal spanning tree suggestion (Prim's) โ€” highlight one suggested bridge + const suggestion = computeMSTSuggestion(); + if (!suggestion) { playErr(); alert('No hint available'); return; } + highlightSuggestion(suggestion); +}); + +/* Save score */ +saveBtn.addEventListener('click', () => { + const score = parseInt(scoreEl.textContent,10); + const name = prompt('Save your score. Enter name:','Player') || 'Player'; + const entry = { name, score, date: new Date().toISOString(), level: LEVELS[state.levelIndex].name }; + const scores = JSON.parse(localStorage.getItem('islandScores') || '[]'); + scores.push(entry); + localStorage.setItem('islandScores', JSON.stringify(scores)); + alert('Saved!'); +}); + +/* level select */ +function populateLevelSelect() { + levelSelect.innerHTML = ''; + LEVELS.forEach((lv, idx) => { + const op = document.createElement('option'); op.value = idx; op.textContent = lv.name; + levelSelect.appendChild(op); + }); + levelSelect.value = state.levelIndex; +} +levelSelect.addEventListener('change', () => { + loadLevel(parseInt(levelSelect.value,10)); +}); + +/* --------------------------- + Hints / MST suggestion (Prim's) + --------------------------- */ +function computeMSTSuggestion() { + const ids = state.islands.map(i=>i.id); + if (ids.length <= 1) return null; + // Prim's algorithm on complete graph with weights distance + const inMST = new Set(); + const edges = []; + const map = {}; // map id->island + state.islands.forEach(i=>map[i.id]=i); + const start = ids[0]; inMST.add(start); + + while (inMST.size < ids.length) { + let best = null; + for (let a of inMST) { + for (let b of ids) { + if (inMST.has(b)) continue; + const A = map[a], B = map[b]; + // skip if edge invalid (intersects obstacle or crosses existing bridge) + const blocked = state.obstacles.some(ob => segmentIntersectsRect({x:A.x,y:A.y},{x:B.x,y:B.y},ob)) + || bridgeCrossesExisting(A,B); + if (blocked) continue; + const w = distance(A,B); + if (!best || w < best.w) best = { a:A.id, b:B.id, w, A, B }; + } + } + if (!best) break; + edges.push(best); + inMST.add(best.b); + } + // pick first suggested edge that is not already built + for (let e of edges) { + if (!state.bridges.some(b => (b.a===e.a && b.b===e.b)||(b.a===e.b && b.b===e.a))) { + return { a:e.A, b:e.B }; + } + } + return null; +} +function highlightSuggestion(sug) { + playClick(); + // flash the suggested bridge a few times + let flashes = 0; + const interval = setInterval(() => { + draw(); + ctx.save(); + ctx.lineWidth = 8; + ctx.strokeStyle = `rgba(255,235,190,${0.12 + 0.3*Math.abs(Math.sin(flashes))})`; + ctx.beginPath(); ctx.moveTo(sug.a.x,sug.a.y); ctx.lineTo(sug.b.x,sug.b.y); ctx.stroke(); + ctx.restore(); + flashes++; + if (flashes > 8) { clearInterval(interval); draw(); } + }, 120); +} + +/* --------------------------- + Sounds + --------------------------- */ +function playClick(){ if (state.soundEnabled) audio.click.play().catch(()=>{}); } +function playSuccess(){ if (state.soundEnabled) audio.success.play().catch(()=>{}); } +function playErr(){ if (state.soundEnabled) audio.error.play().catch(()=>{}); } +function playWin(){ if (state.soundEnabled) audio.win.play().catch(()=>{}); } + +/* --------------------------- + Animation & game loop for time updates + --------------------------- */ +function tick() { + if (!state.paused && state.running) { + updateUI(); + } + setTimeout(tick, 300); +} + +/* --------------------------- + Initialization + --------------------------- */ +function resizeCanvas() { + // keep internal drawing buffer 1:1 with element size for crispness + const style = getComputedStyle(canvas); + const width = parseInt(style.width); + const height = parseInt(style.height); + // set logical size + const ratio = window.devicePixelRatio || 1; + canvas.width = Math.floor(width * ratio); + canvas.height = Math.floor(height * ratio); + canvas.style.width = width + 'px'; + canvas.style.height = height + 'px'; + ctx.setTransform(ratio,0,0,ratio,0,0); +} +function attachUI() { + // read sound default from localStorage + const s = localStorage.getItem('islandSound'); + if (s !== null) { state.soundEnabled = s === '1'; soundToggle.checked = state.soundEnabled; } + soundToggle.addEventListener('change', () => { + state.soundEnabled = soundToggle.checked; + localStorage.setItem('islandSound', state.soundEnabled ? '1' : '0'); + }); +} +function start() { + // set canvas CSS size to match markup + canvas.style.width = '720px'; + canvas.style.height = '640px'; + resizeCanvas(); + window.addEventListener('resize', resizeCanvas); + populateLevelSelect(); + attachUI(); + loadLevel(0); + tick(); + draw(); +} +start(); + +/* make game accessible if loaded in a hub where width differs */ +window.addEventListener('load', () => { + // ensure level select shows current index + levelSelect.value = state.levelIndex; +}); diff --git a/games/island-connector/style.css b/games/island-connector/style.css new file mode 100644 index 00000000..2e2918ec --- /dev/null +++ b/games/island-connector/style.css @@ -0,0 +1,97 @@ +:root{ + --bg:#0b1220; + --card:#0f1724; + --accent:#00e5a8; + --muted:#9aa7b2; + --glass: rgba(255,255,255,0.04); + --glow: 0 8px 30px rgba(0,229,168,0.12), 0 2px 8px rgba(0,229,168,0.06); + --panel-pad:12px; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; +} + +*{box-sizing:border-box;margin:0;padding:0} +html,body,#board{height:100%} +body{ + min-height:100vh; + background: linear-gradient(180deg,#051021 0%, #081827 40%, #071a2a 100%); + color:#dfe9ef; + display:flex; + align-items:stretch; +} + +.app{ + max-width:1200px; + margin:auto; + width:100%; + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.02)); + border-radius:12px; + padding:14px; + box-shadow: 0 12px 40px rgba(2,6,23,0.7); + display:flex; + flex-direction:column; + gap:12px; + width:96%; +} + +/* Top bar */ +.topbar{ + display:flex; + justify-content:space-between; + align-items:center; + padding:10px; +} +.title{display:flex;gap:12px;align-items:center} +.title .icon{font-size:32px} +.title h1{font-size:20px;margin-bottom:2px} +.title small{display:block;color:var(--muted);font-size:12px} +.controls{display:flex;gap:8px;align-items:center} +.control-btn{background:var(--glass);border:1px solid rgba(255,255,255,0.04);color:var(--muted);padding:8px 10px;border-radius:8px;cursor:pointer} +.control-btn:hover{color:var(--accent);box-shadow:var(--glow);border-color:rgba(0,229,168,0.12)} +.control-select{padding:8px;border-radius:8px;background:transparent;border:1px solid rgba(255,255,255,0.04);color:var(--muted)} + +/* Main area */ +.game-area{ + display:flex; + gap:12px; + align-items:flex-start; +} +canvas#board{ + background-image: radial-gradient(circle at 10% 10%, rgba(0,229,168,0.03), transparent 40%), linear-gradient(0deg, rgba(255,255,255,0.01), transparent); + border-radius:10px; + flex:1 1 0; + width:720px; + height:640px; + box-shadow: 0 12px 30px rgba(0,0,0,0.7); + border:1px solid rgba(255,255,255,0.03); + display:block; +} + +/* Sidebar */ +.sidebar{ + width:260px; + display:flex; + flex-direction:column; + gap:10px; +} +.panel{ + background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + padding:var(--panel-pad); + border-radius:10px; + border:1px solid rgba(255,255,255,0.03); + box-shadow: 0 8px 20px rgba(0,0,0,0.45); +} +.stat{display:flex;flex-direction:column;gap:6px} +.stat div:first-child{color:var(--muted);font-size:12px} +.stat div:last-child{font-size:18px;color:#fff} + +.info ul{margin-top:8px;list-style:disc;padding-left:18px;color:var(--muted);font-size:13px} +.actions{display:flex;flex-direction:column;gap:8px} +.actions button{padding:10px;border-radius:8px;background:linear-gradient(90deg,var(--accent),#00bfa5);color:#002219;border:none;cursor:pointer} +.actions .open-new{display:block;text-align:center;padding:8px;border-radius:8px;background:transparent;border:1px solid rgba(255,255,255,0.04);color:var(--muted);text-decoration:none} + +/* small screens */ +@media (max-width:1000px){ + .game-area{flex-direction:column} + canvas#board{width:100%;height:420px} + .sidebar{width:100%} +} diff --git a/games/island-survival/index.html b/games/island-survival/index.html new file mode 100644 index 00000000..2beefc66 --- /dev/null +++ b/games/island-survival/index.html @@ -0,0 +1,36 @@ + + + + + + Island Survival Text Game + + + +
    +

    Island Survival ๐Ÿ๏ธ

    + +
    +

    Day: 1

    +

    Health: 100

    +

    Hunger: 50

    +

    Wood: 0

    +
    + +
    + +
    +

    Welcome! You've washed ashore on a deserted island. Survival depends on your choices.

    +
    + +
    + + + +
    + + +
    + + + \ No newline at end of file diff --git a/games/island-survival/script.js b/games/island-survival/script.js new file mode 100644 index 00000000..8f244a86 --- /dev/null +++ b/games/island-survival/script.js @@ -0,0 +1,157 @@ +// --- Wait for the entire HTML document to load before running any script --- +document.addEventListener('DOMContentLoaded', () => { + + // --- Game State and Constants --- + let player = {}; + const STAT_LIMIT = 100; + const HEALTH_DECAY_RATE = 5; + const HUNGER_DECAY_RATE = 5; + + // --- DOM References (FIXED) --- + // Ensure these IDs match the IDs in your index.html + const healthDisplay = document.getElementById('health-display'); + const hungerDisplay = document.getElementById('hunger-display'); + const woodDisplay = document.getElementById('wood-display'); + const dayDisplay = document.getElementById('day-display'); + const logArea = document.getElementById('log-area'); + + // Individual button references for adding listeners later + const gatherWoodBtn = document.getElementById('gather-wood-btn'); + const searchFoodBtn = document.getElementById('search-food-btn'); + const restBtn = document.getElementById('rest-btn'); + const restartBtn = document.getElementById('restart-btn'); + + // Array of action buttons for easy iteration (like disabling) + const actionButtons = [gatherWoodBtn, searchFoodBtn, restBtn]; + + + // --- Initialization --- + function initializeGame() { + player = { + health: STAT_LIMIT, + hunger: 50, + wood: 0, + day: 1, + log: [] + }; + + // Enable buttons + actionButtons.forEach(btn => btn.disabled = false); + restartBtn.style.display = 'none'; + + player.log.push("Welcome! You awaken on Day 1. Find food and wood to survive."); + renderUI(); + } + + // --- Core UI Update --- + function renderUI() { + // Safety check, in case game is initialized before all elements load + if (healthDisplay) healthDisplay.textContent = player.health; + if (hungerDisplay) hungerDisplay.textContent = player.hunger; + if (woodDisplay) woodDisplay.textContent = player.wood; + if (dayDisplay) dayDisplay.textContent = player.day; + + // Visual feedback for stats + if (healthDisplay) healthDisplay.className = player.health < 25 ? 'stat-danger' : player.health < 50 ? 'stat-warning' : 'stat-good'; + if (hungerDisplay) hungerDisplay.className = player.hunger < 25 ? 'stat-danger' : player.hunger < 50 ? 'stat-warning' : 'stat-good'; + + // Update log display (show last 5 messages, reversed for latest first) + if (logArea) logArea.innerHTML = player.log.slice(-5).reverse().join('
    '); + + // Wood specific check for resting + if (restBtn) { + restBtn.disabled = player.wood < 5 && player.health < STAT_LIMIT; + } + } + + // --- Game Loop (Turn Advance) --- + function nextDay(message) { + if (player.health <= 0) return; // Prevent action if already dead + + player.day++; + player.log.push(message); + + // 1. Passive Decay + player.hunger -= HUNGER_DECAY_RATE; + + // 2. Hunger Penalty + if (player.hunger <= 0) { + player.health -= HEALTH_DECAY_RATE; + player.log.push("โš ๏ธ Starving! Health decreased."); + } + + // 3. Clamp Stats (Keep them in bounds) + player.health = Math.min(Math.max(0, player.health), STAT_LIMIT); + player.hunger = Math.min(Math.max(0, player.hunger), STAT_LIMIT); + + // 4. Check Game Over + if (player.health <= 0) { + player.log.push(`๐Ÿ’€ GAME OVER: You survived ${player.day} days.`); + disableActions(); + } + + renderUI(); + } + + function disableActions() { + actionButtons.forEach(btn => { + if (btn) btn.disabled = true; + }); + if (restartBtn) restartBtn.style.display = 'block'; + } + + // --- Action Handlers --- + + function gatherWood() { + const woodFound = Math.floor(Math.random() * 3) + 1; // Finds 1 to 3 wood + const hungerCost = 8; + + player.wood += woodFound; + player.hunger -= hungerCost; + + nextDay(`โœ… You spent the day gathering wood and found ${woodFound} logs.`); + } + + function searchFood() { + const foodFound = Math.random(); + const hungerCost = 12; + let message; + + if (foodFound < 0.3) { // 30% failure + message = "โŒ You searched all day but only found a few rotten berries. No food gain."; + } else { + const hungerHealed = Math.floor(Math.random() * 20) + 10; // Heals 10 to 30 hunger + player.hunger += hungerHealed; + message = `๐Ÿ– Success! You found enough food to restore ${hungerHealed} hunger.`; + } + + player.hunger -= hungerCost; // Cost applies regardless of success + nextDay(message); + } + + function rest() { + const healthGained = 10; + const woodCost = 5; + + if (player.wood < woodCost) { + player.log.push("โ›” You need at least 5 wood to build a proper shelter and rest."); + renderUI(); // Render without advancing day + return; + } + + player.health += healthGained; + player.wood -= woodCost; + + nextDay(`๐Ÿ›Œ You built a safe shelter and rested. Health restored by ${healthGained}.`); + } + + + // --- Event Listeners and Setup (FIXED) --- + if (gatherWoodBtn) gatherWoodBtn.addEventListener('click', gatherWood); + if (searchFoodBtn) searchFoodBtn.addEventListener('click', searchFood); + if (restBtn) restBtn.addEventListener('click', rest); + if (restartBtn) restartBtn.addEventListener('click', initializeGame); + + // Start the game when the script loads (inside the DOMContentLoaded block) + initializeGame(); +}); \ No newline at end of file diff --git a/games/island-survival/style.css b/games/island-survival/style.css new file mode 100644 index 00000000..82106c87 --- /dev/null +++ b/games/island-survival/style.css @@ -0,0 +1,86 @@ +body { + font-family: 'Georgia', serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f4f7f6; + color: #333; +} + +.game-container { + background-color: #ffffff; + padding: 30px; + border-radius: 10px; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2); + max-width: 600px; + width: 90%; + text-align: center; +} + +h1 { + color: #008080; /* Teal color for survival theme */ +} + +.stats-grid { + display: flex; + justify-content: space-around; + padding: 10px 0; + border: 1px solid #ccc; + border-radius: 5px; + margin-bottom: 20px; +} + +.stats-grid p { + margin: 0; + font-weight: bold; +} + +/* Stat color feedback */ +.stat-good { color: green; } +.stat-warning { color: orange; } +.stat-danger { color: red; } + +.log-area { + background-color: #e9ecef; + border: 1px solid #ddd; + min-height: 150px; + max-height: 150px; + overflow-y: auto; + text-align: left; + padding: 10px; + margin-bottom: 20px; + border-radius: 5px; + font-size: 0.9em; + line-height: 1.4; +} + +hr { border-color: #eee; } + +.actions button { + padding: 10px 15px; + margin: 5px; + background-color: #008080; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; + font-weight: bold; +} + +.actions button:hover:not(:disabled) { + background-color: #006666; +} + +#restart-btn { + background-color: #dc3545; + margin-top: 15px; + width: 100%; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} \ No newline at end of file diff --git a/games/jet-glide/index.html b/games/jet-glide/index.html new file mode 100644 index 00000000..e028ca34 --- /dev/null +++ b/games/jet-glide/index.html @@ -0,0 +1,77 @@ + + + + + + Jet Glide โ€” Mini JS Games Hub + + + + +
    +
    + โ† Back to Hub +

    Jet Glide

    +
    + +
    +
    + +
    +
    +
    +
    Score: 0
    +
    High: 0
    +
    + +
    + +
    +
    +

    Jet Glide

    +

    Control the jet with arrow keys, mouse or touch. Avoid floating mines. Survive as long as possible.

    +
    + + + + +
    +
    Tip: use โ†‘ / โ†“ or drag on mobile. Score increases over time. Mines speed up!
    +
    +
    +
    + +
    + + + + +
    +
    + +
    +

    How to Play

    +
      +
    • Control the jet using arrow keys, mouse (click+drag), or the on-screen buttons.
    • +
    • Dodge floating mines โ€” if you collide, the game ends.
    • +
    • The longer you survive, the faster and more numerous the mines become.
    • +
    + +

    Features

    +
      +
    • Neon glow visuals
    • +
    • Start / Pause / Restart
    • +
    • Local high-score persistence
    • +
    • Sound effects (online) with toggle
    • +
    +
    +
    + +
    + Made with โค๏ธ โ€ข Canvas โ€ข JS โ€ข No build tools +
    +
    + + + + diff --git a/games/jet-glide/script.js b/games/jet-glide/script.js new file mode 100644 index 00000000..77b6ec49 --- /dev/null +++ b/games/jet-glide/script.js @@ -0,0 +1,540 @@ +/* Jet Glide โ€” script.js + Uses Canvas API. Controls: Arrow keys, mouse drag/touch, on-screen buttons. + Sound files: hosted at actions.google.com/sounds (public). +*/ + +(() => { + // ---- Config ---- + const canvas = document.getElementById('gameCanvas'); + const ctx = canvas.getContext('2d', { alpha: false }); + const startBtn = document.getElementById('startBtn'); + const resumeBtn = document.getElementById('resumeBtn'); + const pauseBtn = document.getElementById('pauseBtn'); + const restartBtn = document.getElementById('restartBtn'); + const overlay = document.getElementById('overlay'); + const overlayTitle = document.getElementById('overlay-title'); + const overlayText = document.getElementById('overlay-text'); + const scoreEl = document.getElementById('score'); + const highEl = document.getElementById('highscore'); + const soundToggle = document.getElementById('sound-toggle'); + + // On-screen buttons + const upBtn = document.getElementById('upBtn'); + const downBtn = document.getElementById('downBtn'); + const leftBtn = document.getElementById('leftBtn'); + const rightBtn = document.getElementById('rightBtn'); + + // full-resolution canvas scaling + function resizeCanvas() { + const ratio = devicePixelRatio || 1; + const w = canvas.clientWidth; + const h = canvas.clientHeight; + canvas.width = Math.floor(w * ratio); + canvas.height = Math.floor(h * ratio); + ctx.setTransform(ratio, 0, 0, ratio, 0, 0); + } + // init + resizeCanvas(); + window.addEventListener('resize', () => { resizeCanvas(); drawStaticBackground(); }); + + // Sounds (publicly hosted) + const sounds = { + thrust: new Audio('https://actions.google.com/sounds/v1/foley/wood_thud.ogg'), // subtle thrust + boom: new Audio('https://actions.google.com/sounds/v1/explosions/explosion_large.ogg'), + ding: new Audio('https://actions.google.com/sounds/v1/cartoon/clang_and_wobble.ogg'), + hit: new Audio('https://actions.google.com/sounds/v1/cartoon/metal_clang.ogg') + }; + // default sound on + let soundOn = true; + soundToggle.addEventListener('click', () => { + soundOn = !soundOn; + soundToggle.setAttribute('aria-pressed', String(soundOn)); + soundToggle.textContent = soundOn ? '๐Ÿ”Š' : '๐Ÿ”ˆ'; + }); + + // Utility play + function playSound(s) { if (!soundOn) return; try{ s.currentTime = 0; s.play(); }catch(e){} } + + // Game vars + const state = { + running: false, + paused: false, + time: 0, + speedMultiplier: 1, + spawnTimer: 0, + difficultyTimer: 0, + score: 0, + highscore: Number(localStorage.getItem('jetGlideHighScore') || 0) + }; + highEl.textContent = state.highscore; + + // Jet + const jet = { + x: 140, + y: 220, + w: 68, + h: 28, + vy: 0, + speed: 3.2, + color: '#00f0ff', + thrusting: false + }; + + // Mines (obstacles) + const mines = []; + + // Background particles for parallax (glowing bulbs in a line as requested) + const bulbs = []; + function initBulbs() { + bulbs.length = 0; + const count = 26; + for (let i = 0; i < count; i++) { + bulbs.push({ + x: (i / (count - 1)) * canvas.clientWidth, + y: 40 + (i % 2 === 0 ? 6 : -6), + r: 4 + Math.random() * 6, + glow: 0.4 + Math.random() * 0.8, + hue: 180 + Math.random() * 120, + }); + } + } + initBulbs(); + + // Resize helper calls + function setup() { + resizeCanvas(); + initBulbs(); + drawStaticBackground(); + } + setup(); + + // Draw static background (gradient + bulbs) + function drawStaticBackground() { + const w = canvas.clientWidth; + const h = canvas.clientHeight; + // gradient sky + const g = ctx.createLinearGradient(0, 0, 0, h); + g.addColorStop(0, '#051023'); + g.addColorStop(0.5, '#02141f'); + g.addColorStop(1, '#02040a'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, w, h); + + // subtle stars / noise + for (let i = 0; i < 70; i++) { + const x = Math.random() * w; + const y = Math.random() * h * 0.6; + ctx.fillStyle = `rgba(255,255,255,${Math.random() * 0.06})`; + ctx.fillRect(x, y, 1, 1); + } + + // bulbs line at top + bulbs.forEach(b => { + const grd = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, b.r * 6); + grd.addColorStop(0, `hsla(${b.hue},100%,65%,${0.12 + b.glow * 0.12})`); + grd.addColorStop(0.3, `hsla(${b.hue},100%,55%,${0.06 + b.glow * 0.06})`); + grd.addColorStop(1, 'rgba(2,6,10,0)'); + ctx.fillStyle = grd; + ctx.beginPath(); + ctx.arc(b.x, b.y, b.r * 6, 0, Math.PI * 2); + ctx.fill(); + // central small bulb + ctx.fillStyle = `hsla(${b.hue},100%,75%,${0.9 * b.glow})`; + ctx.beginPath(); + ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); + ctx.fill(); + }); + } + + // Draw Jet (neon stylized) + function drawJet() { + const { x, y, w, h } = jet; + // main body + ctx.save(); + ctx.translate(x, y); + // glow + ctx.shadowBlur = 22; + ctx.shadowColor = '#00f0ff'; + ctx.fillStyle = '#022a34'; + ctx.strokeStyle = '#00f0ff'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.quadraticCurveTo(18, -18, 52, -8); + ctx.lineTo(68, -8); + ctx.quadraticCurveTo(72, -6, 70, 2); + ctx.lineTo(50, 10); + ctx.quadraticCurveTo(18, 18, 0, 10); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + // cockpit glow + ctx.shadowBlur = 22; + ctx.shadowColor = '#7cff6b'; + ctx.fillStyle = 'rgba(124,255,107,0.08)'; + ctx.beginPath(); + ctx.ellipse(30, -2, 13, 7, 0, 0, Math.PI * 2); + ctx.fill(); + + // exhaust (if thrusting) + if (jet.thrusting) { + ctx.shadowBlur = 28; + ctx.shadowColor = 'rgba(255,91,211,0.35)'; + ctx.fillStyle = 'rgba(255,91,211,0.3)'; + ctx.beginPath(); + ctx.moveTo(-6, 2); + ctx.lineTo(-18 + Math.sin(Date.now() / 120) * 4, -6); + ctx.lineTo(-18 + Math.sin(Date.now() / 130) * 4, 12); + ctx.closePath(); + ctx.fill(); + } + + // outline + ctx.shadowBlur = 0; + ctx.restore(); + } + + // Draw mine + function drawMine(m) { + ctx.save(); + ctx.translate(m.x, m.y); + ctx.shadowBlur = 18; + ctx.shadowColor = 'rgba(255,53,90,0.22)'; + // core + ctx.fillStyle = '#ff2f6b'; + ctx.beginPath(); + ctx.arc(0, 0, m.r, 0, Math.PI * 2); + ctx.fill(); + // spikes + for (let i = 0; i < 8; i++) { + const ang = (i / 8) * Math.PI * 2 + (Date.now() / 3000) * (m.spin || 0.4); + const sx = Math.cos(ang) * (m.r + 6); + const sy = Math.sin(ang) * (m.r + 6); + ctx.fillStyle = '#ffd1e8'; + ctx.fillRect(sx - 3, sy - 3, 6, 6); + } + ctx.restore(); + } + + // spawn mine + function spawnMine() { + const w = canvas.clientWidth; + const h = canvas.clientHeight; + const r = 12 + Math.random() * 18; + const y = r + Math.random() * (h - r * 2); + const speed = 2.2 + state.speedMultiplier * (1 + Math.random() * 1.6); + mines.push({ x: w + 60, y, r, vx: -speed, spin: 0.4 + Math.random() * 0.8 }); + } + + // collision detection (circle-rect approx) + function hitTestRectCircle(rx, ry, rw, rh, cx, cy, cr) { + const nearestX = Math.max(rx, Math.min(cx, rx + rw)); + const nearestY = Math.max(ry, Math.min(cy, ry + rh)); + const dx = cx - nearestX; + const dy = cy - nearestY; + return (dx * dx + dy * dy) < cr * cr; + } + + // reset game + function resetGame() { + state.running = false; + state.paused = false; + state.time = 0; + state.spawnTimer = 0; + state.difficultyTimer = 0; + state.score = 0; + state.speedMultiplier = 1; + mines.length = 0; + jet.x = 140; + jet.y = Math.max(80, canvas.clientHeight / 2 - 10); + jet.vy = 0; + jet.thrusting = false; + scoreEl.textContent = '0'; + overlay.style.display = 'flex'; + overlayTitle.textContent = 'Jet Glide'; + overlayText.textContent = 'Press Start to play. Use arrows or drag to move. Avoid mines!'; + resumeBtn.hidden = true; + pauseBtn.hidden = true; + } + + // start / resume / pause handlers + function startGame() { + if (state.running && !state.paused) return; + if (!state.running) { + state.running = true; + overlay.style.display = 'none'; + state.time = 0; + mines.length = 0; + state.score = 0; + state.speedMultiplier = 1; + playSound(sounds.ding); + lastTS = performance.now(); + requestId = requestAnimationFrame(gameLoop); + pauseBtn.hidden = false; + resumeBtn.hidden = true; + startBtn.hidden = true; + } else if (state.paused) { + state.paused = false; + overlay.style.display = 'none'; + lastTS = performance.now(); + requestId = requestAnimationFrame(gameLoop); + pauseBtn.hidden = false; + resumeBtn.hidden = true; + } + } + + function pauseGame() { + if (!state.running || state.paused) return; + state.paused = true; + overlay.style.display = 'flex'; + overlayTitle.textContent = 'Paused'; + overlayText.textContent = 'Resume when you are ready.'; + resumeBtn.hidden = false; + pauseBtn.hidden = true; + cancelAnimationFrame(requestId); + } + + // game over + function gameOver() { + state.running = false; + cancelAnimationFrame(requestId); + overlay.style.display = 'flex'; + overlayTitle.textContent = 'Game Over'; + overlayText.textContent = `Final score: ${Math.floor(state.score)} โ€” Try again!`; + resumeBtn.hidden = true; + pauseBtn.hidden = true; + startBtn.hidden = true; + playSound(sounds.boom); + // highscore + if (Math.floor(state.score) > state.highscore) { + state.highscore = Math.floor(state.score); + localStorage.setItem('jetGlideHighScore', state.highscore); + highEl.textContent = state.highscore; + } + } + + // game loop + let lastTS = 0; + let requestId = null; + function gameLoop(ts) { + if (!lastTS) lastTS = ts; + const dt = (ts - lastTS) / 1000; // seconds + lastTS = ts; + if (!state.running || state.paused) return; + + // update timers and spawn logic + state.time += dt; + state.spawnTimer += dt; + state.difficultyTimer += dt; + + // increase difficulty slowly + if (state.difficultyTimer > 6) { + state.difficultyTimer = 0; + state.speedMultiplier += 0.14; // increase mine speed + } + + // spawn frequency depends on speed multiplier + const spawnEvery = Math.max(0.6, 1.6 - (state.speedMultiplier * 0.12)); + if (state.spawnTimer > spawnEvery) { + spawnMine(); + state.spawnTimer = 0; + } + + // jet physics: simple smooth motion towards target velocity + jet.vy += (jet.thrusting ? -0.2 : 0.18); // thrust vs gravity + jet.vy = Math.max(-8, Math.min(8, jet.vy)); + jet.y += jet.vy * (1 + state.speedMultiplier * 0.04); + + // clamp + const minY = 18; + const maxY = canvas.clientHeight - 18; + if (jet.y < minY) { jet.y = minY; jet.vy = 0; } + if (jet.y > maxY) { jet.y = maxY; jet.vy = 0; } + + // update mines + for (let i = mines.length - 1; i >= 0; i--) { + const m = mines[i]; + m.x += m.vx * (1 + (state.speedMultiplier - 1) * 0.8); + // remove offscreen + if (m.x < -80) mines.splice(i, 1); + } + + // score increases with time and multiplier + state.score += (1 * (1 + state.speedMultiplier * 0.6)) * dt * 10; + scoreEl.textContent = Math.floor(state.score); + + // check collisions (use a few sample points on jet) + const jetRect = { x: jet.x - 10, y: jet.y - 20, w: 72, h: 36 }; + for (let i = 0; i < mines.length; i++) { + const m = mines[i]; + if (hitTestRectCircle(jetRect.x, jetRect.y, jetRect.w, jetRect.h, m.x, m.y, m.r)) { + // hit + playSound(sounds.hit); + gameOver(); + return; + } + } + + // draw + drawStaticBackground(); + + // draw mines + mines.forEach(drawMine); + + // draw jet at its position + ctx.save(); + // slightly tilt jet while moving + ctx.translate(jet.x, jet.y); + ctx.rotate(Math.max(-0.35, Math.min(0.35, jet.vy * 0.035))); + // draw neon jet using path + ctx.shadowBlur = 18; + ctx.shadowColor = 'rgba(0,240,255,0.18)'; + ctx.fillStyle = '#08131a'; + ctx.strokeStyle = '#00f0ff'; + ctx.lineWidth = 1.8; + ctx.beginPath(); + ctx.moveTo(-10, 8); + ctx.quadraticCurveTo(10, -18, 40, -6); + ctx.lineTo(58, -6); + ctx.quadraticCurveTo(68, -4, 66, 6); + ctx.lineTo(38, 12); + ctx.quadraticCurveTo(12, 18, -10, 12); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + // cockpit highlight + ctx.shadowBlur = 12; + ctx.shadowColor = 'rgba(124,255,107,0.14)'; + ctx.fillStyle = 'rgba(124,255,107,0.06)'; + ctx.beginPath(); + ctx.ellipse(22, -1, 12, 6, 0, 0, Math.PI*2); + ctx.fill(); + ctx.restore(); + + // spawn visual hint: faint glow ahead + ctx.save(); + ctx.globalCompositeOperation = 'lighter'; + ctx.fillStyle = 'rgba(0,240,255,0.02)'; + ctx.fillRect(jet.x + 48, 0, 200, canvas.clientHeight); + ctx.restore(); + + // request next frame + requestId = requestAnimationFrame(gameLoop); + } + + // input handling + let pointerDown = false; + let pointerId = null; + + function handlePointerMove(clientY) { + // map clientY to canvas coordinates + const rect = canvas.getBoundingClientRect(); + const y = (clientY - rect.top); + // smooth move: aim towards y + const diff = y - jet.y; + jet.vy += diff * 0.02; + // small thrust visual + jet.thrusting = diff < -3; + } + + // mouse/touch listeners + canvas.addEventListener('pointerdown', (e) => { + pointerDown = true; pointerId = e.pointerId; + handlePointerMove(e.clientY); + canvas.setPointerCapture(pointerId); + }); + canvas.addEventListener('pointermove', (e) => { + if (!pointerDown || e.pointerId !== pointerId) return; + handlePointerMove(e.clientY); + }); + canvas.addEventListener('pointerup', (e) => { + if (e.pointerId === pointerId) { + pointerDown = false; pointerId = null; + canvas.releasePointerCapture(e.pointerId); + jet.thrusting = false; + } + }); + + // keyboard + const keys = {}; + window.addEventListener('keydown', (e) => { + keys[e.key] = true; + // start with any key + if (!state.running && e.key.length === 1 || !state.running && (e.key === 'ArrowUp' || e.key === ' ')) { + startGame(); + } + // immediate controls + if (e.key === 'ArrowUp') { jet.vy -= 2; jet.thrusting = true; playSound(sounds.thrust); } + if (e.key === 'ArrowDown') { jet.vy += 2; } + if (e.key === 'p') { if (!state.running) startGame(); else pauseGame(); } + }); + window.addEventListener('keyup', (e) => { keys[e.key] = false; if (e.key === 'ArrowUp') jet.thrusting = false; }); + + // on-screen buttons + upBtn.addEventListener('pointerdown', () => { jet.vy -= 3; jet.thrusting = true; playSound(sounds.thrust); }); + upBtn.addEventListener('pointerup', () => { jet.thrusting = false; }); + downBtn.addEventListener('pointerdown', () => { jet.vy += 3; }); + leftBtn.addEventListener('pointerdown', () => { /* reserved for lateral movement */ }); + rightBtn.addEventListener('pointerdown', () => { /* reserved for lateral movement */ }); + + // button events + startBtn.addEventListener('click', startGame); + resumeBtn.addEventListener('click', startGame); + pauseBtn.addEventListener('click', pauseGame); + restartBtn.addEventListener('click', () => { + resetGame(); + startBtn.hidden = false; + resumeBtn.hidden = true; + pauseBtn.hidden = true; + }); + + // main loop to update UI (score + spawn behavior) even when not animating every ms + setInterval(() => { + scoreEl.textContent = Math.floor(state.score); + // subtle auto-spawn slight chance when idle to keep dynamic visuals + if (!state.running && Math.random() < 0.02) { + spawnMine(); + setTimeout(() => { mines.pop(); }, 1200); + } + }, 150); + + // initial draw + resetGame(); + drawStaticBackground(); + // render mines / jet even when idle + (function idleDraw() { + if (!state.running) { + // render static overlay preview + drawStaticBackground(); + mines.forEach(drawMine); + // draw jet in center-left + ctx.save(); + ctx.translate(jet.x, jet.y); + ctx.rotate(0); + ctx.shadowBlur = 14; + ctx.shadowColor = 'rgba(0,240,255,0.12)'; + ctx.fillStyle = '#08131a'; + ctx.strokeStyle = '#00f0ff'; + ctx.lineWidth = 1.6; + ctx.beginPath(); + ctx.moveTo(-10, 8); + ctx.quadraticCurveTo(10, -18, 40, -6); + ctx.lineTo(58, -6); + ctx.quadraticCurveTo(68, -4, 66, 6); + ctx.lineTo(38, 12); + ctx.quadraticCurveTo(12, 18, -10, 12); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + ctx.restore(); + // draw mines faint + mines.forEach(drawMine); + } + requestAnimationFrame(idleDraw); + })(); + + // draw loop continues via requestAnimationFrame in gameLoop + +})(); diff --git a/games/jet-glide/style.css b/games/jet-glide/style.css new file mode 100644 index 00000000..2fdc8646 --- /dev/null +++ b/games/jet-glide/style.css @@ -0,0 +1,132 @@ +:root{ + --bg:#06070a; + --card:#0b0d13; + --neon-1:#00f0ff; + --neon-2:#7cff6b; + --accent:#ff5bd3; + --glass: rgba(255,255,255,0.04); + --muted: rgba(255,255,255,0.45); + --glass-2: rgba(255,255,255,0.03); + --radius:14px; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; +} +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + background: radial-gradient(1200px 400px at 10% 10%, rgba(0,240,255,0.04), transparent), + radial-gradient(900px 300px at 90% 80%, rgba(124,255,107,0.02), transparent), + var(--bg); + color:#eaf6ff; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + display:flex; + justify-content:center; + padding:32px; +} + +/* container */ +.page{ + width: 100%; + max-width:1100px; + display:flex; + flex-direction:column; + gap:18px; +} + +/* topbar */ +.topbar{ + display:flex; + justify-content:space-between; + align-items:center; + gap:12px; +} +.topbar .back{ + color:var(--muted); + text-decoration:none; + font-weight:600; + padding:8px 12px; + background:var(--glass); + border-radius:10px; +} +.topbar h1{margin:0;font-size:20px} +.controls-top button{ + background:transparent;border:0;color:var(--muted);font-size:20px;cursor:pointer;padding:6px 8px;border-radius:8px; +} +.controls-top button[aria-pressed="false"]{opacity:0.5} + +/* game area layout */ +.game-area{ + display:grid; + grid-template-columns: 1fr 320px; + gap:18px; + align-items:start; +} + +/* UI card */ +.game-ui{ + background: linear-gradient(180deg, rgba(255,255,255,0.02), transparent); + border-radius:var(--radius); + padding:14px; + box-shadow: 0 8px 30px rgba(0,0,0,0.6); + border: 1px solid rgba(255,255,255,0.03); +} + +/* meta */ +.meta{display:flex;justify-content:space-between;align-items:center;padding:6px 8px 12px 8px} +.meta .score,.meta .highscore{font-weight:700;background:linear-gradient(90deg,var(--glass),transparent);padding:8px 10px;border-radius:10px;font-size:14px} + +/* canvas wrap */ +.canvas-wrap{position:relative;border-radius:12px;overflow:hidden;margin:6px 0;border:1px solid rgba(255,255,255,0.03);} +#gameCanvas{display:block;width:100%;height:auto;background: + linear-gradient(to bottom, rgba(4,6,20,0.9), rgba(2,3,10,0.85));} + +/* overlay */ +.overlay{ + position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none; +} +.overlay-card{ + width:88%;max-width:620px;padding:18px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border-radius:12px;text-align:center;backdrop-filter: blur(6px); + box-shadow: 0 10px 30px rgba(0,0,0,0.6); + border:1px solid rgba(255,255,255,0.04); + pointer-events:auto; +} +.overlay-card h2{margin:6px 0 8px;font-size:22px} +.overlay-card p{color:var(--muted);margin:0 0 12px} +.overlay-actions{display:flex;gap:10px;justify-content:center;margin-bottom:8px} +.cta{ + background:linear-gradient(90deg,var(--neon-1),var(--accent)); + color:#031016;border:0;padding:10px 16px;border-radius:10px;font-weight:700;cursor:pointer; + box-shadow:0 6px 20px rgba(0,240,255,0.09), 0 4px 8px rgba(255,91,211,0.06); +} +.muted{ + background:transparent;border:1px solid rgba(255,255,255,0.06);color:var(--muted);padding:8px 12px;border-radius:10px;cursor:pointer; +} + +/* on-screen controls */ +.game-controls{display:flex;gap:8px;justify-content:center;padding-top:8px} +.touch{padding:10px 14px;border-radius:10px;border:0;background:var(--glass-2);color:var(--muted);font-weight:700;cursor:pointer} + +/* description column */ +.desc{ + background:var(--card); + border-radius:var(--radius); + padding:16px; + border:1px solid rgba(255,255,255,0.03); +} +.desc h3{margin-top:0} +.desc ul{margin:8px 0 0 18px;color:var(--muted)} + +/* footer */ +.foot{color:var(--muted);text-align:center;padding:4px;font-size:13px} + +/* neon glow for canvas elements using CSS filters on the canvas element */ +#gameCanvas { filter: drop-shadow(0 6px 16px rgba(0, 240, 255, 0.06)) drop-shadow(0 -6px 12px rgba(124,255,107,0.02)); } + +/* responsive */ +@media (max-width:980px){ + .game-area{grid-template-columns: 1fr;} + .desc{order:2} + .canvas-wrap{order:1} +} diff --git a/games/jigsaw_game/index.html b/games/jigsaw_game/index.html new file mode 100644 index 00000000..b612e412 --- /dev/null +++ b/games/jigsaw_game/index.html @@ -0,0 +1,40 @@ + + + + + + Fixed Jigsaw Puzzle + + + + +
    +

    ๐Ÿงฉ Fixed Jigsaw Puzzle (4x4)

    + +
    + Moves: 0 | + Time: 0s +
    + +
    +
    +

    Reference:

    +
    +
    + +
    +
    +
    + +
    +

    Click two pieces to swap their positions.

    +
    + +
    + +
    +
    + + + + \ No newline at end of file diff --git a/games/jigsaw_game/script.js b/games/jigsaw_game/script.js new file mode 100644 index 00000000..d963983a --- /dev/null +++ b/games/jigsaw_game/script.js @@ -0,0 +1,196 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. GAME CONSTANTS & ELEMENTS --- + const GRID_SIZE = 4; + const TOTAL_PIECES = GRID_SIZE * GRID_SIZE; // 16 pieces + const PUZZLE_WIDTH = 400; + const PIECE_SIZE = PUZZLE_WIDTH / GRID_SIZE; + + const jigsawGrid = document.getElementById('jigsaw-grid'); + const shuffleButton = document.getElementById('shuffle-button'); + const movesDisplay = document.getElementById('moves-display'); + const timeDisplay = document.getElementById('time-display'); + const feedbackMessage = document.getElementById('feedback-message'); + + // --- 2. GAME STATE VARIABLES --- + let pieces = []; // Array of piece elements in their current DOM order + let selectedPiece = null; // The first piece clicked + let moves = 0; + let timer = 0; + let timerInterval = null; + let gameActive = false; + + // --- 3. CORE LOGIC --- + + /** + * Calculates the CSS background-position for a piece based on its solved index (0-15). + */ + function calculateBackgroundPosition(solvedIndex) { + const row = Math.floor(solvedIndex / GRID_SIZE); + const col = solvedIndex % GRID_SIZE; + // Position is negative because the background is moved opposite to the piece's position + const x = -col * PIECE_SIZE; + const y = -row * PIECE_SIZE; + return `${x}px ${y}px`; + } + + /** + * Shuffles an array in place (Fisher-Yates). + */ + function shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + } + + /** + * Renders the pieces onto the grid based on the current order in the 'pieces' array. + */ + function renderPieces() { + jigsawGrid.innerHTML = ''; // Clear existing DOM + pieces.forEach(piece => { + jigsawGrid.appendChild(piece); + }); + } + + /** + * Swaps two piece elements, both visually and logically in the 'pieces' array. + */ + function swapPieces(piece1, piece2) { + if (piece1 === piece2) return; + + const index1 = pieces.indexOf(piece1); + const index2 = pieces.indexOf(piece2); + + if (index1 !== -1 && index2 !== -1) { + // Swap in the pieces array (logical state) + [pieces[index1], pieces[index2]] = [pieces[index2], pieces[index1]]; + + // Re-render the grid to reflect the swap + renderPieces(); + + moves++; + movesDisplay.textContent = moves; + + // Check win condition after every swap + if (checkWin()) { + endGame(true); + } + } + } + + /** + * Checks if the current arrangement of pieces matches the solved state. + */ + function checkWin() { + // The puzzle is solved if the data-solved-index of each piece matches its current DOM position (index i) + for (let i = 0; i < TOTAL_PIECES; i++) { + const solvedIndex = parseInt(pieces[i].getAttribute('data-solved-index')); + if (solvedIndex !== i) { + return false; + } + } + return true; + } + + /** + * Handles the click event on a piece for selection/swapping. + */ + function handlePieceClick(event) { + if (!gameActive) return; + + const clickedPiece = event.target; + + if (selectedPiece === clickedPiece) { + // Deselect the same piece + selectedPiece.classList.remove('selected'); + selectedPiece = null; + feedbackMessage.textContent = 'Piece deselected.'; + } else if (selectedPiece) { + // Second click: Swap + selectedPiece.classList.remove('selected'); + swapPieces(selectedPiece, clickedPiece); + selectedPiece = null; + } else { + // First click: Select + selectedPiece = clickedPiece; + selectedPiece.classList.add('selected'); + feedbackMessage.textContent = 'Piece selected. Click another to swap.'; + } + } + + // --- 4. GAME FLOW --- + + /** + * Initializes the puzzle pieces in their correct order and attaches handlers. + */ + function createPieces() { + pieces = []; + jigsawGrid.innerHTML = ''; + + for (let i = 0; i < TOTAL_PIECES; i++) { + const piece = document.createElement('div'); + piece.classList.add('jigsaw-piece'); + piece.setAttribute('data-solved-index', i); // Stores the correct position + piece.style.backgroundPosition = calculateBackgroundPosition(i); + piece.addEventListener('click', handlePieceClick); + pieces.push(piece); + } + } + + /** + * Starts the game session (shuffles and starts timer). + */ + function startGame() { + // Stop and reset timer + clearInterval(timerInterval); + moves = 0; + timer = 0; + movesDisplay.textContent = 0; + timeDisplay.textContent = 0; + selectedPiece = null; + gameActive = true; + + // Ensure pieces are created + if (pieces.length === 0) createPieces(); + + // Shuffle (but ensure it's not solved initially) + do { + shuffleArray(pieces); + } while (checkWin()); + + renderPieces(); + feedbackMessage.textContent = 'Puzzle shuffled! Start clicking to swap.'; + + // Start Timer + timerInterval = setInterval(() => { + timer++; + timeDisplay.textContent = timer; + }, 1000); + } + + /** + * Ends the game and stops the timer. + */ + function endGame(win) { + clearInterval(timerInterval); + gameActive = false; + + if (win) { + feedbackMessage.innerHTML = `๐ŸŽ‰ **PUZZLE SOLVED!** Total Moves: ${moves}, Time: ${timer}s.`; + feedbackMessage.style.color = '#2ecc71'; + jigsawGrid.style.border = '5px solid #2ecc71'; + } + } + + // --- 5. EVENT LISTENERS AND INITIAL SETUP --- + + shuffleButton.addEventListener('click', startGame); + + // Initial creation of pieces (in solved order) + createPieces(); + renderPieces(); + + // Initial message + feedbackMessage.textContent = 'Press "Shuffle & Restart" to begin the puzzle!'; +}); \ No newline at end of file diff --git a/games/jigsaw_game/style.css b/games/jigsaw_game/style.css new file mode 100644 index 00000000..b9354bdf --- /dev/null +++ b/games/jigsaw_game/style.css @@ -0,0 +1,119 @@ +:root { + --grid-size: 4; /* 4x4 grid */ + --puzzle-width: 400px; /* Total width/height of the puzzle container */ + --piece-size: calc(var(--puzzle-width) / var(--grid-size)); + --image-url: url('images/jigsaw-image.jpg'); /* !!! UPDATE THIS PATH !!! */ +} + +body { + font-family: 'Verdana', sans-serif; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f7f7f7; + color: #333; +} + +#game-container { + background-color: white; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 900px; + width: 90%; +} + +h1 { + color: #e67e22; /* Orange puzzle theme */ + margin-bottom: 20px; +} + +/* --- Puzzle Area Layout --- */ +#puzzle-area { + display: flex; + justify-content: center; + gap: 50px; + margin-bottom: 20px; +} + +/* --- Reference Image --- */ +#reference-image-box h2 { + font-size: 1em; + border-bottom: 1px solid #ccc; + padding-bottom: 5px; + margin-bottom: 10px; +} +#reference-image, #jigsaw-grid { + width: var(--puzzle-width); + height: var(--puzzle-width); +} +#reference-image { + background-image: var(--image-url); + background-size: cover; + border: 3px solid #ccc; + border-radius: 5px; +} + +/* --- Jigsaw Grid --- */ +#jigsaw-grid { + display: grid; + grid-template-columns: repeat(var(--grid-size), 1fr); + grid-template-rows: repeat(var(--grid-size), 1fr); + border: 3px solid #333; +} + +/* --- Piece Styling --- */ +.jigsaw-piece { + width: var(--piece-size); + height: var(--piece-size); + box-sizing: border-box; + border: 1px solid rgba(255, 255, 255, 0.6); /* White lines between pieces */ + cursor: pointer; + background-image: var(--image-url); + /* CRUCIAL: Ensures the whole image is treated as 400x400 */ + background-size: var(--puzzle-width) var(--puzzle-width); + transition: box-shadow 0.1s, transform 0.3s; +} + +.jigsaw-piece:hover { + box-shadow: inset 0 0 15px rgba(230, 126, 34, 0.8); +} + +.selected { + box-shadow: 0 0 15px 5px #007bff; /* Blue shadow when selected */ + border-color: #007bff; + transform: scale(1.05); + z-index: 10; +} + +/* --- Status and Controls --- */ +#status-area { + font-size: 1.2em; + font-weight: bold; + margin-bottom: 20px; +} + +#feedback-message { + min-height: 20px; + margin-bottom: 20px; +} + +#shuffle-button { + padding: 10px 20px; + font-size: 1.1em; + font-weight: bold; + background-color: #3498db; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s; +} + +#shuffle-button:hover { + background-color: #2980b9; +} \ No newline at end of file diff --git a/games/jump-counter/index.html b/games/jump-counter/index.html new file mode 100644 index 00000000..7dd6dbd3 --- /dev/null +++ b/games/jump-counter/index.html @@ -0,0 +1,29 @@ + + + + + +Jump Counter | Mini JS Games Hub + + + +
    +

    Jump Counter ๐Ÿƒโ€โ™‚๏ธ๐Ÿ’ฅ

    +

    Score: 0

    +
    +
    +
    +
    + + + + +
    +
    + + + + + + + diff --git a/games/jump-counter/script.js b/games/jump-counter/script.js new file mode 100644 index 00000000..2e8c225b --- /dev/null +++ b/games/jump-counter/script.js @@ -0,0 +1,122 @@ +const player = document.getElementById("player"); +const gameArea = document.getElementById("game-area"); +const scoreEl = document.getElementById("score"); +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const resumeBtn = document.getElementById("resume-btn"); +const restartBtn = document.getElementById("restart-btn"); +const jumpSound = document.getElementById("jump-sound"); +const hitSound = document.getElementById("hit-sound"); + +let gameInterval; +let obstacleInterval; +let score = 0; +let playerBottom = 0; +let isJumping = false; +let obstacles = []; +let gamePaused = false; + +function startGame() { + resetGame(); + gameInterval = setInterval(gameLoop, 20); + obstacleInterval = setInterval(createObstacle, 2000); +} + +function pauseGame() { + gamePaused = true; +} + +function resumeGame() { + gamePaused = false; +} + +function restartGame() { + resetGame(); + startGame(); +} + +function resetGame() { + clearInterval(gameInterval); + clearInterval(obstacleInterval); + obstacles.forEach(obs => obs.remove()); + obstacles = []; + playerBottom = 0; + isJumping = false; + score = 0; + scoreEl.textContent = score; + player.style.bottom = playerBottom + "px"; + gamePaused = false; +} + +function jump() { + if (!isJumping && !gamePaused) { + isJumping = true; + jumpSound.play(); + let jumpHeight = 0; + const jumpInterval = setInterval(() => { + if (jumpHeight >= 100) { + clearInterval(jumpInterval); + fall(); + } else { + jumpHeight += 5; + playerBottom += 5; + player.style.bottom = playerBottom + "px"; + } + }, 20); + } +} + +function fall() { + const fallInterval = setInterval(() => { + if (playerBottom <= 0) { + playerBottom = 0; + player.style.bottom = playerBottom + "px"; + isJumping = false; + clearInterval(fallInterval); + } else { + playerBottom -= 5; + player.style.bottom = playerBottom + "px"; + } + }, 20); +} + +function createObstacle() { + if (gamePaused) return; + const obstacle = document.createElement("div"); + obstacle.classList.add("obstacle"); + obstacle.style.left = "600px"; + gameArea.appendChild(obstacle); + obstacles.push(obstacle); + + const moveObstacle = setInterval(() => { + if (gamePaused) return; + let obstacleLeft = parseInt(obstacle.style.left); + if (obstacleLeft <= -40) { + obstacle.remove(); + obstacles.shift(); + score++; + scoreEl.textContent = score; + clearInterval(moveObstacle); + } else if (obstacleLeft < 100 && playerBottom < 40) { + hitSound.play(); + alert("Game Over! Your Score: " + score); + restartGame(); + clearInterval(moveObstacle); + } else { + obstacle.style.left = obstacleLeft - 5 + "px"; + } + }, 20); +} + +function gameLoop() { + // can add glow effect dynamically or more advanced UI later +} + +document.addEventListener("keydown", (e) => { + if (e.code === "Space") jump(); +}); + +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", pauseGame); +resumeBtn.addEventListener("click", resumeGame); +restartBtn.addEventListener("click", restartGame); diff --git a/games/jump-counter/style.css b/games/jump-counter/style.css new file mode 100644 index 00000000..1728ccae --- /dev/null +++ b/games/jump-counter/style.css @@ -0,0 +1,70 @@ +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(to bottom, #141E30, #243B55); + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.game-container { + text-align: center; +} + +#game-area { + position: relative; + width: 600px; + height: 200px; + background: linear-gradient(to right, #2c5364, #203a43, #0f2027); + margin: 20px auto; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 0 20px #0ff; +} + +#player { + position: absolute; + bottom: 0; + left: 50px; + width: 40px; + height: 40px; + background: url('https://img.icons8.com/emoji/48/000000/person-running.png') no-repeat center center; + background-size: contain; + animation: glow 1.5s infinite alternate; +} + +@keyframes glow { + 0% { box-shadow: 0 0 10px #0ff; } + 50% { box-shadow: 0 0 20px #0ff; } + 100% { box-shadow: 0 0 10px #0ff; } +} + +.obstacle { + position: absolute; + bottom: 0; + width: 40px; + height: 40px; + background: url('https://img.icons8.com/emoji/48/000000/rock-emoji.png') no-repeat center center; + background-size: contain; + animation: glow 1s infinite alternate; +} + +.controls button { + margin: 5px; + padding: 8px 15px; + font-size: 16px; + border: none; + border-radius: 5px; + cursor: pointer; + background: #0ff; + color: #000; + font-weight: bold; + transition: all 0.2s; +} + +.controls button:hover { + background: #00f; + color: #fff; +} diff --git a/games/jump-rope/index.html b/games/jump-rope/index.html new file mode 100644 index 00000000..1f721c0f --- /dev/null +++ b/games/jump-rope/index.html @@ -0,0 +1,23 @@ + + + + + + Jump Rope Game + + + +
    +

    Jump Rope

    +

    Click to jump over the ropes! Time your jumps perfectly.

    +
    +
    Time: 30
    +
    Score: 0
    + +
    + +
    +
    + + + \ No newline at end of file diff --git a/games/jump-rope/script.js b/games/jump-rope/script.js new file mode 100644 index 00000000..7cbf5c86 --- /dev/null +++ b/games/jump-rope/script.js @@ -0,0 +1,146 @@ +// Jump Rope Game Script +// Click to jump over swinging ropes + +var canvas = document.getElementById('gameCanvas'); +var ctx = canvas.getContext('2d'); +var timerDisplay = document.getElementById('timer'); +var scoreDisplay = document.getElementById('score'); +var restartBtn = document.getElementById('restart'); +var messageDiv = document.getElementById('message'); + +var player = { x: canvas.width / 2, y: canvas.height - 50, width: 20, height: 40, jumping: false, jumpHeight: 0 }; +var ropes = []; +var score = 0; +var timeLeft = 30; +var timerInterval; +var gameRunning = true; + +// Rope class +function Rope(y, speed) { + this.y = y; + this.x = 0; + this.width = 10; + this.height = 5; + this.speed = speed; + this.direction = 1; // 1 right, -1 left +} + +// Initialize the game +function initGame() { + ropes = []; + score = 0; + timeLeft = 30; + gameRunning = true; + player.jumping = false; + player.jumpHeight = 0; + messageDiv.textContent = ''; + scoreDisplay.textContent = 'Score: ' + score; + // Create some ropes + for (var i = 0; i < 3; i++) { + var y = 100 + i * 80; + var speed = 2 + Math.random() * 3; + ropes.push(new Rope(y, speed)); + } + startTimer(); + draw(); +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw ground + ctx.fillStyle = '#8d6e63'; + ctx.fillRect(0, canvas.height - 20, canvas.width, 20); + + // Draw player + ctx.fillStyle = '#2196f3'; + ctx.fillRect(player.x - player.width / 2, player.y - player.height - player.jumpHeight, player.width, player.height); + + // Draw ropes + ctx.fillStyle = '#f44336'; + for (var i = 0; i < ropes.length; i++) { + var rope = ropes[i]; + ctx.fillRect(rope.x, rope.y, rope.width, rope.height); + ctx.fillRect(rope.x + canvas.width - rope.width, rope.y, rope.width, rope.height); + } + + if (gameRunning) { + update(); + requestAnimationFrame(draw); + } +} + +// Update game state +function update() { + // Update ropes + for (var i = 0; i < ropes.length; i++) { + var rope = ropes[i]; + rope.x += rope.speed * rope.direction; + if (rope.x > canvas.width / 2) { + rope.direction = -1; + } + if (rope.x < 0) { + rope.direction = 1; + } + } + + // Update player jump + if (player.jumping) { + player.jumpHeight += 5; + if (player.jumpHeight > 100) { + player.jumping = false; + } + } else { + if (player.jumpHeight > 0) { + player.jumpHeight -= 5; + } + } + + // Check collisions + for (var i = 0; i < ropes.length; i++) { + var rope = ropes[i]; + if (rope.y > player.y - player.height - player.jumpHeight && rope.y < player.y - player.jumpHeight) { + if ((rope.x < player.x + player.width / 2 && rope.x + rope.width > player.x - player.width / 2) || + (rope.x + canvas.width - rope.width < player.x + player.width / 2 && rope.x + canvas.width > player.x - player.width / 2)) { + if (!player.jumping) { + gameRunning = false; + messageDiv.textContent = 'Ouch! You got hit. Final Score: ' + score; + messageDiv.style.color = 'red'; + clearInterval(timerInterval); + } + } + } + } +} + +// Handle click to jump +canvas.addEventListener('click', function() { + if (!gameRunning) return; + if (!player.jumping && player.jumpHeight === 0) { + player.jumping = true; + score++; + scoreDisplay.textContent = 'Score: ' + score; + } +}); + +// Start the timer +function startTimer() { + clearInterval(timerInterval); + timerInterval = setInterval(function() { + timeLeft--; + timerDisplay.textContent = 'Time: ' + timeLeft; + if (timeLeft <= 0) { + clearInterval(timerInterval); + gameRunning = false; + messageDiv.textContent = 'Time\'s up! Final Score: ' + score; + messageDiv.style.color = 'green'; + } + }, 1000); +} + +// Restart button +restartBtn.addEventListener('click', initGame); + +// Start the game +initGame(); \ No newline at end of file diff --git a/games/jump-rope/style.css b/games/jump-rope/style.css new file mode 100644 index 00000000..41030908 --- /dev/null +++ b/games/jump-rope/style.css @@ -0,0 +1,53 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #fff3e0; +} + +.container { + text-align: center; +} + +h1 { + color: #f57c00; +} + +.game-info { + margin-bottom: 20px; + display: flex; + justify-content: center; + gap: 20px; +} + +#timer, #score { + font-size: 24px; + font-weight: bold; +} + +#restart { + padding: 10px 20px; + font-size: 16px; + background-color: #4caf50; + color: white; + border: none; + cursor: pointer; +} + +#restart:hover { + background-color: #388e3c; +} + +canvas { + border: 2px solid #f57c00; + background-color: #fffde7; +} + +#message { + margin-top: 20px; + font-size: 18px; + font-weight: bold; +} \ No newline at end of file diff --git a/games/jump-tag/index.html b/games/jump-tag/index.html new file mode 100644 index 00000000..b9f95350 --- /dev/null +++ b/games/jump-tag/index.html @@ -0,0 +1,62 @@ + + + + + + Jump Tag โ€” Mini JS Games Hub + + + +
    +
    +
    + ๐Ÿƒโ€โ™‚๏ธ๐Ÿƒโ€โ™€๏ธ +

    Jump Tag

    +

    One chases, one escapes โ€” jump obstacles to survive!

    +
    + +
    + + + + + Open in new tab โ†’ +
    +
    + +
    +
    +
    Time: 0.0s
    +
    Runner: Alive
    +
    Chaser: Chasing
    +
    Round: 1
    +
    + +
    + + + +
    + +
    + Tip: Time your jumps โ€” obstacles slow or trip players. If the chaser reaches the runner's x-position, chaser wins the round. +
    +
    + +
    + Made with โค โ€” Mini JS Games Hub +
    +
    + + + + diff --git a/games/jump-tag/script.js b/games/jump-tag/script.js new file mode 100644 index 00000000..e8b7885e --- /dev/null +++ b/games/jump-tag/script.js @@ -0,0 +1,449 @@ +/* Jump Tag โ€” Advanced playable version + Runner = playerA uses W + Chaser = playerB uses ArrowUp or Space + Buttons: Start / Pause / Restart / Mute + Obstacles spawn and move left. If chaser overlaps runner => chaser wins. +*/ + +(() => { + // Canvas & context + const canvas = document.getElementById('gameCanvas'); + const ctx = canvas.getContext('2d'); + + // Controls + const startBtn = document.getElementById('startBtn'); + const pauseBtn = document.getElementById('pauseBtn'); + const restartBtn = document.getElementById('restartBtn'); + const muteBtn = document.getElementById('muteBtn'); + const openNew = document.getElementById('openNew'); + + // HUD elements + const timeEl = document.getElementById('time'); + const runnerStateEl = document.getElementById('runnerState'); + const chaserStateEl = document.getElementById('chaserState'); + const roundEl = document.getElementById('round'); + + // Sounds (public links) + const SOUND_JUMP = 'https://actions.google.com/sounds/v1/human_voices/bugle_tune.ogg'; + const SOUND_HIT = 'https://actions.google.com/sounds/v1/alarms/alarm_clock.ogg'; + const SOUND_BG = 'https://actions.google.com/sounds/v1/ambiences/office_ambience.ogg'; + + let audioJump = new Audio(SOUND_JUMP); + let audioHit = new Audio(SOUND_HIT); + let audioBg = new Audio(SOUND_BG); + audioBg.loop = true; + audioBg.volume = 0.12; + + let muted = false; + + // Game state + let running = false; + let paused = false; + let lastTime = 0; + let elapsed = 0; + let round = 1; + + // world + const WORLD = { + gravity: 1800, // px/s^2 + groundY: canvas.height - 70, + speedBase: 260 + }; + + // players (rectangles with simple sprite) + function makePlayer(x, color) { + return { + x, + y: WORLD.groundY - 60, + w: 48, + h: 60, + vy: 0, + onGround: true, + color, + speed: 0, + state: 'alive', // alive, fallen, caught + glow: color + }; + } + + const runner = makePlayer(180, '#ffb86b'); // controlled by W + const chaser = makePlayer(360, '#7dd3fc'); // controlled by UP/Space + + // obstacles array + let obstacles = []; + + // spawn settings + let spawnTimer = 0; + let spawnInterval = 1.1; // seconds + + // scoring & round time + let roundTimer = 0; + const ROUND_DURATION = 25 + 5 * 0; // runner must survive this to win + + // images: using online pics (small) as avatar visuals โ€” they will be drawn scaled + const runnerImg = new Image(); + const chaserImg = new Image(); + runnerImg.src = 'https://images.unsplash.com/photo-1541534401786-3f9c8d6d8cde?w=200&q=60'; + chaserImg.src = 'https://images.unsplash.com/photo-1503264116251-35a269479413?w=200&q=60'; + + // helpers + function clamp(v, a, b){ return Math.max(a, Math.min(b, v)); } + + // input + const keys = {}; + window.addEventListener('keydown', (e) => { + keys[e.code] = true; + // jump keys + if (e.code === 'KeyW') attemptJump(runner); + if (e.code === 'ArrowUp' || e.code === 'Space') attemptJump(chaser); + // pause by P + if (e.code === 'KeyP') togglePause(); + }); + window.addEventListener('keyup', (e) => { keys[e.code] = false; }); + + function attemptJump(player) { + if (!running || paused) return; + if (player.onGround && player.state === 'alive') { + player.vy = -720; + player.onGround = false; + if (!muted) audioJump.currentTime = 0, audioJump.play().catch(()=>{}); + } + } + + // game control handlers + startBtn.addEventListener('click', () => { + // track play + trackGamePlay('Jump Tag'); + startGame(); + // openNew link prepared + openNew.href = window.location.href; + }); + pauseBtn.addEventListener('click', togglePause); + restartBtn.addEventListener('click', () => { + resetRound(); + startGame(); + }); + muteBtn.addEventListener('click', () => { + muted = !muted; + if (muted) { + audioBg.pause(); + muteBtn.textContent = '๐Ÿ”‡'; + } else { + audioBg.currentTime = 0; + audioBg.play().catch(()=>{}); + muteBtn.textContent = '๐Ÿ”Š'; + } + }); + + function togglePause() { + if (!running) return; + paused = !paused; + pauseBtn.textContent = paused ? 'Resume' : 'Pause'; + if (paused) { + audioBg.pause(); + } else { + if (!muted) audioBg.play().catch(()=>{}); + lastTime = performance.now(); + requestAnimationFrame(loop); + } + } + + function startGame(){ + if (!running) { + running = true; + paused = false; + pauseBtn.disabled = false; + restartBtn.disabled = false; + startBtn.disabled = true; + if (!muted) audioBg.play().catch(()=>{}); + lastTime = performance.now(); + requestAnimationFrame(loop); + } else { + // already running โ€” resume + paused = false; + if (!muted) audioBg.play().catch(()=>{}); + lastTime = performance.now(); + requestAnimationFrame(loop); + } + } + + function resetRound(){ + // reset state + running = false; + paused = false; + lastTime = 0; + elapsed = 0; + roundTimer = 0; + spawnTimer = 0; + obstacles = []; + runner.x = 180; runner.y = WORLD.groundY - runner.h; runner.vy = 0; runner.onGround = true; runner.state = 'alive'; + chaser.x = 360; chaser.y = WORLD.groundY - chaser.h; chaser.vy = 0; chaser.onGround = true; chaser.state = 'alive'; + round = 1; + timeEl.textContent = '0.0'; + runnerStateEl.textContent = 'Alive'; + chaserStateEl.textContent = 'Chasing'; + roundEl.textContent = round; + pauseBtn.disabled = true; + restartBtn.disabled = true; + startBtn.disabled = false; + if (!muted) audioBg.pause(); + } + + // collisions + function rectsOverlap(a,b){ + return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y; + } + + // obstacle prototype + function spawnObstacle(){ + // random size + const h = 28 + Math.round(Math.random()*42); + const w = 28 + Math.round(Math.random()*40); + const y = WORLD.groundY - h; + const speed = WORLD.speedBase + Math.random()*120 + (elapsed*0.6); + obstacles.push({ + x: canvas.width + 10, + y, + w, + h, + speed, + color: '#ff7b72', + slows: Math.random() < 0.45, // slows on hit + }); + } + + // game loop + function loop(ts){ + if (!running || paused) return; + const dt = Math.min(0.035, (ts - lastTime) / 1000); + lastTime = ts; + elapsed += dt; + roundTimer += dt; + timeEl.textContent = elapsed.toFixed(1); + + // spawn obstacles + spawnTimer += dt; + if (spawnTimer >= spawnInterval){ + spawnTimer = 0; + // variable interval + spawnInterval = 0.8 + Math.random()*1.2 - Math.min(0.6, elapsed/60); + spawnObstacle(); + } + + updatePlayer(runner, dt); + updatePlayer(chaser, dt); + + // move obstacles left + for (let i = obstacles.length-1; i >=0; i--){ + const ob = obstacles[i]; + ob.x -= ob.speed * dt; + if (ob.x + ob.w < -20) obstacles.splice(i,1); + // collision with players + const pr = {x: runner.x, y: runner.y, w: runner.w, h: runner.h}; + const pc = {x: chaser.x, y: chaser.y, w: chaser.w, h: chaser.h}; + const po = {x: ob.x, y: ob.y, w: ob.w, h: ob.h}; + if (rectsOverlap(pr, po) && runner.state === 'alive'){ + // runner hit + runner.state = 'fallen'; + runnerStateEl.textContent = 'Tripped'; + if (!muted) audioHit.currentTime = 0, audioHit.play().catch(()=>{}); + // slow runner + runner.vy = 0; + runner.onGround = true; + // nudge runner back + runner.x = Math.max(50, runner.x - 34); + } + if (rectsOverlap(pc, po) && chaser.state === 'alive'){ + chaser.state = 'slowed'; + chaserStateEl.textContent = 'Slowed'; + if (!muted) audioHit.currentTime = 0, audioHit.play().catch(()=>{}); + chaser.x = Math.min(canvas.width-120, chaser.x - 18); + } + } + + // chasing logic: chaser accelerates toward runner if alive + if (runner.state === 'alive' && chaser.state !== 'caught'){ + // chaser tries to approach runner.x + const dir = (runner.x - chaser.x) > 0 ? 1 : -1; + // chaser moves right if behind, left if ahead moderately + chaser.x += (100 + Math.min(220, elapsed*6)) * dt * Math.sign(runner.x - chaser.x); + // clamp + chaser.x = clamp(chaser.x, 80, canvas.width-120); + } + + // check catch + if (runner.state !== 'caught' && chaser.state !== 'caught'){ + // simple condition: if chaser overlaps runner horizontally within 20 px and vertical overlap + if (Math.abs(chaser.x - runner.x) < 36 && Math.abs(chaser.y - runner.y) < 10){ + // caught + runner.state = 'caught'; + chaser.state = 'victorious'; + runnerStateEl.textContent = 'Caught'; + chaserStateEl.textContent = 'Winner'; + // flash + canvas.classList.add('flash-win'); + setTimeout(()=>canvas.classList.remove('flash-win'),900); + if (!muted) audioHit.currentTime = 0, audioHit.play().catch(()=>{}); + // stop round after small pause + setTimeout(()=>{ endRound('chaser'); }, 800); + } + } + + // runner surviving the round/time = runner wins + if (roundTimer >= ROUND_DURATION){ + // runner wins by survival + runner.state = 'survived'; + runnerStateEl.textContent = 'Survived'; + chaserStateEl.textContent = 'Failed'; + if (!muted) audioBg.pause(); + canvas.classList.add('flash-win'); + setTimeout(()=>canvas.classList.remove('flash-win'),900); + setTimeout(()=>{ endRound('runner'); }, 700); + } + + // draw + drawAll(); + + // continue + if (running && !paused) requestAnimationFrame(loop); + } + + function updatePlayer(p, dt){ + // gravity + p.vy += WORLD.gravity * dt; + p.y += p.vy * dt; + // ground collision + if (p.y >= WORLD.groundY - p.h){ + p.y = WORLD.groundY - p.h; + p.vy = 0; + p.onGround = true; + if (p.state === 'fallen') { + // after falling, recover after delay + setTimeout(()=>{ if (p.state==='fallen') { p.state='alive'; runnerStateEl.textContent='Alive'; } }, 800); + } + } else { + p.onGround = false; + } + // small running bounce for look + // players slowly drift forward/back a bit for dynamic feel + if (p === runner){ + // slight automatic forward movement to keep chase dynamic but not extreme + if (p.x < 190) p.x += 18*dt; + if (p.x > 260) p.x -= 8*dt; + } else { + // chaser hovers near runner + // handled in main loop chase logic + } + } + + function endRound(winner){ + // stop game loop + running = false; + pauseBtn.disabled = true; + restartBtn.disabled = false; + startBtn.disabled = false; + // increment round counter if runner won multiple times + if (winner === 'runner') round += 1; + roundEl.textContent = round; + // small celebration or message + if (!muted) audioBg.pause(); + } + + // draw function + function drawAll(){ + // clear + ctx.clearRect(0,0,canvas.width,canvas.height); + + // sky gradient + const g = ctx.createLinearGradient(0,0,0,canvas.height); + g.addColorStop(0,'#081428'); + g.addColorStop(1,'#071927'); + ctx.fillStyle = g; + ctx.fillRect(0,0,canvas.width,canvas.height); + + // ground + ctx.fillStyle = '#061523'; + ctx.fillRect(0, WORLD.groundY, canvas.width, canvas.height - WORLD.groundY + 80); + + // decorative glowing orbs (parallax) + drawGlow(80,60, 36, '#7c3aed', 0.05); + drawGlow(canvas.width-120,40,26,'#06b6d4', 0.06); + + // draw obstacles + obstacles.forEach(ob=>{ + // glow rectangle + ctx.save(); + ctx.fillStyle = '#ff6b6b'; + ctx.shadowColor = 'rgba(255,107,107,0.25)'; + ctx.shadowBlur = 18; + ctx.fillRect(ob.x, ob.y, ob.w, ob.h); + ctx.restore(); + // small highlight + ctx.fillStyle = 'rgba(255,255,255,0.06)'; + ctx.fillRect(ob.x+6, ob.y+6, Math.max(6, ob.w-12), Math.max(6, ob.h-12)); + }); + + // draw players (image with glow) + drawPlayerSprite(runner, runnerImg); + drawPlayerSprite(chaser, chaserImg); + + // UI overlays: outlines + ctx.strokeStyle = 'rgba(255,255,255,0.02)'; + ctx.lineWidth = 2; + ctx.strokeRect(2,2,canvas.width-4, canvas.height-4); + } + + function drawGlow(x,y,r,color,alpha){ + ctx.save(); + ctx.beginPath(); + const radial = ctx.createRadialGradient(x,y,0,x,y,r); + radial.addColorStop(0, color); + radial.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = radial; + ctx.globalAlpha = alpha; + ctx.fillRect(x-r, y-r, r*2, r*2); + ctx.restore(); + } + + function drawPlayerSprite(p, img){ + // glow + ctx.save(); + ctx.shadowColor = 'rgba(124,58,237,0.18)'; + ctx.shadowBlur = 24; + // draw rectangle base to give silhouette + ctx.fillStyle = p.color; + ctx.fillRect(p.x-2, p.y-2, p.w+4, p.h+4); + + // draw image inside box clipped + if (img.complete && img.naturalWidth){ + // draw img scaled to box + ctx.drawImage(img, p.x, p.y, p.w, p.h); + // small overlay + ctx.fillStyle = 'rgba(0,0,0,0.08)'; + ctx.fillRect(p.x, p.y + p.h - 16, p.w, 16); + } else { + // fallback: emoji + ctx.fillStyle = '#000'; + ctx.fillRect(p.x, p.y, p.w, p.h); + } + ctx.restore(); + } + + // simple play tracker (works with hub) + function trackGamePlay(gameName){ + try { + const raw = localStorage.getItem('gamePlays') || '{}'; + const data = JSON.parse(raw); + if (!data[gameName]) data[gameName] = {plays:0, success:0}; + data[gameName].plays += 1; + localStorage.setItem('gamePlays', JSON.stringify(data)); + // update pro badges if page hub script listens (it does) + } catch(e){} + } + + // initialize + resetRound(); + + // expose small debug on window + window.JumpTag = { startGame, resetRound }; +})(); diff --git a/games/jump-tag/style.css b/games/jump-tag/style.css new file mode 100644 index 00000000..72312226 --- /dev/null +++ b/games/jump-tag/style.css @@ -0,0 +1,55 @@ +/* Jump Tag โ€” polished UI with glow */ +:root{ + --bg:#0f1724; + --card:#0b1220; + --accent:#7c3aed; + --accent-2:#06b6d4; + --muted:#9aa4b2; + --glow: 0 6px 30px rgba(124,58,237,0.18); + --glass: rgba(255,255,255,0.03); + --radius:14px; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; +} + +*{box-sizing:border-box} +html,body{height:100%;margin:0;background:linear-gradient(180deg,#071025 0%, #081426 60%);color:#e6eef6} +.game-shell{max-width:1200px;margin:28px auto;padding:22px;border-radius:18px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));box-shadow:var(--glow)} +.game-header{display:flex;align-items:center;justify-content:space-between;gap:20px;padding-bottom:10px;border-bottom:1px solid rgba(255,255,255,0.03)} +.title{display:flex;gap:14px;align-items:center} +.title .icon{font-size:36px;filter:drop-shadow(0 6px 12px rgba(124,58,237,0.15))} +.title h1{margin:0;font-size:20px;letter-spacing:0.2px} +.title p{margin:0;color:var(--muted);font-size:13px} +.controls{display:flex;gap:8px;align-items:center} +.btn{background:transparent;border:1px solid rgba(255,255,255,0.06);padding:8px 12px;border-radius:10px;color:#e6eef6;cursor:pointer;font-weight:600;backdrop-filter: blur(4px)} +.btn.primary{background:linear-gradient(90deg,var(--accent),var(--accent-2));border:none;color:white;box-shadow:0 10px 30px rgba(6,182,212,0.06),0 4px 10px rgba(124,58,237,0.08);transform:translateZ(0)} +.btn.ghost{background:transparent;border:1px dashed rgba(255,255,255,0.06)} +.btn:disabled{opacity:0.4;cursor:not-allowed} + +.game-area{padding:18px 0} +.hud{display:flex;gap:16px;align-items:center;color:var(--muted);margin-bottom:12px} +.hud .score{background:var(--glass);padding:8px 12px;border-radius:10px;font-size:13px;border:1px solid rgba(255,255,255,0.02)} + +.arena-wrap{display:flex;gap:18px;align-items:flex-start} +canvas{border-radius:12px;background: + linear-gradient(180deg, rgba(6,6,10,0.15), rgba(7,12,18,0.5)); + box-shadow: 0 12px 40px rgba(3,7,18,0.6), inset 0 1px 0 rgba(255,255,255,0.02); + border:1px solid rgba(255,255,255,0.03); + display:block;} + +.legend{width:230px;background:linear-gradient(180deg, rgba(255,255,255,0.01), rgba(255,255,255,0.02));padding:14px;border-radius:12px;border:1px solid rgba(255,255,255,0.03);color:var(--muted)} +.legend h3{margin:6px 0;font-size:14px} +.legend ul{padding-left:18px;margin:0} +.legend kbd{background:#081426;padding:4px 6px;border-radius:6px;border:1px solid rgba(255,255,255,0.03);font-weight:700} + +.tips{margin-top:12px;color:var(--muted);font-size:13px} + +/* HUD glow for big events */ +.flash-win{animation:flashWin 1s ease forwards} +@keyframes flashWin{0%{box-shadow:none}50%{box-shadow:0 0 40px rgba(6,182,212,0.14)}100%{box-shadow:none}} + +/* responsive */ +@media (max-width:920px){ + .arena-wrap{flex-direction:column} + .legend{width:100%} + canvas{width:100%;height:auto} +} diff --git a/games/karaoke-star/index.html b/games/karaoke-star/index.html new file mode 100644 index 00000000..37a9c3da --- /dev/null +++ b/games/karaoke-star/index.html @@ -0,0 +1,99 @@ + + + + + + Karaoke Star - Mini JS Games Hub + + + +
    +
    +

    ๐ŸŽค Karaoke Star

    +

    Sing along to your favorite songs!

    +
    + +
    +
    +

    Choose a Song:

    +
    + + + + +
    +
    + +
    +
    +
    + โ™ช +
    +
    + +
    +
    +
    Select a song to start singing!
    +
    +
    + +
    +
    +
    +
    +
    + 0:00 / 0:00 +
    +
    +
    +
    + +
    + + + + +
    + +
    +
    +

    Score: 0

    +
    + โญ + โญ + โญ + โญ + โญ +
    +
    + +
    +

    Performance

    +
    +
    +
    +
    Ready to sing!
    +
    +
    +
    + +
    +

    How to Play:

    +
      +
    • Select a song from the available options
    • +
    • Click "Play" to start the karaoke session
    • +
    • Sing along with the highlighted lyrics
    • +
    • Try to stay on beat for a higher score
    • +
    • Use Pause/Stop/Restart as needed
    • +
    • Aim for 5 stars by singing perfectly!
    • +
    +
    + +
    +

    ๐Ÿ’ก Pro Tip: For the best experience, use a microphone and sing along!

    +
    +
    + + + + \ No newline at end of file diff --git a/games/karaoke-star/script.js b/games/karaoke-star/script.js new file mode 100644 index 00000000..e2403d7f --- /dev/null +++ b/games/karaoke-star/script.js @@ -0,0 +1,398 @@ +// Karaoke Star Game Logic +class KaraokeStar { + constructor() { + this.currentSong = null; + this.isPlaying = false; + this.currentLineIndex = 0; + this.score = 0; + this.performance = 0; + this.startTime = 0; + this.timer = null; + this.audioContext = null; + + // Song data with lyrics and timing + this.songs = { + 'happy-birthday': { + title: 'Happy Birthday', + duration: 25, + lyrics: [ + { text: 'Happy birthday to you', start: 0, end: 4 }, + { text: 'Happy birthday to you', start: 4, end: 8 }, + { text: 'Happy birthday dear friend', start: 8, end: 12 }, + { text: 'Happy birthday to you!', start: 12, end: 16 }, + { text: '๐ŸŽ‚๐ŸŽˆ๐ŸŽ‰', start: 16, end: 25 } + ] + }, + 'twinkle-twinkle': { + title: 'Twinkle Twinkle Little Star', + duration: 30, + lyrics: [ + { text: 'Twinkle, twinkle, little star', start: 0, end: 4 }, + { text: 'How I wonder what you are', start: 4, end: 8 }, + { text: 'Up above the world so high', start: 8, end: 12 }, + { text: 'Like a diamond in the sky', start: 12, end: 16 }, + { text: 'Twinkle, twinkle, little star', start: 16, end: 20 }, + { text: 'How I wonder what you are', start: 20, end: 24 }, + { text: 'โœจ๐ŸŒŸโญ', start: 24, end: 30 } + ] + }, + 'row-row': { + title: 'Row Row Row Your Boat', + duration: 20, + lyrics: [ + { text: 'Row, row, row your boat', start: 0, end: 3 }, + { text: 'Gently down the stream', start: 3, end: 6 }, + { text: 'Merrily, merrily, merrily, merrily', start: 6, end: 10 }, + { text: 'Life is but a dream', start: 10, end: 13 }, + { text: '๐Ÿšฃโ€โ™€๏ธ๐ŸŒŠ๐Ÿ’ญ', start: 13, end: 20 } + ] + }, + 'amazing-grace': { + title: 'Amazing Grace', + duration: 35, + lyrics: [ + { text: 'Amazing grace, how sweet the sound', start: 0, end: 6 }, + { text: 'That saved a wretch like me', start: 6, end: 10 }, + { text: 'I once was lost, but now am found', start: 10, end: 16 }, + { text: 'Was blind, but now I see', start: 16, end: 20 }, + { text: '๐ŸŽต๐Ÿ™โœจ', start: 20, end: 35 } + ] + } + }; + + this.init(); + } + + init() { + this.initAudio(); + this.bindEvents(); + this.updateDisplay(); + } + + initAudio() { + try { + this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } catch (e) { + console.warn('Web Audio API not supported'); + } + } + + bindEvents() { + // Song selection + document.querySelectorAll('.song-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + this.selectSong(e.target.dataset.song); + }); + }); + + // Control buttons + document.getElementById('play-btn').addEventListener('click', () => this.play()); + document.getElementById('pause-btn').addEventListener('click', () => this.pause()); + document.getElementById('stop-btn').addEventListener('click', () => this.stop()); + document.getElementById('restart-btn').addEventListener('click', () => this.restart()); + + // Keyboard controls + document.addEventListener('keydown', (e) => { + switch(e.code) { + case 'Space': + e.preventDefault(); + this.isPlaying ? this.pause() : this.play(); + break; + case 'KeyR': + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + this.restart(); + } + break; + case 'KeyS': + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + this.stop(); + } + break; + } + }); + } + + selectSong(songKey) { + // Clear previous selection + document.querySelectorAll('.song-btn').forEach(btn => { + btn.classList.remove('active'); + }); + + // Set new selection + document.querySelector(`[data-song="${songKey}"]`).classList.add('active'); + + this.currentSong = this.songs[songKey]; + this.resetGame(); + this.updateDisplay(); + } + + play() { + if (!this.currentSong) { + alert('Please select a song first!'); + return; + } + + this.isPlaying = true; + this.startTime = Date.now() - (this.getCurrentTime() * 1000); + + this.updateControls(); + this.startTimer(); + this.playBackgroundMusic(); + } + + pause() { + this.isPlaying = false; + this.updateControls(); + this.stopTimer(); + } + + stop() { + this.isPlaying = false; + this.currentLineIndex = 0; + this.score = 0; + this.performance = 0; + this.startTime = 0; + this.updateControls(); + this.stopTimer(); + this.updateDisplay(); + } + + restart() { + if (!this.currentSong) return; + + this.stop(); + setTimeout(() => this.play(), 100); + } + + resetGame() { + this.currentLineIndex = 0; + this.score = 0; + this.performance = 0; + this.startTime = 0; + this.isPlaying = false; + this.updateControls(); + this.stopTimer(); + } + + startTimer() { + this.stopTimer(); // Clear any existing timer + this.timer = setInterval(() => { + this.updateProgress(); + this.updateLyrics(); + this.checkSongEnd(); + }, 100); + } + + stopTimer() { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + } + + getCurrentTime() { + if (!this.startTime) return 0; + return (Date.now() - this.startTime) / 1000; + } + + updateProgress() { + const currentTime = this.getCurrentTime(); + const progress = (currentTime / this.currentSong.duration) * 100; + + document.getElementById('progress-fill').style.width = Math.min(progress, 100) + '%'; + document.getElementById('current-time').textContent = this.formatTime(currentTime); + document.getElementById('total-time').textContent = this.formatTime(this.currentSong.duration); + } + + updateLyrics() { + const currentTime = this.getCurrentTime(); + const lyrics = this.currentSong.lyrics; + + // Find current line + let currentIndex = -1; + for (let i = 0; i < lyrics.length; i++) { + if (currentTime >= lyrics[i].start && currentTime < lyrics[i].end) { + currentIndex = i; + break; + } + } + + if (currentIndex !== this.currentLineIndex) { + this.currentLineIndex = currentIndex; + this.updateLyricsDisplay(); + this.updateScore(); + } + } + + updateLyricsDisplay() { + const lyrics = this.currentSong.lyrics; + const currentLineEl = document.getElementById('current-line'); + const nextLineEl = document.getElementById('next-line'); + + if (this.currentLineIndex >= 0 && this.currentLineIndex < lyrics.length) { + currentLineEl.textContent = lyrics[this.currentLineIndex].text; + + const nextIndex = this.currentLineIndex + 1; + if (nextIndex < lyrics.length) { + nextLineEl.textContent = lyrics[nextIndex].text; + } else { + nextLineEl.textContent = ''; + } + } else { + currentLineEl.textContent = 'Select a song to start singing!'; + nextLineEl.textContent = ''; + } + } + + updateScore() { + if (!this.isPlaying) return; + + // Simple scoring based on timing accuracy + const currentTime = this.getCurrentTime(); + const currentLine = this.currentSong.lyrics[this.currentLineIndex]; + + if (currentLine) { + const lineCenter = (currentLine.start + currentLine.end) / 2; + const timingAccuracy = 1 - Math.abs(currentTime - lineCenter) / ((currentLine.end - currentLine.start) / 2); + + if (timingAccuracy > 0.5) { // Good timing + this.score += Math.floor(timingAccuracy * 100); + this.performance = Math.min(100, this.performance + 10); + } + } + + this.updateDisplay(); + } + + checkSongEnd() { + const currentTime = this.getCurrentTime(); + if (currentTime >= this.currentSong.duration) { + this.songComplete(); + } + } + + songComplete() { + this.stop(); + this.showCompletionMessage(); + this.playCompletionSound(); + } + + showCompletionMessage() { + const stars = Math.floor((this.score / 1000) * 5); // Max 5 stars for 1000+ points + const starElements = document.querySelectorAll('.star'); + + starElements.forEach((star, index) => { + if (index < stars) { + star.classList.add('active'); + } else { + star.classList.remove('active'); + } + }); + + setTimeout(() => { + alert(`๐ŸŽ‰ Song Complete! ๐ŸŽ‰\n\nScore: ${this.score}\nStars: ${stars}/5\n\nGreat singing!`); + }, 500); + } + + updateControls() { + const playBtn = document.getElementById('play-btn'); + const pauseBtn = document.getElementById('pause-btn'); + const stopBtn = document.getElementById('stop-btn'); + const restartBtn = document.getElementById('restart-btn'); + + playBtn.disabled = this.isPlaying; + pauseBtn.disabled = !this.isPlaying; + stopBtn.disabled = !this.currentSong; + restartBtn.disabled = !this.currentSong; + } + + updateDisplay() { + document.getElementById('score').textContent = this.score; + document.getElementById('performance-fill').style.width = this.performance + '%'; + + const performanceText = document.getElementById('performance-text'); + if (this.performance >= 80) { + performanceText.textContent = 'Amazing! โญ'; + performanceText.style.color = '#6bcf7f'; + } else if (this.performance >= 60) { + performanceText.textContent = 'Great job! ๐Ÿ‘'; + performanceText.style.color = '#ffd93d'; + } else if (this.performance >= 40) { + performanceText.textContent = 'Keep singing! ๐ŸŽค'; + performanceText.style.color = '#ff6b6b'; + } else { + performanceText.textContent = this.currentSong ? 'Sing along!' : 'Ready to sing!'; + performanceText.style.color = '#ffffff'; + } + } + + playBackgroundMusic() { + if (!this.audioContext) return; + + // Create a simple background melody + const notes = [261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25]; // C4 to C5 + + let noteIndex = 0; + const playNote = () => { + if (!this.isPlaying) return; + + const frequency = notes[noteIndex % notes.length]; + this.playTone(frequency, 0.5); + + noteIndex++; + setTimeout(playNote, 1000); // Play a note every second + }; + + playNote(); + } + + playTone(frequency, duration) { + if (!this.audioContext) return; + + const oscillator = this.audioContext.createOscillator(); + const gainNode = this.audioContext.createGain(); + + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime); + + gainNode.gain.setValueAtTime(0.1, this.audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration); + + oscillator.connect(gainNode); + gainNode.connect(this.audioContext.destination); + + oscillator.start(); + oscillator.stop(this.audioContext.currentTime + duration); + } + + playCompletionSound() { + if (!this.audioContext) return; + + // Play a celebratory chord + const frequencies = [261.63, 329.63, 392.00]; // C4, E4, G4 + frequencies.forEach((freq, index) => { + setTimeout(() => this.playTone(freq, 1), index * 100); + }); + } + + formatTime(seconds) { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; + } +} + +// Initialize the game when the page loads +document.addEventListener('DOMContentLoaded', () => { + new KaraokeStar(); +}); + +// Enable audio on first user interaction +document.addEventListener('click', () => { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + if (audioContext && audioContext.state === 'suspended') { + audioContext.resume(); + } +}, { once: true }); \ No newline at end of file diff --git a/games/karaoke-star/style.css b/games/karaoke-star/style.css new file mode 100644 index 00000000..4779b048 --- /dev/null +++ b/games/karaoke-star/style.css @@ -0,0 +1,419 @@ +/* Karaoke Star Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #ffffff; + min-height: 100vh; + overflow-x: hidden; +} + +body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%), + radial-gradient(circle at 40% 40%, rgba(120, 219, 226, 0.3) 0%, transparent 50%); + pointer-events: none; + z-index: -1; +} + +.container { + max-width: 1000px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 30px; +} + +header h1 { + font-size: 3em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.5); + background: linear-gradient(45deg, #ff6b6b, #ffd93d, #6bcf7f, #4d96ff, #9c6ade); + background-size: 400% 400%; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + animation: gradientShift 3s ease infinite; +} + +@keyframes gradientShift { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +header p { + font-size: 1.3em; + opacity: 0.9; +} + +.game-area { + display: flex; + flex-direction: column; + gap: 20px; +} + +.song-selection { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 15px; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.song-selection h3 { + color: #ffd93d; + margin-bottom: 15px; + text-align: center; +} + +.song-buttons { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 10px; +} + +.song-btn { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + border: none; + padding: 12px 20px; + border-radius: 25px; + font-size: 1em; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.song-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); + background: linear-gradient(135deg, #764ba2, #667eea); +} + +.song-btn.active { + background: linear-gradient(135deg, #ffd93d, #ff6b6b); + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { box-shadow: 0 4px 15px rgba(255, 217, 61, 0.4); } + 50% { box-shadow: 0 4px 25px rgba(255, 217, 61, 0.8); } + 100% { box-shadow: 0 4px 15px rgba(255, 217, 61, 0.4); } +} + +.karaoke-display { + display: grid; + grid-template-columns: 200px 1fr; + gap: 20px; + background: rgba(255, 255, 255, 0.1); + padding: 30px; + border-radius: 20px; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + min-height: 300px; +} + +.album-art { + display: flex; + align-items: center; + justify-content: center; +} + +.album-placeholder { + width: 150px; + height: 150px; + background: linear-gradient(135deg, #ff6b6b, #ffd93d); + border-radius: 20px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + animation: rotate 10s linear infinite; +} + +@keyframes rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.music-note { + font-size: 4em; + color: white; + text-shadow: 2px 2px 4px rgba(0,0,0,0.5); +} + +.lyrics-container { + display: flex; + flex-direction: column; + justify-content: center; + gap: 20px; +} + +.lyrics-display { + text-align: center; +} + +.lyrics-line { + font-size: 1.8em; + line-height: 1.4; + margin: 10px 0; + min-height: 60px; + display: flex; + align-items: center; + justify-content: center; +} + +#current-line { + color: #ffd93d; + font-weight: bold; + text-shadow: 0 0 20px rgba(255, 217, 61, 0.5); + animation: glow 2s ease-in-out infinite alternate; +} + +@keyframes glow { + from { text-shadow: 0 0 20px rgba(255, 217, 61, 0.5); } + to { text-shadow: 0 0 30px rgba(255, 217, 61, 0.8), 0 0 40px rgba(255, 217, 61, 0.6); } +} + +#next-line { + color: rgba(255, 255, 255, 0.6); + font-style: italic; +} + +.progress-container { + width: 100%; +} + +.progress-bar { + width: 100%; + height: 8px; + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; + overflow: hidden; + margin-bottom: 10px; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #ff6b6b, #ffd93d, #6bcf7f); + border-radius: 4px; + width: 0%; + transition: width 0.3s ease; + box-shadow: 0 0 10px rgba(255, 217, 61, 0.5); +} + +.time-display { + text-align: center; + font-size: 0.9em; + color: rgba(255, 255, 255, 0.8); +} + +.controls { + display: flex; + justify-content: center; + gap: 15px; + flex-wrap: wrap; +} + +.control-btn { + background: linear-gradient(135deg, #4d96ff, #9c6ade); + color: white; + border: none; + padding: 12px 20px; + border-radius: 25px; + font-size: 1em; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.control-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); +} + +.control-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.scoring { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 15px; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.score-display h3 { + color: #ffd93d; + margin-bottom: 10px; +} + +#score { + font-size: 2em; + font-weight: bold; +} + +.stars { + display: flex; + gap: 5px; + margin-top: 10px; +} + +.star { + font-size: 1.5em; + opacity: 0.3; + transition: opacity 0.3s ease; +} + +.star.active { + opacity: 1; + animation: starTwinkle 1s ease-in-out; +} + +@keyframes starTwinkle { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.2); } +} + +.performance-meter { + text-align: center; +} + +.performance-meter h4 { + color: #6bcf7f; + margin-bottom: 10px; +} + +.meter { + width: 100%; + height: 20px; + background: rgba(255, 255, 255, 0.2); + border-radius: 10px; + overflow: hidden; + margin-bottom: 10px; +} + +.meter-fill { + height: 100%; + background: linear-gradient(90deg, #ff6b6b, #ffd93d, #6bcf7f); + width: 0%; + transition: width 0.5s ease; + border-radius: 10px; +} + +.performance-text { + font-size: 0.9em; + color: rgba(255, 255, 255, 0.8); +} + +.instructions { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 15px; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.instructions h3 { + color: #4d96ff; + margin-bottom: 15px; +} + +.instructions ul { + list-style: none; + padding: 0; +} + +.instructions li { + margin-bottom: 8px; + padding-left: 20px; + position: relative; +} + +.instructions li:before { + content: "๐ŸŽค"; + position: absolute; + left: 0; + color: #ffd93d; +} + +.microphone-tip { + text-align: center; + margin-top: 20px; + padding: 15px; + background: rgba(255, 119, 198, 0.2); + border-radius: 10px; + border: 1px solid rgba(255, 119, 198, 0.3); +} + +.microphone-tip strong { + color: #ff77c6; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .karaoke-display { + grid-template-columns: 1fr; + text-align: center; + } + + .album-placeholder { + width: 120px; + height: 120px; + } + + .lyrics-line { + font-size: 1.4em; + } + + .song-buttons { + grid-template-columns: 1fr; + } + + .scoring { + grid-template-columns: 1fr; + } + + .controls { + flex-direction: column; + align-items: center; + } +} + +@media (max-width: 480px) { + header h1 { + font-size: 2.2em; + } + + .lyrics-line { + font-size: 1.2em; + min-height: 50px; + } + + .control-btn { + width: 100%; + max-width: 200px; + } +} \ No newline at end of file diff --git a/games/key-flip/index.html b/games/key-flip/index.html new file mode 100644 index 00000000..a2b7c340 --- /dev/null +++ b/games/key-flip/index.html @@ -0,0 +1,33 @@ + + + + + + Key Flip Game | Mini JS Games Hub + + + +
    +

    Key Flip Game ๐ŸŽน

    +
    + Score: 0 + Level: 1 +
    + +
    + +
    + +
    + + + +
    + + + +
    + + + + diff --git a/games/key-flip/script.js b/games/key-flip/script.js new file mode 100644 index 00000000..45201269 --- /dev/null +++ b/games/key-flip/script.js @@ -0,0 +1,83 @@ +const gameArea = document.getElementById("game-area"); +const scoreEl = document.getElementById("score"); +const levelEl = document.getElementById("level"); +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const restartBtn = document.getElementById("restart-btn"); +const hitSound = document.getElementById("hit-sound"); +const successSound = document.getElementById("success-sound"); + +let score = 0; +let level = 1; +let obstacles = []; +let gameInterval; +let gamePaused = false; + +function createObstacle() { + const obs = document.createElement("div"); + obs.classList.add("obstacle"); + obs.style.left = Math.random() * (gameArea.offsetWidth - 50) + "px"; + obs.dataset.key = String.fromCharCode(65 + Math.floor(Math.random() * 6)); // A-F + gameArea.appendChild(obs); + obstacles.push(obs); + obs.style.animationDuration = `${2 - level*0.1}s`; // faster as level increases +} + +function moveObstacles() { + obstacles.forEach((obs, index) => { + let top = parseFloat(getComputedStyle(obs).top); + top += 5; + obs.style.top = top + "px"; + + if (top >= gameArea.offsetHeight) { + gameArea.removeChild(obs); + obstacles.splice(index, 1); + } + }); +} + +function startGame() { + if (gameInterval) clearInterval(gameInterval); + gameInterval = setInterval(() => { + if (!gamePaused) { + createObstacle(); + moveObstacles(); + } + }, 1000 - level*50); +} + +function pauseGame() { + gamePaused = !gamePaused; +} + +function restartGame() { + obstacles.forEach(obs => obs.remove()); + obstacles = []; + score = 0; + level = 1; + scoreEl.textContent = score; + levelEl.textContent = level; + startGame(); +} + +document.addEventListener("keydown", (e) => { + obstacles.forEach((obs, index) => { + if (e.key.toUpperCase() === obs.dataset.key) { + hitSound.currentTime = 0; + hitSound.play(); + score += 10; + scoreEl.textContent = score; + gameArea.removeChild(obs); + obstacles.splice(index, 1); + if (score % 50 === 0) { + level += 1; + levelEl.textContent = level; + successSound.play(); + } + } + }); +}); + +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", pauseGame); +restartBtn.addEventListener("click", restartGame); diff --git a/games/key-flip/style.css b/games/key-flip/style.css new file mode 100644 index 00000000..17577cf0 --- /dev/null +++ b/games/key-flip/style.css @@ -0,0 +1,73 @@ +body { + font-family: 'Arial', sans-serif; + background-color: #111; + color: #fff; + display: flex; + flex-direction: column; + align-items: center; + justify-content: start; + min-height: 100vh; + padding: 20px; +} + +.game-container { + width: 90%; + max-width: 700px; + text-align: center; +} + +h1 { + font-size: 2em; + text-shadow: 0 0 10px #00ffea, 0 0 20px #00ffea; +} + +.game-info { + margin: 15px 0; + font-size: 1.2em; + display: flex; + justify-content: space-around; +} + +.game-area { + position: relative; + width: 100%; + height: 400px; + background: linear-gradient(90deg, #111 0%, #222 100%); + border-radius: 12px; + overflow: hidden; + box-shadow: 0 0 20px #00ffea inset; + margin-bottom: 20px; +} + +.obstacle { + position: absolute; + width: 50px; + height: 50px; + background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Red_circle.svg/2048px-Red_circle.svg.png') no-repeat center center; + background-size: cover; + top: -60px; + animation: fall linear; +} + +@keyframes fall { + 0% { top: -60px; } + 100% { top: 400px; } +} + +.controls button { + padding: 10px 20px; + margin: 0 5px; + border: none; + border-radius: 8px; + background-color: #00ffea; + color: #111; + font-weight: bold; + cursor: pointer; + box-shadow: 0 0 15px #00ffea; + transition: 0.2s; +} + +.controls button:hover { + transform: scale(1.1); + box-shadow: 0 0 25px #00ffea; +} diff --git a/games/keyboard-race/index.html b/games/keyboard-race/index.html new file mode 100644 index 00000000..a9a2d309 --- /dev/null +++ b/games/keyboard-race/index.html @@ -0,0 +1,93 @@ + + + + + + Keyboard Race | Mini JS Games Hub + + + + + +
    +
    +
    + โŒจ๏ธ +
    +

    Keyboard Race

    +

    Press A (Player 1) and L (Player 2). First to the finish wins!

    +
    +
    + +
    +
    + + + +
    +
    + + +
    +
    +
    + +
    +
    + +
    + +
    +
    +
    + Player 1 avatar +
    Player 1 (A)
    +
    +
    +
    0%
    +
    Moves: 0
    +
    +
    + +
    VS
    + +
    +
    + Player 2 avatar +
    Player 2 (L)
    +
    +
    +
    0%
    +
    Moves: 0
    +
    +
    +
    + +
    +
    Press Start to begin the race.
    +
    + Progress + โ— Obstacle +
    +
    +
    + +
    +
    + Tip: Hold down the key for rapid presses but avoid auto-repeat โ€” rapid tapping is more reliable! +
    + +
    +
    + + + + diff --git a/games/keyboard-race/script.js b/games/keyboard-race/script.js new file mode 100644 index 00000000..9d57ca99 --- /dev/null +++ b/games/keyboard-race/script.js @@ -0,0 +1,319 @@ +/* Keyboard Race - Advanced + Player 1: key 'a' or 'A' + Player 2: key 'l' or 'L' + Features: + - Bulb/step track visualization + - Obstacles that freeze player for a short time + - Start / Pause / Restart + - Sounds & music (online URLs) with toggle + - LocalStorage win tracking (so hub can read game plays/wins) +*/ + +(() => { + // CONFIG + const STEPS = 20; // number of bulbs/steps to finish + const MOVE_PER_PRESS = 1; // steps increment per valid press + const OBSTACLE_COUNT = 3; // obstacles per race + const FREEZE_MS = 900; // freeze duration on obstacle hit + + // Online assets (public domain / google actions sounds used where possible) + const ASSETS = { + startSound: 'https://actions.google.com/sounds/v1/cartoon/slide_whistle_to_drum_hit.ogg', + moveSound: 'https://actions.google.com/sounds/v1/alarms/beep_short.ogg', + obstacleSound: 'https://actions.google.com/sounds/v1/cartoon/clang_and_wobble.ogg', + winSound: 'https://actions.google.com/sounds/v1/cartoon/metal_clang.ogg', + bgMusic: 'https://cdn.pixabay.com/download/audio/2021/08/09/audio_0b7b30b578.mp3?filename=upbeat-retro-game-loop-111261.mp3' + }; + + // DOM + const trackEl = document.getElementById('track'); + const startBtn = document.getElementById('startBtn'); + const pauseBtn = document.getElementById('pauseBtn'); + const restartBtn = document.getElementById('restartBtn'); + const soundToggle = document.getElementById('soundToggle'); + const musicToggle = document.getElementById('musicToggle'); + const msgEl = document.getElementById('msg'); + const p1MovesEl = document.getElementById('p1-moves'); + const p2MovesEl = document.getElementById('p2-moves'); + + // state + let steps = []; + let obstacles = new Set(); + let gameRunning = false; + let paused = false; + let p1 = { pos: 0, frozenUntil: 0, moves: 0 }; + let p2 = { pos: 0, frozenUntil: 0, moves: 0 }; + let audioEnabled = true; + let musicEnabled = true; + let bgAudio = null; + let sounds = {}; + + // load or init local win/play data to integrate with hub tracking: + const GAME_KEY = 'Keyboard Race'; + const playData = JSON.parse(localStorage.getItem('gamePlays') || '{}'); + + // helper: safe audio creation + function createAudio(src, loop = false, vol = 0.85) { + try { + const a = new Audio(src); + a.loop = !!loop; + a.volume = Math.min(Math.max(vol, 0), 1); + // some browsers require user gesture before play โ€” we just have audio ready. + return a; + } catch (e) { + return null; + } + } + + function loadSounds() { + sounds.start = createAudio(ASSETS.startSound, false, 0.8); + sounds.move = createAudio(ASSETS.moveSound, false, 0.7); + sounds.obst = createAudio(ASSETS.obstacleSound, false, 0.8); + sounds.win = createAudio(ASSETS.winSound, false, 1); + bgAudio = createAudio(ASSETS.bgMusic, true, 0.45); + } + + // build track + function buildTrack() { + trackEl.innerHTML = ''; + steps = []; + for (let i = 0; i < STEPS; i++) { + const step = document.createElement('div'); + step.className = 'step'; + step.dataset.index = i; + trackEl.appendChild(step); + steps.push(step); + } + + // place finish marker: slightly larger last step + if (steps.length) { + steps[STEPS - 1].style.border = '2px solid rgba(255,255,255,0.06)'; + } + } + + // randomly set obstacles (not first or last) + function placeObstacles() { + obstacles.clear(); + const safeRange = [...Array(STEPS - 2).keys()].map(i => i + 1); // 1..STEPS-2 + // shuffle safeRange + for (let i = safeRange.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [safeRange[i], safeRange[j]] = [safeRange[j], safeRange[i]]; + } + const chosen = safeRange.slice(0, OBSTACLE_COUNT); + chosen.forEach(i => { + obstacles.add(i); + steps[i].classList.add('obstacle'); + }); + } + + function resetUI() { + steps.forEach(s => { + s.classList.remove('lit', 'player1', 'player2'); + s.style.transform = ''; + }); + p1 = { pos: 0, frozenUntil: 0, moves: 0 }; + p2 = { pos: 0, frozenUntil: 0, moves: 0 }; + updatePlayerUI(); + } + + function updatePlayerUI() { + // light steps for each player + steps.forEach((s, idx) => { + s.classList.remove('player1', 'player2', 'lit'); + if (idx <= p1.pos - 1) { + s.classList.add('lit', 'player1'); + } + if (idx <= p2.pos - 1) { + s.classList.add('lit', 'player2'); + } + // if both reached same index keep both styles (player2 overrides visually by later add) + }); + + // set moves & progress text + const p1pct = Math.min(100, Math.round((p1.pos / (STEPS - 1)) * 100)); + const p2pct = Math.min(100, Math.round((p2.pos / (STEPS - 1)) * 100)); + document.querySelector('.player1 .progress').textContent = `${p1pct}%`; + document.querySelector('.player2 .progress').textContent = `${p2pct}%`; + p1MovesEl.textContent = p1.moves; + p2MovesEl.textContent = p2.moves; + } + + function canMove(player) { + return Date.now() >= player.frozenUntil; + } + + function applyObstacle(player, playerId) { + player.frozenUntil = Date.now() + FREEZE_MS; + // small flash on current step + const idx = Math.max(0, player.pos - 1); + const el = steps[idx] || null; + if (el) { + el.classList.add('shake'); + setTimeout(() => el.classList.remove('shake'), 420); + } + if (audioEnabled && sounds.obst) { + try { sounds.obst.currentTime = 0; sounds.obst.play(); } catch (e) {} + } + showMsg(`${playerId} hit an obstacle! Frozen briefly.`); + } + + function handleMove(player, playerId) { + if (!gameRunning || paused) return; + if (!canMove(player)) return; // frozen + // move + player.pos = Math.min(STEPS - 1, player.pos + MOVE_PER_PRESS); + player.moves++; + if (audioEnabled && sounds.move) { + try { sounds.move.currentTime = 0; sounds.move.play(); } catch (e) {} + } + // check obstacle on newly lit position (if not finish) + if (obstacles.has(player.pos - 1)) { + applyObstacle(player, playerId); + } + updatePlayerUI(); + checkWin(); + } + + function checkWin() { + if (p1.pos >= STEPS - 1 || p2.pos >= STEPS - 1) { + gameRunning = false; + pauseBtn.disabled = true; + restartBtn.disabled = false; + startBtn.disabled = false; + const winner = p1.pos >= STEPS - 1 ? 'Player 1' : 'Player 2'; + showMsg(`${winner} wins! ๐ŸŽ‰`); + if (audioEnabled && sounds.win) { + try { sounds.win.currentTime = 0; sounds.win.play(); } catch (e) {} + } + recordWin(winner); + stopBG(); + } + } + + function showMsg(text) { + msgEl.innerHTML = text; + } + + // record win to localStorage so main hub can read stats + function recordWin(winner) { + if (!playData[GAME_KEY]) playData[GAME_KEY] = { plays: 0, wins: { 'Player 1': 0, 'Player 2': 0 } }; + playData[GAME_KEY].plays += 1; + playData[GAME_KEY].wins[winner] = (playData[GAME_KEY].wins[winner] || 0) + 1; + localStorage.setItem('gamePlays', JSON.stringify(playData)); + // also call hub play tracker if present (the hub listens for .play-button clicks, but we can increment here) + } + + // controls + function startGame() { + if (gameRunning) return; + // reset some state + gameRunning = true; + paused = false; + startBtn.disabled = true; + pauseBtn.disabled = false; + restartBtn.disabled = false; + showMsg('Race on! Smash your keys: A (Player 1) and L (Player 2).'); + + resetUI(); + buildTrack(); // rebuild visual if needed + placeObstacles(); + updatePlayerUI(); + + // play start sound and music + if (audioEnabled && sounds.start) { try { sounds.start.currentTime = 0; sounds.start.play(); } catch(e){} } + if (musicEnabled && bgAudio) { try { bgAudio.currentTime = 0; bgAudio.play(); } catch(e) {} } + } + + function pauseGame() { + if (!gameRunning) return; + paused = !paused; + pauseBtn.textContent = paused ? 'Resume' : 'Pause'; + showMsg(paused ? 'Paused' : 'Resumed'); + if (paused) { + // pause music + if (bgAudio) try { bgAudio.pause(); } catch(e){} + } else { + if (musicEnabled && bgAudio) try { bgAudio.play(); } catch(e){} + } + } + + function restartGame() { + gameRunning = false; + paused = false; + startBtn.disabled = false; + pauseBtn.disabled = true; + pauseBtn.textContent = 'Pause'; + restartBtn.disabled = true; + stopBG(); + buildTrack(); + placeObstacles(); + resetUI(); + showMsg('Ready. Press Start to race!'); + } + + function stopBG() { + if (bgAudio) try { bgAudio.pause(); bgAudio.currentTime = 0; } catch(e){} + } + + // keyboard listener + function onKey(e) { + if (!gameRunning || paused) return; + const k = e.key.toLowerCase(); + if (k === 'a') { + handleMove(p1, 'Player 1'); + } else if (k === 'l') { + handleMove(p2, 'Player 2'); + } + } + + // init + function init() { + loadSounds(); + buildTrack(); + placeObstacles(); + resetUI(); + showMsg('Press Start to begin. Player1: A โ€” Player2: L'); + + // attach event handlers + startBtn.addEventListener('click', () => { + startGame(); + // register a play (so hub shows "play" counts when user opens via Play Now) + try { + const hubPlays = JSON.parse(localStorage.getItem('gamePlays') || '{}'); + if (!hubPlays[GAME_KEY]) hubPlays[GAME_KEY] = { plays: 0, wins: { 'Player 1': 0, 'Player 2': 0 } }; + // increment plays when start pressed + hubPlays[GAME_KEY].plays += 1; + localStorage.setItem('gamePlays', JSON.stringify(hubPlays)); + } catch(e){} + }); + + pauseBtn.addEventListener('click', pauseGame); + restartBtn.addEventListener('click', restartGame); + + // toggles + soundToggle.addEventListener('change', (ev) => audioEnabled = ev.target.checked); + musicToggle.addEventListener('change', (ev) => { + musicEnabled = ev.target.checked; + if (!musicEnabled) stopBG(); + else if (musicEnabled && gameRunning) try { bgAudio.play(); } catch(e){} + }); + + // keyboard listener - use keydown for responsive presses + window.addEventListener('keydown', (e) => { + // Prevent default for space/scroll keys only when game running + if (gameRunning && (e.key === ' ' || e.key === 'Spacebar')) e.preventDefault(); + // direct call + onKey(e); + }); + + // enable / disable pause/restart appropriately + // initial button states + pauseBtn.disabled = true; + restartBtn.disabled = true; + } + + // start + init(); + +})(); diff --git a/games/keyboard-race/style.css b/games/keyboard-race/style.css new file mode 100644 index 00000000..42f7806d --- /dev/null +++ b/games/keyboard-race/style.css @@ -0,0 +1,169 @@ +:root{ + --bg:#0f1724; + --card:#071126; + --muted:#9aa7b2; + --accent:#8de1ff; + --accent-2:#ffd166; + --p1:#7efcb0; + --p2:#ffd28a; + --glass: rgba(255,255,255,0.03); + --glass-2: rgba(255,255,255,0.04); + --glow: 0 8px 30px rgba(137, 227, 255, .06); + --glass-border: rgba(255,255,255,0.04); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; +} + +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + background: linear-gradient(180deg, #071129 0%, #07142b 55%, #02111a 100%); + color:#e6f3fb; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + display:flex; + align-items:center; + justify-content:center; + padding:28px; +} + +/* wrapper */ +.wrap{ + width:1100px; + max-width:96%; + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border-radius:14px; + box-shadow: 0 12px 40px rgba(2,8,23,0.6); + overflow:hidden; + border:1px solid rgba(255,255,255,0.03); +} + +/* topbar */ +.topbar{ + display:flex; + justify-content:space-between; + align-items:flex-start; + padding:20px 26px; + gap:20px; + border-bottom:1px solid rgba(255,255,255,0.02); +} +.title{ + display:flex; + gap:14px; + align-items:center; +} +.title .emoji{ + font-size:36px; + transform:translateY(-2px); +} +.title h1{margin:0;font-size:20px} +.title .subtitle{margin:2px 0 0;font-size:13px;color:var(--muted)} + +/* controls */ +.controls-row{display:flex;align-items:center;gap:18px} +.controls .btn{ + background:transparent; + color:var(--accent); + border:1px solid rgba(141,225,255,0.08); + padding:8px 12px; + font-weight:600; + border-radius:8px; + cursor:pointer; + transition:all .16s ease; +} +.controls .btn:hover{transform:translateY(-2px)} +.controls .btn.primary{ + background:linear-gradient(90deg,var(--accent),#6bf0f7); + color:#012028; + box-shadow: 0 8px 28px rgba(0,160,200,0.12); + border:1px solid rgba(255,255,255,0.06); +} +.controls .btn:disabled{opacity:.45;cursor:not-allowed;transform:none} + +.toggles{display:flex;gap:10px;align-items:center;font-size:13px;color:var(--muted)} +.label-inline{display:flex;gap:8px;align-items:center;user-select:none} + +/* arena */ +.arena{padding:24px 30px 36px;display:flex;flex-direction:column;gap:18px} +.track{ + height:120px; + background:linear-gradient(90deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + display:flex; + align-items:center; + padding:18px; + border-radius:12px; + border:1px solid rgba(255,255,255,0.02); + gap:10px; + overflow:hidden; + position:relative; +} + +/* bulbs as track steps */ +.track .step{ + width:calc((100% - 60px) / 20); /* JS sets number of steps to 20, CSS width uses calc fallback */ + min-width:26px; + height:26px; + border-radius:14px; + background:linear-gradient(180deg,rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + display:flex; + align-items:center; + justify-content:center; + color:rgba(255,255,255,0.06); + font-size:12px; + border:1px solid rgba(255,255,255,0.02); + transition:all .12s linear; + box-shadow: inset 0 -6px 16px rgba(0,0,0,0.25); + position:relative; +} +.track .step.lit{ + color:#02131a; + background: linear-gradient(180deg, rgba(142,241,255,1), rgba(125,238,219,0.85)); + transform:scale(1.16) translateY(-2px); + box-shadow: 0 8px 28px rgba(141,225,255,0.12); +} +.track .step.player1.lit{ + background: linear-gradient(180deg,var(--p1), rgba(125,238,219,0.9)); +} +.track .step.player2.lit{ + background: linear-gradient(180deg,var(--p2), rgba(255,210,130,0.9)); +} + +/* obstacle */ +.track .step.obstacle::after{ + content:"โ—"; + position:absolute; + top:-6px;right:-6px; + font-size:14px; + filter:drop-shadow(0 6px 12px rgba(0,0,0,0.6)); +} + +/* players area */ +.players{display:flex;align-items:center;justify-content:space-between;padding:8px 2px} +.player-card{width:320px;background:var(--glass);border-radius:12px;padding:12px;display:flex;gap:12px;align-items:center;border:1px solid var(--glass-border);backdrop-filter: blur(6px)} +.player-card .avatar{width:84px;height:84px;border-radius:10px;overflow:hidden;position:relative;flex-shrink:0;border:1px solid rgba(255,255,255,0.04)} +.player-card img{width:100%;height:100%;object-fit:cover;display:block} +.player-card .player__label{position:absolute;left:8px;bottom:6px;background:rgba(0,0,0,0.28);padding:6px 8px;border-radius:8px;font-weight:700;font-size:12px} +.player-card .stats{flex:1;color:var(--muted);font-size:13px} +.player-card .stats .progress{font-weight:700;color:var(--accent);margin-bottom:6px} + +/* VS */ +.vs{font-weight:900;font-size:18px;color:var(--muted)} + +/* info */ +.info{display:flex;justify-content:space-between;align-items:center;padding:0 2px;color:var(--muted);font-size:13px} +.legend{display:flex;gap:12px;align-items:center} +.legend-item{display:flex;gap:8px;align-items:center} +.bulb.mini{width:12px;height:12px;border-radius:6px;display:inline-block;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.03)} +.bulb.mini.lit{background:linear-gradient(180deg,var(--accent),#6bf0f7);box-shadow:0 6px 18px rgba(107,240,247,0.12)} + +/* footer */ +.footer{display:flex;justify-content:space-between;align-items:center;padding:12px 20px;border-top:1px solid rgba(255,255,255,0.02);font-size:13px;color:var(--muted)} +.footer .back{color:var(--muted);text-decoration:none} + +/* responsive */ +@media (max-width:900px){ + .wrap{width:100%} + .player-card{width:45%} + .vs{display:none} + .track{height:90px} +} diff --git a/games/knife-thrower/index.html b/games/knife-thrower/index.html new file mode 100644 index 00000000..69617d36 --- /dev/null +++ b/games/knife-thrower/index.html @@ -0,0 +1,26 @@ + + + + + + Knife Thrower | Mini JS Games Hub + + + +
    +

    Knife Thrower

    + +
    + + + + +
    +

    Score: 0

    + + + +
    + + + diff --git a/games/knife-thrower/script.js b/games/knife-thrower/script.js new file mode 100644 index 00000000..cb17d466 --- /dev/null +++ b/games/knife-thrower/script.js @@ -0,0 +1,146 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); + +const startBtn = document.getElementById('startBtn'); +const pauseBtn = document.getElementById('pauseBtn'); +const resumeBtn = document.getElementById('resumeBtn'); +const restartBtn = document.getElementById('restartBtn'); + +const hitSound = document.getElementById('hitSound'); +const throwSound = document.getElementById('throwSound'); +const gameOverSound = document.getElementById('gameOverSound'); + +let animationId; +let paused = false; + +// Board +const board = { + x: canvas.width/2, + y: 200, + radius: 80, + knives: [], + rotation: 0, + speed: 0.02 +}; + +let currentKnife = { x: canvas.width/2, y: 550, width: 5, height: 30 }; +let knivesStuck = []; +let score = 0; + +function drawBoard() { + ctx.save(); + ctx.translate(board.x, board.y); + ctx.rotate(board.rotation); + ctx.fillStyle = '#333'; + ctx.shadowColor = '#ff0000'; + ctx.shadowBlur = 20; + ctx.beginPath(); + ctx.arc(0, 0, board.radius, 0, Math.PI*2); + ctx.fill(); + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 4; + ctx.stroke(); + + // Draw stuck knives + knivesStuck.forEach(k => { + ctx.fillStyle = '#fff'; + ctx.fillRect(-k.width/2, -board.radius - k.height, k.width, k.height); + }); + + ctx.restore(); +} + +function drawKnife() { + ctx.fillStyle = '#fff'; + ctx.fillRect(currentKnife.x - currentKnife.width/2, currentKnife.y - currentKnife.height, currentKnife.width, currentKnife.height); +} + +function clear() { + ctx.clearRect(0, 0, canvas.width, canvas.height); +} + +function checkCollision() { + // Check angular collision + for (let k of knivesStuck) { + let angle = Math.atan2(0 - board.y, currentKnife.x - board.x); // approximate + // simple distance check + if(Math.abs(angle) < 0.1) { + return true; + } + } + return false; +} + +function throwKnife() { + throwSound.play(); + let hit = false; + let knifeInterval = setInterval(() => { + currentKnife.y -= 15; + if(currentKnife.y <= board.y + board.radius) { + if(checkCollision()) { + gameOver(); + clearInterval(knifeInterval); + return; + } else { + knivesStuck.push({...currentKnife}); + currentKnife.y = 550; + score++; + hitSound.play(); + document.getElementById('score').textContent = `Score: ${score}`; + clearInterval(knifeInterval); + return; + } + } + draw(); + }, 20); +} + +function draw() { + clear(); + drawBoard(); + drawKnife(); +} + +function update() { + if(!paused){ + board.rotation += board.speed; + draw(); + animationId = requestAnimationFrame(update); + } +} + +function gameOver() { + gameOverSound.play(); + alert(`Game Over! Score: ${score}`); + cancelAnimationFrame(animationId); +} + +startBtn.addEventListener('click', () => { + paused = false; + update(); +}); + +pauseBtn.addEventListener('click', () => { + paused = true; +}); + +resumeBtn.addEventListener('click', () => { + if(paused){ + paused = false; + update(); + } +}); + +restartBtn.addEventListener('click', () => { + paused = false; + knivesStuck = []; + currentKnife.y = 550; + score = 0; + document.getElementById('score').textContent = `Score: ${score}`; + update(); +}); + +canvas.addEventListener('click', throwKnife); + +// Initial draw +draw(); diff --git a/games/knife-thrower/style.css b/games/knife-thrower/style.css new file mode 100644 index 00000000..8f02c5d4 --- /dev/null +++ b/games/knife-thrower/style.css @@ -0,0 +1,49 @@ +body { + margin: 0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: radial-gradient(circle, #0a0a0a, #1f1f1f); + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + text-align: center; +} + +canvas { + background-color: #111; + border: 2px solid #fff; + border-radius: 10px; + box-shadow: 0 0 20px #ff0000, 0 0 40px #ff5500, 0 0 60px #ffaa00; +} + +.controls { + margin-top: 10px; +} + +button { + background: #222; + color: #fff; + border: 1px solid #fff; + padding: 8px 12px; + margin: 5px; + border-radius: 5px; + cursor: pointer; + font-weight: bold; + box-shadow: 0 0 10px #ff0000; + transition: 0.2s; +} + +button:hover { + background: #ff0000; + box-shadow: 0 0 20px #ff6600, 0 0 40px #ffaa00; +} + +#score { + font-size: 20px; + margin-top: 10px; + text-shadow: 0 0 5px #ff0000, 0 0 10px #ff5500; +} diff --git a/games/lantern-path/index.html b/games/lantern-path/index.html new file mode 100644 index 00000000..a280486b --- /dev/null +++ b/games/lantern-path/index.html @@ -0,0 +1,68 @@ + + + + + + Lantern Path โ€” Mini JS Games Hub + + + +
    +
    +
    + +

    Lantern Path

    +
    +
    + + + + + +
    +
    + +
    + + +
    +
    +
    +
    Place lanterns to light the path.
    +
    +
    +
    + +
    +
    Made with โœจ โ€” Lantern Path
    +
    Graphics: Unsplash โ€ข Sound: WebAudio
    +
    +
    + + + + diff --git a/games/lantern-path/script.js b/games/lantern-path/script.js new file mode 100644 index 00000000..c784259d --- /dev/null +++ b/games/lantern-path/script.js @@ -0,0 +1,395 @@ +/* Lantern Path โ€” script.js + - Places nodes along a custom path (line-like) + - Click nodes to place a lantern if you have remaining + - Lantern lights radius (based on Euclidean distance) + - Obstacles block light (a simple blocking rule) + - Win when all path nodes are lit + - Undo, Hint, Pause/Start, Next Level supported + - Sounds generated via Web Audio API (no external files) +*/ + +(() => { + /* ---------- Level definitions ---------- + Each level: nodes: [{x,y}] coordinates 0..1 relative inside board, + obstacles: [nodeIndex] (tile is blocked), + lanterns: max lanterns allowed + radius: how many units (in px) each lantern lights (we'll scale) + */ + const levels = [ + { + name: "Intro", + nodes: [ {x:0.08,y:0.5},{x:0.18,y:0.46},{x:0.28,y:0.5},{x:0.38,y:0.52},{x:0.48,y:0.49},{x:0.58,y:0.47},{x:0.68,y:0.5},{x:0.78,y:0.53},{x:0.88,y:0.5} ], + obstacles: [], + lanterns: 3, + radius: 110 + }, + { + name: "Obstructed Crossing", + nodes: [ {x:0.06,y:0.6},{x:0.16,y:0.55},{x:0.26,y:0.5},{x:0.36,y:0.46},{x:0.46,y:0.44},{x:0.56,y:0.44},{x:0.66,y:0.46},{x:0.76,y:0.51},{x:0.86,y:0.54} ], + obstacles: [4,5], + lanterns: 4, + radius: 110 + }, + { + name: "Twist & Turns", + nodes: [ {x:0.06,y:0.45},{x:0.16,y:0.42},{x:0.26,y:0.45},{x:0.36,y:0.52},{x:0.46,y:0.58},{x:0.56,y:0.56},{x:0.66,y:0.50},{x:0.76,y:0.46},{x:0.86,y:0.44} ], + obstacles: [3], + lanterns: 4, + radius: 105 + }, + { + name: "Advanced", + nodes: [ {x:0.05,y:0.50},{x:0.14,y:0.44},{x:0.22,y:0.40},{x:0.32,y:0.44},{x:0.40,y:0.50},{x:0.48,y:0.56},{x:0.58,y:0.54},{x:0.70,y:0.50},{x:0.82,y:0.46},{x:0.92,y:0.48} ], + obstacles: [2,6], + lanterns: 5, + radius: 100 + } + ]; + + // DOM refs + const boardEl = document.getElementById('board'); + const startBtn = document.getElementById('startBtn'); + const pauseBtn = document.getElementById('pauseBtn'); + const restartBtn = document.getElementById('restartBtn'); + const levelSelect = document.getElementById('levelSelect'); + const levelNumEl = document.getElementById('levelNum'); + const lanternsLeftEl = document.getElementById('lanternsLeft'); + const gameStatusEl = document.getElementById('gameStatus'); + const messageEl = document.getElementById('message'); + const undoBtn = document.getElementById('undoBtn'); + const hintBtn = document.getElementById('hintBtn'); + const nextBtn = document.getElementById('nextBtn'); + const soundToggle = document.getElementById('soundToggle'); + + // state + let currentLevelIndex = 0; + let svg; // main svg + let nodeEls = []; + let placedLanterns = []; // [{nodeIndex, x,y}] + let lit = []; // boolean per node + let remainingLanterns = 0; + let running = false; + let history = []; + + // WebAudio for sounds (generate tones) + const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + function playTone(freq=440, duration=0.12, type='sine', gain=0.06){ + if (!soundToggle.checked) return; + const o = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + o.type = type; + o.frequency.value = freq; + g.gain.value = gain; + o.connect(g); g.connect(audioCtx.destination); + o.start(); + o.stop(audioCtx.currentTime + duration); + } + + /* Build level select */ + function initLevelSelect(){ + levelSelect.innerHTML = ''; + levels.forEach((lvl, i) => { + const opt = document.createElement('option'); + opt.value = i; + opt.textContent = `${i+1}. ${lvl.name}`; + levelSelect.appendChild(opt); + }); + levelSelect.value = currentLevelIndex; + } + + /* Render an SVG board for the level */ + function renderLevel(index){ + const lvl = levels[index]; + nodeEls = []; + placedLanterns = []; + lit = new Array(lvl.nodes.length).fill(false); + history = []; + remainingLanterns = lvl.lanterns; + levelNumEl.textContent = index + 1; + lanternsLeftEl.textContent = remainingLanterns; + gameStatusEl.textContent = 'Idle'; + messageEl.textContent = 'Click tiles to place lanterns. Light all path tiles to win.'; + // create svg + boardEl.innerHTML = ''; + svg = createSVG('svg',{width:'100%', height:'100%', viewBox:'0 0 1000 600', preserveAspectRatio:'xMidYMid meet'}); + boardEl.appendChild(svg); + + // background subtle gradient + const defs = createSVG('defs', {}); + const g1 = createSVG('radialGradient', {id:'g1', cx:'50%', cy:'20%', r:'60%'}); + g1.innerHTML = ``; + defs.appendChild(g1); + svg.appendChild(defs); + + // convert normalized coords to 1000x600 + const W = 1000, H = 600; + const coords = lvl.nodes.map(n => ({x: Math.round(n.x * W), y: Math.round(n.y * H)})); + + // draw path polyline (thin dim line) + const pathPoints = coords.map(p => `${p.x},${p.y}`).join(' '); + const pathLine = createSVG('polyline', {points: pathPoints, fill:'none', stroke:'rgba(255,255,255,0.04)', 'stroke-width':12, 'stroke-linecap':'round', 'stroke-linejoin':'round'}); + svg.appendChild(pathLine); + + // add nodes (clickable) + coords.forEach((p, idx) => { + const g = createSVG('g', {class: 'node', 'data-idx': idx, transform:`translate(${p.x},${p.y})`}); + const circle = createSVG('circle', {class:'node-circle', r:18}); + g.appendChild(circle); + + // glow ring + const glow = createSVG('circle', {class:'glow', r:40, fill:'none', stroke:'url(#g1)', 'stroke-width':40, 'stroke-opacity':0.06}); + g.appendChild(glow); + + // lantern icon (simple svg path of lantern) + const lantern = createSVG('g',{class:'lantern', transform:'translate(-12,-26) scale(1.1)'}); + lantern.innerHTML = ` + `; + g.appendChild(lantern); + + // obstacle indicator if obstacle + if (lvl.obstacles && lvl.obstacles.includes(idx)){ + const obs = createSVG('circle',{class:'obstacle-circle', r:12, fill:'rgba(80,20,20,0.95)'}); + g.appendChild(obs); + const cross = createSVG('text',{x:-6,y:6, 'font-size':16, fill:'#d9bdbd'}); + cross.textContent = 'โ›”'; + g.appendChild(cross); + } + + // append to svg + svg.appendChild(g); + nodeEls.push(g); + + // click handler + g.addEventListener('click', (ev) => { + if (!running) return; + if (lvl.obstacles && lvl.obstacles.includes(idx)) { + flashMessage('Cannot place lantern on obstacle.'); + playTone(220,0.08,'sawtooth',0.03); + return; + } + if (remainingLanterns <= 0){ + flashMessage('No lanterns left โ€” try undo or restart.'); + playTone(190,0.08,'square',0.02); + return; + } + if (isLanternPlacedAt(idx)) { + // toggle off + removeLantern(idx); + return; + } + placeLantern(idx); + }); + }); + + // initial rendering of lit state + updateLitVisuals(); + } + + /* Helpers to create svg elements */ + function createSVG(tag, attrs){ + const el = document.createElementNS('http://www.w3.org/2000/svg', tag); + Object.entries(attrs||{}).forEach(([k,v]) => el.setAttribute(k,v)); + return el; + } + + function isLanternPlacedAt(nodeIdx){ + return placedLanterns.some(l => l.nodeIndex === nodeIdx); + } + function placeLantern(nodeIdx, record=true){ + const lvl = levels[currentLevelIndex]; + if (remainingLanterns <= 0) return false; + const nodeG = nodeEls[nodeIdx]; + nodeG.classList.add('on'); + // compute position from transform + const t = nodeG.getCTM(); + // get coords from translate + const x = parseFloat(nodeG.getAttribute('transform').match(/translate\(([-\d.]+),([-\d.]+)\)/)[1]); + const y = parseFloat(nodeG.getAttribute('transform').match(/translate\(([-\d.]+),([-\d.]+)\)/)[2]); + placedLanterns.push({nodeIndex: nodeIdx, x, y, radius: lvl.radius}); + remainingLanterns--; + lanternsLeftEl.textContent = remainingLanterns; + playTone(880,0.06,'sine',0.06); + flashMessage('Lantern placed.'); + if (record) history.push({type:'place', idx:nodeIdx}); + updateLitState(); + return true; + } + + function removeLantern(nodeIdx, record=true){ + // remove first matching lantern + const idx = placedLanterns.findIndex(l => l.nodeIndex === nodeIdx); + if (idx === -1) return false; + nodeEls[nodeIdx].classList.remove('on'); + placedLanterns.splice(idx,1); + remainingLanterns++; + lanternsLeftEl.textContent = remainingLanterns; + playTone(420,0.05,'triangle',0.04); + if (record) history.push({type:'remove', idx:nodeIdx}); + updateLitState(); + return true; + } + + function updateLitState(){ + const lvl = levels[currentLevelIndex]; + const W = 1000, H = 600; + // compute coords array + const coords = lvl.nodes.map(n => ({x: Math.round(n.x * W), y: Math.round(n.y * H)})); + // Reset + lit = new Array(coords.length).fill(false); + // For each lantern, mark nodes within radius lit, unless blocked by obstacle straight-line (simple blocking: obstacle node itself just can't be lantern but does not fully block light between nodes) + placedLanterns.forEach(l => { + for (let i=0;i { + // node on if a lantern is placed here OR lit by some lantern + const isLit = lit[i] || placedLanterns.some(p=>p.nodeIndex===i); + if (isLit) g.classList.add('on'); else g.classList.remove('on'); + // but obstacle nodes remain with a darker base; we show vignette by opacity + if (lvl.obstacles && lvl.obstacles.includes(i)) { + g.querySelector('.node-circle').style.fill = isLit ? 'rgba(255,210,120,0.95)' : 'rgba(40,40,60,0.96)'; + } else { + g.querySelector('.node-circle').style.fill = isLit ? 'rgba(255,230,180,1)' : 'rgba(255,255,255,0.02)'; + } + // ensure lantern icon is visible only if lantern placed + const lantern = g.querySelector('.lantern'); + if (isLanternPlacedAt(i)){ + lantern.style.opacity = 1; + } else { + lantern.style.opacity = isLit ? 0.35 : 0; + } + }); + + // update lantern circles (visualize radius overlay blurred) + // remove existing radius overlays + svg.querySelectorAll('.radius-overlay').forEach(el => el.remove()); + // add for each lantern a soft radial + placedLanterns.forEach(l => { + const r = createSVG('circle',{class:'radius-overlay', cx:l.x, cy:l.y, r: l.radius}); + r.style.fill = 'rgba(255,200,80,0.06)'; + r.style.filter = 'blur(14px)'; + svg.insertBefore(r, svg.firstChild); + }); + } + + function checkWinCondition(){ + const lvl = levels[currentLevelIndex]; + // require all nodes to be lit + const allLit = lit.every(v => v); + if (allLit){ + running = false; + gameStatusEl.textContent = 'Won'; + messageEl.textContent = 'โœจ You lit the entire path โ€” well done!'; + playTone(1200,0.16,'sine',0.12); + } else if (remainingLanterns <= 0 && !placedLanterns.length){ + // improbable + } else if (remainingLanterns <= 0 && !lit.some(Boolean)){ + // no progress and none left + // not definitive lose โ€” only if no possible lit nodes remain + } else { + gameStatusEl.textContent = running ? 'Playing' : 'Idle'; + } + } + + function isPossibleToWin(){ + // rudimentary check: if there exists any unlit node and no remaining lanterns -> cannot win + if (remainingLanterns <= 0 && !lit.every(v=>v)) return false; + return true; + } + + function flashMessage(txt, timeout=2200){ + messageEl.textContent = txt; + setTimeout(()=> { if (messageEl.textContent === txt) messageEl.textContent = 'Place lanterns to light the path.'; }, timeout); + } + + /* Controls */ + startBtn.addEventListener('click', ()=> { + running = true; + gameStatusEl.textContent = 'Playing'; + messageEl.textContent = 'Game started. Place lanterns!'; + playTone(520,0.08,'sine',0.06); + }); + pauseBtn.addEventListener('click', ()=> { + running = !running; + if (running){ + gameStatusEl.textContent = 'Playing'; + messageEl.textContent = 'Resumed.'; + playTone(640,0.06,'sine',0.04); + } else { + gameStatusEl.textContent = 'Paused'; + messageEl.textContent = 'Paused โ€” click Start to continue.'; + playTone(220,0.06,'sine',0.03); + } + }); + restartBtn.addEventListener('click', ()=> { + renderLevel(currentLevelIndex); + playTone(380,0.08,'sawtooth',0.05); + }); + levelSelect.addEventListener('change', (e) => { + currentLevelIndex = parseInt(e.target.value,10); + renderLevel(currentLevelIndex); + playTone(760,0.06,'sine',0.06); + }); + undoBtn.addEventListener('click', ()=>{ + if (!history.length) return flashMessage('Nothing to undo.'); + const last = history.pop(); + if (last.type === 'place') removeLantern(last.idx, false); + else if (last.type === 'remove') placeLantern(last.idx, false); + playTone(300,0.05,'triangle',0.04); + }); + hintBtn.addEventListener('click', ()=>{ + // naive hint: find the first unlit node and temporarily highlight it + const lvl = levels[currentLevelIndex]; + const firstUnlit = lit.findIndex(v => !v); + if (firstUnlit === -1) { flashMessage('All lit already!'); return; } + const g = nodeEls[firstUnlit]; + const orig = g.querySelector('.node-circle').style.stroke; + g.querySelector('.node-circle').style.stroke = 'rgba(255,200,90,0.95)'; + g.animate([{transform:'scale(1)'},{transform:'scale(1.08)'}], {duration:700, iterations:2}); + setTimeout(()=> g.querySelector('.node-circle').style.stroke = orig, 1200); + playTone(980,0.08,'sine',0.05); + }); + nextBtn.addEventListener('click', ()=>{ + currentLevelIndex = Math.min(levels.length-1, currentLevelIndex+1); + levelSelect.value = currentLevelIndex; + renderLevel(currentLevelIndex); + playTone(980,0.08,'sine',0.06); + }); + + /* Undo / place helpers ended */ + + function removeAllOverlays(){ + svg && svg.querySelectorAll('.radius-overlay').forEach(e=>e.remove()); + } + + /* small utility: show a quick animation when cannot place */ + function animateNode(idx, keyframes=[{transform:'scale(1)'},{transform:'scale(1.06)'},{transform:'scale(1)'}], opts={duration:260}){ + nodeEls[idx] && nodeEls[idx].animate(keyframes, opts); + } + + /* init */ + function init(){ + initLevelSelect(); + renderLevel(currentLevelIndex); + // Start paused; pressing start required to enable placement + running = false; + // ensure audio context resume on first user interaction + document.body.addEventListener('click', ()=> { if (audioCtx.state === 'suspended') audioCtx.resume(); }, {once:true}); + } + init(); + + /* Export for debugging if needed */ + window.LanternPath = {levels, renderLevel, placeLantern, removeLantern}; + +})(); diff --git a/games/lantern-path/style.css b/games/lantern-path/style.css new file mode 100644 index 00000000..e682906b --- /dev/null +++ b/games/lantern-path/style.css @@ -0,0 +1,137 @@ +:root{ + --bg:#0b1020; + --card:#0f1724; + --accent:#ffd27a; + --muted:#9aa7bf; + --glass: rgba(255,255,255,0.04); + --glow: 0 8px 30px rgba(255,210,122,0.16), 0 2px 8px rgba(255,200,80,0.12); + --boardW: min(860px, 92vw); + --tileSize: 52px; +} + +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + font-family:Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; + background: radial-gradient(1000px 400px at 10% 10%, rgba(34,40,64,0.6), transparent), + linear-gradient(180deg,#061124 0%, #071426 50%, #02040a 100%), var(--bg); + color:#e6eef8; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + padding:20px; + display:flex; + align-items:center; + justify-content:center; + min-height:100vh; +} + +.app{ + width:100%; + max-width:1200px; + background: linear-gradient(180deg, rgba(255,255,255,0.02), transparent); + border-radius:14px; + box-shadow: 0 10px 40px rgba(0,0,0,0.6); + overflow:hidden; + border:1px solid rgba(255,255,255,0.03); +} + +/* Topbar */ +.topbar{ + display:flex; + align-items:center; + justify-content:space-between; + gap:12px; + padding:14px 20px; + border-bottom:1px solid rgba(255,255,255,0.03); + background: linear-gradient(90deg, rgba(255,255,255,0.01), transparent); +} +.title{display:flex;align-items:center;gap:12px} +.title h1{font-size:18px;margin:0} +.logo{width:40px;height:40px;border-radius:8px;object-fit:cover;box-shadow:var(--glow)} +.controls{display:flex;gap:8px;align-items:center} +.btn{background:var(--glass);border:none;color:inherit;padding:8px 12px;border-radius:8px;cursor:pointer;font-weight:600} +.btn.primary{background:linear-gradient(90deg,var(--accent), #ffb85c); color:#061226; box-shadow:var(--glow)} +.btn:active{transform:translateY(1px)} +.select{padding:8px;border-radius:8px;border:none;background:rgba(255,255,255,0.02);color:inherit} +.toggle{display:flex;align-items:center;gap:6px} + +/* Main layout */ +.game-wrap{display:flex; gap:16px; padding:18px} +.sidebar{width:260px; padding:12px; display:flex;flex-direction:column; gap:12px} +.panel{background:rgba(255,255,255,0.02); padding:12px;border-radius:10px; border:1px solid rgba(255,255,255,0.02)} +.meta div{display:flex;justify-content:space-between;margin:8px 0;font-size:14px} +.actions{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap} + +/* Board */ +.board-outer{flex:1;display:flex;flex-direction:column;align-items:center;gap:10px} +.board{ + width:var(--boardW); + height:520px; + background: linear-gradient(180deg, rgba(255,255,255,0.01), rgba(255,255,255,0.005)); + border-radius:12px; + display:block; + position:relative; + overflow:hidden; + box-shadow: 0 8px 30px rgba(2,6,23,0.6); + border:1px solid rgba(255,255,255,0.03); + padding:16px; +} + +/* Board svg will be appended and size sized to fill .board */ +.hint-dot{fill:rgba(255,255,255,0.08);} + +/* tile node */ +.node{ + transition: filter 220ms ease, transform 180ms ease; + cursor:pointer; +} +.node-circle{ + stroke:rgba(255,255,255,0.06); + stroke-width:2; + fill:rgba(255,255,255,0.02); + filter:drop-shadow(0 6px 18px rgba(0,0,0,0.6)); +} +.node.on .node-circle{ + fill:var(--accent); + stroke:rgba(255,200,90,0.9); + filter: drop-shadow(0 18px 36px rgba(255,200,90,0.12)) drop-shadow(0 0 26px rgba(255,210,110,0.18)); + transform-origin:center center; + transform:scale(1.05); +} + +/* lantern icon */ +.lantern{ + pointer-events:none; + transform-origin:center; + transition: opacity 200ms ease, transform 200ms ease; + opacity:0; +} +.node.on .lantern{opacity:1; transform:translateY(-6px) scale(1.05)} + +/* glow ring */ +.glow{ + mix-blend-mode:screen; + opacity:0.9; + transition:opacity 220ms ease; +} +.node.on .glow{opacity:1} + +/* obstacle */ +.obstacle-circle{fill: rgba(60,60,80,0.95); stroke: rgba(255,255,255,0.03);} + +/* HUD and message */ +.hud{position:relative;margin-top:6px;width:var(--boardW);display:flex;justify-content:center} +.message{ + background:rgba(0,0,0,0.4); padding:10px 14px;border-radius:14px;border:1px solid rgba(255,255,255,0.02); + color:var(--accent); font-weight:700; box-shadow:var(--glow) +} + +/* Footer */ +.footer{display:flex;justify-content:space-between;gap:12px;padding:10px 18px;border-top:1px solid rgba(255,255,255,0.02)} + +@media (max-width:980px){ + .game-wrap{flex-direction:column} + .sidebar{width:100%} + .board{height:420px} +} diff --git a/games/large_num/index.html b/games/large_num/index.html new file mode 100644 index 00000000..e74ec617 --- /dev/null +++ b/games/large_num/index.html @@ -0,0 +1,37 @@ + + + + + + Number Sequence Game + + + + +
    +

    ๐Ÿ”ข Largest Number Memory Test

    + +
    + Score: 0 +
    + +
    + Press START +
    + +
    + + + +
    + +
    +
    +
    + + + + \ No newline at end of file diff --git a/games/large_num/script.js b/games/large_num/script.js new file mode 100644 index 00000000..cf920ef6 --- /dev/null +++ b/games/large_num/script.js @@ -0,0 +1,137 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. DOM Elements --- + const numberDisplay = document.getElementById('number-display'); + const startButton = document.getElementById('start-button'); + const inputArea = document.getElementById('input-area'); + const answerInput = document.getElementById('answer-input'); + const submitButton = document.getElementById('submit-button'); + const feedbackMessage = document.getElementById('feedback-message'); + const scoreSpan = document.getElementById('score'); + + // --- 2. Game Variables --- + let currentSequence = []; + let largestNumber = 0; + let score = 0; + const SEQUENCE_LENGTH = 7; // Number of numbers to show (5-10 suggested) + const DISPLAY_DURATION = 300; // How long each number is visible (in ms) + const PAUSE_DURATION = 100; // Pause time between numbers (in ms) + const NUMBER_RANGE = 99; // Max number value (from 1 to 99) + + // --- 3. Core Functions --- + + /** + * Generates a unique random integer between 1 and max. + */ + function getRandomNumber(max) { + return Math.floor(Math.random() * max) + 1; + } + + /** + * Prepares the game by generating the number sequence. + */ + function generateSequence() { + currentSequence = []; + largestNumber = 0; + + for (let i = 0; i < SEQUENCE_LENGTH; i++) { + const newNumber = getRandomNumber(NUMBER_RANGE); + currentSequence.push(newNumber); + if (newNumber > largestNumber) { + largestNumber = newNumber; + } + } + console.log("Generated Sequence:", currentSequence); + console.log("Largest Number (Answer):", largestNumber); + } + + /** + * Manages the flashing display of the number sequence. + */ + function startSequenceDisplay() { + startButton.disabled = true; + inputArea.style.display = 'none'; + feedbackMessage.textContent = 'Get ready...'; + numberDisplay.textContent = '๐Ÿ‘๏ธ'; + + let sequenceIndex = 0; + + // Use a recursive setTimeout function for a precise, non-blocking delay loop + function displayNextNumber() { + if (sequenceIndex < currentSequence.length) { + // Display the number + numberDisplay.textContent = currentSequence[sequenceIndex]; + + // Set timeout to clear the number (PAUSE_DURATION) + setTimeout(() => { + numberDisplay.textContent = ''; + sequenceIndex++; + + // Set timeout for the next number (DISPLAY_DURATION + PAUSE_DURATION) + setTimeout(displayNextNumber, PAUSE_DURATION); + }, DISPLAY_DURATION); + + } else { + // Sequence finished, enable input + endSequenceDisplay(); + } + } + + // Start the process after a short delay + setTimeout(displayNextNumber, 1000); + } + + /** + * Enables the user input section after the sequence has finished flashing. + */ + function endSequenceDisplay() { + numberDisplay.textContent = 'โ“'; + feedbackMessage.textContent = 'Input the largest number you saw.'; + + inputArea.style.display = 'flex'; + answerInput.disabled = false; + submitButton.disabled = false; + answerInput.focus(); + } + + /** + * Checks the player's input against the actual largest number. + */ + function checkAnswer() { + const playerAnswer = parseInt(answerInput.value); + + // Disable controls immediately after submission + answerInput.disabled = true; + submitButton.disabled = true; + + if (playerAnswer === largestNumber) { + score++; + scoreSpan.textContent = score; + feedbackMessage.textContent = 'โœ… Correct! Well done!'; + feedbackMessage.style.color = '#1abc9c'; + } else { + feedbackMessage.textContent = `โŒ Incorrect. The largest number was ${largestNumber}.`; + feedbackMessage.style.color = '#e74c3c'; // Red + } + + // Reset for the next round + startButton.textContent = 'NEXT ROUND'; + startButton.disabled = false; + answerInput.value = ''; + } + + // --- 4. Event Listeners --- + + startButton.addEventListener('click', () => { + generateSequence(); + startSequenceDisplay(); + }); + + submitButton.addEventListener('click', checkAnswer); + + // Allow submission via the Enter key in the input field + answerInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter' && !submitButton.disabled) { + checkAnswer(); + } + }); +}); \ No newline at end of file diff --git a/games/large_num/style.css b/games/large_num/style.css new file mode 100644 index 00000000..4205c80a --- /dev/null +++ b/games/large_num/style.css @@ -0,0 +1,122 @@ +body { + font-family: 'Montserrat', sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f7f7f7; + color: #333; +} + +#game-container { + background-color: white; + padding: 35px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 450px; + width: 90%; +} + +h1 { + color: #1abc9c; /* Teal */ + margin-bottom: 20px; +} + +#score-area { + font-size: 1.1em; + font-weight: 600; + margin-bottom: 15px; + color: #34495e; +} + +/* --- Number Display Area --- */ +#number-display-box { + background-color: #2c3e50; /* Dark background for high contrast */ + color: white; + font-size: 4em; + font-weight: bold; + height: 150px; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + border-radius: 10px; + margin-bottom: 30px; + user-select: none; + transition: background-color 0.3s; +} + +/* --- Controls and Input --- */ +#controls { + display: flex; + flex-direction: column; + gap: 15px; + align-items: center; +} + +#start-button { + padding: 12px 25px; + font-size: 1.1em; + font-weight: bold; + background-color: #1abc9c; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +#start-button:hover:not(:disabled) { + background-color: #16a085; +} + +#start-button:disabled { + background-color: #bdc3c7; + cursor: not-allowed; +} + +#input-area { + display: flex; + gap: 10px; + width: 100%; + justify-content: center; +} + +#answer-input { + padding: 10px; + font-size: 1em; + border: 2px solid #ccc; + border-radius: 5px; + width: 150px; + text-align: center; +} + +#submit-button { + padding: 10px 20px; + font-size: 1em; + background-color: #3498db; /* Blue */ + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; +} + +#submit-button:hover:not(:disabled) { + background-color: #2980b9; +} + +#submit-button:disabled { + background-color: #95a5a6; + cursor: not-allowed; +} + +/* --- Feedback --- */ +#feedback-message { + min-height: 1.5em; + margin-top: 20px; + font-weight: bold; + font-size: 1.1em; +} \ No newline at end of file diff --git a/games/lava-jumper/index.html b/games/lava-jumper/index.html new file mode 100644 index 00000000..3aa0769f --- /dev/null +++ b/games/lava-jumper/index.html @@ -0,0 +1,31 @@ + + + + + +Lava Jumper | Mini JS Games Hub + + + +
    +

    Lava Jumper

    +
    + Score: 0 + Lives: 3 +
    + +
    + + + + +
    +
    + + + + + + + + diff --git a/games/lava-jumper/script.js b/games/lava-jumper/script.js new file mode 100644 index 00000000..7f884a72 --- /dev/null +++ b/games/lava-jumper/script.js @@ -0,0 +1,145 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +const startBtn = document.getElementById("startBtn"); +const pauseBtn = document.getElementById("pauseBtn"); +const resumeBtn = document.getElementById("resumeBtn"); +const restartBtn = document.getElementById("restartBtn"); + +const jumpSound = document.getElementById("jumpSound"); +const lavaSound = document.getElementById("lavaSound"); + +let score = 0; +let lives = 3; +let gameInterval; +let paused = false; + +const player = { + x: 180, + y: 500, + width: 40, + height: 40, + dy: 0, + color: "#00ffea", + gravity: 0.5, + jumpForce: -10 +}; + +const lava = { + y: 600, + speed: 0.3, + color: "#ff4500" +}; + +const platforms = []; +for (let i = 0; i < 6; i++) { + platforms.push({ + x: Math.random() * 300, + y: i * 100 + 100, + width: 100, + height: 20, + color: "#fff700" + }); +} + +// Controls +document.addEventListener("keydown", (e) => { + if (e.code === "ArrowLeft") player.x -= 20; + if (e.code === "ArrowRight") player.x += 20; + if (e.code === "Space") { + player.dy = player.jumpForce; + jumpSound.play(); + } +}); + +// Game Functions +function drawPlayer() { + ctx.fillStyle = player.color; + ctx.shadowColor = "#0ff"; + ctx.shadowBlur = 20; + ctx.fillRect(player.x, player.y, player.width, player.height); +} + +function drawPlatforms() { + platforms.forEach(p => { + ctx.fillStyle = p.color; + ctx.shadowColor = "#ff0"; + ctx.shadowBlur = 15; + ctx.fillRect(p.x, p.y, p.width, p.height); + }); +} + +function drawLava() { + ctx.fillStyle = lava.color; + ctx.shadowColor = "#ff0000"; + ctx.shadowBlur = 30; + ctx.fillRect(0, lava.y, canvas.width, canvas.height - lava.y); +} + +function drawScore() { + document.getElementById("score").textContent = "Score: " + Math.floor(score); + document.getElementById("lives").textContent = "Lives: " + lives; +} + +function update() { + if (paused) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Gravity + player.dy += player.gravity; + player.y += player.dy; + + // Platforms collision + platforms.forEach(p => { + if (player.y + player.height > p.y && player.y + player.height < p.y + 10 && + player.x + player.width > p.x && player.x < p.x + p.width && player.dy > 0) { + player.dy = player.jumpForce; + score += 10; + jumpSound.play(); + } + }); + + // Lava collision + if (player.y + player.height > lava.y) { + lives--; + lavaSound.play(); + if (lives <= 0) { + clearInterval(gameInterval); + alert("Game Over! Final Score: " + Math.floor(score)); + return; + } else { + player.y = 500; + player.dy = 0; + } + } + + // Boundaries + if (player.x < 0) player.x = 0; + if (player.x + player.width > canvas.width) player.x = canvas.width - player.width; + + // Lava rising + lava.y -= lava.speed; + + drawPlatforms(); + drawPlayer(); + drawLava(); + drawScore(); +} + +// Controls +startBtn.addEventListener("click", () => { + clearInterval(gameInterval); + gameInterval = setInterval(update, 20); +}); + +pauseBtn.addEventListener("click", () => paused = true); +resumeBtn.addEventListener("click", () => paused = false); +restartBtn.addEventListener("click", () => { + score = 0; + lives = 3; + lava.y = 600; + player.y = 500; + player.dy = 0; + paused = false; +}); diff --git a/games/lava-jumper/style.css b/games/lava-jumper/style.css new file mode 100644 index 00000000..ac2713e7 --- /dev/null +++ b/games/lava-jumper/style.css @@ -0,0 +1,57 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: linear-gradient(to top, #ff4e50, #f9d423); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + text-align: center; + position: relative; +} + +h1 { + color: #fff; + text-shadow: 0 0 10px #fff, 0 0 20px #ff0, 0 0 30px #f00; +} + +.game-info { + display: flex; + justify-content: space-around; + color: #fff; + margin-bottom: 10px; + font-weight: bold; + text-shadow: 0 0 5px #000; +} + +canvas { + background: #222; + border: 3px solid #fff; + border-radius: 10px; + display: block; + margin: 0 auto; +} + +.controls { + margin-top: 10px; +} + +button { + padding: 8px 15px; + margin: 0 5px; + font-size: 16px; + border: none; + border-radius: 6px; + cursor: pointer; + background: linear-gradient(45deg, #ff7e5f, #feb47b); + color: #fff; + box-shadow: 0 0 10px #fff; + transition: 0.3s; +} + +button:hover { + box-shadow: 0 0 20px #fff, 0 0 30px #ff0; +} diff --git a/games/light-ball-trail/index.html b/games/light-ball-trail/index.html new file mode 100644 index 00000000..61de533d --- /dev/null +++ b/games/light-ball-trail/index.html @@ -0,0 +1,76 @@ + + + + + + Light Ball Trail โ€” Mini JS Games Hub + + + + + +
    +
    +

    Light Ball Trail โœจ

    +

    Move a glowing ball โ€” it leaves a beautiful fading trail. Click/touch/arrow keys/WASD to move. Avoid obstacles or bounce off them.

    +
    + +
    + + +
    + +
    +
    +
    + + + + + +
    + +
    + + + + + + + + + +
    + +
    + + +
    + +
    + FPS: 0 + Particles: 0 + Hits: 0 +
    + + +
    +
    +
    + + + + diff --git a/games/light-ball-trail/script.js b/games/light-ball-trail/script.js new file mode 100644 index 00000000..f091b874 --- /dev/null +++ b/games/light-ball-trail/script.js @@ -0,0 +1,572 @@ +/* Light Ball Trail โ€” script.js + Advanced version: pointer / keyboard / touch support, obstacles, pause, restart, + glow + additive blending, particle trail, online sound assets, UI sliders. +*/ + +/* --------------------------- + Asset URLs (online links) + --------------------------- */ +const ASSETS = { + // Ambient loop (subtle) + ambient: "https://www.soundjay.com/nature/sounds/rain-01.mp3", + // Pop / click + pop: "https://www.soundjay.com/button/sounds/button-16.mp3", + // Soft bell for collisions + bell: "https://www.soundjay.com/misc/sounds/bell-ringing-05.mp3", + // background image (subtle pattern). Unsplash "blur" link + bg: "https://images.unsplash.com/photo-1503264116251-35a269479413?q=80&w=1600&auto=format&fit=crop&ixlib=rb-4.0.3&s=8df2c3d1a2c9080a3fae8b4954b5b5b8" +}; + +/* --------------------------- + Canvas & UI refs + --------------------------- */ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d", { alpha: true }); +let cw = canvas.width = Math.floor(canvas.clientWidth); +let ch = canvas.height = Math.floor(canvas.clientHeight); + +const playPauseBtn = document.getElementById("playPause"); +const stepBtn = document.getElementById("stepFrame"); +const restartBtn = document.getElementById("restart"); +const centerBtn = document.getElementById("centerBall"); +const soundToggle = document.getElementById("soundToggle"); + +const speedInput = document.getElementById("speed"); +const speedVal = document.getElementById("speedVal"); +const trailInput = document.getElementById("trailLength"); +const trailVal = document.getElementById("trailVal"); +const glowInput = document.getElementById("glow"); +const glowVal = document.getElementById("glowVal"); +const ballSizeInput = document.getElementById("ballSize"); +const sizeVal = document.getElementById("sizeVal"); +const colorInput = document.getElementById("color"); +const obstaclesToggle = document.getElementById("obstaclesToggle"); +const wrapToggle = document.getElementById("wrapToggle"); + +const fpsEl = document.getElementById("fps"); +const particleCountEl = document.getElementById("particleCount"); +const hitsEl = document.getElementById("hits"); + +/* --------------------------- + Audio setup + --------------------------- */ +const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); +function createAudio(src, loop = false, volume = 0.5) { + const audio = new Audio(src); + audio.crossOrigin = "anonymous"; + audio.loop = loop; + audio.volume = volume; + // connect to audio context to control playback across browsers + const track = audioCtx.createMediaElementSource(audio); + const gain = audioCtx.createGain(); + gain.gain.value = volume; + track.connect(gain).connect(audioCtx.destination); + return audio; +} +const ambientSound = createAudio(ASSETS.ambient, true, 0.25); +ambientSound.loop = true; +const popSound = createAudio(ASSETS.pop, false, 0.8); +const bellSound = createAudio(ASSETS.bell, false, 0.7); + +/* --------------------------- + Game state + --------------------------- */ +let lastTime = 0; +let accumulator = 0; +let running = true; +let frameStep = false; +let hits = 0; +let fps = 0; + +let settings = { + speed: parseFloat(speedInput.value), + trailLength: parseInt(trailInput.value), + glow: parseInt(glowInput.value), + ballSize: parseInt(ballSizeInput.value), + color: colorInput.value, + obstacles: obstaclesToggle.checked, + wrap: wrapToggle.checked, + sound: soundToggle.checked +}; + +/* Particles array for trail */ +let particles = []; + +/* Ball entity */ +const ball = { + x: cw / 2, + y: ch / 2, + vx: 0, + vy: 0, + speed: 220, // base pixels/sec multiplied by settings.speed + radius: settings.ballSize +}; + +/* Obstacles */ +let obstacles = []; + +/* Controls */ +const inputState = { + left:false,right:false,up:false,down:false, + pointerDown:false, pointerX:0,pointerY:0 +}; + +/* Touch joystick refs */ +const joystick = document.getElementById("touch-joystick"); +const joyBase = joystick.querySelector(".joy-base"); +const joyStick = joystick.querySelector(".joy-stick"); + +/* --------------------------- + Utility functions + --------------------------- */ +function resizeCanvas() { + cw = canvas.width = Math.floor(canvas.clientWidth); + ch = canvas.height = Math.floor(canvas.clientHeight); +} +window.addEventListener("resize", () => { + resizeCanvas(); +}); + +function rand(min, max) { return Math.random()*(max-min)+min; } + +function clamp(v,a,b){return Math.max(a,Math.min(b,v));} + +/* --------------------------- + Initialize obstacles + --------------------------- */ +function generateObstacles(count = 6) { + obstacles = []; + for (let i=0;i settings.trailLength) particles.shift(); +} + +/* --------------------------- + Input handling + --------------------------- */ +window.addEventListener("keydown", (e)=>{ + if (e.key === "ArrowLeft" || e.key === "a") inputState.left = true; + if (e.key === "ArrowRight"|| e.key === "d") inputState.right = true; + if (e.key === "ArrowUp" || e.key === "w") inputState.up = true; + if (e.key === "ArrowDown" || e.key === "s") inputState.down = true; + + // space to pause + if (e.key === " ") { + toggleRunning(); + } +}); +window.addEventListener("keyup",(e)=>{ + if (e.key === "ArrowLeft" || e.key === "a") inputState.left = false; + if (e.key === "ArrowRight"|| e.key === "d") inputState.right = false; + if (e.key === "ArrowUp" || e.key === "w") inputState.up = false; + if (e.key === "ArrowDown" || e.key === "s") inputState.down = false; +}); + +/* Pointer */ +canvas.addEventListener("pointerdown",(e)=>{ + canvas.setPointerCapture(e.pointerId); + inputState.pointerDown = true; + inputState.pointerX = e.clientX - canvas.getBoundingClientRect().left; + inputState.pointerY = e.clientY - canvas.getBoundingClientRect().top; + // small nudge towards pointer +}); +canvas.addEventListener("pointerup",(e)=>{ + canvas.releasePointerCapture(e.pointerId); + inputState.pointerDown = false; +}); +canvas.addEventListener("pointermove",(e)=>{ + inputState.pointerX = e.clientX - canvas.getBoundingClientRect().left; + inputState.pointerY = e.clientY - canvas.getBoundingClientRect().top; +}); + +/* Mobile joystick handling */ +let joyActive = false; +let joyCenter = {x:0,y:0}; +function startJoystick(ev){ + const rect = canvas.getBoundingClientRect(); + joyActive = true; + joystick.style.display = "flex"; + joyCenter = { x: ev.touches ? ev.touches[0].clientX - rect.left : ev.clientX - rect.left, + y: ev.touches ? ev.touches[0].clientY - rect.top : ev.clientY - rect.top }; + joyBase.style.transform = `translate(${joyCenter.x - 43}px, ${joyCenter.y - 43}px)`; +} +function moveJoystick(ev){ + if (!joyActive) return; + const rect = canvas.getBoundingClientRect(); + const x = ev.touches ? ev.touches[0].clientX - rect.left : ev.clientX - rect.left; + const y = ev.touches ? ev.touches[0].clientY - rect.top : ev.clientY - rect.top; + const dx = x - joyCenter.x, dy = y - joyCenter.y; + const mag = Math.sqrt(dx*dx + dy*dy); + const max = 36; + const nx = clamp(dx, -max, max); + const ny = clamp(dy, -max, max); + joyStick.style.transform = `translate(${nx}px, ${ny}px)`; + inputState.pointerX = joyCenter.x + nx; + inputState.pointerY = joyCenter.y + ny; +} +function endJoystick(){ + joyActive = false; + joystick.style.display = "none"; + joyStick.style.transform = `translate(0px,0px)`; +} + +canvas.addEventListener("touchstart",(e)=>{ + if (e.touches.length === 1) startJoystick(e); +}, {passive:true}); +canvas.addEventListener("touchmove",(e)=>{ moveJoystick(e); }, {passive:true}); +canvas.addEventListener("touchend",(e)=>{ endJoystick(); }, {passive:true}); + +/* --------------------------- + Rendering + --------------------------- */ +function clearCanvas() { + // paint a subtle background image overlay (low alpha) + ctx.save(); + ctx.globalCompositeOperation = 'source-over'; + ctx.fillStyle = "rgba(5,8,18,0.6)"; + ctx.fillRect(0,0,cw,ch); + ctx.restore(); +} + +function drawBackgroundPattern() { + // subtle vignette/texture using image + if (!drawBackgroundPattern.img) { + const img = new Image(); + img.crossOrigin = "anonymous"; + img.src = ASSETS.bg; + drawBackgroundPattern.img = img; + img.onload = ()=>{}; + } + const img = drawBackgroundPattern.img; + ctx.save(); + ctx.globalAlpha = 0.06; + ctx.drawImage(img, 0, 0, cw, ch); + ctx.restore(); +} + +function drawObstacles() { + if (!settings.obstacles) return; + ctx.save(); + ctx.globalCompositeOperation = 'source-over'; + obstacles.forEach(o=>{ + ctx.fillStyle = o.color; + ctx.fillRect(o.x, o.y, o.w, o.h); + // subtle outline + ctx.strokeStyle = "rgba(255,255,255,0.02)"; + ctx.strokeRect(o.x, o.y, o.w, o.h); + }); + ctx.restore(); +} + +function drawParticles() { + // additive blending and shadow for glow + ctx.save(); + ctx.globalCompositeOperation = 'lighter'; + for (let i = particles.length - 1; i >= 0; i--) { + const p = particles[i]; + p.age += 1/60; + const t = p.age / p.life; + p.alpha = Math.max(0, 1 - t); + p.x += p.vx; + p.y += p.vy; + const size = p.size * (1 - t*0.6); + + ctx.beginPath(); + ctx.globalAlpha = p.alpha * 0.9; + ctx.shadowBlur = settings.glow * (0.6 + 0.6*(1 - t)); + ctx.shadowColor = settings.color; + ctx.fillStyle = settings.color; + ctx.arc(p.x, p.y, size, 0, Math.PI*2); + ctx.fill(); + + if (p.age >= p.life) particles.splice(i,1); + } + ctx.restore(); +} + +function drawBall() { + ctx.save(); + ctx.globalCompositeOperation = 'lighter'; + const r = settings.ballSize; + ctx.shadowBlur = settings.glow; + ctx.shadowColor = settings.color; + // main core + const grad = ctx.createRadialGradient(ball.x, ball.y, 0, ball.x, ball.y, r*3); + grad.addColorStop(0, settings.color); + grad.addColorStop(0.2, settings.color + '88'); + grad.addColorStop(0.6, settings.color + '33'); + grad.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = grad; + ctx.beginPath(); + ctx.arc(ball.x, ball.y, r*1.6, 0, Math.PI*2); + ctx.fill(); + + // core highlight + ctx.globalCompositeOperation = 'screen'; + ctx.beginPath(); + ctx.globalAlpha = 0.9; + ctx.arc(ball.x - r*0.2, ball.y - r*0.2, r*0.9, 0, Math.PI*2); + ctx.fillStyle = 'rgba(255,255,255,0.65)'; + ctx.fill(); + + ctx.restore(); +} + +/* --------------------------- + Physics & update loop + --------------------------- */ +function physicsStep(dt) { + // speed multiplier + const spd = ball.speed * settings.speed; + + // pointer target movement: if pointerDown, move toward pointer + if (inputState.pointerDown || joyActive) { + const dx = inputState.pointerX - ball.x; + const dy = inputState.pointerY - ball.y; + const dist = Math.hypot(dx,dy) || 1; + const nx = dx/dist, ny = dy/dist; + ball.vx = nx * spd * (dt * 1); + ball.vy = ny * spd * (dt * 1); + } else { + // keyboard movement + let ax = 0, ay = 0; + if (inputState.left) ax -= 1; + if (inputState.right) ax += 1; + if (inputState.up) ay -= 1; + if (inputState.down) ay += 1; + if (ax !== 0 || ay !== 0) { + const mag = Math.hypot(ax,ay) || 1; + ball.vx = (ax/mag) * spd * dt * 60; + ball.vy = (ay/mag) * spd * dt * 60; + } else { + // gradual friction + ball.vx *= 0.92; + ball.vy *= 0.92; + } + } + + ball.x += ball.vx; + ball.y += ball.vy; + + // wrap or clamp + if (settings.wrap) { + if (ball.x < -50) ball.x = cw + 50; + if (ball.x > cw + 50) ball.x = -50; + if (ball.y < -50) ball.y = ch + 50; + if (ball.y > ch + 50) ball.y = -50; + } else { + ball.x = clamp(ball.x, settings.ballSize, cw - settings.ballSize); + ball.y = clamp(ball.y, settings.ballSize, ch - settings.ballSize); + } + + // emit particles based on speed + const speedMagnitude = Math.hypot(ball.vx, ball.vy); + const emitCount = Math.ceil(clamp(speedMagnitude * 0.08, 1, 4)); + for (let i=0;i { + if (circleRectCollision(ball.x, ball.y, settings.ballSize*1.0, o.x, o.y, o.w, o.h)) { + // simple response: bounce back by reversing velocity and nudging out + ball.vx *= -0.6; + ball.vy *= -0.6; + // move ball out of obstacle along minimal vector + if (ball.x > o.x && ball.x < o.x + o.w) { + if (ball.y < o.y) ball.y = o.y - settings.ballSize - 2; + else ball.y = o.y + o.h + settings.ballSize + 2; + } else { + if (ball.x < o.x) ball.x = o.x - settings.ballSize - 2; + else ball.x = o.x + o.w + settings.ballSize + 2; + } + // audio cue + hit count + hits++; + if (settings.sound) { bellSound.currentTime = 0; bellSound.play(); } + } + }); + } +} + +/* --------------------------- + Main loop + --------------------------- */ +function loop(t) { + if (!lastTime) lastTime = t; + const dt = Math.min(0.05, (t - lastTime) / 1000); + lastTime = t; + + if (running || frameStep) { + // update + physicsStep(dt); + + // draw + clearCanvas(); + drawBackgroundPattern(); + drawObstacles(); + drawParticles(); + drawBall(); + } + + // update UI stats + fps = Math.round(1 / (dt || 1/60)); + fpsEl.textContent = fps; + particleCountEl.textContent = particles.length; + hitsEl.textContent = hits; + + if (frameStep) frameStep = false; + + requestAnimationFrame(loop); +} + +/* --------------------------- + UI Wiring + --------------------------- */ +function toggleRunning() { + running = !running; + playPauseBtn.textContent = running ? "Pause" : "Play"; + if (running && settings.sound) { + try { ambientSound.play(); } catch(e) { /* Autoplay blocked until user gesture */ } + } else { + try { ambientSound.pause(); } catch(e){} + } +} +playPauseBtn.addEventListener("click", toggleRunning); +stepBtn.addEventListener("click", ()=>{ frameStep = true; toggleRunning(); toggleRunning(); /* trigger one frame */ }); +restartBtn.addEventListener("click", ()=>{ + resetGame(); + if (settings.sound) { popSound.currentTime = 0; popSound.play(); } +}); +centerBtn.addEventListener("click", ()=>{ + ball.x = cw/2; ball.y = ch/2; ball.vx=0; ball.vy=0; +}); + +speedInput.addEventListener("input", e=>{ + settings.speed = parseFloat(e.target.value); speedVal.textContent = settings.speed.toFixed(2); +}); +trailInput.addEventListener("input", e=>{ + settings.trailLength = parseInt(e.target.value); trailVal.textContent = settings.trailLength; +}); +glowInput.addEventListener("input", e=>{ + settings.glow = parseInt(e.target.value); glowVal.textContent = settings.glow; +}); +ballSizeInput.addEventListener("input", e=>{ + settings.ballSize = parseInt(e.target.value); sizeVal.textContent = settings.ballSize; +}); +colorInput.addEventListener("input", e=>{ + settings.color = e.target.value; +}); +obstaclesToggle.addEventListener("change", e=>{ + settings.obstacles = e.target.checked; + if (settings.obstacles && obstacles.length === 0) generateObstacles(6); +}); +wrapToggle.addEventListener("change", e=>{ + settings.wrap = e.target.checked; +}); +soundToggle.addEventListener("change", e=>{ + settings.sound = e.target.checked; + if (!settings.sound) { try { ambientSound.pause(); } catch(e){} } + else { try { ambientSound.play(); } catch(e){} } +}); + +/* Pointer down to toggle pointer control on click */ +canvas.addEventListener("dblclick", ()=>{ + // center ball + ball.x = inputState.pointerX || cw/2; + ball.y = inputState.pointerY || ch/2; +}); + +/* Pointer hold -> treat as target */ +canvas.addEventListener("mousedown", (e)=>{ + inputState.pointerDown = true; + inputState.pointerX = e.clientX - canvas.getBoundingClientRect().left; + inputState.pointerY = e.clientY - canvas.getBoundingClientRect().top; +}); +window.addEventListener("mouseup", ()=>{ inputState.pointerDown = false; }); + +/* --------------------------- + Reset / Init + --------------------------- */ +function resetGame() { + particles = []; + ball.x = cw/2; ball.y = ch/2; ball.vx=0; ball.vy=0; + hits = 0; + particles.length = 0; + if (settings.obstacles) generateObstacles(6); +} + +/* --------------------------- + Start up + --------------------------- */ +function init() { + resizeCanvas(); + // set UI values display + speedVal.textContent = speedInput.value; + trailVal.textContent = trailInput.value; + glowVal.textContent = glowInput.value; + sizeVal.textContent = ballSizeInput.value; + // set initial settings + settings = { + speed: parseFloat(speedInput.value), + trailLength: parseInt(trailInput.value), + glow: parseInt(glowInput.value), + ballSize: parseInt(ballSizeInput.value), + color: colorInput.value, + obstacles: obstaclesToggle.checked, + wrap: wrapToggle.checked, + sound: soundToggle.checked + }; + + // generate obstacles + if (settings.obstacles) generateObstacles(6); + + // play ambient only after user gesture; try to pre-warm audio context on first interaction + function userGesture() { + try{ audioCtx.resume(); }catch(e){} + document.removeEventListener('pointerdown', userGesture); + } + document.addEventListener('pointerdown', userGesture); + + requestAnimationFrame(loop); +} +init(); + +/* Expose for debug */ +window.LBT = { reset: resetGame, settings, particles, ball }; + diff --git a/games/light-ball-trail/style.css b/games/light-ball-trail/style.css new file mode 100644 index 00000000..813639ec --- /dev/null +++ b/games/light-ball-trail/style.css @@ -0,0 +1,153 @@ +:root{ + --bg:#0b1020; + --panel:#0f1724; + --muted:#9aa4b2; + --accent:#00e5ff; + --glass: rgba(255,255,255,0.04); + --card-radius:14px; + font-size:16px; + color-scheme: dark; +} + +*{box-sizing:border-box} +html,body,#gameCanvas{height:100%} +body{ + margin:0; + min-height:100vh; + font-family:Inter, ui-sans-serif, system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial; + background: radial-gradient(1200px 600px at 10% 10%, rgba(0,229,255,0.04), transparent), + radial-gradient(900px 400px at 90% 90%, rgba(255,0,200,0.02), transparent), + var(--bg); + color:#dfe8f2; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + padding:28px; + display:flex; + align-items:flex-start; + justify-content:center; +} + +.app{ + width:1200px; + max-width:calc(100vw - 40px); + display:grid; + grid-template-columns: 1fr 360px; + gap:20px; + align-items:start; +} + +.app__header{ + grid-column:1 / 2; + margin-bottom:6px; +} + +.title{ + margin:0 0 8px; + font-size:22px; + letter-spacing:-0.02em; +} + +.subtitle{ + margin:0; + color:var(--muted); + font-size:13px; +} + +.canvas-wrap{ + background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border-radius: var(--card-radius); + box-shadow: 0 10px 30px rgba(3,8,20,0.5); + padding: 10px; + position:relative; + min-height:640px; + overflow:hidden; + border:1px solid rgba(255,255,255,0.03); +} + +canvas{ + width:100%; + height:640px; + display:block; + border-radius:12px; + background-image: linear-gradient(180deg, rgba(255,255,255,0.01), transparent); + cursor:crosshair; + image-rendering: optimizeQuality; +} + +/* Mobile joystick overlay */ +.overlay-controls{ + position:absolute; + bottom:18px; + left:18px; + width:110px; + height:110px; + display:none; + align-items:center; + justify-content:center; + pointer-events:none; +} +.joy-base{ + width:86px; + height:86px; + background:linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01)); + border-radius:50%; + border:1px solid rgba(255,255,255,0.04); + pointer-events:auto; + display:flex; + align-items:center; + justify-content:center; +} +.joy-stick{ + width:34px; + height:34px; + border-radius:50%; + background:linear-gradient(180deg,#ffffff11,#00000011); + box-shadow:0 6px 18px rgba(0,0,0,0.6), inset 0 -6px 12px rgba(255,255,255,0.02); +} + +/* Right column UI */ +.ui{ + grid-column:2 / 3; +} +.panel{ + background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border-radius:12px; + padding:18px; + border:1px solid rgba(255,255,255,0.03); + box-shadow: 0 8px 24px rgba(2,6,20,0.5); +} +.controls-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px} +.btn{ + background:transparent; + border:1px solid rgba(255,255,255,0.06); + color:var(--muted); + padding:8px 10px; + border-radius:10px; + cursor:pointer; + font-weight:600; + font-size:14px; +} +.btn.primary{ + background:linear-gradient(90deg,var(--accent),#8a00ff); + color:#02101b; + border:none; + box-shadow:0 8px 18px rgba(0,229,255,0.06), inset 0 -4px 14px rgba(0,0,0,0.12); +} + +.btn-check{display:inline-flex;align-items:center;gap:8px;padding:8px 10px;border-radius:10px;background:var(--glass);color:var(--muted);border:1px solid rgba(255,255,255,0.02)} + +.sliders label{display:block;font-size:13px;color:var(--muted);margin:8px 0} +.sliders input[type="range"]{width:100%} +.sliders input[type="color"]{height:36px;width:56px;border-radius:8px;border:none;padding:0;margin-left:8px;vertical-align:middle} + +.toggles{display:flex;gap:12px;margin-top:10px;color:var(--muted)} +.meta{display:flex;gap:12px;margin-top:12px;color:var(--muted);font-size:13px} +.footer-note{color:var(--muted);font-size:12px;margin-top:12px} + +/* Responsive */ +@media (max-width:1100px){ + .app{grid-template-columns:1fr; padding-bottom:24px} + .ui{grid-column:1 / -1; margin-top:12px} + .overlay-controls{display:flex} + canvas{height:56vh} +} diff --git a/games/light-bloom-painter/index.html b/games/light-bloom-painter/index.html new file mode 100644 index 00000000..b01090be --- /dev/null +++ b/games/light-bloom-painter/index.html @@ -0,0 +1,101 @@ + + + + + + Light Bloom Painter โ€” Mini JS Games Hub + + + + +
    +
    +
    + ๐Ÿ’ก +
    +

    Light Bloom Painter

    +

    Paint glowing trails โ€” desktop & mobile friendly

    +
    +
    + + +
    + +
    +
    + +
    Move cursor / touch to paint
    +
    + + +
    + +
    +
    Made with โœจ by the community โ€” Hub
    +
    Touch & hold to paint continuously. Two-finger drag to move without painting.
    +
    +
    + + + + diff --git a/games/light-bloom-painter/script.js b/games/light-bloom-painter/script.js new file mode 100644 index 00000000..65a5736b --- /dev/null +++ b/games/light-bloom-painter/script.js @@ -0,0 +1,487 @@ +/* Light Bloom Painter + - Canvas-based particle bloom painting + - WebAudio generated ambient + stroke sounds (no external files required) + - Touch & mouse support + - Settings persist in localStorage +*/ + +(() => { + // Elements + const canvas = document.getElementById('paintCanvas'); + const ctx = canvas.getContext('2d', { alpha: true }); + const playPauseBtn = document.getElementById('playPause'); + const restartBtn = document.getElementById('restartBtn'); + const saveBtn = document.getElementById('saveBtn'); + const sizeRange = document.getElementById('sizeRange'); + const speedRange = document.getElementById('speedRange'); + const glowRange = document.getElementById('glowRange'); + const trailRange = document.getElementById('trailRange'); + const densityRange = document.getElementById('densityRange'); + const colorPicker = document.getElementById('colorPicker'); + const glowColor = document.getElementById('glowColor'); + const audioToggle = document.getElementById('audioToggle'); + const invertToggle = document.getElementById('invertToggle'); + const clearBtn = document.getElementById('clearBtn'); + const hint = document.getElementById('hint'); + const showUI = document.getElementById('showUI'); + + // labels + const sizeLabel = document.getElementById('sizeLabel'); + const speedLabel = document.getElementById('speedLabel'); + const glowLabel = document.getElementById('glowLabel'); + const trailLabel = document.getElementById('trailLabel'); + const densityLabel = document.getElementById('densityLabel'); + + // state & settings + const storageKey = 'light-bloom-settings-v1'; + const defaultSettings = { + size: 20, + speed: 2, + glow: 0.9, + trail: 0.12, + density: 1, + color: '#66ccff', + glowColor: '#88ddff', + audio: true, + invert: false, + showUI: true + }; + + let settings = Object.assign({}, defaultSettings, JSON.parse(localStorage.getItem(storageKey) || '{}')); + + // apply initial UI values + sizeRange.value = settings.size; + speedRange.value = settings.speed; + glowRange.value = settings.glow; + trailRange.value = settings.trail; + densityRange.value = settings.density; + colorPicker.value = settings.color; + glowColor.value = settings.glowColor; + audioToggle.checked = settings.audio; + invertToggle.checked = settings.invert; + showUI.checked = settings.showUI; + + // label update + const updateLabels = () => { + sizeLabel.textContent = sizeRange.value; + speedLabel.textContent = speedRange.value; + glowLabel.textContent = Number(glowRange.value).toFixed(2); + trailLabel.textContent = Number(trailRange.value).toFixed(2); + densityLabel.textContent = Number(densityRange.value).toFixed(2); + }; + updateLabels(); + + // canvas resize + function resize() { + const dpr = Math.max(1, window.devicePixelRatio || 1); + const rect = canvas.getBoundingClientRect(); + canvas.width = Math.floor(rect.width * dpr); + canvas.height = Math.floor(rect.height * dpr); + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + // set optional background + drawBackground(); + } + + function drawBackground() { + // simple vignette / dark background + if (settings.invert) { + canvas.style.background = 'radial-gradient(circle at 20% 20%, rgba(255,255,255,0.02), rgba(255,255,255,0.01)), #ffffff'; + } else { + canvas.style.background = 'transparent'; + } + } + + window.addEventListener('resize', throttle(resize, 120)); + resize(); + + // Offscreen buffer for bloom effect + const buffer = document.createElement('canvas'); + const bctx = buffer.getContext('2d', { alpha: true }); + function resizeBuffer() { + buffer.width = canvas.width; + buffer.height = canvas.height; + bctx.setTransform(1,0,0,1,0,0); + } + resizeBuffer(); + + // particles + let particles = []; + let running = true; + + // audio setup (WebAudio) + const audioCtx = (window.AudioContext || window.webkitAudioContext) ? new (window.AudioContext || window.webkitAudioContext)() : null; + let masterGain, ambientOsc, ambientGain, strokeGain; + + function initAudio() { + if (!audioCtx) return; + masterGain = audioCtx.createGain(); masterGain.gain.value = 0.6; masterGain.connect(audioCtx.destination); + // ambient drone + ambientOsc = audioCtx.createOscillator(); + ambientGain = audioCtx.createGain(); + ambientOsc.type = 'sine'; + ambientOsc.frequency.value = 120; + ambientGain.gain.value = 0.02; // subtle + ambientOsc.connect(ambientGain); + ambientGain.connect(masterGain); + ambientOsc.start(0); + + // stroke sound gain (per brush event) + strokeGain = audioCtx.createGain(); + strokeGain.gain.value = 0.0; + strokeGain.connect(masterGain); + } + + // call once on first user interaction if audioCtx exists but suspended + function ensureAudio() { + if (!audioCtx) return; + if (audioCtx.state === 'suspended') { + audioCtx.resume().catch(()=>{/* ignore */}); + } + } + + if (audioCtx) initAudio(); + + // helper: play stroke sound via oscillator with short envelope + function playStrokeSound(freq = 600, duration = 0.08, volume = 0.02) { + if (!audioCtx || !settings.audio) return; + const o = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + o.type = 'sine'; + o.frequency.value = freq; + g.gain.value = 0.0001; + o.connect(g); + g.connect(masterGain); + const now = audioCtx.currentTime; + g.gain.setValueAtTime(0.0001, now); + g.gain.exponentialRampToValueAtTime(volume, now + 0.01); + g.gain.exponentialRampToValueAtTime(0.0001, now + duration); + o.start(now); + o.stop(now + duration + 0.02); + } + + // UI interactivity โ€” change settings persistently + function persist() { + settings.size = Number(sizeRange.value); + settings.speed = Number(speedRange.value); + settings.glow = Number(glowRange.value); + settings.trail = Number(trailRange.value); + settings.density = Number(densityRange.value); + settings.color = colorPicker.value; + settings.glowColor = glowColor.value; + settings.audio = audioToggle.checked; + settings.invert = invertToggle.checked; + settings.showUI = showUI.checked; + localStorage.setItem(storageKey, JSON.stringify(settings)); + updateLabels(); + drawBackground(); + } + + // attach UI events + [sizeRange, speedRange, glowRange, trailRange, densityRange, colorPicker, glowColor, audioToggle, invertToggle, showUI].forEach(el => { + el.addEventListener('input', () => { persist(); }); + el.addEventListener('change', () => { persist(); }); + }); + + clearBtn.addEventListener('click', () => { + particles = []; + clearCanvas(); + }); + + restartBtn.addEventListener('click', () => { + particles = []; + clearCanvas(); + }); + + playPauseBtn.addEventListener('click', () => { + running = !running; + playPauseBtn.textContent = running ? 'Pause' : 'Resume'; + if (running) { then = performance.now(); requestAnim(); } + }); + + saveBtn.addEventListener('click', () => { + // create flattened image with extra glow + const link = document.createElement('a'); + link.download = `light-bloom-${Date.now()}.png`; + link.href = canvas.toDataURL('image/png'); + link.click(); + }); + + // painting logic: pointer events + let pointerDown = false; + let pointerId = null; + let last = null; + + // multi-touch: if two touches, treat as move-only if user holds shift-like gesture + let ignorePainting = false; + + function getPointerPos(e) { + const rect = canvas.getBoundingClientRect(); + const dpr = Math.max(1, window.devicePixelRatio || 1); + if (e.touches && e.touches[0]) { + const t = e.touches[0]; + return { x: (t.clientX - rect.left), y: (t.clientY - rect.top) }; + } else { + return { x: (e.clientX - rect.left), y: (e.clientY - rect.top) }; + } + } + + // pointer start + function pointerStart(e) { + ensureAudio(); + if (e.touches && e.touches.length > 1) { + ignorePainting = true; + return; + } else { + ignorePainting = false; + } + pointerDown = true; + last = getPointerPos(e); + if (!ignorePainting) emitParticles(last.x, last.y, 1.0); + // play click sound quickly + playStrokeSound(650, 0.06, 0.02); + e.preventDefault(); + } + + function pointerMove(e) { + if (!pointerDown) return; + if (ignorePainting) return; + const pos = getPointerPos(e); + const dx = pos.x - last.x; + const dy = pos.y - last.y; + const dist = Math.sqrt(dx*dx + dy*dy); + const steps = Math.max(1, Math.floor(dist / (4 / settings.density))); + for (let i=0;i 1.2) { + ctx.globalAlpha = Math.min(0.65, settings.glow * 0.6); + ctx.drawImage(buffer, -1.5, -1.5, canvas.width + 3, canvas.height + 3); + ctx.drawImage(buffer, 1.5, 1.5, canvas.width - 3, canvas.height - 3); + } + + // Draw crisp particle centers + ctx.globalAlpha = 1; + ctx.globalCompositeOperation = 'lighter'; + for (let i=0;i wait){ t = now; fn.apply(this, arguments); } } } + + // clear canvas and buffer completely + function clearCanvas() { + ctx.clearRect(0,0,canvas.width,canvas.height); + bctx.clearRect(0,0,buffer.width,buffer.height); + } + + // save & restore buffer on resize + window.addEventListener('resize', throttle(()=>{ resizeBuffer(); }, 120)); + + // minimal animation loop for ticking + // click sound on heavy strokes + let lastSoundTick = 0; + setInterval(()=> { + if (pointerDown && settings.audio) { + const now = performance.now(); + if (now - lastSoundTick > 100) { + playStrokeSound(500 + Math.random()*600, 0.06, 0.02); + lastSoundTick = now; + } + } + }, 80); + + // helper: ensure offscreen buffer matches scale + function syncBuffer() { + buffer.width = canvas.width; + buffer.height = canvas.height; + } + syncBuffer(); + + // touch gestures: two-finger clears? (already handled by ignorePainting) + // saving persistent settings at intervals + setInterval(()=> localStorage.setItem(storageKey, JSON.stringify(settings)), 2000); + + // small utility functions + function rand(min, max) { return Math.random()*(max-min)+min; } + + // initial clear + clearCanvas(); + + // ensure the canvas uses full container on load + function fitCanvasToContainer() { + const rect = canvas.getBoundingClientRect(); + const dpr = Math.max(1, window.devicePixelRatio || 1); + canvas.width = Math.floor(rect.width * dpr); + canvas.height = Math.floor(rect.height * dpr); + ctx.setTransform(dpr,0,0,dpr,0,0); + buffer.width = canvas.width; + buffer.height = canvas.height; + } + + // run after layout + setTimeout(()=>{ fitCanvasToContainer(); resizeBuffer(); }, 60); + + // make sure settings are reflected in UI + function applySettingsToUI(){ + sizeRange.value = settings.size; + speedRange.value = settings.speed; + glowRange.value = settings.glow; + trailRange.value = settings.trail; + densityRange.value = settings.density; + colorPicker.value = settings.color; + glowColor.value = settings.glowColor; + audioToggle.checked = settings.audio; + invertToggle.checked = settings.invert; + showUI.checked = settings.showUI; + updateLabels(); + } + applySettingsToUI(); + + // helper: when user interacts first time, resume audio if needed + document.addEventListener('pointerdown', () => { + ensureAudio(); + }, { once:true }); + + // expose a short API to hub when opened via Play + window.LIGHT_BLOOM = { + clear: () => { particles=[]; clearCanvas(); }, + pause: () => { running=false; playPauseBtn.textContent='Resume'; }, + resume: () => { running=true; playPauseBtn.textContent='Pause'; then=performance.now(); requestAnim(); }, + getSettings: () => ({...settings}) + }; + + // small helpers used above that need to be in scope + function clamp(v,a,b){return Math.max(a, Math.min(b, v));} +})(); diff --git a/games/light-bloom-painter/style.css b/games/light-bloom-painter/style.css new file mode 100644 index 00000000..c5698636 --- /dev/null +++ b/games/light-bloom-painter/style.css @@ -0,0 +1,73 @@ +:root{ + --bg:#0b0f14; + --panel:#0f1720; + --muted:#9aa4b2; + --accent: #66ccff; + --glass: rgba(255,255,255,0.03); + --radius:12px; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Helvetica Neue", Arial; +} + +*{box-sizing:border-box} +html,body,#paintCanvas{height:100%} +body{ + margin:0; + background: radial-gradient(1200px 600px at 10% 10%, rgba(34,48,60,0.18), transparent), + radial-gradient(700px 350px at 88% 70%, rgba(100,40,160,0.12), transparent), + var(--bg); + color:#e9f0f6; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + min-height:100vh; + display:flex; + align-items:stretch; + justify-content:center; + padding:18px; +} + +/* app layout */ +.app{width:100%;max-width:1400px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.015)); border-radius:16px; overflow:hidden; box-shadow: 0 10px 40px rgba(2,6,23,0.6); display:flex;flex-direction:column} +.topbar{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid rgba(255,255,255,0.03)} +.brand{display:flex;gap:12px;align-items:center} +.brand__icon{font-size:30px} +.brand h1{margin:0;font-size:18px} +.brand .sub{margin:0;font-size:12px;color:var(--muted)} + +.controls-top{display:flex;gap:10px;align-items:center} +.btn{background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));border:1px solid rgba(255,255,255,0.04);color:#e6f2ff;padding:8px 10px;border-radius:10px;cursor:pointer;font-size:13px} +.btn.ghost{background:transparent;border:1px dashed rgba(255,255,255,0.04)} +.btn.full{width:100%} +.btn:active{transform:translateY(1px)} + +/* main layout */ +.main{display:flex;gap:16px;align-items:stretch;padding:18px} +.canvas-wrap{flex:1;position:relative;border-radius:12px;overflow:hidden;min-height:60vh;display:flex;align-items:stretch} +#paintCanvas{width:100%;height:100%;display:block;background:transparent; touch-action:none} + +.hint{position:absolute;left:16px;top:16px;background:rgba(0,0,0,0.35);padding:8px 10px;border-radius:10px;font-size:13px;color:var(--muted);backdrop-filter: blur(6px)} + +.sidebar{width:320px;padding:8px 6px;display:flex;flex-direction:column;gap:12px} +.panel{background:linear-gradient(180deg, rgba(255,255,255,0.015), rgba(255,255,255,0.01));padding:12px;border-radius:12px;border:1px solid rgba(255,255,255,0.02)} +.panel h3{margin:0 0 8px 0;font-size:14px} +.panel label{display:block;margin-bottom:8px;font-size:13px;color:var(--muted)} +.panel input[type="range"]{width:100%} +.panel input[type="color"]{width:48px;height:36px;border-radius:8px;border:0;padding:0;margin-left:8px;vertical-align:middle} +.panel .help{font-size:12px;color:var(--muted)} + +.credits p{margin:0;font-size:13px;color:var(--muted)} +.footer{display:flex;justify-content:space-between;padding:12px 20px;border-top:1px solid rgba(255,255,255,0.02);font-size:13px;color:var(--muted)} + +/* responsive */ +@media (max-width:980px){ + .sidebar{width:260px} + .app{max-width:100%} +} +@media (max-width:760px){ + .main{flex-direction:column} + .sidebar{width:100%;order:2} + .canvas-wrap{order:1;min-height:60vh} + .controls-top{gap:8px} +} + +/* small ui overlay for mobile hide toggle */ +canvas.ui-hidden + .hint{opacity:1} diff --git a/games/light-orb-quest/index.html b/games/light-orb-quest/index.html new file mode 100644 index 00000000..ed96fc38 --- /dev/null +++ b/games/light-orb-quest/index.html @@ -0,0 +1,69 @@ + + + + + + Light Orb Quest โ€” Mini JS Games Hub + + + +
    +
    +
    Level 1 โ€” Whispering Vault
    +
    + Moves: 0 + Light: 4 + Treasures: 0/0 +
    +
    +
    + + + +
    +
    + +
    + + + +
    + + + + + + diff --git a/games/light-orb-quest/script.js b/games/light-orb-quest/script.js new file mode 100644 index 00000000..5581140e --- /dev/null +++ b/games/light-orb-quest/script.js @@ -0,0 +1,553 @@ +/* Light Orb Quest + - Canvas-based tile world + - Fog-of-war with radial glow + LOS (raycast) + - Movement, flares, pause, restart, treasures, traps, exit + - Simple WebAudio-generated SFX +*/ + +/* ========================= + Config & Utilities + ========================= */ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d", { alpha: false }); + +const TILE_SIZE = 48; // size in px +const COLS = 20; // recommended grid 20x12 for 960x576 canvas +const ROWS = 12; +canvas.width = TILE_SIZE * COLS; +canvas.height = TILE_SIZE * ROWS; + +const levelNameEl = document.getElementById("levelName"); +const movesEl = document.getElementById("moves"); +const lightRadiusEl = document.getElementById("lightRadius"); +const collectedEl = document.getElementById("collected"); +const totalTreasuresEl = document.getElementById("totalTreasures"); +const inventoryEl = document.getElementById("inventory"); + +const overlay = document.getElementById("overlayMessage"); +const overlayTitle = document.getElementById("overlayTitle"); +const overlayText = document.getElementById("overlayText"); +const overlayRestart = document.getElementById("overlayRestart"); +const overlayContinue = document.getElementById("overlayContinue"); + +const btnPause = document.getElementById("btnPause"); +const btnRestart = document.getElementById("btnRestart"); +const btnFlare = document.getElementById("btnFlare"); + +let paused = false; + +/* Simple WebAudio for SFX */ +const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); +function playBeep(freq=440, type='sine', length=0.12, gain=0.08){ + const o = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + o.type = type; o.frequency.value = freq; + g.gain.value = gain; + o.connect(g); g.connect(audioCtx.destination); + o.start(); + o.stop(audioCtx.currentTime + length); +} + +/* ========================= + Level generation + ========================= */ + +/* +grid[r][c] values: +0 = floor, 1 = wall, 2 = trap, 3 = treasure, 4 = exit +*/ +function createEmptyGrid(rows, cols, fill=0){ + const g = []; + for(let r=0;r0.2){ + grid[rr][cc]=3; treasures++; + } + } + // place traps + for(let i=0;i<14;i++){ + const rr = randInt(1, ROWS-2), cc = randInt(1, COLS-2); + if(grid[rr][cc]===0 && Math.random()>0.4) grid[rr][cc]=2; + } + // ensure a clear exit location at border + const exits = [ + {r:1,c:Math.floor(COLS/2)}, + {r:ROWS-2,c:Math.floor(COLS/2)}, + {r:Math.floor(ROWS/2),c:1}, + {r:Math.floor(ROWS/2),c:COLS-2} + ]; + const exitChoice = exits[randInt(0, exits.length-1)]; + grid[exitChoice.r][exitChoice.c]=4; + // find a spawn (floor cell) + let spawn = {r:Math.floor(ROWS/2), c:Math.floor(COLS/2)}; + for(let i=0;i<200;i++){ + const rr = randInt(1,ROWS-2), cc = randInt(1,COLS-2); + if(grid[rr][cc]===0) { spawn={r:rr,c:cc}; break; } + } + return {grid, spawn, treasures}; +} + +function randInt(min,max){ return Math.floor(Math.random()*(max-min))+min; } + +/* ========================= + Game state + ========================= */ + +let levelState = null; +let orb = null; // {r,c,lightRadius} +let moves = 0; +let collected = 0; +let visible = createEmptyGrid(ROWS, COLS, false); // boolean map of visible tiles +let flareCooldown = 0; +let inventory = {flares:1, upgrades:0}; +let enemies = []; // simple moving hazards +let tick = 0; + +/* ========================= + Raycast LOS helper + returns true if tile (tr,tc) can be seen from orb given walls blocking + using simple Bresenham line algorithm sampling tiles along line + ========================= */ +function tileVisible(sr, sc, tr, tc) { + // distance check + const dx = tc - sc, dy = tr - sr; + const dist = Math.sqrt(dx*dx + dy*dy); + if(dist > orb.lightRadius + 0.5) return false; + // Bresenham + let x0 = sc + 0.5, y0 = sr + 0.5; + let x1 = tc + 0.5, y1 = tr + 0.5; + const dxLine = Math.abs(x1 - x0), dyLine = Math.abs(y1 - y0); + const sx = x0 < x1 ? 1 : -1; + const sy = y0 < y1 ? 1 : -1; + let err = dxLine - dyLine; + let steps = 0; + while(true){ + const cx = Math.floor(x0), cy = Math.floor(y0); + if(cx===tc && cy===tr) return true; + // if tile is wall (excl source), block + if(!(cx===sc && cy===sr) && levelState.grid[cy] && levelState.grid[cy][cx]===1) return false; + if(Math.abs(x0 - x1) < 0.01 && Math.abs(y0 - y1) < 0.01) break; + const e2 = 2*err; + if(e2 > -dyLine){ err -= dyLine; x0 += sx; } + if(e2 < dxLine){ err += dxLine; y0 += sy; } + if(++steps > 200) break; + } + return true; +} + +/* Compute visible map each frame */ +function computeVisible(){ + visible = createEmptyGrid(ROWS, COLS, false); + for(let r=0;r { + if(visible[en.r][en.c]){ + const x = en.c*TILE_SIZE + TILE_SIZE/2; + const y = en.r*TILE_SIZE + TILE_SIZE/2; + ctx.fillStyle = '#ff5d5d'; + ctx.beginPath(); + ctx.arc(x,y, TILE_SIZE*0.28, 0, Math.PI*2); + ctx.fill(); + } + }); + + // draw orb glow using global composite - big radial gradient + const orbX = orb.c*TILE_SIZE + TILE_SIZE/2; + const orbY = orb.r*TILE_SIZE + TILE_SIZE/2; + + // whole screen dark overlay + ctx.fillStyle = 'rgba(2,3,6,0.85)'; + ctx.fillRect(0,0,canvas.width,canvas.height); + + // cut out illumination by destination-out with radial gradient + // create offscreen gradient canvas for better glow + const grd = ctx.createRadialGradient(orbX, orbY, 0, orbX, orbY, TILE_SIZE*(orb.lightRadius+1.5)); + grd.addColorStop(0, 'rgba(255,255,220,0.98)'); + grd.addColorStop(0.12, 'rgba(255,240,200,0.6)'); + grd.addColorStop(0.3, 'rgba(220,200,180,0.28)'); + grd.addColorStop(0.6, 'rgba(80,82,90,0.08)'); + grd.addColorStop(1, 'rgba(0,0,0,0)'); + + // We will draw gradient only on visible tiles to simulate walls blocking light: + // create a temporary canvas to mask LOS + const tmp = document.createElement('canvas'); + tmp.width = canvas.width; tmp.height = canvas.height; + const tctx = tmp.getContext('2d'); + // fill with transparent black + tctx.clearRect(0,0,tmp.width,tmp.height); + // draw gradient + tctx.fillStyle = grd; + tctx.fillRect(0,0,tmp.width,tmp.height); + + // Mask: set pixels for tiles not visible to fully transparent (erase) + for(let r=0;r{ + // simple random walker but they prefer to move toward orb if close + const dist = Math.hypot(en.r-orb.r, en.c-orb.c); + if(dist < 6 && Math.random() > 0.5){ + // move towards orb + const dr = Math.sign(orb.r - en.r); + const dc = Math.sign(orb.c - en.c); + tryMoveEn(en, en.r+dr, en.c+dc); + } else { + // random move + const d = randInt(0,4); + const nr = en.r + (d===0?1:d===1?-1:0); + const nc = en.c + (d===2?1:d===3?-1:0); + tryMoveEn(en, nr, nc); + } + }); +} + +function tryMoveEn(en, nr, nc){ + if(nr<1||nc<1||nr>=ROWS-1||nc>=COLS-1) return; + if(levelState.grid[nr][nc]===1) return; + // do not stack on orb + if(nr===orb.r && nc===orb.c) { + handleEnemyContact(en); + return; + } + en.r = nr; en.c = nc; + // enemy steps into trap/treasure ignored +} + +function handleEnemyContact(en){ + // if enemy sees orb (i.e., visible tile), it hurts player + if(visible[en.r][en.c]){ + // damage -> restart or penalty + playBeep(120, 'sawtooth', 0.18, 0.12); + showOverlay("Caught!", "An enemy caught your orb in the dark. You lose some light radius.", false); + orb.lightRadius = Math.max(2, orb.lightRadius - 1); + } +} + +/* Move orb */ +function moveOrb(dr, dc){ + if(paused) return; + const nr = orb.r + dr, nc = orb.c + dc; + if(nr<0||nc<0||nr>=ROWS||nc>=COLS) return; + // wall check + if(levelState.grid[nr][nc] === 1) { + playBeep(220, 'square', 0.08, 0.06); // bump + return; + } + orb.r = nr; orb.c = nc; + moves++; + movesEl.textContent = moves; + playBeep(600, 'sine', 0.06, 0.02); + // stepping interactions + const cell = levelState.grid[nr][nc]; + if(cell === 3){ + // treasure + collected++; + levelState.grid[nr][nc] = 0; + collectedEl.textContent = collected; + playBeep(980, 'triangle', 0.12, 0.12); + inventory.coins = (inventory.coins || 0) + 1; + inventoryEl.textContent = `Flares: ${inventory.flares} ยท Coins: ${inventory.coins || 0}`; + if(collected >= levelState.treasures) { + showOverlay("All Treasures Collected", "You found all treasures! Now find the exit.", true, false); + } + } else if(cell === 2) { + // trap: lose light radius + levelState.grid[nr][nc] = 0; // reveal trap used + playBeep(160, 'sawtooth', 0.18, 0.12); + orb.lightRadius = Math.max(2, orb.lightRadius - 1); + lightRadiusEl.textContent = orb.lightRadius; + showOverlay("Trap!", "A hidden trap damaged the orb's light. Light reduced.", false); + } else if(cell===4){ + // exit + if(collected >= Math.max(0, Math.floor(levelState.treasures/2))){ + // allow exit if collected some treasures (design choice) + showOverlay("Level Complete", "You reached the exit. Well done!", true); + } else { + showOverlay("Exit Locked", "You need to collect more treasures to unlock this exit.", false); + } + } + + // enemies may step + if(Math.random() < 0.45) stepEnemies(); +} + +/* Use flare */ +function useFlare(){ + if(paused) return; + if(flareCooldown>0) { + showOverlay("No Flares", "Flare is on cooldown.", false); + return; + } + if(inventory.flares <= 0) { + showOverlay("Out of Flares", "No flares left. Find treasures to get more.", false); + return; + } + inventory.flares--; + inventoryEl.textContent = `Flares: ${inventory.flares} ยท Coins: ${inventory.coins || 0}`; + flareCooldown = 18; // ticks for cooldown + // temporarily increase light radius + const prev = orb.lightRadius; + orb.lightRadius += 3; + lightRadiusEl.textContent = orb.lightRadius; + playBeep(1400, 'sine', 0.25, 0.18); + // schedule revert after some frames + setTimeout(()=>{ + orb.lightRadius = Math.max(2, prev); + lightRadiusEl.textContent = orb.lightRadius; + }, 2200); +} + +/* ========================= + UI Helpers + ========================= */ + +function showOverlay(title, text, allowContinue=true, autoHide=true){ + overlayTitle.textContent = title; + overlayText.textContent = text; + overlay.classList.remove('hidden'); + if(!allowContinue) overlayContinue.style.display = 'none'; else overlayContinue.style.display = 'inline-block'; + if(autoHide && allowContinue){ + setTimeout(()=>{ overlay.classList.add('hidden'); }, 1100); + } +} + +overlayRestart.addEventListener('click', ()=>{ startGame(); overlay.classList.add('hidden'); }); +overlayContinue.addEventListener('click', ()=>{ overlay.classList.add('hidden'); }); + +btnPause.addEventListener('click', ()=>{ paused = !paused; btnPause.textContent = paused ? 'Resume' : 'Pause'; }); +btnRestart.addEventListener('click', ()=>{ startGame(); }); + +btnFlare.addEventListener('click', ()=>{ useFlare(); }); + +/* Keyboard controls */ +window.addEventListener('keydown', (e)=>{ + if(['INPUT','TEXTAREA'].includes(document.activeElement.tagName)) return; + const k = e.key.toLowerCase(); + if(k === 'arrowup' || k === 'w') moveOrb(-1,0); + if(k === 'arrowdown' || k === 's') moveOrb(1,0); + if(k === 'arrowleft' || k === 'a') moveOrb(0,-1); + if(k === 'arrowright' || k === 'd') moveOrb(0,1); + if(k === ' ') { useFlare(); e.preventDefault(); } +}); + +/* On-screen arrow buttons */ +document.querySelectorAll('[data-dir]').forEach(btn=>{ + btn.addEventListener('click', ()=> { + const d = btn.getAttribute('data-dir'); + if(d==='up') moveOrb(-1,0); + if(d==='down') moveOrb(1,0); + if(d==='left') moveOrb(0,-1); + if(d==='right') moveOrb(0,1); + }); +}); + +/* ========================= + Main loop & init + ========================= */ + +function gameTick(){ + if(!paused){ + tick++; + if(flareCooldown>0) flareCooldown = Math.max(0, flareCooldown-1); + computeVisible(); + // ticks: occasionally move enemies + if(tick%60 === 0) stepEnemies(); + draw(); + } + requestAnimationFrame(gameTick); +} + +function startGame(){ + const level = generateLevel(); + levelState = level; + orb = { r: level.spawn.r, c: level.spawn.c, lightRadius: 4 }; + moves = 0; collected = 0; tick = 0; flareCooldown=0; + inventory = {flares:1, coins:0}; + spawnEnemies(); + movesEl.textContent = moves; + lightRadiusEl.textContent = orb.lightRadius; + collectedEl.textContent = collected; + totalTreasuresEl.textContent = level.treasures; + inventoryEl.textContent = `Flares: ${inventory.flares} ยท Coins: ${inventory.coins}`; + levelNameEl.textContent = "Light Orb Quest โ€” Vault"; + paused = false; btnPause.textContent = 'Pause'; + computeVisible(); + playBeep(720, 'sine', 0.12, 0.04); + // small start flash + setTimeout(()=>{ playBeep(980, 'triangle', 0.06, 0.06); }, 120); +} + +/* ========================= + Start everything + ========================= */ +startGame(); +gameTick(); diff --git a/games/light-orb-quest/style.css b/games/light-orb-quest/style.css new file mode 100644 index 00000000..25e84afc --- /dev/null +++ b/games/light-orb-quest/style.css @@ -0,0 +1,46 @@ +:root{ + --bg:#0b0f13; + --panel:#0f1720; + --accent:#7bd389; + --muted:#9aa6b2; + --glass: rgba(255,255,255,0.03); + --card: rgba(255,255,255,0.04); +} + +*{box-sizing:border-box;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial;} +html,body{height:100%;margin:0;background:linear-gradient(180deg,#05060a 0%, #071028 100%);color:#e6eef6;} +.topbar{ + display:flex;justify-content:space-between;align-items:center;padding:12px 18px;background:linear-gradient(90deg,rgba(255,255,255,0.03),transparent); + border-bottom:1px solid rgba(255,255,255,0.03); +} +.topbar .left{display:flex;gap:18px;align-items:center;} +.level-name{font-weight:700;font-size:16px;color:var(--accent);} +.stats{display:flex;gap:12px;color:var(--muted);font-size:13px;align-items:center;} +.right button{margin-left:8px;padding:8px 12px;border-radius:8px;border:0;background:var(--card);color:#fff;cursor:pointer;} +.right button:hover{transform:translateY(-1px);} + +.game-wrap{display:flex;gap:18px;padding:18px;height:calc(100vh - 74px);} +canvas{background:linear-gradient(180deg,#0b1220 0%, #071022 100%);border-radius:10px;box-shadow:0 8px 30px rgba(0,0,0,0.7);flex:1;display:block;margin:auto;} +.hud{width:320px;display:flex;flex-direction:column;gap:12px;} +.panel{background:var(--panel);padding:12px;border-radius:10px;border:1px solid rgba(255,255,255,0.03);} +.panel h3{margin:0 0 8px 0;color:var(--muted);font-weight:600;font-size:14px;} +.controls-grid{display:flex;flex-direction:column;gap:6px;align-items:center;} +.controls-grid .arrow{padding:10px 14px;border-radius:8px;background:var(--glass);border:1px solid rgba(255,255,255,0.03);cursor:pointer;color:#fff;font-weight:700;} +.controls-grid button:hover{transform:translateY(-2px);} + +.overlay{ + position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:linear-gradient(180deg,rgba(1,3,5,0.6),rgba(3,6,10,0.7)); +} +.overlay.hidden{display:none;} +.overlay-card{background:linear-gradient(180deg,rgba(255,255,255,0.02),rgba(255,255,255,0.01));padding:28px;border-radius:12px;border:1px solid rgba(255,255,255,0.04);min-width:320px;text-align:center;} +.overlay-card h2{margin:0 0 8px;color:var(--accent);} +.overlay-card p{color:var(--muted);margin:0 0 16px;} +.overlay-actions{display:flex;gap:10px;justify-content:center;align-items:center;} +.overlay-actions button{padding:8px 12px;border-radius:8px;border:0;background:var(--accent);color:#052018;cursor:pointer;} +.back-hub{color:var(--muted);text-decoration:none;padding:8px 10px;border-radius:8px;border:1px solid rgba(255,255,255,0.03);} + +@media (max-width:1000px){ + .game-wrap{flex-direction:column;padding:12px;} + .hud{width:100%;} + canvas{width:100%;height:480px;} +} diff --git a/games/lights-out/index.html b/games/lights-out/index.html new file mode 100644 index 00000000..1e5a1be0 --- /dev/null +++ b/games/lights-out/index.html @@ -0,0 +1,56 @@ + + + + + + Lights Out Puzzle + + + +
    + +
    +

    Lights Out Puzzle

    +

    Turn all the lights off to win!

    +
    + +
    + + + + + + + + +
    + +
    + Moves: 0 + Time: 00:00 + +
    + +
    + +
    + + + + + + + + + diff --git a/games/lights-out/script.js b/games/lights-out/script.js new file mode 100644 index 00000000..62f4d250 --- /dev/null +++ b/games/lights-out/script.js @@ -0,0 +1,118 @@ +const gridEl = document.getElementById("grid"); +const movesEl = document.getElementById("moves"); +const timerEl = document.getElementById("timer"); +const soundToggle = document.getElementById("soundToggle"); +const modal = document.getElementById("winModal"); +const clickSound = document.getElementById("clickSound"); + +let grid = []; +let moves = 0; +let timer = 0; +let interval; +let gridSize = 5; + +function startTimer() { + clearInterval(interval); + interval = setInterval(() => { + timer++; + let m = String(Math.floor(timer / 60)).padStart(2, '0'); + let s = String(timer % 60).padStart(2, '0'); + timerEl.textContent = `${m}:${s}`; + }, 1000); +} + +function resetStats() { + moves = 0; + timer = 0; + movesEl.textContent = 0; + timerEl.textContent = "00:00"; +} + +function generateGrid(size) { + gridSize = size; + gridEl.style.gridTemplateColumns = `repeat(${size}, 55px)`; + + grid = Array.from({ length: size }, () => + Array.from({ length: size }, () => false) + ); + + render(); + shuffle(); + resetStats(); + startTimer(); +} + +function render() { + gridEl.innerHTML = ""; + + grid.forEach((row, r) => { + row.forEach((cell, c) => { + const div = document.createElement("div"); + div.className = `cell ${cell ? "on" : "off"}`; + div.addEventListener("click", () => toggleCell(r, c)); + gridEl.append(div); + }); + }); +} + +function playSound() { + if (soundToggle.checked) clickSound.play(); +} + +function toggleCell(r, c) { + playSound(); + moves++; + movesEl.textContent = moves; + + const dirs = [ + [0, 0], + [1, 0], + [-1, 0], + [0, 1], + [0, -1], + ]; + + dirs.forEach(([dr, dc]) => { + let nr = r + dr; + let nc = c + dc; + if (grid[nr] && grid[nr][nc] !== undefined) + grid[nr][nc] = !grid[nr][nc]; + }); + + render(); + checkWin(); +} + +function shuffle() { + for (let i = 0; i < gridSize ** 2; i++) { + toggleCell(Math.floor(Math.random() * gridSize), Math.floor(Math.random() * gridSize)); + } + moves = 0; +} + +function checkWin() { + if (grid.every(row => row.every(cell => !cell))) { + clearInterval(interval); + document.getElementById("finalMoves").textContent = moves + " moves"; + document.getElementById("finalTime").textContent = timerEl.textContent; + modal.classList.remove("hidden"); + } +} + +window.closeModal = function () { + modal.classList.add("hidden"); + generateGrid(gridSize); +} + +document.getElementById("shuffleBtn").onclick = shuffle; +document.getElementById("resetBtn").onclick = () => generateGrid(gridSize); +document.getElementById("newGameBtn").onclick = () => generateGrid(gridSize); + +document.getElementById("grid-size").onchange = (e) => generateGrid(+e.target.value); + +document.getElementById("customSizeBtn").onclick = () => { + const n = +document.getElementById("customSizeInput").value; + if (n >= 2 && n <= 12) generateGrid(n); +}; + +generateGrid(5); // Default diff --git a/games/lights-out/style.css b/games/lights-out/style.css new file mode 100644 index 00000000..62dc4040 --- /dev/null +++ b/games/lights-out/style.css @@ -0,0 +1,91 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: Poppins, sans-serif; +} + +body { + background: #0f172a; + color: white; + display: flex; + justify-content: center; + padding: 40px 0; +} + +.game-container { + background: #1e293b; + padding: 25px; + border-radius: 14px; + width: 420px; + text-align: center; + box-shadow: 0 0 12px rgba(0,0,0,0.4); +} + +header h1 { + margin-bottom: 10px; +} + +.controls, .stats { + margin: 12px 0; + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 8px; +} + +.controls button, select, input { + padding: 6px 12px; + border-radius: 6px; + border: none; + cursor: pointer; + outline: none; + font-size: 14px; +} + +.controls button:hover { + background: #475569; +} + +.grid { + margin-top: 18px; + display: grid; + gap: 6px; +} + +.cell { + width: 55px; + height: 55px; + border-radius: 8px; + transition: 0.2s; + cursor: pointer; +} + +.on { + background: #ffd93d; + box-shadow: 0 0 20px #ffd43b; +} + +.off { + background: #334155; +} + +.modal { + position: fixed; + inset: 0; + background: #0009; + display: grid; + place-items: center; +} + +.modal.hidden { + display: none; +} + +.modal-content { + background: #1e293b; + padding: 28px; + border-radius: 10px; + text-align: center; + width: 280px; +} diff --git a/games/lights-out2/index.html b/games/lights-out2/index.html new file mode 100644 index 00000000..ef4572ca --- /dev/null +++ b/games/lights-out2/index.html @@ -0,0 +1,39 @@ + + + + + + Lights Out Puzzle + + + + +
    +

    LIGHTS OUT

    +

    Turn all the dark lights ON (yellow) to win!

    + +
    +
    + MOVES: + 0 +
    + +
    + +
    +
    +
    + + + + \ No newline at end of file diff --git a/games/lights-out2/script.js b/games/lights-out2/script.js new file mode 100644 index 00000000..0f3cd0a6 --- /dev/null +++ b/games/lights-out2/script.js @@ -0,0 +1,176 @@ +// --- Game Constants and State --- +const SIZE = 5; +const LIGHT_ON = 1; +const LIGHT_OFF = 0; +let boardState = []; +let moves = 0; +let isGameOver = false; + +// DOM Elements +const gridElement = document.getElementById('game-grid'); +const movesCounter = document.getElementById('moves-counter'); +const resetButton = document.getElementById('reset-button'); +const winModal = document.getElementById('win-modal'); +const finalMovesText = document.getElementById('final-moves'); +const playAgainButton = document.getElementById('play-again-button'); + +// --- Core Game Logic --- + +/** + * Generates a solvable Lights Out board. + * This is done by starting with a solved board (all OFF) and performing + * a sequence of random, valid moves. + * @returns {number[][]} The initial 5x5 board state (0s and 1s). + */ +function generateSolvableBoard() { + // Start with a solved board (all lights off) + let board = Array.from({ length: SIZE }, () => Array(SIZE).fill(LIGHT_OFF)); + + // Perform 15 to 30 random moves to guarantee solvability + const numMoves = Math.floor(Math.random() * 16) + 15; + + for (let i = 0; i < numMoves; i++) { + const r = Math.floor(Math.random() * SIZE); + const c = Math.floor(Math.random() * SIZE); + // Use the internal toggle function, without updating the UI + toggleLight(board, r, c, false); + } + return board; +} + +/** + * Toggles the state of a single cell and its four orthogonal neighbors. + * @param {number[][]} board - The 2D array representing the game state. + * @param {number} r - Row index (0-4). + * @param {number} c - Column index (0-4). + * @param {boolean} [updateUI=true] - If true, updates the DOM and checks win condition. + */ +function toggleLight(board, r, c, updateUI = true) { + // Offsets: Center, Right, Left, Down, Up + const offsets = [ + [0, 0], [0, 1], [0, -1], [1, 0], [-1, 0] + ]; + + offsets.forEach(([dr, dc]) => { + const nr = r + dr; + const nc = c + dc; + + // Check if the neighbor is within bounds (0 to SIZE-1) + if (nr >= 0 && nr < SIZE && nc >= 0 && nc < SIZE) { + // Toggle the state (0 -> 1, 1 -> 0) + board[nr][nc] = 1 - board[nr][nc]; + + if (updateUI) { + // Update the corresponding DOM element class + const cellElement = document.getElementById(`c-${nr}-${nc}`); + if (cellElement) { + const isOn = board[nr][nc] === LIGHT_ON; + cellElement.classList.toggle('on', isOn); + cellElement.classList.toggle('off', !isOn); + } + } + } + }); + + if (updateUI) { + moves++; + movesCounter.textContent = moves; + checkWinCondition(); + } +} + +/** + * Checks if the board is solved. + */ +function checkWinCondition() { + // Use reduce/flat to sum all elements in the 2D array. If sum is 0, all are OFF. + const totalLightsOn = boardState.flat().reduce((sum, val) => sum + val, 0); + + if (totalLightsOn === 0) { + isGameOver = true; + showModal(); + } +} + +// --- UI and Game Flow --- + +function showModal() { + finalMovesText.textContent = moves; + winModal.classList.remove('hidden'); + // Disable clicks on the grid + gridElement.style.pointerEvents = 'none'; +} + +function hideModal() { + winModal.classList.add('hidden'); + // Re-enable clicks on the grid + gridElement.style.pointerEvents = 'auto'; +} + +function createGridElements() { + gridElement.innerHTML = ''; // Clear existing grid + for (let r = 0; r < SIZE; r++) { + for (let c = 0; c < SIZE; c++) { + const cell = document.createElement('div'); + cell.id = `c-${r}-${c}`; + cell.classList.add('light-cell'); + cell.setAttribute('data-row', r); + cell.setAttribute('data-col', c); + cell.addEventListener('click', handleCellClick); + gridElement.appendChild(cell); + } + } +} + +function updateGridUI() { + for (let r = 0; r < SIZE; r++) { + for (let c = 0; c < SIZE; c++) { + const cell = document.getElementById(`c-${r}-${c}`); + const isOn = boardState[r][c] === LIGHT_ON; + cell.classList.toggle('on', isOn); + cell.classList.toggle('off', !isOn); + } + } +} + +function handleCellClick(event) { + if (isGameOver) return; + + const cell = event.currentTarget; + const r = parseInt(cell.getAttribute('data-row')); + const c = parseInt(cell.getAttribute('data-col')); + + // Perform the game logic + toggleLight(boardState, r, c); +} + +function startGame() { + // Reset state + isGameOver = false; + moves = 0; + movesCounter.textContent = 0; + hideModal(); + + // Initialize board + boardState = generateSolvableBoard(); + + // Setup UI + createGridElements(); + updateGridUI(); +} + +// --- Event Listeners and Initialization --- + +// Start the game when the page loads +document.addEventListener('DOMContentLoaded', startGame); + +// Buttons for starting/restarting the game +resetButton.addEventListener('click', startGame); +playAgainButton.addEventListener('click', startGame); + +// Allow clicking outside the modal to close it +winModal.addEventListener('click', (e) => { + if (e.target === winModal) { + hideModal(); + } +}); \ No newline at end of file diff --git a/games/lights-out2/style.css b/games/lights-out2/style.css new file mode 100644 index 00000000..865a070c --- /dev/null +++ b/games/lights-out2/style.css @@ -0,0 +1,196 @@ +/* Global Reset and Typography */ +:root { + --grid-size: 5; + --gap-size: 8px; + --light-color: #ffde59; + --dark-bg: #0d1117; + --card-bg: #161b22; + --cell-off: #21262d; + --cell-border: #30363d; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Arial', sans-serif; + background-color: var(--dark-bg); + color: #c9d1d9; + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + padding: 20px; +} + +/* Container and Header */ +#game-container { + width: 100%; + max-width: 500px; + background-color: var(--card-bg); + padding: 24px; + border-radius: 12px; + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.5); + text-align: center; +} + +h1 { + font-size: 2.5rem; + color: #fff; + margin-bottom: 5px; +} + +p { + font-size: 0.9rem; + color: #8b949e; + margin-bottom: 20px; +} + +p span { + font-weight: bold; + color: var(--light-color); +} + +/* Stats Bar */ +.stats-bar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.moves-box { + background-color: #0d1117; + padding: 6px 12px; + border-radius: 8px; +} + +.moves-box .label { + font-size: 0.8rem; + color: #8b949e; + margin-right: 5px; +} + +.moves-box .value { + font-size: 1.5rem; + font-weight: bold; + color: var(--light-color); +} + +/* Grid Layout */ +#game-grid { + display: grid; + grid-template-columns: repeat(var(--grid-size), 1fr); + gap: var(--gap-size); + /* Ensure the grid is square */ + width: min(100%, 400px); + height: min(100%, 400px); + margin: 0 auto; +} + +/* Light Cell Styling */ +.light-cell { + width: 100%; + /* Use padding-bottom trick to maintain aspect ratio */ + padding-bottom: 100%; + position: relative; + cursor: pointer; + border-radius: 8px; + transition: all 0.2s ease-in-out; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); +} + +/* State: OFF (Dark) */ +.light-cell.off { + background-color: var(--cell-off); + border: 2px solid var(--cell-border); +} + +/* State: ON (Light) */ +.light-cell.on { + background-color: var(--light-color); + border: 2px solid #ffaa00; + box-shadow: 0 0 15px rgba(255, 222, 89, 0.8), inset 0 0 5px rgba(255, 255, 255, 0.5); +} + +/* Interaction Feedback */ +.light-cell:active { + transform: scale(0.95); + box-shadow: 0 2px 3px rgba(0, 0, 0, 0.5); +} + +/* Buttons */ +button { + background-color: #3b82f6; /* Blue button */ + color: white; + font-weight: bold; + padding: 10px 20px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +button:hover { + background-color: #2563eb; +} + +/* Modal Styling */ +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(3px); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; +} + +.modal.hidden { + display: none; +} + +.modal-content { + background-color: var(--card-bg); + padding: 40px; + border-radius: 12px; + text-align: center; + box-shadow: 0 15px 30px rgba(0, 0, 0, 0.7); + max-width: 350px; + transform: scale(1); + transition: transform 0.3s ease; +} + +.modal-content h2 { + font-size: 3rem; + color: #10b981; /* Green color for win */ + margin-bottom: 15px; +} + +.modal-content p { + font-size: 1.2rem; + color: #fff; + margin-bottom: 25px; +} + +.modal-content #final-moves { + font-weight: bold; + color: var(--light-color); +} + +.modal-content button { + background-color: #10b981; + width: 100%; + padding: 12px; +} + +.modal-content button:hover { + background-color: #059669; +} \ No newline at end of file diff --git a/games/line-game/index.html b/games/line-game/index.html new file mode 100644 index 00000000..e71cf7ff --- /dev/null +++ b/games/line-game/index.html @@ -0,0 +1,22 @@ + + + + + + Line Game | Mini JS Games Hub + + + +
    +

    โšก Line Game โšก

    + + +
    +
    Score: 0
    + +
    +
    + + + + diff --git a/games/line-game/script.js b/games/line-game/script.js new file mode 100644 index 00000000..c82ad9f0 --- /dev/null +++ b/games/line-game/script.js @@ -0,0 +1,152 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const scoreEl = document.getElementById('score'); +const restartBtn = document.getElementById('restartBtn'); + +const player = { + x: canvas.width / 2, + y: canvas.height - 60, + width: 10, + height: 50, + color: '#00ffff', + speed: 5, + dx: 0, + dy: 0, +}; + +let obstacles = []; +let score = 0; +let gameOver = false; +let frame = 0; + +// Control +document.addEventListener('keydown', move); +document.addEventListener('keyup', stop); +canvas.addEventListener('mousemove', mouseControl); +restartBtn.addEventListener('click', restartGame); + +function move(e) { + if (e.key === 'ArrowLeft') player.dx = -player.speed; + if (e.key === 'ArrowRight') player.dx = player.speed; + if (e.key === 'ArrowUp') player.dy = -player.speed; + if (e.key === 'ArrowDown') player.dy = player.speed; +} + +function stop(e) { + if ( + ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key) + ) { + player.dx = 0; + player.dy = 0; + } +} + +function mouseControl(e) { + const rect = canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + player.x = mouseX - player.width / 2; +} + +// Obstacle generation +function generateObstacle() { + const width = Math.random() * 100 + 50; + const x = Math.random() * (canvas.width - width); + const y = -30; + const speed = Math.random() * 2 + 2; + obstacles.push({ x, y, width, height: 20, speed, color: '#ff007f' }); +} + +// Draw player +function drawPlayer() { + ctx.fillStyle = player.color; + ctx.fillRect(player.x, player.y, player.width, player.height); +} + +// Draw obstacles +function drawObstacles() { + obstacles.forEach((ob) => { + ctx.fillStyle = ob.color; + ctx.fillRect(ob.x, ob.y, ob.width, ob.height); + }); +} + +// Move player & obstacles +function updatePositions() { + player.x += player.dx; + player.y += player.dy; + + // Boundaries + if (player.x < 0) player.x = 0; + if (player.x + player.width > canvas.width) + player.x = canvas.width - player.width; + if (player.y < 0) player.y = 0; + if (player.y + player.height > canvas.height) + player.y = canvas.height - player.height; + + obstacles.forEach((ob) => (ob.y += ob.speed)); + obstacles = obstacles.filter((ob) => ob.y < canvas.height + 20); +} + +// Collision detection +function detectCollision() { + for (let ob of obstacles) { + if ( + player.x < ob.x + ob.width && + player.x + player.width > ob.x && + player.y < ob.y + ob.height && + player.y + player.height > ob.y + ) { + gameOver = true; + return true; + } + } + return false; +} + +// Update Score +function updateScore() { + if (!gameOver) { + score++; + scoreEl.textContent = score; + } +} + +// Restart +function restartGame() { + score = 0; + obstacles = []; + frame = 0; + gameOver = false; + player.x = canvas.width / 2; + player.y = canvas.height - 60; + loop(); +} + +// Main game loop +function loop() { + if (gameOver) { + ctx.fillStyle = 'rgba(0,0,0,0.5)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = '#fff'; + ctx.font = '30px Poppins'; + ctx.fillText('๐Ÿ’€ Game Over', canvas.width / 2 - 100, canvas.height / 2); + ctx.font = '18px Poppins'; + ctx.fillText('Press Restart to Play Again', canvas.width / 2 - 120, canvas.height / 2 + 40); + return; + } + + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawPlayer(); + drawObstacles(); + updatePositions(); + + if (frame % 60 === 0) generateObstacle(); + detectCollision(); + updateScore(); + + frame++; + requestAnimationFrame(loop); +} + +// Start +loop(); diff --git a/games/line-game/style.css b/games/line-game/style.css new file mode 100644 index 00000000..d46e9624 --- /dev/null +++ b/games/line-game/style.css @@ -0,0 +1,61 @@ +* { + box-sizing: border-box; +} + +body { + font-family: 'Poppins', sans-serif; + background: radial-gradient(circle at center, #101820 0%, #050a0e 100%); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + color: #fff; +} + +.game-container { + text-align: center; +} + +.game-title { + font-size: 2rem; + margin-bottom: 10px; + letter-spacing: 2px; + color: #00ffff; + text-shadow: 0 0 15px #00ffff; +} + +canvas { + border: 3px solid #00ffff; + border-radius: 10px; + background: linear-gradient(180deg, #0a192f, #112240); + box-shadow: 0 0 20px #00ffff44; +} + +.info-panel { + margin-top: 15px; + display: flex; + justify-content: center; + align-items: center; + gap: 30px; +} + +button { + background-color: #00ffff; + border: none; + border-radius: 5px; + padding: 8px 16px; + font-weight: 600; + color: #0a192f; + cursor: pointer; + transition: 0.3s; +} + +button:hover { + background-color: #08f7fe; + transform: scale(1.05); +} + +.score { + font-size: 1.2rem; +} diff --git a/games/link-game/index.html b/games/link-game/index.html new file mode 100644 index 00000000..a07b53f3 --- /dev/null +++ b/games/link-game/index.html @@ -0,0 +1,34 @@ + + + + + + Link Game | Mini JS Games Hub + + + +
    +
    +

    ๐Ÿ”— Link Game

    +

    Connect matching tiles before you run out of moves!

    +
    + Moves: 0 + Score: 0 +
    +
    + +
    + +
    + +
    + +
    +

    Game Over!

    + +
    +
    + + + + diff --git a/games/link-game/script.js b/games/link-game/script.js new file mode 100644 index 00000000..4fe3eee7 --- /dev/null +++ b/games/link-game/script.js @@ -0,0 +1,98 @@ +const board = document.getElementById("board"); +const movesDisplay = document.getElementById("moves"); +const scoreDisplay = document.getElementById("score"); +const gameOverScreen = document.getElementById("game-over"); +const message = document.getElementById("message"); +const playAgain = document.getElementById("play-again"); +const reset = document.getElementById("reset"); + +let firstTile = null; +let secondTile = null; +let moves = 0; +let score = 0; + +const icons = ["๐ŸŽ", "๐ŸŒ", "๐Ÿ‡", "๐Ÿ“", "๐ŸŠ", "๐Ÿ‰", "๐Ÿ’", "๐Ÿฅ"]; +let tiles = [...icons, ...icons]; + +function shuffle(array) { + return array.sort(() => Math.random() - 0.5); +} + +function createBoard() { + board.innerHTML = ""; + shuffle(tiles); + tiles.forEach((icon, index) => { + const tile = document.createElement("div"); + tile.classList.add("tile"); + tile.dataset.icon = icon; + tile.dataset.index = index; + tile.addEventListener("click", handleTileClick); + board.appendChild(tile); + }); +} + +function handleTileClick(e) { + const tile = e.target; + + if (tile.classList.contains("matched") || tile === firstTile) return; + + tile.textContent = tile.dataset.icon; + + if (!firstTile) { + firstTile = tile; + } else { + secondTile = tile; + moves++; + movesDisplay.textContent = moves; + checkMatch(); + } +} + +function checkMatch() { + if (firstTile.dataset.icon === secondTile.dataset.icon) { + firstTile.classList.add("matched"); + secondTile.classList.add("matched"); + score += 10; + scoreDisplay.textContent = score; + resetSelection(); + checkWin(); + } else { + setTimeout(() => { + firstTile.textContent = ""; + secondTile.textContent = ""; + resetSelection(); + }, 700); + } +} + +function resetSelection() { + firstTile = null; + secondTile = null; +} + +function checkWin() { + const allMatched = document.querySelectorAll(".matched").length === tiles.length; + if (allMatched) { + showGameOver("๐ŸŽ‰ You Won!"); + } +} + +function showGameOver(text) { + message.textContent = text; + gameOverScreen.style.display = "block"; +} + +function restartGame() { + moves = 0; + score = 0; + movesDisplay.textContent = moves; + scoreDisplay.textContent = score; + gameOverScreen.style.display = "none"; + resetSelection(); + createBoard(); +} + +playAgain.addEventListener("click", restartGame); +reset.addEventListener("click", restartGame); + +createBoard(); diff --git a/games/link-game/style.css b/games/link-game/style.css new file mode 100644 index 00000000..0bbb2227 --- /dev/null +++ b/games/link-game/style.css @@ -0,0 +1,102 @@ +body { + margin: 0; + font-family: "Poppins", sans-serif; + background: linear-gradient(135deg, #0d47a1, #1976d2); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + height: 100vh; +} + +.game-container { + background: #ffffff1a; + backdrop-filter: blur(10px); + border-radius: 20px; + padding: 20px 40px; + text-align: center; + width: 360px; + box-shadow: 0 0 20px rgba(0,0,0,0.3); +} + +header h1 { + margin: 0; + font-size: 1.8rem; +} + +.stats { + display: flex; + justify-content: space-between; + margin: 10px 0; + font-weight: 500; +} + +.board { + display: grid; + grid-template-columns: repeat(4, 70px); + grid-gap: 10px; + justify-content: center; + margin: 20px auto; +} + +.tile { + width: 70px; + height: 70px; + border-radius: 10px; + background-color: #64b5f6; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s, background 0.3s; +} + +.tile:hover { + transform: scale(1.1); + background-color: #42a5f5; +} + +.tile.matched { + background-color: #66bb6a; + color: #fff; + cursor: default; + transform: scale(1.05); +} + +.controls { + margin-top: 10px; +} + +button { + background-color: #1565c0; + color: white; + border: none; + border-radius: 8px; + padding: 10px 18px; + cursor: pointer; + font-size: 1rem; + transition: background 0.3s; +} + +button:hover { + background-color: #0d47a1; +} + +.game-over { + display: none; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0,0,0,0.8); + color: #fff; + padding: 30px; + border-radius: 15px; + text-align: center; +} + +.game-over h2 { + margin-bottom: 20px; +} diff --git a/games/local_storage/index.html b/games/local_storage/index.html new file mode 100644 index 00000000..c631cf5d --- /dev/null +++ b/games/local_storage/index.html @@ -0,0 +1,52 @@ + + + + + + The Local Storage Legacy ๐Ÿ’พ + + + +
    +

    The Local Storage Legacy ๐Ÿ’พ

    +

    An idle game where your save file is your primary tool. Fix the corruption!

    +
    + +
    + +
    +

    Current Stats

    +
    +
    + + + + +
    +

    Corruption Status

    +

    System Nominal.

    + +
    +
    + +
    +

    Local Save File (`gameState`)

    +

    ๐Ÿšจ **Warning:** Altering this data incorrectly will trigger a System Reset.

    + + + +
    + + +
    + +
    + Welcome, Detective. Your save file is unencrypted. +
    +
    + +
    + + + + \ No newline at end of file diff --git a/games/local_storage/script.js b/games/local_storage/script.js new file mode 100644 index 00000000..c49d6f2f --- /dev/null +++ b/games/local_storage/script.js @@ -0,0 +1,289 @@ +// --- 1. Global Constants and State --- +const STORAGE_KEY = 'LS_Legacy_Save'; +const TICK_RATE = 1000; // 1 second +const CHECKSUM_SALT = 42; // Used for a simple integrity check + +const INITIAL_STATE = { + gold: 0, + miners: 0, + hackerPoints: 0, + corruptionLevel: 0, + gameTime: 0, + puzzleActive: false, + puzzleId: 0, + checksum: 0 // Placeholder for the integrity check +}; + +let gameState = {}; +let gameLoopInterval = null; + +// --- 2. DOM Elements --- +const D = (id) => document.getElementById(id); +const $ = { + statusDisplay: D('status-display'), + saveEditor: D('save-editor'), + consoleOutput: D('console-output'), + mineGoldBtn: D('mine-gold'), + buyMinerBtn: D('buy-miner'), + loadSaveBtn: D('load-save'), + applyChangesBtn: D('apply-changes'), + corruptionMessage: D('corruption-message'), + progressButton: D('progress-button') +}; + +// --- 3. LocalStorage & State Management --- + +/** + * Calculates a simple integrity checksum. + * This function should be hard to reverse engineer by the player initially. + */ +function calculateChecksum(state) { + // Sum of core values + a fixed salt. + const sum = state.gold + state.miners + state.hackerPoints + CHECKSUM_SALT; + return Math.floor(sum * state.corruptionLevel); // Multiplied by corruption for complexity +} + +/** + * Loads the state from localStorage or initializes a new game. + */ +function loadGameState() { + const storedState = localStorage.getItem(STORAGE_KEY); + if (storedState) { + try { + gameState = JSON.parse(storedState); + // Verify checksum on load + if (calculateChecksum(gameState) !== gameState.checksum) { + console.error("Checksum mismatch on load. Data might be tampered with outside the editor."); + // This could be a tougher penalty, but for now, we trust the in-game editor logic. + } + } catch (e) { + console.error("Save file corrupted, starting new game.", e); + gameState = { ...INITIAL_STATE }; + } + } else { + gameState = { ...INITIAL_STATE }; + } + // Calculate and apply initial checksum for a clean start + gameState.checksum = calculateChecksum(gameState); + saveState(); + updateUI(); + loadEditor(); +} + +/** + * Saves the current state to localStorage after updating the checksum. + */ +function saveState() { + gameState.checksum = calculateChecksum(gameState); + localStorage.setItem(STORAGE_KEY, JSON.stringify(gameState)); + loadEditor(); +} + +/** + * Populates the in-game JSON editor with the current state. + */ +function loadEditor() { + $.saveEditor.value = JSON.stringify(gameState, null, 4); +} + +// --- 4. Game Loop and Idle Mechanics --- + +function idleTick() { + // 1. Idle Production + gameState.gold += gameState.miners * 0.1; // 0.1 gold per miner per second + gameState.gameTime += TICK_RATE / 1000; + + // 2. Puzzle Generation (Based on game time/progress) + if (gameState.gameTime > 15 && gameState.puzzleId === 0) { + introducePuzzle(1); + } else if (gameState.gameTime > 60 && gameState.puzzleId === 1 && !gameState.puzzleActive) { + introducePuzzle(2); + } + + // 3. UI and Save + updateUI(); + saveState(); +} + +function updateUI() { + // Update Stats Display + $.statusDisplay.innerHTML = ` +
    Gold: ${gameState.gold.toFixed(2)}
    +
    Miners: ${gameState.miners}
    +
    Hacker Points: ${gameState.hackerPoints}
    +
    Game Time: ${Math.floor(gameState.gameTime)}s
    + `; + + // Update Action Buttons + $.buyMinerBtn.disabled = gameState.gold < 10; + + // Update Corruption Panel + $.corruptionMessage.textContent = gameState.puzzleActive + ? `CORRUPTION DETECTED (ID:${gameState.puzzleId})! ${getPuzzleDescription(gameState.puzzleId)}` + : "System Nominal. Awaiting next data anomaly."; + + $.progressButton.disabled = !gameState.puzzleActive; +} + + +// --- 5. Corruption Puzzles (The Core Mechanic) --- + +function getPuzzleDescription(id) { + switch(id) { + case 1: + return "The Gold Mine is overloaded! The 'gold' variable is showing a negative overflow. Use the editor to **fix the 'gold' value** to its maximum capacity (20)."; + case 2: + return "The Miner production queue is stalled. The 'miners' variable is incorrectly set to 0. You must **increase 'miners' to 5** to restore production, costing 2 Hacker Points."; + default: + return "ERROR: Unknown corruption type."; + } +} + +function introducePuzzle(id) { + gameState.puzzleActive = true; + gameState.puzzleId = id; + gameState.corruptionLevel = 1; // Increase difficulty/risk + + if (id === 1) { + // Deliberate corruption for puzzle 1 + gameState.gold = -100; + } + if (id === 2) { + // Deliberate corruption for puzzle 2 + gameState.miners = 0; + // Cost is handled upon successful submission + } + updateUI(); + saveState(); +} + +/** + * Checks the player's edited state against the required solution. + */ +function checkPuzzleSolution() { + const puzzleId = gameState.puzzleId; + let requiredState = {}; + + switch(puzzleId) { + case 1: + requiredState = { gold: 20 }; + break; + case 2: + requiredState = { miners: 5 }; + if (gameState.hackerPoints < 2) { + $.consoleOutput.textContent = 'โŒ Insufficient Hacker Points (Need 2) to resolve this corruption level.'; + return false; + } + break; + default: + return false; + } + + let isSolved = true; + + // Check if the player's *new* state matches the required change + for (const key in requiredState) { + if (gameState[key] !== requiredState[key]) { + isSolved = false; + break; + } + } + + if (isSolved) { + resolvePuzzle(puzzleId); + return true; + } else { + $.consoleOutput.textContent = 'โŒ Solution does not match requirements. Check the variable and value required.'; + return false; + } +} + +function resolvePuzzle(id) { + // Reward and reset + gameState.puzzleActive = false; + gameState.corruptionLevel = 0; + + let reward = 1; // Default + let cost = 0; + + if (id === 2) { + cost = 2; // Puzzle 2 costs Hacker Points + reward = 3; + } + + gameState.hackerPoints += reward; + gameState.hackerPoints -= cost; + + $.consoleOutput.textContent = `โœ… Corruption ID:${id} RESOLVED! Gained ${reward} Hacker Points.`; + updateUI(); + saveState(); +} + +// --- 6. Editor and Action Listeners --- + +function handleApplyChanges() { + const editorValue = $.saveEditor.value; + let newGameState; + + try { + newGameState = JSON.parse(editorValue); + } catch (e) { + $.consoleOutput.textContent = 'โŒ JSON Parse Error. Check your syntax (missing commas, brackets, etc.).'; + return; + } + + // 1. Check for Checksum Tampering + const expectedChecksum = calculateChecksum(newGameState); + if (newGameState.checksum !== expectedChecksum) { + $.consoleOutput.textContent = '๐Ÿšจ SYSTEM RESET: Checksum integrity failure! Penalty applied.'; + + // --- Punishment --- + gameState = { ...INITIAL_STATE, gold: 0, miners: 0 }; // Full reset but keep Hacker Points + gameState.hackerPoints = Math.max(0, newGameState.hackerPoints - 1); // Penalize 1 Hacker Point + // --- End Punishment --- + + saveState(); + return; + } + + // 2. Apply Valid Changes + // This allows the player to "cheat" (e.g., set gold to 99999) as long as the checksum is correct. + gameState = newGameState; + $.consoleOutput.textContent = 'โœ… Save file validated and applied. Current game state updated.'; + + // 3. Check for Active Puzzle Solution + if (gameState.puzzleActive) { + checkPuzzleSolution(); + } + + updateUI(); + saveState(); // Re-save with the latest state and correct checksum +} + + +// Simple Idle Game Actions +$.mineGoldBtn.addEventListener('click', () => { + gameState.gold += 1; + updateUI(); + saveState(); +}); + +$.buyMinerBtn.addEventListener('click', () => { + if (gameState.gold >= 10) { + gameState.gold -= 10; + gameState.miners += 1; + updateUI(); + saveState(); + } +}); + +$.loadSaveBtn.addEventListener('click', loadEditor); +$.applyChangesBtn.addEventListener('click', handleApplyChanges); +$.progressButton.addEventListener('click', checkPuzzleSolution); + + +// --- 7. Initialization --- +loadGameState(); + +// Start the idle game loop +gameLoopInterval = setInterval(idleTick, TICK_RATE); \ No newline at end of file diff --git a/games/local_storage/style.css b/games/local_storage/style.css new file mode 100644 index 00000000..3c4b3de0 --- /dev/null +++ b/games/local_storage/style.css @@ -0,0 +1,124 @@ +:root { + --primary-color: #50fa7b; /* Neon Green (Success/Hacker) */ + --secondary-color: #ffb86c; /* Orange (Warning) */ + --error-color: #ff5555; /* Red (Error) */ + --bg-color: #282a36; /* Dracula/Developer Background */ + --text-color: #f8f8f2; +} + +/* Base Styles */ +body { + font-family: 'Consolas', monospace; + background-color: var(--bg-color); + color: var(--text-color); + margin: 0; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 30px; +} + +.game-container { + display: flex; + gap: 30px; + max-width: 1200px; + margin: 0 auto; +} + +/* --- Game View (Idle Elements) --- */ +.game-view { + flex: 1; + padding: 20px; + border: 1px solid var(--primary-color); + border-radius: 8px; + background-color: #3d3f4b; +} + +.status-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; + margin-bottom: 20px; + padding: 10px; +} + +.stat-item { + background-color: #44475a; + padding: 10px; + border-radius: 4px; +} + +#corruption-panel { + margin-top: 30px; + padding: 15px; + border: 2px dashed var(--secondary-color); + background-color: #2a2c33; +} + +#corruption-message { + font-size: 1.1em; + color: var(--secondary-color); +} + + +/* --- Console View (Editor) --- */ +.console-view { + flex: 1; + padding: 20px; + background-color: #1e1e1e; + border-radius: 8px; + box-shadow: 0 0 10px rgba(80, 250, 123, 0.5); +} + +.console-note { + color: var(--error-color); + font-weight: bold; +} + +#save-editor { + width: 100%; + background-color: #000; + color: var(--primary-color); + border: none; + padding: 10px; + font-family: 'Consolas', monospace; + font-size: 14px; + resize: none; + box-sizing: border-box; +} + +.console-controls { + margin-top: 10px; + display: flex; + gap: 10px; +} + +button { + padding: 10px 15px; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: bold; + background-color: var(--primary-color); + color: var(--bg-color); + transition: opacity 0.2s; +} + +#apply-changes { + background-color: var(--secondary-color); +} + +#console-output { + margin-top: 15px; + padding: 10px; + min-height: 50px; + border: 1px dashed var(--primary-color); + white-space: pre-wrap; +} + +/* Dynamic Classes */ +.hacker-currency { + color: #bd93f9; /* Purple highlight for unique currency */ +} \ No newline at end of file diff --git a/games/logic-grid/index.html b/games/logic-grid/index.html new file mode 100644 index 00000000..23934034 --- /dev/null +++ b/games/logic-grid/index.html @@ -0,0 +1,39 @@ + + + + + + Logic Grid Game | Mini JS Games Hub + + + +
    +

    Logic Grid Game ๐Ÿงฉ

    +

    Fill the grid according to the clues. Correct cells turn green, wrong ones turn red.

    + +
    +

    Clues

    +
      +
    • Row 1 must contain exactly one "A".
    • +
    • Column 3 cannot have "B".
    • +
    • Exactly two "C"s must be placed diagonally.
    • +
    • No duplicate letters in a row or column.
    • +
    +
    + +
    + + +
    +
    + +
    + + +

    +
    +
    + + + + diff --git a/games/logic-grid/script.js b/games/logic-grid/script.js new file mode 100644 index 00000000..c53581c6 --- /dev/null +++ b/games/logic-grid/script.js @@ -0,0 +1,133 @@ +const letters = ["A", "B", "C", "D", "E"]; +const gridSize = 5; +let selectedLetter = "A"; +const table = document.getElementById("logic-grid"); +const message = document.getElementById("message"); + +// Create the grid dynamically +function createGrid() { + table.innerHTML = ""; + for (let r = 0; r < gridSize; r++) { + const row = document.createElement("tr"); + for (let c = 0; c < gridSize; c++) { + const cell = document.createElement("td"); + cell.dataset.row = r; + cell.dataset.col = c; + cell.addEventListener("click", () => selectCell(cell)); + row.appendChild(cell); + } + table.appendChild(row); + } +} + +// Handle cell selection and letter input +function selectCell(cell) { + const letter = prompt("Enter a letter (A-E):", "A"); + if (!letters.includes(letter)) return; + cell.textContent = letter; + cell.classList.remove("correct", "wrong"); +} + +// Validate grid based on clues +function checkSolution() { + let correct = true; + + // Reset cell colors + document.querySelectorAll("td").forEach(cell => { + cell.classList.remove("correct", "wrong"); + }); + + const grid = []; + for (let r = 0; r < gridSize; r++) { + grid[r] = []; + for (let c = 0; c < gridSize; c++) { + grid[r][c] = table.rows[r].cells[c].textContent; + } + } + + // Clue 1: Row 1 must contain exactly one "A" + const row1 = grid[0]; + const countA = row1.filter(l => l === "A").length; + if (countA !== 1) { + correct = false; + row1.forEach((_, c) => table.rows[0].cells[c].classList.add("wrong")); + } else { + row1.forEach((_, c) => { + if (table.rows[0].cells[c].textContent === "A") table.rows[0].cells[c].classList.add("correct"); + }); + } + + // Clue 2: Column 3 cannot have "B" + for (let r = 0; r < gridSize; r++) { + const cell = table.rows[r].cells[2]; + if (cell.textContent === "B") { + correct = false; + cell.classList.add("wrong"); + } else if (cell.textContent) cell.classList.add("correct"); + } + + // Clue 3: Two "C"s diagonally + let diagCount = 0; + for (let i = 0; i < gridSize; i++) { + if (grid[i][i] === "C") diagCount++; + } + if (diagCount !== 2) { + correct = false; + for (let i = 0; i < gridSize; i++) { + if (grid[i][i] === "C") table.rows[i].cells[i].classList.add("wrong"); + } + } else { + for (let i = 0; i < gridSize; i++) { + if (grid[i][i] === "C") table.rows[i].cells[i].classList.add("correct"); + } + } + + // Clue 4: No duplicates in any row or column + for (let r = 0; r < gridSize; r++) { + const rowSet = new Set(); + for (let c = 0; c < gridSize; c++) { + const val = grid[r][c]; + if (!val) continue; + if (rowSet.has(val)) { + table.rows[r].cells[c].classList.add("wrong"); + correct = false; + } else { + rowSet.add(val); + } + } + } + for (let c = 0; c < gridSize; c++) { + const colSet = new Set(); + for (let r = 0; r < gridSize; r++) { + const val = grid[r][c]; + if (!val) continue; + if (colSet.has(val)) { + table.rows[r].cells[c].classList.add("wrong"); + correct = false; + } else { + colSet.add(val); + } + } + } + + if (correct) { + message.textContent = "๐ŸŽ‰ Congratulations! All clues satisfied!"; + message.style.color = "green"; + } else { + message.textContent = "โš ๏ธ Some clues are not satisfied. Check highlighted cells."; + message.style.color = "red"; + } +} + +// Reset grid +function resetGrid() { + createGrid(); + message.textContent = ""; +} + +// Event listeners +document.getElementById("check-btn").addEventListener("click", checkSolution); +document.getElementById("reset-btn").addEventListener("click", resetGrid); + +// Initialize +createGrid(); diff --git a/games/logic-grid/style.css b/games/logic-grid/style.css new file mode 100644 index 00000000..6c0daa0f --- /dev/null +++ b/games/logic-grid/style.css @@ -0,0 +1,96 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(to right, #74ebd5, #acb6e5); + display: flex; + justify-content: center; + align-items: flex-start; + min-height: 100vh; + margin: 0; + padding: 20px; +} + +.container { + background-color: #ffffffcc; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 25px rgba(0,0,0,0.2); + max-width: 600px; + width: 100%; + text-align: center; +} + +h1 { + margin-bottom: 10px; + color: #333; +} + +.instructions { + font-size: 14px; + margin-bottom: 20px; +} + +.clues { + text-align: left; + margin-bottom: 20px; +} + +.clues ul { + padding-left: 20px; +} + +.grid-container { + overflow-x: auto; +} + +table { + border-collapse: collapse; + margin: 0 auto; +} + +td { + width: 50px; + height: 50px; + border: 2px solid #333; + text-align: center; + font-size: 24px; + cursor: pointer; + transition: 0.2s; +} + +td.correct { + background-color: #8ef58e; +} + +td.wrong { + background-color: #f58e8e; +} + +td.selected { + outline: 2px solid #333; +} + +.controls { + margin-top: 20px; +} + +button { + padding: 10px 20px; + margin: 5px; + font-size: 16px; + border-radius: 8px; + border: none; + cursor: pointer; + background-color: #6c63ff; + color: white; + transition: 0.2s; +} + +button:hover { + background-color: #574bff; +} + +#message { + margin-top: 15px; + font-weight: bold; + font-size: 16px; +} diff --git a/games/logic-path/index.html b/games/logic-path/index.html new file mode 100644 index 00000000..77b3b89c --- /dev/null +++ b/games/logic-path/index.html @@ -0,0 +1,28 @@ + + + + + +Logic Path + + + +
    +

    Logic Path

    +

    Guide the glowing orb to reach the goal avoiding obstacles!

    + +
    + + + + +
    + + + +

    Score: 0

    +
    + + + + diff --git a/games/logic-path/script.js b/games/logic-path/script.js new file mode 100644 index 00000000..c714f02c --- /dev/null +++ b/games/logic-path/script.js @@ -0,0 +1,134 @@ +const canvas = document.getElementById("game-canvas"); +const ctx = canvas.getContext("2d"); + +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const resumeBtn = document.getElementById("resume-btn"); +const restartBtn = document.getElementById("restart-btn"); +const scoreEl = document.getElementById("score"); + +let animationId; +let paused = false; +let score = 0; + +// Orb +const orb = { + x: 50, + y: canvas.height / 2, + radius: 20, + color: "cyan", + speed: 3 +}; + +// Obstacles +let obstacles = []; +function generateObstacles() { + obstacles = []; + for (let i = 0; i < 10; i++) { + obstacles.push({ + x: 200 + i * 60, + y: Math.random() * (canvas.height - 40), + width: 20, + height: 20, + color: "red" + }); + } +} + +// Sounds (online links) +const hitSound = new Audio("https://freesound.org/data/previews/466/466512_7037-lq.mp3"); +const scoreSound = new Audio("https://freesound.org/data/previews/320/320655_5260872-lq.mp3"); + +// Draw orb +function drawOrb() { + ctx.beginPath(); + ctx.arc(orb.x, orb.y, orb.radius, 0, Math.PI * 2); + ctx.fillStyle = orb.color; + ctx.shadowColor = "#0ff"; + ctx.shadowBlur = 20; + ctx.fill(); + ctx.closePath(); +} + +// Draw obstacles +function drawObstacles() { + obstacles.forEach(obs => { + ctx.fillStyle = obs.color; + ctx.fillRect(obs.x, obs.y, obs.width, obs.height); + }); +} + +// Move orb with arrow keys +document.addEventListener("keydown", e => { + if (!paused) { + if (e.key === "ArrowUp") orb.y -= orb.speed; + if (e.key === "ArrowDown") orb.y += orb.speed; + if (e.key === "ArrowLeft") orb.x -= orb.speed; + if (e.key === "ArrowRight") orb.x += orb.speed; + } +}); + +// Check collisions +function checkCollisions() { + for (let obs of obstacles) { + if ( + orb.x + orb.radius > obs.x && + orb.x - orb.radius < obs.x + obs.width && + orb.y + orb.radius > obs.y && + orb.y - orb.radius < obs.y + obs.height + ) { + hitSound.play(); + cancelAnimationFrame(animationId); + alert("Game Over!"); + return true; + } + } + if (orb.x > canvas.width - orb.radius) { + scoreSound.play(); + alert("You Win! Score: " + score); + cancelAnimationFrame(animationId); + return true; + } + return false; +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawOrb(); + drawObstacles(); + if (!checkCollisions()) { + animationId = requestAnimationFrame(draw); + } +} + +// Button functions +startBtn.addEventListener("click", () => { + paused = false; + score = 0; + orb.x = 50; + orb.y = canvas.height / 2; + generateObstacles(); + draw(); +}); + +pauseBtn.addEventListener("click", () => { + paused = true; + cancelAnimationFrame(animationId); +}); + +resumeBtn.addEventListener("click", () => { + if (paused) { + paused = false; + draw(); + } +}); + +restartBtn.addEventListener("click", () => { + paused = false; + score = 0; + orb.x = 50; + orb.y = canvas.height / 2; + generateObstacles(); + draw(); +}); diff --git a/games/logic-path/style.css b/games/logic-path/style.css new file mode 100644 index 00000000..05cc294a --- /dev/null +++ b/games/logic-path/style.css @@ -0,0 +1,38 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #0b0c10; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + text-align: center; +} + +.controls button { + padding: 10px 20px; + margin: 5px; + font-size: 16px; + border-radius: 8px; + border: none; + cursor: pointer; + background: linear-gradient(45deg, #6e0ad6, #ff0a91); + color: white; + box-shadow: 0 0 10px #ff0a91, 0 0 20px #6e0ad6; + transition: 0.3s; +} + +.controls button:hover { + box-shadow: 0 0 20px #ff0a91, 0 0 40px #6e0ad6; +} + +canvas { + background-color: #111; + display: block; + margin: 20px auto; + border: 2px solid #6e0ad6; + border-radius: 10px; +} diff --git a/games/magnet-maze/index.html b/games/magnet-maze/index.html new file mode 100644 index 00000000..d2c21d84 --- /dev/null +++ b/games/magnet-maze/index.html @@ -0,0 +1,88 @@ + + + + + + Magnet Maze #998 - Mini JS Games Hub + + + + +
    +
    +
    Level: 1
    +
    Moves: 0
    +
    Time: 0.0s
    +
    Best: --
    +
    + + + +
    +
    +

    Magnet Controls

    +
    +
    +
    + + +
    +
    + +
    +
    +

    Magnet Maze #998

    +

    Guide a metal ball through a magnetic maze by activating and deactivating magnets, avoiding traps in this physics puzzle!

    + +

    How to Play:

    +
      +
    • Goal: Guide the metal ball to the goal (green circle)
    • +
    • Magnets: Click magnet buttons to activate/deactivate magnetic fields
    • +
    • Physics: The ball responds to magnetic forces and gravity
    • +
    • Traps: Avoid red hazard zones and spikes
    • +
    • Moves: Minimize moves and time for higher scores
    • +
    • Reset: Use reset button if you get stuck
    • +
    + +

    Magnet Types:

    +
      +
    • Attract: Pulls the ball toward the magnet
    • +
    • Repel: Pushes the ball away from the magnet
    • +
    • Rotate: Creates circular magnetic fields
    • +
    • Pulse: Alternating attract/repel forces
    • +
    + + +
    +
    + + + + + + +
    + + + + \ No newline at end of file diff --git a/games/magnet-maze/script.js b/games/magnet-maze/script.js new file mode 100644 index 00000000..78a5372e --- /dev/null +++ b/games/magnet-maze/script.js @@ -0,0 +1,571 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const startButton = document.getElementById('startButton'); +const resetButton = document.getElementById('resetButton'); +const hintButton = document.getElementById('hintButton'); +const nextLevelButton = document.getElementById('nextLevelButton'); +const tryAgainButton = document.getElementById('tryAgainButton'); +const closeHintButton = document.getElementById('closeHintButton'); +const instructionsOverlay = document.getElementById('instructions-overlay'); +const levelCompleteOverlay = document.getElementById('level-complete-overlay'); +const gameOverOverlay = document.getElementById('game-over-overlay'); +const hintOverlay = document.getElementById('hint-overlay'); +const levelElement = document.getElementById('level'); +const movesElement = document.getElementById('moves'); +const timeElement = document.getElementById('time'); +const bestTimeElement = document.getElementById('best-time'); +const magnetButtonsContainer = document.getElementById('magnet-buttons'); + +canvas.width = 750; +canvas.height = 600; + +let gameRunning = false; +let levelComplete = false; +let gameOver = false; +let ball; +let magnets = []; +let traps = []; +let goal; +let platforms = []; +let currentLevel = 1; +let moves = 0; +let startTime = 0; +let bestTimes = {}; +let gameTimer; + +// Ball class +class Ball { + constructor(x, y) { + this.x = x; + this.y = y; + this.radius = 12; + this.vx = 0; + this.vy = 0; + this.ax = 0; + this.ay = 0; + this.damping = 0.98; + this.trail = []; + this.maxTrailLength = 15; + } + + update() { + // Reset acceleration + this.ax = 0; + this.ay = 0.3; // gravity + + // Apply magnetic forces + magnets.forEach(magnet => { + if (magnet.active) { + const dx = magnet.x - this.x; + const dy = magnet.y - this.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance > 0) { + const force = magnet.getForce(distance); + const forceX = (dx / distance) * force; + const forceY = (dy / distance) * force; + + this.ax += forceX; + this.ay += forceY; + } + } + }); + + // Update velocity + this.vx += this.ax; + this.vy += this.ay; + + // Apply damping + this.vx *= this.damping; + this.vy *= this.damping; + + // Update position + this.x += this.vx; + this.y += this.vy; + + // Add to trail + this.trail.push({ x: this.x, y: this.y }); + if (this.trail.length > this.maxTrailLength) { + this.trail.shift(); + } + + // Platform collisions + platforms.forEach(platform => { + if (this.x + this.radius > platform.x && + this.x - this.radius < platform.x + platform.width && + this.y + this.radius > platform.y && + this.y - this.radius < platform.y + platform.height) { + + // Landing on top + if (this.vy > 0 && this.y - this.radius < platform.y) { + this.y = platform.y - this.radius; + this.vy = 0; + } + // Hitting from below + else if (this.vy < 0 && this.y + this.radius > platform.y + platform.height) { + this.y = platform.y + platform.height + this.radius; + this.vy = 0; + } + // Side collisions + else if (this.vx > 0 && this.x - this.radius < platform.x) { + this.x = platform.x - this.radius; + this.vx = 0; + } else if (this.vx < 0 && this.x + this.radius > platform.x + platform.width) { + this.x = platform.x + platform.width + this.radius; + this.vx = 0; + } + } + }); + + // Boundary checks + if (this.x - this.radius < 0) { + this.x = this.radius; + this.vx *= -0.8; + } + if (this.x + this.radius > canvas.width) { + this.x = canvas.width - this.radius; + this.vx *= -0.8; + } + if (this.y - this.radius < 0) { + this.y = this.radius; + this.vy *= -0.8; + } + if (this.y + this.radius > canvas.height) { + gameOver = true; + showGameOver(); + } + + // Check trap collisions + traps.forEach(trap => { + if (trap.containsPoint(this.x, this.y)) { + gameOver = true; + showGameOver(); + } + }); + + // Check goal collision + if (goal && goal.containsPoint(this.x, this.y)) { + levelComplete = true; + showLevelComplete(); + } + } + + draw() { + // Draw trail + ctx.strokeStyle = 'rgba(0, 255, 255, 0.3)'; + ctx.lineWidth = 2; + ctx.beginPath(); + for (let i = 1; i < this.trail.length; i++) { + ctx.moveTo(this.trail[i-1].x, this.trail[i-1].y); + ctx.lineTo(this.trail[i].x, this.trail[i].y); + } + ctx.stroke(); + + // Draw ball + ctx.fillStyle = '#c0c0c0'; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fill(); + + // Ball highlight + ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; + ctx.beginPath(); + ctx.arc(this.x - this.radius * 0.3, this.y - this.radius * 0.3, this.radius * 0.4, 0, Math.PI * 2); + ctx.fill(); + + // Magnetic field indicator + ctx.strokeStyle = 'rgba(0, 255, 255, 0.5)'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius + 5, 0, Math.PI * 2); + ctx.stroke(); + } + + reset(x, y) { + this.x = x; + this.y = y; + this.vx = 0; + this.vy = 0; + this.trail = []; + } +} + +// Magnet classes +class Magnet { + constructor(x, y, type, strength = 1) { + this.x = x; + this.y = y; + this.type = type; + this.strength = strength; + this.active = false; + this.radius = 40; + this.pulsePhase = 0; + } + + getForce(distance) { + let force = 0; + + switch (this.type) { + case 'attract': + force = this.strength * 200 / (distance * distance + 50); + break; + case 'repel': + force = -this.strength * 200 / (distance * distance + 50); + break; + case 'rotate': + // Circular force + force = this.strength * 50 / (distance + 10); + break; + case 'pulse': + this.pulsePhase += 0.1; + const pulse = Math.sin(this.pulsePhase) > 0 ? 1 : -1; + force = pulse * this.strength * 150 / (distance * distance + 50); + break; + } + + return force; + } + + draw() { + if (!this.active) { + ctx.strokeStyle = 'rgba(128, 128, 128, 0.5)'; + ctx.lineWidth = 2; + } else { + switch (this.type) { + case 'attract': + ctx.strokeStyle = '#ff4444'; + ctx.shadowColor = '#ff4444'; + break; + case 'repel': + ctx.strokeStyle = '#44ff44'; + ctx.shadowColor = '#44ff44'; + break; + case 'rotate': + ctx.strokeStyle = '#ffff44'; + ctx.shadowColor = '#ffff44'; + break; + case 'pulse': + ctx.strokeStyle = '#ff44ff'; + ctx.shadowColor = '#ff44ff'; + break; + } + ctx.shadowBlur = 15; + ctx.lineWidth = 3; + } + + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.stroke(); + + // Draw field lines + if (this.active) { + ctx.strokeStyle = ctx.strokeStyle; + ctx.lineWidth = 1; + ctx.shadowBlur = 5; + + for (let i = 0; i < 8; i++) { + const angle = (i / 8) * Math.PI * 2; + const startRadius = this.radius - 5; + const endRadius = this.radius + 15; + + ctx.beginPath(); + ctx.moveTo( + this.x + Math.cos(angle) * startRadius, + this.y + Math.sin(angle) * startRadius + ); + ctx.lineTo( + this.x + Math.cos(angle) * endRadius, + this.y + Math.sin(angle) * endRadius + ); + ctx.stroke(); + } + } + + ctx.shadowBlur = 0; + } +} + +// Trap class +class Trap { + constructor(x, y, width, height, type = 'spike') { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.type = type; + } + + containsPoint(px, py) { + return px >= this.x && px <= this.x + this.width && + py >= this.y && py <= this.y + this.height; + } + + draw() { + ctx.fillStyle = '#ff0000'; + ctx.fillRect(this.x, this.y, this.width, this.height); + + // Add hazard pattern + ctx.fillStyle = '#ffffff'; + for (let i = 0; i < this.width; i += 10) { + for (let j = 0; j < this.height; j += 10) { + if ((i + j) % 20 === 0) { + ctx.fillRect(this.x + i, this.y + j, 5, 5); + } + } + } + } +} + +// Goal class +class Goal { + constructor(x, y, radius = 25) { + this.x = x; + this.y = y; + this.radius = radius; + this.pulse = 0; + } + + containsPoint(px, py) { + const dx = px - this.x; + const dy = py - this.y; + return Math.sqrt(dx * dx + dy * dy) <= this.radius; + } + + draw() { + this.pulse += 0.1; + + ctx.save(); + ctx.shadowColor = '#00ff00'; + ctx.shadowBlur = 20 + Math.sin(this.pulse) * 10; + + ctx.strokeStyle = '#00ff00'; + ctx.lineWidth = 4; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.stroke(); + + ctx.fillStyle = 'rgba(0, 255, 0, 0.2)'; + ctx.fill(); + + // Inner circle + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius * 0.6, 0, Math.PI * 2); + ctx.stroke(); + + ctx.restore(); + } +} + +// Level definitions +const levels = [ + // Level 1: Simple attract magnet + { + ballStart: { x: 100, y: 100 }, + magnets: [ + { x: 300, y: 200, type: 'attract' } + ], + traps: [ + { x: 200, y: 250, width: 100, height: 20 } + ], + goal: { x: 600, y: 500 }, + platforms: [ + { x: 0, y: 300, width: 200, height: 20 }, + { x: 400, y: 400, width: 200, height: 20 } + ], + hint: "Activate the red attract magnet to pull the ball toward the goal!" + }, + // Level 2: Repel magnet + { + ballStart: { x: 100, y: 300 }, + magnets: [ + { x: 400, y: 300, type: 'repel' } + ], + traps: [ + { x: 200, y: 350, width: 150, height: 20 } + ], + goal: { x: 650, y: 200 }, + platforms: [ + { x: 0, y: 400, width: 300, height: 20 }, + { x: 500, y: 250, width: 200, height: 20 } + ], + hint: "Use the green repel magnet to push the ball around obstacles!" + }, + // Level 3: Multiple magnets + { + ballStart: { x: 100, y: 200 }, + magnets: [ + { x: 250, y: 150, type: 'attract' }, + { x: 450, y: 350, type: 'repel' } + ], + traps: [ + { x: 300, y: 250, width: 100, height: 20 }, + { x: 500, y: 200, width: 100, height: 20 } + ], + goal: { x: 650, y: 450 }, + platforms: [ + { x: 0, y: 300, width: 200, height: 20 }, + { x: 400, y: 400, width: 200, height: 20 } + ], + hint: "Combine attract and repel magnets strategically!" + } +]; + +// Initialize level +function initLevel() { + const level = levels[currentLevel - 1]; + + ball = new Ball(level.ballStart.x, level.ballStart.y); + + magnets = level.magnets.map(m => new Magnet(m.x, m.y, m.type)); + traps = level.traps.map(t => new Trap(t.x, t.y, t.width, t.height)); + goal = new Goal(level.goal.x, level.goal.y); + platforms = level.platforms; + + moves = 0; + startTime = Date.now(); + + createMagnetButtons(); + updateUI(); +} + +// Create magnet control buttons +function createMagnetButtons() { + magnetButtonsContainer.innerHTML = ''; + + magnets.forEach((magnet, index) => { + const button = document.createElement('div'); + button.className = `magnet-button ${magnet.type}`; + button.textContent = `${magnet.type.charAt(0).toUpperCase() + magnet.type.slice(1)} ${index + 1}`; + button.addEventListener('click', () => { + magnet.active = !magnet.active; + button.classList.toggle('active'); + moves++; + updateUI(); + }); + magnetButtonsContainer.appendChild(button); + }); +} + +// Update UI +function updateUI() { + levelElement.textContent = `Level: ${currentLevel}`; + movesElement.textContent = `Moves: ${moves}`; + timeElement.textContent = `Time: ${(Date.now() - startTime) / 1000.0.toFixed(1)}s`; + + const bestTime = bestTimes[currentLevel] || '--'; + bestTimeElement.textContent = `Best: ${bestTime}`; +} + +// Show level complete +function showLevelComplete() { + gameRunning = false; + clearInterval(gameTimer); + + const time = (Date.now() - startTime) / 1000; + const score = Math.max(0, Math.floor(1000 - moves * 10 - time * 5)); + + // Check for new best time + if (!bestTimes[currentLevel] || time < bestTimes[currentLevel]) { + bestTimes[currentLevel] = time; + document.getElementById('new-record').style.display = 'block'; + } else { + document.getElementById('new-record').style.display = 'none'; + } + + document.getElementById('level-stats').textContent = `Moves: ${moves} | Time: ${time.toFixed(1)}s`; + document.getElementById('level-score').textContent = `Score: ${score}`; + + levelCompleteOverlay.style.display = 'flex'; +} + +// Show game over +function showGameOver() { + gameRunning = false; + clearInterval(gameTimer); + gameOverOverlay.style.display = 'flex'; +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw platforms + ctx.fillStyle = '#666666'; + platforms.forEach(platform => { + ctx.fillRect(platform.x, platform.y, platform.width, platform.height); + }); + + // Draw traps + traps.forEach(trap => { + trap.draw(); + }); + + // Draw magnets + magnets.forEach(magnet => { + magnet.draw(); + }); + + // Draw goal + if (goal) goal.draw(); + + // Draw ball + if (ball) ball.draw(); +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + ball.update(); + draw(); + + requestAnimationFrame(gameLoop); +} + +// Event listeners +startButton.addEventListener('click', () => { + instructionsOverlay.style.display = 'none'; + gameRunning = true; + initLevel(); + gameLoop(); +}); + +resetButton.addEventListener('click', () => { + if (ball) { + ball.reset(levels[currentLevel - 1].ballStart.x, levels[currentLevel - 1].ballStart.y); + moves++; + updateUI(); + } +}); + +hintButton.addEventListener('click', () => { + const level = levels[currentLevel - 1]; + document.getElementById('hint-text').textContent = level.hint; + hintOverlay.style.display = 'flex'; +}); + +closeHintButton.addEventListener('click', () => { + hintOverlay.style.display = 'none'; +}); + +nextLevelButton.addEventListener('click', () => { + levelComplete = false; + levelCompleteOverlay.style.display = 'none'; + currentLevel = Math.min(currentLevel + 1, levels.length); + initLevel(); + gameRunning = true; + gameLoop(); +}); + +tryAgainButton.addEventListener('click', () => { + gameOver = false; + gameOverOverlay.style.display = 'none'; + initLevel(); + gameRunning = true; + gameLoop(); +}); + +// Initialize +updateUI(); \ No newline at end of file diff --git a/games/magnet-maze/style.css b/games/magnet-maze/style.css new file mode 100644 index 00000000..ab50914a --- /dev/null +++ b/games/magnet-maze/style.css @@ -0,0 +1,339 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Rajdhani', sans-serif; + background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #16213e 100%); + color: #00ffff; + overflow: hidden; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +} + +#game-container { + position: relative; + width: 1000px; + height: 700px; + border-radius: 15px; + overflow: hidden; + box-shadow: 0 0 30px rgba(0, 255, 255, 0.3); + border: 2px solid rgba(0, 255, 255, 0.5); + display: flex; +} + +#ui-panel { + position: absolute; + top: 15px; + left: 15px; + z-index: 10; + background: rgba(0, 0, 0, 0.8); + padding: 15px; + border-radius: 10px; + border: 1px solid rgba(0, 255, 255, 0.3); + font-family: 'Orbitron', monospace; + font-size: 14px; + min-width: 180px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); +} + +#ui-panel div { + margin-bottom: 8px; + color: #00ffff; + text-shadow: 0 0 5px #00ffff; +} + +#controls-panel { + width: 250px; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border-left: 2px solid rgba(0, 255, 255, 0.3); + padding: 20px; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +#magnet-controls h3 { + color: #00ffff; + font-family: 'Orbitron', monospace; + font-size: 16px; + margin-bottom: 15px; + text-align: center; + text-shadow: 0 0 10px #00ffff; +} + +#magnet-buttons { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; + margin-bottom: 20px; +} + +.magnet-button { + background: linear-gradient(135deg, #333, #555); + border: 2px solid #666; + color: #fff; + padding: 10px 15px; + border-radius: 8px; + cursor: pointer; + font-size: 12px; + font-weight: 600; + text-align: center; + transition: all 0.3s ease; + font-family: 'Rajdhani', sans-serif; + min-height: 40px; + display: flex; + align-items: center; + justify-content: center; +} + +.magnet-button:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 255, 255, 0.3); +} + +.magnet-button.active { + background: linear-gradient(135deg, #00ffff, #0088ff); + border-color: #00ffff; + box-shadow: 0 0 20px rgba(0, 255, 255, 0.5); + color: #000; +} + +.magnet-button.attract { + background: linear-gradient(135deg, #ff4444, #cc0000); +} + +.magnet-button.attract.active { + background: linear-gradient(135deg, #ff6666, #ff0000); + box-shadow: 0 0 20px rgba(255, 68, 68, 0.5); +} + +.magnet-button.repel { + background: linear-gradient(135deg, #44ff44, #00cc00); +} + +.magnet-button.repel.active { + background: linear-gradient(135deg, #66ff66, #00ff00); + box-shadow: 0 0 20px rgba(68, 255, 68, 0.5); +} + +.magnet-button.rotate { + background: linear-gradient(135deg, #ffff44, #cccc00); +} + +.magnet-button.rotate.active { + background: linear-gradient(135deg, #ffff66, #ffff00); + box-shadow: 0 0 20px rgba(255, 255, 68, 0.5); +} + +#game-controls { + display: flex; + flex-direction: column; + gap: 10px; +} + +#game-controls button { + background: linear-gradient(135deg, #666, #888); + border: 2px solid #999; + color: #fff; + padding: 12px; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + transition: all 0.3s ease; + font-family: 'Rajdhani', sans-serif; +} + +#game-controls button:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 255, 255, 0.3); + background: linear-gradient(135deg, #777, #999); +} + +#gameCanvas { + flex: 1; + background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #16213e 100%); + border-radius: 15px; +} + +#instructions-overlay, +#level-complete-overlay, +#game-over-overlay, +#hint-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.9); + display: flex; + justify-content: center; + align-items: center; + z-index: 100; + backdrop-filter: blur(5px); +} + +.instructions-content, +.level-complete-content, +.game-over-content, +.hint-content { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + padding: 30px; + border-radius: 15px; + border: 2px solid rgba(0, 255, 255, 0.5); + box-shadow: 0 0 30px rgba(0, 255, 255, 0.3); + max-width: 600px; + text-align: center; + font-family: 'Rajdhani', sans-serif; + color: #ffffff; +} + +.instructions-content h1 { + font-family: 'Orbitron', monospace; + font-size: 2.5em; + margin-bottom: 20px; + color: #00ffff; + text-shadow: 0 0 15px #00ffff; +} + +.instructions-content h3 { + color: #ffaa00; + margin: 20px 0 10px 0; + font-size: 1.2em; +} + +.instructions-content ul { + text-align: left; + margin: 15px 0; + padding-left: 20px; +} + +.instructions-content li { + margin-bottom: 8px; + line-height: 1.4; +} + +.instructions-content strong { + color: #00ff88; +} + +button { + background: linear-gradient(135deg, #00ffff, #0088ff); + border: none; + color: white; + padding: 12px 24px; + font-size: 16px; + font-weight: 600; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; + margin: 10px; + font-family: 'Rajdhani', sans-serif; + text-transform: uppercase; + letter-spacing: 1px; + box-shadow: 0 4px 15px rgba(0, 255, 255, 0.3); +} + +button:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 255, 255, 0.5); + background: linear-gradient(135deg, #33ffff, #3399ff); +} + +.level-complete-content h2, +.game-over-content h2, +.hint-content h3 { + font-family: 'Orbitron', monospace; + font-size: 2em; + color: #ff4444; + margin-bottom: 20px; + text-shadow: 0 0 10px #ff4444; +} + +.level-complete-content h2 { + color: #44ff44; + text-shadow: 0 0 10px #44ff44; +} + +#level-stats, +#level-score, +#new-record { + font-size: 1.2em; + margin: 10px 0; + color: #ffff88; +} + +#new-record { + color: #ffaa00; + font-weight: 700; +} + +.hint-content { + max-width: 400px; +} + +#hint-text { + font-size: 1.1em; + margin: 20px 0; + color: #ffffff; +} + +/* Glow effects */ +.glow-blue { + box-shadow: 0 0 20px rgba(0, 255, 255, 0.5); +} + +.glow-red { + box-shadow: 0 0 20px rgba(255, 0, 0, 0.5); +} + +.glow-green { + box-shadow: 0 0 20px rgba(0, 255, 0, 0.5); +} + +.glow-yellow { + box-shadow: 0 0 20px rgba(255, 255, 0, 0.5); +} + +/* Responsive design */ +@media (max-width: 1100px) { + #game-container { + width: 95vw; + height: 95vh; + flex-direction: column; + } + + #controls-panel { + width: 100%; + height: 200px; + border-left: none; + border-top: 2px solid rgba(0, 255, 255, 0.3); + flex-direction: row; + padding: 15px; + } + + #magnet-controls { + flex: 1; + margin-right: 20px; + } + + #game-controls { + flex: 1; + flex-direction: row; + justify-content: center; + } + + .instructions-content { + padding: 20px; + max-width: 90vw; + } + + .instructions-content h1 { + font-size: 2.2em; + } +} \ No newline at end of file diff --git a/games/magnetic-fields/index.html b/games/magnetic-fields/index.html new file mode 100644 index 00000000..b445a7a9 --- /dev/null +++ b/games/magnetic-fields/index.html @@ -0,0 +1,28 @@ + + + + + + Magnetic Fields ๐Ÿงฒ + + + +
    +

    ๐Ÿงฒ Magnetic Fields

    + + +
    + + + +

    Use keys: N = North Pole, S = South Pole

    +
    + + + + +
    + + + + diff --git a/games/magnetic-fields/script.js b/games/magnetic-fields/script.js new file mode 100644 index 00000000..a9ab88d9 --- /dev/null +++ b/games/magnetic-fields/script.js @@ -0,0 +1,175 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +let gameRunning = false; +let paused = false; + +const ball = { + x: 100, + y: 250, + radius: 10, + color: "cyan", + vx: 0, + vy: 0, + mass: 1 +}; + +const magnets = [ + { x: 200, y: 100, type: "N" }, + { x: 600, y: 400, type: "S" } +]; + +const obstacles = [ + { x: 300, y: 200, w: 200, h: 20 }, + { x: 400, y: 350, w: 150, h: 20 } +]; + +const goal = { x: 720, y: 240, w: 40, h: 40 }; + +const bgMusic = document.getElementById("bgMusic"); +const goalSound = document.getElementById("goalSound"); +const hitSound = document.getElementById("hitSound"); + +let northActive = false; +let southActive = false; + +// Controls +document.getElementById("startBtn").onclick = () => { + gameRunning = true; + paused = false; + bgMusic.play(); + loop(); +}; + +document.getElementById("pauseBtn").onclick = () => { + paused = !paused; +}; + +document.getElementById("restartBtn").onclick = () => { + resetGame(); +}; + +document.addEventListener("keydown", (e) => { + if (e.key.toLowerCase() === "n") northActive = true; + if (e.key.toLowerCase() === "s") southActive = true; +}); + +document.addEventListener("keyup", (e) => { + if (e.key.toLowerCase() === "n") northActive = false; + if (e.key.toLowerCase() === "s") southActive = false; +}); + +function resetGame() { + ball.x = 100; + ball.y = 250; + ball.vx = 0; + ball.vy = 0; + gameRunning = false; + paused = false; + bgMusic.pause(); + bgMusic.currentTime = 0; +} + +function applyMagneticForce() { + magnets.forEach(mag => { + const dx = mag.x - ball.x; + const dy = mag.y - ball.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist < 250) { + const force = (mag.type === "N" && northActive) || (mag.type === "S" && southActive) + ? 1000 / (dist * dist) + : 0; + ball.vx += (dx / dist) * force * 0.1; + ball.vy += (dy / dist) * force * 0.1; + } + }); +} + +function detectCollisions() { + for (let obs of obstacles) { + if ( + ball.x > obs.x && + ball.x < obs.x + obs.w && + ball.y > obs.y && + ball.y < obs.y + obs.h + ) { + hitSound.play(); + resetGame(); + } + } + + if ( + ball.x > goal.x && + ball.x < goal.x + goal.w && + ball.y > goal.y && + ball.y < goal.y + goal.h + ) { + goalSound.play(); + alert("๐ŸŽฏ You reached the goal!"); + resetGame(); + } +} + +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw magnets + magnets.forEach(m => { + ctx.beginPath(); + const color = m.type === "N" ? "red" : "blue"; + ctx.shadowBlur = 20; + ctx.shadowColor = color; + ctx.fillStyle = color; + ctx.arc(m.x, m.y, 20, 0, Math.PI * 2); + ctx.fill(); + ctx.shadowBlur = 0; + }); + + // Draw obstacles + ctx.fillStyle = "#444"; + obstacles.forEach(o => { + ctx.fillRect(o.x, o.y, o.w, o.h); + }); + + // Draw goal + ctx.shadowBlur = 15; + ctx.shadowColor = "lime"; + ctx.fillStyle = "lime"; + ctx.fillRect(goal.x, goal.y, goal.w, goal.h); + ctx.shadowBlur = 0; + + // Draw ball + ctx.beginPath(); + ctx.shadowBlur = 20; + ctx.shadowColor = "cyan"; + ctx.fillStyle = ball.color; + ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2); + ctx.fill(); + ctx.shadowBlur = 0; +} + +function update() { + applyMagneticForce(); + + ball.x += ball.vx; + ball.y += ball.vy; + + // Add drag + ball.vx *= 0.97; + ball.vy *= 0.97; + + // Wall bounce + if (ball.x < 0 || ball.x > canvas.width) ball.vx *= -0.8; + if (ball.y < 0 || ball.y > canvas.height) ball.vy *= -0.8; + + detectCollisions(); +} + +function loop() { + if (!gameRunning) return; + if (!paused) { + update(); + draw(); + } + requestAnimationFrame(loop); +} diff --git a/games/magnetic-fields/style.css b/games/magnetic-fields/style.css new file mode 100644 index 00000000..c66d3086 --- /dev/null +++ b/games/magnetic-fields/style.css @@ -0,0 +1,48 @@ +body { + margin: 0; + font-family: 'Poppins', sans-serif; + background: radial-gradient(circle at center, #0a0a0a 0%, #1c1c1c 100%); + color: white; + display: flex; + flex-direction: column; + align-items: center; + height: 100vh; + justify-content: center; + overflow: hidden; +} + +h1 { + text-shadow: 0 0 20px cyan; +} + +canvas { + background: linear-gradient(145deg, #141414, #222); + border: 2px solid cyan; + border-radius: 12px; + box-shadow: 0 0 30px cyan; +} + +.controls { + margin-top: 15px; + display: flex; + gap: 10px; + flex-wrap: wrap; + justify-content: center; +} + +button { + background: black; + border: 2px solid cyan; + color: cyan; + padding: 10px 20px; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + transition: 0.3s; +} + +button:hover { + background: cyan; + color: black; + box-shadow: 0 0 15px cyan; +} diff --git a/games/maiolike-block-puzzle/index.html b/games/maiolike-block-puzzle/index.html new file mode 100644 index 00000000..1aa5c805 --- /dev/null +++ b/games/maiolike-block-puzzle/index.html @@ -0,0 +1,36 @@ + + + + + + Maiolike Block Puzzle + + + +
    +
    +

    Maiolike Block Puzzle

    +
    + Score: 0 + +
    +
    + +
    +
    +
    +

    Next Blocks

    +
    +
    +
    + + +
    + + + + diff --git a/games/maiolike-block-puzzle/script.js b/games/maiolike-block-puzzle/script.js new file mode 100644 index 00000000..67bf5d21 --- /dev/null +++ b/games/maiolike-block-puzzle/script.js @@ -0,0 +1,173 @@ +const grid = document.getElementById("grid"); +const nextContainer = document.getElementById("next-blocks-container"); +const scoreEl = document.getElementById("score"); +const gameOverEl = document.getElementById("game-over"); +const finalScoreEl = document.getElementById("final-score"); +const restartBtn = document.getElementById("restart"); +const restartOverBtn = document.getElementById("restart-over"); + +const GRID_SIZE = 10; +let gridArray = []; +let score = 0; +let nextBlocks = []; + +const BLOCKS = [ + [[1]], + [[1,1]], + [[1],[1]], + [[1,1,1]], + [[1],[1],[1]], + [[1,1],[1,1]], + [[1,1,1],[0,1,0]] +]; + +// Initialize grid +function initGrid() { + grid.innerHTML = ""; + gridArray = Array.from({length: GRID_SIZE}, () => Array(GRID_SIZE).fill(0)); + for (let r=0; r { + const r = parseInt(cell.dataset.row); + const c = parseInt(cell.dataset.col); + cell.classList.toggle("filled", gridArray[r][c] === 1); + }); +} + +// Generate random block +function generateBlocks() { + nextBlocks = []; + for(let i=0;i<3;i++){ + const rand = BLOCKS[Math.floor(Math.random()*BLOCKS.length)]; + nextBlocks.push(rand); + } + drawNextBlocks(); +} + +// Draw preview blocks +function drawNextBlocks() { + nextContainer.innerHTML = ""; + nextBlocks.forEach(block => { + const blockDiv = document.createElement("div"); + blockDiv.classList.add("block"); + block.forEach(row => { + row.forEach(cell => { + const cellDiv = document.createElement("div"); + cellDiv.classList.add("cell"); + if(cell) cellDiv.classList.add("filled"); + blockDiv.appendChild(cellDiv); + }); + }); + nextContainer.appendChild(blockDiv); + }); +} + +// Check if block can be placed at row,col +function canPlace(block,row,col){ + for(let r=0;r=GRID_SIZE || newC>=GRID_SIZE || gridArray[newR][newC]===1) return false; + } + } + } + return true; +} + +// Place block +function placeBlock(block,row,col){ + for(let r=0;rcell===1)){ + gridArray[r].fill(0); + score += GRID_SIZE; + } + } + for(let c=0;c{ + for(let r=0;r{ + const blockIndex = Array.from(nextContainer.children).indexOf(e.target.closest(".block")); + if(blockIndex===-1) return; + const block = nextBlocks[blockIndex]; + // Find first placeable spot (simple auto-place for demo) + outer: + for(let r=0;r + + + + + Rogue Map Viewer + + + + +
    +

    ๐Ÿ—บ๏ธ Rogue Map Viewer

    + +
    + Map Size: 25x25 | Tiles Generated: 0 +
    + +
    +
    + +
    + +
    +
    + + + + \ No newline at end of file diff --git a/games/map_viewer/script.js b/games/map_viewer/script.js new file mode 100644 index 00000000..c830d776 --- /dev/null +++ b/games/map_viewer/script.js @@ -0,0 +1,142 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. GAME CONSTANTS & ELEMENTS --- + const GRID_SIZE = 25; // Map dimensions (25x25) + const MAX_TILES_TO_CARVE = 600; // How many floor tiles to generate + const gridContainer = document.getElementById('grid-container'); + const newMapButton = document.getElementById('new-map-button'); + const tilesCountDisplay = document.getElementById('tiles-count'); + const mapSizeDisplay = document.getElementById('map-size-display'); + + // Tile types (used in the map array) + const WALL = 0; + const FLOOR = 1; + + let map = []; // The 2D array representing the dungeon + let tilesCarved = 0; + + // --- 2. UTILITY FUNCTIONS --- + + /** + * Finds a cell's corresponding element in the DOM. + */ + function getCellElement(row, col) { + if (row >= 0 && row < GRID_SIZE && col >= 0 && col < GRID_SIZE) { + return gridContainer.children[row * GRID_SIZE + col]; + } + return null; + } + + /** + * Determines the CSS class based on the tile type. + */ + function getTileClass(tileValue) { + if (tileValue === FLOOR) return 'tile-floor'; + if (tileValue === WALL) return 'tile-wall'; + // Fallback for cells outside the generated area, though generally unused with this method + return 'tile-empty'; + } + + /** + * Generates a random integer between min (inclusive) and max (exclusive). + */ + function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min)) + min; + } + + // --- 3. PROCEDURAL GENERATION (RANDOM WALK) --- + + /** + * Initializes the map array and starts the generation process. + */ + function generateNewMap() { + // 1. Reset state + tilesCarved = 0; + // Fill the entire map with WALLs (0) + map = Array.from({ length: GRID_SIZE }, () => Array(GRID_SIZE).fill(WALL)); + + // 2. Start the Random Walk from a random center point + let currentRow = getRandomInt(5, GRID_SIZE - 5); + let currentCol = getRandomInt(5, GRID_SIZE - 5); + + // 3. Begin the Carving Process + while (tilesCarved < MAX_TILES_TO_CARVE) { + // Ensure walker stays within bounds + if (currentRow >= 1 && currentRow < GRID_SIZE - 1 && + currentCol >= 1 && currentCol < GRID_SIZE - 1) { + + // If this is a new floor tile, count it + if (map[currentRow][currentCol] === WALL) { + map[currentRow][currentCol] = FLOOR; + tilesCarved++; + } + + // Randomly choose the next direction (0=N, 1=E, 2=S, 3=W) + const direction = getRandomInt(0, 4); + + switch (direction) { + case 0: currentRow--; break; // North + case 1: currentCol++; break; // East + case 2: currentRow++; break; // South + case 3: currentCol--; break; // West + } + + } else { + // If out of bounds, jump back to a random central point + currentRow = getRandomInt(5, GRID_SIZE - 5); + currentCol = getRandomInt(5, GRID_SIZE - 5); + } + } + + // 4. Render the final map to the DOM + renderMap(); + + // 5. Update Status + tilesCountDisplay.textContent = tilesCarved; + } + + // --- 4. DOM RENDERING --- + + /** + * Sets the CSS variables and builds the grid structure once. + */ + function setupGridContainer() { + // Update CSS variables for grid sizing + document.documentElement.style.setProperty('--grid-rows', GRID_SIZE); + document.documentElement.style.setProperty('--grid-cols', GRID_SIZE); + mapSizeDisplay.textContent = `${GRID_SIZE}x${GRID_SIZE}`; + + // Create all cell elements (GRID_SIZE * GRID_SIZE) + for (let i = 0; i < GRID_SIZE * GRID_SIZE; i++) { + const cell = document.createElement('div'); + cell.classList.add('grid-cell'); + gridContainer.appendChild(cell); + } + } + + /** + * Updates the class (color) of every cell in the DOM based on the map array. + */ + function renderMap() { + for (let r = 0; r < GRID_SIZE; r++) { + for (let c = 0; c < GRID_SIZE; c++) { + const index = r * GRID_SIZE + c; + const cellElement = gridContainer.children[index]; + + // Clear old classes + cellElement.className = 'grid-cell'; + + // Add new class based on map value + const tileValue = map[r][c]; + cellElement.classList.add(getTileClass(tileValue)); + } + } + } + + // --- 5. EVENT LISTENERS --- + + newMapButton.addEventListener('click', generateNewMap); + + // Initial setup and map generation on load + setupGridContainer(); + generateNewMap(); +}); \ No newline at end of file diff --git a/games/map_viewer/style.css b/games/map_viewer/style.css new file mode 100644 index 00000000..6c1ad258 --- /dev/null +++ b/games/map_viewer/style.css @@ -0,0 +1,86 @@ +:root { + --grid-rows: 25; + --grid-cols: 25; + --cell-size: 15px; /* Size of each individual tile */ +} + +body { + font-family: 'Consolas', monospace; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #1a1a1a; /* Dark background */ + color: #00ff00; /* Green terminal text */ +} + +#game-container { + background-color: #222; + padding: 20px; + border-radius: 10px; + box-shadow: 0 0 20px rgba(0, 255, 0, 0.2); + text-align: center; +} + +h1 { + color: #00ff00; + margin-bottom: 15px; +} + +#status-area { + font-size: 0.9em; + margin-bottom: 10px; +} + +/* --- Map Grid Container --- */ +#grid-container { + width: calc(var(--grid-cols) * var(--cell-size)); + height: calc(var(--grid-rows) * var(--cell-size)); + display: grid; + /* Sets up the grid based on JS variables */ + grid-template-columns: repeat(var(--grid-cols), 1fr); + grid-template-rows: repeat(var(--grid-rows), 1fr); + border: 2px solid #555; + margin: 0 auto 20px; +} + +/* --- Tile Styling --- */ +.grid-cell { + width: var(--cell-size); + height: var(--cell-size); + /* No border needed, background color differentiates tiles */ +} + +.tile-wall { + background-color: #34495e; /* Dark blue/grey wall */ +} + +.tile-floor { + background-color: #8e44ad; /* Purple floor */ + /* Add a small shadow/border to give definition to the floor */ + box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5); +} + +.tile-empty { + /* Background matches container background */ + background-color: #222; +} + +/* --- Controls --- */ +#new-map-button { + padding: 10px 20px; + font-size: 1em; + font-weight: bold; + background-color: #2ecc71; /* Green button */ + color: #222; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s; +} + +#new-map-button:hover { + background-color: #27ae60; +} \ No newline at end of file diff --git a/games/margin_misson/index.html b/games/margin_misson/index.html new file mode 100644 index 00000000..53674105 --- /dev/null +++ b/games/margin_misson/index.html @@ -0,0 +1,68 @@ + + + + + + Margin Mission: Box-Model Mayhem + + + + +
    +

    Margin Mission: Box-Model Mayhem

    +

    Adjust the ball's margins, padding, and border to guide it to the goal without hitting obstacles!

    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +

    Ball Properties

    +
    + + + 10px +
    +
    + + + 10px +
    +
    + + + 10px +
    +
    + + + 10px +
    + +
    + +
    + + + 10px +
    + +
    + + + 5px +
    + + +

    +
    + + + + \ No newline at end of file diff --git a/games/margin_misson/script.js b/games/margin_misson/script.js new file mode 100644 index 00000000..b334702a --- /dev/null +++ b/games/margin_misson/script.js @@ -0,0 +1,143 @@ +document.addEventListener('DOMContentLoaded', () => { + const playerBall = document.getElementById('player-ball'); + const goal = document.getElementById('goal'); + const obstacles = document.querySelectorAll('.obstacle'); + const gameMessage = document.getElementById('game-message'); + const resetButton = document.getElementById('reset-button'); + const gameContainer = document.getElementById('game-container'); + + // Store initial CSS values for reset + const initialStyles = { + marginTop: '10px', + marginRight: '10px', + marginBottom: '10px', + marginLeft: '10px', + padding: '10px', + borderWidth: '5px' + }; + + // --- 1. Control Handling --- + const controls = { + 'margin-top': { prop: 'marginTop', suffix: 'px', element: document.getElementById('margin-top'), display: document.getElementById('margin-top-val') }, + 'margin-right': { prop: 'marginRight', suffix: 'px', element: document.getElementById('margin-right'), display: document.getElementById('margin-right-val') }, + 'margin-bottom': { prop: 'marginBottom', suffix: 'px', element: document.getElementById('margin-bottom'), display: document.getElementById('margin-bottom-val') }, + 'margin-left': { prop: 'marginLeft', suffix: 'px', element: document.getElementById('margin-left'), display: document.getElementById('margin-left-val') }, + 'padding': { prop: 'padding', suffix: 'px', element: document.getElementById('padding'), display: document.getElementById('padding-val') }, + 'border-width': { prop: 'borderWidth', suffix: 'px', element: document.getElementById('border-width'), display: document.getElementById('border-width-val') } + }; + + function updateBallStyle(controlId) { + const control = controls[controlId]; + const value = control.element.value; + playerBall.style[control.prop] = value + control.suffix; + control.display.textContent = value + control.suffix; + checkGameStatus(); // Re-check game status after any style change + } + + // Attach event listeners to all range inputs + for (const key in controls) { + controls[key].element.addEventListener('input', () => updateBallStyle(key)); + } + + // --- 2. Game Logic (Collision Detection) --- + + function getRect(element) { + // getBoundingClientRect gives the element's size and position relative to the viewport. + // We'll adjust it to be relative to the game-container for simpler collision logic. + const rect = element.getBoundingClientRect(); + const containerRect = gameContainer.getBoundingClientRect(); + return { + left: rect.left - containerRect.left, + top: rect.top - containerRect.top, + right: rect.right - containerRect.left, + bottom: rect.bottom - containerRect.top, + width: rect.width, + height: rect.height + }; + } + + function checkCollision(rect1, rect2) { + return rect1.left < rect2.right && + rect1.right > rect2.left && + rect1.top < rect2.bottom && + rect1.bottom > rect2.top; + } + + function checkGameStatus() { + // Ensure styles are applied before measuring + requestAnimationFrame(() => { + const playerRect = getRect(playerBall); + const goalRect = getRect(goal); + const containerRect = getRect(gameContainer); + + // Check Win Condition + if (checkCollision(playerRect, goalRect)) { + gameMessage.textContent = "๐Ÿ† Mission Accomplished! You reached the goal! ๐Ÿ†"; + playerBall.style.backgroundColor = 'lime'; + disableControls(); + return; // Game over, no need to check collisions or boundaries + } + + // Check for Wall Collisions (outside game container) + if (playerRect.left < 0 || playerRect.right > containerRect.width || + playerRect.top < 0 || playerRect.bottom > containerRect.height) { + gameMessage.textContent = "๐Ÿ’ฅ Out of Bounds! You hit the container wall! ๐Ÿ’ฅ"; + playerBall.style.backgroundColor = 'red'; + disableControls(); + return; + } + + // Check for Obstacle Collisions + for (const obstacle of obstacles) { + const obstacleRect = getRect(obstacle); + if (checkCollision(playerRect, obstacleRect)) { + gameMessage.textContent = "๐Ÿ’ฅ CRASH! You hit an obstacle! ๐Ÿ’ฅ"; + playerBall.style.backgroundColor = 'red'; + disableControls(); + return; + } + } + + // If no win/loss conditions met + gameMessage.textContent = "Guide the ball to the goal!"; + playerBall.style.backgroundColor = '#2196F3'; // Revert to normal color + enableControls(); + }); + } + + function disableControls() { + for (const key in controls) { + controls[key].element.disabled = true; + } + resetButton.disabled = true; // Optionally disable reset until game reset + } + + function enableControls() { + for (const key in controls) { + controls[key].element.disabled = false; + } + resetButton.disabled = false; + } + + + // --- 3. Reset Functionality --- + resetButton.addEventListener('click', () => { + // Apply initial styles + for (const prop in initialStyles) { + playerBall.style[prop] = initialStyles[prop]; + // Also update the sliders and their display values + const control = Object.values(controls).find(c => c.prop === prop); + if (control) { + control.element.value = parseInt(initialStyles[prop]); + control.display.textContent = initialStyles[prop]; + } + } + // Reset player color + playerBall.style.backgroundColor = '#2196F3'; + enableControls(); + checkGameStatus(); // Re-evaluate game status after reset + }); + + // Initial setup + resetButton.click(); // Apply initial values and perform first check +}); \ No newline at end of file diff --git a/games/margin_misson/style.css b/games/margin_misson/style.css new file mode 100644 index 00000000..aa4432fe --- /dev/null +++ b/games/margin_misson/style.css @@ -0,0 +1,165 @@ +:root { + --game-width: 600px; + --game-height: 400px; + --ball-base-size: 30px; /* The content box size */ + --goal-color: #4CAF50; + --obstacle-color: #f44336; + --container-bg: #e0e0e0; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + display: flex; + flex-direction: column; + align-items: center; + background-color: #212121; + color: white; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 20px; +} + +#game-container { + position: relative; /* Crucial for absolute positioning of ball, goal, obstacles */ + width: var(--game-width); + height: var(--game-height); + background-color: var(--container-bg); + border: 3px solid #616161; + overflow: hidden; /* Ensures ball/obstacles don't leak out */ + box-sizing: content-box; /* Standard box-sizing for the container */ + margin-bottom: 30px; +} + +/* --- Player Ball --- */ +#player-ball { + position: absolute; /* Allows precise positioning within the container */ + width: var(--ball-base-size); + height: var(--ball-base-size); + background-color: #2196F3; /* Blue */ + border: 5px solid #1976D2; /* Darker blue border */ + border-radius: 50%; /* Makes it a circle */ + box-sizing: border-box; /* Crucial: padding/border grow inward, not outward */ + /* Initial state, overridden by JS */ + margin: 10px; /* Example starting margin */ + padding: 10px; /* Example starting padding */ + left: 0; /* Aligned by margin */ + top: 0; /* Aligned by margin */ + transition: background-color 0.2s; /* Smooth visual feedback */ +} + +/* --- Goal --- */ +#goal { + position: absolute; + width: 60px; + height: 60px; + background-color: var(--goal-color); + border: 2px dashed darkgreen; + border-radius: 5px; + right: 20px; /* Positioned from right */ + bottom: 20px; /* Positioned from bottom */ + display: flex; + justify-content: center; + align-items: center; + font-size: 20px; + color: white; +} + +/* --- Obstacles --- */ +.obstacle { + position: absolute; + background-color: var(--obstacle-color); + border: 1px solid #c62828; +} + +#obs1 { + width: 80px; + height: 120px; + top: 50px; + left: 150px; +} + +#obs2 { + width: 150px; + height: 50px; + bottom: 100px; + left: 80px; +} + +/* A "thin wall" that requires the player to be small */ +.thin-wall { + background-color: #ff9800; /* Orange */ +} + +#obs3 { + width: 20px; /* Very narrow */ + height: 150px; + top: 80px; + left: 300px; +} + +#obs4 { + width: 100px; + height: 70px; + bottom: 30px; + right: 150px; +} + + +/* --- Controls --- */ +#controls { + background-color: #333; + padding: 20px; + border-radius: 8px; + width: var(--game-width); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.4); + text-align: center; +} + +.control-group { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.control-group label { + flex: 1; + text-align: left; + margin-right: 10px; +} + +.control-group input[type="range"] { + flex: 3; + width: 100%; + margin-right: 10px; +} + +.control-group span { + flex: 0.5; + text-align: right; + width: 50px; +} + +#reset-button { + padding: 10px 20px; + background-color: #007bff; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + margin-top: 20px; +} + +#reset-button:hover { + background-color: #0056b3; +} + +#game-message { + margin-top: 15px; + font-size: 1.1em; + color: yellow; + min-height: 2em; /* Prevent layout shift */ +} \ No newline at end of file diff --git a/games/match-pair/index.html b/games/match-pair/index.html new file mode 100644 index 00000000..dbb558bd --- /dev/null +++ b/games/match-pair/index.html @@ -0,0 +1,35 @@ + + + + + + Matching Pairs Puzzle + + + + +

    Matching Pairs Puzzle

    + +
    +
    +

    Items to Match

    +
    ๐ŸฆŠ Fox
    +
    ๐Ÿท Pig
    +
    ๐Ÿ” Chicken
    +
    ๐Ÿป Bear
    +
    + +
    +

    Categories

    +
    ๐ŸŒฒ Forest Animals
    +
    ๐Ÿšœ Farm Animals
    +
    +
    + +
    Drag and drop the items into the correct categories!
    + + + + + + \ No newline at end of file diff --git a/games/match-pair/script.js b/games/match-pair/script.js new file mode 100644 index 00000000..e2051f6c --- /dev/null +++ b/games/match-pair/script.js @@ -0,0 +1,97 @@ +document.addEventListener('DOMContentLoaded', () => { + // Select all draggable items and drop zones + const dragItems = document.querySelectorAll('.drag-item'); + const dropZones = document.querySelectorAll('.drop-zone'); + const messageElement = document.getElementById('message'); + const resetButton = document.getElementById('reset-button'); + let matchedCount = 0; // Tracks the number of successfully matched items + const totalItems = dragItems.length; + + /** + * DRAG EVENT HANDLERS + */ + + dragItems.forEach(item => { + // 1. dragstart: When dragging begins + item.addEventListener('dragstart', (e) => { + // Set the data being dragged (the item's ID and its correct category) + e.dataTransfer.setData('text/plain', e.target.id); + e.dataTransfer.setData('text/category', e.target.getAttribute('data-category')); + + // Add a class for visual feedback + setTimeout(() => { + e.target.classList.add('dragging'); + }, 0); + }); + + // 2. dragend: When dragging stops (dropped or cancelled) + item.addEventListener('dragend', (e) => { + // Remove the visual feedback class + e.target.classList.remove('dragging'); + }); + }); + + /** + * DROP ZONE EVENT HANDLERS + */ + + dropZones.forEach(zone => { + // 1. dragover: Allows an element to be dropped (must prevent default) + zone.addEventListener('dragover', (e) => { + e.preventDefault(); // This is crucial! + e.target.classList.add('drag-over'); + }); + + // 2. dragleave: Removes hover feedback when leaving the zone + zone.addEventListener('dragleave', (e) => { + e.target.classList.remove('drag-over'); + }); + + // 3. drop: When the item is dropped into the zone + zone.addEventListener('drop', (e) => { + e.preventDefault(); + zone.classList.remove('drag-over'); + + // Retrieve the data set in dragstart + const draggedItemId = e.dataTransfer.getData('text/plain'); + const draggedItemCategory = e.dataTransfer.getData('text/category'); + const draggedElement = document.getElementById(draggedItemId); + + const dropZoneAccept = e.target.getAttribute('data-accept'); + + // CHECK FOR CORRECT MATCH + if (draggedItemCategory === dropZoneAccept) { + // Correct Match + e.target.appendChild(draggedElement); // Move the item to the drop zone + draggedElement.draggable = false; // Make it non-draggable once placed + draggedElement.classList.add('matched-item'); // Apply success style + + matchedCount++; + updateMessage(`๐ŸŽ‰ Correct! ${draggedElement.textContent.trim()} matched with ${e.target.textContent.trim()}.`); + + // Check if all items are matched + if (matchedCount === totalItems) { + updateMessage('๐Ÿ† Puzzle Complete! You matched all pairs!'); + } + + } else { + // Incorrect Match + updateMessage(`โŒ Try again! The item belongs to the **${draggedItemCategory.toUpperCase()}** category.`); + } + }); + }); + + /** + * UTILITY FUNCTIONS + */ + + function updateMessage(msg) { + messageElement.innerHTML = msg; + } + + resetButton.addEventListener('click', () => { + // Simple page reload to reset the state + window.location.reload(); + }); + +}); \ No newline at end of file diff --git a/games/match-pair/style.css b/games/match-pair/style.css new file mode 100644 index 00000000..6ed2c95f --- /dev/null +++ b/games/match-pair/style.css @@ -0,0 +1,105 @@ +body { + font-family: sans-serif; + display: flex; + flex-direction: column; + align-items: center; + background-color: #f4f4f9; + padding: 20px; +} + +#puzzle-container { + display: flex; + gap: 50px; + margin-bottom: 30px; + background-color: white; + padding: 20px; + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0,0,0,0.1); +} + +/* --- Drag Items Styling --- */ +#drag-items, #drop-zones { + min-width: 250px; +} + +h2 { + color: #333; + border-bottom: 2px solid #ccc; + padding-bottom: 5px; +} + +.drag-item { + padding: 15px; + margin-bottom: 10px; + background-color: #ffda79; /* Yellowish background */ + border: 2px solid #f7b731; + border-radius: 8px; + cursor: grab; /* Indicates it's draggable */ + transition: all 0.2s ease; + font-weight: bold; +} + +.drag-item:hover { + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +/* Style for when an item is actively being dragged */ +.dragging { + opacity: 0.5; + transform: scale(0.95); +} + +/* --- Drop Zone Styling --- */ +.drop-zone { + padding: 20px; + margin-bottom: 15px; + min-height: 100px; + background-color: #e0e0e0; /* Grey background */ + border: 3px dashed #aaa; + border-radius: 10px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + transition: background-color 0.2s ease, border-color 0.2s ease; + font-weight: 500; +} + +/* Style for when a draggable item is hovering over a drop zone */ +.drop-zone.drag-over { + background-color: #d1ffd6; /* Light green on hover */ + border-color: #4CAF50; + transform: scale(1.02); +} + +/* Style for items successfully placed */ +.matched-item { + background-color: #4CAF50; /* Green background */ + color: white; + cursor: default; + border: 2px solid #388e3c; + margin-top: 10px; +} + +/* --- Message and Button --- */ +#message { + font-size: 1.2em; + margin-bottom: 20px; + font-style: italic; + color: #555; +} + +#reset-button { + padding: 10px 20px; + font-size: 1em; + cursor: pointer; + background-color: #007bff; + color: white; + border: none; + border-radius: 5px; + transition: background-color 0.2s; +} + +#reset-button:hover { + background-color: #0056b3; +} \ No newline at end of file diff --git a/games/math-chain/index.html b/games/math-chain/index.html new file mode 100644 index 00000000..3ee7372a --- /dev/null +++ b/games/math-chain/index.html @@ -0,0 +1,40 @@ + + + + + + Math Chain | Mini JS Games Hub + + + +
    +

    Math Chain

    +

    Target: 0

    + +
    + + = ? +
    + +
    + +
    + +
    + + + + + +
    + +

    +
    + + + + + + + + diff --git a/games/math-chain/script.js b/games/math-chain/script.js new file mode 100644 index 00000000..ec10d7a9 --- /dev/null +++ b/games/math-chain/script.js @@ -0,0 +1,118 @@ +const targetEl = document.getElementById("target-number"); +const tilesContainer = document.getElementById("tiles-container"); +const expressionEl = document.getElementById("current-expression"); +const resultEl = document.getElementById("current-result"); +const messageEl = document.getElementById("message"); + +const clickSound = document.getElementById("click-sound"); +const successSound = document.getElementById("success-sound"); +const errorSound = document.getElementById("error-sound"); + +let targetNumber; +let tiles = []; +let currentExpression = []; +let paused = false; + +// Operators and numbers +const operators = ["+", "-", "*", "/"]; +const numbers = Array.from({length: 9}, (_, i) => (i+1).toString()); + +// Utility to shuffle array +function shuffle(array) { + return array.sort(() => Math.random() - 0.5); +} + +// Generate new round +function generateRound() { + targetNumber = Math.floor(Math.random() * 50) + 10; + targetEl.textContent = targetNumber; + + tiles = shuffle([...numbers, ...operators, ...numbers, ...operators]); + tilesContainer.innerHTML = ""; + tiles.forEach(val => { + const tile = document.createElement("div"); + tile.className = "tile"; + tile.textContent = val; + tile.addEventListener("click", () => { + if(paused) return; + clickSound.play(); + currentExpression.push(val); + updateExpression(); + }); + tilesContainer.appendChild(tile); + }); + + currentExpression = []; + updateExpression(); + messageEl.textContent = ""; +} + +// Update expression display +function updateExpression() { + expressionEl.innerHTML = ""; + currentExpression.forEach((val, idx) => { + const span = document.createElement("span"); + span.textContent = val; + span.addEventListener("click", () => { + if(paused) return; + currentExpression.splice(idx, 1); + updateExpression(); + }); + expressionEl.appendChild(span); + }); + resultEl.textContent = currentExpression.length ? "= ?" : "= ?"; +} + +// Submit expression +document.getElementById("submit-btn").addEventListener("click", () => { + if(paused) return; + try { + const expr = currentExpression.join(""); + const evalResult = Function('"use strict";return (' + expr + ')')(); + if(Math.abs(evalResult - targetNumber) < 0.0001){ + successSound.play(); + messageEl.textContent = "โœ… Correct! Next round..."; + setTimeout(generateRound, 1500); + } else { + errorSound.play(); + messageEl.textContent = `โŒ Incorrect! Result: ${evalResult}`; + } + } catch { + errorSound.play(); + messageEl.textContent = "โŒ Invalid Expression!"; + } +}); + +// Undo +document.getElementById("undo-btn").addEventListener("click", () => { + if(paused) return; + currentExpression.pop(); + updateExpression(); + clickSound.play(); +}); + +// Restart +document.getElementById("restart-btn").addEventListener("click", () => { + paused = false; + document.getElementById("resume-btn").hidden = true; + document.getElementById("pause-btn").hidden = false; + generateRound(); +}); + +// Pause / Resume +document.getElementById("pause-btn").addEventListener("click", () => { + paused = true; + document.getElementById("pause-btn").hidden = true; + document.getElementById("resume-btn").hidden = false; + messageEl.textContent = "โธ๏ธ Game Paused"; +}); + +document.getElementById("resume-btn").addEventListener("click", () => { + paused = false; + document.getElementById("pause-btn").hidden = false; + document.getElementById("resume-btn").hidden = true; + messageEl.textContent = ""; +}); + +// Initialize game +generateRound(); diff --git a/games/math-chain/style.css b/games/math-chain/style.css new file mode 100644 index 00000000..58a5b750 --- /dev/null +++ b/games/math-chain/style.css @@ -0,0 +1,102 @@ +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(120deg, #1e3c72, #2a5298); + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + color: #fff; +} + +.game-container { + background: rgba(0,0,0,0.6); + padding: 30px 40px; + border-radius: 20px; + text-align: center; + box-shadow: 0 0 30px #ffcc00; + width: 90%; + max-width: 600px; +} + +h1 { + font-size: 2rem; + margin-bottom: 10px; + text-shadow: 0 0 10px #ffcc00; +} + +.target { + font-size: 1.5rem; + margin-bottom: 20px; +} + +.expression-display { + font-size: 1.5rem; + margin-bottom: 20px; + padding: 10px; + border: 2px solid #ffcc00; + border-radius: 10px; + display: flex; + justify-content: center; + gap: 10px; +} + +#current-expression span { + padding: 5px 10px; + margin: 0 2px; + background: #333; + border-radius: 5px; + cursor: pointer; + transition: all 0.2s ease; +} + +#current-expression span.glow { + box-shadow: 0 0 15px #00ffcc; +} + +.tiles-container { + display: flex; + flex-wrap: wrap; + justify-content: center; + margin-bottom: 20px; + gap: 10px; +} + +.tile { + background: #444; + padding: 15px 20px; + border-radius: 10px; + font-size: 1.2rem; + cursor: pointer; + transition: all 0.2s ease; + text-shadow: 0 0 5px #fff; +} + +.tile:hover { + transform: scale(1.1); + box-shadow: 0 0 15px #ffcc00; +} + +.controls button { + padding: 10px 15px; + margin: 5px; + border: none; + border-radius: 10px; + cursor: pointer; + font-size: 1rem; + background: #ffcc00; + color: #000; + transition: all 0.2s ease; +} + +.controls button:hover { + transform: scale(1.1); + box-shadow: 0 0 15px #00ffcc; +} + +#message { + margin-top: 20px; + font-size: 1.2rem; + font-weight: bold; + text-shadow: 0 0 10px #fff; +} diff --git a/games/math-challenge/index.html b/games/math-challenge/index.html new file mode 100644 index 00000000..672c3d06 --- /dev/null +++ b/games/math-challenge/index.html @@ -0,0 +1,29 @@ + + + + + + Math Challenge | Mini JS Games Hub + + + +
    +

    Math Challenge ๐Ÿงฎ

    +
    +

    Score: 0

    +

    Time Left: 60s

    +
    +
    +

    Press "Start" to begin!

    +
    + +
    + + + +
    +

    +
    + + + diff --git a/games/math-challenge/script.js b/games/math-challenge/script.js new file mode 100644 index 00000000..67ba8427 --- /dev/null +++ b/games/math-challenge/script.js @@ -0,0 +1,88 @@ +const startBtn = document.getElementById("start-btn"); +const submitBtn = document.getElementById("submit-btn"); +const restartBtn = document.getElementById("restart-btn"); +const questionEl = document.getElementById("question"); +const answerInput = document.getElementById("answer"); +const scoreEl = document.getElementById("score"); +const timerEl = document.getElementById("timer"); +const feedbackEl = document.getElementById("feedback"); + +let score = 0; +let time = 60; +let currentAnswer = 0; +let timerInterval; + +function generateQuestion() { + const operations = ["+", "-", "*", "/"]; + const op = operations[Math.floor(Math.random() * operations.length)]; + let num1 = Math.floor(Math.random() * 50) + 1; + let num2 = Math.floor(Math.random() * 50) + 1; + + if (op === "/") { + // Ensure division is whole number + num1 = num1 * num2; + } + + questionEl.textContent = `Solve: ${num1} ${op} ${num2}`; + switch(op) { + case "+": currentAnswer = num1 + num2; break; + case "-": currentAnswer = num1 - num2; break; + case "*": currentAnswer = num1 * num2; break; + case "/": currentAnswer = num1 / num2; break; + } +} + +function startGame() { + score = 0; + time = 60; + scoreEl.textContent = score; + timerEl.textContent = time; + answerInput.disabled = false; + submitBtn.disabled = false; + startBtn.disabled = true; + feedbackEl.textContent = ""; + answerInput.value = ""; + answerInput.focus(); + generateQuestion(); + + timerInterval = setInterval(() => { + time--; + timerEl.textContent = time; + if(time <= 0) { + clearInterval(timerInterval); + endGame(); + } + }, 1000); +} + +function submitAnswer() { + const userAnswer = parseFloat(answerInput.value); + if (!isNaN(userAnswer)) { + if (userAnswer === currentAnswer) { + score++; + feedbackEl.textContent = "โœ… Correct!"; + } else { + feedbackEl.textContent = `โŒ Wrong! The answer was ${currentAnswer}`; + } + scoreEl.textContent = score; + answerInput.value = ""; + generateQuestion(); + } +} + +function endGame() { + feedbackEl.textContent = `โฐ Time's up! Your final score is ${score}`; + answerInput.disabled = true; + submitBtn.disabled = true; + startBtn.disabled = false; +} + +startBtn.addEventListener("click", startGame); +submitBtn.addEventListener("click", submitAnswer); +restartBtn.addEventListener("click", () => { + clearInterval(timerInterval); + startGame(); +}); +answerInput.addEventListener("keyup", (e) => { + if(e.key === "Enter") submitAnswer(); +}); diff --git a/games/math-challenge/style.css b/games/math-challenge/style.css new file mode 100644 index 00000000..93cdd8b1 --- /dev/null +++ b/games/math-challenge/style.css @@ -0,0 +1,69 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #fceabb, #f8b500); + margin: 0; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.math-container { + background: white; + padding: 30px 40px; + border-radius: 15px; + text-align: center; + box-shadow: 0 10px 25px rgba(0,0,0,0.2); + width: 320px; +} + +h1 { + margin-bottom: 20px; +} + +.stats { + display: flex; + justify-content: space-between; + font-weight: bold; + margin-bottom: 15px; +} + +.question-container { + margin: 20px 0; + font-size: 1.2rem; + min-height: 40px; +} + +input[type="number"] { + padding: 10px; + width: 100%; + font-size: 1rem; + margin-bottom: 15px; + border-radius: 5px; + border: 1px solid #ccc; + text-align: center; +} + +.controls button { + padding: 8px 12px; + margin: 5px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 1rem; + transition: all 0.2s ease; +} + +#start-btn { background: #4caf50; color: white; } +#submit-btn { background: #2196f3; color: white; } +#restart-btn { background: #f44336; color: white; } + +.controls button:hover { + transform: scale(1.05); +} + +#feedback { + margin-top: 15px; + font-weight: bold; + min-height: 20px; +} diff --git a/games/math-duel/index.html b/games/math-duel/index.html new file mode 100644 index 00000000..bdeeb198 --- /dev/null +++ b/games/math-duel/index.html @@ -0,0 +1,52 @@ + + + + + + Math Duel | Mini JS Games Hub + + + +
    +

    Math Duel ๐Ÿงฎโš”๏ธ

    + +
    + + + +
    + +
    +
    +

    Player 1

    +
    +
    ๐ŸŽ๏ธ
    +
    +

    Score: 0

    +
    + +
    +

    Player 2

    +
    +
    ๐Ÿš—
    +
    +

    Score: 0

    +
    +
    + +
    +

    Math Question:

    +

    Press Start!

    + + +
    +
    + + + + + + + + + diff --git a/games/math-duel/script.js b/games/math-duel/script.js new file mode 100644 index 00000000..37bfb4b5 --- /dev/null +++ b/games/math-duel/script.js @@ -0,0 +1,115 @@ +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const restartBtn = document.getElementById("restart-btn"); +const submitBtn = document.getElementById("submit-btn"); +const questionEl = document.getElementById("question"); +const answerInput = document.getElementById("answer"); +const runner1 = document.getElementById("runner1"); +const runner2 = document.getElementById("runner2"); +const score1El = document.getElementById("score1"); +const score2El = document.getElementById("score2"); +const correctSound = document.getElementById("correct-sound"); +const wrongSound = document.getElementById("wrong-sound"); +const winSound = document.getElementById("win-sound"); + +let player1Score = 0; +let player2Score = 0; +let currentPlayer = 1; +let interval; +let paused = true; + +let currentAnswer = 0; + +// Generate random math question +function generateQuestion() { + const a = Math.floor(Math.random() * 20) + 1; + const b = Math.floor(Math.random() * 20) + 1; + const operators = ["+", "-", "*", "/"]; + const op = operators[Math.floor(Math.random() * operators.length)]; + + switch(op) { + case "+": currentAnswer = a + b; break; + case "-": currentAnswer = a - b; break; + case "*": currentAnswer = a * b; break; + case "/": currentAnswer = Math.round(a / b); break; + } + + questionEl.textContent = `Player ${currentPlayer}: ${a} ${op} ${b} = ?`; +} + +// Move runner +function moveRunner(player) { + const track = player === 1 ? runner1 : runner2; + const trackWidth = track.parentElement.offsetWidth - track.offsetWidth; + const newPos = (player === 1 ? player1Score : player2Score) * 10; + track.style.left = `${Math.min(newPos, trackWidth)}px`; +} + +// Check answer +function submitAnswer() { + const val = parseInt(answerInput.value); + if (isNaN(val)) return; + if (val === currentAnswer) { + correctSound.play(); + if (currentPlayer === 1) { + player1Score++; + moveRunner(1); + } else { + player2Score++; + moveRunner(2); + } + } else { + wrongSound.play(); + } + + answerInput.value = ""; + if (player1Score >= 20 || player2Score >= 20) { + endGame(); + } else { + currentPlayer = currentPlayer === 1 ? 2 : 1; + generateQuestion(); + } +} + +// Start game +function startGame() { + if (!paused) return; + paused = false; + generateQuestion(); +} + +// Pause game +function pauseGame() { + paused = true; + questionEl.textContent = "Game Paused"; +} + +// Restart game +function restartGame() { + paused = true; + player1Score = 0; + player2Score = 0; + moveRunner(1); + moveRunner(2); + score1El.textContent = player1Score; + score2El.textContent = player2Score; + currentPlayer = 1; + generateQuestion(); +} + +// End game +function endGame() { + paused = true; + const winner = player1Score > player2Score ? 1 : 2; + questionEl.textContent = `Player ${winner} Wins! ๐ŸŽ‰`; + winSound.play(); +} + +// Event listeners +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", pauseGame); +restartBtn.addEventListener("click", restartGame); +submitBtn.addEventListener("click", submitAnswer); +answerInput.addEventListener("keyup", (e) => { + if (e.key === "Enter") submitAnswer(); +}); diff --git a/games/math-duel/style.css b/games/math-duel/style.css new file mode 100644 index 00000000..3ecb6839 --- /dev/null +++ b/games/math-duel/style.css @@ -0,0 +1,98 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: #0d0d0d; + color: #fff; + display: flex; + justify-content: center; + align-items: flex-start; + padding: 20px; +} + +.math-duel-container { + max-width: 900px; + width: 100%; + text-align: center; +} + +h1 { + text-shadow: 0 0 10px #ffcc00, 0 0 20px #ffcc00; + margin-bottom: 20px; +} + +.controls button { + padding: 10px 20px; + margin: 0 5px; + font-size: 16px; + cursor: pointer; + border: none; + border-radius: 5px; + background: #ffcc00; + color: #000; + font-weight: bold; + transition: 0.3s; +} + +.controls button:hover { + box-shadow: 0 0 10px #ffcc00; +} + +.players-container { + display: flex; + justify-content: space-between; + margin: 30px 0; +} + +.player { + width: 45%; +} + +.track { + width: 100%; + height: 50px; + background: #222; + border-radius: 25px; + position: relative; + overflow: hidden; + box-shadow: inset 0 0 20px #444; +} + +.runner { + position: absolute; + left: 0; + top: 0; + height: 50px; + line-height: 50px; + font-size: 30px; + animation: glow 1s infinite alternate; +} + +@keyframes glow { + 0% { text-shadow: 0 0 5px #ffcc00, 0 0 10px #ffcc00; } + 100% { text-shadow: 0 0 20px #ffcc00, 0 0 30px #ffcc00; } +} + +.question-container { + margin-top: 30px; +} + +.question-container input { + padding: 10px; + font-size: 16px; + width: 100px; + margin-right: 10px; + border-radius: 5px; + border: none; + text-align: center; +} + +.question-container button { + padding: 10px 20px; + font-size: 16px; + border-radius: 5px; + border: none; + background: #ffcc00; + color: #000; + font-weight: bold; + cursor: pointer; +} diff --git a/games/math-game/index.html b/games/math-game/index.html new file mode 100644 index 00000000..e143f0e6 --- /dev/null +++ b/games/math-game/index.html @@ -0,0 +1,23 @@ + + + + + + Math Game + + + +
    +

    Math Game

    +

    Solve the math problems as fast as you can!

    +
    +
    Time: 30
    +
    Score: 0
    + +
    + +
    +
    + + + \ No newline at end of file diff --git a/games/math-game/script.js b/games/math-game/script.js new file mode 100644 index 00000000..7f8d0f75 --- /dev/null +++ b/games/math-game/script.js @@ -0,0 +1,149 @@ +// Math Game Script +// Solve math problems quickly + +var canvas = document.getElementById('gameCanvas'); +var ctx = canvas.getContext('2d'); +var timerDisplay = document.getElementById('timer'); +var scoreDisplay = document.getElementById('score'); +var restartBtn = document.getElementById('restart'); +var messageDiv = document.getElementById('message'); + +var currentProblem = ''; +var correctAnswer = 0; +var options = []; +var score = 0; +var timeLeft = 30; +var timerInterval; +var gameRunning = true; + +// Initialize the game +function initGame() { + score = 0; + timeLeft = 30; + gameRunning = true; + messageDiv.textContent = ''; + scoreDisplay.textContent = 'Score: ' + score; + generateProblem(); + startTimer(); + draw(); +} + +// Generate a random math problem +function generateProblem() { + var num1 = Math.floor(Math.random() * 20) + 1; + var num2 = Math.floor(Math.random() * 20) + 1; + var operations = ['+', '-', '*']; + var op = operations[Math.floor(Math.random() * operations.length)]; + + if (op === '+') { + correctAnswer = num1 + num2; + currentProblem = num1 + ' + ' + num2; + } else if (op === '-') { + // Ensure positive result + if (num1 < num2) { + var temp = num1; + num1 = num2; + num2 = temp; + } + correctAnswer = num1 - num2; + currentProblem = num1 + ' - ' + num2; + } else if (op === '*') { + // Smaller numbers for multiplication + num1 = Math.floor(Math.random() * 10) + 1; + num2 = Math.floor(Math.random() * 10) + 1; + correctAnswer = num1 * num2; + currentProblem = num1 + ' ร— ' + num2; + } + + // Generate options + options = []; + var correctIndex = Math.floor(Math.random() * 4); + for (var i = 0; i < 4; i++) { + if (i === correctIndex) { + options.push(correctAnswer); + } else { + var wrongAnswer; + do { + wrongAnswer = correctAnswer + Math.floor(Math.random() * 20) - 10; + } while (wrongAnswer === correctAnswer || wrongAnswer < 0); + options.push(wrongAnswer); + } + } +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw the problem + ctx.fillStyle = '#000'; + ctx.font = '32px Arial'; + ctx.textAlign = 'center'; + ctx.fillText(currentProblem + ' = ?', canvas.width / 2, 100); + + // Draw the options + for (var i = 0; i < options.length; i++) { + var x = 150 + (i % 2) * 250; + var y = 200 + Math.floor(i / 2) * 100; + + // Draw button background + ctx.fillStyle = '#e1bee7'; + ctx.fillRect(x - 50, y - 25, 100, 50); + ctx.strokeStyle = '#7b1fa2'; + ctx.strokeRect(x - 50, y - 25, 100, 50); + + // Draw the number + ctx.fillStyle = '#000'; + ctx.font = '24px Arial'; + ctx.fillText(options[i], x, y + 8); + } +} + +// Handle canvas click +canvas.addEventListener('click', function(event) { + if (!gameRunning) return; + var rect = canvas.getBoundingClientRect(); + var x = event.clientX - rect.left; + var y = event.clientY - rect.top; + + // Check which option was clicked + for (var i = 0; i < options.length; i++) { + var optX = 150 + (i % 2) * 250; + var optY = 200 + Math.floor(i / 2) * 100; + if (x >= optX - 50 && x <= optX + 50 && y >= optY - 25 && y <= optY + 25) { + if (options[i] === correctAnswer) { + score++; + scoreDisplay.textContent = 'Score: ' + score; + messageDiv.textContent = 'Correct!'; + messageDiv.style.color = 'green'; + } else { + messageDiv.textContent = 'Wrong!'; + messageDiv.style.color = 'red'; + } + setTimeout(generateProblem, 1000); + draw(); + break; + } + } +}); + +// Start the timer +function startTimer() { + clearInterval(timerInterval); + timerInterval = setInterval(function() { + timeLeft--; + timerDisplay.textContent = 'Time: ' + timeLeft; + if (timeLeft <= 0) { + clearInterval(timerInterval); + gameRunning = false; + messageDiv.textContent = 'Time\'s up! Final Score: ' + score; + messageDiv.style.color = 'purple'; + } + }, 1000); +} + +// Restart button +restartBtn.addEventListener('click', initGame); + +// Start the game +initGame(); \ No newline at end of file diff --git a/games/math-game/style.css b/games/math-game/style.css new file mode 100644 index 00000000..757f9387 --- /dev/null +++ b/games/math-game/style.css @@ -0,0 +1,53 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #f3e5f5; +} + +.container { + text-align: center; +} + +h1 { + color: #7b1fa2; +} + +.game-info { + margin-bottom: 20px; + display: flex; + justify-content: center; + gap: 20px; +} + +#timer, #score { + font-size: 24px; + font-weight: bold; +} + +#restart { + padding: 10px 20px; + font-size: 16px; + background-color: #9c27b0; + color: white; + border: none; + cursor: pointer; +} + +#restart:hover { + background-color: #7b1fa2; +} + +canvas { + border: 2px solid #7b1fa2; + background-color: #ffffff; +} + +#message { + margin-top: 20px; + font-size: 18px; + font-weight: bold; +} \ No newline at end of file diff --git a/games/math-rush/index.html b/games/math-rush/index.html new file mode 100644 index 00000000..424f61d2 --- /dev/null +++ b/games/math-rush/index.html @@ -0,0 +1,28 @@ + + + + + + Math Rush โ€” Addition Challenge + + + +
    +

    ๐Ÿงฎ Math Rush

    +

    Solve as many addition problems as you can โ€” 10 seconds per round!

    + +
    +

    Press Start to Begin

    + + +

    โฑ๏ธ Time: 10

    +

    Score: 0

    + +
    + + +
    + + + + diff --git a/games/math-rush/script.js b/games/math-rush/script.js new file mode 100644 index 00000000..ebbc802c --- /dev/null +++ b/games/math-rush/script.js @@ -0,0 +1,75 @@ +let num1, num2, correctAnswer; +let score = 0; +let timeLeft = 10; +let timer; +let gameActive = false; + +const question = document.getElementById("question"); +const answer = document.getElementById("answer"); +const timerDisplay = document.getElementById("timer"); +const scoreDisplay = document.getElementById("score"); +const submitBtn = document.getElementById("submit"); +const startBtn = document.getElementById("start"); + +function startGame() { + score = 0; + scoreDisplay.textContent = "Score: 0"; + gameActive = true; + answer.disabled = false; + submitBtn.disabled = false; + startBtn.disabled = true; + newQuestion(); +} + +function newQuestion() { + num1 = Math.floor(Math.random() * 50) + 1; + num2 = Math.floor(Math.random() * 50) + 1; + correctAnswer = num1 + num2; + question.textContent = `${num1} + ${num2} = ?`; + answer.value = ""; + answer.focus(); + resetTimer(); +} + +function resetTimer() { + clearInterval(timer); + timeLeft = 10; + timerDisplay.textContent = `โฑ๏ธ Time: ${timeLeft}`; + timer = setInterval(() => { + timeLeft--; + timerDisplay.textContent = `โฑ๏ธ Time: ${timeLeft}`; + if (timeLeft <= 0) { + clearInterval(timer); + endGame("โฐ Timeโ€™s up!"); + } + }, 1000); +} + +function checkAnswer() { + if (!gameActive) return; + const playerAnswer = parseInt(answer.value); + if (isNaN(playerAnswer)) return; + + if (playerAnswer === correctAnswer) { + score++; + scoreDisplay.textContent = `Score: ${score}`; + newQuestion(); + } else { + endGame(`โŒ Wrong! The correct answer was ${correctAnswer}.`); + } +} + +function endGame(message) { + gameActive = false; + clearInterval(timer); + question.textContent = message + ` Final Score: ${score}`; + answer.disabled = true; + submitBtn.disabled = true; + startBtn.disabled = false; +} + +submitBtn.addEventListener("click", checkAnswer); +startBtn.addEventListener("click", startGame); +answer.addEventListener("keydown", (e) => { + if (e.key === "Enter") checkAnswer(); +}); diff --git a/games/math-rush/style.css b/games/math-rush/style.css new file mode 100644 index 00000000..59cdedc4 --- /dev/null +++ b/games/math-rush/style.css @@ -0,0 +1,71 @@ +body { + font-family: "Poppins", sans-serif; + background: linear-gradient(135deg, #89f7fe, #66a6ff); + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + color: #222; + margin: 0; + } + + .container { + background: #fff; + padding: 30px 40px; + border-radius: 16px; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2); + text-align: center; + width: 320px; + } + + h1 { + margin-bottom: 5px; + } + + .description { + font-size: 0.9rem; + color: #555; + margin-bottom: 20px; + } + + .game input { + width: 80%; + padding: 10px; + font-size: 1rem; + border: 2px solid #66a6ff; + border-radius: 8px; + margin-bottom: 10px; + } + + button { + background: #66a6ff; + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + font-size: 1rem; + transition: 0.2s ease; + } + + button:hover { + background: #5587dc; + } + + #question { + font-size: 1.4rem; + font-weight: bold; + margin-bottom: 15px; + } + + #timer, #score { + font-size: 1rem; + margin-top: 10px; + } + + .footer { + margin-top: 20px; + font-size: 0.8rem; + color: #888; + } + \ No newline at end of file diff --git a/games/math-sprint/index.html b/games/math-sprint/index.html new file mode 100644 index 00000000..62af4853 --- /dev/null +++ b/games/math-sprint/index.html @@ -0,0 +1,34 @@ + + + + + + Math Sprint + + + +
    +

    Math Sprint

    + +
    + Score: 0 + Time Left: 60s +
    + +
    +
    +
    +
    +
    +
    + + + +
    Press Enter to Start!
    + + +
    + + + + \ No newline at end of file diff --git a/games/math-sprint/script.js b/games/math-sprint/script.js new file mode 100644 index 00000000..2a1383b0 --- /dev/null +++ b/games/math-sprint/script.js @@ -0,0 +1,247 @@ +// --- DOM Elements --- +const equationElement = document.getElementById('equation'); +const answerInput = document.getElementById('answer-input'); +const scoreElement = document.getElementById('score'); +const gameTimerElement = document.getElementById('game-timer'); +const messageElement = document.getElementById('message'); +const restartButton = document.getElementById('restart-button'); +const progressBar = document.getElementById('progress-bar'); +const progressBarContainer = document.getElementById('progress-bar-container'); + +// --- Game Constants --- +const GAME_DURATION_SECONDS = 60; +const PROBLEM_TIME_LIMIT_MS = 3000; // 3 seconds per problem +const OPERATORS = ['+', '-', '*']; +const MAX_OPERAND = 12; // Max number used in equations + +// --- Game State --- +let score = 0; +let timeLeft = GAME_DURATION_SECONDS; +let gameInterval; // For the main game timer +let problemTimeout; // For the per-problem timer +let currentProblem = { + text: '', + answer: 0 +}; +let gameActive = false; + +// --- Equation Generation Logic --- + +/** + * Generates a simple arithmetic problem (A op B = ?). + * Ensures results for subtraction are non-negative and multiplication is simple. + */ +function generateProblem() { + let num1, num2, operator, result, text; + + // Select two random numbers + num1 = Math.floor(Math.random() * MAX_OPERAND) + 1; + num2 = Math.floor(Math.random() * MAX_OPERAND) + 1; + operator = OPERATORS[Math.floor(Math.random() * OPERATORS.length)]; + + // Enforce simple constraints + if (operator === '-') { + // Ensure result is not negative + if (num1 < num2) { + [num1, num2] = [num2, num1]; // Swap them + } + } else if (operator === '*') { + // Keep multiplication simple (e.g., max 9 * 9) + num1 = Math.min(num1, 9); + num2 = Math.min(num2, 9); + if (num1 === 1) num1 = 2; // Avoid 1x + if (num2 === 1) num2 = 2; + } + + // Calculate the result + switch (operator) { + case '+': + result = num1 + num2; + break; + case '-': + result = num1 - num2; + break; + case '*': + result = num1 * num2; + break; + } + + // Format the text display + text = `${num1} ${operator} ${num2} = ?`; + + currentProblem.text = text; + currentProblem.answer = result; + + equationElement.textContent = text; +} + +// --- Game Flow and Timer Management --- + +/** + * Initializes and starts the main game timer. + */ +function startMainTimer() { + gameInterval = setInterval(() => { + timeLeft--; + gameTimerElement.textContent = `Time Left: ${timeLeft}s`; + + if (timeLeft <= 0) { + clearInterval(gameInterval); + gameOver(); + } + }, 1000); +} + +/** + * Starts the timer for the current problem (progress bar). + */ +function startProblemTimer() { + // 1. Reset progress bar visually + progressBar.style.transition = 'none'; + progressBar.style.width = '100%'; + // Forces a reflow to apply 'none' before setting the transition + progressBar.offsetWidth; + + // 2. Start the width transition + progressBar.style.transition = `width ${PROBLEM_TIME_LIMIT_MS / 1000}s linear`; + progressBar.style.width = '0%'; + + // 3. Set a timeout for when the problem expires + problemTimeout = setTimeout(() => { + // Time ran out! + handleIncorrectAnswer(true); + }, PROBLEM_TIME_LIMIT_MS); +} + +/** + * Moves to the next problem, resetting the problem timer. + */ +function nextProblem() { + clearTimeout(problemTimeout); + answerInput.value = ''; + answerInput.disabled = false; + answerInput.focus(); + + generateProblem(); + startProblemTimer(); +} + +/** + * Called when the user submits an answer. + */ +function checkAnswer() { + if (!gameActive) return; + + const userAnswer = parseInt(answerInput.value.trim()); + + if (isNaN(userAnswer)) { + // Allow user to clear input and try again without penalty + return; + } + + if (userAnswer === currentProblem.answer) { + handleCorrectAnswer(); + } else { + handleIncorrectAnswer(false); + } +} + +/** + * Handles a correct answer submission. + */ +function handleCorrectAnswer() { + score++; + scoreElement.textContent = `Score: ${score}`; + messageElement.textContent = 'โœ… Correct!'; + messageElement.style.color = '#a6e3a1'; // Green + + // Immediately move to the next problem + nextProblem(); +} + +/** + * Handles an incorrect answer submission or a timeout. + * @param {boolean} isTimeout - True if the time ran out. + */ +function handleIncorrectAnswer(isTimeout) { + // No score penalty, just move on + messageElement.textContent = isTimeout + ? `โฐ Time Out! The answer was ${currentProblem.answer}.` + : `โŒ Incorrect! The answer was ${currentProblem.answer}.`; + messageElement.style.color = '#f38ba8'; // Pink/Red + + // Disable input briefly + answerInput.disabled = true; + + // Give player a second to read the message, then continue + setTimeout(nextProblem, 1000); +} + +/** + * Ends the game and displays the final score. + */ +function gameOver() { + gameActive = false; + answerInput.disabled = true; + clearTimeout(problemTimeout); + + // Stop the progress bar animation + progressBar.style.width = '0%'; + progressBar.style.transition = 'none'; + + messageElement.textContent = `Time's Up! Final Score: ${score}`; + messageElement.style.color = '#d19a66'; + restartButton.classList.remove('hidden'); + + // Hide progress bar for game over + progressBarContainer.style.display = 'none'; +} + +/** + * Resets all game state and starts the game. + */ +function startGame() { + score = 0; + timeLeft = GAME_DURATION_SECONDS; + gameActive = true; + + // Reset DOM elements + scoreElement.textContent = 'Score: 0'; + gameTimerElement.textContent = `Time Left: ${GAME_DURATION_SECONDS}s`; + messageElement.textContent = ''; + restartButton.classList.add('hidden'); + progressBarContainer.style.display = 'block'; + + // Start timers and the first problem + startMainTimer(); + nextProblem(); +} + +// --- Event Handlers --- + +/** + * Handles 'Enter' key press on the input field. + */ +answerInput.addEventListener('keypress', function(event) { + // Check for Enter key + if (event.key === 'Enter') { + event.preventDefault(); // Prevent default form submission + checkAnswer(); + } +}); + +/** + * Handles the initial start and restart button. + */ +document.addEventListener('keypress', function(event) { + if (event.key === 'Enter' && !gameActive) { + startGame(); + } +}); + +restartButton.addEventListener('click', startGame); + +// --- Initial Setup --- +// Set initial message +messageElement.textContent = 'Press Enter to Start!'; +answerInput.disabled = true; \ No newline at end of file diff --git a/games/math-sprint/style.css b/games/math-sprint/style.css new file mode 100644 index 00000000..82956be7 --- /dev/null +++ b/games/math-sprint/style.css @@ -0,0 +1,91 @@ +body { + background-color: #282c34; /* Dark background */ + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + font-family: 'Consolas', monospace; /* Monospaced font for math */ + color: #abb2bf; +} + +#game-container { + text-align: center; + background-color: #3e4451; + padding: 30px; + border-radius: 10px; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + width: 350px; +} + +h1 { + color: #e06c75; /* Red accent */ + margin-top: 0; +} + +#status-bar { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + font-size: 1.4em; + color: #98c379; /* Green accent */ +} + +#problem-area { + background-color: #21252b; + padding: 20px; + border-radius: 5px; + margin-bottom: 20px; +} + +#equation { + font-size: 3em; + font-weight: bold; + color: #61afef; /* Blue accent for numbers */ + margin-bottom: 15px; + min-height: 1.5em; /* Ensure stable height */ +} + +/* --- Input Field Styling --- */ +#answer-input { + width: 80%; + padding: 10px; + font-size: 1.5em; + text-align: center; + border: 3px solid #56b6c2; /* Cyan accent */ + border-radius: 5px; + outline: none; + background-color: #282c34; + color: #ffffff; +} + +#answer-input:focus { + border-color: #c678dd; /* Purple focus accent */ +} + +/* --- Progress Bar Styling --- */ +#progress-bar-container { + width: 100%; + height: 15px; + background-color: #555; + border-radius: 5px; + overflow: hidden; + margin-top: 10px; +} + +#progress-bar { + height: 100%; + width: 100%; /* Starts full */ + background-color: #f38ba8; /* Pink/Red for time */ + transition: width linear; /* Transition is set in JS */ +} + +#message { + margin-top: 20px; + font-size: 1.2em; + color: #d19a66; /* Yellow accent */ +} + +.hidden { + display: none; +} \ No newline at end of file diff --git a/games/math_flacecard/index.html b/games/math_flacecard/index.html new file mode 100644 index 00000000..7fd95ec0 --- /dev/null +++ b/games/math_flacecard/index.html @@ -0,0 +1,43 @@ + + + + + + Math Flashcards Challenge + + + + +
    +

    ๐Ÿง  Math Flashcards Challenge

    + +
    + Score: 0 +
    + +
    +
    +
    + +
    +

    Press START

    +
    + +
    + + + + +
    + +
    +
    + +
    + +
    +
    + + + + \ No newline at end of file diff --git a/games/math_flacecard/script.js b/games/math_flacecard/script.js new file mode 100644 index 00000000..3d4433f0 --- /dev/null +++ b/games/math_flacecard/script.js @@ -0,0 +1,242 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. DOM Elements --- + const problemDisplay = document.getElementById('problem-display'); + const answerButtons = document.querySelectorAll('.answer-button'); + const feedbackMessage = document.getElementById('feedback-message'); + const scoreSpan = document.getElementById('score'); + const startButton = document.getElementById('start-button'); + const timerBar = document.getElementById('timer-bar'); + + // --- 2. Game Variables --- + let score = 0; + let correctAnswer = 0; + let gameActive = false; + + // Timing variables + const TIME_LIMIT_MS = 5000; // 5 seconds per problem + const TIMER_UPDATE_INTERVAL_MS = 50; + let timerInterval = null; + let timeRemaining = TIME_LIMIT_MS; + + // --- 3. UTILITY FUNCTIONS --- + + /** + * Generates a random integer between min (inclusive) and max (inclusive). + */ + function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + /** + * Calculates the result of a simple math operation. + */ + function calculateResult(num1, operator, num2) { + switch (operator) { + case '+': + return num1 + num2; + case '-': + return num1 - num2; + case '*': + return num1 * num2; + default: + return 0; + } + } + + /** + * Generates a unique incorrect answer (distractor) close to the correct answer. + */ + function generateDistractor(correct, existingAnswers) { + let distractor; + do { + // Generate a distractor within a range (e.g., +/- 5 of the answer) + distractor = correct + getRandomInt(-5, 5); + } while (distractor === correct || existingAnswers.includes(distractor) || distractor < 0); + return distractor; + } + + // --- 4. CORE GAME FUNCTIONS --- + + /** + * Starts the game and initializes the first round. + */ + function startGame() { + if (gameActive) return; + + score = 0; + scoreSpan.textContent = score; + gameActive = true; + startButton.textContent = 'RESTART'; + startButton.disabled = true; + feedbackMessage.textContent = 'Solve the problem!'; + + loadProblem(); + } + + /** + * Creates a new problem, generates options, and starts the timer. + */ + function loadProblem() { + // Stop any existing timer + stopTimer(); + + // 1. Generate Problem + const num1 = getRandomInt(1, 20); + const num2 = getRandomInt(1, 15); + const operator = Math.random() < 0.5 ? '+' : '-'; // Only + and - + // Ensure result is not negative for subtraction + const finalNum1 = (operator === '-') ? Math.max(num1, num2) : num1; + const finalNum2 = (operator === '-') ? Math.min(num1, num2) : num2; + + correctAnswer = calculateResult(finalNum1, operator, finalNum2); + + problemDisplay.textContent = `${finalNum1} ${operator} ${finalNum2} = ?`; + + // 2. Generate Options + let options = [correctAnswer]; + while (options.length < 4) { + const distractor = generateDistractor(correctAnswer, options); + options.push(distractor); + } + + // Shuffle the options array + options.sort(() => Math.random() - 0.5); + + // 3. Update Answer Buttons + answerButtons.forEach((button, index) => { + const answer = options[index]; + button.textContent = answer; + button.setAttribute('data-value', answer); + button.classList.remove('correct', 'incorrect'); + button.disabled = false; + // Re-attach listener to prevent multiple triggers + button.removeEventListener('click', handleAnswer); + button.addEventListener('click', handleAnswer); + }); + + // 4. Start Timer + startTimer(); + } + + /** + * Handles the player clicking an answer button. + */ + function handleAnswer(event) { + if (!gameActive) return; + + // Stop timer immediately + stopTimer(); + + const playerGuess = parseInt(event.target.getAttribute('data-value')); + + // Disable all buttons to prevent further clicks + answerButtons.forEach(btn => btn.disabled = true); + + // Check result + if (playerGuess === correctAnswer) { + score++; + scoreSpan.textContent = score; + feedbackMessage.textContent = 'โœ… Correct! Get ready for the next one...'; + event.target.classList.add('correct'); + } else { + feedbackMessage.textContent = `โŒ Incorrect! The answer was ${correctAnswer}.`; + event.target.classList.add('incorrect'); + // Highlight the correct answer + document.querySelector(`[data-value="${correctAnswer}"]`).classList.add('correct'); + } + + // Load next problem after a short delay + setTimeout(loadProblem, 1500); + } + + /** + * Stops the timer and clears the interval. + */ + function stopTimer() { + clearInterval(timerInterval); + timerBar.style.width = '100%'; // Reset bar for next round + } + + /** + * Starts the countdown timer. + */ + function startTimer() { + timeRemaining = TIME_LIMIT_MS; + + timerInterval = setInterval(() => { + timeRemaining -= TIMER_UPDATE_INTERVAL_MS; + + // Update the bar width + const percent = (timeRemaining / TIME_LIMIT_MS) * 100; + timerBar.style.width = `${percent}%`; + + // Change color as time runs out (optional) + if (percent < 30) { + timerBar.style.backgroundColor = '#e74c3c'; // Red + } else if (percent < 60) { + timerBar.style.backgroundColor = '#f39c12'; // Orange + } else { + timerBar.style.backgroundColor = '#2ecc71'; // Green + } + + if (timeRemaining <= 0) { + // Time's up! + stopTimer(); + handleTimeout(); + } + }, TIMER_UPDATE_INTERVAL_MS); + } + + /** + * Executes when the time limit for a problem is reached. + */ + function handleTimeout() { + if (!gameActive) return; + + feedbackMessage.textContent = `โฐ Time's Up! The answer was ${correctAnswer}.`; + feedbackMessage.style.color = '#e74c3c'; + + // Disable all buttons + answerButtons.forEach(btn => btn.disabled = true); + + // Highlight the correct answer + document.querySelector(`[data-value="${correctAnswer}"]`).classList.add('correct'); + + // Load next problem after a delay + setTimeout(loadProblem, 1500); + } + + /** + * Resets the game to the initial state. + */ + function resetGame() { + stopTimer(); + gameActive = false; + score = 0; + scoreSpan.textContent = score; + problemDisplay.textContent = 'Press START'; + feedbackMessage.textContent = 'Ready to test your speed?'; + startButton.textContent = 'START GAME'; + startButton.disabled = false; + + answerButtons.forEach(btn => { + btn.textContent = ''; + btn.classList.remove('correct', 'incorrect'); + btn.disabled = true; + }); + } + + // --- 5. EVENT LISTENERS --- + startButton.addEventListener('click', () => { + // If the game is already active, this acts as a restart + if (startButton.textContent === 'RESTART') { + resetGame(); + startGame(); + } else { + startGame(); + } + }); + + // Initial setup + resetGame(); +}); \ No newline at end of file diff --git a/games/math_flacecard/style.css b/games/math_flacecard/style.css new file mode 100644 index 00000000..eb14768a --- /dev/null +++ b/games/math_flacecard/style.css @@ -0,0 +1,127 @@ +body { + font-family: 'Poppins', sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f0f4f8; + color: #2c3e50; +} + +#game-container { + background-color: white; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); + text-align: center; + max-width: 450px; + width: 90%; +} + +h1 { + color: #3498db; + margin-bottom: 20px; +} + +#status-area { + font-size: 1.2em; + font-weight: 600; + margin-bottom: 20px; +} + +/* --- Timer Bar --- */ +#timer-bar-container { + width: 100%; + height: 15px; + background-color: #ecf0f1; + border-radius: 8px; + margin-bottom: 25px; + overflow: hidden; +} + +#timer-bar { + height: 100%; + width: 100%; /* Starts full */ + background-color: #2ecc71; /* Green */ + transition: width linear; /* Smooth transition */ +} + +/* --- Problem Display --- */ +#problem-box { + background-color: #34495e; /* Dark background */ + color: white; + padding: 20px 10px; + border-radius: 8px; + margin-bottom: 25px; +} + +#problem-display { + font-size: 2.5em; + font-weight: bold; + margin: 0; +} + +/* --- Options Container --- */ +#options-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; + margin-bottom: 25px; +} + +.answer-button { + padding: 15px 10px; + font-size: 1.3em; + font-weight: bold; + background-color: #f1c40f; /* Yellow */ + color: #333; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.1s, transform 0.1s; +} + +.answer-button:hover:not(:disabled) { + background-color: #f39c12; + transform: translateY(-2px); +} + +.answer-button:disabled { + cursor: not-allowed; + opacity: 0.7; +} + +/* Feedback Styles */ +.correct { + background-color: #2ecc71 !important; /* Green */ + color: white; +} + +.incorrect { + background-color: #e74c3c !important; /* Red */ + color: white; +} + +/* --- Controls --- */ +#feedback-message { + min-height: 1.5em; + font-weight: bold; + margin-bottom: 15px; +} + +#start-button { + padding: 12px 25px; + font-size: 1.2em; + font-weight: bold; + background-color: #3498db; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +#start-button:hover { + background-color: #2980b9; +} \ No newline at end of file diff --git a/games/maze-master/index.html b/games/maze-master/index.html new file mode 100644 index 00000000..67f89128 --- /dev/null +++ b/games/maze-master/index.html @@ -0,0 +1,44 @@ + + + + + + Maze Master + + + +
    +

    ๐ŸŒ€ Maze Master

    +

    Find your way out of complex mazes! Use arrow keys to move.

    + +
    +
    Level: 1
    +
    Score: 0
    +
    Time: 00:00
    +
    + + + +
    + + + +
    + +
    + +
    +

    How to Play:

    +
      +
    • Use arrow keys or WASD to move your character
    • +
    • Reach the red exit square to complete the level
    • +
    • Avoid dead ends and find the optimal path
    • +
    • Complete levels faster for higher scores
    • +
    • Use hints if you get stuck (costs points)
    • +
    +
    +
    + + + + \ No newline at end of file diff --git a/games/maze-master/script.js b/games/maze-master/script.js new file mode 100644 index 00000000..71ad2ad5 --- /dev/null +++ b/games/maze-master/script.js @@ -0,0 +1,255 @@ +// Maze Master Game +// Navigate through procedurally generated mazes + +// DOM elements +const canvas = document.getElementById('maze-canvas'); +const ctx = canvas.getContext('2d'); +const levelEl = document.getElementById('current-level'); +const scoreEl = document.getElementById('current-score'); +const timerEl = document.getElementById('time-display'); +const messageEl = document.getElementById('message'); +const startBtn = document.getElementById('start-btn'); +const resetBtn = document.getElementById('reset-btn'); +const hintBtn = document.getElementById('hint-btn'); + +// Game constants +const CELL_SIZE = 20; +const MAZE_SIZE = 25; // 25x25 grid +const CANVAS_SIZE = CELL_SIZE * MAZE_SIZE; + +// Game variables +let maze = []; +let player = { x: 1, y: 1 }; +let exit = { x: MAZE_SIZE - 2, y: MAZE_SIZE - 2 }; +let level = 1; +let score = 0; +let gameRunning = false; +let startTime; +let timerInterval; +let hintUsed = false; + +// Event listeners +startBtn.addEventListener('click', startGame); +resetBtn.addEventListener('click', resetGame); +hintBtn.addEventListener('click', showHint); +document.addEventListener('keydown', handleKeyPress); + +// Initialize the game +function initGame() { + generateMaze(); + player = { x: 1, y: 1 }; + exit = { x: MAZE_SIZE - 2, y: MAZE_SIZE - 2 }; + hintUsed = false; +} + +// Generate a random maze using recursive backtracking +function generateMaze() { + // Initialize maze with walls + maze = Array(MAZE_SIZE).fill().map(() => Array(MAZE_SIZE).fill(1)); + + // Start from a random position + const startX = Math.floor(Math.random() * (MAZE_SIZE / 2)) * 2 + 1; + const startY = Math.floor(Math.random() * (MAZE_SIZE / 2)) * 2 + 1; + + carvePath(startX, startY); + + // Ensure start and exit are clear + maze[1][1] = 0; + maze[MAZE_SIZE - 2][MAZE_SIZE - 2] = 0; +} + +// Recursive function to carve paths +function carvePath(x, y) { + const directions = [[0, -1], [1, 0], [0, 1], [-1, 0]]; // Up, Right, Down, Left + shuffleArray(directions); + + for (let [dx, dy] of directions) { + const newX = x + dx * 2; + const newY = y + dy * 2; + + if (newX > 0 && newX < MAZE_SIZE - 1 && newY > 0 && newY < MAZE_SIZE - 1 && maze[newY][newX] === 1) { + maze[y + dy][x + dx] = 0; + maze[newY][newX] = 0; + carvePath(newX, newY); + } + } +} + +// Shuffle array (Fisher-Yates algorithm) +function shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } +} + +// Start the game +function startGame() { + initGame(); + gameRunning = true; + startTime = Date.now(); + startTimer(); + + startBtn.style.display = 'none'; + resetBtn.style.display = 'inline-block'; + + messageEl.textContent = 'Find the exit!'; + drawMaze(); +} + +// Reset for a new maze +function resetGame() { + gameRunning = false; + clearInterval(timerInterval); + timerEl.textContent = '00:00'; + messageEl.textContent = ''; + startBtn.style.display = 'inline-block'; + resetBtn.style.display = 'none'; + drawMaze(); +} + +// Handle keyboard input +function handleKeyPress(event) { + if (!gameRunning) return; + + let newX = player.x; + let newY = player.y; + + switch (event.key) { + case 'ArrowUp': + case 'w': + case 'W': + newY--; + break; + case 'ArrowDown': + case 's': + case 'S': + newY++; + break; + case 'ArrowLeft': + case 'a': + case 'A': + newX--; + break; + case 'ArrowRight': + case 'd': + case 'D': + newX++; + break; + default: + return; + } + + event.preventDefault(); + + // Check if move is valid + if (newX >= 0 && newX < MAZE_SIZE && newY >= 0 && newY < MAZE_SIZE && maze[newY][newX] === 0) { + player.x = newX; + player.y = newY; + + // Check if reached exit + if (player.x === exit.x && player.y === exit.y) { + levelComplete(); + } + + drawMaze(); + } +} + +// Show hint (reveal part of the path) +function showHint() { + if (!gameRunning || hintUsed) return; + + hintUsed = true; + score = Math.max(0, score - 50); // Penalty for using hint + scoreEl.textContent = score; + + // Simple hint: show direction to exit + const dx = exit.x - player.x; + const dy = exit.y - player.y; + + let hint = 'Try going '; + if (Math.abs(dx) > Math.abs(dy)) { + hint += dx > 0 ? 'right' : 'left'; + } else { + hint += dy > 0 ? 'down' : 'up'; + } + + messageEl.textContent = `Hint: ${hint}`; + setTimeout(() => messageEl.textContent = '', 3000); +} + +// Level completed +function levelComplete() { + gameRunning = false; + clearInterval(timerInterval); + + const timeTaken = Math.floor((Date.now() - startTime) / 1000); + const timeBonus = Math.max(0, 300 - timeTaken); // Bonus for speed + const levelBonus = level * 100; + const hintPenalty = hintUsed ? 50 : 0; + + const levelScore = timeBonus + levelBonus - hintPenalty; + score += levelScore; + + scoreEl.textContent = score; + levelEl.textContent = level; + + messageEl.textContent = `Level ${level} Complete! Score: +${levelScore} (Time: ${timeTaken}s)`; + + level++; + levelEl.textContent = level; + + setTimeout(() => { + messageEl.textContent = 'Get ready for next level...'; + setTimeout(() => { + startGame(); + }, 2000); + }, 3000); +} + +// Start the timer +function startTimer() { + timerInterval = setInterval(() => { + const elapsed = Math.floor((Date.now() - startTime) / 1000); + const minutes = Math.floor(elapsed / 60).toString().padStart(2, '0'); + const seconds = (elapsed % 60).toString().padStart(2, '0'); + timerEl.textContent = `${minutes}:${seconds}`; + }, 1000); +} + +// Draw the maze +function drawMaze() { + ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE); + + // Draw maze + for (let y = 0; y < MAZE_SIZE; y++) { + for (let x = 0; x < MAZE_SIZE; x++) { + if (maze[y][x] === 1) { + ctx.fillStyle = '#34495e'; + ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE); + } else { + ctx.fillStyle = '#ecf0f1'; + ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE); + } + } + } + + // Draw exit + ctx.fillStyle = '#e74c3c'; + ctx.fillRect(exit.x * CELL_SIZE, exit.y * CELL_SIZE, CELL_SIZE, CELL_SIZE); + + // Draw player + ctx.fillStyle = '#27ae60'; + ctx.beginPath(); + ctx.arc(player.x * CELL_SIZE + CELL_SIZE / 2, player.y * CELL_SIZE + CELL_SIZE / 2, CELL_SIZE / 3, 0, Math.PI * 2); + ctx.fill(); +} + +// Initialize on load +initGame(); +drawMaze(); + +// I spent quite a bit of time figuring out the maze generation +// Recursive backtracking works well but can create some weird patterns +// Maybe I'll try a different algorithm later like Prim's or Kruskal's \ No newline at end of file diff --git a/games/maze-master/style.css b/games/maze-master/style.css new file mode 100644 index 00000000..677244cb --- /dev/null +++ b/games/maze-master/style.css @@ -0,0 +1,142 @@ +/* Basic styling for Maze Master */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea, #764ba2); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + color: white; +} + +.container { + text-align: center; + max-width: 700px; + padding: 20px; +} + +h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +p { + font-size: 1.1em; + margin-bottom: 20px; + opacity: 0.9; +} + +.game-stats { + display: flex; + justify-content: space-around; + margin: 20px 0; + font-size: 1.2em; + font-weight: bold; + background: rgba(255, 255, 255, 0.1); + padding: 10px; + border-radius: 10px; +} + +#maze-canvas { + border: 3px solid white; + border-radius: 10px; + background: #1a1a1a; + display: block; + margin: 20px auto; + box-shadow: 0 4px 8px rgba(0,0,0,0.3); +} + +.controls { + margin: 20px 0; +} + +button { + background: #00b894; + color: white; + border: none; + padding: 12px 24px; + font-size: 1em; + border-radius: 5px; + cursor: pointer; + margin: 0 10px; + transition: background 0.3s; +} + +button:hover { + background: #00a085; +} + +#hint-btn { + background: #fdcb6e; + color: #2d3436; +} + +#hint-btn:hover { + background: #e17055; +} + +#message { + font-size: 1.2em; + margin: 20px 0; + min-height: 30px; + font-weight: bold; +} + +.instructions { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + margin-top: 20px; + text-align: left; + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.instructions h3 { + margin-bottom: 10px; + color: #ffeaa7; +} + +.instructions ul { + list-style-type: disc; + padding-left: 20px; +} + +.instructions li { + margin: 5px 0; + line-height: 1.4; +} + +/* Responsive design */ +@media (max-width: 768px) { + #maze-canvas { + width: 100%; + max-width: 400px; + height: 400px; + } + + .game-stats { + flex-direction: column; + gap: 10px; + } + + .controls { + display: flex; + flex-direction: column; + gap: 10px; + } + + button { + width: 100%; + max-width: 200px; + } +} \ No newline at end of file diff --git a/games/maze-runner/index.html b/games/maze-runner/index.html new file mode 100644 index 00000000..1aa61dfb --- /dev/null +++ b/games/maze-runner/index.html @@ -0,0 +1,23 @@ + + + + + + Maze Runner + + + +
    +

    Maze Runner

    + + + +
    +

    Use Arrow Keys to move. Find the green exit!

    + +
    +
    + + + + \ No newline at end of file diff --git a/games/maze-runner/script.js b/games/maze-runner/script.js new file mode 100644 index 00000000..f20b707b --- /dev/null +++ b/games/maze-runner/script.js @@ -0,0 +1,347 @@ +// --- Canvas Setup --- +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); + +// --- DOM Elements --- +const messageEl = document.getElementById('message'); +const newGameBtn = document.getElementById('new-game-btn'); + +// --- Game Constants --- +const CELL_SIZE = 30; // Size of each cell in the maze +const MAZE_WIDTH = canvas.width; +const MAZE_HEIGHT = canvas.height; +const COLS = MAZE_WIDTH / CELL_SIZE; +const ROWS = MAZE_HEIGHT / CELL_SIZE; + +const WALL_COLOR = '#2c3e50'; // Dark blue-gray +const PATH_COLOR = '#ecf0f1'; // Light gray +const PLAYER_COLOR = '#e74c3c'; // Red +const GOAL_COLOR = '#2ecc71'; // Green + +// --- Game State Variables --- +let maze = []; +let player = { x: 0, y: 0, size: CELL_SIZE * 0.6 }; // Player size slightly smaller than cell +let goal = { x: 0, y: 0 }; +let gameActive = false; +let gameLoopId; + +// --- Maze Cell Class --- +/** + * Represents a single cell in the maze. + * @param {number} col - Column index. + * @param {number} row - Row index. + */ +class Cell { + constructor(col, row) { + this.col = col; + this.row = row; + this.visited = false; + // Walls: [top, right, bottom, left] + this.walls = [true, true, true, true]; + } + + /** + * Draws the cell and its walls on the canvas. + */ + draw() { + const x = this.col * CELL_SIZE; + const y = this.row * CELL_SIZE; + + ctx.strokeStyle = WALL_COLOR; + ctx.lineWidth = 2; + + // Draw Top wall + if (this.walls[0]) { + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + CELL_SIZE, y); + ctx.stroke(); + } + // Draw Right wall + if (this.walls[1]) { + ctx.beginPath(); + ctx.moveTo(x + CELL_SIZE, y); + ctx.lineTo(x + CELL_SIZE, y + CELL_SIZE); + ctx.stroke(); + } + // Draw Bottom wall + if (this.walls[2]) { + ctx.beginPath(); + ctx.moveTo(x + CELL_SIZE, y + CELL_SIZE); + ctx.lineTo(x, y + CELL_SIZE); + ctx.stroke(); + } + // Draw Left wall + if (this.walls[3]) { + ctx.beginPath(); + ctx.moveTo(x, y + CELL_SIZE); + ctx.lineTo(x, y); + ctx.stroke(); + } + } +} + +// --- Maze Generation Algorithm (Recursive Backtracking) --- + +/** + * Initializes the maze grid with Cell objects. + */ +function createGrid() { + maze = Array(ROWS).fill(0).map((_, r) => + Array(COLS).fill(0).map((_, c) => new Cell(c, r)) + ); +} + +/** + * Gets a neighbor of the current cell that has not been visited yet. + * @param {Cell} current - The current cell. + * @returns {Cell|undefined} An unvisited neighbor cell, or undefined if none. + */ +function getUnvisitedNeighbor(current) { + const neighbors = []; + const { col, row } = current; + + // Possible neighbors: [neighbor_cell, wall_to_break, neighbor_wall_to_break] + // Wall indices: 0:Top, 1:Right, 2:Bottom, 3:Left + // Top + if (row > 0 && !maze[row - 1][col].visited) { + neighbors.push([maze[row - 1][col], 0, 2]); + } + // Right + if (col < COLS - 1 && !maze[row][col + 1].visited) { + neighbors.push([maze[row][col + 1], 1, 3]); + } + // Bottom + if (row < ROWS - 1 && !maze[row + 1][col].visited) { + neighbors.push([maze[row + 1][col], 2, 0]); + } + // Left + if (col > 0 && !maze[row][col - 1].visited) { + neighbors.push([maze[row][col - 1], 3, 1]); + } + + if (neighbors.length > 0) { + return neighbors[Math.floor(Math.random() * neighbors.length)]; + } + return undefined; +} + +/** + * Generates the maze using a recursive backtracking algorithm. + */ +function generateMaze() { + createGrid(); + const stack = []; + let current = maze[0][0]; // Start at top-left + current.visited = true; + stack.push(current); + + while (stack.length > 0) { + const next = getUnvisitedNeighbor(current); + + if (next) { + const [nextCell, currentWall, nextWall] = next; + stack.push(nextCell); + + // Break walls between current and next cell + current.walls[currentWall] = false; + nextCell.walls[nextWall] = false; + + current = nextCell; + current.visited = true; + } else if (stack.length > 0) { + current = stack.pop(); // Backtrack + } + } + + // Set player start and goal positions + player.x = CELL_SIZE / 2; + player.y = CELL_SIZE / 2; + goal.x = (COLS - 1) * CELL_SIZE + CELL_SIZE / 2; + goal.y = (ROWS - 1) * CELL_SIZE + CELL_SIZE / 2; +} + +// --- Drawing Functions --- + +/** + * Clears the canvas and draws the entire maze. + */ +function drawMaze() { + ctx.clearRect(0, 0, MAZE_WIDTH, MAZE_HEIGHT); + ctx.fillStyle = PATH_COLOR; + ctx.fillRect(0, 0, MAZE_WIDTH, MAZE_HEIGHT); // Fill background as paths + + // Draw all cell walls + for (let r = 0; r < ROWS; r++) { + for (let c = 0; c < COLS; c++) { + maze[r][c].draw(); + } + } +} + +/** + * Draws the player circle. + */ +function drawPlayer() { + ctx.beginPath(); + ctx.arc(player.x, player.y, player.size / 2, 0, Math.PI * 2); + ctx.fillStyle = PLAYER_COLOR; + ctx.fill(); + ctx.closePath(); +} + +/** + * Draws the goal square. + */ +function drawGoal() { + ctx.fillStyle = GOAL_COLOR; + const goalX = goal.x - CELL_SIZE / 2; + const goalY = goal.y - CELL_SIZE / 2; + ctx.fillRect(goalX, goalY, CELL_SIZE, CELL_SIZE); +} + +// --- Player Movement and Collision --- + +/** + * Handles player movement based on arrow key input. + * @param {KeyboardEvent} e - The keyboard event. + */ +function handleKeyDown(e) { + if (!gameActive) return; + + let newPlayerX = player.x; + let newPlayerY = player.y; + const speed = 5; + + switch (e.key) { + case 'ArrowUp': newPlayerY -= speed; break; + case 'ArrowDown': newPlayerY += speed; break; + case 'ArrowLeft': newPlayerX -= speed; break; + case 'ArrowRight': newPlayerX += speed; break; + default: return; // Ignore other keys + } + + e.preventDefault(); // Prevent page scrolling + + // Check for wall collisions + if (!checkWallCollision(newPlayerX, newPlayerY)) { + player.x = newPlayerX; + player.y = newPlayerY; + } +} + +/** + * Checks if the player's potential new position collides with any maze wall. + * This is the most complex part of the collision detection. + * @param {number} nextX - The player's potential new X position. + * @param {number} nextY - The player's potential new Y position. + * @returns {boolean} True if a collision occurs, false otherwise. + */ +function checkWallCollision(nextX, nextY) { + const playerHalfSize = player.size / 2; + + // Calculate the bounding box for the player's potential new position + const playerLeft = nextX - playerHalfSize; + const playerRight = nextX + playerHalfSize; + const playerTop = nextY - playerHalfSize; + const playerBottom = nextY + playerHalfSize; + + // Clamp player within canvas boundaries + if (playerLeft < 0 || playerRight > MAZE_WIDTH || playerTop < 0 || playerBottom > MAZE_HEIGHT) { + return true; + } + + // Determine which cells the player's bounding box overlaps + // We need to check all cells that the player's bounding box touches + const startCol = Math.floor(playerLeft / CELL_SIZE); + const endCol = Math.floor(playerRight / CELL_SIZE); + const startRow = Math.floor(playerTop / CELL_SIZE); + const endRow = Math.floor(playerBottom / CELL_SIZE); + + for (let r = startRow; r <= endRow; r++) { + for (let c = startCol; c <= endCol; c++) { + // Ensure cell indices are within bounds + if (r < 0 || r >= ROWS || c < 0 || c >= COLS) continue; + + const cell = maze[r][c]; + const cellX = c * CELL_SIZE; + const cellY = r * CELL_SIZE; + + // Check walls relative to the current cell + // Top wall + if (cell.walls[0] && playerTop <= cellY && playerBottom > cellY) { + if (playerRight > cellX && playerLeft < cellX + CELL_SIZE) return true; + } + // Right wall + if (cell.walls[1] && playerRight >= cellX + CELL_SIZE && playerLeft < cellX + CELL_SIZE) { + if (playerBottom > cellY && playerTop < cellY + CELL_SIZE) return true; + } + // Bottom wall + if (cell.walls[2] && playerBottom >= cellY + CELL_SIZE && playerTop < cellY + CELL_SIZE) { + if (playerRight > cellX && playerLeft < cellX + CELL_SIZE) return true; + } + // Left wall + if (cell.walls[3] && playerLeft <= cellX && playerRight > cellX) { + if (playerBottom > cellY && playerTop < cellY + CELL_SIZE) return true; + } + } + } + return false; +} + +/** + * Checks if the player has reached the goal. + * @returns {boolean} True if player is at the goal. + */ +function checkGoalCollision() { + const distanceX = player.x - goal.x; + const distanceY = player.y - goal.y; + const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY); + // Check if the center of the player is within a certain radius of the goal center + return distance < (player.size / 2 + CELL_SIZE / 2) * 0.4; +} + + +// --- Game Loop --- + +function gameLoop() { + if (!gameActive) { + cancelAnimationFrame(gameLoopId); + return; + } + + // 1. Draw everything + drawMaze(); + drawGoal(); + drawPlayer(); + + // 2. Check game conditions + if (checkGoalCollision()) { + messageEl.textContent = "You reached the exit! ๐ŸŽ‰"; + messageEl.style.color = GOAL_COLOR; + gameActive = false; + newGameBtn.textContent = "Play Again"; + } + + gameLoopId = requestAnimationFrame(gameLoop); +} + +/** + * Starts a new game by generating a maze and initializing player. + */ +function newGame() { + generateMaze(); + gameActive = true; + messageEl.textContent = "Use Arrow Keys to move. Find the green exit!"; + messageEl.style.color = 'black'; + newGameBtn.textContent = "New Maze"; + cancelAnimationFrame(gameLoopId); // Stop any existing loop + gameLoopId = requestAnimationFrame(gameLoop); // Start new loop +} + +// --- Event Listeners --- +document.addEventListener('keydown', handleKeyDown); +newGameBtn.addEventListener('click', newGame); + +// --- Initial Game Start --- +newGame(); \ No newline at end of file diff --git a/games/maze-runner/style.css b/games/maze-runner/style.css new file mode 100644 index 00000000..5e59a558 --- /dev/null +++ b/games/maze-runner/style.css @@ -0,0 +1,57 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f0f4f8; + color: #333; +} + +#game-container { + text-align: center; + background-color: #fff; + padding: 20px; + border-radius: 10px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); +} + +h1 { + color: #3498db; + margin-bottom: 20px; +} + +#gameCanvas { + background-color: #ecf0f1; /* Light gray background for maze paths */ + border: 2px solid #2c3e50; /* Dark border for the maze */ + display: block; /* Remove extra space below canvas */ + margin: 0 auto 20px auto; +} + +#game-info { + margin-top: 10px; +} + +#message { + font-size: 1.1em; + font-weight: bold; + min-height: 25px; /* Prevent layout shift */ + color: #555; +} + +#new-game-btn { + padding: 10px 20px; + background-color: #2ecc71; /* Green */ + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 1em; + margin-top: 10px; + transition: background-color 0.2s; +} + +#new-game-btn:hover { + background-color: #27ae60; +} \ No newline at end of file diff --git a/games/meme_generator/index.html b/games/meme_generator/index.html new file mode 100644 index 00000000..b921fc7e --- /dev/null +++ b/games/meme_generator/index.html @@ -0,0 +1,22 @@ + + + + + + FUNHUB - Daily Memes + + + + +

    FUNHUB - Your Daily Dose of Memes

    + + +
    +

    Click the button to see a meme!

    + Meme will appear here +

    +
    + + + + diff --git a/games/meme_generator/script.js b/games/meme_generator/script.js new file mode 100644 index 00000000..856bae2c --- /dev/null +++ b/games/meme_generator/script.js @@ -0,0 +1,24 @@ +const newMemeBtn = document.getElementById("newMemeBtn"); +const memeTitle = document.getElementById("meme-title"); +const memeImg = document.getElementById("meme"); +const errorText = document.getElementById("error"); + +async function getMeme() { + errorText.innerText = ""; // Clear previous error + try { + const response = await fetch("https://meme-api.com/gimme"); + if (!response.ok) throw new Error("Network response not ok"); + const data = await response.json(); + memeTitle.innerText = data.title; + memeImg.src = data.url; + } catch (error) { + memeTitle.innerText = "Oops! Couldn't fetch a meme."; + errorText.innerText = error.message; + } +} + +// Button click +newMemeBtn.addEventListener("click", getMeme); + +// Automatically load one meme on page load +window.onload = getMeme; diff --git a/games/meme_generator/style.css b/games/meme_generator/style.css new file mode 100644 index 00000000..f747f03f --- /dev/null +++ b/games/meme_generator/style.css @@ -0,0 +1,54 @@ +body { + font-family: Arial, sans-serif; + text-align: center; + background: #f4f4f4; + padding: 20px; +} + +h1 { + color: #333; +} + +#meme-container { + margin-top: 20px; +} + +#meme-title { + font-size: 1.2em; + margin-bottom: 10px; + font-weight: bold; +} + +#meme { + max-width: 500px; + width: 100%; + border-radius: 10px; + box-shadow: 0px 2px 5px rgba(0,0,0,0.3); + transition: transform 0.3s ease; +} + +#meme:hover { + transform: scale(1.05); +} + +button { + margin-top: 20px; + padding: 10px 20px; + background: #007bff; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 1em; + transition: background 0.3s ease; +} + +button:hover { + background: #0056b3; +} + +#error { + color: red; + margin-top: 10px; + font-size: 0.9em; +} diff --git a/games/memory-blink/index.html b/games/memory-blink/index.html new file mode 100644 index 00000000..80b301d3 --- /dev/null +++ b/games/memory-blink/index.html @@ -0,0 +1,28 @@ + + + + + +Memory Blink | Mini JS Games Hub + + + +
    +

    Memory Blink

    +

    Remember the sequence of glowing tiles and avoid obstacles!

    + +
    + +
    + + + + +
    + +

    Press Start to play.

    +
    + + + + diff --git a/games/memory-blink/script.js b/games/memory-blink/script.js new file mode 100644 index 00000000..a803eb54 --- /dev/null +++ b/games/memory-blink/script.js @@ -0,0 +1,116 @@ +const tileRow = document.getElementById("tile-row"); +const statusEl = document.getElementById("status"); +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const resumeBtn = document.getElementById("resume-btn"); +const restartBtn = document.getElementById("restart-btn"); + +const sequenceLength = 8; +const tilesCount = 12; +let sequence = []; +let userIndex = 0; +let clickable = false; +let interval; +let paused = false; + +// Sounds via online links +const soundCorrect = new Audio("https://www.soundjay.com/button/sounds/button-3.mp3"); +const soundWrong = new Audio("https://www.soundjay.com/button/sounds/button-10.mp3"); +const soundStart = new Audio("https://www.soundjay.com/button/sounds/button-2.mp3"); +const soundWin = new Audio("https://www.soundjay.com/button/sounds/button-4.mp3"); + +// Initialize tiles +let tiles = []; +for (let i = 0; i < tilesCount; i++) { + const tile = document.createElement("div"); + tile.className = "tile"; + tileRow.appendChild(tile); + tiles.push(tile); + + tile.addEventListener("click", () => { + if (!clickable || paused) return; + if (tile.classList.contains("obstacle")) { + statusEl.textContent = "๐Ÿ’ฅ You clicked an obstacle!"; + soundWrong.play(); + endGame(false); + return; + } + + if (tile === sequence[userIndex]) { + tile.classList.add("glow"); + soundCorrect.play(); + userIndex++; + if (userIndex >= sequence.length) { + endGame(true); + } + } else { + soundWrong.play(); + statusEl.textContent = "โŒ Wrong tile!"; + endGame(false); + } + }); +} + +function generateSequence() { + sequence = []; + const possibleTiles = tiles.filter(t => !t.classList.contains("obstacle")); + for (let i = 0; i < sequenceLength; i++) { + const tile = possibleTiles[Math.floor(Math.random() * possibleTiles.length)]; + sequence.push(tile); + } +} + +// Randomly add obstacles +function addObstacles() { + tiles.forEach(t => t.classList.remove("obstacle")); + const obsCount = 2; // number of obstacles + for (let i = 0; i < obsCount; i++) { + const obsTile = tiles[Math.floor(Math.random() * tiles.length)]; + obsTile.classList.add("obstacle"); + } +} + +// Glow sequence display +function displaySequence() { + let i = 0; + clickable = false; + statusEl.textContent = "Watch the sequence..."; + interval = setInterval(() => { + if (paused) return; + tiles.forEach(t => t.classList.remove("glow")); + if (i >= sequence.length) { + clearInterval(interval); + tiles.forEach(t => t.classList.remove("glow")); + clickable = true; + userIndex = 0; + statusEl.textContent = "Your turn! Click the tiles in order."; + return; + } + sequence[i].classList.add("glow"); + i++; + }, 800); +} + +function startGame() { + tiles.forEach(t => t.classList.remove("glow")); + soundStart.play(); + addObstacles(); + generateSequence(); + displaySequence(); +} + +function endGame(win) { + clickable = false; + clearInterval(interval); + if (win) { + statusEl.textContent = "๐ŸŽ‰ You won!"; + soundWin.play(); + } else { + statusEl.textContent += " Game Over!"; + } +} + +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", () => paused = true); +resumeBtn.addEventListener("click", () => paused = false); +restartBtn.addEventListener("click", startGame); diff --git a/games/memory-blink/style.css b/games/memory-blink/style.css new file mode 100644 index 00000000..01ce7473 --- /dev/null +++ b/games/memory-blink/style.css @@ -0,0 +1,55 @@ +body { + font-family: Arial, sans-serif; + background-color: #111; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.memory-container { + text-align: center; + max-width: 900px; +} + +.tile-row { + display: flex; + justify-content: center; + margin: 20px 0; + gap: 10px; +} + +.tile { + width: 60px; + height: 60px; + border-radius: 10px; + background-color: #333; + box-shadow: 0 0 10px #000; + transition: 0.3s; + cursor: pointer; +} + +.tile.glow { + background-color: #00ffea; + box-shadow: 0 0 20px #00ffea, 0 0 40px #00ffea; +} + +.tile.obstacle { + background-color: #ff0040; + box-shadow: 0 0 15px #ff0040, 0 0 30px #ff0040; +} + +.controls button { + padding: 10px 15px; + margin: 5px; + font-size: 16px; + cursor: pointer; + border-radius: 5px; + border: none; +} + +#status { + margin-top: 15px; + font-weight: bold; +} diff --git a/games/memory-match/index.html b/games/memory-match/index.html new file mode 100644 index 00000000..dbd5ad60 --- /dev/null +++ b/games/memory-match/index.html @@ -0,0 +1,29 @@ + + + + + + Memory Match Game + + + +
    +

    Memory Match! ๐Ÿง 

    +
    + Moves: 0 + +
    + +
    +
    + + +
    + + + + \ No newline at end of file diff --git a/games/memory-match/script.js b/games/memory-match/script.js new file mode 100644 index 00000000..47a42f2d --- /dev/null +++ b/games/memory-match/script.js @@ -0,0 +1,186 @@ +// --- Game State Variables --- +const gameBoard = document.getElementById('game-board'); +const movesDisplay = document.getElementById('moves'); +const resetButton = document.getElementById('reset-button'); +const winMessage = document.getElementById('win-message'); +const finalMovesSpan = document.getElementById('final-moves'); +const playAgainButton = document.getElementById('play-again-button'); + +let hasFlippedCard = false; +let lockBoard = false; // Flag to prevent rapid clicking while cards are flipping +let firstCard, secondCard; +let moves = 0; +let matchedPairs = 0; +const TOTAL_PAIRS = 8; // For a 4x4 grid (16 cards) +const CARD_FLIP_DELAY = 1000; // 1000ms delay for unmatched cards + +// The 8 card pairs. Using simple emojis for the card values. +const cardValues = [ + '๐ŸŽ', '๐ŸŒ', '๐Ÿ‡', '๐Ÿ‰', + '๐Ÿ“', '๐Ÿฅ', '๐Ÿฅญ', '๐Ÿ' +]; + + +// --- Core Functions --- + +/** + * Shuffles an array using the Fisher-Yates (Knuth) algorithm. + * @param {Array} array - The array to shuffle. + */ +function shuffle(array) { + let currentIndex = array.length, randomIndex; + + while (currentIndex !== 0) { + // Pick a remaining element. + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex--; + + // And swap it with the current element. + [array[currentIndex], array[randomIndex]] = [ + array[randomIndex], array[currentIndex]]; + } + return array; +} + +/** + * Initializes and shuffles the board. + */ +function initializeBoard() { + // 1. Create the full set of cards (8 pairs = 16 cards) + let cards = [...cardValues, ...cardValues]; + + // 2. Shuffle the array + cards = shuffle(cards); + + // 3. Reset game state + gameBoard.innerHTML = ''; + moves = 0; + matchedPairs = 0; + movesDisplay.textContent = `Moves: ${moves}`; + winMessage.classList.add('hidden'); + + // 4. Create and append card elements to the DOM + cards.forEach((value, index) => { + const card = document.createElement('div'); + card.classList.add('memory-card'); + card.dataset.value = value; + card.dataset.id = index; // Unique ID for each card instance + + // Card Front (The value) + const frontFace = document.createElement('div'); + frontFace.classList.add('front-face'); + frontFace.textContent = value; + + // Card Back (The cover) + const backFace = document.createElement('div'); + backFace.classList.add('back-face'); + backFace.textContent = '?'; // A simple symbol for the back + + card.appendChild(frontFace); + card.appendChild(backFace); + + // Attach the click listener + card.addEventListener('click', flipCard); + + gameBoard.appendChild(card); + }); +} + +/** + * Handles the card flip action on click. + */ +function flipCard() { + // 1. Prevent action if board is locked (waiting for unmatched cards to flip back) + if (lockBoard) return; + + // 2. Prevent clicking the same card twice + if (this === firstCard) return; + + // 3. Flip the card + this.classList.add('flip'); + + if (!hasFlippedCard) { + // FIRST CARD FLIP + hasFlippedCard = true; + firstCard = this; + return; + } + + // SECOND CARD FLIP + secondCard = this; + moves++; + movesDisplay.textContent = `Moves: ${moves}`; + + checkForMatch(); +} + +/** + * Checks if the two flipped cards are a match. + */ +function checkForMatch() { + let isMatch = firstCard.dataset.value === secondCard.dataset.value; + + isMatch ? disableCards() : unflipCards(); +} + +/** + * Cards match: remove event listeners and mark as matched. + */ +function disableCards() { + firstCard.removeEventListener('click', flipCard); + secondCard.removeEventListener('click', flipCard); + + // Optional: Add a visual cue for matched cards + firstCard.classList.add('match'); + secondCard.classList.add('match'); + + matchedPairs++; + + resetBoard(); + + // Check for win condition + if (matchedPairs === TOTAL_PAIRS) { + showWinMessage(); + } +} + +/** + * Cards DO NOT match: flip them back over after a delay. + */ +function unflipCards() { + lockBoard = true; // Lock the board to prevent more flips + + setTimeout(() => { + firstCard.classList.remove('flip'); + secondCard.classList.remove('flip'); + + resetBoard(); + }, CARD_FLIP_DELAY); // Uses the defined delay +} + +/** + * Resets the variables for the next turn. + */ +function resetBoard() { + [hasFlippedCard, lockBoard] = [false, false]; + [firstCard, secondCard] = [null, null]; +} + +/** + * Shows the congratulatory message. + */ +function showWinMessage() { + finalMovesSpan.textContent = moves; + winMessage.classList.remove('hidden'); +} + + +// --- Event Listeners --- + +// Reset/Play Again button initiates a new game +resetButton.addEventListener('click', initializeBoard); +playAgainButton.addEventListener('click', initializeBoard); + + +// --- Initial Game Setup --- +document.addEventListener('DOMContentLoaded', initializeBoard); \ No newline at end of file diff --git a/games/memory-match/style.css b/games/memory-match/style.css new file mode 100644 index 00000000..94b2c52c --- /dev/null +++ b/games/memory-match/style.css @@ -0,0 +1,140 @@ +:root { + --card-size: 100px; + --grid-gap: 10px; + --primary-color: #4A90E2; /* Blue */ + --secondary-color: #F7DC6F; /* Yellow */ +} + +body { + font-family: Arial, sans-serif; + background-color: #f4f4f9; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; +} + +.game-container { + background: white; + padding: 30px; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + text-align: center; +} + +h1 { + color: var(--primary-color); + margin-top: 0; +} + +/* --- Game Stats --- */ + +.stats { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +#reset-button, #play-again-button { + padding: 8px 15px; + font-size: 16px; + cursor: pointer; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 5px; + transition: background-color 0.3s; +} + +#reset-button:hover, #play-again-button:hover { + background-color: #357ABD; +} + +/* --- Game Board --- */ + +.memory-game { + width: calc(4 * var(--card-size) + 3 * var(--grid-gap)); + height: calc(4 * var(--card-size) + 3 * var(--grid-gap)); + display: grid; + grid-template-columns: repeat(4, var(--card-size)); + grid-template-rows: repeat(4, var(--card-size)); + gap: var(--grid-gap); + perspective: 1000px; /* For 3D flip effect */ +} + +.memory-card { + width: var(--card-size); + height: var(--card-size); + position: relative; + transform: scale(1); + transform-style: preserve-3d; + transition: transform 0.5s; + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1); + border-radius: 8px; + cursor: pointer; +} + +/* Card Flipped State */ +.memory-card.flip { + transform: rotateY(180deg); +} + +.front-face, .back-face { + width: 100%; + height: 100%; + position: absolute; + border-radius: 8px; + backface-visibility: hidden; /* Hide the back when it's facing away */ + display: flex; + justify-content: center; + align-items: center; + font-size: 40px; + font-weight: bold; +} + +/* Card Back (Hidden) */ +.back-face { + background: var(--primary-color); + color: white; + border: 3px solid var(--primary-color); +} + +/* Card Front (Revealed) */ +.front-face { + background: var(--secondary-color); + color: #333; + transform: rotateY(180deg); /* Start flipped to be the front */ +} + +/* Matched Card State */ +.memory-card.match { + cursor: default; + opacity: 0.5; +} + +/* --- Win Message --- */ + +.hidden { + display: none !important; +} + +#win-message { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(255, 255, 255, 0.95); + padding: 40px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + z-index: 10; + text-align: center; + border: 5px solid var(--primary-color); +} + +#win-message h2 { + color: var(--primary-color); + margin-top: 0; +} \ No newline at end of file diff --git a/games/merge-lab/index.html b/games/merge-lab/index.html new file mode 100644 index 00000000..600ab467 --- /dev/null +++ b/games/merge-lab/index.html @@ -0,0 +1,21 @@ + + + + + + Merge LAB | Mini JS Games Hub + + + +
    +

    Merge LAB

    +
    + Score: 0 + Moves: 0 +
    +
    + +
    + + + diff --git a/games/merge-lab/script.js b/games/merge-lab/script.js new file mode 100644 index 00000000..95a674cc --- /dev/null +++ b/games/merge-lab/script.js @@ -0,0 +1,115 @@ +const gridSize = 5; +let grid = []; +let score = 0; +let moves = 0; + +const gridEl = document.getElementById("grid"); +const scoreEl = document.getElementById("score"); +const movesEl = document.getElementById("moves"); +const restartBtn = document.getElementById("restart"); + +// Initialize Grid +function initGrid() { + gridEl.innerHTML = ""; + grid = Array.from({ length: gridSize }, () => Array(gridSize).fill(null)); + for (let r = 0; r < gridSize; r++) { + for (let c = 0; c < gridSize; c++) { + const cell = document.createElement("div"); + cell.classList.add("cell"); + cell.dataset.row = r; + cell.dataset.col = c; + cell.addEventListener("click", () => selectCell(r, c)); + gridEl.appendChild(cell); + } + } + spawnRandom(); + spawnRandom(); + updateUI(); +} + +// Spawn a new level-1 element in a random empty cell +function spawnRandom() { + const emptyCells = []; + for (let r = 0; r < gridSize; r++) { + for (let c = 0; c < gridSize; c++) { + if (!grid[r][c]) emptyCells.push({ r, c }); + } + } + if (emptyCells.length === 0) return; + const { r, c } = emptyCells[Math.floor(Math.random() * emptyCells.length)]; + grid[r][c] = 1; // Level 1 element +} + +// Track selected cell for merging +let selectedCell = null; + +function selectCell(r, c) { + const cellLevel = grid[r][c]; + if (!cellLevel) return; + + if (!selectedCell) { + selectedCell = { r, c }; + highlightCell(r, c); + } else { + if (selectedCell.r === r && selectedCell.c === c) { + selectedCell = null; + removeHighlights(); + } else { + mergeCells(selectedCell.r, selectedCell.c, r, c); + selectedCell = null; + removeHighlights(); + } + } +} + +// Highlight selected cell +function highlightCell(r, c) { + removeHighlights(); + const cell = getCellElement(r, c); + cell.style.border = "2px solid #333"; +} + +// Remove highlights +function removeHighlights() { + document.querySelectorAll(".cell").forEach(cell => cell.style.border = "none"); +} + +// Get cell DOM element +function getCellElement(r, c) { + return gridEl.querySelector(`.cell[data-row='${r}'][data-col='${c}']`); +} + +// Merge logic +function mergeCells(r1, c1, r2, c2) { + if (grid[r1][c1] !== grid[r2][c2]) return; // Only merge same level + grid[r2][c2] += 1; + grid[r1][c1] = null; + score += grid[r2][c2] * 10; + moves++; + spawnRandom(); + updateUI(); +} + +// Update UI +function updateUI() { + for (let r = 0; r < gridSize; r++) { + for (let c = 0; c < gridSize; c++) { + const cellEl = getCellElement(r, c); + const level = grid[r][c]; + cellEl.dataset.level = level || ""; + cellEl.textContent = level ? `L${level}` : ""; + } + } + scoreEl.textContent = score; + movesEl.textContent = moves; +} + +// Restart game +restartBtn.addEventListener("click", () => { + score = 0; + moves = 0; + initGrid(); +}); + +// Initialize game on load +initGrid(); diff --git a/games/merge-lab/style.css b/games/merge-lab/style.css new file mode 100644 index 00000000..e6976ee5 --- /dev/null +++ b/games/merge-lab/style.css @@ -0,0 +1,74 @@ +body { + font-family: Arial, sans-serif; + background: #f0f4f8; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.merge-lab-container { + text-align: center; + background: #ffffff; + padding: 20px 30px; + border-radius: 15px; + box-shadow: 0 10px 25px rgba(0,0,0,0.2); +} + +h1 { + margin-bottom: 15px; + color: #333; +} + +.game-info { + display: flex; + justify-content: space-between; + max-width: 300px; + margin: 10px auto; + font-weight: bold; + color: #555; +} + +.grid { + display: grid; + grid-template-columns: repeat(5, 60px); + grid-template-rows: repeat(5, 60px); + gap: 10px; + justify-content: center; + margin: 20px auto; +} + +.cell { + background: #e0e4e8; + border-radius: 10px; + display: flex; + justify-content: center; + align-items: center; + font-weight: bold; + font-size: 18px; + cursor: pointer; + user-select: none; + transition: all 0.2s ease; +} + +.cell[data-level="1"] { background: #ffd6d6; } +.cell[data-level="2"] { background: #ffb3b3; } +.cell[data-level="3"] { background: #ff8080; } +.cell[data-level="4"] { background: #ff4d4d; } +.cell[data-level="5"] { background: #ff1a1a; color: white; } + +#restart { + padding: 8px 20px; + font-size: 16px; + border: none; + border-radius: 8px; + background: #4caf50; + color: white; + cursor: pointer; + transition: background 0.2s ease; +} + +#restart:hover { + background: #45a049; +} diff --git a/games/minesweeper-clone/index.html b/games/minesweeper-clone/index.html new file mode 100644 index 00000000..c4c96d1b --- /dev/null +++ b/games/minesweeper-clone/index.html @@ -0,0 +1,26 @@ + + + + + + Minesweeper Clone + + + + +

    Minesweeper

    + +
    +
    + +
    +

    Mines Left: 10

    + +
    + +
    +
    + + + + \ No newline at end of file diff --git a/games/minesweeper-clone/script.js b/games/minesweeper-clone/script.js new file mode 100644 index 00000000..996b40a6 --- /dev/null +++ b/games/minesweeper-clone/script.js @@ -0,0 +1,119 @@ +document.addEventListener('DOMContentLoaded', () => { + const gameBoard = document.getElementById('game-board'); + const minesLeftDisplay = document.getElementById('mines-left'); + const resetButton = document.getElementById('reset-button'); + const gameMessage = document.getElementById('game-message'); + + // --- Game Configuration --- + const BOARD_SIZE = 10; + const NUM_MINES = 10; + let board = []; + let isGameOver = false; + let cellsUncovered = 0; + + // --- Utility Functions --- + + // 1. Function to initialize and build the game board HTML + function createBoard() { + // Clear previous board + gameBoard.innerHTML = ''; + board = []; + isGameOver = false; + cellsUncovered = 0; + gameMessage.textContent = ''; + + // Set up CSS Grid layout for the board + gameBoard.style.gridTemplateColumns = `repeat(${BOARD_SIZE}, 1fr)`; + gameBoard.style.gridTemplateRows = `repeat(${BOARD_SIZE}, 1fr)`; + + // Initialize the array that holds the game logic + for (let y = 0; y < BOARD_SIZE; y++) { + board[y] = []; + for (let x = 0; x < BOARD_SIZE; x++) { + const cell = document.createElement('div'); + cell.classList.add('cell'); + cell.dataset.x = x; + cell.dataset.y = y; + + // Set initial state for logic + board[y][x] = { + isMine: false, + isFlagged: false, + isUncovered: false, + neighborMines: 0, + element: cell + }; + + // Add click listeners (left-click for uncovering, right-click for flagging) + cell.addEventListener('click', () => handleCellClick(x, y)); + cell.addEventListener('contextmenu', (e) => { + e.preventDefault(); // Prevent default right-click menu + handleCellFlag(x, y); + }); + + gameBoard.appendChild(cell); + } + } + + placeMines(); + calculateNeighborMines(); + updateMinesLeftDisplay(); + } + + // 2. Function to randomly place mines on the board + function placeMines() { + // Implementation for randomly placing NUM_MINES in the board array + // ... (This is where your main logic development starts) + } + + // 3. Function to calculate the number of adjacent mines for each non-mine cell + function calculateNeighborMines() { + // Implementation for looping through all cells and calculating neighborMines + // ... + } + + // 4. Function to handle left-click (uncovering the cell) + function handleCellClick(x, y) { + if (isGameOver) return; + + // Main game logic for uncovering a cell + // ... + + checkWin(); + } + + // 5. Function to handle right-click (flagging the cell) + function handleCellFlag(x, y) { + if (isGameOver) return; + + // Logic for toggling a flag + // ... + + updateMinesLeftDisplay(); + } + + // 6. Function to update the mines left counter + function updateMinesLeftDisplay() { + // Logic to count unflagged mines and update minesLeftDisplay.textContent + // ... + } + + // 7. Function to check if the player has won + function checkWin() { + // Logic to check if all non-mine cells are uncovered + // ... + } + + // 8. Function to handle game loss + function gameOver() { + isGameOver = true; + gameMessage.textContent = "Game Over! You hit a mine."; + // ... (Reveal all mines) + } + + // --- Event Listeners --- + resetButton.addEventListener('click', createBoard); + + // --- Start the game! --- + createBoard(); +}); \ No newline at end of file diff --git a/games/minesweeper-clone/style.css b/games/minesweeper-clone/style.css new file mode 100644 index 00000000..b791788e --- /dev/null +++ b/games/minesweeper-clone/style.css @@ -0,0 +1,71 @@ +body { + font-family: Arial, sans-serif; + display: flex; + flex-direction: column; + align-items: center; + background-color: #333; + color: #eee; + margin: 20px; +} + +.game-container { + padding: 20px; + border: 3px solid #555; + border-radius: 8px; + background-color: #222; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); +} + +h1 { + color: #4CAF50; /* A nice green color */ +} + +#game-board { + display: grid; + /* Grid columns and rows will be set by JavaScript */ + border: 1px solid #777; +} + +.cell { + width: 30px; + height: 30px; + background-color: #aaa; + border: 1px solid #777; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + font-size: 16px; + font-weight: bold; + color: #000; +} + +.cell:hover { + background-color: #bbb; +} + +.uncovered { + background-color: #ddd; + border-color: #ccc; + cursor: default; +} + +/* Specific colors for numbers */ +.num-1 { color: blue; } +.num-2 { color: green; } +.num-3 { color: red; } +.num-4 { color: navy; } +.num-5 { color: maroon; } +.num-6 { color: teal; } +.num-7 { color: black; } +.num-8 { color: gray; } + +.mine { + background-color: red; + font-size: 20px; +} + +.flag { + color: red; + font-size: 18px; +} \ No newline at end of file diff --git a/games/minesweeper-game/index.html b/games/minesweeper-game/index.html new file mode 100644 index 00000000..ae6c69e9 --- /dev/null +++ b/games/minesweeper-game/index.html @@ -0,0 +1,26 @@ + + + + + + Minesweeper ๐Ÿ’ฃ + + + +
    +

    Minesweeper

    +
    + +
    + Mines: -- + + Time: 0 +
    +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/games/minesweeper-game/script.js b/games/minesweeper-game/script.js new file mode 100644 index 00000000..b0cf63fe --- /dev/null +++ b/games/minesweeper-game/script.js @@ -0,0 +1,266 @@ +// --- 1. Game Configuration --- +const GRID_SIZE = 10; // e.g., 9x9 grid +const NUM_MINES = 10; // Number of mines to place + +// --- 2. DOM Elements --- +const gameBoard = document.getElementById('game-board'); +const minesLeftDisplay = document.getElementById('mines-left'); +const gameStatusDisplay = document.getElementById('game-status'); +const timerDisplay = document.getElementById('timer'); +const resetButton = document.getElementById('resetButton'); + +// --- 3. Game State Variables --- +let board = []; // 2D array to store cell objects +let cells = []; // 1D array of DOM cell elements +let minesLocated = 0; // Flags placed on suspected mines +let revealedCount = 0; // Number of non-mine cells revealed +let gameOver = false; +let gameStarted = false; +let timerInterval; +let secondsElapsed = 0; + +// --- 4. Cell Object (Constructor or Class) --- +// We'll create objects for each cell to store its state +class Cell { + constructor(row, col) { + this.row = row; + this.col = col; + this.isMine = false; + this.isRevealed = false; + this.isFlagged = false; + this.mineCount = 0; // Number of adjacent mines + this.element = null; // Reference to the DOM element + } +} + +// --- 5. Game Initialization --- + +function createBoard() { + gameBoard.innerHTML = ''; // Clear existing board + gameBoard.style.gridTemplateColumns = `repeat(${GRID_SIZE}, 1fr)`; // Set grid columns + + board = []; + cells = []; + minesLocated = 0; + revealedCount = 0; + gameOver = false; + gameStarted = false; + secondsElapsed = 0; + clearInterval(timerInterval); // Stop any running timer + + minesLeftDisplay.textContent = `Mines: ${NUM_MINES}`; + gameStatusDisplay.textContent = ''; + timerDisplay.textContent = `Time: 0`; + + // Initialize 2D board array and create DOM elements + for (let r = 0; r < GRID_SIZE; r++) { + board[r] = []; + for (let c = 0; c < GRID_SIZE; c++) { + const cellObj = new Cell(r, c); + board[r][c] = cellObj; + + const cellElement = document.createElement('div'); + cellElement.classList.add('cell'); + cellElement.dataset.row = r; + cellElement.dataset.col = c; + cellElement.addEventListener('click', () => handleClick(r, c)); + cellElement.addEventListener('contextmenu', (e) => handleRightClick(e, r, c)); // Right-click for flagging + + cellObj.element = cellElement; // Store DOM element reference in cell object + gameBoard.appendChild(cellElement); + cells.push(cellElement); // Add to 1D array for easier iteration if needed + } + } +} + +// Places mines randomly on the board +function placeMines(firstClickRow, firstClickCol) { + let minesPlaced = 0; + while (minesPlaced < NUM_MINES) { + const r = Math.floor(Math.random() * GRID_SIZE); + const c = Math.floor(Math.random() * GRID_SIZE); + + // Ensure mine is not placed on the first clicked cell or its immediate neighbors + const isNearFirstClick = (Math.abs(r - firstClickRow) <= 1 && Math.abs(c - firstClickCol) <= 1); + + if (!board[r][c].isMine && !isNearFirstClick) { + board[r][c].isMine = true; + minesPlaced++; + } + } +} + +// Calculates the number of adjacent mines for each non-mine cell +function calculateMineCounts() { + for (let r = 0; r < GRID_SIZE; r++) { + for (let c = 0; c < GRID_SIZE; c++) { + if (!board[r][c].isMine) { + let count = 0; + // Check all 8 neighbors + for (let dr = -1; dr <= 1; dr++) { + for (let dc = -1; dc <= 1; dc++) { + if (dr === 0 && dc === 0) continue; // Skip self + + const nr = r + dr; + const nc = c + dc; + + if (nr >= 0 && nr < GRID_SIZE && nc >= 0 && nc < GRID_SIZE && board[nr][nc].isMine) { + count++; + } + } + } + board[r][c].mineCount = count; + } + } + } +} + +// --- 6. Game Logic Handlers --- + +// Starts the game timer +function startTimer() { + secondsElapsed = 0; + timerDisplay.textContent = `Time: ${secondsElapsed}`; + timerInterval = setInterval(() => { + secondsElapsed++; + timerDisplay.textContent = `Time: ${secondsElapsed}`; + }, 1000); +} + +// Handles a left-click on a cell +function handleClick(row, col) { + if (gameOver || board[row][col].isRevealed || board[row][col].isFlagged) { + return; + } + + // First click logic: place mines and start timer + if (!gameStarted) { + placeMines(row, col); + calculateMineCounts(); + startTimer(); + gameStarted = true; + } + + if (board[row][col].isMine) { + revealAllMines(); + endGame(false); // Player hit a mine + } else { + revealCell(row, col); + checkWinCondition(); + } +} + +// Handles a right-click (contextmenu) on a cell to flag/unflag +function handleRightClick(event, row, col) { + event.preventDefault(); // Prevent browser context menu + if (gameOver || board[row][col].isRevealed) { + return; + } + + const cell = board[row][col]; + if (cell.isFlagged) { + cell.isFlagged = false; + cell.element.classList.remove('flagged'); + minesLocated--; + } else if (minesLocated < NUM_MINES) { // Only allow flagging if mines are left + cell.isFlagged = true; + cell.element.classList.add('flagged'); + minesLocated++; + } + minesLeftDisplay.textContent = `Mines: ${NUM_MINES - minesLocated}`; + checkWinCondition(); +} + +// Recursively reveals empty cells +function revealCell(row, col) { + if (row < 0 || row >= GRID_SIZE || col < 0 || col >= GRID_SIZE || board[row][col].isRevealed || board[row][col].isFlagged) { + return; // Out of bounds or already revealed/flagged + } + + const cell = board[row][col]; + cell.isRevealed = true; + cell.element.classList.add('revealed'); + revealedCount++; + + if (cell.isMine) { // Should not happen if `handleClick` logic is correct, but good for safety + cell.element.classList.add('mine'); + return; + } + + if (cell.mineCount > 0) { + cell.element.textContent = cell.mineCount; + cell.element.classList.add(`num-${cell.mineCount}`); // Add class for color styling + return; + } + + // If it's an empty cell (mineCount === 0), recursively reveal neighbors + for (let dr = -1; dr <= 1; dr++) { + for (let dc = -1; dc <= 1; dc++) { + if (dr === 0 && dc === 0) continue; + revealCell(row + dr, col + dc); // Recursive call + } + } +} + +// Reveals all mine locations when game ends (lose) +function revealAllMines() { + for (let r = 0; r < GRID_SIZE; r++) { + for (let c = 0; c < GRID_SIZE; c++) { + const cell = board[r][c]; + if (cell.isMine && !cell.isFlagged) { + cell.element.classList.add('mine'); + cell.element.textContent = ''; // Clear flag emoji if any + } else if (!cell.isMine && cell.isFlagged) { + cell.element.classList.add('mine-incorrect'); // Mark incorrectly flagged + } else if (cell.isMine && cell.isFlagged) { + cell.element.classList.add('mine-correct'); // Mark correctly flagged + } + } + } +} + +// Checks if the player has won +function checkWinCondition() { + const totalNonMines = (GRID_SIZE * GRID_SIZE) - NUM_MINES; + + // Win condition 1: All non-mine cells are revealed + if (revealedCount === totalNonMines) { + endGame(true); + } + // Win condition 2 (Alternative for advanced Minesweeper): + // All mines are correctly flagged AND all non-mine cells are revealed. + // This current implementation implies non-mines are always revealed by clicking. + // If you want to enable flagging as a primary win condition, this needs adjustment. +} + +// Ends the game (win/lose) +function endGame(win) { + gameOver = true; + clearInterval(timerInterval); // Stop the timer + + if (win) { + gameStatusDisplay.textContent = 'YOU WIN! ๐ŸŽ‰'; + gameStatusDisplay.classList.add('win'); + // Optionally, flag all remaining mines automatically + for (let r = 0; r < GRID_SIZE; r++) { + for (let c = 0; c < GRID_SIZE; c++) { + const cell = board[r][c]; + if (cell.isMine && !cell.isFlagged) { + cell.isFlagged = true; + cell.element.classList.add('flagged'); + cell.element.classList.add('mine-correct'); // Visual feedback for auto-flag + } + } + } + minesLeftDisplay.textContent = `Mines: 0`; // All mines accounted for + } else { + gameStatusDisplay.textContent = 'GAME OVER! ๐Ÿ’€'; + gameStatusDisplay.classList.add('lose'); + } +} + +// --- 7. Event Listeners --- +resetButton.addEventListener('click', createBoard); + +// --- 8. Initialization --- +createBoard(); \ No newline at end of file diff --git a/games/minesweeper-game/style.css b/games/minesweeper-game/style.css new file mode 100644 index 00000000..e69de29b diff --git a/games/minesweeper/index.html b/games/minesweeper/index.html new file mode 100644 index 00000000..882dec2e --- /dev/null +++ b/games/minesweeper/index.html @@ -0,0 +1,28 @@ + + + + + + Minesweeper | Mini JS Games Hub + + + +
    +

    Minesweeper

    +
    + + + + + + + + +
    +

    +
    +
    + + + + diff --git a/games/minesweeper/script.js b/games/minesweeper/script.js new file mode 100644 index 00000000..10433822 --- /dev/null +++ b/games/minesweeper/script.js @@ -0,0 +1,154 @@ +let rows = 10; +let cols = 10; +let minesCount = 15; +let grid = []; +let gameOver = false; + +const gridEl = document.getElementById('grid'); +const messageEl = document.getElementById('message'); +const rowsInput = document.getElementById('rows'); +const colsInput = document.getElementById('cols'); +const minesInput = document.getElementById('mines'); +const startBtn = document.getElementById('start-btn'); +const restartBtn = document.getElementById('restart-btn'); + +startBtn.addEventListener('click', () => { + rows = parseInt(rowsInput.value); + cols = parseInt(colsInput.value); + minesCount = parseInt(minesInput.value); + startGame(); +}); + +restartBtn.addEventListener('click', startGame); + +function startGame() { + grid = []; + gameOver = false; + messageEl.textContent = ''; + gridEl.innerHTML = ''; + gridEl.style.gridTemplateColumns = `repeat(${cols}, 30px)`; + + // Initialize grid + for(let r=0; r revealCell(r,c)); + cell.addEventListener('contextmenu', (e) => { + e.preventDefault(); + flagCell(r,c); + }); + } + } + + // Place mines + let placed = 0; + while(placed < minesCount){ + const r = Math.floor(Math.random() * rows); + const c = Math.floor(Math.random() * cols); + if(!grid[r][c].mine){ + grid[r][c].mine = true; + placed++; + } + } + + // Calculate adjacent numbers + for(let r=0; r=0 && r+i=0 && c+j 0){ + cell.element.textContent = cell.adjacent; + } else { + // Reveal neighbors recursively + for(let i=-1;i<=1;i++){ + for(let j=-1;j<=1;j++){ + const nr = r+i, nc = c+j; + if(nr>=0 && nr=0 && nc + + + + + Mini Tank Battle + + + +
    +

    ๐Ÿ”ฅ Mini Tank Battle ๐Ÿ”ฅ

    + + +
    + + + +
    + + + + +
    + + + + diff --git a/games/mini-tank-battle/script.js b/games/mini-tank-battle/script.js new file mode 100644 index 00000000..bc300033 --- /dev/null +++ b/games/mini-tank-battle/script.js @@ -0,0 +1,176 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +let gameRunning = false; +let paused = false; +let bullets = []; +let enemies = []; +let tank; +let score = 0; + +const shootSound = document.getElementById("shootSound"); +const explosionSound = document.getElementById("explosionSound"); +const bgMusic = document.getElementById("bgMusic"); + +class Tank { + constructor() { + this.x = canvas.width / 2 - 25; + this.y = canvas.height - 60; + this.width = 50; + this.height = 30; + this.speed = 5; + } + draw() { + ctx.fillStyle = "lime"; + ctx.shadowColor = "lime"; + ctx.shadowBlur = 20; + ctx.fillRect(this.x, this.y, this.width, this.height); + ctx.shadowBlur = 0; + } +} + +class Bullet { + constructor(x, y) { + this.x = x; + this.y = y; + this.radius = 5; + this.speedX = 4 * (Math.random() < 0.5 ? -1 : 1); + this.speedY = -6; + } + update() { + this.x += this.speedX; + this.y += this.speedY; + + // Bounce on walls + if (this.x <= 0 || this.x >= canvas.width) { + this.speedX *= -1; + } + } + draw() { + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fillStyle = "cyan"; + ctx.shadowColor = "cyan"; + ctx.shadowBlur = 15; + ctx.fill(); + ctx.shadowBlur = 0; + ctx.closePath(); + } +} + +class Enemy { + constructor(x, y) { + this.x = x; + this.y = y; + this.width = 40; + this.height = 30; + this.alive = true; + } + draw() { + if (this.alive) { + ctx.fillStyle = "red"; + ctx.shadowColor = "red"; + ctx.shadowBlur = 15; + ctx.fillRect(this.x, this.y, this.width, this.height); + ctx.shadowBlur = 0; + } + } +} + +function createEnemies() { + enemies = []; + for (let i = 0; i < 6; i++) { + let x = 80 + i * 120; + let y = 60; + enemies.push(new Enemy(x, y)); + } +} + +function drawScore() { + ctx.fillStyle = "#0ff"; + ctx.font = "20px Poppins"; + ctx.fillText(`Score: ${score}`, 10, 25); +} + +function detectCollisions() { + bullets.forEach((b, bi) => { + enemies.forEach((e, ei) => { + if ( + e.alive && + b.x > e.x && + b.x < e.x + e.width && + b.y > e.y && + b.y < e.y + e.height + ) { + e.alive = false; + bullets.splice(bi, 1); + score += 10; + explosionSound.play(); + } + }); + }); +} + +function gameLoop() { + if (!gameRunning || paused) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + tank.draw(); + bullets.forEach((b) => { + b.update(); + b.draw(); + }); + enemies.forEach((e) => e.draw()); + detectCollisions(); + drawScore(); + + if (enemies.every((e) => !e.alive)) { + ctx.fillStyle = "yellow"; + ctx.font = "30px Poppins"; + ctx.fillText("๐ŸŽ‰ You Won!", canvas.width / 2 - 80, canvas.height / 2); + bgMusic.pause(); + return; + } + + requestAnimationFrame(gameLoop); +} + +function startGame() { + if (!gameRunning) { + tank = new Tank(); + bullets = []; + createEnemies(); + score = 0; + bgMusic.play(); + gameRunning = true; + paused = false; + gameLoop(); + } +} + +function pauseGame() { + paused = !paused; + if (!paused) gameLoop(); +} + +function restartGame() { + bgMusic.pause(); + bgMusic.currentTime = 0; + gameRunning = false; + startGame(); +} + +document.addEventListener("keydown", (e) => { + if (!tank) return; + if (e.key === "ArrowLeft" || e.key === "a") tank.x -= tank.speed; + if (e.key === "ArrowRight" || e.key === "d") tank.x += tank.speed; + if (e.key === " " && gameRunning && !paused) { + bullets.push(new Bullet(tank.x + tank.width / 2, tank.y)); + shootSound.play(); + } +}); + +document.getElementById("startBtn").addEventListener("click", startGame); +document.getElementById("pauseBtn").addEventListener("click", pauseGame); +document.getElementById("restartBtn").addEventListener("click", restartGame); diff --git a/games/mini-tank-battle/style.css b/games/mini-tank-battle/style.css new file mode 100644 index 00000000..7fb942cb --- /dev/null +++ b/games/mini-tank-battle/style.css @@ -0,0 +1,44 @@ +body { + background-color: #000; + color: #fff; + text-align: center; + font-family: "Poppins", sans-serif; + margin: 0; + overflow: hidden; +} + +.glow-title { + font-size: 2.5rem; + text-shadow: 0 0 15px cyan, 0 0 30px lime, 0 0 60px blue; + margin-top: 10px; +} + +canvas { + border: 3px solid #0ff; + border-radius: 10px; + box-shadow: 0 0 25px #0ff; + background: radial-gradient(circle, #001f3f 10%, #000000 90%); +} + +.controls { + margin-top: 15px; +} + +button { + background-color: #111; + color: #0ff; + border: 2px solid #0ff; + border-radius: 10px; + padding: 10px 20px; + margin: 0 5px; + cursor: pointer; + font-size: 16px; + transition: all 0.3s ease; + box-shadow: 0 0 10px #0ff; +} + +button:hover { + background-color: #0ff; + color: #000; + box-shadow: 0 0 25px #0ff, 0 0 50px #00ffff; +} diff --git a/games/mirror-math/index.html b/games/mirror-math/index.html new file mode 100644 index 00000000..5b8fe763 --- /dev/null +++ b/games/mirror-math/index.html @@ -0,0 +1,133 @@ + + + + + + Mirror Math โ€” Mini JS Games Hub + + + + + + + + + + + +
    +
    +
    + ๐Ÿชž +
    +

    Mirror Math

    +

    Decode transformed equations โ€” train visual rotation + arithmetic

    +
    +
    +
    + + + Back to Hub โ†ฉ +
    +
    + +
    + + +
    +
    +
    +
    Score: 0
    +
    Streak: 0
    +
    +
    +
    Practice
    +
    โ€”
    +
    +
    +
    Mode: Practice
    +
    Difficulty: Medium
    +
    +
    + +
    + + +
    + +
    + + + +
    + + + +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    ยฉ Mirror Math โ€” Built for Mini JS Games Hub
    +
    Images from Unsplash ยท Sounds from Google Actions sound library (public)
    +
    +
    + + + + + + + + + + diff --git a/games/mirror-math/script.js b/games/mirror-math/script.js new file mode 100644 index 00000000..3a272454 --- /dev/null +++ b/games/mirror-math/script.js @@ -0,0 +1,440 @@ +/* Mirror Math โ€” main logic */ + +/* ------------------------- + Helpful notes: + - Canvas is used to render the equation text, then the canvas is transformed + for mirrored/rotated views while internal answer checking uses canonical string. + - Sounds are public Google Actions audio files (CORS-friendly). + ------------------------- */ + +(() => { + // DOM + const canvas = document.getElementById('eq-canvas'); + const ctx = canvas.getContext('2d'); + const input = document.getElementById('answer-input'); + const submitBtn = document.getElementById('submit-btn'); + const skipBtn = document.getElementById('skip-btn'); + const pauseBtn = document.getElementById('pause-resume'); + const restartBtn = document.getElementById('restart'); + const messageEl = document.getElementById('message'); + const timerEl = document.getElementById('timer'); + const scoreEl = document.getElementById('score'); + const streakEl = document.getElementById('streak'); + const modeSelect = document.getElementById('mode-select'); + const diffSelect = document.getElementById('difficulty-select'); + const modeBadge = document.getElementById('mode-badge'); + const hudMode = document.getElementById('hud-mode'); + const hudDiff = document.getElementById('hud-diff'); + const audioToggle = document.getElementById('audio-toggle'); + const bulbs = Array.from(document.querySelectorAll('.bulb')); + + // Sounds + const sfxCorrect = document.getElementById('sfx-correct'); + const sfxWrong = document.getElementById('sfx-wrong'); + const sfxTick = document.getElementById('sfx-tick'); + + // State + let currentEq = null; // {displayStr, answer, orientation, rotationDeg} + let paused = false; + let score = 0; + let streak = 0; + let timer = null; + let timerRemaining = 0; + let perEquationTime = 10; // default + let locked = false; // while evaluating + let currentMode = 'practice'; + let currentDiff = 'medium'; + let orientationPref = 'mirrored'; + + // visual constants + const CANVAS_W = canvas.width = 720; + const CANVAS_H = canvas.height = 140; + + // helper: pick random element + const pick = (a) => a[Math.floor(Math.random()*a.length)]; + + // Operators for difficulty + const OPS = { + easy: ['+','-'], + medium: ['+','-','*'], + hard: ['+','-','*','/'] + }; + + // set initial UI + function setUI() { + hudMode.textContent = capitalize(currentMode); + hudDiff.textContent = capitalize(currentDiff); + modeBadge.textContent = capitalize(currentMode); + scoreEl.textContent = score; + streakEl.textContent = streak; + } + + function capitalize(s){ return s.charAt(0).toUpperCase()+s.slice(1) } + + // Generate canonical equation and numeric answer + function generateEquation(difficulty) { + let a,b,op,answer; + const ops = OPS[difficulty]; + op = pick(ops); + + if (difficulty === 'easy') { + a = randInt(1,9); + b = randInt(1,9); + } else if (difficulty === 'medium') { + a = randInt(10,99); + b = randInt(2,20); + } else { // hard + a = randInt(-50,150); + b = randInt(-12,40); + } + + // ensure no division by zero + if(op === '/') { + // make divisible or force decimal rounding to 2 decimals + if (b === 0) b = 2; + answer = +(a / b).toFixed(2); + } else if (op === '*') { + answer = a * b; + } else if (op === '+') { + answer = a + b; + } else { // - + answer = a - b; + } + + // sometimes flip order for subtraction/division to make it trickier + if(op === '-' && Math.random() < 0.3) { + [a,b] = [b,a]; + if (op === '/') answer = +(a / b).toFixed(2); + else if (op === '-') answer = a - b; + } + + const canonical = `${a} ${op} ${b}`; + return { canonical, answer }; + } + + function randInt(min, max){ + return Math.floor(Math.random()*(max-min+1))+min; + } + + // Convert canonical into display string (we will draw this to canvas) + function canonicalToDisplay(canonical, transformType){ + // For our purposes the display string is same as canonical (numbers and ops). + // Visual transform is done at canvas or CSS level. + return canonical; + } + + // draw on canvas centered + function drawEquationOnCanvas(text, rotationDeg=0, mirrored=false) { + // clear + ctx.clearRect(0,0,CANVAS_W,CANVAS_H); + // background gloss + const g = ctx.createLinearGradient(0,0,CANVAS_W,0); + g.addColorStop(0, 'rgba(255,255,255,0.02)'); + g.addColorStop(1, 'rgba(255,255,255,0.01)'); + ctx.fillStyle = g; + roundRect(ctx, 6, 6, CANVAS_W-12, CANVAS_H-12, 12); + ctx.fill(); + + // apply transform for drawing (we'll draw upright text then transform canvas for display) + ctx.save(); + // translate to center + ctx.translate(CANVAS_W/2, CANVAS_H/2); + // rotation is applied around center AFTER mirroring if needed + if (mirrored) { + ctx.scale(-1, 1); // mirror across vertical + } + ctx.rotate(rotationDeg * Math.PI / 180); + + // text style + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 44px Inter, system-ui, sans-serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + // draw drop-shadow + ctx.save(); + ctx.fillStyle = 'rgba(0,0,0,0.5)'; + ctx.translate(4,6); + ctx.fillText(text, 0, 0); + ctx.restore(); + + // glow-filled text + ctx.fillStyle = '#fff'; + ctx.fillText(text, 0, 0); + + ctx.restore(); + + // add subtle outer glow using CSS on canvas element + canvas.style.boxShadow = '0 18px 60px rgba(124,58,237,0.12), inset 0 2px 20px rgba(110,231,183,0.02)'; + } + + // helper to draw rounded rect for background + function roundRect(ctx,x,y,w,h,r){ + if (w < 2 * r) r = w/2; + if (h < 2 * r) r = h/2; + ctx.beginPath(); + ctx.moveTo(x+r,y); + ctx.arcTo(x+w,y,x+w,y+h,r); + ctx.arcTo(x+w,y+h,x,y+h,r); + ctx.arcTo(x,y+h,x,y,r); + ctx.arcTo(x,y,x+w,y,r); + ctx.closePath(); + } + + // Render equation according to orientation / mode + function renderCurrentEquation() { + if (!currentEq) return; + const { displayStr, orientation, rotation } = currentEq; + + // we'll render the canonical text upright on canvas then visually transform via canvas draw transforms + // Using drawEquationOnCanvas which takes rotation & mirrored flag + const mirrored = orientation === 'mirrored' || (orientation === 'mixed' && Math.random() < 0.5 && currentEq._picked === 'mirrored'); + const rotationDeg = rotation || 0; + drawEquationOnCanvas(displayStr, rotationDeg, mirrored); + + // Update HUD message + messageEl.innerHTML = `Decode and solve: ${orientation.toUpperCase()} ${rotationDeg ? `(${rotationDeg}ยฐ)` : ''}`; + } + + // choose orientation and rotation for new eq + function pickOrientation(pref) { + if (pref === 'mixed') { + const choices = ['mirrored','rotated','mirrored','rotated','rotated','mirrored']; // bias + return pick(choices); + } + return pref; + } + + // prepare a fresh equation + function newEquation() { + const gen = generateEquation(currentDiff); + const orientation = pickOrientation(orientationPref === 'mixed'? 'mixed' : orientationPref); + + // rotation degrees for rotated orientation + let rotationDeg = 0; + if (orientation === 'rotated' || orientation === 'mixed') { + // choose 90/180/270 (but if mirrored chosen above rotation may be 0) + const choice = pick([0,90,180,270]); + rotationDeg = choice; + } + + const displayStr = canonicalToDisplay(gen.canonical, orientation); + currentEq = { + canonical: gen.canonical, + answer: gen.answer, + displayStr, + orientation, + rotation: rotationDeg + }; + + // tag for mixed to know actual applied visual effect (helpful for render decisions) + if (orientation === 'mixed') currentEq._picked = Math.random() < 0.5 ? 'mirrored' : 'rotated'; + + renderCurrentEquation(); + } + + // Timer utilities + function startTimer(seconds) { + clearTimer(); + timerRemaining = seconds; + updateTimerDisplay(); + timer = setInterval(() => { + if (paused) return; + timerRemaining -= 1; + updateTimerDisplay(); + if (audioToggle.checked) sfxTick.play(); + if (timerRemaining <= 0) { + clearTimer(); + onTimeout(); + } + }, 1000); + updateBulbs(seconds, timerRemaining); + } + + function updateTimerDisplay() { + timerEl.textContent = timerRemaining > 0 ? `${timerRemaining}s` : 'โ€”'; + updateBulbs(null, timerRemaining); + } + + function clearTimer(){ + if (timer) { clearInterval(timer); timer = null; } + } + + function updateBulbs(total = null, remaining = 0) { + // light bulbs proportionally (6 bulbs) + const totalBulbs = bulbs.length; + let lit = 0; + if (total && total > 0) { + lit = Math.round((remaining / total) * totalBulbs); + } else if (total === null && typeof remaining === 'number') { + // when called to update only remaining after start + // assume perEquationTime is the base + lit = Math.round((remaining / perEquationTime) * totalBulbs); + } else { + lit = 0; + } + bulbs.forEach((b,i) => { + if (i < lit) b.classList.add('on'); else b.classList.remove('on'); + }); + } + + function onTimeout(){ + locked = true; + messageEl.textContent = `Time's up! Answer was: ${currentEq.answer}`; + if (audioToggle.checked) sfxWrong.play(); + streak = 0; + score = Math.max(0, score - 2); + setUI(); + // auto next (short delay) + setTimeout(()=>{ locked=false; prepareNext(); }, 1400); + } + + // check answer + function checkAnswer(inputVal) { + if (locked) return; + let parsed; + // accept numeric with optional decimal + if (/^-?\d+(\.\d+)?$/.test(inputVal.trim())) { + parsed = Number(inputVal); + } else { + // invalid format + messageEl.textContent = 'Enter a numeric answer (e.g. 42 or -3.5).'; + return; + } + + // compare with small tolerance for floats + const correct = Math.abs(parsed - Number(currentEq.answer)) < 0.005; + + locked = true; + if (correct) { + // correct + streak += 1; + score += 10 + Math.max(0, Math.floor((timerRemaining||perEquationTime)/1)); // faster gives bonus + messageEl.textContent = `Correct! +${10} pts`; + if (audioToggle.checked) sfxCorrect.play(); + setUI(); + // slight highlight + canvas.style.boxShadow = '0 20px 80px rgba(124,58,237,0.24), 0 0 40px rgba(110,231,183,0.06)'; + setTimeout(()=>{ canvas.style.boxShadow = ''; }, 600); + setTimeout(()=>{ locked=false; prepareNext(); }, 700); + } else { + // wrong + streak = 0; + score = Math.max(0, score - 4); + messageEl.textContent = `Wrong โ€” answer was ${currentEq.answer}`; + if (audioToggle.checked) sfxWrong.play(); + setUI(); + setTimeout(()=>{ locked=false; prepareNext(); }, 1000); + } + } + + // prepare next equation depending on mode + function prepareNext() { + // clear input + input.value = ''; + // choose new equation + newEquation(); + // set timer depending on mode + if (currentMode === 'practice') { + clearTimer(); + timerEl.textContent = 'โ€”'; + updateBulbs(perEquationTime,0); + } else if (currentMode === 'timed') { + perEquationTime = currentDiff === 'easy' ? 8 : currentDiff === 'medium' ? 12 : 18; + startTimer(perEquationTime); + } else if (currentMode === 'challenge') { + // challenge: mix times and apply stricter times as streak grows + perEquationTime = Math.max(4, (currentDiff === 'hard'? 10:12) - Math.floor(streak/3)); + startTimer(perEquationTime); + } + setUI(); + } + + // UI events + submitBtn.addEventListener('click', () => { + if (!currentEq) return; + const v = input.value.trim(); + if (v === '') { messageEl.textContent = 'Please type an answer.'; return; } + checkAnswer(v); + }); + + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + submitBtn.click(); + } + }); + + skipBtn.addEventListener('click', () => { + if (locked) return; + messageEl.textContent = `Skipped โ€” answer was ${currentEq.answer}`; + streak = 0; + score = Math.max(0, score - 2); + setUI(); + clearTimer(); + setTimeout(()=> prepareNext(), 700); + }); + + pauseBtn.addEventListener('click', () => { + paused = !paused; + pauseBtn.textContent = paused ? 'Resume' : 'Pause'; + messageEl.textContent = paused ? 'Paused' : 'Resumed'; + if (paused) { + // freeze visuals + canvas.style.filter = 'grayscale(60%) blur(0.6px)'; + } else { + canvas.style.filter = ''; + } + }); + + restartBtn.addEventListener('click', () => { + resetGame(); + }); + + modeSelect.addEventListener('change', (e) => { + currentMode = e.target.value; + setUI(); + prepareNext(); + }); + + diffSelect.addEventListener('change', (e) => { + currentDiff = e.target.value; + setUI(); + prepareNext(); + }); + + // orientation radio + Array.from(document.querySelectorAll('input[name="orientation"]')).forEach(r => { + r.addEventListener('change', (e) => { + orientationPref = e.target.value; + prepareNext(); + }); + }); + + // audio toggle handled by checked state + + // reset + function resetGame() { + score = 0; streak = 0; paused = false; locked = false; + pauseBtn.textContent = 'Pause'; + setUI(); + prepareNext(); + } + + // init + function init() { + // initial picks + currentMode = modeSelect.value; + currentDiff = diffSelect.value; + // set some defaults + setUI(); + // create first eq + prepareNext(); + + // small accessibility: focus input + setTimeout(()=> input.focus(), 300); + } + + // Start app + init(); + +})(); diff --git a/games/mirror-math/style.css b/games/mirror-math/style.css new file mode 100644 index 00000000..f2023ba3 --- /dev/null +++ b/games/mirror-math/style.css @@ -0,0 +1,104 @@ +:root{ + --bg:#0f1724; + --card:#0b1220; + --accent:#6ee7b7; + --accent-2:#7c3aed; + --muted:#9aa6b2; + --glass: rgba(255,255,255,0.03); + --glass-2: rgba(255,255,255,0.02); + --pill-bg: rgba(255,255,255,0.03); + --danger:#ff6b6b; + --success:#4ade80; + --radius:14px; + font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; +} + +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + background: linear-gradient(180deg, #071023 0%, #081125 45%, #071328 100%), url('https://images.unsplash.com/photo-1483729558449-99ef09a8c325?auto=format&fit=crop&w=1200&q=60') center/cover no-repeat fixed; + color:#e6eef8; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + padding:20px; + display:flex; + align-items:stretch; + justify-content:center; + gap:16px; +} + +.app{ + width:1100px; + border-radius:18px; + overflow:hidden; + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + box-shadow: 0 10px 40px rgba(2,6,23,0.7); + display:flex; + flex-direction:column; +} + +.topbar{ + display:flex; + justify-content:space-between; + align-items:center; + padding:18px 22px; + border-bottom:1px solid rgba(255,255,255,0.03); + background:linear-gradient(90deg, rgba(255,255,255,0.01), rgba(255,255,255,0.00)); +} +.brand{display:flex;gap:12px;align-items:center} +.brand__icon{font-size:34px; padding:10px; background:linear-gradient(135deg,var(--accent),var(--accent-2)); border-radius:10px; box-shadow:0 6px 20px rgba(124,58,237,0.22), 0 2px 6px rgba(0,0,0,0.6)} +.brand h1{margin:0;font-size:20px} +.muted{color:var(--muted);font-size:12px;margin-top:4px} + +.controls-header{display:flex;gap:8px;align-items:center} +.btn{ + background:transparent;border:1px solid rgba(255,255,255,0.06);padding:8px 12px;border-radius:10px;color:inherit;cursor:pointer;font-weight:600; + transition:transform .12s ease, box-shadow .12s ease; +} +.btn:hover{transform:translateY(-3px); box-shadow:0 8px 30px rgba(0,0,0,0.4)} +.btn--accent{background:linear-gradient(90deg,var(--accent),var(--accent-2)); color:#021; border:none} +.btn--ghost{background:transparent;border:1px dashed rgba(255,255,255,0.04)} + +.main{display:flex; gap:0; align-items:stretch; padding:18px} +.sidebar{ + width:260px; + border-right:1px solid rgba(255,255,255,0.03); + padding-right:18px; + display:flex;flex-direction:column;gap:12px; +} +.panel{background:var(--glass); padding:12px;border-radius:12px} +.panel.small{font-size:13px;color:var(--muted)} +.panel h3{margin:0 0 10px 0;font-size:13px} +.orientation-row{display:flex;flex-direction:column;gap:8px} +.orientation-row label{font-size:13px;color:var(--muted)} + +.play-area{flex:1;padding-left:18px;display:flex;flex-direction:column;gap:12px} +.hud{display:flex;justify-content:space-between;align-items:center;gap:8px} +.hud__left, .hud__right{display:flex;gap:8px;align-items:center} +.pill{background:var(--pill-bg);padding:8px 12px;border-radius:999px;font-size:13px;color:var(--muted)} +.mode-badge{background:linear-gradient(90deg,var(--accent-2),var(--accent));padding:8px 14px;border-radius:999px;font-weight:700;color:#021;box-shadow:0 8px 30px rgba(124,58,237,0.12)} +.timer{font-size:20px;font-weight:800;color:#fff;padding:6px 10px;border-radius:10px;background:rgba(0,0,0,0.25)} + +.stage{display:flex;justify-content:center;align-items:center;margin-top:6px} +#eq-canvas{background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); border-radius:12px; box-shadow: 0 10px 40px rgba(2,6,23,0.6); width:100%; max-width:760px; height:140px} + +.input-row{display:flex;gap:8px;margin-top:12px} +#answer-input{flex:1;padding:12px 14px;border-radius:12px;border:1px solid rgba(255,255,255,0.05);background:transparent;color:inherit;font-size:16px} +#answer-input:focus{outline:none;box-shadow:0 6px 30px rgba(124,58,237,0.12)} +.feedback{min-height:26px;margin-top:8px} +.message{padding:8px 12px;border-radius:10px;background:rgba(255,255,255,0.02);color:var(--muted)} + +.progress-row{margin-top:12px} +.glow-track{display:flex;gap:8px;align-items:center} +.bulb{width:18px;height:18px;border-radius:50%;background:rgba(255,255,255,0.03);box-shadow:0 2px 6px rgba(0,0,0,0.5)} +.bulb.on{background:linear-gradient(90deg,var(--accent),var(--accent-2));box-shadow:0 6px 20px rgba(124,58,237,0.26), 0 0 18px rgba(110,231,183,0.07)} + +/* footer */ +.foot{display:flex;justify-content:space-between;padding:12px 18px;border-top:1px solid rgba(255,255,255,0.02);font-size:12px;color:var(--muted)} +/* responsive */ +@media (max-width:1120px){ + .app{width:100%;margin:8px} + .sidebar{display:none} + #eq-canvas{max-width:100%} +} diff --git a/games/mole whacking/index.html b/games/mole whacking/index.html new file mode 100644 index 00000000..f2338dcd --- /dev/null +++ b/games/mole whacking/index.html @@ -0,0 +1,30 @@ + + + + + + Whack-a-Mole Game + + + +
    +

    Whack-a-Mole ๐Ÿน

    +

    Score: 0

    +

    Time: 30s

    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + diff --git a/games/mole whacking/readme.md b/games/mole whacking/readme.md new file mode 100644 index 00000000..5d58b2e1 --- /dev/null +++ b/games/mole whacking/readme.md @@ -0,0 +1,79 @@ +# Whack-a-Mole ๐Ÿน + +## Game Details +**Name:** Whack-a-Mole +**Description:** +Whack-a-Mole is a fun and addictive browser game where players try to click (whack) moles as they randomly appear from holes in a grid. Each successful click earns points. The game lasts for 30 seconds, and the player aims to achieve the highest score possible before time runs out. + +--- + +## How to Play +1. Open `index.html` in your browser. +2. Click the **Start Game** button to begin. +3. Moles will randomly pop up from different holes in the grid. +4. Click on a mole before it disappears to earn points. +5. Each mole disappears after a short duration if not clicked. +6. The game ends after 30 seconds. +7. Your final score will be displayed in an alert box. +8. Click the **Restart Game** button to play again. + +--- + +## Controls +- **Start Game:** Begins a new game session. +- **Restart Game:** Starts a new game after the previous session ends. +- **Clicking on a Mole:** Increases your score by 1. +- **Clicking on an Empty Hole:** Does nothing. + +--- + +## Features +- **Interactive Grid:** 3x3 grid of holes with randomly appearing moles. +- **Score Tracking:** Each successful click increments your score. +- **Countdown Timer:** Game runs for 30 seconds, with a visible timer. +- **Animations:** Mole pops up with smooth animation. +- **Beginner-Friendly Code:** Easy to understand and modify. +- **Fun and Engaging:** Simple mechanics that are enjoyable for all ages. + +--- + +## Files Included +- [x] `index.html` โ€” Main HTML file with the game structure. +- [x] `style.css` โ€” CSS styling and animations for holes, moles, and buttons. +- [x] `script.js` โ€” JavaScript logic for game functionality, scoring, and timers. + +--- + +## How to Run the Game +1. Download all three files: `index.html`, `style.css`, and `script.js`. +2. Make sure all files are in the **same folder**. +3. Open `index.html` in any modern web browser (Chrome, Firefox, Edge, etc.). +4. Click **Start Game** to begin playing. + +--- + +## Tips & Tricks +- Stay focused on the grid; moles appear and disappear quickly. +- Try to click each mole as fast as possible to maximize your score. +- You can customize the game: + - Change mole pop-up speed by editing `script.js`. + - Modify the grid size in `index.html` and `style.css`. + - Add images or sounds to make the game more engaging. + +--- + +## Future Enhancements +- Add multiple difficulty levels (easy, medium, hard). +- Add a leaderboard to track high scores. +- Include sound effects when moles pop up or are whacked. +- Replace simple colored moles with fun images. +- Add a timer bar to visualize remaining time. + +--- + +## Gameplay Screenshot +*(Optional: Include a screenshot of the game here if available)* + +--- + +Enjoy playing **Whack-a-Mole** and try to beat your high score! ๐ŸŽฏ diff --git a/games/mole whacking/script.js b/games/mole whacking/script.js new file mode 100644 index 00000000..d3a8fa3d --- /dev/null +++ b/games/mole whacking/script.js @@ -0,0 +1,71 @@ +// Select elements +const holes = document.querySelectorAll('.hole'); +const scoreBoard = document.getElementById('score'); +const timeBoard = document.getElementById('time'); +const startBtn = document.getElementById('start-btn'); +const restartBtn = document.getElementById('restart-btn'); + +let score = 0; +let time = 30; +let moleTimer; +let countdownTimer; + +// Function to show a mole randomly +function showMole() { + const randomIndex = Math.floor(Math.random() * holes.length); + const mole = document.createElement('div'); + mole.classList.add('mole'); + holes[randomIndex].appendChild(mole); + + // Remove mole after 800ms if not clicked + setTimeout(() => { + if (holes[randomIndex].contains(mole)) { + holes[randomIndex].removeChild(mole); + } + }, 800); +} + +// Function to start game +function startGame() { + score = 0; + time = 30; + scoreBoard.textContent = score; + timeBoard.textContent = time; + startBtn.style.display = 'none'; + restartBtn.style.display = 'none'; + + // Mole appears every 1s + moleTimer = setInterval(showMole, 1000); + + // Countdown timer + countdownTimer = setInterval(() => { + time--; + timeBoard.textContent = time; + if (time <= 0) { + endGame(); + } + }, 1000); +} + +// Function to end game +function endGame() { + clearInterval(moleTimer); + clearInterval(countdownTimer); + alert(`Time's up! Your final score is ${score}`); + restartBtn.style.display = 'inline-block'; +} + +// Event listener to whack mole +holes.forEach(hole => { + hole.addEventListener('click', (e) => { + if (e.target.classList.contains('mole')) { + score++; + scoreBoard.textContent = score; + e.target.remove(); + } + }); +}); + +// Button listeners +startBtn.addEventListener('click', startGame); +restartBtn.addEventListener('click', startGame); diff --git a/games/mole whacking/style.css b/games/mole whacking/style.css new file mode 100644 index 00000000..ceb793a5 --- /dev/null +++ b/games/mole whacking/style.css @@ -0,0 +1,77 @@ +/* General styling */ +body { + font-family: 'Arial', sans-serif; + text-align: center; + background: #ffebcd; + color: #333; + margin: 0; + padding: 20px; +} + +h1 { + font-size: 2.5em; + margin-bottom: 10px; +} + +/* Score and Timer */ +p { + font-size: 1.2em; +} + +/* Buttons */ +button { + padding: 10px 20px; + margin: 10px; + font-size: 1em; + cursor: pointer; + border: none; + border-radius: 5px; + background-color: #ff6b6b; + color: white; + transition: transform 0.2s; +} + +button:hover { + transform: scale(1.1); +} + +/* Grid styling */ +.grid { + display: grid; + grid-template-columns: repeat(3, 100px); + grid-gap: 15px; + justify-content: center; + margin-top: 20px; +} + +/* Holes */ +.hole { + width: 100px; + height: 100px; + background-color: #654321; + border-radius: 50%; + position: relative; + overflow: hidden; + cursor: pointer; + box-shadow: inset 0 0 10px #000000aa; +} + +/* Mole */ +.mole { + width: 80px; + height: 80px; + background-color: #ffcc00; + border-radius: 50%; + position: absolute; + bottom: -80px; + left: 50%; + transform: translateX(-50%); + animation: pop-up 0.3s forwards; +} + +/* Pop-up animation */ +@keyframes pop-up { + to { + bottom: 10px; + } +} diff --git a/games/moving_imposter/index.html b/games/moving_imposter/index.html new file mode 100644 index 00000000..49ea0740 --- /dev/null +++ b/games/moving_imposter/index.html @@ -0,0 +1,32 @@ + + + + + + Moving Imposter Game + + + + +
    +

    ๐Ÿ”Ž Moving Imposter

    + +
    + Score: 0 | Time Left: --s +
    + +
    +
    + +
    +

    Click the object that moves differently!

    +
    + +
    + +
    +
    + + + + \ No newline at end of file diff --git a/games/moving_imposter/script.js b/games/moving_imposter/script.js new file mode 100644 index 00000000..69685f2f --- /dev/null +++ b/games/moving_imposter/script.js @@ -0,0 +1,199 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. DOM Elements & Constants --- + const playingField = document.getElementById('playing-field'); + const startButton = document.getElementById('start-button'); + const scoreDisplay = document.getElementById('score-display'); + const timerDisplay = document.getElementById('timer-display'); + const feedbackMessage = document.getElementById('feedback-message'); + + const FIELD_SIZE = 500; + const OBJECT_SIZE = 25; + const NUM_OBJECTS = 8; + const TIME_LIMIT_SECONDS = 10; + const INITIAL_SPEED = 0.5; // Base speed in pixels per frame + const IMPOSTER_DIFFERENCE = 0.3; // Imposter moves 0.3px/frame faster/slower + + // --- 2. GAME STATE VARIABLES --- + let objects = []; // Array of object states {id, x, y, vx, vy, isImposter} + let imposterId = null; + let score = 0; + let timeLeft = TIME_LIMIT_SECONDS; + let gameActive = false; + let animationFrameId = null; + let timerInterval = null; + + // --- 3. CORE LOGIC FUNCTIONS --- + + /** + * Generates an array of moving objects and designates one as the imposter. + */ + function initializeObjects() { + objects = []; + playingField.innerHTML = ''; // Clear existing objects + imposterId = Math.floor(Math.random() * NUM_OBJECTS); + + for (let i = 0; i < NUM_OBJECTS; i++) { + const isImposter = (i === imposterId); + + // Randomize starting position + const x = Math.random() * (FIELD_SIZE - OBJECT_SIZE); + const y = Math.random() * (FIELD_SIZE - OBJECT_SIZE); + + // Randomize initial velocity components + let vx = (Math.random() - 0.5) * 2 * INITIAL_SPEED; + let vy = (Math.random() - 0.5) * 2 * INITIAL_SPEED; + + if (isImposter) { + // Apply the subtle difference (e.g., slightly faster speed) + vx *= (1 + IMPOSTER_DIFFERENCE); + vy *= (1 + IMPOSTER_DIFFERENCE); + } + + objects.push({ + id: i, + x: x, + y: y, + vx: vx, + vy: vy, + isImposter: isImposter, + element: createObjectElement(i) + }); + } + } + + /** + * Creates and attaches a DOM element for one moving object. + */ + function createObjectElement(id) { + const element = document.createElement('div'); + element.classList.add('game-object'); + element.setAttribute('data-id', id); + element.addEventListener('click', handleObjectClick); + playingField.appendChild(element); + return element; + } + + /** + * The main animation loop. Updates positions based on velocity and handles boundaries. + */ + function gameLoop() { + if (!gameActive) return; + + objects.forEach(obj => { + // Update position + obj.x += obj.vx; + obj.y += obj.vy; + + // Boundary checks: Reverse velocity if hitting a wall + if (obj.x <= 0 || obj.x >= FIELD_SIZE - OBJECT_SIZE) { + obj.vx *= -1; + // Keep within bounds + obj.x = Math.min(Math.max(0, obj.x), FIELD_SIZE - OBJECT_SIZE); + } + if (obj.y <= 0 || obj.y >= FIELD_SIZE - OBJECT_SIZE) { + obj.vy *= -1; + obj.y = Math.min(Math.max(0, obj.y), FIELD_SIZE - OBJECT_SIZE); + } + + // Apply position to DOM using CSS transform for performance + obj.element.style.transform = `translate(${obj.x}px, ${obj.y}px)`; + }); + + animationFrameId = requestAnimationFrame(gameLoop); + } + + // --- 4. GAME FLOW --- + + /** + * Starts the game timer. + */ + function startTimer() { + timeLeft = TIME_LIMIT_SECONDS; + timerDisplay.textContent = timeLeft; + + timerInterval = setInterval(() => { + timeLeft--; + timerDisplay.textContent = timeLeft; + + if (timeLeft <= 0) { + endGame(false); // Time's up! + } + }, 1000); + } + + /** + * Starts a new game round. + */ + function startRound() { + // Reset state + if (animationFrameId) cancelAnimationFrame(animationFrameId); + if (timerInterval) clearInterval(timerInterval); + gameActive = true; + startButton.disabled = true; + + initializeObjects(); + startTimer(); + + feedbackMessage.textContent = `Find the difference! ${TIME_LIMIT_SECONDS} seconds to click the imposter.`; + + // Start animation loop + animationFrameId = requestAnimationFrame(gameLoop); + } + + /** + * Handles the player's click on any object. + */ + function handleObjectClick(event) { + if (!gameActive) return; + + const clickedId = parseInt(event.target.getAttribute('data-id')); + const clickedObject = objects.find(obj => obj.id === clickedId); + + if (clickedObject.isImposter) { + // Correct guess! + score++; + scoreDisplay.textContent = score; + clickedObject.element.classList.add('imposter', 'correct-guess'); + endGame(true); + } else { + // Incorrect guess! + clickedObject.element.classList.add('wrong-guess'); + feedbackMessage.textContent = 'โŒ Incorrect! Guess again quickly.'; + } + } + + /** + * Stops the game loop and updates scores/status. + */ + function endGame(win) { + gameActive = false; + cancelAnimationFrame(animationFrameId); + clearInterval(timerInterval); + startButton.disabled = false; + startButton.textContent = 'NEXT ROUND'; + + if (win) { + feedbackMessage.innerHTML = `๐ŸŽ‰ **SUCCESS!** Time remaining: ${timeLeft}s.`; + feedbackMessage.style.color = '#2ecc71'; + } else { + feedbackMessage.innerHTML = `โฐ **TIME UP!** The imposter was object #${imposterId}.`; + feedbackMessage.style.color = '#e74c3c'; + // Show the imposter + const imposterElement = objects.find(obj => obj.isImposter).element; + imposterElement.style.backgroundColor = '#f1c40f'; // Highlight the correct one + } + } + + // --- 5. EVENT LISTENERS AND INITIAL SETUP --- + + startButton.addEventListener('click', startRound); + + // Initial setup + initializeObjects(); + renderGrid(); + function renderGrid() { + objects.forEach(obj => { + obj.element.style.transform = `translate(${obj.x}px, ${obj.y}px)`; + }); + } +}); \ No newline at end of file diff --git a/games/moving_imposter/style.css b/games/moving_imposter/style.css new file mode 100644 index 00000000..84f0fc27 --- /dev/null +++ b/games/moving_imposter/style.css @@ -0,0 +1,95 @@ +:root { + --field-size: 500px; + --object-size: 25px; +} + +body { + font-family: 'Verdana', sans-serif; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f0f4f8; + color: #333; +} + +#game-container { + background-color: white; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 600px; + width: 90%; +} + +h1 { + color: #f39c12; /* Orange */ + margin-bottom: 20px; +} + +#status-area { + font-size: 1.2em; + font-weight: bold; + margin-bottom: 15px; +} + +/* --- Playing Field --- */ +#playing-field { + width: var(--field-size); + height: var(--field-size); + margin: 0 auto; + border: 3px solid #ccc; + background-color: #e9ecef; + position: relative; /* Crucial for absolute positioning of objects */ + overflow: hidden; +} + +/* --- Movable Objects --- */ +.game-object { + width: var(--object-size); + height: var(--object-size); + background-color: #3498db; /* Identical Blue circles */ + border-radius: 50%; + position: absolute; + cursor: pointer; + transition: background-color 0.1s; +} + +.game-object:hover { + box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); +} + +/* Highlight the correct answer upon click */ +.imposter.correct-guess { + background-color: #2ecc71; /* Green when clicked */ +} + +.wrong-guess { + background-color: #e74c3c; /* Red when missed */ +} + +/* --- Controls and Feedback --- */ +#feedback-message { + min-height: 20px; + margin-top: 20px; + margin-bottom: 15px; +} + +#start-button { + padding: 12px 25px; + font-size: 1.2em; + font-weight: bold; + background-color: #f39c12; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +#start-button:hover:not(:disabled) { + background-color: #e67e22; +} \ No newline at end of file diff --git a/games/music-bubble-pop/index.html b/games/music-bubble-pop/index.html new file mode 100644 index 00000000..98db9a16 --- /dev/null +++ b/games/music-bubble-pop/index.html @@ -0,0 +1,64 @@ + + + + + + Music Bubble Pop โ€” Mini JS Games Hub + + + + +
    +
    +

    Music Bubble Pop

    +

    Pop the glowing bubbles โ€” each plays a musical note ๐ŸŽต

    +
    + + + + + + + + + + +
    + +
    +
    Score: 0
    +
    Combo: 0
    +
    High: 0
    +
    +
    + +
    + +
    Click or tap the bubbles to pop them โ€” create melodies!
    +
    + +
    +
    + Low note + Mid note + High note +
    +
    Built with WebAudio โ€ข No downloads โ€ข Images from Unsplash
    +
    +
    + + + + diff --git a/games/music-bubble-pop/script.js b/games/music-bubble-pop/script.js new file mode 100644 index 00000000..20afcfd4 --- /dev/null +++ b/games/music-bubble-pop/script.js @@ -0,0 +1,342 @@ +/* Music Bubble Pop โ€” Advanced + - WebAudio generated notes + - spawn tuning, difficulty, pause/play, restart + - score, combo, highscore (localStorage) + - no external sound files required +*/ + +(() => { + const playArea = document.getElementById('playArea'); + const scoreEl = document.getElementById('score'); + const comboEl = document.getElementById('combo'); + const highEl = document.getElementById('highscore'); + const playPauseBtn = document.getElementById('playPauseBtn'); + const restartBtn = document.getElementById('restartBtn'); + const volumeInput = document.getElementById('volume'); + const difficultySel = document.getElementById('difficulty'); + const spawnRateInput = document.getElementById('spawnRate'); + const bgToggle = document.getElementById('bgToggle'); + const HIGHSCORE_KEY = 'musicBubbleHigh'; + + // Audio setup + const AudioContext = window.AudioContext || window.webkitAudioContext; + let audioCtx = null; + let masterGain = null; + let bgOsc = null; + let running = false; + let spawnInterval = null; + + // Gameplay variables + let score = 0, combo = 0, highscore = Number(localStorage.getItem(HIGHSCORE_KEY) || 0); + let bubbleIdCounter = 0; + let lastPopTime = 0; + + highEl.textContent = highscore; + + // Scale: use pentatonic for pleasant sound + const pentatonic = [0, 2, 4, 7, 9]; // relative semitones + const baseFreq = 220; // A3 + + // utilities + function now() { return performance.now(); } + function rand(min,max){ return Math.random()*(max-min)+min; } + function clamp(v,a,b){ return Math.max(a,Math.min(b,v)); } + + // initialize audio context on first user interaction (mobile policy) + function ensureAudio(){ + if (!audioCtx) { + audioCtx = new AudioContext(); + masterGain = audioCtx.createGain(); + masterGain.gain.value = Number(volumeInput.value || 0.8); + masterGain.connect(audioCtx.destination); + } + } + + // play a short pluck note with frequency + function playNote(freq, type='sine', sustain=0.18) { + ensureAudio(); + const t0 = audioCtx.currentTime; + const osc = audioCtx.createOscillator(); + const gain = audioCtx.createGain(); + osc.type = type; + osc.frequency.value = freq; + gain.gain.value = 0.0001; + + osc.connect(gain); + gain.connect(masterGain); + + const attack = 0.001; + const decay = sustain * 0.9; + gain.gain.exponentialRampToValueAtTime(1.0, t0 + attack); + gain.gain.exponentialRampToValueAtTime(0.0001, t0 + attack + decay); + + osc.start(t0); + osc.stop(t0 + attack + decay + 0.02); + } + + // pop effect (white noise short) + function playPop() { + ensureAudio(); + const bufferSize = audioCtx.sampleRate * 0.05; + const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate); + const data = buffer.getChannelData(0); + for (let i=0;inoise.stop(), 80); + } + + // background ambient oscillator + function startBg() { + ensureAudio(); + if (bgOsc) return; + bgOsc = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + bgOsc.type = 'sine'; + bgOsc.frequency.value = 55; + g.gain.value = 0.03; + bgOsc.connect(g); + g.connect(masterGain); + bgOsc.start(); + } + function stopBg(){ + if (bgOsc) { try { bgOsc.stop(); } catch(e){} bgOsc = null; } + } + + // scoring + function setScore(n){ score=n; scoreEl.textContent = score; if (score>highscore){ highscore=score; highEl.textContent=highscore; localStorage.setItem(HIGHSCORE_KEY, highscore); } } + function addScore(n){ setScore(score + n); } + + // bubble creation + function makeBubble() { + if (!running) return; + const rect = playArea.getBoundingClientRect(); + const size = rand(48,110) * (difficultySel.value==='hard' ? 0.85 : difficultySel.value==='easy' ? 1.12 : 1); + const x = rand(8, rect.width - size - 8); + const y = rect.height + 60; // start below view + const id = `bubble-${++bubbleIdCounter}`; + const $b = document.createElement('div'); + $b.className = 'bubble'; + $b.id = id; + // pick band low/mid/high based on vertical position probability + const bandRoll = Math.random(); + const band = bandRoll < 0.36 ? 'low' : bandRoll < 0.72 ? 'mid' : 'high'; + $b.classList.add(band); + $b.style.width = `${size}px`; + $b.style.height = `${size}px`; + // position (absolute) + $b.style.left = `${x}px`; + $b.style.top = `${y}px`; + // label (note name simplified) + const octaveFactor = band==='low' ? 0.5 : band==='mid' ? 1 : 2; + const noteIndex = pentatonic[Math.floor(Math.random()*pentatonic.length)]; + // compute frequency + const semitone = noteIndex + Math.round(rand(-2,4)); + const freq = baseFreq * octaveFactor * Math.pow(2, semitone/12); + $b.dataset.freq = freq; + $b.dataset.band = band; + + const label = document.createElement('span'); + label.className = 'label'; + label.textContent = ''; // aesthetic: no text, can add shapes later + $b.appendChild(label); + + // small ring glow pseudo by box-shadow and ::after manipulated by style + $b.style.setProperty('--size', `${size}px`); + playArea.appendChild($b); + + // floating animation (manual using requestAnimationFrame to allow pause) + const speed = rand(0.35, 1.1) * (band==='high' ? 1.3 : band==='low' ? 0.6 : 0.9); + const horizWave = rand(-20, 20); + const startTime = now(); + $b._tick = function(t){ + const dt = (t - startTime) / 1000; + const ny = y - dt * 60 * speed * (difficultySel.value === 'hard' ? 1.7 : 1); + $b.style.top = ny + 'px'; + const hx = x + Math.sin(dt*1.2) * horizWave; + $b.style.left = hx + 'px'; + // subtle pulse + const pulse = 1 + 0.03 * Math.sin(dt*4 + (x%20)); + $b.style.transform = `scale(${pulse})`; + // remove if above view + if (ny + size < -80) { + $b.remove(); + return false; + } + return true; + }; + + // click/tap handler + $b.addEventListener('pointerdown', (ev) => { + ev.preventDefault(); + // visual pop + $b.style.animation = 'pop 320ms ease forwards'; + // particles + spawnParticles($b, ev.clientX - rect.left, ev.clientY - rect.top, band); + // play note + const t = Number($b.dataset.freq) || freq; + // different oscillator types for variety + playNote(t, band==='low' ? 'sawtooth' : band==='mid' ? 'triangle' : 'sine', 0.14); + // pop sound + playPop(); + // scoring: smaller = more points; combo increases if rapid pops + const sizeNum = parseFloat(size); + const base = Math.round( Math.max(5, (140 - sizeNum) / 2 ) ); + const timeSinceLast = now() - lastPopTime; + lastPopTime = now(); + if (timeSinceLast < 900) { combo++; } else { combo = 1; } + comboEl.textContent = combo; + addScore(base * combo); + // remove element after animation + setTimeout(()=> $b.remove(), 260); + }); + + // attach to animation loop list + activeBubbles.push($b); + } + + // particles small colorful dots + function spawnParticles(bubbleEl, x, y, band) { + const rect = playArea.getBoundingClientRect(); + for (let i=0;i<8;i++){ + const p = document.createElement('div'); + p.className = 'particle'; + const hue = band === 'low' ? rand(0,20) : band==='mid' ? rand(160,210) : rand(260,320); + p.style.background = `hsl(${hue} ${rand(60,80)}% ${rand(50,65)}%)`; + p.style.left = x + 'px'; + p.style.top = y + 'px'; + playArea.appendChild(p); + const vx = rand(-120,120); + const vy = rand(-220,-30); + const life = rand(420,860); + const start = now(); + (function animatePart(){ + const t = now(); + const dt = (t - start); + const progress = dt / life; + if (progress >= 1) { p.remove(); return; } + p.style.transform = `translate(${vx * progress}px, ${vy * progress}px) scale(${1 - progress})`; + p.style.opacity = String(1 - progress); + requestAnimationFrame(animatePart); + })(); + } + } + + // animation main loop + let activeBubbles = []; + let rafId = null; + function loop(t){ + // update bubbles + activeBubbles = activeBubbles.filter(b => { + if (!b.isConnected) return false; + const alive = b._tick(t); + return alive; + }); + rafId = requestAnimationFrame(loop); + } + + // spawn controller + function startSpawning(){ + if (spawnInterval) clearInterval(spawnInterval); + const rate = Number(spawnRateInput.value) || 900; + spawnInterval = setInterval(() => { + // spawn 1-2 bubbles depending on difficulty + const spawnCount = difficultySel.value === 'hard' ? (Math.random()<0.6 ? 2:1) : 1; + for (let i=0;i { + if (!running) { + await startGame(); + } else { + pauseGame(); + } + }); + restartBtn.addEventListener('click', () => { + restartGame(); + }); + volumeInput.addEventListener('input', (e) => { + if (!masterGain) ensureAudio(); + masterGain.gain.value = Number(e.target.value); + }); + difficultySel.addEventListener('change', () => { + // difficulty affects spawn speed & bubble speed; restart for smoother experience + restartGame(); + }); + spawnRateInput.addEventListener('change', () => { + if (running) startSpawning(); + }); + bgToggle.addEventListener('change', () => { + if (bgToggle.checked) startBg(); else stopBg(); + }); + + // start/pause/restart logic + async function startGame(){ + ensureAudio(); + if (audioCtx.state === 'suspended') await audioCtx.resume(); + running = true; + playPauseBtn.textContent = 'Pause'; + // hide hint + const hint = document.getElementById('hint'); if (hint) hint.style.display = 'none'; + startSpawning(); + if (!rafId) rafId = requestAnimationFrame(loop); + } + function pauseGame(){ + running = false; + playPauseBtn.textContent = 'Play'; + stopSpawning(); + if (rafId) { cancelAnimationFrame(rafId); rafId = null; } + } + function restartGame(){ + // remove all bubbles and particles + activeBubbles.forEach(b=>b.remove()); + document.querySelectorAll('.particle').forEach(p=>p.remove()); + activeBubbles = []; + setScore(0); combo=0; comboEl.textContent=0; + lastPopTime = 0; + bubbleIdCounter = 0; + // restart spawning if running + if (running) { stopSpawning(); startSpawning(); } + } + + // keyboard shortcuts + playArea.addEventListener('keydown', (e)=> { + if (e.code === 'Space') { + e.preventDefault(); + playPauseBtn.click(); + } else if (e.key === 'r') { + restartBtn.click(); + } + }); + + // handle focus to ensure keyboard works + playArea.addEventListener('pointerdown', ()=> playArea.focus()); + + // start with "paused" state; user must click to begin to satisfy mobile audio policy + (function init(){ + score = 0; combo = 0; + scoreEl.textContent = score; comboEl.textContent = combo; + highEl.textContent = highscore; + // Visual tips: spawn a few bubbles as preview (non-interactive) but user must click play to generate audio + for (let i=0;i<6;i++){ + setTimeout(() => { + // quick preview small + makeBubble(); + }, i*120); + } + // stop spawning after preview + setTimeout(()=> { activeBubbles.forEach(b=>b.remove()); activeBubbles = []; }, 2200); + })(); + + // Expose restart externally if needed + window.musicBubblePop = { startGame, pauseGame, restartGame, setVolume: v => { volumeInput.value = v; if (masterGain) masterGain.gain.value = v; } }; + +})(); diff --git a/games/music-bubble-pop/style.css b/games/music-bubble-pop/style.css new file mode 100644 index 00000000..412bc4db --- /dev/null +++ b/games/music-bubble-pop/style.css @@ -0,0 +1,82 @@ +:root{ + --bg:#07071a; + --card:#0e0e25; + --accent1: #7afcff; + --accent2: #ff7aa2; + --glass: rgba(255,255,255,0.04); + --glow: 0 12px 40px rgba(122,255,255,0.12), 0 0 80px rgba(122,255,255,0.06); + --ui-font: "Segoe UI", Roboto, system-ui, -apple-system, "Helvetica Neue", Arial; +} + +*{box-sizing:border-box} +html,body{height:100%;margin:0;font-family:var(--ui-font);background: + radial-gradient(1000px 400px at 10% 10%, rgba(122,255,255,0.03), transparent), + radial-gradient(800px 300px at 90% 80%, rgba(255,122,162,0.03), transparent), + var(--bg); color:#e6eef8;} + +.app{max-width:1100px;margin:28px auto;padding:18px;border-radius:12px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.04));box-shadow: 0 6px 30px rgba(0,0,0,0.6);} +.app-header{display:flex;flex-direction:column;gap:10px;padding:10px 12px} +h1{margin:0;font-size:28px;letter-spacing:0.4px} +.subtitle{margin:0;color:#cfe8ff;font-size:13px} +.controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:6px} +.btn{background:linear-gradient(180deg,#1a1a3a,#0f0f24);border:1px solid rgba(255,255,255,0.05);padding:8px 12px;border-radius:8px;color:#e6eef8;cursor:pointer;box-shadow:var(--glow);font-weight:600} +.btn:active{transform:translateY(1px)} +.control-inline{display:inline-flex;align-items:center;gap:8px;color:#bcdcff;font-size:13px} +.control-inline input[type="range"]{width:110px} +.control-inline select{background:var(--glass);padding:6px;border-radius:6px;border:1px solid rgba(255,255,255,0.03);color:#e6eef8} + +.score-row{display:flex;gap:20px;margin-top:10px;color:#dff3ff;padding:6px 2px;font-size:15px} + +.play-area{ + height:560px;border-radius:10px;margin-top:12px;padding:10px;background: + linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.06)); + overflow:hidden;position:relative;border:1px solid rgba(255,255,255,0.03); + box-shadow: 0 10px 40px rgba(0,0,0,0.6) inset; +} + +/* hint text centered */ +.hint{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);color:#9fbfe8;opacity:0.9;font-size:18px;padding:12px 18px;background:linear-gradient(90deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));border-radius:10px} + +/* Bubble styles */ +.bubble{ + position:absolute;border-radius:50%;display:flex;align-items:center;justify-content:center; + cursor:pointer;user-select:none; + box-shadow: 0 8px 28px rgba(0,0,0,0.6), 0 0 30px rgba(255,255,255,0.02) inset; + transition: transform 160ms ease, opacity 220ms ease; + will-change: transform, opacity, filter; + border: 1px solid rgba(255,255,255,0.06); + backdrop-filter: blur(6px); +} + +/* bubble content text */ +.bubble .label{pointer-events:none;font-weight:700;font-size:12px;color:#051022;text-shadow:0 1px 0 rgba(255,255,255,0.25)} + +/* layered glow variations */ +.bubble.low { background: linear-gradient(135deg,#ff6b6b,#ffb86b); box-shadow: 0 12px 40px rgba(255,107,107,0.12), 0 0 40px rgba(255,184,107,0.12);} +.bubble.mid { background: linear-gradient(135deg,#6bd6ff,#6bffb8); box-shadow: 0 12px 40px rgba(107,214,255,0.12), 0 0 40px rgba(107,255,184,0.12);} +.bubble.high { background: linear-gradient(135deg,#c86bff,#ff6bd1); box-shadow: 0 12px 40px rgba(200,107,255,0.12), 0 0 40px rgba(255,107,209,0.12);} + +/* Large glow ring to emphasize */ +.bubble::after{ + content:"";position:absolute;inset:-4px;border-radius:50%;z-index:-1;filter: blur(14px);opacity:0.9; + transition:opacity 220ms ease, transform 260ms ease; +} + +/* pop animation */ +@keyframes pop { + 0% { transform: scale(1); opacity:1} + 60% { transform: scale(1.25); opacity:0.95} + 100% { transform: scale(0.2); opacity:0; } +} + +/* small particle burst (pseudo) */ +.particle { + position:absolute;width:6px;height:6px;border-radius:50%;opacity:0.95;pointer-events:none; + transform:translate(-50%,-50%);filter:blur(0.2px); +} + +/* footer */ +.app-footer{display:flex;justify-content:space-between;align-items:center;margin-top:10px;color:#9fbfe8;font-size:13px;padding:8px} +.legend{display:flex;gap:10px;align-items:center} +.legend .dot{width:14px;height:14px;border-radius:50%;display:inline-block;margin:0 8px 0 6px;box-shadow:0 8px 28px rgba(0,0,0,0.6)} +.credit{opacity:0.7} diff --git a/games/music-composer/index.html b/games/music-composer/index.html new file mode 100644 index 00000000..3ab566e7 --- /dev/null +++ b/games/music-composer/index.html @@ -0,0 +1,181 @@ + + + + + + Music Composer - Mini JS Games Hub + + + + +
    +
    +

    ๐ŸŽต Music Composer

    +

    Create and compose your own musical masterpieces!

    +
    + +
    + +
    +
    +

    ๐ŸŽผ Composition

    +
    + + + + +
    +
    + +
    +

    ๐ŸŽน Instruments

    +
    + +
    +
    + +
    +

    โฑ๏ธ Tempo

    +
    + + 120 BPM +
    +
    + +
    +

    ๐Ÿ’พ Save & Load

    +
    + + + +
    +
    + +
    +

    ๐Ÿ“Š Stats

    +
    +
    + Notes: + 0 +
    +
    + Duration: + 0s +
    +
    + Status: + Ready +
    +
    +
    +
    + + +
    +
    + + Octave 4 + +
    + +
    + +
    C
    +
    D
    +
    E
    +
    F
    +
    G
    +
    A
    +
    B
    + + +
    C#
    +
    D#
    +
    F#
    +
    G#
    +
    A#
    +
    +
    + + +
    +

    ๐ŸŽผ Your Composition

    +
    +
    + ๐ŸŽต Click "Record" and play some notes to start composing! +
    +
    +
    + + +
    +
    + + +
    +

    ๐ŸŽต Preset Songs

    +
    + + + + + + +
    +
    +
    + + +
    +

    How to Use Music Composer

    +
      +
    • Play: Click piano keys to hear notes instantly
    • +
    • Record: Start recording, then play notes to create a sequence
    • +
    • Playback: Play back your recorded composition
    • +
    • Instruments: Choose different instruments for variety
    • +
    • Tempo: Adjust playback speed (60-200 BPM)
    • +
    • Octaves: Change octave range with arrow buttons
    • +
    • Save/Load: Save compositions as JSON files
    • +
    • Presets: Try pre-made songs for inspiration
    • +
    • Loop: Continuously play your composition
    • +
    +
    + + +
    +
    + + + + \ No newline at end of file diff --git a/games/music-composer/script.js b/games/music-composer/script.js new file mode 100644 index 00000000..7b27c809 --- /dev/null +++ b/games/music-composer/script.js @@ -0,0 +1,658 @@ +// Music Composer Game +// Compose simple tunes with piano keyboard and various instruments + +// DOM elements +const playBtn = document.getElementById('play-btn'); +const recordBtn = document.getElementById('record-btn'); +const stopBtn = document.getElementById('stop-btn'); +const clearBtn = document.getElementById('clear-btn'); +const saveBtn = document.getElementById('save-btn'); +const loadBtn = document.getElementById('load-btn'); +const fileInput = document.getElementById('file-input'); +const loopBtn = document.getElementById('loop-btn'); +const exportBtn = document.getElementById('export-btn'); + +const instrumentSelect = document.getElementById('instrument-select'); +const tempoSlider = document.getElementById('tempo-slider'); +const tempoValue = document.getElementById('tempo-value'); + +const octaveUpBtn = document.getElementById('octave-up'); +const octaveDownBtn = document.getElementById('octave-down'); +const currentOctaveEl = document.getElementById('current-octave'); + +const noteSequenceEl = document.getElementById('note-sequence'); +const noteCountEl = document.getElementById('note-count'); +const compositionDurationEl = document.getElementById('composition-duration'); +const recordingStatusEl = document.getElementById('recording-status'); + +const presetBtns = document.querySelectorAll('.preset-btn'); +const whiteKeys = document.querySelectorAll('.white-key'); +const blackKeys = document.querySelectorAll('.black-key'); + +const messageEl = document.getElementById('message'); + +// Audio context and synthesizer +let audioContext = null; +let currentInstrument = 'piano'; +let currentOctave = 4; +let tempo = 120; // BPM +let isRecording = false; +let isPlaying = false; +let isLooping = false; +let composition = []; +let playbackIndex = 0; +let playbackTimeout = null; + +// Note frequencies (A4 = 440Hz) +const noteFrequencies = { + 'C': 261.63, 'C#': 277.18, 'D': 293.66, 'D#': 311.13, + 'E': 329.63, 'F': 349.23, 'F#': 369.99, 'G': 392.00, + 'G#': 415.30, 'A': 440.00, 'A#': 466.16, 'B': 493.88 +}; + +// Instrument settings +const instruments = { + piano: { + oscillator: 'triangle', + attack: 0.01, + decay: 0.1, + sustain: 0.8, + release: 0.3, + filter: { frequency: 2000, Q: 1 } + }, + guitar: { + oscillator: 'sawtooth', + attack: 0.02, + decay: 0.2, + sustain: 0.6, + release: 0.5, + filter: { frequency: 1500, Q: 2 } + }, + drums: { + oscillator: 'square', + attack: 0.001, + decay: 0.05, + sustain: 0.1, + release: 0.1, + filter: { frequency: 800, Q: 0.5 } + }, + flute: { + oscillator: 'sine', + attack: 0.1, + decay: 0.3, + sustain: 0.7, + release: 0.8, + filter: { frequency: 2500, Q: 1 } + }, + bell: { + oscillator: 'sine', + attack: 0.001, + decay: 0.5, + sustain: 0.2, + release: 2.0, + filter: { frequency: 3000, Q: 3 } + }, + organ: { + oscillator: 'square', + attack: 0.05, + decay: 0.1, + sustain: 0.9, + release: 0.2, + filter: { frequency: 1200, Q: 0.8 } + } +}; + +// Preset songs +const presetSongs = { + twinkle: [ + { note: 'C4', duration: 0.5 }, { note: 'C4', duration: 0.5 }, + { note: 'G4', duration: 0.5 }, { note: 'G4', duration: 0.5 }, + { note: 'A4', duration: 0.5 }, { note: 'A4', duration: 0.5 }, + { note: 'G4', duration: 1.0 }, + { note: 'F4', duration: 0.5 }, { note: 'F4', duration: 0.5 }, + { note: 'E4', duration: 0.5 }, { note: 'E4', duration: 0.5 }, + { note: 'D4', duration: 0.5 }, { note: 'D4', duration: 0.5 }, + { note: 'C4', duration: 1.0 } + ], + 'happy': [ + { note: 'C4', duration: 0.25 }, { note: 'C4', duration: 0.25 }, + { note: 'D4', duration: 0.5 }, { note: 'C4', duration: 0.5 }, + { note: 'F4', duration: 0.5 }, { note: 'E4', duration: 1.0 }, + { note: 'C4', duration: 0.25 }, { note: 'C4', duration: 0.25 }, + { note: 'D4', duration: 0.5 }, { note: 'C4', duration: 0.5 }, + { note: 'G4', duration: 0.5 }, { note: 'F4', duration: 1.0 } + ], + 'fur-elise': [ + { note: 'E5', duration: 0.25 }, { note: 'D#5', duration: 0.25 }, + { note: 'E5', duration: 0.25 }, { note: 'D#5', duration: 0.25 }, + { note: 'E5', duration: 0.25 }, { note: 'B4', duration: 0.25 }, + { note: 'D5', duration: 0.25 }, { note: 'C5', duration: 0.25 }, + { note: 'A4', duration: 0.5 } + ], + 'ode-joy': [ + { note: 'E4', duration: 0.5 }, { note: 'E4', duration: 0.5 }, + { note: 'F4', duration: 0.5 }, { note: 'G4', duration: 0.5 }, + { note: 'G4', duration: 0.5 }, { note: 'F4', duration: 0.5 }, + { note: 'E4', duration: 0.5 }, { note: 'D4', duration: 0.5 }, + { note: 'C4', duration: 0.5 }, { note: 'C4', duration: 0.5 }, + { note: 'D4', duration: 0.5 }, { note: 'E4', duration: 0.5 }, + { note: 'E4', duration: 0.75 }, { note: 'D4', duration: 0.25 }, + { note: 'D4', duration: 1.0 } + ], + 'greensleeves': [ + { note: 'A4', duration: 0.5 }, { note: 'C5', duration: 0.5 }, + { note: 'D5', duration: 0.5 }, { note: 'E5', duration: 0.5 }, + { note: 'D5', duration: 0.5 }, { note: 'C5', duration: 0.5 }, + { note: 'A4', duration: 0.5 }, { note: 'A4', duration: 0.5 }, + { note: 'B4', duration: 0.5 }, { note: 'C5', duration: 0.5 }, + { note: 'D5', duration: 0.5 }, { note: 'C5', duration: 0.5 }, + { note: 'B4', duration: 0.5 }, { note: 'A4', duration: 0.5 } + ], + 'random': [] +}; + +// Initialize the game +function initGame() { + setupAudioContext(); + setupEventListeners(); + updateDisplay(); + showMessage('Welcome to Music Composer! Start by selecting an instrument and playing some notes.', 'success'); +} + +// Setup Web Audio API +function setupAudioContext() { + try { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } catch (e) { + showMessage('Web Audio API not supported in this browser.', 'error'); + } +} + +// Setup event listeners +function setupEventListeners() { + // Control buttons + playBtn.addEventListener('click', togglePlayback); + recordBtn.addEventListener('click', toggleRecording); + stopBtn.addEventListener('click', stopAll); + clearBtn.addEventListener('click', clearComposition); + saveBtn.addEventListener('click', saveComposition); + loadBtn.addEventListener('click', () => fileInput.click()); + loopBtn.addEventListener('click', toggleLoop); + exportBtn.addEventListener('click', exportComposition); + + // File input + fileInput.addEventListener('change', loadComposition); + + // Instrument and tempo + instrumentSelect.addEventListener('change', changeInstrument); + tempoSlider.addEventListener('input', updateTempo); + + // Octave controls + octaveUpBtn.addEventListener('click', () => changeOctave(1)); + octaveDownBtn.addEventListener('click', () => changeOctave(-1)); + + // Piano keys + whiteKeys.forEach(key => { + key.addEventListener('mousedown', () => playNote(key.dataset.note)); + key.addEventListener('mouseup', stopNote); + key.addEventListener('mouseout', stopNote); + }); + + blackKeys.forEach(key => { + key.addEventListener('mousedown', () => playNote(key.dataset.note)); + key.addEventListener('mouseup', stopNote); + key.addEventListener('mouseout', stopNote); + }); + + // Preset songs + presetBtns.forEach(btn => { + btn.addEventListener('click', () => loadPreset(btn.dataset.song)); + }); + + // Keyboard shortcuts + document.addEventListener('keydown', handleKeyDown); + document.addEventListener('keyup', handleKeyUp); +} + +// Play a note +function playNote(noteName) { + if (!audioContext) return; + + const fullNote = noteName + currentOctave; + const frequency = getNoteFrequency(fullNote); + + if (frequency) { + playTone(frequency, currentInstrument); + + // Visual feedback + const key = document.querySelector(`[data-note="${noteName}"]`); + if (key) { + key.classList.add('active'); + } + + // Record if recording is active + if (isRecording) { + recordNote(fullNote); + } + } +} + +// Stop note (visual feedback) +function stopNote(e) { + const key = e.target; + key.classList.remove('active'); +} + +// Get note frequency +function getNoteFrequency(note) { + const match = note.match(/^([A-G]#?)(\d+)$/); + if (!match) return null; + + const noteName = match[1]; + const octave = parseInt(match[2]); + + if (!(noteName in noteFrequencies)) return null; + + const baseFreq = noteFrequencies[noteName]; + const octaveOffset = octave - 4; // A4 is our reference + + return baseFreq * Math.pow(2, octaveOffset); +} + +// Play tone using Web Audio API +function playTone(frequency, instrument = currentInstrument) { + const instrumentSettings = instruments[instrument]; + + // Create oscillator + const oscillator = audioContext.createOscillator(); + oscillator.type = instrumentSettings.oscillator; + oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime); + + // Create gain node for envelope + const gainNode = audioContext.createGain(); + gainNode.gain.setValueAtTime(0, audioContext.currentTime); + + // Envelope + const now = audioContext.currentTime; + const attackTime = now + instrumentSettings.attack; + const decayTime = attackTime + instrumentSettings.decay; + const sustainTime = decayTime + 0.1; // Short sustain period + const releaseTime = sustainTime + instrumentSettings.release; + + gainNode.gain.linearRampToValueAtTime(0.3, attackTime); // Attack + gainNode.gain.linearRampToValueAtTime(instrumentSettings.sustain * 0.3, decayTime); // Decay + gainNode.gain.setValueAtTime(instrumentSettings.sustain * 0.3, sustainTime); // Sustain + gainNode.gain.linearRampToValueAtTime(0, releaseTime); // Release + + // Create filter + const filter = audioContext.createBiquadFilter(); + filter.type = 'lowpass'; + filter.frequency.setValueAtTime(instrumentSettings.filter.frequency, audioContext.currentTime); + filter.Q.setValueAtTime(instrumentSettings.filter.Q, audioContext.currentTime); + + // Connect nodes + oscillator.connect(filter); + filter.connect(gainNode); + gainNode.connect(audioContext.destination); + + // Start and stop + oscillator.start(now); + oscillator.stop(releaseTime); +} + +// Record note +function recordNote(note) { + const timestamp = Date.now(); + composition.push({ + note: note, + duration: 0.5, // Default duration, can be adjusted later + timestamp: timestamp + }); + + updateCompositionDisplay(); +} + +// Toggle recording +function toggleRecording() { + if (isPlaying) { + stopPlayback(); + } + + isRecording = !isRecording; + + if (isRecording) { + composition = []; + recordBtn.classList.add('active'); + recordingStatusEl.textContent = 'Recording...'; + showMessage('Recording started! Play some notes.', 'info'); + } else { + recordBtn.classList.remove('active'); + recordingStatusEl.textContent = 'Ready'; + showMessage(`Recording complete! ${composition.length} notes recorded.`, 'success'); + } + + updateDisplay(); +} + +// Toggle playback +function togglePlayback() { + if (isRecording) { + toggleRecording(); + } + + if (isPlaying) { + stopPlayback(); + } else { + startPlayback(); + } +} + +// Start playback +function startPlayback() { + if (composition.length === 0) { + showMessage('No composition to play! Record some notes first.', 'warning'); + return; + } + + isPlaying = true; + playbackIndex = 0; + playBtn.classList.add('active'); + playBtn.querySelector('.btn-text').textContent = 'Stop'; + + playNextNote(); + showMessage('Playback started!', 'success'); +} + +// Play next note in sequence +function playNextNote() { + if (!isPlaying || playbackIndex >= composition.length) { + if (isLooping) { + playbackIndex = 0; + playNextNote(); + } else { + stopPlayback(); + } + return; + } + + const note = composition[playbackIndex]; + const frequency = getNoteFrequency(note.note); + + if (frequency) { + playTone(frequency, currentInstrument); + highlightNote(playbackIndex); + } + + // Calculate delay to next note based on tempo + const noteDuration = (60 / tempo) * note.duration * 1000; // Convert to milliseconds + playbackTimeout = setTimeout(() => { + playbackIndex++; + playNextNote(); + }, noteDuration); +} + +// Stop playback +function stopPlayback() { + isPlaying = false; + playBtn.classList.remove('active'); + playBtn.querySelector('.btn-text').textContent = 'Play'; + + if (playbackTimeout) { + clearTimeout(playbackTimeout); + playbackTimeout = null; + } + + clearNoteHighlights(); + showMessage('Playback stopped.', 'info'); +} + +// Stop all activities +function stopAll() { + if (isRecording) toggleRecording(); + if (isPlaying) stopPlayback(); +} + +// Clear composition +function clearComposition() { + if (composition.length === 0) return; + + if (confirm('Are you sure you want to clear the composition?')) { + composition = []; + updateCompositionDisplay(); + updateDisplay(); + showMessage('Composition cleared.', 'info'); + } +} + +// Toggle loop +function toggleLoop() { + isLooping = !isLooping; + loopBtn.classList.toggle('active'); + showMessage(isLooping ? 'Loop enabled!' : 'Loop disabled.', 'info'); +} + +// Change instrument +function changeInstrument() { + currentInstrument = instrumentSelect.value; + showMessage(`Instrument changed to ${currentInstrument}!`, 'info'); +} + +// Update tempo +function updateTempo() { + tempo = parseInt(tempoSlider.value); + tempoValue.textContent = tempo + ' BPM'; +} + +// Change octave +function changeOctave(delta) { + currentOctave = Math.max(1, Math.min(7, currentOctave + delta)); + currentOctaveEl.textContent = `Octave ${currentOctave}`; +} + +// Load preset song +function loadPreset(songName) { + if (songName === 'random') { + generateRandomComposition(); + } else { + composition = [...presetSongs[songName]]; + } + + updateCompositionDisplay(); + updateDisplay(); + showMessage(`Loaded preset: ${songName.replace('-', ' ').toUpperCase()}`, 'success'); +} + +// Generate random composition +function generateRandomComposition() { + composition = []; + const notes = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; + const durations = [0.25, 0.5, 0.75, 1.0]; + + for (let i = 0; i < 16; i++) { + const note = notes[Math.floor(Math.random() * notes.length)] + currentOctave; + const duration = durations[Math.floor(Math.random() * durations.length)]; + + composition.push({ + note: note, + duration: duration + }); + } +} + +// Save composition +function saveComposition() { + if (composition.length === 0) { + showMessage('No composition to save!', 'warning'); + return; + } + + const data = { + composition: composition, + instrument: currentInstrument, + tempo: tempo, + timestamp: new Date().toISOString() + }; + + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = `music-composer-${Date.now()}.json`; + a.click(); + + URL.revokeObjectURL(url); + showMessage('Composition saved!', 'success'); +} + +// Load composition +function loadComposition(event) { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const data = JSON.parse(e.target.result); + composition = data.composition || []; + currentInstrument = data.instrument || 'piano'; + tempo = data.tempo || 120; + + instrumentSelect.value = currentInstrument; + tempoSlider.value = tempo; + updateTempo(); + + updateCompositionDisplay(); + updateDisplay(); + showMessage('Composition loaded!', 'success'); + } catch (error) { + showMessage('Error loading composition file.', 'error'); + } + }; + reader.readAsText(file); +} + +// Export as MIDI (simplified) +function exportComposition() { + if (composition.length === 0) { + showMessage('No composition to export!', 'warning'); + return; + } + + // This is a simplified MIDI export - in a real implementation, + // you'd use a proper MIDI library + showMessage('MIDI export feature coming soon!', 'info'); +} + +// Update composition display +function updateCompositionDisplay() { + if (composition.length === 0) { + noteSequenceEl.innerHTML = '
    ๐ŸŽต Click "Record" and play some notes to start composing!
    '; + return; + } + + let html = '
    '; + composition.forEach((note, index) => { + const noteName = note.note.replace(/\d+/, ''); + const octave = note.note.replace(/[A-G]#?/, ''); + html += `
    + ${noteName}${octave} + ${note.duration}s +
    `; + }); + html += '
    '; + + noteSequenceEl.innerHTML = html; +} + +// Highlight note during playback +function highlightNote(index) { + clearNoteHighlights(); + const noteElement = noteSequenceEl.querySelector(`[data-index="${index}"]`); + if (noteElement) { + noteElement.classList.add('playing'); + } +} + +// Clear note highlights +function clearNoteHighlights() { + noteSequenceEl.querySelectorAll('.note-item').forEach(item => { + item.classList.remove('playing'); + }); +} + +// Update display elements +function updateDisplay() { + noteCountEl.textContent = composition.length; + + const totalDuration = composition.reduce((sum, note) => sum + note.duration, 0); + const durationInSeconds = Math.round(totalDuration * (60 / tempo)); + compositionDurationEl.textContent = `${durationInSeconds}s`; +} + +// Keyboard shortcuts +function handleKeyDown(e) { + // Piano key mappings (QWERTY layout) + const keyMap = { + 'a': 'C', 'w': 'C#', 's': 'D', 'e': 'D#', 'd': 'E', + 'f': 'F', 't': 'F#', 'g': 'G', 'y': 'G#', 'h': 'A', + 'u': 'A#', 'j': 'B' + }; + + if (keyMap[e.key.toLowerCase()]) { + e.preventDefault(); + playNote(keyMap[e.key.toLowerCase()]); + } + + // Control shortcuts + switch (e.key.toLowerCase()) { + case ' ': + e.preventDefault(); + togglePlayback(); + break; + case 'r': + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + toggleRecording(); + } + break; + case 'c': + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + clearComposition(); + } + break; + } +} + +function handleKeyUp(e) { + const keyMap = { + 'a': 'C', 'w': 'C#', 's': 'D', 'e': 'D#', 'd': 'E', + 'f': 'F', 't': 'F#', 'g': 'G', 'y': 'G#', 'h': 'A', + 'u': 'A#', 'j': 'B' + }; + + if (keyMap[e.key.toLowerCase()]) { + const key = document.querySelector(`[data-note="${keyMap[e.key.toLowerCase()]}"]`); + if (key) { + key.classList.remove('active'); + } + } +} + +// Show message +function showMessage(text, type) { + messageEl.textContent = text; + messageEl.className = `message ${type} show`; + + setTimeout(() => { + messageEl.classList.remove('show'); + }, 3000); +} + +// Initialize the game +initGame(); + +// This music composer game includes piano keyboard simulation, +// multiple instruments, recording/playback, tempo control, +// save/load functionality, and preset songs \ No newline at end of file diff --git a/games/music-composer/style.css b/games/music-composer/style.css new file mode 100644 index 00000000..c92cf1a4 --- /dev/null +++ b/games/music-composer/style.css @@ -0,0 +1,614 @@ +/* Music Composer Game Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + color: #333; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 30px; + color: white; +} + +header h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +header p { + font-size: 1.2em; + opacity: 0.9; +} + +.composer-container { + display: grid; + grid-template-columns: 250px 1fr; + grid-template-rows: auto auto auto; + gap: 20px; + background: rgba(255, 255, 255, 0.95); + border-radius: 15px; + padding: 20px; + box-shadow: 0 10px 30px rgba(0,0,0,0.2); + backdrop-filter: blur(10px); +} + +/* Control Panel */ +.control-panel { + display: flex; + flex-direction: column; + gap: 20px; +} + +.control-section { + background: #f8f9fa; + border-radius: 10px; + padding: 15px; + border: 2px solid #e9ecef; +} + +.control-section h3 { + margin-bottom: 10px; + color: #495057; + font-size: 1.1em; + border-bottom: 1px solid #dee2e6; + padding-bottom: 5px; +} + +.composition-controls, +.save-load-controls { + display: flex; + flex-direction: column; + gap: 8px; +} + +.control-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 15px; + border: 2px solid #dee2e6; + border-radius: 8px; + background: white; + cursor: pointer; + transition: all 0.3s ease; + font-size: 0.9em; +} + +.control-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.1); +} + +.control-btn.active { + background: #007bff; + color: white; + border-color: #007bff; +} + +.record-btn.active { + background: #dc3545; + border-color: #dc3545; + animation: pulse 1s infinite; +} + +.play-btn.active { + background: #28a745; + border-color: #28a745; +} + +.btn-icon { + font-size: 1.2em; +} + +.btn-text { + font-weight: bold; +} + +/* Instrument Selector */ +.instrument-selector select { + width: 100%; + padding: 8px 12px; + border: 2px solid #dee2e6; + border-radius: 8px; + background: white; + font-size: 1em; + cursor: pointer; +} + +/* Tempo Control */ +.tempo-control { + display: flex; + align-items: center; + gap: 10px; +} + +.tempo-control input[type="range"] { + flex: 1; + height: 6px; + border-radius: 3px; + background: #dee2e6; + outline: none; +} + +.tempo-control input[type="range"]::-webkit-slider-thumb { + appearance: none; + width: 20px; + height: 20px; + border-radius: 50%; + background: #007bff; + cursor: pointer; +} + +#tempo-value { + min-width: 70px; + text-align: center; + font-weight: bold; + color: #007bff; +} + +/* Stats Display */ +.stats-display { + display: flex; + flex-direction: column; + gap: 5px; +} + +.stat-item { + display: flex; + justify-content: space-between; + align-items: center; +} + +.stat-label { + font-weight: bold; + color: #495057; +} + +/* Piano Section */ +.piano-section { + grid-column: 2; + grid-row: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; +} + +.octave-selector { + display: flex; + align-items: center; + gap: 15px; + background: #f8f9fa; + padding: 10px 20px; + border-radius: 25px; + border: 2px solid #dee2e6; +} + +.octave-btn { + padding: 8px 12px; + border: 2px solid #dee2e6; + border-radius: 50%; + background: white; + cursor: pointer; + transition: all 0.3s ease; + font-size: 1.2em; +} + +.octave-btn:hover { + background: #007bff; + color: white; + border-color: #007bff; +} + +#current-octave { + font-weight: bold; + color: #495057; + min-width: 80px; + text-align: center; +} + +/* Piano Keyboard */ +.piano-keyboard { + position: relative; + width: 100%; + max-width: 600px; + height: 150px; + background: #2c3e50; + border-radius: 10px; + padding: 10px; + box-shadow: inset 0 0 20px rgba(0,0,0,0.3); +} + +.white-key { + position: absolute; + bottom: 10px; + width: 60px; + height: 130px; + background: white; + border: 1px solid #ccc; + border-radius: 0 0 5px 5px; + cursor: pointer; + display: flex; + align-items: flex-end; + justify-content: center; + padding-bottom: 10px; + font-weight: bold; + color: #666; + user-select: none; + transition: all 0.1s ease; +} + +.white-key:hover { + background: #f8f8f8; +} + +.white-key:active, +.white-key.active { + background: #e8e8e8; + transform: scale(0.98); +} + +.white-key:nth-child(1) { left: 10px; } +.white-key:nth-child(2) { left: 80px; } +.white-key:nth-child(3) { left: 150px; } +.white-key:nth-child(4) { left: 220px; } +.white-key:nth-child(5) { left: 290px; } +.white-key:nth-child(6) { left: 360px; } +.white-key:nth-child(7) { left: 430px; } + +.black-key { + position: absolute; + top: 10px; + width: 35px; + height: 90px; + background: #333; + border-radius: 0 0 3px 3px; + cursor: pointer; + display: flex; + align-items: flex-end; + justify-content: center; + padding-bottom: 5px; + font-size: 0.8em; + color: white; + font-weight: bold; + user-select: none; + transition: all 0.1s ease; + z-index: 10; +} + +.black-key:hover { + background: #555; +} + +.black-key:active, +.black-key.active { + background: #111; + transform: scale(0.95); +} + +/* Composition Display */ +.composition-display { + grid-column: 1 / -1; + grid-row: 2; + background: #f8f9fa; + border-radius: 10px; + padding: 20px; + border: 2px solid #e9ecef; +} + +.composition-display h3 { + margin-bottom: 15px; + color: #495057; + text-align: center; +} + +.note-sequence { + min-height: 80px; + background: white; + border-radius: 8px; + border: 2px solid #dee2e6; + padding: 15px; + margin-bottom: 15px; + display: flex; + align-items: center; + justify-content: center; +} + +.empty-composition { + color: #6c757d; + font-style: italic; + text-align: center; +} + +.note-display { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} + +.note-item { + background: #007bff; + color: white; + padding: 6px 12px; + border-radius: 20px; + font-size: 0.9em; + font-weight: bold; + display: flex; + align-items: center; + gap: 5px; +} + +.note-item .note-name { + min-width: 25px; + text-align: center; +} + +.note-item .note-duration { + font-size: 0.8em; + opacity: 0.8; +} + +.composition-actions { + display: flex; + justify-content: center; + gap: 15px; +} + +.action-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + border: 2px solid #dee2e6; + border-radius: 8px; + background: white; + cursor: pointer; + transition: all 0.3s ease; + font-size: 0.9em; +} + +.action-btn:hover { + background: #007bff; + color: white; + border-color: #007bff; + transform: translateY(-2px); +} + +/* Presets Section */ +.presets-section { + grid-column: 1 / -1; + grid-row: 3; + background: #f8f9fa; + border-radius: 10px; + padding: 20px; + border: 2px solid #e9ecef; +} + +.presets-section h3 { + margin-bottom: 15px; + color: #495057; + text-align: center; +} + +.preset-buttons { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 10px; +} + +.preset-btn { + padding: 12px 15px; + border: 2px solid #dee2e6; + border-radius: 8px; + background: white; + cursor: pointer; + transition: all 0.3s ease; + font-size: 0.9em; + text-align: center; +} + +.preset-btn:hover { + background: #007bff; + color: white; + border-color: #007bff; + transform: translateY(-2px); +} + +/* Instructions */ +.instructions { + margin-top: 30px; + background: rgba(255, 255, 255, 0.95); + border-radius: 15px; + padding: 20px; + box-shadow: 0 5px 15px rgba(0,0,0,0.1); +} + +.instructions h3 { + margin-bottom: 15px; + color: #495057; + text-align: center; +} + +.instructions ul { + list-style: none; + padding: 0; +} + +.instructions li { + margin-bottom: 8px; + padding: 8px 0; + border-bottom: 1px solid #f8f9fa; +} + +.instructions li:last-child { + border-bottom: none; +} + +.instructions strong { + color: #007bff; +} + +/* Message Display */ +.message { + position: fixed; + top: 20px; + right: 20px; + padding: 15px 25px; + border-radius: 8px; + color: white; + font-weight: bold; + z-index: 1000; + opacity: 0; + transform: translateY(-20px); + transition: all 0.3s ease; + max-width: 300px; +} + +.message.show { + opacity: 1; + transform: translateY(0); +} + +.message.success { + background: #28a745; +} + +.message.error { + background: #dc3545; +} + +.message.info { + background: #17a2b8; +} + +.message.warning { + background: #ffc107; + color: #212529; +} + +/* Animations */ +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .composer-container { + grid-template-columns: 1fr; + grid-template-rows: auto auto auto auto; + } + + .piano-section { + grid-column: 1; + grid-row: 2; + } + + .composition-display { + grid-column: 1; + grid-row: 3; + } + + .presets-section { + grid-column: 1; + grid-row: 4; + } +} + +@media (max-width: 768px) { + .container { + padding: 10px; + } + + .piano-keyboard { + max-width: 100%; + height: 120px; + } + + .white-key { + width: 45px; + height: 100px; + } + + .white-key:nth-child(1) { left: 10px; } + .white-key:nth-child(2) { left: 60px; } + .white-key:nth-child(3) { left: 110px; } + .white-key:nth-child(4) { left: 160px; } + .white-key:nth-child(5) { left: 210px; } + .white-key:nth-child(6) { left: 260px; } + .white-key:nth-child(7) { left: 310px; } + + .black-key { + width: 25px; + height: 70px; + } + + .black-key[data-note="C#"] { left: 45px; } + .black-key[data-note="D#"] { left: 95px; } + .black-key[data-note="F#"] { left: 205px; } + .black-key[data-note="G#"] { left: 255px; } + .black-key[data-note="A#"] { left: 305px; } + + .preset-buttons { + grid-template-columns: repeat(2, 1fr); + } + + header h1 { + font-size: 2em; + } +} + +@media (max-width: 480px) { + .control-panel { + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + } + + .control-section { + flex: 1; + min-width: 200px; + } + + .piano-keyboard { + height: 100px; + } + + .white-key { + width: 35px; + height: 80px; + font-size: 0.8em; + } + + .black-key { + width: 20px; + height: 55px; + font-size: 0.7em; + } + + .octave-selector { + padding: 8px 15px; + } + + .preset-buttons { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/games/mystic-maze/index.html b/games/mystic-maze/index.html new file mode 100644 index 00000000..48cb1fa1 --- /dev/null +++ b/games/mystic-maze/index.html @@ -0,0 +1,18 @@ + + + + + + Mystic Maze Game + + + +
    +

    Mystic Maze

    + +
    Keys: 0
    +
    Use arrow keys to navigate. Collect keys to open doors and find the artifact!
    +
    + + + \ No newline at end of file diff --git a/games/mystic-maze/script.js b/games/mystic-maze/script.js new file mode 100644 index 00000000..466878f6 --- /dev/null +++ b/games/mystic-maze/script.js @@ -0,0 +1,156 @@ +// Mystic Maze Game Script +// Navigate enchanted mazes, solve puzzles, and find the hidden artifact. + +const canvas = document.getElementById('game-canvas'); +const ctx = canvas.getContext('2d'); +const scoreElement = document.getElementById('score'); + +// Game variables +const gridSize = 40; +const cols = Math.floor(canvas.width / gridSize); +const rows = Math.floor(canvas.height / gridSize); + +let player = { x: 1, y: 1 }; +let keys = 0; +let gameRunning = true; + +// Maze layout: 0 = empty, 1 = wall, 2 = key, 3 = door, 4 = trap, 5 = artifact +let maze = [ + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], + [1,0,1,1,1,0,1,1,1,1,1,1,1,0,1,1,1,1,0,1], + [1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1], + [1,0,1,0,1,1,1,0,1,1,1,1,1,1,1,1,0,1,0,1], + [1,0,0,0,0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,1], + [1,0,1,1,1,0,1,0,1,0,1,1,1,1,0,1,0,1,0,1], + [1,0,1,0,0,0,1,0,0,0,0,0,0,1,0,1,0,1,0,1], + [1,0,1,0,1,1,1,1,1,1,1,0,0,1,0,1,0,1,0,1], + [1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1], + [1,0,1,1,1,1,1,1,1,0,1,0,1,1,1,1,1,1,0,1], + [1,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,1], + [1,0,1,0,1,1,1,0,1,1,1,1,1,1,1,1,0,1,0,1], + [1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +]; + +// Place items +maze[2][5] = 2; // Key +maze[4][8] = 3; // Door +maze[6][10] = 4; // Trap +maze[8][12] = 5; // Artifact + +// Initialize game +function init() { + // Start game loop + requestAnimationFrame(gameLoop); +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + update(); + draw(); + + requestAnimationFrame(gameLoop); +} + +// Update game state +function update() { + // Handle input (will be added in event listeners) +} + +// Draw everything +function draw() { + // Clear canvas + ctx.fillStyle = '#111'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw maze + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + const cell = maze[y][x]; + if (cell === 1) { + ctx.fillStyle = '#666'; + ctx.fillRect(x * gridSize, y * gridSize, gridSize, gridSize); + } else if (cell === 2) { + ctx.fillStyle = '#ffff00'; + ctx.fillRect(x * gridSize + 10, y * gridSize + 10, 20, 20); + } else if (cell === 3) { + ctx.fillStyle = '#ff0000'; + ctx.fillRect(x * gridSize, y * gridSize, gridSize, gridSize); + } else if (cell === 4) { + ctx.fillStyle = '#800080'; + ctx.beginPath(); + ctx.moveTo(x * gridSize + 10, y * gridSize + 30); + ctx.lineTo(x * gridSize + 20, y * gridSize + 10); + ctx.lineTo(x * gridSize + 30, y * gridSize + 30); + ctx.closePath(); + ctx.fill(); + } else if (cell === 5) { + ctx.fillStyle = '#00ffff'; + ctx.beginPath(); + ctx.arc(x * gridSize + 20, y * gridSize + 20, 15, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = '#ffffff'; + ctx.beginPath(); + ctx.arc(x * gridSize + 20, y * gridSize + 20, 10, 0, Math.PI * 2); + ctx.fill(); + } + } + } + + // Draw player + ctx.fillStyle = '#00ff00'; + ctx.beginPath(); + ctx.arc(player.x * gridSize + 20, player.y * gridSize + 20, 15, 0, Math.PI * 2); + ctx.fill(); + + // Update score + scoreElement.textContent = 'Keys: ' + keys; +} + +// Handle input +document.addEventListener('keydown', function(event) { + let newX = player.x; + let newY = player.y; + + if (event.code === 'ArrowUp') newY--; + else if (event.code === 'ArrowDown') newY++; + else if (event.code === 'ArrowLeft') newX--; + else if (event.code === 'ArrowRight') newX++; + + // Check bounds + if (newX >= 0 && newX < cols && newY >= 0 && newY < rows) { + const cell = maze[newY][newX]; + if (cell === 0 || cell === 2 || cell === 5) { + player.x = newX; + player.y = newY; + + // Collect key + if (cell === 2) { + maze[newY][newX] = 0; + keys++; + // Open doors + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + if (maze[y][x] === 3) maze[y][x] = 0; + } + } + } + + // Find artifact + if (cell === 5) { + gameRunning = false; + alert('You found the artifact! Congratulations!'); + } + } else if (cell === 4) { + // Trap: reset position + player.x = 1; + player.y = 1; + } + } +}); + +// Start the game +init(); \ No newline at end of file diff --git a/games/mystic-maze/style.css b/games/mystic-maze/style.css new file mode 100644 index 00000000..1d7e9e51 --- /dev/null +++ b/games/mystic-maze/style.css @@ -0,0 +1,38 @@ +body { + font-family: Arial, sans-serif; + background-color: #000; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +#game-container { + text-align: center; +} + +h1 { + font-size: 2.5em; + margin-bottom: 20px; + text-shadow: 0 0 10px #800080; +} + +#game-canvas { + border: 2px solid #800080; + background-color: #111; + box-shadow: 0 0 20px #800080; +} + +#score { + font-size: 1.2em; + margin: 10px 0; + color: #ffff00; +} + +#instructions { + font-size: 1em; + margin-top: 10px; + color: #cccccc; +} \ No newline at end of file diff --git a/games/mystic-merge/index.html b/games/mystic-merge/index.html new file mode 100644 index 00000000..c770fa98 --- /dev/null +++ b/games/mystic-merge/index.html @@ -0,0 +1,32 @@ + + + + + + Mystic Merge - Mini JS Games Hub + + + +
    +

    Mystic Merge

    +
    +
    +
    +
    Reach Rune Level 2
    +
    +
    +
    +
    +
    +

    Level: 1

    +

    Score: 0

    +
    + + +
    +
    +
    Made for Mini JS Games Hub
    +
    + + + \ No newline at end of file diff --git a/games/mystic-merge/screenshot.png b/games/mystic-merge/screenshot.png new file mode 100644 index 00000000..c48703ed --- /dev/null +++ b/games/mystic-merge/screenshot.png @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/games/mystic-merge/script.js b/games/mystic-merge/script.js new file mode 100644 index 00000000..3b3a66fe --- /dev/null +++ b/games/mystic-merge/script.js @@ -0,0 +1,175 @@ +// Mystic Merge Game +const gridSize = 5; +let grid = []; +let selectedOrb = null; +let score = 0; +let level = parseInt(localStorage.getItem('mysticMergeLevel') || 1); +let progress = 0; +let objective = {type: 'rune', level: 2}; + +const gridElement = document.getElementById('grid'); +const orbSelection = document.getElementById('orbSelection'); +const progressBar = document.getElementById('progressBar'); +const objectiveElement = document.getElementById('objective'); +const levelElement = document.getElementById('level'); +const scoreElement = document.getElementById('score'); +const resetBtn = document.getElementById('resetBtn'); +const nextBtn = document.getElementById('nextBtn'); + +function initGame() { + createGrid(); + createOrbSelection(); + updateUI(); + selectedOrb = null; + score = 0; + progress = 0; + updateProgress(); + nextBtn.style.display = 'none'; +} + +function createGrid() { + grid = []; + for (let i = 0; i < gridSize; i++) { + grid[i] = []; + for (let j = 0; j < gridSize; j++) { + grid[i][j] = null; + } + } + renderGrid(); +} + +function renderGrid() { + gridElement.innerHTML = ''; + for (let i = 0; i < gridSize; i++) { + for (let j = 0; j < gridSize; j++) { + const cell = document.createElement('div'); + cell.className = 'cell'; + cell.dataset.row = i; + cell.dataset.col = j; + if (grid[i][j]) { + const orb = document.createElement('div'); + orb.className = `orb level${grid[i][j]}`; + orb.textContent = grid[i][j]; + cell.appendChild(orb); + } + cell.addEventListener('click', () => handleCellClick(i, j)); + gridElement.appendChild(cell); + } + } +} + +function createOrbSelection() { + orbSelection.innerHTML = ''; + const availableLevels = Math.min(3 + Math.floor(level / 2), 7); + for (let i = 1; i <= availableLevels; i++) { + const orb = document.createElement('div'); + orb.className = `selection-orb level${i}`; + orb.textContent = i; + orb.dataset.level = i; + orb.addEventListener('click', () => selectOrb(i)); + orbSelection.appendChild(orb); + } +} + +function selectOrb(level) { + selectedOrb = level; + document.querySelectorAll('.selection-orb').forEach(orb => orb.classList.remove('selected')); + document.querySelector(`.selection-orb[data-level="${level}"]`).classList.add('selected'); +} + +function handleCellClick(row, col) { + if (selectedOrb && !grid[row][col]) { + grid[row][col] = selectedOrb; + selectedOrb = null; + document.querySelectorAll('.selection-orb').forEach(orb => orb.classList.remove('selected')); + renderGrid(); + checkMerges(); + updateProgress(); + } +} + +function checkMerges() { + let merged = true; + while (merged) { + merged = false; + for (let i = 0; i < gridSize; i++) { + for (let j = 0; j < gridSize; j++) { + if (grid[i][j]) { + // Check adjacent cells + const directions = [[0,1], [1,0], [0,-1], [-1,0]]; + for (const [di, dj] of directions) { + const ni = i + di, nj = j + dj; + if (ni >= 0 && ni < gridSize && nj >= 0 && nj < gridSize && grid[ni][nj] === grid[i][j]) { + // Merge + const newLevel = grid[i][j] + 1; + grid[i][j] = newLevel; + grid[ni][nj] = null; + score += newLevel * 10; + merged = true; + // Animate + const cell = document.querySelector(`[data-row="${i}"][data-col="${j}"] .orb`); + if (cell) { + cell.classList.add('merging'); + setTimeout(() => cell.classList.remove('merging'), 600); + createParticles(cell); + } + break; + } + } + if (merged) break; + } + } + if (merged) break; + } + } + renderGrid(); +} + +function createParticles(element) { + const rect = element.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + for (let i = 0; i < 8; i++) { + const particle = document.createElement('div'); + particle.className = 'particle'; + particle.style.left = centerX + 'px'; + particle.style.top = centerY + 'px'; + particle.style.setProperty('--angle', (i * 45) + 'deg'); + document.body.appendChild(particle); + setTimeout(() => particle.remove(), 1000); + } +} + +function updateProgress() { + let maxRune = 0; + for (let i = 0; i < gridSize; i++) { + for (let j = 0; j < gridSize; j++) { + if (grid[i][j] && grid[i][j] > maxRune) maxRune = grid[i][j]; + } + } + if (objective.type === 'rune') { + progress = Math.min(maxRune / objective.level, 1); + } + progressBar.style.width = (progress * 100) + '%'; + if (progress >= 1) { + nextBtn.style.display = 'inline-block'; + } + scoreElement.textContent = 'Score: ' + score; +} + +function updateUI() { + levelElement.textContent = 'Level: ' + level; + objectiveElement.textContent = `Reach Rune Level ${objective.level}`; +} + +function nextLevel() { + level++; + localStorage.setItem('mysticMergeLevel', level); + objective.level = Math.min(objective.level + 1, 7); + initGame(); +} + +resetBtn.addEventListener('click', initGame); +nextBtn.addEventListener('click', nextLevel); + +initGame(); \ No newline at end of file diff --git a/games/mystic-merge/style.css b/games/mystic-merge/style.css new file mode 100644 index 00000000..4fad3381 --- /dev/null +++ b/games/mystic-merge/style.css @@ -0,0 +1,39 @@ +*{box-sizing:border-box;margin:0;padding:0} +body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;background:linear-gradient(135deg,#4facfe 0%,#00f2fe 100%);display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px} +.game-wrap{background:#fff;border-radius:20px;padding:30px;text-align:center;box-shadow:0 20px 40px rgba(0,0,0,0.1);max-width:500px;width:100%} +h1{color:#333;margin-bottom:20px;font-size:2em;text-shadow:0 2px 4px rgba(0,0,0,0.1)} +.game-area{display:flex;flex-direction:column;gap:20px} +.progress-container{position:relative;background:#e9ecef;border-radius:10px;height:30px;overflow:hidden} +.progress-bar{background:linear-gradient(90deg,#28a745,#20c997);height:100%;width:0%;transition:width 0.5s ease;border-radius:10px} +.objective{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:bold;text-shadow:0 1px 2px rgba(0,0,0,0.5)} +.grid{display:grid;grid-template-columns:repeat(5,1fr);gap:8px;background:#f8f9fa;padding:20px;border-radius:10px;border:2px solid #e9ecef;width:fit-content;margin:0 auto} +.cell{width:50px;height:50px;border-radius:10px;border:2px solid #dee2e6;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all 0.3s ease;position:relative;overflow:hidden} +.cell:hover{background:#f1f3f4} +.cell.selected{box-shadow:0 0 0 3px #007bff} +.orb{background:#fff;border-radius:50%;width:40px;height:40px;display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:bold;color:#fff;border:2px solid #fff;box-shadow:0 2px 4px rgba(0,0,0,0.2);transition:all 0.3s ease} +.orb.level1{background:#ff6b6b} +.orb.level2{background:#4ecdc4} +.orb.level3{background:#45b7d1} +.orb.level4{background:#f9ca24} +.orb.level5{background:#f0932b} +.orb.level6{background:#eb4d4b} +.orb.level7{background:#6c5ce7} +.orb.merging{animation:mergePulse 0.6s ease} +@keyframes mergePulse{0%{transform:scale(1)}50%{transform:scale(1.2)}100%{transform:scale(1)}} +.particle{position:absolute;width:4px;height:4px;background:#ffd700;border-radius:50%;animation:particleBurst 1s ease-out forwards} +@keyframes particleBurst{0%{opacity:1;transform:scale(0)}50%{opacity:1;transform:scale(1)}100%{opacity:0;transform:scale(2)}} +.orb-selection{display:flex;gap:10px;justify-content:center;margin-top:10px} +.selection-orb{width:40px;height:40px;border-radius:50%;border:2px solid #dee2e6;cursor:pointer;transition:all 0.3s ease} +.selection-orb:hover{transform:scale(1.1)} +.info{display:flex;justify-content:space-around;margin-top:20px;font-size:18px;color:#333} +.controls{display:flex;gap:10px;justify-content:center;margin-top:10px} +button{padding:10px 20px;border:none;border-radius:8px;background:#007bff;color:#fff;font-size:16px;cursor:pointer;transition:all 0.3s ease} +button:hover{background:#0056b3;transform:translateY(-2px)} +footer{font-size:14px;color:#666;margin-top:20px} +@media (max-width: 600px) { + .game-wrap{padding:20px;max-width:100%} + .grid{grid-template-columns:repeat(5,1fr);gap:5px;padding:15px} + .cell{width:40px;height:40px} + .orb{width:32px;height:32px;font-size:14px} + .selection-orb{width:32px;height:32px} +} \ No newline at end of file diff --git a/games/nebula-navigator/index.html b/games/nebula-navigator/index.html new file mode 100644 index 00000000..33d345fe --- /dev/null +++ b/games/nebula-navigator/index.html @@ -0,0 +1,41 @@ + + + + + + Nebula Navigator + + + + + + + + + +
    +
    +

    NEBULA NAVIGATOR

    +

    Pilot a spaceship through colorful nebulae, collecting energy orbs while dodging cosmic debris and black holes!

    + +

    Controls:

    +
      +
    • Arrow Keys - Move Ship
    • +
    • Space - Boost
    • +
    + + +
    +
    + +
    +
    Score: 0
    +
    Lives: 3
    +
    Energy: 100
    +
    + + + + + + \ No newline at end of file diff --git a/games/nebula-navigator/script.js b/games/nebula-navigator/script.js new file mode 100644 index 00000000..5b880b37 --- /dev/null +++ b/games/nebula-navigator/script.js @@ -0,0 +1,373 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const startButton = document.getElementById('startButton'); +const instructionsOverlay = document.getElementById('instructions-overlay'); +const scoreElement = document.getElementById('score'); +const livesElement = document.getElementById('lives'); +const energyElement = document.getElementById('energy'); + +canvas.width = 800; +canvas.height = 600; + +let gameRunning = false; +let score = 0; +let lives = 3; +let energy = 100; +let ship; +let particles = []; +let energyOrbs = []; +let debris = []; +let blackHoles = []; +const SHIP_SIZE = 20; +const PARTICLE_COUNT = 100; +const ORB_SIZE = 10; +const DEBRIS_SIZE = 15; +const BLACK_HOLE_SIZE = 30; + +// Ship class +class Ship { + constructor(x, y) { + this.x = x; + this.y = y; + this.vx = 0; + this.vy = 0; + this.angle = 0; + } + + update() { + // Friction + this.vx *= 0.98; + this.vy *= 0.98; + + // Update position + this.x += this.vx; + this.y += this.vy; + + // Keep in bounds + if (this.x < 0) this.x = canvas.width; + if (this.x > canvas.width) this.x = 0; + if (this.y < 0) this.y = canvas.height; + if (this.y > canvas.height) this.y = 0; + + // Black hole attraction + blackHoles.forEach(bh => { + const dx = bh.x - this.x; + const dy = bh.y - this.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist < 100) { + const force = (100 - dist) / 1000; + this.vx += (dx / dist) * force; + this.vy += (dy / dist) * force; + } + }); + } + + moveUp() { + this.vy -= 0.5; + } + + moveDown() { + this.vy += 0.5; + } + + moveLeft() { + this.vx -= 0.5; + } + + moveRight() { + this.vx += 0.5; + } + + boost() { + if (energy > 0) { + this.vx += Math.cos(this.angle) * 2; + this.vy += Math.sin(this.angle) * 2; + energy -= 1; + } + } + + draw() { + ctx.save(); + ctx.translate(this.x, this.y); + ctx.rotate(this.angle); + ctx.fillStyle = '#00ffff'; + ctx.beginPath(); + ctx.moveTo(SHIP_SIZE, 0); + ctx.lineTo(-SHIP_SIZE/2, -SHIP_SIZE/2); + ctx.lineTo(-SHIP_SIZE/2, SHIP_SIZE/2); + ctx.closePath(); + ctx.fill(); + ctx.restore(); + } +} + +// Particle class for nebulae +class Particle { + constructor() { + this.x = Math.random() * canvas.width; + this.y = Math.random() * canvas.height; + this.vx = (Math.random() - 0.5) * 0.5; + this.vy = (Math.random() - 0.5) * 0.5; + this.color = `hsl(${Math.random() * 360}, 70%, 50%)`; + this.size = Math.random() * 3 + 1; + } + + update() { + this.x += this.vx; + this.y += this.vy; + if (this.x < 0) this.x = canvas.width; + if (this.x > canvas.width) this.x = 0; + if (this.y < 0) this.y = canvas.height; + if (this.y > canvas.height) this.y = 0; + } + + draw() { + ctx.fillStyle = this.color; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fill(); + } +} + +// Energy Orb class +class EnergyOrb { + constructor() { + this.x = Math.random() * canvas.width; + this.y = Math.random() * canvas.height; + this.vx = (Math.random() - 0.5) * 2; + this.vy = (Math.random() - 0.5) * 2; + } + + update() { + this.x += this.vx; + this.y += this.vy; + if (this.x < 0 || this.x > canvas.width) this.vx *= -1; + if (this.y < 0 || this.y > canvas.height) this.vy *= -1; + } + + draw() { + ctx.fillStyle = '#ffff00'; + ctx.beginPath(); + ctx.arc(this.x, this.y, ORB_SIZE, 0, Math.PI * 2); + ctx.fill(); + } +} + +// Debris class +class Debris { + constructor() { + this.x = Math.random() * canvas.width; + this.y = Math.random() * canvas.height; + this.vx = (Math.random() - 0.5) * 3; + this.vy = (Math.random() - 0.5) * 3; + } + + update() { + this.x += this.vx; + this.y += this.vy; + if (this.x < -DEBRIS_SIZE) this.x = canvas.width + DEBRIS_SIZE; + if (this.x > canvas.width + DEBRIS_SIZE) this.x = -DEBRIS_SIZE; + if (this.y < -DEBRIS_SIZE) this.y = canvas.height + DEBRIS_SIZE; + if (this.y > canvas.height + DEBRIS_SIZE) this.y = -DEBRIS_SIZE; + } + + draw() { + ctx.fillStyle = '#666666'; + ctx.fillRect(this.x - DEBRIS_SIZE/2, this.y - DEBRIS_SIZE/2, DEBRIS_SIZE, DEBRIS_SIZE); + } +} + +// Black Hole class +class BlackHole { + constructor() { + this.x = Math.random() * canvas.width; + this.y = Math.random() * canvas.height; + this.vx = (Math.random() - 0.5) * 1; + this.vy = (Math.random() - 0.5) * 1; + } + + update() { + this.x += this.vx; + this.y += this.vy; + if (this.x < -BLACK_HOLE_SIZE) this.x = canvas.width + BLACK_HOLE_SIZE; + if (this.x > canvas.width + BLACK_HOLE_SIZE) this.x = -BLACK_HOLE_SIZE; + if (this.y < -BLACK_HOLE_SIZE) this.y = canvas.height + BLACK_HOLE_SIZE; + if (this.y > canvas.height + BLACK_HOLE_SIZE) this.y = -BLACK_HOLE_SIZE; + } + + draw() { + ctx.fillStyle = '#000000'; + ctx.beginPath(); + ctx.arc(this.x, this.y, BLACK_HOLE_SIZE, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = '#ff0000'; + ctx.lineWidth = 2; + ctx.stroke(); + } +} + +// Initialize game objects +function initGame() { + ship = new Ship(canvas.width / 2, canvas.height / 2); + particles = []; + energyOrbs = []; + debris = []; + blackHoles = []; + + for (let i = 0; i < PARTICLE_COUNT; i++) { + particles.push(new Particle()); + } + + for (let i = 0; i < 5; i++) { + energyOrbs.push(new EnergyOrb()); + debris.push(new Debris()); + } + + for (let i = 0; i < 2; i++) { + blackHoles.push(new BlackHole()); + } +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + update(); + draw(); + + requestAnimationFrame(gameLoop); +} + +// Update game state +function update() { + ship.update(); + + particles.forEach(p => p.update()); + energyOrbs.forEach(o => o.update()); + debris.forEach(d => d.update()); + blackHoles.forEach(b => b.update()); + + // Check collisions + checkCollisions(); + + // Regenerate energy slowly + energy = Math.min(energy + 0.1, 100); + + updateUI(); +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw particles + particles.forEach(p => p.draw()); + + // Draw game objects + energyOrbs.forEach(o => o.draw()); + debris.forEach(d => d.draw()); + blackHoles.forEach(b => b.draw()); + ship.draw(); +} + +// Check collisions +function checkCollisions() { + // Ship with energy orbs + energyOrbs.forEach((orb, index) => { + const dx = ship.x - orb.x; + const dy = ship.y - orb.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist < SHIP_SIZE + ORB_SIZE) { + energyOrbs.splice(index, 1); + score += 10; + energy = Math.min(energy + 20, 100); + energyOrbs.push(new EnergyOrb()); // Spawn new orb + } + }); + + // Ship with debris + debris.forEach(deb => { + const dx = ship.x - deb.x; + const dy = ship.y - deb.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist < SHIP_SIZE + DEBRIS_SIZE) { + lives--; + if (lives <= 0) { + gameOver(); + } else { + ship.x = canvas.width / 2; + ship.y = canvas.height / 2; + ship.vx = 0; + ship.vy = 0; + } + } + }); + + // Ship with black holes + blackHoles.forEach(bh => { + const dx = ship.x - bh.x; + const dy = ship.y - bh.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist < BLACK_HOLE_SIZE) { + lives = 0; + gameOver(); + } + }); +} + +// Update UI +function updateUI() { + scoreElement.textContent = `Score: ${score}`; + livesElement.textContent = `Lives: ${lives}`; + energyElement.textContent = `Energy: ${Math.floor(energy)}`; +} + +// Game over +function gameOver() { + gameRunning = false; + alert(`Game Over! Final Score: ${score}`); + resetGame(); +} + +// Reset game +function resetGame() { + score = 0; + lives = 3; + energy = 100; + initGame(); + updateUI(); +} + +// Event listeners +startButton.addEventListener('click', () => { + instructionsOverlay.style.display = 'none'; + resetGame(); + gameRunning = true; + gameLoop(); +}); + +document.addEventListener('keydown', (e) => { + if (!gameRunning) return; + switch (e.code) { + case 'ArrowUp': + ship.moveUp(); + break; + case 'ArrowDown': + ship.moveDown(); + break; + case 'ArrowLeft': + ship.moveLeft(); + break; + case 'ArrowRight': + ship.moveRight(); + break; + case 'Space': + e.preventDefault(); + ship.boost(); + break; + } +}); + +// Initialize +initGame(); +updateUI(); \ No newline at end of file diff --git a/games/nebula-navigator/style.css b/games/nebula-navigator/style.css new file mode 100644 index 00000000..30f0d79c --- /dev/null +++ b/games/nebula-navigator/style.css @@ -0,0 +1,131 @@ +/* General Reset & Font */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Poppins', sans-serif; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background: radial-gradient(ellipse at center, #1a1a2e 0%, #16213e 50%, #0f0f23 100%); + color: #eee; + overflow: hidden; +} + +/* Game UI */ +#game-ui { + position: absolute; + top: 20px; + left: 20px; + display: flex; + gap: 20px; + z-index: 5; +} + +#score, #lives, #energy { + background-color: rgba(0, 0, 0, 0.7); + color: #fff; + padding: 10px 15px; + border-radius: 5px; + font-size: 1.1rem; + font-weight: 600; +} + +/* Canvas */ +canvas { + background: radial-gradient(ellipse at center, #0f0f23 0%, #1a1a2e 70%, #16213e 100%); + border: 3px solid #ff6b6b; + box-shadow: 0 0 20px rgba(255, 107, 107, 0.3); + display: block; +} + +/* Instructions Screen */ +#instructions-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +#instructions-content { + background-color: #2a2a2a; + padding: 30px 40px; + border-radius: 10px; + text-align: center; + border: 2px solid #ff6b6b; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); + max-width: 500px; +} + +#instructions-content h2 { + font-size: 2.5rem; + color: #ff6b6b; + margin-bottom: 15px; + letter-spacing: 2px; +} + +#instructions-content p { + font-size: 1.1rem; + margin-bottom: 25px; + color: #ccc; +} + +#instructions-content h3 { + font-size: 1.2rem; + color: #eee; + margin-bottom: 10px; + border-bottom: 1px solid #444; + padding-bottom: 5px; +} + +#instructions-content ul { + list-style: none; + margin-bottom: 30px; + text-align: left; + display: inline-block; +} + +#instructions-content li { + font-size: 1rem; + color: #ccc; + margin-bottom: 8px; +} + +/* Style for keys */ +#instructions-content code { + background-color: #ff6b6b; + color: #fff; + padding: 3px 6px; + border-radius: 4px; + font-family: monospace; + font-size: 0.95rem; + margin-right: 8px; +} + +/* Start Button */ +#startButton { + background-color: #ff6b6b; + color: white; + border: none; + padding: 12px 24px; + font-size: 1.1rem; + font-weight: 600; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} + +#startButton:hover { + background-color: #ff5252; +} \ No newline at end of file diff --git a/games/neon-orbit/index.html b/games/neon-orbit/index.html new file mode 100644 index 00000000..ed6f887e --- /dev/null +++ b/games/neon-orbit/index.html @@ -0,0 +1,28 @@ + + + + + + Neon Orbit - Mini JS Games Hub + + + +
    +

    Neon Orbit

    +
    + +
    +
    +
    + + +
    +

    Tap or click to reverse rotation direction. Align gaps to pass obstacles!

    +

    Score: 0

    +

    High Score: 0

    +
    +
    Made for Mini JS Games Hub
    +
    + + + \ No newline at end of file diff --git a/games/neon-orbit/script.js b/games/neon-orbit/script.js new file mode 100644 index 00000000..8da260a9 --- /dev/null +++ b/games/neon-orbit/script.js @@ -0,0 +1,211 @@ +// Neon Orbit Game +const canvas = document.getElementById('game'); +const ctx = canvas.getContext('2d'); +const BASE_W = 400, BASE_H = 600, ASPECT = BASE_H / BASE_W; +let DPR = window.devicePixelRatio || 1; +let W = BASE_W, H = BASE_H; + +let frame = 0; +let gameState = 'menu'; // 'menu' | 'play' | 'paused' | 'over' +let score = 0; +let highScore = localStorage.getItem('neonOrbitHighScore') || 0; +let speed = 2; + +canvas.setAttribute('role', 'application'); +canvas.setAttribute('aria-label', 'Neon Orbit game canvas'); +canvas.tabIndex = 0; + +function resizeCanvas() { + DPR = window.devicePixelRatio || 1; + const container = canvas.parentElement || document.body; + const maxWidth = Math.min(window.innerWidth - 40, 720); + const cssWidth = Math.min(container.clientWidth - 24 || BASE_W, maxWidth); + const cssHeight = Math.round(cssWidth * ASPECT); + + canvas.style.width = cssWidth + 'px'; + canvas.style.height = cssHeight + 'px'; + + canvas.width = Math.round(cssWidth * DPR); + canvas.height = Math.round(cssHeight * DPR); + ctx.setTransform(DPR, 0, 0, DPR, 0, 0); + + W = cssWidth; + H = cssHeight; +} + +window.addEventListener('resize', resizeCanvas); +resizeCanvas(); + +const centerX = W / 2; +const centerY = H / 2; +const ringRadius = Math.min(W, H) / 4; +const gapSize = Math.PI / 2; // 90 degrees + +let playerAngle = 0; +let rotationDirection = 1; // 1 clockwise, -1 counter + +class Obstacle { + constructor() { + this.distance = H / 2 + 50; + this.angle = Math.random() * Math.PI * 2; + this.gapStart = Math.random() * Math.PI * 2; + this.gapEnd = this.gapStart + gapSize; + this.color = ['#ff00ff', '#00ffff', '#ff4500', '#00ff00'][Math.floor(Math.random() * 4)]; + this.passed = false; + } + update() { + this.distance -= speed; + } + draw() { + const radius = this.distance; + ctx.strokeStyle = this.color; + ctx.lineWidth = 4; + ctx.shadowColor = this.color; + ctx.shadowBlur = 10; + ctx.beginPath(); + ctx.arc(centerX, centerY, radius, this.gapEnd, this.gapStart + Math.PI * 2); + ctx.stroke(); + ctx.shadowBlur = 0; + } +} + +let obstacles = []; + +function reset() { + frame = 0; + score = 0; + speed = 2; + playerAngle = 0; + obstacles = []; + gameState = 'play'; + document.getElementById('score').textContent = 'Score: 0'; + document.getElementById('highScore').textContent = 'High Score: ' + highScore; +} + +function spawnObstacle() { + if (Math.random() < 0.02) { + obstacles.push(new Obstacle()); + } +} + +function checkCollisions() { + for (const obs of obstacles) { + if (obs.distance <= ringRadius + 10 && !obs.passed) { + const playerGapStart = playerAngle % (Math.PI * 2); + const playerGapEnd = (playerAngle + gapSize) % (Math.PI * 2); + const obsGapStart = obs.gapStart; + const obsGapEnd = obs.gapEnd; + // Normalize for overlap check + let pStart = playerGapStart; + let pEnd = playerGapEnd; + if (pEnd < pStart) pEnd += Math.PI * 2; + let oStart = obsGapStart; + let oEnd = obsGapEnd; + if (oEnd < oStart) oEnd += Math.PI * 2; + const overlap = (pStart < oEnd && pEnd > oStart); + if (overlap) { + score++; + obs.passed = true; + speed += 0.05; + if (score > highScore) { + highScore = score; + localStorage.setItem('neonOrbitHighScore', highScore); + } + document.getElementById('score').textContent = 'Score: ' + score; + document.getElementById('highScore').textContent = 'High Score: ' + highScore; + } else { + gameState = 'over'; + } + } + } +} + +function drawRing() { + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 6; + ctx.shadowColor = '#ffffff'; + ctx.shadowBlur = 15; + ctx.beginPath(); + ctx.arc(centerX, centerY, ringRadius, playerAngle + gapSize, playerAngle + Math.PI * 2); + ctx.stroke(); + ctx.shadowBlur = 0; +} + +function draw() { + ctx.clearRect(0, 0, W, H); + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, W, H); + + for (const obs of obstacles) obs.draw(); + drawRing(); + + if (gameState === 'menu') { + ctx.fillStyle = '#fff'; + ctx.font = '24px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Tap or click to start', centerX, centerY + 50); + } + if (gameState === 'over') { + ctx.fillStyle = 'rgba(0,0,0,0.7)'; + ctx.fillRect(20, H / 2 - 60, W - 40, 120); + ctx.fillStyle = '#fff'; + ctx.font = '28px sans-serif'; + ctx.fillText('Game Over', centerX, H / 2 - 20); + ctx.font = '20px sans-serif'; + ctx.fillText('Score: ' + score, centerX, H / 2 + 10); + ctx.fillText('High Score: ' + highScore, centerX, H / 2 + 35); + } +} + +function update() { + if (gameState === 'play') { + frame++; + playerAngle += 0.05 * rotationDirection; + for (const obs of obstacles) obs.update(); + obstacles = obstacles.filter(obs => obs.distance > 0); + spawnObstacle(); + checkCollisions(); + } +} + +function loop() { + update(); + draw(); + requestAnimationFrame(loop); +} + +function rotate() { + if (gameState === 'menu') reset(); + else if (gameState === 'over') reset(); + else if (gameState === 'play') { + rotationDirection *= -1; // reverse direction on tap + } +} + +// Input +canvas.addEventListener('click', rotate); +canvas.addEventListener('keydown', e => { + if (e.code === 'Space' || e.code === 'Enter') { + e.preventDefault(); + rotate(); + } +}); + +// Buttons +document.getElementById('startBtn').addEventListener('click', () => { + if (gameState === 'menu' || gameState === 'over') reset(); +}); + +document.getElementById('pauseBtn').addEventListener('click', () => { + if (gameState === 'play') { + gameState = 'paused'; + document.getElementById('pauseBtn').setAttribute('aria-pressed', 'true'); + document.getElementById('pauseBtn').textContent = 'Resume'; + } else if (gameState === 'paused') { + gameState = 'play'; + document.getElementById('pauseBtn').setAttribute('aria-pressed', 'false'); + document.getElementById('pauseBtn').textContent = 'Pause'; + } +}); + +loop(); \ No newline at end of file diff --git a/games/neon-orbit/style.css b/games/neon-orbit/style.css new file mode 100644 index 00000000..f5540231 --- /dev/null +++ b/games/neon-orbit/style.css @@ -0,0 +1,10 @@ +*{box-sizing:border-box;margin:0;padding:0} +body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;background:#000;display:flex;align-items:center;justify-content:center;height:100vh} +.game-wrap{background:#111;border:4px solid #00ffff;padding:12px;border-radius:8px;text-align:center;box-shadow:0 0 20px #00ffff} +h1{color:#ff00ff;text-shadow:0 0 10px #ff00ff, 0 0 20px #ff00ff} +canvas{background:#000;display:block;margin:8px auto;border:2px solid #00ff00;box-shadow:0 0 10px #00ff00} +.info{color:#fff;text-shadow:0 0 5px #fff} +.controls button{padding:8px 12px;margin:8px;border-radius:6px;border:0;background:#ff4500;color:#fff;text-shadow:0 0 5px #ff4500} +.controls button:hover{box-shadow:0 0 10px #ff4500} +footer{font-size:12px;color:#888;text-shadow:0 0 3px #888} +#score, #highScore{font-size:18px;text-shadow:0 0 5px #00ffff} \ No newline at end of file diff --git a/games/neon-tunnel/index.html b/games/neon-tunnel/index.html new file mode 100644 index 00000000..d81cdfed --- /dev/null +++ b/games/neon-tunnel/index.html @@ -0,0 +1,64 @@ + + + + + + Neon Tunnel โ€ข Mini JS Games Hub + + + +
    +
    +

    Neon Tunnel

    +

    Fly through a glowing tunnel โ€” avoid black barriers. Survive as long as possible.

    +
    + + + + + +
    + +
    +
    Score: 0
    +
    Highscore: 0
    +
    Multiplier: 1x
    +
    + +
    +
    Controls: Arrow keys / A & D / Mouse (move)
    +
    Tip: Higher speed = more points but harder obstacles
    +
    + + +
    + +
    +
    + + +
    + +
    + +
    +
    +
    + + + + diff --git a/games/neon-tunnel/script.js b/games/neon-tunnel/script.js new file mode 100644 index 00000000..c8b99c90 --- /dev/null +++ b/games/neon-tunnel/script.js @@ -0,0 +1,359 @@ +/* Neon Tunnel - advanced + Uses Canvas to create glowing tunnel lanes, obstacles, player ship, sound effects. +*/ + +(() => { + // Canvas setup + const canvas = document.getElementById('gameCanvas'); + const ctx = canvas.getContext('2d', {alpha: true}); + let W = canvas.width = 800; + let H = canvas.height = 600; + + // UI elements + const startBtn = document.getElementById('startBtn'); + const pauseBtn = document.getElementById('pauseBtn'); + const resumeBtn = document.getElementById('resumeBtn'); + const restartBtn = document.getElementById('restartBtn'); + const overlay = document.getElementById('overlay'); + const overlayTitle = document.getElementById('overlayTitle'); + const overlayText = document.getElementById('overlayText'); + const overlayResume = document.getElementById('overlayResume'); + const overlayRestart = document.getElementById('overlayRestart'); + const scoreEl = document.getElementById('score'); + const highscoreEl = document.getElementById('highscore'); + const multEl = document.getElementById('mult'); + const speedRange = document.getElementById('speedRange'); + const muteToggle = document.getElementById('muteToggle'); + + // Sounds (online links). If any blocked, mute toggle helps. + const sfx = { + crash: new Audio('https://cdn.pixabay.com/download/audio/2021/08/04/audio_9a8f3c1330.mp3?filename=arcade-explosion-6042.mp3'), + pass: new Audio('https://cdn.pixabay.com/download/audio/2021/08/04/audio_ba5fd7b532.mp3?filename=arcade-confirmation-6053.mp3'), + bg: new Audio('https://cdn.pixabay.com/download/audio/2021/08/18/audio_47a8ff7d11.mp3?filename=cyberpunk-background-loop-115070.mp3') + }; + sfx.bg.loop = true; + sfx.bg.volume = 0.35; + sfx.pass.volume = 0.8; + sfx.crash.volume = 0.9; + + // Game variables + let running = false; + let paused = false; + let speed = parseFloat(speedRange.value); // base speed + let frame = 0; + let score = 0; + let highscore = parseInt(localStorage.getItem('neonTunnelHigh') || 0); + let multiplier = 1; + highscoreEl.textContent = highscore; + + // Player + const player = { x: W/2, y: H*0.7, r: 12 }; + + // Obstacles array + let obstacles = []; + const laneCount = 5; + + // Resize handler to keep canvas crisp + function resize() { + const ratio = Math.min(window.innerWidth*0.9, 900); + canvas.style.width = ratio + 'px'; + canvas.style.height = (ratio * (H/W)) + 'px'; + } + window.addEventListener('resize', resize); + resize(); + + // Utilities + function rand(min,max){return Math.random()*(max-min)+min} + + // Input + let pointerX = null; + window.addEventListener('mousemove', e => { + const rect = canvas.getBoundingClientRect(); + pointerX = (e.clientX - rect.left) * (canvas.width / rect.width); + }); + window.addEventListener('touchmove', e => { + const t = e.touches[0]; + const rect = canvas.getBoundingClientRect(); + pointerX = (t.clientX - rect.left) * (canvas.width / rect.width); + }); + + window.addEventListener('keydown', (e) => { + if(!running) return; + if(e.key === 'ArrowLeft' || e.key.toLowerCase()==='a') player.x -= 40; + if(e.key === 'ArrowRight' || e.key.toLowerCase()==='d') player.x += 40; + clampPlayer(); + }); + + function clampPlayer() { + player.x = Math.max(40, Math.min(W-40, player.x)); + } + + // Tunnel rendering helpers + function drawGlowRect(x,y,w,h, color, blur=30, alpha=0.9){ + ctx.save(); + ctx.globalAlpha = alpha; + ctx.shadowColor = color; + ctx.shadowBlur = blur; + ctx.fillStyle = color; + ctx.fillRect(x,y,w,h); + ctx.restore(); + } + + // Generate obstacle + function spawnObstacle() { + const laneWidth = W / laneCount; + const lane = Math.floor(rand(0,laneCount)); + const w = laneWidth * (rand(0.6, 0.95)); + const x = lane * laneWidth + (laneWidth - w) / 2; + const h = rand(24, 80); + obstacles.push({x, y: -h, w, h, passed:false}); + } + + // Game loop + let lastTime = performance.now(); + function loop(now) { + if(!running || paused) return; + const dt = (now - lastTime) / (1000/60); + lastTime = now; + frame++; + // update speed dynamic + const gameSpeed = speed + (frame/500); + // move player toward pointer for smooth + if(pointerX !== null) { + player.x += (pointerX - player.x) * 0.12; + clampPlayer(); + } + + // spawn obstacles randomly + if(frame % Math.max(14, Math.floor(60 - gameSpeed*3)) === 0) spawnObstacle(); + + // update obstacles + obstacles.forEach(o => { + o.y += gameSpeed * dt * 0.9; + // passed check + if(!o.passed && o.y > player.y + player.r + 10){ + o.passed = true; + score += Math.floor(10 * multiplier); + if(!muteToggle.checked) sfx.pass.currentTime=0, sfx.pass.play().catch(()=>{}); + // increase multiplier gradually + if(score % 100 === 0) multiplier = Math.min(5, multiplier + 0.2); + } + }); + // remove offscreen + obstacles = obstacles.filter(o => o.y < H + 120); + + // collision detection + for(const o of obstacles){ + if(rectCircleColliding(o, player)){ + endGame(); + return; + } + } + + // draw + render(gameSpeed); + // update UI + scoreEl.textContent = score; + multEl.textContent = multiplier.toFixed(1) + 'x'; + // next frame + requestAnimationFrame(loop); + } + + function rectCircleColliding(rect, circle) { + const distX = Math.abs(circle.x - rect.x - rect.w/2); + const distY = Math.abs(circle.y - rect.y - rect.h/2); + if (distX > (rect.w/2 + circle.r)) return false; + if (distY > (rect.h/2 + circle.r)) return false; + if (distX <= (rect.w/2)) return true; + if (distY <= (rect.h/2)) return true; + const dx = distX - rect.w/2; + const dy = distY - rect.h/2; + return (dx*dx + dy*dy <= (circle.r*circle.r)); + } + + // render scene + function render(speedVal) { + // clear + ctx.clearRect(0,0,W,H); + // background gradient stripes - give tunnel movement + const bands = 12; + for(let i=0;i { + // outer glow + ctx.save(); + ctx.shadowBlur = 28; + ctx.shadowColor = 'rgba(0,0,0,0.9)'; + ctx.fillStyle = '#000'; + ctx.fillRect(o.x-6, o.y-6, o.w+12, o.h+12); + ctx.restore(); + + // inner neon edge + ctx.save(); + ctx.shadowBlur = 18; + ctx.shadowColor = 'rgba(255,0,200,0.9)'; + ctx.fillStyle = 'rgba(20,20,20,1)'; + ctx.fillRect(o.x, o.y, o.w, o.h); + // small glowing rim + ctx.globalCompositeOperation = 'lighter'; + ctx.fillStyle = 'rgba(0,255,230,0.08)'; + ctx.fillRect(o.x-2, o.y-2, o.w+4, 4); + ctx.restore(); + }); + + // player ship glowing + ctx.save(); + ctx.translate(player.x, player.y); + // glow + ctx.beginPath(); + ctx.fillStyle = 'rgba(0,255,200,0.06)'; + ctx.shadowBlur = 40; + ctx.shadowColor = 'rgba(0,255,200,0.9)'; + ctx.arc(0,0,28,0,Math.PI*2); + ctx.fill(); + // ship body + ctx.globalCompositeOperation = 'lighter'; + ctx.fillStyle = 'rgba(255,255,255,0.9)'; + ctx.beginPath(); + ctx.moveTo(-14,12); + ctx.lineTo(14,12); + ctx.lineTo(0,-18); + ctx.closePath(); + ctx.fill(); + // center core + ctx.beginPath(); + ctx.fillStyle = 'rgba(255,20,200,0.9)'; + ctx.arc(0,0,6,0,Math.PI*2); + ctx.fill(); + ctx.restore(); + + // HUD glow effect across bottom + ctx.save(); + ctx.fillStyle = 'rgba(255,255,255,0.02)'; + ctx.fillRect(0, H - 60, W, 60); + ctx.restore(); + } + + // start / pause / resume / restart + function startGame() { + if(!running){ + running = true; + paused = false; + frame = 0; score = 0; multiplier = 1; obstacles = []; + if(!muteToggle.checked) sfx.bg.play().catch(()=>{}); + overlay.classList.add('hidden'); + lastTime = performance.now(); + requestAnimationFrame(loop); + } + } + + function pauseGame() { + if(!running) return; + paused = true; + overlay.classList.remove('hidden'); + overlayTitle.textContent = 'Paused'; + overlayText.textContent = 'Game is paused'; + if(!muteToggle.checked) sfx.bg.pause(); + } + + function resumeGame() { + if(!running) return; + paused = false; + overlay.classList.add('hidden'); + if(!muteToggle.checked) sfx.bg.play().catch(()=>{}); + lastTime = performance.now(); + requestAnimationFrame(loop); + } + + function endGame() { + running = false; + paused = false; + if(!muteToggle.checked) { + sfx.bg.pause(); + sfx.crash.currentTime = 0; sfx.crash.play().catch(()=>{}); + } + overlay.classList.remove('hidden'); + overlayTitle.textContent = 'Crashed'; + overlayText.textContent = `Score: ${score}`; + // update highscore + if(score > highscore) { + highscore = score; + localStorage.setItem('neonTunnelHigh', highscore); + highscoreEl.textContent = highscore; + } + // show restart & overlay options + } + + function restartGame() { + running = false; paused = false; + obstacles = []; frame = 0; score = 0; multiplier = 1; + if(!muteToggle.checked) { sfx.bg.pause(); sfx.bg.currentTime = 0; } + overlay.classList.add('hidden'); + scoreEl.textContent = score; + requestAnimationFrame(loop); + startGame(); + } + + // event wiring + startBtn.addEventListener('click', () => { startGame(); }); + pauseBtn.addEventListener('click', () => { pauseGame(); }); + resumeBtn.addEventListener('click', () => { resumeGame(); }); + restartBtn.addEventListener('click', () => { restartGame(); }); + + overlayResume.addEventListener('click', () => { resumeGame(); }); + overlayRestart.addEventListener('click', () => { restartGame(); }); + + speedRange.addEventListener('input', (e) => { + speed = parseFloat(e.target.value); + }); + + muteToggle.addEventListener('change', () => { + if(muteToggle.checked){ + for(const k in sfx) sfx[k].pause(); + } else { + if(running && !paused) sfx.bg.play().catch(()=>{}); + } + }); + + // track main loop scoring increments + setInterval(() => { + if(running && !paused){ + score += Math.floor(1 * multiplier); + } + }, 500); + + // initial draw frame + render(0); + + // expose for debugging (optional) + window.neonTunnel = { startGame, pauseGame, resumeGame, restartGame }; + +})(); diff --git a/games/neon-tunnel/style.css b/games/neon-tunnel/style.css new file mode 100644 index 00000000..cae3d6af --- /dev/null +++ b/games/neon-tunnel/style.css @@ -0,0 +1,50 @@ +:root{ + --bg:#05030a; + --accent1:#00ffe7; + --accent2:#ff00ff; + --accent3:#00a3ff; + --card: rgba(255,255,255,0.03); + --glass: rgba(255,255,255,0.04); + --glass-2: rgba(255,255,255,0.02); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; +} + +*{box-sizing:border-box} +html,body{height:100%;margin:0;background:linear-gradient(180deg,#07060b 0%, #020103 100%);color:#e6f7ff} +.ui{display:flex;gap:24px;max-width:1200px;margin:28px auto;padding:20px} +.ui__left{flex:1;min-width:300px} +.ui__right{flex:1.3;display:flex;flex-direction:column;align-items:center} + +.title{font-size:28px;margin:0 0 6px;text-shadow:0 4px 18px rgba(0,255,230,0.08)} +.subtitle{margin:0 0 18px;color:#cdeff6} + +.controls{display:flex;flex-wrap:wrap;gap:8px 12px;margin-bottom:12px;align-items:center} +.btn{ + background:linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02)); + border:1px solid rgba(255,255,255,0.06); + padding:8px 12px;border-radius:10px;color:#e6fff9;cursor:pointer; + box-shadow:0 6px 22px rgba(0,0,0,0.6), 0 0 18px rgba(0,160,255,0.06) inset; +} +.btn:hover{transform:translateY(-2px);box-shadow:0 12px 34px rgba(0,160,255,0.08)} + +.label{display:flex;align-items:center;gap:10px;color:#cdeff6} +.label input[type=range]{width:140px} + +.stats{display:flex;gap:18px;margin:12px 0;color:#dff9ff} +.hints{color:#bfeff6;font-size:13px;margin-bottom:6px} + +.canvas-wrap{position:relative;background:radial-gradient(ellipse at center, rgba(0,0,0,0.2), transparent 40%), linear-gradient(135deg, rgba(0,160,255,0.04), rgba(255,0,200,0.02));border-radius:14px;padding:12px;box-shadow:0 10px 40px rgba(0,0,0,0.8)} +#gameCanvas{display:block;border-radius:10px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.2));width:100%;height:auto;max-width:900px;box-shadow: 0 6px 40px rgba(0,0,0,0.7) inset, 0 20px 60px rgba(0,0,0,0.6)} + +.overlay{position:absolute;inset:12px;display:flex;align-items:center;justify-content:center;backdrop-filter: blur(6px)} +.overlay.hidden{display:none} +.overlay__panel{background:linear-gradient(180deg, rgba(2,6,23,0.85), rgba(2,6,23,0.65));padding:24px;border-radius:12px;border:1px solid rgba(255,255,255,0.04);text-align:center} +.overlay__actions{display:flex;gap:10px;justify-content:center;margin-top:10px} + +.audio-controls{margin-top:8px;color:#bfeff6} + +.footer-credits{margin-top:18px;color:#98eaf4;font-size:12px} +@media (max-width:900px){ + .ui{flex-direction:column;padding:12px} + .ui__right{order:-1} +} diff --git a/games/number-cruncher/index.html b/games/number-cruncher/index.html new file mode 100644 index 00000000..c0f9586a --- /dev/null +++ b/games/number-cruncher/index.html @@ -0,0 +1,28 @@ + + + + + + Number Cruncher + + + +
    +

    ๐Ÿงฎ Number Cruncher

    +

    Solve math problems quickly to advance levels!

    +
    +
    Level: 1
    +
    Score: 0
    +
    Time: 30s
    +
    + + + +
    +
    + + +
    + + + \ No newline at end of file diff --git a/games/number-cruncher/script.js b/games/number-cruncher/script.js new file mode 100644 index 00000000..024304a3 --- /dev/null +++ b/games/number-cruncher/script.js @@ -0,0 +1,182 @@ +const startBtn = document.getElementById("start-btn"); +const submitBtn = document.getElementById("submit-btn"); +const nextBtn = document.getElementById("next-btn"); +const resetBtn = document.getElementById("reset-btn"); +const problemEl = document.getElementById("problem"); +const userInput = document.getElementById("user-input"); +const messageEl = document.getElementById("message"); +const levelEl = document.getElementById("current-level"); +const scoreEl = document.getElementById("current-score"); +const timerEl = document.getElementById("time-left"); + +let level = 1; +let score = 0; +let correctCount = 0; +let timer; +let timeLeft; +let currentAnswer; +let gameActive = false; + +startBtn.addEventListener("click", startGame); +submitBtn.addEventListener("click", checkAnswer); +nextBtn.addEventListener("click", nextLevel); +resetBtn.addEventListener("click", resetGame); +userInput.addEventListener("keypress", (e) => { + if (e.key === "Enter") { + checkAnswer(); + } +}); + +function startGame() { + level = 1; + score = 0; + correctCount = 0; + levelEl.textContent = level; + scoreEl.textContent = score; + startBtn.style.display = "none"; + gameActive = true; + generateProblem(); +} + +function generateProblem() { + const operations = getOperationsForLevel(level); + const op = operations[Math.floor(Math.random() * operations.length)]; + let num1, num2; + + switch (level) { + case 1: + num1 = Math.floor(Math.random() * 10) + 1; + num2 = Math.floor(Math.random() * 10) + 1; + break; + case 2: + num1 = Math.floor(Math.random() * 20) + 1; + num2 = Math.floor(Math.random() * 20) + 1; + break; + case 3: + num1 = Math.floor(Math.random() * 50) + 1; + num2 = Math.floor(Math.random() * 50) + 1; + break; + case 4: + num1 = Math.floor(Math.random() * 100) + 1; + num2 = Math.floor(Math.random() * 100) + 1; + break; + default: + num1 = Math.floor(Math.random() * 200) + 1; + num2 = Math.floor(Math.random() * 200) + 1; + } + + if (op === '/' && num1 % num2 !== 0) { + num1 = num2 * Math.floor(Math.random() * 10) + num2; // Make divisible + } + + let problem, answer; + switch (op) { + case '+': + problem = `${num1} + ${num2}`; + answer = num1 + num2; + break; + case '-': + if (num1 < num2) [num1, num2] = [num2, num1]; // Ensure positive result + problem = `${num1} - ${num2}`; + answer = num1 - num2; + break; + case '*': + problem = `${num1} ร— ${num2}`; + answer = num1 * num2; + break; + case '/': + problem = `${num1} รท ${num2}`; + answer = num1 / num2; + break; + } + + problemEl.textContent = problem; + currentAnswer = answer; + userInput.value = ""; + userInput.focus(); + messageEl.textContent = ""; + + timeLeft = Math.max(30 - (level - 1) * 5, 10); // Decrease time with level + timerEl.textContent = timeLeft; + + clearInterval(timer); + timer = setInterval(() => { + timeLeft--; + timerEl.textContent = timeLeft; + if (timeLeft <= 0) { + clearInterval(timer); + wrongAnswer(); + } + }, 1000); +} + +function getOperationsForLevel(lvl) { + if (lvl === 1) return ['+', '-']; + if (lvl === 2) return ['+', '-', '*']; + return ['+', '-', '*', '/']; +} + +function checkAnswer() { + if (!gameActive) return; + const userAnswer = parseFloat(userInput.value); + if (isNaN(userAnswer)) return; + + if (Math.abs(userAnswer - currentAnswer) < 0.01) { // For division + correctAnswer(); + } else { + wrongAnswer(); + } +} + +function correctAnswer() { + clearInterval(timer); + score += level * 10 + timeLeft; // Bonus for speed + scoreEl.textContent = score; + correctCount++; + messageEl.textContent = "โœ… Correct!"; + submitBtn.style.display = "none"; + if (correctCount >= 5) { // 5 correct to advance level + nextBtn.style.display = "inline-block"; + messageEl.textContent += " Level complete!"; + } else { + setTimeout(generateProblem, 1000); + } +} + +function wrongAnswer() { + clearInterval(timer); + messageEl.textContent = `โŒ Wrong! Answer: ${currentAnswer}`; + submitBtn.style.display = "none"; + nextBtn.style.display = "inline-block"; + nextBtn.textContent = "Try Again"; + gameActive = false; +} + +function nextLevel() { + if (correctCount >= 5) { + level++; + levelEl.textContent = level; + correctCount = 0; + } + nextBtn.style.display = "none"; + submitBtn.style.display = "inline-block"; + gameActive = true; + generateProblem(); +} + +function resetGame() { + clearInterval(timer); + level = 1; + score = 0; + correctCount = 0; + levelEl.textContent = level; + scoreEl.textContent = score; + messageEl.textContent = ""; + problemEl.textContent = ""; + userInput.value = ""; + startBtn.style.display = "inline-block"; + submitBtn.style.display = "inline-block"; + nextBtn.style.display = "none"; + resetBtn.style.display = "none"; + gameActive = false; +} \ No newline at end of file diff --git a/games/number-cruncher/style.css b/games/number-cruncher/style.css new file mode 100644 index 00000000..5355c96d --- /dev/null +++ b/games/number-cruncher/style.css @@ -0,0 +1,77 @@ +* { + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: linear-gradient(135deg, #ff6b6b, #4ecdc4); + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + margin: 0; + color: #fff; +} + +.container { + background: rgba(0, 0, 0, 0.3); + padding: 30px; + border-radius: 20px; + text-align: center; + width: 400px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4); +} + +h1 { + margin-bottom: 15px; + font-size: 2rem; + color: #fff; + text-shadow: 1px 1px 3px rgba(0,0,0,0.5); +} + +#game-area { + margin: 20px 0; +} + +#problem { + font-size: 2rem; + margin: 20px 0; + font-weight: bold; +} + +input { + width: 80%; + padding: 10px; + font-size: 18px; + text-align: center; + border: none; + border-radius: 8px; + margin: 10px 0; +} + +button { + background: #2ecc71; + color: #fff; + border: none; + border-radius: 8px; + padding: 10px 20px; + font-size: 16px; + cursor: pointer; + transition: background 0.3s; + margin: 5px; +} + +button:hover { + background: #27ae60; +} + +#level, #score, #timer { + font-size: 18px; + margin: 5px 0; +} + +#message { + margin: 10px 0; + font-size: 16px; + min-height: 20px; +} \ No newline at end of file diff --git a/games/number-game/index.html b/games/number-game/index.html new file mode 100644 index 00000000..c21a975c --- /dev/null +++ b/games/number-game/index.html @@ -0,0 +1,23 @@ + + + + + + Number Game + + + +
    +

    Number Game

    +

    Tap numbers in ascending order! Find and click 1, then 2, then 3, and so on.

    +
    +
    Time: 30
    +
    Next: 1
    + +
    + +
    +
    + + + \ No newline at end of file diff --git a/games/number-game/script.js b/games/number-game/script.js new file mode 100644 index 00000000..1ce8e5e7 --- /dev/null +++ b/games/number-game/script.js @@ -0,0 +1,134 @@ +// Number Game Script +// Tap numbers in ascending order + +var canvas = document.getElementById('gameCanvas'); +var ctx = canvas.getContext('2d'); +var timerDisplay = document.getElementById('timer'); +var scoreDisplay = document.getElementById('score'); +var restartBtn = document.getElementById('restart'); +var messageDiv = document.getElementById('message'); + +var numbers = []; +var currentTarget = 1; +var score = 0; +var timeLeft = 30; +var timerInterval; +var gameRunning = true; + +// Number class +function NumberObj(value, x, y) { + this.value = value; + this.x = x; + this.y = y; + this.size = 25; + this.clicked = false; +} + +// Initialize the game +function initGame() { + numbers = []; + currentTarget = 1; + score = 0; + timeLeft = 30; + gameRunning = true; + messageDiv.textContent = ''; + scoreDisplay.textContent = 'Next: ' + currentTarget; + + // Create numbers 1-20 + var positions = []; + for (var i = 1; i <= 20; i++) { + var x, y; + do { + x = Math.random() * (canvas.width - 100) + 50; + y = Math.random() * (canvas.height - 100) + 50; + } while (positions.some(function(pos) { + return Math.sqrt((pos.x - x) ** 2 + (pos.y - y) ** 2) < 60; + })); + positions.push({ x: x, y: y }); + numbers.push(new NumberObj(i, x, y)); + } + + startTimer(); + draw(); +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + for (var i = 0; i < numbers.length; i++) { + var num = numbers[i]; + if (!num.clicked) { + ctx.fillStyle = '#e65100'; + ctx.beginPath(); + ctx.arc(num.x, num.y, num.size, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = '#fff'; + ctx.stroke(); + + ctx.fillStyle = '#fff'; + ctx.font = '20px Arial'; + ctx.textAlign = 'center'; + ctx.fillText(num.value, num.x, num.y + 7); + } + } +} + +// Handle canvas click +canvas.addEventListener('click', function(event) { + if (!gameRunning) return; + var rect = canvas.getBoundingClientRect(); + var x = event.clientX - rect.left; + var y = event.clientY - rect.top; + + for (var i = 0; i < numbers.length; i++) { + var num = numbers[i]; + if (!num.clicked) { + var dx = x - num.x; + var dy = y - num.y; + var distance = Math.sqrt(dx * dx + dy * dy); + if (distance < num.size) { + if (num.value === currentTarget) { + num.clicked = true; + currentTarget++; + score = currentTarget - 1; + scoreDisplay.textContent = 'Next: ' + currentTarget; + messageDiv.textContent = 'Good!'; + messageDiv.style.color = 'green'; + setTimeout(function() { + messageDiv.textContent = ''; + }, 500); + draw(); + } else { + messageDiv.textContent = 'Wrong number!'; + messageDiv.style.color = 'red'; + setTimeout(function() { + messageDiv.textContent = ''; + }, 1000); + } + break; + } + } + } +}); + +// Start the timer +function startTimer() { + clearInterval(timerInterval); + timerInterval = setInterval(function() { + timeLeft--; + timerDisplay.textContent = 'Time: ' + timeLeft; + if (timeLeft <= 0) { + clearInterval(timerInterval); + gameRunning = false; + messageDiv.textContent = 'Time\'s up! Highest number: ' + score; + messageDiv.style.color = 'orange'; + } + }, 1000); +} + +// Restart button +restartBtn.addEventListener('click', initGame); + +// Start the game +initGame(); \ No newline at end of file diff --git a/games/number-game/style.css b/games/number-game/style.css new file mode 100644 index 00000000..71893e32 --- /dev/null +++ b/games/number-game/style.css @@ -0,0 +1,53 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #fff3e0; +} + +.container { + text-align: center; +} + +h1 { + color: #e65100; +} + +.game-info { + margin-bottom: 20px; + display: flex; + justify-content: center; + gap: 20px; +} + +#timer, #score { + font-size: 24px; + font-weight: bold; +} + +#restart { + padding: 10px 20px; + font-size: 16px; + background-color: #e65100; + color: white; + border: none; + cursor: pointer; +} + +#restart:hover { + background-color: #bf360c; +} + +canvas { + border: 2px solid #e65100; + background-color: #ffffff; +} + +#message { + margin-top: 20px; + font-size: 18px; + font-weight: bold; +} \ No newline at end of file diff --git a/games/number-lock/index.html b/games/number-lock/index.html new file mode 100644 index 00000000..90c5224c --- /dev/null +++ b/games/number-lock/index.html @@ -0,0 +1,101 @@ + + + + + + Number Lock Challenge โ€” Mini JS Games Hub + + + + +
    +
    +
    +

    ๐Ÿ” Number Lock Challenge

    +

    Solve mini-puzzles to reveal the digits and unlock the code.

    +
    +
    +
    +
    Level 1
    +
    Score 0
    +
    Hints 3
    +
    +
    + + + + +
    +
    +
    + +
    +
    +
    + lock image +
    + +
    +
    + +
    +
    + +
    --:--
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +

    Press Start to play

    +
    + +
    + +

    Youโ€™ll be presented with mini-puzzles. Solve to reveal code digits.

    +
    + +
    + + + +
    +
    +
    + + + +
    + Assets: Unsplash & Google Actions Sounds (public). Developed for Mini JS Games Hub. +
    +
    + + + + diff --git a/games/number-lock/script.js b/games/number-lock/script.js new file mode 100644 index 00000000..341581ae --- /dev/null +++ b/games/number-lock/script.js @@ -0,0 +1,519 @@ +/* Number Lock Challenge + - multi-puzzle levels + - reveals digits when puzzles solved + - timer, pause, restart, hints, scoring + - uses online assets for sounds/images +*/ + +(() => { + // DOM + const startBtn = document.getElementById("start-btn"); + const pauseBtn = document.getElementById("pause-btn"); + const resumeBtn = document.getElementById("resume-btn"); + const restartBtn = document.getElementById("restart-btn"); + const hintBtn = document.getElementById("hint-btn"); + const submitBtn = document.getElementById("submit-puzzle"); + const puzzleTitle = document.getElementById("puzzle-title"); + const puzzleBody = document.getElementById("puzzle-body"); + const feedbackEl = document.getElementById("feedback"); + const codeDisplay = document.getElementById("code-display"); + const levelEl = document.getElementById("level"); + const scoreEl = document.getElementById("score"); + const hintsEl = document.getElementById("hints"); + const timerEl = document.getElementById("timer"); + const progressBar = document.getElementById("progress-bar"); + const soundToggle = document.getElementById("sound-toggle"); + + // sounds (online sources) + const SND = { + correct: new Audio("https://actions.google.com/sounds/v1/cartoon/clang_and_wobble.ogg"), + wrong: new Audio("https://actions.google.com/sounds/v1/cartoon/slide_whistle_to_drum_hit.ogg"), + reveal: new Audio("https://actions.google.com/sounds/v1/alarms/beep_short.ogg"), + win: new Audio("https://actions.google.com/sounds/v1/alarms/digital_watch_alarm_short.ogg"), + tick: new Audio("https://actions.google.com/sounds/v1/alarms/alarm_clock.ogg"), + }; + // small utility to play + function playSound(name) { + if (!soundToggle.checked) return; + const s = SND[name]; + if (!s) return; + s.currentTime = 0; + s.play().catch(()=>{}); + } + + // Game state + let level = 1; + let totalScore = 0; + let hints = 3; + let code = []; // digits to reveal + let revealed = []; + let currentPuzzle = null; + let timer = null; + let pauseTimeLeft = 0; + let timeLeft = 0; + let isRunning = false; + let digitsToReveal = 4; + + // Config + const LEVEL_TIME_BASE = 60; // seconds base (reduces for higher difficulty) + const MAX_LEVEL = 20; + + // available puzzle generators + const puzzleGenerators = [ + mathPuzzle, + sequencePuzzle, + memoryPuzzle, + sliderPuzzle, + logicPuzzle, + binaryPuzzle + ]; + + // helpers + function randomInt(min, max){ return Math.floor(Math.random()*(max-min+1))+min; } + + // setup code display + function buildCodeDisplay(len){ + codeDisplay.innerHTML = ""; + for(let i=0;i{ + if (!isRunning) return; + timeLeft--; + updateTimerUI(); + if (timeLeft<=0){ + clearInterval(timer); + onTimeUp(); + } + },1000); + } + function pauseGame(){ + isRunning=false; + pauseBtn.disabled=true; + resumeBtn.disabled=false; + playSound("tick"); + } + function resumeGame(){ + isRunning=true; + pauseBtn.disabled=false; + resumeBtn.disabled=true; + } + function updateTimerUI(){ + const mm = Math.floor(timeLeft/60).toString().padStart(2,'0'); + const ss = (timeLeft%60).toString().padStart(2,'0'); + timerEl.textContent = `${mm}:${ss}`; + const pct = Math.max(0, Math.min(100, Math.round(((code.length - unrevealedCount())/code.length)*100))); + progressBar.style.width = pct + "%"; + } + + function unrevealedCount(){ return code.filter((d,i)=>!revealed[i]).length; } + + // Level flow + function startLevel(){ + isRunning = true; + startBtn.disabled = true; + pauseBtn.disabled = false; + restartBtn.disabled = false; + resumeBtn.disabled = true; + levelEl.textContent = level; + hintsEl.textContent = hints; + // set code length and digits to reveal by level + digitsToReveal = 4 + Math.floor((level-1)/5); // increases slowly + const len = Math.min(6, digitsToReveal); // cap length + code = generateCode(len); + revealed = Array(len).fill(false); + buildCodeDisplay(len); + // pick puzzle types for this level: number equals digits to reveal (1 puzzle can reveal 1 or more digits) + // We'll present puzzles sequentially until all digits revealed + queuePuzzlesForLevel(level, len); + // set timer: base minus difficulty boost + const levelTime = Math.max(20, LEVEL_TIME_BASE - (level*2 + (len*6))); + startTimer(levelTime); + generateNextPuzzle(); + updateTimerUI(); + } + + // When time runs out + function onTimeUp(){ + isRunning=false; + feedback("โฑ๏ธ Time up! Level failed.", true); + playSound("wrong"); + // penalty + totalScore = Math.max(0, totalScore - 20); + scoreEl.textContent = totalScore; + startBtn.disabled = false; + pauseBtn.disabled = true; + resumeBtn.disabled = true; + } + + // Queue puzzles + let puzzleQueue = []; + function queuePuzzlesForLevel(lev, len){ + puzzleQueue = []; + // choose random puzzles (at least len puzzles), each with possible revealCount 1..2 + const want = len; + for(let i=0;i=6) puzzleQueue.push({gen: puzzleGenerators[4], reveal:2}); + if (lev>=10) puzzleQueue.push({gen: puzzleGenerators[5], reveal:2}); + } + + // generate next puzzle from queue + function generateNextPuzzle(){ + if (!isRunning) return; + // check if all digits revealed + if (revealed.every(v=>v===true)){ + onLevelComplete(); + return; + } + // pop next + if (puzzleQueue.length===0) { + // refill simple puzzles + queuePuzzlesForLevel(level, code.length); + } + currentPuzzle = puzzleQueue.shift(); + const difficultyFactor = Math.floor((level-1)/3); + const puzzle = currentPuzzle.gen({level, difficultyFactor}); + renderPuzzle(puzzle); + } + + // sample puzzle renderers + function renderPuzzle(p){ + puzzleTitle.textContent = p.title; + puzzleBody.innerHTML = ""; // clear + + // content depends on type + const container = document.createElement("div"); + container.className = "puzzle-card"; + if (p.type === "math") { + const q = document.createElement("div"); + q.innerHTML = `${p.question}`; + const input = document.createElement("input"); + input.type="number"; input.className="puzzle-input"; input.id="puzzle-input"; + input.placeholder="Answer"; + container.appendChild(q); container.appendChild(input); + submitBtn.disabled=false; + submitBtn.onclick = () => { + const val = input.value.trim(); + if (val === "") return feedback("Enter an answer first.", true); + if (String(p.answer) === String(val)) { + onPuzzleSolved(p); + } else onPuzzleFailed(p); + }; + } else if (p.type === "sequence"){ + const q = document.createElement("div"); + q.innerHTML = `${p.question}`; + const input = document.createElement("input"); + input.type="number"; input.className="puzzle-input"; input.id="puzzle-input"; + input.placeholder="Next number"; + container.appendChild(q); container.appendChild(input); + submitBtn.disabled=false; + submitBtn.onclick = () => { + const val = input.value.trim(); + if (String(p.answer) === String(val)) onPuzzleSolved(p); else onPuzzleFailed(p); + }; + } else if (p.type === "memory"){ + // show sequence briefly then ask + const img = document.createElement("img"); + img.src = "https://source.unsplash.com/400x240/?memory,brain"; + img.className = "puzzle-illu"; + container.appendChild(img); + const seqDiv = document.createElement("div"); + seqDiv.innerHTML = `

    Memorize this sequence:

    `; + const seqSpan = document.createElement("div"); + seqSpan.style.fontSize="26px"; seqSpan.style.letterSpacing="6px"; + seqSpan.style.marginTop="8px"; seqSpan.textContent = p.sequence.join(" "); + seqDiv.appendChild(seqSpan); + container.appendChild(seqDiv); + // show for short time + submitBtn.disabled=true; + setTimeout(()=>{ + seqSpan.textContent = "โ€ข โ€ข โ€ข โ€ข โ€ข"; + const input = document.createElement("input"); + input.type="text"; input.className="puzzle-input"; input.id="puzzle-input"; + input.placeholder="Type the sequence separated by space"; + container.appendChild(input); + submitBtn.disabled=false; + submitBtn.onclick = () => { + const val = input.value.trim(); + const normalized = val.split(/\s+/).map(s=>s.trim()).filter(Boolean); + if (normalized.join(",") === p.sequence.join(",")) onPuzzleSolved(p); else onPuzzleFailed(p); + }; + }, Math.max(1000, 2500 - p.level*50)); + } else if (p.type === "slider"){ + const q = document.createElement("div"); + q.innerHTML = `

    ${p.question}

    `; + const slider = document.createElement("input"); + slider.type="range"; slider.min="0"; slider.max="100"; slider.value="50"; + slider.id = "puzzle-input"; + container.appendChild(q); container.appendChild(slider); + const check = document.createElement("div"); check.style.marginTop="8px"; check.style.fontSize="13px"; check.style.color="var(--muted)"; + check.textContent = "Adjust to correct tile value and submit"; + container.appendChild(check); + submitBtn.disabled=false; + submitBtn.onclick = () => { + // check closeness + const val = Number(slider.value); + if (Math.abs(val - p.answer) <= p.tolerance) onPuzzleSolved(p); else onPuzzleFailed(p); + }; + } else if (p.type === "logic"){ + const q = document.createElement("div"); + q.innerHTML = `${p.question}`; + const input = document.createElement("input"); + input.type="text"; input.className="puzzle-input"; input.id="puzzle-input"; + input.placeholder="Answer (single digit)"; + container.appendChild(q); container.appendChild(input); + submitBtn.disabled=false; + submitBtn.onclick = () => { + const val = input.value.trim().toLowerCase(); + if (String(p.answer) === val) onPuzzleSolved(p); else onPuzzleFailed(p); + }; + } else if (p.type === "binary"){ + const q = document.createElement("div"); + q.innerHTML = `

    ${p.question}

    `; + const input = document.createElement("input"); + input.type="text"; input.className="puzzle-input"; input.id="puzzle-input"; + input.placeholder="Decimal value"; + container.appendChild(q); container.appendChild(input); + submitBtn.disabled=false; + submitBtn.onclick = () => { + const val = input.value.trim(); + if (String(p.answer) === String(val)) onPuzzleSolved(p); else onPuzzleFailed(p); + }; + } else { + container.innerHTML = `

    Unknown puzzle

    `; + submitBtn.disabled=true; + } + + // image or hint area + const img = document.createElement("img"); + img.src = p.image || "https://source.unsplash.com/420x240/?puzzle,challenge"; + img.className = "puzzle-illu"; + puzzleBody.appendChild(img); + puzzleBody.appendChild(container); + feedbackEl.textContent = ""; + } + + // feedback + function feedback(msg, isError=false){ + feedbackEl.textContent = msg; + feedbackEl.style.color = isError ? '#ffb3b3' : '#b8ffd6'; + if (!isError) playSound("correct"); else playSound("wrong"); + setTimeout(()=>{ if (feedbackEl.textContent===msg) feedbackEl.textContent=""; }, 3000); + } + + function onPuzzleSolved(p){ + // reveal random unrevealed digit positions equal to p.reveal + let toReveal = p.reveal || 1; + let indices = []; + for(let i=0;iMath.random()-0.5); + const revealIndices = indices.slice(0,toReveal); + revealIndices.forEach(idx=>revealDigit(idx)); + // scoring: base + speed bonus + const scoreGain = 20 + (p.difficulty*5) + (Math.max(0, Math.floor(timeLeft/10))); + totalScore += scoreGain; + scoreEl.textContent = totalScore; + feedback("โœ”๏ธ Correct! Digit revealed."); + playSound("correct"); + // small delay then next + setTimeout(()=> generateNextPuzzle(), 800); + } + + function onPuzzleFailed(p){ + feedback("โŒ Incorrect. Try next puzzle or use a hint.", true); + totalScore = Math.max(0, totalScore - 8); scoreEl.textContent = totalScore; + playSound("wrong"); + // allow moving on + setTimeout(()=> generateNextPuzzle(), 900); + } + + function onLevelComplete(){ + isRunning=false; + clearInterval(timer); + playSound("win"); + feedback("๐ŸŽ‰ Level complete! Digits unlocked.", false); + totalScore += 50 + level*10; + scoreEl.textContent = totalScore; + // advance level + level = Math.min(MAX_LEVEL, level+1); + levelEl.textContent = level; + startBtn.disabled=false; + pauseBtn.disabled=true; + resumeBtn.disabled=true; + } + + // hints + hintBtn.addEventListener("click", ()=> { + if (!isRunning) return feedback("Start the level first.", true); + if (hints<=0) return feedback("No hints left.", true); + // reveal one digit partially (reveal one unrevealed index) + const idx = code.findIndex((d,i)=>!revealed[i]); + if (idx === -1) return feedback("Nothing to hint.", true); + hints--; + hintsEl.textContent = hints; + // reveal but mark reduced score + revealDigit(idx); + totalScore = Math.max(0, totalScore - 15); + scoreEl.textContent = totalScore; + playSound("reveal"); + feedback("Hint used โ€” a digit was revealed (score penalty)."); + }); + + // Start / pause / resume / restart handlers + startBtn.addEventListener("click", ()=>{ + // reset some game state on fresh start + if (!isRunning && startBtn.disabled===false){ + // keep current level and hints/score + isRunning=true; + startLevel(); + startBtn.disabled=true; + } else if (!isRunning){ + // resume if paused + resumeGame(); + } + }); + + pauseBtn.addEventListener("click", ()=>{ + if (!isRunning) return; + pauseGame(); + }); + resumeBtn.addEventListener("click", ()=>{ + resumeGame(); + }); + restartBtn.addEventListener("click", ()=>{ + // reset everything for the current level + isRunning=false; + clearInterval(timer); + totalScore = Math.max(0,Math.floor(totalScore/2)); + scoreEl.textContent = totalScore; + hints = 3; + hintsEl.textContent = hints; + level = 1; + levelEl.textContent = level; + startBtn.disabled=false; + pauseBtn.disabled=true; + resumeBtn.disabled=true; + restartBtn.disabled=true; + codeDisplay.innerHTML = ""; + puzzleBody.innerHTML = `

    Press Start to play

    `; + playSound("wrong"); + }); + + // puzzle generators + function mathPuzzle({level,difficultyFactor}){ + const op = ["+","-","*"][randomInt(0,2)]; + const a= randomInt(1,10 + level*2); + const b= randomInt(1,6 + difficultyFactor*3); + let q,ans; + if (op==="*"){ q=`${a} ร— ${b}`; ans = a*b; } + else if (op==="+"){ q=`${a} + ${b}`; ans=a+b; } + else { q=`${a} - ${b}`; ans=a-b; } + return {type:"math",title:"Math Puzzle",question:`Compute: ${q}`, answer:ans, image:`https://source.unsplash.com/420x240/?math,calculation`, difficulty:1+difficultyFactor, reveal: currentPuzzle?.reveal || 1,level}; + } + + function sequencePuzzle({level,difficultyFactor}){ + // arithmetic or Fibonacci-like + const base = randomInt(1,6 + difficultyFactor*3); + const step = randomInt(1,5 + difficultyFactor*2); + const len = 4; + const arr = Array.from({length:len},(_,i)=> base + i*step); + const question = `Sequence: ${arr.join(", ")} โ€” what is next?`; + const answer = arr[arr.length-1] + step; + return {type:"sequence",title:"Pattern Sequence",question,answer,image:`https://source.unsplash.com/420x240/?sequence,pattern`,difficulty:1+difficultyFactor,reveal: currentPuzzle?.reveal || 1,level}; + } + + function memoryPuzzle({level,difficultyFactor}){ + const count = Math.min(6,3 + difficultyFactor + (level>6?1:0)); + const seq = Array.from({length:count},()=>String(randomInt(0,9))); + return {type:"memory",title:"Memory Recall",sequence:seq,answer:seq, image:`https://source.unsplash.com/420x240/?memory,brain`, difficulty:1+difficultyFactor,reveal: currentPuzzle?.reveal || 1,level}; + } + + function sliderPuzzle({level,difficultyFactor}){ + // answer is a value near midpoint, tolerance depends on level + const answer = randomInt(20,80); + const tol = Math.max(2, 10 - difficultyFactor*2); + return {type:"slider",title:"Slider Match",question:`Adjust the slider to match the hidden value (0โ€“100)`,answer, tolerance:tol, image:`https://source.unsplash.com/420x240/?slider,control`,difficulty:1+difficultyFactor,reveal: currentPuzzle?.reveal || 1,level}; + } + + function logicPuzzle({level,difficultyFactor}){ + // simple riddles that map to a digit + const riddlePool = [ + {q:"I have four legs in the morning, two at noon, and three at night. What is the first digit of that count?", a:"4"}, + {q:"How many sides does a triangle have?", a:"3"}, + {q:"How many wheels on a tricycle?", a:"3"}, + {q:"How many months have 31 days?", a:"7"}, + {q:"How many continents are there on Earth?", a:"7"}, + {q:"If you roll a standard die, what's the highest face value?", a:"6"} + ]; + const pick = riddlePool[randomInt(0, riddlePool.length-1)]; + return {type:"logic",title:"Logic Riddle",question:pick.q,answer:pick.a,image:`https://source.unsplash.com/420x240/?riddle,logic`,difficulty:1+difficultyFactor,reveal: currentPuzzle?.reveal || 1,level}; + } + + function binaryPuzzle({level,difficultyFactor}){ + const val = randomInt(1,15 + difficultyFactor*5); + const bin = val.toString(2); + return {type:"binary",title:"Binary Decode",question:`Convert binary ${bin} to decimal`,answer:val,image:`https://source.unsplash.com/420x240/?binary,code`,difficulty:1+difficultyFactor,reveal: currentPuzzle?.reveal || 1,level}; + } + + // initial UI setup + function initUI(){ + // code display empty + codeDisplay.innerHTML = ''; + // attract glow + const digits = document.querySelectorAll(".code-digit"); + digits.forEach((d,i)=> d.style.transition = "all 400ms ease"); + // bind keyboard: Enter to submit if input focused + document.addEventListener("keydown",(e)=>{ + if (e.key === "Enter"){ + if (!submitBtn.disabled) submitBtn.click(); + } + }); + + pauseBtn.disabled=true; + resumeBtn.disabled=true; + restartBtn.disabled=true; + } + + // start + initUI(); + + // expose for debug (optional) + window.__NL = { + startLevel, generateNextPuzzle, onLevelComplete + }; +})(); diff --git a/games/number-lock/style.css b/games/number-lock/style.css new file mode 100644 index 00000000..e89dedb3 --- /dev/null +++ b/games/number-lock/style.css @@ -0,0 +1,54 @@ +:root{ + --bg: #0f1724; + --card: #0b1220; + --accent: #ff9f1c; + --accent-2: #6ee7b7; + --muted: #98a2b3; + --glass: rgba(255,255,255,0.04); + --glow: 0 6px 24px rgba(159,108,255,0.12); + --glass-2: rgba(255,255,255,0.03); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; +} + +*{box-sizing:border-box} +html,body{height:100%;margin:0;background: +linear-gradient(180deg,#071029 0%, #071826 60%), url('https://source.unsplash.com/1600x900/?technology,pattern') center/cover no-repeat fixed; color:#e6eef8} +.nl-app{max-width:1200px;margin:28px auto;padding:20px;display:grid;grid-template-columns:1fr 320px;gap:22px;align-items:start} +.nl-header{grid-column:1/-1;display:flex;justify-content:space-between;align-items:center;padding:18px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));border-radius:14px;box-shadow:var(--glow)} +.nl-title{margin:0;font-size:20px;display:flex;gap:10px;align-items:center} +.nl-sub{margin:0;color:var(--muted);font-size:13px} +.nl-controls{display:flex;flex-direction:column;gap:10px;align-items:flex-end} +.nl-stats{display:flex;gap:14px;align-items:center;font-weight:600} +.nl-stats div{background:var(--glass);padding:8px 12px;border-radius:10px;color:var(--accent-2);font-size:13px} +.btn{background:transparent;border:1px solid rgba(255,255,255,0.06);padding:8px 12px;border-radius:8px;color:inherit;cursor:pointer;font-weight:700} +.btn.primary{background:linear-gradient(90deg,var(--accent),#ff6b6b);border:none;color:#08101a;box-shadow:0 6px 18px rgba(255,159,28,0.18)} +.btn.danger{background:transparent;border:1px solid rgba(255,80,80,0.18);color:#ffb3b3} +.nl-main{background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.05));padding:18px;border-radius:12px;box-shadow:var(--glow)} +.lock-area{display:flex;gap:18px;align-items:center} +.lock-visual{position:relative;width:420px;height:260px;border-radius:10px;overflow:hidden;border:1px solid rgba(255,255,255,0.03);background:linear-gradient(180deg, rgba(0,0,0,0.25), rgba(255,255,255,0.01))} +.lock-image{width:100%;height:100%;object-fit:cover;filter:contrast(0.9) saturate(1.05) brightness(.9)} +.code-display{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:flex;gap:10px;backdrop-filter:blur(6px);padding:10px;border-radius:12px;background:linear-gradient(90deg, rgba(0,0,0,0.45), rgba(255,255,255,0.02));box-shadow:0 8px 30px rgba(0,0,0,0.6)} +.code-digit{width:56px;height:66px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:28px;font-weight:800;background:linear-gradient(180deg, rgba(255,255,255,0.03), rgba(0,0,0,0.3));color:#fff;border:1px solid rgba(255,255,255,0.06);box-shadow:0 6px 18px rgba(0,0,0,0.5)} +.code-digit.hidden{color:transparent;letter-spacing:8px;position:relative} +.code-digit.hidden::after{content:"โ€ข";color:rgba(255,255,255,0.2);font-size:32px;position:absolute} +.code-digit.revealed{background:linear-gradient(90deg,var(--accent-2),#fff);color:#07101a} +.timer-panel{min-width:240px;padding:12px;border-radius:10px;background:var(--glass);display:flex;flex-direction:column;gap:12px} +.timer-row{display:flex;justify-content:space-between;align-items:center;font-weight:700} +#timer{font-size:20px;color:var(--accent-2)} +.progress{background:var(--glass-2);height:12px;border-radius:8px;overflow:hidden} +.progress-bar{height:100%;width:0%;background:linear-gradient(90deg,var(--accent),#ff6b6b)} +.puzzle-area{margin-top:20px;display:block;padding:12px;border-radius:12px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.04))} +.puzzle-header h2{margin:6px 0 12px 0;color:var(--accent)} +.puzzle-body{min-height:180px;display:flex;gap:12px;align-items:center} +.puzzle-illu{width:220px;height:140px;object-fit:cover;border-radius:10px;box-shadow:0 10px 30px rgba(4,6,10,0.6)} +.puzzle-instruction{flex:1;color:var(--muted)} +.puzzle-actions{display:flex;gap:12px;align-items:center;margin-top:12px} +.feedback{min-width:220px;color:var(--accent-2);font-weight:700} +.nl-side{padding:16px;border-radius:12px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.02))} +.level-legend h3,.sound-controls h3,.instructions h3{margin:0 0 8px 0;color:var(--muted)} +.nl-footer{grid-column:1/-1;color:var(--muted);text-align:center;margin-top:10px} +@media(max-width:980px){ + .nl-app{grid-template-columns:1fr; padding:12px} + .nl-main{order:2} + .nl-side{order:1} +} diff --git a/games/number-weave/index.html b/games/number-weave/index.html new file mode 100644 index 00000000..0ae031a0 --- /dev/null +++ b/games/number-weave/index.html @@ -0,0 +1,89 @@ + + + + + + Number Weave โ€” Mini JS Games Hub + + + +
    +
    +
    +
    ๐Ÿ”ข
    +
    +

    Number Weave

    +

    Connect numbers in increasing order โ€” lines must not overlap.

    +
    +
    + +
    + + + + + + +
    +
    + +
    + + +
    +
    + +
    +
    + + +
    +
    +
    + + + + diff --git a/games/number-weave/script.js b/games/number-weave/script.js new file mode 100644 index 00000000..eb84d4aa --- /dev/null +++ b/games/number-weave/script.js @@ -0,0 +1,474 @@ +/* Number Weave โ€” script.js + - No external audio/images required (sounds generated with WebAudio). + - Place this file as games/number-weave/script.js +*/ + +(() => { + // ---------- Utilities ---------- + function $(sel, root = document) { return root.querySelector(sel); } + function $all(sel, root = document) { return Array.from(root.querySelectorAll(sel)); } + function rand(min, max) { return Math.random() * (max - min) + min; } + function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); } + + // Geometry helpers (segment intersection) + function orientation(a,b,c){ + return (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y); + } + function onSegment(a,b,c){ + return Math.min(a.x,b.x) <= c.x + 1e-9 && c.x <= Math.max(a.x,b.x) + 1e-9 && + Math.min(a.y,b.y) <= c.y + 1e-9 && c.y <= Math.max(a.y,b.y) + 1e-9; + } + function segmentsIntersect(p1,p2,p3,p4){ + // Classic robust check + const o1 = Math.sign(orientation(p1,p2,p3)); + const o2 = Math.sign(orientation(p1,p2,p4)); + const o3 = Math.sign(orientation(p3,p4,p1)); + const o4 = Math.sign(orientation(p3,p4,p2)); + if (o1 !== o2 && o3 !== o4) return true; + if (o1 === 0 && onSegment(p1,p2,p3)) return true; + if (o2 === 0 && onSegment(p1,p2,p4)) return true; + if (o3 === 0 && onSegment(p3,p4,p1)) return true; + if (o4 === 0 && onSegment(p3,p4,p2)) return true; + return false; + } + + // Check segment intersects rectangle (obstacle) + function segmentIntersectsRect(p1,p2,rect){ + // rect: {x,y,w,h} + const r1 = {x:rect.x, y:rect.y}, r2={x:rect.x+rect.w, y:rect.y}; + const r3 = {x:rect.x, y:rect.y+rect.h}, r4={x:rect.x+rect.w, y:rect.y+rect.h}; + return segmentsIntersect(p1,p2,r1,r2) || segmentsIntersect(p1,p2,r3,r4) || + segmentsIntersect(p1,p2,r1,r3) || segmentsIntersect(p1,p2,r2,r4) || + // or entirely inside rect? + (p1.x >= rect.x && p1.x <= rect.x+rect.w && p1.y >= rect.y && p1.y <= rect.y+rect.h) || + (p2.x >= rect.x && p2.x <= rect.x+rect.w && p2.y >= rect.y && p2.y <= rect.y+rect.h); + } + + // ---------- DOM ---------- + const board = $('#board'); + const canvas = $('#canvas'); + const pinsContainer = $('#pins'); + const startBtn = $('#start-btn'); + const pauseBtn = $('#pause-btn'); + const restartBtn = $('#restart-btn'); + const levelSelect = $('#level-select'); + const nextNumberEl = $('#next-number'); + const movesEl = $('#moves'); + const timerEl = $('#timer'); + const messageEl = $('#message'); + const undoBtn = $('#undo-btn'); + const hintBtn = $('#hint-btn'); + const clearBtn = $('#clear-btn'); + const soundToggle = $('#sound-toggle'); + const vibrateToggle = $('#vibrate-toggle'); + const bestEl = $('#best'); + + // Canvas sizing + function resizeCanvas(){ + canvas.width = board.clientWidth; + canvas.height = board.clientHeight; + canvas.style.width = board.clientWidth + 'px'; + canvas.style.height = board.clientHeight + 'px'; + drawAll(); + } + window.addEventListener('resize', () => requestAnimationFrame(resizeCanvas)); + + // WebAudio simple sound generator + const AudioCtx = window.AudioContext || window.webkitAudioContext; + const audioCtx = AudioCtx ? new AudioCtx() : null; + function playTone(freq=440, time=0.08, type='sine', gain=0.15){ + if (!audioCtx || !soundToggle.checked) return; + const o = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + o.type = type; o.frequency.value = freq; + g.gain.value = gain; + o.connect(g); g.connect(audioCtx.destination); + o.start(); + g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + time); + setTimeout(()=>{ try{o.stop(); o.disconnect(); g.disconnect();}catch(e){} }, time*1000 + 50); + } + function playSuccess(){ playTone(880,0.12,'sine',0.12); playTone(1320,0.06,'sine',0.06); } + function playError(){ playTone(160,0.12,'sawtooth',0.12); } + function playWin(){ playTone(1000,0.16,'triangle',0.18); playTone(1400,0.2,'sine',0.12); playTone(1800,0.06,'square',0.06); } + + // ---------- Game State ---------- + let pins = []; // {id,num,x,y,el,connected} + let obstacles = []; // {x,y,w,h,el} + let lines = []; // [{a:pinIndex,b:pinIndex,from:{x,y},to:{x,y}}] + let nextIndex = 1; + let moves = 0; + let started = false; + let paused = false; + let timer = 0; + let timerId = null; + let levelCfg = {1:6, 2:9, 3:12, 4:16}; + let ctx = canvas.getContext('2d'); + + // Local storage bests + function bestKey(level){ return `number-weave-best-l${level}`; } + function setBest(level, val){ localStorage.setItem(bestKey(level), String(val)); } + function getBest(level){ return Number(localStorage.getItem(bestKey(level)) || 0); } + function updateBestDisplay(){ + bestEl.textContent = getBest(levelSelect.value) || 'โ€”'; + } + + // ---------- Board creation ---------- + function clearBoardDOM(){ + pinsContainer.innerHTML = ''; + // remove obstacles DOM too + board.querySelectorAll('.obstacle').forEach(n => n.remove()); + } + + function createObstaclesForLevel(level, w, h){ + // Procedural obstacles: different for each level + const out = []; + if(level==1){ + // no obstacles for easy + return out; + } + const counts = {2:2,3:3,4:5}[level] || 2; + for(let i=0;i candidate.x >= r.x-18 && candidate.x <= r.x + r.w + 18 && + candidate.y >= r.y-18 && candidate.y <= r.y + r.h + 18)) continue; + // avoid too close to other pins + const tooClose = pins.some(p => Math.hypot(p.x - x, p.y - y) < 68); + if (tooClose) continue; + pins.push({id:`p${n}`, num:n, x, y, connected:false}); + placed = true; + } + if (tries > triesLimit) console.warn('placement tries exceeded'); + } + // shuffle positions a little to create different layouts + return pins; + } + + function renderPins(){ + pins.forEach(p => { + const el = document.createElement('button'); + el.className = 'pin'; + el.innerText = p.num; + el.style.left = p.x + 'px'; + el.style.top = p.y + 'px'; + el.setAttribute('data-num', p.num); + el.setAttribute('aria-label', `Number ${p.num}`); + pinsContainer.appendChild(el); + p.el = el; + el.addEventListener('click', (e) => { + if (!started || paused) return; + handlePinClick(p); + }); + // touch-friendly + el.addEventListener('touchstart', (e) => { + if (!started || paused) return; + e.preventDefault(); + handlePinClick(p); + }, {passive:false}); + }); + } + + // ---------- Drawing ---------- + function clearCanvas(){ + ctx.clearRect(0,0,canvas.width,canvas.height); + } + + function drawAll(){ + clearCanvas(); + // draw existing lines + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + lines.forEach((ln, idx) => { + const gradient = ctx.createLinearGradient(ln.from.x, ln.from.y, ln.to.x, ln.to.y); + gradient.addColorStop(0, 'rgba(124,58,237,0.98)'); + gradient.addColorStop(1, 'rgba(6,182,212,0.98)'); + ctx.strokeStyle = gradient; + ctx.lineWidth = 8; + ctx.shadowBlur = 20; + ctx.shadowColor = 'rgba(124,58,237,0.18)'; + ctx.beginPath(); + ctx.moveTo(ln.from.x, ln.from.y); + ctx.lineTo(ln.to.x, ln.to.y); + ctx.stroke(); + // subtle highlight + ctx.lineWidth = 2; + ctx.strokeStyle = 'rgba(255,255,255,0.08)'; + ctx.beginPath(); + ctx.moveTo(ln.from.x, ln.from.y); + ctx.lineTo(ln.to.x, ln.to.y); + ctx.stroke(); + }); + + // draw obstacles overlay + obstacles.forEach(ob => { + ctx.save(); + ctx.fillStyle = 'rgba(20,20,30,0.6)'; + ctx.fillRect(ob.x, ob.y, ob.w, ob.h); + ctx.restore(); + // slight gloss + ctx.save(); + ctx.fillStyle = 'rgba(255,255,255,0.02)'; + ctx.fillRect(ob.x, ob.y, ob.w, ob.h/2); + ctx.restore(); + }); + } + + // ---------- Game logic ---------- + function handlePinClick(pin){ + // if click is out of order -> error + if (pin.num !== nextIndex) { + message(`Wrong number โ€” tap ${nextIndex}`, 'danger'); + if (soundToggle.checked) playError(); + if (vibrateToggle.checked && navigator.vibrate) navigator.vibrate(80); + // visual hint flash + flashPin(pin); + return; + } + + // Build candidate line from prev to this + const from = (nextIndex === 1) ? null : {x: pins[nextIndex-2].x, y: pins[nextIndex-2].y}; + const to = {x: pin.x, y: pin.y}; + + // If this is not first connect: check crossing with existing lines and obstacles + if (from){ + // check intersects any existing lines + const candidate = {from, to}; + const crossesLine = lines.some(l => segmentsIntersect(candidate.from, candidate.to, l.from, l.to)); + const hitsObstacle = obstacles.some(o => segmentIntersectsRect(candidate.from, candidate.to, o)); + if (crossesLine || hitsObstacle){ + message('Line would overlap or hit obstacle โ€” choose another path', 'danger'); + if (soundToggle.checked) playError(); + if (vibrateToggle.checked && navigator.vibrate) navigator.vibrate([60,30,60]); + flashPin(pin); + return; + } + // if ok, push new line + lines.push({a: nextIndex-1, b: nextIndex, from: from, to: to}); + drawAll(); + } + + // mark pin connected + pin.connected = true; + pin.el.classList.add('connected'); + nextIndex++; + moves++; + updateHUD(); + + // celebration + if (soundToggle.checked) playSuccess(); + if (nextIndex > pins.length) { + // won! + endGame(true); + } else { + message(`Good โ€” now tap ${nextIndex}`, 'ok'); + } + } + + // Undo last line + function undo(){ + if (!lines.length) return; + const last = lines.pop(); + // mark last pin disconnected + const bIndex = last.b - 1; // pin index + pins[bIndex].connected = false; + pins[bIndex].el.classList.remove('connected','hint'); + nextIndex = last.b; + moves++; + updateHUD(); + drawAll(); + } + undoBtn.addEventListener('click', () => { if (!started || paused) return; undo(); }); + + // Clear all connections + clearBtn.addEventListener('click', () => { + if (!started) return; + lines = []; + pins.forEach(p => p.connected = false); + $all('.pin').forEach(el => el.classList.remove('connected','hint')); + nextIndex = 1; moves = 0; + updateHUD(); + drawAll(); + message('Cleared connections', 'info'); + }); + + // Hint: highlight next pin + function showHint(){ + if (!started || paused) return; + const p = pins[nextIndex-1]; + if (!p) return; + // animate glow + $all('.pin').forEach(el => el.classList.remove('hint')); + p.el.classList.add('hint'); + message(`Hint: tap ${p.num}`, 'info'); + if (soundToggle.checked) playTone(660,0.06,'sine',0.09); + setTimeout(()=> p.el.classList.remove('hint'), 1800); + } + hintBtn.addEventListener('click', showHint); + + // ---------- Timer & HUD ---------- + function startTimer(){ stopTimer(); timer = 0; timerId = setInterval(()=>{ timer++; updateTimer(); }, 1000); } + function stopTimer(){ if(timerId) { clearInterval(timerId); timerId=null; } } + function updateTimer(){ + const mm = String(Math.floor(timer/60)).padStart(2,'0'); + const ss = String(timer%60).padStart(2,'0'); + timerEl.textContent = `${mm}:${ss}`; + } + + function updateHUD(){ + movesEl.textContent = moves; + nextNumberEl.textContent = (nextIndex <= pins.length) ? nextIndex : 'โ€”'; + updateBestDisplay(); + drawAll(); + } + + // Messages + function message(txt, type='info'){ + messageEl.textContent = txt; + messageEl.style.color = (type==='danger') ? 'var(--danger)' : (type==='ok') ? '#a7f3d0' : 'var(--muted)'; + } + function flashPin(pin){ + pin.el.classList.add('hint'); + setTimeout(()=>pin.el.classList.remove('hint'),600); + } + + // ---------- Start / Pause / Restart ---------- + startBtn.addEventListener('click', () => { + if (!started) initGame(); + else if (paused) resumeGame(); + }); + + pauseBtn.addEventListener('click', () => { + if (!started) return; + paused = !paused; + pauseBtn.textContent = paused ? 'Resume' : 'Pause'; + message(paused ? 'Paused' : 'Resumed', 'info'); + if (paused) stopTimer(); else startTimer(); + }); + + restartBtn.addEventListener('click', ()=> { + if (!started) { initGame(); return; } + buildBoard(); + }); + + // End game (win or stop) + function endGame(win){ + started = false; + stopTimer(); + if (win){ + message('๐ŸŽ‰ Completed! Great weaving!', 'ok'); + if (soundToggle.checked) playWin(); + // record best time (lower is better) or moves + const level = levelSelect.value; + const prev = getBest(level) || 0; + const score = Math.max(1, Math.floor(10000 / (timer+1)) + Math.max(0, 200 - moves)); // composite + if (score > prev){ + setBest(level, score); + message('New best! ๐ŸŽ‰', 'ok'); + } + } else { + message('Game stopped', 'info'); + } + updateHUD(); + } + + // ---------- Build / Reset logic ---------- + function buildBoard(){ + // reset + lines = []; + moves = 0; + nextIndex = 1; + started = true; + paused = false; + pauseBtn.textContent = 'Pause'; + message('Game in progress โ€” tap numbers in order', 'info'); + + // size + resizeCanvas(); + const w = canvas.width, h = canvas.height; + // obstacles + obstacles = createObstaclesForLevel(Number(levelSelect.value), w, h); + clearBoardDOM(); + obstacles.forEach(addObstacleDOM); + // pins + pins = generatePins(levelCfg[levelSelect.value] || 9); + renderPins(); + updateHUD(); + // start timer + startTimer(); + } + + function initGame(){ + // unlock audio context by playing small sound on first user gesture + if (audioCtx && audioCtx.state === 'suspended') { + audioCtx.resume(); + } + buildBoard(); + } + + // ---------- Initialization ---------- + function init(){ + // setup canvas size + resizeCanvas(); + // wire some button states + pauseBtn.disabled = false; + restartBtn.disabled = false; + updateBestDisplay(); + message('Choose level and press Start', 'info'); + + // keyboard shortcuts + window.addEventListener('keydown', (e) => { + if (e.key === ' '){ // space toggles pause + e.preventDefault(); + pauseBtn.click(); + } else if (e.key === 'z' && (e.ctrlKey || e.metaKey)){ // undo + e.preventDefault(); undo(); + } + }); + } + + // attach misc listeners + hintBtn.addEventListener('click', () => { showHint(); }); + clearBtn.addEventListener('click', () => { clearBtn.click(); }); // already attached + undoBtn.addEventListener('click', () => { undo(); }); + + // initial call + init(); + + // expose small debug + window.NW = { + getState: () => ({pins, lines, obstacles, nextIndex, moves, timer}), + reset: () => buildBoard() + }; + +})(); diff --git a/games/number-weave/style.css b/games/number-weave/style.css new file mode 100644 index 00000000..e5963de3 --- /dev/null +++ b/games/number-weave/style.css @@ -0,0 +1,148 @@ +:root{ + --bg:#0f1724; + --card:#0b1220; + --muted:#9aa6b2; + --accent:#7c3aed; /* violet */ + --accent-2:#06b6d4; /* teal */ + --danger:#ef4444; + --glass: rgba(255,255,255,0.03); + --glow: 0 8px 30px rgba(124,58,237,0.25); + --pin-size:46px; + --pin-font:600 16px/1 "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + color-scheme: dark; +} + +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + background: radial-gradient(1200px 600px at 10% 10%, rgba(12,18,35,0.6), transparent), + radial-gradient(1000px 400px at 90% 90%, rgba(6,182,212,0.08), transparent), + var(--bg); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; + color:#e6eef6; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + padding:28px; +} + +/* App layout */ +.nw-app{ + max-width:1200px; + margin:0 auto; + background: linear-gradient(180deg, rgba(255,255,255,0.02), transparent); + border-radius:14px; + padding:18px; + box-shadow: 0 10px 40px rgba(2,6,23,0.6), var(--glow); + display:flex; + flex-direction:column; + gap:12px; +} + +/* Header */ +.nw-header{ + display:flex; + justify-content:space-between; + align-items:center; + gap:16px; +} +.nw-title{display:flex;gap:12px;align-items:center} +.nw-icon{font-size:34px;background:linear-gradient(45deg,var(--accent),var(--accent-2));padding:10px;border-radius:10px;box-shadow:0 6px 20px rgba(7,10,25,0.6);display:flex;align-items:center;justify-content:center} +.nw-title h1{margin:0;font-size:20px} +.nw-title .sub{margin:0;color:var(--muted);font-size:13px} + +/* Controls */ +.nw-controls{display:flex;align-items:center;gap:10px} +.btn{ + background:var(--glass); + border:1px solid rgba(255,255,255,0.04); + color:var(--muted); + padding:8px 12px;border-radius:10px; + cursor:pointer;font-weight:600; +} +.btn.primary{background:linear-gradient(90deg,var(--accent),var(--accent-2)); color:white; box-shadow:var(--glow)} +.btn.danger{background:linear-gradient(90deg,var(--danger),#ff6b6b); color:white} +.btn:active{transform:translateY(1px)} +select{background:var(--glass); color:var(--muted); border-radius:8px; padding:8px; border:1px solid rgba(255,255,255,0.03)} + +/* Main layout */ +.nw-main{display:flex;gap:16px} +.nw-sidebar{width:240px;background:linear-gradient(180deg, rgba(255,255,255,0.02), transparent); padding:16px;border-radius:12px;border:1px solid rgba(255,255,255,0.02)} +.nw-board-wrap{flex:1;display:flex;flex-direction:column;gap:12px} +.nw-board{position:relative;height:640px;background:linear-gradient(180deg, rgba(255,255,255,0.01), rgba(0,0,0,0.2)); border-radius:12px; overflow:hidden; border:1px solid rgba(255,255,255,0.02)} + +/* Canvas - fills board */ +#canvas{position:absolute;top:0;left:0;width:100%;height:100%;display:block;} + +/* Pins container (numbers) */ +.pins{position:absolute;inset:0;pointer-events:none} + +/* pin style */ +.pin{ + position:absolute; + width:var(--pin-size); + height:var(--pin-size); + display:flex; + align-items:center; + justify-content:center; + border-radius:50%; + background:linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01)); + border: 2px solid rgba(255,255,255,0.06); + color:#dbeafe; + font:var(--pin-font); + cursor:pointer; + transform:translate(-50%,-50%); + box-shadow: 0 6px 20px rgba(2,6,23,0.6); + pointer-events:auto; + user-select:none; + transition: transform .14s ease, box-shadow .14s ease; +} + +/* glowing connected state */ +.pin.connected{ + background: linear-gradient(45deg, rgba(124,58,237,1), rgba(6,182,212,1)); + color:white; + box-shadow: 0 10px 30px rgba(124,58,237,0.28), 0 0 18px rgba(6,182,212,0.12) inset; + transform:scale(1.05); +} + +/* hint style */ +.pin.hint{ + box-shadow: 0 8px 30px rgba(124,58,237,0.2); + animation: pulse 1s infinite; +} +@keyframes pulse{ + 0%{transform:translate(-50%,-50%) scale(1)} + 50%{transform:translate(-50%,-50%) scale(1.08)} + 100%{transform:translate(-50%,-50%) scale(1)} +} + +/* obstacles */ +.obstacle{ + position:absolute; + border-radius:6px; + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.35)); + border:1px solid rgba(255,255,255,0.03); + box-shadow: inset 0 -8px 30px rgba(0,0,0,0.4); + pointer-events:none; + opacity:0.95; +} + +/* footer/message */ +.nw-footer{display:flex;justify-content:space-between;align-items:center;margin-top:6px} +.message{color:var(--muted);font-weight:600} +.action-row{display:flex;gap:8px} + +/* responsive */ +@media (max-width:980px){ + .nw-main{flex-direction:column} + .nw-sidebar{width:100%;order:2} + .nw-board-wrap{order:1} + .nw-board{height:520px} +} + +/* small helpers */ +.stat{display:flex;flex-direction:column;padding:8px 0;border-bottom:1px dashed rgba(255,255,255,0.01)} +.stat-label{color:var(--muted);font-size:12px} +.stat-value{font-weight:700;font-size:18px;margin-top:6px} +.tips{margin-top:12px;color:var(--muted);font-size:13px} diff --git a/games/ocean-cleaner/index.html b/games/ocean-cleaner/index.html new file mode 100644 index 00000000..2dc42f6c --- /dev/null +++ b/games/ocean-cleaner/index.html @@ -0,0 +1,64 @@ + + + + + + Ocean Cleaner โ€” Mini JS Games Hub + + + +
    +
    +

    Ocean Cleaner

    +
    + + + + โ† Back to Hub +
    +
    + +
    +
    + +
    + + +
    + +
    +
    Made with โค๏ธ โ€” Mini JS Games Hub
    +
    +
    + + + + diff --git a/games/ocean-cleaner/script.js b/games/ocean-cleaner/script.js new file mode 100644 index 00000000..a2b31f7f --- /dev/null +++ b/games/ocean-cleaner/script.js @@ -0,0 +1,655 @@ +/* Ocean Cleaner โ€” script.js + Canvas-based ocean-cleaner prototype with upgrades, pooling & touch controls. + Drop into games/ocean-cleaner/ and open index.html. +*/ + +/* ----------------- Utilities & Config ----------------- */ + +const cfg = { + canvasW: 960, canvasH: 540, + spawnInterval: 1200, // ms between trash spawns (will scale with difficulty) + creatureInterval: 4000, + maxTrash: 40, + maxCreatures: 6, + initialHealth: 3, + startingScore: 0, + upgradeCosts: { net: 500, speed: 800, magnet: 1200 }, + baseNetRadius: 32, + magnetRadius: 80 +}; + +const assets = { + // We'll attempt to load images โ€” but the game draws fallback shapes if images fail. + boat: { url: "https://images.unsplash.com/photo-1509395176047-4a66953fd231?auto=format&fit=crop&w=256&q=60" }, // decorative: unsplash + trash: { url: "https://cdn.pixabay.com/photo/2013/07/12/13/58/plastic-bottle-149319_1280.png" }, + creature: { url: "https://cdn.pixabay.com/photo/2016/03/31/23/31/shark-1297717_1280.png" }, + buoy: { url: "https://cdn.pixabay.com/photo/2012/04/13/00/24/buoy-31203_1280.png" } +}; + +const qs = (s) => document.querySelector(s); + +/* ----------------- DOM References ----------------- */ + +const canvas = qs("#gameCanvas"); +const ctx = canvas.getContext("2d", { alpha: true }); +canvas.width = cfg.canvasW; +canvas.height = cfg.canvasH; + +const scoreEl = qs("#score"); +const collectedEl = qs("#collected"); +const healthEl = qs("#health"); +const netEl = qs("#net"); +const timerEl = qs("#timer"); +const bestEl = qs("#best"); + +const btnRestart = qs("#btn-restart"); +const btnPause = qs("#btn-pause"); +const btnMute = qs("#btn-mute"); +const upgradeBtns = Array.from(document.querySelectorAll(".upgrade-btn")); + +/* ----------------- State ----------------- */ + +let running = true, paused = false, muted = false; +let lastSpawn = 0, lastCreature = 0, lastTime = 0; +let score = cfg.startingScore, collected = 0, health = cfg.initialHealth; +let netReady = true, netCooldown = 5000, usingNet = false, netTimer = 0; +let timer = 0, level = 1; +let bestScore = Number(localStorage.getItem("oc_best") || 0); +bestEl.textContent = bestScore; + +const playDataKey = "gamePlays"; // your hub uses this key + +/* ----------------- Input Handling ----------------- */ + +const input = { + x: canvas.width / 2, + y: canvas.height - 120, + left: false, right: false, up: false, down: false, + pointerDown: false, pointerId: null +}; + +function setupInput() { + window.addEventListener("keydown", (e) => { + if (e.key === "ArrowLeft" || e.key === "a") input.left = true; + if (e.key === "ArrowRight" || e.key === "d") input.right = true; + if (e.key === "ArrowUp" || e.key === "w") input.up = true; + if (e.key === "ArrowDown" || e.key === "s") input.down = true; + if (e.code === "Space") attemptNet(); + if (e.key === "p") togglePause(); + }); + window.addEventListener("keyup", (e) => { + if (e.key === "ArrowLeft" || e.key === "a") input.left = false; + if (e.key === "ArrowRight" || e.key === "d") input.right = false; + if (e.key === "ArrowUp" || e.key === "w") input.up = false; + if (e.key === "ArrowDown" || e.key === "s") input.down = false; + }); + + // Touch / pointer drag + canvas.addEventListener("pointerdown", (e) => { + input.pointerDown = true; + input.pointerId = e.pointerId; + movePointer(e); + }); + canvas.addEventListener("pointermove", (e) => { + if (!input.pointerDown || e.pointerId !== input.pointerId) return; + movePointer(e); + }); + window.addEventListener("pointerup", (e) => { + if (e.pointerId === input.pointerId) { + input.pointerDown = false; + input.pointerId = null; + } + }); + + function movePointer(e) { + const r = canvas.getBoundingClientRect(); + input.x = (e.clientX - r.left) * (canvas.width / r.width); + input.y = (e.clientY - r.top) * (canvas.height / r.height); + } +} + +/* ----------------- Sound: WebAudio synth (no external files) ----------------- */ + +const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); +let masterGain = audioCtx.createGain(); +masterGain.connect(audioCtx.destination); +masterGain.gain.value = 0.12; + +function sfxPlay(type = "collect") { + if (muted) return; + const now = audioCtx.currentTime; + if (type === "collect") { + const o = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + o.type = "sine"; o.frequency.setValueAtTime(880, now); + g.gain.setValueAtTime(0.0001, now); + g.gain.exponentialRampToValueAtTime(0.6, now + 0.01); + g.gain.exponentialRampToValueAtTime(0.0001, now + 0.25); + o.connect(g); g.connect(masterGain); + o.start(now); o.stop(now + 0.3); + } else if (type === "crash") { + const o = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + o.type = "sawtooth"; o.frequency.setValueAtTime(120, now); + g.gain.setValueAtTime(1, now); + g.gain.exponentialRampToValueAtTime(0.0001, now + 0.7); + o.connect(g); g.connect(masterGain); + o.start(now); o.stop(now + 0.7); + } else if (type === "win") { + // small arpeggio + const freqs = [600, 800, 1000]; + freqs.forEach((f, i) => { + const o = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + const t = now + i * 0.08; + o.type = "triangle"; o.frequency.setValueAtTime(f, t); + g.gain.setValueAtTime(0.0001, t); + g.gain.exponentialRampToValueAtTime(0.6, t + 0.01); + g.gain.exponentialRampToValueAtTime(0.0001, t + 0.22); + o.connect(g); g.connect(masterGain); + o.start(t); o.stop(t + 0.28); + }); + } +} + +/* ----------------- Object Pooling ----------------- */ + +function createPool(factory, size) { + const pool = []; + for (let i = 0; i < size; i++) pool.push(factory()); + return { + get() { return pool.find(p => !p.active) || (pool.push(factory()), pool[pool.length-1]); }, + all() { return pool; } + }; +} + +/* ----------------- Entities ----------------- */ + +function createTrash() { + return { + active: false, x: 0, y:0, vx:0, vy:0, r: 10, type:"plastic", value:10, + spawn(tx, ty, vx, vy, r, type, val) { + this.active = true; this.x = tx; this.y = ty; this.vx = vx; this.vy = vy; this.r = r; this.type = type; this.value = val; + }, + update(dt) { this.x += this.vx*dt; this.y += this.vy*dt; if (this.x < -50 || this.x > canvas.width+50 || this.y > canvas.height+80) this.active=false; } + }; +} + +function createCreature() { + return { + active:false, x:0,y:0,vx:0,vy:0,r:24, kind:"shark", + spawn(x,y,vx,vy,r,kind){ this.active=true; this.x=x; this.y=y; this.vx=vx; this.vy=vy; this.r=r; this.kind=kind; }, + update(dt){ this.x += this.vx*dt; this.y += this.vy*dt; if(this.x<-100||this.x>canvas.width+100||this.y>canvas.height+100) this.active=false; } + }; +} + +const trashPool = createPool(createTrash, cfg.maxTrash); +const creaturePool = createPool(createCreature, cfg.maxCreatures); + +/* ----------------- Boat (player) ----------------- */ + +const player = { + x: canvas.width/2, + y: canvas.height - 110, + vx: 0, vy: 0, + speed: 220, + width: 84, height: 32, + collectedItems: [], + netRadius: cfg.baseNetRadius, + magnetActive: false, + magnetTimer: 0, + draw(ctx) { + // Draw boat as rounded rectangle + flag; nicer than simple shape + ctx.save(); + ctx.translate(this.x, this.y); + // hull + ctx.fillStyle = "#0b78d1"; + roundRect(ctx, -42, -12, 84, 24, 8); + ctx.fill(); + // deck + ctx.fillStyle = "#ffffff"; + roundRect(ctx, -20, -10, 40, 16, 6); + ctx.fill(); + // small cabin + ctx.fillStyle = "#0b78d1"; + ctx.fillRect(8, -16, 18, 8); + // net visual when using + if (usingNet) { + ctx.beginPath(); + ctx.strokeStyle = "rgba(255,255,255,0.9)"; + ctx.lineWidth = 2; + ctx.arc(0, -6, this.netRadius, 0, Math.PI*2); + ctx.stroke(); + } + ctx.restore(); + }, + update(dt) { + // input-based movement (keyboard) โ€” but pointer drag overrides if pointerDown + let tx = this.x, ty = this.y; + if (input.pointerDown) { + // smooth follow pointer + const dx = input.x - this.x; + const dy = input.y - this.y; + this.vx = dx * 6; + this.vy = dy * 6; + } else { + this.vx = 0; this.vy = 0; + if (input.left) this.vx = -this.speed; + if (input.right) this.vx = this.speed; + if (input.up) this.vy = -this.speed; + if (input.down) this.vy = this.speed; + } + // move + this.x += this.vx * dt; + this.y += this.vy * dt; + + // keep within canvas area + this.x = Math.max(40, Math.min(canvas.width - 40, this.x)); + this.y = Math.max(120, Math.min(canvas.height - 30, this.y)); + + // magnet effect collects nearby trash automatically + if (this.magnetActive) { + this.magnetTimer -= dt*1000; + if (this.magnetTimer <= 0) this.magnetActive = false; + } + }, + attemptCollect(trash) { + // called when trash collides with boat/net + collected += 1; + score += trash.value; + sfxPlay("collect"); + trash.active = false; + this.collectedItems.push(trash.type); + } +}; + +function roundRect(ctx, x, y, w, h, r) { + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.arcTo(x + w, y, x + w, y + h, r); + ctx.arcTo(x + w, y + h, x, y + h, r); + ctx.arcTo(x, y + h, x, y, r); + ctx.arcTo(x, y, x + w, y, r); + ctx.closePath(); +} + +/* ----------------- Spawning logic ----------------- */ + +function spawnTrash() { + const t = trashPool.get(); + // spawn near top half at random x + const x = Math.random() * (canvas.width - 160) + 80; + const y = -20; + const vx = (Math.random() - 0.5) * 30 * (0.9 + level*0.05); + const vy = 30 + Math.random()*40 + level*3; + // assign types/values + const types = [ + {type:"plastic", r:12, value:10}, + {type:"bottle", r:14, value:20}, + {type:"metal", r:18, value:40}, + {type:"drum", r:24, value:80} + ]; + const pick = types[Math.floor(Math.random()*types.length)]; + t.spawn(x,y,vx,vy,pick.r,pick.type,pick.value); +} + +function spawnCreature() { + const c = creaturePool.get(); + const side = Math.random() < 0.5 ? -1 : 1; + const y = Math.random() * (canvas.height/2) + 40; + const x = side === -1 ? canvas.width + 80 : -80; + const vx = -side * (40 + Math.random()*80 + level*10); + const vy = Math.sin(Math.random()*Math.PI)*8; + const kind = Math.random() < 0.5 ? "shark" : "jelly"; + const r = kind === "shark" ? 28 : 20; + c.spawn(x,y,vx,vy,r,kind); +} + +/* ----------------- Collision helpers ----------------- */ + +function circleRectCollision(cx, cy, r, rx, ry, rw, rh) { + // circle (cx,cy,r) colliding with rect centered at rx,ry width rw height rh + const distX = Math.abs(cx - rx); + const distY = Math.abs(cy - ry); + if (distX > (rw/2 + r)) return false; + if (distY > (rh/2 + r)) return false; + if (distX <= (rw/2)) return true; + if (distY <= (rh/2)) return true; + const dx = distX - rw/2; + const dy = distY - rh/2; + return (dx*dx + dy*dy <= r*r); +} + +/* ----------------- Game Loop ----------------- */ + +function resetGame() { + // reset state + score = cfg.startingScore; + collected = 0; + health = cfg.initialHealth; + netReady = true; usingNet = false; netTimer = 0; + timer = 60; // single-level 60 seconds + level = 1; + // reset pools + trashPool.all().forEach(t=>t.active=false); + creaturePool.all().forEach(c=>c.active=false); + player.x = canvas.width/2; + player.y = canvas.height - 110; + player.collectedItems = []; + player.netRadius = cfg.baseNetRadius; + player.speed = 220; + player.magnetActive = false; + updateUI(); +} + +function updateUI() { + scoreEl.textContent = Math.floor(score); + collectedEl.textContent = collected; + healthEl.textContent = health; + netEl.textContent = netReady ? "Ready" : "Cooldown"; + timerEl.textContent = Math.max(0, Math.ceil(timer)) + "s"; + bestEl.textContent = bestScore; + // enable upgrades based on score + upgradeBtns.forEach(b=>{ + const key = b.dataset.upgrade; + const cost = cfg.upgradeCosts[key]; + b.disabled = score < cost; + b.textContent = `${b.textContent.split('(')[0].trim()} (${cost})`; + }); +} + +function togglePause() { + paused = !paused; + btnPause.textContent = paused ? "Resume" : "Pause"; + if (!paused) { + // keep lastTime consistent + lastTime = performance.now(); + requestAnimationFrame(loop); + } +} + +btnPause.addEventListener("click", togglePause); +btnRestart.addEventListener("click", () => { + resetGame(); + sfxPlay("win"); +}); +btnMute.addEventListener("click", () => { + muted = !muted; + btnMute.textContent = muted ? "๐Ÿ”‡" : "๐Ÿ”Š"; + if (muted) masterGain.gain.value = 0; else masterGain.gain.value = 0.12; +}); + +upgradeBtns.forEach(b=>{ + b.addEventListener("click", ()=>{ + const key = b.dataset.upgrade; + const cost = cfg.upgradeCosts[key]; + if (score < cost) return; + score -= cost; + if (key === "net") { + player.netRadius += 18; + } else if (key === "speed") { + player.speed += 80; + } else if (key === "magnet") { + player.magnetActive = true; + player.magnetTimer = 25*1000; // 25 sec + } + updateUI(); + }); +}); + +/* Track plays on the hub (so your hub Pro badges can pick it up). + We increment localStorage counter for this game when user hits Play (page load). */ +function trackHubPlay() { + try { + const key = "gamePlays"; + const existing = JSON.parse(localStorage.getItem(key) || "{}"); + const gameName = "Ocean Cleaner"; + if (!existing[gameName]) existing[gameName] = { plays: 0, success: 0 }; + existing[gameName].plays += 1; + localStorage.setItem(key, JSON.stringify(existing)); + } catch (e) { /* ignore */ } +} + +/* Attempt net: burst that collects nearby objects */ +function attemptNet() { + if (!netReady) return; + usingNet = true; + netReady = false; + netTimer = 0.6; // active 0.6s + setTimeout(()=> netReady = true, netCooldown); + // short magnet effect for net + player.magnetActive = true; + player.magnetTimer = 900; // in ms converted below to seconds logic +} + +/* Save best score to localStorage */ +function saveBestScore() { + if (score > bestScore) { + bestScore = Math.floor(score); + localStorage.setItem("oc_best", bestScore); + sfxPlay("win"); + } +} + +/* Main loop */ +function loop(now) { + if (!running || paused) return; + if (!lastTime) lastTime = now; + const dt = Math.min(0.05, (now - lastTime) / 1000); // cap dt for stability + lastTime = now; + + // spawn logic + lastSpawn += dt*1000; + if (lastSpawn >= Math.max(250, cfg.spawnInterval - level*40)) { + lastSpawn = 0; + spawnTrash(); + } + lastCreature += dt*1000; + if (lastCreature >= Math.max(1200, cfg.creatureInterval - level*120)) { + lastCreature = 0; + spawnCreature(); + } + + // update timer + timer -= dt; + if (timer <= 0) { + // end of level + level += 1; + timer = Math.max(30, 60 - level*4); // shorter each level + score += level * 100; + } + + // update player and entities + player.update(dt); + + // update trash + trashPool.all().forEach(t=>{ + if (!t.active) return; + t.update(dt); + // magnet auto-collect + if ((player.magnetActive && (Math.hypot(t.x-player.x, t.y-player.y) < cfg.magnetRadius)) || (usingNet && Math.hypot(t.x-player.x, t.y-player.y) < player.netRadius + t.r)) { + player.attemptCollect(t); + } + // collision with boat (no net) + if (circleRectCollision(t.x, t.y, t.r, player.x, player.y, player.width, player.height)) { + player.attemptCollect(t); + } + }); + + // update creatures + creaturePool.all().forEach(c=>{ + if (!c.active) return; + c.update(dt); + // collision with player + const d = Math.hypot(c.x-player.x, c.y-player.y); + if (d < c.r + 16) { + // hit! + c.active = false; + health -= 1; + score = Math.max(0, score - 40); + sfxPlay("crash"); + // drop half of collected items (simulate dropping) + const drop = Math.floor(player.collectedItems.length/2); + for (let i = 0; i < drop; i++) player.collectedItems.pop(); + } + }); + + // decrease net timer + if (usingNet) { + netTimer -= dt; + if (netTimer <= 0) usingNet = false; + } + + if (player.magnetActive) { + // magnet timer measured earlier in ms sometimes; normalize + player.magnetTimer -= dt*1000; + if (player.magnetTimer <= 0) player.magnetActive = false; + } + + // update UI + updateUI(); + + // check game over + if (health <= 0) { + running = false; + saveBestScore(); + // show end overlay + setTimeout(()=> { + alert(`Game Over! Score: ${Math.floor(score)} โ€” Best: ${bestScore}`); + resetGame(); running = true; lastTime = 0; + requestAnimationFrame(loop); + }, 80); + return; + } + + // clear canvas + ctx.clearRect(0,0,canvas.width,canvas.height); + drawScene(); + + lastTime = now; + requestAnimationFrame(loop); +} + +/* ----------------- Drawing ----------------- */ + +function drawScene() { + // draw water gradient + const g = ctx.createLinearGradient(0, 0, 0, canvas.height); + g.addColorStop(0, "#bfefff"); + g.addColorStop(0.6, "#89e0ff"); + g.addColorStop(1, "#4fc8e8"); + ctx.fillStyle = g; + ctx.fillRect(0,0,canvas.width,canvas.height); + + // gentle waves overlay + ctx.save(); + ctx.globalAlpha = 0.08; + for (let i=0;i<6;i++){ + ctx.beginPath(); + ctx.ellipse((i*180 + (Date.now()/40)%180), 80 + Math.sin(Date.now()/900+i)*8, 220, 60, 0, 0, Math.PI*2); + ctx.fillStyle = "#ffffff"; + ctx.fill(); + } + ctx.restore(); + + // draw buoys (deposit points) โ€” static along top + for (let i=0;i<3;i++){ + const bx = 120 + i*300; + const by = 80; + drawBuoy(bx, by); + } + + // draw trash + trashPool.all().forEach(t=>{ + if (!t.active) return; + drawTrash(t); + }); + + // draw creatures + creaturePool.all().forEach(c=>{ + if (!c.active) return; + drawCreature(c); + }); + + // draw player + player.draw(ctx); + + // draw HUD mini + ctx.save(); + ctx.fillStyle = "rgba(255,255,255,0.06)"; + ctx.fillRect(8,8,160,56); + ctx.restore(); +} + +function drawTrash(t) { + ctx.save(); + ctx.beginPath(); + // different shapes for types + if (t.type === "metal" || t.type === "drum") { + // barrel/drum + ctx.fillStyle = "#b07b3b"; + ctx.fillRect(t.x - t.r, t.y - t.r, t.r*2, t.r*1.2); + } else if (t.type === "bottle") { + ctx.fillStyle = "#9fd3ff"; + ctx.fillRect(t.x - t.r*0.6, t.y - t.r*1.2, t.r*1.2, t.r*2.0); + ctx.fillStyle = "#bfbfbf"; + ctx.fillRect(t.x - t.r*0.5, t.y - t.r*1.4, t.r*1.0, t.r*0.2); + } else { + // plastic / default: small circle + ctx.fillStyle = "#ffd24a"; + ctx.beginPath(); + ctx.arc(t.x, t.y, t.r, 0, Math.PI*2); + ctx.fill(); + } + ctx.restore(); +} + +function drawCreature(c) { + ctx.save(); + ctx.translate(c.x, c.y); + if (c.kind === "shark") { + ctx.fillStyle = "#6b7280"; + ctx.beginPath(); + ctx.moveTo(-c.r, 0); + ctx.quadraticCurveTo(0, -c.r*1.2, c.r, 0); + ctx.quadraticCurveTo(0, c.r*1.1, -c.r, 0); + ctx.fill(); + ctx.fillStyle = "#fff"; + ctx.fillRect(c.r*0.1, -6, c.r*0.6, 6); + } else { + // jelly-like + ctx.fillStyle = "#a24cf7"; + ctx.beginPath(); + ctx.arc(0, 0, c.r, Math.PI, 0); + ctx.fill(); + ctx.fillRect(-c.r, 0, c.r*2, c.r*0.8); + } + ctx.restore(); +} + +function drawBuoy(x,y) { + ctx.save(); + ctx.translate(x,y); + ctx.fillStyle = "#f97316"; + ctx.beginPath(); + ctx.moveTo(-14,16); ctx.lineTo(0,-12); ctx.lineTo(14,16); + ctx.closePath(); ctx.fill(); + ctx.fillStyle = "#fff"; ctx.fillRect(-6,10,12,14); + ctx.restore(); +} + +/* ----------------- Start / Load ----------------- */ + +function init() { + // resume audio on user gesture + window.addEventListener("click", () => { if (audioCtx.state === "suspended") audioCtx.resume(); }, { once: true }); + + setupInput(); + resetGame(); + trackHubPlay(); + lastTime = performance.now(); + requestAnimationFrame(loop); +} + +// start +init(); diff --git a/games/ocean-cleaner/style.css b/games/ocean-cleaner/style.css new file mode 100644 index 00000000..ca71e1c4 --- /dev/null +++ b/games/ocean-cleaner/style.css @@ -0,0 +1,45 @@ +:root{ + --bg:#e6f7ff; --panel:#ffffff; --accent:#0b78d1; --danger:#e74c3c; + --glass: rgba(255,255,255,0.8); + --muted:#6b7280; + --shadow: 0 6px 18px rgba(11,120,209,0.12); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; +} +*{box-sizing:border-box} +html,body{height:100%; margin:0; background:linear-gradient(180deg,#e6fbff 0%, #c5eefe 100%); color:#032a3b} +.oc-wrapper{max-width:1200px;margin:18px auto;padding:18px} +.oc-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px} +.oc-header h1{margin:0;font-size:1.6rem;letter-spacing:0.3px} +.oc-header-actions{display:flex;gap:8px;align-items:center} +.oc-header-actions button, .oc-header-actions a{background:var(--panel);border:1px solid rgba(0,0,0,0.06);padding:8px 10px;border-radius:8px;cursor:pointer;box-shadow:var(--shadow)} +.open-hub{ text-decoration:none;color:inherit } + +.oc-main{display:grid;grid-template-columns:1fr 320px;gap:16px} +.oc-canvas-wrap{background:linear-gradient(180deg, #bfe9ff, #9fdfff);border-radius:12px;padding:12px;box-shadow:var(--shadow);display:flex;align-items:center;justify-content:center} +canvas{display:block;width:100%;height:auto;border-radius:8px;background:linear-gradient(180deg,rgba(255,255,255,0.03),transparent);} + +.oc-ui{display:flex;flex-direction:column;gap:12px} +.panel{background:var(--panel);padding:12px;border-radius:12px;box-shadow:0 6px 18px rgba(2,6,23,0.06)} +.stat{display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px dashed rgba(0,0,0,0.04)} +.stat:last-child{border-bottom:none} +.stat strong{font-size:0.85rem;color:var(--muted)} +.stat div{font-size:1.2rem;font-weight:700;color:var(--accent)} + +.upgrades h3{margin:0 0 8px 0} +.upgrade-list{display:flex;flex-direction:column;gap:8px} +.upgrade-btn{padding:8px;border-radius:8px;border:1px solid rgba(0,0,0,0.06);background:linear-gradient(180deg,#ffffff,#f6fbff);cursor:pointer} +.upgrade-btn:disabled{opacity:0.5;cursor:not-allowed} +.save-note{margin-top:8px;color:var(--muted);font-size:0.9rem} + +.tips ul{margin:8px 0 0 16px;padding:0;color:var(--muted);font-size:0.9rem} +.tips h4{margin:0 0 6px 0} + +.oc-footer{margin-top:12px;text-align:center;color:var(--muted)} +@media (max-width:1000px){ + .oc-main{grid-template-columns:1fr 300px} +} +@media (max-width:820px){ + .oc-wrapper{padding:12px} + .oc-main{grid-template-columns:1fr;align-items:start} + .oc-ui{order:2} +} diff --git a/games/ocean-drift-2d/index.html b/games/ocean-drift-2d/index.html new file mode 100644 index 00000000..653eaa3d --- /dev/null +++ b/games/ocean-drift-2d/index.html @@ -0,0 +1,24 @@ + + + + + + Ocean Drift 2D + + + +
    +

    Ocean Drift 2D

    + +
    + + + + Score: 0 +
    + + +
    + + + diff --git a/games/ocean-drift-2d/script.js b/games/ocean-drift-2d/script.js new file mode 100644 index 00000000..21d9e5c9 --- /dev/null +++ b/games/ocean-drift-2d/script.js @@ -0,0 +1,141 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); +const startBtn = document.getElementById("startBtn"); +const pauseBtn = document.getElementById("pauseBtn"); +const restartBtn = document.getElementById("restartBtn"); +const scoreDisplay = document.getElementById("scoreDisplay"); + +const bgMusic = document.getElementById("bgMusic"); +const hitSound = document.getElementById("hitSound"); + +let gameInterval; +let obstacles = []; +let score = 0; +let gameRunning = false; + +const leaf = { + x: canvas.width / 2 - 20, + y: 50, + width: 40, + height: 40, + color: "lime", + speed: 5 +}; + +function drawLeaf() { + ctx.fillStyle = leaf.color; + ctx.beginPath(); + ctx.ellipse(leaf.x + leaf.width/2, leaf.y + leaf.height/2, leaf.width/2, leaf.height/2, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.closePath(); + // Glow + ctx.shadowBlur = 20; + ctx.shadowColor = "lime"; +} + +function generateObstacle() { + const width = Math.random() * 60 + 30; + const x = Math.random() * (canvas.width - width); + obstacles.push({ x: x, y: canvas.height, width: width, height: 20, color: "brown" }); +} + +function drawObstacles() { + obstacles.forEach(obs => { + ctx.fillStyle = obs.color; + ctx.fillRect(obs.x, obs.y, obs.width, obs.height); + ctx.shadowBlur = 15; + ctx.shadowColor = "orange"; + }); +} + +function moveObstacles() { + obstacles.forEach(obs => obs.y -= 2); + obstacles = obstacles.filter(obs => obs.y + obs.height > 0); +} + +function detectCollision() { + for (let obs of obstacles) { + if ( + leaf.x < obs.x + obs.width && + leaf.x + leaf.width > obs.x && + leaf.y < obs.y + obs.height && + leaf.y + leaf.height > obs.y + ) { + hitSound.play(); + endGame(); + } + } +} + +function updateScore() { + score += 1; + scoreDisplay.textContent = "Score: " + score; +} + +function clearCanvas() { + ctx.clearRect(0, 0, canvas.width, canvas.height); +} + +function gameLoop() { + clearCanvas(); + drawLeaf(); + drawObstacles(); + moveObstacles(); + detectCollision(); + updateScore(); +} + +function startGame() { + if (!gameRunning) { + gameRunning = true; + bgMusic.play(); + gameInterval = setInterval(gameLoop, 20); + obstacleInterval = setInterval(generateObstacle, 1000); + } +} + +function pauseGame() { + if (gameRunning) { + clearInterval(gameInterval); + clearInterval(obstacleInterval); + gameRunning = false; + bgMusic.pause(); + } +} + +function restartGame() { + pauseGame(); + obstacles = []; + score = 0; + leaf.x = canvas.width / 2 - 20; + leaf.y = 50; + scoreDisplay.textContent = "Score: 0"; + startGame(); +} + +function endGame() { + pauseGame(); + alert("Game Over! Score: " + score); +} + +// Keyboard controls +document.addEventListener("keydown", e => { + if (e.key === "ArrowLeft") leaf.x -= leaf.speed; + if (e.key === "ArrowRight") leaf.x += leaf.speed; + leaf.x = Math.max(0, Math.min(canvas.width - leaf.width, leaf.x)); +}); + +// Mouse / touch controls +canvas.addEventListener("mousemove", e => { + const rect = canvas.getBoundingClientRect(); + leaf.x = e.clientX - rect.left - leaf.width / 2; +}); +canvas.addEventListener("touchmove", e => { + const rect = canvas.getBoundingClientRect(); + leaf.x = e.touches[0].clientX - rect.left - leaf.width / 2; +}); + +// Button events +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", pauseGame); +restartBtn.addEventListener("click", restartGame); diff --git a/games/ocean-drift-2d/style.css b/games/ocean-drift-2d/style.css new file mode 100644 index 00000000..b5521f5b --- /dev/null +++ b/games/ocean-drift-2d/style.css @@ -0,0 +1,48 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: linear-gradient(to bottom, #87cefa, #ffffff); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + text-align: center; +} + +canvas { + background: linear-gradient(to bottom, #87cefa, #00bfff); + border: 3px solid #00f; + border-radius: 10px; + box-shadow: 0 0 20px #00f, 0 0 40px #0ff; +} + +.controls { + margin-top: 10px; +} + +button { + padding: 8px 15px; + margin: 0 5px; + font-size: 16px; + cursor: pointer; + border-radius: 5px; + border: none; + background: linear-gradient(to right, #00f, #0ff); + color: white; + box-shadow: 0 0 10px #0ff; +} + +button:hover { + box-shadow: 0 0 20px #0ff, 0 0 40px #00f; +} + +#scoreDisplay { + font-size: 18px; + margin-left: 15px; + font-weight: bold; + color: #fff; + text-shadow: 1px 1px 5px #000; +} diff --git a/games/odd-one-out/index.html b/games/odd-one-out/index.html new file mode 100644 index 00000000..33b706c7 --- /dev/null +++ b/games/odd-one-out/index.html @@ -0,0 +1,140 @@ + + + + + + Odd One Out ๐ŸŽฏ + + + + + + + +
    +
    +
    +

    Odd One Out ๐ŸŽฏ

    +
    + + + + +
    +
    +
    +
    + Level + 1 +
    +
    + Score + 0 +
    +
    + Best + 0 +
    +
    + Lives +
    + โค๏ธ + โค๏ธ + โค๏ธ +
    +
    +
    + + + + +
    +
    +
    +
    +
    10.0s
    +
    + +
    + +
    + + + +
    +
    + + +
    + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/games/odd-one-out/script.js b/games/odd-one-out/script.js new file mode 100644 index 00000000..024d3b41 --- /dev/null +++ b/games/odd-one-out/script.js @@ -0,0 +1,624 @@ + +(function(){ + 'use strict'; + + // Elements + const el = { + grid: document.getElementById('grid'), + level: document.getElementById('level'), + lives: document.getElementById('lives'), + score: document.getElementById('score'), + best: document.getElementById('best'), + timerFill: document.getElementById('timerFill'), + timerText: document.getElementById('timerText'), + restartBtn: document.getElementById('restartBtn'), + resetStatsBtn: document.getElementById('resetStatsBtn'), + nextLevelBtn: document.getElementById('nextLevelBtn'), + pauseBtn: document.getElementById('pauseBtn'), + modeToggle: document.getElementById('modeToggle'), + themeToggle: document.getElementById('themeToggle'), + soundToggle: document.getElementById('soundToggle'), + overlay: document.getElementById('overlay'), + ovLevel: document.getElementById('ovLevel'), + ovScore: document.getElementById('ovScore'), + ovBest: document.getElementById('ovBest'), + overlayTitle: document.getElementById('overlayTitle'), + overlayMessage: document.getElementById('overlayMessage'), + startBtn: document.getElementById('startBtn'), + shareBtn: document.getElementById('shareBtn'), + toast: document.getElementById('toast'), + fxLayer: document.getElementById('fxLayer'), + shareModal: document.getElementById('shareModal'), + shareLevel: document.getElementById('shareLevel'), + shareScore: document.getElementById('shareScore'), + shareWhatsApp: document.getElementById('shareWhatsApp'), + shareTelegram: document.getElementById('shareTelegram'), + shareX: document.getElementById('shareX'), + shareInstagram: document.getElementById('shareInstagram'), + closeShareModal: document.getElementById('closeShareModal'), + comboDisplay: document.getElementById('comboDisplay'), + comboValue: document.getElementById('comboValue'), + pauseOverlay: document.getElementById('pauseOverlay'), + pauseLevel: document.getElementById('pauseLevel'), + pauseScore: document.getElementById('pauseScore'), + pauseLives: document.getElementById('pauseLives'), + pauseStreak: document.getElementById('pauseStreak'), + resumeBtn: document.getElementById('resumeBtn'), + quitBtn: document.getElementById('quitBtn'), + }; + + // Persistent state + const store = { + get highScore(){ return parseInt(localStorage.getItem('ooo_highScore')||'0',10) }, + set highScore(v){ localStorage.setItem('ooo_highScore', String(v)) }, + get maxLevel(){ return parseInt(localStorage.getItem('ooo_maxLevel')||'1',10) }, + set maxLevel(v){ localStorage.setItem('ooo_maxLevel', String(v)) }, + get preferredMode(){ return localStorage.getItem('ooo_mode') || 'color' }, + set preferredMode(v){ localStorage.setItem('ooo_mode', v) }, + get theme(){ return localStorage.getItem('ooo_theme') || 'dark' }, + set theme(v){ localStorage.setItem('ooo_theme', v) }, + get sound(){ return localStorage.getItem('ooo_sound') !== 'off' }, + set sound(v){ localStorage.setItem('ooo_sound', v ? 'on':'off') }, + }; + + // Game state + const state = { + level: 1, + score: 0, + lives: 3, + combo: 0, + maxCombo: 0, + paused: false, + mode: store.preferredMode, + running: false, + gridSize: 3, + oddIndex: -1, + timer: null, + timeLeftMs: 0, + timeTotalMs: 0, + pausedTime: 0, + }; + + // Emoji sets + const EMOJIS = [ + ['๐ŸŽ','๐Ÿ'], ['๐Ÿถ','๐Ÿ•'], ['๐ŸŒž','๐ŸŒค๏ธ'], ['โญ','โœจ'], ['๐Ÿช','๐Ÿฅ '], ['๐Ÿ“','๐Ÿ’'], ['๐Ÿ‹','๐ŸŠ'], ['๐Ÿ’Ž','๐Ÿ”ท'], ['๐Ÿ”ด','๐ŸŸ '], ['๐Ÿฑ','๐Ÿˆโ€โฌ›'], + ['๐ŸŽˆ','๐ŸŽ‰'], ['๐Ÿ‡','๐Ÿซ'], ['โšฝ','๐Ÿ€'], ['๐ŸŒท','๐ŸŒน'], ['๐ŸŽง','๐ŸŽต'], ['๐Ÿงฉ','๐ŸŽฒ'], ['๐Ÿ“˜','๐Ÿ“—'], ['๐Ÿš—','๐Ÿš™'], ['๐ŸŽฎ','๐Ÿ•น๏ธ'], ['๐Ÿง','๐Ÿฐ'] + ]; + + // Audio (Web Audio API minimal beeps) + const AudioCtx = window.AudioContext || window.webkitAudioContext; + let audioCtx = null; + function ensureAudio(){ if(!audioCtx) audioCtx = new AudioCtx(); } + function beep({freq=440, dur=0.12, type='sine', vol=0.07}={}){ + if(!store.sound) return; + try{ ensureAudio(); const o=audioCtx.createOscillator(); const g=audioCtx.createGain(); + o.type=type; o.frequency.value=freq; g.gain.value=vol; o.connect(g).connect(audioCtx.destination); + o.start(); g.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime+dur); o.stop(audioCtx.currentTime+dur); + }catch(e){/* ignore */} + } + const sfx = { + correct(){ beep({freq:740, dur:.12, type:'triangle'}); }, + wrong(){ beep({freq:200, dur:.2, type:'sawtooth', vol:0.06}); }, + levelUp(){ beep({freq:520, dur:.12}); setTimeout(()=>beep({freq:680,dur:.12}),90); }, + tick(){ beep({freq:900, dur:.04, type:'square', vol:.04}); }, + champion(){ + // Triumphant ascending melody + beep({freq:523, dur:.15}); // C + setTimeout(()=>beep({freq:659, dur:.15}), 150); // E + setTimeout(()=>beep({freq:784, dur:.15}), 300); // G + setTimeout(()=>beep({freq:1047, dur:.3}), 450); // C high + }, + gameOver(){ + // Descending sad melody + beep({freq:440, dur:.2, type:'triangle'}); // A + setTimeout(()=>beep({freq:392, dur:.2, type:'triangle'}), 200); // G + setTimeout(()=>beep({freq:330, dur:.3, type:'triangle'}), 400); // E + } + }; + + // Utilities + const clamp = (v,min,max)=>Math.max(min,Math.min(max,v)); + const rand = (n)=>Math.floor(Math.random()*n); + const lerp = (a,b,t)=>a+(b-a)*t; + + // Color util using HSL for easy lightness tweak + function randomBaseColor(){ + const h = Math.floor(Math.random()*360); + const s = 60 + Math.random()*30; // 60-90 + const l = 45 + Math.random()*10; // 45-55 + return {h,s,l}; + } + function hsl({h,s,l}){ return `hsl(${h} ${s}% ${l}%)`; } + + // Difficulty curves + function gridForLevel(level){ + // 1->3, 2->4, ..., 10->12 + return clamp(2 + level, 3, 12); + } + function colorDeltaForLevel(level){ + // Slightly larger deltas for better visibility + // 1:22%, 5:12%, 10:8% (lightness delta) + const t = (level-1)/9; // 0..1 + return lerp(22, 8, t); + } + function hueDeltaForLevel(level){ + // Add a gentle hue shift that grows with level to offset reduced lightness delta + // 1: +2ยฐ, 10: +8ยฐ + const t = (level-1)/9; + return lerp(2, 8, t); + } + function timerForLevel(level){ + // 1:10s, 5:6s, 10:3s + const t = (level-1)/9; + return lerp(10, 3, t); + } + + // Build grid + function buildGrid(){ + const n = state.gridSize = gridForLevel(state.level); + const total = n*n; + state.oddIndex = rand(total); + + el.grid.classList.add('enter'); + setTimeout(()=>el.grid.classList.remove('enter'), 350); + + el.grid.style.gridTemplateColumns = `repeat(${n}, minmax(32px, 1fr))`; + el.grid.classList.toggle('compact', n>=8 && n<11); + el.grid.classList.toggle('dense', n>=11); + el.grid.innerHTML = ''; + + if(state.mode === 'color'){ + const base = randomBaseColor(); + const delta = colorDeltaForLevel(state.level); + const hDelta = hueDeltaForLevel(state.level) * (Math.random()>.5?1:-1); + const odd = { ...base, + h: (base.h + hDelta + 360) % 360, + l: clamp(base.l + (Math.random()>.5?delta:-delta), 12, 88) + }; + for(let i=0;ionTileClick(i,d)); + el.grid.appendChild(d); + } + }else{ // emoji mode + const pair = EMOJIS[rand(EMOJIS.length)]; + const common = pair[0], odd = pair[1]; + for(let i=0;ionTileClick(i,d)); + el.grid.appendChild(d); + } + } + } + + function tileFontSize(n){ + if(n>=11) return '1.25rem'; + if(n>=9) return '1.5rem'; + if(n>=7) return '1.75rem'; + if(n>=5) return '2rem'; + return '2.2rem'; + } + + // Timer + function startTimer(initialTime = null){ + if(initialTime !== null){ + // Resuming from pause + state.timeLeftMs = initialTime; + } else { + // Starting fresh + const secs = timerForLevel(state.level); + state.timeTotalMs = Math.round(secs*1000); + state.timeLeftMs = state.timeTotalMs; + } + updateTimerUI(); + if(state.timer) clearInterval(state.timer); + let lastTick = performance.now(); + state.timer = setInterval(()=>{ + if(state.paused) return; + const now = performance.now(); + const dt = now - lastTick; lastTick = now; + state.timeLeftMs -= dt; + // Tick sound in last 2 seconds every ~300ms + if(state.timeLeftMs < 2000){ if(Math.round(state.timeLeftMs)%300<20) sfx.tick(); } + if(state.timeLeftMs <= 0){ state.timeLeftMs = 0; updateTimerUI(); onTimeUp(); } + else updateTimerUI(); + }, 50); + } + + function updateTimerUI(){ + const p = state.timeTotalMs? state.timeLeftMs/state.timeTotalMs : 0; + el.timerFill.style.width = `${p*100}%`; + el.timerText.textContent = `${(state.timeLeftMs/1000).toFixed(1)}s`; + } + + // Scoring + function addScore(base=10){ + // Bonus for speed: remaining seconds * 2 rounded + const timeBonus = Math.round((state.timeLeftMs/1000) * 2); + // Combo multiplier: starts at 1ร—, becomes 2ร— on 2nd correct, 3ร— on 3rd correct, etc. + const comboMultiplier = Math.max(1, state.combo); + const gained = (base + timeBonus) * comboMultiplier; + state.score += gained; + el.score.textContent = String(state.score); + return {gained, bonus: timeBonus, multiplier: comboMultiplier}; + } + + // Click handling + function onTileClick(index, node){ + if(!state.running || state.paused) return; + if(index === state.oddIndex){ + node.classList.add('correct'); + state.combo++; + if(state.combo > state.maxCombo) state.maxCombo = state.combo; + updateComboDisplay(); + const {gained, bonus, multiplier} = addScore(10); + sfx.correct(); + sparkle(node); + const msg = multiplier > 1 ? `+${gained} (ร—${multiplier} COMBO!)` : `+${gained} (${bonus} bonus)`; + toast(msg, 900); + nextLevel(); + } else { + node.classList.add('wrong'); + sfx.wrong(); + state.combo = 0; + updateComboDisplay(); + loseLife('Wrong tile!'); + } + } + + function sparkle(node){ + const rect = node.getBoundingClientRect(); + const cx = rect.left + rect.width/2 + window.scrollX; + const cy = rect.top + rect.height/2 + window.scrollY; + for(let i=0;i<12;i++) confetti(cx, cy); + } + + function confetti(x,y){ + const c = document.createElement('div'); c.className='confetti'; + c.style.left = x+'px'; c.style.top = y+'px'; + const hue = Math.floor(Math.random()*360); + c.style.background = `hsl(${hue} 80% 60%)`; + const tx = (Math.random()*2-1)*200 + 'px'; + const rot = (Math.random()*2-1)*720 + 'deg'; + const dur = (0.8 + Math.random()*0.8)+'s'; + c.style.setProperty('--tx', tx); c.style.setProperty('--rot', rot); c.style.setProperty('--dur', dur); + el.fxLayer.appendChild(c); + setTimeout(()=>c.remove(), 1600); + } + + // Flow + function startGame(){ + state.level = 1; state.score = 0; state.lives = 3; state.combo = 0; state.maxCombo = 0; state.running = true; state.paused = false; + el.level.textContent = '1'; el.score.textContent = '0'; + updateLivesDisplay(); + updateComboDisplay(); + el.nextLevelBtn.hidden = true; + el.pauseBtn.disabled = false; + hideOverlay(); + newLevel(); + } + + function newLevel(){ + state.running = true; + updateHUD(); + buildGrid(); + startTimer(); + } + + function nextLevel(){ + clearInterval(state.timer); state.timer = null; + state.level++; + sfx.levelUp(); + if(state.level>10){ + championCelebration(); + return; + } + toast(`Level ${state.level}!`, 1000); + newLevel(); + } + + // Champion animation + function championCelebration(){ + state.running = false; + clearInterval(state.timer); state.timer=null; + sfx.champion(); // Play champion sound + // Burst confetti + for(let i=0;i<60;i++){ + setTimeout(()=>confetti(window.innerWidth/2, window.innerHeight/2), i*18); + } + // Show animated modal + showOverlay({ + title: '๐ŸŽ‰ Champion! ๐ŸŽ‰', + message: 'You beat all 10 levels!
    Congratulations!
    Enjoy the celebration!', + showShare: true, + }); + // Animate modal + const modal = document.querySelector('.modal'); + if(modal){ + modal.style.animation = 'championPop 1.2s cubic-bezier(.2,1.5,.4,1)'; + setTimeout(()=>{ + modal.style.animation = ''; + // After a delay, show game over + setTimeout(()=>gameOver('Champion! You beat level 10!'), 1800); + }, 1200); + } + } + + function onTimeUp(){ + clearInterval(state.timer); state.timer=null; + loseLife("Time's up!"); + } + + function loseLife(reason){ + state.lives--; + state.combo = 0; + updateComboDisplay(); + updateLivesDisplay(); + + if(state.lives <= 0){ + gameOver(reason); + } else { + state.running = false; + toast(`๐Ÿ’” ${reason} ${state.lives} ${state.lives===1?'heart':'hearts'} left`, 1500); + // Continue to same level after brief pause + setTimeout(()=>{ + if(state.lives > 0) newLevel(); + }, 1600); + } + } + + function updateComboDisplay(){ + // Show streak starting from 2 consecutive correct answers + // Multiplier increases by 1 for each consecutive correct answer: ร—2, ร—3, ร—4, etc. + if(state.combo >= 2){ + const multiplier = state.combo; + el.comboValue.textContent = multiplier; + el.comboDisplay.classList.remove('hidden'); + el.comboDisplay.classList.add('show'); + } else { + el.comboDisplay.classList.remove('show'); + setTimeout(()=>{ + if(state.combo < 2) el.comboDisplay.classList.add('hidden'); + }, 300); + } + } + + function updateLivesDisplay(){ + const hearts = el.lives.querySelectorAll('.heart'); + hearts.forEach((heart, index) => { + if(index < state.lives){ + heart.textContent = 'โค๏ธ'; + heart.classList.remove('lost'); + } else { + heart.textContent = '๐Ÿ–ค'; + heart.classList.add('lost'); + } + }); + } + + function gameOver(reason){ + state.running = false; + state.paused = false; + el.pauseBtn.disabled = true; + clearInterval(state.timer); state.timer=null; + sfx.gameOver(); // Play game over sound + // Update bests + if(state.score > store.highScore) store.highScore = state.score; + if(state.level > store.maxLevel) store.maxLevel = state.level; + updateHUD(); + showOverlay({ + title: 'Game Over', + message: `${reason} You reached Level ${state.level}.`, + showShare: true, + }); + } + + function updateHUD(){ + el.level.textContent = String(state.level); + el.score.textContent = String(state.score); + el.best.textContent = String(store.highScore); + } + + // Overlays + function showOverlay({title, message, showShare=false}){ + // Separate emojis from text to preserve emoji colors + const emojiRegex = /(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)/gu; + const parts = title.split(emojiRegex); + let formattedTitle = ''; + parts.forEach(part => { + if(part && part.match(emojiRegex)){ + formattedTitle += part; // Emoji without gradient + } else if(part && part.trim()){ + formattedTitle += `${part}`; // Text with gradient + } + }); + el.overlayTitle.innerHTML = formattedTitle; + el.overlayMessage.innerHTML = message; + el.ovLevel.textContent = String(state.level); + el.ovScore.textContent = String(state.score); + el.ovBest.textContent = String(store.highScore); + el.shareBtn.hidden = !showShare; + el.overlay.classList.remove('hidden'); + el.overlay.setAttribute('aria-hidden','false'); + } + function hideOverlay(){ + el.overlay.classList.add('hidden'); + el.overlay.setAttribute('aria-hidden','true'); + } + + // Toast + let toastT=null; + function toast(text, ms=1200){ + el.toast.textContent = text; + el.toast.classList.add('show'); + clearTimeout(toastT); + toastT = setTimeout(()=>el.toast.classList.remove('show'), ms); + } + + // Toggles + function toggleMode(){ + state.mode = state.mode==='color' ? 'emoji' : 'color'; + store.preferredMode = state.mode; + el.modeToggle.textContent = state.mode==='color' ? '๐Ÿ˜„ Emoji Mode' : '๐ŸŽจ Color Mode'; + if(state.running && !state.paused){ buildGrid(); } + } + function toggleTheme(){ + const root = document.documentElement; + const next = root.getAttribute('data-theme')==='dark' ? 'light':'dark'; + root.setAttribute('data-theme', next); store.theme = next; + } + function toggleSound(){ + store.sound = !store.sound; + el.soundToggle.textContent = store.sound ? '๐Ÿ”Š Sound' : '๐Ÿ”‡ Sound'; + } + + // Pause/Resume + function togglePause(){ + if(!state.running) return; + + if(state.paused){ + // Resume + resumeGame(); + } else { + // Pause + pauseGame(); + } + } + + function pauseGame(){ + state.paused = true; + state.pausedTime = performance.now(); + clearInterval(state.timer); + el.pauseBtn.textContent = 'โ–ถ๏ธ Resume'; + el.pauseLevel.textContent = String(state.level); + el.pauseScore.textContent = String(state.score); + el.pauseLives.textContent = String(state.lives); + el.pauseStreak.textContent = String(state.combo); + el.pauseOverlay.classList.remove('hidden'); + el.pauseOverlay.setAttribute('aria-hidden','false'); + } + + function resumeGame(){ + state.paused = false; + el.pauseBtn.textContent = 'โธ๏ธ Pause'; + el.pauseOverlay.classList.add('hidden'); + el.pauseOverlay.setAttribute('aria-hidden','true'); + // Resume timer from where it left off + startTimer(state.timeLeftMs); + } + + function quitToMenu(){ + state.running = false; + state.paused = false; + clearInterval(state.timer); + el.pauseBtn.disabled = true; + el.pauseOverlay.classList.add('hidden'); + el.pauseOverlay.setAttribute('aria-hidden','true'); + showOverlay({ + title: 'Odd One Out ๐ŸŽฏ', + message: 'Find the tile that looks slightly different before time runs out! Choose a mode and press Start.', + showShare: false, + }); + } + + // Share + function shareScore(){ + el.shareLevel.textContent = String(state.level); + el.shareScore.textContent = String(state.score); + el.shareModal.classList.remove('hidden'); + el.shareModal.setAttribute('aria-hidden','false'); + } + + function closeShareModal(){ + el.shareModal.classList.add('hidden'); + el.shareModal.setAttribute('aria-hidden','true'); + } + + function shareToWhatsApp(){ + const text = encodeURIComponent(`๐ŸŽฏ Odd One Out Challenge!\n\nI scored ${state.score} points and reached Level ${state.level}!\n\nCan you beat my score? Try this awesome reflex game! ๐Ÿ”ฅ`); + window.open(`https://wa.me/?text=${text}`, '_blank'); + closeShareModal(); + } + + function shareToTelegram(){ + const text = encodeURIComponent(`๐ŸŽฏ Odd One Out Game\n\nMy Score: ${state.score}\nLevel Reached: ${state.level}\n\nThink you can beat me? Test your reflexes! ๐Ÿ‘€`); + window.open(`https://t.me/share/url?text=${text}`, '_blank'); + closeShareModal(); + } + + function shareToX(){ + const text = encodeURIComponent(`๐ŸŽฏ Just scored ${state.score} on Odd One Out (Level ${state.level})! Can you beat me? #OddOneOut #GameChallenge`); + window.open(`https://twitter.com/intent/tweet?text=${text}`, '_blank'); + closeShareModal(); + } + + function shareToInstagram(){ + // Instagram doesn't support direct text sharing via URL, so copy to clipboard and redirect + const text = `๐ŸŽฏ Odd One Out Challenge!\n\nScore: ${state.score}\nLevel: ${state.level}\n\nCan you beat me? ๐Ÿ”ฅ`; + navigator.clipboard.writeText(text).then(()=>{ + toast('๐Ÿ“‹ Message copied! Opening Instagram...', 2000); + setTimeout(()=>{ + window.open('https://www.instagram.com/', '_blank'); + }, 500); + closeShareModal(); + }).catch(()=>{ + // Fallback if clipboard fails + window.open('https://www.instagram.com/', '_blank'); + alert(`Copy this message to share on Instagram:\n\n${text}`); + closeShareModal(); + }); + } + + // Reset stats + function resetStats(){ + if(confirm('Are you sure you want to reset all stats (high score and max level)?')){ + store.highScore = 0; + store.maxLevel = 1; + el.best.textContent = '0'; + toast('Stats reset!', 1200); + sfx.correct(); + } + } + + // Buttons + el.restartBtn.addEventListener('click', startGame); + el.resetStatsBtn.addEventListener('click', resetStats); + el.startBtn.addEventListener('click', startGame); + el.nextLevelBtn.addEventListener('click', nextLevel); + el.modeToggle.addEventListener('click', toggleMode); + el.themeToggle.addEventListener('click', toggleTheme); + el.soundToggle.addEventListener('click', toggleSound); + el.shareBtn.addEventListener('click', shareScore); + el.closeShareModal.addEventListener('click', closeShareModal); + el.shareWhatsApp.addEventListener('click', shareToWhatsApp); + el.shareTelegram.addEventListener('click', shareToTelegram); + el.shareX.addEventListener('click', shareToX); + el.shareInstagram.addEventListener('click', shareToInstagram); + el.pauseBtn.addEventListener('click', togglePause); + el.resumeBtn.addEventListener('click', resumeGame); + el.quitBtn.addEventListener('click', quitToMenu); + + // Init from storage + function init(){ + document.documentElement.setAttribute('data-theme', store.theme); + el.modeToggle.textContent = store.preferredMode==='color' ? '๐Ÿ˜„ Emoji Mode' : '๐ŸŽจ Color Mode'; + el.soundToggle.textContent = store.sound ? '๐Ÿ”Š Sound' : '๐Ÿ”‡ Sound'; + el.best.textContent = String(store.highScore); + el.pauseBtn.disabled = true; + showOverlay({ + title: 'Odd One Out ๐ŸŽฏ', + message: 'Find the tile that looks slightly different before time runs out! Choose a mode and press Start.', + showShare: false, + }); + } + + init(); +})(); \ No newline at end of file diff --git a/games/odd-one-out/style.css b/games/odd-one-out/style.css new file mode 100644 index 00000000..9e1a221b --- /dev/null +++ b/games/odd-one-out/style.css @@ -0,0 +1,149 @@ +.modal.champion{animation:championPop 1.2s cubic-bezier(.2,1.5,.4,1)} +@keyframes championPop{ + 0%{transform:scale(.7) rotate(-8deg); box-shadow:0 0 0 0 #ffd70044;} + 60%{transform:scale(1.08) rotate(6deg); box-shadow:0 0 0 24px #ffd70044;} + 80%{transform:scale(.98) rotate(-2deg); box-shadow:0 0 0 8px #ffd70044;} + 100%{transform:scale(1) rotate(0deg); box-shadow:0 0 0 0 #ffd70044;} +} +:root{ + --bg1:#0f1023; --bg2:#1a1b3a; --card:#14162a99; --cardBorder:#2a2d55; + --text:#e7e9ff; --muted:#a8b0ff; --accent:#7b5cff; --accent2:#21d4fd; --good:#2de370; --bad:#ff4d6d; + --tile:#1f2244; --tileHover:#292c58; --shadow:0 10px 30px rgba(0,0,0,.35); +} +html[data-theme="light"]{ + --bg1:#f2f6ff; --bg2:#e7ecff; --card:#ffffffcc; --cardBorder:#d7dcff; + --text:#15172a; --muted:#4c58a6; --accent:#5b2cff; --accent2:#0ea5e9; --good:#16a34a; --bad:#dc2626; + --tile:#eef1ff; --tileHover:#e2e7ff; --shadow:0 10px 25px rgba(0,0,0,.12); +} +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; font-family:'Outfit',system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; + color:var(--text); background:linear-gradient(135deg,var(--bg1),var(--bg2)); + overflow-x:hidden; +} + +#bg{position:fixed; inset:0; pointer-events:none; + background: + radial-gradient(1200px 800px at 10% 10%, #7b5cff10, transparent 60%), + radial-gradient(1000px 700px at 90% 20%, #21d4fd10, transparent 60%), + radial-gradient(1400px 900px at 30% 90%, #ff5ca810, transparent 60%); + filter:saturate(120%); +} + +.container{max-width:1100px; margin:0 auto; padding:24px; position:relative} +.topbar{display:flex; align-items:center; justify-content:space-between; gap:16px; margin-bottom:10px} +.title{margin:0; font-weight:800; letter-spacing:.5px; display:flex; align-items:center; gap:10px; transition:.3s} +.title:hover{transform:scale(1.02); filter:drop-shadow(0 4px 12px #7b5cff44)} +.title .emoji{filter:drop-shadow(0 2px 6px #0006); transition:.3s; display:inline-block} +.title:hover .emoji{transform:rotate(10deg) scale(1.1); filter:drop-shadow(0 4px 10px #0008)} +.controls{display:flex; gap:10px; flex-wrap:wrap} + +.btn{border:1px solid var(--cardBorder); background:linear-gradient(180deg, #ffffff12, #0000000e); + color:var(--text); padding:10px 14px; border-radius:12px; cursor:pointer; transition:.2s transform, .2s background, .2s border, .2s box-shadow; + box-shadow:var(--shadow); backdrop-filter:blur(10px); position:relative; overflow:hidden; +} +.btn::before{content:''; position:absolute; inset:0; background:linear-gradient(135deg, #ffffff18, transparent); opacity:0; transition:.3s} +.btn:hover{transform:translateY(-2px); background:linear-gradient(180deg, #ffffff22, #00000012); box-shadow:0 12px 35px rgba(0,0,0,.4); border-color:var(--accent)} +.btn:hover::before{opacity:1} +.btn:active{transform:translateY(0) scale(.96)} +.btn.primary{background:linear-gradient(135deg,var(--accent2),var(--accent)); color:#fff; border-color:transparent} +.btn.primary:hover{filter:brightness(1.12); box-shadow:0 12px 40px rgba(123,92,255,.35)} +.btn.pill{border-radius:999px} +.btn.lg{padding:12px 18px; font-weight:600} + +.hud{display:grid; grid-template-columns:repeat(4,1fr); gap:14px; margin:14px 0} +.stat{background:var(--card); border:1px solid var(--cardBorder); padding:12px 14px; border-radius:14px; box-shadow:var(--shadow); transition:.3s; cursor:default} +.stat:hover{transform:translateY(-3px); box-shadow:0 14px 35px rgba(0,0,0,.3); border-color:var(--accent); background:linear-gradient(135deg, var(--card), #ffffff08)} +.stat .label{display:block; font-size:.8rem; color:var(--muted); transition:.3s} +.stat:hover .label{color:var(--accent2)} +.stat .value{font-size:1.45rem; font-weight:700; transition:.3s; display:inline-block} +.stat:hover .value{transform:scale(1.08); color:var(--accent)} + +.hearts{display:flex; gap:6px; justify-content:center; font-size:1.4rem; margin-top:4px} +.heart{display:inline-block; transition:.3s; filter:drop-shadow(0 2px 4px #0004)} +.heart.lost{animation:heartBreak .5s ease; filter:grayscale(1) brightness(.7)} +@keyframes heartBreak{0%{transform:scale(1)} 50%{transform:scale(1.3) rotate(15deg)} 100%{transform:scale(1)}} + +.combo-display{position:fixed; top:50%; right:30px; transform:translateY(-50%); text-align:center; pointer-events:none; z-index:900; opacity:0; transition:.3s; background:rgba(0,0,0,.3); backdrop-filter:blur(8px); border:2px solid var(--accent); border-radius:16px; padding:12px 24px; box-shadow:0 8px 24px rgba(0,0,0,.3)} +.combo-display.show{opacity:1; animation:comboFloat 0.6s ease} +.combo-text{font-size:1.2rem; font-weight:700; color:var(--accent2); text-shadow:0 2px 8px #000; letter-spacing:2px} +.combo-value{font-size:3rem; font-weight:900; background:linear-gradient(135deg, #ff6d4d, #ffd700, #ff6d4d); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; filter:drop-shadow(0 4px 12px #ff6d4d88); animation:pulse 1s infinite} +@keyframes comboFloat{0%{transform:translateY(-50%) translateX(40px) scale(0.5); opacity:0} 50%{transform:translateY(-50%) translateX(-5px) scale(1.2)} 100%{transform:translateY(-50%) scale(1); opacity:1}} +@keyframes pulse{0%,100%{transform:scale(1)} 50%{transform:scale(1.1)}} + +.timer{display:grid; gap:8px; margin:10px 0 18px} +.timer-track{width:100%; height:14px; border-radius:999px; background:linear-gradient(0deg,#00000022,#ffffff0e); border:1px solid var(--cardBorder); overflow:hidden; box-shadow:inset 0 2px 8px #00000033; transition:.3s} +.timer-track:hover{transform:scaleY(1.15); box-shadow:inset 0 2px 12px #00000044} +.timer-fill{height:100%; width:100%; background:linear-gradient(90deg,#2de370,#ffcf3c,#ff6d4d); + transition:width .1s linear; transform-origin:left; position:relative} +.timer-fill::after{content:''; position:absolute; inset:0; background:linear-gradient(90deg, transparent, #ffffff44, transparent); animation:shimmer 2s infinite} +@keyframes shimmer{0%,100%{transform:translateX(-100%)} 50%{transform:translateX(100%)}} +.timer-text{font-variant-numeric:tabular-nums; text-align:right; color:var(--muted); transition:.3s} +.timer-text:hover{color:var(--accent2); transform:scale(1.05)} + +.grid{display:grid; gap:10px; place-items:center; padding:16px; border-radius:18px; background:var(--card); border:1px solid var(--cardBorder); box-shadow:var(--shadow); + min-height:320px; transition:.25s} +.grid:hover{box-shadow:0 15px 40px rgba(0,0,0,.4); border-color:var(--accent2)} +.grid.enter{animation:fadeIn .35s ease both} + +.tile{width:72px; aspect-ratio:1; border-radius:12px; background:var(--tile); border:1px solid #ffffff22; display:grid; place-items:center; font-size:2rem; user-select:none; cursor:pointer; position:relative; + transition:.2s transform, .2s box-shadow, .15s filter, .2s border-color} +.tile::before{content:''; position:absolute; inset:-2px; border-radius:12px; background:linear-gradient(135deg, var(--accent2), var(--accent)); opacity:0; transition:.3s; z-index:-1; filter:blur(8px)} +.tile:hover{transform:translateY(-4px) scale(1.05); box-shadow:0 12px 25px #0004; background:var(--tileHover); border-color:var(--accent2)} +.tile:hover::before{opacity:.3} +.tile:active{transform:translateY(0) scale(.96)} +.tile.correct{outline:2px solid var(--good); box-shadow:0 0 0 6px #2de37022; animation:correctPulse .4s ease} +.tile.wrong{outline:2px solid var(--bad); box-shadow:0 0 0 6px #ff4d6d22; animation:wrongShake .5s ease} +.tile.fade{animation:pop .28s ease} + +/* Compact/dense modes for large grids */ +.grid.compact{gap:8px} +.grid.compact .tile{width:56px} +.grid.dense{gap:6px} +.grid.dense .tile{width:42px} + +@keyframes fadeIn{from{opacity:0; transform:translateY(10px)} to{opacity:1; transform:none}} +@keyframes pop{0%{transform:scale(.9); opacity:.6} 60%{transform:scale(1.06); opacity:1} 100%{transform:scale(1)}} +@keyframes correctPulse{0%,100%{transform:scale(1)} 50%{transform:scale(1.12)}} +@keyframes wrongShake{0%,100%{transform:translateX(0)} 20%,60%{transform:translateX(-8px)} 40%,80%{transform:translateX(8px)}} + +.bottombar{display:flex; justify-content:center; gap:14px; margin-top:18px} + +.toast{position:fixed; top:18px; left:50%; transform:translateX(-50%); background:#111a; color:#fff; padding:10px 14px; border-radius:999px; border:1px solid #ffffff33; opacity:0; pointer-events:none; transition:.25s; backdrop-filter:blur(10px); box-shadow:0 8px 20px #0006} +.toast.show{opacity:1; transform:translateX(-50%) translateY(6px); animation:toastBounce .4s ease} +@keyframes toastBounce{0%{transform:translateX(-50%) translateY(-20px)} 60%{transform:translateX(-50%) translateY(8px)} 100%{transform:translateX(-50%) translateY(6px)}} + +.overlay{position:fixed; inset:0; background:linear-gradient(0deg,#0007,#0008); display:grid; place-items:center; z-index:20; backdrop-filter:blur(4px)} +.overlay.hidden{display:none} +.modal{width:min(560px,92vw); background:var(--card); border:1px solid var(--cardBorder); border-radius:18px; padding:22px; box-shadow:var(--shadow); text-align:center; transition:.3s} +.modal:hover{transform:scale(1.02); box-shadow:0 20px 50px rgba(0,0,0,.5); border-color:var(--accent)} +.modal h2{margin-top:0; transition:.3s; color:var(--text)} +.modal h2 .gradient-text{background:linear-gradient(135deg,var(--accent2),var(--accent)); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent} +.modal:hover h2{filter:brightness(1.2)} + +.share-modal{max-width:480px} +.share-modal p{color:var(--muted); margin:8px 0 20px} +.share-buttons{display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:8px} +.share-btn{display:flex; flex-direction:column; align-items:center; gap:8px; padding:16px 12px; font-size:.9rem} +.share-btn .share-icon{font-size:2rem; filter:drop-shadow(0 2px 4px #0004)} +.share-btn.whatsapp:hover{background:linear-gradient(135deg, #25D366, #128C7E); color:#fff; border-color:transparent} +.share-btn.telegram:hover{background:linear-gradient(135deg, #0088cc, #229ED9); color:#fff; border-color:transparent} +.share-btn.x:hover{background:linear-gradient(135deg, #000000, #14171A); color:#fff; border-color:transparent} +.share-btn.instagram:hover{background:linear-gradient(135deg, #833AB4, #FD1D1D, #FCB045); color:#fff; border-color:transparent} + +.overlay-stats{display:flex; justify-content:center; gap:24px; color:var(--muted); margin:16px 0; transition:.3s} +.overlay-stats div{transition:.3s; cursor:default} +.overlay-stats div:hover{color:var(--accent2); transform:scale(1.1)} +.overlay-actions{display:flex; justify-content:center; gap:12px} + +.fx-layer{pointer-events:none; position:fixed; inset:0; overflow:hidden; z-index:15} +.confetti{position:absolute; width:10px; height:14px; border-radius:2px; opacity:.9; transform:translate(-50%,-50%); animation:fall var(--dur) linear forwards; box-shadow:0 2px 8px #0006} +@keyframes fall{to{transform:translate(var(--tx,0), 110vh) rotate(var(--rot,360deg)); opacity:.2}} + +/* Responsive */ +@media (max-width:720px){ + .tile{width:56px} + .hud{grid-template-columns:1fr 1fr; gap:12px} + .hud .stat:nth-child(4){grid-column:span 2} +} \ No newline at end of file diff --git a/games/orb-collector/index.html b/games/orb-collector/index.html new file mode 100644 index 00000000..d283318d --- /dev/null +++ b/games/orb-collector/index.html @@ -0,0 +1,36 @@ + + + + + + Orb Collector | Mini JS Games Hub + + + +
    +
    +

    ๐ŸŸข Orb Collector

    +
    + Score: 0 + Best: 0 +
    +
    + + +
    +
    + + + +
    +

    Use โฌ†๏ธโฌ‡๏ธโฌ…๏ธโžก๏ธ or WASD to move. Collect glowing dots and avoid obstacles!

    +
    +
    + + + + + + + + diff --git a/games/orb-collector/script.js b/games/orb-collector/script.js new file mode 100644 index 00000000..00346f92 --- /dev/null +++ b/games/orb-collector/script.js @@ -0,0 +1,153 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +const collectSound = document.getElementById("collectSound"); +const hitSound = document.getElementById("hitSound"); +const bgMusic = document.getElementById("bgMusic"); + +let orb = { x: 400, y: 250, radius: 15, speed: 4 }; +let keys = {}; +let score = 0; +let best = localStorage.getItem("orbBestScore") || 0; +let paused = false; + +document.getElementById("best").textContent = best; + +const dots = []; +const obstacles = []; + +function randomPos(max) { + return Math.floor(Math.random() * (max - 50)) + 25; +} + +// Create glowing collectible dots +for (let i = 0; i < 8; i++) { + dots.push({ x: randomPos(canvas.width), y: randomPos(canvas.height), radius: 10 }); +} + +// Create obstacles +for (let i = 0; i < 4; i++) { + obstacles.push({ x: randomPos(canvas.width), y: randomPos(canvas.height), size: 50 }); +} + +// Controls +window.addEventListener("keydown", (e) => (keys[e.key.toLowerCase()] = true)); +window.addEventListener("keyup", (e) => (keys[e.key.toLowerCase()] = false)); + +document.getElementById("pauseBtn").addEventListener("click", () => { + paused = !paused; + if (paused) { + bgMusic.pause(); + } else { + bgMusic.play(); + update(); + } +}); + +document.getElementById("restartBtn").addEventListener("click", restartGame); + +function restartGame() { + orb = { x: 400, y: 250, radius: 15, speed: 4 }; + score = 0; + paused = false; + dots.forEach((dot) => { + dot.x = randomPos(canvas.width); + dot.y = randomPos(canvas.height); + }); + bgMusic.currentTime = 0; + bgMusic.play(); + update(); +} + +function moveOrb() { + if (keys["arrowup"] || keys["w"]) orb.y -= orb.speed; + if (keys["arrowdown"] || keys["s"]) orb.y += orb.speed; + if (keys["arrowleft"] || keys["a"]) orb.x -= orb.speed; + if (keys["arrowright"] || keys["d"]) orb.x += orb.speed; + + orb.x = Math.max(orb.radius, Math.min(canvas.width - orb.radius, orb.x)); + orb.y = Math.max(orb.radius, Math.min(canvas.height - orb.radius, orb.y)); +} + +function drawOrb() { + const gradient = ctx.createRadialGradient(orb.x, orb.y, 5, orb.x, orb.y, 20); + gradient.addColorStop(0, "rgba(0,255,150,1)"); + gradient.addColorStop(1, "rgba(0,255,150,0)"); + ctx.beginPath(); + ctx.arc(orb.x, orb.y, orb.radius, 0, Math.PI * 2); + ctx.fillStyle = gradient; + ctx.fill(); + ctx.closePath(); +} + +function drawDots() { + dots.forEach((dot) => { + const gradient = ctx.createRadialGradient(dot.x, dot.y, 0, dot.x, dot.y, 10); + gradient.addColorStop(0, "rgba(255,255,0,1)"); + gradient.addColorStop(1, "rgba(255,255,0,0)"); + ctx.beginPath(); + ctx.arc(dot.x, dot.y, dot.radius, 0, Math.PI * 2); + ctx.fillStyle = gradient; + ctx.fill(); + }); +} + +function drawObstacles() { + obstacles.forEach((ob) => { + ctx.fillStyle = "rgba(255,0,0,0.5)"; + ctx.fillRect(ob.x, ob.y, ob.size, ob.size); + }); +} + +function detectCollisions() { + // Collect dots + dots.forEach((dot) => { + const dx = orb.x - dot.x; + const dy = orb.y - dot.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist < orb.radius + dot.radius) { + score += 10; + document.getElementById("score").textContent = score; + collectSound.currentTime = 0; + collectSound.play(); + dot.x = randomPos(canvas.width); + dot.y = randomPos(canvas.height); + } + }); + + // Hit obstacle + obstacles.forEach((ob) => { + if ( + orb.x + orb.radius > ob.x && + orb.x - orb.radius < ob.x + ob.size && + orb.y + orb.radius > ob.y && + orb.y - orb.radius < ob.y + ob.size + ) { + hitSound.play(); + restartGame(); + } + }); +} + +function update() { + if (paused) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + moveOrb(); + detectCollisions(); + drawDots(); + drawObstacles(); + drawOrb(); + + if (score > best) { + best = score; + localStorage.setItem("orbBestScore", best); + document.getElementById("best").textContent = best; + } + + requestAnimationFrame(update); +} + +bgMusic.volume = 0.3; +bgMusic.play(); +update(); diff --git a/games/orb-collector/style.css b/games/orb-collector/style.css new file mode 100644 index 00000000..18aec065 --- /dev/null +++ b/games/orb-collector/style.css @@ -0,0 +1,49 @@ +body { + background: radial-gradient(circle at center, #0f0f1a, #020204); + color: #fff; + font-family: 'Poppins', sans-serif; + text-align: center; + margin: 0; + overflow: hidden; +} + +.game-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; +} + +header { + margin-bottom: 10px; +} + +.stats { + margin: 5px 0; + font-size: 18px; +} + +.controls button { + margin: 5px; + padding: 6px 14px; + background: #1e90ff; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: bold; + transition: 0.3s; +} + +.controls button:hover { + background: #00bfff; + transform: scale(1.05); +} + +canvas { + background: rgba(255, 255, 255, 0.04); + border: 2px solid rgba(255, 255, 255, 0.1); + border-radius: 10px; + box-shadow: 0 0 30px rgba(0,255,150,0.3); +} diff --git a/games/painter_game/index.html b/games/painter_game/index.html new file mode 100644 index 00000000..4ce4aeca --- /dev/null +++ b/games/painter_game/index.html @@ -0,0 +1,37 @@ + + + + + + Symmetry Painter + + + + +
    +

    โœจ Symmetry Painter

    + +
    + + + + + + + + + 10px + + +
    + + +
    + + + + \ No newline at end of file diff --git a/games/painter_game/script.js b/games/painter_game/script.js new file mode 100644 index 00000000..29a7b5ea --- /dev/null +++ b/games/painter_game/script.js @@ -0,0 +1,147 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. DOM and Canvas Setup --- + const canvas = document.getElementById('symmetry-canvas'); + const ctx = canvas.getContext('2d'); + + const colorPicker = document.getElementById('color-picker'); + const brushSizeInput = document.getElementById('brush-size'); + const sizeDisplay = document.getElementById('size-display'); + const symmetryType = document.getElementById('symmetry-type'); + const clearButton = document.getElementById('clear-button'); + + // Set canvas dimensions + const CANVAS_SIZE = 500; + canvas.width = CANVAS_SIZE; + canvas.height = CANVAS_SIZE; + const CENTER_X = CANVAS_SIZE / 2; + const CENTER_Y = CANVAS_SIZE / 2; + + // --- 2. State Variables --- + let isDrawing = false; + let lastX = 0; + let lastY = 0; + + // Set initial context drawing properties + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; + + // --- 3. Core Drawing Logic --- + + /** + * The main function that draws the line segment and its symmetrical copies. + * @param {number} x1 - Previous X position (relative to canvas). + * @param {number} y1 - Previous Y position (relative to canvas). + * @param {number} x2 - Current X position (relative to canvas). + * @param {number} y2 - Current Y position (relative to canvas). + */ + function drawSymmetrical(x1, y1, x2, y2) { + // 1. Get current settings + const segments = parseInt(symmetryType.value); + ctx.strokeStyle = colorPicker.value; + ctx.lineWidth = brushSizeInput.value; + + // 2. Translate to the center of the canvas + ctx.save(); + ctx.translate(CENTER_X, CENTER_Y); + + // 3. Loop through segments and apply transformations + for (let i = 0; i < segments; i++) { + // Calculate the angle for rotation + const angle = (2 * Math.PI) / segments; + + // --- DRAW THE PRIMARY SEGMENT --- + ctx.beginPath(); + + // Translate coordinates to be relative to the center + const pX1 = x1 - CENTER_X; + const pY1 = y1 - CENTER_Y; + const pX2 = x2 - CENTER_X; + const pY2 = y2 - CENTER_Y; + + ctx.moveTo(pX1, pY1); + ctx.lineTo(pX2, pY2); + ctx.stroke(); + + // --- DRAW THE MIRROR SEGMENT (Across the rotation line) --- + // If symmetry is even (4 or 8), draw a reflection across the Y-axis (vertical) + if (segments % 2 === 0) { + ctx.beginPath(); + // Scale(-1, 1) reflects the coordinates + ctx.moveTo(-pX1, pY1); + ctx.lineTo(-pX2, pY2); + ctx.stroke(); + } + + // Apply rotation for the next segment + ctx.rotate(angle); + } + + // Restore the canvas state (translate, rotate, and strokeStyle reset) + ctx.restore(); + } + + + // --- 4. Event Handlers --- + + // A. MOUSE MOVE: Draw when mouse is down + canvas.addEventListener('mousemove', (e) => { + if (!isDrawing) return; + + // Draw segment from (lastX, lastY) to (current X, Y) + drawSymmetrical(lastX, lastY, e.offsetX, e.offsetY); + + // Update the last position + lastX = e.offsetX; + lastY = e.offsetY; + }); + + // B. MOUSE DOWN: Start drawing + canvas.addEventListener('mousedown', (e) => { + isDrawing = true; + // Start a new path at the current position + [lastX, lastY] = [e.offsetX, e.offsetY]; + + // Draw a single dot to prevent starting segment from (0,0) + drawSymmetrical(lastX, lastY, lastX + 0.1, lastY + 0.1); + }); + + // C. MOUSE UP/OUT: Stop drawing + window.addEventListener('mouseup', () => { + isDrawing = false; + }); + + canvas.addEventListener('mouseout', () => { + isDrawing = false; + }); + + // D. Control Handlers + + // Update brush size display + brushSizeInput.addEventListener('input', () => { + sizeDisplay.textContent = brushSizeInput.value; + }); + + // Clear the canvas + clearButton.addEventListener('click', () => { + ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE); + // Draw center lines for easier symmetry orientation + drawCenterLines(); + }); + + // Helper to draw visual guides + function drawCenterLines() { + ctx.save(); + ctx.strokeStyle = '#cccccc'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(CENTER_X, 0); + ctx.lineTo(CENTER_X, CANVAS_SIZE); + ctx.moveTo(0, CENTER_Y); + ctx.lineTo(CANVAS_SIZE, CENTER_Y); + ctx.stroke(); + ctx.restore(); + } + + // Initial setup + drawCenterLines(); +}); \ No newline at end of file diff --git a/games/painter_game/style.css b/games/painter_game/style.css new file mode 100644 index 00000000..52153b40 --- /dev/null +++ b/games/painter_game/style.css @@ -0,0 +1,61 @@ +body { + font-family: 'Arial', sans-serif; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f0f4f8; +} + +#app-container { + background-color: white; + padding: 25px; + border-radius: 12px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + text-align: center; +} + +h1 { + color: #007bff; + margin-bottom: 20px; +} + +/* --- Controls Styling --- */ +#controls { + margin-bottom: 15px; + padding: 10px; + background-color: #e9e9e9; + border-radius: 5px; + display: flex; + gap: 15px; + justify-content: center; + align-items: center; + flex-wrap: wrap; +} + +#brush-size { + width: 80px; +} + +#clear-button { + padding: 8px 15px; + background-color: #e74c3c; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; +} + +#clear-button:hover { + background-color: #c0392b; +} + +/* --- Canvas Styling --- */ +#symmetry-canvas { + border: 3px solid #333; + background-color: white; + cursor: crosshair; +} \ No newline at end of file diff --git a/games/painting-rush/index.html b/games/painting-rush/index.html new file mode 100644 index 00000000..39e5edca --- /dev/null +++ b/games/painting-rush/index.html @@ -0,0 +1,37 @@ + + + + + + Painting Rush ๐ŸŽจ + + + +
    +
    +

    Painting Rush

    +

    Paint the targets before time runs out!

    +
    + +
    + Level: 1 + Score: 0 + Time: 30s +
    + + + +
    + +
    + + +
    + + + + diff --git a/games/painting-rush/script.js b/games/painting-rush/script.js new file mode 100644 index 00000000..f5d9a751 --- /dev/null +++ b/games/painting-rush/script.js @@ -0,0 +1,135 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const levelEl = document.getElementById('level'); +const scoreEl = document.getElementById('score'); +const timerEl = document.getElementById('timer'); +const restartBtn = document.getElementById('restartBtn'); +const gameOverEl = document.querySelector('.game-over'); +const finalScoreEl = document.getElementById('finalScore'); +const playAgainBtn = document.getElementById('playAgainBtn'); + +canvas.width = 500; +canvas.height = 400; + +// Game Variables +let targets = []; +let score = 0; +let level = 1; +let time = 30; +let gameInterval; +let timerInterval; +let isGameOver = false; + +const colors = ['#ff6b81', '#1e90ff', '#f1c40f', '#2ecc71', '#9b59b6', '#e67e22']; + +// Generate Targets +function createTargets(num = 5) { + targets = []; + for (let i = 0; i < num; i++) { + const radius = 20 + Math.random() * 15; + const x = radius + Math.random() * (canvas.width - radius * 2); + const y = radius + Math.random() * (canvas.height - radius * 2); + const color = colors[Math.floor(Math.random() * colors.length)]; + targets.push({ x, y, radius, color, painted: false }); + } +} + +// Draw Targets +function drawTargets() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + targets.forEach(t => { + ctx.beginPath(); + ctx.arc(t.x, t.y, t.radius, 0, Math.PI * 2); + ctx.fillStyle = t.painted ? '#ccc' : t.color; + ctx.fill(); + ctx.closePath(); + }); +} + +// Check if Click/Touch hits a target +function paintTarget(x, y) { + targets.forEach(t => { + const dist = Math.hypot(t.x - x, t.y - y); + if (dist < t.radius && !t.painted) { + t.painted = true; + score += 10; + scoreEl.textContent = score; + } + }); +} + +// Update Game State +function updateGame() { + drawTargets(); + // Check if all targets painted + if (targets.every(t => t.painted)) { + level++; + levelEl.textContent = level; + time += 10; // Add bonus time + createTargets(5 + level); // More targets each level + } +} + +// Timer +function startTimer() { + timerInterval = setInterval(() => { + time--; + timerEl.textContent = time; + if (time <= 0) endGame(); + }, 1000); +} + +// End Game +function endGame() { + clearInterval(timerInterval); + clearInterval(gameInterval); + isGameOver = true; + finalScoreEl.textContent = score; + gameOverEl.classList.remove('hidden'); +} + +// Game Loop +function gameLoop() { + if (!isGameOver) updateGame(); +} + +// Input Events +canvas.addEventListener('mousedown', (e) => { + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + paintTarget(x, y); +}); + +canvas.addEventListener('touchstart', (e) => { + e.preventDefault(); + const rect = canvas.getBoundingClientRect(); + const touch = e.touches[0]; + const x = touch.clientX - rect.left; + const y = touch.clientY - rect.top; + paintTarget(x, y); +}); + +// Restart Game +function startGame() { + score = 0; + level = 1; + time = 30; + isGameOver = false; + scoreEl.textContent = score; + levelEl.textContent = level; + timerEl.textContent = time; + gameOverEl.classList.add('hidden'); + createTargets(5); + clearInterval(gameInterval); + clearInterval(timerInterval); + startTimer(); + gameInterval = setInterval(gameLoop, 30); +} + +// Buttons +restartBtn.addEventListener('click', startGame); +playAgainBtn.addEventListener('click', startGame); + +// Start the game initially +startGame(); diff --git a/games/painting-rush/style.css b/games/painting-rush/style.css new file mode 100644 index 00000000..862bb9f8 --- /dev/null +++ b/games/painting-rush/style.css @@ -0,0 +1,76 @@ +/* General Styles */ +body { + margin: 0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #FFDEE9, #B5FFFC); + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; +} + +.game-container { + text-align: center; + background-color: rgba(255,255,255,0.9); + padding: 20px; + border-radius: 20px; + box-shadow: 0 8px 20px rgba(0,0,0,0.3); + max-width: 600px; + width: 90%; +} + +header h1 { + margin: 0; + font-size: 2rem; + color: #333; +} + +header .description { + font-size: 1rem; + color: #666; + margin-bottom: 10px; +} + +.game-info { + display: flex; + justify-content: space-around; + margin: 15px 0; + font-weight: bold; +} + +canvas { + border: 3px solid #333; + border-radius: 15px; + background-color: #f8f8f8; + display: block; + margin: 0 auto; + touch-action: none; /* For touch input */ +} + +.controls { + margin-top: 15px; +} + +button { + background-color: #ff6b81; + color: #fff; + border: none; + padding: 10px 20px; + margin: 5px; + border-radius: 10px; + cursor: pointer; + font-weight: bold; + transition: all 0.2s ease; +} + +button:hover { + background-color: #ff4757; +} + +.game-over { + margin-top: 20px; +} + +.hidden { + display: none; +} diff --git a/games/pattern-match/index.html b/games/pattern-match/index.html new file mode 100644 index 00000000..469fa3fb --- /dev/null +++ b/games/pattern-match/index.html @@ -0,0 +1,23 @@ + + + + + + Pattern Match Game + + + +
    +

    Pattern Match

    +

    Memorize the pattern, then click on the matching option below!

    +
    +
    Time:100
    +
    Score: 0
    + +
    + +
    +
    + + + \ No newline at end of file diff --git a/games/pattern-match/script.js b/games/pattern-match/script.js new file mode 100644 index 00000000..18a4575a --- /dev/null +++ b/games/pattern-match/script.js @@ -0,0 +1,181 @@ +// Pattern Match Game Script +// Memorize and match patterns + +var canvas = document.getElementById('gameCanvas'); +var ctx = canvas.getContext('2d'); +var timerDisplay = document.getElementById('timer'); +var scoreDisplay = document.getElementById('score'); +var restartBtn = document.getElementById('restart'); +var messageDiv = document.getElementById('message'); + +var currentPattern = []; +var options = []; +var correctIndex = 0; +var score = 0; +var timeLeft = 30; +var timerInterval; +var gameRunning = true; +var showingPattern = true; + +// Shape types +var shapes = ['circle', 'square', 'triangle']; +var colors = ['red', 'blue', 'green', 'yellow', 'purple']; + +// Initialize the game +function initGame() { + score = 0; + timeLeft = 30; + gameRunning = true; + showingPattern = true; + messageDiv.textContent = ''; + scoreDisplay.textContent = 'Score: ' + score; + generatePattern(); + startTimer(); + draw(); +} + +// Generate a random pattern +function generatePattern() { + currentPattern = []; + for (var i = 0; i < 3; i++) { + var shape = shapes[Math.floor(Math.random() * shapes.length)]; + var color = colors[Math.floor(Math.random() * colors.length)]; + currentPattern.push({ shape: shape, color: color }); + } + + // Generate options + options = []; + correctIndex = Math.floor(Math.random() * 4); + for (var i = 0; i < 4; i++) { + if (i === correctIndex) { + options.push(currentPattern.slice()); + } else { + var wrongPattern = []; + for (var j = 0; j < 3; j++) { + var shape = shapes[Math.floor(Math.random() * shapes.length)]; + var color = colors[Math.floor(Math.random() * colors.length)]; + wrongPattern.push({ shape: shape, color: color }); + } + options.push(wrongPattern); + } + } +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + if (showingPattern) { + // Draw the pattern to memorize + ctx.fillStyle = '#000'; + ctx.font = '20px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('Memorize this pattern:', canvas.width / 2, 50); + + for (var i = 0; i < currentPattern.length; i++) { + drawShape(currentPattern[i], 150 + i * 100, 120, 30); + } + + ctx.fillText('Click anywhere to continue', canvas.width / 2, 200); + } else { + // Draw the options + ctx.fillStyle = '#000'; + ctx.font = '18px Arial'; + ctx.fillText('Which one matches?', canvas.width / 2, 50); + + for (var i = 0; i < options.length; i++) { + var x = 100 + (i % 2) * 250; + var y = 120 + Math.floor(i / 2) * 150; + + // Draw option background + ctx.fillStyle = '#f0f0f0'; + ctx.fillRect(x - 60, y - 40, 120, 80); + ctx.strokeStyle = '#000'; + ctx.strokeRect(x - 60, y - 40, 120, 80); + + // Draw the pattern + for (var j = 0; j < options[i].length; j++) { + drawShape(options[i][j], x - 30 + j * 30, y, 15); + } + } + } +} + +// Draw a shape +function drawShape(item, x, y, size) { + ctx.fillStyle = item.color; + if (item.shape === 'circle') { + ctx.beginPath(); + ctx.arc(x, y, size, 0, Math.PI * 2); + ctx.fill(); + } else if (item.shape === 'square') { + ctx.fillRect(x - size, y - size, size * 2, size * 2); + } else if (item.shape === 'triangle') { + ctx.beginPath(); + ctx.moveTo(x, y - size); + ctx.lineTo(x - size, y + size); + ctx.lineTo(x + size, y + size); + ctx.closePath(); + ctx.fill(); + } +} + +// Handle canvas click +canvas.addEventListener('click', function(event) { + if (!gameRunning) return; + var rect = canvas.getBoundingClientRect(); + var x = event.clientX - rect.left; + var y = event.clientY - rect.top; + + if (showingPattern) { + showingPattern = false; + draw(); + } else { + // Check which option was clicked + for (var i = 0; i < options.length; i++) { + var optX = 100 + (i % 2) * 250; + var optY = 120 + Math.floor(i / 2) * 150; + if (x >= optX - 60 && x <= optX + 60 && y >= optY - 40 && y <= optY + 40) { + if (i === correctIndex) { + score++; + scoreDisplay.textContent = 'Score: ' + score; + messageDiv.textContent = 'Correct!'; + messageDiv.style.color = 'green'; + } else { + messageDiv.textContent = 'Wrong!'; + messageDiv.style.color = 'red'; + } + setTimeout(nextRound, 1000); + break; + } + } + } +}); + +// Next round +function nextRound() { + showingPattern = true; + generatePattern(); + draw(); +} + +// Start the timer +function startTimer() { + clearInterval(timerInterval); + timerInterval = setInterval(function() { + timeLeft--; + timerDisplay.textContent = 'Time: ' + timeLeft; + if (timeLeft <= 0) { + clearInterval(timerInterval); + gameRunning = false; + messageDiv.textContent = 'Time\'s up! Final Score: ' + score; + messageDiv.style.color = 'blue'; + } + }, 1000); +} + +// Restart button +restartBtn.addEventListener('click', initGame); + +// Start the game +initGame(); \ No newline at end of file diff --git a/games/pattern-match/style.css b/games/pattern-match/style.css new file mode 100644 index 00000000..6a11078e --- /dev/null +++ b/games/pattern-match/style.css @@ -0,0 +1,53 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #e1f5fe; +} + +.container { + text-align: center; +} + +h1 { + color: #01579b; +} + +.game-info { + margin-bottom: 20px; + display: flex; + justify-content: center; + gap: 20px; +} + +#timer, #score { + font-size: 24px; + font-weight: bold; +} + +#restart { + padding: 10px 20px; + font-size: 16px; + background-color: #0288d1; + color: white; + border: none; + cursor: pointer; +} + +#restart:hover { + background-color: #0277bd; +} + +canvas { + border: 2px solid #01579b; + background-color: #ffffff; +} + +#message { + margin-top: 20px; + font-size: 18px; + font-weight: bold; +} \ No newline at end of file diff --git a/games/perfect-aim/index.html b/games/perfect-aim/index.html new file mode 100644 index 00000000..11b169e9 --- /dev/null +++ b/games/perfect-aim/index.html @@ -0,0 +1,35 @@ + + + + + + Perfect Aim + + + +
    + + + +
    +
    Score: 0
    + +
    High Score: 0
    +
    + +
    +

    Perfect Aim

    +

    Click or press Spacebar at the exact moment the pointer is inside the target sector.

    + +
    + + +
    + + + + \ No newline at end of file diff --git a/games/perfect-aim/script.js b/games/perfect-aim/script.js new file mode 100644 index 00000000..cdf3c7be --- /dev/null +++ b/games/perfect-aim/script.js @@ -0,0 +1,188 @@ +// --- DOM Elements --- +const gameContainer = document.getElementById('game-container'); +const canvas = document.getElementById('game-canvas'); +const ctx = canvas.getContext('2d'); +const bgCanvas = document.getElementById('background-canvas'); +const bgCtx = bgCanvas.getContext('2d'); +const currentScoreEl = document.getElementById('current-score'); +const highScoreEl = document.getElementById('high-score'); +const comboDisplay = document.getElementById('combo-display'); +const comboCountEl = comboDisplay.querySelector('span'); +const startScreen = document.getElementById('start-screen'); +const gameOverScreen = document.getElementById('game-over-screen'); +const startBtn = document.getElementById('start-btn'); +const restartBtn = document.getElementById('restart-btn'); +const finalScoreEl = document.getElementById('final-score'); + +// --- Game Constants & State --- +const POINTER_COLOR = '#00ffff', TARGET_COLOR = '#f72585'; +const TARGET_RING_RADIUS_RATIO = 0.8, TARGET_THICKNESS_RATIO = 0.1; +const PERFECT_HIT_TOLERANCE = 0.02; + +let gameState = 'menu'; +let score = 0, highScore = 0, combo = 1; +let pointerAngle = 0, lastTime = 0; +let target = { center: 0, size: Math.PI / 4 }; +let rotationSpeed = 1.5; +let stars = []; + +// --- NEW: Sound Effects --- +const sounds = { hit: new Audio(''), miss: new Audio(''), perfect: new Audio('') }; +function playSound(sound) { try { sounds[sound].currentTime = 0; sounds[sound].play(); } catch (e) {} } + +// --- Main Game Loop --- +function gameLoop(timestamp) { + if (gameState === 'playing') { + const deltaTime = (timestamp - lastTime) / 1000; + lastTime = timestamp; + pointerAngle = (pointerAngle + rotationSpeed * deltaTime) % (Math.PI * 2); + draw(); + drawBackground(); + } + requestAnimationFrame(gameLoop); +} + +// --- Drawing Functions --- +function draw(clickMarkerAngle = null, clickMarkerColor = 'white') { + const radius = canvas.width / 2; + const center = { x: radius, y: radius }; + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const pulse = Math.sin(performance.now() / 200) * 0.05 + 0.95; + ctx.beginPath(); + ctx.arc(center.x, center.y, radius * TARGET_RING_RADIUS_RATIO, target.center - target.size / 2, target.center + target.size / 2); + ctx.strokeStyle = TARGET_COLOR; + ctx.lineWidth = radius * TARGET_THICKNESS_RATIO * pulse; + ctx.stroke(); + + ctx.save(); + ctx.translate(center.x, center.y); + ctx.rotate(pointerAngle); + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(radius, 0); + ctx.strokeStyle = POINTER_COLOR; + ctx.lineWidth = 5; + ctx.shadowColor = POINTER_COLOR; + ctx.shadowBlur = 15; + ctx.stroke(); + ctx.restore(); + + // NEW: Draw visual click indicator + if (clickMarkerAngle !== null) { + const markerX = center.x + Math.cos(clickMarkerAngle) * (radius * TARGET_RING_RADIUS_RATIO); + const markerY = center.y + Math.sin(clickMarkerAngle) * (radius * TARGET_RING_RADIUS_RATIO); + ctx.beginPath(); + ctx.arc(markerX, markerY, 5, 0, Math.PI * 2); + ctx.fillStyle = clickMarkerColor; + ctx.fill(); + } +} +function drawBackground() { /* Unchanged from previous version */ } +function createStarfield() { /* Unchanged from previous version */ } + +// --- Game Logic Functions --- +function startGame(event) { + if (event) event.stopPropagation(); + score = 0, combo = 1, rotationSpeed = 1.5; + target.size = Math.PI / 4; + pointerAngle = 0; + updateScoreDisplay(); + spawnTarget(); + startScreen.classList.add('hidden'); + gameOverScreen.classList.add('hidden'); + comboDisplay.classList.remove('hidden'); + gameState = 'playing'; + lastTime = performance.now(); + requestAnimationFrame(gameLoop); +} +function spawnTarget() { + target.center = Math.random() * Math.PI * 2; +} + +function handleInput(event) { + if (gameState !== 'playing') return; + if (event) event.stopPropagation(); + + const clickedAngle = pointerAngle; // Capture angle at the moment of input + + if (isAngleInSector(clickedAngle, target)) { + // HIT + const perfectDiff = getAngleDifference(clickedAngle, target.center); + if (perfectDiff <= PERFECT_HIT_TOLERANCE) { + score += combo * 5; + playSound('perfect'); + createHitPulse(true); + } else { + score += combo; + playSound('hit'); + createHitPulse(false); + } + combo++; + updateScoreDisplay(true); + spawnTarget(); + rotationSpeed *= 1.05; + target.size = Math.max(0.1, target.size * 0.98); + if (score > 5 && Math.random() < 0.2) { rotationSpeed *= -1; } + draw(clickedAngle, 'white'); // Show successful click + } else { + // MISS + playSound('miss'); + gameContainer.classList.add('miss-shake'); + setTimeout(() => gameContainer.classList.remove('miss-shake'), 400); + draw(clickedAngle, 'red'); // Show failed click + gameOver(); + } +} + +function gameOver() { + gameState = 'game_over'; + comboDisplay.classList.add('hidden'); + finalScoreEl.textContent = score; + if (score > highScore) { highScore = score; saveGame(); updateScoreDisplay(); } + setTimeout(() => { gameOverScreen.classList.remove('hidden'); }, 500); // Delay to show miss +} + +// --- UI & Utility Functions --- +function updateScoreDisplay(animateCombo = false) { + currentScoreEl.textContent = score; + highScoreEl.textContent = highScore; + comboCountEl.textContent = combo; + if (animateCombo) { comboDisplay.classList.add('combo-pulse'); setTimeout(() => comboDisplay.classList.remove('combo-pulse'), 300); } +} +function createHitPulse(isPerfect) { /* Unchanged */ } +function getAngleDifference(angle1, angle2) { + const diff = Math.abs(angle1 - angle2); + return Math.min(diff, 2 * Math.PI - diff); +} + +// CRITICAL BUG FIX: This new logic is mathematically perfect and has no edge cases. +function isAngleInSector(angle, sector) { + const diff = getAngleDifference(angle, sector.center); + return diff <= sector.size / 2; +} + +// --- Save/Load & Event Listeners --- +function saveGame() { localStorage.setItem('perfectAim_highScore', highScore); } +function loadGame() { + highScore = parseInt(localStorage.getItem('perfectAim_highScore')) || 0; + updateScoreDisplay(); +} + +startBtn.addEventListener('click', startGame); +restartBtn.addEventListener('click', startGame); +gameContainer.addEventListener('click', handleInput); +document.addEventListener('keydown', (e) => { + if (e.code === 'Space') { + e.preventDefault(); + if (gameState === 'playing') handleInput(e); + else if (gameState === 'menu' && startScreen.offsetParent !== null) startGame(e); + else if (gameState === 'game_over' && gameOverScreen.offsetParent !== null) startGame(e); + } +}); + +// --- Initial Setup --- +loadGame(); +createStarfield(); +draw(); +drawBackground(); \ No newline at end of file diff --git a/games/perfect-aim/style.css b/games/perfect-aim/style.css new file mode 100644 index 00000000..88d10f03 --- /dev/null +++ b/games/perfect-aim/style.css @@ -0,0 +1,35 @@ +/* --- Core Layout --- */ +html, body { height: 100%; margin: 0; overflow: hidden; background-color: #0c0c1e; } +body { font-family: 'Segoe UI', sans-serif; display: flex; align-items: center; justify-content: center; } +#game-container { position: relative; width: clamp(300px, 90vmin, 600px); height: clamp(300px, 90vmin, 600px); box-shadow: 0 10px 30px rgba(0,0,0,0.5); border-radius: 15px; } +#game-canvas, #background-canvas { + position: absolute; top: 0; left: 0; + width: 100%; height: 100%; + border-radius: 15px; +} +#game-canvas { z-index: 10; background-color: transparent; cursor: pointer; } +#background-canvas { z-index: 1; background-color: #16213e; } + +/* --- UI Elements --- */ +#score-display { position: absolute; top: 20px; left: 20px; right: 20px; display: flex; justify-content: space-between; align-items: center; color: white; font-size: clamp(1.2em, 4vmin, 2em); font-weight: bold; z-index: 10; text-shadow: 2px 2px 4px rgba(0,0,0,0.5); } +#combo-display { font-size: 1.5em; color: #f1c40f; font-style: italic; } +.combo-pulse { animation: pulse-combo 0.3s ease-out; } +@keyframes pulse-combo { 0% { transform: scale(1); } 50% { transform: scale(1.4); } 100% { transform: scale(1); } } + +/* --- Overlays --- */ +.overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(22, 33, 62, 0.95); border-radius: 15px; color: white; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; animation: fade-in 0.5s; z-index: 20; } +@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } +.overlay h2 { font-size: 3em; color: #00ffff; margin: 0; } +.overlay p { font-size: 1.2em; max-width: 80%; color: #e0fbfc; } +.overlay button { margin-top: 20px; padding: 15px 30px; font-size: 1.2em; border: none; border-radius: 10px; background-color: #00ffff; color: #16213e; cursor: pointer; transition: all 0.2s; font-weight: bold; } +.overlay button:hover { transform: scale(1.05); box-shadow: 0 0 20px #00ffff; } +.hidden { display: none !important; pointer-events: none; } + +/* --- Visual Feedback Animations --- */ +.miss-shake { animation: shake-effect 0.4s ease-in-out; } +@keyframes shake-effect { 0%, 100% { transform: translateX(0); } 20%, 60% { transform: translateX(-10px); } 40%, 80% { transform: translateX(10px); } } +.perfect-hit-pulse { + position: absolute; border-radius: 50%; border: 5px solid #f1c40f; + animation: perfect-pulse 0.5s ease-out forwards; z-index: 5; pointer-events: none; +} +@keyframes perfect-pulse { from { transform: scale(0.8); opacity: 1; } to { transform: scale(1.5); opacity: 0; } } \ No newline at end of file diff --git a/games/perfect-click/index.html b/games/perfect-click/index.html new file mode 100644 index 00000000..f1f4d8a6 --- /dev/null +++ b/games/perfect-click/index.html @@ -0,0 +1,39 @@ + + + + + + Perfect Click | Mini JS Games Hub + + + +
    +

    ๐ŸŽฏ Perfect Click

    +

    Click the button when the glowing line enters the green zone!

    + +
    +
    +
    +
    +
    + +
    + + + +
    + +
    +

    Score: 0

    +

    Combo: 0

    +

    Speed: 1x

    +
    + + + + +
    + + + + diff --git a/games/perfect-click/script.js b/games/perfect-click/script.js new file mode 100644 index 00000000..df758bb2 --- /dev/null +++ b/games/perfect-click/script.js @@ -0,0 +1,88 @@ +const line = document.querySelector(".line"); +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const restartBtn = document.getElementById("restart-btn"); +const scoreEl = document.getElementById("score"); +const comboEl = document.getElementById("combo"); +const speedEl = document.getElementById("speed"); + +const clickSound = document.getElementById("click-sound"); +const perfectSound = document.getElementById("perfect-sound"); +const failSound = document.getElementById("fail-sound"); + +let animationFrame; +let running = false; +let paused = false; +let direction = 1; +let linePos = 0; +let speed = 3; +let score = 0; +let combo = 0; + +const target = document.querySelector(".target-zone"); + +function animateLine() { + if (!running || paused) return; + + linePos += speed * direction; + if (linePos >= 100 || linePos <= 0) direction *= -1; + line.style.left = `${linePos}%`; + + animationFrame = requestAnimationFrame(animateLine); +} + +function isPerfectHit() { + const lineRect = line.getBoundingClientRect(); + const targetRect = target.getBoundingClientRect(); + return lineRect.left >= targetRect.left && lineRect.right <= targetRect.right; +} + +document.body.addEventListener("click", () => { + if (!running || paused) return; + clickSound.play(); + if (isPerfectHit()) { + perfectSound.play(); + score += 10; + combo++; + if (combo % 5 === 0) speed += 0.5; + speedEl.textContent = `${speed.toFixed(1)}x`; + target.style.boxShadow = "0 0 25px #0f0, 0 0 50px #0f0"; + setTimeout(() => target.style.boxShadow = "0 0 15px #0f0", 300); + } else { + failSound.play(); + combo = 0; + speed = Math.max(3, speed - 0.5); + target.style.boxShadow = "0 0 25px #f00, 0 0 50px #f00"; + setTimeout(() => target.style.boxShadow = "0 0 15px #0f0", 300); + } + scoreEl.textContent = score; + comboEl.textContent = combo; +}); + +startBtn.addEventListener("click", () => { + if (!running) { + running = true; + paused = false; + animateLine(); + } +}); + +pauseBtn.addEventListener("click", () => { + paused = !paused; + if (!paused) animateLine(); +}); + +restartBtn.addEventListener("click", () => { + cancelAnimationFrame(animationFrame); + running = false; + paused = false; + score = 0; + combo = 0; + speed = 3; + direction = 1; + linePos = 0; + scoreEl.textContent = score; + comboEl.textContent = combo; + speedEl.textContent = `${speed}x`; + line.style.left = "0%"; +}); diff --git a/games/perfect-click/style.css b/games/perfect-click/style.css new file mode 100644 index 00000000..9f7c3ad5 --- /dev/null +++ b/games/perfect-click/style.css @@ -0,0 +1,88 @@ +body { + font-family: 'Poppins', sans-serif; + background: radial-gradient(circle at top, #0f2027, #203a43, #2c5364); + color: #fff; + text-align: center; + height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +h1 { + text-shadow: 0 0 20px #0ff, 0 0 40px #00f; +} + +.info { + font-size: 1.1rem; + margin-bottom: 20px; + color: #ccc; +} + +.bar-area { + position: relative; + width: 80%; + height: 20px; + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + overflow: hidden; + margin-bottom: 40px; + box-shadow: 0 0 15px rgba(0,255,255,0.3); +} + +.bar { + position: absolute; + width: 100%; + height: 100%; + background: linear-gradient(90deg, #00f, #0ff); + opacity: 0.2; +} + +.target-zone { + position: absolute; + width: 15%; + height: 100%; + background: rgba(0,255,0,0.4); + border: 2px solid #0f0; + left: 45%; + box-shadow: 0 0 15px #0f0; + border-radius: 6px; +} + +.line { + position: absolute; + width: 4px; + height: 100%; + background: #fff; + left: 0; + box-shadow: 0 0 20px #0ff; + border-radius: 2px; +} + +.controls button { + background: #111; + border: 2px solid #0ff; + color: #0ff; + padding: 10px 20px; + margin: 0 5px; + border-radius: 8px; + cursor: pointer; + transition: 0.2s; +} + +.controls button:hover { + background: #0ff; + color: #111; + box-shadow: 0 0 20px #0ff; +} + +.scoreboard { + margin-top: 20px; + font-size: 1.2rem; +} + +.scoreboard span { + font-weight: bold; + color: #0ff; +} diff --git a/games/persona_quiz/index.html b/games/persona_quiz/index.html new file mode 100644 index 00000000..fd460bd8 --- /dev/null +++ b/games/persona_quiz/index.html @@ -0,0 +1,44 @@ + + + + + + The Persona Quiz + + + + +
    +

    ๐Ÿ‘ค The Persona Quiz

    + +
    +

    Player 1: Setup Your Persona

    +

    Answer these questions about yourself. Your answers will be saved for Player 2 to guess later!

    + +
    +
    + + + +
    + + + + +
    + + + + \ No newline at end of file diff --git a/games/persona_quiz/script.js b/games/persona_quiz/script.js new file mode 100644 index 00000000..d31ff6d2 --- /dev/null +++ b/games/persona_quiz/script.js @@ -0,0 +1,262 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. GAME DATA AND CONSTANTS --- + const QUESTIONS = [ + "What is your favorite color?", + "What is the name of your first pet?", + "What is your dream travel destination?", + "What is your go-to snack?", + "What is your preferred programming language?" + ]; + + const STORAGE_KEY = 'personaQuizAnswers'; + + // --- 2. DOM Elements --- + const gameContainer = document.getElementById('game-container'); + const setupMode = document.getElementById('setup-mode'); + const quizMode = document.getElementById('quiz-mode'); + + // Setup Mode elements + const setupQuestionArea = document.getElementById('setup-question-area'); + const nextSetupButton = document.getElementById('next-setup-button'); + const startQuizButton = document.getElementById('start-quiz-button'); + + // Quiz Mode elements + const quizQuestionArea = document.getElementById('quiz-question-area'); + const submitQuizButton = document.getElementById('submit-quiz-button'); + const quizFeedbackMessage = document.getElementById('quiz-feedback-message'); + const quizScoreSpan = document.getElementById('quiz-score'); + const quizTotalSpan = document.getElementById('quiz-total'); + + // Global elements + const resetButton = document.getElementById('reset-button'); + + // --- 3. GAME STATE VARIABLES --- + let player1Answers = {}; // Stores {question: answer} for P1 + let currentQuestionIndex = 0; + let quizScore = 0; + + // --- 4. CORE WEB STORAGE FUNCTIONS --- + + /** + * Loads answers from localStorage. + * @returns {Object|null} The stored answers or null. + */ + function loadAnswers() { + const storedData = localStorage.getItem(STORAGE_KEY); + if (storedData) { + return JSON.parse(storedData); + } + return null; + } + + /** + * Saves Player 1's answers to localStorage. + */ + function saveAnswers() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(player1Answers)); + } + + // --- 5. INITIALIZATION AND MODE SWITCHING --- + + /** + * Determines the game mode based on localStorage and starts the appropriate phase. + */ + function initGame() { + const storedAnswers = loadAnswers(); + + if (storedAnswers && Object.keys(storedAnswers).length === QUESTIONS.length) { + // Answers exist: Start Quiz Mode (Player 2) + player1Answers = storedAnswers; + quizTotalSpan.textContent = QUESTIONS.length; + switchMode('quiz'); + startQuizMode(); + } else { + // Answers don't exist/incomplete: Start Setup Mode (Player 1) + player1Answers = {}; + switchMode('setup'); + startSetupMode(); + } + } + + /** + * Visually switches between Setup and Quiz mode. + */ + function switchMode(mode) { + if (mode === 'setup') { + setupMode.style.display = 'block'; + quizMode.style.display = 'none'; + } else if (mode === 'quiz') { + setupMode.style.display = 'none'; + quizMode.style.display = 'block'; + } + } + + // --- 6. SETUP MODE (Player 1) --- + + /** + * Displays the current question for Player 1 to answer. + */ + function startSetupMode() { + currentQuestionIndex = 0; + nextSetupButton.style.display = 'block'; + startQuizButton.style.display = 'none'; + + displaySetupQuestion(); + } + + /** + * Renders the current question and input field for Player 1. + */ + function displaySetupQuestion() { + if (currentQuestionIndex >= QUESTIONS.length) { + // All questions answered + nextSetupButton.style.display = 'none'; + startQuizButton.style.display = 'block'; + setupQuestionArea.innerHTML = '

    Setup Complete! Ready to save your persona.

    '; + return; + } + + const question = QUESTIONS[currentQuestionIndex]; + setupQuestionArea.innerHTML = ` + +

    ${question}

    + + `; + + const setupInput = document.getElementById('setup-input'); + setupInput.addEventListener('input', () => { + nextSetupButton.disabled = setupInput.value.trim() === ''; + }); + setupInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter' && !nextSetupButton.disabled) { + handleSetupNext(); + } + }); + setupInput.focus(); + nextSetupButton.disabled = true; + } + + /** + * Saves the current answer and advances to the next question. + */ + function handleSetupNext() { + const input = document.getElementById('setup-input'); + const question = QUESTIONS[currentQuestionIndex]; + + if (input.value.trim() !== '') { + player1Answers[question] = input.value.trim(); + currentQuestionIndex++; + displaySetupQuestion(); + } + } + + // --- 7. QUIZ MODE (Player 2) --- + + /** + * Starts the guessing phase for Player 2. + */ + function startQuizMode() { + currentQuestionIndex = 0; + quizScore = 0; + quizScoreSpan.textContent = quizScore; + + displayQuizQuestion(); + } + + /** + * Displays the current quiz question and input field for Player 2. + */ + function displayQuizQuestion() { + if (currentQuestionIndex >= QUESTIONS.length) { + endGame(); + return; + } + + const question = QUESTIONS[currentQuestionIndex]; + quizQuestionArea.innerHTML = ` + +

    ${question}

    + + `; + quizFeedbackMessage.textContent = ''; + + const quizInput = document.getElementById('quiz-input'); + quizInput.addEventListener('input', () => { + submitQuizButton.disabled = quizInput.value.trim() === ''; + }); + quizInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter' && !submitQuizButton.disabled) { + handleSubmitGuess(); + } + }); + quizInput.focus(); + submitQuizButton.disabled = true; + } + + /** + * Checks Player 2's guess against Player 1's saved answer. + */ + function handleSubmitGuess() { + const input = document.getElementById('quiz-input'); + const question = QUESTIONS[currentQuestionIndex]; + const player2Guess = input.value.trim().toLowerCase(); + const correctAnswer = player1Answers[question].toLowerCase(); + + // Normalize answers for better tolerance (e.g., ignore white space) + const normalizedGuess = player2Guess.replace(/\s/g, ''); + const normalizedAnswer = correctAnswer.replace(/\s/g, ''); + + submitQuizButton.disabled = true; + + if (normalizedGuess === normalizedAnswer) { + quizScore++; + quizScoreSpan.textContent = quizScore; + quizFeedbackMessage.innerHTML = `โœ… CORRECT! The answer was "${player1Answers[question]}".`; + } else { + quizFeedbackMessage.innerHTML = `โŒ INCORRECT. The answer was "${player1Answers[question]}".`; + } + + // Advance to the next question after a brief delay + currentQuestionIndex++; + setTimeout(displayQuizQuestion, 2500); + } + + /** + * Concludes the game and shows the final score. + */ + function endGame() { + quizQuestionArea.innerHTML = ''; + submitQuizButton.style.display = 'none'; + quizFeedbackMessage.style.color = '#007bff'; + quizFeedbackMessage.innerHTML = ` +

    GAME COMPLETE!

    +

    Final Score: ${quizScore} out of ${QUESTIONS.length}.

    +

    Press **Reset Data** to set up a new persona.

    + `; + } + + // --- 8. EVENT LISTENERS --- + + // Setup Mode Listener + nextSetupButton.addEventListener('click', handleSetupNext); + + // Save & Start Quiz Listener + startQuizButton.addEventListener('click', () => { + saveAnswers(); + initGame(); // Re-initialize to start the quiz mode + }); + + // Quiz Mode Listener + submitQuizButton.addEventListener('click', handleSubmitGuess); + + // Global Reset Listener + resetButton.addEventListener('click', () => { + localStorage.removeItem(STORAGE_KEY); + player1Answers = {}; + alert('All saved persona data has been cleared. Starting Player 1 setup.'); + initGame(); + }); + + // Start the whole application + initGame(); +}); \ No newline at end of file diff --git a/games/persona_quiz/style.css b/games/persona_quiz/style.css new file mode 100644 index 00000000..a7164700 --- /dev/null +++ b/games/persona_quiz/style.css @@ -0,0 +1,123 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #e8f4f8; /* Light blue background */ + color: #34495e; +} + +#game-container { + background-color: white; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 500px; + width: 90%; +} + +h1 { + color: #007bff; + margin-bottom: 25px; +} + +h2 { + color: #2c3e50; + border-bottom: 1px solid #ccc; + padding-bottom: 10px; + margin-bottom: 20px; +} + +/* --- Shared Question/Input Styles --- */ +#setup-question-area, #quiz-question-area { + min-height: 100px; /* Ensure consistent height */ + padding: 15px; + border: 1px dashed #999; + border-radius: 8px; + margin-bottom: 20px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 15px; +} + +input[type="text"] { + padding: 10px; + font-size: 1.1em; + width: 80%; + border: 2px solid #ccc; + border-radius: 5px; +} + +/* --- Button Styles --- */ +button { + padding: 10px 20px; + font-size: 1em; + font-weight: bold; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s; + margin: 5px; +} + +#next-setup-button, #submit-quiz-button { + background-color: #2ecc71; /* Green */ + color: white; +} + +#next-setup-button:hover:not(:disabled), #submit-quiz-button:hover:not(:disabled) { + background-color: #27ae60; +} + +#start-quiz-button { + background-color: #007bff; /* Blue */ + color: white; +} + +#start-quiz-button:hover { + background-color: #0056b3; +} + +.global-control { + margin-top: 30px; + background-color: #e74c3c; /* Red */ + color: white; +} + +.global-control:hover { + background-color: #c0392b; +} + +button:disabled { + background-color: #bdc3c7; + cursor: not-allowed; + opacity: 0.7; +} + +/* --- Quiz Mode Status --- */ +#quiz-status { + font-size: 1.2em; + font-weight: bold; + color: #3498db; + margin-bottom: 20px; +} + +#quiz-feedback-message { + min-height: 1.5em; + font-weight: bold; + margin-top: 15px; +} + +/* Specific feedback colors */ +.correct { + color: #2ecc71; +} + +.incorrect { + color: #e74c3c; +} \ No newline at end of file diff --git a/games/pet-care/index.html b/games/pet-care/index.html new file mode 100644 index 00000000..70b13ff8 --- /dev/null +++ b/games/pet-care/index.html @@ -0,0 +1,157 @@ + + + + + + Pet Care - Mini JS Games Hub + + + +
    +
    +

    ๐Ÿพ Pet Care

    +

    Take care of your virtual pet and keep it happy and healthy!

    +
    + +
    +
    +
    +
    +
    +
    +
    +
    โ€ข
    +
    โ€ข
    +
    +
    ฯ‰
    +
    +
    +
    โ–ฒ
    +
    โ–ฒ
    +
    +
    +
    +
    + +
    Buddy
    +
    Happy
    +
    + +
    +
    + +
    +
    +
    + 100% +
    + +
    + +
    +
    +
    + 100% +
    + +
    + +
    +
    +
    + 100% +
    + +
    + +
    +
    +
    + 100% +
    +
    +
    + +
    +

    Care Actions

    +
    + + + + + +
    + +
    +

    Pet Info

    +
    + Age: + 1 day +
    +
    + Level: + 1 +
    +
    + Experience: + 0/100 +
    +
    + Coins: + 50 +
    +
    +
    + +
    +

    Mini Games

    +
    + + + +
    + +
    +
    +

    Choose a mini game to play with your pet!

    +
    ๐ŸŽฎ
    +
    +
    +
    +
    + +
    + +
    +

    How to Play:

    +
      +
    • Keep your pet's hunger, happiness, health, and energy above 20%
    • +
    • Feed your pet regularly to maintain hunger levels
    • +
    • Play with your pet to increase happiness
    • +
    • Clean your pet when it gets dirty
    • +
    • Use medicine when your pet is sick
    • +
    • Let your pet sleep to restore energy
    • +
    • Play mini games to earn coins and experience
    • +
    • Level up your pet by gaining experience
    • +
    +
    +
    + + + + \ No newline at end of file diff --git a/games/pet-care/script.js b/games/pet-care/script.js new file mode 100644 index 00000000..ed266a56 --- /dev/null +++ b/games/pet-care/script.js @@ -0,0 +1,498 @@ +// Pet Care Game Logic +class PetCare { + constructor() { + this.pet = { + name: 'Buddy', + age: 1, + level: 1, + experience: 0, + coins: 50, + hunger: 100, + happiness: 100, + health: 100, + energy: 100, + mood: 'Happy', + isSleeping: false, + lastFed: 0, + lastPlayed: 0, + lastCleaned: 0, + lastHealed: 0, + lastSlept: 0 + }; + + this.gameLoop = null; + this.notifications = []; + this.currentMinigame = null; + this.cooldowns = { + feed: 0, + play: 0, + clean: 0, + heal: 0, + sleep: 0 + }; + + this.init(); + } + + init() { + this.loadGame(); + this.bindEvents(); + this.startGameLoop(); + this.updateDisplay(); + this.showNotification('Welcome back! Take good care of your pet!', 'success'); + } + + bindEvents() { + // Action buttons + document.getElementById('feed-btn').addEventListener('click', () => this.feedPet()); + document.getElementById('play-btn').addEventListener('click', () => this.playWithPet()); + document.getElementById('clean-btn').addEventListener('click', () => this.cleanPet()); + document.getElementById('heal-btn').addEventListener('click', () => this.healPet()); + document.getElementById('sleep-btn').addEventListener('click', () => this.putPetToSleep()); + + // Mini game buttons + document.getElementById('fetch-game-btn').addEventListener('click', () => this.startFetchGame()); + document.getElementById('puzzle-game-btn').addEventListener('click', () => this.startPuzzleGame()); + document.getElementById('memory-game-btn').addEventListener('click', () => this.startMemoryGame()); + + // Keyboard shortcuts + document.addEventListener('keydown', (e) => { + switch(e.key.toLowerCase()) { + case 'f': + if (!e.ctrlKey) this.feedPet(); + break; + case 'p': + if (!e.ctrlKey) this.playWithPet(); + break; + case 'c': + if (!e.ctrlKey) this.cleanPet(); + break; + case 'h': + if (!e.ctrlKey) this.healPet(); + break; + case 's': + if (!e.ctrlKey && e.shiftKey) this.putPetToSleep(); + break; + case 'r': + if (e.ctrlKey) { + e.preventDefault(); + this.resetGame(); + } + break; + } + }); + } + + startGameLoop() { + this.gameLoop = setInterval(() => { + this.updatePetStats(); + this.updateCooldowns(); + this.checkPetStatus(); + this.updateDisplay(); + this.autoSave(); + }, 1000); // Update every second + } + + updatePetStats() { + const now = Date.now(); + + // Decrease stats over time + if (!this.pet.isSleeping) { + this.pet.hunger = Math.max(0, this.pet.hunger - 0.5); + this.pet.happiness = Math.max(0, this.pet.happiness - 0.3); + this.pet.energy = Math.max(0, this.pet.energy - 0.4); + } else { + // Sleeping restores energy + this.pet.energy = Math.min(100, this.pet.energy + 1); + } + + // Health decreases if other stats are low + if (this.pet.hunger < 20 || this.pet.happiness < 20 || this.pet.energy < 20) { + this.pet.health = Math.max(0, this.pet.health - 0.2); + } else if (this.pet.health < 100) { + // Slowly recover health if all stats are good + this.pet.health = Math.min(100, this.pet.health + 0.1); + } + + // Update age (1 day = 1 minute of playtime) + this.pet.age += 1/60; // Roughly 1 minute = 1 day + + // Update mood based on stats + this.updateMood(); + } + + updateMood() { + const avgStats = (this.pet.hunger + this.pet.happiness + this.pet.health + this.pet.energy) / 4; + + if (this.pet.health < 30) { + this.pet.mood = 'Sick'; + } else if (avgStats > 80) { + this.pet.mood = 'Very Happy'; + } else if (avgStats > 60) { + this.pet.mood = 'Happy'; + } else if (avgStats > 40) { + this.pet.mood = 'Okay'; + } else if (avgStats > 20) { + this.pet.mood = 'Sad'; + } else { + this.pet.mood = 'Very Sad'; + } + } + + updateCooldowns() { + const now = Date.now(); + Object.keys(this.cooldowns).forEach(action => { + if (this.cooldowns[action] > 0) { + this.cooldowns[action] = Math.max(0, this.cooldowns[action] - 1000); + } + }); + } + + checkPetStatus() { + // Check for critical conditions + if (this.pet.health <= 0) { + this.showNotification('Your pet has passed away! Game Over.', 'error'); + this.resetGame(); + return; + } + + if (this.pet.hunger <= 10) { + this.showNotification('Your pet is very hungry!', 'warning'); + } + + if (this.pet.happiness <= 10) { + this.showNotification('Your pet is very sad!', 'warning'); + } + + if (this.pet.energy <= 10) { + this.showNotification('Your pet is very tired!', 'warning'); + } + + if (this.pet.health <= 20) { + this.showNotification('Your pet is sick! Use medicine.', 'error'); + } + } + + feedPet() { + if (this.cooldowns.feed > 0) return; + + this.pet.hunger = Math.min(100, this.pet.hunger + 30); + this.pet.happiness = Math.min(100, this.pet.happiness + 5); + this.cooldowns.feed = 10000; // 10 second cooldown + this.pet.lastFed = Date.now(); + + this.showEffect('๐ŸŽ'); + this.showNotification('Yummy! Your pet enjoyed the food!', 'success'); + this.playSound('feed'); + } + + playWithPet() { + if (this.cooldowns.play > 0) return; + + this.pet.happiness = Math.min(100, this.pet.happiness + 25); + this.pet.energy = Math.max(0, this.pet.energy - 10); + this.pet.experience += 10; + this.cooldowns.play = 15000; // 15 second cooldown + this.pet.lastPlayed = Date.now(); + + this.showEffect('๐ŸŽพ'); + this.showNotification('Your pet had fun playing!', 'success'); + this.checkLevelUp(); + this.playSound('play'); + } + + cleanPet() { + if (this.cooldowns.clean > 0) return; + + this.pet.health = Math.min(100, this.pet.health + 15); + this.pet.happiness = Math.min(100, this.pet.happiness + 10); + this.cooldowns.clean = 20000; // 20 second cooldown + this.pet.lastCleaned = Date.now(); + + this.showEffect('๐Ÿงผ'); + this.showNotification('Your pet is clean and happy!', 'success'); + this.playSound('clean'); + } + + healPet() { + if (this.cooldowns.heal > 0 || this.pet.coins < 20) return; + + this.pet.health = Math.min(100, this.pet.health + 40); + this.pet.coins -= 20; + this.cooldowns.heal = 30000; // 30 second cooldown + this.pet.lastHealed = Date.now(); + + this.showEffect('๐Ÿ’Š'); + this.showNotification('Your pet feels much better!', 'success'); + this.playSound('heal'); + } + + putPetToSleep() { + if (this.cooldowns.sleep > 0) return; + + this.pet.isSleeping = !this.pet.isSleeping; + this.cooldowns.sleep = 60000; // 60 second cooldown + this.pet.lastSlept = Date.now(); + + if (this.pet.isSleeping) { + this.showEffect('๐Ÿ˜ด'); + this.showNotification('Your pet is sleeping peacefully...', 'success'); + this.updatePetAppearance('sleeping'); + } else { + this.showNotification('Your pet woke up refreshed!', 'success'); + this.updatePetAppearance('awake'); + } + this.playSound('sleep'); + } + + checkLevelUp() { + const expNeeded = this.pet.level * 100; + if (this.pet.experience >= expNeeded) { + this.pet.level++; + this.pet.experience -= expNeeded; + this.pet.coins += 25; + this.showNotification(`Level up! Your pet is now level ${this.pet.level}!`, 'success'); + this.showEffect('โญ'); + this.playSound('levelup'); + } + } + + startFetchGame() { + this.currentMinigame = 'fetch'; + this.showMinigame('fetch'); + this.showNotification('Play fetch with your pet!', 'success'); + + // Simple fetch game + setTimeout(() => { + const success = Math.random() > 0.3; // 70% success rate + if (success) { + this.pet.happiness += 15; + this.pet.experience += 15; + this.pet.coins += 5; + this.showNotification('Great catch! Your pet earned 5 coins!', 'success'); + this.checkLevelUp(); + } else { + this.showNotification('The ball got away. Try again!', 'warning'); + } + this.endMinigame(); + }, 3000); + } + + startPuzzleGame() { + this.currentMinigame = 'puzzle'; + this.showMinigame('puzzle'); + this.showNotification('Solve the puzzle with your pet!', 'success'); + + // Simple puzzle game + setTimeout(() => { + const success = Math.random() > 0.4; // 60% success rate + if (success) { + this.pet.happiness += 20; + this.pet.experience += 20; + this.pet.coins += 8; + this.showNotification('Puzzle solved! Your pet earned 8 coins!', 'success'); + this.checkLevelUp(); + } else { + this.showNotification('Puzzle was too tricky. Try again!', 'warning'); + } + this.endMinigame(); + }, 4000); + } + + startMemoryGame() { + this.currentMinigame = 'memory'; + this.showMinigame('memory'); + this.showNotification('Test your pet\'s memory!', 'success'); + + // Simple memory game + setTimeout(() => { + const success = Math.random() > 0.5; // 50% success rate + if (success) { + this.pet.happiness += 25; + this.pet.experience += 25; + this.pet.coins += 10; + this.showNotification('Perfect memory! Your pet earned 10 coins!', 'success'); + this.checkLevelUp(); + } else { + this.showNotification('Memory failed. Keep practicing!', 'warning'); + } + this.endMinigame(); + }, 5000); + } + + showMinigame(gameType) { + const gameArea = document.getElementById('game-area-display'); + gameArea.innerHTML = ` +
    +

    ${gameType.charAt(0).toUpperCase() + gameType.slice(1)} Game

    +
    ๐ŸŽฎ
    +

    Playing...

    +
    + `; + } + + endMinigame() { + this.currentMinigame = null; + const gameArea = document.getElementById('game-area-display'); + gameArea.innerHTML = ` +
    +

    Choose a mini game to play with your pet!

    +
    ๐ŸŽฎ
    +
    + `; + } + + showEffect(emoji) { + const effectsContainer = document.getElementById('pet-effects'); + const effect = document.createElement('div'); + effect.className = 'effect'; + effect.textContent = emoji; + effect.style.left = Math.random() * 100 + 'px'; + effectsContainer.appendChild(effect); + + setTimeout(() => { + effect.remove(); + }, 2000); + } + + updatePetAppearance(state) { + const mouth = document.getElementById('mouth'); + const eyes = document.querySelectorAll('.eye'); + + if (state === 'sleeping') { + mouth.textContent = 'u'; + eyes.forEach(eye => eye.textContent = '-'); + } else { + mouth.textContent = 'ฯ‰'; + eyes.forEach(eye => eye.textContent = 'โ€ข'); + } + } + + showNotification(message, type = 'info') { + const notifications = document.getElementById('notifications'); + const notification = document.createElement('div'); + notification.className = `notification ${type}`; + notification.textContent = message; + + notifications.appendChild(notification); + + setTimeout(() => { + notification.remove(); + }, 4000); + } + + playSound(action) { + // Simple sound effects using Web Audio API + try { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + let frequency = 440; // A4 + switch(action) { + case 'feed': frequency = 523; break; // C5 + case 'play': frequency = 659; break; // E5 + case 'clean': frequency = 784; break; // G5 + case 'heal': frequency = 988; break; // B5 + case 'sleep': frequency = 330; break; // E4 + case 'levelup': frequency = 1047; break; // C6 + } + + oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime); + gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.start(); + oscillator.stop(audioContext.currentTime + 0.3); + } catch (e) { + // Web Audio API not supported, skip sound + } + } + + updateDisplay() { + // Update pet info + document.getElementById('pet-name').textContent = this.pet.name; + document.getElementById('pet-mood').textContent = this.pet.mood; + document.getElementById('pet-age').textContent = Math.floor(this.pet.age) + ' days'; + document.getElementById('pet-level').textContent = this.pet.level; + document.getElementById('pet-exp').textContent = `${this.pet.experience}/${this.pet.level * 100}`; + document.getElementById('pet-coins').textContent = this.pet.coins; + + // Update stat bars + document.getElementById('hunger-fill').style.width = this.pet.hunger + '%'; + document.getElementById('happiness-fill').style.width = this.pet.happiness + '%'; + document.getElementById('health-fill').style.width = this.pet.health + '%'; + document.getElementById('energy-fill').style.width = this.pet.energy + '%'; + + // Update stat values + document.getElementById('hunger-value').textContent = Math.round(this.pet.hunger) + '%'; + document.getElementById('happiness-value').textContent = Math.round(this.pet.happiness) + '%'; + document.getElementById('health-value').textContent = Math.round(this.pet.health) + '%'; + document.getElementById('energy-value').textContent = Math.round(this.pet.energy) + '%'; + + // Update cooldown displays + Object.keys(this.cooldowns).forEach(action => { + const cooldownEl = document.getElementById(`${action}-cooldown`); + const btn = document.getElementById(`${action}-btn`); + + if (this.cooldowns[action] > 0) { + const seconds = Math.ceil(this.cooldowns[action] / 1000); + cooldownEl.textContent = seconds + 's'; + btn.disabled = true; + } else { + cooldownEl.textContent = ''; + btn.disabled = false; + } + }); + + // Update heal button based on coins + const healBtn = document.getElementById('heal-btn'); + healBtn.disabled = this.pet.coins < 20 || this.cooldowns.heal > 0; + } + + saveGame() { + const gameData = { + pet: this.pet, + timestamp: Date.now() + }; + localStorage.setItem('petCareGame', JSON.stringify(gameData)); + } + + loadGame() { + const savedData = localStorage.getItem('petCareGame'); + if (savedData) { + const gameData = JSON.parse(savedData); + this.pet = { ...this.pet, ...gameData.pet }; + } + } + + autoSave() { + // Auto-save every 30 seconds + if (Date.now() % 30000 < 1000) { + this.saveGame(); + } + } + + resetGame() { + if (confirm('Are you sure you want to reset the game? All progress will be lost!')) { + localStorage.removeItem('petCareGame'); + location.reload(); + } + } +} + +// Initialize the game when the page loads +document.addEventListener('DOMContentLoaded', () => { + new PetCare(); +}); + +// Enable audio on first user interaction +document.addEventListener('click', () => { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + if (audioContext && audioContext.state === 'suspended') { + audioContext.resume(); + } +}, { once: true }); \ No newline at end of file diff --git a/games/pet-care/style.css b/games/pet-care/style.css new file mode 100644 index 00000000..4bc7fa1c --- /dev/null +++ b/games/pet-care/style.css @@ -0,0 +1,493 @@ +/* Pet Care Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); + color: #2c3e50; + min-height: 100vh; + padding: 20px; +} + +body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle at 20% 80%, rgba(168, 237, 234, 0.3) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(254, 214, 227, 0.3) 0%, transparent 50%), + radial-gradient(circle at 40% 40%, rgba(255, 182, 193, 0.2) 0%, transparent 50%); + pointer-events: none; + z-index: -1; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 30px; +} + +header h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.1); + background: linear-gradient(45deg, #ff6b6b, #ffd93d, #6bcf7f, #4d96ff); + background-size: 400% 400%; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + animation: gradientShift 3s ease infinite; +} + +@keyframes gradientShift { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +header p { + font-size: 1.2em; + opacity: 0.8; +} + +.game-area { + display: grid; + grid-template-columns: 1fr 300px; + gap: 20px; + margin-bottom: 30px; +} + +.pet-display { + background: rgba(255, 255, 255, 0.9); + padding: 30px; + border-radius: 20px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(10px); + border: 2px solid rgba(255, 255, 255, 0.2); +} + +.pet-container { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 30px; +} + +.pet { + position: relative; + width: 200px; + height: 200px; + margin-bottom: 20px; +} + +.pet-body { + position: relative; + width: 150px; + height: 120px; + background: linear-gradient(135deg, #ffb6c1, #ffa07a); + border-radius: 75px 75px 50px 50px; + margin: 0 auto; + border: 3px solid #ff69b4; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + animation: float 3s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-10px); } +} + +.pet-face { + position: absolute; + top: 30px; + left: 50%; + transform: translateX(-50%); + text-align: center; +} + +.eyes { + display: flex; + justify-content: space-between; + width: 40px; + margin: 0 auto 10px; +} + +.eye { + font-size: 1.5em; + color: #2c3e50; + animation: blink 4s infinite; +} + +@keyframes blink { + 0%, 90%, 100% { opacity: 1; } + 95% { opacity: 0; } +} + +.mouth { + font-size: 1.2em; + color: #ff1493; +} + +.pet-ears { + position: absolute; + top: -15px; + left: 50%; + transform: translateX(-50%); + display: flex; + justify-content: space-between; + width: 80px; +} + +.ear { + font-size: 1.5em; + color: #ffb6c1; +} + +.pet-effects { + position: absolute; + top: -20px; + left: 50%; + transform: translateX(-50%); + pointer-events: none; +} + +.effect { + position: absolute; + font-size: 1.5em; + animation: fadeUp 2s ease-out forwards; +} + +@keyframes fadeUp { + 0% { + opacity: 1; + transform: translateY(0px) scale(1); + } + 100% { + opacity: 0; + transform: translateY(-50px) scale(1.5); + } +} + +.pet-name { + font-size: 1.5em; + font-weight: bold; + color: #ff69b4; + text-shadow: 1px 1px 2px rgba(0,0,0,0.1); +} + +.pet-mood { + font-size: 1.1em; + color: #666; + margin-top: 5px; +} + +.pet-stats { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; +} + +.stat { + display: flex; + flex-direction: column; + gap: 5px; +} + +.stat label { + font-weight: bold; + font-size: 0.9em; + color: #2c3e50; +} + +.stat-bar { + width: 100%; + height: 12px; + background: rgba(0, 0, 0, 0.1); + border-radius: 6px; + overflow: hidden; +} + +.stat-fill { + height: 100%; + border-radius: 6px; + transition: width 0.5s ease, background-color 0.3s ease; +} + +.hunger-fill { background: linear-gradient(90deg, #ff6b6b, #ff4757); } +.happiness-fill { background: linear-gradient(90deg, #ffd93d, #ffa502); } +.health-fill { background: linear-gradient(90deg, #6bcf7f, #2ed573); } +.energy-fill { background: linear-gradient(90deg, #4d96ff, #3742fa); } + +.stat-value { + font-size: 0.8em; + font-weight: bold; + text-align: center; +} + +.actions-panel, .minigames-panel { + background: rgba(255, 255, 255, 0.9); + padding: 20px; + border-radius: 15px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.actions-panel h3, .minigames-panel h3 { + color: #ff69b4; + margin-bottom: 15px; + text-align: center; +} + +.action-buttons { + display: grid; + grid-template-columns: 1fr; + gap: 10px; + margin-bottom: 20px; +} + +.action-btn { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + border: none; + padding: 12px 15px; + border-radius: 25px; + font-size: 1em; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + position: relative; + overflow: hidden; +} + +.action-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); +} + +.action-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.action-btn:active:not(:disabled) { + transform: translateY(0px); +} + +.cooldown { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 0.8em; + font-weight: bold; + color: white; + text-shadow: 1px 1px 2px rgba(0,0,0,0.5); +} + +.pet-info { + background: rgba(255, 255, 255, 0.7); + padding: 15px; + border-radius: 10px; +} + +.pet-info h4 { + color: #4d96ff; + margin-bottom: 10px; + text-align: center; +} + +.info-item { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + padding: 5px 0; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} + +.info-item:last-child { + border-bottom: none; +} + +.label { + font-weight: bold; + color: #2c3e50; +} + +.value { + color: #ff69b4; + font-weight: bold; +} + +.minigame-buttons { + display: grid; + grid-template-columns: 1fr; + gap: 8px; + margin-bottom: 15px; +} + +.minigame-btn { + background: linear-gradient(135deg, #ff9ff3, #f368e0); + color: white; + border: none; + padding: 10px 12px; + border-radius: 20px; + font-size: 0.9em; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.minigame-btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.game-area-display { + min-height: 150px; + background: rgba(255, 255, 255, 0.7); + border-radius: 10px; + padding: 15px; + display: flex; + align-items: center; + justify-content: center; +} + +.game-placeholder { + text-align: center; + color: #666; +} + +.pet-game-icon { + font-size: 3em; + margin-top: 10px; +} + +.notifications { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; + max-width: 300px; +} + +.notification { + background: rgba(255, 255, 255, 0.95); + border-radius: 10px; + padding: 15px; + margin-bottom: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + border-left: 4px solid #4d96ff; + animation: slideIn 0.5s ease-out; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.notification.success { border-left-color: #6bcf7f; } +.notification.warning { border-left-color: #ffd93d; } +.notification.error { border-left-color: #ff6b6b; } + +.instructions { + background: rgba(255, 255, 255, 0.9); + padding: 20px; + border-radius: 15px; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.instructions h3 { + color: #ff69b4; + margin-bottom: 15px; +} + +.instructions ul { + list-style: none; + padding: 0; +} + +.instructions li { + margin-bottom: 8px; + padding-left: 20px; + position: relative; +} + +.instructions li:before { + content: "๐Ÿพ"; + position: absolute; + left: 0; + color: #ffd93d; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .game-area { + grid-template-columns: 1fr; + gap: 15px; + } + + .pet-display { + padding: 20px; + } + + .pet { + width: 150px; + height: 150px; + } + + .pet-body { + width: 120px; + height: 100px; + } + + .pet-stats { + grid-template-columns: 1fr; + gap: 10px; + } + + header h1 { + font-size: 2em; + } +} + +@media (max-width: 480px) { + .action-buttons { + grid-template-columns: repeat(2, 1fr); + } + + .minigame-buttons { + grid-template-columns: repeat(2, 1fr); + } + + .pet { + width: 120px; + height: 120px; + } + + .pet-body { + width: 100px; + height: 80px; + } +} \ No newline at end of file diff --git a/games/piano_game/index.html b/games/piano_game/index.html new file mode 100644 index 00000000..e0530f53 --- /dev/null +++ b/games/piano_game/index.html @@ -0,0 +1,25 @@ + + + + + + Falling Tile Game + + + +
    +
    + +
    +

    Score: 0

    +

    Press **Start** to begin!

    +
    + + + +
    +
    + + + + \ No newline at end of file diff --git a/games/piano_game/script.js b/games/piano_game/script.js new file mode 100644 index 00000000..9682873b --- /dev/null +++ b/games/piano_game/script.js @@ -0,0 +1,156 @@ +const gameContainer = document.getElementById('game-container'); +const tileArea = document.getElementById('tile-area'); +const scoreDisplay = document.getElementById('score'); +const messageDisplay = document.getElementById('message'); +const startButton = document.getElementById('start-button'); +const targetLine = document.getElementById('target-line'); + +// Game constants +const TILE_WIDTH = 50; +const TILE_HEIGHT = 50; +// Must match --tile-fall-duration in CSS, but in milliseconds +const FALL_DURATION_MS = 2000; +const TARGET_LINE_BOTTOM = 50; // Must match bottom in CSS for target-line +const HIT_TOLERANCE_PX = 10; // How close to the line the tile must be clicked + +let score = 0; +let isGameRunning = false; +let tileCreationInterval; + +// --- Utility Function to Get Tile Position --- +/** + * Calculates the current bottom position of a falling tile. + * @param {HTMLElement} tile The tile element. + * @returns {number} The distance from the bottom of the game-container to the bottom of the tile. + */ +function getTileBottomPosition(tile) { + const gameHeight = gameContainer.clientHeight; + // Get the current vertical translation value from the CSS 'transform' + const style = window.getComputedStyle(tile); + const matrix = new DOMMatrixReadOnly(style.transform); + const currentY = matrix.m42; // Y-translation component + + // The animation is from 0 (top) to (gameHeight - TILE_HEIGHT) (bottom) + // Distance from the top of the container to the top of the tile is currentY + // Distance from the bottom of the container to the bottom of the tile is: + // (gameHeight) - (currentY + TILE_HEIGHT) + return gameHeight - (currentY + TILE_HEIGHT); +} + + +// --- Game Core Logic --- + +/** + * Creates and starts the animation for a new falling tile. + */ +function createFallingTile() { + if (!isGameRunning) return; + + const newTile = document.createElement('div'); + newTile.classList.add('tile'); + + // Random horizontal position + const maxLeft = tileArea.clientWidth - TILE_WIDTH; + const randomLeft = Math.floor(Math.random() * (maxLeft / TILE_WIDTH)) * TILE_WIDTH; + newTile.style.left = `${randomLeft}px`; + + // Add click listener + newTile.addEventListener('click', () => handleTileClick(newTile)); + + tileArea.appendChild(newTile); + + // Start the falling animation after a brief delay to ensure it's in the DOM + setTimeout(() => { + newTile.classList.add('falling'); + }, 50); + + // Set a timeout for when the tile should have reached the target line + const hitTime = FALL_DURATION_MS; + setTimeout(() => { + checkMissedTile(newTile); + }, hitTime); +} + +/** + * Handles the player clicking a tile. + * @param {HTMLElement} tile The tile element that was clicked. + */ +function handleTileClick(tile) { + if (!isGameRunning || !tile.classList.contains('falling')) return; + + const tileBottom = getTileBottomPosition(tile); + + // Check if the tile is within the hit tolerance of the target line + const targetCenter = TARGET_LINE_BOTTOM + (targetLine.clientHeight / 2); + const tileBottomCenter = tileBottom + (TILE_HEIGHT / 2); + + const distance = Math.abs(tileBottomCenter - targetCenter); + + if (distance <= HIT_TOLERANCE_PX) { + // HIT! + score++; + scoreDisplay.textContent = score; + tile.remove(); // Remove the tile on hit + // Optional: brief visual feedback + gameContainer.style.borderColor = '#00ffaa'; + setTimeout(() => gameContainer.style.borderColor = '#61dafb', 100); + + } else { + // MISS! Clicked too early or too late + tile.style.backgroundColor = 'red'; + tile.removeEventListener('click', handleTileClick); // Prevent double-loss + endGame('Missed the target window! Game Over.'); + } +} + +/** + * Checks if a tile was missed by reaching the end of its animation. + * @param {HTMLElement} tile The tile element to check. + */ +function checkMissedTile(tile) { + // If the tile is still in the DOM and the game is running, it was missed + if (isGameRunning && tileArea.contains(tile)) { + tile.style.backgroundColor = 'red'; + endGame('A tile was missed! Game Over.'); + } +} + +/** + * Starts the game loop. + */ +function startGame() { + score = 0; + scoreDisplay.textContent = score; + messageDisplay.textContent = 'Game On!'; + isGameRunning = true; + startButton.style.display = 'none'; + + // Clear any previous tiles + tileArea.innerHTML = ''; + + // Start generating tiles every 1.5 seconds + tileCreationInterval = setInterval(createFallingTile, 1500); +} + +/** + * Ends the game and displays the final score. + * @param {string} message The game over message to display. + */ +function endGame(message) { + isGameRunning = false; + clearInterval(tileCreationInterval); + messageDisplay.textContent = `${message} Final Score: ${score}`; + startButton.textContent = 'Play Again'; + startButton.style.display = 'block'; + + // Stop all falling tiles immediately + document.querySelectorAll('.tile.falling').forEach(tile => { + tile.style.animationPlayState = 'paused'; + tile.style.cursor = 'default'; + tile.removeEventListener('click', handleTileClick); + }); +} + + +// --- Event Listener --- +startButton.addEventListener('click', startGame); \ No newline at end of file diff --git a/games/piano_game/style.css b/games/piano_game/style.css new file mode 100644 index 00000000..11dbc945 --- /dev/null +++ b/games/piano_game/style.css @@ -0,0 +1,90 @@ +:root { + --game-height: 80vh; + --tile-fall-duration: 2s; /* Time it takes for a tile to fall */ +} + +body { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #282c34; + font-family: Arial, sans-serif; +} + +#game-container { + position: relative; + width: 300px; + height: var(--game-height); + border: 5px solid #61dafb; + background-color: #383c44; + overflow: hidden; /* Important: keeps tiles within the boundary */ +} + +#target-line { + position: absolute; + bottom: 50px; /* Position above the very bottom */ + left: 0; + width: 100%; + height: 10px; + background-color: #ff4500; /* Bright color for the target */ + z-index: 10; +} + +#tile-area { + position: relative; + height: 100%; +} + +/* * Tile Styling + */ +.tile { + position: absolute; + top: 0; /* Start position at the top */ + width: 50px; + height: 50px; + background-color: #00ffaa; + border: 2px solid #000; + cursor: pointer; + box-sizing: border-box; + /* Initial state: start animation only when the class is added */ + transition: background-color 0.1s; +} + +/* * CSS Keyframe Animation for Falling + */ +@keyframes fall { + from { + transform: translateY(0); /* Start at the top */ + } + to { + transform: translateY(calc(var(--game-height) - 50px)); /* End just above the bottom */ + } +} + +.falling { + animation: fall var(--tile-fall-duration) linear forwards; +} + +/* + * Status and Button Styling + */ +#status-display { + position: absolute; + top: 10px; + left: 10px; + color: white; + z-index: 20; +} + +#start-button { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 10px 20px; + font-size: 1.2em; + cursor: pointer; + z-index: 30; +} \ No newline at end of file diff --git a/games/pinball-wizard/index.html b/games/pinball-wizard/index.html new file mode 100644 index 00000000..d5814779 --- /dev/null +++ b/games/pinball-wizard/index.html @@ -0,0 +1,48 @@ + + + + + + Pinball Wizard + + + +
    +

    Pinball Wizard

    +

    Control the flippers and rack up points!

    + +
    +
    Score: 0
    +
    Balls: 3
    +
    Multiplier: 1x
    +
    + + + +
    +
    + + +
    + + +
    + +
    + +
    +

    How to Play:

    +
      +
    • Use A and D keys or click buttons to control flippers
    • +
    • Press Space or click Launch to start the ball
    • +
    • Hit bumpers and targets to score points
    • +
    • Keep the ball in play as long as possible
    • +
    • Complete objectives for bonus multipliers
    • +
    • You have 3 balls per game
    • +
    +
    +
    + + + + \ No newline at end of file diff --git a/games/pinball-wizard/script.js b/games/pinball-wizard/script.js new file mode 100644 index 00000000..89aacdfb --- /dev/null +++ b/games/pinball-wizard/script.js @@ -0,0 +1,422 @@ +// Pinball Wizard Game +// Classic pinball with flippers, bumpers, and scoring + +// DOM elements +const canvas = document.getElementById('pinball-canvas'); +const ctx = canvas.getContext('2d'); +const scoreEl = document.getElementById('current-score'); +const ballsEl = document.getElementById('current-balls'); +const multiplierEl = document.getElementById('current-multiplier'); +const messageEl = document.getElementById('message'); +const leftFlipperBtn = document.getElementById('left-flipper'); +const rightFlipperBtn = document.getElementById('right-flipper'); +const launchBtn = document.getElementById('launch-btn'); +const resetBtn = document.getElementById('reset-btn'); + +// Game constants +const CANVAS_WIDTH = 600; +const CANVAS_HEIGHT = 800; +const GRAVITY = 0.3; +const FRICTION = 0.99; +const BALL_RADIUS = 8; + +// Game variables +let gameRunning = false; +let score = 0; +let ballsLeft = 3; +let multiplier = 1; +let ball = null; +let flippers = []; +let bumpers = []; +let walls = []; +let animationId; + +// Ball class +class Ball { + constructor(x, y) { + this.x = x; + this.y = y; + this.vx = 0; + this.vy = 0; + this.radius = BALL_RADIUS; + this.active = true; + } + + update() { + if (!this.active) return; + + // Apply gravity + this.vy += GRAVITY; + + // Apply friction + this.vx *= FRICTION; + this.vy *= FRICTION; + + // Update position + this.x += this.vx; + this.y += this.vy; + + // Check wall collisions + this.checkWallCollisions(); + + // Check flipper collisions + this.checkFlipperCollisions(); + + // Check bumper collisions + this.checkBumperCollisions(); + + // Check if ball is lost + if (this.y > CANVAS_HEIGHT + 50) { + this.active = false; + ballLost(); + } + } + + checkWallCollisions() { + walls.forEach(wall => { + if (this.circleLineCollision(wall)) { + // Bounce off wall + const normal = this.getNormal(wall); + const dot = this.vx * normal.x + this.vy * normal.y; + this.vx -= 2 * dot * normal.x; + this.vy -= 2 * dot * normal.y; + + // Add some randomness to prevent sticking + this.vx += (Math.random() - 0.5) * 0.5; + this.vy += (Math.random() - 0.5) * 0.5; + } + }); + } + + checkFlipperCollisions() { + flippers.forEach(flipper => { + if (this.circleLineCollision(flipper.getLine())) { + // Bounce off flipper + const normal = this.getNormal(flipper.getLine()); + const dot = this.vx * normal.x + this.vy * normal.y; + this.vx -= 2 * dot * normal.x; + this.vy -= 2 * dot * normal.y; + + // Add flipper force + const force = flipper.isActive ? 8 : 4; + this.vx += normal.x * force; + this.vy += normal.y * force; + + score += 10 * multiplier; + updateScore(); + } + }); + } + + checkBumperCollisions() { + bumpers.forEach(bumper => { + const dx = this.x - bumper.x; + const dy = this.y - bumper.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < this.radius + bumper.radius) { + // Bounce off bumper + const angle = Math.atan2(dy, dx); + const force = 6; + this.vx += Math.cos(angle) * force; + this.vy += Math.sin(angle) * force; + + // Score points + score += bumper.points * multiplier; + updateScore(); + + // Visual feedback + bumper.hit = true; + setTimeout(() => bumper.hit = false, 200); + } + }); + } + + circleLineCollision(line) { + // Check if ball collides with line segment + const dx = line.x2 - line.x1; + const dy = line.y2 - line.y1; + const length = Math.sqrt(dx * dx + dy * dy); + + const dot = ((this.x - line.x1) * dx + (this.y - line.y1) * dy) / (length * length); + + let closestX, closestY; + if (dot < 0) { + closestX = line.x1; + closestY = line.y1; + } else if (dot > 1) { + closestX = line.x2; + closestY = line.y2; + } else { + closestX = line.x1 + dot * dx; + closestY = line.y1 + dot * dy; + } + + const distance = Math.sqrt((this.x - closestX) ** 2 + (this.y - closestY) ** 2); + return distance < this.radius; + } + + getNormal(line) { + const dx = line.x2 - line.x1; + const dy = line.y2 - line.y1; + const length = Math.sqrt(dx * dx + dy * dy); + return { x: -dy / length, y: dx / length }; + } + + draw() { + if (!this.active) return; + + ctx.fillStyle = '#ffffff'; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fill(); + + // Add shine effect + ctx.fillStyle = '#ffff88'; + ctx.beginPath(); + ctx.arc(this.x - 2, this.y - 2, this.radius / 3, 0, Math.PI * 2); + ctx.fill(); + } +} + +// Flipper class +class Flipper { + constructor(x, y, length, isLeft) { + this.x = x; + this.y = y; + this.length = length; + this.angle = isLeft ? -Math.PI / 6 : Math.PI / 6; + this.restAngle = this.angle; + this.flipAngle = isLeft ? Math.PI / 4 : -Math.PI / 4; + this.isLeft = isLeft; + this.isActive = false; + } + + activate() { + this.isActive = true; + this.angle = this.flipAngle; + } + + deactivate() { + this.isActive = false; + this.angle = this.restAngle; + } + + getLine() { + const endX = this.x + Math.cos(this.angle) * this.length; + const endY = this.y + Math.sin(this.angle) * this.length; + return { x1: this.x, y1: this.y, x2: endX, y2: endY }; + } + + draw() { + const line = this.getLine(); + ctx.strokeStyle = this.isActive ? '#ffff00' : '#ff6b35'; + ctx.lineWidth = 8; + ctx.lineCap = 'round'; + ctx.beginPath(); + ctx.moveTo(line.x1, line.y1); + ctx.lineTo(line.x2, line.y2); + ctx.stroke(); + } +} + +// Bumper class +class Bumper { + constructor(x, y, radius, points) { + this.x = x; + this.y = y; + this.radius = radius; + this.points = points; + this.hit = false; + } + + draw() { + ctx.fillStyle = this.hit ? '#ffff00' : '#e74c3c'; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fill(); + + // Draw inner circle + ctx.fillStyle = '#ffffff'; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius / 2, 0, Math.PI * 2); + ctx.fill(); + } +} + +// Initialize game objects +function initGame() { + // Create walls (table boundaries) + walls = [ + // Top + { x1: 0, y1: 0, x2: CANVAS_WIDTH, y2: 0 }, + // Left + { x1: 0, y1: 0, x2: 0, y2: CANVAS_HEIGHT }, + // Right + { x1: CANVAS_WIDTH, y1: 0, x2: CANVAS_WIDTH, y2: CANVAS_HEIGHT }, + // Bottom (only partial for ball exit) + { x1: 100, y1: CANVAS_HEIGHT, x2: CANVAS_WIDTH - 100, y2: CANVAS_HEIGHT }, + // Sloped walls + { x1: 50, y1: 100, x2: 150, y2: 200 }, + { x1: CANVAS_WIDTH - 50, y1: 100, x2: CANVAS_WIDTH - 150, y2: 200 }, + { x1: 200, y1: 300, x2: 300, y2: 400 }, + { x1: CANVAS_WIDTH - 200, y1: 300, x2: CANVAS_WIDTH - 300, y2: 400 } + ]; + + // Create flippers + flippers = [ + new Flipper(150, CANVAS_HEIGHT - 100, 80, true), // Left flipper + new Flipper(CANVAS_WIDTH - 150, CANVAS_HEIGHT - 100, 80, false) // Right flipper + ]; + + // Create bumpers + bumpers = [ + new Bumper(150, 250, 25, 50), + new Bumper(CANVAS_WIDTH - 150, 250, 25, 50), + new Bumper(CANVAS_WIDTH / 2, 350, 25, 100), + new Bumper(200, 450, 20, 75), + new Bumper(CANVAS_WIDTH - 200, 450, 20, 75) + ]; +} + +// Game functions +function startGame() { + if (ballsLeft <= 0) return; + + ball = new Ball(CANVAS_WIDTH / 2, CANVAS_HEIGHT - 150); + gameRunning = true; + messageEl.textContent = 'Ball launched! Use flippers to keep it in play.'; + launchBtn.style.display = 'none'; +} + +function ballLost() { + gameRunning = false; + ballsLeft--; + ballsEl.textContent = ballsLeft; + + if (ballsLeft > 0) { + messageEl.textContent = `Ball lost! ${ballsLeft} balls remaining.`; + setTimeout(() => { + launchBtn.style.display = 'inline-block'; + messageEl.textContent = 'Press Launch Ball to continue.'; + }, 2000); + } else { + messageEl.textContent = 'Game Over! No balls remaining.'; + launchBtn.style.display = 'none'; + } +} + +function updateScore() { + scoreEl.textContent = score.toLocaleString(); + + // Update multiplier based on score + multiplier = Math.floor(score / 1000) + 1; + multiplierEl.textContent = multiplier + 'x'; +} + +function resetGame() { + gameRunning = false; + score = 0; + ballsLeft = 3; + multiplier = 1; + ball = null; + + scoreEl.textContent = score; + ballsEl.textContent = ballsLeft; + multiplierEl.textContent = multiplier + 'x'; + messageEl.textContent = 'New game started! Press Launch Ball to begin.'; + launchBtn.style.display = 'inline-block'; +} + +// Main game loop +function gameLoop() { + // Clear canvas + ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + // Draw background + ctx.fillStyle = '#1a1a2e'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + // Draw walls + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 3; + walls.forEach(wall => { + ctx.beginPath(); + ctx.moveTo(wall.x1, wall.y1); + ctx.lineTo(wall.x2, wall.y2); + ctx.stroke(); + }); + + // Draw bumpers + bumpers.forEach(bumper => bumper.draw()); + + // Draw flippers + flippers.forEach(flipper => flipper.draw()); + + // Update and draw ball + if (ball) { + ball.update(); + ball.draw(); + } + + animationId = requestAnimationFrame(gameLoop); +} + +// Event listeners +document.addEventListener('keydown', (event) => { + if (event.code === 'KeyA') { + flippers[0].activate(); + leftFlipperBtn.style.background = '#ffff00'; + } + if (event.code === 'KeyD') { + flippers[1].activate(); + rightFlipperBtn.style.background = '#ffff00'; + } + if (event.code === 'Space' && !gameRunning && ballsLeft > 0) { + event.preventDefault(); + startGame(); + } +}); + +document.addEventListener('keyup', (event) => { + if (event.code === 'KeyA') { + flippers[0].deactivate(); + leftFlipperBtn.style.background = ''; + } + if (event.code === 'KeyD') { + flippers[1].deactivate(); + rightFlipperBtn.style.background = ''; + } +}); + +leftFlipperBtn.addEventListener('mousedown', () => { + flippers[0].activate(); + leftFlipperBtn.style.background = '#ffff00'; +}); + +leftFlipperBtn.addEventListener('mouseup', () => { + flippers[0].deactivate(); + leftFlipperBtn.style.background = ''; +}); + +rightFlipperBtn.addEventListener('mousedown', () => { + flippers[1].activate(); + rightFlipperBtn.style.background = '#ffff00'; +}); + +rightFlipperBtn.addEventListener('mouseup', () => { + flippers[1].deactivate(); + rightFlipperBtn.style.background = ''; +}); + +launchBtn.addEventListener('click', startGame); +resetBtn.addEventListener('click', resetGame); + +// Initialize and start +initGame(); +gameLoop(); +updateScore(); + +// This pinball game has realistic physics +// The flippers work well for controlling the ball +// Could add more features like ramps or special targets \ No newline at end of file diff --git a/games/pinball-wizard/style.css b/games/pinball-wizard/style.css new file mode 100644 index 00000000..d9de7e7f --- /dev/null +++ b/games/pinball-wizard/style.css @@ -0,0 +1,176 @@ +/* Pinball Wizard Game Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #1a1a2e, #16213e); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + color: white; +} + +.container { + text-align: center; + max-width: 700px; + padding: 20px; +} + +h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.5); + color: #ffd700; +} + +p { + font-size: 1.1em; + margin-bottom: 20px; + opacity: 0.9; +} + +.game-stats { + display: flex; + justify-content: space-around; + flex-wrap: wrap; + margin: 20px 0; + font-size: 1.1em; + font-weight: bold; + background: rgba(255, 215, 0, 0.1); + padding: 15px; + border-radius: 10px; + border: 2px solid #ffd700; +} + +#pinball-canvas { + border: 4px solid #ffd700; + border-radius: 15px; + background: linear-gradient(180deg, #2c3e50, #34495e); + display: block; + margin: 20px auto; + box-shadow: 0 8px 16px rgba(0,0,0,0.4); +} + +.controls { + margin: 20px 0; +} + +.flipper-controls { + display: flex; + justify-content: center; + gap: 20px; + margin-bottom: 15px; +} + +button { + background: #e74c3c; + color: white; + border: none; + padding: 12px 24px; + font-size: 1em; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s; + font-weight: bold; +} + +button:hover { + background: #c0392b; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.3); +} + +button:active { + transform: translateY(0); +} + +#left-flipper, #right-flipper { + background: #3498db; + min-width: 120px; +} + +#left-flipper:hover, #right-flipper:hover { + background: #2980b9; +} + +#launch-btn { + background: #27ae60; + width: 100%; + max-width: 200px; +} + +#launch-btn:hover { + background: #229954; +} + +#reset-btn { + background: #f39c12; +} + +#reset-btn:hover { + background: #e67e22; +} + +#message { + font-size: 1.2em; + margin: 20px 0; + min-height: 30px; + font-weight: bold; + color: #ffd700; +} + +.instructions { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + margin-top: 20px; + text-align: left; + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.instructions h3 { + margin-bottom: 10px; + color: #ffd700; +} + +.instructions ul { + list-style-type: disc; + padding-left: 20px; +} + +.instructions li { + margin: 5px 0; + line-height: 1.4; +} + +/* Responsive design */ +@media (max-width: 768px) { + #pinball-canvas { + width: 100%; + max-width: 400px; + height: 600px; + } + + .flipper-controls { + flex-direction: column; + gap: 10px; + } + + button { + width: 100%; + max-width: 250px; + } + + .game-stats { + font-size: 1em; + padding: 10px; + } +} \ No newline at end of file diff --git a/games/pipe-mania/index.html b/games/pipe-mania/index.html new file mode 100644 index 00000000..671d7443 --- /dev/null +++ b/games/pipe-mania/index.html @@ -0,0 +1,25 @@ + + + + + + Flow Connect + + + +
    +

    Flow Connect

    +
    + Time: 60s + Arrange the pipes! +
    + +
    +
    + + +
    + + + + \ No newline at end of file diff --git a/games/pipe-mania/script.js b/games/pipe-mania/script.js new file mode 100644 index 00000000..7f74ea82 --- /dev/null +++ b/games/pipe-mania/script.js @@ -0,0 +1,376 @@ +// --- Game Constants and Setup --- +const GRID_SIZE = 6; +const TILE_SIZE = 60; // Must match CSS +const TIMER_SECONDS = 60; +const PIPE_TYPES = { + STRAIGHT: 'straight', + CORNER: 'corner', + T_JUNCTION: 'tjunction' +}; + +const gridElement = document.getElementById('pipe-grid'); +const timerElement = document.getElementById('timer'); +const messageElement = document.getElementById('message'); +const restartButton = document.getElementById('restart-button'); + +let grid = []; // The 2D array holding tile data +let timerInterval; +let timeLeft; +let gameActive = false; + +// --- Pipe Connection Data --- +// Defines which sides (N, E, S, W) are open for a pipe piece at 0 degrees rotation (0 * 90deg) +// The pipe class will handle rotating these connections. +const PIPE_CONNECTIONS = { + // [N, E, S, W] where 1 is open, 0 is closed + [PIPE_TYPES.STRAIGHT]: [1, 0, 1, 0], // Vertical straight pipe + [PIPE_TYPES.CORNER]: [1, 1, 0, 0], // Top-right corner + [PIPE_TYPES.T_JUNCTION]: [1, 1, 1, 0] // T-junction open N, E, S +}; + +// --- Tile Class (Data Model) --- +class Tile { + constructor(row, col, type, rotation) { + this.row = row; + this.col = col; + this.type = type; + this.rotation = rotation; // 0, 90, 180, 270 (degrees) + this.isStart = false; + this.isEnd = false; + this.element = null; // Reference to the DOM element + } + + /** + * Gets the current connection points [N, E, S, W] based on type and rotation. + * @returns {Array} An array of 0s and 1s representing open connections. + */ + getConnections() { + if (this.isStart || this.isEnd) { + // Start/End connections are fixed and set at generation + return this.type; // type holds the [N, E, S, W] for start/end + } + + const baseConnections = PIPE_CONNECTIONS[this.type]; + const numRotations = this.rotation / 90; + + // Rotate the array based on the rotation value + // Example: [1, 0, 1, 0] rotated 90deg becomes [0, 1, 0, 1] + let connections = [...baseConnections]; + for (let i = 0; i < numRotations; i++) { + connections.unshift(connections.pop()); + } + return connections; + } + + rotate() { + this.rotation = (this.rotation + 90) % 360; + this.render(); + } + + render() { + const pipeElement = this.element.querySelector('.pipe'); + if (pipeElement) { + pipeElement.style.setProperty('--rotation', `${this.rotation}deg`); + } + } +} + +// --- Game Initialization --- + +function initGame() { + // Clear previous state + clearInterval(timerInterval); + gridElement.innerHTML = ''; + grid = []; + gameActive = true; + timeLeft = TIMER_SECONDS; + restartButton.classList.add('hidden'); + messageElement.textContent = 'Rotate pipes to connect the flow!'; + + // Set up the grid structure in CSS via JS (optional, but good practice) + gridElement.style.gridTemplateColumns = `repeat(${GRID_SIZE}, ${TILE_SIZE}px)`; + gridElement.style.gridTemplateRows = `repeat(${GRID_SIZE}, ${TILE_SIZE}px)`; + + createGrid(); + startGameTimer(); +} + +/** + * Creates the randomized grid with Start and End points. + */ +function createGrid() { + // Randomly select start and end points on the border + const borderTiles = []; + for(let i = 0; i < GRID_SIZE; i++) { + // Top/Bottom rows + borderTiles.push({r: 0, c: i}); + borderTiles.push({r: GRID_SIZE - 1, c: i}); + // Left/Right columns (excluding corners already added) + if (i > 0 && i < GRID_SIZE - 1) { + borderTiles.push({r: i, c: 0}); + borderTiles.push({r: i, c: GRID_SIZE - 1}); + } + } + + // Select two random, distinct border tiles for start/end + const startIndex = Math.floor(Math.random() * borderTiles.length); + let endIndex; + do { + endIndex = Math.floor(Math.random() * borderTiles.length); + } while (endIndex === startIndex); + + const startPos = borderTiles[startIndex]; + const endPos = borderTiles[endIndex]; + + for (let r = 0; r < GRID_SIZE; r++) { + grid[r] = []; + for (let c = 0; c < GRID_SIZE; c++) { + let tile; + let typeKey = Object.keys(PIPE_TYPES); + let randomType = PIPE_TYPES[typeKey[Math.floor(Math.random() * typeKey.length)]]; + let randomRotation = Math.floor(Math.random() * 4) * 90; + + if (r === startPos.r && c === startPos.c) { + // Start tile setup + tile = new Tile(r, c, getFixedConnection(r, c), 0); + tile.isStart = true; + } else if (r === endPos.r && c === endPos.c) { + // End tile setup + tile = new Tile(r, c, getFixedConnection(r, c), 0); + tile.isEnd = true; + } else { + // Regular pipe tile + tile = new Tile(r, c, randomType, randomRotation); + } + + grid[r][c] = tile; + createTileElement(tile); + } + } +} + +/** + * Determines the fixed connection for border Start/End tiles. + * @param {number} r - Row index. + * @param {number} c - Column index. + * @returns {Array} The fixed connection array [N, E, S, W]. + */ +function getFixedConnection(r, c) { + if (r === 0) return [0, 0, 1, 0]; // Connects South + if (r === GRID_SIZE - 1) return [1, 0, 0, 0]; // Connects North + if (c === 0) return [0, 1, 0, 0]; // Connects East + if (c === GRID_SIZE - 1) return [0, 0, 0, 1]; // Connects West + // Should not happen for border tiles + return [0, 0, 0, 0]; +} + +/** + * Creates the DOM element for a tile and attaches the click handler. + * @param {Tile} tile - The Tile data object. + */ +function createTileElement(tile) { + const tileDiv = document.createElement('div'); + tileDiv.classList.add('tile'); + + // Add pipe element inside the tile to handle rotation + const pipeDiv = document.createElement('div'); + pipeDiv.classList.add('pipe'); + + if (tile.isStart) { + tileDiv.classList.add('start-tile'); + } else if (tile.isEnd) { + tileDiv.classList.add('end-tile'); + } else { + pipeDiv.classList.add(`pipe-${tile.type}`); + } + + tileDiv.appendChild(pipeDiv); + tile.element = tileDiv; // Store reference + + // Initial render for the rotation + tile.render(); + + // Attach event listener for rotation + if (!tile.isStart && !tile.isEnd) { + tileDiv.addEventListener('click', () => { + if (gameActive) { + tile.rotate(); + checkWinCondition(); + } + }); + } + + gridElement.appendChild(tileDiv); +} + +// --- Timer Logic --- + +function startGameTimer() { + timerElement.textContent = `Time: ${timeLeft}s`; + timerInterval = setInterval(() => { + timeLeft--; + timerElement.textContent = `Time: ${timeLeft}s`; + + if (timeLeft <= 0) { + clearInterval(timerInterval); + gameOver(false); + } + }, 1000); +} + +// --- Path Validation Logic (The Core Challenge) --- + +/** + * The main function to check if a continuous path exists from start to end. + */ +function checkWinCondition() { + let startTile; + let endTile; + let visitedTiles = new Set(); + + // 1. Find Start and End + for (let r = 0; r < GRID_SIZE; r++) { + for (let c = 0; c < GRID_SIZE; c++) { + if (grid[r][c].isStart) startTile = grid[r][c]; + if (grid[r][c].isEnd) endTile = grid[r][c]; + } + } + + if (!startTile || !endTile) return; // Should never happen + + // 2. Identify initial movement direction from the Start tile + let startConnections = startTile.getConnections(); + let initialDirection; // 0=N, 1=E, 2=S, 3=W + + // Find the single open side of the Start tile + for(let i = 0; i < 4; i++) { + if(startConnections[i] === 1) { + initialDirection = i; + break; + } + } + + // 3. Start the recursive search + const startRow = startTile.row; + const startCol = startTile.col; + + // Determine the coordinates of the first tile to visit + let nextRow = startRow, nextCol = startCol; + if (initialDirection === 0) nextRow--; // North + else if (initialDirection === 1) nextCol++; // East + else if (initialDirection === 2) nextRow++; // South + else if (initialDirection === 3) nextCol--; // West + + // Direction the *pipe leaves* the current tile (initialDirection) + // The opposite is the direction the *pipe enters* the next tile (entryDirection) + const entryDirection = (initialDirection + 2) % 4; + + // Clear connection highlights + grid.flat().forEach(tile => tile.element.classList.remove('connected')); + + // Perform the depth-first search (DFS) + const pathFound = recursiveValidate(nextRow, nextCol, entryDirection, endTile, visitedTiles); + + if (pathFound) { + gameOver(true); + } +} + +/** + * Recursive Depth-First Search (DFS) to find the path. + * @param {number} r - Current tile row. + * @param {number} c - Current tile column. + * @param {number} entryDirection - The side the flow *enters* this tile (0=N, 1=E, 2=S, 3=W). + * @param {Tile} endTile - The target end tile. + * @param {Set} visited - Set of previously visited tile coordinates (as "r,c"). + * @returns {boolean} True if the path reaches the end tile. + */ +function recursiveValidate(r, c, entryDirection, endTile, visited) { + const key = `${r},${c}`; + + // 1. Out of Bounds or Already Visited + if (r < 0 || r >= GRID_SIZE || c < 0 || c >= GRID_SIZE || visited.has(key)) { + return false; + } + + const currentTile = grid[r][c]; + visited.add(key); + + // Highlight connection (temporarily, or permanently on win) + currentTile.element.classList.add('connected'); + + // 2. Check for Connection Match + const currentConnections = currentTile.getConnections(); + + // The tile MUST have an open connection on the side the flow is ENTERING + if (currentConnections[entryDirection] === 0) { + currentTile.element.classList.remove('connected'); // Backtrack highlight + return false; + } + + // 3. Check for Win (End Tile) + if (currentTile === endTile) { + // Must check if the tile's exit matches the END tile's entry point + const endConnections = endTile.getConnections(); + if (endConnections[entryDirection] === 1) { + return true; // WIN! + } + currentTile.element.classList.remove('connected'); // Backtrack highlight + return false; + } + + // 4. Explore Next Tile + // Find the exit point(s) from the current tile + for (let exitDirection = 0; exitDirection < 4; exitDirection++) { + // Exit direction cannot be the same as the entry direction (0 vs 2, 1 vs 3) + if (exitDirection === (entryDirection + 2) % 4) continue; + + // If this side is open for exit + if (currentConnections[exitDirection] === 1) { + let nextR = r, nextC = c; + + // Calculate coordinates of the neighbor tile + if (exitDirection === 0) nextR--; // North + else if (exitDirection === 1) nextC++; // East + else if (exitDirection === 2) nextR++; // South + else if (exitDirection === 3) nextC--; // West + + // The direction the flow *exits* the current tile is the opposite of the direction + // the flow *enters* the next tile. + const nextEntryDirection = (exitDirection + 2) % 4; + + // Recurse to the next tile + if (recursiveValidate(nextR, nextC, nextEntryDirection, endTile, visited)) { + return true; // Path found down this branch + } + } + } + + // 5. Backtrack + // If we get here, the path failed at this tile. Remove highlight and backtrack. + currentTile.element.classList.remove('connected'); + visited.delete(key); + return false; +} + +// --- Game End --- + +function gameOver(won) { + gameActive = false; + clearInterval(timerInterval); + + if (won) { + messageElement.textContent = 'SUCCESS! Flow Connected!'; + grid.flat().forEach(tile => tile.element.classList.add('connected')); + } else { + messageElement.textContent = 'TIME UP! Try again.'; + } + + restartButton.classList.remove('hidden'); +} + +// --- Event Listener --- +restartButton.addEventListener('click', initGame); + +// --- Start Game --- +initGame(); \ No newline at end of file diff --git a/games/pipe-mania/stle.css b/games/pipe-mania/stle.css new file mode 100644 index 00000000..0923b97e --- /dev/null +++ b/games/pipe-mania/stle.css @@ -0,0 +1,120 @@ +/* General styling and centering */ +body { + background-color: #1e1e2e; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + font-family: sans-serif; + color: #cdd6f4; +} + +#game-container { + text-align: center; + background-color: #313244; + padding: 25px; + border-radius: 12px; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.5); +} + +h1 { + color: #89b4fa; + margin-bottom: 10px; +} + +#status-bar { + display: flex; + justify-content: space-between; + margin-bottom: 15px; + font-size: 1.2em; + padding: 0 10px; +} + +/* ----------------------------------- */ +/* CSS Grid and Tile Styling */ +/* ----------------------------------- */ + +#pipe-grid { + display: grid; + /* This will be configured by JavaScript to be N x N */ + grid-template-columns: repeat(6, 60px); + grid-template-rows: repeat(6, 60px); + gap: 2px; + border: 3px solid #6c7086; + background-color: #45475a; + margin: 0 auto; +} + +.tile { + width: 60px; + height: 60px; + box-sizing: border-box; + cursor: pointer; + background-color: #6c7086; /* Background color for the tile */ + transition: transform 0.2s ease-out; /* Smooth rotation */ + border: 1px solid #45475a; + position: relative; /* Needed for absolute pipe */ +} + +/* Pipe segment styling */ +.pipe { + width: 100%; + height: 100%; + /* The actual pipe visual is a child element that rotates */ + position: absolute; + top: 0; + left: 0; + /* Initial rotation (0 degrees) */ + transform: rotate(var(--rotation, 0deg)); +} + +/* Styling for the CONNECTED path */ +.connected .pipe { + /* Brighter color when the pipe is part of the solution path */ + filter: drop-shadow(0 0 5px #a6e3a1); +} + +/* ----------------------------------- */ +/* Pipe Shape Visuals using background gradients */ +/* ----------------------------------- */ +.pipe-straight { + background: linear-gradient(to right, + #6c7086 0%, #6c7086 40%, + #a6e3a1 40%, #a6e3a1 60%, + #6c7086 60%, #6c7086 100%); +} + +.pipe-corner { + /* Uses a radial gradient and colors for the corner effect */ + background: radial-gradient(circle 30px at 0% 0%, + #a6e3a1 0%, #a6e3a1 60%, + #6c7086 60%, #6c7086 100%); +} + +.pipe-tjunction { + /* Combination of straight and corner ideas */ + background: + /* Vertical line */ + linear-gradient(to right, + transparent 40%, #a6e3a1 40%, #a6e3a1 60%, transparent 60%) top center / 100% 100%, + /* Horizontal part (right side) */ + linear-gradient(to right, + #a6e3a1 50%, transparent 50%) center right / 50% 100%; + background-repeat: no-repeat; +} + +/* Special tiles: Start and End */ +.start-tile .pipe { + background-color: #f38ba8; /* Pink for start */ + border-radius: 50%; /* Circle source */ +} + +.end-tile .pipe { + background-color: #fab387; /* Orange for end */ + border-radius: 50%; /* Circle sink */ +} + +.hidden { + display: none; +} \ No newline at end of file diff --git a/games/pipe-twister/index.html b/games/pipe-twister/index.html new file mode 100644 index 00000000..3e767764 --- /dev/null +++ b/games/pipe-twister/index.html @@ -0,0 +1,27 @@ + + + + + + Pipe Twister | Mini JS Games Hub + + + +
    +

    Pipe Twister

    +
    + + + +
    + +

    +
    + + + + + + + + diff --git a/games/pipe-twister/script.js b/games/pipe-twister/script.js new file mode 100644 index 00000000..4887f4bb --- /dev/null +++ b/games/pipe-twister/script.js @@ -0,0 +1,125 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); +const status = document.getElementById("status"); +const rotateSound = document.getElementById("rotate-sound"); +const successSound = document.getElementById("success-sound"); +const failSound = document.getElementById("fail-sound"); + +const restartBtn = document.getElementById("restart"); +const pauseBtn = document.getElementById("pause"); +const resumeBtn = document.getElementById("resume"); + +let pipes = []; +let running = true; + +// Pipe Class +class Pipe { + constructor(x, y, type = 'line', rotation = 0) { + this.x = x; + this.y = y; + this.type = type; // 'line', 'elbow', 't', 'cross', 'blocked' + this.rotation = rotation; + this.size = 60; + } + + draw(ctx) { + ctx.save(); + ctx.translate(this.x, this.y); + ctx.rotate(this.rotation * Math.PI / 2); + ctx.lineWidth = 10; + ctx.strokeStyle = "#0ff"; + ctx.shadowColor = "#0ff"; + ctx.shadowBlur = 20; + ctx.beginPath(); + if (this.type === 'line') { + ctx.moveTo(-this.size/2, 0); + ctx.lineTo(this.size/2, 0); + } else if (this.type === 'elbow') { + ctx.moveTo(0,0); + ctx.lineTo(this.size/2,0); + ctx.lineTo(this.size/2,this.size/2); + } else if (this.type === 't') { + ctx.moveTo(-this.size/2,0); + ctx.lineTo(this.size/2,0); + ctx.moveTo(0,0); + ctx.lineTo(0,this.size/2); + } else if (this.type === 'cross') { + ctx.moveTo(-this.size/2,0); + ctx.lineTo(this.size/2,0); + ctx.moveTo(0,-this.size/2); + ctx.lineTo(0,this.size/2); + } + ctx.stroke(); + ctx.restore(); + } + + contains(mx, my) { + return mx > this.x - this.size/2 && mx < this.x + this.size/2 && + my > this.y - this.size/2 && my < this.y + this.size/2; + } + + rotate() { + this.rotation = (this.rotation + 1) % 4; + rotateSound.play(); + } +} + +// Generate Pipes +function initPipes() { + pipes = []; + for(let i=0;i<5;i++) { + for(let j=0;j<5;j++){ + let type = ['line','elbow','t'][Math.floor(Math.random()*3)]; + let rotation = Math.floor(Math.random()*4); + pipes.push(new Pipe(60 + j*100, 60 + i*100, type, rotation)); + } + } +} + +function draw() { + ctx.clearRect(0,0,canvas.width,canvas.height); + pipes.forEach(p => p.draw(ctx)); +} + +canvas.addEventListener('click', e => { + if(!running) return; + const rect = canvas.getBoundingClientRect(); + const mx = e.clientX - rect.left; + const my = e.clientY - rect.top; + pipes.forEach(p => { + if(p.contains(mx,my)) { + p.rotate(); + } + }); + draw(); + checkWin(); +}); + +function checkWin() { + // Simple placeholder: if all rotations are 0 => win + if(pipes.every(p => p.rotation===0)) { + status.textContent = "๐ŸŽ‰ You Completed the Puzzle!"; + successSound.play(); + running = false; + } +} + +restartBtn.addEventListener('click', () => { + initPipes(); + running = true; + status.textContent = ""; + draw(); +}); + +pauseBtn.addEventListener('click', () => { + running = false; + resumeBtn.disabled = false; +}); + +resumeBtn.addEventListener('click', () => { + running = true; + resumeBtn.disabled = true; +}); + +initPipes(); +draw(); diff --git a/games/pipe-twister/style.css b/games/pipe-twister/style.css new file mode 100644 index 00000000..aaaa7d14 --- /dev/null +++ b/games/pipe-twister/style.css @@ -0,0 +1,38 @@ +body { + font-family: 'Arial', sans-serif; + background: #111; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + text-align: center; +} + +canvas { + background: #222; + border-radius: 20px; + box-shadow: 0 0 30px #0ff, 0 0 60px #0ff inset; + margin-top: 20px; +} + +.controls button { + margin: 10px; + padding: 10px 15px; + font-size: 16px; + border: none; + border-radius: 8px; + cursor: pointer; + background: #0ff; + color: #000; + font-weight: bold; + box-shadow: 0 0 10px #0ff; + transition: 0.2s; +} + +.controls button:hover { + box-shadow: 0 0 20px #0ff, 0 0 40px #0ff inset; +} diff --git a/games/pixel-art-creator/index.html b/games/pixel-art-creator/index.html new file mode 100644 index 00000000..8def476a --- /dev/null +++ b/games/pixel-art-creator/index.html @@ -0,0 +1,43 @@ + + + + + + Pixel Art Creator + + + +
    +

    Pixel Art Creator

    +
    + +
    +
    +
    +

    Colors

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + +
    +
    +
    + + + \ No newline at end of file diff --git a/games/pixel-art-creator/script.js b/games/pixel-art-creator/script.js new file mode 100644 index 00000000..9260d101 --- /dev/null +++ b/games/pixel-art-creator/script.js @@ -0,0 +1,102 @@ +// Pixel Art Creator - A simple drawing tool +// Made with love for the Mini JS Games Hub + +const canvas = document.getElementById('canvas'); +const ctx = canvas.getContext('2d'); +const gridSize = 16; // 16x16 grid +const pixelSize = canvas.width / gridSize; // Size of each pixel + +let currentColor = '#000000'; // Start with black + +// Fill canvas with white initially +ctx.fillStyle = '#FFFFFF'; +ctx.fillRect(0, 0, canvas.width, canvas.height); + +// Function to draw the grid lines +function drawGrid() { + ctx.strokeStyle = '#ddd'; + ctx.lineWidth = 1; + for (let i = 0; i <= gridSize; i++) { + // Draw vertical lines + ctx.beginPath(); + ctx.moveTo(i * pixelSize, 0); + ctx.lineTo(i * pixelSize, canvas.height); + ctx.stroke(); + // Draw horizontal lines + ctx.beginPath(); + ctx.moveTo(0, i * pixelSize); + ctx.lineTo(canvas.width, i * pixelSize); + ctx.stroke(); + } +} + +// Draw the initial grid +drawGrid(); + +// Set up color palette +const colors = document.querySelectorAll('.color'); +colors.forEach(color => { + color.style.backgroundColor = color.dataset.color; + color.addEventListener('click', () => { + currentColor = color.dataset.color; + // Remove selected class from all + colors.forEach(c => c.classList.remove('selected')); + // Add to clicked one + color.classList.add('selected'); + }); +}); + +// Select the first color by default +colors[0].classList.add('selected'); + +// Handle canvas clicks to paint pixels +canvas.addEventListener('click', (e) => { + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + // Calculate which grid cell was clicked + const gridX = Math.floor(x / pixelSize); + const gridY = Math.floor(y / pixelSize); + + // Paint the pixel + ctx.fillStyle = currentColor; + ctx.fillRect(gridX * pixelSize, gridY * pixelSize, pixelSize, pixelSize); + // Redraw grid to keep lines visible + drawGrid(); +}); + +// Clear button +document.getElementById('clear-btn').addEventListener('click', () => { + ctx.fillStyle = '#FFFFFF'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + drawGrid(); +}); + +// Save button - store in localStorage +document.getElementById('save-btn').addEventListener('click', () => { + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = Array.from(imageData.data); + localStorage.setItem('pixelArt', JSON.stringify(data)); + alert('Your art has been saved!'); +}); + +// Load button - retrieve from localStorage +document.getElementById('load-btn').addEventListener('click', () => { + const data = localStorage.getItem('pixelArt'); + if (data) { + const imageData = new ImageData(new Uint8ClampedArray(JSON.parse(data)), canvas.width, canvas.height); + ctx.putImageData(imageData, 0, 0); + drawGrid(); + alert('Art loaded successfully!'); + } else { + alert('No saved art found. Draw something first!'); + } +}); + +// Export button - download as PNG +document.getElementById('export-btn').addEventListener('click', () => { + const link = document.createElement('a'); + link.download = 'my-pixel-art.png'; + link.href = canvas.toDataURL('image/png'); + link.click(); +}); \ No newline at end of file diff --git a/games/pixel-art-creator/style.css b/games/pixel-art-creator/style.css new file mode 100644 index 00000000..5dc80d42 --- /dev/null +++ b/games/pixel-art-creator/style.css @@ -0,0 +1,91 @@ +body { + font-family: Arial, sans-serif; + background-color: #f0f0f0; + margin: 0; + padding: 20px; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; +} + +.container { + background-color: white; + padding: 20px; + border-radius: 10px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + max-width: 800px; + width: 100%; +} + +h1 { + text-align: center; + color: #333; +} + +.canvas-container { + display: flex; + justify-content: center; + margin-bottom: 20px; +} + +#canvas { + border: 2px solid #333; + cursor: crosshair; +} + +.tools { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.color-palette { + flex: 1; +} + +.colors { + display: flex; + flex-wrap: wrap; + gap: 5px; +} + +.color { + width: 30px; + height: 30px; + border: 2px solid #333; + cursor: pointer; + border-radius: 5px; +} + +.color.selected { + border-color: #ff0000; +} + +.controls { + display: flex; + flex-direction: column; + gap: 10px; +} + +button { + padding: 10px 15px; + background-color: #4CAF50; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; +} + +button:hover { + background-color: #45a049; +} + +#clear-btn { + background-color: #f44336; +} + +#clear-btn:hover { + background-color: #da190b; +} \ No newline at end of file diff --git a/games/pixel-harmony/index.html b/games/pixel-harmony/index.html new file mode 100644 index 00000000..ef3172e2 --- /dev/null +++ b/games/pixel-harmony/index.html @@ -0,0 +1,82 @@ + + + + + + Pixel Harmony + + + +
    +
    +

    Pixel Harmony

    +

    Match the colors. Feel the vibe.

    +
    + +
    +
    +
    +
    #FF6B6B
    +
    + +
    +
    +
    #FFFFFF
    +
    +
    + +
    +
    +
    + + + 255 +
    +
    + + + 255 +
    +
    + + + 255 +
    +
    + +
    + + +
    +
    + +
    +
    +
    0
    +
    Score
    +
    +
    +
    0
    +
    Streak
    +
    +
    +
    0%
    +
    Accuracy
    +
    +
    + + +
    + +
    + + + + \ No newline at end of file diff --git a/games/pixel-harmony/script.js b/games/pixel-harmony/script.js new file mode 100644 index 00000000..6b0c54a7 --- /dev/null +++ b/games/pixel-harmony/script.js @@ -0,0 +1,240 @@ +class PixelHarmony { + constructor() { + this.score = 0; + this.streak = 0; + this.totalAttempts = 0; + this.successfulAttempts = 0; + this.targetColor = this.generateRandomColor(); + + this.initializeGame(); + this.setupEventListeners(); + } + + initializeGame() { + this.updateTargetColor(); + this.resetPlayerColor(); + this.updateDisplay(); + } + + generateRandomColor() { + return { + r: Math.floor(Math.random() * 256), + g: Math.floor(Math.random() * 256), + b: Math.floor(Math.random() * 256) + }; + } + + updateTargetColor() { + this.targetColor = this.generateRandomColor(); + const targetColorElement = document.getElementById('targetColor'); + const targetCodeElement = document.getElementById('targetCode'); + + const colorString = this.rgbToHex(this.targetColor); + targetColorElement.style.background = colorString; + targetCodeElement.textContent = colorString; + } + + resetPlayerColor() { + document.getElementById('redSlider').value = 255; + document.getElementById('greenSlider').value = 255; + document.getElementById('blueSlider').value = 255; + this.updatePlayerColor(); + } + + updatePlayerColor() { + const r = parseInt(document.getElementById('redSlider').value); + const g = parseInt(document.getElementById('greenSlider').value); + const b = parseInt(document.getElementById('blueSlider').value); + + const playerColor = { r, g, b }; + const colorString = this.rgbToHex(playerColor); + + document.getElementById('playerColor').style.background = colorString; + document.getElementById('playerCode').textContent = colorString; + + document.getElementById('redValue').textContent = r; + document.getElementById('greenValue').textContent = g; + document.getElementById('blueValue').textContent = b; + + return playerColor; + } + + rgbToHex(color) { + return `#${this.componentToHex(color.r)}${this.componentToHex(color.g)}${this.componentToHex(color.b)}`; + } + + componentToHex(c) { + const hex = c.toString(16); + return hex.length === 1 ? '0' + hex : hex; + } + + calculateColorDifference(color1, color2) { + const dr = color1.r - color2.r; + const dg = color1.g - color2.g; + const db = color1.b - color2.b; + + return Math.sqrt(dr * dr + dg * dg + db * db); + } + + checkMatch() { + const playerColor = this.updatePlayerColor(); + const difference = this.calculateColorDifference(playerColor, this.targetColor); + const maxDifference = Math.sqrt(3 * 255 * 255); + const similarity = 100 - (difference / maxDifference) * 100; + + this.totalAttempts++; + + let feedbackText = ''; + let points = 0; + + if (similarity >= 95) { + feedbackText = 'Perfect Match! ๐ŸŽฏ'; + points = 100; + this.streak++; + this.successfulAttempts++; + this.celebrate(); + } else if (similarity >= 85) { + feedbackText = 'Great Job! โœจ'; + points = 70; + this.streak++; + this.successfulAttempts++; + } else if (similarity >= 70) { + feedbackText = 'Good! ๐Ÿ‘'; + points = 40; + this.streak = 0; + this.successfulAttempts++; + } else if (similarity >= 50) { + feedbackText = 'Getting Closer! ๐Ÿ’ช'; + points = 20; + this.streak = 0; + } else { + feedbackText = 'Keep Trying! ๐Ÿ”„'; + points = 0; + this.streak = 0; + } + + this.score += points; + + document.getElementById('feedbackText').textContent = feedbackText; + document.getElementById('differenceFill').style.width = `${similarity}%`; + + document.getElementById('playerColor').classList.add('pulse'); + setTimeout(() => { + document.getElementById('playerColor').classList.remove('pulse'); + }, 500); + + this.updateDisplay(); + + if (similarity >= 95) { + setTimeout(() => { + this.nextColor(); + }, 1500); + } + } + + nextColor() { + this.updateTargetColor(); + this.resetPlayerColor(); + document.getElementById('differenceFill').style.width = '100%'; + document.getElementById('feedbackText').textContent = 'Match the new color!'; + } + + celebrate() { + const confetti = document.getElementById('confetti'); + confetti.style.opacity = '1'; + + for (let i = 0; i < 50; i++) { + const particle = document.createElement('div'); + particle.style.position = 'absolute'; + particle.style.width = '10px'; + particle.style.height = '10px'; + particle.style.background = this.getRandomColor(); + particle.style.borderRadius = '50%'; + particle.style.left = Math.random() * 100 + 'vw'; + particle.style.animation = `fall ${Math.random() * 2 + 1}s linear forwards`; + confetti.appendChild(particle); + + setTimeout(() => { + particle.remove(); + }, 3000); + } + + setTimeout(() => { + confetti.style.opacity = '0'; + confetti.innerHTML = ''; + }, 2000); + + document.getElementById('targetColor').classList.add('celebrate'); + setTimeout(() => { + document.getElementById('targetColor').classList.remove('celebrate'); + }, 600); + } + + getRandomColor() { + const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD']; + return colors[Math.floor(Math.random() * colors.length)]; + } + + updateDisplay() { + document.getElementById('score').textContent = this.score; + document.getElementById('streak').textContent = this.streak; + + const accuracy = this.totalAttempts > 0 + ? Math.round((this.successfulAttempts / this.totalAttempts) * 100) + : 0; + document.getElementById('accuracy').textContent = accuracy + '%'; + } + + setupEventListeners() { + document.getElementById('redSlider').addEventListener('input', () => { + this.updatePlayerColor(); + }); + + document.getElementById('greenSlider').addEventListener('input', () => { + this.updatePlayerColor(); + }); + + document.getElementById('blueSlider').addEventListener('input', () => { + this.updatePlayerColor(); + }); + + // Button events + document.getElementById('checkBtn').addEventListener('click', () => { + this.checkMatch(); + }); + + document.getElementById('newColorBtn').addEventListener('click', () => { + this.nextColor(); + }); + + // Keyboard shortcuts + document.addEventListener('keydown', (e) => { + if (e.code === 'Space') { + e.preventDefault(); + this.checkMatch(); + } else if (e.code === 'Enter') { + e.preventDefault(); + this.nextColor(); + } + }); + } +} + +const style = document.createElement('style'); +style.textContent = ` + @keyframes fall { + 0% { + transform: translateY(-100px) rotate(0deg); + opacity: 1; + } + 100% { + transform: translateY(100vh) rotate(360deg); + opacity: 0; + } + } +`; +document.head.appendChild(style); + +window.addEventListener('load', () => { + new PixelHarmony(); +}); \ No newline at end of file diff --git a/games/pixel-harmony/style.css b/games/pixel-harmony/style.css new file mode 100644 index 00000000..99368f68 --- /dev/null +++ b/games/pixel-harmony/style.css @@ -0,0 +1,310 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; +} + +body { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #fff; + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; +} + +.container { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(20px); + border-radius: 24px; + padding: 40px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + max-width: 500px; + width: 100%; +} + +header { + text-align: center; + margin-bottom: 40px; +} + +h1 { + font-size: 3rem; + font-weight: 700; + margin-bottom: 8px; + background: linear-gradient(45deg, #fff, #f0f0f0); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + text-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.subtitle { + font-size: 1.1rem; + opacity: 0.9; + font-weight: 300; +} + +.game-board { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 30px; + margin-bottom: 40px; +} + +.target-section, .player-section { + text-align: center; +} + +.target-color, .player-color { + width: 140px; + height: 140px; + border-radius: 20px; + margin: 0 auto 20px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + border: 3px solid rgba(255, 255, 255, 0.3); + transition: all 0.3s ease; +} + +.target-color { + background: #FF6B6B; +} + +.player-color { + background: #ffffff; +} + +.color-code { + font-family: 'Monaco', 'Menlo', monospace; + font-size: 1.1rem; + font-weight: 600; + background: rgba(0, 0, 0, 0.2); + padding: 8px 16px; + border-radius: 12px; + display: inline-block; +} + +.controls { + margin-bottom: 40px; +} + +.color-sliders { + margin-bottom: 30px; +} + +.slider-group { + display: flex; + align-items: center; + margin-bottom: 20px; + gap: 15px; +} + +.slider-group label { + font-weight: 600; + width: 20px; + text-align: center; +} + +.slider { + flex: 1; + height: 8px; + border-radius: 4px; + background: rgba(255, 255, 255, 0.2); + outline: none; + -webkit-appearance: none; +} + +.slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 24px; + height: 24px; + border-radius: 50%; + cursor: pointer; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); +} + +.red::-webkit-slider-thumb { + background: #FF6B6B; +} +.green::-webkit-slider-thumb { + background: #4ECDC4; +} +.blue::-webkit-slider-thumb { + background: #45B7D1; +} + +.slider-group span { + font-family: 'Monaco', 'Menlo', monospace; + font-weight: 600; + width: 35px; + text-align: center; + background: rgba(0, 0, 0, 0.2); + padding: 4px 8px; + border-radius: 6px; +} + +.actions { + display: flex; + gap: 15px; +} + +button { + flex: 1; + padding: 16px 24px; + border: none; + border-radius: 12px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2); +} + +.primary-btn { + background: linear-gradient(45deg, #4ECDC4, #44A08D); + color: white; +} + +.primary-btn:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(78, 205, 196, 0.4); +} + +.secondary-btn { + background: rgba(255, 255, 255, 0.2); + color: white; + border: 1px solid rgba(255, 255, 255, 0.3); +} + +.secondary-btn:hover { + background: rgba(255, 255, 255, 0.3); + transform: translateY(-2px); +} + +.stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; + margin-bottom: 30px; +} + +.stat { + text-align: center; + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 16px; + backdrop-filter: blur(10px); +} + +.stat-value { + font-size: 2rem; + font-weight: 700; + margin-bottom: 5px; + color: #4ECDC4; +} + +.stat-label { + font-size: 0.9rem; + opacity: 0.8; + font-weight: 500; +} + +.feedback { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 16px; + backdrop-filter: blur(10px); +} + +.feedback-content { + text-align: center; +} + +#feedbackText { + font-size: 1.1rem; + font-weight: 500; + margin-bottom: 15px; + display: block; +} + +.difference-bar { + width: 100%; + height: 8px; + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; + overflow: hidden; +} + +.difference-fill { + height: 100%; + background: linear-gradient(90deg, #4ECDC4, #44A08D); + border-radius: 4px; + transition: width 0.5s ease; + width: 100%; +} + +.confetti { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1000; + opacity: 0; + transition: opacity 0.3s ease; +} + +/* Animations */ +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +@keyframes celebrate { + 0% { transform: translateY(0) rotate(0deg); } + 50% { transform: translateY(-10px) rotate(5deg); } + 100% { transform: translateY(0) rotate(0deg); } +} + +.pulse { + animation: pulse 0.5s ease; +} + +.celebrate { + animation: celebrate 0.6s ease; +} + +/* Responsive Design */ +@media (max-width: 600px) { + .container { + padding: 30px 20px; + } + + h1 { + font-size: 2.5rem; + } + + .game-board { + grid-template-columns: 1fr; + gap: 20px; + } + + .target-color, .player-color { + width: 120px; + height: 120px; + } + + .actions { + flex-direction: column; + } + + .stats { + grid-template-columns: 1fr; + gap: 15px; + } +} \ No newline at end of file diff --git a/games/pixel-painter/index.html b/games/pixel-painter/index.html new file mode 100644 index 00000000..fb256528 --- /dev/null +++ b/games/pixel-painter/index.html @@ -0,0 +1,43 @@ + + + + + + Pixel Painter + + + + + + + + + +
    +
    +

    PIXEL PAINTER

    +

    Use your cursor to "paint" by hitting colored pixel blocks, creating artwork while racing against a timer!

    + +

    Controls:

    +
      +
    • Mouse - Move cursor
    • +
    • Click or Space - Paint pixels
    • +
    • R - Reset canvas
    • +
    + + +
    +
    + +
    +
    Pixels Painted: 0
    +
    Time: 60
    +
    Current Color: Red
    +
    + + + + + + + \ No newline at end of file diff --git a/games/pixel-painter/script.js b/games/pixel-painter/script.js new file mode 100644 index 00000000..107a5323 --- /dev/null +++ b/games/pixel-painter/script.js @@ -0,0 +1,199 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const startButton = document.getElementById('startButton'); +const instructionsOverlay = document.getElementById('instructions-overlay'); +const scoreElement = document.getElementById('score'); +const timerElement = document.getElementById('timer'); +const colorElement = document.getElementById('color'); + +canvas.width = 800; +canvas.height = 600; + +const PIXEL_SIZE = 20; +const GRID_WIDTH = canvas.width / PIXEL_SIZE; +const GRID_HEIGHT = canvas.height / PIXEL_SIZE; + +let gameRunning = false; +let pixels = []; +let cursor = { x: 0, y: 0 }; +let score = 0; +let timeLeft = 60; +let currentColor = '#ff0000'; +let gameTimer; +let mouseDown = false; + +// Colors available +const colors = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff', '#ffffff', '#000000']; +let colorIndex = 0; + +// Initialize pixels grid +function initPixels() { + pixels = []; + for (let y = 0; y < GRID_HEIGHT; y++) { + pixels[y] = []; + for (let x = 0; x < GRID_WIDTH; x++) { + pixels[y][x] = '#cccccc'; // Unpainted gray + } + } +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw pixels + for (let y = 0; y < GRID_HEIGHT; y++) { + for (let x = 0; x < GRID_WIDTH; x++) { + ctx.fillStyle = pixels[y][x]; + ctx.fillRect(x * PIXEL_SIZE, y * PIXEL_SIZE, PIXEL_SIZE, PIXEL_SIZE); + ctx.strokeStyle = '#999'; + ctx.lineWidth = 1; + ctx.strokeRect(x * PIXEL_SIZE, y * PIXEL_SIZE, PIXEL_SIZE, PIXEL_SIZE); + } + } + + // Draw cursor + ctx.strokeStyle = currentColor; + ctx.lineWidth = 3; + ctx.strokeRect(cursor.x * PIXEL_SIZE, cursor.y * PIXEL_SIZE, PIXEL_SIZE, PIXEL_SIZE); + + // Draw cursor crosshair + ctx.beginPath(); + ctx.moveTo(cursor.x * PIXEL_SIZE + PIXEL_SIZE/2, cursor.y * PIXEL_SIZE); + ctx.lineTo(cursor.x * PIXEL_SIZE + PIXEL_SIZE/2, cursor.y * PIXEL_SIZE + PIXEL_SIZE); + ctx.moveTo(cursor.x * PIXEL_SIZE, cursor.y * PIXEL_SIZE + PIXEL_SIZE/2); + ctx.lineTo(cursor.x * PIXEL_SIZE + PIXEL_SIZE, cursor.y * PIXEL_SIZE + PIXEL_SIZE/2); + ctx.stroke(); +} + +// Paint pixel +function paintPixel(x, y) { + if (x >= 0 && x < GRID_WIDTH && y >= 0 && y < GRID_HEIGHT) { + if (pixels[y][x] === '#cccccc') { // Only paint unpainted pixels + pixels[y][x] = currentColor; + score++; + updateUI(); + } + } +} + +// Change color +function changeColor() { + colorIndex = (colorIndex + 1) % colors.length; + currentColor = colors[colorIndex]; + updateUI(); +} + +// Update UI +function updateUI() { + scoreElement.textContent = `Pixels Painted: ${score}`; + timerElement.textContent = `Time: ${timeLeft}`; + const colorNames = ['Red', 'Green', 'Blue', 'Yellow', 'Magenta', 'Cyan', 'White', 'Black']; + colorElement.textContent = `Current Color: ${colorNames[colorIndex]}`; +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + draw(); + requestAnimationFrame(gameLoop); +} + +// Start game +function startGame() { + gameRunning = true; + timeLeft = 60; + score = 0; + colorIndex = 0; + currentColor = colors[0]; + initPixels(); + updateUI(); + + gameTimer = setInterval(() => { + timeLeft--; + updateUI(); + if (timeLeft <= 0) { + endGame(); + } + }, 1000); + + gameLoop(); +} + +// End game +function endGame() { + gameRunning = false; + clearInterval(gameTimer); + alert(`Time's up! You painted ${score} pixels.`); +} + +// Reset canvas +function resetCanvas() { + initPixels(); + score = 0; + updateUI(); +} + +// Event listeners +startButton.addEventListener('click', () => { + instructionsOverlay.style.display = 'none'; + startGame(); +}); + +canvas.addEventListener('mousemove', (e) => { + if (!gameRunning) return; + const rect = canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / PIXEL_SIZE); + const y = Math.floor((e.clientY - rect.top) / PIXEL_SIZE); + cursor.x = Math.max(0, Math.min(GRID_WIDTH - 1, x)); + cursor.y = Math.max(0, Math.min(GRID_HEIGHT - 1, y)); +}); + +canvas.addEventListener('mousedown', (e) => { + if (!gameRunning) return; + mouseDown = true; + paintPixel(cursor.x, cursor.y); +}); + +canvas.addEventListener('mouseup', () => { + mouseDown = false; +}); + +canvas.addEventListener('mousemove', (e) => { + if (!gameRunning || !mouseDown) return; + paintPixel(cursor.x, cursor.y); +}); + +document.addEventListener('keydown', (e) => { + if (!gameRunning) return; + + switch (e.code) { + case 'Space': + e.preventDefault(); + paintPixel(cursor.x, cursor.y); + break; + case 'KeyR': + resetCanvas(); + break; + case 'KeyC': + changeColor(); + break; + case 'ArrowUp': + cursor.y = Math.max(0, cursor.y - 1); + break; + case 'ArrowDown': + cursor.y = Math.min(GRID_HEIGHT - 1, cursor.y + 1); + break; + case 'ArrowLeft': + cursor.x = Math.max(0, cursor.x - 1); + break; + case 'ArrowRight': + cursor.x = Math.min(GRID_WIDTH - 1, cursor.x + 1); + break; + } +}); + +// Initialize +initPixels(); +updateUI(); diff --git a/games/pixel-painter/style.css b/games/pixel-painter/style.css new file mode 100644 index 00000000..7f00dc43 --- /dev/null +++ b/games/pixel-painter/style.css @@ -0,0 +1,132 @@ +/* General Reset & Font */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Poppins', sans-serif; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #eee; + overflow: hidden; +} + +/* Game UI */ +#game-ui { + position: absolute; + top: 20px; + left: 20px; + display: flex; + gap: 20px; + z-index: 5; +} + +#score, #timer, #color { + background-color: rgba(0, 0, 0, 0.7); + color: #fff; + padding: 10px 15px; + border-radius: 5px; + font-size: 1.1rem; + font-weight: 600; +} + +/* Canvas */ +canvas { + background: #ffffff; + border: 3px solid #333; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.3); + display: block; + cursor: crosshair; +} + +/* Instructions Screen */ +#instructions-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +#instructions-content { + background-color: #2a2a2a; + padding: 30px 40px; + border-radius: 10px; + text-align: center; + border: 2px solid #667eea; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); + max-width: 500px; +} + +#instructions-content h2 { + font-size: 2.5rem; + color: #667eea; + margin-bottom: 15px; + letter-spacing: 2px; +} + +#instructions-content p { + font-size: 1.1rem; + margin-bottom: 25px; + color: #ccc; +} + +#instructions-content h3 { + font-size: 1.2rem; + color: #eee; + margin-bottom: 10px; + border-bottom: 1px solid #444; + padding-bottom: 5px; +} + +#instructions-content ul { + list-style: none; + margin-bottom: 30px; + text-align: left; + display: inline-block; +} + +#instructions-content li { + font-size: 1rem; + color: #ccc; + margin-bottom: 8px; +} + +/* Style for keys */ +#instructions-content code { + background-color: #667eea; + color: #fff; + padding: 3px 6px; + border-radius: 4px; + font-family: monospace; + font-size: 0.95rem; + margin-right: 8px; +} + +/* Start Button */ +#startButton { + background-color: #667eea; + color: white; + border: none; + padding: 12px 24px; + font-size: 1.1rem; + font-weight: 600; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} + +#startButton:hover { + background-color: #5a6fd8; +} \ No newline at end of file diff --git a/games/pixel-pirate/index.html b/games/pixel-pirate/index.html new file mode 100644 index 00000000..5bf4014b --- /dev/null +++ b/games/pixel-pirate/index.html @@ -0,0 +1,19 @@ + + + + + + Pixel Pirate Game + + + +
    +

    Pixel Pirate

    + +
    Score: 0
    +
    Health: 100
    +
    Use WASD to sail your ship. Click to fire cannons at enemy ships and collect treasure!
    +
    + + + \ No newline at end of file diff --git a/games/pixel-pirate/script.js b/games/pixel-pirate/script.js new file mode 100644 index 00000000..24666ece --- /dev/null +++ b/games/pixel-pirate/script.js @@ -0,0 +1,217 @@ +// Pixel Pirate Game Script +// Sail the seas, battle enemy ships, and hunt for treasure. + +const canvas = document.getElementById('game-canvas'); +const ctx = canvas.getContext('2d'); +const scoreElement = document.getElementById('score'); +const healthElement = document.getElementById('health'); + +// Game variables +let player = { x: 400, y: 300, width: 30, height: 20, speed: 3, health: 100 }; +let enemies = []; +let cannonballs = []; +let treasures = []; +let score = 0; +let gameRunning = true; +let keys = {}; + +// Constants +const enemySpeed = 1; +const cannonballSpeed = 5; +const treasureValue = 50; + +// Initialize game +function init() { + // Create initial enemies and treasures + for (let i = 0; i < 3; i++) { + createEnemy(); + createTreasure(); + } + + // Start game loop + requestAnimationFrame(gameLoop); +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + update(); + draw(); + + requestAnimationFrame(gameLoop); +} + +// Update game state +function update() { + // Handle input + if (keys.w) player.y = Math.max(player.y - player.speed, 0); + if (keys.s) player.y = Math.min(player.y + player.speed, canvas.height - player.height); + if (keys.a) player.x = Math.max(player.x - player.speed, 0); + if (keys.d) player.x = Math.min(player.x + player.speed, canvas.width - player.width); + + // Move enemies + enemies.forEach(enemy => { + // Simple AI: move towards player + const dx = player.x - enemy.x; + const dy = player.y - enemy.y; + const dist = Math.sqrt(dx*dx + dy*dy); + if (dist > 0) { + enemy.x += (dx / dist) * enemySpeed; + enemy.y += (dy / dist) * enemySpeed; + } + }); + + // Move cannonballs + cannonballs.forEach(ball => { + ball.x += ball.dx; + ball.y += ball.dy; + }); + + // Remove off-screen cannonballs + cannonballs = cannonballs.filter(ball => + ball.x > 0 && ball.x < canvas.width && ball.y > 0 && ball.y < canvas.height + ); + + // Check collisions + checkCollisions(); +} + +// Draw everything +function draw() { + // Clear canvas with sea effect + ctx.fillStyle = '#001122'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw player ship + ctx.fillStyle = '#8B4513'; + ctx.fillRect(player.x, player.y, player.width, player.height); + ctx.fillStyle = '#FFFF00'; + ctx.fillRect(player.x + 25, player.y + 5, 5, 10); // Sail + + // Draw enemies + ctx.fillStyle = '#FF0000'; + enemies.forEach(enemy => { + ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height); + ctx.fillStyle = '#000000'; + ctx.fillRect(enemy.x + 25, enemy.y + 5, 5, 10); // Enemy sail + ctx.fillStyle = '#FF0000'; + }); + + // Draw cannonballs + ctx.fillStyle = '#000000'; + cannonballs.forEach(ball => { + ctx.beginPath(); + ctx.arc(ball.x, ball.y, 3, 0, Math.PI * 2); + ctx.fill(); + }); + + // Draw treasures + ctx.fillStyle = '#FFD700'; + treasures.forEach(treasure => { + ctx.beginPath(); + ctx.arc(treasure.x, treasure.y, 8, 0, Math.PI * 2); + ctx.fill(); + }); + + // Update displays + scoreElement.textContent = 'Score: ' + score; + healthElement.textContent = 'Health: ' + player.health; +} + +// Handle input +document.addEventListener('keydown', e => { + if (e.key.toLowerCase() === 'w') keys.w = true; + if (e.key.toLowerCase() === 'a') keys.a = true; + if (e.key.toLowerCase() === 's') keys.s = true; + if (e.key.toLowerCase() === 'd') keys.d = true; +}); + +document.addEventListener('keyup', e => { + if (e.key.toLowerCase() === 'w') keys.w = false; + if (e.key.toLowerCase() === 'a') keys.a = false; + if (e.key.toLowerCase() === 's') keys.s = false; + if (e.key.toLowerCase() === 'd') keys.d = false; +}); + +canvas.addEventListener('click', e => { + const rect = canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + // Fire cannonball towards mouse + const dx = mouseX - (player.x + player.width/2); + const dy = mouseY - (player.y + player.height/2); + const dist = Math.sqrt(dx*dx + dy*dy); + if (dist > 0) { + cannonballs.push({ + x: player.x + player.width/2, + y: player.y + player.height/2, + dx: (dx / dist) * cannonballSpeed, + dy: (dy / dist) * cannonballSpeed + }); + } +}); + +// Create enemy +function createEnemy() { + enemies.push({ + x: Math.random() * (canvas.width - 30), + y: Math.random() * (canvas.height - 20), + width: 30, + height: 20, + health: 2 + }); +} + +// Create treasure +function createTreasure() { + treasures.push({ + x: Math.random() * canvas.width, + y: Math.random() * canvas.height + }); +} + +// Check collisions +function checkCollisions() { + // Cannonballs with enemies + cannonballs.forEach((ball, bi) => { + enemies.forEach((enemy, ei) => { + if (ball.x > enemy.x && ball.x < enemy.x + enemy.width && + ball.y > enemy.y && ball.y < enemy.y + enemy.height) { + cannonballs.splice(bi, 1); + enemy.health--; + if (enemy.health <= 0) { + enemies.splice(ei, 1); + score += 100; + createEnemy(); // Spawn new enemy + } + } + }); + }); + + // Player with treasures + treasures.forEach((treasure, i) => { + if (Math.abs(player.x + player.width/2 - treasure.x) < 20 && + Math.abs(player.y + player.height/2 - treasure.y) < 20) { + treasures.splice(i, 1); + score += treasureValue; + createTreasure(); // Spawn new treasure + } + }); + + // Player with enemies (collision damage) + enemies.forEach(enemy => { + if (player.x < enemy.x + enemy.width && player.x + player.width > enemy.x && + player.y < enemy.y + enemy.height && player.y + player.height > enemy.y) { + player.health -= 10; + if (player.health <= 0) { + gameRunning = false; + alert('Your ship sank! Game Over. Score: ' + score); + } + } + }); +} + +// Start the game +init(); \ No newline at end of file diff --git a/games/pixel-pirate/style.css b/games/pixel-pirate/style.css new file mode 100644 index 00000000..1e03f100 --- /dev/null +++ b/games/pixel-pirate/style.css @@ -0,0 +1,39 @@ +body { + font-family: 'Courier New', monospace; + background-color: #000; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +#game-container { + text-align: center; +} + +h1 { + font-size: 2.5em; + margin-bottom: 20px; + text-shadow: 0 0 10px #ffff00; +} + +#game-canvas { + border: 2px solid #ffff00; + background-color: #001100; + box-shadow: 0 0 20px #ffff00; + cursor: crosshair; +} + +#score, #health { + font-size: 1.2em; + margin: 5px 0; + color: #00ff00; +} + +#instructions { + font-size: 1em; + margin-top: 10px; + color: #cccccc; +} \ No newline at end of file diff --git a/games/pixel-wall-breaker/index.html b/games/pixel-wall-breaker/index.html new file mode 100644 index 00000000..e404c512 --- /dev/null +++ b/games/pixel-wall-breaker/index.html @@ -0,0 +1,101 @@ + + + + + + Pixel Wall Breaker โ€” Mini JS Games Hub + + + + + + + + +
    +
    +
    +

    Pixel Wall Breaker

    + Retro ยท Arcade ยท Advanced +
    + +
    +
    +
    Score: 0
    +
    Level: 1
    +
    Lives: 3
    +
    + +
    + + + + โ† Back to Hub +
    +
    +
    + +
    + + +
    +
    + + + + +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    Built with โค๏ธ โ€ข Uses public sound resources
    +
    +
    + + + + + diff --git a/games/pixel-wall-breaker/script.js b/games/pixel-wall-breaker/script.js new file mode 100644 index 00000000..f220f066 --- /dev/null +++ b/games/pixel-wall-breaker/script.js @@ -0,0 +1,489 @@ +/* Pixel Wall Breaker โ€” script.js + Place in games/pixel-wall-breaker/script.js +*/ + +// ---------- Asset URLs (public, online) ---------- +const SOUND_BOUNCE = "https://actions.google.com/sounds/v1/cartoon/cartoon_boing.ogg"; +const SOUND_POP = "https://assets.mixkit.co/sfx/preview/mixkit-fast-small-burst-1683.mp3"; +const SOUND_LEVEL = "https://actions.google.com/sounds/v1/cartoon/clang_and_wobble.ogg"; +const SOUND_SHOOT = "https://actions.google.com/sounds/v1/cartoon/slide_whistle_to_drum_hit.ogg"; +const AMBIENT = "https://actions.google.com/sounds/v1/ambiences/arcade_room.ogg"; // ambient loop (optional) + +// ---------- Canvas & context ---------- +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +// Keep internal size consistent; canvas looks crisp on high DPI +function resizeCanvas() { + const rect = canvas.getBoundingClientRect(); + const dpr = window.devicePixelRatio || 1; + canvas.width = Math.floor(rect.width * dpr); + canvas.height = Math.floor(rect.height * dpr); + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); +} +resizeCanvas(); +window.addEventListener("resize", () => { + resizeCanvas(); + draw(); +}); + +// ---------- UI elements ---------- +const scoreEl = document.getElementById("score"); +const levelEl = document.getElementById("level"); +const livesEl = document.getElementById("lives"); +const btnPause = document.getElementById("btn-pause"); +const btnRestart = document.getElementById("btn-restart"); +const btnSound = document.getElementById("btn-sound"); +const centerInfo = document.getElementById("centerInfo"); +const centerTitle = document.getElementById("centerTitle"); +const centerMsg = document.getElementById("centerMsg"); +const centerContinue = document.getElementById("centerContinue"); +const centerRetry = document.getElementById("centerRetry"); +const powerProgress = document.getElementById("power-progress"); +const ballsCountSelect = document.getElementById("balls-count"); + +// ---------- Sound handling ---------- +let soundEnabled = true; +const sounds = {}; +function loadSound(name, url) { + const a = new Audio(url); + a.preload = "auto"; + a.volume = 0.6; + sounds[name] = a; +} +loadSound("bounce", SOUND_BOUNCE); +loadSound("pop", SOUND_POP); +loadSound("shoot", SOUND_SHOOT); +loadSound("level", SOUND_LEVEL); +const ambientAudio = new Audio(AMBIENT); +ambientAudio.loop = true; +ambientAudio.volume = 0.25; + +// helper to play +function playSound(name) { + if (!soundEnabled) return; + const s = sounds[name]; + if (!s) return; + // clone/pause trick for overlapping play + const clone = s.cloneNode(); + clone.volume = s.volume; + clone.play().catch(()=>{}); +} + +// toggle sound +btnSound.addEventListener("click", () => { + soundEnabled = !soundEnabled; + btnSound.textContent = `Sound: ${soundEnabled ? "On" : "Off"}`; + if (soundEnabled) ambientAudio.play().catch(()=>{}); + else ambientAudio.pause(); +}); + +// ---------- Game state ---------- +let state = { + score: 0, + level: 1, + lives: 3, + running: true, + paused: false +}; + +// playfield bounds (logical) +const pf = { + x: 10, y: 10, + width: canvas.getBoundingClientRect().width - 20, + height: canvas.getBoundingClientRect().height - 20 +}; + +// shooter location (bottom-left) +let shooter = { x: 50, y: (canvas.getBoundingClientRect().height - 40), size: 36 }; + +// aiming +let isAiming = false; +let aimStart = null; +let aimCurrent = null; +let power = 50; + +// ball physics parameters +const gravity = 0.18; // slight gravity for arc +const friction = 0.998; // velocity retention on bounces + +// arrays +let balls = []; +let bricks = []; + +// level definitions generator +function generateLevel(level) { + // clear arrays + balls = []; + bricks = []; + const rows = Math.min(6 + Math.floor(level/2), 10); + const cols = 9; + const brickW = Math.floor((pf.width - 60) / cols); + const brickH = 28; + const startX = 30; + const startY = 40; + + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + // Only some bricks exist โ€” create patterns + const probability = 0.88 - level*0.01; // fewer holes as level increases + if (Math.random() < probability) { + const hp = 1 + Math.floor((level + r) / 4); // HP increases with level + bricks.push({ + x: startX + c * (brickW + 6), + y: startY + r * (brickH + 8), + w: brickW, + h: brickH, + hp: hp, + colorSeed: (r + c) % 5 + }); + } + } + } + // ensure at least some bricks exist + if (bricks.length === 0) { + bricks.push({x:startX, y:startY, w:brickW, h:brickH, hp:level, colorSeed:0}); + } +} + +// reset level/start +function startLevel(levelNum) { + state.level = levelNum; + state.running = true; + state.paused = false; + balls = []; + generateLevel(levelNum); + updateUI(); + playSound("level"); + // show small center message + showCenter(`Level ${levelNum}`, `Clear all bricks to progress!`, 900); +} + +// game over +function gameOver() { + state.running = false; + showCenter("Game Over", `Your score: ${state.score}`, null, false); +} + +// show center overlay +function showCenter(title, msg, autoClose=0, showContinue=true) { + centerTitle.textContent = title; + centerMsg.textContent = msg; + centerContinue.style.display = showContinue ? "inline-block" : "none"; + centerInfo.hidden = false; + centerInfo.style.pointerEvents = "auto"; + if (autoClose) { + setTimeout(()=>{ centerInfo.hidden = true; centerInfo.style.pointerEvents="none";}, autoClose); + } +} + +// hide center +function hideCenter() { + centerInfo.hidden = true; + centerInfo.style.pointerEvents = "none"; +} + +// UI update +function updateUI() { + scoreEl.textContent = state.score; + levelEl.textContent = state.level; + livesEl.textContent = state.lives; + powerProgress.value = power; +} + +// ------------ Shooting / aiming logic ------------ +function spawnBall(x, y, vx, vy) { + balls.push({ + x, y, vx, vy, r: 7 + Math.random()*3, life: 200, stuck:false + }); + playSound("shoot"); +} + +function fireFromAim() { + const count = parseInt(ballsCountSelect.value,10) || 1; + // compute vector + if (!aimStart || !aimCurrent) return; + const dx = aimStart.x - aimCurrent.x; + const dy = aimStart.y - aimCurrent.y; + const mag = Math.hypot(dx, dy) || 1; + const normX = dx / mag; + const normY = dy / mag; + const speedScale = Math.min(2.5 + power/30, 8); + // spawn multiple balls with slight spread + for (let i=0;i{ + // physics + b.vy += gravity * (dt/16); + b.x += b.vx * (dt/16); + b.y += b.vy * (dt/16); + b.vx *= friction; + b.vy *= friction; + + // bounce walls (playfield boundaries) + if (b.x - b.r < pf.x) { b.x = pf.x + b.r; b.vx = -b.vx * 0.92; playSound("bounce"); } + if (b.x + b.r > pf.x + pf.width) { b.x = pf.x + pf.width - b.r; b.vx = -b.vx * 0.92; playSound("bounce"); } + if (b.y - b.r < pf.y) { b.y = pf.y + b.r; b.vy = -b.vy * 0.92; playSound("bounce"); } + // floor: if ball goes below bottom + if (b.y - b.r > pf.y + pf.height) { + b.dead = true; + } + + // collisions with bricks + for (let i = bricks.length - 1; i >= 0; i--) { + const br = bricks[i]; + if (rectCircleCollide(b, br)) { + // simple normal: push ball out by reversing velocity depending on collision side + // compute centers + const cx = b.x, cy = b.y; + // if collision predominantly vertical or horizontal + const overlapX = Math.max(br.x - cx, 0, cx - (br.x + br.w)); + const overlapY = Math.max(br.y - cy, 0, cy - (br.y + br.h)); + // approximate response + b.vy = -b.vy * 0.92; + b.vx = b.vx * 0.95; + // damage brick + br.hp -= 1; + if (br.hp <= 0) { + bricks.splice(i,1); + state.score += 10; + playSound("pop"); + } else { + state.score += 2; + playSound("bounce"); + } + } + } + }); + + // remove dead balls + balls = balls.filter(b => !b.dead && (b.life === undefined || b.life-- > 0)); + + // level clear check + if (bricks.length === 0) { + // next level + state.score += 100 * state.level; + startLevel(state.level + 1); + } + + // no balls and player fired previously -> lose life and reset shooting position + if (balls.length === 0 && !isAiming) { + // Nothing active - keep waiting for player + } + + updateUI(); + draw(); + requestAnimationFrame(update); +} + +// ---------- Drawing ---------- +function draw() { + // clear + const W = canvas.width / (window.devicePixelRatio || 1); + const H = canvas.height / (window.devicePixelRatio || 1); + ctx.clearRect(0,0,W,H); + + // playfield background + ctx.fillStyle = "#041826"; + ctx.fillRect(pf.x, pf.y, pf.width, pf.height); + + // draw grid lines subtle + ctx.strokeStyle = "rgba(255,255,255,0.02)"; + ctx.lineWidth = 1; + for (let gx = pf.x; gx < pf.x + pf.width; gx += 40) { + ctx.beginPath(); ctx.moveTo(gx, pf.y); ctx.lineTo(gx, pf.y + pf.height); ctx.stroke(); + } + + // draw bricks + bricks.forEach(br=>{ + // choose color based on hp or seed + let hue = 200 + (br.colorSeed * 30); + let sat = 80; + // color shifts with hp + const light = Math.max(30, 70 - br.hp * 6); + ctx.fillStyle = `hsl(${hue} ${sat}% ${light}%)`; + // draw rounded rect + roundRect(ctx, br.x, br.y, br.w, br.h, 6, true, false); + // inner glossy + ctx.fillStyle = `rgba(255,255,255,0.06)`; + roundRect(ctx, br.x+4, br.y+4, br.w-8, 6, 3, true, false); + // hp indicator small bar + ctx.fillStyle = `rgba(0,0,0,0.35)`; + roundRect(ctx, br.x + 6, br.y + br.h - 10, br.w - 12, 6, 3, true, false); + ctx.fillStyle = `rgba(255,255,255,0.9)`; + const hpWidth = Math.max(4, (br.w - 12) * Math.min(1, br.hp / (1+state.level/3))); + ctx.fillRect(br.x + 6, br.y + br.h - 10, hpWidth, 6); + }); + + // draw balls + balls.forEach(b=>{ + // neon glow + ctx.beginPath(); + const grd = ctx.createRadialGradient(b.x, b.y, b.r*0.2, b.x, b.y, b.r*3); + grd.addColorStop(0, "rgba(255,255,255,0.95)"); + grd.addColorStop(0.2, "rgba(134,255,255,0.9)"); + grd.addColorStop(1, "rgba(54,240,255,0.06)"); + ctx.fillStyle = grd; + ctx.arc(b.x, b.y, b.r, 0, Math.PI*2); + ctx.fill(); + // outline + ctx.lineWidth = 1; + ctx.strokeStyle = "rgba(255,255,255,0.18)"; + ctx.stroke(); + }); + + // draw shooter indicator + ctx.save(); + ctx.translate(shooter.x, shooter.y); + ctx.fillStyle = "#9bf1ff"; + ctx.strokeStyle = "rgba(255,255,255,0.15)"; + roundRect(ctx, -12, -12, shooter.size, shooter.size, 8, true, true); + ctx.restore(); + + // draw aim line if aiming + if (isAiming && aimStart && aimCurrent) { + ctx.save(); + ctx.strokeStyle = "rgba(54,240,255,0.9)"; + ctx.lineWidth = 2; + ctx.setLineDash([6,6]); + ctx.beginPath(); + ctx.moveTo(aimStart.x, aimStart.y); + ctx.lineTo(aimCurrent.x, aimCurrent.y); + ctx.stroke(); + ctx.setLineDash([]); + // power arc + const dx = aimStart.x - aimCurrent.x; + const dy = aimStart.y - aimCurrent.y; + const p = Math.min(100, Math.floor(Math.hypot(dx,dy))); + ctx.fillStyle = "rgba(54,240,255,0.06)"; + ctx.fillRect(aimStart.x+20, aimStart.y-20, p, 6); + ctx.restore(); + } +} + +// rounded rect helper +function roundRect(ctx, x, y, w, h, r, fill, stroke) { + if (w < 2 * r) r = w / 2; + if (h < 2 * r) r = h / 2; + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.arcTo(x + w, y, x + w, y + h, r); + ctx.arcTo(x + w, y + h, x, y + h, r); + ctx.arcTo(x, y + h, x, y, r); + ctx.arcTo(x, y, x + w, y, r); + ctx.closePath(); + if (fill) ctx.fill(); + if (stroke) ctx.stroke(); +} + +// ---------- Input handling ---------- +const rect = canvas.getBoundingClientRect(); + +function getPointer(e) { + const P = e.touches ? e.touches[0] : e; + // convert to canvas coords + const r = canvas.getBoundingClientRect(); + return { x: P.clientX - r.left, y: P.clientY - r.top }; +} + +canvas.addEventListener("pointerdown", (e) => { + if (state.paused || !state.running) return; + isAiming = true; + aimStart = { x: shooter.x, y: shooter.y }; + aimCurrent = getPointer(e); +}); + +canvas.addEventListener("pointermove", (e) => { + if (!isAiming) return; + aimCurrent = getPointer(e); + const dx = aimStart.x - aimCurrent.x; + const dy = aimStart.y - aimCurrent.y; + const mag = Math.min(120, Math.hypot(dx,dy)); + power = Math.floor(Math.min(100, mag)); + powerProgress.value = power; +}); + +canvas.addEventListener("pointerup", (e) => { + if (!isAiming) return; + isAiming = false; + aimCurrent = getPointer(e); + fireFromAim(); + aimStart = null; aimCurrent = null; +}); + +// support touch cancel +canvas.addEventListener("pointercancel", ()=>{ + isAiming = false; aimStart=null; aimCurrent=null; +}); + +// ---------- Buttons ---------- +btnPause.addEventListener("click", () => { + state.paused = !state.paused; + btnPause.textContent = state.paused ? "Resume" : "Pause"; + if (!state.paused) { + last = performance.now(); + requestAnimationFrame(update); + } +}); +btnRestart.addEventListener("click", () => { + state.score = 0; + state.lives = 3; + startLevel(1); +}); +centerContinue.addEventListener("click", () => { + hideCenter(); + if (!state.running) startLevel(1); +}); +centerRetry.addEventListener("click", () => { + hideCenter(); + state.score = 0; + state.lives = 3; + startLevel(1); +}); + +// ---------- Start game ---------- +startLevel(1); +updateUI(); +requestAnimationFrame(update); + +// Start ambient if sound enabled +if (soundEnabled) ambientAudio.play().catch(()=>{}); + +// ---------- Optional: keyboard shortcuts ---------- +window.addEventListener("keydown", (e) => { + if (e.key === "p") btnPause.click(); + if (e.key === "r") btnRestart.click(); + if (e.key === "m") btnSound.click(); +}); + +// that's it โ€” functions are intentionally compact and robust. diff --git a/games/pixel-wall-breaker/style.css b/games/pixel-wall-breaker/style.css new file mode 100644 index 00000000..b116600b --- /dev/null +++ b/games/pixel-wall-breaker/style.css @@ -0,0 +1,58 @@ +:root{ + --bg:#071025; + --card:#081326; + --accent:#36f0ff; + --accent-2:#8a2be2; + --muted:#9aa8bf; + --neon: 0 8px 32px rgba(54,240,255,0.12); + --glass: rgba(255,255,255,0.03); +} + +*{box-sizing:border-box} +html,body{height:100%;margin:0;font-family:Inter,ui-sans-serif,system-ui,Segoe UI,Roboto,"Helvetica Neue",Arial; background:linear-gradient(180deg,var(--bg),#02101b 80%); color:#eaf7ff} +.shell{max-width:1150px;margin:28px auto;padding:18px} + +.topbar{display:flex;flex-direction:column;gap:12px;margin-bottom:14px} +.topbar .left{display:flex;align-items:baseline;gap:12px} +.brand{font-size:20px;margin:0} +.muted{color:var(--muted);font-size:12px} +.controls-top{display:flex;justify-content:space-between;align-items:center;gap:12px} +.hud{display:flex;gap:18px;color:var(--muted);font-weight:600} +.buttons{display:flex;gap:8px;align-items:center} +.btn{background:linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.06);padding:8px 10px;border-radius:10px;color:#eaf7ff;cursor:pointer;backdrop-filter: blur(6px);box-shadow:var(--neon)} +.btn.ghost{background:transparent;border:1px dashed rgba(255,255,255,0.06)} +.btn:active{transform:translateY(1px)} + +.game-area{display:flex;gap:16px} +.left-panel{width:260px} +.panel-card{background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));padding:14px;border-radius:12px;border:1px solid rgba(255,255,255,0.03);box-shadow: 0 8px 30px rgba(0,0,0,0.6)} +.panel-card.small{margin-top:12px} + +.playfield-wrap{flex:1;display:flex;flex-direction:column;align-items:center} +.playfield{position:relative;width:100%;max-width:820px;background:linear-gradient(180deg,#021632,#001623);border-radius:14px;padding:18px;border:1px solid rgba(255,255,255,0.04);box-shadow: 0 20px 60px rgba(9,14,20,0.6) inset} +#gameCanvas{display:block;width:100%;height:auto;border-radius:10px;background:linear-gradient(180deg,#06141f,#041020);box-shadow:0 12px 40px rgba(0,0,0,0.6) inset} + +.overlay{position:absolute;inset:18px;pointer-events:none} +.aim-hud{position:absolute;left:22px;bottom:22px;width:220px;height:140px;pointer-events:auto} +.shooter{position:absolute;left:18px;bottom:18px;width:36px;height:36px;border-radius:10px;background:linear-gradient(180deg,#9bf1ff,#36f0ff);box-shadow:0 6px 18px rgba(54,240,255,0.12),0 0 16px rgba(54,240,255,0.25);border:2px solid rgba(255,255,255,0.06)} +.aim-line{position:absolute;left:56px;bottom:36px;height:3px;width:260px;transform-origin:left center;opacity:0.8} + +.center-info{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;background:linear-gradient(180deg, rgba(2,6,12,0.6), rgba(2,6,12,0.45));border-radius:10px;pointer-events:auto} +.center-info h2{font-size:26px;margin:0} +.center-info p{color:var(--muted);margin:0} + +.foot{margin-top:18px;color:var(--muted);font-size:13px} + +/* neon bricks: each brick has glow when drawn */ +.brick { + border-radius:4px; + box-shadow: 0 4px 14px rgba(138,43,226,0.08), 0 0 12px rgba(54,240,255,0.06) inset; + background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02)); +} + +/* small responsive */ +@media (max-width:980px){ + .game-area{flex-direction:column} + .left-panel{width:100%} + .playfield-wrap{order:1} +} diff --git a/games/pixel_array/index.html b/games/pixel_array/index.html new file mode 100644 index 00000000..499b867e --- /dev/null +++ b/games/pixel_array/index.html @@ -0,0 +1,33 @@ + + + + + + Pixel Array Reversal + + + + +
    +

    Pixel Array Reversal: Image Puzzle

    +

    Fix the scrambled image by applying a sequence of array transformations.

    +

    Memorize this image!

    +
    + +
    + +
    + +
    +

    Transformations

    + + + + +
    + +

    + + + + \ No newline at end of file diff --git a/games/pixel_array/script.js b/games/pixel_array/script.js new file mode 100644 index 00000000..c255b8ea --- /dev/null +++ b/games/pixel_array/script.js @@ -0,0 +1,193 @@ +document.addEventListener('DOMContentLoaded', () => { + const canvas = document.getElementById('puzzleCanvas'); + const ctx = canvas.getContext('2d'); + const messageDisplay = document.getElementById('game-message'); + const memoryHint = document.getElementById('memory-hint'); + + const WIDTH = canvas.width; + const HEIGHT = canvas.height; + + // Global variables to store the original (solved) and current pixel data + let originalImageData; + let currentImageData; + + // --- 1. Initialization and Setup --- + + /** + * Draws a simple pattern onto the canvas to serve as the target image. + */ + function drawOriginalImage() { + // Draw a simple, asymmetric pattern for easy recognition and testing + ctx.fillStyle = '#FF5733'; // Red-Orange + ctx.fillRect(0, 0, WIDTH / 2, HEIGHT / 2); + + ctx.fillStyle = '#33FF57'; // Green + ctx.fillRect(WIDTH / 2, 0, WIDTH / 2, HEIGHT / 2); + + ctx.fillStyle = '#3357FF'; // Blue + ctx.fillRect(0, HEIGHT / 2, WIDTH / 2, HEIGHT / 2); + + ctx.fillStyle = '#FF33A1'; // Pink + ctx.fillRect(WIDTH / 2, HEIGHT / 2, WIDTH / 2, HEIGHT / 2); + + // Add a line or circle for asymmetry (makes reversal more noticeable) + ctx.fillStyle = '#000000'; + ctx.beginPath(); + ctx.arc(WIDTH / 4, HEIGHT / 4, 10, 0, Math.PI * 2); + ctx.fill(); + } + + /** + * Gets the current pixel array from the canvas. + */ + function getCanvasData() { + return ctx.getImageData(0, 0, WIDTH, HEIGHT); + } + + /** + * Puts the current pixel array back onto the canvas. + */ + function putCanvasData(imageData) { + ctx.putImageData(imageData, 0, 0); + } + + /** + * Generates the scrambled starting state and kicks off the game. + */ + function startGame() { + drawOriginalImage(); + + // 1. Store the solved state + originalImageData = getCanvasData(); + + // 2. Perform a random sequence of transformations to create the puzzle + currentImageData = new ImageData(new Uint8ClampedArray(originalImageData.data), WIDTH, HEIGHT); + let scrambleCount = 0; + + // Apply 2-3 random transformations + for (let i = 0; i < 3; i++) { + const randomTransform = Math.floor(Math.random() * 3); + if (randomTransform === 0) { + applyReverse(currentImageData); + scrambleCount++; + } else if (randomTransform === 1) { + applyChannelShift(currentImageData); + scrambleCount++; + } else if (randomTransform === 2) { + applyInvert(currentImageData); + scrambleCount++; + } + } + + putCanvasData(originalImageData); // Show original for memory + messageDisplay.textContent = `Scrambling the image with ${scrambleCount} operations...`; + + // Show the scrambled image after a 3-second memory window + setTimeout(() => { + putCanvasData(currentImageData); + memoryHint.classList.add('hidden'); + messageDisplay.textContent = "Start solving the puzzle!"; + }, 3000); + } + + // --- 2. Transformation Functions (Core Game Mechanics) --- + + /** + * Reverses the entire 1D pixel array. This results in an image that is + * flipped horizontally and vertically (180 degree rotation). + */ + function applyReverse(imageData) { + // Must convert to regular Array for Array.prototype.reverse(), then back. + const dataArray = Array.from(imageData.data); + const reversedArray = dataArray.reverse(); + + // Update the original Uint8ClampedArray + imageData.data.set(reversedArray); + } + + /** + * Shifts the color channels R->G, G->B, B->R for every pixel. + * Alpha channel (A) remains unchanged. + * Array structure: [R, G, B, A, R, G, B, A, ...] + */ + function applyChannelShift(imageData) { + const data = imageData.data; + for (let i = 0; i < data.length; i += 4) { + const R = data[i]; + const G = data[i + 1]; + const B = data[i + 2]; + // A = data[i + 3] (unchanged) + + // Perform the shift: R gets G's value, G gets B's, B gets R's + data[i] = G; // New R + data[i + 1] = B; // New G + data[i + 2] = R; // New B + } + } + + /** + * Inverts the image colors by subtracting each channel value from 255. + * Alpha channel (A) remains unchanged (255 - 255 = 0, which would make it invisible). + */ + function applyInvert(imageData) { + const data = imageData.data; + for (let i = 0; i < data.length; i += 4) { + data[i] = 255 - data[i]; // Invert R + data[i + 1] = 255 - data[i + 1]; // Invert G + data[i + 2] = 255 - data[i + 2]; // Invert B + // data[i + 3] (Alpha) is left alone + } + } + + // --- 3. Win Condition and Event Handling --- + + /** + * Compares the current pixel array against the original solved pixel array. + */ + function checkWinCondition() { + // Compare the raw data buffers for exact match + const currentData = currentImageData.data; + const originalData = originalImageData.data; + + if (currentData.length !== originalData.length) return false; + + let match = true; + for (let i = 0; i < currentData.length; i++) { + if (currentData[i] !== originalData[i]) { + match = false; + break; + } + } + + if (match) { + messageDisplay.textContent = "โœ… PUZZLE SOLVED! The pixel arrays match! ๐ŸŽ‰"; + // Optional: Disable buttons + } else { + messageDisplay.textContent = "Puzzle state updated. Keep trying!"; + } + } + + // Attach transformation buttons to functions + document.getElementById('btn-reverse').addEventListener('click', () => { + applyReverse(currentImageData); + putCanvasData(currentImageData); + checkWinCondition(); + }); + + document.getElementById('btn-channel-shift').addEventListener('click', () => { + applyChannelShift(currentImageData); + putCanvasData(currentImageData); + checkWinCondition(); + }); + + document.getElementById('btn-invert').addEventListener('click', () => { + applyInvert(currentImageData); + putCanvasData(currentImageData); + checkWinCondition(); + }); + + document.getElementById('btn-reset').addEventListener('click', startGame); + + // Start the game! + startGame(); +}); \ No newline at end of file diff --git a/games/pixel_array/style.css b/games/pixel_array/style.css new file mode 100644 index 00000000..70bb0ba4 --- /dev/null +++ b/games/pixel_array/style.css @@ -0,0 +1,49 @@ +body { + font-family: sans-serif; + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + background-color: #f4f4f4; +} + +#puzzleCanvas { + border: 3px solid #333; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + margin: 20px 0; +} + +#controls { + margin-top: 20px; + padding: 15px; + border: 1px solid #ccc; + border-radius: 8px; + background-color: white; + text-align: center; +} + +#controls button { + padding: 10px 15px; + margin: 5px; + font-size: 16px; + cursor: pointer; + border: none; + border-radius: 5px; + transition: background-color 0.2s; +} + +#btn-reverse { background-color: #007bff; color: white; } +#btn-channel-shift { background-color: #28a745; color: white; } +#btn-invert { background-color: #ffc107; } +#btn-reset { background-color: #dc3545; color: white; } + +#game-message { + margin-top: 20px; + font-weight: bold; + min-height: 20px; /* To prevent layout shift */ +} + +/* Class to visually hide the memory hint after a delay */ +.hidden { + display: none; +} \ No newline at end of file diff --git a/games/plinko_drop/index.html b/games/plinko_drop/index.html new file mode 100644 index 00000000..620f6e43 --- /dev/null +++ b/games/plinko_drop/index.html @@ -0,0 +1,34 @@ + + + + + + Plinko Drop Physics Game + + + + +
    +

    โšช Plinko Drop

    + +
    + Score: 0 | Last Drop: -- +
    + +
    + +
    + +
    + + +
    + +
    +

    Click "Drop Ball" and watch the physics!

    +
    +
    + + + + \ No newline at end of file diff --git a/games/plinko_drop/script.js b/games/plinko_drop/script.js new file mode 100644 index 00000000..f6362dfc --- /dev/null +++ b/games/plinko_drop/script.js @@ -0,0 +1,232 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. CANVAS SETUP --- + const canvas = document.getElementById('plinko-canvas'); + const ctx = canvas.getContext('2d'); + + // Set fixed dimensions + const CANVAS_WIDTH = 500; + const CANVAS_HEIGHT = 600; + canvas.width = CANVAS_WIDTH; + canvas.height = CANVAS_HEIGHT; + + // --- 2. DOM Elements --- + const dropButton = document.getElementById('drop-button'); + const resetButton = document.getElementById('reset-button'); + const totalScoreDisplay = document.getElementById('total-score'); + const lastDropScoreDisplay = document.getElementById('last-drop-score'); + const feedbackMessage = document.getElementById('feedback-message'); + + // --- 3. PHYSICS & GAME STATE CONSTANTS --- + const BALL_RADIUS = 8; + const PEG_RADIUS = 5; + const GRAVITY = 0.4; + const FRICTION = 0.9; // Horizontal friction (air resistance) + const RESTITUTION = 0.8; // Bounciness factor (how much velocity is retained) + const SCORE_SLOTS = [10, 50, 100, 50, 10]; // Scores for the bottom slots (5 slots) + const SCORE_SLOT_WIDTH = CANVAS_WIDTH / SCORE_SLOTS.length; + + // Game State + let ball = null; // Stores the current ball object {x, y, vx, vy} + let pegs = []; // Array of peg coordinates {x, y} + let totalScore = 0; + let animationFrameId = null; + + // --- 4. GAME OBJECTS & INITIALIZATION --- + + /** + * Creates the arrangement of pegs on the board. + */ + function createPegs() { + pegs = []; + const startY = 100; + const rows = 10; + const spacingX = 40; + const spacingY = 45; + + for (let r = 0; r < rows; r++) { + // Alternate the starting position of each row for the zig-zag pattern + const offsetX = (r % 2 === 0) ? 0 : spacingX / 2; + const numPegs = Math.floor((CANVAS_WIDTH - offsetX * 2) / spacingX) - 1; + + for (let i = 0; i < numPegs; i++) { + pegs.push({ + x: offsetX + spacingX + i * spacingX, + y: startY + r * spacingY + }); + } + } + } + + /** + * Initializes a new ball object at the top center. + */ + function dropNewBall() { + if (ball !== null) return; // Only one ball at a time + + ball = { + x: CANVAS_WIDTH / 2, + y: 0, + vx: (Math.random() - 0.5) * 2, // Initial small random horizontal velocity + vy: 0, // Initial vertical velocity + active: true + }; + + dropButton.disabled = true; + feedbackMessage.textContent = 'Ball in play...'; + + // Start the game loop + animationFrameId = requestAnimationFrame(gameLoop); + } + + // --- 5. PHYSICS LOOP --- + + /** + * The main simulation loop (called repeatedly by rAF). + */ + function gameLoop() { + if (!ball || !ball.active) { + cancelAnimationFrame(animationFrameId); + return; + } + + // 1. Clear and Draw + draw(); + + // 2. Apply Physics + ball.vy += GRAVITY; // Apply gravity (vertical acceleration) + ball.vx *= FRICTION; // Apply friction (slows horizontal movement) + + ball.x += ball.vx; + ball.y += ball.vy; + + // 3. Check Collisions (Pegs) + pegs.forEach(peg => { + const dx = ball.x - peg.x; + const dy = ball.y - peg.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + const minDistance = BALL_RADIUS + PEG_RADIUS; + + if (distance < minDistance) { + // COLLISION OCCURRED: Reverse/deflect velocity + + // Calculate the normal vector (line from peg center to ball center) + const normalX = dx / distance; + const normalY = dy / distance; + + // Calculate the impact velocity along the normal + const impactVelocity = ball.vx * normalX + ball.vy * normalY; + + // If the impact velocity is positive, the objects are separating or just touching + if (impactVelocity > 0) return; + + // Resolve collision (apply impulse perpendicular to the normal) + const impulseX = impactVelocity * normalX * (1 + RESTITUTION); + const impulseY = impactVelocity * normalY * (1 + RESTITUTION); + + // Apply the reflection and restitution (bounce) + ball.vx -= impulseX; + ball.vy -= impulseY; + + // Separate objects to prevent sticking (push ball back by the overlap amount) + const overlap = minDistance - distance; + ball.x += normalX * overlap; + ball.y += normalY * overlap; + } + }); + + // 4. Check Boundary (Side walls) + if (ball.x < BALL_RADIUS || ball.x > CANVAS_WIDTH - BALL_RADIUS) { + ball.vx *= -RESTITUTION; // Reverse horizontal velocity + ball.x = Math.max(BALL_RADIUS, Math.min(CANVAS_WIDTH - BALL_RADIUS, ball.x)); + } + + // 5. Check Scoring (Bottom) + if (ball.y >= CANVAS_HEIGHT - BALL_RADIUS) { + scoreDrop(); + return; + } + + requestAnimationFrame(gameLoop); + } + + // --- 6. DRAWING FUNCTIONS --- + + /** + * Clears the canvas and draws all game elements (pegs, score slots, ball). + */ + function draw() { + ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + // Draw Score Slots + ctx.fillStyle = '#95a5a6'; + for (let i = 0; i < SCORE_SLOTS.length; i++) { + const x = i * SCORE_SLOT_WIDTH; + ctx.fillRect(x, CANVAS_HEIGHT - 30, SCORE_SLOT_WIDTH, 30); + + // Draw score text + ctx.fillStyle = 'white'; + ctx.font = '14px Arial'; + ctx.textAlign = 'center'; + ctx.fillText(SCORE_SLOTS[i], x + SCORE_SLOT_WIDTH / 2, CANVAS_HEIGHT - 10); + ctx.fillStyle = '#333'; + } + + // Draw Pegs + ctx.fillStyle = '#34495e'; + pegs.forEach(peg => { + ctx.beginPath(); + ctx.arc(peg.x, peg.y, PEG_RADIUS, 0, Math.PI * 2); + ctx.fill(); + }); + + // Draw Ball + if (ball && ball.active) { + ctx.fillStyle = '#e74c3c'; + ctx.beginPath(); + ctx.arc(ball.x, ball.y, BALL_RADIUS, 0, Math.PI * 2); + ctx.fill(); + } + } + + // --- 7. SCORING --- + + /** + * Calculates the score when the ball lands at the bottom. + */ + function scoreDrop() { + // Find which slot the ball landed in + const slotIndex = Math.floor(ball.x / SCORE_SLOT_WIDTH); + const scoreAwarded = SCORE_SLOTS[slotIndex] || 0; // Use 0 for out-of-bounds safety + + totalScore += scoreAwarded; + + // Update DOM + totalScoreDisplay.textContent = totalScore; + lastDropScoreDisplay.textContent = scoreAwarded; + feedbackMessage.textContent = `Scored ${scoreAwarded}! Total: ${totalScore}.`; + + // End the ball's life and re-enable the drop button + ball.active = false; + ball = null; + dropButton.disabled = false; + + // Final draw to remove the ball from the canvas + draw(); + } + + // --- 8. EVENT LISTENERS AND INITIAL SETUP --- + + dropButton.addEventListener('click', dropNewBall); + + resetButton.addEventListener('click', () => { + totalScore = 0; + totalScoreDisplay.textContent = 0; + lastDropScoreDisplay.textContent = '--'; + feedbackMessage.textContent = 'Score reset. Ready to drop!'; + }); + + // Initial setup + createPegs(); + draw(); // Draw the empty board +}); \ No newline at end of file diff --git a/games/plinko_drop/style.css b/games/plinko_drop/style.css new file mode 100644 index 00000000..540cb48b --- /dev/null +++ b/games/plinko_drop/style.css @@ -0,0 +1,84 @@ +body { + font-family: 'Arial', sans-serif; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f0f4f8; + color: #333; +} + +#game-container { + background-color: white; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); + text-align: center; + max-width: 600px; + width: 90%; +} + +h1 { + color: #e67e22; /* Orange Plinko color */ + margin-bottom: 20px; +} + +/* --- Status and Canvas --- */ +#status-area { + font-size: 1.2em; + font-weight: bold; + margin-bottom: 20px; +} + +#plinko-canvas { + border: 3px solid #34495e; + background-color: #ecf0f1; /* Light background for the board */ + margin: 0 auto; + display: block; +} + +/* --- Controls and Feedback --- */ +#controls { + margin-top: 20px; + display: flex; + justify-content: center; + gap: 15px; +} + +#drop-button { + padding: 12px 25px; + font-size: 1.2em; + font-weight: bold; + background-color: #2ecc71; /* Green drop button */ + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +#drop-button:hover:not(:disabled) { + background-color: #27ae60; +} + +#drop-button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +#reset-button { + padding: 12px 25px; + background-color: #95a5a6; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; +} + +#feedback-message { + min-height: 20px; + margin-top: 15px; + font-style: italic; +} \ No newline at end of file diff --git a/games/pong_game/index.html b/games/pong_game/index.html new file mode 100644 index 00000000..42f686b4 --- /dev/null +++ b/games/pong_game/index.html @@ -0,0 +1,21 @@ + + + + + + Classic Pong + + + +
    + +
    +

    PONG

    +

    Use **W** (Up) and **S** (Down) to control the left paddle.

    + +
    +
    + + + + \ No newline at end of file diff --git a/games/pong_game/script.js b/games/pong_game/script.js new file mode 100644 index 00000000..a0c11460 --- /dev/null +++ b/games/pong_game/script.js @@ -0,0 +1,287 @@ +// --- 1. Canvas Setup and Constants --- +const canvas = document.getElementById('pongCanvas'); +const ctx = canvas.getContext('2d'); +const startScreen = document.getElementById('startScreen'); +const startButton = document.getElementById('startButton'); + +// Set Canvas Dimensions +canvas.width = 700; +canvas.height = 500; + +// Game constants +const PADDLE_WIDTH = 10; +const PADDLE_HEIGHT = 80; +const BALL_SIZE = 10; +const MAX_SCORE = 5; + +// --- 2. Game Objects --- +class Paddle { + constructor(x, y) { + this.x = x; + this.y = y; + this.width = PADDLE_WIDTH; + this.height = PADDLE_HEIGHT; + this.color = 'WHITE'; + this.speed = 5; + this.dy = 0; // vertical velocity + this.score = 0; + } + + draw() { + ctx.fillStyle = this.color; + ctx.fillRect(this.x, this.y, this.width, this.height); + } + + update() { + // Simple boundary check + this.y += this.dy; + if (this.y < 0) { + this.y = 0; + } else if (this.y + this.height > canvas.height) { + this.y = canvas.height - this.height; + } + } +} + +class Ball { + constructor(x, y) { + this.x = x; + this.y = y; + this.size = BALL_SIZE; + this.color = 'WHITE'; + this.resetSpeed(); + } + + // Resets ball position and direction + resetSpeed() { + this.speed = 5; + + // Random horizontal direction (+/- 1) + const directionX = Math.random() < 0.5 ? 1 : -1; + + // Random vertical angle (slight, not completely flat) + let angle = Math.random() * (Math.PI / 4) - (Math.PI / 8); // +/- 22.5 degrees + + this.dx = directionX * this.speed * Math.cos(angle); + this.dy = this.speed * Math.sin(angle); + + // Center position + this.x = canvas.width / 2; + this.y = canvas.height / 2; + } + + draw() { + ctx.fillStyle = this.color; + ctx.beginPath(); + ctx.fillRect(this.x, this.y, this.size, this.size); // Drawing as a square for classic Pong look + ctx.closePath(); + } + + update() { + this.x += this.dx; + this.y += this.dy; + } +} + +// Instantiate game objects +const player = new Paddle(10, canvas.height / 2 - PADDLE_HEIGHT / 2); +const computer = new Paddle(canvas.width - PADDLE_WIDTH - 10, canvas.height / 2 - PADDLE_HEIGHT / 2); +const ball = new Ball(canvas.width / 2, canvas.height / 2); + +// --- 3. Game Logic Functions --- + +// Function to draw the middle net and scores +function drawCourt() { + // Draw scores + ctx.fillStyle = 'WHITE'; + ctx.font = '40px Arial'; + ctx.fillText(player.score, canvas.width / 4, 40); + ctx.fillText(computer.score, canvas.width * 3 / 4 - 20, 40); + + // Draw dashed center line (net) + ctx.beginPath(); + ctx.setLineDash([10, 10]); // Dash length, space length + ctx.moveTo(canvas.width / 2, 0); + ctx.lineTo(canvas.width / 2, canvas.height); + ctx.strokeStyle = 'WHITE'; + ctx.stroke(); + ctx.setLineDash([]); // Reset line style +} + +// Handles ball collision with top/bottom walls +function checkWallCollision() { + // Top wall + if (ball.y < 0) { + ball.y = 0; + ball.dy *= -1; + } + // Bottom wall + if (ball.y + ball.size > canvas.height) { + ball.y = canvas.height - ball.size; + ball.dy *= -1; + } +} + +// A simple AABB (Axis-Aligned Bounding Box) collision check +function collides(obj1, obj2) { + return obj1.x < obj2.x + obj2.width && + obj1.x + obj1.size > obj2.x && + obj1.y < obj2.y + obj2.height && + obj1.y + obj1.size > obj2.y; +} + +// Function to handle ball-paddle collision and angle calculation +function checkPaddleCollision(paddle) { + if (collides(ball, paddle)) { + // 1. Reverse horizontal direction + ball.dx *= -1.05; // Increase speed slightly with each hit + + // 2. Calculate bounce angle + // Where did the ball hit on the paddle? (-0.5 top, 0 center, +0.5 bottom) + const relativeIntersectY = (paddle.y + (paddle.height / 2)) - (ball.y + (ball.size / 2)); + const normalizedRelativeIntersection = relativeIntersectY / (paddle.height / 2); + const maxBounceAngle = Math.PI / 3; // 60 degrees + + // Adjust vertical velocity based on hit location + const bounceAngle = normalizedRelativeIntersection * maxBounceAngle; + + ball.dx = Math.cos(bounceAngle) * ball.speed * (paddle === player ? 1 : -1); + ball.dy = Math.sin(bounceAngle) * ball.speed * -1; + + // Ensure ball is outside of paddle to prevent sticky collision + if (paddle === player) { + ball.x = paddle.x + paddle.width; + } else { + ball.x = paddle.x - ball.size; + } + } +} + +// Handles scoring and game reset +function checkScore() { + // Player missed (Computer scores) + if (ball.x < 0) { + computer.score++; + ball.resetSpeed(); + } + // Computer missed (Player scores) + else if (ball.x + ball.size > canvas.width) { + player.score++; + ball.resetSpeed(); + } + + // Check for game winner + if (player.score >= MAX_SCORE || computer.score >= MAX_SCORE) { + endGame(); + } +} + +// Simple AI Logic for the Computer Paddle +function updateAI() { + // Simple logic: if ball is moving towards the computer, try to track it. + if (ball.dx > 0) { + const targetY = ball.y - PADDLE_HEIGHT / 2; + + // Move paddle towards the ball's center position + if (computer.y + computer.height / 2 < ball.y - 15) { + computer.dy = computer.speed * 0.9; // AI is slightly slower + } else if (computer.y + computer.height / 2 > ball.y + 15) { + computer.dy = -computer.speed * 0.9; + } else { + computer.dy = 0; // Stop when aligned + } + } else { + // If ball is moving away, slowly return to the center + const center = canvas.height / 2 - PADDLE_HEIGHT / 2; + if (computer.y < center - 5) { + computer.dy = computer.speed * 0.5; + } else if (computer.y > center + 5) { + computer.dy = -computer.speed * 0.5; + } else { + computer.dy = 0; + } + } + computer.update(); +} + +// --- 4. Game Loop and Control --- + +let gameRunning = false; + +function loop() { + if (!gameRunning) return; + + // 1. CLEAR THE CANVAS (Essential for animation) + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = 'black'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // 2. UPDATE POSITIONS + player.update(); + updateAI(); // Update computer paddle position + ball.update(); + + // 3. HANDLE COLLISIONS & SCORE + checkWallCollision(); + checkPaddleCollision(player); + checkPaddleCollision(computer); + checkScore(); + + // 4. DRAW EVERYTHING + drawCourt(); + player.draw(); + computer.draw(); + ball.draw(); + + // Request the next frame to continue the loop + requestAnimationFrame(loop); +} + +function endGame() { + gameRunning = false; + startScreen.classList.remove('hidden'); + startButton.textContent = player.score >= MAX_SCORE ? + 'YOU WIN! Click to Play Again' : + 'GAME OVER! Click to Play Again'; +} + +function startGame() { + // Reset scores + player.score = 0; + computer.score = 0; + + // Reset paddle positions + player.y = canvas.height / 2 - PADDLE_HEIGHT / 2; + computer.y = canvas.height / 2 - PADDLE_HEIGHT / 2; + + // Reset ball + ball.resetSpeed(); + + // Start game + startScreen.classList.add('hidden'); + gameRunning = true; + loop(); // Start the game loop +} + +// --- 5. Event Listeners (Player Controls) --- +document.addEventListener('keydown', (e) => { + if (e.key === 'w' || e.key === 'W') { + player.dy = -player.speed; + } else if (e.key === 's' || e.key === 'S') { + player.dy = player.speed; + } +}); + +document.addEventListener('keyup', (e) => { + if ((e.key === 'w' || e.key === 'W') && player.dy < 0) { + player.dy = 0; + } else if ((e.key === 's' || e.key === 'S') && player.dy > 0) { + player.dy = 0; + } +}); + +// Start button listener +startButton.addEventListener('click', startGame); + +// Draw the initial screen before the game starts +drawCourt(); \ No newline at end of file diff --git a/games/pong_game/style.css b/games/pong_game/style.css new file mode 100644 index 00000000..aaf8e065 --- /dev/null +++ b/games/pong_game/style.css @@ -0,0 +1,69 @@ +body { + font-family: Arial, sans-serif; + background-color: #222; /* Dark background */ + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + overflow: hidden; /* Prevent scroll bars */ +} + +.game-container { + position: relative; + box-shadow: 0 0 20px rgba(0, 255, 0, 0.5); /* Green glow effect */ +} + +#pongCanvas { + /* Dimensions will be set in JavaScript, but we define the basic look here */ + background-color: black; + border: 2px solid white; + display: block; /* Removes any default canvas spacing */ +} + +/* --- Start/Overlay Screen Styling --- */ +.overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + color: white; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + z-index: 10; +} + +.overlay h1 { + font-size: 4em; + margin-bottom: 0.5em; + color: #4CAF50; /* Green highlight for the title */ +} + +.overlay p { + margin-bottom: 1.5em; + font-size: 1.2em; +} + +#startButton { + padding: 10px 20px; + font-size: 1.5em; + cursor: pointer; + background-color: #4CAF50; + color: white; + border: none; + border-radius: 5px; + transition: background-color 0.3s; +} + +#startButton:hover { + background-color: #45a049; +} + +.hidden { + display: none; +} \ No newline at end of file diff --git a/games/portal-logic/index.html b/games/portal-logic/index.html new file mode 100644 index 00000000..5b68fa5c --- /dev/null +++ b/games/portal-logic/index.html @@ -0,0 +1,27 @@ + + + + + + Portal Logic | Mini JS Games Hub + + + +
    +

    ๐ŸŒ€ Portal Logic

    + +
    + + + +
    +

    Use arrow keys or WASD to move. Reach the glowing goal!

    +
    + + + + + + + + diff --git a/games/portal-logic/script.js b/games/portal-logic/script.js new file mode 100644 index 00000000..275fe4c7 --- /dev/null +++ b/games/portal-logic/script.js @@ -0,0 +1,136 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +const tileSize = 60; +const rows = 8; +const cols = 8; + +const player = { x: 0, y: 0, color: "cyan" }; +let gameRunning = false; + +const portalSound = document.getElementById("portalSound"); +const moveSound = document.getElementById("moveSound"); +const winSound = document.getElementById("winSound"); + +const goal = { x: 7, y: 7 }; +const obstacles = [ + { x: 3, y: 3 }, + { x: 4, y: 3 }, + { x: 2, y: 5 }, +]; + +const portals = [ + { x: 1, y: 1, link: { x: 6, y: 2 }, color: "violet" }, + { x: 6, y: 2, link: { x: 1, y: 1 }, color: "violet" }, + { x: 5, y: 5, link: { x: 2, y: 6 }, color: "lime" }, + { x: 2, y: 6, link: { x: 5, y: 5 }, color: "lime" }, +]; + +function drawGrid() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + ctx.strokeStyle = "#003"; + ctx.strokeRect(c * tileSize, r * tileSize, tileSize, tileSize); + } + } + + // Obstacles + obstacles.forEach(o => { + ctx.fillStyle = "red"; + ctx.shadowColor = "red"; + ctx.shadowBlur = 15; + ctx.fillRect(o.x * tileSize + 10, o.y * tileSize + 10, tileSize - 20, tileSize - 20); + }); + + // Portals + portals.forEach(p => { + ctx.fillStyle = p.color; + ctx.beginPath(); + ctx.arc(p.x * tileSize + tileSize / 2, p.y * tileSize + tileSize / 2, 18, 0, Math.PI * 2); + ctx.fill(); + ctx.shadowColor = p.color; + ctx.shadowBlur = 25; + }); + + // Goal + ctx.fillStyle = "gold"; + ctx.shadowColor = "yellow"; + ctx.shadowBlur = 25; + ctx.beginPath(); + ctx.arc(goal.x * tileSize + tileSize / 2, goal.y * tileSize + tileSize / 2, 15, 0, Math.PI * 2); + ctx.fill(); + + // Player + ctx.fillStyle = player.color; + ctx.shadowColor = "cyan"; + ctx.shadowBlur = 20; + ctx.beginPath(); + ctx.arc(player.x * tileSize + tileSize / 2, player.y * tileSize + tileSize / 2, 12, 0, Math.PI * 2); + ctx.fill(); +} + +function movePlayer(dx, dy) { + if (!gameRunning) return; + + const newX = player.x + dx; + const newY = player.y + dy; + + // Out of bounds + if (newX < 0 || newX >= cols || newY < 0 || newY >= rows) return; + + // Collision with obstacle + if (obstacles.some(o => o.x === newX && o.y === newY)) return; + + player.x = newX; + player.y = newY; + moveSound.play(); + + // Check for portal + const portal = portals.find(p => p.x === player.x && p.y === player.y); + if (portal) { + portalSound.play(); + player.x = portal.link.x; + player.y = portal.link.y; + } + + // Check goal + if (player.x === goal.x && player.y === goal.y) { + winSound.play(); + gameRunning = false; + alert("๐ŸŽ‰ You reached the goal!"); + } + + drawGrid(); +} + +document.addEventListener("keydown", (e) => { + switch (e.key) { + case "ArrowUp": + case "w": movePlayer(0, -1); break; + case "ArrowDown": + case "s": movePlayer(0, 1); break; + case "ArrowLeft": + case "a": movePlayer(-1, 0); break; + case "ArrowRight": + case "d": movePlayer(1, 0); break; + } +}); + +document.getElementById("startBtn").addEventListener("click", () => { + gameRunning = true; +}); + +document.getElementById("pauseBtn").addEventListener("click", () => { + gameRunning = false; +}); + +document.getElementById("restartBtn").addEventListener("click", () => { + player.x = 0; + player.y = 0; + gameRunning = true; + drawGrid(); +}); + +drawGrid(); diff --git a/games/portal-logic/style.css b/games/portal-logic/style.css new file mode 100644 index 00000000..06451e08 --- /dev/null +++ b/games/portal-logic/style.css @@ -0,0 +1,54 @@ +body { + background: radial-gradient(circle at top, #0a0f1f, #000); + color: #fff; + font-family: "Poppins", sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + flex-direction: column; +} + +h1 { + text-shadow: 0 0 15px cyan, 0 0 25px blue; +} + +.game-container { + text-align: center; +} + +canvas { + border: 3px solid #00ffff; + border-radius: 8px; + box-shadow: 0 0 30px cyan, inset 0 0 20px #0077ff; + background: linear-gradient(180deg, #050d1a, #0b162a); +} + +.controls { + margin-top: 10px; +} + +button { + margin: 6px; + padding: 10px 16px; + font-size: 16px; + border: none; + border-radius: 8px; + cursor: pointer; + color: #fff; + background: linear-gradient(90deg, #00ffff, #0077ff); + box-shadow: 0 0 15px #00ffff; + transition: 0.2s; +} + +button:hover { + transform: scale(1.1); + box-shadow: 0 0 25px #00ffff, 0 0 40px #0077ff; +} + +.info { + margin-top: 10px; + font-size: 14px; + color: #aeeaff; +} diff --git a/games/potato_timer/index.html b/games/potato_timer/index.html new file mode 100644 index 00000000..c3fbc1d0 --- /dev/null +++ b/games/potato_timer/index.html @@ -0,0 +1,26 @@ + + + + + + Hot Potato Timer + + + + +
    +

    ๐Ÿฅ” Hot Potato Timer ๐Ÿ’ฃ

    + +
    +

    Press **START** to begin the game!

    +
    + +
    + + +
    +
    + + + + \ No newline at end of file diff --git a/games/potato_timer/script.js b/games/potato_timer/script.js new file mode 100644 index 00000000..cfd99328 --- /dev/null +++ b/games/potato_timer/script.js @@ -0,0 +1,89 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. DOM Elements --- + const startButton = document.getElementById('start-button'); + const passButton = document.getElementById('pass-button'); + const messageDisplay = document.getElementById('message'); + const statusDisplay = document.getElementById('status-display'); + + // --- 2. Game Variables --- + let timerId = null; // Will hold the ID returned by setTimeout + const MIN_TIME_MS = 3000; // Minimum countdown time (3 seconds) + const MAX_TIME_MS = 8000; // Maximum countdown time (8 seconds) + + // --- 3. Functions --- + + /** + * Generates a random integer time (in milliseconds) within the defined range. + * @returns {number} The random time in milliseconds. + */ + function getRandomTime() { + // Formula: Math.random() * (max - min) + min + return Math.floor(Math.random() * (MAX_TIME_MS - MIN_TIME_MS + 1)) + MIN_TIME_MS; + } + + /** + * Sets the game state to 'playing' and starts the random countdown. + */ + function startGame() { + // Reset and prepare UI + messageDisplay.textContent = '๐Ÿ”ฅ The potato is HOT! PASS it quick!'; + statusDisplay.classList.add('hot'); + startButton.disabled = true; + passButton.disabled = false; + + // 1. Get a random time limit + const randomTime = getRandomTime(); + console.log(`New countdown set for ${randomTime / 1000} seconds.`); + + // 2. Start the timer (the "explosion" countdown) + timerId = setTimeout(() => { + handleExplosion(); + }, randomTime); + } + + /** + * Executes when the timer runs out. The player loses. + */ + function handleExplosion() { + // Clear the timer (though it has already executed, this is good practice) + clearTimeout(timerId); + + // Update UI for explosion state + messageDisplay.textContent = '๐Ÿ’ฅ BOOM! The potato exploded! You were too slow!'; + messageDisplay.style.color = '#ff4500'; // Set text color to red/orange + statusDisplay.classList.remove('hot'); + statusDisplay.style.backgroundColor = '#4a0000'; // Make background darker + + // Reset controls + startButton.textContent = 'PLAY AGAIN'; + startButton.disabled = false; + passButton.disabled = true; + } + + /** + * Executes when the "Pass" button is clicked. The player survives this round. + */ + function handlePass() { + if (!timerId) return; // Ignore clicks if the game isn't running + + // 1. Stop the current timer (successfully passed) + clearTimeout(timerId); + timerId = null; + + // 2. Update UI for successful pass + messageDisplay.textContent = 'โœ… Phew! You passed the potato!'; + messageDisplay.style.color = '#4CAF50'; // Set text color to green + statusDisplay.classList.remove('hot'); + statusDisplay.style.backgroundColor = '#1a1a1a'; + + // 3. Reset controls and prepare for next round + startButton.textContent = 'START NEXT ROUND'; + startButton.disabled = false; + passButton.disabled = true; + } + + // --- 4. Event Listeners --- + + startButton.addEventListener('click', startGame); + passButton.addEventListener('click', handlePass); +}); \ No newline at end of file diff --git a/games/potato_timer/style.css b/games/potato_timer/style.css new file mode 100644 index 00000000..8229356a --- /dev/null +++ b/games/potato_timer/style.css @@ -0,0 +1,100 @@ +body { + font-family: 'Arial', sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #333; /* Dark background */ + color: white; +} + +#game-container { + background-color: #222; + padding: 40px; + border-radius: 15px; + box-shadow: 0 0 20px rgba(255, 69, 0, 0.5); /* Orange glow */ + text-align: center; + width: 90%; + max-width: 450px; +} + +h1 { + color: #ff4500; /* Orange-red for 'hot' */ + margin-bottom: 20px; +} + +/* --- Status Display --- */ +#status-display { + min-height: 100px; + display: flex; + align-items: center; + justify-content: center; + margin: 30px 0; + padding: 15px; + border: 3px dashed #888; + border-radius: 10px; + font-size: 1.5em; + font-weight: bold; + background-color: #1a1a1a; + transition: background-color 0.5s; +} + +#message { + margin: 0; +} + +/* State when the potato is "hot" */ +.hot { + border-color: #ffd700 !important; /* Gold */ + background-color: #4a0000; + animation: pulse 1s infinite alternate; +} + +@keyframes pulse { + from { box-shadow: 0 0 10px rgba(255, 69, 0, 0.5); } + to { box-shadow: 0 0 25px rgba(255, 69, 0, 0.8), 0 0 5px rgba(255, 255, 0, 0.5); } +} + +/* --- Controls --- */ +#controls { + display: flex; + justify-content: space-around; + gap: 15px; +} + +.action-button { + padding: 15px 25px; + font-size: 1.2em; + font-weight: bold; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s, transform 0.1s; + flex-grow: 1; +} + +#start-button { + background-color: #4CAF50; /* Green */ + color: white; +} + +#start-button:hover { + background-color: #43a047; +} + +#pass-button { + background-color: #00bcd4; /* Cyan/Blue */ + color: white; +} + +#pass-button:hover:not(:disabled) { + background-color: #00acc1; + transform: scale(1.05); +} + +.action-button:disabled { + background-color: #555; + cursor: not-allowed; + opacity: 0.6; +} \ No newline at end of file diff --git a/games/potion-mixer/index.html b/games/potion-mixer/index.html new file mode 100644 index 00000000..a43d5448 --- /dev/null +++ b/games/potion-mixer/index.html @@ -0,0 +1,112 @@ + + + + + + Potion Mixer - Magical Alchemy Game + + + +
    +
    +

    ๐Ÿงช Potion Mixer

    +
    +
    Level: 1
    +
    Score: 0
    +
    Potions: 0
    +
    +
    + +
    +
    +

    Ingredients

    +
    + +
    +
    + +
    + + +
    +
    +
    +
    +
    + +
    +
    + +
    +

    Effects

    +
    + +
    +
    +

    Recipe Book

    +
    + +
    +
    +
    +
    + +
    + + + +
    +
    + + +
    +
    +

    Welcome to Potion Mixer!

    +
    +

    How to Play:

    +
      +
    • Drag ingredients from the panel into the cauldron
    • +
    • Mix potions by clicking the "Mix Potion" button
    • +
    • Discover magical effects by experimenting with combinations
    • +
    • Complete recipes to earn points and unlock new ingredients
    • +
    • Watch out for dangerous combinations!
    • +
    +

    Ingredient Types:

    +
      +
    • ๐ŸŒฟ Herbs - Natural magical plants
    • +
    • ๐Ÿ’Ž Crystals - Magical gemstones
    • +
    • โœจ Essences - Pure magical energy
    • +
    • ๐Ÿง‚ Powders - Ground magical substances
    • +
    +
    + +
    +
    + + +
    +
    +

    Level Complete!

    +
    +
    + +
    +
    + + +
    +
    +

    Hint

    +

    + +
    +
    + + +
    +
    ๐Ÿ’ฅ BOOM!
    +
    + + + + \ No newline at end of file diff --git a/games/potion-mixer/script.js b/games/potion-mixer/script.js new file mode 100644 index 00000000..a4b45abc --- /dev/null +++ b/games/potion-mixer/script.js @@ -0,0 +1,558 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const startButton = document.getElementById('start-button'); +const mixButton = document.getElementById('mix-button'); +const resetButton = document.getElementById('reset-button'); +const hintButton = document.getElementById('hint-button'); +const nextLevelButton = document.getElementById('next-level-button'); +const nextLevelButtonOverlay = document.getElementById('next-level-button-overlay'); +const closeHintButton = document.getElementById('close-hint-button'); +const instructionsOverlay = document.getElementById('instructions-overlay'); +const levelCompleteOverlay = document.getElementById('level-complete-overlay'); +const hintOverlay = document.getElementById('hint-overlay'); +const explosionOverlay = document.getElementById('explosion-overlay'); +const ingredientsContainer = document.getElementById('ingredients-container'); +const effectsDisplay = document.getElementById('effects-display'); +const recipeList = document.getElementById('recipe-list'); +const cauldronLiquid = document.getElementById('cauldron-liquid'); +const bubbles = document.getElementById('bubbles'); + +canvas.width = 600; +canvas.height = 400; + +let gameRunning = false; +let currentLevel = 1; +let score = 0; +let potionsCreated = 0; +let draggedIngredient = null; +let ingredients = []; +let cauldronContents = []; +let discoveredRecipes = new Set(); +let currentEffects = []; +let dragOffset = { x: 0, y: 0 }; + +// Ingredient definitions +const ingredientTypes = { + herb: { emoji: '๐ŸŒฟ', color: '#32cd32', name: 'Herb' }, + crystal: { emoji: '๐Ÿ’Ž', color: '#9370db', name: 'Crystal' }, + essence: { emoji: 'โœจ', color: '#ffd700', name: 'Essence' }, + powder: { emoji: '๐Ÿง‚', color: '#cd853f', name: 'Powder' } +}; + +const allIngredients = [ + { id: 'mandrake', name: 'Mandrake Root', type: 'herb', rarity: 'common' }, + { id: 'wolfsbane', name: 'Wolfsbane', type: 'herb', rarity: 'uncommon' }, + { id: 'nightshade', name: 'Nightshade', type: 'herb', rarity: 'rare' }, + { id: 'dragonscale', name: 'Dragon Scale', type: 'herb', rarity: 'legendary' }, + + { id: 'amethyst', name: 'Amethyst', type: 'crystal', rarity: 'common' }, + { id: 'sapphire', name: 'Sapphire', type: 'crystal', rarity: 'uncommon' }, + { id: 'ruby', name: 'Ruby', type: 'crystal', rarity: 'rare' }, + { id: 'diamond', name: 'Diamond', type: 'crystal', rarity: 'legendary' }, + + { id: 'fire', name: 'Fire Essence', type: 'essence', rarity: 'common' }, + { id: 'water', name: 'Water Essence', type: 'essence', rarity: 'uncommon' }, + { id: 'air', name: 'Air Essence', type: 'essence', rarity: 'rare' }, + { id: 'earth', name: 'Earth Essence', type: 'essence', rarity: 'legendary' }, + + { id: 'salt', name: 'Sea Salt', type: 'powder', rarity: 'common' }, + { id: 'sulfur', name: 'Sulfur', type: 'powder', rarity: 'uncommon' }, + { id: 'mercury', name: 'Quicksilver', type: 'powder', rarity: 'rare' }, + { id: 'phoenix', name: 'Phoenix Ash', type: 'powder', rarity: 'legendary' } +]; + +// Recipe definitions +const recipes = { + // Basic potions + 'healing': { + ingredients: ['mandrake', 'amethyst', 'fire'], + effects: ['Healing', 'Restoration'], + color: '#ff6b6b', + description: 'Restores health and vitality' + }, + 'strength': { + ingredients: ['dragonscale', 'ruby', 'earth'], + effects: ['Strength', 'Power'], + color: '#4ecdc4', + description: 'Grants immense physical strength' + }, + 'invisibility': { + ingredients: ['nightshade', 'sapphire', 'air'], + effects: ['Invisibility', 'Stealth'], + color: '#45b7d1', + description: 'Makes the drinker invisible' + }, + 'wisdom': { + ingredients: ['wolfsbane', 'diamond', 'water'], + effects: ['Wisdom', 'Knowledge'], + color: '#f9ca24', + description: 'Enhances intelligence and wisdom' + }, + + // Advanced potions + 'fire_breath': { + ingredients: ['dragonscale', 'fire', 'sulfur'], + effects: ['Fire Breath', 'Immunity'], + color: '#e17055', + description: 'Allows breathing fire and heat resistance' + }, + 'teleportation': { + ingredients: ['phoenix', 'air', 'diamond'], + effects: ['Teleportation', 'Speed'], + color: '#a29bfe', + description: 'Enables short-range teleportation' + }, + 'transformation': { + ingredients: ['wolfsbane', 'mercury', 'earth'], + effects: ['Transformation', 'Shape-shifting'], + color: '#fd79a8', + description: 'Allows changing physical form' + }, + + // Dangerous combinations + 'explosion': { + ingredients: ['sulfur', 'fire', 'salt'], + effects: ['Explosion', 'Destruction'], + color: '#ff0000', + description: 'Creates a massive explosion', + dangerous: true + }, + 'poison': { + ingredients: ['nightshade', 'mercury', 'wolfsbane'], + effects: ['Poison', 'Death'], + color: '#6c5ce7', + description: 'Highly toxic poison', + dangerous: true + } +}; + +// Level configurations +const levels = [ + { + availableIngredients: ['mandrake', 'amethyst', 'fire', 'salt'], + targetRecipes: ['healing'], + hint: "Try mixing Mandrake Root, Amethyst, and Fire Essence for a basic healing potion!" + }, + { + availableIngredients: ['mandrake', 'amethyst', 'fire', 'wolfsbane', 'sapphire', 'air'], + targetRecipes: ['healing', 'invisibility'], + hint: "Nightshade and Sapphire with Air Essence might reveal hidden potential..." + }, + { + availableIngredients: ['mandrake', 'amethyst', 'fire', 'wolfsbane', 'sapphire', 'air', 'dragonscale', 'ruby', 'earth'], + targetRecipes: ['healing', 'invisibility', 'strength'], + hint: "Dragon Scale with Ruby and Earth Essence could grant incredible power!" + }, + { + availableIngredients: ['mandrake', 'amethyst', 'fire', 'wolfsbane', 'sapphire', 'air', 'dragonscale', 'ruby', 'earth', 'diamond', 'water'], + targetRecipes: ['healing', 'invisibility', 'strength', 'wisdom'], + hint: "Diamond and Water Essence with Wolfsbane might unlock ancient knowledge..." + }, + { + availableIngredients: ['mandrake', 'amethyst', 'fire', 'wolfsbane', 'sapphire', 'air', 'dragonscale', 'ruby', 'earth', 'diamond', 'water', 'sulfur', 'phoenix'], + targetRecipes: ['healing', 'invisibility', 'strength', 'wisdom', 'fire_breath', 'teleportation'], + hint: "Experiment with Phoenix Ash and Sulfur for some explosive results!" + } +]; + +// Initialize game +function initGame() { + ingredients = []; + cauldronContents = []; + currentEffects = []; + updateUI(); + createIngredients(); + updateRecipeBook(); +} + +// Create ingredient elements +function createIngredients() { + ingredientsContainer.innerHTML = ''; + const levelIngredients = levels[Math.min(currentLevel - 1, levels.length - 1)].availableIngredients; + + levelIngredients.forEach(ingredientId => { + const ingredient = allIngredients.find(i => i.id === ingredientId); + if (ingredient) { + const element = document.createElement('div'); + element.className = `ingredient ${ingredient.type}`; + element.textContent = `${ingredientTypes[ingredient.type].emoji} ${ingredient.name}`; + element.dataset.id = ingredient.id; + element.draggable = true; + + // Add drag event listeners + element.addEventListener('dragstart', handleDragStart); + element.addEventListener('dragend', handleDragEnd); + + ingredientsContainer.appendChild(element); + ingredients.push({ element, data: ingredient }); + } + }); +} + +// Drag and drop handlers +function handleDragStart(e) { + draggedIngredient = e.target; + draggedIngredient.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'copy'; + + // Calculate offset for smooth dragging + const rect = draggedIngredient.getBoundingClientRect(); + dragOffset.x = e.clientX - rect.left; + dragOffset.y = e.clientY - rect.top; +} + +function handleDragEnd(e) { + if (draggedIngredient) { + draggedIngredient.classList.remove('dragging'); + draggedIngredient = null; + } +} + +// Cauldron drop zone +document.addEventListener('dragover', (e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; +}); + +document.addEventListener('drop', (e) => { + e.preventDefault(); + + if (draggedIngredient) { + const ingredientId = draggedIngredient.dataset.id; + addToCauldron(ingredientId); + draggedIngredient.style.opacity = '0.5'; + setTimeout(() => { + draggedIngredient.style.opacity = '1'; + }, 200); + } +}); + +// Add ingredient to cauldron +function addToCauldron(ingredientId) { + const ingredient = allIngredients.find(i => i.id === ingredientId); + if (ingredient && cauldronContents.length < 5) { + cauldronContents.push(ingredient); + updateCauldronVisual(); + updateEffects(); + } +} + +// Update cauldron visual +function updateCauldronVisual() { + const fillPercent = (cauldronContents.length / 5) * 100; + cauldronLiquid.style.height = `${fillPercent}%`; + + // Update liquid color based on ingredients + if (cauldronContents.length > 0) { + const colors = cauldronContents.map(ing => ingredientTypes[ing.type].color); + const avgColor = blendColors(colors); + cauldronLiquid.style.background = `linear-gradient(180deg, ${avgColor}, ${darkenColor(avgColor)})`; + } + + // Show bubbles when mixing + if (cauldronContents.length > 0) { + bubbles.style.display = 'block'; + createBubbles(); + } else { + bubbles.style.display = 'none'; + } +} + +// Create bubble animation +function createBubbles() { + bubbles.innerHTML = ''; + for (let i = 0; i < 4; i++) { + const bubble = document.createElement('div'); + bubble.className = 'bubble'; + bubble.style.left = `${Math.random() * 100}px`; + bubble.style.animationDelay = `${Math.random() * 2}s`; + bubbles.appendChild(bubble); + } +} + +// Blend colors for potion liquid +function blendColors(colors) { + if (colors.length === 1) return colors[0]; + + let r = 0, g = 0, b = 0; + colors.forEach(color => { + const rgb = hexToRgb(color); + r += rgb.r; + g += rgb.g; + b += rgb.b; + }); + + r = Math.round(r / colors.length); + g = Math.round(g / colors.length); + b = Math.round(b / colors.length); + + return `rgb(${r}, ${g}, ${b})`; +} + +function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +} + +function darkenColor(color) { + const rgb = hexToRgb(color); + return `rgb(${Math.max(0, rgb.r - 50)}, ${Math.max(0, rgb.g - 50)}, ${Math.max(0, rgb.b - 50)})`; +} + +// Update effects display +function updateEffects() { + effectsDisplay.innerHTML = ''; + + if (cauldronContents.length === 0) { + effectsDisplay.innerHTML = '

    Add ingredients to see effects...

    '; + return; + } + + // Show current ingredients + const ingredientsList = document.createElement('div'); + ingredientsList.innerHTML = '

    Current Ingredients:

    '; + cauldronContents.forEach(ing => { + const item = document.createElement('div'); + item.className = 'effect-item'; + item.textContent = `${ingredientTypes[ing.type].emoji} ${ing.name}`; + ingredientsList.appendChild(item); + }); + effectsDisplay.appendChild(ingredientsList); + + // Show potential effects + if (cauldronContents.length >= 2) { + const potentialEffects = document.createElement('div'); + potentialEffects.innerHTML = '

    Potential Effects:

    '; + + // Check for partial matches + Object.entries(recipes).forEach(([recipeId, recipe]) => { + const matchCount = recipe.ingredients.filter(ing => + cauldronContents.some(cauldronIng => cauldronIng.id === ing) + ).length; + + if (matchCount > 0 && matchCount < recipe.ingredients.length) { + const item = document.createElement('div'); + item.className = 'effect-item'; + item.innerHTML = `${matchCount}/${recipe.ingredients.length} ingredients for: ${recipe.effects.join(', ')}`; + if (recipe.dangerous) { + item.style.borderLeftColor = '#ff0000'; + item.innerHTML += ' โš ๏ธ DANGEROUS'; + } + potentialEffects.appendChild(item); + } + }); + + if (potentialEffects.children.length > 1) { + effectsDisplay.appendChild(potentialEffects); + } + } +} + +// Mix potion +function mixPotion() { + if (cauldronContents.length === 0) return; + + // Check for exact recipe matches + let matchedRecipe = null; + let bestMatch = 0; + + Object.entries(recipes).forEach(([recipeId, recipe]) => { + const matchCount = recipe.ingredients.filter(ing => + cauldronContents.some(cauldronIng => cauldronIng.id === ing) + ).length; + + if (matchCount === recipe.ingredients.length && matchCount === cauldronContents.length) { + matchedRecipe = { id: recipeId, ...recipe }; + } else if (matchCount > bestMatch) { + bestMatch = matchCount; + } + }); + + if (matchedRecipe) { + // Perfect match! + discoveredRecipes.add(matchedRecipe.id); + score += matchedRecipe.dangerous ? 50 : 100; + potionsCreated++; + + showPotionResult(matchedRecipe); + updateRecipeBook(); + + // Check level completion + checkLevelComplete(); + } else if (cauldronContents.length >= 3) { + // Partial or experimental result + const experimentalEffects = generateExperimentalEffects(); + score += 25; + showPotionResult({ + effects: experimentalEffects, + color: '#888888', + description: 'Experimental mixture - unpredictable results!', + experimental: true + }); + } else { + // Failed mixture + showPotionResult({ + effects: ['Failed Mixture'], + color: '#666666', + description: 'Nothing interesting happened...', + failed: true + }); + } + + // Clear cauldron + cauldronContents = []; + updateCauldronVisual(); + updateEffects(); + updateUI(); +} + +// Generate experimental effects +function generateExperimentalEffects() { + const effects = [ + 'Strange Glow', 'Unusual Odor', 'Mild Sparkles', 'Gentle Humming', + 'Color Change', 'Temperature Shift', 'Light Vibration', 'Subtle Mist' + ]; + + const numEffects = Math.min(cauldronContents.length, 3); + const shuffled = effects.sort(() => 0.5 - Math.random()); + return shuffled.slice(0, numEffects); +} + +// Show potion result +function showPotionResult(result) { + const overlay = document.createElement('div'); + overlay.className = 'overlay'; + overlay.innerHTML = ` +
    +

    ${result.dangerous ? 'โš ๏ธ DANGEROUS' : 'โœจ'} Potion Created!

    +
    +
    ๐Ÿงช
    +

    ${result.effects.join(', ')}

    +

    ${result.description}

    + ${result.experimental ? '

    +25 Experimental Points!

    ' : ''} + ${result.failed ? '

    Try different combinations...

    ' : ''} +
    + +
    + `; + + if (result.dangerous) { + // Show explosion effect + explosionOverlay.style.display = 'flex'; + setTimeout(() => { + explosionOverlay.style.display = 'none'; + }, 500); + } + + document.body.appendChild(overlay); +} + +// Update recipe book +function updateRecipeBook() { + recipeList.innerHTML = ''; + + Object.entries(recipes).forEach(([recipeId, recipe]) => { + const item = document.createElement('div'); + item.className = `recipe-item ${discoveredRecipes.has(recipeId) ? 'discovered' : ''}`; + + if (discoveredRecipes.has(recipeId)) { + item.innerHTML = `${recipe.effects.join(', ')}
    ${recipe.description}`; + } else { + item.innerHTML = `Unknown Recipe
    ??? (${recipe.ingredients.length} ingredients)`; + } + + recipeList.appendChild(item); + }); +} + +// Check level completion +function checkLevelComplete() { + const level = levels[Math.min(currentLevel - 1, levels.length - 1)]; + const discoveredTargets = level.targetRecipes.filter(recipe => discoveredRecipes.has(recipe)); + + if (discoveredTargets.length === level.targetRecipes.length) { + // Level complete! + setTimeout(() => { + showLevelComplete(); + }, 1000); + } +} + +// Show level complete +function showLevelComplete() { + const level = levels[Math.min(currentLevel - 1, levels.length - 1)]; + const discoveredCount = level.targetRecipes.filter(recipe => discoveredRecipes.has(recipe)).length; + + document.getElementById('level-stats').innerHTML = ` +

    Level ${currentLevel} Complete!

    +

    Recipes Discovered: ${discoveredCount}/${level.targetRecipes.length}

    +

    Total Score: ${score}

    +

    Potions Created: ${potionsCreated}

    + `; + + const newRecipes = level.targetRecipes.filter(recipe => discoveredRecipes.has(recipe)); + if (newRecipes.length > 0) { + document.getElementById('new-recipes').innerHTML = ` +

    New Recipes Unlocked:

    +
      + ${newRecipes.map(recipeId => `
    • ${recipes[recipeId].effects.join(', ')}
    • `).join('')} +
    + `; + } + + levelCompleteOverlay.style.display = 'flex'; +} + +// Update UI +function updateUI() { + document.getElementById('level').textContent = currentLevel; + document.getElementById('score').textContent = score; + document.getElementById('potions').textContent = potionsCreated; +} + +// Event listeners +startButton.addEventListener('click', () => { + instructionsOverlay.style.display = 'none'; + gameRunning = true; + initGame(); +}); + +mixButton.addEventListener('click', mixPotion); + +resetButton.addEventListener('click', () => { + cauldronContents = []; + updateCauldronVisual(); + updateEffects(); +}); + +hintButton.addEventListener('click', () => { + const level = levels[Math.min(currentLevel - 1, levels.length - 1)]; + document.getElementById('hint-text').textContent = level.hint; + hintOverlay.style.display = 'flex'; +}); + +closeHintButton.addEventListener('click', () => { + hintOverlay.style.display = 'none'; +}); + +nextLevelButton.addEventListener('click', () => { + if (currentLevel < levels.length) { + currentLevel++; + initGame(); + } +}); + +nextLevelButtonOverlay.addEventListener('click', () => { + levelCompleteOverlay.style.display = 'none'; + if (currentLevel < levels.length) { + currentLevel++; + initGame(); + } +}); + +// Initialize +updateRecipeBook(); \ No newline at end of file diff --git a/games/potion-mixer/style.css b/games/potion-mixer/style.css new file mode 100644 index 00000000..d6f8cf41 --- /dev/null +++ b/games/potion-mixer/style.css @@ -0,0 +1,417 @@ +/* Potion Mixer Game Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #1a1a2e, #16213e, #0f3460); + color: #e8e8e8; + min-height: 100vh; + overflow-x: hidden; +} + +.game-container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 20px; +} + +h1 { + font-size: 2.5em; + color: #ffd700; + text-shadow: 0 0 20px rgba(255, 215, 0, 0.5); + margin-bottom: 10px; +} + +.game-stats { + display: flex; + justify-content: center; + gap: 30px; + margin-bottom: 20px; +} + +.stat { + background: rgba(255, 255, 255, 0.1); + padding: 10px 20px; + border-radius: 20px; + border: 1px solid rgba(255, 215, 0, 0.3); + font-weight: bold; +} + +.game-area { + display: grid; + grid-template-columns: 250px 1fr 250px; + gap: 20px; + min-height: 600px; +} + +.ingredients-panel, .effects-panel { + background: rgba(255, 255, 255, 0.1); + border-radius: 15px; + padding: 20px; + border: 2px solid rgba(255, 215, 0, 0.3); + backdrop-filter: blur(10px); +} + +.ingredients-panel h3, .effects-panel h3 { + color: #ffd700; + margin-bottom: 15px; + text-align: center; + font-size: 1.2em; +} + +.ingredients-container { + display: grid; + grid-template-columns: 1fr; + gap: 10px; + max-height: 500px; + overflow-y: auto; +} + +.ingredient { + background: rgba(255, 255, 255, 0.1); + border: 2px solid rgba(255, 215, 0, 0.3); + border-radius: 10px; + padding: 10px; + cursor: grab; + transition: all 0.3s ease; + text-align: center; + font-weight: bold; + position: relative; +} + +.ingredient:hover { + transform: scale(1.05); + box-shadow: 0 5px 15px rgba(255, 215, 0, 0.3); +} + +.ingredient.dragging { + opacity: 0.5; + transform: rotate(5deg); + cursor: grabbing; +} + +.ingredient.herb { + border-color: #32cd32; + background: linear-gradient(45deg, rgba(50, 205, 50, 0.2), rgba(34, 139, 34, 0.2)); +} + +.ingredient.crystal { + border-color: #9370db; + background: linear-gradient(45deg, rgba(147, 112, 219, 0.2), rgba(106, 90, 205, 0.2)); +} + +.ingredient.essence { + border-color: #ffd700; + background: linear-gradient(45deg, rgba(255, 215, 0, 0.2), rgba(255, 165, 0, 0.2)); +} + +.ingredient.powder { + border-color: #cd853f; + background: linear-gradient(45deg, rgba(205, 133, 63, 0.2), rgba(160, 82, 45, 0.2)); +} + +.main-game { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; +} + +#gameCanvas { + border: 3px solid rgba(255, 215, 0, 0.5); + border-radius: 15px; + background: rgba(0, 0, 0, 0.3); + box-shadow: 0 0 30px rgba(255, 215, 0, 0.2); +} + +.cauldron-area { + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; +} + +.cauldron { + width: 150px; + height: 120px; + background: linear-gradient(45deg, #2c2c2c, #1a1a1a); + border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%; + position: relative; + border: 4px solid #8b4513; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + overflow: hidden; +} + +.cauldron-liquid { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 0; + background: linear-gradient(180deg, #4169e1, #000080); + transition: height 0.5s ease; + border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%; +} + +.bubbles { + position: absolute; + bottom: 10px; + left: 50%; + transform: translateX(-50%); + display: none; +} + +.bubble { + position: absolute; + background: rgba(255, 255, 255, 0.8); + border-radius: 50%; + animation: bubble 2s infinite ease-in-out; +} + +.bubble:nth-child(1) { left: -20px; animation-delay: 0s; } +.bubble:nth-child(2) { left: -10px; animation-delay: 0.5s; } +.bubble:nth-child(3) { left: 10px; animation-delay: 1s; } +.bubble:nth-child(4) { left: 20px; animation-delay: 1.5s; } + +@keyframes bubble { + 0% { transform: translateY(0) scale(0); opacity: 1; } + 50% { transform: translateY(-30px) scale(1); opacity: 0.8; } + 100% { transform: translateY(-60px) scale(0); opacity: 0; } +} + +.mix-button { + background: linear-gradient(45deg, #ffd700, #ff8c00); + color: #000; + border: none; + padding: 12px 24px; + border-radius: 25px; + font-size: 1.1em; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3); +} + +.mix-button:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(255, 215, 0, 0.5); +} + +.mix-button:active { + transform: translateY(0); +} + +.effects-display { + min-height: 200px; + background: rgba(0, 0, 0, 0.3); + border-radius: 10px; + padding: 15px; + margin-bottom: 20px; +} + +.effect-item { + background: rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 10px; + margin-bottom: 8px; + border-left: 4px solid #ffd700; + animation: fadeIn 0.5s ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.recipe-book { + background: rgba(0, 0, 0, 0.3); + border-radius: 10px; + padding: 15px; +} + +.recipe-book h4 { + color: #ffd700; + margin-bottom: 10px; +} + +.recipe-list { + max-height: 200px; + overflow-y: auto; +} + +.recipe-item { + background: rgba(255, 255, 255, 0.1); + border-radius: 5px; + padding: 8px; + margin-bottom: 5px; + font-size: 0.9em; +} + +.recipe-item.discovered { + border-left: 3px solid #32cd32; +} + +.controls { + display: flex; + justify-content: center; + gap: 20px; + margin-top: 20px; +} + +.controls button { + background: rgba(255, 255, 255, 0.1); + color: #e8e8e8; + border: 2px solid rgba(255, 215, 0, 0.3); + padding: 10px 20px; + border-radius: 20px; + cursor: pointer; + transition: all 0.3s ease; + font-weight: bold; +} + +.controls button:hover { + background: rgba(255, 215, 0, 0.2); + transform: translateY(-2px); +} + +/* Overlay Styles */ +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + backdrop-filter: blur(5px); +} + +.overlay-content { + background: linear-gradient(135deg, #2c2c2c, #1a1a1a); + border: 2px solid #ffd700; + border-radius: 20px; + padding: 30px; + max-width: 600px; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 0 50px rgba(255, 215, 0, 0.3); +} + +.overlay-content h2 { + color: #ffd700; + text-align: center; + margin-bottom: 20px; + font-size: 2em; +} + +.instructions { + margin-bottom: 30px; +} + +.instructions ul { + margin: 15px 0; + padding-left: 20px; +} + +.instructions li { + margin-bottom: 8px; +} + +.ingredient-type { + font-weight: bold; +} + +.start-button, .next-level-button { + background: linear-gradient(45deg, #ffd700, #ff8c00); + color: #000; + border: none; + padding: 15px 30px; + border-radius: 25px; + font-size: 1.2em; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + display: block; + margin: 0 auto; +} + +.start-button:hover, .next-level-button:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(255, 215, 0, 0.5); +} + +/* Explosion Effect */ +.explosion-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 0, 0, 0.8); + display: none; + justify-content: center; + align-items: center; + z-index: 2000; + animation: explosion 0.5s ease-out; +} + +.explosion-text { + font-size: 5em; + color: #fff; + text-shadow: 0 0 20px #ff0000; + animation: explosionText 0.5s ease-out; +} + +@keyframes explosion { + 0% { opacity: 0; } + 10% { opacity: 1; } + 90% { opacity: 1; } + 100% { opacity: 0; } +} + +@keyframes explosionText { + 0% { transform: scale(0); } + 50% { transform: scale(1.2); } + 100% { transform: scale(1); } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .game-area { + grid-template-columns: 1fr; + gap: 15px; + } + + .ingredients-panel, .effects-panel { + order: 2; + } + + .main-game { + order: 1; + } + + .game-stats { + flex-wrap: wrap; + gap: 15px; + } + + .controls { + flex-wrap: wrap; + } + + h1 { + font-size: 2em; + } +} \ No newline at end of file diff --git a/games/precision-archer/index.html b/games/precision-archer/index.html new file mode 100644 index 00000000..56ea166e --- /dev/null +++ b/games/precision-archer/index.html @@ -0,0 +1,82 @@ + + + + + + Precision Archer + + + +
    +
    +

    ๐Ÿน Precision Archer

    +
    +
    Score: 0
    +
    Shots: 0
    +
    Accuracy: 0%
    +
    +
    + +
    +
    + + +
    +
    + + +
    + +
    + + +
    + + + +
    + +
    +
    Wind: โ†’ 5 mph
    +
    +
    +
    +
    +
    + + +
    + +
    +

    Master the art of precision archery with physics-based gameplay!

    +
    +
    + + + + \ No newline at end of file diff --git a/games/precision-archer/script.js b/games/precision-archer/script.js new file mode 100644 index 00000000..1d48b617 --- /dev/null +++ b/games/precision-archer/script.js @@ -0,0 +1,504 @@ +// Precision Archer Target Game +// Physics-based archery with wind effects and moving targets + +class PrecisionArcherGame { + constructor() { + this.canvas = document.getElementById('game-canvas'); + this.ctx = this.canvas.getContext('2d'); + this.groundY = this.canvas.height - 50; + this.bowX = 100; + this.bowY = this.groundY - 20; + + // Game state + this.arrows = []; + this.targets = []; + this.wind = { direction: 1, strength: 5 }; + this.score = 0; + this.shots = 0; + this.hits = 0; + this.gameRunning = true; + + // Controls + this.power = 50; + this.angle = 45; + + // Physics constants + this.gravity = 0.3; + this.airResistance = 0.99; + + // Achievements + this.achievements = this.initializeAchievements(); + + this.initializeGame(); + this.setupEventListeners(); + this.generateTargets(); + this.updateWind(); + this.animate(); + } + + initializeGame() { + // Set initial control values + document.getElementById('power-slider').value = this.power; + document.getElementById('angle-slider').value = this.angle; + this.updateControlDisplays(); + } + + setupEventListeners() { + document.getElementById('power-slider').addEventListener('input', (e) => { + this.power = parseInt(e.target.value); + this.updateControlDisplays(); + }); + + document.getElementById('angle-slider').addEventListener('input', (e) => { + this.angle = parseInt(e.target.value); + this.updateControlDisplays(); + }); + + document.getElementById('shoot-btn').addEventListener('click', () => this.shootArrow()); + document.getElementById('reset-btn').addEventListener('click', () => this.resetGame()); + } + + updateControlDisplays() { + document.getElementById('power-value').textContent = this.power; + document.getElementById('angle-value').textContent = this.angle; + } + + shootArrow() { + if (!this.gameRunning) return; + + this.shots++; + + // Calculate initial velocity + const angleRad = (this.angle * Math.PI) / 180; + const velocity = (this.power / 100) * 15; // Max velocity of 15 + + const arrow = { + x: this.bowX, + y: this.bowY, + vx: Math.cos(angleRad) * velocity, + vy: -Math.sin(angleRad) * velocity, + trail: [], + active: true + }; + + this.arrows.push(arrow); + this.playShootSound(); + + // Update stats + this.updateStats(); + } + + updateArrows() { + this.arrows.forEach((arrow, index) => { + if (!arrow.active) return; + + // Apply physics + arrow.vy += this.gravity; // Gravity + arrow.vx *= this.airResistance; // Air resistance + arrow.vy *= this.airResistance; + + // Apply wind + arrow.vx += (this.wind.direction * this.wind.strength) * 0.01; + + // Update position + arrow.x += arrow.vx; + arrow.y += arrow.vy; + + // Add to trail + arrow.trail.push({ x: arrow.x, y: arrow.y }); + if (arrow.trail.length > 20) { + arrow.trail.shift(); + } + + // Check ground collision + if (arrow.y >= this.groundY) { + arrow.active = false; + this.playMissSound(); + } + + // Check canvas bounds + if (arrow.x < 0 || arrow.x > this.canvas.width || arrow.y > this.canvas.height) { + arrow.active = false; + } + + // Check target collisions + this.checkTargetCollisions(arrow); + }); + + // Remove inactive arrows + this.arrows = this.arrows.filter(arrow => arrow.active || arrow.trail.length > 0); + } + + checkTargetCollisions(arrow) { + this.targets.forEach((target, targetIndex) => { + if (!target.active) return; + + const distance = Math.sqrt( + Math.pow(arrow.x - target.x, 2) + Math.pow(arrow.y - target.y, 2) + ); + + if (distance < target.radius) { + // Hit! + arrow.active = false; + target.hit = true; + this.hits++; + + // Calculate score based on accuracy + const centerDistance = distance / target.radius; + let points = Math.round((1 - centerDistance) * target.maxPoints); + + if (points < 1) points = 1; + this.score += points; + + // Visual feedback + target.hitAnimation = 30; + this.playHitSound(); + + // Generate new target + setTimeout(() => { + this.targets[targetIndex] = this.createTarget(); + }, 1000); + + this.updateStats(); + this.checkAchievements(); + } + }); + } + + generateTargets() { + this.targets = []; + for (let i = 0; i < 3; i++) { + this.targets.push(this.createTarget()); + } + } + + createTarget() { + const distance = 200 + Math.random() * 400; // 200-600 pixels away + const height = this.groundY - 50 - Math.random() * 200; // 50-250 pixels high + + return { + x: this.bowX + distance, + y: height, + radius: 25 + Math.random() * 15, // 25-40 pixel radius + maxPoints: 10, + active: true, + hit: false, + hitAnimation: 0, + movement: Math.random() > 0.5 ? 'vertical' : 'horizontal', + moveSpeed: 1 + Math.random() * 2, + moveDirection: Math.random() > 0.5 ? 1 : -1, + originalY: height, + originalX: this.bowX + distance + }; + } + + updateTargets() { + this.targets.forEach(target => { + if (!target.active) return; + + if (target.movement === 'vertical') { + target.y += target.moveSpeed * target.moveDirection; + if (target.y > target.originalY + 50 || target.y < target.originalY - 50) { + target.moveDirection *= -1; + } + } else if (target.movement === 'horizontal') { + target.x += target.moveSpeed * target.moveDirection; + if (target.x > target.originalX + 30 || target.x < target.originalX - 30) { + target.moveDirection *= -1; + } + } + + if (target.hitAnimation > 0) { + target.hitAnimation--; + } + }); + } + + updateWind() { + setInterval(() => { + // Random wind changes + this.wind.direction = Math.random() > 0.5 ? 1 : -1; + this.wind.strength = Math.random() * 10; + + this.updateWindDisplay(); + }, 3000 + Math.random() * 4000); // Change every 3-7 seconds + } + + updateWindDisplay() { + const directionSymbol = this.wind.direction > 0 ? 'โ†’' : 'โ†'; + document.getElementById('wind-direction').textContent = directionSymbol; + document.getElementById('wind-strength').textContent = Math.round(this.wind.strength); + + // Update wind bar + const windBar = document.getElementById('wind-bar-fill'); + const percentage = Math.min(Math.abs(this.wind.strength) / 10 * 100, 100); + windBar.style.width = percentage + '%'; + + // Color based on wind strength + if (Math.abs(this.wind.strength) < 3) { + windBar.style.background = 'linear-gradient(90deg, #32CD32, #228B22)'; // Green for light wind + } else if (Math.abs(this.wind.strength) < 7) { + windBar.style.background = 'linear-gradient(90deg, #FFD700, #DAA520)'; // Yellow for moderate wind + } else { + windBar.style.background = 'linear-gradient(90deg, #FF6347, #DC143C)'; // Red for strong wind + } + } + + updateStats() { + document.getElementById('score').textContent = this.score; + document.getElementById('shots').textContent = this.shots; + const accuracy = this.shots > 0 ? Math.round((this.hits / this.shots) * 100) : 0; + document.getElementById('accuracy').textContent = accuracy + '%'; + + // Update target info (show info for first active target) + const activeTarget = this.targets.find(t => t.active); + if (activeTarget) { + const distance = Math.round((activeTarget.x - this.bowX) / 10); // Convert to meters + document.getElementById('target-distance').textContent = distance + 'm'; + document.getElementById('target-movement').textContent = activeTarget.movement === 'vertical' ? 'Up/Down' : 'Left/Right'; + document.getElementById('target-points').textContent = activeTarget.maxPoints; + } + } + + initializeAchievements() { + return [ + { id: 'first_shot', name: 'First Flight', description: 'Fire your first arrow', unlocked: false }, + { id: 'first_hit', name: 'Bullseye Beginner', description: 'Hit your first target', unlocked: false }, + { id: 'accuracy_50', name: 'Sharp Shooter', description: 'Achieve 50% accuracy', unlocked: false }, + { id: 'accuracy_80', name: 'Marksman', description: 'Achieve 80% accuracy', unlocked: false }, + { id: 'score_100', name: 'Century Scorer', description: 'Score 100 points', unlocked: false }, + { id: 'score_500', name: 'Master Archer', description: 'Score 500 points', unlocked: false }, + { id: 'perfect_shot', name: 'Perfectionist', description: 'Hit the exact center of a target', unlocked: false } + ]; + } + + checkAchievements() { + const accuracy = this.shots > 0 ? (this.hits / this.shots) * 100 : 0; + + if (this.shots > 0) { + this.achievements.find(a => a.id === 'first_shot').unlocked = true; + } + if (this.hits > 0) { + this.achievements.find(a => a.id === 'first_hit').unlocked = true; + } + if (accuracy >= 50) { + this.achievements.find(a => a.id === 'accuracy_50').unlocked = true; + } + if (accuracy >= 80) { + this.achievements.find(a => a.id === 'accuracy_80').unlocked = true; + } + if (this.score >= 100) { + this.achievements.find(a => a.id === 'score_100').unlocked = true; + } + if (this.score >= 500) { + this.achievements.find(a => a.id === 'score_500').unlocked = true; + } + + this.renderAchievements(); + } + + renderAchievements() { + const achievementList = document.getElementById('achievement-list'); + achievementList.innerHTML = ''; + + this.achievements.forEach(achievement => { + const item = document.createElement('div'); + item.className = `achievement-item ${achievement.unlocked ? 'unlocked' : ''}`; + item.innerHTML = ` +
    ${achievement.name}
    +
    ${achievement.description}
    + `; + achievementList.appendChild(item); + }); + } + + playShootSound() { + this.playTone(800, 0.1, 'square'); + } + + playHitSound() { + this.playTone(600, 0.2, 'sine'); + } + + playMissSound() { + this.playTone(200, 0.3, 'sawtooth'); + } + + playTone(frequency, duration, type = 'sine') { + try { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime); + oscillator.type = type; + + gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration); + + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + duration); + } catch (e) { + // Silently fail if Web Audio API not supported + } + } + + resetGame() { + this.arrows = []; + this.score = 0; + this.shots = 0; + this.hits = 0; + this.generateTargets(); + this.updateStats(); + this.renderAchievements(); + } + + animate() { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + this.drawBackground(); + this.drawBow(); + this.drawTargets(); + this.drawArrows(); + this.updateArrows(); + this.updateTargets(); + + requestAnimationFrame(() => this.animate()); + } + + drawBackground() { + // Sky gradient + const gradient = this.ctx.createLinearGradient(0, 0, 0, this.canvas.height); + gradient.addColorStop(0, '#87CEEB'); + gradient.addColorStop(0.3, '#98FB98'); + gradient.addColorStop(0.7, '#F0E68C'); + gradient.addColorStop(1, '#8B4513'); + + this.ctx.fillStyle = gradient; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + // Ground + this.ctx.fillStyle = '#8B4513'; + this.ctx.fillRect(0, this.groundY, this.canvas.width, this.canvas.height - this.groundY); + + // Grass texture + this.ctx.fillStyle = '#228B22'; + for (let i = 0; i < this.canvas.width; i += 4) { + const height = Math.random() * 10 + 5; + this.ctx.fillRect(i, this.groundY - height, 2, height); + } + } + + drawBow() { + // Bow + this.ctx.strokeStyle = '#8B4513'; + this.ctx.lineWidth = 4; + this.ctx.beginPath(); + this.ctx.arc(this.bowX, this.bowY, 30, Math.PI * 0.2, Math.PI * 0.8); + this.ctx.stroke(); + + // Bow string + this.ctx.beginPath(); + this.ctx.moveTo(this.bowX, this.bowY - 30); + this.ctx.lineTo(this.bowX, this.bowY + 30); + this.ctx.stroke(); + + // Arrow on bow + this.drawArrow(this.bowX + 10, this.bowY, 0, 0, false); + } + + drawTargets() { + this.targets.forEach(target => { + if (!target.active) return; + + const scale = target.hitAnimation > 0 ? 1.2 : 1; + + // Target rings + const colors = ['#FFFFFF', '#FFD700', '#FF6347', '#DC143C', '#8B0000']; + for (let i = 0; i < 5; i++) { + this.ctx.beginPath(); + this.ctx.arc(target.x, target.y, (target.radius * scale) * (1 - i * 0.15), 0, Math.PI * 2); + this.ctx.fillStyle = colors[i]; + this.ctx.fill(); + this.ctx.strokeStyle = '#000'; + this.ctx.lineWidth = 1; + this.ctx.stroke(); + } + + // Center dot + this.ctx.beginPath(); + this.ctx.arc(target.x, target.y, 3 * scale, 0, Math.PI * 2); + this.ctx.fillStyle = '#000'; + this.ctx.fill(); + }); + } + + drawArrows() { + this.arrows.forEach(arrow => { + if (arrow.active) { + this.drawArrow(arrow.x, arrow.y, arrow.vx, arrow.vy, true); + } + + // Draw trail + arrow.trail.forEach((point, index) => { + const alpha = index / arrow.trail.length; + this.ctx.beginPath(); + this.ctx.arc(point.x, point.y, 1, 0, Math.PI * 2); + this.ctx.fillStyle = `rgba(139, 69, 19, ${alpha * 0.5})`; + this.ctx.fill(); + }); + }); + } + + drawArrow(x, y, vx, vy, showMotion = false) { + const angle = Math.atan2(vy, vx); + + this.ctx.save(); + this.ctx.translate(x, y); + this.ctx.rotate(angle); + + // Arrow shaft + this.ctx.strokeStyle = '#8B4513'; + this.ctx.lineWidth = 2; + this.ctx.beginPath(); + this.ctx.moveTo(0, 0); + this.ctx.lineTo(-20, 0); + this.ctx.stroke(); + + // Arrow head + this.ctx.fillStyle = '#C0C0C0'; + this.ctx.beginPath(); + this.ctx.moveTo(0, 0); + this.ctx.lineTo(-5, -3); + this.ctx.lineTo(-5, 3); + this.ctx.closePath(); + this.ctx.fill(); + + // Fletching + this.ctx.fillStyle = '#FF0000'; + this.ctx.fillRect(-18, -2, 4, 4); + this.ctx.fillStyle = '#0000FF'; + this.ctx.fillRect(-14, -2, 4, 4); + + this.ctx.restore(); + + // Motion blur effect + if (showMotion && (Math.abs(vx) > 2 || Math.abs(vy) > 2)) { + this.ctx.save(); + this.ctx.translate(x, y); + this.ctx.rotate(angle); + this.ctx.globalAlpha = 0.3; + this.ctx.fillStyle = '#8B4513'; + this.ctx.fillRect(-25, -1, 5, 2); + this.ctx.restore(); + } + } +} + +// Initialize the game when the page loads +document.addEventListener('DOMContentLoaded', () => { + new PrecisionArcherGame(); +}); \ No newline at end of file diff --git a/games/precision-archer/style.css b/games/precision-archer/style.css new file mode 100644 index 00000000..b244b6ca --- /dev/null +++ b/games/precision-archer/style.css @@ -0,0 +1,282 @@ +body { + margin: 0; + padding: 0; + background: linear-gradient(135deg, #87CEEB 0%, #98FB98 50%, #F0E68C 100%); + font-family: 'Arial', sans-serif; + color: #2F4F2F; + overflow-x: hidden; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 20px; +} + +h1 { + color: #8B4513; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + font-size: 2.5em; + margin-bottom: 10px; +} + +.stats { + display: flex; + justify-content: center; + gap: 20px; + flex-wrap: wrap; +} + +.score, .shots, .accuracy { + background: linear-gradient(145deg, #DAA520, #B8860B); + color: white; + padding: 10px 20px; + border-radius: 25px; + font-weight: bold; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + border: 2px solid #8B4513; +} + +main { + display: flex; + gap: 20px; + align-items: flex-start; +} + +.game-area { + flex: 1; + position: relative; +} + +#game-canvas { + background: linear-gradient(180deg, #87CEEB 0%, #98FB98 30%, #F0E68C 70%, #8B4513 100%); + border-radius: 15px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + border: 4px solid #8B4513; + display: block; + margin: 0 auto; +} + +.controls { + display: flex; + justify-content: center; + gap: 20px; + margin-top: 20px; + flex-wrap: wrap; + align-items: center; +} + +.control-group { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.control-group label { + font-weight: bold; + color: #2F4F2F; + text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.5); +} + +input[type="range"] { + width: 150px; + height: 8px; + border-radius: 4px; + background: #DAA520; + outline: none; + appearance: none; + -webkit-appearance: none; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + border-radius: 50%; + background: #8B4513; + cursor: pointer; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); +} + +input[type="range"]::-moz-range-thumb { + width: 20px; + height: 20px; + border-radius: 50%; + background: #8B4513; + cursor: pointer; + border: none; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); +} + +.shoot-btn, .reset-btn { + background: linear-gradient(145deg, #228B22, #006400); + color: white; + border: none; + padding: 15px 30px; + border-radius: 25px; + font-size: 18px; + font-weight: bold; + cursor: pointer; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); + transition: all 0.3s ease; +} + +.shoot-btn:hover { + background: linear-gradient(145deg, #32CD32, #228B22); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); +} + +.reset-btn { + background: linear-gradient(145deg, #DC143C, #B22222); +} + +.reset-btn:hover { + background: linear-gradient(145deg, #FF6347, #DC143C); +} + +.wind-indicator { + background: rgba(255, 255, 255, 0.9); + padding: 15px; + border-radius: 10px; + margin-top: 15px; + text-align: center; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.wind-text { + font-weight: bold; + margin-bottom: 10px; + color: #2F4F2F; +} + +.wind-bar { + width: 200px; + height: 8px; + background: #ddd; + border-radius: 4px; + margin: 0 auto; + overflow: hidden; +} + +.wind-bar-fill { + height: 100%; + background: linear-gradient(90deg, #FF6347, #FFD700, #32CD32); + transition: width 0.5s ease; +} + +.sidebar { + width: 280px; + background: rgba(255, 255, 255, 0.95); + border-radius: 15px; + padding: 20px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); +} + +.target-info h3, .achievements h3, .instructions h3 { + color: #8B4513; + margin-bottom: 15px; + text-align: center; +} + +.info-item { + display: flex; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px solid #eee; + font-weight: bold; +} + +.achievement-item { + background: rgba(218, 165, 32, 0.1); + padding: 10px; + margin-bottom: 8px; + border-radius: 8px; + border-left: 4px solid #DAA520; + transition: all 0.3s ease; +} + +.achievement-item.unlocked { + background: rgba(34, 139, 34, 0.1); + border-left-color: #228B22; +} + +.achievement-item.unlocked .achievement-name { + color: #228B22; + font-weight: bold; +} + +.instructions ul { + padding-left: 20px; +} + +.instructions li { + margin-bottom: 8px; + line-height: 1.4; +} + +footer { + text-align: center; + margin-top: 20px; + color: #666; + font-style: italic; +} + +/* Responsive design */ +@media (max-width: 768px) { + main { + flex-direction: column; + } + + .sidebar { + width: 100%; + } + + #game-canvas { + width: 100%; + max-width: 100%; + height: 400px; + } + + .controls { + flex-direction: column; + gap: 15px; + } + + .stats { + flex-direction: column; + gap: 10px; + } + + h1 { + font-size: 2em; + } +} + +/* Target hit animation */ +@keyframes targetHit { + 0% { transform: scale(1); } + 50% { transform: scale(1.2); } + 100% { transform: scale(1); } +} + +.target-hit { + animation: targetHit 0.5s ease; +} + +/* Arrow trail effect */ +.arrow-trail { + position: absolute; + width: 2px; + height: 2px; + background: rgba(139, 69, 19, 0.8); + border-radius: 50%; + pointer-events: none; +} \ No newline at end of file diff --git a/games/prism-puzzle/index.html b/games/prism-puzzle/index.html new file mode 100644 index 00000000..604fc7f6 --- /dev/null +++ b/games/prism-puzzle/index.html @@ -0,0 +1,18 @@ + + + + + + Prism Puzzle Game + + + +
    +

    Prism Puzzle

    + +
    Targets Lit: 0/3
    +
    Click and drag prisms to rotate. Bend light beams to illuminate all targets!
    +
    + + + \ No newline at end of file diff --git a/games/prism-puzzle/script.js b/games/prism-puzzle/script.js new file mode 100644 index 00000000..035564ea --- /dev/null +++ b/games/prism-puzzle/script.js @@ -0,0 +1,183 @@ +// Prism Puzzle Game Script +// Bend light through prisms to illuminate targets and solve optical challenges. + +const canvas = document.getElementById('game-canvas'); +const ctx = canvas.getContext('2d'); +const scoreElement = document.getElementById('score'); + +// Game variables +let lightSource = { x: 50, y: 300 }; +let prisms = []; +let targets = []; +let beams = []; +let draggedPrism = null; +let litTargets = 0; + +// Initialize game +function init() { + // Create prisms + prisms.push({ x: 300, y: 200, angle: 0 }); + prisms.push({ x: 500, y: 400, angle: 0 }); + + // Create targets + targets.push({ x: 700, y: 150, lit: false }); + targets.push({ x: 700, y: 300, lit: false }); + targets.push({ x: 700, y: 450, lit: false }); + + // Start game loop + requestAnimationFrame(gameLoop); +} + +// Game loop +function gameLoop() { + update(); + draw(); + + requestAnimationFrame(gameLoop); +} + +// Update game state +function update() { + // Calculate beams + beams = []; + castBeam(lightSource.x, lightSource.y, 0, 0); // Horizontal beam + + // Check targets + litTargets = 0; + targets.forEach(target => { + target.lit = false; + beams.forEach(beam => { + if (Math.abs(beam.endX - target.x) < 20 && Math.abs(beam.endY - target.y) < 20) { + target.lit = true; + litTargets++; + } + }); + }); + + scoreElement.textContent = 'Targets Lit: ' + litTargets + '/3'; + if (litTargets === 3) { + setTimeout(() => alert('All targets illuminated! Puzzle solved!'), 100); + } +} + +// Cast beam recursively +function castBeam(startX, startY, dirX, dirY) { + let currentX = startX; + let currentY = startY; + let currentDirX = dirX; + let currentDirY = dirY; + + for (let i = 0; i < 1000; i++) { // Max length + currentX += currentDirX; + currentY += currentDirY; + + // Check prism collision + let hitPrism = null; + prisms.forEach(prism => { + const dist = Math.sqrt((currentX - prism.x)**2 + (currentY - prism.y)**2); + if (dist < 30) { // Prism size + hitPrism = prism; + } + }); + + if (hitPrism) { + // Reflect: simple 90 degree turn for demo + if (currentDirX > 0) { currentDirX = 0; currentDirY = currentDirY > 0 ? -1 : 1; } + else if (currentDirX < 0) { currentDirX = 0; currentDirY = currentDirY > 0 ? -1 : 1; } + else if (currentDirY > 0) { currentDirY = 0; currentDirX = currentDirX > 0 ? -1 : 1; } + else { currentDirY = 0; currentDirX = currentDirX > 0 ? -1 : 1; } + } + + // Check canvas bounds + if (currentX < 0 || currentX > canvas.width || currentY < 0 || currentY > canvas.height) { + beams.push({ startX: startX, startY: startY, endX: currentX, endY: currentY }); + break; + } + } +} + +// Draw everything +function draw() { + // Clear canvas + ctx.fillStyle = '#000022'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw light source + ctx.fillStyle = '#ffff00'; + ctx.beginPath(); + ctx.arc(lightSource.x, lightSource.y, 10, 0, Math.PI * 2); + ctx.fill(); + + // Draw prisms + ctx.fillStyle = '#00ffff'; + prisms.forEach(prism => { + ctx.save(); + ctx.translate(prism.x, prism.y); + ctx.rotate(prism.angle); + ctx.beginPath(); + ctx.moveTo(0, -20); + ctx.lineTo(20, 20); + ctx.lineTo(-20, 20); + ctx.closePath(); + ctx.fill(); + ctx.restore(); + }); + + // Draw targets + targets.forEach(target => { + ctx.fillStyle = target.lit ? '#00ff00' : '#ff0000'; + ctx.beginPath(); + ctx.arc(target.x, target.y, 15, 0, Math.PI * 2); + ctx.fill(); + }); + + // Draw beams + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 2; + beams.forEach(beam => { + ctx.beginPath(); + ctx.moveTo(beam.startX, beam.startY); + ctx.lineTo(beam.endX, beam.endY); + ctx.stroke(); + }); +} + +// Handle mouse +let isDragging = false; +let dragStartX, dragStartY; + +canvas.addEventListener('mousedown', e => { + const rect = canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + prisms.forEach(prism => { + const dist = Math.sqrt((mouseX - prism.x)**2 + (mouseY - prism.y)**2); + if (dist < 30) { + draggedPrism = prism; + dragStartX = mouseX; + dragStartY = mouseY; + isDragging = true; + } + }); +}); + +canvas.addEventListener('mousemove', e => { + if (isDragging && draggedPrism) { + const rect = canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + const deltaX = mouseX - dragStartX; + const deltaY = mouseY - dragStartY; + draggedPrism.angle = Math.atan2(deltaY, deltaX); + } +}); + +canvas.addEventListener('mouseup', () => { + isDragging = false; + draggedPrism = null; +}); + +// Start the game +init(); \ No newline at end of file diff --git a/games/prism-puzzle/style.css b/games/prism-puzzle/style.css new file mode 100644 index 00000000..eeca12cc --- /dev/null +++ b/games/prism-puzzle/style.css @@ -0,0 +1,39 @@ +body { + font-family: Arial, sans-serif; + background-color: #000; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +#game-container { + text-align: center; +} + +h1 { + font-size: 2.5em; + margin-bottom: 20px; + text-shadow: 0 0 10px #00ffff; +} + +#game-canvas { + border: 2px solid #00ffff; + background-color: #000022; + box-shadow: 0 0 20px #00ffff; + cursor: pointer; +} + +#score { + font-size: 1.2em; + margin: 10px 0; + color: #ffff00; +} + +#instructions { + font-size: 1em; + margin-top: 10px; + color: #cccccc; +} \ No newline at end of file diff --git a/games/quantum-leap-frog/index.html b/games/quantum-leap-frog/index.html new file mode 100644 index 00000000..b09edde2 --- /dev/null +++ b/games/quantum-leap-frog/index.html @@ -0,0 +1,40 @@ + + + + + + Quantum Leap Frog + + + + + + + + + +
    +
    +

    QUANTUM LEAP FROG

    +

    A physics-based platformer where a frog jumps across lily pads, but quantum mechanics allow unpredictable teleports!

    + +

    Controls:

    +
      +
    • Space or ArrowUp - Jump
    • +
    • ArrowLeft / ArrowRight - Move
    • +
    + + +
    +
    + +
    +
    Score: 0
    +
    Lives: 3
    +
    + + + + + + \ No newline at end of file diff --git a/games/quantum-leap-frog/script.js b/games/quantum-leap-frog/script.js new file mode 100644 index 00000000..754dd8b5 --- /dev/null +++ b/games/quantum-leap-frog/script.js @@ -0,0 +1,230 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const startButton = document.getElementById('startButton'); +const instructionsOverlay = document.getElementById('instructions-overlay'); +const scoreElement = document.getElementById('score'); +const livesElement = document.getElementById('lives'); + +canvas.width = 800; +canvas.height = 600; + +let gameRunning = false; +let score = 0; +let lives = 3; +let frog; +let lilyPads = []; +let cameraX = 0; +const GRAVITY = 0.5; +const JUMP_FORCE = -12; +const MOVE_SPEED = 5; +const PAD_WIDTH = 80; +const PAD_HEIGHT = 20; +const FROG_SIZE = 30; + +// Frog class +class Frog { + constructor(x, y) { + this.x = x; + this.y = y; + this.vx = 0; + this.vy = 0; + this.onGround = false; + this.width = FROG_SIZE; + this.height = FROG_SIZE; + } + + update() { + // Apply gravity + if (!this.onGround) { + this.vy += GRAVITY; + } + + // Update position + this.x += this.vx; + this.y += this.vy; + + // Check ground collision + this.onGround = false; + for (let pad of lilyPads) { + if (this.x < pad.x + PAD_WIDTH && + this.x + this.width > pad.x && + this.y + this.height >= pad.y && + this.y + this.height <= pad.y + PAD_HEIGHT + 10 && + this.vy >= 0) { + this.y = pad.y - this.height; + this.vy = 0; + this.onGround = true; + // Quantum teleport chance + if (Math.random() < 0.1) { // 10% chance + this.quantumTeleport(); + } + break; + } + } + + // Check if fell in water + if (this.y > canvas.height) { + this.resetPosition(); + lives--; + updateUI(); + if (lives <= 0) { + gameOver(); + } + } + + // Keep frog in bounds horizontally + if (this.x < cameraX) this.x = cameraX; + } + + jump() { + if (this.onGround) { + this.vy = JUMP_FORCE; + this.onGround = false; + } + } + + moveLeft() { + this.vx = -MOVE_SPEED; + } + + moveRight() { + this.vx = MOVE_SPEED; + } + + stopMove() { + this.vx = 0; + } + + quantumTeleport() { + if (lilyPads.length > 1) { + let randomPad = lilyPads[Math.floor(Math.random() * lilyPads.length)]; + this.x = randomPad.x + PAD_WIDTH / 2 - this.width / 2; + this.y = randomPad.y - this.height; + this.vy = 0; + } + } + + resetPosition() { + this.x = 100; + this.y = canvas.height - 200; + this.vx = 0; + this.vy = 0; + } + + draw() { + ctx.fillStyle = '#32CD32'; + ctx.fillRect(this.x - cameraX, this.y, this.width, this.height); + // Simple frog face + ctx.fillStyle = '#000'; + ctx.fillRect(this.x - cameraX + 8, this.y + 8, 4, 4); + ctx.fillRect(this.x - cameraX + 18, this.y + 8, 4, 4); + } +} + +// Generate lily pads +function generateLilyPads() { + lilyPads = []; + for (let i = 0; i < 20; i++) { + let x = 100 + i * 150 + Math.random() * 100; + let y = canvas.height - 100 - Math.random() * 200; + lilyPads.push({x, y}); + } +} + +// Draw lily pads +function drawLilyPads() { + ctx.fillStyle = '#228B22'; + for (let pad of lilyPads) { + ctx.fillRect(pad.x - cameraX, pad.y, PAD_WIDTH, PAD_HEIGHT); + } +} + +// Draw water +function drawWater() { + ctx.fillStyle = '#4169E1'; + ctx.fillRect(0, canvas.height - 50, canvas.width, 50); +} + +// Update camera +function updateCamera() { + cameraX = frog.x - canvas.width / 3; + if (cameraX < 0) cameraX = 0; +} + +// Update UI +function updateUI() { + scoreElement.textContent = `Score: ${Math.floor(score)}`; + livesElement.textContent = `Lives: ${lives}`; +} + +// Game over +function gameOver() { + gameRunning = false; + alert(`Game Over! Final Score: ${Math.floor(score)}`); + resetGame(); +} + +// Reset game +function resetGame() { + score = 0; + lives = 3; + frog.resetPosition(); + generateLilyPads(); + cameraX = 0; + updateUI(); +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + drawWater(); + drawLilyPads(); + frog.update(); + frog.draw(); + updateCamera(); + + score += 0.1; // Score increases over time + updateUI(); + + requestAnimationFrame(gameLoop); +} + +// Event listeners +startButton.addEventListener('click', () => { + instructionsOverlay.style.display = 'none'; + resetGame(); + gameRunning = true; + gameLoop(); +}); + +document.addEventListener('keydown', (e) => { + if (!gameRunning) return; + switch (e.code) { + case 'Space': + case 'ArrowUp': + e.preventDefault(); + frog.jump(); + break; + case 'ArrowLeft': + frog.moveLeft(); + break; + case 'ArrowRight': + frog.moveRight(); + break; + } +}); + +document.addEventListener('keyup', (e) => { + if (!gameRunning) return; + if (e.code === 'ArrowLeft' || e.code === 'ArrowRight') { + frog.stopMove(); + } +}); + +// Initialize +frog = new Frog(100, canvas.height - 200); +generateLilyPads(); +updateUI(); \ No newline at end of file diff --git a/games/quantum-leap-frog/style.css b/games/quantum-leap-frog/style.css new file mode 100644 index 00000000..d8a0bcee --- /dev/null +++ b/games/quantum-leap-frog/style.css @@ -0,0 +1,131 @@ +/* General Reset & Font */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Poppins', sans-serif; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); + color: #eee; + overflow: hidden; +} + +/* Game UI */ +#game-ui { + position: absolute; + top: 20px; + left: 20px; + display: flex; + gap: 20px; + z-index: 5; +} + +#score, #lives { + background-color: rgba(0, 0, 0, 0.7); + color: #fff; + padding: 10px 15px; + border-radius: 5px; + font-size: 1.1rem; + font-weight: 600; +} + +/* Canvas */ +canvas { + background: linear-gradient(135deg, #87CEEB 0%, #98FB98 100%); + border: 3px solid #4CAF50; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.3); + display: block; +} + +/* Instructions Screen */ +#instructions-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +#instructions-content { + background-color: #2a2a2a; + padding: 30px 40px; + border-radius: 10px; + text-align: center; + border: 2px solid #4CAF50; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); + max-width: 500px; +} + +#instructions-content h2 { + font-size: 2.5rem; + color: #4CAF50; + margin-bottom: 15px; + letter-spacing: 2px; +} + +#instructions-content p { + font-size: 1.1rem; + margin-bottom: 25px; + color: #ccc; +} + +#instructions-content h3 { + font-size: 1.2rem; + color: #eee; + margin-bottom: 10px; + border-bottom: 1px solid #444; + padding-bottom: 5px; +} + +#instructions-content ul { + list-style: none; + margin-bottom: 30px; + text-align: left; + display: inline-block; +} + +#instructions-content li { + font-size: 1rem; + color: #ccc; + margin-bottom: 8px; +} + +/* Style for keys */ +#instructions-content code { + background-color: #4CAF50; + color: #fff; + padding: 3px 6px; + border-radius: 4px; + font-family: monospace; + font-size: 0.95rem; + margin-right: 8px; +} + +/* Start Button */ +#startButton { + background-color: #4CAF50; + color: white; + border: none; + padding: 12px 24px; + font-size: 1.1rem; + font-weight: 600; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} + +#startButton:hover { + background-color: #45a049; +} \ No newline at end of file diff --git a/games/quantum-leap/index.html b/games/quantum-leap/index.html new file mode 100644 index 00000000..b8c78886 --- /dev/null +++ b/games/quantum-leap/index.html @@ -0,0 +1,18 @@ + + + + + + Quantum Leap Game + + + +
    +

    Quantum Leap

    + +
    Score: 0
    +
    Use SPACE to jump through portals. Avoid obstacles and collect energy particles!
    +
    + + + \ No newline at end of file diff --git a/games/quantum-leap/script.js b/games/quantum-leap/script.js new file mode 100644 index 00000000..6307109b --- /dev/null +++ b/games/quantum-leap/script.js @@ -0,0 +1,159 @@ +// Quantum Leap Game Script +// This game lets you jump through quantum portals, avoiding obstacles and collecting energy particles. + +const canvas = document.getElementById('game-canvas'); +const ctx = canvas.getContext('2d'); +const scoreElement = document.getElementById('score'); + +// Game variables +let player = { x: 100, y: 400, width: 20, height: 20, velocityY: 0, onGround: true }; +let obstacles = []; +let particles = []; +let portals = []; +let score = 0; +let gameRunning = true; + +// Constants +const gravity = 0.5; +const jumpStrength = -12; +const groundY = 400; +const scrollSpeed = 3; + +// Initialize game +function init() { + // Create initial obstacles and portals + createObstacle(600); + createPortal(800); + createParticle(500); + createParticle(700); + + // Start game loop + requestAnimationFrame(gameLoop); +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + update(); + draw(); + + requestAnimationFrame(gameLoop); +} + +// Update game state +function update() { + // Player physics + player.velocityY += gravity; + player.y += player.velocityY; + + if (player.y >= groundY) { + player.y = groundY; + player.velocityY = 0; + player.onGround = true; + } + + // Move obstacles, portals, particles left + obstacles.forEach(ob => ob.x -= scrollSpeed); + portals.forEach(p => p.x -= scrollSpeed); + particles.forEach(p => p.x -= scrollSpeed); + + // Remove off-screen elements + obstacles = obstacles.filter(ob => ob.x > -50); + portals = portals.filter(p => p.x > -50); + particles = particles.filter(p => p.x > -50); + + // Add new elements randomly + if (Math.random() < 0.01) createObstacle(canvas.width + 50); + if (Math.random() < 0.005) createPortal(canvas.width + 50); + if (Math.random() < 0.02) createParticle(canvas.width + 50); + + // Check collisions + checkCollisions(); +} + +// Draw everything +function draw() { + // Clear canvas + ctx.fillStyle = '#001122'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw ground + ctx.fillStyle = '#333'; + ctx.fillRect(0, groundY + 20, canvas.width, canvas.height - groundY - 20); + + // Draw player + ctx.fillStyle = '#00ffff'; + ctx.fillRect(player.x, player.y, player.width, player.height); + + // Draw obstacles + ctx.fillStyle = '#ff0000'; + obstacles.forEach(ob => ctx.fillRect(ob.x, ob.y, ob.width, ob.height)); + + // Draw portals + ctx.fillStyle = '#800080'; + portals.forEach(p => ctx.fillRect(p.x, p.y, p.width, p.height)); + + // Draw particles + ctx.fillStyle = '#ffff00'; + particles.forEach(p => ctx.beginPath(), ctx.arc(p.x, p.y, 5, 0, Math.PI * 2), ctx.fill()); + + // Update score display + scoreElement.textContent = 'Score: ' + score; +} + +// Handle input +document.addEventListener('keydown', function(event) { + if (event.code === 'Space' && player.onGround) { + player.velocityY = jumpStrength; + player.onGround = false; + } +}); + +// Create obstacle +function createObstacle(x) { + obstacles.push({ x: x, y: groundY - 30, width: 20, height: 30 }); +} + +// Create portal +function createPortal(x) { + portals.push({ x: x, y: groundY - 50, width: 30, height: 50 }); +} + +// Create particle +function createParticle(x) { + particles.push({ x: x, y: Math.random() * 200 + 200 }); +} + +// Check collisions +function checkCollisions() { + // Player with obstacles + obstacles.forEach(ob => { + if (player.x < ob.x + ob.width && player.x + player.width > ob.x && + player.y < ob.y + ob.height && player.y + player.height > ob.y) { + gameRunning = false; + alert('Game Over! Score: ' + score); + } + }); + + // Player with portals + portals.forEach(p => { + if (player.x < p.x + p.width && player.x + player.width > p.x && + player.y < p.y + p.height && player.y + player.height > p.y) { + // Teleport forward + player.x += 200; + score += 10; + } + }); + + // Player with particles + particles.forEach((p, index) => { + if (Math.abs(player.x - p.x) < 15 && Math.abs(player.y - p.y) < 15) { + particles.splice(index, 1); + score += 5; + } + }); +} + +// Start the game +init(); \ No newline at end of file diff --git a/games/quantum-leap/style.css b/games/quantum-leap/style.css new file mode 100644 index 00000000..2464d9cb --- /dev/null +++ b/games/quantum-leap/style.css @@ -0,0 +1,38 @@ +body { + font-family: Arial, sans-serif; + background-color: #000; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +#game-container { + text-align: center; +} + +h1 { + font-size: 2.5em; + margin-bottom: 20px; + text-shadow: 0 0 10px #00ffff; +} + +#game-canvas { + border: 2px solid #00ffff; + background-color: #001122; + box-shadow: 0 0 20px #00ffff; +} + +#score { + font-size: 1.5em; + margin: 10px 0; + color: #ffff00; +} + +#instructions { + font-size: 1em; + margin-top: 10px; + color: #cccccc; +} \ No newline at end of file diff --git a/games/quantum-quirk/index.html b/games/quantum-quirk/index.html new file mode 100644 index 00000000..b69d2bed --- /dev/null +++ b/games/quantum-quirk/index.html @@ -0,0 +1,73 @@ + + + + + + Quantum Quirk + + + +
    +

    QUANTUM QUIRK

    +
    Manipulate quantum particles to match the target pattern
    +
    + +
    +
    +
    Level
    +
    1
    +
    +
    +
    Moves
    +
    0
    +
    +
    +
    Accuracy
    +
    0%
    +
    +
    + +
    +
    +
    +
    Rule: Gravity Inversion
    +
    + +
    +
    Target Pattern
    +
    +
    + +
    +
    + + + +
    +
    + + + +
    +
    + +
    +

    Quantum Rules

    +

    โ€ข Particles change behavior based on active quantum rules

    +

    โ€ข Match the target pattern by manipulating particles with different forces

    +

    โ€ข Each level introduces new quantum behaviors

    +

    โ€ข Complete levels with the fewest moves for a higher score

    +
    + +
    +
    +

    QUANTUM SYNCHRONIZED!

    +

    You've mastered level 1!

    +

    Moves: 0 | Accuracy: 0%

    + +
    +
    + + + + \ No newline at end of file diff --git a/games/quantum-quirk/script.js b/games/quantum-quirk/script.js new file mode 100644 index 00000000..608d4a33 --- /dev/null +++ b/games/quantum-quirk/script.js @@ -0,0 +1,462 @@ +const gameState = { +level: 1, +moves: 0, +accuracy: 0, +particles: [], +targetPattern: [], +quantumRules: [ +"Gravity Inversion", +"Quantum Entanglement", +"Particle Attraction", +"Particle Repulsion", +"Dimensional Shift", +"Chronal Instability" +], +activeRule: "Gravity Inversion", +gravityEnabled: false, +entanglementEnabled: false, +attractionEnabled: false, +repulsionEnabled: false +}; + +const gameBoard = document.getElementById('gameBoard'); +const targetPattern = document.getElementById('targetPattern'); +let quantumField = document.getElementById('quantumField'); +let quantumRule = document.getElementById('quantumRule'); +const levelDisplay = document.getElementById('level'); +const movesDisplay = document.getElementById('moves'); +const accuracyDisplay = document.getElementById('accuracy'); +const winMessage = document.getElementById('winMessage'); +const winLevel = document.getElementById('winLevel'); +const winMoves = document.getElementById('winMoves'); +const winAccuracy = document.getElementById('winAccuracy'); + +const gravityBtn = document.getElementById('gravityBtn'); +const quantumBtn = document.getElementById('quantumBtn'); +const resetBtn = document.getElementById('resetBtn'); +const attractBtn = document.getElementById('attractBtn'); +const repelBtn = document.getElementById('repelBtn'); +const nextLevelBtn = document.getElementById('nextLevelBtn'); +const continueBtn = document.getElementById('continueBtn'); + +function initGame() { +createParticles(); +createTargetPattern(); +updateDisplay(); +setupEventListeners(); +animateQuantumField(); +} + +function createParticles() { +gameBoard.innerHTML = '
    Rule: ' + gameState.activeRule + '
    '; +quantumField = document.getElementById('quantumField'); +quantumRule = document.getElementById('quantumRule'); + +gameState.particles = []; +const particleCount = 5 + gameState.level; + +for (let i = 0; i < particleCount; i++) { +const particle = document.createElement('div'); +particle.className = 'particle'; + +const x = Math.random() * (gameBoard.offsetWidth - 40) + 20; +const y = Math.random() * (gameBoard.offsetHeight - 40) + 20; + +const size = 15 + Math.random() * 25; +const hue = Math.random() * 360; + +particle.style.width = `${size}px`; +particle.style.height = `${size}px`; +particle.style.left = `${x}px`; +particle.style.top = `${y}px`; +particle.style.backgroundColor = `hsl(${hue}, 70%, 60%)`; +particle.style.boxShadow = `0 0 15px hsl(${hue}, 70%, 60%)`; + +makeDraggable(particle); + +gameBoard.appendChild(particle); +gameState.particles.push({ + element: particle, + x: x, + y: y, + size: size, + hue: hue, + vx: 0, + vy: 0 +}); +} +} + +function createTargetPattern() { +targetPattern.innerHTML = '
    Target Pattern
    '; +gameState.targetPattern = []; + +const patternCount = 5 + gameState.level; +const centerX = targetPattern.offsetWidth / 2; +const centerY = targetPattern.offsetHeight / 2; +const radius = Math.min(centerX, centerY) - 50; + +let positions = []; +if (gameState.level <= 3) { +for (let i = 0; i < patternCount; i++) { + const angle = (i / patternCount) * Math.PI * 2; + const x = centerX + Math.cos(angle) * radius; + const y = centerY + Math.sin(angle) * radius; + positions.push({x, y}); +} +} else if (gameState.level <= 6) { +for (let i = 0; i < patternCount; i++) { + const angle = (i / patternCount) * Math.PI * 4; + const spiralRadius = 30 + (i / patternCount) * (radius - 30); + const x = centerX + Math.cos(angle) * spiralRadius; + const y = centerY + Math.sin(angle) * spiralRadius; + positions.push({x, y}); +} +} else { +for (let i = 0; i < patternCount; i++) { + const x = centerX + (Math.random() - 0.5) * radius * 1.5; + const y = centerY + (Math.random() - 0.5) * radius * 1.5; + positions.push({x, y}); +} +} + +for (let i = 0; i < patternCount; i++) { +const targetParticle = document.createElement('div'); +targetParticle.className = 'particle'; + +const size = 20 + Math.random() * 20; +const hue = Math.random() * 360; + +targetParticle.style.width = `${size}px`; +targetParticle.style.height = `${size}px`; +targetParticle.style.left = `${positions[i].x}px`; +targetParticle.style.top = `${positions[i].y}px`; +targetParticle.style.backgroundColor = `hsl(${hue}, 70%, 60%)`; +targetParticle.style.boxShadow = `0 0 15px hsl(${hue}, 70%, 60%)`; +targetParticle.style.opacity = '0.7'; + +targetPattern.appendChild(targetParticle); +gameState.targetPattern.push({ + x: positions[i].x, + y: positions[i].y, + size: size, + hue: hue +}); +} +} + +function makeDraggable(element) { +let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; + +element.onmousedown = dragMouseDown; + +function dragMouseDown(e) { +e.preventDefault(); + +pos3 = e.clientX; +pos4 = e.clientY; +document.onmouseup = closeDragElement; +document.onmousemove = elementDrag; +gameState.moves++; +updateDisplay(); +} + +function elementDrag(e) { +e.preventDefault(); + +pos1 = pos3 - e.clientX; +pos2 = pos4 - e.clientY; +pos3 = e.clientX; +pos4 = e.clientY; + +const newTop = element.offsetTop - pos2; +const newLeft = element.offsetLeft - pos1; + +if (newTop >= 0 && newTop <= gameBoard.offsetHeight - element.offsetHeight) { + element.style.top = newTop + "px"; +} +if (newLeft >= 0 && newLeft <= gameBoard.offsetWidth - element.offsetWidth) { + element.style.left = newLeft + "px"; +} + +const particleIndex = gameState.particles.findIndex(p => p.element === element); +if (particleIndex !== -1) { + gameState.particles[particleIndex].x = newLeft; + gameState.particles[particleIndex].y = newTop; +} + +checkWinCondition(); +} + +function closeDragElement() { +document.onmouseup = null; +document.onmousemove = null; +} +} + +function checkWinCondition() { +if (gameState.particles.length !== gameState.targetPattern.length) return; + +let matched = 0; +const tolerance = 30; + +for (let i = 0; i < gameState.particles.length; i++) { +const particle = gameState.particles[i]; +const target = gameState.targetPattern[i]; + +const distance = Math.sqrt( + Math.pow(particle.x - target.x, 2) + + Math.pow(particle.y - target.y, 2) +); + +if (distance < tolerance) { + matched++; +} +} + +gameState.accuracy = Math.round((matched / gameState.particles.length) * 100); +updateDisplay(); + +if (matched === gameState.particles.length) { +showWinMessage(); +} +} + +function showWinMessage() { +winLevel.textContent = gameState.level; +winMoves.textContent = gameState.moves; +winAccuracy.textContent = gameState.accuracy + '%'; +winMessage.classList.add('active'); +} + +function updateDisplay() { +levelDisplay.textContent = gameState.level; +movesDisplay.textContent = gameState.moves; +accuracyDisplay.textContent = gameState.accuracy + '%'; + +quantumRule.textContent = 'Rule: ' + gameState.activeRule; +} + +function animateQuantumField() { +let time = 0; + +function updateField() { +time += 0.02; +const gradient = `radial-gradient(circle at ${50 + 20 * Math.sin(time)}% ${50 + 20 * Math.cos(time)}%, + rgba(74, 63, 224, 0.3) 0%, + rgba(229, 63, 140, 0.2) 30%, + rgba(0, 201, 255, 0.1) 70%, + transparent 100%)`; + +quantumField.style.background = gradient; + +requestAnimationFrame(updateField); +} + +updateField(); +} + +function applyQuantumRules() { +if (gameState.gravityEnabled) { +gameState.particles.forEach(particle => { + particle.vy += 0.1; + particle.y += particle.vy; + + if (particle.y <= 0 || particle.y >= gameBoard.offsetHeight - particle.size) { + particle.vy *= -0.8; + } + + particle.y = Math.max(0, Math.min(particle.y, gameBoard.offsetHeight - particle.size)); + particle.element.style.top = `${particle.y}px`; +}); +} + +if (gameState.entanglementEnabled) { +const centerX = gameBoard.offsetWidth / 2; +const centerY = gameBoard.offsetHeight / 2; + +gameState.particles.forEach(particle => { + const dx = particle.x - centerX; + const dy = particle.y - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + const force = 0.5; + + particle.vx -= (dx / distance) * force; + particle.vy -= (dy / distance) * force; + + particle.x += particle.vx; + particle.y += particle.vy; + + + particle.vx *= 0.95; + particle.vy *= 0.95; + + + particle.x = Math.max(0, Math.min(particle.x, gameBoard.offsetWidth - particle.size)); + particle.y = Math.max(0, Math.min(particle.y, gameBoard.offsetHeight - particle.size)); + + particle.element.style.left = `${particle.x}px`; + particle.element.style.top = `${particle.y}px`; +}); +} + +if (gameState.attractionEnabled) { +for (let i = 0; i < gameState.particles.length; i++) { + for (let j = i + 1; j < gameState.particles.length; j++) { + const p1 = gameState.particles[i]; + const p2 = gameState.particles[j]; + + const dx = p2.x - p1.x; + const dy = p2.y - p1.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance > 0) { + const force = 0.5 / distance; + + p1.vx += dx * force * 0.05; + p1.vy += dy * force * 0.05; + p2.vx -= dx * force * 0.05; + p2.vy -= dy * force * 0.05; + } + } +} +} + +if (gameState.repulsionEnabled) { +for (let i = 0; i < gameState.particles.length; i++) { + for (let j = i + 1; j < gameState.particles.length; j++) { + const p1 = gameState.particles[i]; + const p2 = gameState.particles[j]; + + const dx = p2.x - p1.x; + const dy = p2.y - p1.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance > 0 && distance < 150) { + const force = 1 / (distance * distance); + + p1.vx -= dx * force * 0.1; + p1.vy -= dy * force * 0.1; + p2.vx += dx * force * 0.1; + p2.vy += dy * force * 0.1; + } + } +} +} +gameState.particles.forEach(particle => { +particle.x += particle.vx; +particle.y += particle.vy; + +particle.x = Math.max(0, Math.min(particle.x, gameBoard.offsetWidth - particle.size)); +particle.y = Math.max(0, Math.min(particle.y, gameBoard.offsetHeight - particle.size)); + +particle.element.style.left = `${particle.x}px`; +particle.element.style.top = `${particle.y}px`; +}); + +checkWinCondition(); +requestAnimationFrame(applyQuantumRules); +} +function setupEventListeners() { +gravityBtn.addEventListener('click', () => { +gameState.gravityEnabled = !gameState.gravityEnabled; +gameState.moves++; +updateDisplay(); + +if (gameState.gravityEnabled) { + gravityBtn.style.background = 'linear-gradient(90deg, #e53f8c, #ff8a00)'; + gameState.activeRule = "Gravity Inversion"; +} else { + gravityBtn.style.background = 'linear-gradient(90deg, #4a3fe0, #e53f8c)'; + gameState.activeRule = gameState.quantumRules[gameState.level % gameState.quantumRules.length]; +} + +updateDisplay(); +}); + +quantumBtn.addEventListener('click', () => { +gameState.entanglementEnabled = !gameState.entanglementEnabled; +gameState.moves++; +updateDisplay(); + +if (gameState.entanglementEnabled) { + quantumBtn.style.background = 'linear-gradient(90deg, #e53f8c, #ff8a00)'; + gameState.activeRule = "Quantum Entanglement"; +} else { + quantumBtn.style.background = 'linear-gradient(90deg, #4a3fe0, #e53f8c)'; + gameState.activeRule = gameState.quantumRules[gameState.level % gameState.quantumRules.length]; +} + +updateDisplay(); +}); + +attractBtn.addEventListener('click', () => { +gameState.attractionEnabled = !gameState.attractionEnabled; +gameState.repulsionEnabled = false; +gameState.moves++; +updateDisplay(); + +if (gameState.attractionEnabled) { + attractBtn.style.background = 'linear-gradient(90deg, #e53f8c, #ff8a00)'; + repelBtn.style.background = 'linear-gradient(90deg, #4a3fe0, #e53f8c)'; + gameState.activeRule = "Particle Attraction"; +} else { + attractBtn.style.background = 'linear-gradient(90deg, #4a3fe0, #e53f8c)'; + gameState.activeRule = gameState.quantumRules[gameState.level % gameState.quantumRules.length]; +} + +updateDisplay(); +}); + +repelBtn.addEventListener('click', () => { +gameState.repulsionEnabled = !gameState.repulsionEnabled; +gameState.attractionEnabled = false; +gameState.moves++; +updateDisplay(); + +if (gameState.repulsionEnabled) { + repelBtn.style.background = 'linear-gradient(90deg, #e53f8c, #ff8a00)'; + attractBtn.style.background = 'linear-gradient(90deg, #4a3fe0, #e53f8c)'; + gameState.activeRule = "Particle Repulsion"; +} else { + repelBtn.style.background = 'linear-gradient(90deg, #4a3fe0, #e53f8c)'; + gameState.activeRule = gameState.quantumRules[gameState.level % gameState.quantumRules.length]; +} + +updateDisplay(); +}); + +resetBtn.addEventListener('click', () => { +createParticles(); +gameState.moves++; +updateDisplay(); +}); + +continueBtn.addEventListener('click', () => { +gameState.level++; +gameState.moves = 0; +gameState.accuracy = 0; + +gameState.activeRule = gameState.quantumRules[gameState.level % gameState.quantumRules.length]; + +gameState.gravityEnabled = false; +gameState.entanglementEnabled = false; +gameState.attractionEnabled = false; +gameState.repulsionEnabled = false; + +gravityBtn.style.background = 'linear-gradient(90deg, #4a3fe0, #e53f8c)'; +quantumBtn.style.background = 'linear-gradient(90deg, #4a3fe0, #e53f8c)'; +attractBtn.style.background = 'linear-gradient(90deg, #4a3fe0, #e53f8c)'; +repelBtn.style.background = 'linear-gradient(90deg, #4a3fe0, #e53f8c)'; + +createParticles(); +createTargetPattern(); +updateDisplay(); +winMessage.classList.remove('active'); +}); +} + +window.onload = function() { +initGame(); +applyQuantumRules(); +}; \ No newline at end of file diff --git a/games/quantum-quirk/style.css b/games/quantum-quirk/style.css new file mode 100644 index 00000000..1d88a6e1 --- /dev/null +++ b/games/quantum-quirk/style.css @@ -0,0 +1,221 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +body { + background: linear-gradient(135deg, #0f0c29, #302b63, #24243e); + color: #fff; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + overflow-x: hidden; +} + +header { + text-align: center; + margin-bottom: 20px; + width: 100%; +} + +h1 { + font-size: 3rem; + margin-bottom: 10px; + background: linear-gradient(90deg, #ff8a00, #e52e71); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + text-shadow: 0 0 15px rgba(229, 46, 113, 0.5); +} + +.subtitle { + font-size: 1.2rem; + color: #a0a0e0; + margin-bottom: 30px; +} + +.game-container { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 30px; + max-width: 1200px; + width: 100%; +} + +.game-board { + background: rgba(16, 18, 27, 0.7); + border-radius: 15px; + padding: 20px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + width: 500px; + height: 500px; + position: relative; + overflow: hidden; + border: 2px solid #4a3fe0; +} + +.target-pattern { + background: rgba(16, 18, 27, 0.7); + border-radius: 15px; + padding: 20px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + width: 500px; + height: 500px; + position: relative; + overflow: hidden; + border: 2px solid #e53f8c; +} + +.controls { + display: flex; + flex-direction: column; + gap: 15px; + width: 100%; + max-width: 500px; + margin-top: 20px; +} + +.control-row { + display: flex; + justify-content: space-between; + gap: 10px; +} + +button { + background: linear-gradient(90deg, #4a3fe0, #e53f8c); + border: none; + color: white; + padding: 12px 20px; + border-radius: 8px; + cursor: pointer; + font-size: 1rem; + font-weight: bold; + transition: all 0.3s ease; + flex: 1; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); +} + +button:hover { + transform: translateY(-3px); + box-shadow: 0 6px 15px rgba(0, 0, 0, 0.4); +} + +button:active { + transform: translateY(1px); +} + +.rules { + background: rgba(16, 18, 27, 0.7); + border-radius: 15px; + padding: 20px; + margin-top: 20px; + max-width: 1030px; + width: 100%; + border: 2px solid #00c9ff; +} + +.rules h2 { + color: #00c9ff; + margin-bottom: 10px; +} + +.rules p { + margin-bottom: 10px; + line-height: 1.5; +} + +.particle { + position: absolute; + border-radius: 50%; + transition: all 0.5s ease; + box-shadow: 0 0 10px currentColor; +} + +.quantum-field { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + pointer-events: none; + opacity: 0.3; +} + +.stats { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 1030px; + margin-bottom: 20px; +} + +.stat-box { + background: rgba(16, 18, 27, 0.7); + border-radius: 10px; + padding: 15px; + text-align: center; + flex: 1; + margin: 0 10px; + border: 1px solid #4a3fe0; +} + +.stat-value { + font-size: 2rem; + font-weight: bold; + color: #ff8a00; +} + +.win-message { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 100; + opacity: 0; + pointer-events: none; + transition: opacity 0.5s ease; +} + +.win-message.active { + opacity: 1; + pointer-events: all; +} + +.win-content { + background: linear-gradient(135deg, #0f0c29, #302b63); + padding: 40px; + border-radius: 20px; + text-align: center; + max-width: 500px; + width: 90%; + border: 3px solid #ff8a00; + box-shadow: 0 0 30px rgba(255, 138, 0, 0.7); +} + +.win-content h2 { + font-size: 3rem; + margin-bottom: 20px; + color: #ff8a00; +} + +.quantum-rule { + position: absolute; + top: 10px; + right: 10px; + background: rgba(16, 18, 27, 0.8); + padding: 10px; + border-radius: 8px; + border: 1px solid #00c9ff; + font-size: 0.9rem; +} \ No newline at end of file diff --git a/games/quick-draw/index.html b/games/quick-draw/index.html new file mode 100644 index 00000000..b81549c6 --- /dev/null +++ b/games/quick-draw/index.html @@ -0,0 +1,103 @@ + + + + + + Quick Draw | Mini JS Games Hub + + + + + + + +
    +
    +
    +

    Quick Draw

    +

    Wait for DRAW! โ€” shoot faster than the AI. Don't shoot early!

    +
    +
    + + + + + + + +
    +
    + +
    +
    +
    +
    + AI opponent +
    AI
    +
    AI: 0
    +
    + +
    +
    WAIT...
    +
    + + + +
    +
    Ready
    +
    +
    +
    + +
    + Player +
    You
    +
    You: 0
    +
    +
    + +
    +
    + + 0 / 0 +
    +
    + + โ€” +
    +
    + + โ€” +
    +
    +
    + +
    +

    How to play

    +
      +
    • Click Start. Wait for the big DRAW! signal.
    • +
    • When you see DRAW!, press the Shoot button or press Space/Enter.
    • +
    • Shoot too early and you automatically lose the round.
    • +
    • Modes: Best of 3 or Timed 30s mode.
    • +
    +
    + + +
    +
    + + + + diff --git a/games/quick-draw/script.js b/games/quick-draw/script.js new file mode 100644 index 00000000..b373e9ae --- /dev/null +++ b/games/quick-draw/script.js @@ -0,0 +1,438 @@ +// Quick Draw โ€” advanced version +// Place at games/quick-draw/script.js + +(() => { + // Elements + const startBtn = document.getElementById('start-btn'); + const pauseBtn = document.getElementById('pause-btn'); + const restartBtn = document.getElementById('restart-btn'); + const shootBtn = document.getElementById('shoot-btn'); + const signalEl = document.getElementById('signal'); + const roundResultEl = document.getElementById('round-result'); + const aiScoreEl = document.getElementById('ai-score'); + const playerScoreEl = document.getElementById('player-score'); + const roundInfo = document.getElementById('round-info'); + const lastReaction = document.getElementById('last-reaction'); + const bestScoreEl = document.getElementById('best-score'); + const countdownEl = document.getElementById('countdown'); + const modeTimerEl = document.getElementById('mode-timer'); + + const modeSelect = document.getElementById('mode-select'); + const difficultySelect = document.getElementById('difficulty-select'); + + // Sound assets (online) + const SOUND_DRAW = 'https://actions.google.com/sounds/v1/alarms/beep_short.ogg'; + const SOUND_SHOT = 'https://actions.google.com/sounds/v1/impacts/pop.ogg'; + const SOUND_FOUL = 'https://actions.google.com/sounds/v1/human_voices/oh_no.ogg'; + const SOUND_WIN = 'https://actions.google.com/sounds/v1/alarms/winding_alert.ogg'; + + const sDraw = new Audio(SOUND_DRAW); + const sShot = new Audio(SOUND_SHOT); + const sFoul = new Audio(SOUND_FOUL); + const sWin = new Audio(SOUND_WIN); + + // State + let running = false; + let paused = false; + let mode = 'bestof3'; // or 'timed' + let difficulty = 'normal'; + let roundsTotal = 3; + let roundNumber = 0; + let playerScore = 0; + let aiScore = 0; + let currentRoundActive = false; + let drawTime = 0; + let playerFired = false; + let aiFired = false; + let aiTimeout = null; + let drawTimeout = null; + let modeTimer = null; + let timedLeft = 30; // 30s for timed mode + const storageKey = 'quickdraw_best'; + + // Settings mapping per difficulty (AI reaction range in ms) + const difficultyMap = { + easy: { aiMin: 300, aiMax: 700 }, + normal: { aiMin: 180, aiMax: 380 }, + hard: { aiMin: 90, aiMax: 240 } + }; + + // Helpers + function resetRoundState(){ + currentRoundActive = false; + playerFired = false; + aiFired = false; + clearTimeout(aiTimeout); + clearTimeout(drawTimeout); + signalEl.classList.remove('draw'); + signalEl.classList.add('wait'); + signalEl.textContent = 'WAIT...'; + shootBtn.disabled = true; + roundResultEl.textContent = 'Ready'; + } + + function updateHUD(){ + aiScoreEl.textContent = `AI: ${aiScore}`; + playerScoreEl.textContent = `You: ${playerScore}`; + roundInfo.textContent = `${roundNumber} / ${roundsTotal}`; + bestScoreEl.textContent = localStorage.getItem(storageKey) || 'โ€”'; + } + + function enableControls(state){ + startBtn.disabled = state; + pauseBtn.disabled = !state; + restartBtn.disabled = !state; + } + + function playSound(audio){ + // try-catch to avoid console noise in blocked environments + try { + audio.currentTime = 0; + audio.play().catch(()=>{/* ignored */}); + } catch(e){} + } + + function randomBetween(min,max){ return Math.floor(Math.random()*(max-min))+min; } + + function scheduleDraw(){ + // Random delay before draw (1 โ€” 2.8s) to avoid predictability + const delay = randomBetween(1000, 2800); + signalEl.textContent = 'Get Ready...'; + countdownEl.textContent = ''; + drawTimeout = setTimeout(() => { + // draw signal + signalEl.classList.remove('wait'); + signalEl.classList.add('draw'); + signalEl.textContent = 'DRAW!'; + playSound(sDraw); + drawTime = performance.now(); + currentRoundActive = true; + playerFired = false; + aiFired = false; + shootBtn.disabled = false; + + // Schedule AI reaction + const diff = difficultyMap[difficulty] || difficultyMap.normal; + const aiReact = randomBetween(diff.aiMin, diff.aiMax); + aiTimeout = setTimeout(() => aiShoot('ai'), aiReact); + + // Visual flash for draw + flashSignal(); + }, delay); + } + + function flashSignal(){ + // small animation loop for glow pulsing + signalEl.animate([ + { boxShadow: '0 8px 30px rgba(255,204,0,0.16)', transform: 'scale(1)' }, + { boxShadow: '0 20px 64px rgba(255,204,0,0.28)', transform: 'scale(1.02)' }, + { boxShadow: '0 8px 30px rgba(255,204,0,0.16)', transform: 'scale(1)' } + ], { duration: 600, iterations: 2 }); + } + + function aiShoot(role='ai'){ + if(!currentRoundActive || aiFired) return; + aiFired = true; + const t = performance.now(); + const rt = t - drawTime; + // If player hasn't fired yet, AI wins + if(!playerFired){ + // AI wins + aiScore++; + roundResultEl.textContent = `AI shot: ${Math.round(rt)}ms โ€” AI wins!`; + playSound(sWin); + lastReaction.textContent = `${Math.round(rt)} ms (AI)`; + endRound('ai'); + } + } + + function endRound(winner){ + currentRoundActive = false; + shootBtn.disabled = true; + clearTimeout(aiTimeout); + clearTimeout(drawTimeout); + + if(mode === 'bestof3'){ + // check if match ended + if(aiScore > Math.floor(roundsTotal/2) || playerScore > Math.floor(roundsTotal/2) || roundNumber >= roundsTotal){ + finishMatch(); + } else { + // small pause then next round + setTimeout(() => { + roundNumber++; + resetRoundState(); + updateHUD(); + scheduleRound(); + }, 900); + } + } else if(mode === 'timed'){ + // in timed mode, we just proceed; rounds are for display + setTimeout(() => { + roundNumber++; + resetRoundState(); + updateHUD(); + scheduleRound(); + }, 600); + } else { + setTimeout(() => { + roundNumber++; + resetRoundState(); + updateHUD(); + scheduleRound(); + }, 600); + } + } + + function finishMatch(){ + running = false; + enableControls(false); + pauseBtn.disabled = true; + startBtn.disabled = false; + shootBtn.disabled = true; + // determine winner + let message = ''; + if(playerScore === aiScore) message = `Match tied ${playerScore} - ${aiScore}`; + else if(playerScore > aiScore) { + message = `You win ${playerScore} : ${aiScore} โ€” Great shot!`; + playSound(sWin); + saveBest(playerScore); + } else { + message = `You lost ${playerScore} : ${aiScore} โ€” AI was quicker.`; + playSound(sFoul); + } + roundResultEl.textContent = message; + updateHUD(); + } + + function saveBest(score){ + const prev = parseInt(localStorage.getItem(storageKey) || '0', 10) || 0; + if(score > prev){ + localStorage.setItem(storageKey, score); + bestScoreEl.textContent = score; + } + } + + // Player shot action + function playerShoot(){ + if(!running) return; + if(paused) return; + const now = performance.now(); + // if shot before draw signal -> foul + if(!currentRoundActive){ + // Early fire + playSound(sFoul); + aiScore++; + roundResultEl.textContent = 'Too early! Foul โ€” AI wins that round.'; + lastReaction.textContent = 'Foul'; + endRound('ai'); + return; + } + if(playerFired) return; // ignore multi-shots + playerFired = true; + const reaction = now - drawTime; + lastReaction.textContent = `${Math.round(reaction)} ms`; + playSound(sShot); + + // If AI hasn't fired yet, player wins + if(!aiFired){ + playerScore++; + roundResultEl.textContent = `You shot: ${Math.round(reaction)}ms โ€” You win!`; + endRound('player'); + } else { + // AI already fired => compare times + roundResultEl.textContent = `Too slow! AI fired earlier.`; + endRound('ai'); + } + updateHUD(); + } + + // Input handlers + shootBtn.addEventListener('click', () => playerShoot()); + document.addEventListener('keydown', (e) => { + if([' ','Spacebar','Enter'].includes(e.key)){ + e.preventDefault(); + if(!shootBtn.disabled) playerShoot(); + } + // quick shortcuts + if(e.key === 'p' || e.key === 'P') togglePause(); + if(e.key === 'r' || e.key === 'R') restartGame(); + }); + + // Pause / resume + function togglePause(){ + if(!running) return; + paused = !paused; + pauseBtn.textContent = paused ? 'Resume' : 'Pause'; + if(paused){ + // freeze timers + clearTimeout(aiTimeout); + clearTimeout(drawTimeout); + clearInterval(modeTimer); + roundResultEl.textContent = 'Paused'; + shootBtn.disabled = true; + } else { + // resume: if draw not yet triggered, schedule small delay before draw to avoid instant triggers + roundResultEl.textContent = 'Resuming...'; + if(!currentRoundActive){ + drawTimeout = setTimeout(scheduleDraw, 350); + } else { + // current round active but no AI timer? schedule AI to react + if(!aiFired){ + const diff = difficultyMap[difficulty] || difficultyMap.normal; + const aiReact = randomBetween(diff.aiMin, diff.aiMax); + aiTimeout = setTimeout(() => aiShoot('ai'), aiReact); + } + shootBtn.disabled = false; + } + // resume mode timer if timed mode + if(mode === 'timed'){ + startModeTimer(); + } + } + } + + pauseBtn.addEventListener('click', togglePause); + + // Restart + function restartGame(){ + clearAll(); + initState(); + updateHUD(); + roundResultEl.textContent = 'Restarted'; + startBtn.disabled = false; + pauseBtn.disabled = true; + restartBtn.disabled = true; + } + restartBtn.addEventListener('click', restartGame); + + function clearAll(){ + running = false; paused = false; + clearTimeout(aiTimeout); clearTimeout(drawTimeout); clearInterval(modeTimer); + } + + // Start match + function startGame(){ + if(running) return; + running = true; + paused = false; + playerScore = 0; aiScore = 0; roundNumber = 1; + mode = modeSelect.value; + difficulty = difficultySelect.value; + + if(mode === 'bestof3'){ + roundsTotal = 3; + timedLeft = 0; + } else { + roundsTotal = 9999; // arbitrary + timedLeft = 30; + } + + enableControls(true); + updateHUD(); + + // schedule first round + resetRoundState(); + scheduleRound(); + + // start mode timer if timed + if(mode === 'timed'){ + startModeTimer(); + } + } + + function scheduleRound(){ + // small pre-round bounce + signalEl.classList.remove('draw'); + signalEl.classList.add('wait'); + signalEl.textContent = 'Wait...'; + shootBtn.disabled = true; + + // small visual countdown supplement (3..1) + let mini = 3; + countdownEl.textContent = ''; + const miniTick = setInterval(() => { + if(paused || !running){ clearInterval(miniTick); countdownEl.textContent = ''; return; } + if(mini <= 1){ + clearInterval(miniTick); + countdownEl.textContent = ''; + // actual draw scheduled randomly to avoid predictability + scheduleDraw(); + return; + } + countdownEl.textContent = `${mini-1}`; + mini--; + }, 240); + + updateHUD(); + } + + // Mode timer for timed mode + function startModeTimer(){ + clearInterval(modeTimer); + modeTimerEl.textContent = `Time: ${timedLeft}s`; + modeTimer = setInterval(() => { + if(paused || !running) return; + timedLeft--; + modeTimerEl.textContent = `Time: ${timedLeft}s`; + if(timedLeft <= 0){ + clearInterval(modeTimer); + // finish match + finishMatch(); + } + }, 1000); + } + + // Setup UI & initialization + function initState(){ + running = false; + paused = false; + playerScore = 0; aiScore = 0; + roundNumber = 0; + currentRoundActive = false; + shootBtn.disabled = true; + startBtn.disabled = false; + pauseBtn.disabled = true; + restartBtn.disabled = true; + signalEl.classList.remove('draw'); + signalEl.classList.add('wait'); + signalEl.textContent = 'WAIT...'; + countdownEl.textContent = ''; + modeTimerEl.textContent = ''; + lastReaction.textContent = 'โ€”'; + } + + // Start button + startBtn.addEventListener('click', () => { + mode = modeSelect.value; + difficulty = difficultySelect.value; + startBtn.disabled = true; + pauseBtn.disabled = false; + restartBtn.disabled = false; + startGame(); + }); + + // End of match cleanup + function endAll(){ + clearTimeout(aiTimeout); + clearTimeout(drawTimeout); + clearInterval(modeTimer); + running = false; + paused = false; + enableControls(false); + } + + // Save best when page unload (optional) + window.addEventListener('beforeunload', () => { + const prev = parseInt(localStorage.getItem(storageKey) || '0',10) || 0; + if(playerScore > prev) localStorage.setItem(storageKey, playerScore); + }); + + // Attach shoot via pointer down (fast) + shootBtn.addEventListener('pointerdown', () => { + playerShoot(); + }); + + // initialize + initState(); + updateHUD(); +})(); diff --git a/games/quick-draw/style.css b/games/quick-draw/style.css new file mode 100644 index 00000000..12894bb1 --- /dev/null +++ b/games/quick-draw/style.css @@ -0,0 +1,217 @@ +:root{ + --bg:#0f1724; + --panel:#0b1220; + --muted:#9aa7b2; + --accent:#ffcc00; + --danger:#ff4d4f; + --glass: rgba(255,255,255,0.04); + --glow: 0 8px 30px rgba(255,204,0,0.18), 0 2px 6px rgba(0,0,0,0.6); + --radius:14px; + --glass-2: rgba(255,255,255,0.02); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; +} + +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + background: linear-gradient(180deg,#071129 0%, #071327 60%); + color:#e6eef6; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + display:flex; + align-items:center; + justify-content:center; + padding:24px; +} + +.wrapper{ + width:100%; + max-width:1100px; + background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border-radius:20px; + padding:18px; + box-shadow: 0 20px 60px rgba(2,6,23,0.7); + border:1px solid rgba(255,255,255,0.03); +} + +.qd-header{ + display:flex; + align-items:center; + justify-content:space-between; + gap:18px; + margin-bottom:18px; +} + +.qd-title h1{ + margin:0; + font-size:28px; + letter-spacing:0.4px; +} +.subtitle{margin:6px 0 0;color:var(--muted);font-size:13px} + +.qd-controls{ + display:flex; + gap:10px; + align-items:center; +} + +select, .btn { + background:var(--glass); + border:1px solid rgba(255,255,255,0.04); + padding:8px 12px; + color: #e6eef6; + border-radius:10px; + font-size:14px; + outline:none; +} + +.btn{cursor:pointer; box-shadow: 0 4px 18px rgba(2,6,23,0.6); transition:transform .12s ease, box-shadow .12s} +.btn.primary{ + background: linear-gradient(90deg,#ffdc64,#ffb347); + color:#061018; + font-weight:700; + box-shadow: var(--glow); +} + +.btn:active{transform:translateY(1px)} +.btn[disabled]{opacity:.5; cursor:not-allowed; transform:none} + +.qd-main{padding: 10px 6px 18px} + +.arena{ + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.04)); + border-radius:14px; + padding:18px; + border:1px solid rgba(255,255,255,0.02); +} + +.target-area{ + display:flex; + align-items:center; + justify-content:space-between; + gap:20px; + padding: 12px; +} + +.baddie{ + width:210px; + background:linear-gradient(180deg, rgba(255,255,255,0.01), rgba(255,255,255,0.02)); + border-radius:12px; + padding:12px; + text-align:center; + box-shadow: 0 6px 24px rgba(0,0,0,0.6); + border: 1px solid rgba(255,255,255,0.02); + position:relative; +} + +.baddie-img{ + width:100%; + height:120px; + object-fit:cover; + border-radius:8px; + display:block; + margin-bottom:8px; + filter: contrast(.95) saturate(.9); +} + +.baddie .name{font-weight:700; margin-bottom:6px} +.baddie .score{font-size:13px;color:var(--muted)} + +.center-panel{ + flex:1; + display:flex; + flex-direction:column; + align-items:center; + gap:12px; + padding:10px; +} + +.signal{ + width:360px; + max-width:100%; + text-align:center; + font-weight:800; + font-size:34px; + padding:18px 16px; + border-radius:14px; + letter-spacing:2px; + box-shadow: 0 10px 36px rgba(1,6,24,0.6); + border: 1px solid rgba(255,255,255,0.02); + transition: all .18s ease; + background: linear-gradient(180deg, rgba(255,255,255,0.01), rgba(255,255,255,0.02)); +} + +.signal.wait{ + color:var(--muted); + text-shadow:none; + transform:translateY(0); +} + +.signal.draw{ + color:#061018; + background: linear-gradient(90deg,#ffdc64,#ffb347); + box-shadow: var(--glow); + transform:scale(1.03); +} + +.shoot-btn{ + margin-top:8px; + font-size:18px; + padding:12px 28px; + border-radius:999px; + border: none; + outline:none; + cursor:pointer; + background: linear-gradient(90deg,#fff 0%, #ffc 40%); + color:#111; + font-weight:800; + box-shadow: 0 14px 30px rgba(0,0,0,0.5), 0 6px 18px rgba(255,204,0,0.12); + transition: transform .06s ease; +} + +.shoot-btn:active{ transform: translateY(1px) scale(.997) } +.shoot-btn[disabled]{opacity:.48;filter:grayscale(.2);cursor:not-allowed} + +.controls-row{ + display:flex; + gap:18px; + align-items:center; + margin-top:8px; +} + +.result{ + font-weight:700; + padding:8px 10px; + border-radius:12px; + background:var(--glass-2); + color:var(--muted); +} + +.timer{ color:var(--muted); font-size:14px } + +.hud{ + display:flex; + gap:18px; + margin-top:14px; + align-items:center; + justify-content:space-between; + padding:8px 4px; +} + +.hud .stat{ font-size:13px; color:var(--muted) } +.hud label{ color:var(--muted); margin-right:6px; font-weight:600; } + +.howto{ margin-top:18px; color:var(--muted); font-size:14px; line-height:1.6} +.howto ul{padding-left:18px} + +.qd-footer{ display:flex; justify-content:space-between; align-items:center; margin-top:14px; color:var(--muted) } +.hub-link{ color: #dbeafe; opacity:.9; text-decoration:none; } +.credits{ font-size:12px } + +@media (max-width:880px){ + .target-area{flex-direction:column} + .baddie{width:100%} + .center-panel{width:100%} + .signal{font-size:26px} +} diff --git a/games/quick-math-battle-quiz/index.html b/games/quick-math-battle-quiz/index.html new file mode 100644 index 00000000..42eea844 --- /dev/null +++ b/games/quick-math-battle-quiz/index.html @@ -0,0 +1,33 @@ + + + + + + Quick Math Battle Quiz + + + +
    +

    Quick Math Battle Quiz

    +
    +
    Score: 0
    +
    Time Left: 10s
    +
    + +
    +

    Loading...

    + + +

    +
    + + +
    + + + + diff --git a/games/quick-math-battle-quiz/script.js b/games/quick-math-battle-quiz/script.js new file mode 100644 index 00000000..b2805f8d --- /dev/null +++ b/games/quick-math-battle-quiz/script.js @@ -0,0 +1,91 @@ +const questionEl = document.getElementById("question"); +const answerInput = document.getElementById("answer"); +const submitBtn = document.getElementById("submit-btn"); +const feedbackEl = document.getElementById("feedback"); +const scoreEl = document.getElementById("score"); +const timerEl = document.getElementById("timer"); +const resultContainer = document.querySelector(".result-container"); +const finalScoreEl = document.getElementById("final-score"); +const restartBtn = document.getElementById("restart-btn"); + +let score = 0; +let timeLeft = 10; +let currentAnswer = 0; +let timer; + +function generateQuestion() { + const operations = ["+", "-", "*", "/"]; + const num1 = Math.floor(Math.random() * 20) + 1; + const num2 = Math.floor(Math.random() * 20) + 1; + const op = operations[Math.floor(Math.random() * operations.length)]; + + switch(op) { + case "+": + currentAnswer = num1 + num2; + break; + case "-": + currentAnswer = num1 - num2; + break; + case "*": + currentAnswer = num1 * num2; + break; + case "/": + currentAnswer = parseFloat((num1 / num2).toFixed(2)); + break; + } + questionEl.textContent = `What is ${num1} ${op} ${num2}?`; + answerInput.value = ""; + feedbackEl.textContent = ""; + answerInput.focus(); + timeLeft = 10; + timerEl.textContent = timeLeft; + clearInterval(timer); + timer = setInterval(() => { + timeLeft--; + timerEl.textContent = timeLeft; + if(timeLeft <= 0) { + clearInterval(timer); + feedbackEl.textContent = `โฐ Time's up! Answer was ${currentAnswer}`; + setTimeout(generateQuestion, 1500); + } + }, 1000); +} + +submitBtn.addEventListener("click", () => { + checkAnswer(); +}); + +answerInput.addEventListener("keypress", (e) => { + if(e.key === "Enter") checkAnswer(); +}); + +function checkAnswer() { + const userAnswer = parseFloat(answerInput.value); + if(userAnswer === currentAnswer) { + feedbackEl.textContent = "โœ… Correct!"; + score += 10; + } else { + feedbackEl.textContent = `โŒ Wrong! Answer was ${currentAnswer}`; + } + scoreEl.textContent = score; + clearInterval(timer); + setTimeout(generateQuestion, 1000); +} + +function endGame() { + clearInterval(timer); + questionEl.parentElement.hidden = true; + resultContainer.hidden = false; + finalScoreEl.textContent = score; +} + +restartBtn.addEventListener("click", () => { + score = 0; + scoreEl.textContent = score; + questionEl.parentElement.hidden = false; + resultContainer.hidden = true; + generateQuestion(); +}); + +// Start the first question +generateQuestion(); diff --git a/games/quick-math-battle-quiz/style.css b/games/quick-math-battle-quiz/style.css new file mode 100644 index 00000000..7387f9ed --- /dev/null +++ b/games/quick-math-battle-quiz/style.css @@ -0,0 +1,91 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(to right, #ffecd2, #fcb69f); + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; +} + +.quiz-container { + background-color: rgba(255,255,255,0.95); + padding: 30px 40px; + border-radius: 15px; + box-shadow: 0 8px 20px rgba(0,0,0,0.2); + width: 100%; + max-width: 450px; + text-align: center; +} + +h1 { + margin-bottom: 20px; + color: #333; +} + +.scoreboard { + display: flex; + justify-content: space-between; + font-size: 18px; + margin-bottom: 20px; + font-weight: bold; +} + +.question-container { + margin-bottom: 20px; +} + +#question { + font-size: 22px; + margin-bottom: 10px; + color: #444; +} + +#answer { + padding: 10px; + font-size: 16px; + width: 80%; + border-radius: 8px; + border: 1px solid #ccc; +} + +#submit-btn { + padding: 10px 20px; + margin-left: 10px; + border: none; + background-color: #ff6b6b; + color: white; + font-size: 16px; + border-radius: 8px; + cursor: pointer; + transition: background 0.3s; +} + +#submit-btn:hover { + background-color: #ff4757; +} + +#feedback { + margin-top: 10px; + font-size: 16px; + height: 24px; +} + +.result-container h2 { + margin-bottom: 10px; + color: #ff6b6b; +} + +#restart-btn { + padding: 10px 20px; + font-size: 16px; + border-radius: 8px; + border: none; + background-color: #1dd1a1; + color: white; + cursor: pointer; +} + +#restart-btn:hover { + background-color: #10ac84; +} diff --git a/games/quiz-game-new/index.html b/games/quiz-game-new/index.html new file mode 100644 index 00000000..bb50e67b --- /dev/null +++ b/games/quiz-game-new/index.html @@ -0,0 +1,35 @@ + + + + + + Simple Quiz Game + + + +
    +
    + Score: 0 + Question: 1/5 +
    + +
    +

    + +
    +
    + + + +
    + + +
    + + + + \ No newline at end of file diff --git a/games/quiz-game-new/script.js b/games/quiz-game-new/script.js new file mode 100644 index 00000000..3c5620e5 --- /dev/null +++ b/games/quiz-game-new/script.js @@ -0,0 +1,145 @@ +// --- 1. Question Data --- +const questions = [ + { + question: "What is the capital of France?", + choices: ["Berlin", "Madrid", "Paris", "Rome"], + correct: "Paris" + }, + { + question: "Which planet is known as the Red Planet?", + choices: ["Mars", "Venus", "Jupiter", "Saturn"], + correct: "Mars" + }, + { + question: "What is the largest ocean on Earth?", + choices: ["Atlantic Ocean", "Indian Ocean", "Arctic Ocean", "Pacific Ocean"], + correct: "Pacific Ocean" + }, + { + question: "In which year did the first man walk on the moon?", + choices: ["1965", "1969", "1971", "1975"], + correct: "1969" + }, + { + question: "What is 7 multiplied by 8?", + choices: ["54", "56", "64", "49"], + correct: "56" + } +]; + +// --- 2. Game State Variables --- +let currentQuestionIndex = 0; +let score = 0; +let answered = false; // Flag to prevent multiple clicks per question + +// --- 3. DOM Element References --- +const scoreDisplay = document.getElementById('score'); +const questionCountDisplay = document.getElementById('question-count'); +const questionText = document.getElementById('question-text'); +const answerButtonsContainer = document.getElementById('answer-buttons'); +const feedbackElement = document.getElementById('feedback'); +const nextButton = document.getElementById('next-button'); +const quizArea = document.getElementById('quiz-area'); +const resultScreen = document.getElementById('result-screen'); +const finalScoreDisplay = document.getElementById('final-score'); +const restartButton = document.getElementById('restart-button'); + +// --- 4. Main Game Functions --- + +// Renders the current question to the screen +function displayQuestion() { + // Hide results screen and show quiz area + quizArea.classList.remove('hidden'); + resultScreen.classList.add('hidden'); + nextButton.classList.add('hidden'); + feedbackElement.classList.add('hidden'); + + // Update header + scoreDisplay.textContent = `Score: ${score}`; + questionCountDisplay.textContent = `Question: ${currentQuestionIndex + 1}/${questions.length}`; + + // Get current question data + const currentQuestion = questions[currentQuestionIndex]; + questionText.textContent = currentQuestion.question; + + // Clear previous buttons + answerButtonsContainer.innerHTML = ''; + + // Create new buttons for choices + currentQuestion.choices.forEach(choice => { + const button = document.createElement('button'); + button.textContent = choice; + button.classList.add('answer-btn'); + // Attach event listener to check the answer when clicked + button.addEventListener('click', () => checkAnswer(choice, currentQuestion.correct, button)); + answerButtonsContainer.appendChild(button); + }); + + answered = false; // Reset answered flag for the new question +} + +// Checks the player's selected answer +function checkAnswer(selectedChoice, correctAnswer, clickedButton) { + if (answered) return; // Prevent double-clicking + answered = true; + + // Disable all buttons + Array.from(answerButtonsContainer.children).forEach(button => { + button.disabled = true; + }); + + // Update feedback message and style + if (selectedChoice === correctAnswer) { + score++; + feedbackElement.textContent = 'โœ… Correct!'; + feedbackElement.className = 'correct'; + clickedButton.classList.add('correct'); + } else { + feedbackElement.textContent = `โŒ Incorrect. The correct answer was: ${correctAnswer}`; + feedbackElement.className = 'incorrect'; + clickedButton.classList.add('incorrect'); + + // Highlight the correct answer + Array.from(answerButtonsContainer.children).forEach(button => { + if (button.textContent === correctAnswer) { + button.classList.add('correct'); + } + }); + } + + feedbackElement.classList.remove('hidden'); + nextButton.classList.remove('hidden'); + scoreDisplay.textContent = `Score: ${score}`; // Update score immediately +} + +// Moves to the next question or ends the game +function nextQuestion() { + currentQuestionIndex++; + if (currentQuestionIndex < questions.length) { + displayQuestion(); + } else { + endGame(); + } +} + +// Shows the final score screen +function endGame() { + quizArea.classList.add('hidden'); + resultScreen.classList.remove('hidden'); + finalScoreDisplay.textContent = `You scored ${score} out of ${questions.length}!`; +} + +// Resets game state and starts over +function restartGame() { + currentQuestionIndex = 0; + score = 0; + displayQuestion(); +} + +// --- 5. Event Listeners --- +nextButton.addEventListener('click', nextQuestion); +restartButton.addEventListener('click', restartGame); + +// --- 6. Initialization --- +// Start the quiz when the script loads +displayQuestion(); \ No newline at end of file diff --git a/games/quiz-game-new/style.css b/games/quiz-game-new/style.css new file mode 100644 index 00000000..fb54a9aa --- /dev/null +++ b/games/quiz-game-new/style.css @@ -0,0 +1,119 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #f4f7f6; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; +} + +.quiz-container { + background: #ffffff; + padding: 30px; + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + width: 90%; + max-width: 600px; +} + +.header { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 2px solid #eee; + font-size: 1.1em; + font-weight: bold; + color: #333; +} + +#question-text { + font-size: 1.5em; + margin-bottom: 25px; + color: #4CAF50; /* Primary color */ +} + +/* --- Answer Buttons Grid --- */ +.btn-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); /* Two columns */ + gap: 15px; + margin-bottom: 20px; +} + +.answer-btn { + padding: 15px; + font-size: 1em; + border: 2px solid #ddd; + border-radius: 8px; + background-color: #f9f9f9; + cursor: pointer; + transition: background-color 0.2s, border-color 0.2s; +} + +.answer-btn:hover:not(:disabled) { + background-color: #e6e6e6; + border-color: #bbb; +} + +.answer-btn:disabled { + cursor: default; + opacity: 0.8; +} + +/* --- Feedback and Result Styling --- */ +.correct { + background-color: #d4edda !important; + border-color: #4CAF50 !important; + color: #155724; +} + +.incorrect { + background-color: #f8d7da !important; + border-color: #dc3545 !important; + color: #721c24; +} + +#feedback { + margin-bottom: 15px; + padding: 10px; + border-radius: 5px; + font-weight: bold; +} + +/* --- Navigation Buttons --- */ +#next-button, #restart-button { + width: 100%; + padding: 12px; + font-size: 1.2em; + border: none; + border-radius: 8px; + background-color: #4CAF50; + color: white; + cursor: pointer; + margin-top: 10px; + transition: background-color 0.2s; +} + +#next-button:hover, #restart-button:hover { + background-color: #45a049; +} + +/* --- Utility Classes --- */ +.hidden { + display: none; +} + +#result-screen { + text-align: center; + padding: 20px; + background: #e9ecef; + border-radius: 8px; +} + +#final-score { + font-size: 1.8em; + margin: 20px 0; + color: #333; +} \ No newline at end of file diff --git a/games/quiz-game/index.html b/games/quiz-game/index.html new file mode 100644 index 00000000..91d405f0 --- /dev/null +++ b/games/quiz-game/index.html @@ -0,0 +1,31 @@ + + + + + + Quiz Game (MCQ) | Mini JS Games Hub + + + +
    +

    ๐Ÿง  Quiz Game (MCQ)

    +
    +
    +
    Question 1 of 5
    +
    Score: 0
    +
    +

    Loading question...

    +
    + +
    + + +
    + + + + diff --git a/games/quiz-game/script.js b/games/quiz-game/script.js new file mode 100644 index 00000000..26797ffc --- /dev/null +++ b/games/quiz-game/script.js @@ -0,0 +1,111 @@ +const questions = [ + { + question: "What does HTML stand for?", + options: [ + "Hyper Text Markup Language", + "Home Tool Markup Language", + "Hyperlinks and Text Markup Language", + "High Transfer Markup Logic" + ], + answer: 0 + }, + { + question: "Which language runs in a web browser?", + options: ["Java", "C", "Python", "JavaScript"], + answer: 3 + }, + { + question: "What year was JavaScript launched?", + options: ["1996", "1995", "1994", "1997"], + answer: 1 + }, + { + question: "Which CSS property controls text size?", + options: ["font-weight", "text-style", "font-size", "text-size"], + answer: 2 + }, + { + question: "What does DOM stand for?", + options: [ + "Document Object Model", + "Display Object Management", + "Digital Ordinance Model", + "Desktop Oriented Mode" + ], + answer: 0 + } +]; + +let currentQuestion = 0; +let score = 0; + +const questionText = document.getElementById("question-text"); +const optionsContainer = document.getElementById("options"); +const nextBtn = document.getElementById("next-btn"); +const scoreDisplay = document.getElementById("score"); +const questionNumber = document.getElementById("question-number"); +const resultBox = document.getElementById("result-box"); +const quizBox = document.getElementById("quiz-box"); +const finalScore = document.getElementById("final-score"); +const restartBtn = document.getElementById("restart-btn"); + +function loadQuestion() { + const q = questions[currentQuestion]; + questionText.textContent = q.question; + optionsContainer.innerHTML = ""; + questionNumber.textContent = `Question ${currentQuestion + 1} of ${questions.length}`; + + q.options.forEach((opt, index) => { + const btn = document.createElement("div"); + btn.classList.add("option"); + btn.textContent = opt; + btn.addEventListener("click", () => selectAnswer(index, btn)); + optionsContainer.appendChild(btn); + }); + + nextBtn.disabled = true; +} + +function selectAnswer(selected, btn) { + const q = questions[currentQuestion]; + const allOptions = document.querySelectorAll(".option"); + + allOptions.forEach(o => o.classList.add("disabled")); + + if (selected === q.answer) { + btn.classList.add("correct"); + score++; + scoreDisplay.textContent = `Score: ${score}`; + } else { + btn.classList.add("wrong"); + allOptions[q.answer].classList.add("correct"); + } + + nextBtn.disabled = false; +} + +nextBtn.addEventListener("click", () => { + currentQuestion++; + if (currentQuestion < questions.length) { + loadQuestion(); + } else { + showResults(); + } +}); + +function showResults() { + quizBox.classList.add("hidden"); + resultBox.classList.remove("hidden"); + finalScore.textContent = `Your Final Score: ${score} / ${questions.length}`; +} + +restartBtn.addEventListener("click", () => { + currentQuestion = 0; + score = 0; + scoreDisplay.textContent = `Score: ${score}`; + resultBox.classList.add("hidden"); + quizBox.classList.remove("hidden"); + loadQuestion(); +}); + +loadQuestion(); diff --git a/games/quiz-game/style.css b/games/quiz-game/style.css new file mode 100644 index 00000000..2de098fc --- /dev/null +++ b/games/quiz-game/style.css @@ -0,0 +1,92 @@ +body { + font-family: 'Poppins', sans-serif; + background: linear-gradient(135deg, #4facfe, #00f2fe); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.quiz-container { + background: #fff; + border-radius: 16px; + box-shadow: 0 10px 30px rgba(0,0,0,0.15); + width: 400px; + padding: 30px; + text-align: center; + animation: fadeIn 0.8s ease; +} + +.quiz-title { + font-size: 24px; + margin-bottom: 20px; +} + +.quiz-header { + display: flex; + justify-content: space-between; + margin-bottom: 15px; +} + +.question-text { + font-size: 18px; + font-weight: 500; + margin-bottom: 20px; +} + +.options { + display: flex; + flex-direction: column; + gap: 10px; +} + +.option { + background: #f4f4f4; + padding: 10px 15px; + border-radius: 10px; + cursor: pointer; + border: 2px solid transparent; + transition: 0.3s ease; +} + +.option:hover { + background: #eaf6ff; +} + +.option.correct { + background: #d4edda; + border-color: #28a745; +} + +.option.wrong { + background: #f8d7da; + border-color: #dc3545; +} + +.next-btn, +.restart-btn { + margin-top: 20px; + background: #4facfe; + border: none; + color: white; + padding: 10px 20px; + font-size: 16px; + border-radius: 8px; + cursor: pointer; + transition: 0.3s; +} + +.next-btn:hover, +.restart-btn:hover { + background: #00c6ff; +} + +.hidden { + display: none; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} diff --git a/games/quote/index.html b/games/quote/index.html new file mode 100644 index 00000000..a191d58f --- /dev/null +++ b/games/quote/index.html @@ -0,0 +1,19 @@ + + + + + + Random Quote Generator + + + +
    +
    +

    Click the button to get inspired!

    +

    โ€”

    + +
    +
    + + + \ No newline at end of file diff --git a/games/quote/script.js b/games/quote/script.js new file mode 100644 index 00000000..668540af --- /dev/null +++ b/games/quote/script.js @@ -0,0 +1,18 @@ +const quotes = [ + { quote: "Be yourself; everyone else is already taken.", author: "Oscar Wilde" }, + { quote: "In the middle of difficulty lies opportunity.", author: "Albert Einstein" }, + { quote: "Success is not final, failure is not fatal: It is the courage to continue that counts.", author: "Winston Churchill" }, + { quote: "Do what you can, with what you have, where you are.", author: "Theodore Roosevelt" }, + { quote: "Happiness is not something ready made. It comes from your own actions.", author: "Dalai Lama" }, + { quote: "Everything you can imagine is real.", author: "Pablo Picasso" }, + { quote: "Turn your wounds into wisdom.", author: "Oprah Winfrey" } +]; + +function generateQuote() { + const randomIndex = Math.floor(Math.random() * quotes.length); + const quoteText = quotes[randomIndex].quote; + const quoteAuthor = quotes[randomIndex].author; + + document.getElementById("quote").textContent = quoteText; + document.getElementById("author").textContent = `โ€” ${quoteAuthor}`; +} \ No newline at end of file diff --git a/games/quote/style.css b/games/quote/style.css new file mode 100644 index 00000000..8b8407fe --- /dev/null +++ b/games/quote/style.css @@ -0,0 +1,57 @@ +body { + margin: 0; + font-family: 'Segoe UI', sans-serif; + background: linear-gradient(135deg, #74ebd5, #9face6); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.container { + text-align: center; +} + +.quote-box { + background: white; + padding: 2rem; + border-radius: 12px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); + max-width: 500px; + margin: auto; + transition: all 0.3s ease; +} + +#quote { + font-size: 1.5rem; + color: #333; + margin-bottom: 1rem; + animation: fadeIn 0.5s ease; +} + +#author { + font-size: 1.2rem; + color: #666; + margin-bottom: 2rem; + animation: fadeIn 0.5s ease; +} + +button { + background-color: #6c63ff; + color: white; + border: none; + padding: 0.8rem 1.5rem; + border-radius: 8px; + font-size: 1rem; + cursor: pointer; + transition: background-color 0.3s ease; +} + +button:hover { + background-color: #574fd6; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} \ No newline at end of file diff --git a/games/race-track/index.html b/games/race-track/index.html new file mode 100644 index 00000000..662d08d2 --- /dev/null +++ b/games/race-track/index.html @@ -0,0 +1,46 @@ + + + + + + Race Track + + + +
    +

    ๐ŸŽ๏ธ Race Track

    +

    Race cars around the track! Use arrow keys to control your car.

    + +
    +
    Score: 0
    +
    Level: 1
    +
    Speed: 0
    +
    Laps: 0/3
    +
    + + + +
    + + + +
    + +
    + +
    +

    How to Play:

    +
      +
    • Use arrow keys to steer and accelerate your car
    • +
    • Stay on the track and avoid obstacles
    • +
    • Collect power-ups for speed boosts
    • +
    • Complete laps to increase your score
    • +
    • Don't crash into walls or other cars!
    • +
    +

    Controls: โ†‘ Accelerate, โ† โ†’ Steer, โ†“ Brake

    +
    +
    + + + + \ No newline at end of file diff --git a/games/race-track/script.js b/games/race-track/script.js new file mode 100644 index 00000000..6e9de69e --- /dev/null +++ b/games/race-track/script.js @@ -0,0 +1,368 @@ +// Race Track Game +// Race around the track, avoid obstacles, collect power-ups + +// DOM elements +const canvas = document.getElementById('track-canvas'); +const ctx = canvas.getContext('2d'); +const scoreEl = document.getElementById('current-score'); +const levelEl = document.getElementById('current-level'); +const speedEl = document.getElementById('current-speed'); +const lapEl = document.getElementById('current-lap'); +const totalLapsEl = document.getElementById('total-laps'); +const messageEl = document.getElementById('message'); +const startBtn = document.getElementById('start-btn'); +const pauseBtn = document.getElementById('pause-btn'); +const resetBtn = document.getElementById('reset-btn'); + +// Game constants +const CANVAS_WIDTH = 800; +const CANVAS_HEIGHT = 600; +const CAR_WIDTH = 20; +const CAR_HEIGHT = 10; +const MAX_SPEED = 5; +const ACCELERATION = 0.2; +const FRICTION = 0.05; +const TURN_SPEED = 0.1; + +// Game variables +let gameRunning = false; +let gamePaused = false; +let score = 0; +let level = 1; +let currentLap = 0; +let totalLaps = 3; +let car = { + x: CANVAS_WIDTH / 2, + y: CANVAS_HEIGHT - 100, + angle: -Math.PI / 2, // Facing up + speed: 0, + vx: 0, + vy: 0 +}; +let obstacles = []; +let powerUps = []; +let checkpoint = { x: CANVAS_WIDTH / 2, y: CANVAS_HEIGHT - 150, passed: false }; +let animationId; +let keys = {}; + +// Track boundaries (simple oval shape) +const trackInner = []; +const trackOuter = []; + +// Initialize track +function initTrack() { + // Create oval track boundaries + const centerX = CANVAS_WIDTH / 2; + const centerY = CANVAS_HEIGHT / 2; + const innerRadiusX = 200; + const innerRadiusY = 150; + const outerRadiusX = 300; + const outerRadiusY = 200; + + // Generate track points + for (let angle = 0; angle < Math.PI * 2; angle += 0.1) { + // Inner boundary + trackInner.push({ + x: centerX + Math.cos(angle) * innerRadiusX, + y: centerY + Math.sin(angle) * innerRadiusY + }); + // Outer boundary + trackOuter.push({ + x: centerX + Math.cos(angle) * outerRadiusX, + y: centerY + Math.sin(angle) * outerRadiusY + }); + } +} + +// Check if point is on track +function isOnTrack(x, y) { + const centerX = CANVAS_WIDTH / 2; + const centerY = CANVAS_HEIGHT / 2; + const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2); + + // Simple check: between inner and outer radius + return distance > 180 && distance < 320; +} + +// Spawn obstacles and power-ups +function spawnItems() { + obstacles = []; + powerUps = []; + + // Spawn some obstacles + for (let i = 0; i < 5 + level; i++) { + let x, y; + do { + x = Math.random() * CANVAS_WIDTH; + y = Math.random() * CANVAS_HEIGHT; + } while (!isOnTrack(x, y) || Math.abs(x - car.x) < 100 || Math.abs(y - car.y) < 100); + + obstacles.push({ x, y, size: 15 }); + } + + // Spawn power-ups + for (let i = 0; i < 3; i++) { + let x, y; + do { + x = Math.random() * CANVAS_WIDTH; + y = Math.random() * CANVAS_HEIGHT; + } while (!isOnTrack(x, y)); + + powerUps.push({ x, y, size: 12, type: 'speed' }); + } +} + +// Start the game +function startGame() { + score = 0; + level = 1; + currentLap = 0; + car.x = CANVAS_WIDTH / 2; + car.y = CANVAS_HEIGHT - 100; + car.angle = -Math.PI / 2; + car.speed = 0; + car.vx = 0; + car.vy = 0; + checkpoint.passed = false; + + scoreEl.textContent = score; + levelEl.textContent = level; + lapEl.textContent = currentLap; + totalLapsEl.textContent = totalLaps; + messageEl.textContent = ''; + + gameRunning = true; + gamePaused = false; + startBtn.style.display = 'none'; + pauseBtn.style.display = 'inline-block'; + + spawnItems(); + gameLoop(); +} + +// Main game loop +function gameLoop() { + if (!gameRunning || gamePaused) return; + + updateGame(); + drawGame(); + + animationId = requestAnimationFrame(gameLoop); +} + +// Update game state +function updateGame() { + // Handle input + if (keys.ArrowUp) { + car.speed = Math.min(car.speed + ACCELERATION, MAX_SPEED); + } else if (keys.ArrowDown) { + car.speed = Math.max(car.speed - ACCELERATION, -MAX_SPEED / 2); + } else { + car.speed *= (1 - FRICTION); + } + + if (keys.ArrowLeft) { + car.angle -= TURN_SPEED; + } + if (keys.ArrowRight) { + car.angle += TURN_SPEED; + } + + // Update velocity + car.vx = Math.cos(car.angle) * car.speed; + car.vy = Math.sin(car.angle) * car.speed; + + // Move car + car.x += car.vx; + car.y += car.vy; + + // Check track boundaries + if (!isOnTrack(car.x, car.y)) { + crash(); + return; + } + + // Check obstacles + for (let obstacle of obstacles) { + const distance = Math.sqrt((car.x - obstacle.x) ** 2 + (car.y - obstacle.y) ** 2); + if (distance < CAR_WIDTH / 2 + obstacle.size / 2) { + crash(); + return; + } + } + + // Check power-ups + for (let i = powerUps.length - 1; i >= 0; i--) { + const powerUp = powerUps[i]; + const distance = Math.sqrt((car.x - powerUp.x) ** 2 + (car.y - powerUp.y) ** 2); + if (distance < CAR_WIDTH / 2 + powerUp.size / 2) { + collectPowerUp(powerUp); + powerUps.splice(i, 1); + } + } + + // Check checkpoint + const checkpointDistance = Math.sqrt((car.x - checkpoint.x) ** 2 + (car.y - checkpoint.y) ** 2); + if (checkpointDistance < 30) { + if (!checkpoint.passed) { + checkpoint.passed = true; + } + } + + // Check finish line (back at start) + const finishDistance = Math.sqrt((car.x - CANVAS_WIDTH / 2) ** 2 + (car.y - (CANVAS_HEIGHT - 100)) ** 2); + if (finishDistance < 30 && checkpoint.passed) { + completeLap(); + } + + // Update speed display + speedEl.textContent = Math.round(car.speed * 10); +} + +// Collect power-up +function collectPowerUp(powerUp) { + if (powerUp.type === 'speed') { + car.speed = Math.min(car.speed + 2, MAX_SPEED); + score += 50; + messageEl.textContent = 'Speed boost!'; + setTimeout(() => messageEl.textContent = '', 1000); + } + scoreEl.textContent = score; +} + +// Complete a lap +function completeLap() { + currentLap++; + checkpoint.passed = false; + score += 100 * level; + + if (currentLap >= totalLaps) { + levelComplete(); + } else { + lapEl.textContent = currentLap; + scoreEl.textContent = score; + messageEl.textContent = `Lap ${currentLap} complete!`; + setTimeout(() => messageEl.textContent = '', 2000); + } +} + +// Level complete +function levelComplete() { + gameRunning = false; + cancelAnimationFrame(animationId); + level++; + levelEl.textContent = level; + messageEl.textContent = `All laps complete! Level ${level} unlocked.`; + setTimeout(() => { + messageEl.textContent = 'Get ready for next level...'; + setTimeout(() => { + startGame(); + }, 2000); + }, 3000); +} + +// Crash +function crash() { + gameRunning = false; + cancelAnimationFrame(animationId); + messageEl.textContent = 'Crashed! Game Over.'; + pauseBtn.style.display = 'none'; + resetBtn.style.display = 'inline-block'; +} + +// Toggle pause +function togglePause() { + gamePaused = !gamePaused; + if (gamePaused) { + pauseBtn.textContent = 'Resume'; + messageEl.textContent = 'Game Paused'; + } else { + pauseBtn.textContent = 'Pause'; + messageEl.textContent = ''; + gameLoop(); + } +} + +// Reset game +function resetGame() { + gameRunning = false; + cancelAnimationFrame(animationId); + score = 0; + level = 1; + currentLap = 0; + scoreEl.textContent = score; + levelEl.textContent = level; + lapEl.textContent = currentLap; + messageEl.textContent = ''; + resetBtn.style.display = 'none'; + startBtn.style.display = 'inline-block'; + pauseBtn.style.display = 'none'; +} + +// Draw the game +function drawGame() { + // Clear canvas + ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + // Draw track + ctx.fillStyle = '#27ae60'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + // Draw track boundaries (simplified) + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.ellipse(CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2, 200, 150, 0, 0, Math.PI * 2); + ctx.stroke(); + ctx.beginPath(); + ctx.ellipse(CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2, 300, 200, 0, 0, Math.PI * 2); + ctx.stroke(); + + // Draw obstacles + ctx.fillStyle = '#e74c3c'; + obstacles.forEach(obstacle => { + ctx.beginPath(); + ctx.arc(obstacle.x, obstacle.y, obstacle.size, 0, Math.PI * 2); + ctx.fill(); + }); + + // Draw power-ups + ctx.fillStyle = '#f39c12'; + powerUps.forEach(powerUp => { + ctx.beginPath(); + ctx.arc(powerUp.x, powerUp.y, powerUp.size, 0, Math.PI * 2); + ctx.fill(); + }); + + // Draw checkpoint + ctx.fillStyle = '#9b59b6'; + ctx.fillRect(checkpoint.x - 15, checkpoint.y - 5, 30, 10); + + // Draw car + ctx.save(); + ctx.translate(car.x, car.y); + ctx.rotate(car.angle); + ctx.fillStyle = '#3498db'; + ctx.fillRect(-CAR_WIDTH / 2, -CAR_HEIGHT / 2, CAR_WIDTH, CAR_HEIGHT); + ctx.restore(); +} + +// Event listeners +startBtn.addEventListener('click', startGame); +pauseBtn.addEventListener('click', togglePause); +resetBtn.addEventListener('click', resetGame); + +document.addEventListener('keydown', (e) => { + keys[e.key] = true; +}); + +document.addEventListener('keyup', (e) => { + keys[e.key] = false; +}); + +// Initialize +initTrack(); + +// This racing game was challenging but fun +// The car physics took some tweaking to feel right +// Maybe add AI opponents or different track shapes later \ No newline at end of file diff --git a/games/race-track/style.css b/games/race-track/style.css new file mode 100644 index 00000000..00f75417 --- /dev/null +++ b/games/race-track/style.css @@ -0,0 +1,139 @@ +/* Race Track Game Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #2c3e50, #34495e); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + color: white; +} + +.container { + text-align: center; + max-width: 900px; + padding: 20px; +} + +h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +p { + font-size: 1.1em; + margin-bottom: 20px; + opacity: 0.9; +} + +.game-stats { + display: flex; + justify-content: space-around; + flex-wrap: wrap; + margin: 20px 0; + font-size: 1.1em; + font-weight: bold; + background: rgba(255, 255, 255, 0.1); + padding: 15px; + border-radius: 10px; +} + +#track-canvas { + border: 3px solid white; + border-radius: 10px; + background: #27ae60; + display: block; + margin: 20px auto; + box-shadow: 0 4px 8px rgba(0,0,0,0.3); +} + +.controls { + margin: 20px 0; +} + +button { + background: #e74c3c; + color: white; + border: none; + padding: 12px 24px; + font-size: 1em; + border-radius: 5px; + cursor: pointer; + margin: 0 10px; + transition: background 0.3s; +} + +button:hover { + background: #c0392b; +} + +#message { + font-size: 1.2em; + margin: 20px 0; + min-height: 30px; + font-weight: bold; +} + +.instructions { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + margin-top: 20px; + text-align: left; + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.instructions h3 { + margin-bottom: 10px; + color: #ffeaa7; +} + +.instructions ul { + list-style-type: disc; + padding-left: 20px; +} + +.instructions li { + margin: 5px 0; + line-height: 1.4; +} + +.instructions p { + margin-top: 15px; + font-weight: bold; + text-align: center; +} + +/* Responsive design */ +@media (max-width: 768px) { + #track-canvas { + width: 100%; + max-width: 600px; + height: 450px; + } + + .game-stats { + font-size: 1em; + } + + .controls { + display: flex; + flex-direction: column; + gap: 10px; + } + + button { + width: 100%; + max-width: 200px; + } +} \ No newline at end of file diff --git a/games/rain-catcher/index.html b/games/rain-catcher/index.html new file mode 100644 index 00000000..8bc91e3b --- /dev/null +++ b/games/rain-catcher/index.html @@ -0,0 +1,29 @@ + + + + + +Rain Catcher ๐ŸŽฎ + + + +
    +
    +

    Rain Catcher

    +
    + + + +
    +
    + Score: 0 + Lives: 3 + High Score: 0 +
    +
    + +
    + + + + diff --git a/games/rain-catcher/script.js b/games/rain-catcher/script.js new file mode 100644 index 00000000..c986cb6b --- /dev/null +++ b/games/rain-catcher/script.js @@ -0,0 +1,129 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); + +canvas.width = 480; +canvas.height = 640; + +let score = 0; +let lives = 3; +let highScore = localStorage.getItem('rainCatcherHigh') || 0; +let gameInterval; +let dropInterval = 1500; +let objects = []; +let gamePaused = false; + +// Bucket +const bucket = { + x: canvas.width / 2 - 40, + y: canvas.height - 60, + width: 80, + height: 40, + speed: 7 +}; + +// Controls +const keys = { left: false, right: false }; +document.addEventListener('keydown', e => { + if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = true; + if (e.key === 'ArrowRight' || e.key === 'd') keys.right = true; +}); +document.addEventListener('keyup', e => { + if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = false; + if (e.key === 'ArrowRight' || e.key === 'd') keys.right = false; +}); + +// Online Assets +const raindropImg = new Image(); +raindropImg.src = 'https://i.ibb.co/F0KjXv5/raindrop.png'; +const thunderImg = new Image(); +thunderImg.src = 'https://i.ibb.co/qgqQXKz/thunder.png'; +const bucketImg = new Image(); +bucketImg.src = 'https://i.ibb.co/FxzM3kL/bucket.png'; + +// Sounds +const catchSound = new Audio('https://www.soundjay.com/button/beep-07.wav'); +const thunderSound = new Audio('https://www.soundjay.com/button/beep-10.wav'); + +// Create falling objects +function createObject() { + const typeChance = Math.random(); + let obj = { x: Math.random() * (canvas.width - 30), y: -30, vy: 3 + Math.random() * 2, width: 30, height: 30 }; + if (typeChance < 0.8) obj.type = 'raindrop'; + else obj.type = 'thunder'; + objects.push(obj); +} + +// Collision detection +function isColliding(a, b) { + return a.x < b.x + b.width && a.x + a.width > b.x && + a.y < b.y + b.height && a.y + a.height > b.y; +} + +// Game loop +function gameLoop() { + if (gamePaused) return; + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Move bucket + if (keys.left && bucket.x > 0) bucket.x -= bucket.speed; + if (keys.right && bucket.x + bucket.width < canvas.width) bucket.x += bucket.speed; + ctx.drawImage(bucketImg, bucket.x, bucket.y, bucket.width, bucket.height); + + // Draw & move objects + objects.forEach((obj, i) => { + obj.y += obj.vy; + if (obj.type === 'raindrop') ctx.drawImage(raindropImg, obj.x, obj.y, obj.width, obj.height); + else if (obj.type === 'thunder') ctx.drawImage(thunderImg, obj.x, obj.y, obj.width, obj.height); + + if (isColliding(bucket, obj)) { + if (obj.type === 'raindrop') { + score++; + catchSound.play(); + } else if (obj.type === 'thunder') { + lives--; + thunderSound.play(); + } + objects.splice(i, 1); + } else if (obj.y > canvas.height) { + objects.splice(i, 1); + } + }); + + // Update Scoreboard + document.getElementById('score').textContent = score; + document.getElementById('lives').textContent = lives; + document.getElementById('highscore').textContent = highScore; + + // Check game over + if (lives <= 0) { + clearInterval(gameInterval); + if (score > highScore) { + highScore = score; + localStorage.setItem('rainCatcherHigh', highScore); + } + alert('Game Over! Your Score: ' + score); + restartGame(); + return; + } + + requestAnimationFrame(gameLoop); +} + +// Controls buttons +document.getElementById('start-btn').addEventListener('click', () => { + if (!gameInterval) gameInterval = setInterval(createObject, dropInterval); + gamePaused = false; + gameLoop(); +}); +document.getElementById('pause-btn').addEventListener('click', () => gamePaused = true); +document.getElementById('restart-btn').addEventListener('click', restartGame); + +function restartGame() { + score = 0; + lives = 3; + objects = []; + gamePaused = true; + clearInterval(gameInterval); + gameInterval = null; + gameLoop(); +} diff --git a/games/rain-catcher/style.css b/games/rain-catcher/style.css new file mode 100644 index 00000000..e47385fd --- /dev/null +++ b/games/rain-catcher/style.css @@ -0,0 +1,47 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: linear-gradient(to top, #0f2027, #203a43, #2c5364); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + color: #fff; +} + +.game-container { + text-align: center; +} + +header h1 { + font-size: 2em; + margin-bottom: 10px; + text-shadow: 0 0 10px #00f, 0 0 20px #0ff; +} + +.controls button { + padding: 10px 15px; + margin: 0 5px; + font-size: 16px; + border: none; + border-radius: 5px; + cursor: pointer; + background: linear-gradient(to right, #00f, #0ff); + color: #000; + font-weight: bold; + text-shadow: 0 0 5px #fff; +} + +.score-board { + margin: 10px 0; + font-size: 18px; +} + +canvas { + background: #87ceeb; + display: block; + margin: 0 auto; + border: 3px solid #fff; + border-radius: 10px; + box-shadow: 0 0 30px #0ff; +} diff --git a/games/rain-drop-rhythm/index.html b/games/rain-drop-rhythm/index.html new file mode 100644 index 00000000..04d4c373 --- /dev/null +++ b/games/rain-drop-rhythm/index.html @@ -0,0 +1,23 @@ + + + + + +Rain Drop Rhythm + + + +
    +

    Rain Drop Rhythm ๐ŸŽต

    + +
    + + + + +
    +

    Score: 0

    +
    + + + diff --git a/games/rain-drop-rhythm/script.js b/games/rain-drop-rhythm/script.js new file mode 100644 index 00000000..32d45d37 --- /dev/null +++ b/games/rain-drop-rhythm/script.js @@ -0,0 +1,100 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); + +const startBtn = document.getElementById('startBtn'); +const pauseBtn = document.getElementById('pauseBtn'); +const restartBtn = document.getElementById('restartBtn'); +const sourceBtn = document.getElementById('sourceBtn'); +const scoreEl = document.getElementById('score'); + +let animationId; +let raindrops = []; +let score = 0; +let gameRunning = false; + +// Online sound for hit +const hitSound = new Audio('https://freesound.org/data/previews/256/256113_3263906-lq.mp3'); +// Background music +const bgMusic = new Audio('https://freesound.org/data/previews/436/436070_2305276-lq.mp3'); +bgMusic.loop = true; + +function createRaindrop() { + const x = Math.random() * canvas.width; + const y = -20; + const size = Math.random() * 20 + 10; + const speed = Math.random() * 3 + 2; + raindrops.push({ x, y, size, speed, hit: false }); +} + +function drawRaindrops() { + ctx.clearRect(0,0,canvas.width,canvas.height); + raindrops.forEach(drop => { + ctx.beginPath(); + ctx.arc(drop.x, drop.y, drop.size, 0, Math.PI * 2); + ctx.fillStyle = drop.hit ? 'rgba(255,255,255,0.8)' : 'rgba(0,255,255,0.8)'; + ctx.shadowColor = 'cyan'; + ctx.shadowBlur = 20; + ctx.fill(); + ctx.closePath(); + drop.y += drop.speed; + }); +} + +function checkHit(e) { + const rect = canvas.getBoundingClientRect(); + const mx = e.clientX - rect.left; + const my = e.clientY - rect.top; + + raindrops.forEach(drop => { + const dx = drop.x - mx; + const dy = drop.y - my; + if (!drop.hit && Math.sqrt(dx*dx + dy*dy) < drop.size) { + drop.hit = true; + score += 10; + scoreEl.textContent = `Score: ${score}`; + hitSound.currentTime = 0; + hitSound.play(); + } + }); +} + +function gameLoop() { + drawRaindrops(); + if(Math.random() < 0.02) createRaindrop(); + animationId = requestAnimationFrame(gameLoop); +} + +// Event listeners +canvas.addEventListener('click', checkHit); + +startBtn.addEventListener('click', () => { + if(!gameRunning) { + gameRunning = true; + bgMusic.play(); + gameLoop(); + } +}); + +pauseBtn.addEventListener('click', () => { + if(gameRunning) { + gameRunning = false; + cancelAnimationFrame(animationId); + bgMusic.pause(); + } +}); + +restartBtn.addEventListener('click', () => { + raindrops = []; + score = 0; + scoreEl.textContent = `Score: ${score}`; + if(!gameRunning) { + gameRunning = true; + bgMusic.currentTime = 0; + bgMusic.play(); + gameLoop(); + } +}); + +sourceBtn.addEventListener('click', () => { + window.open('https://github.com/yourusername/mini-js-games-hub/tree/main/games/rain-drop-rhythm', '_blank'); +}); diff --git a/games/rain-drop-rhythm/style.css b/games/rain-drop-rhythm/style.css new file mode 100644 index 00000000..82c4df23 --- /dev/null +++ b/games/rain-drop-rhythm/style.css @@ -0,0 +1,46 @@ +body { + margin: 0; + padding: 0; + font-family: 'Arial', sans-serif; + background: linear-gradient(to bottom, #0f2027, #203a43, #2c5364); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + color: #fff; +} + +.game-container { + text-align: center; +} + +canvas { + border: 2px solid #fff; + border-radius: 10px; + background: rgba(0,0,0,0.7); + display: block; + margin: 20px auto; +} + +.controls button { + margin: 10px; + padding: 10px 20px; + font-size: 16px; + cursor: pointer; + border: none; + border-radius: 5px; + background: linear-gradient(45deg, #ff6ec4, #7873f5); + color: #fff; + box-shadow: 0 0 10px rgba(255,255,255,0.7); + transition: all 0.2s ease; +} + +.controls button:hover { + transform: scale(1.1); + box-shadow: 0 0 20px rgba(255,255,255,1); +} + +#score { + font-size: 20px; + margin-top: 10px; +} diff --git a/games/rain-ripple-effect/index.html b/games/rain-ripple-effect/index.html new file mode 100644 index 00000000..2b7cd88a --- /dev/null +++ b/games/rain-ripple-effect/index.html @@ -0,0 +1,22 @@ + + + + + + Rain Ripple Effect | Mini JS Games Hub + + + + + +
    + + + +
    + + + + + + diff --git a/games/rain-ripple-effect/script.js b/games/rain-ripple-effect/script.js new file mode 100644 index 00000000..e7e6aa7e --- /dev/null +++ b/games/rain-ripple-effect/script.js @@ -0,0 +1,89 @@ +const canvas = document.getElementById("rippleCanvas"); +const ctx = canvas.getContext("2d"); +const rainSound = document.getElementById("rain-sound"); +let ripples = []; +let animationFrame; +let isPaused = true; + +// Set canvas size +function resizeCanvas() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; +} +window.addEventListener("resize", resizeCanvas); +resizeCanvas(); + +// Ripple class +class Ripple { + constructor(x, y) { + this.x = x; + this.y = y; + this.radius = 0; + this.alpha = 1; + this.maxRadius = 200; + } + + update() { + this.radius += 2; + this.alpha -= 0.01; + } + + draw() { + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + const gradient = ctx.createRadialGradient(this.x, this.y, this.radius * 0.3, this.x, this.y, this.radius); + gradient.addColorStop(0, `rgba(0,255,255,${this.alpha})`); + gradient.addColorStop(1, `rgba(0,50,100,0)`); + ctx.strokeStyle = gradient; + ctx.lineWidth = 2; + ctx.shadowBlur = 20; + ctx.shadowColor = "#00ffff"; + ctx.stroke(); + } +} + +canvas.addEventListener("click", (e) => { + if (!isPaused) { + ripples.push(new Ripple(e.clientX, e.clientY)); + } +}); + +function animate() { + ctx.fillStyle = "rgba(0, 10, 20, 0.2)"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ripples.forEach((ripple, index) => { + ripple.update(); + ripple.draw(); + if (ripple.alpha <= 0) ripples.splice(index, 1); + }); + + if (!isPaused) { + animationFrame = requestAnimationFrame(animate); + } +} + +// Button controls +document.getElementById("start-btn").addEventListener("click", () => { + if (isPaused) { + isPaused = false; + rainSound.volume = 0.5; + rainSound.play(); + animate(); + } +}); + +document.getElementById("pause-btn").addEventListener("click", () => { + isPaused = true; + rainSound.pause(); + cancelAnimationFrame(animationFrame); +}); + +document.getElementById("restart-btn").addEventListener("click", () => { + ripples = []; + ctx.clearRect(0, 0, canvas.width, canvas.height); + isPaused = false; + rainSound.currentTime = 0; + rainSound.play(); + animate(); +}); diff --git a/games/rain-ripple-effect/style.css b/games/rain-ripple-effect/style.css new file mode 100644 index 00000000..fe4d937c --- /dev/null +++ b/games/rain-ripple-effect/style.css @@ -0,0 +1,47 @@ +html, body { + margin: 0; + padding: 0; + overflow: hidden; + background: radial-gradient(circle at center, #00111a, #000); + height: 100%; + display: flex; + justify-content: center; + align-items: center; + font-family: 'Poppins', sans-serif; +} + +canvas { + position: absolute; + top: 0; + left: 0; +} + +.controls { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 10; +} + +button { + background: rgba(0, 150, 255, 0.3); + border: 2px solid #00ffff; + color: #fff; + padding: 8px 18px; + border-radius: 10px; + font-size: 16px; + margin: 0 5px; + cursor: pointer; + transition: all 0.3s ease; + text-shadow: 0 0 8px #00ffff; +} + +button:hover { + background: rgba(0, 200, 255, 0.6); + box-shadow: 0 0 15px #00ffff; +} + +button:active { + transform: scale(0.95); +} diff --git a/games/reaction-duel/index.html b/games/reaction-duel/index.html new file mode 100644 index 00000000..cffe0b5b --- /dev/null +++ b/games/reaction-duel/index.html @@ -0,0 +1,39 @@ + + + + + + Reaction Duel | Mini JS Games Hub + + + +
    +

    Reaction Duel

    +

    Player 1: A   |   Player 2: L

    + +
    +

    Get Ready...

    +
    + +
    +
    +

    Player 1

    + 0 +
    +
    +

    Player 2

    + 0 +
    +
    + +
    + + +
    + +

    +
    + + + + diff --git a/games/reaction-duel/script.js b/games/reaction-duel/script.js new file mode 100644 index 00000000..ea86d4e4 --- /dev/null +++ b/games/reaction-duel/script.js @@ -0,0 +1,85 @@ +const startBtn = document.getElementById("start-btn"); +const resetBtn = document.getElementById("reset-btn"); +const signalArea = document.getElementById("signal-area"); +const signalText = document.getElementById("signal-text"); +const score1El = document.getElementById("score1"); +const score2El = document.getElementById("score2"); +const messageEl = document.getElementById("message"); + +let score1 = 0; +let score2 = 0; +let gameActive = false; +let signalTimeout; +let signalVisible = false; +let reactionRecorded = false; + +function randomDelay() { + return Math.floor(Math.random() * 3000) + 2000; // 2-5 seconds +} + +function startRound() { + signalText.textContent = "Get Ready..."; + signalArea.style.backgroundColor = "#444"; + reactionRecorded = false; + + signalTimeout = setTimeout(() => { + signalText.textContent = "GO!"; + signalArea.style.backgroundColor = "#4caf50"; + signalVisible = true; + }, randomDelay()); +} + +function handleKeyPress(e) { + if (!gameActive) return; + + if (!signalVisible && (e.key.toLowerCase() === "a" || e.key.toLowerCase() === "l")) { + messageEl.textContent = "Too soon! Wait for the signal!"; + return; + } + + if (signalVisible && !reactionRecorded) { + reactionRecorded = true; + signalVisible = false; + + if (e.key.toLowerCase() === "a") { + score1++; + score1El.textContent = score1; + messageEl.textContent = "Player 1 wins this round!"; + } else if (e.key.toLowerCase() === "l") { + score2++; + score2El.textContent = score2; + messageEl.textContent = "Player 2 wins this round!"; + } + + // Automatically start next round after a short delay + setTimeout(() => { + if (gameActive) startRound(); + }, 1500); + } +} + +startBtn.addEventListener("click", () => { + score1 = 0; + score2 = 0; + score1El.textContent = score1; + score2El.textContent = score2; + messageEl.textContent = ""; + gameActive = true; + startBtn.disabled = true; + startRound(); +}); + +resetBtn.addEventListener("click", () => { + gameActive = false; + clearTimeout(signalTimeout); + signalText.textContent = "Get Ready..."; + signalArea.style.backgroundColor = "#444"; + messageEl.textContent = ""; + startBtn.disabled = false; + score1 = 0; + score2 = 0; + score1El.textContent = score1; + score2El.textContent = score2; +}); + +document.addEventListener("keydown", handleKeyPress); diff --git a/games/reaction-duel/style.css b/games/reaction-duel/style.css new file mode 100644 index 00000000..e4ead34c --- /dev/null +++ b/games/reaction-duel/style.css @@ -0,0 +1,82 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #1e3c72, #2a5298); + color: #fff; + margin: 0; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.container { + text-align: center; + background-color: rgba(0,0,0,0.6); + padding: 40px; + border-radius: 15px; + width: 90%; + max-width: 500px; + box-shadow: 0 0 20px rgba(0,0,0,0.5); +} + +h1 { + margin-bottom: 10px; + font-size: 2rem; + letter-spacing: 1px; +} + +.instructions { + font-size: 1rem; + margin-bottom: 20px; +} + +.signal-area { + background-color: #444; + height: 150px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 15px; + margin-bottom: 30px; + font-size: 1.5rem; + transition: background-color 0.3s ease; +} + +.scores { + display: flex; + justify-content: space-around; + margin-bottom: 20px; +} + +.player-score h2 { + margin: 0; + font-size: 1.2rem; +} + +.player-score span { + font-size: 2rem; + display: block; + margin-top: 5px; +} + +.controls button { + padding: 10px 20px; + margin: 0 10px; + border: none; + border-radius: 10px; + background-color: #ffdd57; + color: #000; + font-size: 1rem; + cursor: pointer; + transition: background 0.3s ease; +} + +.controls button:hover { + background-color: #ffc107; +} + +#message { + margin-top: 20px; + font-size: 1.2rem; + font-weight: bold; +} diff --git a/games/reaction-timer-game/index.html b/games/reaction-timer-game/index.html new file mode 100644 index 00000000..c6a460d1 --- /dev/null +++ b/games/reaction-timer-game/index.html @@ -0,0 +1,23 @@ + + + + + + Reaction Timer Game + + + +
    +

    Click anywhere to start.

    +
    + +
    +

    Reaction Timer

    +

    Click the screen when the color changes from **Red** to **Green**.

    +

    Score is the time elapsed in milliseconds (**ms**). Lower is better!

    +

    Your last score: **-** ms

    +
    + + + + \ No newline at end of file diff --git a/games/reaction-timer-game/script.js b/games/reaction-timer-game/script.js new file mode 100644 index 00000000..a1d1f5e5 --- /dev/null +++ b/games/reaction-timer-game/script.js @@ -0,0 +1,98 @@ +// --- DOM Elements --- +const gameArea = document.getElementById('game-area'); +const messageDisplay = document.getElementById('message'); +const scoreDisplay = document.getElementById('score-display'); + +// --- Game State Variables --- +let currentState = 'start'; // 'start', 'wait', 'ready', 'result' +let timeoutId; +let startTime; // Stores the time when the screen turned green (Ready state) + +// --- Helper Functions --- + +/** + * Changes the game state and updates the UI accordingly. + * @param {string} newState - The new state ('start', 'wait', 'ready', 'result'). + */ +function updateState(newState) { + // Clean up old class and set the new one + gameArea.className = ''; + gameArea.classList.add(newState); + + currentState = newState; + + // Update the on-screen message based on the state + switch (newState) { + case 'start': + messageDisplay.textContent = 'Click anywhere to start.'; + break; + case 'wait': + messageDisplay.textContent = 'Wait for green...'; + break; + case 'ready': + // Record the precise time when the screen turns green + startTime = performance.now(); + messageDisplay.textContent = 'CLICK NOW!'; + break; + case 'result': + // The message is set inside the handleClick function when a score is calculated + break; + } +} + +/** + * Initiates the waiting period and sets a random delay before turning green. + */ +function startWaitPhase() { + // Clear any previous timeout to be safe + clearTimeout(timeoutId); + + updateState('wait'); + + // Generate a random delay between 1.5 and 4 seconds (1500ms to 4000ms) + const randomDelay = Math.random() * (4000 - 1500) + 1500; + + // Set a timeout to change the state to 'ready' after the random delay + timeoutId = setTimeout(() => { + updateState('ready'); + }, randomDelay); +} + + +/** + * Main game control function, called on every click/tap. + */ +function handleClick() { + switch (currentState) { + case 'start': + // From the initial screen, start the waiting phase + startWaitPhase(); + break; + + case 'wait': + // Penalty for clicking before the screen turns green + clearTimeout(timeoutId); // Stop the timer + updateState('result'); + messageDisplay.textContent = 'TOO SOON! โ›” Click to try again.'; + scoreDisplay.textContent = 'Your last score: Early Click Penalty!'; + break; + + case 'ready': + // Player clicked at the right time! Calculate the score. + const endTime = performance.now(); + const reactionTime = Math.round(endTime - startTime); // Round to nearest millisecond + + updateState('result'); + messageDisplay.textContent = `Your time: ${reactionTime} ms!`; + scoreDisplay.textContent = `Your last score: ${reactionTime} ms`; + break; + + case 'result': + // From the result screen, reset to the initial state to play again + updateState('start'); + break; + } +} + +// Initialize the game on load +updateState('start'); \ No newline at end of file diff --git a/games/reaction-timer-game/style.css b/games/reaction-timer-game/style.css new file mode 100644 index 00000000..060c4403 --- /dev/null +++ b/games/reaction-timer-game/style.css @@ -0,0 +1,88 @@ +body { + font-family: Arial, sans-serif; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + margin: 0; + background-color: #f4f4f9; + color: #333; +} + +#game-area { + width: 90%; + max-width: 600px; + height: 300px; + border-radius: 15px; + margin-bottom: 20px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + transition: background-color 0.4s ease; /* Smooth color transitions */ + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +#message { + font-size: 1.8em; + font-weight: bold; + color: white; + text-shadow: 0 0 5px rgba(0, 0, 0, 0.5); + padding: 20px; +} + +/* --- Game States --- */ + +/* Initial State (Start/Wait) */ +#game-area.start { + background-color: #3498db; /* Blue */ +} + +/* Delay/Wait State (After first click, waiting for green) */ +#game-area.wait { + background-color: #e74c3c; /* Red */ +} + +/* Ready State (Player must click now) */ +#game-area.ready { + background-color: #2ecc71; /* Green */ +} + +/* Result State (Showing the score) */ +#game-area.result { + background-color: #9b59b6; /* Purple/Violet */ +} + +/* --- Info/Score Display --- */ + +#info { + max-width: 600px; + padding: 20px; + background-color: #fff; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + text-align: center; +} + +#info h2 { + color: #3498db; + margin-top: 0; +} + +#score-display { + font-size: 1.2em; + margin-top: 15px; + font-weight: bold; + color: #2c3e50; +} + +@media (max-width: 480px) { + #message { + font-size: 1.4em; + } + #game-area { + height: 200px; + } +} \ No newline at end of file diff --git a/games/reflex-game/index.html b/games/reflex-game/index.html new file mode 100644 index 00000000..509b4ca2 --- /dev/null +++ b/games/reflex-game/index.html @@ -0,0 +1,23 @@ + + + + + + Reflex Game + + + +
    +

    Reflex Game

    +

    Click on the targets as fast as you can! Test your reaction time.

    +
    +
    Time: 30
    +
    Score: 0
    + +
    + +
    +
    + + + \ No newline at end of file diff --git a/games/reflex-game/script.js b/games/reflex-game/script.js new file mode 100644 index 00000000..ce8b3121 --- /dev/null +++ b/games/reflex-game/script.js @@ -0,0 +1,104 @@ +// Reflex Game Script +// Click targets as fast as possible + +var canvas = document.getElementById('gameCanvas'); +var ctx = canvas.getContext('2d'); +var timerDisplay = document.getElementById('timer'); +var scoreDisplay = document.getElementById('score'); +var restartBtn = document.getElementById('restart'); +var messageDiv = document.getElementById('message'); + +var target = null; +var score = 0; +var timeLeft = 30; +var timerInterval; +var gameRunning = true; + +// Target class +function Target(x, y, radius) { + this.x = x; + this.y = y; + this.radius = radius; +} + +// Initialize the game +function initGame() { + target = null; + score = 0; + timeLeft = 30; + gameRunning = true; + messageDiv.textContent = ''; + scoreDisplay.textContent = 'Score: ' + score; + spawnTarget(); + startTimer(); + draw(); +} + +// Spawn a new target +function spawnTarget() { + if (!gameRunning) return; + var x = Math.random() * (canvas.width - 100) + 50; + var y = Math.random() * (canvas.height - 100) + 50; + var radius = 25 + Math.random() * 25; // 25-50 + target = new Target(x, y, radius); + draw(); +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + if (target) { + ctx.beginPath(); + ctx.arc(target.x, target.y, target.radius, 0, Math.PI * 2); + ctx.fillStyle = '#ff5722'; + ctx.fill(); + ctx.strokeStyle = '#fff'; + ctx.stroke(); + ctx.fillStyle = '#fff'; + ctx.font = '16px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('CLICK!', target.x, target.y + 5); + } +} + +// Handle canvas click +canvas.addEventListener('click', function(event) { + if (!gameRunning || !target) return; + var rect = canvas.getBoundingClientRect(); + var x = event.clientX - rect.left; + var y = event.clientY - rect.top; + var dx = x - target.x; + var dy = y - target.y; + var distance = Math.sqrt(dx * dx + dy * dy); + if (distance < target.radius) { + score++; + scoreDisplay.textContent = 'Score: ' + score; + target = null; + draw(); + // Spawn new target after random delay + setTimeout(spawnTarget, 500 + Math.random() * 1500); // 0.5-2 seconds + } +}); + +// Start the timer +function startTimer() { + clearInterval(timerInterval); + timerInterval = setInterval(function() { + timeLeft--; + timerDisplay.textContent = 'Time: ' + timeLeft; + if (timeLeft <= 0) { + clearInterval(timerInterval); + gameRunning = false; + target = null; + draw(); + messageDiv.textContent = 'Time\'s up! Final Score: ' + score; + messageDiv.style.color = 'yellow'; + } + }, 1000); +} + +// Restart button +restartBtn.addEventListener('click', initGame); + +// Start the game +initGame(); \ No newline at end of file diff --git a/games/reflex-game/style.css b/games/reflex-game/style.css new file mode 100644 index 00000000..40b2ae6e --- /dev/null +++ b/games/reflex-game/style.css @@ -0,0 +1,54 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #212121; + color: #fff; +} + +.container { + text-align: center; +} + +h1 { + color: #ff5722; +} + +.game-info { + margin-bottom: 20px; + display: flex; + justify-content: center; + gap: 20px; +} + +#timer, #score { + font-size: 24px; + font-weight: bold; +} + +#restart { + padding: 10px 20px; + font-size: 16px; + background-color: #4caf50; + color: white; + border: none; + cursor: pointer; +} + +#restart:hover { + background-color: #388e3c; +} + +canvas { + border: 2px solid #ff5722; + background-color: #424242; +} + +#message { + margin-top: 20px; + font-size: 18px; + font-weight: bold; +} \ No newline at end of file diff --git a/games/render_riot/index.html b/games/render_riot/index.html new file mode 100644 index 00000000..536c4e53 --- /dev/null +++ b/games/render_riot/index.html @@ -0,0 +1,53 @@ + + + + + + Render Riot: The Frame Rate Fight + + + + +
    +

    Render Riot: The Frame Rate Fight

    +

    Outpace your opponent by maintaining higher FPS. Sabotage their performance with heavy CSS and JavaScript injections!

    +
    + +
    +
    +

    Your Frame Rate

    +
    FPS: 60
    +
    Budget: $100
    +
    +
    +
    + +
    +

    Opponent Frame Rate

    +
    FPS: 60
    +
    Budget: $100
    +
    +
    +
    +
    + +
    + +
    +

    Strategic Console

    +
    + + + +
    + +
    + +
    + +

    Game ready. Start attacking!

    +
    + + + + \ No newline at end of file diff --git a/games/render_riot/script.js b/games/render_riot/script.js new file mode 100644 index 00000000..d32c07b2 --- /dev/null +++ b/games/render_riot/script.js @@ -0,0 +1,199 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- DOM Elements --- + const playerSide = document.getElementById('player-side'); + const opponentSide = document.getElementById('opponent-side'); + const playerFPSDisplay = document.getElementById('player-fps'); + const opponentFPSDisplay = document.getElementById('opponent-fps'); + const playerBudgetDisplay = document.getElementById('player-budget'); + const opponentBudgetDisplay = document.getElementById('opponent-budget'); + const attackButtons = document.querySelectorAll('.attack-btn'); + const defenseButton = document.getElementById('defense-btn'); + + // --- Game State --- + let gameState = { + playerBudget: 100, + opponentBudget: 100, + playerFPS: 60, + opponentFPS: 60, + // Track active attack classes on each side + playerDebuffs: [], + opponentDebuffs: [] + }; + + // --- 1. FPS Monitoring (Core Mechanic) --- + let lastTime = performance.now(); + let frameCount = 0; + const FPS_CAP = 60; // Target FPS + + /** + * Measures and updates the FPS for a given side. + * Since the entire game runs on one browser window, we'll monitor the overall FPS + * and use a simulated performance impact for the opponent's side. + * * NOTE: For a true 2-player game, you would need two separate browser tabs + * or a complex Web Worker setup. We simplify by monitoring one and calculating the other. + */ + function monitorFPS(timestamp) { + const delta = timestamp - lastTime; + + if (delta >= 1000) { // Update FPS once per second + // Calculate actual FPS (for the player's experience) + gameState.playerFPS = Math.min(FPS_CAP, Math.round((frameCount * 1000) / delta)); + + // Calculate Opponent's SIMULATED FPS + // Start at player's FPS and subtract for each debuff + let opponentLag = gameState.playerDebuffs.length * 5; // 5 FPS per player debuff + if (opponentSide.classList.contains('heavy-shadow-attack')) opponentLag += 10; + if (opponentSide.classList.contains('paint-churn-attack')) opponentLag += 15; + + gameState.opponentFPS = Math.max(1, gameState.playerFPS - opponentLag); + + // Update DOM + playerFPSDisplay.textContent = gameState.playerFPS; + opponentFPSDisplay.textContent = gameState.opponentFPS; + + frameCount = 0; + lastTime = timestamp; + + updateRaceProgress(); + manageBudget(); + } + + frameCount++; + requestAnimationFrame(monitorFPS); + } + + // --- 2. Attack and Defense Logic --- + + // Object to hold any active JavaScript performance tasks (like the Reflow loop) + let activeJsAttacks = { + 'opponent': null, + 'player': null + }; + + function launchAttack(attackType, cost) { + if (gameState.playerBudget < cost) { + document.getElementById('game-status').textContent = "Insufficient budget to launch attack!"; + return; + } + + gameState.playerBudget -= cost; + playerBudgetDisplay.textContent = gameState.playerBudget; + + const targetElement = opponentSide.querySelector('.animation-target'); + + if (attackType === 'reflow-loop') { + // Reflow Attack (Pure JS-based sabotage) + if (activeJsAttacks.opponent) clearInterval(activeJsAttacks.opponent); + + const reflowFunc = () => { + // Forcing Reflow/Layout Thrashing: repeatedly read and write a layout property + // This is the heavy part of the attack! + const targetHeight = targetElement.offsetHeight; + targetElement.style.height = `${targetHeight + 1}px`; + targetElement.style.height = `${targetHeight}px`; + }; + + // Run the reflow function quickly and repeatedly + activeJsAttacks.opponent = setInterval(reflowFunc, 10); + document.getElementById('game-status').textContent = "Reflow Attack Injected! Opponent is recalculating layout!"; + + } else { + // CSS Attacks + const cssClass = attackType + '-attack'; + opponentSide.classList.add(cssClass); + gameState.opponentDebuffs.push(cssClass); + document.getElementById('game-status').textContent = `${attackType} Injected! Opponent's frame render is choked!`; + } + } + + function cleanseDefense(cost) { + if (gameState.playerDebuffs.length === 0 && !activeJsAttacks.player) { + document.getElementById('game-status').textContent = "No active debuffs to cleanse."; + return; + } + + if (gameState.playerBudget < cost) { + document.getElementById('game-status').textContent = "Insufficient budget to cleanse!"; + return; + } + + gameState.playerBudget -= cost; + playerBudgetDisplay.textContent = gameState.playerBudget; + + // 1. Remove the last CSS debuff + const lastDebuff = gameState.playerDebuffs.pop(); + if (lastDebuff) { + playerSide.classList.remove(lastDebuff); + document.getElementById('game-status').textContent = `Removed ${lastDebuff}! Performance slightly recovered.`; + } + + // 2. Clear any active JS debuff (Reflow Attack) + if (activeJsAttacks.player) { + clearInterval(activeJsAttacks.player); + activeJsAttacks.player = null; + document.getElementById('game-status').textContent = `Reflow attack neutralized!`; + } + } + + // Attach event listeners for attacks + attackButtons.forEach(button => { + button.addEventListener('click', () => { + const attackType = button.dataset.attackType; + const cost = parseInt(button.dataset.attackCost); + launchAttack(attackType, cost); + }); + }); + + // Attach event listener for defense + defenseButton.addEventListener('click', () => { + const cost = parseInt(defenseButton.dataset.defenseCost); + cleanseDefense(cost); + }); + + // --- 3. Race & Budget Management --- + + let playerProgress = 0; + let opponentProgress = 0; + const RACE_DISTANCE = 1000; + const playerRaceCar = document.getElementById('player-race-car'); + const opponentRaceCar = document.getElementById('opponent-race-car'); + + function updateRaceProgress() { + // Progress is proportional to FPS + playerProgress += gameState.playerFPS / FPS_CAP; + opponentProgress += gameState.opponentFPS / FPS_CAP; + + // Visual update (simple example using opacity or a custom metric) + playerRaceCar.style.opacity = playerProgress / RACE_DISTANCE; + opponentRaceCar.style.opacity = opponentProgress / RACE_DISTANCE; + + if (playerProgress >= RACE_DISTANCE) { + endGame("Player 1 Wins!"); + } else if (opponentProgress >= RACE_DISTANCE) { + endGame("Opponent Wins!"); + } + } + + function manageBudget() { + // Both players generate income based on their FPS (simulated) + const incomeRate = 0.5; // Budget gained per FPS point per second + gameState.playerBudget += gameState.playerFPS / FPS_CAP * incomeRate; + gameState.opponentBudget += gameState.opponentFPS / FPS_CAP * incomeRate; + + playerBudgetDisplay.textContent = Math.round(gameState.playerBudget); + opponentBudgetDisplay.textContent = Math.round(gameState.opponentBudget); + } + + function endGame(winner) { + document.getElementById('game-status').textContent = `GAME OVER: ${winner}`; + // Stop all animations and monitoring + // For simplicity, we'll rely on the lack of a next requestAnimationFrame call + // In a real game, you would clear intervals and stop the RAF loop properly. + alert(winner); + // Reload to play again (simple way to reset) + // location.reload(); + } + + // Start the FPS monitoring loop + monitorFPS(performance.now()); +}); \ No newline at end of file diff --git a/games/render_riot/style.css b/games/render_riot/style.css new file mode 100644 index 00000000..5da355ec --- /dev/null +++ b/games/render_riot/style.css @@ -0,0 +1,117 @@ +:root { + --side-width: 45%; + --animation-speed: 10s; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + display: flex; + flex-direction: column; + align-items: center; + background-color: #2e2e2e; + color: #e0e0e0; +} + +#game-board { + display: flex; + justify-content: space-around; + width: 90%; + margin-bottom: 20px; +} + +.side { + width: var(--side-width); + padding: 20px; + border: 2px solid #555; + background-color: #3e3e3e; + min-height: 400px; + position: relative; + overflow: hidden; +} + +/* --- Performance Monitoring & Animation --- */ + +.fps-meter, .budget-meter { + font-weight: bold; + margin-bottom: 10px; +} + +/* The element that will constantly be animated and affected by lag */ +.animation-target { + width: 50px; + height: 50px; + background-color: #00bcd4; + position: absolute; + bottom: 20px; + left: 10%; + border-radius: 5px; + animation: slide var(--animation-speed) linear infinite alternate; +} + +@keyframes slide { + from { transform: translateX(0); } + to { transform: translateX(80%); } +} + +/* --- Core Attack Classes (The Sabotage) --- */ + +/* 1. Heavy Load Attack: Complex Box Shadow (forces heavy composite/paint) */ +.heavy-shadow-attack { + /* Nested, large, and complex box shadow that is computationally expensive */ + box-shadow: + 0 0 50px 20px rgba(255, 0, 0, 0.7), + 0 0 100px 40px rgba(0, 255, 0, 0.5) inset, + 0 0 150px 60px rgba(0, 0, 255, 0.3), + -5px -5px 10px 0px rgba(255, 255, 0, 0.2), + 5px 5px 10px 0px rgba(255, 0, 255, 0.4); + transition: box-shadow 0.1s; /* Ensure the browser re-evaluates frequently */ + will-change: box-shadow; +} + +/* 2. Reflow Attack: (Handled primarily by JavaScript forcing offsetHeight reads) */ +/* This class is just a visual placeholder if needed */ + +/* 3. Paint Churn Attack: Forcing constant repaints on a large area */ +.paint-churn-attack { + /* Use a repeating gradient animation on a large element */ + background-image: repeating-linear-gradient( + 45deg, + #f06292, + #f06292 10px, + #e91e63 10px, + #e91e63 20px + ); + animation: paint-shift 0.5s linear infinite; + will-change: background-image, transform; +} + +@keyframes paint-shift { + to { + background-position: 40px 0; + } +} + +/* --- Controls Styling --- */ +#controls { + text-align: center; + width: 90%; + padding: 20px; + background-color: #4a4a4a; + border-radius: 8px; +} + +.attack-btn, #defense-btn { + padding: 10px 15px; + margin: 5px; + cursor: pointer; + border: none; + border-radius: 5px; + font-weight: bold; + transition: background-color 0.2s; +} + +.attack-btn { background-color: #ff5722; color: white; } +.attack-btn:hover { background-color: #e64a19; } + +#defense-btn { background-color: #4CAF50; color: white; } +#defense-btn:hover { background-color: #388E3C; } \ No newline at end of file diff --git a/games/rhythm-catch/index.html b/games/rhythm-catch/index.html new file mode 100644 index 00000000..f09a3b5c --- /dev/null +++ b/games/rhythm-catch/index.html @@ -0,0 +1,92 @@ + + + + + + Rhythm Catch โ€” Mini JS Games Hub + + + + +
    +
    +

    Rhythm Catch

    +

    Catch falling beats in perfect rhythm โ€” arrow keys or touch to move.

    +
    + +
    +
    + + +
    +
    + + + +
    + +
    + + + +
    +
    + +
    +
    Score: 0
    +
    Combo: 0
    +
    Last: โ€”
    +
    + +
    + + +
    +
    + + +
    + +
    + Made with โ™ฅ โ€” Drop improvements as a PR. +
    +
    + + + + + + + + diff --git a/games/rhythm-catch/script.js b/games/rhythm-catch/script.js new file mode 100644 index 00000000..a52ba58f --- /dev/null +++ b/games/rhythm-catch/script.js @@ -0,0 +1,353 @@ +/* Rhythm Catch v1 + - canvas-based falling beat catcher + - audio sync via BPM-based spawns while audio is playing + - keyboard + touch, scoring, combo, difficulty +*/ + +(() => { + // DOM + const canvas = document.getElementById('gameCanvas'); + const ctx = canvas.getContext('2d'); + const playBtn = document.getElementById('playBtn'); + const pauseBtn = document.getElementById('pauseBtn'); + const restartBtn = document.getElementById('restartBtn'); + const volumeEl = document.getElementById('volume'); + const bpmEl = document.getElementById('bpm'); + const bpmVal = document.getElementById('bpmVal'); + const diffEl = document.getElementById('difficulty'); + const scoreEl = document.getElementById('score'); + const comboEl = document.getElementById('combo'); + const lastHitEl = document.getElementById('lastHit'); + const leftBtn = document.getElementById('leftBtn'); + const rightBtn = document.getElementById('rightBtn'); + const mobileControls = document.getElementById('mobileControls'); + const playNowLink = document.getElementById('playNowLink'); + + // audio + const audio = document.getElementById('track'); + audio.loop = true; + audio.volume = parseFloat(volumeEl.value); + + // canvas sizing + function fitCanvas(){ + const ratio = canvas.width / canvas.height; + const parentWidth = canvas.parentElement.clientWidth; + canvas.style.width = '100%'; + // height auto; canvas internal size already set + } + window.addEventListener('resize', fitCanvas); + fitCanvas(); + + // Game state + let state = { + running: false, + lastTime: 0, + beats: [], + spawnTimer: 0, + spawnInterval: 500, + catcher: { x: 450, y: 520, w: 140, h: 20, speed: 720 }, + score: 0, + combo: 0, + bestCombo:0, + bpm: parseInt(bpmEl.value,10), + difficulty: diffEl.value, // easy|normal|hard + hitWindow: { perfect: 32, good: 72 }, // px tolerance + gravity: 320, // base falling speed (will be scaled) + }; + + // adjust difficulty + function applyDifficulty(){ + const d = state.difficulty; + if(d==='easy'){ state.gravity = 240; state.catcher.w = 180; } + else if(d==='normal'){ state.gravity = 320; state.catcher.w = 140; } + else { state.gravity = 420; state.catcher.w = 120; } + } + applyDifficulty(); + + // spawn interval by BPM + function updateSpawnInterval(){ + state.bpm = parseInt(bpmEl.value,10); + bpmVal.textContent = state.bpm; + // spawn at quarter notes: + state.spawnInterval = Math.round(60000 / state.bpm); + } + updateSpawnInterval(); + + bpmEl.addEventListener('input', updateSpawnInterval); + volumeEl.addEventListener('input', ()=> audio.volume = parseFloat(volumeEl.value)); + diffEl.addEventListener('change', ()=>{ state.difficulty = diffEl.value; applyDifficulty(); }); + + // play/pause/restart + function startGame(){ + if(!state.running){ + state.running = true; + state.lastTime = performance.now(); + requestAnimationFrame(loop); + // only track plays if launched via a play-button on hub main page + if(typeof trackGamePlay === 'function'){ + // safe call; trackGamePlay defined in main page + const gameName = 'Rhythm Catch'; + try{ trackGamePlay(gameName); }catch(e){} + } + } + if(audio.paused) audio.play().catch(()=>{ /* autoplay blocked; user interaction required */ }); + } + function pauseGame(){ + state.running = false; + audio.pause(); + } + function restartGame(){ + state.beats = []; + state.score = 0; state.combo = 0; state.bestCombo = 0; + state.spawnTimer = 0; + scoreEl.textContent = state.score; comboEl.textContent = state.combo; lastHitEl.textContent = 'โ€”'; + // reset audio to start + audio.currentTime = 0; + audio.pause(); + setTimeout(()=>{ audio.play().catch(()=>{}); }, 80); + if(!state.running){ state.running = true; state.lastTime = performance.now(); requestAnimationFrame(loop); } + } + + playBtn.addEventListener('click', startGame); + pauseBtn.addEventListener('click', pauseGame); + restartBtn.addEventListener('click', restartGame); + playNowLink.addEventListener('click', (e)=>{ e.preventDefault(); startGame(); }); + + // mobile controls + leftBtn.addEventListener('touchstart', (e)=>{ e.preventDefault(); state.left=true; }); + leftBtn.addEventListener('touchend', (e)=>{ e.preventDefault(); state.left=false; }); + rightBtn.addEventListener('touchstart', (e)=>{ e.preventDefault(); state.right=true; }); + rightBtn.addEventListener('touchend', (e)=>{ e.preventDefault(); state.right=false; }); + leftBtn.addEventListener('mousedown', ()=> state.left = true); + leftBtn.addEventListener('mouseup', ()=> state.left = false); + rightBtn.addEventListener('mousedown', ()=> state.right = true); + rightBtn.addEventListener('mouseup', ()=> state.right = false); + + // keyboard + window.addEventListener('keydown', (e)=>{ + if(e.key === 'ArrowLeft' || e.key === 'a') state.left = true; + if(e.key === 'ArrowRight' || e.key === 'd') state.right = true; + if(e.key === ' '){ // space toggle + e.preventDefault(); + state.running ? pauseGame() : startGame(); + } + if(e.key === 'r') restartGame(); + }); + window.addEventListener('keyup', (e)=>{ + if(e.key === 'ArrowLeft' || e.key === 'a') state.left = false; + if(e.key === 'ArrowRight' || e.key === 'd') state.right = false; + }); + + // spawn beat + function spawnBeat(){ + // spawn a beat at random x within canvas + const x = 40 + Math.random() * (canvas.width - 80); + // small variation in size & color + const size = 16 + Math.random()*18; + const hue = 200 + Math.random()*140; + const speedMultiplier = 0.9 + Math.random()*0.6; + state.beats.push({ + x, y: -40, r: size, color:`hsl(${hue}deg 85% 62% / 0.98)`, + glow: `0 0 ${8 + size/2}px hsl(${hue}deg 85% 62% / 0.7)`, + vy: state.gravity * speedMultiplier / 1000 // pixels per ms + }); + } + + // Hit detection and scoring + function evaluateHit(beat){ + // catcher is rectangular zone + const cx = state.catcher.x + state.catcher.w/2; + const distance = Math.abs(beat.x - cx); + // convert distance to judgement + if(distance <= state.hitWindow.perfect){ + const base = 100; + state.score += base + Math.floor(state.combo * 5); + state.combo += 1; + lastHitEl.textContent = 'Perfect'; + showPopup('Perfect', '#7bffb4'); + } else if(distance <= state.hitWindow.good){ + const base = 40; + state.score += base + Math.floor(state.combo * 2); + state.combo += 1; + lastHitEl.textContent = 'Good'; + showPopup('Good', '#ffd27b'); + } else { + // near miss counts as miss + state.combo = 0; + lastHitEl.textContent = 'Miss'; + showPopup('Miss', '#ff7b7b'); + } + if(state.combo > state.bestCombo) state.bestCombo = state.combo; + // update scoreboard + scoreEl.textContent = state.score; + comboEl.textContent = state.combo; + } + + // show transient popup + function showPopup(text, color){ + const popup = document.createElement('div'); + popup.className = 'hit-pop'; + popup.style.background = `linear-gradient(90deg, ${color}22, ${color}12)`; + popup.style.border = `1px solid ${color}55`; + popup.style.color = '#021'; + popup.textContent = text; + document.querySelector('.game-area').appendChild(popup); + setTimeout(()=>popup.remove(), 900); + } + + // main loop + function loop(now){ + if(!state.running){ state.lastTime = now; return; } + const dt = now - state.lastTime; + state.lastTime = now; + + // spawn logic: use interval param derived from BPM + state.spawnTimer += dt; + // slightly speed spawn when difficulty harder + const spawnIntervalAdjusted = state.spawnInterval * (state.difficulty === 'easy' ? 1.2 : state.difficulty === 'hard' ? 0.82 : 1); + while(state.spawnTimer >= spawnIntervalAdjusted){ + state.spawnTimer -= spawnIntervalAdjusted; + // spawn some beats; harder difficulty has more simultaneous + const count = state.difficulty === 'hard' ? (Math.random()>0.6?2:1) : 1; + for(let i=0;i=0; i--){ + const b = state.beats[i]; + b.y += b.vy * dt * 1000; // vy stored per ms scaled; multiply by dt and scale back + // if beat reaches catcher zone (y threshold) + const catcherY = state.catcher.y; + if(b.y + b.r >= catcherY){ + // check horizontal proximity + const left = state.catcher.x; + const right = state.catcher.x + state.catcher.w; + if(b.x >= left - 6 && b.x <= right + 6){ + evaluateHit(b); + } else { + // missed + state.combo = 0; + lastHitEl.textContent = 'Miss'; + showPopup('Miss', '#ff7b7b'); + comboEl.textContent = state.combo; + } + // remove + state.beats.splice(i,1); + } else if(b.y > canvas.height + 120){ + // clean up overflow + state.beats.splice(i,1); + } + } + + // draw frame + render(); + + requestAnimationFrame(loop); + } + + // Render visuals + function render(){ + // clear + ctx.clearRect(0,0,canvas.width,canvas.height); + // background gradient + const g = ctx.createLinearGradient(0,0,0,canvas.height); + g.addColorStop(0,'#051027'); + g.addColorStop(1,'#020414'); + ctx.fillStyle = g; + ctx.fillRect(0,0,canvas.width,canvas.height); + + // draw falling glow/particles + state.beats.forEach(b => { + // glow + ctx.beginPath(); + const glow = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, b.r*3); + glow.addColorStop(0, b.color); + glow.addColorStop(0.5, b.color.replace(' /',' / 0.25') ); + glow.addColorStop(1, 'rgba(2,6,23,0)'); + ctx.fillStyle = glow; + ctx.arc(b.x, b.y, b.r*3, 0, Math.PI*2); + ctx.fill(); + + // core circle + ctx.beginPath(); + ctx.fillStyle = b.color; + ctx.shadowColor = b.color; + ctx.shadowBlur = Math.min(28, b.r*3); + ctx.arc(b.x, b.y, b.r, 0, Math.PI*2); + ctx.fill(); + ctx.shadowBlur = 0; + }); + + // draw catcher zone + const c = state.catcher; + const cx = c.x, cy = c.y, cw = c.w, ch = c.h; + // glowing base + ctx.save(); + ctx.beginPath(); + ctx.fillStyle = 'rgba(255,255,255,0.02)'; + ctx.roundRect = function(x,y,w,h,r){ this.moveTo(x+r,y); this.arcTo(x+w,y,x+w,y+h,r); this.arcTo(x+w,y+h,x,y+h,r); this.arcTo(x,y+h,x,y,r); this.arcTo(x,y,x+w,y,r); }; + ctx.roundRect(cx-16, cy-12, cw+32, ch+24, 18); + ctx.fill(); + + // catcher gradient + const cg = ctx.createLinearGradient(cx,cy, cx+cw, cy); + cg.addColorStop(0,'#ffffff15'); + cg.addColorStop(0.5,'#ffffff08'); + cg.addColorStop(1,'#ffffff10'); + + ctx.beginPath(); + ctx.fillStyle = cg; + ctx.roundRect(cx, cy-6, cw, ch+6, 10); + ctx.fill(); + + // inner glow + ctx.beginPath(); + ctx.fillStyle = 'rgba(124,92,255,0.12)'; + ctx.roundRect(cx+6, cy-4, cw-12, ch+2, 8); + ctx.fill(); + + // center indicator + ctx.beginPath(); + ctx.strokeStyle = 'rgba(255,255,255,0.06)'; + ctx.setLineDash([6,10]); + ctx.moveTo(cx + cw/2, cy-26); + ctx.lineTo(cx + cw/2, cy+ch+26); + ctx.stroke(); + ctx.setLineDash([]); + ctx.restore(); + + // overlay score text drawn in canvas if needed + // (we already have DOM scoreboard; canvas used for visuals) + } + + // small helper to smooth resizing canvas internal pixels + function setCanvasSize(){ + // keep internal resolution for crisp rendering + const w = 900, h = 600; + canvas.width = w; + canvas.height = h; + fitCanvas(); + } + setCanvasSize(); + + // helper to show mobile controls when small + function updateMobileControls(){ + if(window.innerWidth <= 640) mobileControls.style.display = 'flex'; + else mobileControls.style.display = 'none'; + } + window.addEventListener('resize', updateMobileControls); + updateMobileControls(); + + // auto-start muted if interaction allowed? keep paused until user clicks play. + // Provide friendly tip + showPopup('Tap Play to start', '#7bffb4'); + + // Expose restart for external triggers if any + window.rcRestart = restartGame; +})(); diff --git a/games/rhythm-catch/style.css b/games/rhythm-catch/style.css new file mode 100644 index 00000000..050e72d6 --- /dev/null +++ b/games/rhythm-catch/style.css @@ -0,0 +1,78 @@ +:root{ + --bg:#0b1020; + --panel:#0f1724; + --accent:#7c5cff; + --accent-2:#00e5ff; + --glass: rgba(255,255,255,0.04); + --glass-2: rgba(255,255,255,0.02); + --text:#e6eef8; + --muted:#9fb0c8; +} + +*{box-sizing:border-box} +html,body{height:100%;margin:0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial;color:var(--text);background: +radial-gradient(1200px 600px at 10% 10%, rgba(124,92,255,0.12), transparent 8%), +radial-gradient(800px 400px at 90% 90%, rgba(0,229,255,0.06), transparent 6%), +var(--bg);} + +.page{max-width:1200px;margin:24px auto;padding:20px} + +.rc-header h1{font-size:28px;margin:6px 0} +.rc-header .subtitle{color:var(--muted);margin:0 0 16px 0} + +.rc-main{display:grid;grid-template-columns: 1fr 360px;gap:20px;align-items:start} +@media (max-width:980px){.rc-main{grid-template-columns:1fr} .info-panel{order:2}} + +.game-area{background:linear-gradient(180deg, rgba(255,255,255,0.02), transparent);padding:16px;border-radius:14px;box-shadow:0 10px 30px rgba(2,6,23,0.6);position:relative;overflow:hidden} + +/* Canvas style */ +#gameCanvas{width:100%;height:auto;border-radius:10px;background: +linear-gradient(180deg, rgba(10,12,24,0.9), rgba(6,8,16,0.8));display:block;} + +/* HUD */ +.top-hud{position:absolute;top:12px;left:14px;right:14px;display:flex;justify-content:space-between;gap:12px;pointer-events:none} +.top-hud .control{pointer-events:all} +.control{background:linear-gradient(90deg,var(--accent),var(--accent-2));border:none;padding:8px 12px;border-radius:8px;color:#061023;font-weight:700;margin-right:8px;cursor:pointer;box-shadow:0 8px 24px rgba(124,92,255,0.16);transition:transform .12s ease} +.control:active{transform:translateY(2px)} +.small{display:inline-flex;align-items:center;gap:6px;color:var(--muted)} + +.scoreboard{position:absolute;left:18px;bottom:14px;background:var(--glass);padding:8px 12px;border-radius:10px;display:flex;gap:14px;align-items:center;backdrop-filter: blur(6px)} +.scoreboard div{font-weight:700;color:var(--text)} + +.mobile-controls{display:none} +@media (max-width:640px){ + .mobile-controls{display:flex;gap:12px;justify-content:center;position:fixed;bottom:16px;left:50%;transform:translateX(-50%);pointer-events:all} + .touch{padding:14px 18px;border-radius:12px;background:linear-gradient(180deg,var(--accent),#5aa8ff87);border:none;color:#021; font-weight:800; box-shadow:0 10px 30px rgba(0,0,0,0.45)} + .top-hud{display:none} +} + +.info-panel{background:linear-gradient(180deg, rgba(255,255,255,0.02), transparent);padding:18px;border-radius:12px} +.info-panel h2{margin-top:0} +.info-panel ul{padding-left:18px;color:var(--muted)} +.note{color:var(--muted);font-size:13px} +.metadata{margin-top:8px;color:var(--muted)} +.play-now{margin-top:16px;display:flex;gap:10px} +.play-button, .open-new-tab{display:inline-block;padding:8px 12px;border-radius:8px;text-decoration:none;font-weight:700} +.play-button{background:linear-gradient(90deg,var(--accent),var(--accent-2));color:#021} +.open-new-tab{background:transparent;border:1px solid rgba(255,255,255,0.06);color:var(--text)} + +.subtitle{color:var(--muted)} + +.rc-footer{margin-top:10px;color:var(--muted);text-align:center;font-size:13px} + +/* Accuracy popup */ +.hit-pop{ + position:absolute; + left:50%; transform:translateX(-50%); + top:50px; pointer-events:none; + font-weight:900;font-size:20px;padding:6px 12px;border-radius:8px; + background:linear-gradient(90deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02)); + color:var(--text); + animation:pop .9s ease forwards; + box-shadow:0 8px 30px rgba(0,0,0,0.4); +} +@keyframes pop{ + 0%{opacity:0; transform:translate(-50%, -6px) scale(.98)} + 10%{opacity:1; transform:translate(-50%,0) scale(1.06)} + 100%{opacity:0; transform:translate(-50%,-18px) scale(.96)} +} diff --git a/games/rhythm-tap/index.html b/games/rhythm-tap/index.html new file mode 100644 index 00000000..29597c24 --- /dev/null +++ b/games/rhythm-tap/index.html @@ -0,0 +1,96 @@ + + + + + + Rhythm Tap + + + +
    +
    +

    Rhythm Tap

    +

    Feel the beat. Tap the rhythm.

    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + +
    + +
    + + + +
    +
    + +
    +
    +
    0
    +
    Score
    +
    +
    +
    0
    +
    Combo
    +
    +
    +
    100%
    +
    Accuracy
    +
    +
    +
    0
    +
    Perfect Streak
    +
    +
    + + + +
    +

    โ€ข Tap anywhere when the circle reaches the line

    +

    โ€ข Perfect timing gives more points

    +

    โ€ข Build combos for score multipliers

    +

    โ€ข Use Spacebar or click to tap

    +
    +
    + + + + + \ No newline at end of file diff --git a/games/rhythm-tap/script.js b/games/rhythm-tap/script.js new file mode 100644 index 00000000..3bfa7c23 --- /dev/null +++ b/games/rhythm-tap/script.js @@ -0,0 +1,281 @@ +class RhythmTap { + constructor() { + this.isPlaying = false; + this.score = 0; + this.combo = 0; + this.streak = 0; + this.totalTaps = 0; + this.accurateTaps = 0; + this.currentBeat = 0; + this.beatInterval = null; + this.circleAnimation = null; + this.difficulty = 'easy'; + this.song = 'electronic'; + + this.beatPatterns = { + electronic: [1, 0, 1, 0, 1, 0, 1, 1], + jazz: [1, 0, 0, 1, 0, 1, 0, 0], + hiphop: [1, 0, 0, 0, 1, 0, 1, 0], + latin: [1, 0, 1, 1, 0, 1, 0, 1] + }; + + this.difficultySettings = { + easy: { speed: 120, tolerance: 150 }, + medium: { speed: 140, tolerance: 100 }, + hard: { speed: 160, tolerance: 70 } + }; + + this.initializeGame(); + this.setupEventListeners(); + } + + initializeGame() { + this.updateDisplay(); + this.createBeatMarkers(); + } + + createBeatMarkers() { + const indicators = document.getElementById('beatIndicators'); + indicators.innerHTML = ''; + + const pattern = this.beatPatterns[this.song]; + const trackWidth = document.querySelector('.track').offsetWidth; + + pattern.forEach((beat, index) => { + if (beat === 1) { + const marker = document.createElement('div'); + marker.className = 'beat-marker'; + marker.style.left = `${(index / pattern.length) * 100}%`; + indicators.appendChild(marker); + } + }); + } + + startGame() { + if (this.isPlaying) return; + + this.isPlaying = true; + this.currentBeat = 0; + this.updateDisplay(); + + const settings = this.difficultySettings[this.difficulty]; + const beatDuration = 60000 / settings.speed; + + this.beatInterval = setInterval(() => { + this.playBeat(); + }, beatDuration); + + this.animateCircle(); + document.getElementById('playBtn').textContent = 'Stop'; + } + + stopGame() { + this.isPlaying = false; + clearInterval(this.beatInterval); + cancelAnimationFrame(this.circleAnimation); + document.getElementById('playBtn').textContent = 'Play'; + this.resetCircle(); + } + + playBeat() { + const pattern = this.beatPatterns[this.song]; + const shouldTap = pattern[this.currentBeat] === 1; + + if (shouldTap) { + this.createVisualizerEffect(); + } + + this.currentBeat = (this.currentBeat + 1) % pattern.length; + } + + animateCircle() { + const circle = document.getElementById('tapCircle'); + const track = document.querySelector('.track'); + const trackWidth = track.offsetWidth; + const circleWidth = circle.offsetWidth; + const duration = this.difficultySettings[this.difficulty].speed / 60 * 1000; + + let startTime = null; + + const animate = (timestamp) => { + if (!startTime) startTime = timestamp; + const progress = (timestamp - startTime) % duration; + const position = (progress / duration) * (trackWidth + circleWidth) - circleWidth; + + circle.style.left = `${position}px`; + + if (this.isPlaying) { + this.circleAnimation = requestAnimationFrame(animate); + } + }; + + this.circleAnimation = requestAnimationFrame(animate); + } + + resetCircle() { + const circle = document.getElementById('tapCircle'); + circle.style.left = '-60px'; + } + + handleTap() { + if (!this.isPlaying) return; + + this.totalTaps++; + const circle = document.getElementById('tapCircle'); + const track = document.querySelector('.track'); + const trackWidth = track.offsetWidth; + const circlePos = parseInt(circle.style.left); + const targetPos = trackWidth / 2; + const distance = Math.abs(circlePos - targetPos); + const tolerance = this.difficultySettings[this.difficulty].tolerance; + + let rating = 'miss'; + let points = 0; + let feedback = ''; + + if (distance < tolerance * 0.3) { + rating = 'perfect'; + points = 100; + feedback = 'PERFECT! ๐ŸŽฏ'; + this.combo++; + this.streak++; + this.accurateTaps++; + circle.classList.add('glow'); + setTimeout(() => circle.classList.remove('glow'), 300); + } else if (distance < tolerance * 0.6) { + rating = 'good'; + points = 70; + feedback = 'Good! ๐Ÿ‘'; + this.combo++; + this.streak = 0; + this.accurateTaps++; + } else if (distance < tolerance) { + rating = 'ok'; + points = 40; + feedback = 'OK ๐Ÿ‘Œ'; + this.combo = 0; + this.streak = 0; + this.accurateTaps++; + } else { + rating = 'miss'; + points = 0; + feedback = 'Miss! โŒ'; + this.combo = 0; + this.streak = 0; + circle.classList.add('shake'); + setTimeout(() => circle.classList.remove('shake'), 300); + } + + if (this.combo > 1) { + points *= Math.min(this.combo, 5); + feedback += ` x${Math.min(this.combo, 5)}`; + } + + this.score += points; + + const timingIndicator = document.getElementById('currentTiming'); + const timingPos = (distance / tolerance) * 100; + timingIndicator.style.left = `${Math.min(timingPos, 100)}%`; + + document.getElementById('feedbackText').textContent = feedback; + + this.playTapSound(); + + circle.classList.add('pulse'); + setTimeout(() => circle.classList.remove('pulse'), 100); + + this.updateDisplay(); + } + + playTapSound() { + const audio = document.getElementById('tapSound'); + audio.currentTime = 0; + audio.play().catch(() => { + + }); + } + + createVisualizerEffect() { + const bars = document.querySelectorAll('.frequency-bar'); + bars.forEach(bar => { + bar.style.animation = 'none'; + void bar.offsetWidth; + bar.style.animation = null; + }); + } + + updateDisplay() { + document.getElementById('score').textContent = this.score; + document.getElementById('combo').textContent = this.combo; + document.getElementById('streak').textContent = this.streak; + + const accuracy = this.totalTaps > 0 + ? Math.round((this.accurateTaps / this.totalTaps) * 100) + : 100; + document.getElementById('accuracy').textContent = accuracy + '%'; + } + + changeDifficulty(level) { + this.difficulty = level; + document.querySelectorAll('.difficulty-btn').forEach(btn => { + btn.classList.remove('active'); + }); + document.querySelector(`[data-difficulty="${level}"]`).classList.add('active'); + + if (this.isPlaying) { + this.stopGame(); + this.startGame(); + } + } + + changeSong(song) { + this.song = song; + this.createBeatMarkers(); + + if (this.isPlaying) { + this.stopGame(); + this.startGame(); + } + } + + setupEventListeners() { + document.getElementById('playBtn').addEventListener('click', () => { + if (this.isPlaying) { + this.stopGame(); + } else { + this.startGame(); + } + }); + + document.querySelectorAll('.difficulty-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + this.changeDifficulty(e.target.dataset.difficulty); + }); + }); + + document.getElementById('songSelect').addEventListener('change', (e) => { + this.changeSong(e.target.value); + }); + + document.addEventListener('click', () => { + this.handleTap(); + }); + + document.addEventListener('keydown', (e) => { + if (e.code === 'Space') { + e.preventDefault(); + this.handleTap(); + } + }); + + document.addEventListener('keydown', (e) => { + if (e.code === 'Space' && e.target === document.body) { + e.preventDefault(); + } + }); + } +} + +window.addEventListener('load', () => { + new RhythmTap(); +}); diff --git a/games/rhythm-tap/style.css b/games/rhythm-tap/style.css new file mode 100644 index 00000000..3d6cb45b --- /dev/null +++ b/games/rhythm-tap/style.css @@ -0,0 +1,385 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; +} + +body { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #fff; + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; +} + +.container { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(20px); + border-radius: 24px; + padding: 40px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + max-width: 600px; + width: 100%; +} + +header { + text-align: center; + margin-bottom: 40px; +} + +h1 { + font-size: 3rem; + font-weight: 700; + margin-bottom: 8px; + background: linear-gradient(45deg, #fff, #f0f0f0); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + text-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.subtitle { + font-size: 1.1rem; + opacity: 0.9; + font-weight: 300; +} + +.game-area { + margin-bottom: 40px; +} + +.track { + position: relative; + height: 200px; + background: rgba(255, 255, 255, 0.05); + border-radius: 20px; + margin-bottom: 20px; + overflow: hidden; + border: 2px solid rgba(255, 255, 255, 0.1); +} + +.beat-line { + position: absolute; + top: 50%; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, transparent, #4ECDC4, transparent); + transform: translateY(-50%); +} + +.tap-circle { + position: absolute; + top: 50%; + left: -60px; + width: 60px; + height: 60px; + transform: translateY(-50%); + background: linear-gradient(135deg, #FF6B6B, #FF8E8E); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8px 25px rgba(255, 107, 107, 0.4); + border: 3px solid rgba(255, 255, 255, 0.8); + transition: transform 0.1s ease; +} + +.inner-circle { + width: 30px; + height: 30px; + background: rgba(255, 255, 255, 0.9); + border-radius: 50%; + box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.2); +} + +.beat-indicators { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + pointer-events: none; +} + +.beat-marker { + position: absolute; + top: 50%; + width: 4px; + height: 20px; + background: rgba(78, 205, 196, 0.6); + transform: translateY(-50%); + border-radius: 2px; +} + +.visualizer { + display: flex; + align-items: end; + justify-content: center; + gap: 4px; + height: 80px; + background: rgba(0, 0, 0, 0.2); + border-radius: 15px; + padding: 20px; +} + +.frequency-bar { + width: 8px; + background: linear-gradient(to top, #4ECDC4, #FF6B6B); + border-radius: 4px; + animation: visualizerPulse 0.5s ease-in-out infinite; + animation-delay: calc(var(--delay) * 0.1s); +} + +.controls { + margin-bottom: 30px; +} + +.song-selector { + display: flex; + gap: 15px; + margin-bottom: 20px; + align-items: center; +} + +select { + flex: 1; + padding: 12px 16px; + border: none; + border-radius: 12px; + background: rgba(255, 255, 255, 0.1); + color: white; + font-size: 1rem; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +select option { + background: #667eea; + color: white; +} + +.primary-btn { + padding: 12px 24px; + background: linear-gradient(45deg, #4ECDC4, #44A08D); + color: white; + border: none; + border-radius: 12px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 6px 20px rgba(78, 205, 196, 0.3); +} + +.primary-btn:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(78, 205, 196, 0.4); +} + +.difficulty { + display: flex; + gap: 10px; + justify-content: center; +} + +.difficulty-btn { + padding: 10px 20px; + background: rgba(255, 255, 255, 0.1); + color: white; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 10px; + cursor: pointer; + transition: all 0.3s ease; + font-weight: 500; +} + +.difficulty-btn.active { + background: linear-gradient(45deg, #FF6B6B, #FF8E8E); + border-color: rgba(255, 255, 255, 0.4); + box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3); +} + +.stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 15px; + margin-bottom: 30px; +} + +.stat { + text-align: center; + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 16px; + backdrop-filter: blur(10px); +} + +.stat-value { + font-size: 1.8rem; + font-weight: 700; + margin-bottom: 5px; + color: #4ECDC4; +} + +.stat-label { + font-size: 0.8rem; + opacity: 0.8; + font-weight: 500; +} + +.feedback { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 16px; + backdrop-filter: blur(10px); + margin-bottom: 20px; +} + +.feedback-text { + text-align: center; + font-size: 1.2rem; + font-weight: 600; + margin-bottom: 15px; + min-height: 30px; +} + +.timing-indicator { + position: relative; + height: 8px; + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; + margin: 0 40px; +} + +.timing-marker { + position: absolute; + top: 50%; + width: 3px; + height: 16px; + transform: translateY(-50%); +} + +.timing-marker.perfect { + left: 50%; + background: #4ECDC4; + transform: translate(-50%, -50%); +} + +.timing-marker.good { + left: 35%; + background: #96CEB4; + transform: translateY(-50%); +} + +.timing-marker.ok { + left: 65%; + background: #FFEAA7; + transform: translateY(-50%); +} + +.timing-marker.miss { + left: 20%; + right: 20%; + background: rgba(255, 107, 107, 0.3); + width: auto; +} + +.current-timing { + position: absolute; + top: 50%; + width: 12px; + height: 12px; + background: #FF6B6B; + border-radius: 50%; + transform: translate(-50%, -50%); + transition: left 0.1s ease; + box-shadow: 0 0 10px rgba(255, 107, 107, 0.8); +} + +.instructions { + text-align: center; + background: rgba(0, 0, 0, 0.2); + padding: 20px; + border-radius: 15px; +} + +.instructions p { + margin-bottom: 8px; + opacity: 0.9; +} + +@keyframes visualizerPulse { + 0%, 100% { + height: 20px; + } + 50% { + height: 40px; + } +} + +@keyframes pulse { + 0% { transform: translateY(-50%) scale(1); } + 50% { transform: translateY(-50%) scale(1.1); } + 100% { transform: translateY(-50%) scale(1); } +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } +} + +@keyframes glow { + 0%, 100% { + box-shadow: 0 8px 25px rgba(78, 205, 196, 0.4); + } + 50% { + box-shadow: 0 8px 35px rgba(78, 205, 196, 0.8); + } +} + +.pulse { + animation: pulse 0.3s ease; +} + +.shake { + animation: shake 0.3s ease; +} + +.glow { + animation: glow 1s ease-in-out; +} + +@media (max-width: 600px) { + .container { + padding: 30px 20px; + } + + h1 { + font-size: 2.5rem; + } + + .stats { + grid-template-columns: repeat(2, 1fr); + } + + .track { + height: 150px; + } + + .tap-circle { + width: 50px; + height: 50px; + left: -50px; + } + + .inner-circle { + width: 25px; + height: 25px; + } +} \ No newline at end of file diff --git a/games/right_click/index.html b/games/right_click/index.html new file mode 100644 index 00000000..56f27179 --- /dev/null +++ b/games/right_click/index.html @@ -0,0 +1,46 @@ + + + + + + The Right-Click Ritual ๐Ÿ–ฑ๏ธ + + + + +
    +
    + The Right-Click Ritual +
    + +
    + A faint, pulsing **light** in the center of the screen. +
    + +
    +
    + A faded **journal**. +
    + +
    + An old, **locked door**. +
    + +
    + An empty **picture frame**. +
    +
    + +
    +

    Click and right-click to interact. Find the truth.

    +
    +
    + + + + + + \ No newline at end of file diff --git a/games/right_click/script.js b/games/right_click/script.js new file mode 100644 index 00000000..8113bbda --- /dev/null +++ b/games/right_click/script.js @@ -0,0 +1,190 @@ +// --- 1. Game State --- +const GAME_STATE = { + hasKey: false, + hasJournalEntry: false, + doorUnlocked: false, + finalPuzzleSolved: false +}; + +const D = (id) => document.getElementById(id); +const $ = { + gameScreen: D('game-screen'), + messageBox: D('message-box'), + customMenu: D('custom-menu'), + strangeObject: D('strange-object'), + lockedDoor: D('locked-door'), + emptyFrame: D('empty-frame'), + centralFocus: D('central-focus'), +}; + +let currentTarget = null; // The element that was right-clicked + +// --- 2. Custom Context Menu Handler --- + +// 1. Disable default context menu and show custom one +$.gameScreen.addEventListener('contextmenu', (e) => { + // Only intercept if an interactable element was clicked + if (e.target.classList.contains('interactable')) { + e.preventDefault(); + currentTarget = e.target; + + // Populate menu options dynamically + $.customMenu.innerHTML = ` + + + `; + + // Add context-specific actions + if (currentTarget.id === 'locked-door' && GAME_STATE.hasKey) { + $.customMenu.innerHTML += ``; + } + + // Position and show the custom menu + $.customMenu.style.top = `${e.clientY}px`; + $.customMenu.style.left = `${e.clientX}px`; + $.customMenu.classList.remove('hidden'); + } +}); + +// 2. Hide custom menu on any click outside +document.addEventListener('click', () => { + $.customMenu.classList.add('hidden'); + currentTarget = null; +}); + +// 3. Handle custom menu option clicks +$.customMenu.addEventListener('click', (e) => { + if (!e.target.classList.contains('menu-item')) return; + + const option = e.target.dataset.option; + if (currentTarget) { + handleInteraction(currentTarget, option); + } +}); + +// --- 3. Interaction Logic --- + +function handleInteraction(target, option) { + const targetId = target.id; + + switch (targetId) { + case 'strange-object': + if (option === 'look') { + showMessage("The journal is filled with scribbles. The first page reads: 'THE KEY IS IN THE SOURCE.'"); + GAME_STATE.hasJournalEntry = true; + } else if (option === 'touch') { + showMessage("The cover is cold, like old leather. Nothing happens."); + } + break; + + case 'locked-door': + if (option === 'look') { + showMessage("Itโ€™s an old, heavy wooden door. It requires a 4-digit code, not a physical key."); + } else if (option === 'unlock') { + // This 'unlock' option only appears if hasKey is true + GAME_STATE.doorUnlocked = true; + showMessage("๐Ÿ”‘ The code 1709 clicks. The door is unlocked! Go to the central light."); + $.lockedDoor.textContent = "An old, UNLOCKED door."; + $.lockedDoor.classList.remove('interactable'); + } else { + showMessage("The door is locked."); + } + break; + + case 'empty-frame': + if (option === 'look') { + showMessage("The frame is empty. The wood feels cheap."); + // Check hidden clue in alt text + const hiddenClue = target.getAttribute('alt'); + if (hiddenClue) { + showMessage(`You notice a tiny inscription: "${hiddenClue}"`); + } + } else if (option === 'touch') { + showMessage("You run your finger along the dusty glass. Nothing."); + } + break; + + default: + showMessage("I can't do that right now."); + } +} + +// --- 4. Metagaming/Dev Tool Puzzle Logic --- + +/** + * Puzzle 1: Find the Key/Code (1709) + * Goal: Player must use "Inspect Element" on the #locked-door to find the "content" of the ::after pseudo-element in style.css. + * Result: Sets GAME_STATE.hasKey = true. + */ +function checkForKey() { + // This function doesn't actually 'check' anything dynamically, it just relies on the player + // to find the clue (1709) and then click the right option on the door. + // We can simulate the finding by letting the player type it in the console. + + // For a real game, the simple act of clicking 'examine' on the door would prompt the player + // to find the code. We'll set hasKey to true upon finding the journal entry. + if (GAME_STATE.hasJournalEntry) { + GAME_STATE.hasKey = true; // The player has the meta-knowledge to find the code. + } +} + +/** + * Final Puzzle: The Source Code + * Goal: The player must find the comment in index.html inside #central-focus. + * Clue: "THE TARGET IS THE SOURCE" + * Action: Player must right-click the central focus and choose an action based on this final clue. + */ +function handleCentralFocusClick(e) { + e.preventDefault(); // Prevent default context menu + + if (!GAME_STATE.doorUnlocked) { + showMessage("The light pulses faintly. I should focus on the door first."); + return; + } + + // This is the trigger for the final puzzle + if (e.type === 'contextmenu') { + // Find the HTML comment hidden inside the element + const commentNode = Array.from(e.target.childNodes).find(node => node.nodeType === 8); // Node.COMMENT_NODE is 8 + + if (commentNode) { + const finalClue = commentNode.textContent.trim(); + if (finalClue.includes("THE TARGET IS THE SOURCE")) { + // The player is inspecting the source, which is the target. + if (!GAME_STATE.finalPuzzleSolved) { + GAME_STATE.finalPuzzleSolved = true; + showMessage("You hear a whisper: 'The source is the target.' A void opens behind the light."); + $.centralFocus.style.backgroundColor = 'black'; + $.centralFocus.style.color = 'var(--pulse-color)'; + $.centralFocus.textContent = "EXIT: Click to escape the ritual."; + + $.centralFocus.addEventListener('click', () => { + alert("The Ritual is Complete. You have escaped the source."); + document.body.innerHTML = `

    RITUAL COMPLETE.

    Thank you for playing The Right-Click Ritual.

    `; + }); + } + } else { + showMessage("The light simply pulses."); + } + } + } +} + +// --- 5. Utility --- + +function showMessage(text) { + $.messageBox.querySelector('p').textContent = text; +} + + +// --- 6. Initialization and Setup --- + +function initGame() { + // Start listeners + $.centralFocus.addEventListener('contextmenu', handleCentralFocusClick); + + // Check if player has found the initial clue + setInterval(checkForKey, 500); +} + +initGame(); \ No newline at end of file diff --git a/games/right_click/style.css b/games/right_click/style.css new file mode 100644 index 00000000..8f547fd4 --- /dev/null +++ b/games/right_click/style.css @@ -0,0 +1,124 @@ +:root { + --bg-color: #0d0d0d; + --text-color: #f0f0f0; + --pulse-color: #ff5555; /* Red/Glow */ + --interactable-color: #bd93f9; +} + +/* Base Styles */ +body, html { + margin: 0; + padding: 0; + height: 100%; + overflow: hidden; + background-color: var(--bg-color); + color: var(--text-color); + font-family: serif; +} + +#game-screen { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + height: 100vh; + padding: 5vh; + box-sizing: border-box; +} + +#title-text { + font-size: 2em; + color: var(--pulse-color); + text-shadow: 0 0 10px var(--pulse-color); +} + +/* Central Pulsing Light */ +#central-focus { + width: 200px; + height: 200px; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + color: #444; /* Dim text */ + font-size: 1.2em; + cursor: default; + + /* Pulsing effect */ + box-shadow: 0 0 10px 5px rgba(255, 85, 85, 0.5); + animation: pulse 2s infinite alternate; +} + +@keyframes pulse { + from { box-shadow: 0 0 10px 5px rgba(255, 85, 85, 0.5); } + to { box-shadow: 0 0 20px 10px rgba(255, 85, 85, 0.8); } +} + + +/* Interactable Objects */ +#object-container { + display: flex; + gap: 50px; + margin-bottom: 50px; +} + +.interactable { + padding: 15px; + border: 1px solid #333; + cursor: pointer; + text-align: center; + transition: color 0.3s; +} + +.interactable:hover { + color: var(--interactable-color); + text-decoration: underline; +} + +/* --- Puzzle 1: Hidden CSS clue --- */ +/* The key to the door is hidden in a pseudo-element */ +#locked-door::after { + content: "Key: 1709"; /* The actual clue */ + /* THIS MAKES IT INVISIBLE NORMALLY */ + color: transparent; + font-size: 1px; + position: absolute; + top: -9999px; + /* Player must use Inspect Element to find this 'content' property */ +} + +/* --- Message Box --- */ +#message-box { + width: 80%; + min-height: 50px; + background-color: #1a1a1a; + border: 1px solid #444; + padding: 10px; + text-align: center; +} + +/* --- Custom Context Menu --- */ +#custom-menu { + position: absolute; + background-color: #111; + border: 1px solid var(--pulse-color); + padding: 5px 0; + z-index: 1000; +} + +.menu-item { + padding: 8px 15px; + cursor: pointer; + font-size: 0.9em; + white-space: nowrap; + transition: background-color 0.1s; +} + +.menu-item:hover { + background-color: var(--pulse-color); + color: var(--bg-color); +} + +.hidden { + display: none !important; +} \ No newline at end of file diff --git a/games/ripple-runner/index.html b/games/ripple-runner/index.html new file mode 100644 index 00000000..25ccc2d0 --- /dev/null +++ b/games/ripple-runner/index.html @@ -0,0 +1,31 @@ + + + + + + Ripple Runner - Mini JS Games Hub + + + +
    +

    Ripple Runner

    +
    + +
    +
    +
    +

    Score: 0

    +

    Multiplier: 1x

    +

    Theme: Classic

    +
    +
    + + +
    +

    Tap or press arrows to switch lanes. Time with the beat!

    +
    +
    Made for Mini JS Games Hub
    +
    + + + \ No newline at end of file diff --git a/games/ripple-runner/screenshot.png b/games/ripple-runner/screenshot.png new file mode 100644 index 00000000..c48703ed --- /dev/null +++ b/games/ripple-runner/screenshot.png @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/games/ripple-runner/script.js b/games/ripple-runner/script.js new file mode 100644 index 00000000..0458acd8 --- /dev/null +++ b/games/ripple-runner/script.js @@ -0,0 +1,252 @@ +// Ripple Runner Game +const canvas = document.getElementById('game'); +const ctx = canvas.getContext('2d'); +const BASE_W = 400, BASE_H = 600, ASPECT = BASE_H / BASE_W; +let DPR = window.devicePixelRatio || 1; +let W = BASE_W, H = BASE_H; + +let frame = 0; +let gameState = 'menu'; // 'menu' | 'play' | 'paused' | 'over' +let score = 0; +let multiplier = 1; +let consecutiveBeats = 0; +let theme = 0; +let track = 0; +let muted = false; + +const lanes = 4; +const laneWidth = W / lanes; +let playerLane = 1; +let obstacles = []; +let speed = 3; +let beatInterval = 60; // frames + +const themes = [ + {bg: '#0f0f23', lanes: '#1a1a2e', player: '#00d4ff', obstacle: '#ff6b6b', beatFlash: '#00d4ff'}, + {bg: '#2d1b69', lanes: '#4c2a85', player: '#ff6b6b', obstacle: '#4ecdc4', beatFlash: '#ff6b6b'}, + {bg: '#1e3c72', lanes: '#2a5298', player: '#f9ca24', obstacle: '#45b7d1', beatFlash: '#f9ca24'} +]; + +const tracks = [ + {freq: 440, name: 'Classic'}, + {freq: 523, name: 'Upbeat'}, + {freq: 659, name: 'Energetic'} +]; + +canvas.setAttribute('role', 'application'); +canvas.setAttribute('aria-label', 'Ripple Runner game canvas'); +canvas.tabIndex = 0; + +function resizeCanvas() { + DPR = window.devicePixelRatio || 1; + const container = canvas.parentElement || document.body; + const maxWidth = Math.min(window.innerWidth - 40, 450); + const cssWidth = Math.min(container.clientWidth - 24 || BASE_W, maxWidth); + const cssHeight = Math.round(cssWidth * ASPECT); + + canvas.style.width = cssWidth + 'px'; + canvas.style.height = cssHeight + 'px'; + + canvas.width = Math.round(cssWidth * DPR); + canvas.height = Math.round(cssHeight * DPR); + ctx.setTransform(DPR, 0, 0, DPR, 0, 0); + + W = cssWidth; + H = cssHeight; +} + +window.addEventListener('resize', resizeCanvas); +resizeCanvas(); + +// Audio +let audioCtx; +try { + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); +} catch (e) { + console.warn('Web Audio API not supported'); +} + +function playBeat(freq) { + if (muted || !audioCtx) return; + const osc = audioCtx.createOscillator(); + const gain = audioCtx.createGain(); + osc.frequency.setValueAtTime(freq, audioCtx.currentTime); + gain.gain.setValueAtTime(0.3, audioCtx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2); + osc.connect(gain); + gain.connect(audioCtx.destination); + osc.start(); + osc.stop(audioCtx.currentTime + 0.2); +} + +function reset() { + frame = 0; + score = 0; + multiplier = 1; + consecutiveBeats = 0; + playerLane = 1; + obstacles = []; + speed = 3; + gameState = 'play'; + document.getElementById('score').textContent = 'Score: 0'; + document.getElementById('multiplier').textContent = 'Multiplier: 1x'; + document.getElementById('theme').textContent = 'Theme: ' + themes[theme].name || 'Classic'; +} + +function spawnObstacle() { + if (Math.random() < 0.02) { + const lane = Math.floor(Math.random() * lanes); + obstacles.push({lane, y: -50, height: 30 + Math.random() * 20}); + } +} + +function update() { + if (gameState === 'play') { + frame++; + score += Math.floor(speed * multiplier / 10); + + // Beat + if (frame % beatInterval === 0) { + playBeat(tracks[track].freq); + // Flash effect + ctx.fillStyle = themes[theme].beatFlash + '20'; + ctx.fillRect(0, 0, W, H); + } + + // Update obstacles + obstacles.forEach(obs => obs.y += speed); + obstacles = obstacles.filter(obs => obs.y < H + 50); + + // Check collisions + obstacles.forEach(obs => { + if (obs.lane === playerLane && obs.y + obs.height > H - 60 && obs.y < H - 10) { + gameState = 'over'; + } + }); + + spawnObstacle(); + + // Increase difficulty + speed += 0.001; + if (frame % 600 === 0) { + beatInterval = Math.max(30, beatInterval - 2); + } + + document.getElementById('score').textContent = 'Score: ' + score; + } +} + +function draw() { + ctx.clearRect(0, 0, W, H); + + // Background + ctx.fillStyle = themes[theme].bg; + ctx.fillRect(0, 0, W, H); + + // Lanes + ctx.strokeStyle = themes[theme].lanes; + ctx.lineWidth = 2; + for (let i = 1; i < lanes; i++) { + ctx.beginPath(); + ctx.moveTo(i * laneWidth, 0); + ctx.lineTo(i * laneWidth, H); + ctx.stroke(); + } + + // Obstacles + ctx.fillStyle = themes[theme].obstacle; + obstacles.forEach(obs => { + ctx.fillRect(obs.lane * laneWidth + 5, obs.y, laneWidth - 10, obs.height); + }); + + // Player + ctx.fillStyle = themes[theme].player; + ctx.beginPath(); + ctx.arc((playerLane + 0.5) * laneWidth, H - 30, 15, 0, Math.PI * 2); + ctx.fill(); + + if (gameState === 'menu') { + ctx.fillStyle = 'rgba(0,0,0,0.5)'; + ctx.fillRect(0, 0, W, H); + ctx.fillStyle = '#fff'; + ctx.font = '24px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Tap or press Space to start', W / 2, H / 2); + } + if (gameState === 'over') { + ctx.fillStyle = 'rgba(0,0,0,0.7)'; + ctx.fillRect(20, H / 2 - 60, W - 40, 120); + ctx.fillStyle = '#fff'; + ctx.font = '28px sans-serif'; + ctx.fillText('Game Over', W / 2, H / 2 - 20); + ctx.font = '20px sans-serif'; + ctx.fillText('Score: ' + score, W / 2, H / 2 + 10); + ctx.fillText('Multiplier: ' + multiplier + 'x', W / 2, H / 2 + 35); + } +} + +function loop() { + update(); + draw(); + requestAnimationFrame(loop); +} + +function switchLane(dir) { + if (gameState === 'play') { + const newLane = playerLane + dir; + if (newLane >= 0 && newLane < lanes) { + playerLane = newLane; + // Check if on beat + if (frame % beatInterval < 10) { // Within 10 frames of beat + consecutiveBeats++; + multiplier = Math.min(10, 1 + Math.floor(consecutiveBeats / 5)); + } else { + consecutiveBeats = 0; + multiplier = 1; + } + document.getElementById('multiplier').textContent = 'Multiplier: ' + multiplier + 'x'; + + // Unlock themes/tracks + if (score > 1000 && theme < themes.length - 1) theme++; + if (score > 2000 && track < tracks.length - 1) track++; + document.getElementById('theme').textContent = 'Theme: ' + (themes[theme].name || 'Classic'); + } + } +} + +// Input +canvas.addEventListener('touchstart', e => { + e.preventDefault(); + if (gameState === 'menu') reset(); + else if (gameState === 'over') reset(); + else { + const touchX = e.touches[0].clientX - canvas.getBoundingClientRect().left; + switchLane(touchX < W / 2 ? -1 : 1); + } +}); + +canvas.addEventListener('keydown', e => { + if (e.code === 'Space') { + e.preventDefault(); + if (gameState === 'menu') reset(); + else if (gameState === 'over') reset(); + } else if (e.code === 'ArrowLeft') { + e.preventDefault(); + switchLane(-1); + } else if (e.code === 'ArrowRight') { + e.preventDefault(); + switchLane(1); + } +}); + +// Buttons +document.getElementById('startBtn').addEventListener('click', () => { + if (gameState === 'menu' || gameState === 'over') reset(); +}); + +document.getElementById('muteBtn').addEventListener('click', () => { + muted = !muted; + document.getElementById('muteBtn').textContent = muted ? 'Unmute' : 'Mute'; +}); + +loop(); \ No newline at end of file diff --git a/games/ripple-runner/style.css b/games/ripple-runner/style.css new file mode 100644 index 00000000..277ef74f --- /dev/null +++ b/games/ripple-runner/style.css @@ -0,0 +1,19 @@ +*{ + box-sizing:border-box; + margin:0;padding:0 +} +body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;background:#1a1a2e;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px} +.game-wrap{background:#16213e;border-radius:15px;padding:20px;text-align:center;box-shadow:0 10px 30px rgba(0,0,0,0.3);max-width:450px;width:100%;color:#fff} +h1{color:#00d4ff;margin-bottom:15px;font-size:1.8em;text-shadow:0 0 10px #00d4ff} +canvas{background:#0f0f23;display:block;margin:0 auto;border-radius:10px;max-width:100%;height:auto;border:2px solid #00d4ff} +.info{margin-top:15px} +.stats{display:flex;justify-content:space-around;margin-bottom:10px;font-size:16px} +.controls{display:flex;gap:10px;justify-content:center;margin-bottom:10px} +button{padding:8px 16px;border:none;border-radius:8px;background:#00d4ff;color:#16213e;font-size:14px;cursor:pointer;transition:all 0.3s ease} +button:hover{background:#0099cc} +footer{font-size:12px;color:#666;margin-top:15px} +@media (max-width: 600px) { + .game-wrap{padding:15px} + canvas{width:100%;height:auto} + .stats{flex-direction:column;gap:5px} +} \ No newline at end of file diff --git a/games/rippleweaver/index.html b/games/rippleweaver/index.html new file mode 100644 index 00000000..a319306f --- /dev/null +++ b/games/rippleweaver/index.html @@ -0,0 +1,27 @@ + + + + + + Ripple Weaver + + + +
    +
    +
    Score: 0
    +
    Time: 60s
    +
    Click to create ripples โ€ข Guide particles through gates โ€ข Chain combos for bonus!
    +
    + +
    +

    Game Over!

    +

    Final Score: 0

    +

    Rating: Novice Weaver

    + +
    +
    + + + + \ No newline at end of file diff --git a/games/rippleweaver/script.js b/games/rippleweaver/script.js new file mode 100644 index 00000000..0ac7e938 --- /dev/null +++ b/games/rippleweaver/script.js @@ -0,0 +1,318 @@ + +const canvas = document.getElementById('gameCanvas'); +let ctx = null; +if (canvas) { + ctx = canvas.getContext('2d'); +} else { + console.error('Ripple Weaver: canvas element #gameCanvas not found'); +} + +let score = 0; +let timeLeft = 60; +let gameActive = true; +let particles = []; +let ripples = []; +let gates = []; +let combo = 0; +let lastScoreTime = 0; + +class Particle { + constructor(x, y) { + this.x = x; + this.y = y; + this.vx = (Math.random() - 0.5) * 2; + this.vy = (Math.random() - 0.5) * 2; + this.radius = 6; + this.color = `hsl(${Math.random() * 360}, 100%, 60%)`; + this.trail = []; + } + + update() { + ripples.forEach(ripple => { + const dx = this.x - ripple.x; + const dy = this.y - ripple.y; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < ripple.radius && dist > ripple.radius - 50) { + const force = (ripple.strength / dist) * 0.5; + this.vx += (dx / dist) * force; + this.vy += (dy / dist) * force; + } + }); + + this.x += this.vx; + this.y += this.vy; + + this.vx *= 0.98; + this.vy *= 0.98; + + if (this.x < this.radius) { this.x = this.radius; this.vx *= -0.8; } + if (this.x > canvas.width - this.radius) { this.x = canvas.width - this.radius; this.vx *= -0.8; } + if (this.y < this.radius) { this.y = this.radius; this.vy *= -0.8; } + if (this.y > canvas.height - this.radius) { this.y = canvas.height - this.radius; this.vy *= -0.8; } + + this.trail.push({x: this.x, y: this.y}); + if (this.trail.length > 10) this.trail.shift(); + } + + draw() { + ctx.shadowBlur = 15; + ctx.shadowColor = this.color; + + ctx.globalAlpha = 0.3; + for (let i = 0; i < this.trail.length; i++) { + ctx.beginPath(); + ctx.arc(this.trail[i].x, this.trail[i].y, this.radius * (i / this.trail.length), 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + } + + ctx.globalAlpha = 1; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + + ctx.shadowBlur = 0; + } +} + +class Ripple { + constructor(x, y) { + this.x = x; + this.y = y; + this.radius = 0; + this.maxRadius = 200; + this.strength = 15; + this.alpha = 1; + } + + update() { + this.radius += 4; + this.alpha = 1 - (this.radius / this.maxRadius); + return this.radius < this.maxRadius; + } + + draw() { + ctx.save(); + ctx.globalAlpha = this.alpha; + ctx.strokeStyle = '#8a2be2'; + ctx.lineWidth = 3; + ctx.shadowBlur = 10; + ctx.shadowColor = '#8a2be2'; + + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.stroke(); + + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius - 10, 0, Math.PI * 2); + ctx.stroke(); + + ctx.restore(); + } +} + +class Gate { + constructor() { + this.x = Math.random() * (canvas.width - 100) + 50; + this.y = Math.random() * (canvas.height - 100) + 50; + this.width = 80; + this.height = 15; + this.angle = Math.random() * Math.PI; + this.active = true; + this.pulsePhase = Math.random() * Math.PI * 2; + } + + checkCollision(particle) { + if (!this.active) return false; + + const cos = Math.cos(-this.angle); + const sin = Math.sin(-this.angle); + + const dx = particle.x - this.x; + const dy = particle.y - this.y; + + const rotX = dx * cos - dy * sin; + const rotY = dx * sin + dy * cos; + + return Math.abs(rotX) < this.width / 2 && Math.abs(rotY) < this.height / 2; + } + + draw() { + ctx.save(); + ctx.translate(this.x, this.y); + ctx.rotate(this.angle); + + if (this.active) { + const pulse = Math.sin(this.pulsePhase) * 0.3 + 0.7; + ctx.shadowBlur = 20 * pulse; + ctx.shadowColor = '#00ff88'; + ctx.strokeStyle = '#00ff88'; + ctx.lineWidth = 4; + + ctx.strokeRect(-this.width / 2, -this.height / 2, this.width, this.height); + + ctx.fillStyle = 'rgba(0, 255, 136, 0.2)'; + ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); + } else { + ctx.strokeStyle = '#333'; + ctx.lineWidth = 2; + ctx.strokeRect(-this.width / 2, -this.height / 2, this.width, this.height); + } + + ctx.restore(); + this.pulsePhase += 0.05; + } +} + +function init() { + score = 0; + timeLeft = 60; + gameActive = true; + particles = []; + ripples = []; + gates = []; + combo = 0; + + for (let i = 0; i < 8; i++) { + particles.push(new Particle( + Math.random() * canvas.width, + Math.random() * canvas.height + )); + } + + for (let i = 0; i < 5; i++) { + gates.push(new Gate()); + } + + document.getElementById('gameOver').style.display = 'none'; +} + +function update() { + if (!gameActive) return; + + particles.forEach(p => p.update()); + + ripples = ripples.filter(r => r.update()); + + gates.forEach(gate => { + particles.forEach(particle => { + if (gate.checkCollision(particle) && gate.active) { + gate.active = false; + + const now = Date.now(); + if (now - lastScoreTime < 1000) { + combo++; + } else { + combo = 1; + } + lastScoreTime = now; + + const points = 10 * combo; + score += points; + + setTimeout(() => { + gates[gates.indexOf(gate)] = new Gate(); + }, 2000); + + for (let i = 0; i < 10; i++) { + ctx.fillStyle = `hsl(${Math.random() * 360}, 100%, 60%)`; + ctx.beginPath(); + ctx.arc( + gate.x + (Math.random() - 0.5) * 40, + gate.y + (Math.random() - 0.5) * 40, + Math.random() * 3 + 1, + 0, Math.PI * 2 + ); + ctx.fill(); + } + } + }); + }); +} + +function draw() { + ctx.fillStyle = 'rgba(5, 2, 16, 0.3)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ripples.forEach(r => r.draw()); + gates.forEach(g => g.draw()); + particles.forEach(p => p.draw()); + + if (combo > 1) { + ctx.fillStyle = '#ffff00'; + ctx.font = 'bold 24px Courier New'; + ctx.textAlign = 'center'; + ctx.shadowBlur = 10; + ctx.shadowColor = '#ffff00'; + ctx.fillText(`COMBO x${combo}!`, canvas.width / 2, 50); + ctx.shadowBlur = 0; + } +} + +function gameLoop() { + if (!ctx) return; // abort if no rendering context + + update(); + draw(); + + const scoreEl = document.getElementById('score'); + const timerEl = document.getElementById('timer'); + if (scoreEl) scoreEl.textContent = `Score: ${score}`; + if (timerEl) timerEl.textContent = `Time: ${timeLeft}s`; + + requestAnimationFrame(gameLoop); +} + +function endGame() { + gameActive = false; + document.getElementById('finalScore').textContent = `Final Score: ${score}`; + + let rating = 'Novice Weaver'; + if (score > 500) rating = 'Master Weaver'; + else if (score > 300) rating = 'Expert Weaver'; + else if (score > 150) rating = 'Skilled Weaver'; + else if (score > 50) rating = 'Apprentice Weaver'; + + document.getElementById('rating').textContent = `Rating: ${rating}`; + document.getElementById('gameOver').style.display = 'block'; +} + +canvas.addEventListener('click', (e) => { + if (!gameActive) return; + + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + ripples.push(new Ripple(x, y)); +}); + +// Wait until DOM is ready before adding listeners and starting the game +window.addEventListener('DOMContentLoaded', () => { + if (!canvas || !ctx) { + console.error('Ripple Weaver: required elements missing, aborting init'); + return; + } + + // Ensure the canvas is visually below overlays + canvas.style.zIndex = '0'; + + const restartBtn = document.getElementById('restartBtn'); + if (restartBtn) { + restartBtn.addEventListener('click', () => { + init(); + }); + } + + setInterval(() => { + if (gameActive && timeLeft > 0) { + timeLeft--; + if (timeLeft === 0) endGame(); + } + }, 1000); + + init(); + requestAnimationFrame(gameLoop); +}); \ No newline at end of file diff --git a/games/rippleweaver/style.css b/games/rippleweaver/style.css new file mode 100644 index 00000000..4fa070bb --- /dev/null +++ b/games/rippleweaver/style.css @@ -0,0 +1,111 @@ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background: linear-gradient(135deg, #0a0a1f 0%, #1a0a2e 50%, #0f0a1f 100%); + font-family: 'Courier New', monospace; + overflow: hidden; +} + +#gameContainer { + text-align: center; + position: relative; +} + +canvas { + border: 3px solid #4a0e78; + border-radius: 10px; + background: radial-gradient(circle at center, #0d0520, #050210); + box-shadow: 0 0 40px rgba(138, 43, 226, 0.4); + cursor: crosshair; + /* ensure canvas sits below overlays */ + position: relative; + z-index: 0; +} + +#ui { + position: absolute; + top: 20px; + left: 20px; + right: 20px; + color: #fff; + font-size: 18px; + text-shadow: 0 0 10px rgba(138, 43, 226, 0.8); + pointer-events: none; +} + +#score { + float: left; + background: rgba(74, 14, 120, 0.3); + padding: 10px 20px; + border-radius: 5px; +} + +#timer { + float: right; + background: rgba(74, 14, 120, 0.3); + padding: 10px 20px; + border-radius: 5px; +} + +#instructions { + clear: both; + margin-top: 80px; + background: rgba(74, 14, 120, 0.3); + padding: 10px; + border-radius: 5px; + font-size: 14px; +} + +#gameOver { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(10, 5, 20, 0.95); + padding: 40px; + border-radius: 15px; + border: 3px solid #8a2be2; + display: none; + box-shadow: 0 0 50px rgba(138, 43, 226, 0.6); + z-index: 10; /* keep game-over on top so buttons are clickable */ +} + +#gameOver h2 { + color: #8a2be2; + font-size: 36px; + margin-bottom: 20px; +} + +#gameOver p { + color: #fff; + font-size: 24px; + margin: 10px 0; +} + +#restartBtn { + margin-top: 20px; + padding: 15px 40px; + font-size: 20px; + background: linear-gradient(135deg, #8a2be2, #4a0e78); + color: #fff; + border: none; + border-radius: 8px; + cursor: pointer; + font-family: 'Courier New', monospace; + pointer-events: all; + transition: all 0.3s; +} + +#restartBtn:hover { + transform: scale(1.1); + box-shadow: 0 0 20px rgba(138, 43, 226, 0.8); +} \ No newline at end of file diff --git a/games/roulette-wheel/index.html b/games/roulette-wheel/index.html new file mode 100644 index 00000000..08079224 --- /dev/null +++ b/games/roulette-wheel/index.html @@ -0,0 +1,127 @@ + + + + + + Roulette Wheel + + + +
    +

    ๐ŸŽฐ Roulette Wheel

    +

    Place your bets and spin the wheel!

    + +
    +
    Balance: $1000
    +
    Current Bet: $0
    +
    Last Spin: -
    +
    + +
    +
    + +
    + + +
    +
    + +
    +
    + Chip Value: +
    + + + + + +
    +
    + +
    + +
    0
    + + +
    1
    +
    2
    +
    3
    +
    4
    +
    5
    +
    6
    +
    7
    +
    8
    +
    9
    +
    10
    +
    11
    +
    12
    +
    13
    +
    14
    +
    15
    +
    16
    +
    17
    +
    18
    +
    19
    +
    20
    +
    21
    +
    22
    +
    23
    +
    24
    +
    25
    +
    26
    +
    27
    +
    28
    +
    29
    +
    30
    +
    31
    +
    32
    +
    33
    +
    34
    +
    35
    +
    36
    +
    + +
    + + + + + + + + + +
    +
    +
    + +
    + +
    +

    Payout Table:

    +
    +
    Straight Up (single number): 35:1
    +
    Split (2 numbers): 17:1
    +
    Street (3 numbers): 11:1
    +
    Corner (4 numbers): 8:1
    +
    Outside Bets: 2:1 or 1:1
    +
    +
    + +
    +

    How to Play:

    +
      +
    • Select a chip value and click on numbers or betting areas
    • +
    • Place multiple bets before spinning
    • +
    • Click SPIN WHEEL to start the game
    • +
    • Different bets pay different amounts
    • +
    • Straight up bets on single numbers pay 35:1
    • +
    • Outside bets like Red/Black pay 1:1
    • +
    • Start with $1000 and try to build your fortune
    • +
    +
    +
    + + + + \ No newline at end of file diff --git a/games/roulette-wheel/script.js b/games/roulette-wheel/script.js new file mode 100644 index 00000000..db3c49aa --- /dev/null +++ b/games/roulette-wheel/script.js @@ -0,0 +1,356 @@ +// Roulette Wheel Game +// European roulette with betting system and physics-based wheel + +// DOM elements +const canvas = document.getElementById('roulette-canvas'); +const ctx = canvas.getContext('2d'); +const balanceEl = document.getElementById('current-balance'); +const betAmountEl = document.getElementById('bet-amount'); +const resultNumberEl = document.getElementById('result-number'); +const messageEl = document.getElementById('message'); +const spinBtn = document.getElementById('spin-btn'); +const clearBetsBtn = document.getElementById('clear-bets-btn'); + +// Game constants +const CANVAS_SIZE = 400; +const CENTER_X = CANVAS_SIZE / 2; +const CENTER_Y = CANVAS_SIZE / 2; +const WHEEL_RADIUS = 180; + +// Roulette numbers (European: 0-36) +const NUMBERS = [ + 0, 32, 15, 19, 4, 21, 2, 25, 17, 34, 6, 27, 13, 36, 11, 30, 8, 23, + 10, 5, 24, 16, 33, 1, 20, 14, 31, 9, 22, 18, 29, 7, 28, 12, 35, 3, 26 +]; + +const RED_NUMBERS = [1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36]; +const BLACK_NUMBERS = [2, 4, 6, 8, 10, 11, 13, 15, 17, 20, 22, 24, 26, 28, 29, 31, 33, 35]; + +// Game variables +let balance = 1000; +let currentChipValue = 1; +let bets = {}; // {position: amount} +let totalBet = 0; +let isSpinning = false; +let wheelAngle = 0; +let spinVelocity = 0; +let resultNumber = null; +let animationId; + +// Initialize game +function initGame() { + drawWheel(); + updateDisplay(); + setupEventListeners(); +} + +// Draw the roulette wheel +function drawWheel() { + ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE); + + // Draw outer circle + ctx.beginPath(); + ctx.arc(CENTER_X, CENTER_Y, WHEEL_RADIUS, 0, Math.PI * 2); + ctx.fillStyle = '#8B4513'; + ctx.fill(); + ctx.strokeStyle = '#FFD700'; + ctx.lineWidth = 3; + ctx.stroke(); + + // Draw numbers + const angleStep = (Math.PI * 2) / NUMBERS.length; + NUMBERS.forEach((number, index) => { + const angle = wheelAngle + index * angleStep; + const x = CENTER_X + Math.cos(angle) * (WHEEL_RADIUS - 30); + const y = CENTER_Y + Math.sin(angle) * (WHEEL_RADIUS - 30); + + // Number background + ctx.beginPath(); + ctx.arc(x, y, 18, 0, Math.PI * 2); + ctx.fillStyle = getNumberColor(number); + ctx.fill(); + ctx.strokeStyle = '#FFF'; + ctx.lineWidth = 2; + ctx.stroke(); + + // Number text + ctx.fillStyle = '#FFF'; + ctx.font = 'bold 14px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(number.toString(), x, y); + }); + + // Draw center circle + ctx.beginPath(); + ctx.arc(CENTER_X, CENTER_Y, 20, 0, Math.PI * 2); + ctx.fillStyle = '#FFD700'; + ctx.fill(); + + // Draw pointer + ctx.beginPath(); + ctx.moveTo(CENTER_X, CENTER_Y - WHEEL_RADIUS - 10); + ctx.lineTo(CENTER_X - 10, CENTER_Y - WHEEL_RADIUS + 10); + ctx.lineTo(CENTER_X + 10, CENTER_Y - WHEEL_RADIUS + 10); + ctx.closePath(); + ctx.fillStyle = '#FF0000'; + ctx.fill(); +} + +// Get color for number +function getNumberColor(number) { + if (number === 0) return '#00FF00'; + return RED_NUMBERS.includes(number) ? '#FF0000' : '#000000'; +} + +// Update display elements +function updateDisplay() { + balanceEl.textContent = balance.toLocaleString(); + betAmountEl.textContent = totalBet.toLocaleString(); + if (resultNumber !== null) { + resultNumberEl.textContent = resultNumber; + resultNumberEl.style.color = getNumberColor(resultNumber); + } +} + +// Setup event listeners +function setupEventListeners() { + // Chip selection + document.querySelectorAll('.chip-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.chip-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + currentChipValue = parseInt(btn.dataset.value); + }); + }); + + // Number betting + document.querySelectorAll('.number-cell').forEach(cell => { + cell.addEventListener('click', () => { + if (isSpinning) return; + placeBet(cell.dataset.number, 'straight'); + }); + }); + + // Outside bets + document.querySelectorAll('.outside-bet').forEach(btn => { + btn.addEventListener('click', () => { + if (isSpinning) return; + placeBet(btn.dataset.bet, 'outside'); + }); + }); + + // Control buttons + spinBtn.addEventListener('click', spinWheel); + clearBetsBtn.addEventListener('click', clearBets); +} + +// Place a bet +function placeBet(position, type) { + if (balance < currentChipValue) { + messageEl.textContent = 'Not enough balance!'; + setTimeout(() => messageEl.textContent = '', 2000); + return; + } + + if (!bets[position]) { + bets[position] = 0; + } + + bets[position] += currentChipValue; + balance -= currentChipValue; + totalBet += currentChipValue; + + updateDisplay(); + updateBetDisplay(position); + messageEl.textContent = `Placed $${currentChipValue} on ${getBetDescription(position, type)}`; +} + +// Update bet display on table +function updateBetDisplay(position) { + // Remove existing chip stack + const existingChip = document.querySelector(`[data-number="${position}"] .chip-stack`) || + document.querySelector(`[data-bet="${position}"].chip-stack`); + if (existingChip) { + existingChip.remove(); + } + + // Add new chip stack + const betElement = document.querySelector(`[data-number="${position}"]`) || + document.querySelector(`[data-bet="${position}"]`); + + if (betElement && bets[position]) { + const chipStack = document.createElement('div'); + chipStack.className = 'chip-stack'; + chipStack.textContent = bets[position]; + betElement.appendChild(chipStack); + betElement.classList.add('bet-placed'); + } +} + +// Get bet description +function getBetDescription(position, type) { + if (type === 'straight') { + return `number ${position}`; + } + + switch (position) { + case '1st12': return '1st 12'; + case '2nd12': return '2nd 12'; + case '3rd12': return '3rd 12'; + case '1to18': return '1 to 18'; + case '19to36': return '19 to 36'; + case 'even': return 'Even'; + case 'odd': return 'Odd'; + case 'red': return 'Red'; + case 'black': return 'Black'; + default: return position; + } +} + +// Clear all bets +function clearBets() { + if (isSpinning) return; + + // Refund bets + Object.values(bets).forEach(amount => { + balance += amount; + }); + + // Clear bets + bets = {}; + totalBet = 0; + + // Remove visual bets + document.querySelectorAll('.chip-stack').forEach(chip => chip.remove()); + document.querySelectorAll('.bet-placed').forEach(el => el.classList.remove('bet-placed')); + + updateDisplay(); + messageEl.textContent = 'Bets cleared'; +} + +// Spin the wheel +function spinWheel() { + if (isSpinning || totalBet === 0) { + if (totalBet === 0) { + messageEl.textContent = 'Place a bet first!'; + } + return; + } + + isSpinning = true; + spinBtn.textContent = 'SPINNING...'; + spinBtn.classList.add('spinning'); + + // Random spin duration and final position + const spinDuration = 3000 + Math.random() * 2000; // 3-5 seconds + const finalAngle = Math.random() * Math.PI * 2; + + // Physics-based spinning + spinVelocity = 0.5 + Math.random() * 0.3; // Initial velocity + const startTime = Date.now(); + + function animate() { + const elapsed = Date.now() - startTime; + const progress = elapsed / spinDuration; + + if (progress < 1) { + // Decelerating spin + const easeOut = 1 - Math.pow(1 - progress, 3); + wheelAngle += spinVelocity * (1 - easeOut) * 0.1; + spinVelocity *= 0.995; // Friction + + drawWheel(); + animationId = requestAnimationFrame(animate); + } else { + // Spin complete + wheelAngle = finalAngle; + drawWheel(); + + // Determine winning number + determineWinner(); + } + } + + animate(); +} + +// Determine winning number based on wheel position +function determineWinner() { + // Calculate which number the pointer is pointing to + const normalizedAngle = (wheelAngle % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2); + const angleStep = (Math.PI * 2) / NUMBERS.length; + const numberIndex = Math.round(normalizedAngle / angleStep) % NUMBERS.length; + resultNumber = NUMBERS[numberIndex]; + + // Calculate winnings + let totalWinnings = 0; + Object.entries(bets).forEach(([position, amount]) => { + const winnings = calculateWinnings(position, amount, resultNumber); + totalWinnings += winnings; + }); + + // Update balance + balance += totalWinnings; + + // Display result + updateDisplay(); + messageEl.textContent = `Ball landed on ${resultNumber}! You won $${totalWinnings.toLocaleString()}!`; + + // Reset for next round + setTimeout(() => { + clearBets(); + isSpinning = false; + spinBtn.textContent = 'SPIN WHEEL'; + spinBtn.classList.remove('spinning'); + messageEl.textContent = 'Place your bets for the next spin!'; + }, 3000); +} + +// Calculate winnings for a bet +function calculateWinnings(position, amount, winningNumber) { + const pos = parseInt(position); + + // Straight up bet (single number) + if (!isNaN(pos) && pos === winningNumber) { + return amount * 36; // 35:1 payout + original bet + } + + // Outside bets + if (position === 'red' && RED_NUMBERS.includes(winningNumber)) { + return amount * 2; + } + if (position === 'black' && BLACK_NUMBERS.includes(winningNumber)) { + return amount * 2; + } + if (position === 'even' && winningNumber !== 0 && winningNumber % 2 === 0) { + return amount * 2; + } + if (position === 'odd' && winningNumber !== 0 && winningNumber % 2 === 1) { + return amount * 2; + } + if (position === '1to18' && winningNumber >= 1 && winningNumber <= 18) { + return amount * 2; + } + if (position === '19to36' && winningNumber >= 19 && winningNumber <= 36) { + return amount * 2; + } + if (position === '1st12' && winningNumber >= 1 && winningNumber <= 12) { + return amount * 3; + } + if (position === '2nd12' && winningNumber >= 13 && winningNumber <= 24) { + return amount * 3; + } + if (position === '3rd12' && winningNumber >= 25 && winningNumber <= 36) { + return amount * 3; + } + + return 0; // No win +} + +// Start the game +initGame(); + +// This roulette game has realistic physics +// The wheel spins with deceleration for authentic feel +// Multiple bet types with proper payouts \ No newline at end of file diff --git a/games/roulette-wheel/style.css b/games/roulette-wheel/style.css new file mode 100644 index 00000000..9e4be6a4 --- /dev/null +++ b/games/roulette-wheel/style.css @@ -0,0 +1,370 @@ +/* Roulette Wheel Game Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #0f0f23, #1a1a2e); + min-height: 100vh; + color: white; + overflow-x: hidden; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +h1 { + font-size: 2.5em; + text-align: center; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.5); + color: #ffd700; +} + +p { + text-align: center; + font-size: 1.1em; + margin-bottom: 20px; + opacity: 0.9; +} + +.game-stats { + display: flex; + justify-content: space-around; + flex-wrap: wrap; + margin: 20px 0; + font-size: 1.1em; + font-weight: bold; + background: rgba(255, 215, 0, 0.1); + padding: 15px; + border-radius: 10px; + border: 2px solid #ffd700; +} + +.game-area { + display: flex; + gap: 30px; + margin: 20px 0; + align-items: flex-start; +} + +.wheel-section { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; +} + +#roulette-canvas { + border: 5px solid #ffd700; + border-radius: 50%; + background: #1a1a1a; + box-shadow: 0 8px 32px rgba(0,0,0,0.5); + margin-bottom: 20px; +} + +.wheel-controls { + display: flex; + gap: 15px; +} + +.betting-table { + flex: 1; + background: #2a2a2a; + border-radius: 15px; + padding: 20px; + border: 3px solid #ffd700; +} + +.chip-selector { + margin-bottom: 20px; + text-align: center; +} + +.chip-selector span { + font-weight: bold; + margin-bottom: 10px; + display: block; +} + +.chip-buttons { + display: flex; + gap: 10px; + justify-content: center; + flex-wrap: wrap; +} + +.chip-btn { + background: #3498db; + color: white; + border: none; + border-radius: 50%; + width: 50px; + height: 50px; + font-weight: bold; + cursor: pointer; + transition: all 0.3s; + display: flex; + align-items: center; + justify-content: center; +} + +.chip-btn:hover { + background: #2980b9; + transform: scale(1.1); +} + +.chip-btn.active { + background: #ffd700; + color: #000; + box-shadow: 0 0 15px #ffd700; +} + +.table-grid { + display: grid; + grid-template-columns: 60px repeat(12, 1fr); + grid-template-rows: repeat(3, 50px); + gap: 2px; + margin-bottom: 20px; +} + +.number-cell { + background: #4a4a4a; + border: 1px solid #666; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + cursor: pointer; + transition: all 0.3s; + position: relative; + border-radius: 5px; +} + +.number-cell:hover { + background: #5a5a5a; + transform: scale(1.05); +} + +.number-cell.red { + background: #e74c3c; +} + +.number-cell.black { + background: #2c3e50; +} + +.number-cell.zero { + background: #27ae60; + grid-row: span 3; +} + +.chip-stack { + position: absolute; + top: -5px; + right: -5px; + background: #ffd700; + color: #000; + border-radius: 50%; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.7em; + font-weight: bold; + box-shadow: 0 2px 4px rgba(0,0,0,0.3); +} + +.outside-bets { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; +} + +.outside-bet { + background: #34495e; + color: white; + border: 2px solid #ffd700; + padding: 10px; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s; + font-weight: bold; + text-align: center; +} + +.outside-bet:hover { + background: #2c3e50; + transform: translateY(-2px); +} + +.outside-bet.bet-placed { + background: #ffd700; + color: #000; +} + +button { + background: #e74c3c; + color: white; + border: none; + padding: 12px 24px; + font-size: 1em; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s; + font-weight: bold; +} + +button:hover { + background: #c0392b; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.3); +} + +button:disabled { + background: #666; + cursor: not-allowed; + transform: none; +} + +#spin-btn { + background: #27ae60; + font-size: 1.2em; + padding: 15px 30px; +} + +#spin-btn:hover { + background: #229954; +} + +#spin-btn.spinning { + background: #666; +} + +#clear-bets-btn { + background: #f39c12; +} + +#clear-bets-btn:hover { + background: #e67e22; +} + +#message { + font-size: 1.2em; + margin: 20px 0; + min-height: 30px; + font-weight: bold; + color: #ffd700; + text-align: center; +} + +.payouts { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + margin: 20px 0; + text-align: center; +} + +.payouts h3 { + margin-bottom: 10px; + color: #ffd700; +} + +.payout-info { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 15px; +} + +.payout-info div { + background: rgba(255, 215, 0, 0.1); + padding: 8px 12px; + border-radius: 5px; + border: 1px solid #ffd700; +} + +.instructions { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + margin-top: 20px; + text-align: left; + max-width: 800px; + margin-left: auto; + margin-right: auto; +} + +.instructions h3 { + margin-bottom: 10px; + color: #ffd700; + text-align: center; +} + +.instructions ul { + list-style-type: disc; + padding-left: 20px; +} + +.instructions li { + margin: 5px 0; + line-height: 1.4; +} + +/* Responsive design */ +@media (max-width: 1024px) { + .game-area { + flex-direction: column; + align-items: center; + } + + .table-grid { + grid-template-columns: 50px repeat(12, 1fr); + grid-template-rows: repeat(3, 40px); + } + + .number-cell { + font-size: 0.9em; + } +} + +@media (max-width: 768px) { + #roulette-canvas { + width: 300px; + height: 300px; + } + + .table-grid { + grid-template-columns: 40px repeat(12, 1fr); + grid-template-rows: repeat(3, 35px); + font-size: 0.8em; + } + + .chip-buttons { + gap: 5px; + } + + .chip-btn { + width: 40px; + height: 40px; + font-size: 0.8em; + } + + .outside-bets { + grid-template-columns: repeat(2, 1fr); + } + + .wheel-controls { + flex-direction: column; + gap: 10px; + } +} \ No newline at end of file diff --git a/games/rpg_game/index.html b/games/rpg_game/index.html new file mode 100644 index 00000000..da3befff --- /dev/null +++ b/games/rpg_game/index.html @@ -0,0 +1,45 @@ + + + + + + The Form Submission RPG ๐Ÿ“œ + + + +
    +
    +

    The Form Submission RPG ๐Ÿ“œ

    +

    Your journey is dictated by the `<form>` tag.

    +
    + +
    +

    Character Status

    +

    Health: 10

    +

    Inventory: None

    +
    + +
    +

    You wake up in a damp, musty dungeon cell. The only light comes from a small crack in the wall. You must find a way out. What do you do?

    +
    + +
    +
    + + + + + +
    + + +
    +
    + + + + \ No newline at end of file diff --git a/games/rpg_game/script.js b/games/rpg_game/script.js new file mode 100644 index 00000000..aa6149de --- /dev/null +++ b/games/rpg_game/script.js @@ -0,0 +1,182 @@ +// --- 1. Game State and Constants --- +const GAME_STATE = { + health: 10, + inventory: ['Rusted Key'], + location: 'Cell', + enemyPresent: false, + enemyHealth: 5 +}; + +const LOCATIONS = { + 'Cell': { + description: "You are in a damp cell. There is a locked door (north) and a cracked wall (west).", + options: { + examine: { + wall: "The cracked wall is thin. Perhaps a heavy blow could break it.", + door: "The door is locked. It looks like it needs a key.", + key: (target) => GAME_STATE.inventory.includes(target) ? `A simple key, rusty but functional.` : `You don't have a ${target}.` + }, + move: { + north: (action) => GAME_STATE.inventory.includes('Rusted Key') ? 'You unlock the door! You move north.' : 'The door is locked. You need a key!', + west: (action) => 'The wall is too thick to break without a tool.', + east: 'There is a solid wall here.', + south: 'There is a solid wall here.' + } + } + }, + 'Corridor': { + description: "You are in a dark corridor. A Goblin stands before you!", + enemyPresent: true, + options: { + move: { + south: 'You retreat back into the cell.', + north: 'A solid wall blocks your path.' + }, + examine: { + goblin: 'A sickly looking goblin. Easy pickings.', + floor: 'The corridor floor is slimy.' + } + } + } + // Add more locations here... +}; + +// --- 2. DOM Elements --- +const D = (id) => document.getElementById(id); +const $ = { + form: D('action-form'), + gameText: D('game-text'), + health: D('player-health'), + inventory: D('player-inventory'), + actionSelect: D('action-select'), + targetInput: D('target-input') +}; + +// --- 3. Core Logic: Form Submission Handler --- + +$.form.addEventListener('submit', (e) => { + e.preventDefault(); // STOP the default form submission (Crucial for RPG loop) + + // Gather data from the form using FormData API + const formData = new FormData($.form); + const action = formData.get('action').toLowerCase(); + const target = formData.get('target').toLowerCase().trim(); + + let narrative = "You stand confused. That command yielded no results."; + + // Process the command + if (GAME_STATE.location === 'Corridor' && action === 'attack') { + narrative = handleCombat(target); + } else { + narrative = handleExploration(action, target); + } + + // Update the game state based on the outcome + updateGameState(narrative); +}); + + +// --- 4. Game Command Handlers --- + +function handleExploration(action, target) { + const locationData = LOCATIONS[GAME_STATE.location]; + const targetHandler = locationData.options[action]; + + if (!targetHandler) { + return `I do not understand the action: ${action}.`; + } + + let result = targetHandler[target]; + + if (typeof result === 'function') { + result = result(action); // Execute function if it's dynamic logic + } + + if (result) { + // Handle successful location change if the result is a move command + if (action === 'move' && result.includes('You move')) { + if (target === 'north' && GAME_STATE.location === 'Cell') { + GAME_STATE.location = 'Corridor'; + return 'The door creaks open. You step into a dark corridor. Be careful!'; + } else if (target === 'south' && GAME_STATE.location === 'Corridor') { + GAME_STATE.location = 'Cell'; + return 'You cautiously retreat into the cell, feeling safer there.'; + } + } + return result; + } else { + return `You cannot ${action} the ${target} here.`; + } +} + +function handleCombat(target) { + if (!GAME_STATE.enemyPresent) { + return "You swing at the air. No enemies here."; + } + + if (target !== 'goblin') { + return `You should attack the Goblin!`; + } + + // Player attack + const playerDamage = 2; + GAME_STATE.enemyHealth -= playerDamage; + let narrative = `You strike the Goblin for ${playerDamage} damage! (Goblin Health: ${GAME_STATE.enemyHealth}).\n`; + + if (GAME_STATE.enemyHealth <= 0) { + GAME_STATE.enemyPresent = false; + narrative += "The Goblin falls dead. The corridor is clear!"; + return narrative; + } + + // Enemy counter-attack + const enemyDamage = 1; + GAME_STATE.health -= enemyDamage; + narrative += `The Goblin strikes back, hitting you for ${enemyDamage} damage!`; + + return narrative; +} + + +// --- 5. Game State and UI Renderer --- + +function updateGameState(narrative) { + // 1. Render Narrative + if (narrative) { + $.gameText.textContent = `> ${narrative}\n\n${LOCATIONS[GAME_STATE.location].description}`; + } + + // 2. Render Status + $.health.textContent = GAME_STATE.health; + $.inventory.textContent = GAME_STATE.inventory.join(', ') || 'None'; + + // 3. Update Form Controls based on location/state + const locationData = LOCATIONS[GAME_STATE.location]; + + // Update the Attack option + if (locationData.enemyPresent) { + // If enemy is present, ensure Attack is enabled + $.actionSelect.querySelector('option[value="attack"]').disabled = false; + $.actionSelect.value = 'attack'; + $.targetInput.placeholder = 'Enter target (e.g., goblin)'; + } else { + // If no enemy, disable Attack + $.actionSelect.querySelector('option[value="attack"]').disabled = true; + $.targetInput.placeholder = 'Enter target or direction...'; + $.actionSelect.value = 'examine'; // Default back to examine + } + + // Check for Game Over + if (GAME_STATE.health <= 0) { + $.gameText.textContent = "You collapse from your wounds. GAME OVER."; + $.form.style.display = 'none'; + } +} + +// --- 6. Initialization --- + +// Set initial description +$.gameText.textContent = LOCATIONS[GAME_STATE.location].description; + +// Run initial UI update +updateGameState(null); \ No newline at end of file diff --git a/games/rpg_game/style.css b/games/rpg_game/style.css new file mode 100644 index 00000000..52be1554 --- /dev/null +++ b/games/rpg_game/style.css @@ -0,0 +1,102 @@ +:root { + --bg-color: #1a1a1a; + --text-color: #e0e0e0; + --accent-color: #50fa7b; + --form-bg: #282c34; +} + +/* Base Styles */ +body { + font-family: 'Courier New', monospace; + background-color: var(--bg-color); + color: var(--text-color); + margin: 0; + padding: 20px; + display: flex; + justify-content: center; +} + +#game-container { + width: 90%; + max-width: 800px; + border: 3px solid var(--accent-color); + padding: 20px; + box-shadow: 0 0 20px rgba(80, 250, 123, 0.5); +} + +header { + text-align: center; + margin-bottom: 20px; +} + +/* Status Display */ +#status-display { + border: 1px dashed var(--text-color); + padding: 10px; + margin-bottom: 20px; +} + +#status-display h2 { + color: var(--accent-color); + margin-top: 0; + font-size: 1.2em; +} + +/* Narrative Area */ +#narrative-display { + min-height: 100px; + margin-bottom: 20px; + padding: 10px; + background-color: #111; + border-left: 5px solid var(--accent-color); +} + +#game-text { + white-space: pre-wrap; /* Preserve formatting if needed */ + line-height: 1.5; +} + +/* Action Form (The Command Interface) */ +#action-form { + background-color: var(--form-bg); + padding: 20px; + border-radius: 4px; +} + +#form-controls { + display: grid; + grid-template-columns: auto 1fr; + gap: 10px 20px; + align-items: center; + margin-bottom: 15px; +} + +label { + text-align: right; + color: var(--accent-color); +} + +input[type="text"], select { + padding: 8px; + border: 1px solid #444; + background-color: #000; + color: var(--text-color); + font-family: inherit; + font-size: 1em; +} + +button[type="submit"] { + width: 100%; + padding: 12px; + background-color: var(--accent-color); + color: var(--bg-color); + border: none; + cursor: pointer; + font-weight: bold; + font-size: 1.1em; + transition: background-color 0.2s; +} + +button[type="submit"]:hover { + background-color: #7aff9b; +} \ No newline at end of file diff --git a/games/sand-draw/index.html b/games/sand-draw/index.html new file mode 100644 index 00000000..24df651f --- /dev/null +++ b/games/sand-draw/index.html @@ -0,0 +1,28 @@ + + + + + + Sand Draw | Mini JS Games Hub + + + +
    +

    Sand Draw ๐ŸŽจ

    +
    + + + + + + +
    + +
    + + + + + + + diff --git a/games/sand-draw/script.js b/games/sand-draw/script.js new file mode 100644 index 00000000..1b2f730b --- /dev/null +++ b/games/sand-draw/script.js @@ -0,0 +1,94 @@ +const canvas = document.getElementById("sandCanvas"); +const ctx = canvas.getContext("2d"); +canvas.width = window.innerWidth * 0.9; +canvas.height = window.innerHeight * 0.7; + +let drawing = false; +let particles = []; +let animationId; +let color = document.getElementById("color-picker").value; +let thickness = document.getElementById("thickness").value; + +const drawSound = document.getElementById("draw-sound"); + +// Obstacles +const obstacles = []; +for(let i=0; i<5; i++){ + obstacles.push({ + x: Math.random() * canvas.width, + y: Math.random() * canvas.height, + radius: 30 + Math.random()*50 + }); +} + +// Particle class +class Particle { + constructor(x, y, color, size){ + this.x = x; + this.y = y; + this.color = color; + this.size = size; + this.life = 50; + } + draw(){ + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI*2); + ctx.fillStyle = this.color; + ctx.shadowColor = this.color; + ctx.shadowBlur = 15; + ctx.fill(); + } + update(){ + this.life--; + this.y -= 0.5; + this.draw(); + } +} + +// Mouse events +canvas.addEventListener("mousedown", e => drawing = true); +canvas.addEventListener("mouseup", e => drawing = false); +canvas.addEventListener("mouseleave", e => drawing = false); +canvas.addEventListener("mousemove", e => { + if(drawing){ + const particle = new Particle(e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop, color, thickness); + particles.push(particle); + drawSound.currentTime = 0; + drawSound.play(); + } +}); + +// Animation loop +function animate(){ + ctx.fillStyle = "rgba(17,17,17,0.2)"; + ctx.fillRect(0,0,canvas.width,canvas.height); + + // Draw obstacles + obstacles.forEach(obs => { + ctx.beginPath(); + ctx.arc(obs.x, obs.y, obs.radius,0,Math.PI*2); + ctx.fillStyle = "rgba(255,0,255,0.2)"; + ctx.shadowColor = "#ff00ff"; + ctx.shadowBlur = 20; + ctx.fill(); + }); + + particles.forEach((p, index) => { + p.update(); + if(p.life <=0) particles.splice(index,1); + }); + + animationId = requestAnimationFrame(animate); +} + +// Controls +document.getElementById("start-btn").addEventListener("click", ()=> animate()); +document.getElementById("pause-btn").addEventListener("click", ()=> cancelAnimationFrame(animationId)); +document.getElementById("resume-btn").addEventListener("click", ()=> animate()); +document.getElementById("restart-btn").addEventListener("click", ()=>{ + particles = []; + ctx.clearRect(0,0,canvas.width,canvas.height); +}); + +document.getElementById("color-picker").addEventListener("change", (e)=> color = e.target.value); +document.getElementById("thickness").addEventListener("change", (e)=> thickness = e.target.value); diff --git a/games/sand-draw/style.css b/games/sand-draw/style.css new file mode 100644 index 00000000..4a55876a --- /dev/null +++ b/games/sand-draw/style.css @@ -0,0 +1,46 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: #0d0d0d; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + overflow: hidden; + color: #fff; +} + +.game-ui { + text-align: center; +} + +h1 { + font-size: 2rem; + margin-bottom: 10px; + text-shadow: 0 0 10px #ff00ff; +} + +.controls { + margin-bottom: 10px; +} + +.controls button, .controls input { + margin: 0 5px; + padding: 8px 12px; + border-radius: 5px; + border: none; + font-size: 1rem; + cursor: pointer; + transition: 0.3s; +} + +.controls button:hover { + box-shadow: 0 0 10px #00ffff; +} + +canvas { + background: #111; + display: block; + border: 2px solid #ff00ff; + border-radius: 10px; +} diff --git a/games/sand-flow-simulator/index.html b/games/sand-flow-simulator/index.html new file mode 100644 index 00000000..d8d47675 --- /dev/null +++ b/games/sand-flow-simulator/index.html @@ -0,0 +1,27 @@ + + + + + + Sand Flow Simulator | Mini JS Games Hub + + + +
    +

    ๐ŸŒพ Sand Flow Simulator

    + + + +
    + + + +
    +
    + + + + + + + diff --git a/games/sand-flow-simulator/script.js b/games/sand-flow-simulator/script.js new file mode 100644 index 00000000..49e60528 --- /dev/null +++ b/games/sand-flow-simulator/script.js @@ -0,0 +1,106 @@ +const canvas = document.getElementById("sandCanvas"); +const ctx = canvas.getContext("2d"); + +canvas.width = 800; +canvas.height = 500; + +let particles = []; +let isPaused = false; +let isRunning = false; + +const sandSound = document.getElementById("sandSound"); +const resetSound = document.getElementById("resetSound"); + +class Particle { + constructor(x, y, color) { + this.x = x; + this.y = y; + this.color = color; + this.size = 2; + this.vy = 0; + } + + update() { + if (this.y + this.size < canvas.height) { + this.vy += 0.2; + this.y += this.vy; + + // Check collision + const below = particles.find(p => Math.abs(p.x - this.x) < this.size && Math.abs(p.y - (this.y + this.size)) < this.size); + if (below) { + this.y -= this.vy; + this.vy = 0; + // slide slightly + this.x += (Math.random() - 0.5) * 2; + } + } else { + this.vy = 0; + } + } + + draw() { + ctx.beginPath(); + ctx.fillStyle = this.color; + ctx.shadowBlur = 15; + ctx.shadowColor = this.color; + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fill(); + ctx.closePath(); + } +} + +function addSand(x, y) { + for (let i = 0; i < 5; i++) { + const color = `hsl(${40 + Math.random() * 30}, 100%, 60%)`; + particles.push(new Particle(x + (Math.random() - 0.5) * 10, y + (Math.random() - 0.5) * 10, color)); + } +} + +canvas.addEventListener("mousedown", (e) => { + if (!isRunning) return; + sandSound.play(); + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + addSand(x, y); + const interval = setInterval(() => { + if (!isPaused) addSand(x, y); + }, 50); + + const stop = () => { + clearInterval(interval); + canvas.removeEventListener("mouseup", stop); + }; + canvas.addEventListener("mouseup", stop); +}); + +function animate() { + if (!isPaused) { + ctx.fillStyle = "rgba(0, 0, 0, 0.2)"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + particles.forEach(p => { + p.update(); + p.draw(); + }); + } + requestAnimationFrame(animate); +} + +document.getElementById("startBtn").addEventListener("click", () => { + isRunning = true; + isPaused = false; + sandSound.play(); +}); + +document.getElementById("pauseBtn").addEventListener("click", () => { + isPaused = !isPaused; +}); + +document.getElementById("resetBtn").addEventListener("click", () => { + particles = []; + ctx.clearRect(0, 0, canvas.width, canvas.height); + resetSound.play(); +}); + +animate(); diff --git a/games/sand-flow-simulator/style.css b/games/sand-flow-simulator/style.css new file mode 100644 index 00000000..d7d9cb87 --- /dev/null +++ b/games/sand-flow-simulator/style.css @@ -0,0 +1,54 @@ +body { + margin: 0; + height: 100vh; + background: radial-gradient(circle at top, #1e1e2f, #0a0a12); + display: flex; + justify-content: center; + align-items: center; + color: #fff; + font-family: "Poppins", sans-serif; + overflow: hidden; +} + +.container { + text-align: center; +} + +h1 { + font-size: 2rem; + color: #ffe97f; + text-shadow: 0 0 10px #ffbf00, 0 0 20px #ffbf00; +} + +canvas { + display: block; + background: linear-gradient(to bottom, #1e1e2f, #141422); + margin: 20px auto; + border: 2px solid #ffbf00; + border-radius: 10px; + box-shadow: 0 0 20px #ffbf00; + cursor: crosshair; +} + +.controls { + margin-top: 10px; +} + +button { + background: linear-gradient(90deg, #ffbf00, #ffea00); + border: none; + border-radius: 8px; + color: #000; + padding: 10px 18px; + font-weight: bold; + font-size: 1rem; + cursor: pointer; + transition: 0.2s; + margin: 0 5px; + box-shadow: 0 0 10px #ffbf00; +} + +button:hover { + transform: scale(1.1); + box-shadow: 0 0 20px #ffe97f, 0 0 30px #ffbf00; +} diff --git a/games/science-lab/index.html b/games/science-lab/index.html new file mode 100644 index 00000000..d7b50bf8 --- /dev/null +++ b/games/science-lab/index.html @@ -0,0 +1,136 @@ + + + + + + Science Lab - Mini JS Games Hub + + + +
    +
    +

    Science Lab

    +

    Conduct virtual experiments and discover the wonders of science!

    +
    + +
    +
    +
    + Score: + 0 +
    +
    + Experiment: + None +
    +
    + Accuracy: + 0% +
    +
    + Time: + 60 +
    +
    + + +
    +

    Choose Your Experiment

    +
    +
    +
    โฐ
    +

    Pendulum Physics

    +

    Study gravity and oscillation

    +
    +
    +
    โšก
    +

    Electric Circuit

    +

    Build and test electrical circuits

    +
    +
    +
    ๐ŸŽจ
    +

    Color Chemistry

    +

    Mix colors and learn about pigments

    +
    +
    +
    ๐ŸŒฑ
    +

    Plant Biology

    +

    Observe plant growth factors

    +
    +
    +
    ๐Ÿš€
    +

    Projectile Motion

    +

    Launch objects and study trajectories

    +
    +
    +
    ๐ŸŠ
    +

    Density Layers

    +

    Explore density with liquids

    +
    +
    +
    + + + + +
    + +
    + + + +
    +
    + + +
    + + + + \ No newline at end of file diff --git a/games/science-lab/script.js b/games/science-lab/script.js new file mode 100644 index 00000000..70f94fc0 --- /dev/null +++ b/games/science-lab/script.js @@ -0,0 +1,937 @@ +// Science Lab Game +// Conduct virtual experiments and learn scientific principles + +// DOM elements +const scoreEl = document.getElementById('current-score'); +const experimentNameEl = document.getElementById('current-experiment'); +const accuracyEl = document.getElementById('current-accuracy'); +const timeLeftEl = document.getElementById('time-left'); +const experimentSelectionEl = document.getElementById('experiment-selection'); +const labWorkspaceEl = document.getElementById('lab-workspace'); +const experimentTitleEl = document.getElementById('experiment-title'); +const toolsPanelEl = document.getElementById('tools-panel'); +const experimentCanvasEl = document.getElementById('experiment-canvas'); +const resultsPanelEl = document.getElementById('results-panel'); +const observationsEl = document.getElementById('observations'); +const measurementsEl = document.getElementById('measurements'); +const resetBtn = document.getElementById('reset-btn'); +const hintBtn = document.getElementById('hint-btn'); +const checkBtn = document.getElementById('check-btn'); +const backToSelectionBtn = document.getElementById('back-to-selection-btn'); +const startBtn = document.getElementById('start-btn'); +const quitBtn = document.getElementById('quit-btn'); +const messageEl = document.getElementById('message'); +const resultsEl = document.getElementById('results'); +const finalScoreEl = document.getElementById('final-score'); +const experimentsCompletedEl = document.getElementById('experiments-completed'); +const averageAccuracyEl = document.getElementById('average-accuracy'); +const scientificGradeEl = document.getElementById('scientific-grade'); +const playAgainBtn = document.getElementById('play-again-btn'); + +// Game variables +let currentExperiment = null; +let score = 0; +let timeLeft = 60; +let timerInterval = null; +let gameActive = false; +let experimentsCompleted = 0; +let totalAccuracy = 0; +let hintUsed = false; + +// Experiment configurations +const experiments = { + pendulum: { + name: "Pendulum Physics", + description: "Study how pendulum length affects oscillation period", + objective: "Measure the relationship between pendulum length and period", + tools: [ + { id: 'length-tool', icon: '๐Ÿ“', label: 'Adjust Length', action: 'adjustLength' }, + { id: 'release-tool', icon: '๐ŸŽฏ', label: 'Release Bob', action: 'releaseBob' }, + { id: 'measure-tool', icon: 'โฑ๏ธ', label: 'Measure Period', action: 'measurePeriod' } + ], + setup: setupPendulumExperiment, + check: checkPendulumResults, + hint: "Longer pendulums take more time to complete one swing" + }, + + circuit: { + name: "Electric Circuit", + description: "Build a complete circuit to light the bulb", + objective: "Connect battery, wires, and bulb in the correct sequence", + tools: [ + { id: 'battery-tool', icon: '๐Ÿ”‹', label: 'Battery', action: 'placeBattery' }, + { id: 'wire-tool', icon: '๐Ÿ”Œ', label: 'Wire', action: 'placeWire' }, + { id: 'bulb-tool', icon: '๐Ÿ’ก', label: 'Bulb', action: 'placeBulb' }, + { id: 'resistor-tool', icon: 'โšก', label: 'Resistor', action: 'placeResistor' } + ], + setup: setupCircuitExperiment, + check: checkCircuitResults, + hint: "Connect positive (+) to negative (-) through the bulb" + }, + + 'color-mixing': { + name: "Color Chemistry", + description: "Mix primary colors to create secondary colors", + objective: "Predict and create the correct mixed color", + tools: [ + { id: 'red-paint', icon: '๐Ÿ”ด', label: 'Red Paint', action: 'addRed' }, + { id: 'blue-paint', icon: '๐Ÿ”ต', label: 'Blue Paint', action: 'addBlue' }, + { id: 'yellow-paint', icon: '๐ŸŸก', label: 'Yellow Paint', action: 'addYellow' }, + { id: 'mix-tool', icon: '๐ŸŒ€', label: 'Mix Colors', action: 'mixColors' } + ], + setup: setupColorMixingExperiment, + check: checkColorMixingResults, + hint: "Red + Blue = Purple, Red + Yellow = Orange, Blue + Yellow = Green" + }, + + 'plant-growth': { + name: "Plant Biology", + description: "Observe how different conditions affect plant growth", + objective: "Determine the optimal conditions for plant growth", + tools: [ + { id: 'water-tool', icon: '๐Ÿ’ง', label: 'Add Water', action: 'addWater' }, + { id: 'sunlight-tool', icon: 'โ˜€๏ธ', label: 'Adjust Light', action: 'adjustLight' }, + { id: 'soil-tool', icon: '๐ŸŒฑ', label: 'Change Soil', action: 'changeSoil' }, + { id: 'time-tool', icon: 'โฐ', label: 'Advance Time', action: 'advanceTime' } + ], + setup: setupPlantGrowthExperiment, + check: checkPlantGrowthResults, + hint: "Plants need water, sunlight, and good soil to grow" + }, + + projectile: { + name: "Projectile Motion", + description: "Launch objects and study their trajectory", + objective: "Predict where the projectile will land", + tools: [ + { id: 'angle-tool', icon: '๐Ÿ“', label: 'Set Angle', action: 'setAngle' }, + { id: 'power-tool', icon: '๐Ÿ’ช', label: 'Set Power', action: 'setPower' }, + { id: 'launch-tool', icon: '๐Ÿš€', label: 'Launch', action: 'launchProjectile' }, + { id: 'measure-tool', icon: '๐Ÿ“', label: 'Measure Distance', action: 'measureDistance' } + ], + setup: setupProjectileExperiment, + check: checkProjectileResults, + hint: "Higher angles give longer range, higher power gives more distance" + }, + + density: { + name: "Density Layers", + description: "Observe how liquids of different densities separate", + objective: "Arrange liquids in order of density", + tools: [ + { id: 'honey-tool', icon: '๐Ÿฏ', label: 'Add Honey', action: 'addHoney' }, + { id: 'oil-tool', icon: '๐Ÿ›ข๏ธ', label: 'Add Oil', action: 'addOil' }, + { id: 'water-tool', icon: '๐Ÿ’ง', label: 'Add Water', action: 'addWater' }, + { id: 'alcohol-tool', icon: '๐Ÿฅƒ', label: 'Add Alcohol', action: 'addAlcohol' } + ], + setup: setupDensityExperiment, + check: checkDensityResults, + hint: "Denser liquids sink below less dense liquids" + } +}; + +// Initialize game +function initGame() { + setupEventListeners(); + updateDisplay(); +} + +// Setup event listeners +function setupEventListeners() { + // Experiment selection + document.querySelectorAll('.experiment-card').forEach(card => { + card.addEventListener('click', () => selectExperiment(card.dataset.experiment)); + }); + + // Game controls + startBtn.addEventListener('click', startGame); + quitBtn.addEventListener('click', endGame); + playAgainBtn.addEventListener('click', resetGame); + backToSelectionBtn.addEventListener('click', backToSelection); + + // Lab controls + resetBtn.addEventListener('click', resetExperiment); + hintBtn.addEventListener('click', useHint); + checkBtn.addEventListener('click', checkResults); +} + +// Start the game +function startGame() { + gameActive = true; + score = 0; + experimentsCompleted = 0; + totalAccuracy = 0; + + startBtn.style.display = 'none'; + quitBtn.style.display = 'inline-block'; + + updateDisplay(); + showMessage('Choose an experiment to begin your scientific journey!', 'hint'); +} + +// Select experiment +function selectExperiment(experimentId) { + if (!gameActive) return; + + currentExperiment = experiments[experimentId]; + experimentNameEl.textContent = currentExperiment.name; + experimentTitleEl.textContent = currentExperiment.name; + + // Hide selection, show lab + experimentSelectionEl.style.display = 'none'; + labWorkspaceEl.style.display = 'block'; + backToSelectionBtn.style.display = 'inline-block'; + + // Setup experiment + setupTools(); + currentExperiment.setup(); + + // Start timer + startTimer(); + + showMessage(`Objective: ${currentExperiment.objective}`, 'hint'); +} + +// Setup tools panel +function setupTools() { + toolsPanelEl.innerHTML = '

    Tools

    '; + + currentExperiment.tools.forEach(tool => { + const toolElement = document.createElement('div'); + toolElement.className = 'tool-item'; + toolElement.dataset.tool = tool.id; + toolElement.innerHTML = ` +
    ${tool.icon}
    +
    ${tool.label}
    + `; + toolElement.addEventListener('click', () => useTool(tool.action)); + toolsPanelEl.appendChild(toolElement); + }); +} + +// Use tool +function useTool(action) { + if (!currentExperiment || !gameActive) return; + + // Highlight selected tool + document.querySelectorAll('.tool-item').forEach(item => { + item.classList.remove('selected'); + }); + event.target.closest('.tool-item').classList.add('selected'); + + // Execute tool action + const experimentActions = { + // Pendulum actions + adjustLength: () => adjustPendulumLength(), + releaseBob: () => releasePendulumBob(), + measurePeriod: () => measurePendulumPeriod(), + + // Circuit actions + placeBattery: () => placeCircuitComponent('battery'), + placeWire: () => placeCircuitComponent('wire'), + placeBulb: () => placeCircuitComponent('bulb'), + placeResistor: () => placeCircuitComponent('resistor'), + + // Color mixing actions + addRed: () => addColor('red'), + addBlue: () => addColor('blue'), + addYellow: () => addColor('yellow'), + mixColors: () => mixColors(), + + // Plant growth actions + addWater: () => adjustPlantCondition('water'), + adjustLight: () => adjustPlantCondition('light'), + changeSoil: () => adjustPlantCondition('soil'), + advanceTime: () => advancePlantTime(), + + // Projectile actions + setAngle: () => setProjectileAngle(), + setPower: () => setProjectilePower(), + launchProjectile: () => launchProjectile(), + measureDistance: () => measureProjectileDistance(), + + // Density actions + addHoney: () => addLiquid('honey'), + addOil: () => addLiquid('oil'), + addWater: () => addLiquid('water'), + addAlcohol: () => addLiquid('alcohol') + }; + + if (experimentActions[action]) { + experimentActions[action](); + } +} + +// Pendulum Experiment +function setupPendulumExperiment() { + experimentCanvasEl.innerHTML = ` +
    +
    +
    +
    +
    +
    + `; + + // Initialize pendulum state + window.pendulumState = { + length: 200, + angle: 0, + swinging: false, + period: 0, + measurements: [] + }; + + updateObservations('Pendulum ready for experimentation'); +} + +function adjustPendulumLength() { + const lengths = [150, 200, 250, 300]; + const currentIndex = lengths.indexOf(window.pendulumState.length); + const newIndex = (currentIndex + 1) % lengths.length; + window.pendulumState.length = lengths[newIndex]; + + const pendulumString = document.getElementById('pendulum-string'); + pendulumString.style.height = window.pendulumState.length + 'px'; + + updateObservations(`Pendulum length adjusted to ${window.pendulumState.length}px`); +} + +function releasePendulumBob() { + if (window.pendulumState.swinging) return; + + window.pendulumState.swinging = true; + window.pendulumState.startTime = Date.now(); + + const pendulumString = document.getElementById('pendulum-string'); + let angle = 30; + let direction = 1; + + const swing = () => { + angle += direction * 2; + if (Math.abs(angle) >= 30) { + direction *= -1; + } + + pendulumString.style.transform = `rotate(${angle}deg)`; + + if (window.pendulumState.swinging) { + requestAnimationFrame(swing); + } + }; + + swing(); + updateObservations('Pendulum released and swinging'); +} + +function measurePendulumPeriod() { + if (!window.pendulumState.swinging) { + showMessage('Release the pendulum first!', 'incorrect'); + return; + } + + const period = Math.sqrt(window.pendulumState.length / 980) * 2 * Math.PI; + window.pendulumState.period = period; + window.pendulumState.measurements.push(period); + + updateMeasurements(`Period: ${period.toFixed(2)}s (Length: ${window.pendulumState.length}px)`); + updateObservations(`Measured period: ${period.toFixed(2)} seconds`); +} + +// Circuit Experiment +function setupCircuitExperiment() { + experimentCanvasEl.innerHTML = ` +
    + + + +
    + `; + + window.circuitState = { + components: [], + wires: [], + circuitComplete: false + }; + + updateObservations('Circuit board ready. Place components to complete the circuit.'); +} + +function placeCircuitComponent(type) { + const board = document.getElementById('circuit-board'); + const component = document.getElementById(type); + + if (!component) return; + + component.style.display = 'block'; + component.style.left = Math.random() * 300 + 'px'; + component.style.top = Math.random() * 200 + 'px'; + + // Make component draggable + makeDraggable(component); + + window.circuitState.components.push({ type, element: component }); + updateObservations(`${type.charAt(0).toUpperCase() + type.slice(1)} placed on circuit board`); +} + +function makeDraggable(element) { + let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; + + element.onmousedown = dragMouseDown; + + function dragMouseDown(e) { + e.preventDefault(); + pos3 = e.clientX; + pos4 = e.clientY; + document.onmouseup = closeDragElement; + document.onmousemove = elementDrag; + } + + function elementDrag(e) { + e.preventDefault(); + pos1 = pos3 - e.clientX; + pos2 = pos4 - e.clientY; + pos3 = e.clientX; + pos4 = e.clientY; + element.style.top = (element.offsetTop - pos2) + "px"; + element.style.left = (element.offsetLeft - pos1) + "px"; + } + + function closeDragElement() { + document.onmouseup = null; + document.onmousemove = null; + checkCircuitCompletion(); + } +} + +function checkCircuitCompletion() { + const battery = document.getElementById('battery'); + const bulb = document.getElementById('bulb'); + const resistor = document.getElementById('resistor'); + + if (battery.style.display !== 'none' && bulb.style.display !== 'none') { + // Simple proximity check + const batteryRect = battery.getBoundingClientRect(); + const bulbRect = bulb.getBoundingClientRect(); + + const distance = Math.sqrt( + Math.pow(batteryRect.left - bulbRect.left, 2) + + Math.pow(batteryRect.top - bulbRect.top, 2) + ); + + if (distance < 150) { + bulb.classList.add('lit'); + window.circuitState.circuitComplete = true; + updateObservations('Circuit complete! Bulb is lit.'); + } else { + bulb.classList.remove('lit'); + window.circuitState.circuitComplete = false; + } + } +} + +// Color Mixing Experiment +function setupColorMixingExperiment() { + experimentCanvasEl.innerHTML = ` +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + `; + + window.colorState = { + container1: { red: 0, blue: 0, yellow: 0 }, + container2: { red: 0, blue: 0, yellow: 0 }, + mixed: false + }; + + updateObservations('Add colors to containers and mix them to create new colors'); +} + +function addColor(color) { + // Alternate between containers + const container = window.colorState.mixed ? 'container1' : 'container2'; + const containerState = window.colorState[container]; + + containerState[color] = Math.min(100, containerState[color] + 25); + + const liquid = document.getElementById(`liquid${container.slice(-1)}`); + const total = containerState.red + containerState.blue + containerState.yellow; + liquid.style.height = Math.min(100, total) + '%'; + + // Update color based on mixture + const r = containerState.red; + const g = containerState.yellow; + const b = containerState.blue; + liquid.style.background = `rgb(${r * 2.55}, ${g * 2.55}, ${b * 2.55})`; + + updateObservations(`Added ${color} to ${container}`); +} + +function mixColors() { + const c1 = window.colorState.container1; + const c2 = window.colorState.container2; + + const mixed = { + red: (c1.red + c2.red) / 2, + green: (c1.yellow + c2.yellow) / 2, + blue: (c1.blue + c2.blue) / 2 + }; + + const mixedLiquid = document.getElementById('mixed-liquid'); + mixedLiquid.style.height = '100%'; + mixedLiquid.style.background = `rgb(${mixed.red * 2.55}, ${mixed.green * 2.55}, ${mixed.blue * 2.55})`; + + window.colorState.mixed = true; + updateObservations('Colors mixed! Observe the resulting color.'); +} + +// Plant Growth Experiment +function setupPlantGrowthExperiment() { + experimentCanvasEl.innerHTML = ` +
    +
    +
    +
    +
    + `; + + window.plantState = { + water: 50, + light: 50, + soil: 50, + time: 0, + height: 20 + }; + + updateObservations('Adjust conditions to help the plant grow'); + updateMeasurements('Height: 20px'); +} + +function adjustPlantCondition(condition) { + const levels = [25, 50, 75, 100]; + const currentIndex = levels.indexOf(window.plantState[condition]); + const newIndex = (currentIndex + 1) % levels.length; + window.plantState[condition] = levels[newIndex]; + + updateObservations(`${condition.charAt(0).toUpperCase() + condition.slice(1)} level: ${window.plantState[condition]}%`); +} + +function advancePlantTime() { + window.plantState.time += 1; + + // Calculate growth based on conditions + const waterFactor = window.plantState.water / 100; + const lightFactor = window.plantState.light / 100; + const soilFactor = window.plantState.soil / 100; + + const growthRate = (waterFactor + lightFactor + soilFactor) / 3; + const growth = Math.round(growthRate * 10); + + window.plantState.height += growth; + + const plant = document.getElementById('plant'); + plant.style.height = window.plantState.height + 'px'; + + if (growth > 5) { + plant.classList.add('growing'); + setTimeout(() => plant.classList.remove('growing'), 1000); + } + + updateMeasurements(`Height: ${window.plantState.height}px (Time: ${window.plantState.time} days)`); + updateObservations(`Plant grew ${growth}px in the last day`); +} + +// Projectile Experiment +function setupProjectileExperiment() { + experimentCanvasEl.innerHTML = ` +
    +
    + +
    +
    + `; + + window.projectileState = { + angle: 45, + power: 50, + launched: false, + distance: 0 + }; + + updateObservations('Set angle and power, then launch the projectile'); +} + +function setProjectileAngle() { + const angles = [30, 45, 60, 75]; + const currentIndex = angles.indexOf(window.projectileState.angle); + const newIndex = (currentIndex + 1) % angles.length; + window.projectileState.angle = angles[newIndex]; + + const cannon = document.getElementById('cannon'); + cannon.style.transform = `rotate(${-window.projectileState.angle}deg)`; + + updateObservations(`Launch angle set to ${window.projectileState.angle}ยฐ`); +} + +function setProjectilePower() { + const powers = [30, 50, 70, 90]; + const currentIndex = powers.indexOf(window.projectileState.power); + const newIndex = (currentIndex + 1) % powers.length; + window.projectileState.power = powers[newIndex]; + + updateObservations(`Launch power set to ${window.projectileState.power}%`); +} + +function launchProjectile() { + if (window.projectileState.launched) return; + + window.projectileState.launched = true; + const projectile = document.getElementById('projectile'); + projectile.style.display = 'block'; + + const angle = window.projectileState.angle * Math.PI / 180; + const power = window.projectileState.power / 10; + const gravity = 0.5; + + let x = 80; + let y = 350; + let vx = Math.cos(angle) * power; + let vy = -Math.sin(angle) * power; + + const animate = () => { + x += vx; + y += vy; + vy += gravity; + + projectile.style.left = x + 'px'; + projectile.style.top = y + 'px'; + + if (y < 380 && x < 800) { + requestAnimationFrame(animate); + } else { + window.projectileState.distance = x - 80; + updateObservations(`Projectile landed at ${window.projectileState.distance.toFixed(0)}px from launch point`); + } + }; + + animate(); +} + +function measureProjectileDistance() { + if (!window.projectileState.launched) { + showMessage('Launch the projectile first!', 'incorrect'); + return; + } + + updateMeasurements(`Distance: ${window.projectileState.distance.toFixed(0)}px (Angle: ${window.projectileState.angle}ยฐ, Power: ${window.projectileState.power}%)`); +} + +// Density Experiment +function setupDensityExperiment() { + experimentCanvasEl.innerHTML = ` +
    +
    +
    +
    + `; + + window.densityState = { + liquids: [], + order: [] + }; + + updateObservations('Add liquids to see how they separate by density'); +} + +function addLiquid(type) { + const cylinder = document.getElementById('density-cylinder'); + + const colors = { + honey: '#D2691E', + oil: '#FFD700', + water: '#4169E1', + alcohol: '#F0F8FF' + }; + + const densities = { + honey: 1.42, + oil: 0.92, + water: 1.00, + alcohol: 0.79 + }; + + window.densityState.liquids.push({ + type, + density: densities[type], + color: colors[type] + }); + + // Sort by density (higher density sinks) + window.densityState.liquids.sort((a, b) => b.density - a.density); + + // Update visual + cylinder.innerHTML = ''; + let currentHeight = 0; + const layerHeight = 300 / window.densityState.liquids.length; + + window.densityState.liquids.forEach(liquid => { + const layer = document.createElement('div'); + layer.style.position = 'absolute'; + layer.style.bottom = currentHeight + 'px'; + layer.style.width = '100%'; + layer.style.height = layerHeight + 'px'; + layer.style.background = liquid.color; + layer.style.opacity = '0.8'; + cylinder.appendChild(layer); + currentHeight += layerHeight; + }); + + updateObservations(`Added ${type} to the cylinder`); +} + +// Check results +function checkResults() { + if (!currentExperiment) return; + + clearInterval(timerInterval); + hintUsed = false; + + const accuracy = currentExperiment.check(); + const points = Math.round(accuracy * 10); + + score += points; + experimentsCompleted++; + totalAccuracy += accuracy; + + const grade = accuracy >= 90 ? 'A' : accuracy >= 80 ? 'B' : accuracy >= 70 ? 'C' : accuracy >= 60 ? 'D' : 'F'; + + showMessage(`Experiment complete! Accuracy: ${accuracy}%, Grade: ${grade}, Points: ${points}`, 'correct'); + + setTimeout(() => { + backToSelection(); + }, 3000); +} + +// Individual experiment check functions +function checkPendulumResults() { + if (!window.pendulumState || window.pendulumState.measurements.length === 0) { + return 0; + } + + // Check if period roughly follows physics (T = 2ฯ€โˆš(L/g)) + const measuredPeriod = window.pendulumState.measurements[0]; + const expectedPeriod = Math.sqrt(window.pendulumState.length / 980) * 2 * Math.PI; + const accuracy = Math.max(0, 100 - Math.abs(measuredPeriod - expectedPeriod) * 10); + + return Math.min(100, accuracy); +} + +function checkCircuitResults() { + return window.circuitState && window.circuitState.circuitComplete ? 100 : 0; +} + +function checkColorMixingResults() { + if (!window.colorState || !window.colorState.mixed) { + return 0; + } + + // Check if mixed color is reasonable + const c1 = window.colorState.container1; + const c2 = window.colorState.container2; + + // Simple check for primary color mixing + const hasRed = c1.red > 0 || c2.red > 0; + const hasBlue = c1.blue > 0 || c2.blue > 0; + const hasYellow = c1.yellow > 0 || c2.yellow > 0; + + if ((hasRed && hasBlue) || (hasRed && hasYellow) || (hasBlue && hasYellow)) { + return 85; // Good mixing + } + + return 40; // Poor mixing +} + +function checkPlantGrowthResults() { + if (!window.plantState) return 0; + + const height = window.plantState.height; + const optimalConditions = window.plantState.water > 60 && window.plantState.light > 60 && window.plantState.soil > 60; + + if (optimalConditions && height > 100) return 100; + if (optimalConditions && height > 50) return 80; + if (height > 50) return 60; + + return 30; +} + +function checkProjectileResults() { + if (!window.projectileState || !window.projectileState.launched) return 0; + + // Calculate expected distance using projectile motion formula + const angle = window.projectileState.angle * Math.PI / 180; + const power = window.projectileState.power / 10; + const expectedDistance = (power * power * Math.sin(2 * angle)) / 9.8 * 100; + + const actualDistance = window.projectileState.distance; + const accuracy = Math.max(0, 100 - Math.abs(actualDistance - expectedDistance) / expectedDistance * 100); + + return Math.min(100, accuracy); +} + +function checkDensityResults() { + if (!window.densityState || window.densityState.liquids.length < 2) return 0; + + // Check if liquids are in correct density order (honey > water > oil > alcohol) + const correctOrder = ['honey', 'water', 'oil', 'alcohol']; + const currentOrder = window.densityState.liquids.map(l => l.type); + + let correct = 0; + for (let i = 0; i < Math.min(currentOrder.length, correctOrder.length); i++) { + if (currentOrder[i] === correctOrder[i]) correct++; + } + + return (correct / currentOrder.length) * 100; +} + +// Use hint +function useHint() { + if (!currentExperiment || hintUsed || score < 10) return; + + if (score < 10) { + showMessage('Not enough points for hint! (10 points required)', 'incorrect'); + return; + } + + score -= 10; + hintUsed = true; + showMessage(`Hint: ${currentExperiment.hint}`, 'hint'); + updateDisplay(); +} + +// Reset experiment +function resetExperiment() { + if (currentExperiment) { + currentExperiment.setup(); + hintUsed = false; + showMessage('Experiment reset. Start over!', 'hint'); + } +} + +// Back to selection +function backToSelection() { + labWorkspaceEl.style.display = 'none'; + experimentSelectionEl.style.display = 'block'; + backToSelectionBtn.style.display = 'none'; + currentExperiment = null; + experimentNameEl.textContent = 'None'; + clearInterval(timerInterval); + timeLeft = 60; + updateDisplay(); +} + +// Start timer +function startTimer() { + if (timerInterval) clearInterval(timerInterval); + + timerInterval = setInterval(() => { + timeLeft--; + timeLeftEl.textContent = timeLeft; + + if (timeLeft <= 0) { + clearInterval(timerInterval); + timeUp(); + } + }, 1000); +} + +// Time up +function timeUp() { + showMessage('Time\'s up! Checking your results...', 'incorrect'); + setTimeout(checkResults, 2000); +} + +// Update observations +function updateObservations(text) { + observationsEl.innerHTML = `
    ${text}
    ` + observationsEl.innerHTML; +} + +// Update measurements +function updateMeasurements(text) { + measurementsEl.innerHTML = `
    + ${text} +
    ` + measurementsEl.innerHTML; +} + +// Show message +function showMessage(text, type) { + messageEl.textContent = text; + messageEl.className = `message ${type}`; +} + +// End game +function endGame() { + gameActive = false; + clearInterval(timerInterval); + + // Show results + showResults(); +} + +// Show final results +function showResults() { + const averageAccuracy = experimentsCompleted > 0 ? Math.round(totalAccuracy / experimentsCompleted) : 0; + + finalScoreEl.textContent = score.toLocaleString(); + experimentsCompletedEl.textContent = experimentsCompleted; + averageAccuracyEl.textContent = averageAccuracy + '%'; + + // Calculate grade + let grade = 'F'; + if (averageAccuracy >= 90) grade = 'A'; + else if (averageAccuracy >= 80) grade = 'B'; + else if (averageAccuracy >= 70) grade = 'C'; + else if (averageAccuracy >= 60) grade = 'D'; + + scientificGradeEl.textContent = grade; + scientificGradeEl.className = `final-value grade ${grade}`; + + resultsEl.style.display = 'block'; + startBtn.style.display = 'none'; + quitBtn.style.display = 'none'; +} + +// Reset game +function resetGame() { + resultsEl.style.display = 'none'; + startBtn.style.display = 'inline-block'; + quitBtn.style.display = 'none'; + + gameActive = false; + clearInterval(timerInterval); + backToSelection(); + updateDisplay(); + messageEl.textContent = 'Ready for a new science lab session?'; +} + +// Update display elements +function updateDisplay() { + scoreEl.textContent = score.toLocaleString(); + accuracyEl.textContent = experimentsCompleted > 0 ? Math.round(totalAccuracy / experimentsCompleted) + '%' : '0%'; + timeLeftEl.textContent = timeLeft; +} + +// Initialize the game +initGame(); + +// This science lab game includes multiple experiments with interactive tools, +// educational content, scoring, and scientific accuracy validation \ No newline at end of file diff --git a/games/science-lab/style.css b/games/science-lab/style.css new file mode 100644 index 00000000..faeb1cdb --- /dev/null +++ b/games/science-lab/style.css @@ -0,0 +1,640 @@ +/* Science Lab Game Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #e8f4fd 0%, #d1ecf1 100%); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.container { + max-width: 1400px; + width: 100%; + background: white; + border-radius: 20px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +header { + background: linear-gradient(135deg, #28a745 0%, #20c997 100%); + color: white; + padding: 30px; + text-align: center; +} + +header h1 { + font-size: 2.5rem; + margin-bottom: 10px; + font-weight: 700; +} + +header p { + font-size: 1.1rem; + opacity: 0.9; +} + +.game-area { + padding: 30px; +} + +.stats-panel { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 20px; + margin-bottom: 30px; + padding: 20px; + background: #f8f9fa; + border-radius: 15px; +} + +.stat { + display: flex; + align-items: center; + gap: 8px; + font-size: 1.1rem; + font-weight: 600; +} + +.stat-label { + color: #666; +} + +.stat-value { + color: #333; + font-weight: 700; +} + +/* Experiment Selection */ +.experiment-selection { + margin-bottom: 30px; +} + +.experiment-selection h3 { + text-align: center; + margin-bottom: 20px; + color: #2c3e50; + font-size: 1.5rem; +} + +.experiment-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; +} + +.experiment-card { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 25px; + border-radius: 15px; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.experiment-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); +} + +.experiment-icon { + font-size: 3rem; + margin-bottom: 15px; +} + +.experiment-card h4 { + font-size: 1.3rem; + margin-bottom: 10px; + font-weight: 600; +} + +.experiment-card p { + opacity: 0.9; + line-height: 1.4; +} + +/* Lab Workspace */ +.lab-workspace { + background: #f8f9fa; + border-radius: 15px; + padding: 25px; +} + +.lab-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 15px; +} + +.lab-header h3 { + color: #2c3e50; + font-size: 1.5rem; +} + +.experiment-controls { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.lab-btn { + padding: 8px 16px; + border: 2px solid #6c757d; + background: white; + color: #6c757d; + border-radius: 20px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 600; + transition: all 0.3s ease; +} + +.lab-btn:hover { + background: #6c757d; + color: white; +} + +.lab-area { + display: grid; + grid-template-columns: 250px 1fr 250px; + gap: 20px; + min-height: 500px; +} + +/* Tools Panel */ +.tools-panel { + background: white; + border-radius: 10px; + padding: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.tools-panel h4 { + margin-bottom: 15px; + color: #2c3e50; + font-size: 1.1rem; +} + +.tool-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px; + margin-bottom: 10px; + background: #f8f9fa; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; +} + +.tool-item:hover { + background: #e9ecef; +} + +.tool-item.selected { + background: #007bff; + color: white; +} + +.tool-icon { + font-size: 1.5rem; + width: 30px; + text-align: center; +} + +.tool-label { + flex: 1; + font-weight: 500; +} + +/* Experiment Canvas */ +.experiment-canvas { + background: white; + border-radius: 10px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + position: relative; + overflow: hidden; +} + +/* Pendulum Experiment */ +.pendulum-setup { + position: relative; + width: 100%; + height: 100%; + min-height: 400px; + background: linear-gradient(180deg, #e8f4fd 0%, #d1ecf1 50%, #f8f9fa 100%); +} + +.pendulum-pivot { + position: absolute; + top: 50px; + left: 50%; + transform: translateX(-50%); + width: 10px; + height: 10px; + background: #333; + border-radius: 50%; +} + +.pendulum-string { + position: absolute; + top: 55px; + left: 50%; + transform-origin: top; + width: 2px; + height: 200px; + background: #666; +} + +.pendulum-bob { + position: absolute; + bottom: 0; + left: -15px; + width: 30px; + height: 30px; + background: #e74c3c; + border-radius: 50%; + cursor: pointer; +} + +/* Circuit Experiment */ +.circuit-board { + position: relative; + width: 100%; + height: 100%; + min-height: 400px; + background: #f4e4bc; + border: 2px solid #8b4513; +} + +.circuit-component { + position: absolute; + cursor: pointer; + transition: all 0.3s ease; +} + +.circuit-component:hover { + transform: scale(1.1); +} + +.battery { + width: 40px; + height: 20px; + background: #333; + border-radius: 3px; +} + +.resistor { + width: 30px; + height: 10px; + background: #8b4513; + border-radius: 2px; +} + +.bulb { + width: 20px; + height: 20px; + background: #fff; + border: 2px solid #333; + border-radius: 50%; +} + +.bulb.lit { + background: #ffd700; + box-shadow: 0 0 20px #ffd700; +} + +.wire { + position: absolute; + height: 3px; + background: #c0c0c0; + border-radius: 2px; +} + +/* Color Mixing Experiment */ +.color-mixing { + display: flex; + flex-direction: column; + align-items: center; + padding: 30px; + gap: 20px; +} + +.color-containers { + display: flex; + gap: 20px; + margin-bottom: 20px; +} + +.color-container { + width: 100px; + height: 120px; + border: 2px solid #333; + border-radius: 8px; + background: white; + position: relative; + overflow: hidden; +} + +.color-liquid { + position: absolute; + bottom: 0; + width: 100%; + transition: all 0.5s ease; +} + +.mixing-result { + width: 150px; + height: 120px; + border: 2px solid #333; + border-radius: 8px; + background: white; +} + +/* Plant Growth Experiment */ +.plant-experiment { + position: relative; + width: 100%; + height: 100%; + min-height: 400px; + background: linear-gradient(180deg, #87ceeb 0%, #98fb98 100%); +} + +.plant-pot { + position: absolute; + bottom: 50px; + width: 80px; + height: 60px; + background: #8b4513; + border-radius: 5px 5px 0 0; +} + +.plant { + position: absolute; + bottom: 110px; + left: 50%; + transform: translateX(-50%); + width: 4px; + background: #228b22; + transition: all 0.5s ease; +} + +.plant.growing { + height: 150px; +} + +/* Results Panel */ +.results-panel { + background: white; + border-radius: 10px; + padding: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.results-panel h4 { + margin-bottom: 15px; + color: #2c3e50; + font-size: 1.1rem; +} + +.observations, .measurements { + margin-bottom: 15px; +} + +.observation-item, .measurement-item { + padding: 8px 0; + border-bottom: 1px solid #eee; + font-size: 0.9rem; +} + +.measurement-item { + display: flex; + justify-content: space-between; +} + +.measurement-label { + font-weight: 500; +} + +.measurement-value { + color: #007bff; + font-weight: 600; +} + +.message { + text-align: center; + font-size: 1.2rem; + font-weight: 600; + min-height: 2rem; + margin: 20px 0; + padding: 15px; + border-radius: 10px; + transition: all 0.3s ease; +} + +.message.correct { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.message.incorrect { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.message.hint { + background: #fff3cd; + color: #856404; + border: 1px solid #ffeaa7; +} + +.game-controls { + text-align: center; + margin-top: 20px; + display: flex; + justify-content: center; + gap: 15px; + flex-wrap: wrap; +} + +.primary-btn, .secondary-btn { + padding: 12px 24px; + border: none; + border-radius: 25px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.primary-btn { + background: linear-gradient(135deg, #28a745 0%, #20c997 100%); + color: white; +} + +.primary-btn:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(40, 167, 69, 0.4); +} + +.secondary-btn { + background: #f8f9fa; + color: #666; + border: 2px solid #e0e0e0; +} + +.secondary-btn:hover { + background: #e9ecef; + border-color: #dee2e6; +} + +.results { + padding: 30px; + text-align: center; + background: linear-gradient(135deg, #28a745 0%, #20c997 100%); + color: white; +} + +.results h2 { + font-size: 2rem; + margin-bottom: 30px; +} + +.final-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.final-stat { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + backdrop-filter: blur(10px); +} + +.final-label { + display: block; + font-size: 0.9rem; + opacity: 0.8; + margin-bottom: 5px; +} + +.final-value { + display: block; + font-size: 1.8rem; + font-weight: 700; +} + +.grade { + font-size: 2rem !important; +} + +.grade.A { color: #28a745; } +.grade.B { color: #17a2b8; } +.grade.C { color: #ffc107; } +.grade.D { color: #fd7e14; } +.grade.F { color: #dc3545; } + +/* Responsive Design */ +@media (max-width: 1024px) { + .lab-area { + grid-template-columns: 1fr; + grid-template-rows: auto 1fr auto; + } + + .tools-panel, .results-panel { + order: 1; + } + + .experiment-canvas { + order: 2; + min-height: 300px; + } +} + +@media (max-width: 768px) { + header { + padding: 20px; + } + + header h1 { + font-size: 2rem; + } + + .game-area { + padding: 20px; + } + + .stats-panel { + flex-direction: column; + gap: 10px; + } + + .experiment-grid { + grid-template-columns: 1fr; + } + + .lab-header { + flex-direction: column; + align-items: stretch; + } + + .experiment-controls { + justify-content: center; + } + + .final-stats { + grid-template-columns: 1fr; + } +} + +/* Animations */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.experiment-card, .tool-item { + animation: fadeIn 0.5s ease-out; +} + +.correct-animation { + animation: correctPulse 0.6s ease-out; +} + +.incorrect-animation { + animation: incorrectShake 0.6s ease-out; +} + +@keyframes correctPulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +@keyframes incorrectShake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } +} \ No newline at end of file diff --git a/games/sequence-maker/index.html b/games/sequence-maker/index.html new file mode 100644 index 00000000..72b90a67 --- /dev/null +++ b/games/sequence-maker/index.html @@ -0,0 +1,27 @@ + + + + + + Sequence Maker | Mini JS Games Hub + + + +
    +

    Sequence Maker

    +

    Level: 1

    +
    + +
    +
    + + + + +
    +

    +
    + + + + diff --git a/games/sequence-maker/script.js b/games/sequence-maker/script.js new file mode 100644 index 00000000..c9c0af8b --- /dev/null +++ b/games/sequence-maker/script.js @@ -0,0 +1,116 @@ +const symbols = ["๐Ÿ”ด","๐ŸŸข","๐Ÿ”ต","๐ŸŸก","๐ŸŸฃ"]; +const sequenceLine = document.getElementById("sequence-line"); +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const resumeBtn = document.getElementById("resume-btn"); +const restartBtn = document.getElementById("restart-btn"); +const message = document.getElementById("message"); +const levelDisplay = document.getElementById("level-display"); + +let sequence = []; +let userSequence = []; +let level = 1; +let playing = false; +let paused = false; +let speed = 1000; +let symbolSounds = { + "๐Ÿ”ด":"https://freesound.org/data/previews/273/273146_5121236-lq.mp3", + "๐ŸŸข":"https://freesound.org/data/previews/273/273144_5121236-lq.mp3", + "๐Ÿ”ต":"https://freesound.org/data/previews/273/273147_5121236-lq.mp3", + "๐ŸŸก":"https://freesound.org/data/previews/273/273148_5121236-lq.mp3", + "๐ŸŸฃ":"https://freesound.org/data/previews/273/273149_5121236-lq.mp3" +}; + +let playTimeout; + +function playSound(symbol) { + const audio = new Audio(symbolSounds[symbol]); + audio.play(); +} + +function showSequence() { + sequenceLine.innerHTML = ""; + sequence.forEach((sym, index) => { + const span = document.createElement("div"); + span.className = "symbol"; + span.textContent = sym; + sequenceLine.appendChild(span); + setTimeout(() => { + if(!paused){ + span.classList.add("glow"); + playSound(sym); + setTimeout(() => span.classList.remove("glow"), speed/2); + } + }, index * speed); + }); + setTimeout(() => { + enableUserInput(); + }, sequence.length * speed); +} + +function enableUserInput() { + userSequence = []; + sequenceLine.querySelectorAll(".symbol").forEach((symDiv, index) => { + symDiv.addEventListener("click", handleUserClick); + }); +} + +function handleUserClick(e) { + if(!playing || paused) return; + const clicked = e.currentTarget.textContent; + playSound(clicked); + userSequence.push(clicked); + const currentIndex = userSequence.length - 1; + if(userSequence[currentIndex] !== sequence[currentIndex]) { + message.textContent = "โŒ Wrong! Game Over."; + playing = false; + return; + } + if(userSequence.length === sequence.length) { + message.textContent = "โœ… Correct!"; + nextLevel(); + } +} + +function nextLevel() { + level++; + levelDisplay.textContent = `Level: ${level}`; + sequence.push(symbols[Math.floor(Math.random()*symbols.length)]); + speed = Math.max(400, 1000 - level*50); // increase speed + setTimeout(showSequence, 1000); +} + +function startGame() { + sequence = []; + level = 1; + playing = true; + paused = false; + message.textContent = ""; + levelDisplay.textContent = `Level: ${level}`; + sequence.push(symbols[Math.floor(Math.random()*symbols.length)]); + showSequence(); +} + +function pauseGame() { + paused = true; + clearTimeout(playTimeout); + message.textContent = "โธ Paused"; +} + +function resumeGame() { + if(!paused) return; + paused = false; + message.textContent = ""; + showSequence(); +} + +function restartGame() { + paused = false; + playing = false; + startGame(); +} + +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", pauseGame); +resumeBtn.addEventListener("click", resumeGame); +restartBtn.addEventListener("click", restartGame); diff --git a/games/sequence-maker/style.css b/games/sequence-maker/style.css new file mode 100644 index 00000000..39daba87 --- /dev/null +++ b/games/sequence-maker/style.css @@ -0,0 +1,76 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background-color: #0b0c1c; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.sequence-container { + text-align: center; + width: 90%; + max-width: 900px; +} + +h1 { + font-size: 2em; + margin-bottom: 10px; +} + +#level-display { + font-size: 1.2em; + margin-bottom: 20px; +} + +.sequence-line { + display: flex; + justify-content: center; + align-items: center; + gap: 15px; + flex-wrap: wrap; + margin-bottom: 20px; +} + +.symbol { + width: 60px; + height: 60px; + border-radius: 50%; + background-color: #333; + display: flex; + justify-content: center; + align-items: center; + font-size: 1.5em; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 0 5px #000; +} + +.symbol.glow { + box-shadow: 0 0 20px #ff0, 0 0 40px #ff0, 0 0 60px #ff0; + transform: scale(1.2); +} + +.controls button { + margin: 5px; + padding: 8px 15px; + font-size: 16px; + cursor: pointer; + border: none; + border-radius: 5px; + background-color: #222; + color: #fff; + transition: 0.2s; +} + +.controls button:hover { + background-color: #ff0; + color: #000; +} + +#message { + margin-top: 15px; + font-size: 1.2em; +} diff --git a/games/sequence_master/index.html b/games/sequence_master/index.html new file mode 100644 index 00000000..7bf13995 --- /dev/null +++ b/games/sequence_master/index.html @@ -0,0 +1,36 @@ + + + + + + Button Sequence Master + + + + +
    +

    ๐Ÿง  Sequence Master

    + +
    + Level: 1 | Sequence Length: 1 +
    + +
    + + + + +
    + +
    +

    Click **START** to see the first sequence!

    +
    + +
    + +
    +
    + + + + \ No newline at end of file diff --git a/games/sequence_master/script.js b/games/sequence_master/script.js new file mode 100644 index 00000000..24cc1008 --- /dev/null +++ b/games/sequence_master/script.js @@ -0,0 +1,216 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. DOM Elements --- + const gameButtons = document.querySelectorAll('.game-button'); + const startButton = document.getElementById('start-button'); + const levelDisplay = document.getElementById('level-display'); + const lengthDisplay = document.getElementById('length-display'); + const feedbackMessage = document.getElementById('feedback-message'); + + // --- 2. GAME STATE VARIABLES --- + let sequence = []; // The computer's sequence of button IDs (e.g., [2, 1, 4]) + let playerSequence = [];// The sequence the player has clicked so far + let level = 1; + let gameActive = false; + let playingSequence = false; // Flag to prevent player clicks during playback + + // Timing variables + let playbackDuration = 800; // Duration each button is lit up (decreases with level) + let maxClickTime = 1500; // Max time allowed between player clicks (decreases with level) + let clickTimeoutId = null; // ID for the player click timer + + // --- 3. CORE LOGIC FUNCTIONS --- + + /** + * Initializes the game for the first time or after a restart. + */ + function initGame() { + level = 1; + gameActive = true; + sequence = []; + playerSequence = []; + playbackDuration = 800; + maxClickTime = 1500; + + startButton.textContent = 'RESTART'; + startButton.disabled = true; + + updateStatus(); + nextRound(); + } + + /** + * Advances to the next round by increasing the sequence length and difficulty. + */ + function nextRound() { + stopClickTimer(); + playerSequence = []; + playingSequence = true; + disablePlayerInput(); + + // Add a new random button ID (1-4) to the sequence + const newButtonId = String(Math.floor(Math.random() * 4) + 1); + sequence.push(newButtonId); + + // Increase difficulty every 3 levels + if (level > 1 && (level - 1) % 3 === 0) { + playbackDuration = Math.max(250, playbackDuration - 100); + maxClickTime = Math.max(500, maxClickTime - 200); + feedbackMessage.textContent = `Difficulty UP! Speed: ${playbackDuration}ms.`; + } else { + feedbackMessage.textContent = 'Watch closely...'; + } + + updateStatus(); + playSequence(); + } + + /** + * Visually and temporally plays back the current sequence. + */ + function playSequence() { + let i = 0; + const sequenceInterval = setInterval(() => { + if (i < sequence.length) { + const buttonId = sequence[i]; + lightUpButton(buttonId); + i++; + } else { + // Sequence finished playing + clearInterval(sequenceInterval); + playingSequence = false; + enablePlayerInput(); + feedbackMessage.textContent = 'Your turn! Repeat the sequence.'; + startClickTimer(); + } + }, playbackDuration * 2); // Interval is twice the light-up duration + } + + /** + * Toggles the active class on a button for the playback duration. + */ + function lightUpButton(buttonId) { + const button = document.querySelector(`.game-button[data-id="${buttonId}"]`); + if (button) { + button.classList.add('active'); + setTimeout(() => { + button.classList.remove('active'); + }, playbackDuration); + } + } + + // --- 4. PLAYER INPUT & VALIDATION --- + + /** + * Handles the player's button click. + */ + function handlePlayerClick(event) { + if (!gameActive || playingSequence) return; + + stopClickTimer(); // Reset the timer on every successful click + + const clickedId = event.target.getAttribute('data-id'); + const expectedId = sequence[playerSequence.length]; + + if (clickedId === expectedId) { + // Correct click + playerSequence.push(clickedId); + feedbackMessage.textContent = `Good! (${playerSequence.length} / ${sequence.length})`; + + if (playerSequence.length === sequence.length) { + // Full sequence correctly completed! + level++; + feedbackMessage.textContent = '๐ŸŽ‰ CORRECT! Getting faster...'; + setTimeout(nextRound, 1000); // Start next round after a delay + } else { + // Correct but not finished, restart timer for next click + startClickTimer(); + } + } else { + // Incorrect click + endGame(); + } + } + + // --- 5. TIMING AND CONTROL --- + + /** + * Starts the timer for the player's next click. + */ + function startClickTimer() { + stopClickTimer(); // Clear any existing timer + + // Set a timeout to trigger loss if the player doesn't click in time + clickTimeoutId = setTimeout(() => { + endGame('timeout'); + }, maxClickTime); + } + + /** + * Stops the player's click timer. + */ + function stopClickTimer() { + clearTimeout(clickTimeoutId); + } + + /** + * Disables the game buttons during sequence playback. + */ + function disablePlayerInput() { + gameButtons.forEach(btn => btn.disabled = true); + } + + /** + * Enables the game buttons for player input. + */ + function enablePlayerInput() { + gameButtons.forEach(btn => btn.disabled = false); + } + + /** + * Updates the status display elements. + */ + function updateStatus() { + levelDisplay.textContent = level; + lengthDisplay.textContent = sequence.length; + } + + /** + * Ends the game and resets the interface. + */ + function endGame(reason) { + stopClickTimer(); + gameActive = false; + disablePlayerInput(); + startButton.disabled = false; + + if (reason === 'timeout') { + feedbackMessage.innerHTML = `โฐ **TIME OUT!** You took too long. Final Level: ${level}.`; + } else { + feedbackMessage.innerHTML = `โŒ **GAME OVER!** Incorrect sequence. Final Level: ${level}.`; + } + feedbackMessage.style.color = '#e74c3c'; + + // Flash all buttons red to signal loss + gameButtons.forEach(btn => { + btn.classList.add('active'); + btn.style.backgroundColor = '#c0392b'; + }); + setTimeout(() => { + gameButtons.forEach(btn => { + btn.classList.remove('active'); + btn.style.backgroundColor = ''; // Reset to CSS defined color + }); + }, 800); + } + + // --- 6. EVENT LISTENERS --- + + startButton.addEventListener('click', initGame); + + gameButtons.forEach(button => { + button.addEventListener('click', handlePlayerClick); + }); + + // Initial setup + disablePlayerInput(); +}); \ No newline at end of file diff --git a/games/sequence_master/style.css b/games/sequence_master/style.css new file mode 100644 index 00000000..fa310740 --- /dev/null +++ b/games/sequence_master/style.css @@ -0,0 +1,99 @@ +body { + font-family: 'Press Start 2P', cursive, monospace; /* Retro arcade font style */ + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #2c3e50; /* Dark blue background */ + color: #ecf0f1; +} + +#game-container { + background-color: #34495e; + padding: 30px; + border-radius: 15px; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + text-align: center; + max-width: 450px; + width: 90%; +} + +h1 { + color: #f1c40f; /* Yellow title */ + margin-bottom: 25px; +} + +#status-area { + font-size: 0.9em; + font-weight: bold; + margin-bottom: 30px; + color: #bdc3c7; +} + +/* --- Button Grid --- */ +#button-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + width: 300px; + height: 300px; + margin: 0 auto; + margin-bottom: 30px; +} + +.game-button { + height: 100%; + font-size: 2.5em; + font-weight: bold; + border: 5px solid #2c3e50; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.1s ease-in; + color: white; +} + +/* Initial Colors (Simulated off-state) */ +.game-button[data-id="1"] { background-color: #e74c3c; } /* Red */ +.game-button[data-id="2"] { background-color: #2ecc71; } /* Green */ +.game-button[data-id="3"] { background-color: #3498db; } /* Blue */ +.game-button[data-id="4"] { background-color: #f39c12; } /* Orange */ + +/* --- The Active (Lit Up) State --- */ +.game-button.active { + /* Brighter version of the color */ + box-shadow: 0 0 25px 5px rgba(255, 255, 255, 0.8), 0 0 10px rgba(255, 255, 255, 0.5); + transform: scale(1.05); +} + +.game-button[data-id="1"].active { background-color: #ff6b6b; } +.game-button[data-id="2"].active { background-color: #4cd964; } +.game-button[data-id="3"].active { background-color: #5d9cec; } +.game-button[data-id="4"].active { background-color: #ffae19; } + +.game-button:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +/* --- Controls and Feedback --- */ +#feedback-message { + min-height: 20px; + margin-bottom: 20px; +} + +#start-button { + padding: 12px 25px; + font-size: 1.1em; + font-weight: bold; + background-color: #9b59b6; /* Purple */ + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +#start-button:hover { + background-color: #8e44ad; +} \ No newline at end of file diff --git a/games/shadow-catcher/index.html b/games/shadow-catcher/index.html new file mode 100644 index 00000000..8f7ae23d --- /dev/null +++ b/games/shadow-catcher/index.html @@ -0,0 +1,60 @@ + + + + + + Shadow Catcher + + + +
    +
    +

    Shadow Catcher

    +
    + + + +
    +
    + +
    +
    +
    +
    + + +
    +
    + + + \ No newline at end of file diff --git a/games/shadow-catcher/script.js b/games/shadow-catcher/script.js new file mode 100644 index 00000000..11d0ec91 --- /dev/null +++ b/games/shadow-catcher/script.js @@ -0,0 +1,242 @@ + (()=>{ + const stage = document.getElementById('stage'); + const startBtn = document.getElementById('startBtn'); + const nextBtn = document.getElementById('nextBtn'); + const choicesEl = document.getElementById('choices'); + const timeMeter = document.getElementById('timeMeter'); + const scoreEl = document.getElementById('score'); + const roundInfo = document.getElementById('roundInfo'); + const difficultySel = document.getElementById('difficulty'); + const muteCb = document.getElementById('mute'); + + const SHAPES = ['Circle','Square','Triangle','Rectangle','Star']; + let round=0,score=0,current=null,timer=null,timeLeft=0,timeMax=8; + + function randInt(a,b){return Math.floor(Math.random()*(b-a+1))+a} + + function pickRandom(exclude){ + let arr = SHAPES.filter(s=>s!==exclude); + const choices = []; + while(choices.length<3){ + const idx = randInt(0,arr.length-1); + choices.push(arr.splice(idx,1)[0]); + } + return choices; + } + + function clearStage(){stage.innerHTML=''} + + function createShadow(shapeName,opts={}){ + const el = document.createElement('div'); + el.className = 'shadow-entity'; + el.setAttribute('data-shape',shapeName); + // base size & style + let size = 110; + if(shapeName==='Rectangle') size=140; + if(shapeName==='Star') size=120; + + // choose inner content + const inner = document.createElement('div'); + inner.style.width = size+'px'; + inner.style.height = size+'px'; + + switch(shapeName){ + case 'Circle': inner.className='shape-circle'; break; + case 'Square': inner.className='shape-square'; break; + case 'Rectangle': inner.className='shape-rect'; break; + case 'Triangle': inner.className='shape-triangle'; break; + case 'Star': inner.className='shape-star'; inner.innerHTML = ` + + + `; break; + } + + el.appendChild(inner); + stage.appendChild(el); + + // random start position + const pad = 40; + const left = randInt(pad, stage.clientWidth - pad - size); + const top = randInt(pad, stage.clientHeight - pad - size); + el.style.left = left+'px'; + el.style.top = top+'px'; + + // animation duration depends on difficulty + const diff = difficultySel.value; + let speedMult = diff==='easy'?1.8: diff==='medium'?1:0.6; + const duration = (randInt(6,12) * speedMult).toFixed(2); + el.style.animation = `floatX ${duration}s ease-in-out infinite`; + + // rotate randomly and slightly + el.style.transformOrigin = '50% 50%'; + + // also add subtle vertical float + inner.style.animation = `floatY ${ (randInt(6,12) * speedMult).toFixed(2) }s ease-in-out infinite`; + + // adjust blur and opacity per difficulty + const blur = diff==='easy'?6: diff==='medium'?8:12; + inner.style.filter = `blur(${blur}px)`; + inner.style.opacity = diff==='hard'?0.95:0.86; + + return el; + } + + function buildChoices(correct){ + choicesEl.innerHTML=''; + const wrongs = pickRandom(correct); + const pool = [correct, ...wrongs].sort(()=>Math.random()-.5); + pool.forEach(text=>{ + const c = document.createElement('button'); + c.className='choice'; + c.textContent = text; + c.dataset.val = text; + c.onclick = ()=>handleChoice(c); + choicesEl.appendChild(c); + }); + } + + function handleChoice(button){ + if(!current) return; + const val = button.dataset.val; + // prevent double + if(button.classList.contains('checked')) return; + button.classList.add('checked'); + + if(val === current){ + // correct + button.classList.add('correct'); + score += 2; + playSound('correct'); + flashMeter('+2'); + } else { + button.classList.add('wrong'); + score = Math.max(0, score-1); + playSound('wrong'); + flashMeter('-1'); + } + updateScore(); + endRound(); + } + + function flashMeter(text){ + const f = document.createElement('div'); + f.textContent = text; + f.style.position='absolute';f.style.right='18px';f.style.top='18px';f.style.color='white';f.style.fontWeight=700;f.style.opacity=0.95;f.style.padding='6px 10px';f.style.borderRadius='8px';f.style.background='rgba(0,0,0,0.3)'; + document.querySelector('.wrap').appendChild(f); + setTimeout(()=>{f.style.transition='all .6s';f.style.opacity='0';f.style.transform='translateY(-8px)';},800); + setTimeout(()=>f.remove(),1400); + } + + function updateScore(){ + scoreEl.textContent = 'Score: '+score; + roundInfo.textContent = 'Round: '+round; + } + + function startRound(){ + clearStage(); + round++; + updateScore(); + // select shape + const shape = SHAPES[randInt(0,SHAPES.length-1)]; + current = shape; + + // difficulty impacts timer + const diff = difficultySel.value; + timeMax = diff==='easy'?10: diff==='medium'?7:5; + timeLeft = timeMax; + + // create one or two shadow entities for extra confusion + const count = diff==='hard'?2:1; + for(let i=0;i{ + if(b.dataset.val === current) b.classList.add('correct'); + }); + current=null; + setTimeout(()=>{ + // auto start next + // don't auto-start; wait for Start/Next button as per acceptance + },900); + } + + function startTimer(){ + timeMeter.style.width = '100%'; + clearInterval(timer); + timer = setInterval(()=>{ + timeLeft -= 0.1; + const pct = Math.max(0,(timeLeft/timeMax)*100); + timeMeter.style.width = pct+'%'; + if(timeLeft <= 0){ + // time up + score = Math.max(0, score-1); + updateScore(); + playSound('timeout'); + endRound(); + clearInterval(timer); + } + },100); + } + + // basic sounds (beeps) created with WebAudio + const audioCtx = window.AudioContext ? new AudioContext() : null; + function playBeep(freq,dur,vol=0.09){ + if(!audioCtx || muteCb.checked) return; + const o = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + o.type='sine';o.frequency.value=freq;g.gain.value=vol; + o.connect(g);g.connect(audioCtx.destination); + o.start(); + setTimeout(()=>{o.stop()},dur); + } + function playSound(kind){ + if(muteCb.checked) return; + if(!audioCtx) return; + if(kind==='correct') playBeep(880,140,0.09); + else if(kind==='wrong') playBeep(220,200,0.06); + else if(kind==='timeout') playBeep(120,250,0.04); + } + + // Skip/next button behaviour + nextBtn.onclick = ()=>{ + if(!current){ startRound(); return; } + // penalty + score = Math.max(0, score-1); + updateScore(); + playSound('wrong'); + endRound(); + } + + startBtn.onclick = ()=>{ if(current) return; startRound(); } + + // keyboard support + window.addEventListener('keydown',e=>{ + if(e.key===' '){ if(!current) startRound(); } + }); + + // initial UI population (first placeholder) + function initUI(){ + choicesEl.innerHTML=''; + // show sample placeholders + const shuffled = SHAPES.slice().sort(()=>Math.random()-.5).slice(0,4); + shuffled.forEach(t=>{ const b=document.createElement('div'); b.className='choice'; b.textContent=t; choicesEl.appendChild(b) }); + updateScore(); + } + + initUI(); + + })(); diff --git a/games/shadow-catcher/style.css b/games/shadow-catcher/style.css new file mode 100644 index 00000000..3b1f7a86 --- /dev/null +++ b/games/shadow-catcher/style.css @@ -0,0 +1,207 @@ + + :root{ + --bg:#0f1624; --panel:#0b1220; --accent:#ffb84d; --muted:#9aa7bf; + --shadow-color: rgba(0,0,0,0.85); + --ui-radius:12px; + --glow: 0 0 15px rgba(255, 184, 77, 0.3); + } + *{box-sizing:border-box} + html,body{height:100%;margin:0;font-family:Inter,Segoe UI,Roboto,system-ui,Arial;color:#e6eef8;background:linear-gradient(180deg,#071025 0%, #081827 60%);} + .wrap{max-width:980px;margin:36px auto;padding:20px} + + header{display:flex;align-items:center;justify-content:space-between;margin-bottom:18px} + h1{font-size:20px;margin:0;font-weight:700;letter-spacing:0.5px;} + .controls{display:flex;gap:8px;align-items:center} + select,.btn{background:var(--panel);border:1px solid rgba(255,255,255,0.04);padding:8px 12px;border-radius:10px;color:var(--muted);} + .btn{cursor:pointer;transition:all 0.2s ease;position:relative;overflow:hidden;} + + /* Enhanced button styles */ + .btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,0.3);} + .btn:active{transform:translateY(0);} + + #startBtn { + background: linear-gradient(135deg, #ffb84d, #ff9a3d); + color: #0f1624; + font-weight: 600; + border: none; + box-shadow: var(--glow); + padding: 10px 18px; + } + + #startBtn:hover { + background: linear-gradient(135deg, #ffc266, #ffa64d); + box-shadow: 0 0 20px rgba(255, 184, 77, 0.5), 0 4px 12px rgba(0,0,0,0.3); + } + + #nextBtn { + background: rgba(255, 184, 77, 0.1); + color: var(--accent); + border: 1px solid rgba(255, 184, 77, 0.3); + font-weight: 500; + } + + #nextBtn:hover { + background: rgba(255, 184, 77, 0.15); + box-shadow: 0 0 10px rgba(255, 184, 77, 0.2); + } + + .game-area{display:grid;grid-template-columns:1fr 340px;gap:18px} + + /* Left: stage */ + .stage-wrap{ + background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + padding:18px; + border-radius:16px; + box-shadow:0 8px 32px rgba(2,6,23,0.8); + min-height:420px; + position:relative; + overflow:hidden; + border: 1px solid rgba(255,255,255,0.03); + } + + .stage{ + position:relative; + width:100%; + height:440px; + border-radius:12px; + background:linear-gradient(180deg,#0b1420, #07111b); + display:flex; + align-items:center; + justify-content:center; + overflow:hidden; + box-shadow: inset 0 0 30px rgba(0,0,0,0.5); + } + + /* shadow container */ + .shadow-entity{ + position:absolute; + filter:blur(8px) saturate(0); + opacity:0.85; + pointer-events:none; + mix-blend-mode:multiply; + box-shadow: 0 0 30px rgba(0,0,0,0.7); + } + + /* shape variants - rendered as solid black (shadow) */ + .shape-circle{width:110px;height:110px;border-radius:999px;background:var(--shadow-color)} + .shape-square{width:110px;height:110px;background:var(--shadow-color)} + .shape-rect{width:140px;height:80px;background:var(--shadow-color);border-radius:8px} + .shape-triangle{width:0;height:0;border-left:60px solid transparent;border-right:60px solid transparent;border-bottom:110px solid var(--shadow-color)} + .shape-star{width:120px;height:120px} + .shape-star svg{width:120px;height:120px;display:block} + + /* motion helpers */ + @keyframes floatX{0%{transform:translateX(-20%) translateY(0) rotate(0deg)}50%{transform:translateX(20%) translateY(-8%) rotate(5deg)}100%{transform:translateX(-20%) translateY(0) rotate(0deg)}} + @keyframes floatY{0%{transform:translateY(0)}50%{transform:translateY(-18%)}100%{transform:translateY(0)}} + + /* UI right column */ + .panel{ + background:linear-gradient(180deg,#071525,#06101a); + border-radius:var(--ui-radius); + padding:16px; + color:var(--muted); + height:100%; + box-shadow:0 8px 32px rgba(2,6,23,0.6); + border: 1px solid rgba(255,255,255,0.03); + } + + .scoreline{ + display:flex; + justify-content:space-between; + align-items:center; + margin-bottom:12px; + padding-bottom: 12px; + border-bottom: 1px solid rgba(255,255,255,0.05); + } + + .meter{ + height:12px; + background:rgba(255,255,255,0.03); + border-radius:8px; + overflow:hidden; + box-shadow: inset 0 1px 3px rgba(0,0,0,0.3); + } + + .meter > i{ + display:block; + height:100%; + background:linear-gradient(90deg,var(--accent),#ffd48a); + width:0%; + border-radius: 8px; + transition: width 0.1s linear; + box-shadow: 0 0 8px rgba(255, 184, 77, 0.4); + } + + .choices{ + display:grid; + grid-template-columns:1fr 1fr; + gap:10px; + margin-top:12px; + } + + .choice{ + background:rgba(255,255,255,0.02); + padding:12px 10px; + border-radius:10px; + text-align:center; + cursor:pointer; + border:1px solid rgba(255,255,255,0.05); + transition: all 0.2s ease; + font-weight: 500; + } + + .choice:hover{ + transform:translateY(-4px); + background: rgba(255,255,255,0.05); + box-shadow: 0 4px 12px rgba(0,0,0,0.2); + } + + .choice.correct{ + outline:2px solid rgba(80,200,120,0.4); + background: rgba(80,200,120,0.1); + box-shadow: 0 0 15px rgba(80,200,120,0.2); + } + + .choice.wrong{ + outline:2px solid rgba(255,80,80,0.4); + background: rgba(255,80,80,0.1); + box-shadow: 0 0 15px rgba(255,80,80,0.2); + } + + .footer{ + display:flex; + gap:8px; + align-items:center; + margin-top:12px; + } + + .muter{ + opacity:0.7; + display: flex; + align-items: center; + gap: 6px; + } + + .muter input[type="checkbox"] { + accent-color: var(--accent); + } + + /* small screens */ + @media (max-width:880px){ + .game-area{grid-template-columns:1fr;} + .stage{height:360px} + } + + #howtoplay { + font-size: 12px; + color:#ff9a3d; + font-weight: 600; + font-family: Tahoma, Geneva, Verdana, sans-serif; + } + #howtoplay2 { + font-weight: 1000; + color: #f0da0f; + text-align: center; + padding: 0; + margin: 0; + } \ No newline at end of file diff --git a/games/shadow-puppet/index.html b/games/shadow-puppet/index.html new file mode 100644 index 00000000..fc9748e4 --- /dev/null +++ b/games/shadow-puppet/index.html @@ -0,0 +1,87 @@ + + + + + + Shadow Puppet - Mini JS Games Hub + + + + +
    +
    +
    Level: 1
    +
    Moves: 0
    +
    Light: 0%
    +
    Stealth: Perfect
    +
    + + + +
    +
    +

    Actions

    +
    +
    +
    + + +
    +
    + +
    +
    +

    Shadow Puppet #999

    +

    Control shadow figures to evade light beams and reach goals, using darkness and light mechanics in this stealth-based game.

    + +

    How to Play:

    +
      +
    • Move: Click and drag your shadow puppet to move
    • +
    • Goal: Reach the glowing goal while staying in shadows
    • +
    • Light Beams: Avoid red light beams that dissolve shadows
    • +
    • Darkness: Use action buttons to manipulate light and darkness
    • +
    • Stealth: Stay hidden from spotlights and security systems
    • +
    • Timing: Some lights move or pulse - time your movements
    • +
    + +

    Shadow Actions:

    +
      +
    • Extinguish: Put out nearby light sources
    • +
    • Block: Create shadow barriers to block light beams
    • +
    • Phase: Temporarily become intangible to pass through lights
    • +
    • Merge: Combine with other shadows for special abilities
    • +
    + + +
    +
    + + + + + + +
    + + + + \ No newline at end of file diff --git a/games/shadow-puppet/script.js b/games/shadow-puppet/script.js new file mode 100644 index 00000000..84b185e2 --- /dev/null +++ b/games/shadow-puppet/script.js @@ -0,0 +1,717 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const startButton = document.getElementById('startButton'); +const resetButton = document.getElementById('resetButton'); +const hintButton = document.getElementById('hintButton'); +const nextLevelButton = document.getElementById('nextLevelButton'); +const tryAgainButton = document.getElementById('tryAgainButton'); +const closeHintButton = document.getElementById('closeHintButton'); +const instructionsOverlay = document.getElementById('instructions-overlay'); +const levelCompleteOverlay = document.getElementById('level-complete-overlay'); +const gameOverOverlay = document.getElementById('game-over-overlay'); +const hintOverlay = document.getElementById('hint-overlay'); +const levelElement = document.getElementById('level'); +const movesElement = document.getElementById('moves'); +const lightLevelElement = document.getElementById('light-level'); +const stealthElement = document.getElementById('stealth'); +const actionButtonsContainer = document.getElementById('action-buttons'); + +canvas.width = 750; +canvas.height = 600; + +let gameRunning = false; +let levelComplete = false; +let gameOver = false; +let shadowPuppet; +let lightBeams = []; +let spotlights = []; +let lightSources = []; +let shadowBarriers = []; +let goal; +let platforms = []; +let currentLevel = 1; +let moves = 0; +let lightExposure = 0; +let maxLightExposure = 100; +let actions = []; +let dragging = false; +let dragOffset = { x: 0, y: 0 }; + +// Shadow Puppet class +class ShadowPuppet { + constructor(x, y) { + this.x = x; + this.y = y; + this.width = 24; + this.height = 36; + this.targetX = x; + this.targetY = y; + this.speed = 3; + this.intangible = false; + this.intangibleTimer = 0; + this.merged = false; + this.glow = 0; + } + + update() { + // Move towards target position + const dx = this.targetX - this.x; + const dy = this.targetY - this.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance > 1) { + this.x += (dx / distance) * this.speed; + this.y += (dy / distance) * this.speed; + } + + // Intangible effect + if (this.intangible) { + this.intangibleTimer--; + if (this.intangibleTimer <= 0) { + this.intangible = false; + } + } + + // Glow effect + this.glow += 0.1; + + // Check light exposure + this.checkLightExposure(); + + // Check goal collision + if (goal && this.containsPoint(goal.x, goal.y)) { + levelComplete = true; + showLevelComplete(); + } + } + + checkLightExposure() { + let exposure = 0; + + // Check light beams + lightBeams.forEach(beam => { + if (beam.active && this.intersectsBeam(beam)) { + exposure += beam.intensity; + } + }); + + // Check spotlights + spotlights.forEach(spotlight => { + if (spotlight.active && this.inSpotlight(spotlight)) { + exposure += spotlight.intensity; + } + }); + + // Check light sources + lightSources.forEach(source => { + if (source.active) { + const dx = this.x + this.width/2 - source.x; + const dy = this.y + this.height/2 - source.y; + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance < source.radius) { + exposure += source.intensity * (1 - distance / source.radius); + } + } + }); + + lightExposure = Math.min(maxLightExposure, lightExposure + exposure); + + if (lightExposure >= maxLightExposure && !this.intangible) { + gameOver = true; + showGameOver(); + } + } + + intersectsBeam(beam) { + // Simple beam intersection check + const puppetCenterX = this.x + this.width / 2; + const puppetCenterY = this.y + this.height / 2; + + // Check if puppet center is within beam bounds + if (puppetCenterX >= beam.x && puppetCenterX <= beam.x + beam.width && + puppetCenterY >= beam.y && puppetCenterY <= beam.y + beam.height) { + return true; + } + + return false; + } + + inSpotlight(spotlight) { + const dx = this.x + this.width/2 - spotlight.x; + const dy = this.y + this.height/2 - spotlight.y; + const distance = Math.sqrt(dx * dx + dy * dy); + return distance <= spotlight.radius; + } + + containsPoint(px, py) { + return px >= this.x && px <= this.x + this.width && + py >= this.y && py <= this.y + this.height; + } + + setTarget(x, y) { + // Check if target is in shadow barrier + let validTarget = true; + shadowBarriers.forEach(barrier => { + if (x >= barrier.x && x <= barrier.x + barrier.width && + y >= barrier.y && y <= barrier.y + barrier.height) { + validTarget = false; + } + }); + + if (validTarget) { + this.targetX = x - this.width / 2; + this.targetY = y - this.height / 2; + moves++; + updateUI(); + } + } + + draw() { + ctx.save(); + + // Shadow effect + ctx.shadowColor = 'rgba(255, 255, 255, 0.3)'; + ctx.shadowBlur = 10; + + // Puppet body + ctx.fillStyle = this.intangible ? 'rgba(255, 255, 255, 0.5)' : '#ffffff'; + ctx.fillRect(this.x, this.y, this.width, this.height); + + // Puppet details + ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; + // Head + ctx.fillRect(this.x + 6, this.y, 12, 12); + // Body + ctx.fillRect(this.x + 8, this.y + 12, 8, 16); + // Arms + ctx.fillRect(this.x + 2, this.y + 14, 4, 12); + ctx.fillRect(this.x + 18, this.y + 14, 4, 12); + // Legs + ctx.fillRect(this.x + 6, this.y + 28, 4, 8); + ctx.fillRect(this.x + 14, this.y + 28, 4, 8); + + // Glow effect when intangible + if (this.intangible) { + ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)'; + ctx.lineWidth = 2; + ctx.strokeRect(this.x - 2, this.y - 2, this.width + 4, this.height + 4); + } + + // Target indicator + if (dragging) { + ctx.strokeStyle = 'rgba(255, 255, 0, 0.8)'; + ctx.lineWidth = 2; + ctx.setLineDash([5, 5]); + ctx.strokeRect(this.targetX, this.targetY, this.width, this.height); + ctx.setLineDash([]); + } + + ctx.restore(); + } + + useAction(actionType) { + switch (actionType) { + case 'extinguish': + this.extinguishNearbyLights(); + break; + case 'block': + this.createShadowBarrier(); + break; + case 'phase': + this.phaseThrough(); + break; + case 'merge': + this.mergeWithShadows(); + break; + } + moves++; + updateUI(); + } + + extinguishNearbyLights() { + lightSources.forEach(source => { + const dx = this.x + this.width/2 - source.x; + const dy = this.y + this.height/2 - source.y; + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance < 100) { + source.active = false; + setTimeout(() => source.active = true, 10000); // Reactivate after 10 seconds + } + }); + } + + createShadowBarrier() { + const barrier = { + x: this.x + this.width + 10, + y: this.y, + width: 40, + height: 60, + duration: 300 // frames + }; + shadowBarriers.push(barrier); + } + + phaseThrough() { + this.intangible = true; + this.intangibleTimer = 180; // 3 seconds at 60fps + } + + mergeWithShadows() { + // Find nearby shadow barriers to merge with + shadowBarriers.forEach(barrier => { + const dx = this.x + this.width/2 - (barrier.x + barrier.width/2); + const dy = this.y + this.height/2 - (barrier.y + barrier.height/2); + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance < 80) { + this.merged = true; + this.speed = 6; // Faster movement when merged + setTimeout(() => { + this.merged = false; + this.speed = 3; + }, 5000); + } + }); + } +} + +// Light Beam class +class LightBeam { + constructor(x, y, width, height, direction = 'horizontal', moving = false) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.direction = direction; + this.moving = moving; + this.active = true; + this.intensity = 2; + this.phase = 0; + this.originalX = x; + this.originalY = y; + } + + update() { + this.phase += 0.05; + + if (this.moving) { + if (this.direction === 'horizontal') { + this.x = this.originalX + Math.sin(this.phase) * 50; + } else { + this.y = this.originalY + Math.sin(this.phase) * 50; + } + } + } + + draw() { + if (!this.active) return; + + ctx.save(); + ctx.fillStyle = `rgba(255, 0, 0, ${0.3 + Math.sin(this.phase) * 0.2})`; + ctx.fillRect(this.x, this.y, this.width, this.height); + + // Beam lines + ctx.strokeStyle = 'rgba(255, 100, 100, 0.8)'; + ctx.lineWidth = 2; + ctx.setLineDash([5, 5]); + ctx.strokeRect(this.x, this.y, this.width, this.height); + ctx.setLineDash([]); + + ctx.restore(); + } +} + +// Spotlight class +class Spotlight { + constructor(x, y, radius = 80) { + this.x = x; + this.y = y; + this.radius = radius; + this.active = true; + this.intensity = 1.5; + this.angle = 0; + this.sweepSpeed = 0.02; + this.sweeping = false; + } + + update() { + if (this.sweeping) { + this.angle += this.sweepSpeed; + } + } + + draw() { + if (!this.active) return; + + ctx.save(); + ctx.globalAlpha = 0.4; + ctx.fillStyle = 'rgba(255, 255, 0, 0.3)'; + + // Spotlight cone + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, this.angle - 0.5, this.angle + 0.5); + ctx.lineTo(this.x, this.y); + ctx.fill(); + + // Spotlight border + ctx.strokeStyle = 'rgba(255, 255, 0, 0.8)'; + ctx.lineWidth = 2; + ctx.stroke(); + + ctx.restore(); + } +} + +// Light Source class +class LightSource { + constructor(x, y, radius = 60) { + this.x = x; + this.y = y; + this.radius = radius; + this.active = true; + this.intensity = 1; + this.flickering = false; + this.flickerPhase = 0; + } + + update() { + if (this.flickering) { + this.flickerPhase += 0.2; + } + } + + draw() { + if (!this.active) return; + + ctx.save(); + + let alpha = 0.3; + if (this.flickering) { + alpha *= (0.5 + Math.sin(this.flickerPhase) * 0.5); + } + + ctx.globalAlpha = alpha; + ctx.fillStyle = 'rgba(255, 200, 0, 0.4)'; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fill(); + + // Light source core + ctx.globalAlpha = 0.8; + ctx.fillStyle = '#ffaa00'; + ctx.beginPath(); + ctx.arc(this.x, this.y, 8, 0, Math.PI * 2); + ctx.fill(); + + ctx.restore(); + } +} + +// Goal class +class Goal { + constructor(x, y, radius = 25) { + this.x = x; + this.y = y; + this.radius = radius; + this.pulse = 0; + } + + draw() { + this.pulse += 0.1; + + ctx.save(); + ctx.shadowColor = '#00ff00'; + ctx.shadowBlur = 20 + Math.sin(this.pulse) * 10; + + ctx.strokeStyle = '#00ff00'; + ctx.lineWidth = 4; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.stroke(); + + ctx.fillStyle = 'rgba(0, 255, 0, 0.2)'; + ctx.fill(); + + // Inner glow + ctx.shadowBlur = 10; + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius * 0.6, 0, Math.PI * 2); + ctx.stroke(); + + ctx.restore(); + } +} + +// Level definitions +const levels = [ + // Level 1: Simple light beam + { + puppetStart: { x: 100, y: 300 }, + lightBeams: [ + { x: 200, y: 250, width: 150, height: 20 } + ], + spotlights: [], + lightSources: [ + { x: 400, y: 200, radius: 60 } + ], + goal: { x: 600, y: 400 }, + platforms: [ + { x: 0, y: 400, width: 300, height: 20 }, + { x: 500, y: 450, width: 250, height: 20 } + ], + actions: ['extinguish'], + hint: "Use the extinguish action to turn off the light source and create a safe path!" + }, + // Level 2: Moving spotlight + { + puppetStart: { x: 100, y: 200 }, + lightBeams: [], + spotlights: [ + { x: 400, y: 300, radius: 100, sweeping: true } + ], + lightSources: [], + goal: { x: 650, y: 500 }, + platforms: [ + { x: 0, y: 300, width: 200, height: 20 }, + { x: 300, y: 400, width: 200, height: 20 }, + { x: 550, y: 350, width: 200, height: 20 } + ], + actions: ['block', 'phase'], + hint: "The spotlight sweeps back and forth. Time your movement or use phase to pass through!" + }, + // Level 3: Complex setup + { + puppetStart: { x: 50, y: 250 }, + lightBeams: [ + { x: 150, y: 200, width: 20, height: 200, direction: 'vertical', moving: true }, + { x: 300, y: 350, width: 150, height: 20 } + ], + spotlights: [ + { x: 500, y: 200, radius: 80 } + ], + lightSources: [ + { x: 450, y: 450, radius: 50, flickering: true } + ], + goal: { x: 680, y: 520 }, + platforms: [ + { x: 0, y: 350, width: 150, height: 20 }, + { x: 200, y: 450, width: 150, height: 20 }, + { x: 400, y: 300, width: 150, height: 20 }, + { x: 600, y: 400, width: 150, height: 20 } + ], + actions: ['extinguish', 'block', 'phase', 'merge'], + hint: "Combine multiple actions: block light beams, extinguish sources, and merge with shadows for safety!" + } +]; + +// Initialize level +function initLevel() { + const level = levels[currentLevel - 1]; + + shadowPuppet = new ShadowPuppet(level.puppetStart.x, level.puppetStart.y); + + lightBeams = level.lightBeams.map(b => new LightBeam(b.x, b.y, b.width, b.height, b.direction, b.moving)); + spotlights = level.spotlights.map(s => { + const spotlight = new Spotlight(s.x, s.y, s.radius); + spotlight.sweeping = s.sweeping || false; + return spotlight; + }); + lightSources = level.lightSources.map(l => { + const source = new LightSource(l.x, l.y, l.radius); + source.flickering = l.flickering || false; + return source; + }); + shadowBarriers = []; + goal = new Goal(level.goal.x, level.goal.y); + platforms = level.platforms; + + actions = level.actions; + lightExposure = 0; + moves = 0; + + createActionButtons(); + updateUI(); +} + +// Create action buttons +function createActionButtons() { + actionButtonsContainer.innerHTML = ''; + + actions.forEach(action => { + const button = document.createElement('div'); + button.className = 'action-button'; + button.textContent = action.charAt(0).toUpperCase() + action.slice(1); + button.addEventListener('click', () => { + shadowPuppet.useAction(action); + button.classList.add('active'); + setTimeout(() => button.classList.remove('active'), 200); + }); + actionButtonsContainer.appendChild(button); + }); +} + +// Update UI +function updateUI() { + levelElement.textContent = `Level: ${currentLevel}`; + movesElement.textContent = `Moves: ${moves}`; + lightLevelElement.textContent = `Light: ${Math.round(lightExposure)}%`; + + let stealthRating = 'Perfect'; + if (lightExposure > 75) stealthRating = 'Poor'; + else if (lightExposure > 50) stealthRating = 'Fair'; + else if (lightExposure > 25) stealthRating = 'Good'; + else stealthRating = 'Perfect'; + + stealthElement.textContent = `Stealth: ${stealthRating}`; +} + +// Show level complete +function showLevelComplete() { + gameRunning = false; + + const lightPercent = Math.round((lightExposure / maxLightExposure) * 100); + document.getElementById('level-stats').textContent = `Moves: ${moves} | Light Exposure: ${lightPercent}%`; + + let rating = 'Shadow Master'; + if (lightPercent > 75) rating = 'Detected'; + else if (lightPercent > 50) rating = 'Exposed'; + else if (lightPercent > 25) rating = 'Caution'; + else rating = 'Shadow Master'; + + document.getElementById('stealth-rating').textContent = `Stealth Rating: ${rating}`; + + levelCompleteOverlay.style.display = 'flex'; +} + +// Show game over +function showGameOver() { + gameRunning = false; + gameOverOverlay.style.display = 'flex'; +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw platforms + ctx.fillStyle = '#333333'; + platforms.forEach(platform => { + ctx.fillRect(platform.x, platform.y, platform.width, platform.height); + }); + + // Draw shadow barriers + ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; + shadowBarriers.forEach(barrier => { + ctx.fillRect(barrier.x, barrier.y, barrier.width, barrier.height); + barrier.duration--; + if (barrier.duration <= 0) { + shadowBarriers.splice(shadowBarriers.indexOf(barrier), 1); + } + }); + + // Draw light sources + lightSources.forEach(source => { + source.update(); + source.draw(); + }); + + // Draw light beams + lightBeams.forEach(beam => { + beam.update(); + beam.draw(); + }); + + // Draw spotlights + spotlights.forEach(spotlight => { + spotlight.update(); + spotlight.draw(); + }); + + // Draw goal + if (goal) goal.draw(); + + // Draw shadow puppet + if (shadowPuppet) shadowPuppet.draw(); +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + shadowPuppet.update(); + draw(); + + requestAnimationFrame(gameLoop); +} + +// Event listeners +startButton.addEventListener('click', () => { + instructionsOverlay.style.display = 'none'; + gameRunning = true; + initLevel(); + gameLoop(); +}); + +resetButton.addEventListener('click', () => { + initLevel(); +}); + +hintButton.addEventListener('click', () => { + const level = levels[currentLevel - 1]; + document.getElementById('hint-text').textContent = level.hint; + hintOverlay.style.display = 'flex'; +}); + +closeHintButton.addEventListener('click', () => { + hintOverlay.style.display = 'none'; +}); + +nextLevelButton.addEventListener('click', () => { + levelComplete = false; + levelCompleteOverlay.style.display = 'none'; + currentLevel = Math.min(currentLevel + 1, levels.length); + initLevel(); + gameRunning = true; + gameLoop(); +}); + +tryAgainButton.addEventListener('click', () => { + gameOver = false; + gameOverOverlay.style.display = 'none'; + initLevel(); + gameRunning = true; + gameLoop(); +}); + +// Mouse controls for puppet movement +canvas.addEventListener('mousedown', (e) => { + if (!gameRunning) return; + + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + if (shadowPuppet.containsPoint(x, y)) { + dragging = true; + dragOffset.x = x - shadowPuppet.x; + dragOffset.y = y - shadowPuppet.y; + } +}); + +canvas.addEventListener('mousemove', (e) => { + if (!dragging || !gameRunning) return; + + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + shadowPuppet.setTarget(x, y); +}); + +canvas.addEventListener('mouseup', () => { + dragging = false; +}); + +// Initialize +updateUI(); \ No newline at end of file diff --git a/games/shadow-puppet/style.css b/games/shadow-puppet/style.css new file mode 100644 index 00000000..30a30703 --- /dev/null +++ b/games/shadow-puppet/style.css @@ -0,0 +1,332 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Crimson Text', serif; + background: linear-gradient(135deg, #000000 0%, #1a0a1e 50%, #0a0a0a 100%); + color: #ffffff; + overflow: hidden; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +} + +#game-container { + position: relative; + width: 1000px; + height: 700px; + border-radius: 15px; + overflow: hidden; + box-shadow: 0 0 30px rgba(255, 255, 255, 0.1); + border: 2px solid rgba(255, 255, 255, 0.2); + display: flex; +} + +#ui-panel { + position: absolute; + top: 15px; + left: 15px; + z-index: 10; + background: rgba(0, 0, 0, 0.8); + padding: 15px; + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.3); + font-family: 'Special Elite', cursive; + font-size: 14px; + min-width: 180px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5); +} + +#ui-panel div { + margin-bottom: 8px; + color: #ffffff; + text-shadow: 0 0 5px #ffffff; +} + +#light-level { + color: #ffaa00; + text-shadow: 0 0 5px #ffaa00; +} + +#stealth { + color: #00ff88; + text-shadow: 0 0 5px #00ff88; +} + +#controls-panel { + width: 250px; + background: linear-gradient(135deg, #1a0a1e 0%, #0a0a0a 100%); + border-left: 2px solid rgba(255, 255, 255, 0.2); + padding: 20px; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +#action-controls h3 { + color: #ffffff; + font-family: 'Special Elite', cursive; + font-size: 16px; + margin-bottom: 15px; + text-align: center; + text-shadow: 0 0 10px #ffffff; +} + +#action-buttons { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; + margin-bottom: 20px; +} + +.action-button { + background: linear-gradient(135deg, #2a2a2a, #4a4a4a); + border: 2px solid #666; + color: #fff; + padding: 8px 12px; + border-radius: 8px; + cursor: pointer; + font-size: 11px; + font-weight: 600; + text-align: center; + transition: all 0.3s ease; + font-family: 'Crimson Text', serif; + min-height: 35px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); +} + +.action-button:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(255, 255, 255, 0.2); +} + +.action-button.active { + background: linear-gradient(135deg, #ffaa00, #ff8800); + border-color: #ffaa00; + box-shadow: 0 0 15px rgba(255, 170, 0, 0.5); + color: #000; +} + +.action-button.cooldown { + background: linear-gradient(135deg, #666, #888); + opacity: 0.6; + cursor: not-allowed; +} + +#game-controls { + display: flex; + flex-direction: column; + gap: 10px; +} + +#game-controls button { + background: linear-gradient(135deg, #333, #555); + border: 2px solid #666; + color: #fff; + padding: 12px; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + transition: all 0.3s ease; + font-family: 'Crimson Text', serif; +} + +#game-controls button:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(255, 255, 255, 0.2); + background: linear-gradient(135deg, #444, #666); +} + +#gameCanvas { + flex: 1; + background: linear-gradient(135deg, #000000 0%, #1a0a1e 50%, #0a0a0a 100%); + border-radius: 15px; + cursor: grab; +} + +#gameCanvas:active { + cursor: grabbing; +} + +#instructions-overlay, +#level-complete-overlay, +#game-over-overlay, +#hint-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.9); + display: flex; + justify-content: center; + align-items: center; + z-index: 100; + backdrop-filter: blur(8px); +} + +.instructions-content, +.level-complete-content, +.game-over-content, +.hint-content { + background: linear-gradient(135deg, #1a0a1e 0%, #0a0a0a 100%); + padding: 30px; + border-radius: 15px; + border: 2px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 0 30px rgba(255, 255, 255, 0.1); + max-width: 600px; + text-align: center; + font-family: 'Crimson Text', serif; + color: #ffffff; +} + +.instructions-content h1 { + font-family: 'Special Elite', cursive; + font-size: 2.5em; + margin-bottom: 20px; + color: #ffffff; + text-shadow: 0 0 15px #ffffff; +} + +.instructions-content h3 { + color: #ffaa00; + margin: 20px 0 10px 0; + font-size: 1.2em; + font-weight: 600; +} + +.instructions-content ul { + text-align: left; + margin: 15px 0; + padding-left: 20px; +} + +.instructions-content li { + margin-bottom: 8px; + line-height: 1.5; +} + +.instructions-content strong { + color: #00ff88; + font-weight: 600; +} + +button { + background: linear-gradient(135deg, #ffaa00, #ff8800); + border: none; + color: #000; + padding: 12px 24px; + font-size: 16px; + font-weight: 600; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; + margin: 10px; + font-family: 'Crimson Text', serif; + text-transform: uppercase; + letter-spacing: 1px; + box-shadow: 0 4px 15px rgba(255, 170, 0, 0.3); + border: 2px solid rgba(255, 255, 255, 0.2); +} + +button:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(255, 170, 0, 0.5); + background: linear-gradient(135deg, #ffbb22, #ff9900); +} + +.level-complete-content h2, +.game-over-content h2, +.hint-content h3 { + font-family: 'Special Elite', cursive; + font-size: 2em; + color: #ff4444; + margin-bottom: 20px; + text-shadow: 0 0 10px #ff4444; +} + +.level-complete-content h2 { + color: #00ff88; + text-shadow: 0 0 10px #00ff88; +} + +#level-stats, +#stealth-rating { + font-size: 1.2em; + margin: 10px 0; + color: #ffaa00; +} + +#stealth-rating { + color: #00ff88; + font-weight: 600; +} + +.hint-content { + max-width: 400px; +} + +#hint-text { + font-size: 1.1em; + margin: 20px 0; + color: #ffffff; +} + +/* Shadow and light effects */ +.shadow-glow { + box-shadow: 0 0 20px rgba(255, 255, 255, 0.3); +} + +.light-beam { + box-shadow: 0 0 15px rgba(255, 0, 0, 0.8); +} + +.spotlight { + box-shadow: 0 0 25px rgba(255, 255, 0, 0.6); +} + +/* Responsive design */ +@media (max-width: 1100px) { + #game-container { + width: 95vw; + height: 95vh; + flex-direction: column; + } + + #controls-panel { + width: 100%; + height: 200px; + border-left: none; + border-top: 2px solid rgba(255, 255, 255, 0.2); + flex-direction: row; + padding: 15px; + } + + #action-controls { + flex: 1; + margin-right: 20px; + } + + #game-controls { + flex: 1; + flex-direction: row; + justify-content: center; + } + + .instructions-content { + padding: 20px; + max-width: 90vw; + } + + .instructions-content h1 { + font-size: 2.2em; + } +} \ No newline at end of file diff --git a/games/shadow-puzzle-2d/index.html b/games/shadow-puzzle-2d/index.html new file mode 100644 index 00000000..bfdce2e1 --- /dev/null +++ b/games/shadow-puzzle-2d/index.html @@ -0,0 +1,79 @@ + + + + + + Shadow Puzzle 2D โ€” Mini JS Games Hub + + + +
    +
    +

    Shadow Puzzle 2D

    +

    Move objects or the light to match the target silhouette.

    +
    + +
    +
    + + + +
    + +
    +
    + + +
    + +
    + + Ready +
    +
    + + + + + + + + + + diff --git a/games/shadow-puzzle-2d/script.js b/games/shadow-puzzle-2d/script.js new file mode 100644 index 00000000..d30388c0 --- /dev/null +++ b/games/shadow-puzzle-2d/script.js @@ -0,0 +1,470 @@ +/* Shadow Puzzle 2D + - Canvas-driven shapes (rect, circle, triangle) + - Drag shapes or drag the light source + - Shadows are rendered as projected polygons away from light + - Compare generated silhouette against target silhouette (pixel-compare) + - Uses online sound assets (Google Actions sound library) +*/ + +// ========== Helpers ========== +const $ = id => document.getElementById(id); +const clamp = (v,min,max)=>Math.max(min,Math.min(max,v)); +const dist = (a,b)=>Math.hypot(a.x-b.x,a.y-b.y); + +// ========== Canvas & State ========== +const canvas = $('gameCanvas'); +const ctx = canvas.getContext('2d', { willReadFrequently: true }); +let W = canvas.width, H = canvas.height; + +// highDPI scaling for crispness +function fitCanvas(){ + const ratio = window.devicePixelRatio || 1; + const rect = canvas.getBoundingClientRect(); + canvas.width = Math.floor(rect.width * ratio); + canvas.height = Math.floor(rect.height * ratio); + ctx.setTransform(ratio,0,0,ratio,0,0); + W = rect.width; H = rect.height; +} +fitCanvas(); +window.addEventListener('resize', () => { fitCanvas(); render(); }); + +// SFX & Ambience +const sfxClick = $('sfxClick'); const sfxWin = $('sfxWin'); const sfxDrop = $('sfxDrop'); const ambience = $('ambience'); +const soundEnabled = ()=>$('sfxToggle').checked; +const musicEnabled = ()=>$('musicToggle').checked; +$('sfxToggle').addEventListener('change', ()=> {}); +$('musicToggle').addEventListener('change', ()=> { + if(musicEnabled()) { ambience.volume=0.25; ambience.play().catch(()=>{}); } + else { ambience.pause(); ambience.currentTime=0; } +}); + +// ========== Game Objects ========== +let levelIndex = 0; + +// Each level: objects array (type, x,y,rotation,scale), target silhouette (built from shapes) +const LEVELS = [ + // Level 1: simple 2-piece silhouette (a circle + rectangle overlap) + { + name: 'Gentle Lamp', + objects: [ + {type:'rect', x:300, y:280, w:140, h:30, angle: -0.25, fill:'#2b6b9a'}, + {type:'circle', x:360, y:230, r:40, fill:'#5bb8ff'} + ], + target: [ + {type:'rect', x:420, y:260, w:160, h:90, angle:0, fill:'#000'}, + ], + attempts: 999 + }, + // Level 2: three objects + { + name: 'Keyhole', + objects: [ + {type:'circle', x:220, y:260, r:28, fill:'#e07a5f'}, + {type:'rect', x:270, y:300, w:120, h:28, angle:0.12, fill:'#f4d35e'}, + {type:'triangle', x:360, y:220, size:60, angle:-0.3, fill:'#81b29a'} + ], + target: [ + {type:'rect', x:360, y:240, w:240, h:120, angle:0, fill:'#000'} + ] + }, + // Level 3: advanced silhouette (cross-like) + { + name: 'Cross Light', + objects: [ + {type:'rect', x:240, y:240, w:32, h:150, angle:0.0, fill:'#9d4edd'}, + {type:'rect', x:300, y:280, w:150, h:32, angle:0, fill:'#ff7b00'}, + {type:'circle', x:360, y:200, r:28, fill:'#ffd166'} + ], + target:[ + {type:'rect', x:380, y:220, w:220, h:160, angle:0, fill:'#000'} + ] + } +]; + +// Clone helper +function deepClone(obj){ return JSON.parse(JSON.stringify(obj)); } + +// In-level runtime arrays +let objects = []; +let light = { x: 520, y: 120, intensity: 480, glow:1.0 }; +let dragging = null; // {type:'light'|'obj', ptrId, offset} +let paused = false; +let lastRender = 0; + +// ========== Input (mouse/touch) ========== +canvas.addEventListener('pointerdown', (e)=>{ + const rect = canvas.getBoundingClientRect(); + const p = { x: e.clientX - rect.left, y: e.clientY - rect.top }; + // prefer object drag if pointer near object + let found = null; + for(let i=objects.length-1;i>=0;i--){ + const o = objects[i]; + if(pointInObject(p,o)){ found = {obj:o, idx:i}; break;} + } + if(found){ + dragging = {type:'obj', obj:found.obj, startX:found.obj.x, startY:found.obj.y, offset:{x:p.x - found.obj.x, y:p.y - found.obj.y}, id:e.pointerId}; + canvas.setPointerCapture(e.pointerId); + if(soundEnabled()) sfxClick.play().catch(()=>{}); + return; + } + // if click near light -> drag light + if(dist(p, light) < 48){ + dragging = {type:'light', id:e.pointerId, offset:{x:p.x - light.x, y:p.y - light.y}}; + canvas.setPointerCapture(e.pointerId); + if(soundEnabled()) sfxClick.play().catch(()=>{}); + return; + } +}); + +canvas.addEventListener('pointermove', (e)=>{ + if(!dragging) return; + const rect = canvas.getBoundingClientRect(); + const p = { x: e.clientX - rect.left, y: e.clientY - rect.top }; + if(dragging.type === 'light'){ + light.x = clamp(p.x - dragging.offset.x, 0, W); + light.y = clamp(p.y - dragging.offset.y, 0, H); + render(); + } else if(dragging.type === 'obj'){ + dragging.obj.x = p.x - dragging.offset.x; + dragging.obj.y = p.y - dragging.offset.y; + render(); + } +}); + +canvas.addEventListener('pointerup', (e)=>{ + if(dragging && dragging.type==='obj' && soundEnabled()) sfxDrop.play().catch(()=>{}); + if(dragging) { canvas.releasePointerCapture(e.pointerId); dragging = null; } +}); + +// convenience: click on canvas toggles a small nudge for light +canvas.addEventListener('dblclick', (e)=>{ + const rect = canvas.getBoundingClientRect(); + light.x = e.clientX - rect.left; + light.y = e.clientY - rect.top; + render(); +}); + +// ========== Geometry utilities ========== +function pointInObject(p,o){ + if(o.type==='circle') return (Math.hypot(p.x-o.x, p.y-o.y) <= o.r); + if(o.type==='rect'){ + // rotate point inverse + const cos = Math.cos(-o.angle||0), sin = Math.sin(-o.angle||0); + const dx = p.x - o.x, dy = p.y - o.y; + const rx = dx * cos - dy * sin; + const ry = dx * sin + dy * cos; + return Math.abs(rx) <= (o.w/2) && Math.abs(ry) <= (o.h/2); + } + if(o.type==='triangle'){ + // equilateral triangle by center and size + const pts = trianglePoints(o); + return pointInPoly(p, pts); + } + return false; +} +function pointInPoly(pt, poly){ + // ray-casting + let inside=false; + for(let i=0,j=poly.length-1;ipt.y)!=(yj>pt.y)) && (pt.x < (xj-xi)*(pt.y-yi)/(yj-yi)+xi); + if(intersect) inside=!inside; + } + return inside; +} +function rectPoints(o){ + const hw=o.w/2, hh=o.h/2, a=o.angle||0; + const cos=Math.cos(a), sin=Math.sin(a); + return [ + {x:o.x + (-hw)*cos - (-hh)*sin, y:o.y + (-hw)*sin + (-hh)*cos}, + {x:o.x + ( hw)*cos - (-hh)*sin, y:o.y + ( hw)*sin + (-hh)*cos}, + {x:o.x + ( hw)*cos - ( hh)*sin, y:o.y + ( hw)*sin + ( hh)*cos}, + {x:o.x + (-hw)*cos - ( hh)*sin, y:o.y + (-hw)*sin + ( hh)*cos} + ]; +} +function trianglePoints(o){ + const s=o.size||60, a=o.angle||0; + // triangle centered at (x,y), point up by default + const h = s * Math.sqrt(3)/2; + const pts = [ + {x:o.x, y:o.y - (2/3)*h}, + {x:o.x - s/2, y:o.y + (1/3)*h}, + {x:o.x + s/2, y:o.y + (1/3)*h} + ]; + // rotate around center + const cos=Math.cos(a), sin=Math.sin(a); + return pts.map(p=>{ + const dx=p.x - o.x, dy=p.y - o.y; + return { x: o.x + dx * cos - dy * sin, y: o.y + dx * sin + dy * cos }; + }); +} + +// project polygon points away from light to create shadow poly +function projectPoints(pts, lightPos, distanceScale=6.0){ + return pts.map(p=>{ + const vx = p.x - lightPos.x; + const vy = p.y - lightPos.y; + return { x: p.x + vx * distanceScale, y: p.y + vy * distanceScale }; + }); +} + +// draw helpers +function drawObject(o){ + ctx.save(); + ctx.fillStyle = o.fill || '#888'; + ctx.strokeStyle = 'rgba(0,0,0,0.08)'; + ctx.lineWidth = 1.5; + if(o.type==='circle'){ + ctx.beginPath(); ctx.arc(o.x,o.y,o.r,0,Math.PI*2); ctx.fill(); ctx.stroke(); + } else if(o.type==='rect'){ + ctx.translate(o.x,o.y); ctx.rotate(o.angle||0); + ctx.beginPath(); ctx.roundRect(-o.w/2, -o.h/2, o.w, o.h, 6); ctx.fill(); ctx.stroke(); + ctx.setTransform(1,0,0,1,0,0); // reset + } else if(o.type==='triangle'){ + const pts = trianglePoints(o); + ctx.beginPath(); ctx.moveTo(pts[0].x, pts[0].y); + ctx.lineTo(pts[1].x, pts[1].y); ctx.lineTo(pts[2].x, pts[2].y); ctx.closePath(); + ctx.fill(); ctx.stroke(); + } + ctx.restore(); +} + +// ========== Rendering ========== +function clear(){ + ctx.clearRect(0,0,W,H); + // subtle vignette + const g = ctx.createLinearGradient(0,0,0,H); + g.addColorStop(0,'rgba(255,255,255,0.01)'); g.addColorStop(1,'rgba(0,0,0,0.08)'); + ctx.fillStyle = g; ctx.fillRect(0,0,W,H); +} + +function renderShadows(){ + // render shadow shapes by drawing large projected polygons in dark color, then blur/alpha + ctx.save(); + ctx.globalCompositeOperation = 'multiply'; + ctx.fillStyle = 'rgba(0,0,0,0.95)'; + objects.forEach(o=>{ + let pts = []; + if(o.type==='circle'){ + // approximate circle with poly + const seg=18; + for(let i=0;i=0;i--) ctx.lineTo(proj[i].x, proj[i].y); + ctx.closePath(); + ctx.fill(); + }); + ctx.restore(); +} + +function renderLight(){ + // radial gradient to simulate glow; multiply to brighten objects + const grd = ctx.createRadialGradient(light.x, light.y, 0, light.x, light.y, light.intensity); + grd.addColorStop(0, `rgba(255,220,120,${0.45 * light.glow})`); + grd.addColorStop(0.25, `rgba(255,180,80,${0.25 * light.glow})`); + grd.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.save(); + ctx.globalCompositeOperation = 'lighter'; + ctx.fillStyle = grd; + ctx.beginPath(); ctx.arc(light.x, light.y, light.intensity, 0, Math.PI*2); ctx.fill(); + ctx.restore(); + + // draw central bulb + ctx.save(); + ctx.beginPath(); ctx.arc(light.x, light.y, 8 + light.glow*6, 0, Math.PI*2); + ctx.fillStyle = 'rgba(255,235,170,0.98)'; ctx.fill(); + ctx.restore(); +} + +function renderObjects(){ + // draw objects on top of shadows (silhouette visible) + objects.forEach(o => { + // subtle inner glow when near light + const d = Math.hypot(o.x - light.x, o.y - light.y); + const glowAlpha = clamp(1 - (d / (light.intensity*1.2)), 0, 0.55); + ctx.save(); + ctx.shadowColor = 'rgba(255,200,120,' + (0.32 * glowAlpha * light.glow) + ')'; + ctx.shadowBlur = 14 * glowAlpha * light.glow; + drawObject(o); + ctx.restore(); + }); +} + +function renderTargetSilhouette(){ + // draw the target silhouette in top-right preview area inside canvas + // small inset preview + const px = W - 220, py = 20, pw = 200, ph = 160; + ctx.save(); + ctx.translate(px, py); + ctx.fillStyle = 'rgba(255,255,255,0.03)'; + ctx.fillRect(0,0,pw,ph); + // draw target shapes scaled to preview box + const level = LEVELS[levelIndex]; + ctx.save(); + // center target content + ctx.translate(20,20); + level.target.forEach(t=>{ + ctx.fillStyle = '#ffffff'; + if(t.type==='rect'){ + ctx.beginPath(); ctx.roundRect(t.x - 360, t.y - 200, t.w, t.h, 6); ctx.fill(); + } else if(t.type==='circle'){ + ctx.beginPath(); ctx.arc(t.x - 360, t.y - 200, t.r, 0, Math.PI*2); ctx.fill(); + } + }); + ctx.restore(); + // label + ctx.fillStyle = 'rgba(255,255,255,0.7)'; + ctx.font = '12px Inter, system-ui, -apple-system'; + ctx.fillText('Target silhouette', 8, ph + 15); + ctx.restore(); +} + +// main render loop +function render(){ + clear(); + // shadow area + renderShadows(); + // ambient light + renderLight(); + // shapes on top + renderObjects(); + // overlay target preview + renderTargetSilhouette(); + + // status: calculate match percent + const pct = calcMatchPercent(); + $('matchPct').textContent = Math.round(pct) + '%'; + $('matchBar').value = pct; + if(pct >= 92){ + // success! + $('statusMsg').textContent = 'Matched!'; + if(soundEnabled()) { sfxWin.play().catch(()=>{}); } + } else { + $('statusMsg').textContent = 'Keep adjusting to match target'; + } +} + +// ========== Silhouette matching ========== +function renderSilhouetteToTemp(drawTarget){ + // Draw scene's silhouette (shadows+objects) or target silhouette to an offscreen canvas, then compare + const temp = document.createElement('canvas'); + temp.width = 240; temp.height = 160; + const tctx = temp.getContext('2d'); + tctx.fillStyle = '#000'; tctx.fillRect(0,0,temp.width,temp.height); + tctx.save(); + // scale & translate so target and scene align roughly + tctx.scale(0.32, 0.32); // approximate mapping from main canvas + tctx.translate(160, 40); + if(drawTarget){ + const level = LEVELS[levelIndex]; + tctx.fillStyle = '#fff'; + level.target.forEach(t => { + if(t.type === 'rect') { + tctx.fillRect(t.x - 360, t.y - 200, t.w, t.h); + } else if(t.type === 'circle'){ + tctx.beginPath(); tctx.arc(t.x - 360, t.y - 200, t.r, 0, Math.PI*2); tctx.fill(); + } + }); + } else { + // draw shadows and objects as white silhouette + tctx.fillStyle = '#fff'; + // draw projected shadow polygons + objects.forEach(o=>{ + let pts = []; + if(o.type==='circle'){ + const seg = 12; + for(let i=0;i=0;i--) tctx.lineTo(proj[i].x, proj[i].y); + tctx.closePath(); tctx.fill(); + }); + // draw objects as filled (they also contribute) + objects.forEach(o=>{ + if(o.type==='circle'){ tctx.beginPath(); tctx.arc(o.x, o.y, o.r, 0, Math.PI*2); tctx.fill(); } + if(o.type==='rect'){ const pts = rectPoints(o); tctx.beginPath(); tctx.moveTo(pts[0].x,pts[0].y); pts.forEach((p)=>tctx.lineTo(p.x,p.y)); tctx.closePath(); tctx.fill(); } + if(o.type==='triangle'){ const pts = trianglePoints(o); tctx.beginPath(); tctx.moveTo(pts[0].x,pts[0].y); pts.forEach((p)=>tctx.lineTo(p.x,p.y)); tctx.closePath(); tctx.fill(); } + }); + } + tctx.restore(); + return temp; +} + +function calcMatchPercent(){ + const scene = renderSilhouetteToTemp(false); + const target = renderSilhouetteToTemp(true); + const sctx = scene.getContext('2d'), tctx = target.getContext('2d'); + const sdata = sctx.getImageData(0,0,scene.width, scene.height).data; + const tdata = tctx.getImageData(0,0,target.width, target.height).data; + let matchCount = 0, totalCount = 0; + for(let i=0;i 120; + const tOn = tdata[i] > 120; + if(tOn) totalCount++; + if(tOn && sOn) matchCount++; + } + if(totalCount === 0) return 0; + return clamp((matchCount / totalCount) * 100, 0, 100); +} + +// ========== Level management ========== +function loadLevel(idx){ + levelIndex = clamp(idx, 0, LEVELS.length - 1); + const lvl = LEVELS[levelIndex]; + objects = deepClone(lvl.objects); + // default light position + light = { x: W*0.55, y: H*0.18, intensity: Math.min(W,H)*0.9, glow:1.0 }; + $('levelNum').textContent = levelIndex + 1; + if(soundEnabled()) sfxClick.play().catch(()=>{}); + render(); +} + +$('restartBtn').addEventListener('click', () => { if(soundEnabled()) sfxClick.play().catch(()=>{}); loadLevel(levelIndex); }); +$('resetObjectsBtn').addEventListener('click', () => { objects = deepClone(LEVELS[levelIndex].objects); if(soundEnabled()) sfxDrop.play().catch(()=>{}); render(); }); +$('prevLevelBtn').addEventListener('click', ()=>{ loadLevel(levelIndex-1); }); +$('nextLevelBtn').addEventListener('click', ()=>{ loadLevel(levelIndex+1); }); + +$('playPauseBtn').addEventListener('click', ()=> { + paused = !paused; + $('playPauseBtn').textContent = paused ? 'Resume' : 'Pause'; + if(paused) { ambience.pause(); } else { if(musicEnabled()) ambience.play().catch(()=>{}); } +}); + +// open hub button (if placed in hub) +$('openHub').addEventListener('click', ()=>{ window.history.back(); }); + +// UI tick (glow slider could be added later) +function gameTick(ts){ + if(paused) return; + // gentle light pulsate + light.glow = 0.9 + 0.15 * Math.sin(ts/600); + render(); + lastRender = ts; + requestAnimationFrame(gameTick); +} + +// ========== Init ========== +function init(){ + // populate UI toggles initial + if(musicEnabled()) ambience.play().catch(()=>{}); + loadLevel(0); + requestAnimationFrame(gameTick); +} +init(); + +// Expose some internals in console for quick debugging +window.SHADOWPUZZLE = {LEVELS, loadLevel, objects, light, render}; diff --git a/games/shadow-puzzle-2d/style.css b/games/shadow-puzzle-2d/style.css new file mode 100644 index 00000000..ae53b5f7 --- /dev/null +++ b/games/shadow-puzzle-2d/style.css @@ -0,0 +1,50 @@ +:root{ + --bg:#0f1724; + --panel:#0b1220; + --accent:#ffd166; + --muted:#9aa6b2; + --glass: rgba(255,255,255,0.03); + --card-radius:12px; +} + +*{box-sizing:border-box;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial} +html,body{height:100%;margin:0;background:linear-gradient(180deg,#071021 0%, #0f1724 100%);color:#e6eef6} +.app{max-width:1200px;margin:22px auto;padding:18px;border-radius:14px;position:relative} +.app-header{display:flex;flex-direction:column;gap:6px;margin-bottom:12px} +.app-header h1{margin:0;font-size:22px;letter-spacing:0.2px} +.subtitle{margin:0;color:var(--muted);font-size:13px} + +.game-area{display:flex;gap:16px;align-items:flex-start} +.canvas-wrap{flex:1;position:relative;background:linear-gradient(180deg,rgba(255,255,255,0.02), rgba(255,255,255,0.01));padding:14px;border-radius:12px;box-shadow:0 8px 30px rgba(2,6,23,0.6)} +#gameCanvas{display:block;width:100%;height:auto;border-radius:10px;background:linear-gradient(180deg,#0b1220,#071021);box-shadow:0 12px 30px rgba(0,0,0,0.6) inset} + +.controls{width:320px;display:flex;flex-direction:column;gap:12px} +.panel{background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));border-radius:var(--card-radius);padding:12px;box-shadow:0 6px 18px rgba(2,6,23,0.6)} +.row{display:flex;gap:8px;align-items:center;margin-bottom:8px} +.btn{background:linear-gradient(180deg,#132033,#0b1b2a);border:1px solid rgba(255,255,255,0.04);color:#e6eef6;padding:8px 10px;border-radius:8px;cursor:pointer;font-weight:600} +.btn:active{transform:translateY(1px)} +.level-display{flex:1;text-align:center;color:var(--accent)} + +.info{margin-top:6px} +progress{width:100%;height:10px;border-radius:8px;background:#07121a;overflow:hidden} +#matchBar::-webkit-progress-value{background:linear-gradient(90deg,#ffd166,#ff8a00)} + +.hint p{margin:0;color:var(--muted);font-size:13px} + +.audio-controls{display:flex;flex-direction:column;gap:8px} +.audio-controls label{font-size:13px;color:var(--muted)} +.audio-controls input{transform:scale(1.15)} + +.credits small{color:var(--muted)} + +.app-footer{display:flex;justify-content:space-between;align-items:center;margin-top:14px;color:var(--muted)} +.ghost-btn{background:transparent;border:1px solid rgba(255,255,255,0.05);color:var(--muted);padding:8px 10px;border-radius:8px;cursor:pointer} + +.light-hud{position:absolute;right:22px;top:22px;pointer-events:none} +.bulb-glow{width:48px;height:48px;border-radius:999px;background:radial-gradient(circle at 30% 30%, rgba(255,209,102,0.95), rgba(255,140,0,0.2) 40%, rgba(255,140,0,0.04) 70%);box-shadow:0 0 40px rgba(255,180,80,0.45), 0 0 88px rgba(255,140,0,0.12)} + +/* small screens */ +@media (max-width:980px){ + .game-area{flex-direction:column} + .controls{width:100%} +} diff --git a/games/shadow-shift/index.html b/games/shadow-shift/index.html new file mode 100644 index 00000000..0fcd669d --- /dev/null +++ b/games/shadow-shift/index.html @@ -0,0 +1,27 @@ + + + + + + Shadow Shift - Mini JS Games Hub + + + +
    +

    Shadow Shift

    +
    + +
    +
    +
    + + +
    +

    Use arrow keys to move and jump. Click lights to toggle them.

    +

    Reach the goal by toggling lights to shift platforms!

    +
    +
    Made for Mini JS Games Hub
    +
    + + + \ No newline at end of file diff --git a/games/shadow-shift/screenshot.png b/games/shadow-shift/screenshot.png new file mode 100644 index 00000000..c48703ed --- /dev/null +++ b/games/shadow-shift/screenshot.png @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/games/shadow-shift/script.js b/games/shadow-shift/script.js new file mode 100644 index 00000000..cdeccc10 --- /dev/null +++ b/games/shadow-shift/script.js @@ -0,0 +1,274 @@ +// Shadow Shift Game +const canvas = document.getElementById('game'); +const ctx = canvas.getContext('2d'); +const BASE_W = 800, BASE_H = 400, ASPECT = BASE_H / BASE_W; +let DPR = window.devicePixelRatio || 1; +let W = BASE_W, H = BASE_H; + +let frame = 0; +let gameState = 'menu'; // 'menu' | 'play' | 'paused' | 'over' | 'win' +let particles = []; + +const level = { + player: {x: 50, y: 300}, + goal: {x: 750, y: 300, w: 20, h: 40}, + platforms: [ + {x: 0, y: 350, w: 100, h: 50, lightId: null, opacity: 1}, + {x: 200, y: 300, w: 100, h: 20, lightId: 0, opacity: 0}, + {x: 400, y: 250, w: 100, h: 20, lightId: 1, opacity: 0}, + {x: 600, y: 200, w: 100, h: 20, lightId: 0, opacity: 0}, + {x: 720, y: 350, w: 80, h: 50, lightId: null, opacity: 1} + ], + lights: [ + {x: 150, y: 250, r: 15, toggled: false}, + {x: 350, y: 200, r: 15, toggled: false} + ] +}; + +let player = {...level.player, vx: 0, vy: 0, w: 20, h: 40, onGround: false}; + +canvas.setAttribute('role', 'application'); +canvas.setAttribute('aria-label', 'Shadow Shift game canvas'); +canvas.tabIndex = 0; + +function resizeCanvas() { + DPR = window.devicePixelRatio || 1; + const container = canvas.parentElement || document.body; + const maxWidth = Math.min(window.innerWidth - 40, 850); + const cssWidth = Math.min(container.clientWidth - 24 || BASE_W, maxWidth); + const cssHeight = Math.round(cssWidth * ASPECT); + + canvas.style.width = cssWidth + 'px'; + canvas.style.height = cssHeight + 'px'; + + canvas.width = Math.round(cssWidth * DPR); + canvas.height = Math.round(cssHeight * DPR); + ctx.setTransform(DPR, 0, 0, DPR, 0, 0); + + W = cssWidth; + H = cssHeight; +} + +window.addEventListener('resize', resizeCanvas); +resizeCanvas(); + +function reset() { + frame = 0; + player = {...level.player, vx: 0, vy: 0, w: 20, h: 40, onGround: false}; + level.platforms.forEach(p => { + p.opacity = p.lightId === null ? 1 : 0; + }); + level.lights.forEach(l => l.toggled = false); + particles = []; + gameState = 'play'; + document.getElementById('status').textContent = 'Reach the goal by toggling lights!'; +} + +function updatePlatforms() { + level.platforms.forEach(p => { + if (p.lightId !== null) { + const targetOpacity = level.lights[p.lightId].toggled ? 1 : 0; + p.opacity += (targetOpacity - p.opacity) * 0.1; + } + }); +} + +function updatePlayer() { + // Gravity + player.vy += 0.5; + player.y += player.vy; + + // Move + player.x += player.vx; + player.vx *= 0.8; + + // Collision with platforms + player.onGround = false; + level.platforms.forEach(p => { + if (p.opacity > 0.5) { // Only collide if visible enough + if (player.x + player.w > p.x && player.x < p.x + p.w && + player.y + player.h > p.y && player.y < p.y + p.h) { + if (player.vy > 0 && player.y < p.y) { + player.y = p.y - player.h; + player.vy = 0; + player.onGround = true; + } else if (player.vy < 0 && player.y > p.y) { + player.y = p.y + p.h; + player.vy = 0; + } + } + } + }); + + // Bounds + if (player.x < 0) player.x = 0; + if (player.x + player.w > W) player.x = W - player.w; + if (player.y > H) gameState = 'over'; + + // Goal + if (player.x + player.w > level.goal.x && player.x < level.goal.x + level.goal.w && + player.y + player.h > level.goal.y && player.y < level.goal.y + level.goal.h) { + gameState = 'win'; + } + + // Particles + if (Math.abs(player.vx) > 1 && player.onGround) { + particles.push({x: player.x + player.w / 2, y: player.y + player.h, vx: (Math.random() - 0.5) * 2, vy: -Math.random() * 2, life: 30}); + } +} + +function updateParticles() { + particles.forEach(p => { + p.x += p.vx; + p.y += p.vy; + p.vy += 0.1; + p.life--; + }); + particles = particles.filter(p => p.life > 0); +} + +function update() { + if (gameState === 'play') { + frame++; + updatePlatforms(); + updatePlayer(); + updateParticles(); + } +} + +function draw() { + ctx.clearRect(0, 0, W, H); + + // Background + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, W, H); + + // Platforms + ctx.fillStyle = '#000'; + level.platforms.forEach(p => { + ctx.globalAlpha = p.opacity; + ctx.fillRect(p.x, p.y, p.w, p.h); + ctx.globalAlpha = 1; + }); + + // Lights + level.lights.forEach(l => { + ctx.fillStyle = l.toggled ? '#ffff00' : '#666'; + ctx.beginPath(); + ctx.arc(l.x, l.y, l.r, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.stroke(); + }); + + // Goal + ctx.fillStyle = '#000'; + ctx.fillRect(level.goal.x, level.goal.y, level.goal.w, level.goal.h); + + // Player + ctx.fillStyle = '#000'; + ctx.fillRect(player.x, player.y, player.w, player.h); + + // Particles + ctx.fillStyle = '#000'; + particles.forEach(p => { + ctx.globalAlpha = p.life / 30; + ctx.fillRect(p.x - 1, p.y - 1, 2, 2); + ctx.globalAlpha = 1; + }); + + if (gameState === 'menu') { + ctx.fillStyle = 'rgba(255,255,255,0.9)'; + ctx.fillRect(0, 0, W, H); + ctx.fillStyle = '#000'; + ctx.font = '24px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Press Space to start', W / 2, H / 2); + } + if (gameState === 'over') { + ctx.fillStyle = 'rgba(255,255,255,0.9)'; + ctx.fillRect(0, 0, W, H); + ctx.fillStyle = '#000'; + ctx.font = '28px sans-serif'; + ctx.fillText('Game Over', W / 2, H / 2 - 20); + ctx.font = '20px sans-serif'; + ctx.fillText('Press Space to restart', W / 2, H / 2 + 10); + } + if (gameState === 'win') { + ctx.fillStyle = 'rgba(255,255,255,0.9)'; + ctx.fillRect(0, 0, W, H); + ctx.fillStyle = '#000'; + ctx.font = '28px sans-serif'; + ctx.fillText('Level Complete!', W / 2, H / 2 - 20); + ctx.font = '20px sans-serif'; + ctx.fillText('Press Space to play again', W / 2, H / 2 + 10); + } +} + +function loop() { + update(); + draw(); + requestAnimationFrame(loop); +} + +function toggleLight(index) { + level.lights[index].toggled = !level.lights[index].toggled; +} + +function handleClick(x, y) { + level.lights.forEach((l, i) => { + const dx = x - l.x; + const dy = y - l.y; + if (dx * dx + dy * dy < l.r * l.r) { + toggleLight(i); + } + }); +} + +// Input +canvas.addEventListener('click', e => { + const rect = canvas.getBoundingClientRect(); + const x = (e.clientX - rect.left) / (rect.width / W); + const y = (e.clientY - rect.top) / (rect.height / H); + handleClick(x, y); +}); + +canvas.addEventListener('keydown', e => { + if (e.code === 'Space') { + e.preventDefault(); + if (gameState === 'menu' || gameState === 'over' || gameState === 'win') reset(); + else if (player.onGround) { + player.vy = -12; + } + } else if (e.code === 'ArrowLeft') { + e.preventDefault(); + player.vx = -5; + } else if (e.code === 'ArrowRight') { + e.preventDefault(); + player.vx = 5; + } +}); + +canvas.addEventListener('keyup', e => { + if (e.code === 'ArrowLeft' || e.code === 'ArrowRight') { + player.vx = 0; + } +}); + +// Buttons +document.getElementById('startBtn').addEventListener('click', reset); + +document.getElementById('pauseBtn').addEventListener('click', () => { + if (gameState === 'play') { + gameState = 'paused'; + document.getElementById('pauseBtn').setAttribute('aria-pressed', 'true'); + document.getElementById('pauseBtn').textContent = 'Resume'; + } else if (gameState === 'paused') { + gameState = 'play'; + document.getElementById('pauseBtn').setAttribute('aria-pressed', 'false'); + document.getElementById('pauseBtn').textContent = 'Pause'; + } +}); + +loop(); \ No newline at end of file diff --git a/games/shadow-shift/style.css b/games/shadow-shift/style.css new file mode 100644 index 00000000..ed7e371e --- /dev/null +++ b/games/shadow-shift/style.css @@ -0,0 +1,14 @@ +*{box-sizing:border-box;margin:0;padding:0} +body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;background:#fff;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px} +.game-wrap{background:#000;border-radius:15px;padding:20px;text-align:center;box-shadow:0 10px 30px rgba(0,0,0,0.3);max-width:850px;width:100%;color:#fff} +h1{color:#fff;margin-bottom:15px;font-size:1.8em} +canvas{background:#fff;display:block;margin:0 auto;border-radius:10px;max-width:100%;height:auto;border:2px solid #000} +.info{margin-top:15px} +.controls{display:flex;gap:10px;justify-content:center;margin-bottom:10px} +button{padding:8px 16px;border:none;border-radius:8px;background:#fff;color:#000;font-size:14px;cursor:pointer;transition:all 0.3s ease} +button:hover{background:#ccc} +footer{font-size:12px;color:#666;margin-top:15px} +@media (max-width: 600px) { + .game-wrap{padding:15px} + canvas{width:100%;height:auto} +} \ No newline at end of file diff --git a/games/shadowshift/index.html b/games/shadowshift/index.html new file mode 100644 index 00000000..6af676a8 --- /dev/null +++ b/games/shadowshift/index.html @@ -0,0 +1,40 @@ + + + + + +ShadowShift + + + + +
    +
    +
    + ShadowShift + Time: 0.0s + Cloak: Ready +
    +
    + +
    +
    +
    + +
    +
    +

    ShadowShift

    +

    WASD/Arrows to move โ€ข Drag panels to block light โ€ข Space to cloak โ€ข Reach the glowing ring

    + +
    +
    +
    +
    + +
    + + + diff --git a/games/shadowshift/script.js b/games/shadowshift/script.js new file mode 100644 index 00000000..a11f35f7 --- /dev/null +++ b/games/shadowshift/script.js @@ -0,0 +1,61 @@ +const canvas=document.getElementById("game");const ctx=canvas.getContext("2d");const overlay=document.getElementById("overlay");const btnStart=document.getElementById("btnStart");const btnRestart=document.getElementById("btnRestart");const timeEl=document.getElementById("time");const cloakEl=document.getElementById("cloak");const toast=document.getElementById("toast"); +const W=960,H=600;canvas.width=W;canvas.height=H; +const keys=new Set();let mouse={x:0,y:0,down:false};let dragging=null; +const clamp=(v,a,b)=>Math.max(a,Math.min(b,v)); +function rectsIntersect(a,b){return !(a.x+a.wr*r)return null;const d=Math.sqrt(d2)||1;const px=dx/d*(r-d),py=dy/d*(r-d);return {px,py}} +function segIntersects(a,b,c,d){const s1x=b.x-a.x,s1y=b.y-a.y,s2x=d.x-c.x,s2y=d.y-c.y;const s=(-s1y*(a.x-c.x)+s1x*(a.y-c.y))/(-s2x*s1y+s1x*s2y);const t=( s2x*(a.y-c.y)-s2y*(a.x-c.x))/(-s2x*s1y+s1x*s2y);return s>=0&&s<=1&&t>=0&&t<=1} +function segRectBlock(ax,ay,bx,by,r){const e=[{a:{x:r.x,y:r.y},b:{x:r.x+r.w,y:r.y}},{a:{x:r.x+r.w,y:r.y},b:{x:r.x+r.w,y:r.y+r.h}},{a:{x:r.x+r.w,y:r.y+r.h},b:{x:r.x,y:r.y+r.h}},{a:{x:r.x,y:r.y+r.h},b:{x:r.x,y:r.y}}];for(const ed of e){if(segIntersects({x:ax,y:ay},{x:bx,y:by},ed.a,ed.b))return true}return false} +function losBlocked(ax,ay,bx,by,blocks){for(const r of blocks){if(segRectBlock(ax,ay,bx,by,r))return true}return false} +function rnd(a,b){return Math.random()*(b-a)+a} +function gradientBG(){const g=ctx.createLinearGradient(0,0,0,H);g.addColorStop(0,"#0b0b14");g.addColorStop(1,"#0a0a12");return g} +const level={ + player:{x:90,y:520,r:12,speed:2.7}, + goal:{x:860,y:70,r:20}, + walls:[ + {x:150,y:470,w:660,h:18}, + {x:150,y:112,w:660,h:18}, + {x:150,y:130,w:18,h:340}, + {x:792,y:130,w:18,h:340}, + {x:260,y:230,w:110,h:20}, + {x:430,y:160,w:20,h:120}, + {x:520,y:300,w:150,h:20}, + {x:720,y:220,w:20,h:120} + ], + blockers:[ + {x:310,y:350,w:70,h:28,color:"#7dd3fc"}, + {x:590,y:200,w:84,h:28,color:"#a7f3d0"} + ], + lights:[ + {x:330,y:190,r:220,intensity:0.9,color:"rgba(255,240,180,1)"}, + {x:700,y:430,r:240,intensity:0.85,color:"rgba(190,220,255,1)"}, + {x:500,y:380,r:160,intensity:0.95,color:"rgba(255,190,230,1)"} + ] +}; +let state="idle";let t0=0;let elapsed=0;let spotted=false;let win=false; +let cloak={ready:true,active:false,time:0,duration:2.5,cooldown:6,cdLeft:0}; +function showToast(msg){toast.textContent=msg;toast.classList.add("show");setTimeout(()=>toast.classList.remove("show"),1200)} +function reset(){player.x=level.player.x;player.y=level.player.y;elapsed=0;spotted=false;win=false;cloak.ready=true;cloak.active=false;cloak.time=0;cloak.cdLeft=0} +const player={x:level.player.x,y:level.player.y,r:level.player.r,speed:level.player.speed,color:"#e8e8f3"}; +function updateCloak(dt){if(cloak.active){cloak.time+=dt;if(cloak.time>=cloak.duration){cloak.active=false;cloak.time=0;cloak.cdLeft=cloak.cooldown}}else if(!cloak.ready){cloak.cdLeft-=dt;if(cloak.cdLeft<=0){cloak.cdLeft=0;cloak.ready=true}}cloakEl.textContent=cloak.active?`${(cloak.duration-cloak.time).toFixed(1)}s`:(cloak.ready?"Ready":`${cloak.cdLeft.toFixed(1)}s`)} +function handleInput(dt){let dx=0,dy=0;if(keys.has("ArrowUp")||keys.has("w"))dy-=1;if(keys.has("ArrowDown")||keys.has("s"))dy+=1;if(keys.has("ArrowLeft")||keys.has("a"))dx-=1;if(keys.has("ArrowRight")||keys.has("d"))dx+=1;const len=Math.hypot(dx,dy)||1;dx/=len;dy/=len;const sp=player.speed;let nx=player.x+dx*sp,ny=player.y+dy*sp;for(const r of [...level.walls,...level.blockers]){const pen=circleRectPenetration(nx,ny,player.r,r.x,r.y,r.w,r.h);if(pen){nx+=pen.px;ny+=pen.py}}nx=clamp(nx,player.r,W-player.r);ny=clamp(ny,player.r,H-player.r);player.x=nx;player.y=ny} +function drawLights(){for(const l of level.lights){const g=ctx.createRadialGradient(l.x,l.y,0,l.x,l.y,l.r);g.addColorStop(0,`${l.color}`);g.addColorStop(0.15,`${l.color}`);g.addColorStop(1,"rgba(0,0,0,0)");ctx.globalCompositeOperation="lighter";ctx.fillStyle=g;ctx.beginPath();ctx.arc(l.x,l.y,l.r,0,Math.PI*2);ctx.fill();ctx.globalCompositeOperation="source-over"}} +function drawWalls(){ctx.fillStyle="#22223b";for(const r of level.walls){ctx.fillRect(r.x,r.y,r.w,r.h)}} +function drawBlockers(){for(const b of level.blockers){ctx.fillStyle=b.color;ctx.fillRect(b.x,b.y,b.w,b.h);ctx.strokeStyle="rgba(255,255,255,.15)";ctx.strokeRect(b.x,b.y,b.w,b.h)}} +function drawPlayer(){ctx.save();if(cloak.active){ctx.globalAlpha=0.5}ctx.fillStyle=player.color;ctx.beginPath();ctx.arc(player.x,player.y,player.r,0,Math.PI*2);ctx.fill();ctx.restore()} +function drawGoal(){ctx.strokeStyle="#9afc8b";ctx.lineWidth=4;ctx.beginPath();ctx.arc(level.goal.x,level.goal.y,level.goal.r,0,Math.PI*2);ctx.stroke();ctx.lineWidth=1} +function drawOcclusion(){ctx.fillStyle="rgba(10,10,16,.86)";for(const r of level.walls){ctx.fillRect(r.x,r.y,r.w,r.h)}for(const b of level.blockers){ctx.fillRect(b.x,b.y,b.w,b.h)}} +function drawDetectionOverlay(){if(spotted){ctx.fillStyle="rgba(255,0,70,.15)";ctx.fillRect(0,0,W,H)}} +function checkDetection(){if(cloak.active)return false;for(const l of level.lights){const dx=player.x-l.x,dy=player.y-l.y;const d2=dx*dx+dy*dy;const r2=l.r*l.r;if(d2<=r2){if(!losBlocked(l.x,l.y,player.x,player.y,[...level.walls,...level.blockers]))return true}}return false} +function checkWin(){const d=Math.hypot(player.x-level.goal.x,player.y-level.goal.y);return d<=level.goal.r-2} +function update(dt){if(state!=="play")return;elapsed+=dt;timeEl.textContent=elapsed.toFixed(1);handleInput(dt);updateCloak(dt);if(checkWin()){win=true;state="idle";overlay.style.display="grid";overlay.firstElementChild.querySelector("h1").textContent="Level Complete";overlay.firstElementChild.querySelector("p").textContent=`Time ${elapsed.toFixed(1)}s`;overlay.firstElementChild.querySelector("button").textContent="Play Again";return}spotted=checkDetection();if(spotted){state="idle";overlay.style.display="grid";overlay.firstElementChild.querySelector("h1").textContent="Detected";overlay.firstElementChild.querySelector("p").textContent="Try moving panels or using cloak";overlay.firstElementChild.querySelector("button").textContent="Retry"}} +function render(){ctx.fillStyle=gradientBG();ctx.fillRect(0,0,W,H);drawLights();drawOcclusion();drawWalls();drawBlockers();drawGoal();drawPlayer();drawDetectionOverlay()} +let last=0;function loop(ts){const dt=Math.min(0.033,(ts-last)/1000||0);last=ts;update(dt);render();requestAnimationFrame(loop)}requestAnimationFrame(loop); +canvas.addEventListener("mousemove",e=>{const rect=canvas.getBoundingClientRect();mouse.x=(e.clientX-rect.left)*(canvas.width/rect.width);mouse.y=(e.clientY-rect.top)*(canvas.height/rect.height);if(dragging){const b=dragging;const ox=b.w*0.5,oy=b.h*0.5;b.x=clamp(mouse.x-ox,0,W-b.w);b.y=clamp(mouse.y-oy,0,H-b.h);for(const w of level.walls){if(rectsIntersect(b,w)){if(b.x+ox< w.x) b.x=w.x-b.w; else if(b.x+ox> w.x+w.w) b.x=w.x+w.w; if(b.y+oy< w.y) b.y=w.y-b.h; else if(b.y+oy> w.y+w.h) b.y=w.y+w.h}}}}); +canvas.addEventListener("mousedown",e=>{mouse.down=true;for(const b of level.blockers){if(mouse.x>=b.x&&mouse.x<=b.x+b.w&&mouse.y>=b.y&&mouse.y<=b.y+b.h){dragging=b;break}}}); +window.addEventListener("mouseup",()=>{mouse.down=false;dragging=null}); +window.addEventListener("keydown",e=>{if(e.key===" "){if(state==="play"){if(cloak.ready&&!cloak.active){cloak.active=true;cloak.ready=false;cloak.time=0;showToast("Cloak engaged")}}e.preventDefault()}if(e.key==="r"||e.key==="R"){reset();overlay.style.display="none";state="play"}keys.add(e.key)}); +window.addEventListener("keyup",e=>{keys.delete(e.key)}); +btnStart.addEventListener("click",()=>{overlay.style.display="none";reset();state="play";t0=performance.now()}); +btnRestart.addEventListener("click",()=>{reset();overlay.style.display="none";state="play"}); +overlay.addEventListener("click",e=>{if(e.target.id==="overlay")return;overlay.style.display="none";reset();state="play"}); diff --git a/games/shadowshift/style.css b/games/shadowshift/style.css new file mode 100644 index 00000000..da854962 --- /dev/null +++ b/games/shadowshift/style.css @@ -0,0 +1,34 @@ +:root { + --bg:#0f0f17; + --panel:#161623; + --accent:#8a7dff; + --accent2:#34f6ff; + --text:#e8e8f3; + --muted:#9aa0b4; + --danger:#ff3b6b; + --good:#9afc8b; +} +* { box-sizing:border-box; } +html,body { height:100%; margin:0; background:radial-gradient(1200px 600px at 70% 10%, #181828, #0e0e16 55%, #0a0a12); color:var(--text); font-family:Poppins, system-ui, -apple-system, Segoe UI, Roboto, Arial; } +.wrap { max-width:1080px; margin:0 auto; padding:20px; display:flex; flex-direction:column; gap:16px; } +.hud { display:flex; justify-content:space-between; align-items:center; background:linear-gradient(180deg, #191927, #131320); padding:12px 16px; border:1px solid #24243a; border-radius:14px; box-shadow:0 10px 40px rgba(0,0,0,.4), inset 0 1px 0 #2a2a45; } +.hud .title { font-weight:800; letter-spacing:.5px; margin-right:16px; color:var(--accent); } +.hud .stat { margin-right:14px; font-weight:600; color:var(--muted); } +.hud .stat b { color:var(--text); } +.hud-right button { background:linear-gradient(135deg, var(--accent), var(--accent2)); color:#0b0b12; border:none; padding:10px 14px; font-weight:800; border-radius:10px; cursor:pointer; } +.hud-right button:active { transform:translateY(1px); } +.game { position:relative; border-radius:18px; overflow:hidden; border:1px solid #24243a; box-shadow:0 20px 60px rgba(0,0,0,.45), inset 0 1px 0 #26263f; } +canvas { display:block; width:100%; height:auto; background:linear-gradient(180deg, #0b0b14, #0a0a12); } +.overlay { position:absolute; inset:0; background:linear-gradient(180deg, rgba(10,10,18,.75), rgba(8,8,14,.9)); display:grid; place-items:center; backdrop-filter: blur(6px); } +.overlay .panel { background:linear-gradient(180deg, #16162a, #101021); border:1px solid #2a2a45; padding:28px; max-width:640px; width:86%; text-align:center; border-radius:16px; box-shadow:0 10px 40px rgba(0,0,0,.5); } +.overlay h1 { margin:0 0 10px; font-size:32px; color:var(--accent2); } +.overlay p { margin:0 0 18px; color:var(--muted); } +.overlay button { background:linear-gradient(135deg, var(--accent), var(--accent2)); color:#0b0b12; border:none; padding:12px 18px; font-weight:800; border-radius:12px; cursor:pointer; } +.toast { position:absolute; left:50%; top:16px; transform:translateX(-50%); background:#121224; border:1px solid #2b2b47; color:var(--text); padding:10px 14px; font-weight:600; border-radius:10px; opacity:0; pointer-events:none; transition:opacity .25s, transform .25s; } +.toast.show { opacity:1; transform:translateX(-50%) translateY(0); } +.footer { display:flex; justify-content:space-between; align-items:center; color:var(--muted); font-size:12px; padding-inline:6px; } +.win-banner, .lose-banner { position:absolute; inset:0; display:grid; place-items:center; pointer-events:none; } +.win-banner .msg, .lose-banner .msg { background:linear-gradient(180deg, #16162a, #101021); border:1px solid #2a2a45; padding:20px 24px; border-radius:14px; font-weight:800; } +.win-banner .msg { color:var(--good); } +.lose-banner .msg { color:var(--danger); } +#btnRestart { white-space:nowrap; } diff --git a/games/shape-logic/index.html b/games/shape-logic/index.html new file mode 100644 index 00000000..7f1fc42d --- /dev/null +++ b/games/shape-logic/index.html @@ -0,0 +1,40 @@ + + + + + + Shape Logic | Mini JS Games Hub + + + + + + +
    +

    Shape Logic ๐Ÿง 

    +
    + + + +
    +
    + +
    +
    + Target Shape +
    + +
    + + + +
    +
    + +
    +

    Click โ€œStartโ€ to play!

    +
    + + + + diff --git a/games/shape-logic/script.js b/games/shape-logic/script.js new file mode 100644 index 00000000..3b1c4acd --- /dev/null +++ b/games/shape-logic/script.js @@ -0,0 +1,92 @@ +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const restartBtn = document.getElementById("restart-btn"); +const bgMusic = document.getElementById("bg-music"); +const snapSound = document.getElementById("snap-sound"); +const shapes = document.querySelectorAll(".shape"); +const statusText = document.getElementById("status-text"); + +let isGameActive = false; +let isPaused = false; + +startBtn.addEventListener("click", () => { + if (!isGameActive) { + isGameActive = true; + isPaused = false; + bgMusic.play(); + statusText.textContent = "๐ŸŽฏ Drag and rotate the shapes to match the outline!"; + enableShapeInteraction(); + } +}); + +pauseBtn.addEventListener("click", () => { + if (isGameActive) { + isPaused = !isPaused; + if (isPaused) { + bgMusic.pause(); + statusText.textContent = "โธ๏ธ Game Paused"; + } else { + bgMusic.play(); + statusText.textContent = "๐ŸŽฎ Game Resumed!"; + } + } +}); + +restartBtn.addEventListener("click", () => { + location.reload(); +}); + +function enableShapeInteraction() { + shapes.forEach(shape => { + shape.addEventListener("mousedown", startDrag); + shape.addEventListener("dblclick", rotateShape); + }); +} + +function startDrag(e) { + if (!isGameActive || isPaused) return; + + const shape = e.target; + let offsetX = e.clientX - shape.getBoundingClientRect().left; + let offsetY = e.clientY - shape.getBoundingClientRect().top; + + function moveShape(ev) { + shape.style.position = "absolute"; + shape.style.left = ev.clientX - offsetX + "px"; + shape.style.top = ev.clientY - offsetY + "px"; + } + + function stopDrag() { + document.removeEventListener("mousemove", moveShape); + document.removeEventListener("mouseup", stopDrag); + checkSnap(shape); + } + + document.addEventListener("mousemove", moveShape); + document.addEventListener("mouseup", stopDrag); +} + +function rotateShape(e) { + if (!isGameActive || isPaused) return; + const shape = e.target; + let angle = parseInt(shape.dataset.rotate) + 45; + shape.dataset.rotate = angle; + shape.style.transform = `rotate(${angle}deg)`; +} + +function checkSnap(shape) { + const target = document.getElementById("target-outline").getBoundingClientRect(); + const s = shape.getBoundingClientRect(); + + const isNear = + Math.abs(s.left - target.left) < 30 && + Math.abs(s.top - target.top) < 30; + + if (isNear) { + shape.style.left = target.left + "px"; + shape.style.top = target.top + "px"; + shape.style.filter = "drop-shadow(0 0 25px lime)"; + snapSound.play(); + statusText.textContent = "โœจ Shape Snapped!"; + } +} diff --git a/games/shape-logic/style.css b/games/shape-logic/style.css new file mode 100644 index 00000000..7faa1828 --- /dev/null +++ b/games/shape-logic/style.css @@ -0,0 +1,80 @@ +body { + background: radial-gradient(circle at center, #1b2735, #090a0f); + color: #fff; + font-family: 'Poppins', sans-serif; + overflow: hidden; + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; +} + +.game-header { + text-align: center; + margin-top: 10px; +} + +h1 { + font-size: 2.2rem; + text-shadow: 0 0 15px #00ffff; +} + +.controls button { + margin: 8px; + padding: 10px 20px; + border: none; + border-radius: 8px; + background: linear-gradient(90deg, #00ffff, #0077ff); + color: #fff; + cursor: pointer; + font-weight: 600; + box-shadow: 0 0 15px #00ffff; + transition: transform 0.2s ease; +} + +.controls button:hover { + transform: scale(1.1); +} + +#game-container { + position: relative; + width: 80%; + height: 70vh; + margin-top: 20px; + border: 2px dashed #00ffff; + border-radius: 10px; + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0 0 20px #00ffff; +} + +#target-outline img { + width: 150px; + opacity: 0.3; + filter: drop-shadow(0 0 15px cyan); +} + +#shapes { + position: absolute; + bottom: 20px; + display: flex; + gap: 20px; +} + +.shape { + width: 80px; + cursor: grab; + filter: drop-shadow(0 0 10px #00ffff); + transition: filter 0.2s ease; +} + +.shape:hover { + filter: drop-shadow(0 0 25px #00ffff); +} + +#status-panel { + margin-top: 10px; + font-size: 1.1rem; + text-shadow: 0 0 10px #00ffff; +} diff --git a/games/shape-rotation-puzzle/index.html b/games/shape-rotation-puzzle/index.html new file mode 100644 index 00000000..66e4838e --- /dev/null +++ b/games/shape-rotation-puzzle/index.html @@ -0,0 +1,28 @@ + + + + + + Shape Rotation Puzzle + + + +
    +

    Shape Rotation Puzzle

    +
    + Score: 0 + Level: 1 +
    + +
    + + + + + +
    +
    + + + + diff --git a/games/shape-rotation-puzzle/script.js b/games/shape-rotation-puzzle/script.js new file mode 100644 index 00000000..c5001e26 --- /dev/null +++ b/games/shape-rotation-puzzle/script.js @@ -0,0 +1,200 @@ +const canvas = document.getElementById("game-canvas"); +const ctx = canvas.getContext("2d"); + +const ROWS = 20; +const COLS = 10; +const BLOCK_SIZE = 30; + +let board = Array.from({length: ROWS}, () => Array(COLS).fill(0)); + +let score = 0; +let level = 1; +let dropInterval = 800; +let dropCounter = 0; +let lastTime = 0; + +const colors = [ + null, + '#FF0D72', + '#0DC2FF', + '#0DFF72', + '#F538FF', + '#FF8E0D', + '#FFE138', + '#3877FF' +]; + +// Tetromino shapes +const SHAPES = [ + [], + [[1,1,1],[0,1,0]], // T + [[2,2],[2,2]], // O + [[0,3,3],[3,3,0]], // S + [[4,4,0],[0,4,4]], // Z + [[5,0,0],[5,5,5]], // L + [[0,0,6],[6,6,6]], // J + [[7,7,7,7]] // I +]; + +let currentPiece; + +function randomPiece() { + const typeId = Math.floor(Math.random() * (SHAPES.length - 1)) + 1; + const shape = SHAPES[typeId]; + return { + shape, + x: Math.floor(COLS / 2) - Math.ceil(shape[0].length / 2), + y: 0, + color: colors[typeId] + }; +} + +function collide(board, piece) { + for (let y = 0; y < piece.shape.length; y++) { + for (let x = 0; x < piece.shape[y].length; x++) { + if (piece.shape[y][x] !== 0 && + (board[y + piece.y] && board[y + piece.y][x + piece.x]) !== 0) { + return true; + } + } + } + return false; +} + +function merge(board, piece) { + piece.shape.forEach((row, y) => { + row.forEach((value, x) => { + if (value !== 0) { + board[y + piece.y][x + piece.x] = value; + } + }); + }); +} + +function rotate(matrix) { + return matrix[0].map((_, i) => matrix.map(row => row[i]).reverse()); +} + +function playerRotate() { + const rotated = rotate(currentPiece.shape); + const oldX = currentPiece.x; + let offset = 1; + currentPiece.shape = rotated; + while (collide(board, currentPiece)) { + currentPiece.x += offset; + offset = -(offset + (offset > 0 ? 1 : -1)); + if (offset > currentPiece.shape[0].length) { + currentPiece.shape = rotate(rotate(rotate(currentPiece.shape))); + currentPiece.x = oldX; + return; + } + } +} + +function drawBlock(x, y, color) { + ctx.fillStyle = color; + ctx.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); + ctx.strokeStyle = '#000'; + ctx.strokeRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); +} + +function draw() { + ctx.fillStyle = '#111'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw board + board.forEach((row, y) => { + row.forEach((value, x) => { + if (value !== 0) drawBlock(x, y, colors[value]); + }); + }); + + // Draw current piece + currentPiece.shape.forEach((row, y) => { + row.forEach((value, x) => { + if (value !== 0) drawBlock(x + currentPiece.x, y + currentPiece.y, currentPiece.color); + }); + }); +} + +function mergeAndReset() { + merge(board, currentPiece); + clearLines(); + currentPiece = randomPiece(); + if (collide(board, currentPiece)) { + board.forEach(row => row.fill(0)); + score = 0; + level = 1; + dropInterval = 800; + alert('Game Over! Restarting...'); + } +} + +function clearLines() { + let rowCount = 0; + outer: for (let y = ROWS - 1; y >= 0; y--) { + for (let x = 0; x < COLS; x++) { + if (board[y][x] === 0) continue outer; + } + const row = board.splice(y, 1)[0].fill(0); + board.unshift(row); + rowCount++; + y++; + } + if (rowCount > 0) { + score += rowCount * 10; + document.getElementById('score').textContent = score; + if (score % 50 === 0) { + level++; + dropInterval *= 0.9; // speed up + document.getElementById('level').textContent = level; + } + } +} + +function playerDrop() { + currentPiece.y++; + if (collide(board, currentPiece)) { + currentPiece.y--; + mergeAndReset(); + } + dropCounter = 0; +} + +function playerMove(dir) { + currentPiece.x += dir; + if (collide(board, currentPiece)) currentPiece.x -= dir; +} + +document.getElementById('rotate').addEventListener('click', () => playerRotate()); +document.getElementById('left').addEventListener('click', () => playerMove(-1)); +document.getElementById('right').addEventListener('click', () => playerMove(1)); +document.getElementById('down').addEventListener('click', () => playerDrop()); +document.getElementById('restart').addEventListener('click', () => { + board = Array.from({length: ROWS}, () => Array(COLS).fill(0)); + score = 0; + level = 1; + dropInterval = 800; + currentPiece = randomPiece(); + document.getElementById('score').textContent = score; + document.getElementById('level').textContent = level; +}); + +document.addEventListener('keydown', event => { + if (event.key === 'ArrowLeft') playerMove(-1); + if (event.key === 'ArrowRight') playerMove(1); + if (event.key === 'ArrowDown') playerDrop(); + if (event.key === 'ArrowUp') playerRotate(); +}); + +function update(time = 0) { + const deltaTime = time - lastTime; + lastTime = time; + dropCounter += deltaTime; + if (dropCounter > dropInterval) playerDrop(); + draw(); + requestAnimationFrame(update); +} + +currentPiece = randomPiece(); +update(); diff --git a/games/shape-rotation-puzzle/style.css b/games/shape-rotation-puzzle/style.css new file mode 100644 index 00000000..cb1de8a4 --- /dev/null +++ b/games/shape-rotation-puzzle/style.css @@ -0,0 +1,59 @@ +body { + margin: 0; + padding: 0; + font-family: 'Segoe UI', sans-serif; + background: linear-gradient(135deg, #89f7fe 0%, #66a6ff 100%); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + color: #fff; +} + +.game-container { + background-color: rgba(0,0,0,0.8); + padding: 20px; + border-radius: 12px; + box-shadow: 0 0 20px rgba(0,0,0,0.5); + text-align: center; +} + +h1 { + margin-bottom: 10px; +} + +.score-board { + display: flex; + justify-content: space-between; + margin-bottom: 10px; + font-size: 18px; +} + +canvas { + background-color: #111; + display: block; + margin: 0 auto; + border: 2px solid #fff; + border-radius: 8px; +} + +.controls { + margin-top: 10px; +} + +.controls button { + padding: 8px 12px; + margin: 5px; + font-size: 16px; + border-radius: 6px; + border: none; + cursor: pointer; + background-color: #66a6ff; + color: #fff; + transition: all 0.2s ease; +} + +.controls button:hover { + background-color: #89f7fe; + transform: scale(1.05); +} diff --git a/games/shape-shifter/index.html b/games/shape-shifter/index.html new file mode 100644 index 00000000..8baa064c --- /dev/null +++ b/games/shape-shifter/index.html @@ -0,0 +1,42 @@ + + + + + + Shape Shifter + + + +
    +

    ๐Ÿ”„ Shape Shifter

    +

    Change shapes to fit through obstacles! Press SPACE to morph.

    + +
    +
    Score: 0
    +
    Level: 1
    +
    + + + +
    + + + +
    + +
    + +
    +

    How to Play:

    +
      +
    • Use SPACEBAR to change your shape
    • +
    • Match your shape to the obstacle holes to pass through
    • +
    • Avoid hitting the obstacles!
    • +
    • Survive as long as possible to increase your score
    • +
    +
    +
    + + + + \ No newline at end of file diff --git a/games/shape-shifter/script.js b/games/shape-shifter/script.js new file mode 100644 index 00000000..6a8817ec --- /dev/null +++ b/games/shape-shifter/script.js @@ -0,0 +1,251 @@ +// Shape Shifter Game +// A simple game where you change shapes to navigate through obstacles + +// Get DOM elements +const canvas = document.getElementById('game-canvas'); +const ctx = canvas.getContext('2d'); +const scoreEl = document.getElementById('current-score'); +const levelEl = document.getElementById('current-level'); +const messageEl = document.getElementById('message'); +const startBtn = document.getElementById('start-btn'); +const pauseBtn = document.getElementById('pause-btn'); +const resetBtn = document.getElementById('reset-btn'); + +// Game constants +const CANVAS_WIDTH = 800; +const CANVAS_HEIGHT = 400; +const PLAYER_SIZE = 20; +const OBSTACLE_WIDTH = 30; +const OBSTACLE_SPEED = 2; +const SHAPES = ['circle', 'square', 'triangle']; + +// Game variables +let gameRunning = false; +let gamePaused = false; +let score = 0; +let level = 1; +let playerShape = 0; // 0: circle, 1: square, 2: triangle +let playerY = CANVAS_HEIGHT / 2; +let obstacles = []; +let animationId; +let lastObstacleTime = 0; + +// Event listeners +startBtn.addEventListener('click', startGame); +pauseBtn.addEventListener('click', togglePause); +resetBtn.addEventListener('click', resetGame); +document.addEventListener('keydown', handleKeyPress); + +// Start the game +function startGame() { + // Reset everything + score = 0; + level = 1; + playerShape = 0; + playerY = CANVAS_HEIGHT / 2; + obstacles = []; + lastObstacleTime = 0; + gameRunning = true; + gamePaused = false; + + // Update UI + scoreEl.textContent = score; + levelEl.textContent = level; + messageEl.textContent = ''; + startBtn.style.display = 'none'; + pauseBtn.style.display = 'inline-block'; + + // Start game loop + gameLoop(); +} + +// Main game loop +function gameLoop() { + if (!gameRunning || gamePaused) return; + + updateGame(); + drawGame(); + + animationId = requestAnimationFrame(gameLoop); +} + +// Update game state +function updateGame() { + // Move obstacles + obstacles.forEach(obstacle => { + obstacle.x -= OBSTACLE_SPEED + (level - 1) * 0.5; // Speed increases with level + }); + + // Remove off-screen obstacles + obstacles = obstacles.filter(obstacle => obstacle.x > -OBSTACLE_WIDTH); + + // Add new obstacles + if (Date.now() - lastObstacleTime > 2000 - level * 100) { // Spawn faster at higher levels + addObstacle(); + lastObstacleTime = Date.now(); + } + + // Check collisions + checkCollisions(); + + // Increase score + score += 1; + scoreEl.textContent = score; + + // Level up every 1000 points + if (score > 0 && score % 1000 === 0) { + levelUp(); + } +} + +// Add a new obstacle +function addObstacle() { + const holeShape = Math.floor(Math.random() * 3); // Random hole shape + const holeY = Math.random() * (CANVAS_HEIGHT - 100) + 50; // Random hole position + + obstacles.push({ + x: CANVAS_WIDTH, + holeY: holeY, + holeShape: holeShape + }); +} + +// Check if player collides with obstacles +function checkCollisions() { + const playerLeft = 50; + const playerRight = playerLeft + PLAYER_SIZE; + const playerTop = playerY - PLAYER_SIZE / 2; + const playerBottom = playerY + PLAYER_SIZE / 2; + + for (let obstacle of obstacles) { + // Check if obstacle is at player position + if (obstacle.x < playerRight && obstacle.x + OBSTACLE_WIDTH > playerLeft) { + // Check if player shape matches hole shape + if (playerShape !== obstacle.holeShape) { + // Check if player is in the hole area + if (!(playerTop >= obstacle.holeY - 25 && playerBottom <= obstacle.holeY + 25)) { + gameOver(); + return; + } + } + } + } +} + +// Level up +function levelUp() { + level++; + levelEl.textContent = level; + messageEl.textContent = `Level ${level}! Speed increased!`; + setTimeout(() => messageEl.textContent = '', 2000); +} + +// Game over +function gameOver() { + gameRunning = false; + cancelAnimationFrame(animationId); + messageEl.textContent = `Game Over! Final Score: ${score}`; + pauseBtn.style.display = 'none'; + resetBtn.style.display = 'inline-block'; +} + +// Toggle pause +function togglePause() { + gamePaused = !gamePaused; + if (gamePaused) { + pauseBtn.textContent = 'Resume'; + messageEl.textContent = 'Game Paused'; + } else { + pauseBtn.textContent = 'Pause'; + messageEl.textContent = ''; + gameLoop(); // Resume the loop + } +} + +// Reset game +function resetGame() { + gameRunning = false; + cancelAnimationFrame(animationId); + score = 0; + level = 1; + scoreEl.textContent = score; + levelEl.textContent = level; + messageEl.textContent = ''; + resetBtn.style.display = 'none'; + startBtn.style.display = 'inline-block'; + pauseBtn.style.display = 'none'; + + // Clear canvas + ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); +} + +// Handle key presses +function handleKeyPress(event) { + if (event.code === 'Space') { + event.preventDefault(); + if (gameRunning && !gamePaused) { + changeShape(); + } + } +} + +// Change player shape +function changeShape() { + playerShape = (playerShape + 1) % 3; +} + +// Draw the game +function drawGame() { + // Clear canvas + ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + // Draw player + drawShape(50, playerY, playerShape, PLAYER_SIZE, 'white'); + + // Draw obstacles + obstacles.forEach(obstacle => { + // Draw obstacle body + ctx.fillStyle = '#e17055'; + ctx.fillRect(obstacle.x, 0, OBSTACLE_WIDTH, CANVAS_HEIGHT); + + // Draw hole + ctx.clearRect(obstacle.x, obstacle.holeY - 25, OBSTACLE_WIDTH, 50); + // Draw hole border + ctx.strokeStyle = 'white'; + ctx.lineWidth = 2; + ctx.strokeRect(obstacle.x, obstacle.holeY - 25, OBSTACLE_WIDTH, 50); + + // Draw shape indicator in hole + drawShape(obstacle.x + OBSTACLE_WIDTH / 2, obstacle.holeY, obstacle.holeShape, 15, 'rgba(255,255,255,0.7)'); + }); +} + +// Draw a shape at given position +function drawShape(x, y, shapeType, size, color) { + ctx.fillStyle = color; + ctx.strokeStyle = color; + ctx.lineWidth = 2; + + switch (shapeType) { + case 0: // Circle + ctx.beginPath(); + ctx.arc(x, y, size / 2, 0, Math.PI * 2); + ctx.fill(); + break; + case 1: // Square + ctx.fillRect(x - size / 2, y - size / 2, size, size); + break; + case 2: // Triangle + ctx.beginPath(); + ctx.moveTo(x, y - size / 2); + ctx.lineTo(x - size / 2, y + size / 2); + ctx.lineTo(x + size / 2, y + size / 2); + ctx.closePath(); + ctx.fill(); + break; + } +} + +// I added this to make the game more interesting +// Maybe add some particle effects or sounds later +console.log('Shape Shifter game loaded!'); \ No newline at end of file diff --git a/games/shape-shifter/style.css b/games/shape-shifter/style.css new file mode 100644 index 00000000..da3af0a5 --- /dev/null +++ b/games/shape-shifter/style.css @@ -0,0 +1,104 @@ +/* Basic reset and font setup */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #74b9ff, #0984e3); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + color: white; +} + +.container { + text-align: center; + max-width: 900px; + padding: 20px; +} + +h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +p { + font-size: 1.2em; + margin-bottom: 20px; + opacity: 0.9; +} + +.game-info { + display: flex; + justify-content: space-around; + margin: 20px 0; + font-size: 1.3em; + font-weight: bold; +} + +#game-canvas { + border: 3px solid white; + border-radius: 10px; + background: #2d3436; + display: block; + margin: 20px auto; + box-shadow: 0 4px 8px rgba(0,0,0,0.3); +} + +.controls { + margin: 20px 0; +} + +button { + background: #00b894; + color: white; + border: none; + padding: 12px 24px; + font-size: 1em; + border-radius: 5px; + cursor: pointer; + margin: 0 10px; + transition: background 0.3s; +} + +button:hover { + background: #00a085; +} + +#message { + font-size: 1.2em; + margin: 20px 0; + min-height: 30px; + font-weight: bold; +} + +.instructions { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + margin-top: 20px; + text-align: left; + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.instructions h3 { + margin-bottom: 10px; + color: #ffeaa7; +} + +.instructions ul { + list-style-type: disc; + padding-left: 20px; +} + +.instructions li { + margin: 5px 0; + line-height: 1.4; +} \ No newline at end of file diff --git a/games/shape-shooter/index.html b/games/shape-shooter/index.html new file mode 100644 index 00000000..de1e3dad --- /dev/null +++ b/games/shape-shooter/index.html @@ -0,0 +1,23 @@ + + + + + + Shape Shooter Game + + + +
    +

    Shape Shooter

    +

    Sort shapes by color! Click a shape, then click the matching color category.

    +
    +
    Time: 30
    +
    Score: 0
    + +
    + +
    +
    + + + \ No newline at end of file diff --git a/games/shape-shooter/script.js b/games/shape-shooter/script.js new file mode 100644 index 00000000..3d519256 --- /dev/null +++ b/games/shape-shooter/script.js @@ -0,0 +1,203 @@ +// Shape Shooter Game Script +// Sort shapes by color + +var canvas = document.getElementById('gameCanvas'); +var ctx = canvas.getContext('2d'); +var timerDisplay = document.getElementById('timer'); +var scoreDisplay = document.getElementById('score'); +var restartBtn = document.getElementById('restart'); +var messageDiv = document.getElementById('message'); + +var shapes = []; +var categories = []; +var selectedShape = null; +var score = 0; +var timeLeft = 30; +var timerInterval; +var gameRunning = true; + +// Shape class +function Shape(x, y, type, color) { + this.x = x; + this.y = y; + this.type = type; + this.color = color; + this.size = 30; + this.selected = false; +} + +// Category class +function Category(x, y, width, height, color, label) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.color = color; + this.label = label; +} + +// Initialize the game +function initGame() { + shapes = []; + categories = []; + selectedShape = null; + score = 0; + timeLeft = 30; + gameRunning = true; + messageDiv.textContent = ''; + scoreDisplay.textContent = 'Score: ' + score; + + // Create categories + var colors = ['red', 'blue', 'green', 'yellow']; + var colorValues = ['#f44336', '#2196f3', '#4caf50', '#ffeb3b']; + for (var i = 0; i < colors.length; i++) { + var x = 50 + i * 130; + var y = canvas.height - 80; + categories.push(new Category(x, y, 100, 60, colorValues[i], colors[i])); + } + + // Create shapes + var shapeTypes = ['circle', 'square', 'triangle']; + for (var i = 0; i < 10; i++) { + var x = Math.random() * (canvas.width - 100) + 50; + var y = Math.random() * (canvas.height - 200) + 50; + var type = shapeTypes[Math.floor(Math.random() * shapeTypes.length)]; + var colorIndex = Math.floor(Math.random() * colors.length); + var color = colors[colorIndex]; + shapes.push(new Shape(x, y, type, color)); + } + + startTimer(); + draw(); +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw categories + for (var i = 0; i < categories.length; i++) { + var cat = categories[i]; + ctx.fillStyle = cat.color; + ctx.fillRect(cat.x, cat.y, cat.width, cat.height); + ctx.strokeStyle = '#000'; + ctx.strokeRect(cat.x, cat.y, cat.width, cat.height); + ctx.fillStyle = '#000'; + ctx.font = '16px Arial'; + ctx.textAlign = 'center'; + ctx.fillText(cat.label, cat.x + cat.width / 2, cat.y + cat.height / 2 + 5); + } + + // Draw shapes + for (var i = 0; i < shapes.length; i++) { + var shape = shapes[i]; + ctx.fillStyle = shape.color; + if (shape.selected) { + ctx.strokeStyle = '#000'; + ctx.lineWidth = 3; + } else { + ctx.strokeStyle = '#333'; + ctx.lineWidth = 1; + } + + if (shape.type === 'circle') { + ctx.beginPath(); + ctx.arc(shape.x, shape.y, shape.size, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + } else if (shape.type === 'square') { + ctx.fillRect(shape.x - shape.size, shape.y - shape.size, shape.size * 2, shape.size * 2); + ctx.strokeRect(shape.x - shape.size, shape.y - shape.size, shape.size * 2, shape.size * 2); + } else if (shape.type === 'triangle') { + ctx.beginPath(); + ctx.moveTo(shape.x, shape.y - shape.size); + ctx.lineTo(shape.x - shape.size, shape.y + shape.size); + ctx.lineTo(shape.x + shape.size, shape.y + shape.size); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } + } +} + +// Handle canvas click +canvas.addEventListener('click', function(event) { + if (!gameRunning) return; + var rect = canvas.getBoundingClientRect(); + var x = event.clientX - rect.left; + var y = event.clientY - rect.top; + + // Check if clicked on a shape + for (var i = shapes.length - 1; i >= 0; i--) { + var shape = shapes[i]; + var dx = x - shape.x; + var dy = y - shape.y; + var distance = Math.sqrt(dx * dx + dy * dy); + if (distance < shape.size) { + if (selectedShape) { + selectedShape.selected = false; + } + selectedShape = shape; + shape.selected = true; + draw(); + return; + } + } + + // Check if clicked on a category + if (selectedShape) { + for (var i = 0; i < categories.length; i++) { + var cat = categories[i]; + if (x >= cat.x && x <= cat.x + cat.width && y >= cat.y && y <= cat.y + cat.height) { + if (selectedShape.color === cat.label) { + // Correct + score++; + scoreDisplay.textContent = 'Score: ' + score; + messageDiv.textContent = 'Correct!'; + messageDiv.style.color = 'green'; + shapes.splice(shapes.indexOf(selectedShape), 1); + selectedShape = null; + } else { + // Wrong + messageDiv.textContent = 'Wrong!'; + messageDiv.style.color = 'red'; + selectedShape.selected = false; + selectedShape = null; + } + draw(); + setTimeout(function() { + messageDiv.textContent = ''; + }, 1000); + return; + } + } + } + + // Clicked elsewhere, deselect + if (selectedShape) { + selectedShape.selected = false; + selectedShape = null; + draw(); + } +}); + +// Start the timer +function startTimer() { + clearInterval(timerInterval); + timerInterval = setInterval(function() { + timeLeft--; + timerDisplay.textContent = 'Time: ' + timeLeft; + if (timeLeft <= 0) { + clearInterval(timerInterval); + gameRunning = false; + messageDiv.textContent = 'Time\'s up! Final Score: ' + score; + messageDiv.style.color = 'purple'; + } + }, 1000); +} + +// Restart button +restartBtn.addEventListener('click', initGame); + +// Start the game +initGame(); \ No newline at end of file diff --git a/games/shape-shooter/style.css b/games/shape-shooter/style.css new file mode 100644 index 00000000..e29c182b --- /dev/null +++ b/games/shape-shooter/style.css @@ -0,0 +1,53 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #e8eaf6; +} + +.container { + text-align: center; +} + +h1 { + color: #5e35b1; +} + +.game-info { + margin-bottom: 20px; + display: flex; + justify-content: center; + gap: 20px; +} + +#timer, #score { + font-size: 24px; + font-weight: bold; +} + +#restart { + padding: 10px 20px; + font-size: 16px; + background-color: #5e35b1; + color: white; + border: none; + cursor: pointer; +} + +#restart:hover { + background-color: #4527a0; +} + +canvas { + border: 2px solid #5e35b1; + background-color: #ffffff; +} + +#message { + margin-top: 20px; + font-size: 18px; + font-weight: bold; +} \ No newline at end of file diff --git a/games/shape-sorter/index.html b/games/shape-sorter/index.html new file mode 100644 index 00000000..084908a5 --- /dev/null +++ b/games/shape-sorter/index.html @@ -0,0 +1,24 @@ + + + + + + Shape Sorter + + + +

    Shape Sorter

    +

    Sort shapes by their color or type. Click on shapes to move them to the correct category.

    +
    + +
    +

    Red shapes go left, Blue shapes go right.

    +
    +
    +
    Time: 30
    +
    Score: 0
    +
    +
    + + + \ No newline at end of file diff --git a/games/shape-sorter/script.js b/games/shape-sorter/script.js new file mode 100644 index 00000000..ec9cdd2f --- /dev/null +++ b/games/shape-sorter/script.js @@ -0,0 +1,94 @@ +const canvas = document.getElementById('game-canvas'); +const ctx = canvas.getContext('2d'); +const timerDisplay = document.getElementById('timer'); +const scoreDisplay = document.getElementById('score'); + +let shapes = []; +let score = 0; +let time = 30; +let gameRunning = true; + +function createShape() { + const types = ['circle', 'square']; + const colors = ['red', 'blue']; + const type = types[Math.floor(Math.random() * types.length)]; + const color = colors[Math.floor(Math.random() * colors.length)]; + const x = Math.random() * (canvas.width - 100) + 50; + const y = Math.random() * (canvas.height - 100) + 50; + const size = 30; + shapes.push({ type, color, x, y, size }); +} + +function drawShape(shape) { + ctx.fillStyle = shape.color; + if (shape.type === 'circle') { + ctx.beginPath(); + ctx.arc(shape.x, shape.y, shape.size / 2, 0, Math.PI * 2); + ctx.fill(); + } else { + ctx.fillRect(shape.x - shape.size / 2, shape.y - shape.size / 2, shape.size, shape.size); + } +} + +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + // Draw divider + ctx.strokeStyle = 'black'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(canvas.width / 2, 0); + ctx.lineTo(canvas.width / 2, canvas.height); + ctx.stroke(); + ctx.fillStyle = 'black'; + ctx.font = '16px Arial'; + ctx.fillText('Red', canvas.width / 4, 20); + ctx.fillText('Blue', 3 * canvas.width / 4, 20); + shapes.forEach(drawShape); +} + +function updateTimer() { + timerDisplay.textContent = `Time: ${time}`; + if (time <= 0) { + gameRunning = false; + alert(`Time's up! Final score: ${score}`); + } else { + time--; + } +} + +canvas.addEventListener('click', (e) => { + if (!gameRunning) return; + const rect = canvas.getBoundingClientRect(); + const clickX = e.clientX - rect.left; + const clickY = e.clientY - rect.top; + for (let i = shapes.length - 1; i >= 0; i--) { + const shape = shapes[i]; + const dist = Math.sqrt((clickX - shape.x) ** 2 + (clickY - shape.y) ** 2); + if (dist < shape.size / 2) { + // Move to correct side + if (shape.color === 'red') { + shape.x = canvas.width / 4; + } else { + shape.x = 3 * canvas.width / 4; + } + score++; + scoreDisplay.textContent = `Score: ${score}`; + shapes.splice(i, 1); + createShape(); + break; + } + } +}); + +// Initialize +for (let i = 0; i < 5; i++) { + createShape(); +} +draw(); + +setInterval(() => { + if (gameRunning) { + updateTimer(); + draw(); + } +}, 1000); \ No newline at end of file diff --git a/games/shape-sorter/style.css b/games/shape-sorter/style.css new file mode 100644 index 00000000..b626be20 --- /dev/null +++ b/games/shape-sorter/style.css @@ -0,0 +1,47 @@ +body { + font-family: Arial, sans-serif; + text-align: center; + background-color: #f0f0f0; + margin: 0; + padding: 20px; +} + +h1 { + color: #333; +} + +p { + color: #666; + margin-bottom: 20px; +} + +#game-container { + display: inline-block; + border: 2px solid #333; + background-color: white; + padding: 20px; + border-radius: 10px; +} + +#game-canvas { + border: 1px solid #ccc; + background-color: #fafafa; + cursor: pointer; +} + +#instructions { + margin-top: 10px; + font-size: 16px; + color: #333; +} + +#stats { + display: flex; + justify-content: space-around; + margin-top: 10px; +} + +#timer, #score { + font-size: 20px; + color: #333; +} \ No newline at end of file diff --git a/games/shooting-range/index.html b/games/shooting-range/index.html new file mode 100644 index 00000000..e63b38d0 --- /dev/null +++ b/games/shooting-range/index.html @@ -0,0 +1,46 @@ + + + + + + Shooting Range + + + +
    +

    ๐ŸŽฏ Shooting Range

    +

    Hit targets with your shots! Click to fire.

    + +
    +
    Score: 0
    +
    Level: 1
    +
    Ammo: 10
    +
    Time: 30s
    +
    + + + +
    + + + +
    + +
    + +
    +

    How to Play:

    +
      +
    • Click anywhere on the range to shoot
    • +
    • Hit the center of targets for maximum points
    • +
    • Different targets give different scores
    • +
    • Watch out for moving targets!
    • +
    • Reload when you run out of ammo
    • +
    • Survive the time limit to advance levels
    • +
    +
    +
    + + + + \ No newline at end of file diff --git a/games/shooting-range/script.js b/games/shooting-range/script.js new file mode 100644 index 00000000..a79c7ace --- /dev/null +++ b/games/shooting-range/script.js @@ -0,0 +1,261 @@ +// Shooting Range Game +// Click to shoot targets and score points + +// DOM elements +const canvas = document.getElementById('range-canvas'); +const ctx = canvas.getContext('2d'); +const scoreEl = document.getElementById('current-score'); +const levelEl = document.getElementById('current-level'); +const ammoEl = document.getElementById('current-ammo'); +const timerEl = document.getElementById('time-left'); +const messageEl = document.getElementById('message'); +const startBtn = document.getElementById('start-btn'); +const reloadBtn = document.getElementById('reload-btn'); +const resetBtn = document.getElementById('reset-btn'); + +// Game constants +const CANVAS_WIDTH = 800; +const CANVAS_HEIGHT = 500; + +// Game variables +let gameRunning = false; +let score = 0; +let level = 1; +let ammo = 10; +let timeLeft = 30; +let targets = []; +let animationId; +let lastTargetSpawn = 0; + +// Target types +const targetTypes = [ + { name: 'bullseye', size: 40, points: 50, color: '#e74c3c', speed: 0 }, + { name: 'moving', size: 30, points: 75, color: '#f39c12', speed: 2 }, + { name: 'bonus', size: 20, points: 100, color: '#27ae60', speed: 1 } +]; + +// Start the game +function startGame() { + score = 0; + level = 1; + ammo = 10; + timeLeft = 30; + targets = []; + + scoreEl.textContent = score; + levelEl.textContent = level; + ammoEl.textContent = ammo; + timerEl.textContent = timeLeft; + messageEl.textContent = ''; + + gameRunning = true; + startBtn.style.display = 'none'; + reloadBtn.style.display = 'none'; + + gameLoop(); + startTimer(); +} + +// Main game loop +function gameLoop() { + if (!gameRunning) return; + + updateGame(); + drawGame(); + + animationId = requestAnimationFrame(gameLoop); +} + +// Update game state +function updateGame() { + // Spawn new targets + if (Date.now() - lastTargetSpawn > 2000 - level * 100) { + spawnTarget(); + lastTargetSpawn = Date.now(); + } + + // Move targets + targets.forEach(target => { + if (target.type === 'moving') { + target.x += target.speed; + // Bounce off edges + if (target.x <= target.size / 2 || target.x >= CANVAS_WIDTH - target.size / 2) { + target.speed *= -1; + } + } + }); + + // Remove off-screen targets + targets = targets.filter(target => target.x > -50 && target.x < CANVAS_WIDTH + 50); +} + +// Spawn a random target +function spawnTarget() { + const type = targetTypes[Math.floor(Math.random() * targetTypes.length)]; + const y = Math.random() * (CANVAS_HEIGHT - 100) + 50; + let x; + + if (type.name === 'moving') { + x = Math.random() < 0.5 ? -25 : CANVAS_WIDTH + 25; + } else { + x = Math.random() * (CANVAS_WIDTH - 100) + 50; + } + + targets.push({ + x: x, + y: y, + ...type, + lifetime: 0 + }); +} + +// Handle canvas click (shooting) +canvas.addEventListener('click', (event) => { + if (!gameRunning) return; + + if (ammo <= 0) { + messageEl.textContent = 'Out of ammo! Reload needed.'; + reloadBtn.style.display = 'inline-block'; + return; + } + + const rect = canvas.getBoundingClientRect(); + const clickX = event.clientX - rect.left; + const clickY = event.clientY - rect.top; + + ammo--; + ammoEl.textContent = ammo; + + // Check if hit any target + let hit = false; + targets.forEach((target, index) => { + const distance = Math.sqrt((clickX - target.x) ** 2 + (clickY - target.y) ** 2); + if (distance < target.size / 2) { + hitTarget(target, index); + hit = true; + } + }); + + if (!hit) { + // Miss + messageEl.textContent = 'Miss!'; + setTimeout(() => messageEl.textContent = '', 500); + } + + if (ammo <= 0) { + reloadBtn.style.display = 'inline-block'; + } +}); + +// Hit a target +function hitTarget(target, index) { + targets.splice(index, 1); + score += target.points; + scoreEl.textContent = score; + + messageEl.textContent = `Hit! +${target.points} points`; + setTimeout(() => messageEl.textContent = '', 1000); +} + +// Reload ammo +function reloadAmmo() { + if (score >= 50) { + score -= 50; + ammo = 10; + scoreEl.textContent = score; + ammoEl.textContent = ammo; + reloadBtn.style.display = 'none'; + messageEl.textContent = 'Reloaded!'; + setTimeout(() => messageEl.textContent = '', 1000); + } else { + messageEl.textContent = 'Not enough points to reload!'; + setTimeout(() => messageEl.textContent = '', 2000); + } +} + +// Start timer +function startTimer() { + const timerInterval = setInterval(() => { + timeLeft--; + timerEl.textContent = timeLeft; + + if (timeLeft <= 0) { + clearInterval(timerInterval); + levelComplete(); + } + }, 1000); +} + +// Level complete +function levelComplete() { + gameRunning = false; + cancelAnimationFrame(animationId); + level++; + levelEl.textContent = level; + messageEl.textContent = `Time's up! Level ${level} unlocked.`; + setTimeout(() => { + messageEl.textContent = 'Get ready for next level...'; + setTimeout(() => { + startGame(); + }, 2000); + }, 3000); +} + +// Reset game +function resetGame() { + gameRunning = false; + cancelAnimationFrame(animationId); + score = 0; + level = 1; + ammo = 10; + scoreEl.textContent = score; + levelEl.textContent = level; + ammoEl.textContent = ammo; + timerEl.textContent = '30'; + messageEl.textContent = ''; + resetBtn.style.display = 'none'; + startBtn.style.display = 'inline-block'; + reloadBtn.style.display = 'none'; +} + +// Draw the game +function drawGame() { + // Clear canvas + ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + // Draw background + ctx.fillStyle = '#87CEEB'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + // Draw ground + ctx.fillStyle = '#8B4513'; + ctx.fillRect(0, CANVAS_HEIGHT - 50, CANVAS_WIDTH, 50); + + // Draw targets + targets.forEach(target => { + ctx.fillStyle = target.color; + ctx.beginPath(); + ctx.arc(target.x, target.y, target.size / 2, 0, Math.PI * 2); + ctx.fill(); + + // Draw target rings + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(target.x, target.y, target.size / 4, 0, Math.PI * 2); + ctx.stroke(); + + ctx.beginPath(); + ctx.arc(target.x, target.y, target.size / 8, 0, Math.PI * 2); + ctx.stroke(); + }); +} + +// Event listeners +startBtn.addEventListener('click', startGame); +reloadBtn.addEventListener('click', reloadAmmo); +resetBtn.addEventListener('click', resetGame); + +// This shooting game was straightforward but fun +// The click-to-shoot mechanic works well with canvas +// Could add different weapons or target patterns later \ No newline at end of file diff --git a/games/shooting-range/style.css b/games/shooting-range/style.css new file mode 100644 index 00000000..5e2554b5 --- /dev/null +++ b/games/shooting-range/style.css @@ -0,0 +1,142 @@ +/* Shooting Range Game Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #2c3e50, #34495e); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + color: white; + cursor: crosshair; +} + +.container { + text-align: center; + max-width: 900px; + padding: 20px; +} + +h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +p { + font-size: 1.1em; + margin-bottom: 20px; + opacity: 0.9; +} + +.game-stats { + display: flex; + justify-content: space-around; + flex-wrap: wrap; + margin: 20px 0; + font-size: 1.1em; + font-weight: bold; + background: rgba(255, 255, 255, 0.1); + padding: 15px; + border-radius: 10px; +} + +#range-canvas { + border: 3px solid white; + border-radius: 10px; + background: linear-gradient(to bottom, #87CEEB, #4682B4); + display: block; + margin: 20px auto; + box-shadow: 0 4px 8px rgba(0,0,0,0.3); +} + +.controls { + margin: 20px 0; +} + +button { + background: #e74c3c; + color: white; + border: none; + padding: 12px 24px; + font-size: 1em; + border-radius: 5px; + cursor: pointer; + margin: 0 10px; + transition: background 0.3s; +} + +button:hover { + background: #c0392b; +} + +#reload-btn { + background: #f39c12; +} + +#reload-btn:hover { + background: #e67e22; +} + +#message { + font-size: 1.2em; + margin: 20px 0; + min-height: 30px; + font-weight: bold; +} + +.instructions { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + margin-top: 20px; + text-align: left; + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.instructions h3 { + margin-bottom: 10px; + color: #ffeaa7; +} + +.instructions ul { + list-style-type: disc; + padding-left: 20px; +} + +.instructions li { + margin: 5px 0; + line-height: 1.4; +} + +/* Responsive design */ +@media (max-width: 768px) { + #range-canvas { + width: 100%; + max-width: 600px; + height: 400px; + } + + .game-stats { + font-size: 1em; + } + + .controls { + display: flex; + flex-direction: column; + gap: 10px; + } + + button { + width: 100%; + max-width: 200px; + } +} \ No newline at end of file diff --git a/games/shortest_path/index.html b/games/shortest_path/index.html new file mode 100644 index 00000000..b91a3700 --- /dev/null +++ b/games/shortest_path/index.html @@ -0,0 +1,33 @@ + + + + + + The Shortest Path Puzzle + + + + +
    +

    ๐Ÿ—บ๏ธ The Shortest Path

    + +
    + Path Length: 0 | Clicks: 0 +
    + +
    +
    + +
    +

    Click on adjacent empty squares to draw a path from Start (S) to End (E)!

    +
    + +
    + + +
    +
    + + + + \ No newline at end of file diff --git a/games/shortest_path/script.js b/games/shortest_path/script.js new file mode 100644 index 00000000..b38d83e6 --- /dev/null +++ b/games/shortest_path/script.js @@ -0,0 +1,241 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. GAME CONSTANTS & VARIABLES --- + const GRID_SIZE = 10; + const GRID_TOTAL_CELLS = GRID_SIZE * GRID_SIZE; + const gridContainer = document.getElementById('grid-container'); + const pathLengthSpan = document.getElementById('path-length'); + const clickCountSpan = document.getElementById('click-count'); + const feedbackMessage = document.getElementById('feedback-message'); + const resetButton = document.getElementById('reset-button'); + const newPuzzleButton = document.getElementById('new-puzzle-button'); + + let gridState = []; // 1D array representing the state (0: empty, 1: wall, 2: path, 3: start, 4: end) + let startCellIndex = 0; + let endCellIndex = 0; + let currentPath = []; // Array of cell indices making up the player's path + let clickCount = 0; + let gameActive = false; + + // --- 2. UTILITY FUNCTIONS --- + + /** + * Converts a 1D index (0-99) to a 2D coordinate {row, col}. + */ + function getCoords(index) { + return { + row: Math.floor(index / GRID_SIZE), + col: index % GRID_SIZE + }; + } + + /** + * Converts a 2D coordinate {row, col} to a 1D index (0-99). + */ + function getIndex(row, col) { + if (row < 0 || row >= GRID_SIZE || col < 0 || col >= GRID_SIZE) { + return -1; // Out of bounds + } + return row * GRID_SIZE + col; + } + + /** + * Resets the path and re-renders the grid without changing the walls, start, or end. + */ + function resetPath() { + if (!gameActive) return; + + // Clear old path classes from DOM and reset grid state to 0 + currentPath.forEach(index => { + const cell = gridContainer.children[index]; + if (cell && cell.classList.contains('path')) { + cell.classList.remove('path'); + gridState[index] = 0; // Set back to empty + } + }); + + currentPath = []; + clickCount = 0; + pathLengthSpan.textContent = 0; + clickCountSpan.textContent = 0; + feedbackMessage.textContent = 'Path reset. Start clicking from the Start (S) cell!'; + + // Re-add start cell to currentPath and update display + const startCell = gridContainer.children[startCellIndex]; + if (startCell) { + startCell.click(); // Simulate a click on start to begin path + } + } + + // --- 3. GAME LOGIC --- + + /** + * Generates a new random grid layout with walls and sets start/end points. + */ + function generateNewPuzzle() { + // 1. Reset state + gridState = Array(GRID_TOTAL_CELLS).fill(0); // 0 = empty + currentPath = []; + clickCount = 0; + gameActive = true; + gridContainer.innerHTML = ''; // Clear old DOM elements + + // 2. Randomly set Walls (20% chance) + for (let i = 0; i < GRID_TOTAL_CELLS; i++) { + if (Math.random() < 0.20) { // 20% chance of a wall + gridState[i] = 1; // 1 = wall + } + } + + // 3. Randomly set Start (3) and End (4) points (must be unique and not walls) + do { + startCellIndex = Math.floor(Math.random() * GRID_TOTAL_CELLS); + } while (gridState[startCellIndex] !== 0); + gridState[startCellIndex] = 3; + + do { + endCellIndex = Math.floor(Math.random() * GRID_TOTAL_CELLS); + } while (gridState[endCellIndex] !== 0 || endCellIndex === startCellIndex); + gridState[endCellIndex] = 4; + + // 4. Render the grid and initial path + renderGrid(); + + feedbackMessage.textContent = 'New puzzle generated. Click on the Start (S) cell to begin.'; + } + + /** + * Renders the grid based on the gridState array. + */ + function renderGrid() { + gridContainer.innerHTML = ''; + + for (let i = 0; i < GRID_TOTAL_CELLS; i++) { + const cell = document.createElement('div'); + cell.classList.add('grid-cell'); + cell.setAttribute('data-index', i); + cell.addEventListener('click', handleCellClick); + + switch (gridState[i]) { + case 1: // Wall + cell.classList.add('wall'); + break; + case 3: // Start + cell.classList.add('start'); + cell.textContent = 'S'; + break; + case 4: // End + cell.classList.add('end'); + cell.textContent = 'E'; + break; + } + gridContainer.appendChild(cell); + } + } + + /** + * Checks if a click on a new cell is a valid move from the last cell in the path. + */ + function isValidMove(newIndex) { + if (currentPath.length === 0) { + // First click must be on the start cell + return newIndex === startCellIndex; + } + + const lastIndex = currentPath[currentPath.length - 1]; + const lastCoords = getCoords(lastIndex); + const newCoords = getCoords(newIndex); + + // 1. Check if the cell is a wall + if (gridState[newIndex] === 1) { + feedbackMessage.textContent = "Cannot cross a Wall!"; + return false; + } + + // 2. Check adjacency (must be 1 unit away horizontally or vertically) + const rowDiff = Math.abs(lastCoords.row - newCoords.row); + const colDiff = Math.abs(lastCoords.col - newCoords.col); + + // Valid if (rowDiff=1 AND colDiff=0) OR (rowDiff=0 AND colDiff=1) + if ((rowDiff === 1 && colDiff === 0) || (rowDiff === 0 && colDiff === 1)) { + // 3. Check if the cell is already in the path (to prevent overlapping) + if (currentPath.includes(newIndex) && newIndex !== endCellIndex) { + // Allow clicking the cell BEFORE the last one to backtrack + if (currentPath.indexOf(newIndex) === currentPath.length - 2) { + return 'backtrack'; + } + feedbackMessage.textContent = "Cannot cross your own path!"; + return false; + } + return true; + } + + feedbackMessage.textContent = "You can only move to adjacent cells (up, down, left, right)!"; + return false; + } + + /** + * Main handler for all cell clicks. + */ + function handleCellClick(event) { + if (!gameActive) return; + + const cell = event.target; + const newIndex = parseInt(cell.getAttribute('data-index')); + const moveStatus = isValidMove(newIndex); + + if (moveStatus === 'backtrack') { + // Remove the last cell (the one we're clicking *back* to) + const lastCellIndex = currentPath.pop(); + const lastCell = gridContainer.children[lastCellIndex]; + if (lastCell) { + lastCell.classList.remove('path'); + gridState[lastCellIndex] = 0; // Reset state + } + currentPath.pop(); // Remove the second-to-last cell (the one we're standing on now) + + // Re-call the function to place the click correctly + handleCellClick(event); + return; + } + + if (moveStatus === true) { + // Valid forward move + + // If the cell isn't the start or end, mark it as part of the path + if (gridState[newIndex] !== 3 && gridState[newIndex] !== 4) { + cell.classList.add('path'); + gridState[newIndex] = 2; // 2 = path + } + + currentPath.push(newIndex); + clickCount++; + + // Update stats + pathLengthSpan.textContent = currentPath.length - 1; // Subtract 1 because the start cell is included + clickCountSpan.textContent = clickCount; + feedbackMessage.textContent = `Moved to cell (${getCoords(newIndex).row}, ${getCoords(newIndex).col}).`; + + // Check win condition + if (newIndex === endCellIndex) { + feedbackMessage.innerHTML = `๐ŸŽ‰ **SUCCESS!** Path found in ${currentPath.length - 1} steps. Try to beat your score!`; + gameActive = false; + } + + } else if (currentPath.length === 0 && newIndex === startCellIndex) { + // Initial click on 'S' to start the path + currentPath.push(newIndex); + clickCount++; + pathLengthSpan.textContent = 0; + clickCountSpan.textContent = clickCount; + feedbackMessage.textContent = 'Path started. Follow the empty spaces!'; + } + } + + // --- 4. EVENT LISTENERS --- + + resetButton.addEventListener('click', resetPath); + newPuzzleButton.addEventListener('click', generateNewPuzzle); + + // Initial game start + generateNewPuzzle(); +}); \ No newline at end of file diff --git a/games/shortest_path/style.css b/games/shortest_path/style.css new file mode 100644 index 00000000..9080a786 --- /dev/null +++ b/games/shortest_path/style.css @@ -0,0 +1,130 @@ +:root { + --grid-rows: 10; + --grid-cols: 10; + --cell-size: 30px; + --grid-width: calc(var(--grid-cols) * var(--cell-size)); +} + +body { + font-family: 'Consolas', monospace; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #34495e; /* Dark background */ + color: #ecf0f1; +} + +#game-container { + background-color: #2c3e50; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4); + text-align: center; + width: fit-content; + max-width: 90%; +} + +h1 { + color: #f1c40f; + margin-bottom: 20px; +} + +#status-area { + font-size: 1.1em; + font-weight: bold; + margin-bottom: 20px; +} + +/* --- Grid Container --- */ +#grid-container { + width: var(--grid-width); + height: var(--grid-width); + display: grid; + grid-template-columns: repeat(var(--grid-cols), 1fr); + grid-template-rows: repeat(var(--grid-rows), 1fr); + border: 3px solid #7f8c8d; + margin: 0 auto 20px; +} + +/* --- Cell Styling (The core visual) --- */ +.grid-cell { + width: var(--cell-size); + height: var(--cell-size); + border: 1px solid rgba(127, 140, 141, 0.5); /* Faint grid lines */ + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + user-select: none; + transition: background-color 0.1s; +} + +/* Cell Types */ +.start { + background-color: #2ecc71; /* Green */ + color: white; + font-weight: bold; +} + +.end { + background-color: #e74c3c; /* Red */ + color: white; + font-weight: bold; +} + +.wall { + background-color: #34495e; /* Dark obstacle */ + cursor: not-allowed; +} + +.path { + background-color: #3498db; /* Blue path */ +} + +/* Interaction */ +.grid-cell:hover:not(.wall):not(.start):not(.end) { + background-color: #4CAF50; /* Hover to show it's clickable */ +} + +/* --- Controls and Feedback --- */ +#feedback-message { + min-height: 20px; + margin-bottom: 20px; +} + +#controls { + display: flex; + justify-content: center; + gap: 15px; +} + +#controls button { + padding: 10px 20px; + font-size: 1em; + font-weight: bold; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s; +} + +#reset-button { + background-color: #f39c12; /* Orange reset */ + color: white; +} + +#reset-button:hover { + background-color: #e67e22; +} + +#new-puzzle-button { + background-color: #3498db; /* Blue new puzzle */ + color: white; +} + +#new-puzzle-button:hover { + background-color: #2980b9; +} \ No newline at end of file diff --git a/games/simon_game/index.html b/games/simon_game/index.html new file mode 100644 index 00000000..9166a3f5 --- /dev/null +++ b/games/simon_game/index.html @@ -0,0 +1,25 @@ + + + + + + Simon says agame + + + +

    Simon says Game

    +

    Press any key to start

    +
    +
    +
    1
    +
    2
    +
    +
    +
    3
    +
    4
    +
    + +
    + + + \ No newline at end of file diff --git a/games/simon_game/simon.css b/games/simon_game/simon.css new file mode 100644 index 00000000..62a1a6f3 --- /dev/null +++ b/games/simon_game/simon.css @@ -0,0 +1,77 @@ +body { + text-align: center; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + margin: 0; + padding: 2rem; + background: linear-gradient(135deg, #fceabb, #f8b500); + background-size: 400% 400%; + animation: gradientShift 15s ease infinite; +} + +@keyframes gradientShift { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +h1 { + font-size: 3rem; + margin-bottom: 0.5rem; + color: #333; +} + +h2 { + font-size: 1.5rem; + margin-bottom: 2rem; + color: #444; +} + +.btn-container { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 2rem; + max-width: 600px; + margin: 0 auto; +} + +.btn { + height: 150px; + width: 150px; + border-radius: 20%; + border: 8px solid #333; + font-size: 1.5rem; + font-weight: bold; + color: white; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.btn:hover { + transform: scale(1.05); + box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); +} + +.red { + background-color: #d95980; +} + +.green { + background-color: #63aac0; +} + +.yellow { + background-color: #f99b45; +} + +.purple { + background-color: #e27fe2; +} + +.flash { + background-color: white !important; + color: #333; +} \ No newline at end of file diff --git a/games/simon_game/simon.js b/games/simon_game/simon.js new file mode 100644 index 00000000..6ed01493 --- /dev/null +++ b/games/simon_game/simon.js @@ -0,0 +1,69 @@ +let gameSeq=[]; +let userSeq=[]; +let started=false; +let level=0; +let btns=["yellow",'red','purple','green']; +let h2=document.querySelector('h2'); +document.addEventListener("keypress",function(){ + if(started==false){ + console.log("game started"); + started=true; + + levelUp(); + } + +}) +function btnflash(btn){ + btn.classList.add("flash"); + setTimeout(function(){ + btn.classList.remove("flash"); + },700) +} + +function levelUp(){ + userSeq=[]; + level++; +h2.innerText=`level${level}`; +let randcolor=btns[Math.floor(Math.random()*4)]; +let btn=document.querySelector(`.${randcolor}`); +gameSeq.push(randcolor); +console.log(gameSeq); +btnflash(btn); +} + + +function btnpress(){ + btnflash(this); + userSeq.push(this.getAttribute("id")); + console.log(userSeq); + checkAns(userSeq.length-1); +} +function checkAns(idx){ + + if(userSeq[idx]===gameSeq[idx]){ + //last element + if(gameSeq.length==userSeq.length){ + setTimeout(()=>{levelUp()},1000); + } + + } + else{ + h2.innerHTML=`Game over! Score was ${level}
    Press any key to restart`; + document.querySelector("body").style.backgroundColor="red"; + setTimeout(()=>{ + document.querySelector("body").style.backgroundColor="white";},200); + reset(); + } +} + +let allBtns=document.querySelectorAll(".btn"); +for(butns of allBtns){ + butns.addEventListener("click",btnpress); +} + +function reset(){ + gameSeq=[]; + userSeq=[]; + started=false; + level=0; +} \ No newline at end of file diff --git a/games/simple_jumper/index.html b/games/simple_jumper/index.html new file mode 100644 index 00000000..ff517d8d --- /dev/null +++ b/games/simple_jumper/index.html @@ -0,0 +1,12 @@ + + + + + Basic Platformer Engine + + + + + + + \ No newline at end of file diff --git a/games/simple_jumper/script.js b/games/simple_jumper/script.js new file mode 100644 index 00000000..fc312597 --- /dev/null +++ b/games/simple_jumper/script.js @@ -0,0 +1,29 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const CANVAS_WIDTH = canvas.width; +const CANVAS_HEIGHT = canvas.height; + +// --- Physics & Map Constants --- +const TILE_SIZE = 40; +const GRAVITY = 0.5; +const JUMP_VELOCITY = -10; +const PLAYER_SPEED = 5; + +// Level Map: 0=Air, 1=Ground, 2=Hazard +const LEVEL_MAP = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1] +]; + +const MAP_COLS = LEVEL_MAP[0].length; +const MAP_ROWS = LEVEL_MAP.length; +const LEVEL_WIDTH = MAP_COLS * TILE_SIZE; +const LEVEL_HEIGHT = MAP_ROWS * TILE_SIZE; \ No newline at end of file diff --git a/games/simple_jumper/style.css b/games/simple_jumper/style.css new file mode 100644 index 00000000..a71ff2ef --- /dev/null +++ b/games/simple_jumper/style.css @@ -0,0 +1,13 @@ +body { + background-color: #333; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} +#gameCanvas { + border: 2px solid #fff; + background-color: #87ceeb; /* Sky background */ + display: block; +} \ No newline at end of file diff --git a/games/simple_platformer/index.html b/games/simple_platformer/index.html new file mode 100644 index 00000000..958dfa4d --- /dev/null +++ b/games/simple_platformer/index.html @@ -0,0 +1,26 @@ + + + + + + Canvas Runner Game ๐Ÿƒโ€โ™‚๏ธ + + + +
    + +
    +

    RUNNER GAME

    +

    Press **Spacebar** to Jump

    + +
    + +
    + + + + \ No newline at end of file diff --git a/games/simple_platformer/script.js b/games/simple_platformer/script.js new file mode 100644 index 00000000..afb558ef --- /dev/null +++ b/games/simple_platformer/script.js @@ -0,0 +1,225 @@ +// --- 1. Canvas Setup --- +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const startScreen = document.getElementById('startScreen'); +const startButton = document.getElementById('startButton'); +const gameOverScreen = document.getElementById('gameOverScreen'); +const finalScoreDisplay = document.getElementById('finalScoreDisplay'); +const restartButton = document.getElementById('restartButton'); + +// Set Canvas Dimensions (common runner game size) +canvas.width = 900; +canvas.height = 400; + +// --- 2. Game Constants and Variables --- +const GRAVITY = 0.5; +const JUMP_STRENGTH = -10; +const OBSTACLE_SPEED = 4; +const OBSTACLE_SPAWN_INTERVAL = 1500; // milliseconds +const BACKGROUND_SPEED = 1; + +let player; +let obstacles = []; +let gameActive = false; +let score = 0; +let lastObstacleTime = 0; +let animationFrameId; // To control the game loop + +// --- 3. Game Objects (Classes) --- + +// Player Character +class Player { + constructor() { + this.width = 30; + this.height = 50; + this.x = 50; + this.y = canvas.height - this.height - 20; // Start on ground, 20px above actual bottom + this.velocityY = 0; + this.isOnGround = true; + this.color = '#3498db'; // Blue + } + + draw() { + ctx.fillStyle = this.color; + ctx.fillRect(this.x, this.y, this.width, this.height); + } + + update() { + // Apply gravity + this.velocityY += GRAVITY; + this.y += this.velocityY; + + // Ground collision + const groundLevel = canvas.height - this.height - 20; // Same as initial y + if (this.y >= groundLevel) { + this.y = groundLevel; + this.velocityY = 0; + this.isOnGround = true; + } + } + + jump() { + if (this.isOnGround) { + this.velocityY = JUMP_STRENGTH; + this.isOnGround = false; + } + } +} + +// Obstacle +class Obstacle { + constructor(x) { + this.width = 20 + Math.random() * 30; // Random width + this.height = 20 + Math.random() * 40; // Random height + this.x = x; + this.y = canvas.height - this.height - 20; // Position on ground + this.color = '#e74c3c'; // Red + } + + draw() { + ctx.fillStyle = this.color; + ctx.fillRect(this.x, this.y, this.width, this.height); + } + + update() { + this.x -= OBSTACLE_SPEED; + } +} + +// --- 4. Game Setup and Initialization --- + +function initGame() { + // Reset all game state + player = new Player(); + obstacles = []; + score = 0; + lastObstacleTime = 0; + gameActive = false; + + // Hide game over screen, show start screen initially + gameOverScreen.classList.add('hidden'); + startScreen.classList.remove('hidden'); + + // Clear canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = '#87ceeb'; // Draw sky + ctx.fillRect(0, 0, canvas.width, canvas.height); + drawGround(); // Draw ground + player.draw(); // Draw player at start + drawScore(); +} + +// Draw the ground +function drawGround() { + ctx.fillStyle = '#2ecc71'; // Green ground + ctx.fillRect(0, canvas.height - 20, canvas.width, 20); +} + +// Draw current score +function drawScore() { + ctx.fillStyle = 'black'; + ctx.font = '20px "Press Start 2P"'; + ctx.textAlign = 'right'; + ctx.fillText(`Score: ${score}`, canvas.width - 20, 30); +} + +// --- 5. Main Game Loop --- + +function gameLoop(currentTime) { + if (!gameActive) return; + + // Calculate delta time (optional, for frame-rate independent movement) + // let deltaTime = currentTime - lastFrameTime; + // lastFrameTime = currentTime; + + // 1. Clear Canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // 2. Draw Background (e.g., sky and ground) + ctx.fillStyle = '#87ceeb'; // Sky + ctx.fillRect(0, 0, canvas.width, canvas.height); + drawGround(); + + // 3. Update Game Objects + player.update(); + updateObstacles(currentTime); // Pass currentTime for spawn logic + + // 4. Check Collisions + checkCollisions(); + + // 5. Draw Game Objects + player.draw(); + obstacles.forEach(obstacle => obstacle.draw()); + drawScore(); + + // 6. Request next frame + animationFrameId = requestAnimationFrame(gameLoop); + + // 7. Update Score + score++; // Score increases simply by surviving more frames/time +} + +function updateObstacles(currentTime) { + // Spawn new obstacles + if (currentTime - lastObstacleTime > OBSTACLE_SPAWN_INTERVAL) { + obstacles.push(new Obstacle(canvas.width)); + lastObstacleTime = currentTime; + } + + // Update existing obstacles and remove if off-screen + obstacles = obstacles.filter(obstacle => { + obstacle.update(); + return obstacle.x + obstacle.width > 0; // Keep if still on screen + }); +} + +// AABB (Axis-Aligned Bounding Box) collision detection +function collides(obj1, obj2) { + return obj1.x < obj2.x + obj2.width && + obj1.x + obj1.width > obj2.x && + obj1.y < obj2.y + obj2.height && + obj1.y + obj1.height > obj2.y; +} + +function checkCollisions() { + obstacles.forEach(obstacle => { + if (collides(player, obstacle)) { + endGame(); // Player hit an obstacle + } + }); +} + +function endGame() { + gameActive = false; + cancelAnimationFrame(animationFrameId); // Stop the game loop + + // Show game over screen + gameOverScreen.classList.remove('hidden'); + finalScoreDisplay.textContent = `Your final score: ${Math.floor(score / 100)}!`; // Adjust score for display +} + +// --- 6. Event Listeners (Input) --- + +document.addEventListener('keydown', (e) => { + if (e.code === 'Space' && gameActive) { + player.jump(); + } +}); + +startButton.addEventListener('click', () => { + startScreen.classList.add('hidden'); + gameActive = true; + score = 0; // Reset score before starting new game + requestAnimationFrame(gameLoop); // Start the game loop +}); + +restartButton.addEventListener('click', () => { + initGame(); // Reset all game data and UI + startScreen.classList.add('hidden'); // Ensure start screen is hidden + gameActive = true; // Set game to active + requestAnimationFrame(gameLoop); // Start the game loop again +}); + + +// --- 7. Initialization --- +initGame(); \ No newline at end of file diff --git a/games/simple_platformer/style.css b/games/simple_platformer/style.css new file mode 100644 index 00000000..7acb4961 --- /dev/null +++ b/games/simple_platformer/style.css @@ -0,0 +1,93 @@ +body { + font-family: 'Press Start 2P', cursive, Arial, sans-serif; /* Pixel font or fallback */ + background-color: #2c3e50; /* Dark blue background */ + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + overflow: hidden; /* Hide scrollbars */ + color: #ecf0f1; +} + +/* Import a pixel font if desired */ +@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); + +.game-wrapper { + position: relative; + border: 5px solid #3498db; /* Blue border */ + box-shadow: 0 0 25px rgba(52, 152, 219, 0.8); /* Blue glow */ + border-radius: 8px; + overflow: hidden; /* Ensure game elements stay inside */ +} + +#gameCanvas { + background-color: #87ceeb; /* Sky blue */ + display: block; /* Remove extra space below canvas */ +} + +/* --- Overlay Screens (Start/Game Over) --- */ +.overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.85); + color: white; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + z-index: 10; + padding: 20px; + box-sizing: border-box; /* Include padding in width/height */ +} + +.overlay h1 { + font-size: 3.5em; + margin-bottom: 0.8em; + color: #3498db; /* Blue highlight */ + text-shadow: 0 0 10px #3498db; +} + +.overlay p { + margin-bottom: 1.5em; + font-size: 1.2em; + color: #bbb; +} + +.overlay button { + padding: 15px 30px; + font-size: 1.8em; + cursor: pointer; + background-color: #2ecc71; /* Green button */ + color: #333; + border: 2px solid #2ecc71; + border-radius: 8px; + transition: background-color 0.3s, color 0.3s; + font-family: 'Press Start 2P', cursive; +} + +.overlay button:hover { + background-color: #27ae60; + border-color: #27ae60; + color: white; +} + +#gameOverScreen h1 { + color: #e74c3c; /* Red for Game Over */ + text-shadow: 0 0 10px #e74c3c; +} + +#finalScoreDisplay { + font-size: 1.8em; + font-weight: bold; + color: #f1c40f; /* Yellow for score */ +} + +/* Utility class to hide elements */ +.hidden { + display: none; +} \ No newline at end of file diff --git a/games/skyline-sprint/index.html b/games/skyline-sprint/index.html new file mode 100644 index 00000000..52d4ff33 --- /dev/null +++ b/games/skyline-sprint/index.html @@ -0,0 +1,29 @@ + + + + + + Skyline Sprint - Mini JS Games Hub + + + +
    +

    Skyline Sprint

    +
    + +
    +
    +
    + + +
    +

    Tap or press Space to jump. Swipe down or press Down arrow to slide.

    +

    Score: 0

    +

    Stars: 0

    +

    Theme: Classic

    +
    +
    Made for Mini JS Games Hub
    +
    + + + \ No newline at end of file diff --git a/games/skyline-sprint/screenshot.png b/games/skyline-sprint/screenshot.png new file mode 100644 index 00000000..c48703ed --- /dev/null +++ b/games/skyline-sprint/screenshot.png @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/games/skyline-sprint/script.js b/games/skyline-sprint/script.js new file mode 100644 index 00000000..5d851aef --- /dev/null +++ b/games/skyline-sprint/script.js @@ -0,0 +1,305 @@ +// Skyline Sprint Game +const canvas = document.getElementById('game'); +const ctx = canvas.getContext('2d'); +const BASE_W = 800, BASE_H = 400, ASPECT = BASE_H / BASE_W; +let DPR = window.devicePixelRatio || 1; +let W = BASE_W, H = BASE_H; + +let frame = 0; +let gameState = 'menu'; // 'menu' | 'play' | 'paused' | 'over' +let score = 0; +let stars = 0; +let theme = 0; +const themes = [ + {sky: '#87ceeb', buildings: ['#666', '#555', '#777'], ground: '#8B4513'}, + {sky: '#ff69b4', buildings: ['#800080', '#4B0082', '#9370DB'], ground: '#DDA0DD'}, + {sky: '#ffa500', buildings: ['#ff4500', '#ff6347', '#ffd700'], ground: '#daa520'} +]; + +canvas.setAttribute('role', 'application'); +canvas.setAttribute('aria-label', 'Skyline Sprint game canvas'); +canvas.tabIndex = 0; + +function resizeCanvas() { + DPR = window.devicePixelRatio || 1; + const container = canvas.parentElement || document.body; + const maxWidth = Math.min(window.innerWidth - 40, 850); + const cssWidth = Math.min(container.clientWidth - 24 || BASE_W, maxWidth); + const cssHeight = Math.round(cssWidth * ASPECT); + + canvas.style.width = cssWidth + 'px'; + canvas.style.height = cssHeight + 'px'; + + canvas.width = Math.round(cssWidth * DPR); + canvas.height = Math.round(cssHeight * DPR); + ctx.setTransform(DPR, 0, 0, DPR, 0, 0); + + W = cssWidth; + H = cssHeight; +} + +window.addEventListener('resize', resizeCanvas); +resizeCanvas(); + +const player = { + x: 100, y: H - 100, w: 20, h: 40, + vy: 0, gravity: 0.6, jumpPower: -15, + state: 'running', // 'running' | 'jumping' | 'sliding' + slideTime: 0, + draw() { + ctx.fillStyle = '#000'; + if (this.state === 'sliding') { + ctx.fillRect(this.x, this.y + 20, this.w, this.h - 20); // sliding silhouette + } else { + ctx.fillRect(this.x, this.y, this.w, this.h); // standing/running + } + }, + update() { + if (this.state === 'jumping') { + this.vy += this.gravity; + this.y += this.vy; + if (this.y >= H - 100) { + this.y = H - 100; + this.vy = 0; + this.state = 'running'; + } + } + if (this.state === 'sliding') { + this.slideTime--; + if (this.slideTime <= 0) { + this.state = 'running'; + } + } + }, + jump() { + if (this.state === 'running') { + this.state = 'jumping'; + this.vy = this.jumpPower; + } + }, + slide() { + if (this.state === 'running') { + this.state = 'sliding'; + this.slideTime = 30; // frames + } + } +}; + +let bgOffset = 0; +const bgLayers = [ + {speed: 0.5, height: H * 0.8, color: themes[theme].buildings[0]}, + {speed: 1, height: H * 0.6, color: themes[theme].buildings[1]}, + {speed: 1.5, height: H * 0.4, color: themes[theme].buildings[2]} +]; + +let obstacles = []; +let starsList = []; +let speed = 5; + +function reset() { + frame = 0; + score = 0; + stars = 0; + speed = 5; + player.x = 100; + player.y = H - 100; + player.vy = 0; + player.state = 'running'; + bgOffset = 0; + obstacles = []; + starsList = []; + gameState = 'play'; + document.getElementById('score').textContent = 'Score: 0'; + document.getElementById('stars').textContent = 'Stars: 0'; + document.getElementById('theme').textContent = 'Theme: ' + ['Classic', 'Neon', 'Sunset'][theme]; +} + +function spawnObstacle() { + if (Math.random() < 0.01) { + const type = Math.random() < 0.5 ? 'gap' : 'spike'; + obstacles.push({ + x: W + 50, + type, + width: type === 'gap' ? 80 : 30, + height: type === 'gap' ? 0 : 50 + }); + } +} + +function spawnStar() { + if (Math.random() < 0.005) { + starsList.push({ + x: W + 50, + y: H - 150 - Math.random() * 100 + }); + } +} + +function update() { + if (gameState === 'play') { + frame++; + bgOffset += speed * 0.1; + player.update(); + score += Math.floor(speed / 5); + speed += 0.01; + + // Update obstacles + obstacles.forEach(obs => obs.x -= speed); + obstacles = obstacles.filter(obs => obs.x > -50); + + // Update stars + starsList.forEach(star => star.x -= speed); + starsList = starsList.filter(star => star.x > -50); + + spawnObstacle(); + spawnStar(); + + // Check collisions + obstacles.forEach(obs => { + if (player.x + player.w > obs.x && player.x < obs.x + obs.width) { + if (obs.type === 'gap' && player.state !== 'jumping') { + gameState = 'over'; + } else if (obs.type === 'spike' && player.y + player.h > H - obs.height && player.state !== 'sliding') { + gameState = 'over'; + } + } + }); + + // Check star collection + starsList.forEach((star, index) => { + if (player.x + player.w > star.x && player.x < star.x + 20 && + player.y + player.h > star.y && player.y < star.y + 20) { + stars++; + starsList.splice(index, 1); + document.getElementById('stars').textContent = 'Stars: ' + stars; + if (stars % 10 === 0 && theme < themes.length - 1) { + theme++; + document.getElementById('theme').textContent = 'Theme: ' + ['Classic', 'Neon', 'Sunset'][theme]; + } + } + }); + + document.getElementById('score').textContent = 'Score: ' + score; + } +} + +function draw() { + ctx.clearRect(0, 0, W, H); + + // Sky + ctx.fillStyle = themes[theme].sky; + ctx.fillRect(0, 0, W, H); + + // Background layers + bgLayers.forEach(layer => { + ctx.fillStyle = layer.color; + for (let i = -1; i < 3; i++) { + const x = (i * 200 - bgOffset * layer.speed) % (W + 200); + ctx.fillRect(x, H - layer.height, 150, layer.height); + } + }); + + // Ground + ctx.fillStyle = themes[theme].ground; + ctx.fillRect(0, H - 50, W, 50); + + // Obstacles + obstacles.forEach(obs => { + ctx.fillStyle = '#000'; + if (obs.type === 'gap') { + // Draw gap in ground + ctx.clearRect(obs.x, H - 50, obs.width, 50); + } else if (obs.type === 'spike') { + ctx.beginPath(); + ctx.moveTo(obs.x, H - 50); + ctx.lineTo(obs.x + obs.width / 2, H - 50 - obs.height); + ctx.lineTo(obs.x + obs.width, H - 50); + ctx.fill(); + } + }); + + // Stars + ctx.fillStyle = '#ffd700'; + starsList.forEach(star => { + ctx.beginPath(); + ctx.arc(star.x + 10, star.y + 10, 5, 0, Math.PI * 2); + ctx.fill(); + }); + + player.draw(); + + if (gameState === 'menu') { + ctx.fillStyle = 'rgba(0,0,0,0.5)'; + ctx.fillRect(0, 0, W, H); + ctx.fillStyle = '#fff'; + ctx.font = '24px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Tap or press Space to start', W / 2, H / 2); + } + if (gameState === 'over') { + ctx.fillStyle = 'rgba(0,0,0,0.7)'; + ctx.fillRect(20, H / 2 - 60, W - 40, 120); + ctx.fillStyle = '#fff'; + ctx.font = '28px sans-serif'; + ctx.fillText('Game Over', W / 2, H / 2 - 20); + ctx.font = '20px sans-serif'; + ctx.fillText('Score: ' + score, W / 2, H / 2 + 10); + ctx.fillText('Stars: ' + stars, W / 2, H / 2 + 35); + } +} + +function loop() { + update(); + draw(); + requestAnimationFrame(loop); +} + +// Input +let touchStartY = 0; +canvas.addEventListener('touchstart', e => { + e.preventDefault(); + touchStartY = e.touches[0].clientY; + if (gameState === 'menu') reset(); + else if (gameState === 'over') reset(); + else player.jump(); +}); + +canvas.addEventListener('touchend', e => { + e.preventDefault(); + const touchEndY = e.changedTouches[0].clientY; + if (touchEndY > touchStartY + 50) { + player.slide(); + } +}); + +canvas.addEventListener('keydown', e => { + if (e.code === 'Space') { + e.preventDefault(); + if (gameState === 'menu') reset(); + else if (gameState === 'over') reset(); + else player.jump(); + } + if (e.code === 'ArrowDown') { + e.preventDefault(); + player.slide(); + } +}); + +// Buttons +document.getElementById('startBtn').addEventListener('click', () => { + if (gameState === 'menu' || gameState === 'over') reset(); +}); + +document.getElementById('pauseBtn').addEventListener('click', () => { + if (gameState === 'play') { + gameState = 'paused'; + document.getElementById('pauseBtn').setAttribute('aria-pressed', 'true'); + document.getElementById('pauseBtn').textContent = 'Resume'; + } else if (gameState === 'paused') { + gameState = 'play'; + document.getElementById('pauseBtn').setAttribute('aria-pressed', 'false'); + document.getElementById('pauseBtn').textContent = 'Pause'; + } +}); + +loop(); \ No newline at end of file diff --git a/games/skyline-sprint/style.css b/games/skyline-sprint/style.css new file mode 100644 index 00000000..91b39fc5 --- /dev/null +++ b/games/skyline-sprint/style.css @@ -0,0 +1,17 @@ +*{box-sizing:border-box;margin:0;padding:0} +body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;background:#87ceeb;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:10px} +.game-wrap{background:#fff;border-radius:15px;padding:20px;text-align:center;box-shadow:0 10px 30px rgba(0,0,0,0.2);max-width:850px;width:100%} +h1{color:#333;margin-bottom:15px;font-size:2em} +canvas{background:#87ceeb;display:block;margin:0 auto;border-radius:10px;max-width:100%;height:auto} +.info{color:#333;margin-top:15px} +.controls{display:flex;gap:10px;justify-content:center;margin-bottom:10px} +button{padding:8px 16px;border:none;border-radius:8px;background:#007bff;color:#fff;font-size:14px;cursor:pointer;transition:all 0.3s ease} +button:hover{background:#0056b3} +footer{font-size:12px;color:#666;margin-top:15px} +@media (max-width: 600px) { + .game-wrap{padding:15px} + h1{font-size:1.5em} + canvas{width:100%;height:auto} + .info{font-size:14px} + button{padding:6px 12px;font-size:12px} +} \ No newline at end of file diff --git a/games/sliding-puzzle/index.html b/games/sliding-puzzle/index.html new file mode 100644 index 00000000..bd528d4f --- /dev/null +++ b/games/sliding-puzzle/index.html @@ -0,0 +1,31 @@ + + + + + + 15 Sliding Puzzle Game + + + +
    +

    The 15 Puzzle

    + +
    + Moves: 0 + Time: 0s + +
    + +
    +
    + + +
    + + + + \ No newline at end of file diff --git a/games/sliding-puzzle/script.js b/games/sliding-puzzle/script.js new file mode 100644 index 00000000..62caa7dc --- /dev/null +++ b/games/sliding-puzzle/script.js @@ -0,0 +1,271 @@ +// --- Constants and DOM Elements --- +const BOARD_SIZE = 4; +const TOTAL_TILES = BOARD_SIZE * BOARD_SIZE; // 16 tiles +const EMPTY_TILE_VALUE = TOTAL_TILES; // The 16th tile is the empty space + +const boardElement = document.getElementById('puzzle-board'); +const movesDisplay = document.getElementById('moves'); +const timeDisplay = document.getElementById('time'); +const resetButton = document.getElementById('reset-button'); +const playAgainButton = document.getElementById('play-again-button'); +const winMessage = document.getElementById('win-message'); +const finalMovesSpan = document.getElementById('final-moves'); +const finalTimeSpan = document.getElementById('final-time'); + +// Game state variables +let boardState = []; // 1D array representing the 4x4 grid (1 to 16) +let moves = 0; +let timerInterval; +let timeElapsed = 0; +let isGameRunning = false; + +// --- Core Game Logic --- + +/** + * Initializes the board by generating a solvable state and rendering it. + */ +function initializeBoard() { + // 1. Reset Game State + moves = 0; + timeElapsed = 0; + isGameRunning = true; + movesDisplay.textContent = 'Moves: 0'; + timeDisplay.textContent = 'Time: 0s'; + winMessage.classList.add('hidden'); + clearInterval(timerInterval); + + // 2. Generate a random, solvable board state + boardState = generateSolvableBoard(); + + // 3. Render the board to the DOM + renderBoard(); + + // 4. Start the timer + startTimer(); + + // 5. Attach event listeners for arrow keys + document.addEventListener('keydown', handleKeyPress); +} + +/** + * Generates a shuffled 1D array that is guaranteed to be solvable. + * Solvability Logic: + * * For a 4x4 grid (N is even): + * - If the number of inversions is ODD, the puzzle is solvable if the empty space (row counting from bottom) is on an EVEN row. + * - If the number of inversions is EVEN, the puzzle is solvable if the empty space (row counting from bottom) is on an ODD row. + * * Simplified Rule (N even): Solvable if (Inversions + Empty_Row_from_Bottom) is EVEN. + * * Inversion Count: The number of times a larger tile precedes a smaller tile. + */ +function generateSolvableBoard() { + const tiles = Array.from({ length: TOTAL_TILES }, (_, i) => i + 1); // [1, 2, ..., 16] + let shuffled; + let inversions; + let emptyRowFromBottom; + let isSolvable = false; + + while (!isSolvable) { + // 1. Basic shuffle (Fisher-Yates) + shuffled = [...tiles]; + for (let i = shuffled.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; + } + + // 2. Calculate Inversions + inversions = 0; + for (let i = 0; i < TOTAL_TILES - 1; i++) { + if (shuffled[i] === EMPTY_TILE_VALUE) continue; + for (let j = i + 1; j < TOTAL_TILES; j++) { + if (shuffled[j] === EMPTY_TILE_VALUE) continue; + if (shuffled[i] > shuffled[j]) { + inversions++; + } + } + } + + // 3. Find Empty Tile Position + const emptyIndex = shuffled.indexOf(EMPTY_TILE_VALUE); + // Row index from 0 to 3 + const emptyRow = Math.floor(emptyIndex / BOARD_SIZE); + // Row index counting from the bottom (1-indexed: 4, 3, 2, 1) + emptyRowFromBottom = BOARD_SIZE - emptyRow; + + // 4. Solvability Check: (Inversions + Empty_Row_from_Bottom) must be EVEN + isSolvable = (inversions + emptyRowFromBottom) % 2 === 0; + } + + return shuffled; +} + +/** + * Renders the current boardState array to the DOM. + */ +function renderBoard() { + boardElement.innerHTML = ''; // Clear existing tiles + + boardState.forEach((value, index) => { + const tile = document.createElement('div'); + tile.classList.add('tile'); + tile.dataset.value = value; + tile.dataset.index = index; // Store current position + + if (value === EMPTY_TILE_VALUE) { + tile.classList.add('empty-tile'); + tile.textContent = ''; // Empty text + } else { + tile.textContent = value; + tile.addEventListener('click', handleTileClick); + } + + boardElement.appendChild(tile); + }); +} + +/** + * Handles the logic for moving a tile when clicked. + * @param {Event} event - The click event. + */ +function handleTileClick(event) { + if (!isGameRunning) return; + + const clickedTile = event.target; + // Current 1D index of the clicked tile + const tileIndex = parseInt(clickedTile.dataset.index); + + // Find the current 1D index of the empty tile (value 16) + const emptyIndex = boardState.indexOf(EMPTY_TILE_VALUE); + + // Check if the clicked tile is adjacent to the empty space + if (isAdjacent(tileIndex, emptyIndex)) { + swapTiles(tileIndex, emptyIndex); + checkWinCondition(); + } +} + +/** + * Handles tile movement using Arrow Keys. + * @param {Event} event - The keydown event. + */ +function handleKeyPress(event) { + if (!isGameRunning) return; + + const emptyIndex = boardState.indexOf(EMPTY_TILE_VALUE); + let targetIndex = -1; + + switch (event.key) { + case 'ArrowUp': + // The tile *above* the empty space moves *down* into the empty space. + targetIndex = emptyIndex + BOARD_SIZE; + break; + case 'ArrowDown': + // The tile *below* the empty space moves *up* into the empty space. + targetIndex = emptyIndex - BOARD_SIZE; + break; + case 'ArrowLeft': + // The tile to the *left* of the empty space moves *right* into the empty space. + targetIndex = emptyIndex + 1; + break; + case 'ArrowRight': + // The tile to the *right* of the empty space moves *left* into the empty space. + targetIndex = emptyIndex - 1; + break; + default: + return; // Ignore other keys + } + + // Prevent default scrolling behavior for arrow keys + event.preventDefault(); + + if (targetIndex >= 0 && targetIndex < TOTAL_TILES) { + // Must also ensure the move is valid (e.g., preventing wraps from right to left) + if (isAdjacent(targetIndex, emptyIndex)) { + swapTiles(targetIndex, emptyIndex); + checkWinCondition(); + } + } +} + +/** + * Checks if two indices in the 1D array correspond to adjacent positions in the 2D grid. + * @param {number} idx1 - First index. + * @param {number} idx2 - Second index. + * @returns {boolean} True if adjacent. + */ +function isAdjacent(idx1, idx2) { + const row1 = Math.floor(idx1 / BOARD_SIZE); + const col1 = idx1 % BOARD_SIZE; + const row2 = Math.floor(idx2 / BOARD_SIZE); + const col2 = idx2 % BOARD_SIZE; + + // Adjacent if rows are the same and columns differ by 1 (horizontal move) + // OR if columns are the same and rows differ by 1 (vertical move) + const isHorizontal = row1 === row2 && Math.abs(col1 - col2) === 1; + const isVertical = col1 === col2 && Math.abs(row1 - row2) === 1; + + return isHorizontal || isVertical; +} + +/** + * Swaps the values of two tiles in the boardState array and updates the DOM. + * @param {number} tileIndex - Index of the tile being moved. + * @param {number} emptyIndex - Index of the empty space. + */ +function swapTiles(tileIndex, emptyIndex) { + // 1. Update the 1D state array + [boardState[tileIndex], boardState[emptyIndex]] = + [boardState[emptyIndex], boardState[tileIndex]]; + + // 2. Update the DOM elements' positions/values + const tileElement = boardElement.children[tileIndex]; + const emptyElement = boardElement.children[emptyIndex]; + + // Swap the elements in the DOM (this is the cleanest way to move them visually) + boardElement.insertBefore(tileElement, emptyElement); + boardElement.insertBefore(emptyElement, boardElement.children[tileIndex]); + + // Re-assign the data-index attribute to reflect the new position for click handler + tileElement.dataset.index = emptyIndex; + emptyElement.dataset.index = tileIndex; + + // 3. Update moves count + moves++; + movesDisplay.textContent = `Moves: ${moves}`; +} + +/** + * Starts the game timer. + */ +function startTimer() { + timerInterval = setInterval(() => { + timeElapsed++; + timeDisplay.textContent = `Time: ${timeElapsed}s`; + }, 1000); +} + +/** + * Checks if the board is in the solved state (1, 2, 3, ..., 15, 16). + */ +function checkWinCondition() { + const isSolved = boardState.every((value, index) => value === index + 1); + + if (isSolved) { + clearInterval(timerInterval); + isGameRunning = false; + + // Remove keyboard listener + document.removeEventListener('keydown', handleKeyPress); + + // Display win message + finalMovesSpan.textContent = moves; + finalTimeSpan.textContent = timeElapsed; + winMessage.classList.remove('hidden'); + } +} + +// --- Event Listeners --- +resetButton.addEventListener('click', initializeBoard); +playAgainButton.addEventListener('click', initializeBoard); + + +// --- Initial Setup --- +document.addEventListener('DOMContentLoaded', initializeBoard); \ No newline at end of file diff --git a/games/sliding-puzzle/style.css b/games/sliding-puzzle/style.css new file mode 100644 index 00000000..e977418e --- /dev/null +++ b/games/sliding-puzzle/style.css @@ -0,0 +1,131 @@ +:root { + --grid-size: 4; + --tile-size: 80px; + --grid-gap: 5px; + --board-width: calc(var(--grid-size) * var(--tile-size) + (var(--grid-size) - 1) * var(--grid-gap)); + --primary-color: #3f51b5; /* Indigo */ + --secondary-color: #ffc107; /* Amber */ +} + +body { + font-family: 'Arial', sans-serif; + background-color: #e8eaf6; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + user-select: none; +} + +.game-container { + background: white; + padding: 30px; + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); + text-align: center; +} + +h1 { + color: var(--primary-color); + margin-bottom: 20px; +} + +/* --- Stats and Controls --- */ + +.stats { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding: 0 10px; + font-size: 1.1em; + font-weight: bold; +} + +#reset-button, #play-again-button { + padding: 8px 15px; + font-size: 1em; + cursor: pointer; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 6px; + transition: background-color 0.3s; +} + +#reset-button:hover, #play-again-button:hover { + background-color: #303f9f; +} + + +/* --- Puzzle Board --- */ + +.puzzle-board { + width: var(--board-width); + height: var(--board-width); + display: grid; + grid-template-columns: repeat(var(--grid-size), var(--tile-size)); + grid-template-rows: repeat(var(--grid-size), var(--tile-size)); + gap: var(--grid-gap); + background-color: #c5cae9; /* Light board background */ + padding: var(--grid-gap); + border-radius: 8px; + box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1); +} + +.tile { + width: var(--tile-size); + height: var(--tile-size); + background-color: var(--secondary-color); + color: var(--primary-color); + display: flex; + justify-content: center; + align-items: center; + font-size: 2em; + font-weight: 900; + border-radius: 4px; + cursor: pointer; + transition: transform 0.2s ease-in-out, box-shadow 0.2s; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + border: 1px solid #ffeb3b; +} + +.tile:hover { + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); + transform: scale(1.02); +} + +/* Style for the empty space (the 16th tile) */ +.empty-tile { + background-color: transparent; + cursor: default; + box-shadow: none; + border: none; + pointer-events: none; /* Ensure no interaction */ +} + +/* --- Win Message --- */ + +.hidden { + display: none !important; +} + +#win-message { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(255, 255, 255, 0.98); + padding: 50px; + border-radius: 15px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); + z-index: 10; + text-align: center; + border: 5px solid var(--primary-color); +} + +#win-message h2 { + color: var(--primary-color); + margin-top: 0; +} \ No newline at end of file diff --git a/games/sliding_tile/index.html b/games/sliding_tile/index.html new file mode 100644 index 00000000..aa9955bd --- /dev/null +++ b/games/sliding_tile/index.html @@ -0,0 +1,26 @@ + + + + + + Sliding Tile Puzzle + + + + +
    +

    ๐Ÿ–ผ๏ธ Sliding Tile Puzzle

    + +
    +
    + +
    +

    Click a tile next to the empty space to slide it!

    +
    + + +
    + + + + \ No newline at end of file diff --git a/games/sliding_tile/script.js b/games/sliding_tile/script.js new file mode 100644 index 00000000..f7d153c1 --- /dev/null +++ b/games/sliding_tile/script.js @@ -0,0 +1,169 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. GAME CONSTANTS AND VARIABLES --- + const GRID_SIZE = 3; + const PUZZLE_SIZE = GRID_SIZE * GRID_SIZE; // 9 tiles for 3x3 + // !!! IMPORTANT: CHANGE THIS PATH TO YOUR IMAGE FILE !!! + const IMAGE_URL = 'images/puzzle-image.jpg'; + const TILE_LENGTH = 300 / GRID_SIZE; // 100px per tile (based on 300px puzzle width in CSS) + + const gridElement = document.getElementById('puzzle-grid'); + const messageElement = document.getElementById('message'); + const resetButton = document.getElementById('reset-button'); + + let tiles = []; // Array of tile elements in their current on-screen order + let gameActive = false; + + // --- 2. UTILITY FUNCTIONS --- + + /** + * Calculates the CSS background-position to show the correct slice of the full image. + */ + function calculateBackgroundPosition(index) { + const row = Math.floor(index / GRID_SIZE); + const col = index % GRID_SIZE; + // The background image is moved in the opposite direction of the tile's position. + const x = -col * TILE_LENGTH; + const y = -row * TILE_LENGTH; + return `${x}px ${y}px`; + } + + /** + * Simple array shuffle (Fisher-Yates). + */ + function shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + } + + /** + * Finds the index (0-8) of the empty tile in the current 'tiles' array. + */ + function findEmptyTileIndex() { + return tiles.findIndex(tile => tile.classList.contains('empty-tile')); + } + + /** + * Checks if two tiles are adjacent on the grid. + */ + function isAdjacent(tileIndex, emptyIndex) { + const tileRow = Math.floor(tileIndex / GRID_SIZE); + const tileCol = tileIndex % GRID_SIZE; + const emptyRow = Math.floor(emptyIndex / GRID_SIZE); + const emptyCol = emptyIndex % GRID_SIZE; + + const rowDiff = Math.abs(tileRow - emptyRow); + const colDiff = Math.abs(tileCol - emptyCol); + + // Must be exactly one step in ONE direction (horizontal or vertical) + return (rowDiff === 1 && colDiff === 0) || (rowDiff === 0 && colDiff === 1); + } + + // --- 3. GAME LOGIC FUNCTIONS --- + + /** + * Initializes the puzzle by creating tiles, setting background, and shuffling. + */ + function initPuzzle() { + // Clear previous grid + gridElement.innerHTML = ''; + tiles = []; + gameActive = true; + + // 1. Create Tiles + for (let i = 0; i < PUZZLE_SIZE; i++) { + const tileElement = document.createElement('div'); + tileElement.classList.add('tile'); + // data-solved-index stores the tile's permanent, correct position (0-8) + tileElement.setAttribute('data-solved-index', i); + + // Set the unique background slice for this tile + tileElement.style.backgroundImage = `url(${IMAGE_URL})`; + tileElement.style.backgroundPosition = calculateBackgroundPosition(i); + + tiles.push(tileElement); + tileElement.addEventListener('click', handleTileClick); + } + + // 2. Designate the last tile (index 8) as the empty space + tiles[PUZZLE_SIZE - 1].classList.add('empty-tile'); + + // 3. Shuffle + shuffleArray(tiles); + + // 4. Render the initial (shuffled) state + renderTiles(); + + messageElement.textContent = 'Puzzle ready! Slide a tile.'; + } + + /** + * Renders the tile elements onto the grid based on the current order in the 'tiles' array. + */ + function renderTiles() { + tiles.forEach(tile => { + gridElement.appendChild(tile); + }); + } + + /** + * Handles the click event on a puzzle tile. + */ + function handleTileClick(event) { + if (!gameActive) return; + + const clickedTileElement = event.target; + const clickedTileIndex = tiles.indexOf(clickedTileElement); + const emptyTileIndex = findEmptyTileIndex(); + + if (clickedTileIndex !== -1 && isAdjacent(clickedTileIndex, emptyTileIndex)) { + // Logically swap the clicked tile and the empty tile in the array + [tiles[clickedTileIndex], tiles[emptyTileIndex]] = + [tiles[emptyTileIndex], tiles[clickedTileIndex]]; + + // Re-render the grid to reflect the new array order + gridElement.innerHTML = ''; + renderTiles(); + + // Check win condition + if (checkWin()) { + endGame(true); + } + } + } + + /** + * Checks if the current arrangement of tiles matches the solved state. + */ + function checkWin() { + // The puzzle is solved if the data-solved-index of each tile matches its position in the 'tiles' array (i) + for (let i = 0; i < PUZZLE_SIZE; i++) { + const solvedIndex = parseInt(tiles[i].getAttribute('data-solved-index')); + if (solvedIndex !== i) { + return false; + } + } + return true; + } + + /** + * Ends the game and displays the result. + */ + function endGame(win) { + gameActive = false; + if (win) { + messageElement.textContent = '๐ŸŒŸ CONGRATULATIONS! Puzzle Solved!'; + // Reveal the last tile to complete the image + tiles[PUZZLE_SIZE - 1].classList.remove('empty-tile'); + gridElement.classList.add('solved'); + } + // Disable click listeners if needed here, or rely on the `if (!gameActive)` check + } + + // --- 4. EVENT LISTENERS AND INITIALIZATION --- + resetButton.addEventListener('click', initPuzzle); + + // Start the game immediately + initPuzzle(); +}); \ No newline at end of file diff --git a/games/sliding_tile/style.css b/games/sliding_tile/style.css new file mode 100644 index 00000000..4a4b8369 --- /dev/null +++ b/games/sliding_tile/style.css @@ -0,0 +1,90 @@ +:root { + --grid-size: 3; /* Defines the number of rows/columns */ + --puzzle-width: 300px; /* Total width/height of the puzzle container */ + --tile-size: calc(var(--puzzle-width) / var(--grid-size)); +} + +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f4f7f6; + color: #333; +} + +#game-container { + text-align: center; + background-color: white; + padding: 25px; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +h1 { + color: #007bff; + margin-bottom: 20px; +} + +/* --- Puzzle Grid Container --- */ +#puzzle-grid { + width: var(--puzzle-width); + height: var(--puzzle-width); + margin: 0 auto 20px; + border: 3px solid #333; + display: grid; + /* Sets up 3 equal columns */ + grid-template-columns: repeat(var(--grid-size), 1fr); + grid-template-rows: repeat(var(--grid-size), 1fr); +} + +/* --- Tile Styling --- */ +.tile { + width: var(--tile-size); + height: var(--tile-size); + border: 1px solid #aaa; + box-sizing: border-box; + cursor: pointer; + /* Ensures the image fills the entire 300x300 area, ready to be positioned */ + background-size: var(--puzzle-width) var(--puzzle-width); + transition: transform 0.3s ease, box-shadow 0.2s; +} + +.tile:hover:not(.empty-tile) { + box-shadow: inset 0 0 10px rgba(0, 123, 255, 0.5); +} + +/* --- Empty Tile Styling --- */ +.empty-tile { + background-image: none !important; + background-color: #333; /* Dark background for the missing piece */ + cursor: default; +} + +/* --- Status and Controls --- */ +#status-area { + margin-bottom: 20px; + min-height: 20px; +} + +#message { + font-style: italic; + color: #555; +} + +#reset-button { + padding: 10px 20px; + font-size: 1em; + background-color: #28a745; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; +} + +#reset-button:hover { + background-color: #1e7e34; +} \ No newline at end of file diff --git a/games/slot-spinner/index.html b/games/slot-spinner/index.html new file mode 100644 index 00000000..7d0ff766 --- /dev/null +++ b/games/slot-spinner/index.html @@ -0,0 +1,117 @@ + + + + + + Slot Spinner + + + +
    +

    ๐ŸŽฐ Slot Spinner

    +

    Spin the reels and try your luck!

    + +
    +
    Balance: $1000
    +
    Bet: $10
    +
    Last Win: $0
    +
    + +
    +
    +
    +
    ๐Ÿ’
    +
    ๐Ÿ‹
    +
    ๐ŸŠ
    +
    ๐Ÿ‡
    +
    ๐Ÿ””
    +
    โญ
    +
    7๏ธโƒฃ
    +
    +
    +
    ๐Ÿ’
    +
    ๐Ÿ‹
    +
    ๐ŸŠ
    +
    ๐Ÿ‡
    +
    ๐Ÿ””
    +
    โญ
    +
    7๏ธโƒฃ
    +
    +
    +
    ๐Ÿ’
    +
    ๐Ÿ‹
    +
    ๐ŸŠ
    +
    ๐Ÿ‡
    +
    ๐Ÿ””
    +
    โญ
    +
    7๏ธโƒฃ
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    + + $10 + +
    + + +
    + +
    + +
    +

    Paytable:

    +
    + ๐Ÿ’ ๐Ÿ’ ๐Ÿ’ + $50 +
    +
    + ๐Ÿ‹ ๐Ÿ‹ ๐Ÿ‹ + $75 +
    +
    + ๐ŸŠ ๐ŸŠ ๐ŸŠ + $100 +
    +
    + ๐Ÿ‡ ๐Ÿ‡ ๐Ÿ‡ + $150 +
    +
    + ๐Ÿ”” ๐Ÿ”” ๐Ÿ”” + $200 +
    +
    + โญ โญ โญ + $300 +
    +
    + 7๏ธโƒฃ 7๏ธโƒฃ 7๏ธโƒฃ + $1000 +
    +
    + +
    +

    How to Play:

    +
      +
    • Set your bet amount using the + and - buttons
    • +
    • Click SPIN to start the reels
    • +
    • Match 3 symbols on the center line to win
    • +
    • Different symbols pay different amounts
    • +
    • Three 7's is the jackpot!
    • +
    • Start with $1000 and try to build your fortune
    • +
    +
    +
    + + + + \ No newline at end of file diff --git a/games/slot-spinner/script.js b/games/slot-spinner/script.js new file mode 100644 index 00000000..c7289c31 --- /dev/null +++ b/games/slot-spinner/script.js @@ -0,0 +1,230 @@ +// Slot Spinner Game +// Classic slot machine with spinning reels and payouts + +// DOM elements +const balanceEl = document.getElementById('current-balance'); +const betEl = document.getElementById('current-bet'); +const winEl = document.getElementById('current-win'); +const betAmountEl = document.getElementById('bet-amount'); +const messageEl = document.getElementById('message'); +const spinBtn = document.getElementById('spin-btn'); +const maxBetBtn = document.getElementById('max-bet-btn'); +const decreaseBetBtn = document.getElementById('decrease-bet'); +const increaseBetBtn = document.getElementById('increase-bet'); + +const reel1 = document.getElementById('reel1'); +const reel2 = document.getElementById('reel2'); +const reel3 = document.getElementById('reel3'); + +// Game constants +const SYMBOLS = ['๐Ÿ’', '๐Ÿ‹', '๐ŸŠ', '๐Ÿ‡', '๐Ÿ””', 'โญ', '7๏ธโƒฃ']; +const PAYOUTS = { + '๐Ÿ’': 50, + '๐Ÿ‹': 75, + '๐ŸŠ': 100, + '๐Ÿ‡': 150, + '๐Ÿ””': 200, + 'โญ': 300, + '7๏ธโƒฃ': 1000 +}; + +// Game variables +let balance = 1000; +let currentBet = 10; +let lastWin = 0; +let isSpinning = false; + +// Initialize game +function initGame() { + updateDisplay(); +} + +// Update display elements +function updateDisplay() { + balanceEl.textContent = balance.toLocaleString(); + betEl.textContent = currentBet.toLocaleString(); + winEl.textContent = lastWin.toLocaleString(); + betAmountEl.textContent = '$' + currentBet.toLocaleString(); +} + +// Adjust bet amount +function adjustBet(amount) { + if (isSpinning) return; + + currentBet += amount; + + // Ensure bet is within valid range + if (currentBet < 1) currentBet = 1; + if (currentBet > balance) currentBet = balance; + if (currentBet > 100) currentBet = 100; // Max bet limit + + updateDisplay(); +} + +// Set maximum bet +function setMaxBet() { + if (isSpinning) return; + + currentBet = Math.min(balance, 100); + updateDisplay(); +} + +// Spin the reels +function spin() { + if (isSpinning || balance < currentBet) { + if (balance < currentBet) { + messageEl.textContent = 'Not enough balance!'; + setTimeout(() => messageEl.textContent = '', 2000); + } + return; + } + + isSpinning = true; + spinBtn.textContent = 'SPINNING...'; + spinBtn.classList.add('spinning'); + messageEl.textContent = 'Good luck!'; + + // Deduct bet from balance + balance -= currentBet; + updateDisplay(); + + // Start spinning animation + startSpinAnimation(); + + // Stop reels at different times for realistic effect + setTimeout(() => stopReel(reel1, 0), 1000 + Math.random() * 500); + setTimeout(() => stopReel(reel2, 1), 1500 + Math.random() * 500); + setTimeout(() => stopReel(reel3, 2), 2000 + Math.random() * 500); +} + +// Start spin animation +function startSpinAnimation() { + [reel1, reel2, reel3].forEach(reel => { + reel.classList.add('spinning'); + }); +} + +// Stop a specific reel +function stopReel(reel, index) { + reel.classList.remove('spinning'); + + // Generate random symbol + const randomIndex = Math.floor(Math.random() * SYMBOLS.length); + const symbol = SYMBOLS[randomIndex]; + + // Update reel display + updateReelDisplay(reel, randomIndex); + + // Store result for win checking + reel.result = symbol; + + // Check for win when all reels stopped + if (index === 2) { + setTimeout(() => checkWin(), 500); + } +} + +// Update reel visual display +function updateReelDisplay(reel, targetIndex) { + const symbols = reel.querySelectorAll('.symbol'); + + // Calculate offset to show target symbol in center + const offset = -50 * (targetIndex - 1); // -50px per symbol, center is at index 1 + + symbols.forEach((symbol, index) => { + const position = (index - 1) * 50 + offset; + symbol.style.top = position + 'px'; + }); +} + +// Check for winning combinations +function checkWin() { + const result1 = reel1.result; + const result2 = reel2.result; + const result3 = reel3.result; + + let winAmount = 0; + let winMessage = ''; + + // Check for three of a kind + if (result1 === result2 && result2 === result3) { + winAmount = PAYOUTS[result1] * currentBet / 10; // Scale payout based on bet + winMessage = `JACKPOT! Three ${result1} - $${winAmount.toLocaleString()}!`; + } + // Check for two of a kind (partial wins) + else if (result1 === result2 || result2 === result3 || result1 === result3) { + const matchingSymbol = result1 === result2 ? result1 : (result2 === result3 ? result2 : result1); + winAmount = Math.floor(PAYOUTS[matchingSymbol] * 0.3 * currentBet / 10); // 30% of three-match payout + winMessage = `Nice! Two ${matchingSymbol} - $${winAmount.toLocaleString()}`; + } + else { + winMessage = 'No win this time. Try again!'; + } + + // Award winnings + if (winAmount > 0) { + balance += winAmount; + lastWin = winAmount; + } else { + lastWin = 0; + } + + // Update display + updateDisplay(); + + // Show result message + messageEl.textContent = winMessage; + + // Reset spin state + isSpinning = false; + spinBtn.textContent = 'SPIN!'; + spinBtn.classList.remove('spinning'); + + // Check for game over + if (balance <= 0) { + messageEl.textContent = 'Game Over! You ran out of money.'; + spinBtn.disabled = true; + setTimeout(() => { + resetGame(); + }, 3000); + } +} + +// Reset the game +function resetGame() { + balance = 1000; + currentBet = 10; + lastWin = 0; + updateDisplay(); + messageEl.textContent = 'New game started! Good luck!'; + spinBtn.disabled = false; +} + +// Event listeners +spinBtn.addEventListener('click', spin); +maxBetBtn.addEventListener('click', setMaxBet); +decreaseBetBtn.addEventListener('click', () => adjustBet(-1)); +increaseBetBtn.addEventListener('click', () => adjustBet(1)); + +// Keyboard controls +document.addEventListener('keydown', (event) => { + if (event.code === 'Space' && !isSpinning) { + event.preventDefault(); + spin(); + } + if (event.code === 'ArrowUp') { + event.preventDefault(); + adjustBet(1); + } + if (event.code === 'ArrowDown') { + event.preventDefault(); + adjustBet(-1); + } +}); + +// Initialize the game +initGame(); + +// This slot machine has realistic spinning animations +// The payout system is balanced for fun gameplay +// Could add more paylines or bonus features later \ No newline at end of file diff --git a/games/slot-spinner/style.css b/games/slot-spinner/style.css new file mode 100644 index 00000000..89731f4b --- /dev/null +++ b/games/slot-spinner/style.css @@ -0,0 +1,345 @@ +/* Slot Spinner Game Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #2c3e50, #34495e); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + color: white; +} + +.container { + text-align: center; + max-width: 800px; + padding: 20px; +} + +h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); + color: #ffd700; +} + +p { + font-size: 1.1em; + margin-bottom: 20px; + opacity: 0.9; +} + +.game-stats { + display: flex; + justify-content: space-around; + flex-wrap: wrap; + margin: 20px 0; + font-size: 1.1em; + font-weight: bold; + background: rgba(255, 215, 0, 0.1); + padding: 15px; + border-radius: 10px; + border: 2px solid #ffd700; +} + +.slot-machine { + position: relative; + background: linear-gradient(145deg, #1a1a1a, #2d2d2d); + border: 5px solid #ffd700; + border-radius: 20px; + padding: 30px; + margin: 20px auto; + box-shadow: 0 10px 30px rgba(0,0,0,0.5); + max-width: 600px; +} + +.reels { + display: flex; + justify-content: center; + gap: 20px; + margin-bottom: 20px; +} + +.reel { + width: 100px; + height: 150px; + background: #000; + border: 3px solid #333; + border-radius: 10px; + overflow: hidden; + position: relative; + box-shadow: inset 0 0 20px rgba(0,0,0,0.8); +} + +.reel.spinning { + animation: spin 2s ease-out; +} + +.symbol { + height: 50px; + display: flex; + align-items: center; + justify-content: center; + font-size: 2.5em; + background: linear-gradient(45deg, #fff, #f0f0f0); + border-bottom: 1px solid #ccc; + position: absolute; + width: 100%; + top: 50px; +} + +.reel .symbol:nth-child(1) { top: -50px; } +.reel .symbol:nth-child(2) { top: 0px; } +.reel .symbol:nth-child(3) { top: 50px; } +.reel .symbol:nth-child(4) { top: 100px; } +.reel .symbol:nth-child(5) { top: 150px; } +.reel .symbol:nth-child(6) { top: 200px; } +.reel .symbol:nth-child(7) { top: 250px; } + +.paylines { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; +} + +.payline { + position: absolute; + left: 30px; + right: 30px; + height: 2px; + background: rgba(255, 215, 0, 0.8); + box-shadow: 0 0 10px #ffd700; +} + +.payline-center { + top: 75px; +} + +.payline-top { + top: 25px; + opacity: 0.6; +} + +.payline-bottom { + top: 125px; + opacity: 0.6; +} + +.controls { + margin: 20px 0; +} + +.bet-controls { + display: flex; + align-items: center; + justify-content: center; + gap: 15px; + margin-bottom: 15px; + font-size: 1.2em; + font-weight: bold; +} + +button { + background: #e74c3c; + color: white; + border: none; + padding: 12px 24px; + font-size: 1em; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s; + font-weight: bold; +} + +button:hover { + background: #c0392b; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.3); +} + +button:active { + transform: translateY(0); +} + +button:disabled { + background: #666; + cursor: not-allowed; + transform: none; +} + +#spin-btn { + background: #27ae60; + font-size: 1.2em; + padding: 15px 30px; + animation: pulse 2s infinite; +} + +#spin-btn:hover { + background: #229954; +} + +#spin-btn.spinning { + animation: none; + background: #666; +} + +#decrease-bet, #increase-bet { + background: #3498db; + width: 40px; + height: 40px; + border-radius: 50%; + font-size: 1.5em; + display: flex; + align-items: center; + justify-content: center; +} + +#decrease-bet:hover, #increase-bet:hover { + background: #2980b9; +} + +#max-bet-btn { + background: #f39c12; +} + +#max-bet-btn:hover { + background: #e67e22; +} + +#message { + font-size: 1.2em; + margin: 20px 0; + min-height: 30px; + font-weight: bold; + color: #ffd700; +} + +.paytable { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + margin: 20px 0; + text-align: left; + max-width: 400px; + margin-left: auto; + margin-right: auto; +} + +.paytable h3 { + margin-bottom: 10px; + color: #ffd700; + text-align: center; +} + +.paytable-row { + display: flex; + justify-content: space-between; + padding: 5px 0; + border-bottom: 1px solid rgba(255,255,255,0.2); +} + +.paytable-row:last-child { + border-bottom: none; +} + +.instructions { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + margin-top: 20px; + text-align: left; + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.instructions h3 { + margin-bottom: 10px; + color: #ffd700; +} + +.instructions ul { + list-style-type: disc; + padding-left: 20px; +} + +.instructions li { + margin: 5px 0; + line-height: 1.4; +} + +/* Animations */ +@keyframes spin { + 0% { transform: translateY(0); } + 100% { transform: translateY(-200px); } +} + +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +/* Responsive design */ +@media (max-width: 768px) { + .slot-machine { + padding: 20px; + margin: 10px; + } + + .reels { + gap: 10px; + } + + .reel { + width: 80px; + height: 120px; + } + + .symbol { + font-size: 2em; + height: 40px; + } + + .reel .symbol:nth-child(1) { top: -40px; } + .reel .symbol:nth-child(2) { top: 0px; } + .reel .symbol:nth-child(3) { top: 40px; } + .reel .symbol:nth-child(4) { top: 80px; } + .reel .symbol:nth-child(5) { top: 120px; } + .reel .symbol:nth-child(6) { top: 160px; } + .reel .symbol:nth-child(7) { top: 200px; } + + .payline-center { + top: 60px; + } + + .payline-top { + top: 20px; + } + + .payline-bottom { + top: 100px; + } + + .bet-controls { + font-size: 1em; + } + + button { + padding: 10px 20px; + font-size: 0.9em; + } + + #spin-btn { + padding: 12px 25px; + } +} \ No newline at end of file diff --git a/games/snake-clone/index.html b/games/snake-clone/index.html new file mode 100644 index 00000000..a338f158 --- /dev/null +++ b/games/snake-clone/index.html @@ -0,0 +1,19 @@ + + + + + + Snake Clone + + + +
    +

    Snake Clone

    + +
    Score: 0
    + +
    + + + + \ No newline at end of file diff --git a/games/snake-clone/script.js b/games/snake-clone/script.js new file mode 100644 index 00000000..99a8be92 --- /dev/null +++ b/games/snake-clone/script.js @@ -0,0 +1,218 @@ +// --- Game Setup and Constants --- +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const scoreElement = document.getElementById('score'); +const restartButton = document.getElementById('restartButton'); + +const GRID_SIZE = 20; // Size of each snake segment and food item +const CANVAS_WIDTH = canvas.width; +const CANVAS_HEIGHT = canvas.height; + +let snake; +let food; +let direction; // 'up', 'down', 'left', 'right' +let score; +let gameInterval; +let gameSpeed = 150; // Milliseconds per frame, lower is faster +let changingDirection = false; // To prevent rapid direction changes in one frame + +// --- Initialization Function --- +function initGame() { + snake = [ + { x: 10 * GRID_SIZE, y: 10 * GRID_SIZE } // Starting head position + ]; + direction = 'right'; + score = 0; + scoreElement.textContent = `Score: ${score}`; + restartButton.style.display = 'none'; + + generateFood(); + if (gameInterval) clearInterval(gameInterval); // Clear any existing interval + gameInterval = setInterval(gameLoop, gameSpeed); + changingDirection = false; // Reset for new game + draw(); // Initial draw +} + +// --- Game Logic Functions --- + +/** + * Generates a new food item at a random, unoccupied grid position. + */ +function generateFood() { + let newFoodX, newFoodY; + let collisionWithSnake; + + do { + newFoodX = Math.floor(Math.random() * (CANVAS_WIDTH / GRID_SIZE)) * GRID_SIZE; + newFoodY = Math.floor(Math.random() * (CANVAS_HEIGHT / GRID_SIZE)) * GRID_SIZE; + + collisionWithSnake = snake.some(segment => segment.x === newFoodX && segment.y === newFoodY); + } while (collisionWithSnake); + + food = { x: newFoodX, y: newFoodY }; +} + +/** + * Updates the snake's position, checks for collisions, and handles food consumption. + */ +function updateGame() { + // Create new head based on current direction + const head = { x: snake[0].x, y: snake[0].y }; + + switch (direction) { + case 'up': + head.y -= GRID_SIZE; + break; + case 'down': + head.y += GRID_SIZE; + break; + case 'left': + head.x -= GRID_SIZE; + break; + case 'right': + head.x += GRID_SIZE; + break; + } + + // Add new head to the front of the snake + snake.unshift(head); + + // Check for collisions + if (checkCollision(head)) { + gameOver(); + return; + } + + // Check if food was eaten + if (head.x === food.x && head.y === food.y) { + score++; + scoreElement.textContent = `Score: ${score}`; + generateFood(); // Generate new food + // Snake grows: don't remove tail in this step + } else { + snake.pop(); // Remove tail segment if no food was eaten + } + + changingDirection = false; // Allow direction change for next frame +} + +/** + * Checks if the snake's head has collided with boundaries or its own body. + * @param {object} head - The current head coordinates of the snake. + * @returns {boolean} True if a collision occurred, false otherwise. + */ +function checkCollision(head) { + // Collision with walls + const hitLeftWall = head.x < 0; + const hitRightWall = head.x >= CANVAS_WIDTH; + const hitTopWall = head.y < 0; + const hitBottomWall = head.y >= CANVAS_HEIGHT; + + if (hitLeftWall || hitRightWall || hitTopWall || hitBottomWall) { + return true; + } + + // Collision with self (start checking from the 4th segment to avoid immediate self-collision) + for (let i = 4; i < snake.length; i++) { + if (head.x === snake[i].x && head.y === snake[i].y) { + return true; + } + } + + return false; +} + +/** + * Handles game over state, stops the game loop, and shows restart button. + */ +function gameOver() { + clearInterval(gameInterval); // Stop the game loop + alert(`Game Over! Your Score: ${score}`); + restartButton.style.display = 'block'; +} + +// --- Rendering Functions --- + +/** + * Clears the canvas. + */ +function clearCanvas() { + ctx.fillStyle = 'black'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); +} + +/** + * Draws a single segment of the snake. + * @param {object} segment - An object with x and y coordinates. + */ +function drawSnakeSegment(segment, color = '#61afef') { // Blue color for snake + ctx.fillStyle = color; + ctx.strokeStyle = '#282c34'; // Darker border for segments + ctx.fillRect(segment.x, segment.y, GRID_SIZE, GRID_SIZE); + ctx.strokeRect(segment.x, segment.y, GRID_SIZE, GRID_SIZE); +} + +/** + * Draws the food item. + */ +function drawFood() { + ctx.fillStyle = '#e06c75'; // Red color for food + ctx.strokeStyle = '#282c34'; + ctx.fillRect(food.x, food.y, GRID_SIZE, GRID_SIZE); + ctx.strokeRect(food.x, food.y, GRID_SIZE, GRID_SIZE); +} + +/** + * Main draw function, called every frame. + */ +function draw() { + clearCanvas(); + drawFood(); + // Draw each snake segment + snake.forEach((segment, index) => { + // You could vary color for head vs body if desired + drawSnakeSegment(segment, index === 0 ? '#98c379' : '#61afef'); // Green head, blue body + }); +} + +// --- Main Game Loop --- +function gameLoop() { + updateGame(); + draw(); +} + +// --- Event Listeners --- + +/** + * Handles arrow key presses to change the snake's direction. + */ +function changeDirection(event) { + if (changingDirection) return; // Prevent multiple direction changes per game tick + + const keyPressed = event.key; + const goingUp = direction === 'up'; + const goingDown = direction === 'down'; + const goingLeft = direction === 'left'; + const goingRight = direction === 'right'; + + // Prevent immediate reverse direction + if (keyPressed === 'ArrowLeft' && !goingRight) { + direction = 'left'; + changingDirection = true; + } else if (keyPressed === 'ArrowRight' && !goingLeft) { + direction = 'right'; + changingDirection = true; + } else if (keyPressed === 'ArrowUp' && !goingDown) { + direction = 'up'; + changingDirection = true; + } else if (keyPressed === 'ArrowDown' && !goingUp) { + direction = 'down'; + changingDirection = true; + } +} + +document.addEventListener('keydown', changeDirection); +restartButton.addEventListener('click', initGame); + +// --- Start the Game --- +initGame(); \ No newline at end of file diff --git a/games/snake-clone/style.css b/games/snake-clone/style.css new file mode 100644 index 00000000..3efc9340 --- /dev/null +++ b/games/snake-clone/style.css @@ -0,0 +1,53 @@ +body { + background-color: #282c34; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + font-family: 'Arial', sans-serif; + color: #abb2bf; + overflow: hidden; /* Prevent scrollbars */ +} + +#game-container { + text-align: center; + background-color: #3e4451; + padding: 20px; + border-radius: 8px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5); +} + +h1 { + color: #61afef; + margin-bottom: 20px; +} + +#gameCanvas { + background-color: #000; + border: 2px solid #56b6c2; + display: block; /* Remove extra space below canvas */ + margin: 0 auto; +} + +#score { + margin-top: 15px; + font-size: 1.8em; + color: #98c379; +} + +#restartButton { + background-color: #e06c75; + color: white; + border: none; + padding: 10px 20px; + margin-top: 20px; + font-size: 1.2em; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +#restartButton:hover { + background-color: #be504f; +} \ No newline at end of file diff --git a/games/snake_game/index.html b/games/snake_game/index.html new file mode 100644 index 00000000..45c76047 --- /dev/null +++ b/games/snake_game/index.html @@ -0,0 +1,259 @@ + + + + + + Classic Snake Game + + + + + + +
    +

    Classic Snake

    + +
    + Score: 0 | Speed: 150ms +
    + +
    + +
    + +

    + +
    + + + + diff --git a/games/sneak-stop/index.html b/games/sneak-stop/index.html new file mode 100644 index 00000000..f6bdd27b --- /dev/null +++ b/games/sneak-stop/index.html @@ -0,0 +1,43 @@ + + + + + + Sneak Stop | Mini JS Games Hub + + + +
    +

    ๐Ÿ•ต๏ธโ€โ™‚๏ธ Sneak Stop

    + +
    +
    + Guard +
    +
    + +
    +
    +
    ๐Ÿ
    +
    +
    + +
    + + + +
    + +
    +

    Wait for Green Light...

    +
    +
    + + + + + + + + + diff --git a/games/sneak-stop/script.js b/games/sneak-stop/script.js new file mode 100644 index 00000000..b0a8a609 --- /dev/null +++ b/games/sneak-stop/script.js @@ -0,0 +1,88 @@ +const player = document.getElementById("player"); +const guard = document.getElementById("guard"); +const light = document.getElementById("light"); +const statusText = document.getElementById("statusText"); +const bgMusic = document.getElementById("bg-music"); +const alertSound = document.getElementById("alert-sound"); +const winSound = document.getElementById("win-sound"); +const loseSound = document.getElementById("lose-sound"); + +const pauseBtn = document.getElementById("pauseBtn"); +const resumeBtn = document.getElementById("resumeBtn"); +const restartBtn = document.getElementById("restartBtn"); + +let gameRunning = true; +let greenLight = true; +let position = 0; + +bgMusic.volume = 0.4; +bgMusic.play(); + +function toggleLight() { + if (!gameRunning) return; + greenLight = !greenLight; + + if (greenLight) { + light.className = "light green"; + guard.style.transform = "rotateY(0deg)"; + statusText.textContent = "๐ŸŸข Move!"; + } else { + light.className = "light red"; + guard.style.transform = "rotateY(180deg)"; + statusText.textContent = "๐Ÿ”ด Stop!"; + alertSound.play(); + } + + const nextChange = Math.random() * 3000 + 2000; + setTimeout(toggleLight, nextChange); +} + +function movePlayer() { + if (!gameRunning || !greenLight) { + if (!greenLight) loseGame(); + return; + } + + position += 4; + player.style.left = `${position}px`; + + if (position >= 610) winGame(); +} + +function winGame() { + gameRunning = false; + statusText.textContent = "๐ŸŽ‰ You Reached the Goal!"; + winSound.play(); +} + +function loseGame() { + gameRunning = false; + statusText.textContent = "๐Ÿ’€ Caught! You Moved on Red."; + loseSound.play(); +} + +let moveInterval = setInterval(() => { + if (gameRunning && greenLight) movePlayer(); +}, 200); + +toggleLight(); + +pauseBtn.addEventListener("click", () => { + gameRunning = false; + bgMusic.pause(); + statusText.textContent = "โธ๏ธ Paused"; + pauseBtn.disabled = true; + resumeBtn.disabled = false; +}); + +resumeBtn.addEventListener("click", () => { + gameRunning = true; + bgMusic.play(); + statusText.textContent = greenLight ? "๐ŸŸข Move!" : "๐Ÿ”ด Stop!"; + pauseBtn.disabled = false; + resumeBtn.disabled = true; +}); + +restartBtn.addEventListener("click", () => { + location.reload(); +}); diff --git a/games/sneak-stop/style.css b/games/sneak-stop/style.css new file mode 100644 index 00000000..42d871d0 --- /dev/null +++ b/games/sneak-stop/style.css @@ -0,0 +1,114 @@ +body { + background: radial-gradient(circle at top, #0f0f0f, #000); + color: white; + font-family: 'Poppins', sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + overflow: hidden; + margin: 0; +} + +.title { + text-align: center; + text-shadow: 0 0 15px #0ff; + animation: glow 2s infinite alternate; +} + +@keyframes glow { + from { text-shadow: 0 0 10px #0ff; } + to { text-shadow: 0 0 25px #0ff, 0 0 40px #00f; } +} + +.game-container { + text-align: center; +} + +.game-area { + position: relative; + width: 800px; + height: 300px; + background: linear-gradient(to bottom, #1b1b1b, #2b2b2b); + border: 3px solid #00ffff; + border-radius: 15px; + overflow: hidden; + margin-top: 20px; + box-shadow: 0 0 30px #0ff; +} + +.guard-area { + position: absolute; + top: 10px; + left: 20px; +} + +#guard { + width: 100px; + transition: transform 0.5s; +} + +.light { + width: 25px; + height: 25px; + border-radius: 50%; + margin-top: 10px; + box-shadow: 0 0 15px; +} + +.green { background: lime; box-shadow: 0 0 25px lime; } +.red { background: red; box-shadow: 0 0 25px red; } + +.path { + position: absolute; + bottom: 50px; + left: 120px; + width: 650px; + height: 10px; + background: #444; + border-radius: 5px; +} + +#player { + position: absolute; + width: 40px; + height: 40px; + background: url("https://cdn-icons-png.flaticon.com/512/1477/1477022.png") no-repeat center/cover; + left: 0; + bottom: -15px; + transition: left 0.1s; + filter: drop-shadow(0 0 10px #0ff); +} + +.goal { + position: absolute; + right: 0; + bottom: -20px; + font-size: 40px; +} + +.controls { + margin-top: 15px; +} + +button { + background: #00ffff33; + color: white; + border: 2px solid #0ff; + border-radius: 8px; + padding: 8px 15px; + margin: 5px; + cursor: pointer; + transition: all 0.3s; +} + +button:hover { + background: #00ffff77; + transform: scale(1.1); +} + +.status p { + font-size: 18px; + text-shadow: 0 0 10px #0ff; + margin-top: 10px; +} diff --git a/games/snowball-rush/index.html b/games/snowball-rush/index.html new file mode 100644 index 00000000..a72e063e --- /dev/null +++ b/games/snowball-rush/index.html @@ -0,0 +1,29 @@ + + + + + + Snowball Rush | Mini JS Games Hub + + + +
    +

    โ„๏ธ Snowball Rush

    +
    + + + + + Score: 0 +
    + +
    + + + + + + + + + diff --git a/games/snowball-rush/script.js b/games/snowball-rush/script.js new file mode 100644 index 00000000..890935a5 --- /dev/null +++ b/games/snowball-rush/script.js @@ -0,0 +1,167 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +let gameRunning = false; +let paused = false; +let score = 0; +let snowball = { x: 400, y: 400, r: 20, vx: 2, vy: 0 }; +let flakes = []; +let obstacles = []; +let keys = {}; +let gravity = 0.1; +let friction = 0.99; +let canvasWidth = canvas.width; +let canvasHeight = canvas.height; + +// Sounds +const flakeSound = document.getElementById("flake-sound"); +const hitSound = document.getElementById("hit-sound"); +const jumpSound = document.getElementById("jump-sound"); +let muted = false; + +// Generate random flakes +function spawnFlake() { + flakes.push({ x: Math.random() * canvasWidth, y: -20, r: 5 + Math.random() * 5 }); +} + +// Generate obstacles +function spawnObstacle() { + obstacles.push({ x: Math.random() * canvasWidth, y: -30, w: 30, h: 30 }); +} + +// Draw Snowball with glow +function drawSnowball() { + ctx.save(); + ctx.beginPath(); + ctx.arc(snowball.x, snowball.y, snowball.r, 0, Math.PI * 2); + ctx.fillStyle = "#ffffff"; + ctx.shadowColor = "#00f0ff"; + ctx.shadowBlur = 20; + ctx.fill(); + ctx.restore(); +} + +// Draw flakes +function drawFlakes() { + flakes.forEach(f => { + ctx.beginPath(); + ctx.arc(f.x, f.y, f.r, 0, Math.PI * 2); + ctx.fillStyle = "#a2d5f2"; + ctx.shadowColor = "#00f0ff"; + ctx.shadowBlur = 10; + ctx.fill(); + }); +} + +// Draw obstacles +function drawObstacles() { + obstacles.forEach(o => { + ctx.fillStyle = "#ff4c4c"; + ctx.shadowColor = "#ff0000"; + ctx.shadowBlur = 15; + ctx.fillRect(o.x, o.y, o.w, o.h); + }); +} + +// Update positions +function update() { + if (!gameRunning || paused) return; + + // Snowball physics + snowball.vy += gravity; + snowball.x += snowball.vx; + snowball.y += snowball.vy; + snowball.vx *= friction; + snowball.vy *= friction; + + // Control + if (keys["ArrowLeft"] || keys["a"]) snowball.vx -= 0.2; + if (keys["ArrowRight"] || keys["d"]) snowball.vx += 0.2; + if ((keys[" "] || keys["Space"]) && snowball.y >= canvasHeight - snowball.r - 1) { + snowball.vy = -5; + if (!muted) jumpSound.play(); + } + + // Keep inside canvas + if (snowball.x < snowball.r) snowball.x = snowball.r; + if (snowball.x > canvasWidth - snowball.r) snowball.x = canvasWidth - snowball.r; + if (snowball.y > canvasHeight - snowball.r) snowball.y = canvasHeight - snowball.r; + + // Flakes movement + flakes.forEach((f, i) => { + f.y += 2; + // Collision + let dx = snowball.x - f.x; + let dy = snowball.y - f.y; + let dist = Math.sqrt(dx*dx + dy*dy); + if (dist < snowball.r + f.r) { + flakes.splice(i,1); + snowball.r += 1; + score += 10; + if (!muted) flakeSound.play(); + } + if (f.y > canvasHeight) flakes.splice(i,1); + }); + + // Obstacles movement + obstacles.forEach((o, i) => { + o.y += 2; + if ( + snowball.x + snowball.r > o.x && + snowball.x - snowball.r < o.x + o.w && + snowball.y + snowball.r > o.y && + snowball.y - snowball.r < o.y + o.h + ) { + obstacles.splice(i,1); + snowball.r -= 2; + score -= 5; + if (!muted) hitSound.play(); + if (snowball.r < 10) { + alert("Game Over! Final Score: " + score); + resetGame(); + } + } + if (o.y > canvasHeight) obstacles.splice(i,1); + }); +} + +// Draw everything +function draw() { + ctx.clearRect(0,0,canvasWidth,canvasHeight); + drawSnowball(); + drawFlakes(); + drawObstacles(); + document.getElementById("score").textContent = score; +} + +// Game Loop +function gameLoop() { + update(); + draw(); + if (gameRunning) requestAnimationFrame(gameLoop); +} + +// Spawn flakes and obstacles +setInterval(spawnFlake, 800); +setInterval(spawnObstacle, 1500); + +// Controls +window.addEventListener("keydown", (e) => keys[e.key] = true); +window.addEventListener("keyup", (e) => keys[e.key] = false); + +// Buttons +document.getElementById("start-btn").onclick = () => { gameRunning = true; paused = false; gameLoop(); }; +document.getElementById("pause-btn").onclick = () => { paused = !paused; }; +document.getElementById("restart-btn").onclick = resetGame; +document.getElementById("mute-btn").onclick = () => { muted = !muted; alert(muted ? "Muted" : "Sound On"); }; + +// Reset Game +function resetGame() { + gameRunning = false; + paused = false; + snowball = { x: 400, y: 400, r: 20, vx: 2, vy: 0 }; + flakes = []; + obstacles = []; + score = 0; + draw(); +} diff --git a/games/snowball-rush/style.css b/games/snowball-rush/style.css new file mode 100644 index 00000000..5694dfdf --- /dev/null +++ b/games/snowball-rush/style.css @@ -0,0 +1,45 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: linear-gradient(to top, #a2d5f2, #ffffff); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-ui { + text-align: center; +} + +canvas { + background: url("https://cdn.pixabay.com/photo/2016/03/26/23/23/snow-1284612_1280.png") repeat-x; + display: block; + margin: 20px auto; + border: 3px solid #00f0ff; + border-radius: 15px; + box-shadow: 0 0 20px #00f0ff, 0 0 40px #00f0ff inset; +} + +.controls button { + padding: 8px 12px; + margin: 5px; + font-size: 16px; + border: none; + border-radius: 8px; + cursor: pointer; + background-color: #00bfff; + color: white; + box-shadow: 0 0 10px #00f0ff; + transition: all 0.2s; +} +.controls button:hover { + background-color: #0080ff; + box-shadow: 0 0 20px #00f0ff; +} + +.score { + font-weight: bold; + font-size: 18px; + margin-left: 15px; +} diff --git a/games/snowflake-catcher/index.html b/games/snowflake-catcher/index.html new file mode 100644 index 00000000..122885c9 --- /dev/null +++ b/games/snowflake-catcher/index.html @@ -0,0 +1,22 @@ + + + + + +Snowflake Catcher | Mini JS Games Hub + + + +
    +

    โ„๏ธ Snowflake Catcher โ„๏ธ

    + +
    + + + + Score: 0 +
    +
    + + + diff --git a/games/snowflake-catcher/script.js b/games/snowflake-catcher/script.js new file mode 100644 index 00000000..fbcbc394 --- /dev/null +++ b/games/snowflake-catcher/script.js @@ -0,0 +1,109 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +const startBtn = document.getElementById("startBtn"); +const pauseBtn = document.getElementById("pauseBtn"); +const restartBtn = document.getElementById("restartBtn"); +const scoreEl = document.getElementById("score"); + +// Game variables +let basket = { x: 220, y: 550, width: 60, height: 20, speed: 7 }; +let snowflakes = []; +let score = 0; +let gameInterval; +let paused = false; + +// Online image and sound links +const snowflakeImg = new Image(); +snowflakeImg.src = "https://i.postimg.cc/J4Q76qfp/snowflake.png"; + +const basketImg = new Image(); +basketImg.src = "https://i.postimg.cc/3x0RrxR0/basket.png"; + +const catchSound = new Audio("https://freesound.org/data/previews/341/341695_6240037-lq.mp3"); +const missSound = new Audio("https://freesound.org/data/previews/522/522264_5634465-lq.mp3"); + +// Snowflake class +class Snowflake { + constructor() { + this.x = Math.random() * (canvas.width - 30); + this.y = -30; + this.size = Math.random() * 25 + 15; + this.speed = Math.random() * 2 + 1; + } + draw() { + ctx.drawImage(snowflakeImg, this.x, this.y, this.size, this.size); + } + update() { + this.y += this.speed; + } +} + +// Draw basket +function drawBasket() { + ctx.drawImage(basketImg, basket.x, basket.y, basket.width, basket.height); +} + +// Detect collision +function detectCollision(snowflake) { + if ( + snowflake.y + snowflake.size >= basket.y && + snowflake.x + snowflake.size >= basket.x && + snowflake.x <= basket.x + basket.width + ) { + return true; + } + return false; +} + +// Draw and update game +function updateGame() { + if (paused) return; + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawBasket(); + + // Add new snowflake + if (Math.random() < 0.03) { + snowflakes.push(new Snowflake()); + } + + snowflakes.forEach((s, i) => { + s.update(); + s.draw(); + + if (detectCollision(s)) { + snowflakes.splice(i, 1); + score++; + catchSound.play(); + scoreEl.textContent = "Score: " + score; + } else if (s.y > canvas.height) { + snowflakes.splice(i, 1); + score = Math.max(0, score - 1); + missSound.play(); + scoreEl.textContent = "Score: " + score; + } + }); + + requestAnimationFrame(updateGame); +} + +// Basket movement +document.addEventListener("keydown", (e) => { + if (e.key === "ArrowLeft" && basket.x > 0) basket.x -= basket.speed; + if (e.key === "ArrowRight" && basket.x + basket.width < canvas.width) basket.x += basket.speed; +}); + +// Buttons +startBtn.addEventListener("click", () => { + paused = false; + updateGame(); +}); +pauseBtn.addEventListener("click", () => (paused = true)); +restartBtn.addEventListener("click", () => { + snowflakes = []; + score = 0; + scoreEl.textContent = "Score: " + score; + paused = false; + basket.x = 220; + updateGame(); +}); diff --git a/games/snowflake-catcher/style.css b/games/snowflake-catcher/style.css new file mode 100644 index 00000000..9c1b77b2 --- /dev/null +++ b/games/snowflake-catcher/style.css @@ -0,0 +1,48 @@ +body { + background: linear-gradient(to bottom, #0a1f44, #1a2a5c); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + font-family: 'Arial', sans-serif; + margin: 0; + color: #fff; +} + +.game-container { + text-align: center; +} + +canvas { + background: linear-gradient(to bottom, #1e3c72, #2a5298); + border: 2px solid #fff; + box-shadow: 0 0 20px #fff, 0 0 40px #0ff, 0 0 60px #0ff inset; + border-radius: 10px; +} + +.controls { + margin-top: 10px; +} + +button { + padding: 8px 16px; + margin: 5px; + font-size: 16px; + border: none; + border-radius: 5px; + cursor: pointer; + background-color: #0ff; + color: #000; + font-weight: bold; + box-shadow: 0 0 10px #0ff; +} + +button:hover { + box-shadow: 0 0 20px #0ff, 0 0 40px #0ff inset; +} + +#score { + margin-left: 15px; + font-size: 18px; + font-weight: bold; +} diff --git a/games/space-bounce/index.html b/games/space-bounce/index.html new file mode 100644 index 00000000..2bde0e62 --- /dev/null +++ b/games/space-bounce/index.html @@ -0,0 +1,31 @@ + + + + + + Space Bounce | Mini JS Games Hub + + + +
    + + +
    + + + +
    + +
    +

    โญ Score: 0

    +

    ๐Ÿ† High Score: 0

    +
    +
    + + + + + + + + diff --git a/games/space-bounce/script.js b/games/space-bounce/script.js new file mode 100644 index 00000000..57b71bcc --- /dev/null +++ b/games/space-bounce/script.js @@ -0,0 +1,223 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +canvas.width = 800; +canvas.height = 500; + +const bgMusic = document.getElementById("bgMusic"); +const bounceSound = document.getElementById("bounceSound"); +const powerSound = document.getElementById("powerSound"); + +const startBtn = document.getElementById("startBtn"); +const pauseBtn = document.getElementById("pauseBtn"); +const restartBtn = document.getElementById("restartBtn"); +const scoreEl = document.getElementById("score"); +const highscoreEl = document.getElementById("highscore"); + +let gameRunning = false; +let paused = false; +let score = 0; +let highscore = localStorage.getItem("spaceBounceHigh") || 0; +highscoreEl.textContent = highscore; + +const player = { + x: 400, + y: 250, + radius: 20, + vy: 0, + gravity: 0.4, + color: "#0ff", +}; + +let planets = []; +let powerUps = []; +let obstacles = []; + +function resetGame() { + player.y = 250; + player.vy = 0; + planets = []; + powerUps = []; + obstacles = []; + score = 0; + scoreEl.textContent = score; +} + +function createPlanet() { + planets.push({ + x: Math.random() * canvas.width, + y: Math.random() * (canvas.height - 100) + 50, + radius: Math.random() * 20 + 30, + color: "rgba(0,255,255,0.8)", + }); +} + +function createObstacle() { + obstacles.push({ + x: canvas.width, + y: Math.random() * canvas.height, + width: 30, + height: 30, + color: "red", + speed: 3, + }); +} + +function createPowerUp() { + powerUps.push({ + x: canvas.width, + y: Math.random() * canvas.height, + radius: 15, + color: "gold", + speed: 2.5, + }); +} + +function drawPlanet(p) { + ctx.beginPath(); + ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); + ctx.fillStyle = p.color; + ctx.shadowColor = "#0ff"; + ctx.shadowBlur = 20; + ctx.fill(); + ctx.closePath(); +} + +function drawPlayer() { + ctx.beginPath(); + ctx.arc(player.x, player.y, player.radius, 0, Math.PI * 2); + ctx.fillStyle = player.color; + ctx.shadowColor = "#0ff"; + ctx.shadowBlur = 20; + ctx.fill(); + ctx.closePath(); +} + +function drawObstacle(o) { + ctx.fillStyle = o.color; + ctx.fillRect(o.x, o.y, o.width, o.height); +} + +function drawPowerUp(p) { + ctx.beginPath(); + ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); + ctx.fillStyle = p.color; + ctx.shadowColor = "gold"; + ctx.shadowBlur = 20; + ctx.fill(); +} + +function update() { + if (!gameRunning || paused) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + player.vy += player.gravity; + player.y += player.vy; + + // Keep player in bounds + if (player.y + player.radius > canvas.height) { + player.y = canvas.height - player.radius; + player.vy = -player.vy * 0.8; + bounceSound.play(); + } + + planets.forEach((planet) => { + planet.x -= 2; + if (planet.x + planet.radius < 0) planets.splice(planets.indexOf(planet), 1); + drawPlanet(planet); + }); + + obstacles.forEach((o, i) => { + o.x -= o.speed; + if (o.x + o.width < 0) obstacles.splice(i, 1); + drawObstacle(o); + + if ( + player.x + player.radius > o.x && + player.x - player.radius < o.x + o.width && + player.y + player.radius > o.y && + player.y - player.radius < o.y + o.height + ) { + gameRunning = false; + bgMusic.pause(); + alert("๐Ÿ’ฅ You hit an obstacle! Final Score: " + score); + } + }); + + powerUps.forEach((p, i) => { + p.x -= p.speed; + if (p.x + p.radius < 0) powerUps.splice(i, 1); + drawPowerUp(p); + + let dx = player.x - p.x; + let dy = player.y - p.y; + let dist = Math.sqrt(dx * dx + dy * dy); + if (dist < player.radius + p.radius) { + powerSound.play(); + score += 10; + scoreEl.textContent = score; + powerUps.splice(i, 1); + } + }); + + drawPlayer(); + + if (Math.random() < 0.02) createPlanet(); + if (Math.random() < 0.01) createObstacle(); + if (Math.random() < 0.008) createPowerUp(); + + score++; + scoreEl.textContent = score; + + if (score > highscore) { + highscore = score; + localStorage.setItem("spaceBounceHigh", highscore); + highscoreEl.textContent = highscore; + } + + requestAnimationFrame(update); +} + +function startGame() { + if (!gameRunning) { + gameRunning = true; + paused = false; + bgMusic.play(); + update(); + } +} + +function pauseGame() { + paused = !paused; + if (paused) { + bgMusic.pause(); + } else { + bgMusic.play(); + update(); + } +} + +function restartGame() { + resetGame(); + gameRunning = true; + paused = false; + bgMusic.currentTime = 0; + bgMusic.play(); + update(); +} + +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", pauseGame); +restartBtn.addEventListener("click", restartGame); + +window.addEventListener("keydown", (e) => { + if (e.code === "Space") { + player.vy = -8; + bounceSound.play(); + } +}); +canvas.addEventListener("click", () => { + player.vy = -8; + bounceSound.play(); +}); diff --git a/games/space-bounce/style.css b/games/space-bounce/style.css new file mode 100644 index 00000000..e0415279 --- /dev/null +++ b/games/space-bounce/style.css @@ -0,0 +1,56 @@ +body { + margin: 0; + background: radial-gradient(ellipse at bottom, #0d1b2a, #000); + overflow: hidden; + font-family: 'Poppins', sans-serif; + color: #fff; +} + +.game-container { + text-align: center; + position: relative; + height: 100vh; +} + +canvas { + border: 2px solid #0ff; + background: url('https://images.unsplash.com/photo-1446776811953-b23d57bd21aa?auto=format&fit=crop&w=1600&q=80') center/cover no-repeat; + box-shadow: 0 0 25px #0ff; + display: block; + margin: 0 auto; + margin-top: 20px; +} + +.controls { + position: absolute; + top: 10px; + left: 50%; + transform: translateX(-50%); +} + +button { + background: rgba(0, 255, 255, 0.2); + border: 2px solid #0ff; + color: #fff; + font-size: 18px; + margin: 5px; + padding: 8px 16px; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s; +} + +button:hover { + background: #0ff; + color: #000; + box-shadow: 0 0 15px #0ff, 0 0 30px #0ff; +} + +.scoreboard { + position: absolute; + bottom: 10px; + left: 50%; + transform: translateX(-50%); + text-shadow: 0 0 10px #0ff; + font-size: 20px; +} diff --git a/games/space_invader/index.html b/games/space_invader/index.html new file mode 100644 index 00000000..9564e3d0 --- /dev/null +++ b/games/space_invader/index.html @@ -0,0 +1,26 @@ + + + + + + Simple Space Invaders ๐Ÿ‘พ + + + +
    + +
    +

    SPACE INVADERS

    +

    Use **Left/Right Arrow Keys** to Move

    +

    Press **Spacebar** to Shoot

    + +
    + +
    + + + + \ No newline at end of file diff --git a/games/space_invader/script.js b/games/space_invader/script.js new file mode 100644 index 00000000..3dc4bdbf --- /dev/null +++ b/games/space_invader/script.js @@ -0,0 +1,365 @@ +// --- 1. Canvas Setup --- +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const startScreen = document.getElementById('startScreen'); +const startButton = document.getElementById('startButton'); +const gameOverScreen = document.getElementById('gameOverScreen'); +const gameOverMessage = document.getElementById('gameOverMessage'); +const restartButton = document.getElementById('restartButton'); + +// Set Canvas Dimensions +canvas.width = 800; +canvas.height = 600; + +// --- 2. Game Constants and Variables --- +const PLAYER_SPEED = 5; +const PLAYER_BULLET_SPEED = 7; +const ALIEN_SPEED_X = 1; +const ALIEN_SPEED_Y = 20; // How much aliens drop when hitting wall +const ALIEN_BULLET_SPEED = 3; +const ALIEN_FIRE_RATE = 200; // Lower is faster (chance to fire each frame) + +let player; +let playerBullets = []; +let aliens = []; +let alienBullets = []; +let alienDirection = 1; // 1 for right, -1 for left +let gameActive = false; +let score = 0; +let level = 1; +let lastAlienFire = 0; + +// Input tracking +let keys = { + ArrowLeft: false, + ArrowRight: false, + Space: false +}; + +// --- 3. Game Objects (Classes) --- + +// Player Ship +class Player { + constructor() { + this.width = 40; + this.height = 20; + this.x = canvas.width / 2 - this.width / 2; + this.y = canvas.height - this.height - 30; // Position above bottom + this.color = '#00ff00'; // Green + } + + draw() { + ctx.fillStyle = this.color; + // Simple triangular ship + ctx.beginPath(); + ctx.moveTo(this.x + this.width / 2, this.y); + ctx.lineTo(this.x, this.y + this.height); + ctx.lineTo(this.x + this.width, this.y + this.height); + ctx.closePath(); + ctx.fill(); + // Base + ctx.fillRect(this.x, this.y + this.height - 5, this.width, 5); + } + + update() { + if (keys.ArrowLeft) { + this.x -= PLAYER_SPEED; + } + if (keys.ArrowRight) { + this.x += PLAYER_SPEED; + } + + // Boundary checks + if (this.x < 0) this.x = 0; + if (this.x + this.width > canvas.width) this.x = canvas.width - this.width; + } + + shoot() { + playerBullets.push(new PlayerBullet(this.x + this.width / 2, this.y)); + } +} + +// Player Bullet +class PlayerBullet { + constructor(x, y) { + this.x = x - 2; // Center bullet + this.y = y; + this.width = 4; + this.height = 10; + this.color = '#00ffff'; // Cyan + } + + draw() { + ctx.fillStyle = this.color; + ctx.fillRect(this.x, this.y, this.width, this.height); + } + + update() { + this.y -= PLAYER_BULLET_SPEED; + } +} + +// Alien +class Alien { + constructor(x, y) { + this.x = x; + this.y = y; + this.width = 30; + this.height = 20; + this.color = '#ff00ff'; // Magenta + } + + draw() { + ctx.fillStyle = this.color; + // Simple blocky alien + ctx.fillRect(this.x, this.y, this.width, this.height); + ctx.fillRect(this.x - 5, this.y + 5, 5, 10); // Legs + ctx.fillRect(this.x + this.width, this.y + 5, 5, 10); + } + + update() { + // Aliens are moved by a collective update function, not individually + } +} + +// Alien Bullet +class AlienBullet { + constructor(x, y) { + this.x = x - 2; // Center bullet + this.y = y; + this.width = 4; + this.height = 10; + this.color = '#ff0000'; // Red + } + + draw() { + ctx.fillStyle = this.color; + ctx.fillRect(this.x, this.y, this.width, this.height); + } + + update() { + this.y += ALIEN_BULLET_SPEED; + } +} + +// --- 4. Game Setup and Initialization --- + +function initGame() { + // Reset all game state + player = new Player(); + playerBullets = []; + aliens = []; + alienBullets = []; + alienDirection = 1; + score = 0; + level = 1; + gameActive = false; + + // Create aliens grid + const numRows = 4; + const aliensPerRow = 8; + const startX = (canvas.width - (aliensPerRow * 40)) / 2; // Center aliens + const startY = 50; + + for (let r = 0; r < numRows; r++) { + for (let c = 0; c < aliensPerRow; c++) { + aliens.push(new Alien(startX + c * 40, startY + r * 30)); + } + } + + // Hide game over screen, show start screen initially + gameOverScreen.classList.add('hidden'); + startScreen.classList.remove('hidden'); +} + +// --- 5. Main Game Loop --- + +function gameLoop() { + if (!gameActive) return; + + // 1. Clear Canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // 2. Update Game Objects + player.update(); + updateAliens(); + updateBullets(); + + // 3. Handle Collisions + checkCollisions(); + + // 4. Draw Game Objects + player.draw(); + playerBullets.forEach(bullet => bullet.draw()); + aliens.forEach(alien => alien.draw()); + alienBullets.forEach(bullet => bullet.draw()); + + // 5. Draw Score/Level + ctx.fillStyle = 'white'; + ctx.font = '16px "Press Start 2P"'; + ctx.fillText(`Score: ${score}`, 10, 25); + ctx.fillText(`Level: ${level}`, canvas.width - 120, 25); + + + // 6. Check Win/Lose Conditions + if (aliens.length === 0) { + // Player wins the level + level++; + initNextLevel(); + return; // Skip remaining frame logic for this loop + } + + // Check if aliens reached player's level + if (aliens.some(alien => alien.y + alien.height > player.y)) { + endGame(false); // Player loses + return; + } + + requestAnimationFrame(gameLoop); +} + +function updateAliens() { + let hitWall = false; + + aliens.forEach(alien => { + alien.x += ALIEN_SPEED_X * alienDirection * level * 0.5; // Aliens get faster with level + if (alien.x + alien.width > canvas.width || alien.x < 0) { + hitWall = true; + } + }); + + if (hitWall) { + alienDirection *= -1; // Reverse direction + aliens.forEach(alien => { + alien.y += ALIEN_SPEED_Y; // Drop down + alien.x += ALIEN_SPEED_X * alienDirection; // Adjust position slightly to be off wall + }); + } + + // Alien firing logic + if (Math.random() * ALIEN_FIRE_RATE < level) { // Chance to fire, increases with level + const firingAlien = aliens[Math.floor(Math.random() * aliens.length)]; + if (firingAlien) { + alienBullets.push(new AlienBullet(firingAlien.x + firingAlien.width / 2, firingAlien.y + firingAlien.height)); + } + } +} + +function updateBullets() { + // Update player bullets and remove off-screen + playerBullets = playerBullets.filter(bullet => { + bullet.update(); + return bullet.y > 0; + }); + + // Update alien bullets and remove off-screen + alienBullets = alienBullets.filter(bullet => { + bullet.update(); + return bullet.y < canvas.height; + }); +} + +// AABB (Axis-Aligned Bounding Box) collision detection +function collides(obj1, obj2) { + return obj1.x < obj2.x + obj2.width && + obj1.x + obj1.width > obj2.x && + obj1.y < obj2.y + obj2.height && + obj1.y + obj1.height > obj2.y; +} + +function checkCollisions() { + // Player bullet - Alien collision + playerBullets.forEach((pBullet, pIndex) => { + aliens.forEach((alien, aIndex) => { + if (collides(pBullet, alien)) { + // Remove bullet and alien + playerBullets.splice(pIndex, 1); + aliens.splice(aIndex, 1); + score += 100; // Increase score + } + }); + }); + + // Alien bullet - Player collision + alienBullets.forEach((aBullet, bIndex) => { + if (collides(aBullet, player)) { + alienBullets.splice(bIndex, 1); // Remove bullet + endGame(false); // Player hit, game over + } + }); +} + +function endGame(win) { + gameActive = false; + clearInterval(gameLoop); // Stop game loop (though requestAnimationFrame is better for this) + + // Show game over screen with appropriate message + gameOverScreen.classList.remove('hidden'); + gameOverMessage.textContent = win ? 'YOU WIN! ๐ŸŽ‰' : 'GAME OVER!'; + gameOverMessage.style.color = win ? '#00ff00' : '#ff0000'; +} + +function initNextLevel() { + // Increase difficulty or just reset board with more aliens/faster movement + gameActive = false; // Pause briefly + playerBullets = []; // Clear bullets + alienBullets = []; + alienDirection = 1; + + // Re-populate aliens + const numRows = 4 + (level - 1); // More rows each level + const aliensPerRow = 8 + (level - 1); + const startX = (canvas.width - (aliensPerRow * 40)) / 2; + const startY = 50; + + aliens = []; + for (let r = 0; r < numRows; r++) { + for (let c = 0; c < aliensPerRow; c++) { + aliens.push(new Alien(startX + c * 40, startY + r * 30)); + } + } + + setTimeout(() => { // Give a brief pause before starting next level + gameActive = true; + requestAnimationFrame(gameLoop); + }, 1000); +} + + +// --- 6. Event Listeners (Input) --- + +document.addEventListener('keydown', (e) => { + if (e.code === 'ArrowLeft') keys.ArrowLeft = true; + if (e.code === 'ArrowRight') keys.ArrowRight = true; + if (e.code === 'Space') { + if (!keys.Space && gameActive) { // Prevent continuous firing on hold + player.shoot(); + } + keys.Space = true; + } +}); + +document.addEventListener('keyup', (e) => { + if (e.code === 'ArrowLeft') keys.ArrowLeft = false; + if (e.code === 'ArrowRight') keys.ArrowRight = false; + if (e.code === 'Space') keys.Space = false; +}); + +startButton.addEventListener('click', () => { + startScreen.classList.add('hidden'); + gameActive = true; + requestAnimationFrame(gameLoop); +}); + +restartButton.addEventListener('click', () => { + initGame(); // Re-initialize everything + startScreen.classList.add('hidden'); // Ensure start screen is hidden on restart + gameActive = true; + requestAnimationFrame(gameLoop); // Start game loop again +}); + + +// --- 7. Initialization --- +initGame(); \ No newline at end of file diff --git a/games/space_invader/style.css b/games/space_invader/style.css new file mode 100644 index 00000000..faceef90 --- /dev/null +++ b/games/space_invader/style.css @@ -0,0 +1,84 @@ +body { + font-family: 'Press Start 2P', cursive, Arial, sans-serif; /* A classic arcade font */ + background-color: #1a1a2e; /* Dark background */ + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + overflow: hidden; /* Prevent scrollbars */ + color: #eee; +} + +/* Optional: Import a pixel-style font */ +@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); + +.game-wrapper { + position: relative; + border: 4px solid #00ff00; /* Neon green border */ + box-shadow: 0 0 20px rgba(0, 255, 0, 0.7); /* Green glow */ + border-radius: 5px; +} + +#gameCanvas { + background-color: #000; /* Black background for the game area */ + display: block; /* Remove extra space below canvas */ +} + +/* --- Overlay Screens (Start/Game Over) --- */ +.overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + color: white; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + z-index: 10; +} + +.overlay h1 { + font-size: 3em; + margin-bottom: 0.8em; + color: #00ff00; /* Green highlight */ + text-shadow: 0 0 10px #00ff00; +} + +.overlay p { + margin-bottom: 0.8em; + font-size: 1.1em; + color: #bbb; +} + +.overlay button { + padding: 12px 25px; + font-size: 1.5em; + cursor: pointer; + background-color: #00ff00; + color: #000; + border: 2px solid #00ff00; + border-radius: 5px; + transition: background-color 0.3s, color 0.3s; + font-family: 'Press Start 2P', cursive; +} + +.overlay button:hover { + background-color: #00a000; + border-color: #00a000; + color: white; +} + +#gameOverMessage { + color: #ff0000; /* Red for Game Over */ + text-shadow: 0 0 10px #ff0000; +} + +/* Utility class to hide elements */ +.hidden { + display: none; +} \ No newline at end of file diff --git a/games/spectral-sprint/index.html b/games/spectral-sprint/index.html new file mode 100644 index 00000000..e9e0aaff --- /dev/null +++ b/games/spectral-sprint/index.html @@ -0,0 +1,18 @@ + + + + + + Spectral Sprint Game + + + +
    +

    Spectral Sprint

    + +
    Distance: 0
    +
    Hold SHIFT to phase through ghostly walls. Avoid solid obstacles!
    +
    + + + \ No newline at end of file diff --git a/games/spectral-sprint/script.js b/games/spectral-sprint/script.js new file mode 100644 index 00000000..4f85ee4c --- /dev/null +++ b/games/spectral-sprint/script.js @@ -0,0 +1,171 @@ +// Spectral Sprint Game Script +// Run through ghostly realms, phasing through walls at the right moments. + +const canvas = document.getElementById('game-canvas'); +const ctx = canvas.getContext('2d'); +const scoreElement = document.getElementById('score'); + +// Game variables +let player = { x: 100, y: 400, vy: 0, onGround: true, phasing: false }; +let walls = []; +let obstacles = []; +let score = 0; +let gameRunning = true; +let cameraX = 0; + +// Constants +const gravity = 0.6; +const jumpStrength = -15; +const groundY = 400; +const scrollSpeed = 5; + +// Initialize game +function init() { + // Create initial walls and obstacles + createWall(300, true); + createWall(500, false); + createWall(700, true); + createObstacle(400); + createObstacle(600); + + // Start game loop + requestAnimationFrame(gameLoop); +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + update(); + draw(); + + requestAnimationFrame(gameLoop); +} + +// Update game state +function update() { + // Player physics + player.vy += gravity; + player.y += player.vy; + + if (player.y >= groundY) { + player.y = groundY; + player.vy = 0; + player.onGround = true; + } + + // Move camera + cameraX += scrollSpeed; + score = Math.floor(cameraX / 10); + + // Move walls and obstacles + walls.forEach(wall => wall.x -= scrollSpeed); + obstacles.forEach(obs => obs.x -= scrollSpeed); + + // Remove off-screen elements + walls = walls.filter(wall => wall.x > -100); + obstacles = obstacles.filter(obs => obs.x > -100); + + // Add new elements + if (Math.random() < 0.02) createWall(canvas.width + cameraX + 50, Math.random() < 0.5); + if (Math.random() < 0.01) createObstacle(canvas.width + cameraX + 50); + + // Check collisions + checkCollisions(); +} + +// Draw everything +function draw() { + // Clear canvas + ctx.fillStyle = '#111'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw ground + ctx.fillStyle = '#333'; + ctx.fillRect(0, groundY + 20, canvas.width, canvas.height - groundY - 20); + + // Draw walls + walls.forEach(wall => { + if (wall.ghostly) { + ctx.fillStyle = 'rgba(128, 0, 128, 0.5)'; + } else { + ctx.fillStyle = '#666'; + } + ctx.fillRect(wall.x - cameraX, wall.y, wall.width, wall.height); + }); + + // Draw obstacles + ctx.fillStyle = '#ff0000'; + obstacles.forEach(obs => { + ctx.beginPath(); + ctx.moveTo(obs.x - cameraX + 10, groundY + 20); + ctx.lineTo(obs.x - cameraX + 20, groundY); + ctx.lineTo(obs.x - cameraX + 30, groundY + 20); + ctx.closePath(); + ctx.fill(); + }); + + // Draw player + if (player.phasing) { + ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; + } else { + ctx.fillStyle = '#ffffff'; + } + ctx.fillRect(player.x, player.y, 20, 20); + + // Update score + scoreElement.textContent = 'Distance: ' + score; +} + +// Handle input +document.addEventListener('keydown', function(event) { + if (event.code === 'Space' && player.onGround) { + player.vy = jumpStrength; + player.onGround = false; + } + if (event.code === 'ShiftLeft' || event.code === 'ShiftRight') { + player.phasing = true; + } +}); + +document.addEventListener('keyup', function(event) { + if (event.code === 'ShiftLeft' || event.code === 'ShiftRight') { + player.phasing = false; + } +}); + +// Create wall +function createWall(x, ghostly) { + walls.push({ x: x, y: groundY - 100, width: 20, height: 100, ghostly: ghostly }); +} + +// Create obstacle +function createObstacle(x) { + obstacles.push({ x: x }); +} + +// Check collisions +function checkCollisions() { + // Player with walls + walls.forEach(wall => { + if (player.x < wall.x - cameraX + wall.width && player.x + 20 > wall.x - cameraX && + player.y < wall.y + wall.height && player.y + 20 > wall.y) { + if (!wall.ghostly || !player.phasing) { + gameRunning = false; + alert('Collision! Game Over. Distance: ' + score); + } + } + }); + + // Player with obstacles + obstacles.forEach(obs => { + if (player.x < obs.x - cameraX + 30 && player.x + 20 > obs.x - cameraX && + player.y + 20 > groundY) { + gameRunning = false; + alert('Hit obstacle! Game Over. Distance: ' + score); + } + }); +} + +// Start the game +init(); \ No newline at end of file diff --git a/games/spectral-sprint/style.css b/games/spectral-sprint/style.css new file mode 100644 index 00000000..a0ab51fd --- /dev/null +++ b/games/spectral-sprint/style.css @@ -0,0 +1,38 @@ +body { + font-family: Arial, sans-serif; + background-color: #000; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +#game-container { + text-align: center; +} + +h1 { + font-size: 2.5em; + margin-bottom: 20px; + text-shadow: 0 0 10px #800080; +} + +#game-canvas { + border: 2px solid #800080; + background-color: #111; + box-shadow: 0 0 20px #800080; +} + +#score { + font-size: 1.2em; + margin: 10px 0; + color: #00ff00; +} + +#instructions { + font-size: 1em; + margin-top: 10px; + color: #cccccc; +} \ No newline at end of file diff --git a/games/speed-match/index.html b/games/speed-match/index.html new file mode 100644 index 00000000..1d49b1b5 --- /dev/null +++ b/games/speed-match/index.html @@ -0,0 +1,37 @@ + + + + + +Speed Match | Mini JS Games Hub + + + +
    +

    โšก Speed Match โšก

    +
    +
    Score: 0
    +
    Time: 30s
    +
    Combo: 0
    +
    +
    +

    Target:

    +
    +
    +
    + +
    +
    + + + +
    +
    + + + + + + + + diff --git a/games/speed-match/script.js b/games/speed-match/script.js new file mode 100644 index 00000000..40c9ce60 --- /dev/null +++ b/games/speed-match/script.js @@ -0,0 +1,84 @@ +const targetSymbolEl = document.getElementById("target-symbol"); +const playArea = document.getElementById("play-area"); +const scoreEl = document.getElementById("score"); +const comboEl = document.getElementById("combo"); +const timerEl = document.getElementById("timer"); +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const restartBtn = document.getElementById("restart-btn"); + +const hitSound = document.getElementById("hit-sound"); +const missSound = document.getElementById("miss-sound"); + +let symbols = ["๐ŸŽ","๐ŸŒ","๐Ÿ‡","๐Ÿ“","๐Ÿ’","๐Ÿ‰","๐Ÿฅ","๐Ÿ"]; +let currentTarget = ""; +let score = 0; +let combo = 0; +let timeLeft = 30; +let gameInterval; +let countdownInterval; +let isPaused = false; + +function pickTarget() { + currentTarget = symbols[Math.floor(Math.random() * symbols.length)]; + targetSymbolEl.textContent = currentTarget; +} + +function generateSymbols() { + playArea.innerHTML = ""; + let shuffled = [...symbols].sort(() => 0.5 - Math.random()); + shuffled.forEach(sym => { + const div = document.createElement("div"); + div.textContent = sym; + div.className = "symbol"; + div.addEventListener("click", () => handleClick(sym)); + playArea.appendChild(div); + }); +} + +function handleClick(symbol) { + if (isPaused) return; + if(symbol === currentTarget) { + score++; + combo++; + scoreEl.textContent = score; + comboEl.textContent = combo; + hitSound.currentTime = 0; + hitSound.play(); + pickTarget(); + generateSymbols(); + } else { + combo = 0; + comboEl.textContent = combo; + missSound.currentTime = 0; + missSound.play(); + } +} + +function startGame() { + if(gameInterval) clearInterval(gameInterval); + if(countdownInterval) clearInterval(countdownInterval); + isPaused = false; + score = 0; + combo = 0; + timeLeft = 30; + scoreEl.textContent = score; + comboEl.textContent = combo; + timerEl.textContent = timeLeft; + pickTarget(); + generateSymbols(); + countdownInterval = setInterval(() => { + if(!isPaused) { + timeLeft--; + timerEl.textContent = timeLeft; + if(timeLeft <= 0) { + clearInterval(countdownInterval); + alert(`Time's up! Final Score: ${score}`); + } + } + }, 1000); +} + +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", () => { isPaused = !isPaused; }); +restartBtn.addEventListener("click", startGame); diff --git a/games/speed-match/style.css b/games/speed-match/style.css new file mode 100644 index 00000000..81f18d3d --- /dev/null +++ b/games/speed-match/style.css @@ -0,0 +1,76 @@ +body { + font-family: Arial, sans-serif; + background: #111; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + flex-direction: column; +} + +.speed-match-container { + text-align: center; + padding: 20px; + border: 2px solid #fff; + border-radius: 12px; + width: 90%; + max-width: 600px; + background: #1c1c1c; +} + +.info-bar { + display: flex; + justify-content: space-around; + margin-bottom: 20px; + font-size: 1.2em; +} + +.target-symbol-container { + margin-bottom: 20px; +} + +.symbol { + font-size: 3em; + display: inline-block; + padding: 20px; + margin: 10px; + border-radius: 12px; + transition: all 0.2s ease; + box-shadow: 0 0 10px #fff; +} + +.symbol:hover { + transform: scale(1.2); + box-shadow: 0 0 20px #0f0, 0 0 30px #0f0; + cursor: pointer; +} + +.play-area { + display: flex; + justify-content: center; + flex-wrap: wrap; + min-height: 100px; +} + +.controls { + margin-top: 20px; +} + +button { + padding: 10px 20px; + margin: 5px; + font-size: 1em; + border: none; + border-radius: 8px; + cursor: pointer; + background: #222; + color: #fff; + box-shadow: 0 0 10px #fff; + transition: all 0.2s ease; +} + +button:hover { + background: #444; + box-shadow: 0 0 20px #0ff; +} diff --git a/games/speed-tap-grid/index.html b/games/speed-tap-grid/index.html new file mode 100644 index 00000000..e06786a6 --- /dev/null +++ b/games/speed-tap-grid/index.html @@ -0,0 +1,31 @@ + + + + + + Speed Tap Grid | Mini JS Games Hub + + + +
    +

    Speed Tap Grid

    +
    + Score: 0 + Lives: 3 +
    +
    +
    + + + + +
    +

    +
    + + + + + + + diff --git a/games/speed-tap-grid/script.js b/games/speed-tap-grid/script.js new file mode 100644 index 00000000..85ac0df1 --- /dev/null +++ b/games/speed-tap-grid/script.js @@ -0,0 +1,106 @@ +const grid = document.getElementById("grid"); +const scoreEl = document.getElementById("score"); +const livesEl = document.getElementById("lives"); +const messageEl = document.getElementById("message"); +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const resumeBtn = document.getElementById("resume-btn"); +const restartBtn = document.getElementById("restart-btn"); + +const tapSound = document.getElementById("tap-sound"); +const wrongSound = document.getElementById("wrong-sound"); +const winSound = document.getElementById("win-sound"); + +let score = 0; +let lives = 3; +let interval; +let isPaused = false; +let activeIndex = -1; +const gridSize = 5; // 5x5 line +const cells = []; + +function createGrid() { + grid.innerHTML = ""; + for (let i = 0; i < gridSize; i++) { + const cell = document.createElement("div"); + cell.classList.add("grid-cell"); + cell.addEventListener("click", () => handleClick(i)); + grid.appendChild(cell); + cells.push(cell); + } +} + +function randomActiveCell() { + if (activeIndex >= 0) cells[activeIndex].classList.remove("active"); + activeIndex = Math.floor(Math.random() * gridSize); + cells[activeIndex].classList.add("active"); + + // Random obstacle + const obstacleIndex = Math.floor(Math.random() * gridSize); + if (obstacleIndex !== activeIndex) { + cells[obstacleIndex].classList.add("obstacle"); + } +} + +function handleClick(index) { + if (isPaused) return; + if (index === activeIndex) { + score++; + tapSound.play(); + scoreEl.textContent = score; + nextTurn(); + } else if (cells[index].classList.contains("obstacle")) { + lives--; + wrongSound.play(); + livesEl.textContent = lives; + if (lives <= 0) endGame(); + } else { + lives--; + wrongSound.play(); + livesEl.textContent = lives; + if (lives <= 0) endGame(); + } +} + +function nextTurn() { + cells.forEach(c => c.classList.remove("active", "obstacle")); + randomActiveCell(); +} + +function startGame() { + score = 0; + lives = 3; + scoreEl.textContent = score; + livesEl.textContent = lives; + messageEl.textContent = ""; + createGrid(); + nextTurn(); + interval = setInterval(() => { + if (!isPaused) nextTurn(); + }, 1500); +} + +function pauseGame() { + isPaused = true; +} + +function resumeGame() { + isPaused = false; +} + +function restartGame() { + clearInterval(interval); + cells.forEach(c => c.classList.remove("active", "obstacle")); + startGame(); +} + +function endGame() { + clearInterval(interval); + messageEl.textContent = `Game Over! Your score: ${score}`; + winSound.play(); +} + +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", pauseGame); +resumeBtn.addEventListener("click", resumeGame); +restartBtn.addEventListener("click", restartGame); diff --git a/games/speed-tap-grid/style.css b/games/speed-tap-grid/style.css new file mode 100644 index 00000000..17d179ef --- /dev/null +++ b/games/speed-tap-grid/style.css @@ -0,0 +1,72 @@ +body { + font-family: Arial, sans-serif; + background-color: #111; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; +} + +.game-container { + text-align: center; + max-width: 600px; +} + +h1 { + font-size: 2em; + color: #ffea00; + text-shadow: 0 0 10px #ffea00; +} + +.scoreboard { + display: flex; + justify-content: space-between; + margin: 10px 0; + font-size: 1.2em; +} + +.grid-container { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 10px; + margin: 20px 0; +} + +.grid-cell { + width: 60px; + height: 60px; + border-radius: 50%; + background-color: #222; + box-shadow: 0 0 5px #444; + cursor: pointer; + transition: 0.2s; +} + +.grid-cell.active { + background-color: #00ff99; + box-shadow: 0 0 20px #00ff99, 0 0 40px #00ff99, 0 0 60px #00ff99; +} + +.grid-cell.obstacle { + background-color: #ff0033; + box-shadow: 0 0 10px #ff0033; +} + +.controls button { + margin: 5px; + padding: 10px 15px; + font-size: 1em; + cursor: pointer; + border: none; + border-radius: 5px; + background-color: #222; + color: #fff; + transition: 0.2s; +} + +.controls button:hover { + background-color: #00ff99; + color: #000; +} diff --git a/games/speedy-typing/index.html b/games/speedy-typing/index.html new file mode 100644 index 00000000..780aa71f --- /dev/null +++ b/games/speedy-typing/index.html @@ -0,0 +1,26 @@ + + + + + + Speedy Typing Game | Mini JS Games Hub + + + +
    +

    Speedy Typing Game โŒจ๏ธ

    +
    + Score: 0 + Time: 60s +
    +
    Press Start!
    + +
    + + +
    +
    +
    + + + diff --git a/games/speedy-typing/script.js b/games/speedy-typing/script.js new file mode 100644 index 00000000..30bbca26 --- /dev/null +++ b/games/speedy-typing/script.js @@ -0,0 +1,73 @@ +const words = [ + "javascript", "developer", "algorithm", "function", "variable", + "object", "array", "string", "boolean", "event", + "document", "element", "style", "keyboard", "performance" +]; + +let currentWord = ""; +let score = 0; +let time = 60; +let timer; +let gameActive = false; + +const wordDisplay = document.getElementById("word-display"); +const input = document.getElementById("typing-input"); +const scoreEl = document.getElementById("score"); +const timeEl = document.getElementById("time"); +const messageEl = document.getElementById("message"); +const startBtn = document.getElementById("start-btn"); +const restartBtn = document.getElementById("restart-btn"); + +function pickRandomWord() { + currentWord = words[Math.floor(Math.random() * words.length)]; + wordDisplay.textContent = currentWord; +} + +function startGame() { + if (gameActive) return; + gameActive = true; + score = 0; + time = 60; + scoreEl.textContent = score; + timeEl.textContent = time; + messageEl.textContent = ""; + input.value = ""; + input.focus(); + pickRandomWord(); + + timer = setInterval(() => { + time--; + timeEl.textContent = time; + if (time <= 0) { + clearInterval(timer); + gameActive = false; + messageEl.textContent = `โฐ Time's up! Final Score: ${score}`; + wordDisplay.textContent = "Game Over!"; + } + }, 1000); +} + +function restartGame() { + clearInterval(timer); + gameActive = false; + score = 0; + time = 60; + scoreEl.textContent = score; + timeEl.textContent = time; + input.value = ""; + messageEl.textContent = ""; + wordDisplay.textContent = "Press Start!"; +} + +input.addEventListener("input", () => { + if (!gameActive) return; + if (input.value.trim().toLowerCase() === currentWord.toLowerCase()) { + score++; + scoreEl.textContent = score; + input.value = ""; + pickRandomWord(); + } +}); + +startBtn.addEventListener("click", startGame); +restartBtn.addEventListener("click", restartGame); diff --git a/games/speedy-typing/style.css b/games/speedy-typing/style.css new file mode 100644 index 00000000..bbcb0966 --- /dev/null +++ b/games/speedy-typing/style.css @@ -0,0 +1,72 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea, #764ba2); + color: #fff; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; +} + +.typing-container { + background: rgba(0,0,0,0.4); + padding: 30px 40px; + border-radius: 15px; + text-align: center; + width: 100%; + max-width: 500px; + box-shadow: 0 0 20px rgba(0,0,0,0.5); +} + +h1 { + font-size: 2rem; + margin-bottom: 20px; +} + +.stats { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + font-weight: bold; +} + +.word-display { + font-size: 1.8rem; + margin-bottom: 20px; + padding: 15px; + background: rgba(255,255,255,0.1); + border-radius: 10px; + letter-spacing: 2px; +} + +input#typing-input { + width: 100%; + padding: 12px 15px; + font-size: 1.2rem; + border-radius: 10px; + border: none; + outline: none; + margin-bottom: 20px; +} + +.controls button { + padding: 10px 20px; + margin: 0 5px; + border: none; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + background-color: #ff6b6b; + color: #fff; + transition: 0.2s; +} + +.controls button:hover { + background-color: #ff4757; +} + +#message { + margin-top: 15px; + font-size: 1.1rem; +} diff --git a/games/spelling-champion/index.html b/games/spelling-champion/index.html new file mode 100644 index 00000000..71fa558c --- /dev/null +++ b/games/spelling-champion/index.html @@ -0,0 +1,93 @@ + + + + + + Spelling Champion - Mini JS Games Hub + + + +
    +
    +

    Spelling Champion

    +

    Test your spelling skills! Spell the words correctly to earn points.

    +
    + +
    +
    +
    + Score: + 0 +
    +
    + Words: + 0 + / + 10 +
    +
    + Streak: + 0 +
    +
    + Time: + 60 +
    +
    + +
    +
    General
    +
    Loading...
    +
    +
    + +
    + +
    + + + +
    +
    + +
    + +
    + + +
    +
    + + +
    + + + + \ No newline at end of file diff --git a/games/spelling-champion/script.js b/games/spelling-champion/script.js new file mode 100644 index 00000000..24e34cf8 --- /dev/null +++ b/games/spelling-champion/script.js @@ -0,0 +1,353 @@ +// Spelling Champion Game +// Test your spelling skills with words of varying difficulty + +// DOM elements +const scoreEl = document.getElementById('current-score'); +const wordCountEl = document.getElementById('current-word'); +const totalWordsEl = document.getElementById('total-words'); +const streakEl = document.getElementById('current-streak'); +const timeLeftEl = document.getElementById('time-left'); +const wordCategoryEl = document.getElementById('word-category'); +const wordTextEl = document.getElementById('word-text'); +const wordHintEl = document.getElementById('word-hint'); +const spellingInput = document.getElementById('spelling-input'); +const checkBtn = document.getElementById('check-btn'); +const hintBtn = document.getElementById('hint-btn'); +const skipBtn = document.getElementById('skip-btn'); +const startBtn = document.getElementById('start-btn'); +const quitBtn = document.getElementById('quit-btn'); +const messageEl = document.getElementById('message'); +const resultsEl = document.getElementById('results'); +const finalScoreEl = document.getElementById('final-score'); +const wordsCorrectEl = document.getElementById('words-correct'); +const wordsTotalEl = document.getElementById('words-total'); +const accuracyEl = document.getElementById('accuracy'); +const bestStreakEl = document.getElementById('best-streak'); +const gradeEl = document.getElementById('grade'); +const playAgainBtn = document.getElementById('play-again-btn'); + +// Game variables +let currentWordIndex = 0; +let score = 0; +let streak = 0; +let bestStreak = 0; +let timeLeft = 60; +let timerInterval = null; +let gameActive = false; +let words = []; +let currentWord = null; +let hintUsed = false; +let wordsCorrect = 0; + +// Spelling words database +const spellingWords = [ + // Easy words + { word: "cat", category: "Animals", difficulty: "easy", hint: "A common pet that says meow" }, + { word: "dog", category: "Animals", difficulty: "easy", hint: "A loyal pet that barks" }, + { word: "sun", category: "Nature", difficulty: "easy", hint: "It shines during the day" }, + { word: "moon", category: "Nature", difficulty: "easy", hint: "It shines at night" }, + { word: "book", category: "Objects", difficulty: "easy", hint: "You read this" }, + { word: "tree", category: "Nature", difficulty: "easy", hint: "It has leaves and branches" }, + { word: "house", category: "Buildings", difficulty: "easy", hint: "Where people live" }, + { word: "water", category: "Nature", difficulty: "easy", hint: "You drink this" }, + { word: "apple", category: "Food", difficulty: "easy", hint: "A red or green fruit" }, + { word: "school", category: "Places", difficulty: "easy", hint: "Where children learn" }, + + // Medium words + { word: "elephant", category: "Animals", difficulty: "medium", hint: "A large animal with a trunk" }, + { word: "computer", category: "Technology", difficulty: "medium", hint: "You use this to browse the internet" }, + { word: "beautiful", category: "Adjectives", difficulty: "medium", hint: "Something very pretty" }, + { word: "restaurant", category: "Places", difficulty: "medium", hint: "Where you eat food" }, + { word: "chocolate", category: "Food", difficulty: "medium", hint: "A sweet treat" }, + { word: "mountain", category: "Nature", difficulty: "medium", hint: "A very tall hill" }, + { word: "library", category: "Places", difficulty: "medium", hint: "A place with many books" }, + { word: "telephone", category: "Technology", difficulty: "medium", hint: "You use this to call people" }, + { word: "adventure", category: "Nouns", difficulty: "medium", hint: "An exciting experience" }, + { word: "happiness", category: "Emotions", difficulty: "medium", hint: "Feeling very joyful" }, + + // Hard words + { word: "chrysanthemum", category: "Plants", difficulty: "hard", hint: "A type of flower" }, + { word: "entrepreneurship", category: "Business", difficulty: "hard", hint: "Starting and running a business" }, + { word: "incomprehensible", category: "Adjectives", difficulty: "hard", hint: "Very difficult to understand" }, + { word: "responsibility", category: "Nouns", difficulty: "hard", hint: "Being accountable for something" }, + { word: "unbelievable", category: "Adjectives", difficulty: "hard", hint: "Extremely surprising" }, + { word: "pronunciation", category: "Language", difficulty: "hard", hint: "How to say a word correctly" }, + { word: "extraordinary", category: "Adjectives", difficulty: "hard", hint: "Very unusual or remarkable" }, + { word: "consciousness", category: "Psychology", difficulty: "hard", hint: "Being aware of your surroundings" }, + { word: "infrastructure", category: "Engineering", difficulty: "hard", hint: "Basic facilities and systems" }, + { word: "bureaucracy", category: "Government", difficulty: "hard", hint: "Complex administrative system" } +]; + +// Initialize game +function initGame() { + shuffleWords(); + setupEventListeners(); + updateDisplay(); +} + +// Shuffle words for random order +function shuffleWords() { + words = [...spellingWords]; + for (let i = words.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [words[i], words[j]] = [words[j], words[i]]; + } + // Take first 15 words + words = words.slice(0, 15); + totalWordsEl.textContent = words.length; +} + +// Setup event listeners +function setupEventListeners() { + startBtn.addEventListener('click', startGame); + checkBtn.addEventListener('click', checkSpelling); + hintBtn.addEventListener('click', useHint); + skipBtn.addEventListener('click', skipWord); + quitBtn.addEventListener('click', endGame); + playAgainBtn.addEventListener('click', resetGame); + + spellingInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + checkSpelling(); + } + }); + + spellingInput.addEventListener('input', () => { + if (gameActive) { + messageEl.textContent = ''; + messageEl.className = 'message'; + } + }); +} + +// Start the game +function startGame() { + gameActive = true; + currentWordIndex = 0; + score = 0; + streak = 0; + bestStreak = 0; + wordsCorrect = 0; + timeLeft = 60; + + startBtn.style.display = 'none'; + quitBtn.style.display = 'inline-block'; + checkBtn.disabled = false; + hintBtn.disabled = false; + skipBtn.disabled = false; + + resultsEl.style.display = 'none'; + messageEl.textContent = ''; + spellingInput.value = ''; + spellingInput.focus(); + + loadWord(); +} + +// Load current word +function loadWord() { + if (currentWordIndex >= words.length) { + endGame(); + return; + } + + currentWord = words[currentWordIndex]; + hintUsed = false; + + // Update UI + wordCategoryEl.textContent = currentWord.category; + wordTextEl.textContent = currentWord.word; + wordHintEl.textContent = ''; + + // Clear input and focus + spellingInput.value = ''; + spellingInput.focus(); + + updateDisplay(); +} + +// Check spelling +function checkSpelling() { + if (!gameActive) return; + + const userSpelling = spellingInput.value.trim().toLowerCase(); + const correctSpelling = currentWord.word.toLowerCase(); + + if (userSpelling === '') { + showMessage('Please type a spelling first!', 'incorrect'); + return; + } + + if (userSpelling === correctSpelling) { + correctAnswer(); + } else { + incorrectAnswer(); + } +} + +// Handle correct answer +function correctAnswer() { + wordsCorrect++; + streak++; + if (streak > bestStreak) bestStreak = streak; + + // Calculate points based on difficulty and time + let points = 10; // Base points + + // Difficulty bonus + if (currentWord.difficulty === 'hard') points *= 3; + else if (currentWord.difficulty === 'medium') points *= 2; + + // Streak bonus + if (streak >= 3) points += streak * 5; + + // Hint penalty + if (hintUsed) points = Math.floor(points * 0.7); + + score += points; + + showMessage(`Correct! +${points} points (Streak: ${streak})`, 'correct'); + wordTextEl.classList.add('correct-animation'); + + setTimeout(() => { + wordTextEl.classList.remove('correct-animation'); + nextWord(); + }, 1500); +} + +// Handle incorrect answer +function incorrectAnswer() { + streak = 0; + showMessage(`Incorrect! The correct spelling is: ${currentWord.word}`, 'incorrect'); + wordTextEl.classList.add('incorrect-animation'); + + setTimeout(() => { + wordTextEl.classList.remove('incorrect-animation'); + nextWord(); + }, 2500); +} + +// Use hint +function useHint() { + if (!gameActive || hintUsed || score < 20) return; + + if (score < 20) { + showMessage('Not enough points for hint! (20 points required)', 'incorrect'); + return; + } + + score -= 20; + hintUsed = true; + wordHintEl.textContent = currentWord.hint; + showMessage('Hint used! -20 points', 'hint'); + updateDisplay(); +} + +// Skip word +function skipWord() { + if (!gameActive || score < 10) return; + + if (score < 10) { + showMessage('Not enough points to skip! (10 points required)', 'incorrect'); + return; + } + + score -= 10; + streak = 0; + showMessage('Word skipped! -10 points', 'hint'); + updateDisplay(); + + setTimeout(nextWord, 1500); +} + +// Next word +function nextWord() { + currentWordIndex++; + loadWord(); +} + +// Show message +function showMessage(text, type) { + messageEl.textContent = text; + messageEl.className = `message ${type}`; +} + +// End game +function endGame() { + gameActive = false; + clearInterval(timerInterval); + + // Show results + showResults(); +} + +// Show final results +function showResults() { + const accuracy = words.length > 0 ? Math.round((wordsCorrect / words.length) * 100) : 0; + + finalScoreEl.textContent = score.toLocaleString(); + wordsCorrectEl.textContent = wordsCorrect; + wordsTotalEl.textContent = words.length; + accuracyEl.textContent = accuracy + '%'; + bestStreakEl.textContent = bestStreak; + + // Calculate grade + let grade = 'F'; + if (accuracy >= 90) grade = 'A'; + else if (accuracy >= 80) grade = 'B'; + else if (accuracy >= 70) grade = 'C'; + else if (accuracy >= 60) grade = 'D'; + + gradeEl.textContent = grade; + gradeEl.className = `final-value grade ${grade}`; + + resultsEl.style.display = 'block'; + startBtn.style.display = 'none'; + quitBtn.style.display = 'none'; + checkBtn.disabled = true; + hintBtn.disabled = true; + skipBtn.disabled = true; +} + +// Reset game +function resetGame() { + resultsEl.style.display = 'none'; + startBtn.style.display = 'inline-block'; + quitBtn.style.display = 'none'; + checkBtn.disabled = true; + hintBtn.disabled = true; + skipBtn.disabled = true; + + shuffleWords(); + updateDisplay(); + messageEl.textContent = 'Ready for another spelling challenge?'; +} + +// Update display elements +function updateDisplay() { + scoreEl.textContent = score.toLocaleString(); + wordCountEl.textContent = currentWordIndex + 1; + streakEl.textContent = streak; + timeLeftEl.textContent = timeLeft; +} + +// Start timer (optional - could be added later) +function startTimer() { + if (timerInterval) clearInterval(timerInterval); + + timerInterval = setInterval(() => { + timeLeft--; + timeLeftEl.textContent = timeLeft; + + if (timeLeft <= 0) { + clearInterval(timerInterval); + endGame(); + } + }, 1000); +} + +// Initialize the game +initGame(); + +// This spelling game includes word categories, hints, scoring, and streaks +// Players can test their spelling skills across different difficulty levels \ No newline at end of file diff --git a/games/spelling-champion/style.css b/games/spelling-champion/style.css new file mode 100644 index 00000000..3ca95994 --- /dev/null +++ b/games/spelling-champion/style.css @@ -0,0 +1,337 @@ +/* Spelling Champion Game Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.container { + max-width: 800px; + width: 100%; + background: white; + border-radius: 20px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +header { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + color: white; + padding: 30px; + text-align: center; +} + +header h1 { + font-size: 2.5rem; + margin-bottom: 10px; + font-weight: 700; +} + +header p { + font-size: 1.1rem; + opacity: 0.9; +} + +.game-area { + padding: 30px; +} + +.stats-panel { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 20px; + margin-bottom: 30px; + padding: 20px; + background: #f8f9fa; + border-radius: 15px; +} + +.stat { + display: flex; + align-items: center; + gap: 8px; + font-size: 1.1rem; + font-weight: 600; +} + +.stat-label { + color: #666; +} + +.stat-value { + color: #333; + font-weight: 700; +} + +.stat-separator { + color: #999; + margin: 0 5px; +} + +.word-display { + text-align: center; + margin-bottom: 30px; + padding: 30px; + background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); + border-radius: 15px; + position: relative; +} + +.word-category { + position: absolute; + top: 15px; + right: 15px; + background: rgba(255, 255, 255, 0.9); + padding: 5px 12px; + border-radius: 20px; + font-size: 0.9rem; + font-weight: 600; + color: #666; +} + +.word-text { + font-size: 3rem; + font-weight: 700; + color: #333; + margin-bottom: 10px; + letter-spacing: 2px; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1); +} + +.word-hint { + font-size: 1.2rem; + color: #666; + font-style: italic; + min-height: 1.5rem; +} + +.input-section { + margin-bottom: 20px; +} + +#spelling-input { + width: 100%; + padding: 15px 20px; + font-size: 1.5rem; + border: 3px solid #e0e0e0; + border-radius: 10px; + text-align: center; + font-weight: 600; + margin-bottom: 20px; + transition: all 0.3s ease; +} + +#spelling-input:focus { + outline: none; + border-color: #4facfe; + box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.1); +} + +.input-buttons { + display: flex; + gap: 15px; + justify-content: center; + flex-wrap: wrap; +} + +.primary-btn, .secondary-btn { + padding: 12px 24px; + border: none; + border-radius: 25px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.primary-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.primary-btn:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); +} + +.secondary-btn { + background: #f8f9fa; + color: #666; + border: 2px solid #e0e0e0; +} + +.secondary-btn:hover { + background: #e9ecef; + border-color: #dee2e6; +} + +.message { + text-align: center; + font-size: 1.2rem; + font-weight: 600; + min-height: 2rem; + margin: 20px 0; + padding: 15px; + border-radius: 10px; + transition: all 0.3s ease; +} + +.message.correct { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.message.incorrect { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.message.hint { + background: #fff3cd; + color: #856404; + border: 1px solid #ffeaa7; +} + +.controls { + text-align: center; + margin-top: 20px; +} + +.results { + padding: 30px; + text-align: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.results h2 { + font-size: 2rem; + margin-bottom: 30px; +} + +.final-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.final-stat { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + backdrop-filter: blur(10px); +} + +.final-label { + display: block; + font-size: 0.9rem; + opacity: 0.8; + margin-bottom: 5px; +} + +.final-value { + display: block; + font-size: 1.8rem; + font-weight: 700; +} + +.final-separator { + margin: 0 5px; +} + +.grade { + font-size: 2rem !important; +} + +.grade.A { color: #28a745; } +.grade.B { color: #17a2b8; } +.grade.C { color: #ffc107; } +.grade.D { color: #fd7e14; } +.grade.F { color: #dc3545; } + +/* Responsive Design */ +@media (max-width: 768px) { + header { + padding: 20px; + } + + header h1 { + font-size: 2rem; + } + + .game-area { + padding: 20px; + } + + .stats-panel { + flex-direction: column; + gap: 10px; + } + + .word-text { + font-size: 2.5rem; + } + + #spelling-input { + font-size: 1.2rem; + } + + .input-buttons { + flex-direction: column; + } + + .primary-btn, .secondary-btn { + width: 100%; + } + + .final-stats { + grid-template-columns: 1fr; + } +} + +/* Animations */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.word-display, .input-section, .message { + animation: fadeIn 0.5s ease-out; +} + +.correct-animation { + animation: correctPulse 0.6s ease-out; +} + +.incorrect-animation { + animation: incorrectShake 0.6s ease-out; +} + +@keyframes correctPulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +@keyframes incorrectShake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } +} \ No newline at end of file diff --git a/games/spot-the-difference/index.html b/games/spot-the-difference/index.html new file mode 100644 index 00000000..8380ab43 --- /dev/null +++ b/games/spot-the-difference/index.html @@ -0,0 +1,28 @@ + + + + + + Spot the Difference | Mini JS Games Hub + + + +
    +

    Spot the Difference

    +

    Find all differences! 0 found.

    +
    +
    + Original Image +
    +
    + Differences Image +
    +
    +

    Time Left: 60s

    + +

    +
    + + + + diff --git a/games/spot-the-difference/script.js b/games/spot-the-difference/script.js new file mode 100644 index 00000000..7c02e311 --- /dev/null +++ b/games/spot-the-difference/script.js @@ -0,0 +1,95 @@ +// Coordinates of differences (x, y, width, height) relative to image +const differences = [ + { x: 120, y: 80, w: 40, h: 40 }, + { x: 280, y: 150, w: 35, h: 35 }, + { x: 50, y: 220, w: 30, h: 30 }, + { x: 360, y: 300, w: 25, h: 25 }, + { x: 200, y: 350, w: 30, h: 30 } +]; + +let foundCount = 0; +const totalDiff = differences.length; +const img2 = document.getElementById('img2'); +const foundDisplay = document.getElementById('found'); +const totalDisplay = document.getElementById('total-diff'); +const timerDisplay = document.getElementById('timer'); +const message = document.getElementById('message'); +let timeLeft = 60; +let timer; + +totalDisplay.textContent = totalDiff; +foundDisplay.textContent = foundCount; + +// Start Timer +function startTimer() { + clearInterval(timer); + timeLeft = 60; + timerDisplay.textContent = timeLeft; + timer = setInterval(() => { + timeLeft--; + timerDisplay.textContent = timeLeft; + if (timeLeft <= 0) { + clearInterval(timer); + message.textContent = `โฐ Time's up! You found ${foundCount} out of ${totalDiff} differences.`; + } + }, 1000); +} + +// Check if click is inside a difference area +function isInDiff(x, y, diff) { + return x >= diff.x && x <= diff.x + diff.w && y >= diff.y && y <= diff.y + diff.h; +} + +// Handle click +img2.addEventListener('click', (e) => { + const rect = img2.getBoundingClientRect(); + const clickX = e.clientX - rect.left; + const clickY = e.clientY - rect.top; + + let hit = false; + differences.forEach((diff, index) => { + if (!diff.found && isInDiff(clickX, clickY, diff)) { + diff.found = true; + hit = true; + foundCount++; + foundDisplay.textContent = foundCount; + + const marker = document.createElement('div'); + marker.classList.add('found-marker'); + marker.style.left = `${diff.x}px`; + marker.style.top = `${diff.y}px`; + img2.parentElement.appendChild(marker); + + if (foundCount === totalDiff) { + clearInterval(timer); + message.textContent = "๐ŸŽ‰ Congratulations! You found all differences!"; + } + } + }); + + if (!hit) { + const wrongMarker = document.createElement('div'); + wrongMarker.classList.add('wrong-marker'); + wrongMarker.style.left = `${clickX - 15}px`; + wrongMarker.style.top = `${clickY - 15}px`; + img2.parentElement.appendChild(wrongMarker); + setTimeout(() => wrongMarker.remove(), 800); + } +}); + +// Restart game +document.getElementById('restart').addEventListener('click', () => { + foundCount = 0; + foundDisplay.textContent = foundCount; + message.textContent = ''; + differences.forEach(diff => diff.found = false); + + // Remove markers + document.querySelectorAll('.found-marker').forEach(el => el.remove()); + document.querySelectorAll('.wrong-marker').forEach(el => el.remove()); + + startTimer(); +}); + +// Initialize +startTimer(); diff --git a/games/spot-the-difference/style.css b/games/spot-the-difference/style.css new file mode 100644 index 00000000..e470cc75 --- /dev/null +++ b/games/spot-the-difference/style.css @@ -0,0 +1,89 @@ +body { + font-family: Arial, sans-serif; + background: #f0f4f8; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; +} + +.game-container { + text-align: center; + background: #fff; + padding: 20px 30px; + border-radius: 12px; + box-shadow: 0 5px 15px rgba(0,0,0,0.2); + max-width: 900px; +} + +h1 { + margin-bottom: 10px; + color: #333; +} + +.images-container { + display: flex; + justify-content: center; + gap: 20px; + margin: 20px 0; +} + +.image-wrapper { + position: relative; + border: 2px solid #ddd; + border-radius: 8px; + overflow: hidden; +} + +.image-wrapper img { + width: 400px; + height: auto; + display: block; +} + +.found-marker { + position: absolute; + width: 30px; + height: 30px; + border-radius: 50%; + background: rgba(0, 200, 0, 0.5); + border: 2px solid #0a0; + pointer-events: none; +} + +.wrong-marker { + position: absolute; + width: 30px; + height: 30px; + border-radius: 50%; + background: rgba(255, 0, 0, 0.5); + border: 2px solid #a00; + pointer-events: none; +} + +.timer { + font-weight: bold; + margin-top: 10px; +} + +button { + padding: 10px 15px; + margin-top: 15px; + font-size: 16px; + cursor: pointer; + border: none; + border-radius: 6px; + background-color: #007bff; + color: white; +} + +button:hover { + background-color: #0056b3; +} + +#message { + font-size: 18px; + font-weight: bold; + margin-top: 10px; +} diff --git a/games/spot_the_diff/index.html b/games/spot_the_diff/index.html new file mode 100644 index 00000000..b21917d6 --- /dev/null +++ b/games/spot_the_diff/index.html @@ -0,0 +1,40 @@ + + + + + + Spot the Difference Puzzle + + + + +
    +

    ๐Ÿ”Ž Spot the Difference

    + +
    + Score: 0 | Time Left: 60s +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    + +
    +

    Find the differences! Clicks on the right image are checked.

    +
    + +
    + +
    +
    + + + + \ No newline at end of file diff --git a/games/spot_the_diff/script.js b/games/spot_the_diff/script.js new file mode 100644 index 00000000..3c1696b5 --- /dev/null +++ b/games/spot_the_diff/script.js @@ -0,0 +1,181 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. GAME DATA (DIFFERENCE COORDINATES) --- + // Coordinates are normalized to the top-left corner (0, 0) of the image. + // The values (X, Y) are in pixels, corresponding to the CSS image size (400x300). + const differences = [ + { x: 50, y: 50, found: false }, // Top-left area + { x: 350, y: 80, found: false }, // Top-right area + { x: 150, y: 250, found: false }, // Bottom-middle area + { x: 280, y: 180, found: false } // Center area + // NOTE: You must match these coordinates to your actual image differences! + ]; + + // --- 2. DOM Elements & Constants --- + const imageRightOverlay = document.querySelector('#image-right .clickable-overlay'); + const puzzleArea = document.getElementById('puzzle-area'); + const scoreSpan = document.getElementById('score'); + const timerSpan = document.getElementById('timer'); + const feedbackMessage = document.getElementById('feedback-message'); + const startButton = document.getElementById('start-button'); + + const IMAGE_WIDTH = 400; // Must match CSS variable + const IMAGE_HEIGHT = 300; // Must match CSS variable + const TOTAL_DIFFERENCES = differences.length; + const CLICK_TOLERANCE = 30; // Max distance in pixels for a successful click + + // --- 3. Game State Variables --- + let score = 0; + let timeLeft = 0; + let timerInterval = null; + let gameActive = false; + + // --- 4. CORE FUNCTIONS --- + + /** + * Initializes the game state and UI for a new round. + */ + function initGame() { + // Reset state + score = 0; + timeLeft = 60; // 60 seconds + gameActive = true; + scoreSpan.textContent = score; + timerSpan.textContent = timeLeft; + feedbackMessage.textContent = 'Find the differences! Clicks on the right image are checked.'; + startButton.textContent = 'Restart'; + + // Reset difference state and remove markers + differences.forEach(d => d.found = false); + document.querySelectorAll('.difference-marker').forEach(m => m.remove()); + + // Add markers for the right image (initially hidden) + renderDifferenceMarkers(); + + // Start timer + startTimer(); + } + + /** + * Creates and attaches the hidden markers to the puzzle area for later display. + */ + function renderDifferenceMarkers() { + const rightImage = document.getElementById('image-right'); + + // Get the offset of the right image relative to the puzzle-area container + // This is needed because markers are positioned absolutely within #puzzle-area + const puzzleAreaRect = puzzleArea.getBoundingClientRect(); + const rightImageRect = rightImage.getBoundingClientRect(); + + const offsetLeft = rightImageRect.left - puzzleAreaRect.left; + const offsetTop = rightImageRect.top - puzzleAreaRect.top; + + differences.forEach((diff, index) => { + const marker = document.createElement('div'); + marker.classList.add('difference-marker'); + marker.setAttribute('data-index', index); + + // Set the absolute position within the #puzzle-area container + marker.style.left = `${diff.x + offsetLeft}px`; + marker.style.top = `${diff.y + offsetTop}px`; + + puzzleArea.appendChild(marker); + }); + } + + /** + * Starts the countdown timer. + */ + function startTimer() { + clearInterval(timerInterval); + + timerInterval = setInterval(() => { + timeLeft--; + timerSpan.textContent = timeLeft; + + if (timeLeft <= 0) { + endGame(false); // Time's up! + } + }, 1000); + } + + /** + * Handles the click event on the right image overlay. + */ + function handleImageClick(event) { + if (!gameActive) return; + + // Get the coordinates relative to the top-left of the image itself + const clickX = event.offsetX; + const clickY = event.offsetY; + + let differenceFound = false; + + differences.forEach((diff, index) => { + // Calculate the distance between the click point and the difference point + const dx = clickX - diff.x; + const dy = clickY - diff.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + // Check if the click is within the tolerance radius AND if the difference hasn't been found yet + if (distance <= CLICK_TOLERANCE && !diff.found) { + diff.found = true; + differenceFound = true; + score++; + scoreSpan.textContent = score; + + // Highlight the found difference on the screen + const marker = document.querySelector(`.difference-marker[data-index="${index}"]`); + if (marker) { + marker.classList.add('found'); + } + + feedbackMessage.textContent = `โœ… Found one! (${TOTAL_DIFFERENCES - score} left)`; + + // Check win condition + if (score === TOTAL_DIFFERENCES) { + endGame(true); + } + } + }); + + if (!differenceFound && score < TOTAL_DIFFERENCES) { + feedbackMessage.textContent = `โŒ Miss! Keep looking.`; + } + } + + /** + * Ends the game and displays the final status. + */ + function endGame(win) { + gameActive = false; + clearInterval(timerInterval); + + if (win) { + feedbackMessage.innerHTML = '๐Ÿ† **PUZZLE SOLVED!** You found all the differences.'; + feedbackMessage.style.color = '#4CAF50'; + } else { + feedbackMessage.innerHTML = 'โŒ› **TIME\'S UP!** Game Over.'; + feedbackMessage.style.color = '#f44336'; + // Show all markers that were missed + document.querySelectorAll('.difference-marker:not(.found)').forEach(m => m.style.opacity = 0.5); + } + } + + // --- 5. EVENT LISTENERS --- + + // Listen for clicks only on the right image's overlay + imageRightOverlay.addEventListener('click', handleImageClick); + + startButton.addEventListener('click', () => { + if (gameActive) { + // Restart functionality + clearInterval(timerInterval); + initGame(); + } else { + initGame(); + } + }); + + // Initial setup message + feedbackMessage.textContent = `Ready to find ${TOTAL_DIFFERENCES} differences?`; +}); \ No newline at end of file diff --git a/games/spot_the_diff/style.css b/games/spot_the_diff/style.css new file mode 100644 index 00000000..e0e1a553 --- /dev/null +++ b/games/spot_the_diff/style.css @@ -0,0 +1,118 @@ +:root { + --image-width: 400px; /* Width of a single image */ + --image-height: 300px; /* Height of a single image */ + --image-url: url('images/main-puzzle-image.jpg'); /* !!! UPDATE THIS PATH !!! */ +} + +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f0f4f8; + color: #333; +} + +#game-container { + background-color: white; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); + text-align: center; + max-width: calc(2 * var(--image-width) + 80px); /* 2 images + gap + padding */ + width: 90%; +} + +h1 { + color: #007bff; + margin-bottom: 20px; +} + +#status-area { + font-size: 1.2em; + font-weight: 600; + margin-bottom: 20px; +} + +/* --- Puzzle Area and Images --- */ +#puzzle-area { + display: flex; + gap: 20px; + margin-bottom: 20px; + position: relative; /* Crucial for positioning the markers absolutely */ + padding: 10px; + border: 2px solid #ccc; + border-radius: 8px; +} + +.image-box { + width: var(--image-width); + height: var(--image-height); + background-image: var(--image-url); + background-size: cover; + border: 1px solid #999; + position: relative; +} + +/* The invisible layer for intercepting clicks */ +.clickable-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10; + /* We only listen for clicks on the right image */ + cursor: pointer; +} + +#image-left .clickable-overlay { + /* Clicks on the left image do nothing */ + cursor: default; +} + +/* --- Difference Marker Styling --- */ +.difference-marker { + position: absolute; + width: 30px; + height: 30px; + border-radius: 50%; + border: 3px solid #f44336; /* Red ring */ + box-shadow: 0 0 10px rgba(255, 0, 0, 0.8); + pointer-events: none; /* Allows clicks to pass through to the overlay */ + transition: opacity 0.3s; + opacity: 0; /* Hidden initially */ + z-index: 20; + /* Center the circle on the coordinate */ + transform: translate(-50%, -50%); +} + +.difference-marker.found { + border-color: #4CAF50; /* Green when found */ + box-shadow: 0 0 10px rgba(0, 255, 0, 0.8); + opacity: 1; /* Visible when found */ +} + +/* --- Feedback and Controls --- */ +#feedback-message { + min-height: 1.5em; + margin-bottom: 20px; +} + +#start-button { + padding: 12px 25px; + font-size: 1.2em; + font-weight: bold; + background-color: #28a745; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +#start-button:hover { + background-color: #1e7e34; +} \ No newline at end of file diff --git a/games/stack-tower/index.html b/games/stack-tower/index.html new file mode 100644 index 00000000..ae9d6f1a --- /dev/null +++ b/games/stack-tower/index.html @@ -0,0 +1,32 @@ + + + + + + Stack Tower + + + +
    + +
    + + +
    +
    + + + \ No newline at end of file diff --git a/games/stack-tower/script.js b/games/stack-tower/script.js new file mode 100644 index 00000000..ae703d4c --- /dev/null +++ b/games/stack-tower/script.js @@ -0,0 +1,213 @@ +var canvas = document.getElementById('game-canvas'); +var ctx = canvas.getContext('2d'); +var scoreEl = document.getElementById('score-value'); +var finalScoreEl = document.getElementById('final-score'); +var restartBtn = document.getElementById('restart-btn'); +var playAgainBtn = document.getElementById('play-again-btn'); +var gameOverDiv = document.getElementById('game-over'); + +var blocks = []; +var particles = []; +var currentBlock = null; +var score = 0; +var gameOver = false; +var gravity = 0.5; +var blockWidth = 100; +var blockHeight = 20; +var baseY = canvas.height - 50; +var cameraY = 0; +var colors = ['#e74c3c', '#f39c12', '#f1c40f', '#2ecc71', '#3498db', '#9b59b6', '#e67e22']; + +function Block(x, y, width, height, color) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.color = color; + this.vx = 0; + this.vy = 0; + this.stable = true; + this.rotation = 0; + this.rotationSpeed = 0; +} + +Block.prototype.draw = function() { + ctx.save(); + ctx.translate(this.x + this.width / 2, this.y + this.height / 2 - cameraY); + ctx.rotate(this.rotation); + ctx.fillStyle = this.color; + ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 2; + ctx.strokeRect(-this.width / 2, -this.height / 2, this.width, this.height); + ctx.restore(); +}; + +Block.prototype.update = function() { + if (!this.stable) { + this.vy += gravity; + this.y += this.vy; + this.x += this.vx; + this.rotation += this.rotationSpeed; + if (this.y > canvas.height + 100) { + gameOver = true; + } + } +}; + +function Particle(x, y, vx, vy, color, life) { + this.x = x; + this.y = y; + this.vx = vx; + this.vy = vy; + this.color = color; + this.life = life; + this.maxLife = life; +} + +Particle.prototype.update = function() { + this.x += this.vx; + this.y += this.vy; + this.vy += 0.1; + this.life--; +}; + +Particle.prototype.draw = function() { + var alpha = this.life / this.maxLife; + ctx.save(); + ctx.globalAlpha = alpha; + ctx.fillStyle = this.color; + ctx.fillRect(this.x, this.y - cameraY, 4, 4); + ctx.restore(); +}; + +function createParticles(x, y, color) { + for (var i = 0; i < 10; i++) { + particles.push(new Particle(x + Math.random() * 20 - 10, y, (Math.random() - 0.5) * 5, -Math.random() * 5, color, 60)); + } +} + +function init() { + blocks = []; + particles = []; + currentBlock = new Block(canvas.width / 2 - blockWidth / 2, 50, blockWidth, blockHeight, colors[Math.floor(Math.random() * colors.length)]); + score = 0; + gameOver = false; + cameraY = 0; + updateScore(); + gameOverDiv.classList.add('hidden'); +} + +function updateScore() { + scoreEl.textContent = score; +} + +function placeBlock() { + if (gameOver || !currentBlock) return; + + var prevBlock = blocks[blocks.length - 1]; + if (prevBlock) { + var overlap = Math.min(currentBlock.x + currentBlock.width, prevBlock.x + prevBlock.width) - Math.max(currentBlock.x, prevBlock.x); + if (overlap < currentBlock.width * 0.4) { + // Misplaced, make it fall + currentBlock.stable = false; + currentBlock.vx = (Math.random() - 0.5) * 6; + currentBlock.rotationSpeed = (Math.random() - 0.5) * 0.1; + createParticles(currentBlock.x + currentBlock.width / 2, currentBlock.y, currentBlock.color); + } else { + // Adjust position and size + currentBlock.x = Math.max(currentBlock.x, prevBlock.x); + currentBlock.width = overlap; + currentBlock.stable = true; + } + } else { + currentBlock.stable = true; + } + + blocks.push(currentBlock); + score++; + updateScore(); + + // Adjust camera + if (currentBlock.y - cameraY < 100) { + cameraY = currentBlock.y - 100; + } + + if (currentBlock.stable) { + currentBlock = new Block(canvas.width / 2 - blockWidth / 2, currentBlock.y - blockHeight, blockWidth, blockHeight, colors[Math.floor(Math.random() * colors.length)]); + } else { + currentBlock = null; + } +} + +function update() { + if (gameOver) return; + + blocks.forEach(function(block) { + block.update(); + }); + + if (currentBlock) { + currentBlock.update(); + } + + particles = particles.filter(function(p) { + p.update(); + return p.life > 0; + }); +} + +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw sky gradient + var gradient = ctx.createLinearGradient(0, 0, 0, canvas.height); + gradient.addColorStop(0, '#87ceeb'); + gradient.addColorStop(1, '#fff'); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw base + ctx.fillStyle = '#2c3e50'; + ctx.fillRect(0, baseY - cameraY, canvas.width, canvas.height - baseY + cameraY); + + blocks.forEach(function(block) { + block.draw(); + }); + + if (currentBlock) { + currentBlock.draw(); + } + + particles.forEach(function(p) { + p.draw(); + }); + + if (gameOver) { + finalScoreEl.textContent = score; + gameOverDiv.classList.remove('hidden'); + } +} + +function gameLoop() { + update(); + draw(); + requestAnimationFrame(gameLoop); +} + +canvas.addEventListener('mousemove', function(e) { + if (currentBlock && currentBlock.stable) { + var rect = canvas.getBoundingClientRect(); + var x = e.clientX - rect.left; + currentBlock.x = x - currentBlock.width / 2; + currentBlock.x = Math.max(0, Math.min(canvas.width - currentBlock.width, currentBlock.x)); + } +}); + +canvas.addEventListener('click', placeBlock); + +restartBtn.addEventListener('click', init); +playAgainBtn.addEventListener('click', init); + +init(); +gameLoop(); \ No newline at end of file diff --git a/games/stack-tower/style.css b/games/stack-tower/style.css new file mode 100644 index 00000000..a981a292 --- /dev/null +++ b/games/stack-tower/style.css @@ -0,0 +1,139 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + margin: 0; + padding: 0; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + color: #fff; +} + +#game-wrapper { + display: flex; + background: rgba(0, 0, 0, 0.8); + border-radius: 20px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); + overflow: hidden; + max-width: 800px; + width: 90%; +} + +#sidebar { + flex: 1; + padding: 30px; + background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-width: 250px; +} + +h1 { + font-size: 2.5em; + margin-bottom: 20px; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); + background: linear-gradient(45deg, #f39c12, #e74c3c); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +#score { + font-size: 1.5em; + margin: 20px 0; + text-align: center; +} + +#score-value { + font-weight: bold; + color: #f39c12; +} + +#instructions { + text-align: center; + margin: 20px 0; + line-height: 1.6; +} + +#restart-btn, #play-again-btn { + padding: 12px 24px; + font-size: 16px; + background: linear-gradient(45deg, #27ae60, #2ecc71); + color: white; + border: none; + border-radius: 25px; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + font-weight: bold; +} + +#restart-btn:hover, #play-again-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); +} + +#game-area { + flex: 2; + position: relative; + display: flex; + justify-content: center; + align-items: center; + background: #1a1a1a; +} + +#game-canvas { + border: none; + background: linear-gradient(to bottom, #87ceeb, #fff); + border-radius: 10px; + box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.3); + cursor: crosshair; +} + +#game-over { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + background: rgba(0, 0, 0, 0.9); + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + animation: fadeIn 0.5s ease; +} + +#game-over h2 { + margin: 0 0 10px 0; + font-size: 2em; + color: #e74c3c; +} + +#game-over p { + margin: 10px 0; + font-size: 1.2em; +} + +.hidden { + display: none; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translate(-50%, -60%); } + to { opacity: 1; transform: translate(-50%, -50%); } +} + +@media (max-width: 768px) { + #game-wrapper { + flex-direction: column; + } + #sidebar { + padding: 20px; + } + h1 { + font-size: 2em; + } +} \ No newline at end of file diff --git a/games/star-chain-reaction/index.html b/games/star-chain-reaction/index.html new file mode 100644 index 00000000..990f0006 --- /dev/null +++ b/games/star-chain-reaction/index.html @@ -0,0 +1,78 @@ + + + + + + Star Chain Reaction โ€” Mini JS Games Hub + + + + + + +
    +
    +
    +

    Star Chain Reaction โœจ

    +

    One click. One chain. Catch the stars.

    +
    +
    +
    + + + + + + + +
    + +
    +
    Score: 0
    +
    High: 0
    +
    Caught: 0
    +
    +
    +
    + +
    + + + +
    + +
    +
    Made with โค๏ธ โ€” Mini JS Games Hub
    + +
    +
    + + + + diff --git a/games/star-chain-reaction/script.js b/games/star-chain-reaction/script.js new file mode 100644 index 00000000..eea58f48 --- /dev/null +++ b/games/star-chain-reaction/script.js @@ -0,0 +1,448 @@ +// script.js (Star Chain Reaction) +// Path: games/star-chain-reaction/script.js +// Uses Canvas API, requestAnimationFrame, and online sound assets. + +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d', { alpha: true }); + +let W, H, rafId; +function resize() { + W = canvas.width = Math.floor(canvas.clientWidth * devicePixelRatio); + H = canvas.height = Math.floor(canvas.clientHeight * devicePixelRatio); + ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0); +} +window.addEventListener('resize', () => { resize(); }); + +resize(); + +/* -------------------------- + Online sound assets (Google Actions public sounds) + -------------------------- */ +const sounds = { + click: new Audio('https://actions.google.com/sounds/v1/buttons/button_press.ogg'), + explode: new Audio('https://actions.google.com/sounds/v1/explosions/explosion_crunch.ogg'), + catch: new Audio('https://actions.google.com/sounds/v1/cartoon/clang_and_wobble.ogg'), + particle: new Audio('https://actions.google.com/sounds/v1/ambiences/whoosh.ogg') +}; +sounds.click.volume = 0.6; +sounds.explode.volume = 0.6; +sounds.catch.volume = 0.6; +sounds.particle.volume = 0.16; + +let muted = false; +const muteBtn = document.getElementById('muteBtn'); +muteBtn.addEventListener('click', () => { + muted = !muted; + muteBtn.textContent = muted ? '๐Ÿ”‡' : '๐Ÿ”Š'; +}); +function playSound(name) { + if (muted) return; + const s = sounds[name]; + if (!s) return; + try { + s.currentTime = 0; + s.play().catch(()=>{/* autoplay block might stop first play */}); + } catch (e) {} +} + +/* -------------------------- + Game variables + -------------------------- */ + +const scoreEl = document.getElementById('score'); +const highEl = document.getElementById('highscore'); +const caughtEl = document.getElementById('caught'); +const starCountEl = document.getElementById('starCount'); +const attemptsLeftEl = document.getElementById('attemptsLeft'); +const startBtn = document.getElementById('startBtn'); +const pauseBtn = document.getElementById('pauseBtn'); +const restartBtn = document.getElementById('restartBtn'); +const levelSelect = document.getElementById('level'); +const visualGlow = document.getElementById('visualGlow'); +const particlesOn = document.getElementById('particlesOn'); + +let stars = []; +let explosions = []; +let particles = []; +let running = false; +let paused = false; +let score = 0; +let caught = 0; +let highScore = parseInt(localStorage.getItem('scr_high')||"0",10) || 0; +highEl.textContent = highScore; +let attemptsLeft = 1; + +const LEVELS = [ + { count: 14, speed: 0.6 }, + { count: 22, speed: 1.0 }, + { count: 30, speed: 1.7 }, + { count: 46, speed: 2.4 } +]; + +/* Helper utils */ +function rand(min, max){ return Math.random()*(max-min)+min; } +function dist(a,b){ const dx=a.x-b.x, dy=a.y-b.y; return Math.sqrt(dx*dx+dy*dy); } + +/* Star Class */ +class Star { + constructor(x,y,radius=8,speed=1){ + this.x = x; + this.y = y; + this.r = radius; + const ang = rand(0, Math.PI*2); + this.vx = Math.cos(ang)*speed; + this.vy = Math.sin(ang)*speed; + this.colorHue = Math.floor(rand(180, 360)); + this.caught = false; + this.exploding = false; + this.born = Date.now(); + this.glow = rand(6, 18); + } + step(dt){ + if(this.caught || this.exploding) return; + this.x += this.vx * dt; + this.y += this.vy * dt; + // bounce edges + if(this.x < this.r || this.x > canvas.clientWidth - this.r){ + this.vx *= -1; this.x = Math.max(this.r, Math.min(canvas.clientWidth - this.r, this.x)); + } + if(this.y < this.r || this.y > canvas.clientHeight - this.r){ + this.vy *= -1; this.y = Math.max(this.r, Math.min(canvas.clientHeight - this.r, this.y)); + } + } + draw(ctx){ + ctx.save(); + const glowOn = visualGlow.checked; + if(glowOn){ + ctx.shadowColor = `hsla(${this.colorHue}, 90%, 60%, 0.9)`; + ctx.shadowBlur = this.glow + 12; + } else { + ctx.shadowBlur = 0; + } + + // star body (draw 5-point star) + drawStar(ctx, this.x, this.y, 5, this.r, this.r*0.45, `hsl(${this.colorHue}, 95%, 60%)`); + ctx.restore(); + } + explode(){ + if(this.exploding) return; + this.exploding = true; + // spawn explosion circle + explosions.push(new Explosion(this.x, this.y)); + // spawn particles + if(particlesOn.checked){ + for(let i=0;i<18;i++){ + particles.push(new Particle(this.x, this.y, this.colorHue)); + } + } + playSound('explode'); + // mark as caught + this.caught = true; + caught++; + score += 10; + updateUI(); + } +} + +/* Explosion circle - expands and then fades */ +class Explosion { + constructor(x,y){ + this.x=x; this.y=y; + this.radius = 10; + this.maxRadius = rand(60, 95); + this.growth = rand(160, 260); + this.life = 0; + this.alpha = 1; + } + step(dt){ + this.radius += (this.growth * dt) / 1000; + this.life += dt; + if(this.radius >= this.maxRadius){ + this.alpha -= dt/500; + } + } + draw(ctx){ + ctx.save(); + const grd = ctx.createRadialGradient(this.x,this.y,this.radius*0.2,this.x,this.y,this.radius); + grd.addColorStop(0, `rgba(255,230,150,${0.85*this.alpha})`); + grd.addColorStop(0.2, `rgba(255,120,60,${0.6*this.alpha})`); + grd.addColorStop(1, `rgba(255,40,30,${0.02*this.alpha})`); + ctx.beginPath(); + ctx.fillStyle = grd; + ctx.globalCompositeOperation = 'lighter'; + ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); + ctx.fill(); + + // rim glow + ctx.lineWidth = 2; + ctx.strokeStyle = `rgba(255,255,255,${0.06*this.alpha})`; + ctx.stroke(); + ctx.restore(); + } +} + +/* Particle */ +class Particle { + constructor(x,y,hue){ + this.x=x;this.y=y; + const a=rand(0,Math.PI*2), s=rand(0.6,2.2); + this.vx=Math.cos(a)*s*rand(30,90)/60; + this.vy=Math.sin(a)*s*rand(30,90)/60; + this.life=rand(600,1400); + this.age=0; + this.size = rand(1.2,3.2); + this.hue = hue||rand(200,340); + this.alpha = 1; + } + step(dt){ + this.age += dt; + this.x += this.vx*dt/16; + this.y += this.vy*dt/16; + this.vy += 0.012*dt/16; + this.alpha = Math.max(0, 1 - this.age / this.life); + } + draw(ctx){ + ctx.save(); + ctx.globalAlpha = this.alpha; + ctx.fillStyle = `hsl(${this.hue}, 90%, 60%)`; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI*2); + ctx.fill(); + ctx.restore(); + } +} + +/* utility: draw a n-point star at x,y */ +function drawStar(ctx, x, y, points, outerR, innerR, fillStyle){ + ctx.save(); + ctx.beginPath(); + const step = Math.PI / points; + for(let i=0;i<2*points;i++){ + const r = i%2===0 ? outerR : innerR; + const a = i*step - Math.PI/2; + const px = x + Math.cos(a)*r; + const py = y + Math.sin(a)*r; + if(i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py); + } + ctx.closePath(); + ctx.fillStyle = fillStyle; + ctx.fill(); + ctx.restore(); +} + +/* -------------------------- + Boot / Round management + -------------------------- */ +function spawnStars(count, lvlSpeed){ + stars = []; + for(let i=0;i { + if(!running || paused) return; + const rect = canvas.getBoundingClientRect(); + const x = (e.clientX - rect.left); + const y = (e.clientY - rect.top); + sounds.click && playSound('click'); + // spawn initial explosion with stronger radius + const ex = new Explosion(x, y); + ex.maxRadius = rand(120, 180); + ex.growth = rand(240,400); + explosions.push(ex); +}); + +/* Collision detection */ +function checkCollisions(){ + // for each explosion, check stars within radius + for(let i=explosions.length-1;i>=0;i--){ + const e = explosions[i]; + if(e.alpha <= 0.02) continue; + for(let j=0;j e.alpha > 0.02); + particles = particles.filter(p => p.alpha > 0.02); + + // rendering + ctx.clearRect(0,0,canvas.clientWidth, canvas.clientHeight); + + // subtle background overlay for more contrast + ctx.save(); + ctx.fillStyle = 'rgba(0,0,0,0.12)'; + ctx.fillRect(0,0,canvas.clientWidth, canvas.clientHeight); + ctx.restore(); + + // draw explosions under stars (so glow shows from behind) + for(const e of explosions) e.draw(ctx); + // draw particles + for(const p of particles) p.draw(ctx); + // draw stars + for(const s of stars) s.draw(ctx); + + // optional HUD small hint + ctx.save(); + ctx.font = '12px Inter, sans-serif'; + ctx.fillStyle = 'rgba(255,255,255,0.06)'; + ctx.fillText('Click once to start a chain reaction', 12, canvas.clientHeight - 12); + ctx.restore(); +} + +/* End round detection: if all stars are caught or time passes (optional) */ +function checkRoundEnd(){ + if(!running) return; + const remaining = stars.filter(s=>!s.caught).length; + if(remaining === 0){ + // round finished + running = false; + startBtn.disabled = false; + updateUI(); + // award bonus based on chain strength + playSound('catch'); + // update highscore + highScore = Math.max(highScore, score); + localStorage.setItem('scr_high', String(highScore)); + setTimeout(()=> { + alert(`Round complete!\nScore: ${score}\nHigh: ${highScore}`); + }, 150); + } +} + +/* animation loop wrapper with round end checks */ +function loopWrapper(t){ + loop(t); + checkRoundEnd(); +} +function loopStart(){ + lastTime = performance.now(); + rafId = requestAnimationFrame(loopWrapper); +} + +/* attach UI buttons */ +startBtn.addEventListener('click', () => { + if(!running) { + startRound(); + loopStart(); + } +}); +pauseBtn.addEventListener('click', pauseToggle); +restartBtn.addEventListener('click', () => { + restartRound(); +}); + +/* quick start on page load for UX */ +window.addEventListener('load', () => { + // initial spawn (preview) + spawnStars(LEVELS[1].count, LEVELS[1].speed); + updateUI(); + // small gentle animation preview + lastTime = performance.now(); + running = false; + paused = false; + // allow spacebar to toggle start/pause + window.addEventListener('keydown', (e) => { + if(e.code === 'Space'){ e.preventDefault(); if(!running) { startBtn.click(); } else pauseBtn.click(); } + }); +}); + +/* small interval to check round end too */ +setInterval(checkRoundEnd, 800); + +/* expose for debug */ +window.SCR = { startRound, restartRound, togglePause: pauseToggle }; + diff --git a/games/star-chain-reaction/style.css b/games/star-chain-reaction/style.css new file mode 100644 index 00000000..81b5bd5d --- /dev/null +++ b/games/star-chain-reaction/style.css @@ -0,0 +1,112 @@ +:root{ + --bg:#04020a; + --panel:#0f1724; + --accent:#ffd166; + --muted:#9aa8b2; + --glass: rgba(255,255,255,0.03); + --glass-2: rgba(255,255,255,0.02); + --radius:14px; +} + +*{box-sizing:border-box} +html,body,#gameCanvas{height:100%} +body{ + margin:0; + font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial; + background: radial-gradient(circle at 10% 20%, rgba(34,15,78,0.45), transparent 10%), + radial-gradient(circle at 90% 80%, rgba(12,90,120,0.25), transparent 15%), + var(--bg); + color:#e6eef6; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + min-height:100vh; + display:flex; + align-items:stretch; + flex-direction:column; +} + +/* topbar */ +.topbar{ + display:flex; + justify-content:space-between; + align-items:center; + padding:18px 28px; + gap:12px; + backdrop-filter: blur(6px); +} +.topbar .left h1{ + margin:0;font-size:20px;letter-spacing:0.2px; +} +.subtitle{margin:2px 0 0;color:var(--muted);font-size:13px} + +/* controls */ +.controls-row{display:flex;align-items:center;gap:8px} +.controls-row select, .controls-row .btn{ + padding:8px 10px;border-radius:10px;border:1px solid rgba(255,255,255,0.06); + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + color:inherit; font-size:13px; +} +.btn{cursor:pointer} +.btn.primary{background:linear-gradient(180deg,var(--accent),#ffb74a);color:#03060a;border:none;box-shadow:0 6px 18px rgba(255,177,64,0.14)} + +/* scores */ +.score-row{display:flex;gap:14px;align-items:center;margin-top:8px} +.score-row .score,.score-row .highscore,.score-row .catches{font-weight:600;background:var(--glass);padding:8px 12px;border-radius:10px;color:#fff} + +/* main area */ +.game-area{ + display:flex; + gap:18px; + padding:14px; + flex:1; + position:relative; +} + +/* canvas area */ +canvas#gameCanvas{ + flex:1; + border-radius:16px; + background-image: url("https://images.unsplash.com/photo-1454789548928-9efd52dc4031?auto=format&fit=crop&w=1600&q=80"); + background-size:cover; + background-position:center; + box-shadow: + inset 0 0 80px rgba(255,255,255,0.02), + 0 12px 30px rgba(2,6,23,0.6); + display:block; + width:100%; + height:100%; +} + +/* right panel */ +.right-panel{ + width:300px; + display:flex; + flex-direction:column; + gap:12px; +} +.panel-card{ + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border-radius:12px; + padding:12px; + backdrop-filter: blur(6px); + border:1px solid rgba(255,255,255,0.03); +} +.panel-card h3{margin:0 0 8px 0} +.panel-card.small p{margin:6px 0;color:var(--muted)} +.panel-card ul{margin:0;padding-left:18px;color:var(--muted);font-size:14px} + +.footer{ + display:flex; + justify-content:space-between; + align-items:center; + padding:10px 18px; + color:var(--muted); + font-size:13px; + border-top:1px solid rgba(255,255,255,0.02); +} + +/* responsive */ +@media (max-width:960px){ + .right-panel{display:none} + canvas#gameCanvas{border-radius:0} +} diff --git a/games/star-collector/index.html b/games/star-collector/index.html new file mode 100644 index 00000000..c2958f93 --- /dev/null +++ b/games/star-collector/index.html @@ -0,0 +1,26 @@ + + + + + + Star Collector + + + +
    +

    Star Collector

    +
    Score: 0
    + +
    +

    Use arrow keys to move your spaceship. Collect yellow stars to score points, avoid gray asteroids!

    +

    Stars: +10 points | Asteroids: Game Over

    +
    + +
    + + + \ No newline at end of file diff --git a/games/star-collector/script.js b/games/star-collector/script.js new file mode 100644 index 00000000..433ac2fd --- /dev/null +++ b/games/star-collector/script.js @@ -0,0 +1,199 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const scoreElement = document.getElementById('score'); +const gameOverElement = document.getElementById('gameOver'); +const finalScoreElement = document.getElementById('finalScore'); +const restartBtn = document.getElementById('restartBtn'); + +let score = 0; +let gameRunning = true; +let player = { x: canvas.width / 2, y: canvas.height - 50, width: 30, height: 20 }; +let stars = []; +let asteroids = []; +let keys = {}; + +class Star { + constructor(x, y) { + this.x = x; + this.y = y; + this.radius = 8; + this.speed = 2; + } + + update() { + this.y += this.speed; + if (this.y > canvas.height) { + stars = stars.filter(s => s !== this); + } + } + + draw() { + ctx.fillStyle = '#ffd700'; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fill(); + // Add sparkle effect + ctx.fillStyle = '#ffffff'; + ctx.beginPath(); + ctx.arc(this.x - 3, this.y - 3, 2, 0, Math.PI * 2); + ctx.arc(this.x + 3, this.y - 3, 2, 0, Math.PI * 2); + ctx.arc(this.x - 3, this.y + 3, 2, 0, Math.PI * 2); + ctx.arc(this.x + 3, this.y + 3, 2, 0, Math.PI * 2); + ctx.fill(); + } + + collidesWith(player) { + return this.x < player.x + player.width && + this.x + this.radius * 2 > player.x && + this.y < player.y + player.height && + this.y + this.radius * 2 > player.y; + } +} + +class Asteroid { + constructor(x, y) { + this.x = x; + this.y = y; + this.size = Math.random() * 20 + 15; + this.speed = Math.random() * 2 + 1; + this.rotation = 0; + this.rotationSpeed = Math.random() * 0.1 - 0.05; + } + + update() { + this.y += this.speed; + this.rotation += this.rotationSpeed; + if (this.y > canvas.height) { + asteroids = asteroids.filter(a => a !== this); + } + } + + draw() { + ctx.save(); + ctx.translate(this.x, this.y); + ctx.rotate(this.rotation); + ctx.fillStyle = '#666666'; + ctx.beginPath(); + // Irregular asteroid shape + ctx.moveTo(0, -this.size); + for (let i = 0; i < 8; i++) { + const angle = (i / 8) * Math.PI * 2; + const radius = this.size * (0.8 + Math.random() * 0.4); + ctx.lineTo(Math.cos(angle) * radius, Math.sin(angle) * radius); + } + ctx.closePath(); + ctx.fill(); + ctx.strokeStyle = '#999999'; + ctx.lineWidth = 2; + ctx.stroke(); + ctx.restore(); + } + + collidesWith(player) { + return this.x - this.size < player.x + player.width && + this.x + this.size > player.x && + this.y - this.size < player.y + player.height && + this.y + this.size > player.y; + } +} + +function drawPlayer() { + ctx.fillStyle = '#00ff00'; + ctx.beginPath(); + ctx.moveTo(player.x + player.width / 2, player.y); + ctx.lineTo(player.x, player.y + player.height); + ctx.lineTo(player.x + player.width, player.y + player.height); + ctx.closePath(); + ctx.fill(); + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 2; + ctx.stroke(); +} + +function update() { + // Move player + if (keys['ArrowLeft'] && player.x > 0) { + player.x -= 5; + } + if (keys['ArrowRight'] && player.x < canvas.width - player.width) { + player.x += 5; + } + + // Update stars + stars.forEach(star => { + star.update(); + if (star.collidesWith(player)) { + score += 10; + scoreElement.textContent = score; + stars = stars.filter(s => s !== star); + } + }); + + // Update asteroids + asteroids.forEach(asteroid => { + asteroid.update(); + if (asteroid.collidesWith(player)) { + gameOver(); + } + }); +} + +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawPlayer(); + stars.forEach(star => star.draw()); + asteroids.forEach(asteroid => asteroid.draw()); +} + +function gameLoop() { + if (gameRunning) { + update(); + draw(); + requestAnimationFrame(gameLoop); + } +} + +function createStar() { + const x = Math.random() * (canvas.width - 30) + 15; + stars.push(new Star(x, -10)); +} + +function createAsteroid() { + const x = Math.random() * (canvas.width - 30) + 15; + asteroids.push(new Asteroid(x, -30)); +} + +function gameOver() { + gameRunning = false; + finalScoreElement.textContent = score; + gameOverElement.classList.remove('hidden'); +} + +function restartGame() { + score = 0; + scoreElement.textContent = score; + player.x = canvas.width / 2; + stars = []; + asteroids = []; + gameRunning = true; + gameOverElement.classList.add('hidden'); + gameLoop(); +} + +// Event listeners +document.addEventListener('keydown', (e) => { + keys[e.key] = true; +}); + +document.addEventListener('keyup', (e) => { + keys[e.key] = false; +}); + +restartBtn.addEventListener('click', restartGame); + +// Start spawning objects +setInterval(createStar, 1500); +setInterval(createAsteroid, 3000); + +// Start the game +gameLoop(); \ No newline at end of file diff --git a/games/star-collector/style.css b/games/star-collector/style.css new file mode 100644 index 00000000..0ed8cbce --- /dev/null +++ b/games/star-collector/style.css @@ -0,0 +1,88 @@ +body { + font-family: Arial, sans-serif; + background: linear-gradient(135deg, #0c0c0c 0%, #1a1a2e 50%, #16213e 100%); + margin: 0; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + color: white; +} + +.game-container { + text-align: center; + background: rgba(255, 255, 255, 0.05); + padding: 20px; + border-radius: 10px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); +} + +h1 { + margin-bottom: 10px; + font-size: 2.5em; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8); + color: #ffd700; +} + +.score { + font-size: 1.5em; + margin-bottom: 20px; + color: #00ff00; +} + +canvas { + border: 2px solid #ffd700; + border-radius: 10px; + background: #000011; + cursor: default; +} + +.instructions { + margin-top: 20px; + font-size: 1.1em; + line-height: 1.5; + color: #cccccc; +} + +.game-over { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.9); + padding: 30px; + border-radius: 15px; + text-align: center; + border: 2px solid #ff4444; +} + +.game-over h2 { + color: #ff4444; + margin-bottom: 20px; +} + +.game-over p { + font-size: 1.2em; + margin-bottom: 20px; + color: #ffffff; +} + +#restartBtn { + background: #4CAF50; + color: white; + border: none; + padding: 10px 20px; + font-size: 1.1em; + border-radius: 5px; + cursor: pointer; + transition: background 0.3s; +} + +#restartBtn:hover { + background: #45a049; +} + +.hidden { + display: none; +} \ No newline at end of file diff --git a/games/star-jump/index.html b/games/star-jump/index.html new file mode 100644 index 00000000..a5509d60 --- /dev/null +++ b/games/star-jump/index.html @@ -0,0 +1,20 @@ + + + + + + Star Jump | Mini JS Games Hub + + + +
    + +
    + Score: 0 + +
    +

    Use Arrow Keys or Tap/Click to jump between stars.

    +
    + + + diff --git a/games/star-jump/script.js b/games/star-jump/script.js new file mode 100644 index 00000000..13ab5bd0 --- /dev/null +++ b/games/star-jump/script.js @@ -0,0 +1,138 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); + +canvas.width = 480; +canvas.height = 700; + +let stars = []; +let blackHoles = []; +let player = { x: canvas.width/2, y: canvas.height - 100, radius: 15, dy: 0 }; +let gravity = 0.6; +let jumpPower = -12; +let score = 0; +let gameOver = false; + +// Generate stars +function createStars() { + for(let i = 0; i < 5; i++) { + stars.push({ + x: Math.random() * (canvas.width-50) + 25, + y: canvas.height - i*150 - 150, + radius: 15 + }); + } +} + +// Generate black holes +function createBlackHoles() { + for(let i=0; i<3; i++) { + blackHoles.push({ + x: Math.random() * (canvas.width-50) + 25, + y: Math.random() * 400, + radius: 25 + }); + } +} + +function drawPlayer() { + ctx.beginPath(); + ctx.arc(player.x, player.y, player.radius, 0, Math.PI*2); + ctx.fillStyle = 'yellow'; + ctx.fill(); + ctx.closePath(); +} + +function drawStars() { + stars.forEach(star => { + ctx.beginPath(); + ctx.arc(star.x, star.y, star.radius, 0, Math.PI*2); + ctx.fillStyle = 'white'; + ctx.fill(); + ctx.closePath(); + }); +} + +function drawBlackHoles() { + blackHoles.forEach(bh => { + ctx.beginPath(); + ctx.arc(bh.x, bh.y, bh.radius, 0, Math.PI*2); + ctx.fillStyle = 'purple'; + ctx.fill(); + ctx.closePath(); + }); +} + +function detectCollision(obj1, obj2) { + let dx = obj1.x - obj2.x; + let dy = obj1.y - obj2.y; + let distance = Math.sqrt(dx*dx + dy*dy); + return distance < obj1.radius + obj2.radius; +} + +function update() { + if(gameOver) return; + ctx.clearRect(0,0,canvas.width,canvas.height); + + player.dy += gravity; + player.y += player.dy; + + // Check collision with stars + stars.forEach(star => { + if(detectCollision(player, star) && player.dy > 0) { + player.dy = jumpPower; + score++; + document.getElementById('score').textContent = `Score: ${score}`; + star.y = Math.random() * -100; // Reposition star above + star.x = Math.random() * (canvas.width-50) + 25; + } + }); + + // Check collision with black holes + blackHoles.forEach(bh => { + if(detectCollision(player, bh)) { + gameOver = true; + document.getElementById('score').textContent = `Game Over! Final Score: ${score}`; + } + }); + + // Wrap player horizontally + if(player.x - player.radius < 0) player.x = player.radius; + if(player.x + player.radius > canvas.width) player.x = canvas.width - player.radius; + + // Game over if player falls + if(player.y - player.radius > canvas.height) { + gameOver = true; + document.getElementById('score').textContent = `Game Over! Final Score: ${score}`; + } + + drawStars(); + drawBlackHoles(); + drawPlayer(); + + requestAnimationFrame(update); +} + +document.addEventListener('keydown', e => { + if(e.key === 'ArrowLeft') player.x -= 20; + if(e.key === 'ArrowRight') player.x += 20; + if(e.key === 'ArrowUp') player.dy = jumpPower; +}); + +canvas.addEventListener('click', () => { + player.dy = jumpPower; +}); + +document.getElementById('restartBtn').addEventListener('click', () => { + player = { x: canvas.width/2, y: canvas.height - 100, radius: 15, dy: 0 }; + stars = []; + blackHoles = []; + score = 0; + gameOver = false; + createStars(); + createBlackHoles(); + document.getElementById('score').textContent = `Score: ${score}`; +}); + +createStars(); +createBlackHoles(); +update(); diff --git a/games/star-jump/style.css b/games/star-jump/style.css new file mode 100644 index 00000000..65b9f1f9 --- /dev/null +++ b/games/star-jump/style.css @@ -0,0 +1,65 @@ +body, html { + margin: 0; + padding: 0; + height: 100%; + overflow: hidden; + font-family: 'Arial', sans-serif; + background: linear-gradient(to top, #020024, #090979, #00d4ff); + display: flex; + justify-content: center; + align-items: center; +} + +.game-container { + position: relative; + width: 100%; + max-width: 480px; + height: 700px; + border-radius: 15px; + overflow: hidden; + background-color: rgba(0,0,0,0.5); + box-shadow: 0 0 20px rgba(255,255,255,0.2); +} + +canvas { + width: 100%; + height: 100%; + display: block; + background: transparent; +} + +.ui { + position: absolute; + top: 10px; + left: 50%; + transform: translateX(-50%); + color: white; + font-size: 18px; + display: flex; + gap: 20px; + align-items: center; +} + +button#restartBtn { + padding: 6px 12px; + border: none; + border-radius: 6px; + background: #ffcc00; + color: #000; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; +} + +button#restartBtn:hover { + transform: scale(1.1); +} + +.instructions { + position: absolute; + bottom: 10px; + width: 100%; + text-align: center; + color: white; + font-size: 14px; +} diff --git a/games/star-trail-drawer/index.html b/games/star-trail-drawer/index.html new file mode 100644 index 00000000..4031a73d --- /dev/null +++ b/games/star-trail-drawer/index.html @@ -0,0 +1,31 @@ + + + + + + Star Trail Drawer | Mini JS Games Hub + + + +
    +

    Star Trail Drawer

    + + +
    + + + + + +
    + +

    +
    + + + + + + + + diff --git a/games/star-trail-drawer/script.js b/games/star-trail-drawer/script.js new file mode 100644 index 00000000..4d88b8c9 --- /dev/null +++ b/games/star-trail-drawer/script.js @@ -0,0 +1,126 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); +canvas.width = 800; +canvas.height = 500; + +let stars = []; +let trail = []; +let targetTrail = []; +let isDrawing = false; +let paused = false; + +const connectSound = document.getElementById("connectSound"); +const successSound = document.getElementById("successSound"); + +// Generate stars +function createStars(count = 20) { + stars = []; + for (let i = 0; i < count; i++) { + stars.push({ + x: Math.random() * (canvas.width - 60) + 30, + y: Math.random() * (canvas.height - 60) + 30, + radius: 6, + id: i, + glow: 0 + }); + } +} + +// Draw stars +function drawStars() { + stars.forEach(star => { + ctx.beginPath(); + ctx.arc(star.x, star.y, star.radius, 0, Math.PI*2); + ctx.fillStyle = `rgba(255,255,255,${0.5 + star.glow})`; + ctx.shadowBlur = 20 * star.glow; + ctx.shadowColor = 'white'; + ctx.fill(); + ctx.closePath(); + star.glow += Math.random()*0.05; + if(star.glow>1) star.glow=0.5; + }); +} + +// Draw trail +function drawTrail() { + if(trail.length < 2) return; + ctx.beginPath(); + ctx.moveTo(trail[0].x, trail[0].y); + for(let i=1;i { + const dx = x - star.x; + const dy = y - star.y; + return Math.sqrt(dx*dx + dy*dy) < star.radius + 10; + }); +} + +// Event listeners +canvas.addEventListener("mousedown", (e)=>{ + if(paused) return; + isDrawing = true; + const star = getStarAt(e.offsetX, e.offsetY); + if(star && !trail.includes(star)){ + trail.push(star); + connectSound.play(); + } +}); + +canvas.addEventListener("mousemove", (e)=>{ + if(!isDrawing || paused) return; + const star = getStarAt(e.offsetX, e.offsetY); + if(star && !trail.includes(star)){ + trail.push(star); + connectSound.play(); + } +}); + +canvas.addEventListener("mouseup", ()=>{ + isDrawing = false; +}); + +// Controls +document.getElementById("pauseBtn").addEventListener("click", ()=>{ + paused = !paused; + if(!paused) animate(); +}); + +document.getElementById("restartBtn").addEventListener("click", ()=>{ + trail = []; + createStars(); + paused=false; + animate(); + document.getElementById("message").textContent=""; +}); + +document.getElementById("clearBtn").addEventListener("click", ()=>{ + trail=[]; +}); + +document.getElementById("hintBtn").addEventListener("click", ()=>{ + document.getElementById("message").textContent="Hint: Connect the first 3 stars!"; +}); + +// Initialize +createStars(); +animate(); diff --git a/games/star-trail-drawer/style.css b/games/star-trail-drawer/style.css new file mode 100644 index 00000000..c9ffdc36 --- /dev/null +++ b/games/star-trail-drawer/style.css @@ -0,0 +1,49 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: radial-gradient(#000011, #000); + color: #fff; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + height: 100vh; +} + +.game-container { + text-align: center; +} + +canvas { + border: 2px solid #fff; + display: block; + margin: 20px auto; + background: url('https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=800&q=80') no-repeat center center/cover; + border-radius: 10px; +} + +.controls { + margin-top: 10px; +} + +button { + margin: 5px; + padding: 8px 15px; + font-size: 16px; + cursor: pointer; + border-radius: 5px; + border: none; + background: #222; + color: #fff; + transition: 0.3s; +} + +button:hover { + background: #444; +} + +#message { + margin-top: 10px; + font-weight: bold; + font-size: 18px; +} diff --git a/games/starlight-draw/index.html b/games/starlight-draw/index.html new file mode 100644 index 00000000..a6de72b3 --- /dev/null +++ b/games/starlight-draw/index.html @@ -0,0 +1,25 @@ + + + + + +Starlight Draw | Mini JS Games Hub + + + +
    +

    Starlight Draw โœจ

    + +
    + + + + Level: 1 + Score: 0 +
    + + +
    + + + diff --git a/games/starlight-draw/script.js b/games/starlight-draw/script.js new file mode 100644 index 00000000..02a35b8d --- /dev/null +++ b/games/starlight-draw/script.js @@ -0,0 +1,208 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); +canvas.width = window.innerWidth * 0.8; +canvas.height = window.innerHeight * 0.6; + +const startBtn = document.getElementById("startBtn"); +const pauseBtn = document.getElementById("pauseBtn"); +const restartBtn = document.getElementById("restartBtn"); +const levelDisplay = document.getElementById("levelDisplay"); +const scoreDisplay = document.getElementById("scoreDisplay"); + +const connectSound = document.getElementById("connectSound"); +const levelUpSound = document.getElementById("levelUpSound"); + +let stars = []; +let connections = []; +let obstacles = []; +let level = 1; +let score = 0; +let gameRunning = false; +let currentLine = null; + +const STAR_RADIUS = 10; +const OBSTACLE_RADIUS = 20; +const LINE_WIDTH = 4; + +// Initialize stars and obstacles +function setupLevel() { + stars = []; + obstacles = []; + connections = []; + currentLine = null; + const numStars = 5 + level; + for(let i=0;i{ + ctx.beginPath(); + ctx.fillStyle = "red"; + ctx.globalAlpha = 0.6; + ctx.arc(o.x,o.y,OBSTACLE_RADIUS,0,Math.PI*2); + ctx.fill(); + ctx.globalAlpha = 1; + }); + + // Draw stars + stars.forEach(s=>{ + ctx.beginPath(); + ctx.fillStyle = s.connected ? "yellow" : "white"; + ctx.shadowColor = "yellow"; + ctx.shadowBlur = 20; + ctx.arc(s.x,s.y,STAR_RADIUS,0,Math.PI*2); + ctx.fill(); + ctx.shadowBlur = 0; + }); + + // Draw connections + connections.forEach(line=>{ + ctx.beginPath(); + ctx.strokeStyle = "cyan"; + ctx.lineWidth = LINE_WIDTH; + ctx.shadowColor = "cyan"; + ctx.shadowBlur = 15; + ctx.moveTo(line.start.x,line.start.y); + ctx.lineTo(line.end.x,line.end.y); + ctx.stroke(); + ctx.shadowBlur = 0; + }); + + // Draw current line + if(currentLine){ + ctx.beginPath(); + ctx.strokeStyle = "lime"; + ctx.lineWidth = LINE_WIDTH; + ctx.shadowColor = "lime"; + ctx.shadowBlur = 15; + ctx.moveTo(currentLine.start.x,currentLine.start.y); + ctx.lineTo(currentLine.end.x,currentLine.end.y); + ctx.stroke(); + ctx.shadowBlur = 0; + } +} + +// Check if line intersects obstacle +function checkObstacleCollision(x1,y1,x2,y2){ + for(const o of obstacles){ + const dx = x2 - x1; + const dy = y2 - y1; + const t = ((o.x - x1)*dx + (o.y - y1)*dy) / (dx*dx + dy*dy); + const nearestX = x1 + t*dx; + const nearestY = y1 + t*dy; + const dist = Math.hypot(nearestX - o.x, nearestY - o.y); + if(dist < OBSTACLE_RADIUS + 2) return true; + } + return false; +} + +// Mouse / touch events +let draggingStar = null; + +canvas.addEventListener("mousedown", startLine); +canvas.addEventListener("touchstart", e=>startLine(e.touches[0])); + +canvas.addEventListener("mousemove", moveLine); +canvas.addEventListener("touchmove", e=>moveLine(e.touches[0])); + +canvas.addEventListener("mouseup", endLine); +canvas.addEventListener("touchend", endLine); + +function startLine(e){ + if(!gameRunning) return; + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + for(const s of stars){ + if(!s.connected && Math.hypot(s.x-x,s.y-y) < STAR_RADIUS+5){ + draggingStar = s; + currentLine = {start:{x:s.x,y:s.y},end:{x:x,y:y}}; + break; + } + } +} + +function moveLine(e){ + if(!draggingStar) return; + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + currentLine.end.x = x; + currentLine.end.y = y; +} + +function endLine(e){ + if(!draggingStar) return; + const rect = canvas.getBoundingClientRect(); + const x = (e.changedTouches ? e.changedTouches[0].clientX : e.clientX) - rect.left; + const y = (e.changedTouches ? e.changedTouches[0].clientY : e.clientY) - rect.top; + for(const s of stars){ + if(s!==draggingStar && !s.connected && Math.hypot(s.x-x,s.y-y)s.connected)){ + levelUpSound.play(); + level++; + setupLevel(); + } +} + +// Game loop +function loop(){ + draw(); + if(gameRunning) requestAnimationFrame(loop); +} + +// Button events +startBtn.addEventListener("click", ()=>{ + gameRunning = true; + loop(); +}); + +pauseBtn.addEventListener("click", ()=>{ + gameRunning = false; +}); + +restartBtn.addEventListener("click", ()=>{ + gameRunning = true; + score = 0; + level = 1; + scoreDisplay.textContent = `Score: ${score}`; + setupLevel(); + loop(); +}); + +// Init first level +setupLevel(); +draw(); diff --git a/games/starlight-draw/style.css b/games/starlight-draw/style.css new file mode 100644 index 00000000..9ae4d9f6 --- /dev/null +++ b/games/starlight-draw/style.css @@ -0,0 +1,49 @@ +body { + margin: 0; + padding: 0; + background-color: #050505; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + height: 100vh; + font-family: 'Arial', sans-serif; + color: #fff; +} + +.game-container { + text-align: center; +} + +canvas { + background-color: #0a0a0a; + border: 2px solid #fff; + border-radius: 10px; + display: block; + margin: 20px auto; +} + +.controls { + margin-top: 10px; +} + +button { + padding: 8px 15px; + margin: 0 5px; + font-size: 16px; + cursor: pointer; + border-radius: 6px; + border: none; + background-color: #222; + color: #fff; + transition: 0.3s; +} +button:hover { + background-color: #444; +} + +#levelDisplay, #scoreDisplay { + margin-left: 10px; + font-weight: bold; + font-size: 18px; +} diff --git a/games/starlight-sequence/index.html b/games/starlight-sequence/index.html new file mode 100644 index 00000000..6f3ab1ae --- /dev/null +++ b/games/starlight-sequence/index.html @@ -0,0 +1,42 @@ + + + + + + Starlight Sequence - Mini JS Games Hub + + + +
    +

    Starlight Sequence

    +
    + + + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    Select Difficulty & Start
    +
    Sequence: 0
    +
    +
    +
    +

    High Score: 0

    +
    + + +
    +
    +
    Made for Mini JS Games Hub
    +
    + + + \ No newline at end of file diff --git a/games/starlight-sequence/screenshot.png b/games/starlight-sequence/screenshot.png new file mode 100644 index 00000000..c48703ed --- /dev/null +++ b/games/starlight-sequence/screenshot.png @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/games/starlight-sequence/script.js b/games/starlight-sequence/script.js new file mode 100644 index 00000000..24e9a261 --- /dev/null +++ b/games/starlight-sequence/script.js @@ -0,0 +1,175 @@ +// Starlight Sequence Game +let difficulty = 'easy'; +let sequence = []; +let playerSequence = []; +let step = 0; +let speed = 1000; +let soundOn = true; +let gameActive = false; +let showingSequence = false; +const highScores = JSON.parse(localStorage.getItem('starlightSequenceHighScores') || '{"easy":0,"medium":0,"hard":0}'); + +const orbs = document.querySelectorAll('.orb'); +const messageEl = document.getElementById('message'); +const sequenceEl = document.getElementById('sequence'); +const highScoreEl = document.getElementById('highScore'); +const startBtn = document.getElementById('startBtn'); +const soundBtn = document.getElementById('soundBtn'); + +// Audio context +let audioCtx; +try { + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); +} catch (e) { + console.warn('Web Audio API not supported'); +} + +function initAudio() { + if (!audioCtx) return; + // Create ambient sound + const ambientOsc = audioCtx.createOscillator(); + const ambientGain = audioCtx.createGain(); + ambientOsc.frequency.setValueAtTime(220, audioCtx.currentTime); + ambientGain.gain.setValueAtTime(0.1, audioCtx.currentTime); + ambientOsc.connect(ambientGain); + ambientGain.connect(audioCtx.destination); + ambientOsc.start(); +} + +function playTone(freq, duration = 0.5) { + if (!soundOn || !audioCtx) return; + const osc = audioCtx.createOscillator(); + const gain = audioCtx.createGain(); + osc.frequency.setValueAtTime(freq, audioCtx.currentTime); + gain.gain.setValueAtTime(0.3, audioCtx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration); + osc.connect(gain); + gain.connect(audioCtx.destination); + osc.start(); + osc.stop(audioCtx.currentTime + duration); +} + +function lightOrb(id, duration = 500) { + const orb = document.querySelector(`.orb[data-id="${id}"]`); + orb.classList.add('active'); + const freq = parseFloat(orb.dataset.freq); + playTone(freq, duration / 1000); + setTimeout(() => orb.classList.remove('active'), duration); +} + +function setDifficulty(diff) { + difficulty = diff; + document.querySelectorAll('.diff-btn').forEach(btn => btn.classList.remove('active')); + document.querySelector(`[data-diff="${diff}"]`).classList.add('active'); + updateHighScore(); + resetGame(); +} + +function updateHighScore() { + highScoreEl.textContent = `High Score: ${highScores[difficulty]}`; +} + +function resetGame() { + sequence = []; + playerSequence = []; + step = 0; + gameActive = false; + showingSequence = false; + messageEl.textContent = 'Press Start to Play'; + sequenceEl.textContent = 'Sequence: 0'; + orbs.forEach(orb => orb.style.display = difficulty === 'hard' ? 'block' : (parseInt(orb.dataset.id) < 4 ? 'block' : 'none')); +} + +function startGame() { + if (gameActive) return; + gameActive = true; + sequence = []; + playerSequence = []; + step = 0; + speed = difficulty === 'easy' ? 1000 : difficulty === 'medium' ? 700 : 500; + nextRound(); +} + +function nextRound() { + step++; + sequence.push(Math.floor(Math.random() * (difficulty === 'hard' ? 6 : 4))); + playerSequence = []; + sequenceEl.textContent = `Sequence: ${step}`; + showSequence(); +} + +function showSequence() { + showingSequence = true; + messageEl.textContent = 'Watch the sequence...'; + let i = 0; + const interval = setInterval(() => { + if (i < sequence.length) { + lightOrb(sequence[i]); + i++; + } else { + clearInterval(interval); + showingSequence = false; + messageEl.textContent = 'Your turn!'; + } + }, speed); +} + +function playerInput(id) { + if (!gameActive || showingSequence) return; + playerSequence.push(id); + lightOrb(id, 300); + if (playerSequence[playerSequence.length - 1] !== sequence[playerSequence.length - 1]) { + gameOver(); + return; + } + if (playerSequence.length === sequence.length) { + setTimeout(() => { + messageEl.textContent = 'Good! Next round...'; + setTimeout(nextRound, 1000); + }, 500); + } +} + +function gameOver() { + gameActive = false; + messageEl.textContent = 'Wrong! Game Over'; + playTone(100, 1); // Low tone for error + if (step - 1 > highScores[difficulty]) { + highScores[difficulty] = step - 1; + localStorage.setItem('starlightSequenceHighScores', JSON.stringify(highScores)); + updateHighScore(); + messageEl.textContent = 'New High Score!'; + } + setTimeout(() => { + messageEl.textContent = 'Select Difficulty & Start'; + }, 2000); +} + +// Event listeners +document.querySelectorAll('.diff-btn').forEach(btn => { + btn.addEventListener('click', () => setDifficulty(btn.dataset.diff)); +}); + +orbs.forEach(orb => { + orb.addEventListener('click', () => playerInput(parseInt(orb.dataset.id))); +}); + +document.addEventListener('keydown', e => { + const key = parseInt(e.key); + if (key >= 1 && key <= 6) { + playerInput(key - 1); + } +}); + +startBtn.addEventListener('click', startGame); +soundBtn.addEventListener('click', () => { + soundOn = !soundOn; + soundBtn.textContent = `Sound: ${soundOn ? 'On' : 'Off'}`; + if (soundOn && audioCtx && audioCtx.state === 'suspended') { + audioCtx.resume(); + } +}); + +// Initialize +updateHighScore(); +initAudio(); \ No newline at end of file diff --git a/games/starlight-sequence/style.css b/games/starlight-sequence/style.css new file mode 100644 index 00000000..62480b3b --- /dev/null +++ b/games/starlight-sequence/style.css @@ -0,0 +1,31 @@ +*{box-sizing:border-box;margin:0;padding:0} +body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;background:linear-gradient(135deg,#0c0c0c 0%,#1a1a2e 100%);display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px;color:#fff} +.game-wrap{background:#1a1a2e;border-radius:20px;padding:30px;text-align:center;box-shadow:0 20px 40px rgba(0,0,0,0.5);max-width:500px;width:100%;border:1px solid #00d4ff} +h1{color:#00d4ff;margin-bottom:20px;font-size:2em;text-shadow:0 0 10px #00d4ff} +.difficulty-selector{display:flex;gap:10px;justify-content:center;margin-bottom:20px} +.diff-btn{padding:8px 16px;border:1px solid #00d4ff;border-radius:8px;background:#1a1a2e;color:#00d4ff;cursor:pointer;transition:all 0.3s ease} +.diff-btn:hover,.diff-btn.active{background:#00d4ff;color:#1a1a2e} +.game-area{position:relative;display:flex;align-items:center;justify-content:center;min-height:400px} +.orbs{position:relative;width:300px;height:300px} +.orb{position:absolute;width:60px;height:60px;border-radius:50%;cursor:pointer;transition:all 0.3s ease;transform-origin:center;border:2px solid #fff} +.orb:nth-child(1){top:0;left:50%;transform:translateX(-50%)} +.orb:nth-child(2){top:25%;right:0;transform:translateY(-50%)} +.orb:nth-child(3){bottom:25%;right:0;transform:translateY(50%)} +.orb:nth-child(4){bottom:0;left:50%;transform:translateX(-50%)} +.orb:nth-child(5){top:25%;left:0;transform:translateY(-50%)} +.orb:nth-child(6){bottom:25%;left:0;transform:translateY(50%)} +.orb.active{box-shadow:0 0 20px currentColor, 0 0 40px currentColor, 0 0 60px currentColor;transform:scale(1.2)} +.center-display{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center} +#message{font-size:24px;margin-bottom:10px;text-shadow:0 0 10px #00d4ff} +#sequence{font-size:18px} +.info{margin-top:20px} +.controls{display:flex;gap:10px;justify-content:center;margin-top:10px} +button{padding:10px 20px;border:none;border-radius:8px;background:#00d4ff;color:#1a1a2e;font-size:16px;cursor:pointer;transition:all 0.3s ease} +button:hover{background:#0099cc} +footer{font-size:14px;color:#666;margin-top:20px} +@media (max-width: 600px) { + .game-wrap{padding:20px;max-width:100%} + .orbs{width:250px;height:250px} + .orb{width:50px;height:50px} + .center-display{font-size:20px} +} \ No newline at end of file diff --git a/games/starry-night-sky/index.html b/games/starry-night-sky/index.html new file mode 100644 index 00000000..24684c46 --- /dev/null +++ b/games/starry-night-sky/index.html @@ -0,0 +1,56 @@ + + + + + + Starry Night Sky + + + +
    +
    +

    Starry Night Sky

    +
    + + +
    Constellations: 0
    +
    +
    + +
    +
    + +
    +

    Click on stars to connect them and form constellations. Take deep breaths and relax while you play.

    +
    +
    +

    Breathe In...

    +
    +
    +
    + + +
    + +
    +

    Take a moment to relax and connect the stars. Each constellation brings peace and mindfulness.

    +
    +
    + + + + \ No newline at end of file diff --git a/games/starry-night-sky/script.js b/games/starry-night-sky/script.js new file mode 100644 index 00000000..eb6fb999 --- /dev/null +++ b/games/starry-night-sky/script.js @@ -0,0 +1,351 @@ +// Starry Night Sky Relaxation Game +// A calming experience for mindfulness and stress relief + +class StarryNightGame { + constructor() { + this.canvas = document.getElementById('star-canvas'); + this.ctx = this.canvas.getContext('2d'); + this.stars = []; + this.connections = []; + this.selectedStars = []; + this.constellations = []; + this.achievements = this.initializeAchievements(); + this.musicEnabled = true; + this.audioContext = null; + this.musicSource = null; + this.startTime = Date.now(); + this.sessionTimer = null; + + this.initializeGame(); + this.setupEventListeners(); + this.startSessionTimer(); + this.createBreathingGuide(); + this.renderAchievements(); + } + + initializeGame() { + this.generateStars(); + this.setupAudio(); + this.animate(); + } + + generateStars() { + this.stars = []; + const numStars = 50 + Math.random() * 30; // 50-80 stars + + for (let i = 0; i < numStars; i++) { + const star = { + x: Math.random() * this.canvas.width, + y: Math.random() * this.canvas.height, + radius: Math.random() * 2 + 1, + brightness: Math.random(), + twinkleSpeed: Math.random() * 0.02 + 0.01, + id: i + }; + this.stars.push(star); + } + } + + setupAudio() { + try { + this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); + this.createAmbientMusic(); + } catch (e) { + console.log('Web Audio API not supported'); + } + } + + createAmbientMusic() { + if (!this.audioContext) return; + + // Create a soothing ambient sound using oscillators + const createOscillator = (frequency, gain) => { + const oscillator = this.audioContext.createOscillator(); + const gainNode = this.audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(this.audioContext.destination); + + oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime); + oscillator.type = 'sine'; + + gainNode.gain.setValueAtTime(0, this.audioContext.currentTime); + gainNode.gain.linearRampToValueAtTime(gain, this.audioContext.currentTime + 2); + + oscillator.start(); + return { oscillator, gainNode }; + }; + + // Create multiple gentle tones for ambient atmosphere + this.musicLayers = [ + createOscillator(220, 0.05), // A3 + createOscillator(330, 0.03), // E4 + createOscillator(440, 0.02), // A4 + createOscillator(165, 0.04), // E3 + ]; + } + + toggleMusic() { + if (!this.audioContext) return; + + if (this.musicEnabled) { + this.musicLayers.forEach(layer => { + layer.gainNode.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + 1); + }); + } else { + this.musicLayers.forEach(layer => { + layer.gainNode.gain.linearRampToValueAtTime(layer.gainNode.gain.value * 2, this.audioContext.currentTime + 1); + }); + } + this.musicEnabled = !this.musicEnabled; + } + + setupEventListeners() { + this.canvas.addEventListener('click', (e) => this.handleCanvasClick(e)); + document.getElementById('music-toggle').addEventListener('click', () => this.toggleMusic()); + document.getElementById('reset-btn').addEventListener('click', () => this.resetGame()); + } + + handleCanvasClick(event) { + const rect = this.canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + + // Find clicked star + const clickedStar = this.stars.find(star => { + const distance = Math.sqrt((star.x - x) ** 2 + (star.y - y) ** 2); + return distance < star.radius + 10; + }); + + if (clickedStar) { + this.selectStar(clickedStar); + } + } + + selectStar(star) { + if (this.selectedStars.includes(star)) { + // Deselect if already selected + this.selectedStars = this.selectedStars.filter(s => s !== star); + } else { + this.selectedStars.push(star); + } + + // Check for constellation when we have 3+ stars + if (this.selectedStars.length >= 3) { + this.checkForConstellation(); + } + + this.updateStats(); + } + + checkForConstellation() { + // Simple constellation detection - check if stars form a reasonable shape + if (this.selectedStars.length >= 4) { + const constellation = { + stars: [...this.selectedStars], + id: this.constellations.length, + discovered: true + }; + + this.constellations.push(constellation); + this.selectedStars = []; // Clear selection after forming constellation + + // Play success sound + this.playSuccessSound(); + + // Update achievements + this.checkAchievements(); + + document.getElementById('constellation-count').textContent = this.constellations.length; + document.getElementById('total-constellations').textContent = this.constellations.length; + } + } + + playSuccessSound() { + if (!this.audioContext) return; + + const oscillator = this.audioContext.createOscillator(); + const gainNode = this.audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(this.audioContext.destination); + + oscillator.frequency.setValueAtTime(523, this.audioContext.currentTime); // C5 + oscillator.frequency.exponentialRampToValueAtTime(659, this.audioContext.currentTime + 0.5); // E5 + + gainNode.gain.setValueAtTime(0.1, this.audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.5); + + oscillator.start(); + oscillator.stop(this.audioContext.currentTime + 0.5); + } + + initializeAchievements() { + return [ + { id: 'first_star', name: 'First Light', description: 'Connect your first star', unlocked: false }, + { id: 'constellation_1', name: 'Stargazer', description: 'Discover 1 constellation', unlocked: false }, + { id: 'constellation_5', name: 'Astronomer', description: 'Discover 5 constellations', unlocked: false }, + { id: 'constellation_10', name: 'Master Observer', description: 'Discover 10 constellations', unlocked: false }, + { id: 'relax_5min', name: 'Mindful Moment', description: 'Relax for 5 minutes', unlocked: false }, + { id: 'relax_15min', name: 'Deep Meditation', description: 'Relax for 15 minutes', unlocked: false }, + { id: 'big_constellation', name: 'Celestial Network', description: 'Create a constellation with 7+ stars', unlocked: false } + ]; + } + + checkAchievements() { + const constellationCount = this.constellations.length; + const sessionMinutes = (Date.now() - this.startTime) / 60000; + + // Update achievement statuses + if (this.selectedStars.length > 0 || this.connections.length > 0) { + this.achievements.find(a => a.id === 'first_star').unlocked = true; + } + + if (constellationCount >= 1) { + this.achievements.find(a => a.id === 'constellation_1').unlocked = true; + } + if (constellationCount >= 5) { + this.achievements.find(a => a.id === 'constellation_5').unlocked = true; + } + if (constellationCount >= 10) { + this.achievements.find(a => a.id === 'constellation_10').unlocked = true; + } + + if (sessionMinutes >= 5) { + this.achievements.find(a => a.id === 'relax_5min').unlocked = true; + } + if (sessionMinutes >= 15) { + this.achievements.find(a => a.id === 'relax_15min').unlocked = true; + } + + // Check for large constellation + if (this.constellations.some(c => c.stars.length >= 7)) { + this.achievements.find(a => a.id === 'big_constellation').unlocked = true; + } + + this.renderAchievements(); + } + + renderAchievements() { + const achievementList = document.getElementById('achievement-list'); + achievementList.innerHTML = ''; + + this.achievements.forEach(achievement => { + const item = document.createElement('div'); + item.className = `achievement-item ${achievement.unlocked ? 'unlocked' : ''}`; + item.innerHTML = ` +
    ${achievement.name}
    +
    ${achievement.description}
    + `; + achievementList.appendChild(item); + }); + } + + createBreathingGuide() { + const breathText = document.getElementById('breath-text'); + const phases = ['Breathe In...', 'Hold...', 'Breathe Out...', 'Hold...']; + let phaseIndex = 0; + + setInterval(() => { + breathText.textContent = phases[phaseIndex]; + phaseIndex = (phaseIndex + 1) % phases.length; + }, 2000); // 2 seconds per phase = 8 second cycle + } + + startSessionTimer() { + this.sessionTimer = setInterval(() => { + const elapsed = Math.floor((Date.now() - this.startTime) / 1000); + const minutes = Math.floor(elapsed / 60); + const seconds = elapsed % 60; + document.getElementById('relax-time').textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; + + this.checkAchievements(); // Check time-based achievements + }, 1000); + } + + updateStats() { + document.getElementById('stars-connected').textContent = this.selectedStars.length; + } + + resetGame() { + this.selectedStars = []; + this.connections = []; + this.constellations = []; + this.generateStars(); + this.startTime = Date.now(); + this.updateStats(); + document.getElementById('constellation-count').textContent = '0'; + document.getElementById('total-constellations').textContent = '0'; + document.getElementById('relax-time').textContent = '0:00'; + } + + animate() { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + // Draw stars + this.stars.forEach(star => { + star.brightness += star.twinkleSpeed; + if (star.brightness > 1) star.brightness = 0; + + const alpha = 0.3 + star.brightness * 0.7; + this.ctx.beginPath(); + this.ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2); + this.ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`; + this.ctx.fill(); + + // Glow effect + this.ctx.beginPath(); + this.ctx.arc(star.x, star.y, star.radius * 3, 0, Math.PI * 2); + this.ctx.fillStyle = `rgba(0, 212, 255, ${alpha * 0.1})`; + this.ctx.fill(); + }); + + // Draw connections + this.ctx.strokeStyle = 'rgba(0, 212, 255, 0.6)'; + this.ctx.lineWidth = 2; + this.ctx.beginPath(); + + for (let i = 0; i < this.selectedStars.length - 1; i++) { + const star1 = this.selectedStars[i]; + const star2 = this.selectedStars[i + 1]; + this.ctx.moveTo(star1.x, star1.y); + this.ctx.lineTo(star2.x, star2.y); + } + this.ctx.stroke(); + + // Draw constellation outlines + this.constellations.forEach(constellation => { + this.ctx.strokeStyle = 'rgba(0, 255, 136, 0.8)'; + this.ctx.lineWidth = 3; + this.ctx.beginPath(); + + for (let i = 0; i < constellation.stars.length - 1; i++) { + const star1 = constellation.stars[i]; + const star2 = constellation.stars[i + 1]; + this.ctx.moveTo(star1.x, star1.y); + this.ctx.lineTo(star2.x, star2.y); + } + // Close the constellation + if (constellation.stars.length > 2) { + this.ctx.lineTo(constellation.stars[0].x, constellation.stars[0].y); + } + this.ctx.stroke(); + }); + + // Highlight selected stars + this.selectedStars.forEach(star => { + this.ctx.beginPath(); + this.ctx.arc(star.x, star.y, star.radius + 3, 0, Math.PI * 2); + this.ctx.strokeStyle = 'rgba(0, 212, 255, 0.8)'; + this.ctx.lineWidth = 2; + this.ctx.stroke(); + }); + + requestAnimationFrame(() => this.animate()); + } +} + +// Initialize the game when the page loads +document.addEventListener('DOMContentLoaded', () => { + new StarryNightGame(); +}); \ No newline at end of file diff --git a/games/starry-night-sky/style.css b/games/starry-night-sky/style.css new file mode 100644 index 00000000..cb1b0e39 --- /dev/null +++ b/games/starry-night-sky/style.css @@ -0,0 +1,214 @@ +body { + margin: 0; + padding: 0; + background: linear-gradient(180deg, #0c0c0c 0%, #1a1a2e 50%, #16213e 100%); + font-family: 'Arial', sans-serif; + color: #e6e6e6; + overflow-x: hidden; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 20px; +} + +h1 { + color: #ffffff; + text-shadow: 0 0 20px #00d4ff; + font-size: 2.5em; + margin-bottom: 10px; +} + +.controls { + display: flex; + justify-content: center; + gap: 15px; + align-items: center; + flex-wrap: wrap; +} + +button { + background: linear-gradient(145deg, #2a2a2a, #1a1a1a); + border: 1px solid #444; + color: #e6e6e6; + padding: 10px 20px; + border-radius: 25px; + cursor: pointer; + font-size: 14px; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); +} + +button:hover { + background: linear-gradient(145deg, #3a3a3a, #2a2a2a); + box-shadow: 0 6px 20px rgba(0, 212, 255, 0.3); + transform: translateY(-2px); +} + +.score { + background: rgba(0, 212, 255, 0.1); + padding: 10px 20px; + border-radius: 25px; + border: 1px solid rgba(0, 212, 255, 0.3); + font-weight: bold; +} + +main { + display: flex; + gap: 20px; + align-items: flex-start; +} + +.game-area { + flex: 1; + position: relative; +} + +#star-canvas { + background: radial-gradient(ellipse at center, #1a1a2e 0%, #0c0c0c 100%); + border-radius: 15px; + box-shadow: 0 0 50px rgba(0, 212, 255, 0.2); + cursor: crosshair; + display: block; + margin: 0 auto; +} + +.instructions { + position: absolute; + top: 20px; + left: 20px; + background: rgba(0, 0, 0, 0.7); + padding: 15px; + border-radius: 10px; + max-width: 300px; + backdrop-filter: blur(10px); +} + +.breathing-guide { + text-align: center; + margin-top: 15px; +} + +.breath-circle { + width: 60px; + height: 60px; + border: 3px solid rgba(0, 212, 255, 0.5); + border-radius: 50%; + margin: 10px auto; + animation: breathe 8s infinite ease-in-out; +} + +@keyframes breathe { + 0%, 100% { transform: scale(1); opacity: 0.5; } + 50% { transform: scale(1.2); opacity: 1; } +} + +#breath-text { + font-size: 12px; + color: #00d4ff; + margin-top: 5px; +} + +.sidebar { + width: 250px; + background: rgba(0, 0, 0, 0.5); + border-radius: 15px; + padding: 20px; + backdrop-filter: blur(10px); + box-shadow: 0 0 30px rgba(0, 0, 0, 0.3); +} + +.achievements h3, .stats h3 { + color: #00d4ff; + margin-bottom: 15px; + text-align: center; +} + +.achievement-item { + background: rgba(255, 255, 255, 0.05); + padding: 10px; + margin-bottom: 8px; + border-radius: 8px; + border-left: 3px solid #00d4ff; + transition: all 0.3s ease; +} + +.achievement-item.unlocked { + background: rgba(0, 212, 255, 0.1); + border-left-color: #00ff88; +} + +.achievement-item.unlocked .achievement-name { + color: #00ff88; +} + +.stat-item { + display: flex; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +footer { + text-align: center; + margin-top: 20px; + color: #888; + font-style: italic; +} + +/* Responsive design */ +@media (max-width: 768px) { + main { + flex-direction: column; + } + + .sidebar { + width: 100%; + } + + #star-canvas { + width: 100%; + height: 400px; + } + + .controls { + flex-direction: column; + gap: 10px; + } + + h1 { + font-size: 2em; + } +} + +/* Twinkling stars background effect */ +body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: + radial-gradient(2px 2px at 20px 30px, #eee, transparent), + radial-gradient(2px 2px at 40px 70px, rgba(255,255,255,0.8), transparent), + radial-gradient(1px 1px at 90px 40px, #fff, transparent), + radial-gradient(1px 1px at 130px 80px, rgba(255,255,255,0.6), transparent), + radial-gradient(2px 2px at 160px 30px, #ddd, transparent); + background-repeat: repeat; + background-size: 200px 100px; + animation: twinkle 4s ease-in-out infinite alternate; + pointer-events: none; + z-index: -1; +} + +@keyframes twinkle { + 0% { opacity: 0.3; } + 100% { opacity: 0.8; } +} \ No newline at end of file diff --git a/games/sticky-ball/index.html b/games/sticky-ball/index.html new file mode 100644 index 00000000..e2dc4592 --- /dev/null +++ b/games/sticky-ball/index.html @@ -0,0 +1,28 @@ + + + + + + Sticky Ball | Mini JS Games Hub + + + +
    +

    ๐ŸŸฃ Sticky Ball

    +
    + + + + +
    +
    + + + + + + + + + + diff --git a/games/sticky-ball/script.js b/games/sticky-ball/script.js new file mode 100644 index 00000000..c7d4f3b4 --- /dev/null +++ b/games/sticky-ball/script.js @@ -0,0 +1,154 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +canvas.width = 800; +canvas.height = 450; + +// Load sounds +const jumpSound = document.getElementById("jumpSound"); +const hitSound = document.getElementById("hitSound"); +const bgMusic = document.getElementById("bgMusic"); + +// Game variables +let gravity = 0.6; +let isPaused = false; +let gameRunning = false; +let keys = { left: false, right: false, up: false }; + +const ball = { + x: 100, + y: 350, + radius: 20, + dx: 0, + dy: 0, + sticky: false, + color: "rgba(0,255,255,0.9)" +}; + +const obstacles = [ + { x: 300, y: 400, width: 100, height: 20 }, + { x: 500, y: 350, width: 100, height: 20 }, + { x: 700, y: 300, width: 80, height: 20 }, + { x: 400, y: 250, width: 80, height: 20 }, + { x: 650, y: 200, width: 100, height: 20 }, +]; + +function drawBall() { + const glow = ctx.createRadialGradient(ball.x, ball.y, 5, ball.x, ball.y, 25); + glow.addColorStop(0, "cyan"); + glow.addColorStop(1, "rgba(0,255,255,0)"); + ctx.beginPath(); + ctx.fillStyle = glow; + ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2); + ctx.fill(); + ctx.closePath(); +} + +function drawObstacles() { + ctx.shadowBlur = 10; + ctx.shadowColor = "#0ff"; + obstacles.forEach(o => { + ctx.fillStyle = "#00ffff44"; + ctx.fillRect(o.x, o.y, o.width, o.height); + ctx.strokeStyle = "#0ff"; + ctx.strokeRect(o.x, o.y, o.width, o.height); + }); +} + +function update() { + if (!gameRunning || isPaused) return; + + ball.dy += gravity; + ball.x += ball.dx; + ball.y += ball.dy; + + // Ground + if (ball.y + ball.radius > canvas.height) { + ball.y = canvas.height - ball.radius; + ball.dy = 0; + } + + // Wall stick logic + if (ball.sticky) { + ball.dy *= 0.9; + } + + // Collisions + obstacles.forEach(o => { + if ( + ball.x + ball.radius > o.x && + ball.x - ball.radius < o.x + o.width && + ball.y + ball.radius > o.y && + ball.y - ball.radius < o.y + o.height + ) { + ball.dy = -10; + hitSound.play(); + } + }); + + // Movement + if (keys.left) ball.dx = -4; + else if (keys.right) ball.dx = 4; + else ball.dx = 0; + + // Ceiling bounce + if (ball.y - ball.radius < 0) { + ball.y = ball.radius; + ball.dy = 2; + } + + draw(); + requestAnimationFrame(update); +} + +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawObstacles(); + drawBall(); +} + +document.addEventListener("keydown", (e) => { + if (e.key === "ArrowLeft" || e.key === "a") keys.left = true; + if (e.key === "ArrowRight" || e.key === "d") keys.right = true; + if ((e.key === "ArrowUp" || e.key === "w") && ball.dy === 0) { + ball.dy = -12; + jumpSound.play(); + } +}); + +document.addEventListener("keyup", (e) => { + if (e.key === "ArrowLeft" || e.key === "a") keys.left = false; + if (e.key === "ArrowRight" || e.key === "d") keys.right = false; +}); + +document.getElementById("startBtn").onclick = () => { + if (!gameRunning) { + bgMusic.play(); + gameRunning = true; + update(); + } +}; + +document.getElementById("pauseBtn").onclick = () => { + isPaused = !isPaused; + document.getElementById("pauseBtn").textContent = isPaused ? "โ–ถ๏ธ Resume" : "โธ๏ธ Pause"; + if (!isPaused) update(); +}; + +document.getElementById("restartBtn").onclick = () => { + ball.x = 100; + ball.y = 350; + ball.dy = 0; + gameRunning = true; + isPaused = false; + bgMusic.currentTime = 0; + bgMusic.play(); + update(); +}; + +document.getElementById("muteBtn").onclick = () => { + bgMusic.muted = !bgMusic.muted; + hitSound.muted = bgMusic.muted; + jumpSound.muted = bgMusic.muted; + document.getElementById("muteBtn").textContent = bgMusic.muted ? "๐Ÿ”Š Unmute" : "๐Ÿ”‡ Mute"; +}; diff --git a/games/sticky-ball/style.css b/games/sticky-ball/style.css new file mode 100644 index 00000000..1a2b8555 --- /dev/null +++ b/games/sticky-ball/style.css @@ -0,0 +1,43 @@ +body { + margin: 0; + overflow: hidden; + background: radial-gradient(circle at center, #0f0f1f, #000); + font-family: 'Poppins', sans-serif; + color: white; + text-align: center; +} + +.game-header { + position: absolute; + top: 10px; + width: 100%; + text-align: center; + z-index: 10; +} + +.controls button { + background: linear-gradient(45deg, #00f, #0ff); + border: none; + padding: 10px 20px; + margin: 5px; + border-radius: 8px; + color: #fff; + font-weight: bold; + cursor: pointer; + box-shadow: 0 0 10px #0ff; + transition: all 0.3s; +} + +.controls button:hover { + box-shadow: 0 0 20px #00f, 0 0 40px #0ff; + transform: scale(1.05); +} + +canvas { + display: block; + margin: 0 auto; + background: radial-gradient(circle at bottom, #222, #111); + border: 3px solid #0ff; + box-shadow: 0 0 30px #0ff; + border-radius: 10px; +} diff --git a/games/sticky-runner/index.html b/games/sticky-runner/index.html new file mode 100644 index 00000000..083e4c2e --- /dev/null +++ b/games/sticky-runner/index.html @@ -0,0 +1,77 @@ + + + + + + Sticky Runner โ€” Mini JS Games Hub + + + +
    +
    +
    + ๐Ÿงฒ +

    Sticky Runner

    +
    + +
    +
    +
    Score 0
    +
    Best 0
    +
    + +
    + + + + +
    +
    +
    + +
    + + + + +
    + +
    + + + + + + + + +
    +
    + Press Space to flip + โ€ข + Tap screen on mobile +
    +
    + + +
    +
    +
    + +
    + Built with โค๏ธ โ€” HTML โ€ข CSS โ€ข JS +
    +
    + + + + diff --git a/games/sticky-runner/script.js b/games/sticky-runner/script.js new file mode 100644 index 00000000..356a7a16 --- /dev/null +++ b/games/sticky-runner/script.js @@ -0,0 +1,407 @@ +/* Sticky Runner โ€” main game logic + - Player sticks to either floor or ceiling. + - Flip to avoid obstacles. + - Speed increases over time. + - Play/Pause/Restart/Mute controls. + - Highscore saved to localStorage. +*/ + +/* ----------------------- + CONFIG & ASSETS (online) + ----------------------- */ +const CONFIG = { + canvasWidth: 1000, + canvasHeight: 320, + trackY: 160, // center line + playerSize: 34, + gravityStickDelay: 0, // unused here but kept for concept + baseSpeed: 4, + spawnInterval: 1400, // ms initial + speedIncreaseEvery: 4000, // ms + spawnMinimumInterval: 520, + obstacleMinGap: 220, + obstacleMaxGap: 420 +}; + +// sound assets (public links) +const SOUNDS = { + jump: "https://actions.google.com/sounds/v1/cartoon/slide_whistle_to_drum_hit.ogg", + hit: "https://actions.google.com/sounds/v1/cartoon/clang_and_wobble.ogg", + coin: "https://actions.google.com/sounds/v1/alarms/beep_short.ogg", + bgm: "https://cdn.jsdelivr.net/gh/anars/blank-audio@master/1-seconds-of-silence.mp3" // silent placeholder (optional) +}; + +/* ----------------------- + DOM & state + ----------------------- */ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); +canvas.width = CONFIG.canvasWidth; +canvas.height = CONFIG.canvasHeight; + +const playBtn = document.getElementById("playBtn"); +const pauseBtn = document.getElementById("pauseBtn"); +const restartBtn = document.getElementById("restartBtn"); +const muteBtn = document.getElementById("muteBtn"); +const overlay = document.getElementById("overlay"); +const overlayPlay = document.getElementById("overlay-play"); +const overlayRestart = document.getElementById("overlay-restart"); +const scoreEl = document.getElementById("score"); +const bestEl = document.getElementById("best"); +const difficultyEl = document.getElementById("difficulty"); + +let lastTimestamp = 0; +let running = false; +let paused = false; +let muted = false; +let gameSpeed = CONFIG.baseSpeed; +let spawnTimer = 0; +let spawnInterval = CONFIG.spawnInterval; +let speedTimer = 0; +let score = 0; +let best = parseInt(localStorage.getItem("sticky-runner-best") || "0", 10); + +/* audio */ +function createAudio(src, loop=false){ + const a = new Audio(src); + a.loop = loop; + a.preload = "auto"; + return a; +} +const sfxJump = createAudio(SOUNDS.jump); +const sfxHit = createAudio(SOUNDS.hit); +const sfxCoin = createAudio(SOUNDS.coin); +const bgm = createAudio(SOUNDS.bgm, true); +bgm.volume = 0.05; + +/* update DOM best */ +bestEl.textContent = best; + +/* ----------------------- + GAME ENTITIES + ----------------------- */ +const player = { + x: 120, + y: CONFIG.trackY, + width: CONFIG.playerSize, + height: CONFIG.playerSize, + stickTo: "floor", // "floor" or "ceiling" + color: "#7c3aed", + vy: 0, + isAlive: true +}; + +let obstacles = []; + +/* ----------------------- + UTIL + ----------------------- */ +function rand(min, max){ return Math.random()*(max-min)+min; } +function clamp(v,min,max){return Math.max(min,Math.min(max,v));} + +/* ----------------------- + OBSTACLE Factory + Each obstacle is a rectangle either on floor or ceiling. + ----------------------- */ +function createObstacle(speed){ + // random gap from other obstacles to create variation + const type = Math.random() < 0.5 ? "floor" : "ceiling"; + const h = rand(22, 66); // height of obstacle + const gapFromEdge = 8; + const y = (type === "floor") ? (CONFIG.trackY + gapFromEdge) : (CONFIG.trackY - gapFromEdge - h); + const w = rand(28, 62); + return { + x: canvas.width + 20, + y: y, + w: w, + h: h, + type: type, + speed: speed + rand(-0.6, 1.2) + }; +} + +/* ----------------------- + CONTROLS + ----------------------- */ +function toggleMute(){ + muted = !muted; + muteBtn.textContent = muted ? "๐Ÿ”‡" : "๐Ÿ”Š"; + [sfxJump,sfxHit,sfxCoin,bgm].forEach(a=>a.muted = muted); +} + +function playSfx(audio){ + if(!muted && audio && audio.play) { + // clone to allow overlap + const cl = audio.cloneNode ? audio.cloneNode(true) : audio; + try{ cl.currentTime = 0; cl.play(); } catch(e){} + } +} + +function flipPlayer(){ + if(!player.isAlive) return; + player.stickTo = (player.stickTo === "floor") ? "ceiling" : "floor"; + playSfx(sfxJump); +} + +/* keyboard & touch */ +window.addEventListener("keydown", (e)=>{ + if(e.code === "Space"){ e.preventDefault(); flipPlayer(); } + if(e.code === "KeyP"){ togglePause(); } +}); +canvas.addEventListener("pointerdown", (e)=>{ + // tap to flip; if overlay visible, start + if(!running){ startGame(); return; } + if(paused){ togglePause(); return; } + flipPlayer(); +}); + +/* UI buttons */ +playBtn.addEventListener("click", ()=>{ if(!running) startGame(); else if(paused) togglePause(); }); +pauseBtn.addEventListener("click", togglePause); +restartBtn.addEventListener("click", restartGame); +muteBtn.addEventListener("click", toggleMute); +overlayPlay.addEventListener("click", ()=>{ startGame(); }); +overlayRestart.addEventListener("click", ()=>{ restartGame(); }); + +/* ----------------------- + GAME STATE functions + ----------------------- */ +function startGame(){ + // reset state + running = true; paused = false; + overlay.classList.add("hidden"); + playBtn.disabled = true; pauseBtn.disabled = false; restartBtn.disabled = false; + obstacles = []; + score = 0; scoreEl.textContent = 0; + player.isAlive = true; + player.stickTo = "floor"; + spawnInterval = CONFIG.spawnInterval - (difficultyEl.value - 1) * 120; + gameSpeed = CONFIG.baseSpeed + (difficultyEl.value - 1); + spawnTimer = 0; speedTimer = 0; + lastTimestamp = 0; + try{ bgm.play(); } catch(e){} + requestAnimationFrame(loop); +} + +function togglePause(){ + if(!running) return; + paused = !paused; + pauseBtn.textContent = paused ? "Resume" : "Pause"; + playBtn.disabled = !paused; + if(paused){ /* stop audio if needed */ bgm.pause(); } + else { try{ bgm.play(); } catch(e){} requestAnimationFrame(loop); } +} + +function endGame(){ + running = false; + player.isAlive = false; + overlay.classList.remove("hidden"); + document.getElementById("overlay-title").textContent = "Game Over"; + document.getElementById("overlay-sub").textContent = `Score: ${score} โ€ข Best: ${best}`; + playBtn.disabled = false; pauseBtn.disabled = true; + try{ sfxHit.play(); } catch(e){} + bgm.pause(); + // save best + if(score > best){ + best = score; + localStorage.setItem("sticky-runner-best", best); + bestEl.textContent = best; + } +} + +function restartGame(){ + running = false; paused = false; + overlay.classList.remove("hidden"); + document.getElementById("overlay-title").textContent = "Sticky Runner"; + document.getElementById("overlay-sub").textContent = "Tap to start โ€” flip between floor and ceiling."; + playBtn.disabled = false; pauseBtn.disabled = true; restartBtn.disabled = true; + try{ bgm.pause(); bgm.currentTime = 0; } catch(e){} + obstacles = []; + score = 0; scoreEl.textContent = 0; +} + +/* ----------------------- + Collision (AABB) + ----------------------- */ +function rectsOverlap(a, b){ + return !(a.x + a.w < b.x || a.x > b.x + b.w || a.y + a.h < b.y || a.y > b.y + b.h); +} + +/* ----------------------- + RENDER + ----------------------- */ +function drawScene(){ + // clear + ctx.clearRect(0,0,canvas.width,canvas.height); + + // ground/ceiling guide + const mid = CONFIG.trackY; + // draw floor and ceiling strips + ctx.fillStyle = "rgba(255,255,255,0.02)"; + ctx.fillRect(0, mid+48, canvas.width, 2); + ctx.fillRect(0, mid-48-2, canvas.width, 2); + + // draw obstacles + obstacles.forEach(ob => { + // gradient + const g = ctx.createLinearGradient(ob.x, ob.y, ob.x+ob.w, ob.y+ob.h); + g.addColorStop(0, "rgba(255,80,120,0.9)"); + g.addColorStop(1, "rgba(124,58,237,0.85)"); + ctx.fillStyle = g; + ctx.fillRect(ob.x, ob.y, ob.w, ob.h); + + // spike decoration (triangles) + ctx.fillStyle = "rgba(255,255,255,0.06)"; + if(ob.type === "floor"){ + const step = 8; + for(let sx = 0; sx < ob.w; sx += step){ + ctx.beginPath(); + ctx.moveTo(ob.x+sx, ob.y); + ctx.lineTo(ob.x+sx+step/2, ob.y-8); + ctx.lineTo(ob.x+sx+step, ob.y); + ctx.closePath(); + ctx.fill(); + } + } else { + const step = 8; + for(let sx = 0; sx < ob.w; sx += step){ + ctx.beginPath(); + ctx.moveTo(ob.x+sx, ob.y+ob.h); + ctx.lineTo(ob.x+sx+step/2, ob.y+ob.h+8); + ctx.lineTo(ob.x+sx+step, ob.y+ob.h); + ctx.closePath(); + ctx.fill(); + } + } + }); + + // draw player + const p = { + w: player.width, + h: player.height, + x: player.x, + y: (player.stickTo === "floor") ? (CONFIG.trackY + 48 - player.height) : (CONFIG.trackY - 48) + }; + // player glow + const pg = ctx.createLinearGradient(p.x, p.y, p.x + p.w, p.y + p.h); + pg.addColorStop(0, "#9be7ff"); + pg.addColorStop(1, "#7c3aed"); + ctx.fillStyle = pg; + // rounded rect + roundRect(ctx, p.x, p.y, p.w, p.h, 8); + ctx.fill(); + + // little face + ctx.fillStyle = "rgba(0,0,0,0.18)"; + ctx.fillRect(p.x + 8, p.y + p.h/2 - 6, 6, 4); + ctx.fillRect(p.x + p.w - 14, p.y + p.h/2 - 6, 6, 4); + + // score text overlay (canvas) + ctx.fillStyle = "rgba(255,255,255,0.06)"; + ctx.font = "12px Inter, sans-serif"; +} + +/* round rect helper */ +function roundRect(ctx, x, y, w, h, r){ + const radius = r || 6; + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.arcTo(x + w, y, x + w, y + h, radius); + ctx.arcTo(x + w, y + h, x, y + h, radius); + ctx.arcTo(x, y + h, x, y, radius); + ctx.arcTo(x, y, x + w, y, radius); + ctx.closePath(); +} + +/* ----------------------- + GAME Loop + ----------------------- */ +function loop(timestamp){ + if(!running || paused){ lastTimestamp = timestamp; return; } + if(!lastTimestamp) lastTimestamp = timestamp; + const dt = Math.min(40, timestamp - lastTimestamp); // clamp dt + lastTimestamp = timestamp; + + // update timers + spawnTimer += dt; + speedTimer += dt; + + // speed increases over time + if(speedTimer > CONFIG.speedIncreaseEvery){ + gameSpeed += 0.35; + speedTimer = 0; + // clamp + gameSpeed = clamp(gameSpeed, CONFIG.baseSpeed, 26); + // also shorten spawn interval gradually + spawnInterval = Math.max(CONFIG.spawnMinimumInterval, spawnInterval - 60); + } + + // spawn obstacles + if(spawnTimer > spawnInterval){ + spawnTimer = 0; + const ob = createObstacle(gameSpeed); + obstacles.push(ob); + } + + // move obstacles + obstacles.forEach(o => { + o.x -= (o.speed + gameSpeed) * (dt / 16.666); // scale movement w.r.t frame time + }); + // remove offscreen + obstacles = obstacles.filter(o => o.x + o.w > -20); + + // check collisions + const pBox = { + x: player.x, y: (player.stickTo === "floor") ? (CONFIG.trackY + 48 - player.height) : (CONFIG.trackY - 48), + w: player.width, h: player.height + }; + for(let o of obstacles){ + const oBox = {x:o.x, y:o.y, w:o.w, h:o.h}; + if(rectsOverlap(pBox, oBox) && player.isAlive){ + // if overlap, die + player.isAlive = false; + playSfx(sfxHit); + endGame(); + return; + } + } + + // scoring: each frame survived adds fractional score โ€” convert to int + score += (dt / 1000) * (1 + (gameSpeed/6)); + scoreEl.textContent = Math.floor(score); + + // draw + drawScene(); + + // schedule next frame + requestAnimationFrame(loop); +} + +/* ----------------------- + BULBS INITIALIZATION (visual) + ----------------------- */ +function populateBulbs(){ + const bulbsContainer = document.getElementById("bulbs"); + bulbsContainer.innerHTML = ""; + // create many bulbs across width (visual only) + const count = 60; + for(let i=0;i + + + + + Sudoku Game + + + +

    Sudoku

    +
    + + + \ No newline at end of file diff --git a/games/sudoku/script.js b/games/sudoku/script.js new file mode 100644 index 00000000..726add1f --- /dev/null +++ b/games/sudoku/script.js @@ -0,0 +1,41 @@ +const board = document.getElementById("sudoku-board"); + +// Sample puzzle (0 = empty) +const puzzle = [ + [5, 3, 0, 0, 7, 0, 0, 0, 0], + [6, 0, 0, 1, 9, 5, 0, 0, 0], + [0, 9, 8, 0, 0, 0, 0, 6, 0], + [8, 0, 0, 0, 6, 0, 0, 0, 3], + [4, 0, 0, 8, 0, 3, 0, 0, 1], + [7, 0, 0, 0, 2, 0, 0, 0, 6], + [0, 6, 0, 0, 0, 0, 2, 8, 0], + [0, 0, 0, 4, 1, 9, 0, 0, 5], + [0, 0, 0, 0, 8, 0, 0, 7, 9], +]; + +function createBoard() { + for (let row = 0; row < 9; row++) { + for (let col = 0; col < 9; col++) { + const cell = document.createElement("input"); + cell.classList.add("cell"); + cell.maxLength = 1; + + if (puzzle[row][col] !== 0) { + cell.value = puzzle[row][col]; + cell.disabled = true; + cell.classList.add("prefilled"); + } + + cell.addEventListener("input", () => { + const val = cell.value; + if (!/^[1-9]$/.test(val)) { + cell.value = ""; + } + }); + + board.appendChild(cell); + } + } +} + +createBoard(); \ No newline at end of file diff --git a/games/sudoku/style.css b/games/sudoku/style.css new file mode 100644 index 00000000..3b365c0d --- /dev/null +++ b/games/sudoku/style.css @@ -0,0 +1,38 @@ +body { + font-family: Arial, sans-serif; + text-align: center; + background-color: purple; +} + +h1 { + margin-top: 20px; + color: white; +} + +#sudoku-board { + display: grid; + grid-template-columns: repeat(9, 50px); + grid-template-rows: repeat(9, 50px); + gap: 2px; + margin: 20px auto; + width: fit-content; +} + +.cell { + width: 50px; + height: 50px; + font-size: 20px; + text-align: center; + border: 1px solid rgb(0, 0, 0); + background-color: white; +} + +.cell:focus { + outline: none; + background-color: #e0f7fa; +} + +.prefilled { + background-color: rgb(250, 165, 250); + font-weight: bold; +} \ No newline at end of file diff --git a/games/suduko_clone/index.html b/games/suduko_clone/index.html new file mode 100644 index 00000000..0601bddb --- /dev/null +++ b/games/suduko_clone/index.html @@ -0,0 +1,42 @@ + + + + + + Mini Sudoku (4x4) + + + + +
    +

    ๐Ÿ”ข Mini Sudoku (4x4)

    + +
    + +
    +
    + +
    +

    Select Number:

    +
    + + + + + +
    +
    + +
    +

    Click a cell, then click a number to place it (1-4).

    +
    + +
    + + +
    +
    + + + + \ No newline at end of file diff --git a/games/suduko_clone/script.js b/games/suduko_clone/script.js new file mode 100644 index 00000000..50bc40a2 --- /dev/null +++ b/games/suduko_clone/script.js @@ -0,0 +1,231 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. GAME DATA --- + + // Puzzle definition: 0 represents an empty cell + // This is the starting configuration + const PUZZLE = [ + [0, 3, 0, 0], + [4, 0, 0, 2], + [0, 0, 1, 0], + [0, 4, 2, 0] + ]; + + // Complete Solution (used for validation/solving) + const SOLUTION = [ + [1, 3, 4, 2], + [4, 2, 1, 3], + [2, 3, 1, 4], // Error in original prompt solution, fixed for 4x4 rules + [3, 4, 2, 1] + ]; + + // --- 2. DOM Elements & Constants --- + const gridContainer = document.getElementById('sudoku-grid'); + const numberButtons = document.querySelectorAll('.num-input'); + const feedbackMessage = document.getElementById('feedback-message'); + const resetButton = document.getElementById('reset-button'); + const solveButton = document.getElementById('solve-button'); + + const GRID_SIZE = 4; + + // Game State + let currentGrid = []; + let initialGrid = []; + let selectedCell = null; // {row, col, element} + + // --- 3. CORE VALIDATION LOGIC --- + + /** + * Checks if placing 'num' at (r, c) violates any of the three Sudoku rules. + * @param {Array>} grid - The current 4x4 puzzle grid state. + * @param {number} r - Row index. + * @param {number} c - Column index. + * @param {number} num - The number to test (1-4). + * @returns {boolean} True if the placement is valid, false otherwise. + */ + function isValidPlacement(grid, r, c, num) { + if (num === 0) return true; // Clearing a cell is always valid + + // 1. Check Row (Horizontal) + for (let col = 0; col < GRID_SIZE; col++) { + if (col !== c && grid[r][col] === num) { + return false; + } + } + + // 2. Check Column (Vertical) + for (let row = 0; row < GRID_SIZE; row++) { + if (row !== r && grid[row][c] === num) { + return false; + } + } + + // 3. Check 2x2 Box (Subgrid) + // Determine the top-left corner of the 2x2 box + const startRow = Math.floor(r / 2) * 2; + const startCol = Math.floor(c / 2) * 2; + + for (let row = startRow; row < startRow + 2; row++) { + for (let col = startCol; col < startCol + 2; col++) { + if (row !== r && col !== c && grid[row][col] === num) { + return false; + } + } + } + + return true; + } + + /** + * Iterates over the entire grid and highlights any cells that cause a conflict. + */ + function validateAndHighlightGrid() { + let isComplete = true; + + for (let r = 0; r < GRID_SIZE; r++) { + for (let c = 0; c < GRID_SIZE; c++) { + const cellElement = gridContainer.rows[r].cells[c]; + const value = currentGrid[r][c]; + + // Remove error class from previous check + cellElement.classList.remove('error'); + + if (value !== 0) { + // Temporarily set cell to 0, then check if placing 'value' back creates an error + // This is necessary because we need to check the value against its peers, not itself. + currentGrid[r][c] = 0; + const isConflict = !isValidPlacement(currentGrid, r, c, value); + currentGrid[r][c] = value; // Restore the value + + if (isConflict) { + cellElement.classList.add('error'); + } + } + + if (value === 0) { + isComplete = false; + } + } + } + + // Check for Win Condition + if (isComplete && document.querySelectorAll('.error').length === 0) { + feedbackMessage.innerHTML = '๐ŸŽ‰ **PUZZLE SOLVED!** All rules satisfied.'; + feedbackMessage.style.color = '#28a745'; + gridContainer.removeEventListener('click', handleCellSelect); + } else if (isComplete) { + feedbackMessage.innerHTML = 'Almost there! Check the highlighted errors.'; + } + } + + // --- 4. GAME FLOW AND RENDERING --- + + /** + * Renders the initial puzzle grid structure. + */ + function renderGrid() { + gridContainer.innerHTML = ''; // Clear existing table + + // Deep copy the puzzle to currentGrid and initialGrid + initialGrid = PUZZLE.map(row => [...row]); + currentGrid = initialGrid.map(row => [...row]); + + for (let r = 0; r < GRID_SIZE; r++) { + const row = gridContainer.insertRow(); + for (let c = 0; c < GRID_SIZE; c++) { + const cell = row.insertCell(); + cell.classList.add('sudoku-cell'); + cell.setAttribute('data-row', r); + cell.setAttribute('data-col', c); + + const value = currentGrid[r][c]; + if (value !== 0) { + cell.textContent = value; + cell.classList.add('initial'); + } else { + cell.addEventListener('click', handleCellSelect); + } + } + } + } + + /** + * Handles the click on a grid cell, marking it as selected. + */ + function handleCellSelect(event) { + // Clear previous selection highlight + if (selectedCell && selectedCell.element) { + selectedCell.element.classList.remove('selected'); + } + + // Set new selection + const cell = event.target; + const r = parseInt(cell.getAttribute('data-row')); + const c = parseInt(cell.getAttribute('data-col')); + + cell.classList.add('selected'); + selectedCell = { row: r, col: c, element: cell }; + + feedbackMessage.textContent = `Cell (${r + 1}, ${c + 1}) selected. Click a number to place it.`; + } + + /** + * Handles the click on an input button (1-4 or Clear). + */ + function handleNumberInput(event) { + if (!selectedCell) { + feedbackMessage.textContent = 'Please click an empty cell first!'; + return; + } + + const num = parseInt(event.target.getAttribute('data-value')); + const { row: r, col: c, element: cell } = selectedCell; + + // 1. Check if placement is valid (only check if placing 1-4) + if (num !== 0 && !isValidPlacement(currentGrid, r, c, num)) { + feedbackMessage.textContent = `โŒ Cannot place ${num}! Conflict in row, column, or box.`; + cell.classList.add('error'); // Instant feedback + return; + } + + // 2. Update model and view + currentGrid[r][c] = num; + cell.textContent = num === 0 ? '' : num; + cell.classList.remove('selected'); + + // Clear selection and validation check + selectedCell = null; + validateAndHighlightGrid(); + + feedbackMessage.textContent = num === 0 ? 'Cell cleared.' : `${num} placed.`; + } + + // --- 5. EVENT LISTENERS AND SETUP --- + + function solvePuzzle() { + // Render the solution array + for (let r = 0; r < GRID_SIZE; r++) { + for (let c = 0; c < GRID_SIZE; c++) { + const cell = gridContainer.rows[r].cells[c]; + if (!cell.classList.contains('initial')) { + cell.textContent = SOLUTION[r][c]; + cell.classList.remove('error'); + cell.classList.remove('selected'); + } + } + } + currentGrid = SOLUTION.map(row => [...row]); + feedbackMessage.innerHTML = 'Solution revealed.'; + gridContainer.removeEventListener('click', handleCellSelect); + } + + // Attach event listeners + numberButtons.forEach(button => { + button.addEventListener('click', handleNumberInput); + }); + + resetButton.addEventListener('click', renderGrid); + solveButton.addEventListener('click', solvePuzzle); + + // Initial setup + renderGrid(); +}); \ No newline at end of file diff --git a/games/suduko_clone/style.css b/games/suduko_clone/style.css new file mode 100644 index 00000000..c112ebf8 --- /dev/null +++ b/games/suduko_clone/style.css @@ -0,0 +1,144 @@ +:root { + --grid-size: 4; + --cell-size: 50px; +} + +body { + font-family: 'Arial', sans-serif; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f8f9fa; + color: #343a40; +} + +#game-container { + background-color: white; + padding: 30px; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + text-align: center; +} + +h1 { + color: #007bff; + margin-bottom: 20px; +} + +/* --- Sudoku Grid Table --- */ +#sudoku-grid { + border-collapse: collapse; + margin: 0 auto; + border: 3px solid #333; /* Outer border */ +} + +.sudoku-cell { + width: var(--cell-size); + height: var(--cell-size); + border: 1px solid #ccc; /* Inner cell borders */ + text-align: center; + font-size: 1.5em; + font-weight: bold; + cursor: pointer; + transition: background-color 0.1s; +} + +/* Thicker borders to delineate 2x2 subgrids */ +.sudoku-cell:nth-child(2n) { + border-right: 2px solid #555; +} +tr:nth-child(2n) .sudoku-cell { + border-bottom: 2px solid #555; +} + +/* Fix the outer borders */ +.sudoku-cell:nth-child(4n) { + border-right: 1px solid #ccc; +} +tr:nth-child(4n) .sudoku-cell { + border-bottom: 1px solid #ccc; +} + + +/* Cell States */ +.initial { + background-color: #e9ecef; /* Grey background for pre-filled cells */ + color: #343a40; + cursor: default; +} + +.selected { + background-color: #fff3cd; /* Light yellow highlight for selected cell */ +} + +.error { + background-color: #f8d7da !important; /* Light red for conflicts */ + color: #721c24 !important; +} + +/* --- Input Controls --- */ +#input-controls { + margin-top: 20px; +} + +#number-buttons { + display: flex; + justify-content: center; + gap: 5px; + margin-top: 10px; +} + +.num-input { + padding: 10px 15px; + font-size: 1.1em; + font-weight: bold; + border: 1px solid #007bff; + border-radius: 5px; + background-color: #e9f5ff; + color: #007bff; + cursor: pointer; + transition: background-color 0.1s; +} + +.num-input:hover { + background-color: #cce5ff; +} + +.num-input[data-value="0"] { + background-color: #6c757d; + color: white; +} + +/* --- Feedback and Controls --- */ +#feedback-message { + min-height: 20px; + margin-top: 15px; +} + +#controls { + margin-top: 20px; +} + +#controls button { + padding: 10px 15px; + font-size: 1em; + font-weight: bold; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; + margin: 0 5px; +} + +#reset-button { + background-color: #dc3545; + color: white; +} + +#solve-button { + background-color: #28a745; + color: white; +} \ No newline at end of file diff --git a/games/symbol-path/index.html b/games/symbol-path/index.html new file mode 100644 index 00000000..80234610 --- /dev/null +++ b/games/symbol-path/index.html @@ -0,0 +1,34 @@ + + + + + + Symbol Path | Mini JS Games Hub + + + + + + + +
    +

    Symbol Path ๐Ÿ”—

    +
    + + + +
    +
    + +
    + +
    + +
    +

    Objective: Draw a glowing path connecting symbols in the right order!

    +

    Hint: Avoid obstacles ๐Ÿšง and complete the pattern ๐Ÿ’ก.

    +
    + + + + diff --git a/games/symbol-path/script.js b/games/symbol-path/script.js new file mode 100644 index 00000000..10b58de7 --- /dev/null +++ b/games/symbol-path/script.js @@ -0,0 +1,170 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +const successSound = document.getElementById("success-sound"); +const failSound = document.getElementById("fail-sound"); +const drawSound = document.getElementById("draw-sound"); + +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const restartBtn = document.getElementById("restart-btn"); + +let isDrawing = false; +let isPaused = false; +let pathPoints = []; +let symbols = []; +let obstacles = []; +let currentLevel = 1; +let targetSequence = []; +let sequenceIndex = 0; + +const LEVELS = { + 1: { symbols: 4, obstacles: 2 }, + 2: { symbols: 6, obstacles: 3 }, + 3: { symbols: 8, obstacles: 4 }, +}; + +function random(min, max) { + return Math.floor(Math.random() * (max - min) + min); +} + +function initLevel(level) { + const { symbols: symCount, obstacles: obsCount } = LEVELS[level]; + symbols = []; + obstacles = []; + pathPoints = []; + sequenceIndex = 0; + + for (let i = 0; i < symCount; i++) { + symbols.push({ + x: random(50, 550), + y: random(50, 550), + label: i + 1, + }); + } + + for (let i = 0; i < obsCount; i++) { + obstacles.push({ + x: random(100, 500), + y: random(100, 500), + size: random(40, 60), + }); + } + + targetSequence = symbols.map(s => s.label); + drawGame(); +} + +function drawGame() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Obstacles + ctx.fillStyle = "rgba(255,0,0,0.5)"; + obstacles.forEach(o => { + ctx.beginPath(); + ctx.rect(o.x, o.y, o.size, o.size); + ctx.fill(); + }); + + // Symbols + symbols.forEach((s, index) => { + const glow = index === sequenceIndex ? 20 : 10; + ctx.beginPath(); + ctx.arc(s.x, s.y, 20, 0, Math.PI * 2); + ctx.shadowBlur = glow; + ctx.shadowColor = "#00eaff"; + ctx.fillStyle = "#00eaff"; + ctx.fill(); + ctx.font = "bold 16px Poppins"; + ctx.fillStyle = "#000"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(s.label, s.x, s.y); + }); + + // Path + ctx.beginPath(); + ctx.strokeStyle = "#00ff99"; + ctx.lineWidth = 5; + for (let i = 0; i < pathPoints.length - 1; i++) { + ctx.moveTo(pathPoints[i].x, pathPoints[i].y); + ctx.lineTo(pathPoints[i + 1].x, pathPoints[i + 1].y); + } + ctx.stroke(); + ctx.shadowBlur = 0; +} + +function checkCollision(x, y) { + return obstacles.some(o => + x > o.x && x < o.x + o.size && y > o.y && y < o.y + o.size + ); +} + +canvas.addEventListener("mousedown", (e) => { + if (isPaused) return; + isDrawing = true; + pathPoints = [{ x: e.offsetX, y: e.offsetY }]; + drawSound.play(); +}); + +canvas.addEventListener("mousemove", (e) => { + if (isDrawing && !isPaused) { + const x = e.offsetX; + const y = e.offsetY; + if (checkCollision(x, y)) { + failSound.play(); + alert("โŒ You hit an obstacle! Try again."); + initLevel(currentLevel); + return; + } + pathPoints.push({ x, y }); + drawGame(); + } +}); + +canvas.addEventListener("mouseup", () => { + isDrawing = false; + validatePath(); +}); + +function validatePath() { + const lastPoint = pathPoints[pathPoints.length - 1]; + const target = symbols[sequenceIndex]; + if (!target) return; + + const distance = Math.hypot(target.x - lastPoint.x, target.y - lastPoint.y); + if (distance < 30) { + sequenceIndex++; + if (sequenceIndex === symbols.length) { + successSound.play(); + alert(`๐ŸŽ‰ Level ${currentLevel} Complete!`); + currentLevel++; + if (currentLevel > Object.keys(LEVELS).length) { + alert("๐Ÿ† You completed all levels!"); + currentLevel = 1; + } + initLevel(currentLevel); + } else { + successSound.play(); + drawGame(); + } + } else { + failSound.play(); + alert("โš ๏ธ Wrong path, try again!"); + initLevel(currentLevel); + } +} + +startBtn.addEventListener("click", () => { + isPaused = false; + initLevel(currentLevel); +}); + +pauseBtn.addEventListener("click", () => { + isPaused = !isPaused; + pauseBtn.textContent = isPaused ? "โ–ถ๏ธ Resume" : "โธ๏ธ Pause"; +}); + +restartBtn.addEventListener("click", () => { + initLevel(currentLevel); +}); diff --git a/games/symbol-path/style.css b/games/symbol-path/style.css new file mode 100644 index 00000000..490768d7 --- /dev/null +++ b/games/symbol-path/style.css @@ -0,0 +1,62 @@ +body { + margin: 0; + background: radial-gradient(circle at center, #0f0f0f, #000); + color: #fff; + font-family: "Poppins", sans-serif; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + overflow: hidden; +} + +.game-header { + display: flex; + align-items: center; + justify-content: space-between; + width: 600px; + margin-bottom: 10px; +} + +h1 { + font-size: 1.8rem; + color: #00eaff; + text-shadow: 0 0 10px #00eaff, 0 0 20px #00eaff; +} + +.controls button { + background: none; + border: 2px solid #00eaff; + color: #00eaff; + font-size: 1rem; + margin-left: 8px; + padding: 6px 12px; + border-radius: 8px; + cursor: pointer; + transition: 0.2s ease; +} + +.controls button:hover { + background: #00eaff; + color: #000; + box-shadow: 0 0 10px #00eaff, 0 0 20px #00eaff; +} + +.game-container { + position: relative; +} + +canvas { + background: #111; + border-radius: 15px; + box-shadow: 0 0 20px #00eaff, 0 0 40px #00eaff; +} + +.info { + margin-top: 12px; + font-size: 0.95rem; + color: #ccc; + text-align: center; + width: 600px; +} diff --git a/games/symbol-swap/index.html b/games/symbol-swap/index.html new file mode 100644 index 00000000..0552aaaa --- /dev/null +++ b/games/symbol-swap/index.html @@ -0,0 +1,35 @@ + + + + + + Symbol Swap | Mini JS Games Hub + + + +
    +

    Symbol Swap ๐Ÿ”ฅ

    +

    Swap symbols to match the target pattern before time runs out!

    + +
    + + + + Time: 60s +
    + +
    + +
    + +
    +
    + + + + + + + + + diff --git a/games/symbol-swap/script.js b/games/symbol-swap/script.js new file mode 100644 index 00000000..cb3fc6c9 --- /dev/null +++ b/games/symbol-swap/script.js @@ -0,0 +1,109 @@ +const symbols = ["๐Ÿ”ด", "๐Ÿ”ต", "๐ŸŸข", "๐ŸŸก", "๐ŸŸฃ", "๐ŸŸ "]; +let targetPattern = []; +let currentPattern = []; +let selectedIndex = null; +let timer = 60; +let interval = null; +let paused = false; + +const line = document.getElementById("symbol-line"); +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const restartBtn = document.getElementById("restart-btn"); +const messageEl = document.getElementById("message"); +const timerEl = document.getElementById("timer"); + +const swapSound = document.getElementById("swap-sound"); +const successSound = document.getElementById("success-sound"); +const failSound = document.getElementById("fail-sound"); + +// Generate random pattern +function generatePattern(length = 8) { + const arr = []; + for (let i = 0; i < length; i++) { + arr.push(symbols[Math.floor(Math.random() * symbols.length)]); + } + return arr; +} + +// Render symbols +function renderLine() { + line.innerHTML = ""; + currentPattern.forEach((sym, i) => { + const span = document.createElement("span"); + span.className = "symbol"; + span.textContent = sym; + span.addEventListener("click", () => selectSymbol(i)); + if (i === selectedIndex) span.classList.add("selected"); + line.appendChild(span); + }); +} + +// Handle selection and swap +function selectSymbol(index) { + if (paused) return; + if (selectedIndex === null) { + selectedIndex = index; + renderLine(); + } else { + [currentPattern[selectedIndex], currentPattern[index]] = [currentPattern[index], currentPattern[selectedIndex]]; + swapSound.play(); + selectedIndex = null; + renderLine(); + checkWin(); + } +} + +// Check win +function checkWin() { + if (currentPattern.join("") === targetPattern.join("")) { + messageEl.textContent = "๐ŸŽ‰ You Matched the Pattern!"; + successSound.play(); + clearInterval(interval); + } +} + +// Timer +function startTimer() { + interval = setInterval(() => { + if (!paused) { + timer--; + timerEl.textContent = `Time: ${timer}s`; + if (timer <= 0) { + clearInterval(interval); + messageEl.textContent = `๐Ÿ’€ Time's up! Pattern was: ${targetPattern.join("")}`; + failSound.play(); + } + } + }, 1000); +} + +// Controls +startBtn.addEventListener("click", () => { + timer = 60; + paused = false; + targetPattern = generatePattern(); + currentPattern = [...generatePattern()]; + selectedIndex = null; + messageEl.textContent = ""; + renderLine(); + clearInterval(interval); + startTimer(); +}); + +pauseBtn.addEventListener("click", () => { + paused = !paused; + pauseBtn.textContent = paused ? "Resume" : "Pause"; +}); + +restartBtn.addEventListener("click", () => { + timer = 60; + paused = false; + targetPattern = generatePattern(); + currentPattern = [...generatePattern()]; + selectedIndex = null; + messageEl.textContent = ""; + renderLine(); + clearInterval(interval); + startTimer(); +}); diff --git a/games/symbol-swap/style.css b/games/symbol-swap/style.css new file mode 100644 index 00000000..83d769c3 --- /dev/null +++ b/games/symbol-swap/style.css @@ -0,0 +1,89 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: #0d0d0d; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; +} + +.game-container { + background: #1a1a1a; + padding: 30px 40px; + border-radius: 15px; + box-shadow: 0 0 20px #ff00ff, 0 0 40px #00ffff; + text-align: center; + max-width: 800px; + width: 100%; +} + +h1 { + font-size: 2.2rem; + margin-bottom: 10px; + text-shadow: 0 0 10px #ff00ff, 0 0 20px #00ffff; +} + +.description { + font-size: 1rem; + margin-bottom: 20px; + color: #ccc; +} + +.controls { + margin-bottom: 20px; +} + +.controls button { + background: linear-gradient(45deg, #ff00ff, #00ffff); + border: none; + color: #fff; + padding: 8px 16px; + margin: 0 5px; + border-radius: 8px; + cursor: pointer; + font-size: 1rem; + transition: 0.3s; + box-shadow: 0 0 10px #ff00ff, 0 0 20px #00ffff; +} + +.controls button:hover { + transform: scale(1.1); + box-shadow: 0 0 20px #ff00ff, 0 0 40px #00ffff; +} + +#timer { + margin-left: 20px; + font-weight: bold; + font-size: 1.1rem; +} + +.symbol-line { + display: flex; + justify-content: center; + gap: 15px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.symbol { + font-size: 2rem; + padding: 15px; + background: #222; + border-radius: 10px; + cursor: pointer; + transition: 0.3s; + box-shadow: 0 0 5px #ff00ff; +} + +.symbol.selected { + box-shadow: 0 0 20px #ff00ff, 0 0 40px #00ffff; + transform: scale(1.2); +} + +.message { + font-size: 1.2rem; + margin-top: 15px; + min-height: 30px; +} diff --git a/games/symon-says/index.html b/games/symon-says/index.html new file mode 100644 index 00000000..3edd4363 --- /dev/null +++ b/games/symon-says/index.html @@ -0,0 +1,38 @@ + + + + + + Simon Says + + + + + + + +
    +

    SIMON SAYS

    + +
    + +
    +
    +
    +
    +
    + + +
    +
    + SCORE: 0 +
    + +
    Click START to begin!
    +
    +
    +
    +
    + + + \ No newline at end of file diff --git a/games/symon-says/script.js b/games/symon-says/script.js new file mode 100644 index 00000000..dafedf3e --- /dev/null +++ b/games/symon-says/script.js @@ -0,0 +1,179 @@ +// --- Tone.js Setup (Synthesizer and Notes) --- +const synth = new Tone.Synth().toDestination(); +// Map quadrant IDs to their corresponding flash classes and notes (pitches) +const QUADRANTS = { + 'green': { note: 'E4', flashClass: 'flash-green' }, + 'red': { note: 'G#4', flashClass: 'flash-red' }, + 'yellow': { note: 'C#5', flashClass: 'flash-yellow' }, + 'blue': { note: 'E5', flashClass: 'flash-blue' } +}; +const colors = ['green', 'red', 'yellow', 'blue']; + +// --- Game State --- +let sequence = []; // The sequence the player must follow +let playerInput = []; // The sequence the player is currently entering +let score = 0; +let isGameRunning = false; +let isPlayerTurn = false; +const SEQUENCE_DELAY = 600; // Time in ms between flashes +const FLASH_DURATION = 300; // Time in ms a light stays lit + +// --- DOM Elements --- +const scoreValue = document.getElementById('score-value'); +const startButton = document.getElementById('start-button'); +const messageDisplay = document.getElementById('message'); +const quadrantElements = document.querySelectorAll('.quadrant'); + + +// --- Game Flow Functions --- + +/** + * Initializes the game state and starts the first round. + */ +function startGame() { + // Tone.js requires a user interaction to start the audio context + if (Tone.context.state !== 'running') { + Tone.start(); + } + + isGameRunning = true; + isPlayerTurn = false; + score = 0; + sequence = []; + playerInput = []; + scoreValue.textContent = score; + startButton.disabled = true; + messageDisplay.textContent = 'Watch the sequence...'; + + // Attach click handlers only when game starts + quadrantElements.forEach(q => q.addEventListener('click', handlePlayerClick)); + + nextRound(); +} + +/** + * Adds a new random step to the sequence and starts showing it. + */ +function nextRound() { + score++; + scoreValue.textContent = score; + + // Add one random color to the sequence + const randomColor = colors[Math.floor(Math.random() * colors.length)]; + sequence.push(randomColor); + + playerInput = []; + isPlayerTurn = false; + messageDisplay.textContent = `Level ${score}: Watch!`; + + // Delay before showing sequence begins + setTimeout(showSequence, SEQUENCE_DELAY); +} + +/** + * Iterates through the sequence, flashing lights and playing sounds. + */ +function showSequence() { + let i = 0; + const intervalId = setInterval(() => { + if (i < sequence.length) { + const color = sequence[i]; + flashQuadrant(color); + i++; + } else { + clearInterval(intervalId); + startPlayerTurn(); + } + }, SEQUENCE_DELAY); +} + +/** + * Sets the game state to receive player input. + */ +function startPlayerTurn() { + isPlayerTurn = true; + messageDisplay.textContent = 'Your turn! Repeat the sequence.'; +} + + +// --- Interaction Functions --- + +/** + * Visually flashes a quadrant and plays its corresponding tone. + * @param {string} color - The ID of the quadrant (e.g., 'green'). + */ +function flashQuadrant(color) { + const element = document.getElementById(color); + const { note, flashClass } = QUADRANTS[color]; + + // 1. Play Sound + synth.triggerAttackRelease(note, "8n"); + + // 2. Flash Visual + element.classList.add(flashClass); + setTimeout(() => { + element.classList.remove(flashClass); + }, FLASH_DURATION); +} + +/** + * Handles a click event from the player on one of the quadrants. + * @param {Event} event + */ +function handlePlayerClick(event) { + if (!isGameRunning || !isPlayerTurn) { + return; + } + + const clickedColor = event.currentTarget.id; + playerInput.push(clickedColor); + + // Provide immediate feedback + flashQuadrant(clickedColor); + + // Check if the input is correct so far + const inputIndex = playerInput.length - 1; + if (playerInput[inputIndex] === sequence[inputIndex]) { + // Correct input + + // Check if the sequence is complete + if (playerInput.length === sequence.length) { + isPlayerTurn = false; + messageDisplay.textContent = 'Success! Moving to the next round...'; + + // Advance to the next round + setTimeout(nextRound, SEQUENCE_DELAY * 1.5); + } + } else { + // Incorrect input + gameOver(); + } +} + +/** + * Ends the game and displays the final score. + */ +function gameOver() { + isGameRunning = false; + isPlayerTurn = false; + + // Play a failure tone (low frequency, short duration) + synth.triggerAttackRelease("F2", "2n"); + + messageDisplay.textContent = `Game Over! Final Score: ${score - 1}`; + startButton.textContent = "PLAY AGAIN"; + startButton.disabled = false; + + // Remove click handlers + quadrantElements.forEach(q => q.removeEventListener('click', handlePlayerClick)); +} + + +// --- Initialization --- +document.addEventListener('DOMContentLoaded', () => { + // Set initial message + messageDisplay.textContent = 'Click START to begin!'; + + // Add start button handler + startButton.addEventListener('click', startGame); +}); diff --git a/games/symon-says/style.css b/games/symon-says/style.css new file mode 100644 index 00000000..2d89d074 --- /dev/null +++ b/games/symon-says/style.css @@ -0,0 +1,134 @@ +:root { + --green-default: #00a74a; + --green-flash: #13ff7c; + --red-default: #9f0f17; + --red-flash: #ff4139; + --yellow-default: #ccb900; + --yellow-flash: #ffff40; + --blue-default: #094a8f; + --blue-flash: #1c8cff; + --center-bg: #303030; + --body-bg: #0d1117; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: Arial, sans-serif; + background-color: var(--body-bg); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; +} + +#game-container { + text-align: center; +} + +h1 { + font-size: 2.5rem; + margin-bottom: 20px; + color: #c9d1d9; +} + +#simon-game { + position: relative; + width: 400px; + height: 400px; + margin: 0 auto; + border-radius: 50%; + background: var(--center-bg); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.4); + border: 15px solid #202020; +} + +/* Quadrants Layout */ +#quadrants { + width: 100%; + height: 100%; + border-radius: 50%; + overflow: hidden; + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; +} + +.quadrant { + cursor: pointer; + border: none; + transition: background-color 0.1s; +} + +/* Quadrant Specific Colors */ +#green { background-color: var(--green-default); border-radius: 100% 0 0 0; } +#red { background-color: var(--red-default); border-radius: 0 100% 0 0; } +#yellow { background-color: var(--yellow-default); border-radius: 0 0 0 100%; } +#blue { background-color: var(--blue-default); border-radius: 0 0 100% 0; } + +/* Center Display */ +#center-display { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 45%; + height: 45%; + background-color: var(--center-bg); + border-radius: 50%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5) inset; + padding: 10px; + color: #fff; +} + +#score-display { + font-size: 1rem; + font-weight: bold; + color: var(--light-color); + margin-bottom: 10px; +} +#score-value { + color: var(--light-color); +} + +#message { + font-size: 0.8rem; + margin-top: 5px; + color: #8b949e; +} + +/* Button */ +#start-button { + background-color: var(--green-default); + color: white; + padding: 8px 15px; + border: 2px solid #fff; + border-radius: 5px; + cursor: pointer; + font-weight: bold; + transition: background-color 0.2s; + font-size: 1rem; +} + +#start-button:hover:not(:disabled) { + background-color: var(--green-flash); +} +#start-button:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +/* Flash Animation Classes */ +.quadrant.flash-green { background-color: var(--green-flash); } +.quadrant.flash-red { background-color: var(--red-flash); } +.quadrant.flash-yellow { background-color: var(--yellow-flash); } +.quadrant.flash-blue { background-color: var(--blue-flash); } diff --git a/games/tab_holder/index.html b/games/tab_holder/index.html new file mode 100644 index 00000000..be1699de --- /dev/null +++ b/games/tab_holder/index.html @@ -0,0 +1,48 @@ + + + + + + The Tab Hoarder - Loading... + + + +
    +
    +

    Initializing Facility...

    +
    + ๐Ÿ”ฉ Metal: 0 + ๐ŸŽ Food: 0 + ๐Ÿงช Science: 0 +
    +
    + +
    +
    +

    System Status:

    +

    Awaiting initial data...

    +
    + + +
    +
    + +
    +

    Production Controls

    + +

    +
    + +
    +

    Facility Management

    +

    Required to Unlock New Facility: N/A

    + + +
    +
    +
    + + + + \ No newline at end of file diff --git a/games/tab_holder/script.js b/games/tab_holder/script.js new file mode 100644 index 00000000..02302689 --- /dev/null +++ b/games/tab_holder/script.js @@ -0,0 +1,326 @@ +// --- 1. Global State Management & Constants --- + +const STORAGE_KEY = 'tabHoarderGameData'; +const FACILITY_KEY = new URLSearchParams(window.location.search).get('facility') || 'mine'; + +const FACILITY_DATA = { + mine: { + name: "Deep Core Mine โ›๏ธ", + theme: "mine-theme", + dangerLabel: "Overheat Level", + resourceOutput: { metal: 5, food: -1, science: 0 }, // Mine consumes food + actionCooldown: 5000, // 5 seconds + unlocks: 'farm', + unlockCost: { metal: 50, food: 20 }, + dangerIncreaseRate: 2, // Per second when inactive + dangerThreshold: 90, + disaster: "Mine Overheat! ๐Ÿ”ฉ Metal production halved until reset." + }, + farm: { + name: "Hydroponics Farm ๐ŸŽ", + theme: "farm-theme", + dangerLabel: "Decay Rate", + resourceOutput: { metal: 0, food: 10, science: -2 }, // Farm consumes science + actionCooldown: 4000, + unlocks: 'lab', + unlockCost: { food: 100, science: 50 }, + dangerIncreaseRate: 3, // Per second when inactive + dangerThreshold: 80, + disaster: "Crop Rot! ๐ŸŽ Food production halved until reset." + }, + lab: { + name: "Research Lab ๐Ÿงช", + theme: "lab-theme", + dangerLabel: "Contamination", + resourceOutput: { metal: -2, food: 0, science: 10 }, // Lab consumes metal + actionCooldown: 7000, + unlocks: null, // End of available facilities for now + unlockCost: {}, + dangerIncreaseRate: 1.5, // Per second when inactive + dangerThreshold: 95, + disaster: "Contamination! ๐Ÿงช Science production halved until reset." + } +}; + +let gameData = {}; +let lastUpdateTime = performance.now(); +let facilityCooldown = 0; +let facilityState = FACILITY_DATA[FACILITY_KEY]; +let productionRunning = false; +let gameLoopInterval = null; + +// --- 2. DOM Elements --- +const D = (id) => document.getElementById(id); +const $ = { + container: D('game-container'), + name: D('facility-name'), + resMetal: D('res-metal'), + resFood: D('res-food'), + resScience: D('res-science'), + statusText: D('status-text'), + dangerBar: D('danger-bar'), + dangerLabel: D('danger-label'), + actionButton: D('action-button'), + cooldownText: D('cooldown-text'), + unlockButton: D('unlock-button'), + unlockCostText: D('unlock-cost-text'), + unlockedLinks: D('unlocked-links') +}; + +// --- 3. LocalStorage & State Functions --- + +/** + * Initializes or loads the global game state from localStorage. + */ +function loadInitialState() { + const storedData = localStorage.getItem(STORAGE_KEY); + if (storedData) { + gameData = JSON.parse(storedData); + } else { + // Initial setup for a new game + gameData = { + resources: { metal: 10, food: 10, science: 0 }, + facilities: { + mine: { unlocked: true, danger: 0, disaster: false, lastAction: 0 }, + farm: { unlocked: false, danger: 0, disaster: false, lastAction: 0 }, + lab: { unlocked: false, danger: 0, disaster: false, lastAction: 0 } + } + }; + } + // Update local state and UI + updateUI(); + initializeFacilityUI(); +} + +/** + * Saves the global game state to localStorage. + */ +function saveState() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(gameData)); +} + +/** + * Updates the resource and facility status in the UI. + */ +function updateUI() { + // Update Global Resources + $.resMetal.textContent = `๐Ÿ”ฉ Metal: ${Math.floor(gameData.resources.metal)}`; + $.resFood.textContent = `๐ŸŽ Food: ${Math.floor(gameData.resources.food)}`; + $.resScience.textContent = `๐Ÿงช Science: ${Math.floor(gameData.resources.science)}`; + + // Update Current Facility Status + const currentFacilityData = gameData.facilities[FACILITY_KEY]; + $.dangerBar.value = currentFacilityData.danger; + + // Status text and disaster + if (currentFacilityData.disaster) { + $.statusText.textContent = `๐Ÿšจ DISASTER: ${facilityState.disaster} Click action button to reset!`; + $.statusText.classList.add('disaster-text'); + $.actionButton.textContent = "RESET FACILITY"; + } else if (productionRunning) { + $.statusText.textContent = "SYSTEM NOMINAL: Production is active and running."; + $.statusText.classList.remove('disaster-text'); + $.actionButton.textContent = "START CYCLE"; + } else { + $.statusText.textContent = "SYSTEM PAUSED: Wait for cooldown or refocus tab."; + $.statusText.classList.remove('disaster-text'); + $.actionButton.textContent = "START CYCLE"; + } + + updateUnlockUI(); + saveState(); +} + +// --- 4. Game Loop and Time Dilation Logic --- + +/** + * The main game loop that runs only when the tab is focused. + */ +function gameLoop() { + // Read the latest state from storage in case another tab updated it + loadInitialState(); + + const now = performance.now(); + const deltaTime = now - lastUpdateTime; // Time since last frame (in milliseconds) + const currentFacility = gameData.facilities[FACILITY_KEY]; + + // 1. Production Logic (Runs only if not in disaster and action has been taken) + if (!currentFacility.disaster && productionRunning) { + const secondsElapsed = deltaTime / 1000; + + // Resource generation (per second) + gameData.resources.metal += facilityState.resourceOutput.metal * secondsElapsed; + gameData.resources.food += facilityState.resourceOutput.food * secondsElapsed; + gameData.resources.science += facilityState.resourceOutput.science * secondsElapsed; + + // Ensure resources don't go below zero + gameData.resources.metal = Math.max(0, gameData.resources.metal); + gameData.resources.food = Math.max(0, gameData.resources.food); + gameData.resources.science = Math.max(0, gameData.resources.science); + + // Danger reduction when actively managed + currentFacility.danger = Math.max(0, currentFacility.danger - (0.5 * secondsElapsed)); + } + + // 2. Cooldown Logic + facilityCooldown = Math.max(0, currentFacility.lastAction + facilityState.actionCooldown - now); + $.cooldownText.textContent = facilityCooldown > 0 + ? `Ready in: ${Math.ceil(facilityCooldown / 1000)}s` + : "READY!"; + + // Button activation logic + $.actionButton.disabled = currentFacility.disaster ? false : facilityCooldown > 0; + + lastUpdateTime = now; + updateUI(); + saveState(); +} + + +/** + * Core Time Dilation handler: Calculates offline time and applies decay. + */ +function handleVisibilityChange() { + if (document.hidden) { + // Tab loses focus: Pause the loop and record time. + clearInterval(gameLoopInterval); + lastUpdateTime = performance.now(); // Record current time + productionRunning = false; + console.log(`${facilityState.name} paused.`); + $.statusText.textContent = "SYSTEM DORMANT: Tab is inactive. Danger is RISING!"; + } else { + // Tab gains focus: Calculate offline time and apply decay/disaster. + const timePaused = performance.now() - lastUpdateTime; + const secondsOffline = timePaused / 1000; + const currentFacility = gameData.facilities[FACILITY_KEY]; + + if (secondsOffline > 1) { // Only process if more than 1 second has passed + + // 1. Danger Calculation (Decay) + const dangerIncrease = facilityState.dangerIncreaseRate * secondsOffline; + currentFacility.danger += dangerIncrease; + console.log(`Offline for ${secondsOffline.toFixed(1)}s. Danger +${dangerIncrease.toFixed(1)}.`); + + // 2. Disaster Check + if (currentFacility.danger >= facilityState.dangerThreshold) { + currentFacility.disaster = true; + currentFacility.danger = 100; // Max out danger on disaster + console.warn(`${facilityState.name} suffered a DISASTER!`); + } + } + + // Re-start the loop + lastUpdateTime = performance.now(); // Reset update time for a clean start + gameLoopInterval = setInterval(gameLoop, 50); // Run at 20 FPS + console.log(`${facilityState.name} resumed.`); + + updateUI(); // Immediately update UI after potential offline decay + } +} + +// --- 5. UI and Action Handlers --- + +/** + * Sets up facility-specific styles and information. + */ +function initializeFacilityUI() { + document.title = facilityState.name; + $.container.className = facilityState.theme; + $.name.textContent = facilityState.name; + $.dangerLabel.textContent = facilityState.dangerLabel; + + // Apply danger bar styling + $.dangerBar.classList.add(`danger-${FACILITY_KEY}`); +} + +/** + * Handles the main action button click (Start Production or Reset Disaster). + */ +function handleActionButton() { + const currentFacility = gameData.facilities[FACILITY_KEY]; + + if (currentFacility.disaster) { + // Reset Disaster + currentFacility.disaster = false; + currentFacility.danger = 0; + currentFacility.lastAction = performance.now(); // Give player a fresh cooldown + productionRunning = false; + $.actionButton.disabled = true; // Set to disabled until cooldown is over + $.statusText.textContent = "System Reset Successful. Start production when ready."; + } else if (facilityCooldown <= 0) { + // Start Production Cycle + currentFacility.lastAction = performance.now(); + productionRunning = true; + $.actionButton.disabled = true; + $.statusText.textContent = "Production Cycle Initiated!"; + } + saveState(); + updateUI(); +} + +/** + * Handles the logic for unlocking and generating new facility links. + */ +function updateUnlockUI() { + const nextFacilityKey = facilityState.unlocks; + const currentFacilityData = gameData.facilities[FACILITY_KEY]; + + $.unlockedLinks.innerHTML = '

    Active Facilities:

    '; + let canUnlock = true; + + // Display links to all UNLOCKED facilities + for (const key in gameData.facilities) { + if (gameData.facilities[key].unlocked) { + const link = document.createElement('a'); + link.href = `index.html?facility=${key}`; + link.target = '_blank'; + link.textContent = FACILITY_DATA[key].name; + $.unlockedLinks.appendChild(link); + $.unlockedLinks.appendChild(document.createElement('br')); + } + } + + // Handle Unlock Button and Cost + if (nextFacilityKey && !gameData.facilities[nextFacilityKey].unlocked) { + const cost = facilityState.unlockCost; + let costString = ''; + + for (const res in cost) { + costString += `${cost[res]} ${res} / `; + if (gameData.resources[res] < cost[res]) { + canUnlock = false; + } + } + + $.unlockCostText.textContent = costString.slice(0, -3); // Remove trailing ' / ' + $.unlockButton.disabled = !canUnlock; + $.unlockButton.onclick = () => { + if (canUnlock) { + // Deduct cost + for (const res in cost) { + gameData.resources[res] -= cost[res]; + } + // Unlock facility + gameData.facilities[nextFacilityKey].unlocked = true; + saveState(); + updateUI(); // Re-render links + alert(`Facility ${FACILITY_DATA[nextFacilityKey].name} Unlocked! Open it in a NEW TAB!`); + } + }; + } else { + $.unlockCostText.textContent = "N/A (All available facilities unlocked)"; + $.unlockButton.style.display = 'none'; + } +} + +// --- 6. Initialization --- + +// Start listening for tab focus changes immediately +document.addEventListener('visibilitychange', handleVisibilityChange); + +// Handle the main button click +$.actionButton.addEventListener('click', handleActionButton); + +// Initial setup and start of the loop +loadInitialState(); +handleVisibilityChange(); // This starts the game loop if the tab is focused. \ No newline at end of file diff --git a/games/tab_holder/style.css b/games/tab_holder/style.css new file mode 100644 index 00000000..aa6bf924 --- /dev/null +++ b/games/tab_holder/style.css @@ -0,0 +1,116 @@ +:root { + --mine-color: #ff5555; /* Red/Orange for Danger/Metal */ + --farm-color: #50fa7b; /* Green for Life/Food */ + --lab-color: #bd93f9; /* Purple for Science/Cool */ + --base-bg: #282a36; + --text-color: #f8f8f2; +} + +/* Base Styles */ +body { + font-family: sans-serif; + background-color: var(--base-bg); + color: var(--text-color); + margin: 0; + padding: 20px; +} + +#game-container { + max-width: 800px; + margin: 0 auto; + border-radius: 8px; + padding: 30px; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + transition: all 0.5s; +} + +header { + text-align: center; + margin-bottom: 20px; +} + +.resource-display span { + margin-right: 15px; + font-size: 1.1em; + font-weight: bold; +} + +/* Facility-Specific Theming (Applied by JS) */ +.mine-theme { + border: 3px solid var(--mine-color); + background-color: #3d3f4b; +} + +.farm-theme { + border: 3px solid var(--farm-color); + background-color: #3d3f4b; +} + +.lab-theme { + border: 3px solid var(--lab-color); + background-color: #3d3f4b; +} + +/* Status Box & Danger Bar */ +.status-box { + padding: 15px; + border-radius: 6px; + margin-bottom: 20px; + background-color: #44475a; +} + +#danger-bar-container { + margin-top: 10px; +} + +#danger-bar { + width: 100%; + height: 20px; + border: none; + background-color: #6272a4; +} + +#danger-bar::-webkit-progress-bar { + background-color: #6272a4; + border-radius: 3px; +} + +/* Dynamic bar coloring based on class applied by JS */ +.danger-mine::-webkit-progress-value { background-color: var(--mine-color); } +.danger-farm::-webkit-progress-value { background-color: var(--farm-color); } +.danger-lab::-webkit-progress-value { background-color: var(--lab-color); } + + +/* Controls and Cooldown */ +.controls-section, .unlock-section { + background-color: #3d3f4b; + padding: 15px; + border-radius: 6px; + margin-bottom: 15px; +} + +button { + padding: 10px 20px; + margin-top: 10px; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: bold; + background-color: #6272a4; + color: var(--text-color); + transition: background-color 0.2s; +} + +button:hover:not(:disabled) { + background-color: #8be9fd; + color: var(--base-bg); +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.cooldown { + color: #ffb86c; +} \ No newline at end of file diff --git a/games/tamagochi_game/index.html b/games/tamagochi_game/index.html new file mode 100644 index 00000000..eeebf953 --- /dev/null +++ b/games/tamagochi_game/index.html @@ -0,0 +1,53 @@ + + + + + + Digital Pet Simulator - Pixel ๐Ÿพ + + + +
    +

    Pixel the Digi-Pet

    + +
    + ๐Ÿ˜Š +
    + +
    +
    + + + 100 +
    + +
    + + + 100 +
    + +
    + + + 100 +
    +
    + +
    + + + +
    + +

    Pixel is waiting for attention...

    + + +
    + + + + \ No newline at end of file diff --git a/games/tamagochi_game/scrpt.js b/games/tamagochi_game/scrpt.js new file mode 100644 index 00000000..3da5ea81 --- /dev/null +++ b/games/tamagochi_game/scrpt.js @@ -0,0 +1,164 @@ +// --- 1. Game Configuration --- +const MAX_STAT = 100; +const DECAY_RATE = 1; // How much each stat drops per tick +const REFRESH_RATE_MS = 1000; // Tick rate (1 second) +const CRITICAL_THRESHOLD = 30; // Threshold for warning emoji/low bar color + +// --- 2. Game State Variables --- +let hunger = MAX_STAT; +let happiness = MAX_STAT; +let energy = MAX_STAT; +let gameInterval; +let petAlive = true; + +// --- 3. DOM Element References --- +const petSprite = document.getElementById('pet-sprite'); +const petDisplay = document.getElementById('pet-display'); +const hungerBar = document.getElementById('hunger-bar'); +const happinessBar = document.getElementById('happiness-bar'); +const sleepBar = document.getElementById('sleep-bar'); +const hungerValue = document.getElementById('hunger-value'); +const happinessValue = document.getElementById('happiness-value'); +const sleepValue = document.getElementById('sleep-value'); +const messageArea = document.getElementById('message-area'); + +const feedButton = document.getElementById('feed-button'); +const playButton = document.getElementById('play-button'); +const sleepButton = document.getElementById('sleep-button'); +const deathScreen = document.getElementById('death-screen'); +const deathMessage = document.getElementById('death-message'); +const restartButton = document.getElementById('restart-button'); + +// --- 4. Core Logic Functions --- + +// Clamps a value between 0 and MAX_STAT +const clamp = (value) => Math.max(0, Math.min(MAX_STAT, value)); + +// Updates the progress bars and status text +function updateUI() { + hungerBar.value = hunger; + happinessBar.value = happiness; + sleepBar.value = energy; + + hungerValue.textContent = hunger; + happinessValue.textContent = happiness; + sleepValue.textContent = energy; + + // Apply 'low' class for bar color change + hungerBar.classList.toggle('low', hunger <= CRITICAL_THRESHOLD); + happinessBar.classList.toggle('low', happiness <= CRITICAL_THRESHOLD); + sleepBar.classList.toggle('low', energy <= CRITICAL_THRESHOLD); + + // Update pet sprite/emoji based on state + if (hunger === 0 || happiness === 0 || energy === 0) { + petSprite.textContent = "๐Ÿ˜ต"; + petDisplay.className = 'pet-state-sad'; + } else if (hunger <= CRITICAL_THRESHOLD || happiness <= CRITICAL_THRESHOLD) { + petSprite.textContent = "๐Ÿ˜Ÿ"; + petDisplay.className = 'pet-state-sad'; + messageArea.textContent = "Pixel is very unhappy! Please help!"; + } else if (energy <= CRITICAL_THRESHOLD) { + petSprite.textContent = "๐Ÿ˜ด"; + petDisplay.className = 'pet-state-sleepy'; + messageArea.textContent = "Pixel is tired and needs rest."; + } else { + petSprite.textContent = "๐Ÿ˜Š"; + petDisplay.className = 'pet-state-happy'; + messageArea.textContent = "Pixel is happy and healthy!"; + } +} + +// Handles the automatic decay of stats over time +function decayStats() { + if (!petAlive) return; + + // Stats decay at different rates + hunger = clamp(hunger - DECAY_RATE); + happiness = clamp(happiness - (DECAY_RATE / 2)); // Slower decay + energy = clamp(energy - DECAY_RATE); + + // Check for game over + if (hunger === 0 || happiness === 0) { + endGame(hunger === 0 ? "starvation" : "neglect"); + return; + } + + updateUI(); +} + +// --- 5. Player Action Functions --- + +function performAction(stat, amount, message, sideEffectStat, sideEffectAmount) { + if (!petAlive) return; + + // Dynamically update the target stat + let currentStatValue = window[stat]; + window[stat] = clamp(currentStatValue + amount); + + // Apply side effect + if (sideEffectStat) { + let currentSideEffectValue = window[sideEffectStat]; + window[sideEffectStat] = clamp(currentSideEffectValue + sideEffectAmount); + } + + messageArea.textContent = message; + updateUI(); +} + +feedButton.addEventListener('click', () => { + performAction('hunger', 40, "Pixel enjoyed a hearty meal! Hunger +40", 'energy', -5); +}); + +playButton.addEventListener('click', () => { + performAction('happiness', 30, "Pixel is energized from playing! Happiness +30", 'hunger', -10); +}); + +sleepButton.addEventListener('click', () => { + performAction('energy', 50, "Pixel is well-rested. Energy +50", 'hunger', -5); +}); + +// --- 6. Game Management --- + +function startGame() { + // Reset state + hunger = MAX_STAT; + happiness = MAX_STAT; + energy = MAX_STAT; + petAlive = true; + + // Reset UI + deathScreen.classList.add('hidden'); + feedButton.disabled = false; + playButton.disabled = false; + sleepButton.disabled = false; + + // Clear old interval and start new decay loop + clearInterval(gameInterval); + gameInterval = setInterval(decayStats, REFRESH_RATE_MS); + + messageArea.textContent = "Pixel has been reset and is ready to play!"; + updateUI(); +} + +function endGame(reason) { + petAlive = false; + clearInterval(gameInterval); + + // Show death screen + deathScreen.classList.remove('hidden'); + deathMessage.textContent = `Pixel died from ${reason}! Final Stats: H:${hunger}, P:${happiness}, E:${energy}`; + + // Disable main action buttons + feedButton.disabled = true; + playButton.disabled = true; + sleepButton.disabled = true; + + // Final UI update + updateUI(); +} + +// --- 7. Initialization --- +restartButton.addEventListener('click', startGame); + +// Start the game when the page loads +startGame(); \ No newline at end of file diff --git a/games/tamagochi_game/style.css b/games/tamagochi_game/style.css new file mode 100644 index 00000000..d3c94e78 --- /dev/null +++ b/games/tamagochi_game/style.css @@ -0,0 +1,156 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #f0f8ff; /* Light, calming blue */ + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; +} + +.pet-container { + background: #ffffff; + padding: 35px; + border-radius: 25px; + box-shadow: 0 15px 50px rgba(0, 0, 0, 0.2); + text-align: center; + width: 90%; + max-width: 480px; + border: 5px solid #a9d6e5; + position: relative; +} + +h1 { + color: #0077b6; + margin-bottom: 25px; +} + +/* --- Pet Display and Sprites (Placeholder) --- */ +#pet-display { + width: 150px; + height: 150px; + margin: 0 auto 30px; + border-radius: 50%; + background-color: #ffe6a7; + display: flex; + justify-content: center; + align-items: center; + transition: all 0.5s ease; +} + +#pet-sprite { + font-size: 5em; + transition: transform 0.2s; +} + +/* Sprite classes (For custom image backgrounds or different emojis) */ +.pet-state-sad { background-color: #f7b7a3; } +.pet-state-sleepy { background-color: #c9e0f6; } + +/* --- Stats Panel --- */ +.stats-panel { + margin-bottom: 30px; + padding: 15px; + background-color: #f0f0f0; + border-radius: 10px; +} + +.stat-group { + display: flex; + align-items: center; + margin: 10px 0; +} + +.stat-group label { + font-weight: bold; + color: #555; + width: 90px; + text-align: left; + margin-right: 10px; +} + +progress { + flex-grow: 1; + height: 20px; + margin-right: 10px; + appearance: none; + border-radius: 5px; + border: 1px solid #ccc; +} + +/* Base progress bar style */ +progress::-webkit-progress-bar { + background-color: #eee; + border-radius: 5px; +} + +/* Happiness Bar: Green/Yellow */ +#happiness-bar::-webkit-progress-value { background-color: #8ac926; } +/* Hunger Bar: Orange/Red */ +#hunger-bar::-webkit-progress-value { background-color: #ff9f1c; } +/* Sleep Bar: Blue/Purple */ +#sleep-bar::-webkit-progress-value { background-color: #4361ee; } + +/* Low Stat Warning (Used by JS) */ +progress.low::-webkit-progress-value { background-color: #e63946 !important; } + +/* --- Action Buttons and Messages --- */ +.action-buttons { + display: flex; + justify-content: space-around; + gap: 10px; +} + +.action-buttons button { + padding: 12px 10px; + font-size: 1em; + border: none; + border-radius: 8px; + background-color: #0077b6; + color: white; + cursor: pointer; + transition: background-color 0.2s; + flex-grow: 1; +} + +.action-buttons button:hover { + background-color: #005f93; +} + +.status-message { + margin-top: 20px; + font-style: italic; + color: #333; + min-height: 20px; /* Prevent layout shift */ +} + +/* --- Death Screen --- */ +#death-screen { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.95); + border-radius: 25px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 10; +} + +.death-text { + font-size: 1.5em; + color: #e63946; + font-weight: bold; + margin-bottom: 20px; +} + +#restart-button { + background-color: #e63946; +} + +.hidden { + display: none !important; +} \ No newline at end of file diff --git a/games/tap-counter/index.html b/games/tap-counter/index.html new file mode 100644 index 00000000..e68852d3 --- /dev/null +++ b/games/tap-counter/index.html @@ -0,0 +1,32 @@ + + + + + + Tap Counter | Mini JS Games Hub + + + +
    +

    Tap Counter

    +

    Tap 100 times as fast as you can!

    +
    + +
    +

    0 / 100

    +
    + + + + +
    +

    +
    + + + + + + + + diff --git a/games/tap-counter/script.js b/games/tap-counter/script.js new file mode 100644 index 00000000..2ee42c0e --- /dev/null +++ b/games/tap-counter/script.js @@ -0,0 +1,71 @@ +const tapLine = document.getElementById("tap-line"); +const tapBtn = document.getElementById("tap-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const resumeBtn = document.getElementById("resume-btn"); +const restartBtn = document.getElementById("restart-btn"); +const countDisplay = document.getElementById("count-display"); +const message = document.getElementById("message"); + +const tapSound = document.getElementById("tap-sound"); +const winSound = document.getElementById("win-sound"); +const failSound = document.getElementById("fail-sound"); + +let count = 0; +let maxCount = 100; +let paused = false; + +// Create bulbs +for(let i=0; i { + if(i < count) b.classList.add("active"); + else b.classList.remove("active"); + }); + if(count >= maxCount){ + message.textContent = "๐ŸŽ‰ You Win!"; + winSound.play(); + tapBtn.disabled = true; + } +} + +tapBtn.addEventListener("click", () => { + if(paused) return; + count++; + tapSound.play(); + updateDisplay(); +}); + +pauseBtn.addEventListener("click", () => { + paused = true; + pauseBtn.disabled = true; + resumeBtn.disabled = false; + message.textContent = "โธ๏ธ Paused"; +}); + +resumeBtn.addEventListener("click", () => { + paused = false; + pauseBtn.disabled = false; + resumeBtn.disabled = true; + message.textContent = ""; +}); + +restartBtn.addEventListener("click", () => { + count = 0; + paused = false; + tapBtn.disabled = false; + pauseBtn.disabled = false; + resumeBtn.disabled = true; + message.textContent = ""; + updateDisplay(); +}); + +// Initial display +updateDisplay(); diff --git a/games/tap-counter/style.css b/games/tap-counter/style.css new file mode 100644 index 00000000..9a3c47dc --- /dev/null +++ b/games/tap-counter/style.css @@ -0,0 +1,62 @@ +body { + font-family: Arial, sans-serif; + background: linear-gradient(135deg, #1f1c2c, #928dab); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + color: #fff; +} + +.tap-container { + text-align: center; + background: rgba(0,0,0,0.5); + padding: 30px 50px; + border-radius: 15px; + box-shadow: 0 0 20px rgba(255,255,255,0.2); +} + +.tap-line { + display: flex; + justify-content: center; + flex-wrap: wrap; + margin: 20px 0; +} + +.bulb { + width: 15px; + height: 15px; + margin: 3px; + border-radius: 50%; + background: #444; + box-shadow: 0 0 5px #000; + transition: 0.3s; +} + +.bulb.active { + background: #00ffea; + box-shadow: 0 0 15px #00ffea, 0 0 30px #00ffea, 0 0 45px #00ffea; +} + +.controls button { + padding: 10px 20px; + margin: 5px; + font-size: 16px; + border: none; + border-radius: 5px; + cursor: pointer; + transition: 0.2s; + background: #222; + color: #00ffea; +} + +.controls button:hover { + background: #00ffea; + color: #222; +} + +#message { + margin-top: 20px; + font-size: 20px; + font-weight: bold; +} diff --git a/games/tap-pop-clouds/index.html b/games/tap-pop-clouds/index.html new file mode 100644 index 00000000..668c16d0 --- /dev/null +++ b/games/tap-pop-clouds/index.html @@ -0,0 +1,20 @@ + + + + + + Tap & Pop Clouds | Mini JS Games Hub + + + +
    +

    โ˜๏ธ Tap & Pop Clouds ๐ŸŒˆ

    +

    Score: 0

    +

    Lives: 3

    + + +
    + + + + diff --git a/games/tap-pop-clouds/script.js b/games/tap-pop-clouds/script.js new file mode 100644 index 00000000..4ce08d13 --- /dev/null +++ b/games/tap-pop-clouds/script.js @@ -0,0 +1,96 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +canvas.width = 500; +canvas.height = 400; + +let score = 0; +let lives = 3; +const scoreEl = document.getElementById('score'); +const livesEl = document.getElementById('lives'); +const restartBtn = document.getElementById('restart-btn'); + +class Cloud { + constructor() { + this.width = 80 + Math.random() * 40; + this.height = 50 + Math.random() * 20; + this.x = Math.random() * (canvas.width - this.width); + this.y = canvas.height + this.height; + this.speed = 1 + Math.random() * 2; + this.color = '#fff'; + } + + draw() { + ctx.fillStyle = this.color; + ctx.beginPath(); + ctx.ellipse(this.x + this.width/2, this.y, this.width/2, this.height/2, 0, 0, Math.PI * 2); + ctx.fill(); + } + + update() { + this.y -= this.speed; + if(this.y + this.height < 0) { + lives--; + livesEl.textContent = lives; + this.reset(); + } + } + + reset() { + this.width = 80 + Math.random() * 40; + this.height = 50 + Math.random() * 20; + this.x = Math.random() * (canvas.width - this.width); + this.y = canvas.height + this.height; + this.speed = 1 + Math.random() * 2; + } + + isClicked(mouseX, mouseY) { + const dx = mouseX - (this.x + this.width / 2); + const dy = mouseY - this.y; + return (dx*dx)/(this.width*this.width/4) + (dy*dy)/(this.height*this.height/4) <= 1; + } +} + +const clouds = []; +for(let i=0;i<5;i++) clouds.push(new Cloud()); + +function draw() { + ctx.clearRect(0,0,canvas.width,canvas.height); + clouds.forEach(cloud => cloud.draw()); +} + +function update() { + clouds.forEach(cloud => cloud.update()); +} + +function gameLoop() { + draw(); + update(); + if(lives > 0) requestAnimationFrame(gameLoop); + else alert(`Game Over! Your score: ${score}`); +} + +canvas.addEventListener('click', (e)=>{ + const rect = canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + clouds.forEach(cloud => { + if(cloud.isClicked(mouseX, mouseY)) { + score++; + scoreEl.textContent = score; + cloud.reset(); + } + }); +}); + +restartBtn.addEventListener('click', ()=>{ + score = 0; + lives = 3; + scoreEl.textContent = score; + livesEl.textContent = lives; + clouds.forEach(c => c.reset()); + gameLoop(); +}); + +// Start game +gameLoop(); diff --git a/games/tap-pop-clouds/style.css b/games/tap-pop-clouds/style.css new file mode 100644 index 00000000..9a0a540f --- /dev/null +++ b/games/tap-pop-clouds/style.css @@ -0,0 +1,39 @@ +body { + margin: 0; + font-family: 'Poppins', sans-serif; + background: linear-gradient(to top, #87CEEB, #fff); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + text-align: center; + width: 100%; + max-width: 600px; +} + +canvas { + border-radius: 20px; + box-shadow: 0 10px 30px rgba(0,0,0,0.2); + background: rgba(255, 255, 255, 0.3); + display: block; + margin: 20px auto; +} + +button { + padding: 10px 20px; + font-size: 16px; + margin-top: 10px; + cursor: pointer; + border-radius: 10px; + background: #fff; + border: 2px solid #87CEEB; + transition: 0.3s; +} + +button:hover { + background: #87CEEB; + color: #fff; +} diff --git a/games/tap-reveal/index.html b/games/tap-reveal/index.html new file mode 100644 index 00000000..bfcca48b --- /dev/null +++ b/games/tap-reveal/index.html @@ -0,0 +1,18 @@ + + + + + + Tap Reveal | Mini JS Games Hub + + + +
    +

    Tap Reveal

    +

    Moves: 0 | Time: 0s

    +
    + +
    + + + diff --git a/games/tap-reveal/script.js b/games/tap-reveal/script.js new file mode 100644 index 00000000..1cc0981d --- /dev/null +++ b/games/tap-reveal/script.js @@ -0,0 +1,91 @@ +const gameGrid = document.getElementById("game-grid"); +const movesEl = document.getElementById("moves"); +const timerEl = document.getElementById("timer"); +const restartBtn = document.getElementById("restart-btn"); + +let moves = 0; +let matched = 0; +let flippedCards = []; +let lockBoard = false; +let timer = 0; +let interval; + +// Define card values (8 pairs) +const icons = ["๐ŸŽ","๐ŸŒ","๐Ÿ‡","๐Ÿ‰","๐Ÿ’","๐Ÿฅ","๐Ÿ‘","๐Ÿ"]; +let cardsArray = [...icons, ...icons]; + +function shuffle(array) { + return array.sort(() => Math.random() - 0.5); +} + +function startGame() { + // Reset variables + gameGrid.innerHTML = ""; + moves = 0; + matched = 0; + flippedCards = []; + lockBoard = false; + movesEl.textContent = moves; + timer = 0; + timerEl.textContent = timer; + clearInterval(interval); + interval = setInterval(() => { + timer++; + timerEl.textContent = timer; + }, 1000); + + const shuffled = shuffle(cardsArray); + shuffled.forEach((icon) => { + const card = document.createElement("div"); + card.classList.add("card"); + card.innerHTML = ` +
    +
    ?
    +
    ${icon}
    +
    + `; + gameGrid.appendChild(card); + + card.addEventListener("click", () => flipCard(card)); + }); +} + +function flipCard(card) { + if (lockBoard || card.classList.contains("flip")) return; + card.classList.add("flip"); + flippedCards.push(card); + + if (flippedCards.length === 2) { + moves++; + movesEl.textContent = moves; + checkMatch(); + } +} + +function checkMatch() { + const [first, second] = flippedCards; + const firstIcon = first.querySelector(".card-back").textContent; + const secondIcon = second.querySelector(".card-back").textContent; + + if (firstIcon === secondIcon) { + matched += 2; + flippedCards = []; + if (matched === cardsArray.length) { + setTimeout(() => alert(`๐ŸŽ‰ You won in ${moves} moves and ${timer} seconds!`), 300); + clearInterval(interval); + } + } else { + lockBoard = true; + setTimeout(() => { + first.classList.remove("flip"); + second.classList.remove("flip"); + flippedCards = []; + lockBoard = false; + }, 1000); + } +} + +restartBtn.addEventListener("click", startGame); + +// Initialize game on load +startGame(); diff --git a/games/tap-reveal/style.css b/games/tap-reveal/style.css new file mode 100644 index 00000000..6ccd84e4 --- /dev/null +++ b/games/tap-reveal/style.css @@ -0,0 +1,103 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: 'Poppins', sans-serif; +} + +body { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background: linear-gradient(135deg, #ff9a9e, #fad0c4); +} + +.tap-reveal-container { + background: #fff; + padding: 20px 30px; + border-radius: 15px; + box-shadow: 0 8px 20px rgba(0,0,0,0.2); + text-align: center; + max-width: 500px; + width: 90%; +} + +h1 { + margin-bottom: 10px; +} + +.stats { + margin-bottom: 20px; + font-size: 1.1rem; + color: #555; +} + +.game-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 15px; + margin-bottom: 20px; +} + +.card { + width: 100%; + padding-top: 100%; /* 1:1 aspect ratio */ + position: relative; + perspective: 1000px; + cursor: pointer; +} + +.card-inner { + position: absolute; + width: 100%; + height: 100%; + transition: transform 0.6s; + transform-style: preserve-3d; + border-radius: 10px; +} + +.card.flip .card-inner { + transform: rotateY(180deg); +} + +.card-front, .card-back { + position: absolute; + width: 100%; + height: 100%; + border-radius: 10px; + backface-visibility: hidden; +} + +.card-front { + background: #4e73df; + display: flex; + justify-content: center; + align-items: center; + font-size: 2rem; + color: #fff; +} + +.card-back { + background: #f8f9fc; + transform: rotateY(180deg); + display: flex; + justify-content: center; + align-items: center; + font-size: 2rem; +} + +#restart-btn { + padding: 10px 20px; + background-color: #4e73df; + color: #fff; + border: none; + border-radius: 8px; + font-size: 1rem; + cursor: pointer; + transition: background 0.3s; +} + +#restart-btn:hover { + background-color: #2e59d9; +} diff --git a/games/tap-the-beat/index.html b/games/tap-the-beat/index.html new file mode 100644 index 00000000..8c2fd293 --- /dev/null +++ b/games/tap-the-beat/index.html @@ -0,0 +1,100 @@ + + + + + + Tap the Beat โ€” Mini JS Games Hub + + + + + +
    +
    +
    + ๐ŸŽง +

    Tap the Beat

    +

    Tap in time โ€” hit combos and earn a high score!

    +
    + +
    + + +
    + + + +
    +
    +
    + +
    +
    +
    + + + +
    + +
    + +

    Tap button or press Space

    +
    +
    + + +
    + + +
    + + + + diff --git a/games/tap-the-beat/script.js b/games/tap-the-beat/script.js new file mode 100644 index 00000000..50c61f09 --- /dev/null +++ b/games/tap-the-beat/script.js @@ -0,0 +1,519 @@ +/* Tap the Beat โ€” script.js + Uses Web Audio API to play a hosted track and schedules simple beat events. + Tap evaluation uses timing windows for Perfect/Good/Miss. +*/ + +/* ========== Config & Assets (online) ========== */ +const ASSETS = { + tracks: { + "drum-01": { + url: "https://file-examples.com/wp-content/uploads/2017/11/file_example_MP3_1MG.mp3", + name: "Ambient Beat" + }, + "pop-01": { + url: "https://file-examples.com/wp-content/uploads/2017/11/file_example_MP3_700KB.mp3", + name: "Upbeat Pop" + } + }, + click: "https://www.soundjay.com/button/sounds/button-16.mp3" // simple click for hits +}; + +/* ========== UI refs ========== */ +const startBtn = document.getElementById("startBtn"); +const pauseBtn = document.getElementById("pauseBtn"); +const restartBtn = document.getElementById("restartBtn"); +const tapBtn = document.getElementById("tapBtn"); +const feedbackEl = document.getElementById("feedback"); +const stage = document.getElementById("stage"); +const scoreEl = document.getElementById("score"); +const comboEl = document.getElementById("combo"); +const accuracyEl = document.getElementById("accuracy"); +const progressFill = document.getElementById("progressFill"); +const timeElapsed = document.getElementById("timeElapsed"); +const durationEl = document.getElementById("duration"); +const difficultySel = document.getElementById("difficulty"); +const trackSelect = document.getElementById("trackSelect"); +const muteBtn = document.getElementById("muteBtn"); +const volUp = document.getElementById("volUp"); +const volDown = document.getElementById("volDown"); + +let audioCtx, trackBuffer, trackSource; +let clickBuffer; +let gainNode; +let playStartTime = 0; +let pauseTime = 0; +let scheduledBeats = []; // {time, el} +let schedulerId = null; +let isPlaying = false; +let trackDuration = 0; + +/* Game state */ +let score = 0, combo = 0, hits = 0, attempts = 0; +let accuracy = 0; +let beatIndex = 0; +let bpm = 100; // adjustable by difficulty +let beatPattern = []; // seconds offsets of beats +let volume = 0.7; +let muted = false; + +/* Timing windows (seconds) */ +const windows = { perfect: 0.08, good: 0.15, miss: 0.25 }; + +/* ========== Helpers ========== */ +function toFixed(n, d=1){ return Number(n.toFixed(d)); } +function clamp(v, a, b){ return Math.max(a, Math.min(b, v)); } + +/* ========== Audio setup ========== */ +async function initAudio(){ + if (audioCtx) return; + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + gainNode = audioCtx.createGain(); + gainNode.gain.value = volume; + gainNode.connect(audioCtx.destination); + + // load click + clickBuffer = await fetchAndDecode(ASSETS.click); +} + +async function fetchAndDecode(url){ + const res = await fetch(url); + const ab = await res.arrayBuffer(); + return await audioCtx.decodeAudioData(ab); +} + +async function loadTrack(key){ + await initAudio(); + const url = ASSETS.tracks[key].url; + const res = await fetch(url); + const ab = await res.arrayBuffer(); + trackBuffer = await audioCtx.decodeAudioData(ab); + trackDuration = trackBuffer.duration; + durationEl.textContent = `${toFixed(trackDuration,1)}s`; +} + +/* ========== Beatmap generation ========== + We'll generate a simple beat pattern from a BPM and a difficulty multiplier. + For production you'd use pre-authored beatmaps; this is procedural. +*/ +function generateBeatmap(bpmVal, difficulty="normal"){ + const secondsPerBeat = 60 / bpmVal; + const lengthInBeats = Math.floor(trackDuration / secondsPerBeat) || Math.floor(60 / secondsPerBeat); + beatPattern = []; + const density = difficulty === "easy" ? 0.6 : difficulty === "hard" ? 1.0 : 0.8; + for(let i=0;ia-b); + // ensure at least one beat exists + if (beatPattern.length===0){ + for(let i=0;i { + endRun(); + }; + + // reset beat index relative to offset + beatIndex = 0; + while(beatIndex < beatPattern.length && (beatPattern[beatIndex] <= offset)) beatIndex++; + + // start scheduler tick + schedulerTick(); +} + +function pausePlayback(){ + if (!isPlaying) return; + // stop current track source and preserve offset + try{ trackSource.stop(); } catch(e){} + pauseTime = audioCtx.currentTime - playStartTime; + isPlaying = false; + startBtn.disabled = false; + pauseBtn.disabled = true; + // stop scheduler + if (schedulerId) { cancelAnimationFrame(schedulerId); schedulerId = null; } +} + +function restartPlayback(){ + // clear visuals + clearScheduledVisuals(); + try{ if (trackSource) trackSource.stop(); } catch(e){} + pauseTime = 0; + score = 0; combo = 0; hits = 0; attempts = 0; accuracy = 0; + updateUI(); + startPlayback(); +} + +/* ========== run loop for visuals & interaction ========= */ +function schedulerTick(){ + // schedule upcoming beats + scheduleBeatsUntil(2.2); + + // update visuals: move beats that were spawned to animate to hit-line using computed fraction + const now = audioCtx.currentTime; + scheduledBeats.forEach(item => { + const {time, el} = item; + // time until hit in seconds + const tleft = time - now; + const life = Math.max(0.001, Math.min(4.0, (time - el.dataset.spawnedAt))); + // compute percent from spawn to hit (we approximate with window 3.0s) + const totalTravel = 3.0; // seconds + const pct = clamp(1 - Math.max(-1, tleft)/totalTravel, 0, 1); + // map pct to bottom value (start offscreen bottom -120px to hit-line 86px) + const startBottom = -120; + const hitBottom = 86; + const curBottom = startBottom + (hitBottom - startBottom) * pct; + el.style.transform = `translateY(${-curBottom}px)`; // negative because translateY up + // small scale pulse near hit + if (Math.abs(tleft) < 0.18){ + el.style.transform += " scale(1.08)"; + el.style.opacity = "1"; + } else { + el.style.opacity = `${clamp(0.4 + pct, 0, 1)}`; + } + // if passed well beyond miss window, remove as missed + if (tleft < -windows.miss - 0.2){ + markMiss(el, time); + } + }); + + // cleanup any elements that have been removed + scheduledBeats = scheduledBeats.filter(i => stage.contains(i.el)); + + // update time UI + if (isPlaying){ + const elapsed = audioCtx.currentTime - playStartTime; + timeElapsed.textContent = `${toFixed(elapsed,1)}s`; + const pct = clamp((elapsed / trackDuration) * 100, 0, 100); + progressFill.style.width = `${pct}%`; + } + + schedulerId = requestAnimationFrame(schedulerTick); +} + +/* ========== Hit detection ========== + When player taps, compare current audio time to closest scheduled beat time. +*/ +function evaluateTap(){ + if (!audioCtx || (!isPlaying && !pauseTime)) return; + const now = audioCtx.currentTime; + // find closest scheduled beat (including those not yet spawned) + let closestDiff = Infinity; + let closestTime = null; + for(const t of beatPattern){ + const abs = playStartTime + t; + const diff = Math.abs(abs - now); + if (diff < closestDiff){ + closestDiff = diff; + closestTime = abs; + } + } + // decide outcome + attempts++; + if (closestDiff <= windows.perfect){ + registerHit("Perfect", 300, 2.25); + } else if (closestDiff <= windows.good){ + registerHit("Good", 120, 1.25); + } else if (closestDiff <= windows.miss){ + registerMiss(); + } else { + registerMiss(); + } +} + +/* registerHit / Miss */ +function registerHit(label, baseScore=100, comboMult=1.0){ + hits++; + combo++; + score += Math.floor(baseScore * comboMult + (combo * 2)); + displayFeedback(label, true); + playClick(0.3); + updateUI(); + // remove the earliest beat that is near now so it won't be matched again + const now = audioCtx.currentTime; + let removed = false; + for(let i=0;i{ try{ it.el.remove(); }catch(e){} }, 140); + scheduledBeats.splice(i,1); + removed = true; + break; + } + } + // also remove earliest beat in beatPattern if within window + for(let j=0;j{ try{ it.el.remove(); }catch(e){} },280); + scheduledBeats.splice(i,1); + break; + } + } + updateUI(); +} + +/* markMiss for elements that passed without hit */ +function markMiss(el, time){ + try{ + el.style.transition = "transform 0.3s ease, opacity 0.3s ease"; + el.style.opacity = "0.06"; + el.style.transform += " scale(.9)"; + setTimeout(()=>{ el.remove(); }, 320); + }catch(e){} +} + +/* Snackbar feedback */ +let feedbackTimeout; +function displayFeedback(text, positive){ + feedbackEl.textContent = text; + feedbackEl.style.color = positive ? "#9ef6c9" : "#ff9ea8"; + feedbackEl.style.transform = "translateY(-8px)"; + if (feedbackTimeout) clearTimeout(feedbackTimeout); + feedbackTimeout = setTimeout(()=>{ + feedbackEl.style.transform = "translateY(0)"; + feedbackEl.textContent = ""; + }, 600); +} + +/* play click sound quickly */ +function playClick(vol=0.5){ + if (muted) return; + const s = audioCtx.createBufferSource(); + s.buffer = clickBuffer; + const g = audioCtx.createGain(); + g.gain.value = vol; + s.connect(g); g.connect(gainNode); + s.start(audioCtx.currentTime); +} + +/* ========== UI updates ========= */ +function updateUI(){ + scoreEl.textContent = score; + comboEl.textContent = combo; + accuracy = attempts ? Math.round((hits / attempts) * 100) : 0; + accuracyEl.textContent = attempts ? `${accuracy}%` : "โ€”"; +} + +/* clean visuals */ +function clearScheduledVisuals(){ + scheduledBeats.forEach(item => { try{ item.el.remove(); } catch(e){} }); + scheduledBeats = []; +} + +/* run end */ +function endRun(){ + isPlaying = false; + startBtn.disabled = false; + pauseBtn.disabled = true; + // show summary + displayFeedback(`Run finished โ€” Score ${score}`, true); +} + +/* ========== UI event handlers ========== */ +startBtn.addEventListener("click", async ()=>{ + startBtn.disabled = true; + pauseBtn.disabled = false; + // init audio & load track selected + const trackKey = trackSelect.value || "drum-01"; + await loadTrack(trackKey); + // difficulty based bpm: easy slower, hard faster + const diff = difficultySel.value; + bpm = diff === "easy" ? 80 : diff === "hard" ? 140 : 100; + generateBeatmap(bpm, diff); + // reset state + clearScheduledVisuals(); + score = 0; combo = 0; hits = 0; attempts = 0; + updateUI(); + // start playback + await initAudio(); + startPlayback(); +}); + +pauseBtn.addEventListener("click", ()=>{ + if (isPlaying) pausePlayback(); +}); + +restartBtn.addEventListener("click", ()=>{ + restartPlayback(); +}); + +tapBtn.addEventListener("click", ()=>{ + if (!audioCtx) initAudio(); + if (!isPlaying) { + // if not playing, start immediately (quick play) + startBtn.click(); + setTimeout(()=> evaluateTap(), 150); + } else { + evaluateTap(); + } +}); + +// keyboard support +window.addEventListener("keydown", (e)=>{ + if (e.code === "Space"){ + e.preventDefault(); + tapBtn.classList.add("active"); + setTimeout(()=>tapBtn.classList.remove("active"), 80); + evaluateTap(); + } + if (e.code === "KeyP"){ + // toggle pause + if (isPlaying) pauseBtn.click(); + else startBtn.click(); + } +}); + +/* volume / mute */ +muteBtn.addEventListener("click", ()=>{ + muted = !muted; + muteBtn.textContent = muted ? "Unmute" : "Mute"; + gainNode.gain.value = muted ? 0 : volume; +}); + +volUp.addEventListener("click", ()=>{ + volume = clamp(volume + 0.1, 0, 1); + gainNode.gain.value = volume; +}); +volDown.addEventListener("click", ()=>{ + volume = clamp(volume - 0.1, 0, 1); + gainNode.gain.value = volume; +}); + +/* track select change: preload chosen track */ +trackSelect.addEventListener("change", async ()=>{ + const key = trackSelect.value; + await loadTrack(key); +}); + +/* initialize default track in background (non-blocking) */ +(async function preLoad(){ + try{ + await initAudio(); + await loadTrack(trackSelect.value || "drum-01"); + }catch(e){ + console.warn("Preload failed", e); + } +})(); + +/* keep UI ticking even when not playing for progress bar */ +setInterval(()=> { + if (!audioCtx) return; + if (!isPlaying && pauseTime){ + timeElapsed.textContent = `${toFixed(pauseTime,1)}s`; + } +}, 300); diff --git a/games/tap-the-beat/style.css b/games/tap-the-beat/style.css new file mode 100644 index 00000000..240e5d66 --- /dev/null +++ b/games/tap-the-beat/style.css @@ -0,0 +1,165 @@ +:root{ + --bg:#0f1724; + --card:#0b1220; + --accent:#ff4d6d; + --accent-2:#6df0ff; + --muted:#9aa7b2; + --glass: rgba(255,255,255,0.04); + --glow: 0 10px 30px rgba(255,77,109,0.12); + --soft-glow: 0 6px 18px rgba(109,240,255,0.06); + --glass-2: rgba(255,255,255,0.03); +} + +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial; + background: linear-gradient(180deg, #071224 0%, #081827 50%, #071224 100%); + color:#dfeef5; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + padding:24px; + display:flex; + justify-content:center; + align-items:flex-start; +} + +/* shell */ +.game-shell{ + width:100%; + max-width:1100px; + border-radius:14px; + overflow:hidden; + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + box-shadow: var(--glow); + border: 1px solid rgba(255,255,255,0.04); +} + +/* header */ +.game-header{ + display:flex; + justify-content:space-between; + align-items:center; + padding:20px; + gap:16px; + background-image: linear-gradient(90deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); +} +.game-title{display:flex;gap:12px;align-items:center} +.game-title .icon{font-size:30px} +.game-title h1{margin:0;font-size:20px;letter-spacing:0.2px} +.game-title p{margin:0;color:var(--muted);font-size:13px} +.controls-top{display:flex;gap:12px;align-items:center} +select{padding:8px 10px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:var(--glass);color:inherit} +.buttons{display:flex;gap:8px} +.btn{padding:8px 12px;border-radius:8px;border:none;background:transparent;color:inherit;cursor:pointer;backdrop-filter:blur(6px)} +.btn.primary{background:linear-gradient(90deg,var(--accent),var(--accent-2));color:#0b1220;font-weight:600;box-shadow: var(--soft-glow);} + +/* main layout */ +.game-main{display:flex;gap:18px;padding:20px} +.visual-column{flex:1;display:flex;flex-direction:column;gap:12px;align-items:center} +.stage{ + width:100%; + max-width:700px; + height:420px; + position:relative; + border-radius:12px; + overflow:hidden; + background-image:url("https://source.unsplash.com/1200x800/?music,concert"); + background-size:cover; + background-position:center; + box-shadow: inset 0 0 120px rgba(0,0,0,0.4), 0 8px 30px rgba(5,9,15,0.6); + display:flex; + align-items:flex-end; + justify-content:center; +} + +/* translucent overlay */ +.stage::after{ + content:""; + position:absolute;inset:0; + background:linear-gradient(180deg, rgba(10,15,25,0.2), rgba(0,0,0,0.6)); + pointer-events:none; +} + +/* hit-line */ +.hit-line{ + position:absolute; + bottom:86px; + left:0;right:0; + height:6px; + background:linear-gradient(90deg, rgba(255,255,255,0.06), rgba(255,255,255,0.12)); + box-shadow: 0 6px 20px rgba(0,0,0,0.6), 0 0 22px rgba(109,240,255,0.06); + z-index:6; + border-radius:4px; +} + +/* beat circles */ +.beat{ + position:absolute; + left:50%; + transform:translateX(-50%); + bottom:-40px; + width:64px; + height:64px; + border-radius:999px; + display:flex; + align-items:center; + justify-content:center; + color:#071224; + font-weight:700; + z-index:5; + box-shadow: 0 18px 30px rgba(0,0,0,0.45); + user-select:none; +} +.beat.glow{ + box-shadow: 0 12px 36px rgba(255,77,109,0.18), 0 6px 22px rgba(109,240,255,0.08); +} + +/* feedback text */ +.feedback{ + position:absolute; + bottom:140px; + font-size:28px; + font-weight:700; + letter-spacing:1px; + z-index:10; + pointer-events:none; + text-shadow: 0 6px 18px rgba(0,0,0,0.6); + transform:translateY(0); + transition:all .18s ease-out; +} + +/* tap area */ +.tap-area{display:flex;flex-direction:column;align-items:center;gap:8px} +.tap-btn{ + width:180px;height:60px;border-radius:12px;border:none;font-size:20px;background:linear-gradient(180deg,var(--accent),var(--accent-2));color:#06181f;font-weight:800;box-shadow:var(--glow);cursor:pointer; +} +.tap-btn:active{transform:translateY(1px)} + +/* stats column */ +.stats-column{width:260px;display:flex;flex-direction:column;gap:12px;padding:6px 0} +.score-card{padding:14px;border-radius:10px;background:linear-gradient(180deg,var(--glass),var(--glass-2));border:1px solid rgba(255,255,255,0.03)} +.score-card h3{margin:0;color:var(--muted);font-size:12px} +.score-card > div{font-size:28px;font-weight:800;margin-top:8px} +.score-card.glow{box-shadow:0 12px 40px rgba(255,77,109,0.1);border:1px solid rgba(255,77,109,0.12)} + +.progress-card{padding:12px;border-radius:10px;background:linear-gradient(180deg,var(--glass),transparent)} +.progress-bar{height:12px;background:rgba(255,255,255,0.04);border-radius:999px;overflow:hidden;margin-top:8px} +.progress-bar > div{height:100%;background:linear-gradient(90deg,var(--accent),var(--accent-2));width:0%;transition:width 0.12s linear} + +/* audio controls */ +.audio-controls{padding:8px;border-radius:10px;background:rgba(255,255,255,0.02);display:flex;flex-direction:column;gap:8px} +.audio-buttons{display:flex;gap:8px;margin-top:6px} + +/* footer */ +.game-footer{display:flex;justify-content:space-between;align-items:center;padding:14px 20px;border-top:1px solid rgba(255,255,255,0.02);background:transparent} +.ghost{color:var(--muted);text-decoration:none} +.best{color:var(--muted);font-size:13px} + +/* small screens */ +@media (max-width:880px){ + .game-main{flex-direction:column;padding:14px} + .stats-column{width:100%;flex-direction:row;flex-wrap:wrap;justify-content:space-between} + .stage{height:360px} +} diff --git a/games/tap-the-bubble/index.html b/games/tap-the-bubble/index.html new file mode 100644 index 00000000..c7b79a89 --- /dev/null +++ b/games/tap-the-bubble/index.html @@ -0,0 +1,202 @@ + + + + + + Tap the Bubble + + + + + +
    + + + + + + + + + + + + + + +
    + +
    +
    +
    + Score + 0 +
    +
    + Combo + x1 +
    +
    +
    +
    + + + + + 60 +
    +
    +
    + +
    +
    + + +
    + + +
    + + +
    + +
    + + +
    + + +
    +
    + + +
    + +
    + + + + + +
    + + + + diff --git a/games/tap-the-bubble/script.js b/games/tap-the-bubble/script.js new file mode 100644 index 00000000..f695543c --- /dev/null +++ b/games/tap-the-bubble/script.js @@ -0,0 +1,928 @@ +// ========== GAME STATE ========== +const gameState = { + score: 0, + combo: 0, + comboTimer: null, + maxCombo: 0, + bubblesPopped: 0, + bubblesMissed: 0, + timeLeft: 60, + isPaused: false, + isPlaying: false, + difficulty: 'medium', + gameMode: 'timed', + theme: 'ocean', + playerName: 'Player', + + // Power-up states + doublePoints: false, + freezeTime: false, + + // Settings + musicEnabled: true, + sfxEnabled: true, + vibrationEnabled: true, + + // Achievements + achievements: [], + unlockedAchievements: [] +}; + +// ========== DIFFICULTY SETTINGS ========== +const difficultySettings = { + easy: { spawnInterval: 1500, bubbleLifetime: 5000, maxBubbles: 8 }, + medium: { spawnInterval: 1000, bubbleLifetime: 4000, maxBubbles: 12 }, + hard: { spawnInterval: 700, bubbleLifetime: 3000, maxBubbles: 15 } +}; + +// ========== AUDIO CONTEXT ========== +const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + +function playSound(frequency, duration, type = 'sine') { + if (!gameState.sfxEnabled) return; + + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.frequency.value = frequency; + oscillator.type = type; + + gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration); + + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + duration); +} + +function playPopSound(size) { + const baseFreq = 800 - (size * 2); + playSound(baseFreq, 0.1, 'sine'); + setTimeout(() => playSound(baseFreq * 1.5, 0.05, 'sine'), 50); +} + +function playPowerUpSound() { + playSound(1000, 0.1); + setTimeout(() => playSound(1200, 0.1), 100); + setTimeout(() => playSound(1500, 0.2), 200); +} + +function playComboSound() { + playSound(600 + (gameState.combo * 50), 0.15, 'square'); +} + +// ========== VIBRATION ========== +function vibrate(pattern) { + if (gameState.vibrationEnabled && navigator.vibrate) { + navigator.vibrate(pattern); + } +} + +// ========== DOM ELEMENTS ========== +const elements = { + // Screens + startMenu: document.getElementById('startMenu'), + howToPlayMenu: document.getElementById('howToPlayMenu'), + settingsMenu: document.getElementById('settingsMenu'), + leaderboardMenu: document.getElementById('leaderboardMenu'), + gameScreen: document.getElementById('gameScreen'), + pauseMenu: document.getElementById('pauseMenu'), + endScreen: document.getElementById('endScreen'), + + // Game elements + gameContainer: document.getElementById('gameContainer'), + countdown: document.getElementById('countdown'), + floatingText: document.getElementById('floatingText'), + + // HUD + score: document.getElementById('score'), + combo: document.getElementById('combo'), + comboDisplay: document.getElementById('comboDisplay'), + timer: document.getElementById('timer'), + timerProgress: document.getElementById('timerProgress'), + + // Buttons + playBtn: document.getElementById('playBtn'), + howToPlayBtn: document.getElementById('howToPlayBtn'), + settingsBtn: document.getElementById('settingsBtn'), + leaderboardBtn: document.getElementById('leaderboardBtn'), + backFromHowTo: document.getElementById('backFromHowTo'), + backFromSettings: document.getElementById('backFromSettings'), + backFromLeaderboard: document.getElementById('backFromLeaderboard'), + resetStatsBtn: document.getElementById('resetStatsBtn'), + pauseBtn: document.getElementById('pauseBtn'), + muteBtn: document.getElementById('muteBtn'), + resumeBtn: document.getElementById('resumeBtn'), + restartFromPause: document.getElementById('restartFromPause'), + quitBtn: document.getElementById('quitBtn'), + playAgainBtn: document.getElementById('playAgainBtn'), + backToMenuBtn: document.getElementById('backToMenuBtn'), + + // Settings + playerName: document.getElementById('playerName'), + musicToggle: document.getElementById('musicToggle'), + sfxToggle: document.getElementById('sfxToggle'), + vibrationToggle: document.getElementById('vibrationToggle'), + difficultySelect: document.getElementById('difficultySelect'), + gameModeSelect: document.getElementById('gameModeSelect'), + themeSelect: document.getElementById('themeSelect'), + + // End screen + finalScore: document.getElementById('finalScore'), + bubblesPopped: document.getElementById('bubblesPopped'), + bubblesMissed: document.getElementById('bubblesMissed'), + accuracy: document.getElementById('accuracy'), + bestCombo: document.getElementById('bestCombo'), + highScore: document.getElementById('highScore'), + achievementsUnlocked: document.getElementById('achievementsUnlocked'), + + // Leaderboard + leaderboardList: document.getElementById('leaderboardList') +}; + +// ========== INITIALIZATION ========== +function init() { + loadSettings(); + applyTheme(); + setupEventListeners(); + updateLeaderboard(); + initCursor(); + + // Load player name from localStorage + const savedName = localStorage.getItem('playerName'); + if (savedName) { + elements.playerName.value = savedName; + gameState.playerName = savedName; + } +} + +function loadSettings() { + const settings = JSON.parse(localStorage.getItem('gameSettings') || '{}'); + + gameState.musicEnabled = settings.musicEnabled !== false; + gameState.sfxEnabled = settings.sfxEnabled !== false; + gameState.vibrationEnabled = settings.vibrationEnabled !== false; + gameState.difficulty = settings.difficulty || 'medium'; + gameState.gameMode = settings.gameMode || 'timed'; + gameState.theme = settings.theme || 'ocean'; + + elements.musicToggle.textContent = gameState.musicEnabled ? 'ON' : 'OFF'; + elements.musicToggle.classList.toggle('active', gameState.musicEnabled); + + elements.sfxToggle.textContent = gameState.sfxEnabled ? 'ON' : 'OFF'; + elements.sfxToggle.classList.toggle('active', gameState.sfxEnabled); + + elements.vibrationToggle.textContent = gameState.vibrationEnabled ? 'ON' : 'OFF'; + elements.vibrationToggle.classList.toggle('active', gameState.vibrationEnabled); + + elements.difficultySelect.value = gameState.difficulty; + elements.gameModeSelect.value = gameState.gameMode; + elements.themeSelect.value = gameState.theme; +} + +function saveSettings() { + const settings = { + musicEnabled: gameState.musicEnabled, + sfxEnabled: gameState.sfxEnabled, + vibrationEnabled: gameState.vibrationEnabled, + difficulty: gameState.difficulty, + gameMode: gameState.gameMode, + theme: gameState.theme + }; + localStorage.setItem('gameSettings', JSON.stringify(settings)); +} + +function applyTheme() { + const body = document.body; + body.className = ''; + + if (gameState.theme === 'auto') { + const hour = new Date().getHours(); + if (hour >= 6 && hour < 18) { + body.classList.add('day-mode'); + } else { + body.classList.add('night-mode'); + } + } else { + body.classList.add('theme-' + gameState.theme); + } +} + +// ========== CUSTOM CURSOR ========== +function initCursor() { + const cursorTrail = document.querySelector('.cursor-trail'); + + document.addEventListener('mousemove', (e) => { + cursorTrail.style.left = e.clientX - 15 + 'px'; + cursorTrail.style.top = e.clientY - 15 + 'px'; + }); +} + +// ========== EVENT LISTENERS ========== +function setupEventListeners() { + // Menu navigation + elements.playBtn.addEventListener('click', startGame); + elements.howToPlayBtn.addEventListener('click', () => showScreen('howToPlayMenu')); + elements.settingsBtn.addEventListener('click', () => showScreen('settingsMenu')); + elements.leaderboardBtn.addEventListener('click', () => { + updateLeaderboard(); + showScreen('leaderboardMenu'); + }); + + elements.backFromHowTo.addEventListener('click', () => showScreen('startMenu')); + elements.backFromSettings.addEventListener('click', () => { + saveSettings(); + showScreen('startMenu'); + }); + elements.backFromLeaderboard.addEventListener('click', () => showScreen('startMenu')); + + // Reset stats + elements.resetStatsBtn.addEventListener('click', resetStats); + + // Player name + elements.playerName.addEventListener('input', (e) => { + gameState.playerName = e.target.value || 'Player'; + localStorage.setItem('playerName', gameState.playerName); + }); + + // Settings toggles + elements.musicToggle.addEventListener('click', () => { + gameState.musicEnabled = !gameState.musicEnabled; + elements.musicToggle.textContent = gameState.musicEnabled ? 'ON' : 'OFF'; + elements.musicToggle.classList.toggle('active'); + }); + + elements.sfxToggle.addEventListener('click', () => { + gameState.sfxEnabled = !gameState.sfxEnabled; + elements.sfxToggle.textContent = gameState.sfxEnabled ? 'ON' : 'OFF'; + elements.sfxToggle.classList.toggle('active'); + playSound(800, 0.1); + }); + + elements.vibrationToggle.addEventListener('click', () => { + gameState.vibrationEnabled = !gameState.vibrationEnabled; + elements.vibrationToggle.textContent = gameState.vibrationEnabled ? 'ON' : 'OFF'; + elements.vibrationToggle.classList.toggle('active'); + vibrate(50); + }); + + elements.difficultySelect.addEventListener('change', (e) => { + gameState.difficulty = e.target.value; + }); + + elements.gameModeSelect.addEventListener('change', (e) => { + gameState.gameMode = e.target.value; + }); + + elements.themeSelect.addEventListener('change', (e) => { + gameState.theme = e.target.value; + applyTheme(); + }); + + // Game controls + elements.pauseBtn.addEventListener('click', () => { + if (gameState.isPaused) { + resumeGame(); + } else { + pauseGame(); + } + }); + elements.muteBtn.addEventListener('click', toggleMute); + elements.resumeBtn.addEventListener('click', resumeGame); + elements.restartFromPause.addEventListener('click', () => { + // Close pause menu and clear intervals + elements.pauseMenu.classList.remove('active'); + gameState.isPaused = false; + clearInterval(gameState.spawnInterval); + if (gameState.timerInterval) clearInterval(gameState.timerInterval); + clearInterval(gameState.difficultyInterval); + // Start fresh game + startGame(); + }); + elements.quitBtn.addEventListener('click', quitToMenu); + + // End screen + elements.playAgainBtn.addEventListener('click', startGame); + elements.backToMenuBtn.addEventListener('click', () => showScreen('startMenu')); + + // Keyboard controls + document.addEventListener('keydown', (e) => { + if (e.code === 'Space' && gameState.isPlaying) { + e.preventDefault(); + if (gameState.isPaused) { + resumeGame(); + } else { + pauseGame(); + } + } + }); + + // Click effects + document.addEventListener('click', createRipple); +} + +function showScreen(screenId) { + document.querySelectorAll('.menu-screen, .game-screen, .overlay-menu').forEach(screen => { + screen.classList.remove('active'); + }); + + const screen = document.getElementById(screenId); + if (screen) { + screen.classList.add('active'); + } +} + +// ========== GAME FLOW ========== +function startGame() { + // Reset game state + gameState.score = 0; + gameState.combo = 0; + gameState.maxCombo = 0; + gameState.bubblesPopped = 0; + gameState.bubblesMissed = 0; + gameState.isPaused = false; + gameState.isPlaying = false; + gameState.doublePoints = false; + gameState.freezeTime = false; + gameState.unlockedAchievements = []; + + // Set time based on game mode + if (gameState.gameMode === 'timed') { + gameState.timeLeft = 60; + } else { + gameState.timeLeft = 0; + } + + // Clear game container + elements.gameContainer.innerHTML = ''; + + // Update UI + updateScore(); + updateCombo(); + updateTimer(); + + // Reset pause button to pause emoji + elements.pauseBtn.textContent = 'โธ๏ธ'; + + // Show game screen + showScreen('gameScreen'); + + // Start countdown + startCountdown(); +} + +function startCountdown() { + let count = 3; + elements.countdown.classList.add('active'); + elements.countdown.textContent = count; + playSound(800, 0.2); + + const countdownInterval = setInterval(() => { + count--; + if (count > 0) { + elements.countdown.textContent = count; + playSound(800, 0.2); + } else { + elements.countdown.textContent = 'GO!'; + playSound(1200, 0.3); + setTimeout(() => { + elements.countdown.classList.remove('active'); + beginGame(); + }, 500); + clearInterval(countdownInterval); + } + }, 1000); +} + +function beginGame() { + gameState.isPlaying = true; + + // Delay bubble spawning by 500ms after countdown ends + setTimeout(() => { + // Start bubble spawning + gameState.spawnInterval = setInterval(spawnBubble, difficultySettings[gameState.difficulty].spawnInterval); + }, 500); + + // Start timer (for timed mode) + if (gameState.gameMode === 'timed') { + gameState.timerInterval = setInterval(updateGameTimer, 1000); + } + + // Gradually increase difficulty + gameState.difficultyInterval = setInterval(increaseDifficulty, 10000); +} + +function pauseGame() { + if (!gameState.isPlaying || gameState.isPaused) return; + + gameState.isPaused = true; + clearInterval(gameState.spawnInterval); + if (gameState.timerInterval) clearInterval(gameState.timerInterval); + clearInterval(gameState.difficultyInterval); + + elements.pauseMenu.classList.add('active'); + elements.pauseBtn.textContent = 'โ–ถ๏ธ'; +} + +function resumeGame() { + if (!gameState.isPaused) return; + + gameState.isPaused = false; + elements.pauseMenu.classList.remove('active'); + elements.pauseBtn.textContent = 'โธ๏ธ'; + + gameState.spawnInterval = setInterval(spawnBubble, difficultySettings[gameState.difficulty].spawnInterval); + + if (gameState.gameMode === 'timed' && gameState.timeLeft > 0) { + gameState.timerInterval = setInterval(updateGameTimer, 1000); + } + + gameState.difficultyInterval = setInterval(increaseDifficulty, 10000); +} + +function toggleMute() { + gameState.sfxEnabled = !gameState.sfxEnabled; + gameState.musicEnabled = !gameState.musicEnabled; + elements.muteBtn.textContent = gameState.sfxEnabled ? '๐Ÿ”Š' : '๐Ÿ”‡'; +} + +function quitToMenu() { + endGame(false); + showScreen('startMenu'); +} + +function endGame(showEndScreen = true) { + gameState.isPlaying = false; + gameState.isPaused = false; + + clearInterval(gameState.spawnInterval); + clearInterval(gameState.timerInterval); + clearInterval(gameState.difficultyInterval); + clearTimeout(gameState.comboTimer); + + // Clear all bubbles + elements.gameContainer.innerHTML = ''; + + if (showEndScreen) { + // Calculate stats + const totalBubbles = gameState.bubblesPopped + gameState.bubblesMissed; + const accuracy = totalBubbles > 0 ? Math.round((gameState.bubblesPopped / totalBubbles) * 100) : 0; + + // Update end screen + elements.finalScore.textContent = gameState.score; + elements.bubblesPopped.textContent = gameState.bubblesPopped; + elements.bubblesMissed.textContent = gameState.bubblesMissed; + elements.accuracy.textContent = accuracy + '%'; + elements.bestCombo.textContent = gameState.maxCombo; + + // Check and save high score + const highScore = parseInt(localStorage.getItem('highScore') || '0'); + if (gameState.score > highScore) { + localStorage.setItem('highScore', gameState.score); + elements.highScore.textContent = gameState.score + ' ๐ŸŽ‰ NEW!'; + } else { + elements.highScore.textContent = highScore; + } + + // Save to leaderboard + saveToLeaderboard(); + + // Check achievements + checkAchievements(); + displayAchievements(); + + // Show end screen + showScreen('endScreen'); + } +} + +// ========== GAME TIMER ========== +function updateGameTimer() { + if (gameState.freezeTime) return; + + gameState.timeLeft--; + updateTimer(); + + if (gameState.timeLeft <= 0) { + endGame(); + } +} + +function updateTimer() { + if (gameState.gameMode === 'timed') { + elements.timer.textContent = gameState.timeLeft; + + // Update circle progress + const maxTime = 60; + const progress = (gameState.timeLeft / maxTime) * 283; + elements.timerProgress.style.strokeDashoffset = 283 - progress; + } else if (gameState.gameMode === 'endless') { + elements.timer.textContent = 'โˆž'; + elements.timerProgress.style.strokeDashoffset = 0; + } +} + +// ========== BUBBLE SPAWNING ========== +function spawnBubble() { + const settings = difficultySettings[gameState.difficulty]; + const currentBubbles = elements.gameContainer.children.length; + + if (currentBubbles >= settings.maxBubbles) return; + + const bubble = document.createElement('div'); + bubble.className = 'bubble'; + + // Random size (smaller = more points) + const size = Math.random() * 60 + 40; // 40-100px + bubble.style.width = size + 'px'; + bubble.style.height = size + 'px'; + + // Random position + const maxX = window.innerWidth - size - 20; + const maxY = window.innerHeight - size - 150; + bubble.style.left = Math.random() * maxX + 'px'; + bubble.style.top = Math.random() * maxY + 100 + 'px'; + + // Determine bubble type + const rand = Math.random(); + if (rand < 0.05) { + // 5% trap bubble + bubble.classList.add('trap'); + bubble.dataset.type = 'trap'; + } else if (rand < 0.08) { + // 3% power-up + const powerUps = ['freeze', 'bomb', 'double']; + const powerUp = powerUps[Math.floor(Math.random() * powerUps.length)]; + bubble.classList.add('powerup-' + powerUp); + bubble.dataset.type = 'powerup'; + bubble.dataset.powerup = powerUp; + } else { + // Normal bubble with random color + const hue = Math.random() * 360; + bubble.style.background = `linear-gradient(135deg, hsl(${hue}, 70%, 60%), hsl(${hue + 30}, 70%, 70%))`; + bubble.dataset.type = 'normal'; + } + + bubble.dataset.size = size; + bubble.dataset.points = Math.round(150 - size); + + // Add click event + bubble.addEventListener('click', (e) => { + e.stopPropagation(); + popBubble(bubble); + }); + + // Add touch event for mobile + bubble.addEventListener('touchstart', (e) => { + e.preventDefault(); + e.stopPropagation(); + popBubble(bubble); + }); + + elements.gameContainer.appendChild(bubble); + + // Auto-pop after lifetime + bubble.autoPopTimeout = setTimeout(() => { + if (bubble.parentElement && bubble.dataset.type !== 'trap') { + missedBubble(bubble); + } + }, settings.bubbleLifetime); +} + +// ========== BUBBLE INTERACTION ========== +function popBubble(bubble) { + if (!gameState.isPlaying || gameState.isPaused) return; + if (bubble.classList.contains('popping')) return; + + clearTimeout(bubble.autoPopTimeout); + + const type = bubble.dataset.type; + const size = parseFloat(bubble.dataset.size); + const rect = bubble.getBoundingClientRect(); + const x = rect.left + rect.width / 2; + const y = rect.top + rect.height / 2; + + if (type === 'trap') { + // Trap bubble - lose points + const penalty = Math.max(100, Math.floor(gameState.score * 0.1)); + gameState.score = Math.max(0, gameState.score - penalty); + updateScore(); + playSound(200, 0.3); + vibrate(200); + showFloatingText('OOPS! -' + penalty, x, y, '#ff0000'); + + // Reset combo + gameState.combo = 0; + updateCombo(); + } else if (type === 'powerup') { + // Power-up bubble + activatePowerUp(bubble.dataset.powerup, x, y); + playPowerUpSound(); + vibrate(50); + } else { + // Normal bubble + let points = parseInt(bubble.dataset.points); + + // Apply combo multiplier + gameState.combo++; + if (gameState.combo > gameState.maxCombo) { + gameState.maxCombo = gameState.combo; + } + + const multiplier = Math.min(Math.floor(gameState.combo / 5) + 1, 10); + points *= multiplier; + + // Apply double points power-up + if (gameState.doublePoints) { + points *= 2; + } + + gameState.score += points; + gameState.bubblesPopped++; + + updateScore(); + updateCombo(); + + playPopSound(size); + vibrate(30); + + // Show points + showFloatingText('+' + points, x, y, '#00d9ff'); + + // Combo feedback + if (gameState.combo % 5 === 0 && gameState.combo > 0) { + playComboSound(); + showFloatingText('COMBO x' + multiplier + '!', x, y - 50, '#ffeb3b'); + elements.comboDisplay.classList.add('active'); + setTimeout(() => elements.comboDisplay.classList.remove('active'), 300); + } + + // Reset combo timer + clearTimeout(gameState.comboTimer); + gameState.comboTimer = setTimeout(() => { + gameState.combo = 0; + updateCombo(); + }, 2000); + } + + // Pop animation + bubble.classList.add('popping'); + createParticles(x, y, bubble.style.background || '#00d9ff'); + + setTimeout(() => { + if (bubble.parentElement) { + bubble.remove(); + } + }, 300); +} + +function missedBubble(bubble) { + gameState.bubblesMissed++; + + // Reset combo + gameState.combo = 0; + updateCombo(); + + playSound(150, 0.2); + + if (bubble.parentElement) { + bubble.classList.add('popping'); + setTimeout(() => bubble.remove(), 300); + } +} + +// ========== POWER-UPS ========== +function activatePowerUp(type, x, y) { + switch(type) { + case 'freeze': + gameState.freezeTime = true; + showFloatingText('๐ŸงŠ TIME FROZEN!', x, y, '#00d9ff'); + setTimeout(() => { + gameState.freezeTime = false; + }, 5000); + break; + + case 'bomb': + showFloatingText('๐Ÿ’ฅ BOMB!', x, y, '#ff6b00'); + const bubbles = elements.gameContainer.querySelectorAll('.bubble'); + bubbles.forEach(b => { + if (b.dataset.type === 'normal') { + setTimeout(() => popBubble(b), Math.random() * 500); + } + }); + break; + + case 'double': + gameState.doublePoints = true; + showFloatingText('โญ DOUBLE POINTS!', x, y, '#ffd700'); + setTimeout(() => { + gameState.doublePoints = false; + }, 10000); + break; + } +} + +// ========== UI UPDATES ========== +function updateScore() { + elements.score.textContent = gameState.score; +} + +function updateCombo() { + if (gameState.combo > 1) { + elements.combo.textContent = 'x' + gameState.combo; + elements.comboDisplay.style.opacity = '1'; + } else { + elements.combo.textContent = 'x1'; + elements.comboDisplay.style.opacity = '0.5'; + } +} + +// ========== VISUAL EFFECTS ========== +function createParticles(x, y, color) { + const particleCount = 8; + + for (let i = 0; i < particleCount; i++) { + const particle = document.createElement('div'); + particle.className = 'particle'; + particle.style.left = x + 'px'; + particle.style.top = y + 'px'; + particle.style.background = color; + + const angle = (Math.PI * 2 * i) / particleCount; + const velocity = 50 + Math.random() * 50; + const tx = Math.cos(angle) * velocity; + const ty = Math.sin(angle) * velocity; + + particle.style.setProperty('--tx', tx + 'px'); + particle.style.setProperty('--ty', ty + 'px'); + + document.body.appendChild(particle); + + setTimeout(() => particle.remove(), 600); + } +} + +function showFloatingText(text, x, y, color) { + const floatingText = document.createElement('div'); + floatingText.className = 'floating-text'; + floatingText.textContent = text; + floatingText.style.left = x + 'px'; + floatingText.style.top = y + 'px'; + floatingText.style.color = color; + + document.body.appendChild(floatingText); + + setTimeout(() => floatingText.remove(), 1000); +} + +function createRipple(e) { + if (!gameState.isPlaying) return; + + const ripple = document.createElement('div'); + ripple.className = 'ripple'; + ripple.style.left = e.clientX - 50 + 'px'; + ripple.style.top = e.clientY - 50 + 'px'; + + document.body.appendChild(ripple); + + setTimeout(() => ripple.remove(), 600); +} + +// ========== DIFFICULTY PROGRESSION ========== +function increaseDifficulty() { + const settings = difficultySettings[gameState.difficulty]; + + // Increase spawn rate + if (settings.spawnInterval > 300) { + settings.spawnInterval -= 50; + clearInterval(gameState.spawnInterval); + gameState.spawnInterval = setInterval(spawnBubble, settings.spawnInterval); + } + + // Decrease bubble lifetime + if (settings.bubbleLifetime > 2000) { + settings.bubbleLifetime -= 200; + } +} + +// ========== LEADERBOARD ========== +function saveToLeaderboard() { + const leaderboard = JSON.parse(localStorage.getItem('leaderboard') || '[]'); + + leaderboard.push({ + name: gameState.playerName, + score: gameState.score, + date: new Date().toLocaleDateString(), + difficulty: gameState.difficulty, + mode: gameState.gameMode + }); + + // Sort and keep top 10 + leaderboard.sort((a, b) => b.score - a.score); + const top10 = leaderboard.slice(0, 10); + + localStorage.setItem('leaderboard', JSON.stringify(top10)); +} + +function updateLeaderboard() { + const leaderboard = JSON.parse(localStorage.getItem('leaderboard') || '[]'); + elements.leaderboardList.innerHTML = ''; + + if (leaderboard.length === 0) { + elements.leaderboardList.innerHTML = '

    No scores yet!

    '; + return; + } + + leaderboard.forEach((entry, index) => { + const item = document.createElement('div'); + item.className = 'leaderboard-item'; + + if (index === 0) item.classList.add('rank-1'); + else if (index === 1) item.classList.add('rank-2'); + else if (index === 2) item.classList.add('rank-3'); + + item.innerHTML = ` + ${index + 1}. ${entry.name} + ${entry.score} + `; + + elements.leaderboardList.appendChild(item); + }); +} + + function resetStats() { + // Confirmation dialog + const confirmed = confirm('Are you sure you want to reset all stats? This will clear:\n\nโ€ข Leaderboard\nโ€ข High Score\nโ€ข Achievements\n\nThis action cannot be undone!'); + + if (confirmed) { + // Clear leaderboard + localStorage.removeItem('leaderboard'); + + // Clear high score + localStorage.removeItem('highScore'); + + // Clear achievements + localStorage.removeItem('achievements'); + + // Update leaderboard display + updateLeaderboard(); + + // Play sound feedback + playSound(600, 0.1); + setTimeout(() => playSound(400, 0.1), 100); + setTimeout(() => playSound(200, 0.2), 200); + + // Visual feedback + alert('โœ… All stats have been reset!'); + } + } + +// ========== ACHIEVEMENTS ========== +function checkAchievements() { + const achievements = [ + { id: 'first_pop', name: '๐Ÿซง First Pop', condition: () => gameState.bubblesPopped >= 1 }, + { id: 'century', name: '๐Ÿ’ฏ Century Club', condition: () => gameState.bubblesPopped >= 100 }, + { id: 'combo_master', name: 'โšก Combo Master', condition: () => gameState.maxCombo >= 20 }, + { id: 'perfectionist', name: '๐ŸŽฏ Perfectionist', condition: () => { + const total = gameState.bubblesPopped + gameState.bubblesMissed; + return total > 0 && (gameState.bubblesPopped / total) >= 0.95; + }}, + { id: 'high_scorer', name: '๐Ÿ† High Scorer', condition: () => gameState.score >= 5000 }, + { id: 'survivor', name: '๐Ÿ’ช Survivor', condition: () => gameState.bubblesPopped >= 50 } + ]; + + const unlockedBefore = JSON.parse(localStorage.getItem('achievements') || '[]'); + + achievements.forEach(achievement => { + if (achievement.condition() && !unlockedBefore.includes(achievement.id)) { + gameState.unlockedAchievements.push(achievement); + unlockedBefore.push(achievement.id); + } + }); + + localStorage.setItem('achievements', JSON.stringify(unlockedBefore)); +} + +function displayAchievements() { + elements.achievementsUnlocked.innerHTML = ''; + + if (gameState.unlockedAchievements.length > 0) { + const title = document.createElement('h3'); + title.textContent = '๐ŸŽ‰ New Achievements!'; + title.style.color = 'var(--text-color)'; + title.style.marginBottom = '10px'; + elements.achievementsUnlocked.appendChild(title); + + gameState.unlockedAchievements.forEach(achievement => { + const badge = document.createElement('div'); + badge.className = 'achievement'; + badge.textContent = achievement.name; + elements.achievementsUnlocked.appendChild(badge); + }); + } +} + +// ========== START THE GAME ========== +init(); diff --git a/games/tap-the-bubble/style.css b/games/tap-the-bubble/style.css new file mode 100644 index 00000000..445bdf85 --- /dev/null +++ b/games/tap-the-bubble/style.css @@ -0,0 +1,763 @@ +/* ========== RESET & VARIABLES ========== */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #00d9ff; + --secondary-color: #ff6b9d; + --accent-color: #ffeb3b; + --bg-gradient-1: #667eea; + --bg-gradient-2: #764ba2; + --text-color: #ffffff; + --shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + --glow: 0 0 20px rgba(0, 217, 255, 0.5); +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + overflow: hidden; + height: 100vh; + width: 100vw; + position: relative; + cursor: none; + user-select: none; +} + +/* ========== THEMES ========== */ +body.theme-ocean { + --bg-gradient-1: #1e3c72; + --bg-gradient-2: #2a5298; + --primary-color: #00d9ff; +} + +body.theme-space { + --bg-gradient-1: #0f0c29; + --bg-gradient-2: #302b63; + --primary-color: #c471ed; +} + +body.theme-candy { + --bg-gradient-1: #ff9a9e; + --bg-gradient-2: #fecfef; + --primary-color: #ff6b9d; +} + +body.day-mode { + --bg-gradient-1: #74ebd5; + --bg-gradient-2: #acb6e5; + --primary-color: #ff6b9d; +} + +body.night-mode { + --bg-gradient-1: #232526; + --bg-gradient-2: #414345; + --primary-color: #00d9ff; +} + +/* ========== BACKGROUND ANIMATION ========== */ +.background-animation { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(135deg, var(--bg-gradient-1), var(--bg-gradient-2)); + animation: gradientShift 15s ease infinite; + z-index: -1; +} + +@keyframes gradientShift { + 0%, 100% { filter: hue-rotate(0deg); } + 50% { filter: hue-rotate(30deg); } +} + +/* Add animated waves for ocean theme */ +body.theme-ocean .background-animation::before, +body.theme-ocean .background-animation::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 200%; + height: 150px; + background: rgba(255, 255, 255, 0.1); + border-radius: 40%; +} + +body.theme-ocean .background-animation::before { + animation: wave 10s linear infinite; +} + +body.theme-ocean .background-animation::after { + animation: wave 15s linear infinite reverse; + opacity: 0.5; +} + +@keyframes wave { + 0% { transform: translateX(0); } + 100% { transform: translateX(-50%); } +} + +/* ========== CUSTOM CURSOR ========== */ +.cursor-trail { + position: fixed; + width: 30px; + height: 30px; + border: 2px solid var(--primary-color); + border-radius: 50%; + pointer-events: none; + z-index: 9999; + transition: transform 0.1s ease; + box-shadow: var(--glow); +} + +/* ========== MENU SCREENS ========== */ +.menu-screen { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: none; + justify-content: center; + align-items: center; + z-index: 100; + animation: fadeIn 0.3s ease; +} + +.menu-screen.active { + display: flex; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.menu-content { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(20px); + border-radius: 30px; + padding: 40px; + box-shadow: var(--shadow); + border: 2px solid rgba(255, 255, 255, 0.2); + text-align: center; + max-width: 500px; + width: 90%; + max-height: 90vh; + overflow-y: auto; + animation: slideUp 0.5s ease; +} + +@keyframes slideUp { + from { transform: translateY(50px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +.game-title { + font-size: 3em; + color: var(--text-color); + text-shadow: 0 0 20px var(--primary-color); + margin-bottom: 30px; + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} + +h2 { + color: var(--text-color); + margin-bottom: 20px; + font-size: 2em; +} + +.menu-content p { + text-align: center; + margin: 10px 0; +} + +.menu-content p.text-left { + text-align: left; +} + +#playerName { + width: 100%; + padding: 15px; + border: 2px solid var(--primary-color); + border-radius: 15px; + background: rgba(255, 255, 255, 0.2); + color: var(--text-color); + font-size: 1.1em; + margin-bottom: 20px; + text-align: center; + outline: none; + transition: all 0.3s ease; +} + +#playerName::placeholder { + color: rgba(255, 255, 255, 0.6); +} + +#playerName:focus { + box-shadow: var(--glow); + transform: scale(1.02); +} + +.menu-buttons { + display: flex; + flex-direction: column; + gap: 15px; +} + +.btn-primary, .btn-secondary, .icon-btn { + padding: 15px 30px; + border: none; + border-radius: 15px; + font-size: 1.2em; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 1px; +} + +.btn-primary { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + color: white; + box-shadow: var(--shadow); +} + +.btn-primary:hover { + transform: translateY(-3px) scale(1.05); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4); +} + +.btn-secondary { + background: rgba(255, 255, 255, 0.2); + color: var(--text-color); + border: 2px solid var(--primary-color); +} + +.btn-secondary:hover { + background: rgba(255, 255, 255, 0.3); + transform: translateY(-3px); +} + +/* ========== INSTRUCTIONS ========== */ +.instructions { + text-align: left; + color: var(--text-color); + line-height: 1.8; + margin-bottom: 20px; +} + +.instructions p { + margin: 10px 0; +} + +/* ========== SETTINGS ========== */ +.settings-grid { + display: grid; + grid-template-columns: 1fr; + gap: 15px; + margin-bottom: 20px; +} + +.setting-item { + display: flex; + justify-content: space-between; + align-items: center; + background: rgba(255, 255, 255, 0.1); + padding: 15px; + border-radius: 10px; +} + +.setting-item label { + color: var(--text-color); + font-weight: bold; +} + +.toggle-btn { + padding: 8px 20px; + border: 2px solid var(--primary-color); + border-radius: 20px; + background: rgba(255, 255, 255, 0.2); + color: var(--text-color); + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; +} + +.toggle-btn.active { + background: var(--primary-color); + color: white; +} + +select { + padding: 8px 15px; + border: 2px solid var(--primary-color); + border-radius: 10px; + background: rgba(255, 255, 255, 0.2); + color: var(--text-color); + font-weight: bold; + cursor: pointer; + outline: none; +} + +select option { + background: #333; + color: white; +} + +/* ========== LEADERBOARD ========== */ +.leaderboard-list { + margin: 20px 0; + max-height: 400px; + overflow-y: auto; +} + +.leaderboard-item { + display: flex; + justify-content: space-between; + padding: 15px; + margin: 10px 0; + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + color: var(--text-color); + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { transform: translateX(-20px); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +.leaderboard-item.rank-1 { + background: linear-gradient(135deg, #ffd700, #ffed4e); + color: #333; + font-size: 1.2em; +} + +.leaderboard-item.rank-2 { + background: linear-gradient(135deg, #c0c0c0, #e8e8e8); + color: #333; +} + +.leaderboard-item.rank-3 { + background: linear-gradient(135deg, #cd7f32, #e89d5f); + color: #fff; +} + +/* ========== GAME SCREEN ========== */ +.game-screen { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: none; +} + +.game-screen.active { + display: block; +} + +/* ========== HUD ========== */ +.hud { + position: absolute; + top: 20px; + left: 0; + right: 0; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 30px; + z-index: 10; +} + +.hud-left, .hud-right { + display: flex; + gap: 20px; +} + +.score-display, .combo-display { + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); + padding: 10px 20px; + border-radius: 15px; + box-shadow: var(--shadow); + display: flex; + flex-direction: column; + align-items: center; +} + +.label { + font-size: 0.9em; + color: rgba(255, 255, 255, 0.8); + text-transform: uppercase; +} + +.value { + font-size: 1.8em; + font-weight: bold; + color: var(--text-color); + text-shadow: 0 0 10px var(--primary-color); +} + +/* Timer Circle */ +.timer-container { + position: relative; + width: 100px; + height: 100px; +} + +.timer-circle { + width: 100%; + height: 100%; + transform: rotate(-90deg); +} + +.timer-bg { + fill: none; + stroke: rgba(255, 255, 255, 0.2); + stroke-width: 8; +} + +.timer-progress { + fill: none; + stroke: var(--primary-color); + stroke-width: 8; + stroke-dasharray: 283; + stroke-dashoffset: 0; + transition: stroke-dashoffset 1s linear; + filter: drop-shadow(0 0 10px var(--primary-color)); +} + +.timer-text { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 2em; + font-weight: bold; + color: var(--text-color); +} + +/* ========== GAME CONTROLS ========== */ +.game-controls { + position: absolute; + top: 20px; + right: 30px; + display: flex; + gap: 10px; + z-index: 15; +} + +.icon-btn { + width: 50px; + height: 50px; + font-size: 1.5em; + padding: 0; + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); + box-shadow: var(--shadow); +} + +.icon-btn:hover { + background: rgba(255, 255, 255, 0.3); + transform: scale(1.1); +} + +/* ========== GAME CONTAINER ========== */ +.game-container { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; +} + +/* ========== BUBBLES ========== */ +.bubble { + position: absolute; + border-radius: 50%; + cursor: pointer; + animation: float 3s ease-in-out infinite, appear 0.3s ease; + box-shadow: inset -10px -10px 20px rgba(0, 0, 0, 0.2), + 0 0 20px rgba(255, 255, 255, 0.5); + transition: transform 0.1s ease; +} + +.bubble::before { + content: ''; + position: absolute; + top: 10%; + left: 15%; + width: 40%; + height: 40%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.8), transparent); + border-radius: 50%; +} + +.bubble:hover { + transform: scale(1.1); +} + +@keyframes appear { + from { transform: scale(0); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} + +@keyframes float { + 0%, 100% { transform: translateY(0) rotate(0deg); } + 50% { transform: translateY(-20px) rotate(5deg); } +} + +/* Bubble Types */ +.bubble.trap { + background: linear-gradient(135deg, #ff0000, #990000); + animation: float 3s ease-in-out infinite, pulse 1s ease-in-out infinite; +} + +.bubble.powerup-freeze { + background: linear-gradient(135deg, #00d9ff, #0099ff); +} + +.bubble.powerup-bomb { + background: linear-gradient(135deg, #ff6b00, #ff3d00); +} + +.bubble.powerup-double { + background: linear-gradient(135deg, #ffd700, #ffed4e); +} + +.bubble.powerup-life { + background: linear-gradient(135deg, #ff69b4, #ff1493); +} + +/* Pop Animation */ +.bubble.popping { + animation: pop 0.3s ease forwards; +} + +@keyframes pop { + 0% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.5); opacity: 0.5; } + 100% { transform: scale(0); opacity: 0; } +} + +/* ========== PARTICLES ========== */ +.particle { + position: absolute; + width: 10px; + height: 10px; + border-radius: 50%; + pointer-events: none; + animation: particleBurst 0.6s ease forwards; +} + +@keyframes particleBurst { + 0% { transform: translate(0, 0) scale(1); opacity: 1; } + 100% { transform: translate(var(--tx), var(--ty)) scale(0); opacity: 0; } +} + +/* ========== COUNTDOWN ========== */ +.countdown { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 10em; + font-weight: bold; + color: var(--text-color); + text-shadow: 0 0 50px var(--primary-color); + z-index: 200; + display: none; + animation: countdownPulse 1s ease; +} + +.countdown.active { + display: block; +} + +@keyframes countdownPulse { + 0% { transform: translate(-50%, -50%) scale(0); opacity: 0; } + 50% { transform: translate(-50%, -50%) scale(1.2); opacity: 1; } + 100% { transform: translate(-50%, -50%) scale(1); opacity: 1; } +} + +/* ========== FLOATING TEXT ========== */ +.floating-text { + position: fixed; + font-size: 2em; + font-weight: bold; + pointer-events: none; + z-index: 150; + animation: floatUp 1s ease forwards; + text-shadow: 0 0 10px var(--primary-color); +} + +@keyframes floatUp { + 0% { transform: translateY(0) scale(1); opacity: 1; } + 100% { transform: translateY(-100px) scale(1.5); opacity: 0; } +} + +/* ========== CLICK RIPPLE ========== */ +.ripple { + position: fixed; + border: 2px solid var(--primary-color); + border-radius: 50%; + pointer-events: none; + animation: rippleEffect 0.6s ease forwards; + z-index: 50; +} + +@keyframes rippleEffect { + 0% { width: 0; height: 0; opacity: 1; } + 100% { width: 100px; height: 100px; opacity: 0; } +} + +/* ========== PAUSE MENU ========== */ +.overlay-menu { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(10px); + display: none; + justify-content: center; + align-items: center; + z-index: 300; +} + +.overlay-menu.active { + display: flex; + animation: fadeIn 0.3s ease; +} + +/* ========== STATS DISPLAY ========== */ +.stats-display { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + margin: 20px 0; +} + +.stat-item { + background: rgba(255, 255, 255, 0.1); + padding: 15px; + border-radius: 10px; + display: flex; + flex-direction: column; +} + +.stat-label { + font-size: 0.9em; + color: rgba(255, 255, 255, 0.8); + margin-bottom: 5px; +} + +.stat-value { + font-size: 1.8em; + font-weight: bold; + color: var(--primary-color); +} + +/* ========== ACHIEVEMENTS ========== */ +.achievements { + margin: 20px 0; +} + +.achievement { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + padding: 10px 20px; + border-radius: 20px; + margin: 5px; + display: inline-block; + animation: slideIn 0.5s ease; +} + +/* ========== COMBO EFFECTS ========== */ +.combo-display.active { + animation: comboShake 0.3s ease; +} + +@keyframes comboShake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } +} + +/* ========== RESPONSIVE DESIGN ========== */ +@media (max-width: 768px) { + .game-title { + font-size: 2em; + } + + .hud { + padding: 0 10px; + flex-wrap: wrap; + } + + .hud-left, .hud-right { + gap: 10px; + } + + .score-display, .combo-display, .lives-display { + padding: 8px 12px; + } + + .value { + font-size: 1.3em; + } + + .timer-container { + width: 70px; + height: 70px; + } + + .timer-text { + font-size: 1.5em; + } + + .stats-display { + grid-template-columns: 1fr; + } + + .menu-content { + padding: 20px; + } + + body { + cursor: auto; + } + + .cursor-trail { + display: none; + } +} + +/* ========== SCROLLBAR ========== */ +::-webkit-scrollbar { + width: 10px; +} + +::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; +} + +::-webkit-scrollbar-thumb { + background: var(--primary-color); + border-radius: 10px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--secondary-color); +} diff --git a/games/target-zoom/index.html b/games/target-zoom/index.html new file mode 100644 index 00000000..d861a463 --- /dev/null +++ b/games/target-zoom/index.html @@ -0,0 +1,32 @@ + + + + + + Target Zoom ๐ŸŽฏ + + + +
    +

    ๐ŸŽฏ Target Zoom

    +
    + Score: 0 + Lives: 3 +
    + +
    + +
    + + + +
    + + + + +
    + + + + diff --git a/games/target-zoom/script.js b/games/target-zoom/script.js new file mode 100644 index 00000000..24943742 --- /dev/null +++ b/games/target-zoom/script.js @@ -0,0 +1,121 @@ +const gameArea = document.getElementById('game-area'); +const scoreEl = document.getElementById('score'); +const livesEl = document.getElementById('lives'); +const startBtn = document.getElementById('start-btn'); +const pauseBtn = document.getElementById('pause-btn'); +const restartBtn = document.getElementById('restart-btn'); + +const hitSound = document.getElementById('hit-sound'); +const missSound = document.getElementById('miss-sound'); +const gameOverSound = document.getElementById('gameover-sound'); + +let score = 0; +let lives = 3; +let gameRunning = false; +let paused = false; +let shrinkInterval; +let target; +let size = 100; +let spawnDelay = 1500; + +function startGame() { + resetGame(); + gameRunning = true; + paused = false; + startBtn.disabled = true; + pauseBtn.disabled = false; + restartBtn.disabled = false; + spawnTarget(); +} + +function pauseGame() { + paused = !paused; + pauseBtn.textContent = paused ? "Resume" : "Pause"; + if (paused) { + clearInterval(shrinkInterval); + if (target) target.remove(); + } else { + spawnTarget(); + } +} + +function restartGame() { + clearInterval(shrinkInterval); + resetGame(); + spawnTarget(); +} + +function resetGame() { + score = 0; + lives = 3; + scoreEl.textContent = score; + livesEl.textContent = lives; + clearInterval(shrinkInterval); + gameArea.innerHTML = ''; +} + +function spawnTarget() { + if (!gameRunning || paused) return; + + target = document.createElement('div'); + target.classList.add('target'); + size = 100; + + const x = Math.random() * (gameArea.clientWidth - size); + const y = Math.random() * (gameArea.clientHeight - size); + target.style.width = size + 'px'; + target.style.height = size + 'px'; + target.style.left = x + 'px'; + target.style.top = y + 'px'; + gameArea.appendChild(target); + + target.addEventListener('click', hitTarget); + + shrinkInterval = setInterval(() => { + if (paused || !gameRunning) return; + size -= 3; + target.style.width = size + 'px'; + target.style.height = size + 'px'; + if (size <= 0) { + missTarget(); + } + }, 60); +} + +function hitTarget() { + score++; + scoreEl.textContent = score; + hitSound.currentTime = 0; + hitSound.play(); + clearInterval(shrinkInterval); + target.remove(); + spawnDelay = Math.max(500, spawnDelay - 50); + setTimeout(spawnTarget, 400); +} + +function missTarget() { + clearInterval(shrinkInterval); + target.remove(); + lives--; + livesEl.textContent = lives; + missSound.currentTime = 0; + missSound.play(); + + if (lives <= 0) { + endGame(); + } else { + setTimeout(spawnTarget, 700); + } +} + +function endGame() { + gameRunning = false; + pauseBtn.disabled = true; + startBtn.disabled = false; + gameOverSound.play(); + alert(`๐Ÿ’ฅ Game Over! Final Score: ${score}`); +} + +startBtn.addEventListener('click', startGame); +pauseBtn.addEventListener('click', pauseGame); +restartBtn.addEventListener('click', restartGame); diff --git a/games/target-zoom/style.css b/games/target-zoom/style.css new file mode 100644 index 00000000..5763d5f5 --- /dev/null +++ b/games/target-zoom/style.css @@ -0,0 +1,74 @@ +body { + font-family: 'Poppins', sans-serif; + background: radial-gradient(circle at center, #0f0c29, #302b63, #24243e); + color: white; + height: 100vh; + margin: 0; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; +} + +.game-container { + text-align: center; + position: relative; +} + +h1 { + text-shadow: 0 0 20px #ff0080; + margin-bottom: 10px; +} + +.hud { + display: flex; + justify-content: space-between; + width: 250px; + margin: 0 auto 10px; + font-size: 18px; +} + +#game-area { + position: relative; + width: 600px; + height: 400px; + background: rgba(255, 255, 255, 0.1); + border: 2px solid #fff; + border-radius: 12px; + overflow: hidden; + cursor: crosshair; +} + +.target { + position: absolute; + border-radius: 50%; + background: radial-gradient(circle, #ff3366, #ff004c); + box-shadow: 0 0 20px 8px #ff004c; + transition: transform 0.1s linear; +} + +.controls { + margin-top: 15px; +} + +button { + background: linear-gradient(90deg, #ff0080, #ff8c00); + color: white; + border: none; + padding: 10px 20px; + margin: 0 5px; + border-radius: 6px; + font-size: 16px; + cursor: pointer; + transition: 0.2s; +} + +button:hover { + transform: scale(1.05); + box-shadow: 0 0 12px #ff0080; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/games/target_shooter/index.html b/games/target_shooter/index.html new file mode 100644 index 00000000..da0cac9a --- /dev/null +++ b/games/target_shooter/index.html @@ -0,0 +1,33 @@ + + + + + + Hit the Dot - Reaction Test + + + + +
    +

    ๐ŸŽฏ Hit the Dot!

    + +
    + Score: 0 | Misses: 0 +
    + +
    +
    +
    + +
    +

    Click the dot before it disappears! Try to beat your score.

    +
    + +
    + +
    +
    + + + + \ No newline at end of file diff --git a/games/target_shooter/script.js b/games/target_shooter/script.js new file mode 100644 index 00000000..082e3fd3 --- /dev/null +++ b/games/target_shooter/script.js @@ -0,0 +1,161 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. DOM Elements & Constants --- + const targetDot = document.getElementById('target-dot'); + const playingField = document.getElementById('playing-field'); + const startButton = document.getElementById('start-button'); + const scoreDisplay = document.getElementById('score-display'); + const missesDisplay = document.getElementById('misses-display'); + const feedbackMessage = document.getElementById('feedback-message'); + + const FIELD_SIZE = 500; + const DOT_SIZE = 40; + const GAME_DURATION_ROUNDS = 10; // Total number of dots to hit + const BASE_DISPLAY_TIME_MS = 1000; // Base time the dot is visible (1 second) + + // --- 2. GAME STATE VARIABLES --- + let score = 0; + let misses = 0; + let roundsPlayed = 0; + let gameActive = false; + let timeoutId = null; // ID for the dot disappearance timer + + // --- 3. CORE LOGIC FUNCTIONS --- + + /** + * Generates a new random position for the dot, ensuring it stays within the field. + */ + function getNewRandomPosition() { + const maxX = FIELD_SIZE - DOT_SIZE; + const maxY = FIELD_SIZE - DOT_SIZE; + + const x = Math.floor(Math.random() * maxX); + const y = Math.floor(Math.random() * maxY); + + return { x, y }; + } + + /** + * Shows the dot at a new random location and starts the disappearance timer. + */ + function showNewDot() { + if (roundsPlayed >= GAME_DURATION_ROUNDS) { + endGame(); + return; + } + + const { x, y } = getNewRandomPosition(); + + targetDot.style.left = `${x}px`; + targetDot.style.top = `${y}px`; + targetDot.style.display = 'block'; + + roundsPlayed++; + + // Determine display time (can decrease with score/rounds for difficulty) + // Let's keep it constant for simplicity in the base version + const displayTime = BASE_DISPLAY_TIME_MS; + + // Set timeout for dot disappearance (a "miss") + timeoutId = setTimeout(handleMiss, displayTime); + } + + /** + * Handles the player clicking the dot successfully. + */ + function handleHit() { + if (!gameActive) return; + + // 1. Stop the disappearance timer immediately + clearTimeout(timeoutId); + + // 2. Score and Hide + score++; + targetDot.style.display = 'none'; + + // 3. Update Status + scoreDisplay.textContent = score; + feedbackMessage.textContent = `๐ŸŽฏ HIT! (${roundsPlayed} / ${GAME_DURATION_ROUNDS})`; + feedbackMessage.style.color = '#2ecc71'; + + // 4. Next Round (Start immediately) + setTimeout(showNewDot, 200); // Small delay before the next dot appears + } + + /** + * Handles the dot disappearing before the player clicks it (a "miss"). + */ + function handleMiss() { + if (!gameActive) return; + + // 1. Record Miss and Hide + misses++; + targetDot.style.display = 'none'; + + // 2. Update Status + missesDisplay.textContent = misses; + feedbackMessage.innerHTML = `โŒ MISS! Dot disappeared. (${roundsPlayed} / ${GAME_DURATION_ROUNDS})`; + feedbackMessage.style.color = '#e74c3c'; + + // 3. Check for game end or continue + if (roundsPlayed >= GAME_DURATION_ROUNDS) { + endGame(); + } else { + setTimeout(showNewDot, 500); // Slightly longer delay after a miss + } + } + + // --- 4. GAME FLOW --- + + /** + * Starts the overall game session. + */ + function startGame() { + // Reset state + score = 0; + misses = 0; + roundsPlayed = 0; + gameActive = true; + + scoreDisplay.textContent = score; + missesDisplay.textContent = misses; + startButton.disabled = true; + startButton.textContent = 'Playing...'; + + feedbackMessage.textContent = 'Get ready...'; + + // Hide dot initially and then show the first one after a brief delay + targetDot.style.display = 'none'; + setTimeout(showNewDot, 1000); + } + + /** + * Ends the game and displays final results. + */ + function endGame() { + gameActive = false; + clearTimeout(timeoutId); + targetDot.style.display = 'none'; + + const finalAccuracy = (score / roundsPlayed) * 100; + + feedbackMessage.innerHTML = ` +

    GAME OVER!

    +

    Final Score: ${score} hits / ${misses} misses

    +

    Accuracy: ${finalAccuracy.toFixed(1)}%

    + `; + feedbackMessage.style.color = '#3498db'; // Blue final score color + + startButton.textContent = 'PLAY AGAIN'; + startButton.disabled = false; + } + + // --- 5. EVENT LISTENERS --- + + startButton.addEventListener('click', startGame); + + // Attach click handler to the dot itself (only active when visible) + targetDot.addEventListener('click', handleHit); + + // Initial setup + feedbackMessage.textContent = `Ready for ${GAME_DURATION_ROUNDS} rounds!`; +}); \ No newline at end of file diff --git a/games/target_shooter/style.css b/games/target_shooter/style.css new file mode 100644 index 00000000..079ef5d7 --- /dev/null +++ b/games/target_shooter/style.css @@ -0,0 +1,84 @@ +:root { + --field-size: 500px; + --dot-size: 40px; +} + +body { + font-family: 'Verdana', sans-serif; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #34495e; /* Dark background */ + color: #ecf0f1; +} + +#game-container { + background-color: #2c3e50; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4); + text-align: center; + max-width: 600px; + width: 90%; +} + +h1 { + color: #f1c40f; /* Yellow */ + margin-bottom: 20px; +} + +#status-area { + font-size: 1.2em; + font-weight: bold; + margin-bottom: 15px; +} + +/* --- Playing Field --- */ +#playing-field { + width: var(--field-size); + height: var(--field-size); + margin: 0 auto; + border: 3px solid #bdc3c7; + background-color: #34495e; + position: relative; /* CRUCIAL for absolute positioning of the dot */ + overflow: hidden; +} + +/* --- Target Dot --- */ +#target-dot { + width: var(--dot-size); + height: var(--dot-size); + background-color: #e74c3c; /* Bright Red target */ + border-radius: 50%; + position: absolute; + cursor: pointer; + box-shadow: 0 0 15px rgba(255, 0, 0, 0.8); + display: none; /* Hidden by default */ +} + +/* --- Controls and Feedback --- */ +#feedback-message { + min-height: 20px; + margin-top: 20px; + margin-bottom: 15px; + font-size: 1.1em; +} + +#start-button { + padding: 15px 30px; + font-size: 1.2em; + font-weight: bold; + background-color: #3498db; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +#start-button:hover:not(:disabled) { + background-color: #2980b9; +} \ No newline at end of file diff --git a/games/temperature-control/index.html b/games/temperature-control/index.html new file mode 100644 index 00000000..1224b7a0 --- /dev/null +++ b/games/temperature-control/index.html @@ -0,0 +1,124 @@ + + + + + + Temperature Control โ€” Mini JS Games Hub + + + + + + + + + + + +
    +
    +
    +

    Temperature Control

    +

    Keep the system inside the target range โ€” handle disturbances & changing setpoints.

    +
    +
    + + + +
    Idle
    +
    +
    + +
    +
    +
    +
    Current Temp
    +
    --ยฐC
    +
    +
    +
    +
    + Target: -- โ€ข Safety: -- +
    +
    + +
    +
    + + +
    +
    + + +
    + +
    + + + +
    + +
    + +
    +
    + +
    +
    Events & Logs
    +
      +
      +
      + +
      +
      + +
      + +
      +
      +
      Elapsed
      +
      00:00
      +
      +
      +
      In-Range Time
      +
      0s
      +
      +
      +
      Win Condition
      +
      Keep within range for 30s
      +
      +
      + +
      + + +
      +
      +
      + + +
      + + + + + + + + + + + + + + diff --git a/games/temperature-control/script.js b/games/temperature-control/script.js new file mode 100644 index 00000000..7ec10a99 --- /dev/null +++ b/games/temperature-control/script.js @@ -0,0 +1,406 @@ +/* Temperature Control Game + - drop into games/temperature-control/ + - uses online images & sounds (no downloads) +*/ + +(() => { + // Elements + const startBtn = document.getElementById('startBtn'); + const pauseBtn = document.getElementById('pauseBtn'); + const restartBtn = document.getElementById('restartBtn'); + const difficulty = document.getElementById('difficulty'); + + const heatSlider = document.getElementById('heatSlider'); + const coolSlider = document.getElementById('coolSlider'); + const centerSlider = document.getElementById('centerSlider'); + const tolSlider = document.getElementById('tolSlider'); + + const heatVal = document.getElementById('heatVal'); + const coolVal = document.getElementById('coolVal'); + const centerVal = document.getElementById('centerVal'); + const tolVal = document.getElementById('tolVal'); + + const currentTempEl = document.getElementById('currentTemp'); + const targetRangeEl = document.getElementById('targetRange'); + const timeRunningEl = document.getElementById('timeRunning'); + const insideMsEl = document.getElementById('insideMs'); + const distCountEl = document.getElementById('distCount'); + const stateEl = document.getElementById('state'); + const bulb = document.getElementById('bulb'); + + const winSfx = document.getElementById('sfxWin'); + const loseSfx = document.getElementById('sfxLose'); + const distSfx = document.getElementById('sfxDisturb'); + const soundToggle = document.getElementById('soundToggle'); + + // Canvas chart + const canvas = document.getElementById('tempChart'); + const ctx = canvas.getContext('2d'); + + // Play button just brings focus/tracking (keeps compatibility with hub) + document.getElementById('playNow').addEventListener('click', (e) => { + e.preventDefault(); + startBtn.click(); + }); + + // Game state + let running = false; + let paused = false; + let lastTs = null; + let elapsed = 0; + let insideMs = 0; + let distCount = 0; + let interval = null; + let updateRate = 60; // updates per second + let history = []; + + // Simulation variables + let temp = 22 + Math.random() * 2 - 1; // start near center + let targetCenter = parseFloat(centerSlider.value); + let tolerance = parseFloat(tolSlider.value); + let safetyLimit = 20; // absolute allowed delta before immediate fail + let winDuration = 30000; // keep in range for 30s + let loseOutsideLimit = 10000; // ms outside allowed range before loss + let outsideTimer = 0; + + // Difficulty affects disturbance frequency & magnitude + function difficultyParams(level) { + if (level === 'easy') return { freq: 12000, mag: 0.8, win: 20000 }; + if (level === 'normal') return { freq: 9000, mag: 1.6, win: 30000 }; + return { freq: 6000, mag: 2.6, win: 40000 }; + } + + // update UI slider labels + function updateSliderLabels() { + heatVal.textContent = `${heatSlider.value}%`; + coolVal.textContent = `${coolSlider.value}%`; + centerVal.textContent = `${centerSlider.value}ยฐC`; + tolVal.textContent = `ยฑ${tolSlider.value}ยฐC`; + targetRangeEl.textContent = `${centerSlider.value - tolSlider.value}โ€“${parseInt(centerSlider.value) + parseInt(tolSlider.value)}ยฐC`; + document.getElementById('winDuration').textContent = (difficultyParams(difficulty.value).win / 1000) + 's'; + } + + [heatSlider, coolSlider, centerSlider, tolSlider, difficulty].forEach(el => { + el.addEventListener('input', () => { + updateSliderLabels(); + targetCenter = parseFloat(centerSlider.value); + tolerance = parseFloat(tolSlider.value); + // adjust win duration from difficulty + winDuration = difficultyParams(difficulty.value).win; + }); + }); + + updateSliderLabels(); + + // Sound helper + function playSound(audioEl) { + if (!soundToggle.checked) return; + try { + audioEl.currentTime = 0; + audioEl.play(); + } catch(e){} + } + + // Disturbance generator + function spawnDisturbance() { + const d = difficultyParams(difficulty.value); + const magnitude = (Math.random() * d.mag * 2 - d.mag) * (Math.random() < 0.5 ? -1 : 1); + // immediate temp impulse + temp += magnitude; + distCount += 1; + distCountEl.textContent = distCount; + playSound(distSfx); + // small visual flash + bulb.classList.add('glow'); + setTimeout(() => bulb.classList.remove('glow'), 900); + } + + // schedule disturbances + let disturbTimer = null; + function scheduleDisturbances() { + if (disturbTimer) clearInterval(disturbTimer); + const d = difficultyParams(difficulty.value); + disturbTimer = setInterval(() => { + if (running && !paused) spawnDisturbance(); + }, d.freq + Math.random() * d.freq); + } + + // Chart drawing + function drawChart() { + const w = canvas.width = canvas.clientWidth * devicePixelRatio; + const h = canvas.height = canvas.clientHeight * devicePixelRatio; + ctx.clearRect(0,0,w,h); + + // background grid + ctx.save(); + ctx.scale(devicePixelRatio, devicePixelRatio); + const cw = canvas.clientWidth; + const ch = canvas.clientHeight; + + ctx.fillStyle = 'rgba(255,255,255,0.02)'; + ctx.fillRect(0,0,cw,ch); + + // draw axes lines + ctx.strokeStyle = 'rgba(255,255,255,0.06)'; + ctx.lineWidth = 1; + for(let i=0;i<5;i++){ + ctx.beginPath(); + ctx.moveTo(0, (i+1)*ch/6); + ctx.lineTo(cw, (i+1)*ch/6); + ctx.stroke(); + } + + // target band + const minT = targetCenter - tolerance; + const maxT = targetCenter + tolerance; + // map temp to vertical pixel (higher temp -> lower y) + const temps = history.length ? history.map(h=>h.t) : [targetCenter]; + const allTemps = temps.concat([minT, maxT]); + const min = Math.min(...allTemps) - 5; + const max = Math.max(...allTemps) + 5; + + function yOf(t){ + return ch - ((t - min) / (max - min)) * ch; + } + + ctx.fillStyle = 'rgba(126,224,255,0.06)'; + ctx.fillRect(0, yOf(maxT), cw, yOf(minT) - yOf(maxT)); + + // draw temp polyline + if(history.length){ + ctx.beginPath(); + ctx.lineWidth = 2; + ctx.strokeStyle = '#ffb86b'; + history.forEach((pt, i) => { + const x = (i / Math.max(1, history.length - 1)) * cw; + const y = yOf(pt.t); + if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); + }); + ctx.stroke(); + + // area fill + ctx.globalAlpha = 0.08; + ctx.fillStyle = '#ffb86b'; + ctx.lineTo(cw, ch); + ctx.lineTo(0, ch); + ctx.closePath(); + ctx.fill(); + ctx.globalAlpha = 1; + } + + // draw center line + ctx.beginPath(); + ctx.setLineDash([6,6]); + ctx.strokeStyle = 'rgba(255,255,255,0.08)'; + ctx.moveTo(0, yOf(targetCenter)); + ctx.lineTo(cw, yOf(targetCenter)); + ctx.stroke(); + ctx.setLineDash([]); + + // labels + ctx.fillStyle = 'rgba(255,255,255,0.6)'; + ctx.font = '12px Inter, Arial'; + ctx.fillText(`${max.toFixed(1)}ยฐC`, 6, 12); + ctx.fillText(`${min.toFixed(1)}ยฐC`, 6, ch - 6); + + ctx.restore(); + } + + // Simulation step + function step(dt) { + // heating and cooling produce change rates per second + const heatPower = parseFloat(heatSlider.value) / 100; // 0..1 + const coolPower = parseFloat(coolSlider.value) / 100; // 0..1 + + // convert to degrees per second rates + // heating effect stronger than cooling slightly + const heatRate = heatPower * 6.0; // max +6 ยฐC/s + const coolRate = coolPower * 6.4; // max -6.4 ยฐC/s + + // passive ambient drift toward ambient (20ยฐC) + const ambient = 20; + const drift = (ambient - temp) * 0.02; // small stabilizing effect + + // net change = heat - cool + drift + temp += (heatRate - coolRate) * (dt / 1000) + drift * (dt / 1000); + + // small natural inertia (smoothing) + temp += (Math.random() - 0.5) * 0.02 * (dt / 1000); + + // clamp for numeric safety + if (temp < -100) temp = -100; + if (temp > 200) temp = 200; + + // record history + history.push({ t: temp, ts: Date.now() }); + if (history.length > 400) history.shift(); + + // UI updates + currentTempEl.textContent = `${temp.toFixed(1)}ยฐC`; + + // bulb glow intensity mapping + const center = targetCenter; + const delta = temp - center; + const intensity = Math.min(1, Math.abs(delta) / (tolerance + 4)); + if (delta > 0.2) { + bulb.classList.add('glow'); + bulb.style.boxShadow = `0 12px 40px rgba(255,130,0,${0.15 + intensity*0.3})`; + } else { + bulb.classList.remove('glow'); + bulb.style.boxShadow = ''; + } + + // inside range? + const inRange = (temp >= targetCenter - tolerance) && (temp <= targetCenter + tolerance); + if (inRange) { + insideMs += dt; + insideMsEl.textContent = `${Math.round(insideMs)} ms`; + outsideTimer = 0; + } else { + outsideTimer += dt; + } + + // win/lose conditions + if (insideMs >= winDuration) { + endGame(true, 'You maintained stability โ€” WIN!'); + } else if (Math.abs(temp - targetCenter) >= safetyLimit) { + endGame(false, 'Safety limit exceeded โ€” SYSTEM FAILURE!'); + } else if (outsideTimer >= loseOutsideLimit) { + endGame(false, 'Temperature was outside range for too long โ€” LOSE'); + } + + // draw chart occasionally (not every frame for perf) + if (Math.random() < 0.25) drawChart(); + } + + // Game loop + function loop(ts) { + if (!running || paused) { + lastTs = ts; + return; + } + if (!lastTs) lastTs = ts; + const dt = ts - lastTs; + lastTs = ts; + elapsed += dt; + // step simulation at updateRate times per second (approx) + step(dt); + + // update time display + timeRunningEl.textContent = formatMs(elapsed); + + // schedule next frame + requestAnimationFrame(loop); + } + + function formatMs(ms) { + const s = Math.floor(ms / 1000); + const mm = String(Math.floor(s / 60)).padStart(2,'0'); + const ss = String(s % 60).padStart(2,'0'); + return `${mm}:${ss}`; + } + + // Start / Pause / Restart handlers + function startGame() { + if (running) return; + running = true; + paused = false; + lastTs = null; + elapsed = 0; + insideMs = 0; + outsideTimer = 0; + distCount = 0; + history = []; + temp = parseFloat(centerSlider.value) + (Math.random() - 0.5) * 2; + distCountEl.textContent = distCount; + insideMsEl.textContent = `${insideMs} ms`; + timeRunningEl.textContent = '00:00'; + stateEl.textContent = 'Running'; + startBtn.disabled = true; + pauseBtn.disabled = false; + restartBtn.disabled = false; + scheduleDisturbances(); + requestAnimationFrame(loop); + } + + function pauseGame() { + paused = !paused; + stateEl.textContent = paused ? 'Paused' : 'Running'; + pauseBtn.textContent = paused ? 'Resume' : 'Pause'; + if (!paused) requestAnimationFrame(loop); + } + + function restartGame() { + running = false; + paused = false; + lastTs = null; + elapsed = 0; + insideMs = 0; + distCount = 0; + history = []; + temp = parseFloat(centerSlider.value); + timeRunningEl.textContent = '00:00'; + insideMsEl.textContent = '0 ms'; + distCountEl.textContent = '0'; + currentTempEl.textContent = `${temp.toFixed(1)}ยฐC`; + stateEl.textContent = 'Idle'; + startBtn.disabled = false; + pauseBtn.disabled = true; + restartBtn.disabled = true; + clearInterval(disturbTimer); + drawChart(); + } + + // End game + function endGame(won, message) { + running = false; + stateEl.textContent = won ? 'Victory' : 'Defeat'; + startBtn.disabled = false; + pauseBtn.disabled = true; + pauseBtn.textContent = 'Pause'; + restartBtn.disabled = false; + clearInterval(disturbTimer); + + // sound + alert box + if (won) playSound(winSfx); + else playSound(loseSfx); + + // visual summary modal (simple) + setTimeout(() => { + alert(message + '\nTime: ' + formatMs(elapsed) + '\nDisturbances: ' + distCount); + }, 150); + } + + // bind + startBtn.addEventListener('click', startGame); + pauseBtn.addEventListener('click', pauseGame); + restartBtn.addEventListener('click', restartGame); + + // optional quick keyboard shortcuts + window.addEventListener('keydown', (e) => { + if (e.key === ' ') { e.preventDefault(); if (!running) startGame(); else pauseGame(); } + if (e.key === 'r') restartGame(); + }); + + // initialize chart + drawChart(); + + // initial UI sync + updateSliderLabels(); + scheduleDisturbances(); + + // every second update glow intensity and labels + setInterval(() => { + // tiny aesthetic pulse when temp high + const dt = 1000; + // also update chart more often + drawChart(); + }, 1000); + + // expose a minimal API for hub tracking if needed + window.TemperatureControlGame = { + start: startGame, + pause: pauseGame, + restart: restartGame + }; +})(); diff --git a/games/temperature-control/style.css b/games/temperature-control/style.css new file mode 100644 index 00000000..d817266b --- /dev/null +++ b/games/temperature-control/style.css @@ -0,0 +1,135 @@ +:root{ + --bg:#071126; + --card:#0f1724; + --muted:#9fb3c8; + --accent:#ff9f1c; /* warm accent */ + --accent-2:#2dd4bf; /* cool accent */ + --glass: rgba(255,255,255,0.03); + --glass-2: rgba(255,255,255,0.02); + --glow: 0 10px 30px rgba(255,159,28,0.12); + --glass-border: rgba(255,255,255,0.04); + font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial; +} + +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + background: radial-gradient(1200px 600px at 10% 10%, rgba(45,212,191,0.07), transparent 6%), + radial-gradient(900px 400px at 90% 90%, rgba(255,159,28,0.05), transparent 6%), + var(--bg); + color:#e6f0f2; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + padding:28px; +} + +/* Layout */ +.app{ + max-width:1200px; + margin:0 auto; + display:flex; + flex-direction:column; + gap:20px; +} + +/* Header */ +.app__header{ + display:flex; + justify-content:space-between; + align-items:flex-start; + gap:16px; +} +.title h1{margin:0;font-size:26px;letter-spacing:-0.6px} +.subtitle{margin:6px 0 0;color:var(--muted);font-size:13px} + +/* Buttons */ +.btn{ + background:transparent; + border:1px solid var(--glass-border); + color:inherit; + padding:8px 12px; + border-radius:8px; + cursor:pointer; + font-weight:600; + margin-left:8px; +} +.btn:hover{transform:translateY(-2px)} +.btn.primary{ + background:linear-gradient(90deg,var(--accent),#ffb470); + color:#081018; + border:none; + box-shadow: var(--glow); +} +.btn.ghost{ + background:transparent; + border:1px dashed rgba(255,255,255,0.06); +} + +/* Status */ +.status-pill{ + margin-left:14px; + background:linear-gradient(90deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01)); + padding:6px 10px; + border-radius:999px; + font-weight:600; + color:var(--muted); + border:1px solid var(--glass-border); +} + +/* Board */ +.board{ + display:grid; + grid-template-columns: 420px 1fr; + gap:20px; +} + +/* Cards */ +.glow{ + background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + padding:16px; + border-radius:12px; + border:1px solid var(--glass-border); + box-shadow: 0 8px 30px rgba(2,6,23,0.6), 0 0 24px rgba(45,212,191,0.03); +} + +.meter-card{display:flex;flex-direction:column;gap:8px;align-items:center} +.meter-title{color:var(--muted);font-size:13px} +.meter-value{font-size:44px;font-weight:800;letter-spacing:-1px} +.meter-bar{width:100%;height:12px;background:var(--glass-2);border-radius:8px;overflow:hidden;border:1px solid rgba(255,255,255,0.02)} +.meter-fill{height:100%;width:0%;background:linear-gradient(90deg,var(--accent),var(--accent-2));transition:width 300ms ease;box-shadow:0 6px 20px rgba(0,0,0,0.4) inset} + +.range-hint{font-size:13px;color:var(--muted)} + +/* sliders */ +.sliders-card{margin-top:12px;background:linear-gradient(180deg, rgba(255,255,255,0.01), transparent);padding:12px;border-radius:10px} +.slider-row{display:flex;flex-direction:column;gap:8px;margin-bottom:10px} +.slider-row label{font-size:13px;color:var(--muted);display:flex;justify-content:space-between;align-items:center} +.slider-row input[type=range]{width:100%} + +/* events */ +.events-card{margin-top:12px;padding:12px;border-radius:10px;background:linear-gradient(180deg, rgba(255,255,255,0.01), transparent)} +.events-title{font-weight:700;margin-bottom:8px} +.events-list{list-style:none;padding:0;margin:0;max-height:160px;overflow:auto} +.events-list li{font-size:13px;padding:6px;border-bottom:1px solid rgba(255,255,255,0.02);color:var(--muted)} + +/* right column */ +.graph-card{padding:12px;border-radius:12px} +canvas{width:100%;height:300px;display:block;background:linear-gradient(180deg, rgba(255,255,255,0.01), transparent);border-radius:8px} + +/* info & small cards */ +.info-cards{display:flex;gap:10px;margin-top:10px;align-items:center} +.info{background:rgba(255,255,255,0.02);padding:10px;border-radius:8px;min-width:120px} +.info.small{min-width:100px;text-align:center} +.info div:first-child{font-size:12px;color:var(--muted);margin-bottom:6px} + +/* footer */ +.app__footer{display:flex;justify-content:space-between;align-items:center;padding-top:12px;color:var(--muted)} +.play-links a{margin-left:8px} + +/* responsive */ +@media (max-width:980px){ + .board{grid-template-columns:1fr; } + .right{order:2} + .left{order:1} +} diff --git a/games/terris-clone/index.html b/games/terris-clone/index.html new file mode 100644 index 00000000..c6dd48ef --- /dev/null +++ b/games/terris-clone/index.html @@ -0,0 +1,47 @@ + + + + + + Tetris Clone + + + +
      +

      Tetris! ๐Ÿงฑ

      + +
      + + + +
      +

      Score: 0

      +

      Lines: 0

      + +
      + +

      Next Piece:

      + + +
      + +
      +

      โฌ…๏ธ โžก๏ธ: Move

      +

      โฌ‡๏ธ: Soft Drop

      +

      โฌ†๏ธ/X: Rotate

      +
      + + +
      +
      + + +
      + + + + \ No newline at end of file diff --git a/games/terris-clone/script.js b/games/terris-clone/script.js new file mode 100644 index 00000000..ba6e959b --- /dev/null +++ b/games/terris-clone/script.js @@ -0,0 +1,460 @@ +// --- Canvas Setup --- +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const nextCanvas = document.getElementById('nextPieceCanvas'); +const nextCtx = nextCanvas.getContext('2d'); + +// --- DOM Elements --- +const scoreElement = document.getElementById('score'); +const linesElement = document.getElementById('lines'); +const startButton = document.getElementById('start-button'); +const gameOverMessage = document.getElementById('game-over-message'); +const finalScoreSpan = document.getElementById('final-score'); +const restartButton = document.getElementById('restart-button'); + +// --- Game Configuration --- +const COLS = 10; +const ROWS = 20; +const BLOCK_SIZE = 30; // 300px width / 10 cols = 30px +canvas.width = COLS * BLOCK_SIZE; +canvas.height = ROWS * BLOCK_SIZE; + +// --- Tetrominoes (Shapes) --- +// Each color is tied to the block type/index. 0 is empty. +const COLORS = [ + '#000000', // 0: Empty/Black + '#00FFFF', // 1: I (Cyan) + '#0000FF', // 2: J (Blue) + '#FFA500', // 3: L (Orange) + '#FFFF00', // 4: O (Yellow) + '#008000', // 5: S (Green) + '#800080', // 6: T (Purple) + '#FF0000', // 7: Z (Red) +]; + +// Shapes represented as 4x4 matrices (using color index 1-7) +const SHAPES = [ + null, // Index 0 is null/empty + [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]], // I + [[2, 0, 0, 0], [2, 2, 2, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // J + [[0, 0, 3, 0], [3, 3, 3, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // L + [[0, 4, 4, 0], [0, 4, 4, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // O + [[0, 5, 5, 0], [5, 5, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // S + [[0, 6, 0, 0], [6, 6, 6, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // T + [[7, 7, 0, 0], [0, 7, 7, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // Z +]; + +// --- Game State Variables --- +let board; // The main 2D grid (COLS x ROWS) +let currentPiece; // The currently falling piece object +let nextPiece; // The piece to fall next +let score = 0; +let lines = 0; +let level = 1; +let dropCounter = 0; +let dropInterval = 1000; // Time in ms between automatic drops +let lastTime = 0; +let isGameOver = false; + +// --- Piece Class --- +class Piece { + constructor(shapeIndex) { + this.shapeIndex = shapeIndex; + this.matrix = SHAPES[shapeIndex]; + // Starting position: top row, center column + this.x = Math.floor(COLS / 2) - Math.floor(this.matrix[0].length / 2); + this.y = 0; + } +} + +// --- Core Game Functions --- + +/** + * Creates the initial empty game grid. + * @returns {Array>} The 2D grid array. + */ +function createBoard() { + const matrix = []; + while (matrix.length < ROWS) { + matrix.push(new Array(COLS).fill(0)); + } + return matrix; +} + +/** + * Gets a random piece (1-7) and returns a new Piece object. + * @returns {Piece} A new Tetromino piece. + */ +function getRandomPiece() { + const randIndex = Math.floor(Math.random() * (SHAPES.length - 1)) + 1; // 1 to 7 + return new Piece(randIndex); +} + +/** + * Draws a single block on the canvas. + * @param {number} x - Column index. + * @param {number} y - Row index. + * @param {number} colorIndex - Index into the COLORS array. + */ +function drawBlock(ctxToUse, x, y, colorIndex) { + if (colorIndex === 0) return; // Don't draw empty space + + const color = COLORS[colorIndex]; + ctxToUse.fillStyle = color; + ctxToUse.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); + + // Add border/highlight for depth + ctxToUse.strokeStyle = '#fff'; + ctxToUse.lineWidth = 1; + ctxToUse.strokeRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); +} + +/** + * Main drawing function to render the board and the current piece. + */ +function draw() { + // 1. Clear main canvas + ctx.fillStyle = '#111'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // 2. Draw static blocks on the board + board.forEach((row, y) => { + row.forEach((value, x) => { + drawBlock(ctx, x, y, value); + }); + }); + + // 3. Draw the falling piece + currentPiece.matrix.forEach((row, y) => { + row.forEach((value, x) => { + if (value !== 0) { + drawBlock(ctx, currentPiece.x + x, currentPiece.y + y, value); + } + }); + }); +} + +/** + * Draws the next piece in the side panel. + */ +function drawNextPiece() { + nextCtx.fillStyle = '#eee'; + nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height); + + nextPiece.matrix.forEach((row, y) => { + row.forEach((value, x) => { + if (value !== 0) { + // Center the piece in the preview box + const offsetX = (nextCanvas.width / 2) - (nextPiece.matrix[0].length / 2 * BLOCK_SIZE) + BLOCK_SIZE/2; + const offsetY = (nextCanvas.height / 2) - (nextPiece.matrix.length / 2 * BLOCK_SIZE); + + // Use a smaller block size for the preview (15px) + const PREVIEW_BLOCK_SIZE = BLOCK_SIZE / 2; + + const color = COLORS[value]; + nextCtx.fillStyle = color; + nextCtx.fillRect( + offsetX + x * PREVIEW_BLOCK_SIZE, + offsetY + y * PREVIEW_BLOCK_SIZE, + PREVIEW_BLOCK_SIZE, + PREVIEW_BLOCK_SIZE + ); + } + }); + }); +} + + +// --- Collision and Movement --- + +/** + * Checks if the current piece can be placed at its current (x, y) coordinates + * without colliding with the static board or walls. + * @param {Array>} matrix - The piece's matrix. + * @param {number} offsetX - The piece's x position. + * @param {number} offsetY - The piece's y position. + * @returns {boolean} True if a collision exists, false otherwise. + */ +function checkCollision(matrix, offsetX, offsetY) { + for (let y = 0; y < matrix.length; ++y) { + for (let x = 0; x < matrix[y].length; ++x) { + if (matrix[y][x] !== 0) { + const boardY = offsetY + y; + const boardX = offsetX + x; + + // Check if block is outside the board (top/bottom/sides) + if (boardY < 0 || boardY >= ROWS || boardX < 0 || boardX >= COLS) { + return true; + } + + // Check collision with other static blocks + if (board[boardY] && board[boardY][boardX] !== 0) { + return true; + } + } + } + } + return false; +} + +/** + * Locks the current piece into the static board when it hits the bottom or another piece. + */ +function mergePiece() { + currentPiece.matrix.forEach((row, y) => { + row.forEach((value, x) => { + if (value !== 0) { + board[currentPiece.y + y][currentPiece.x + x] = value; + } + }); + }); + + // Check for game over (if the piece merged above the top of the well) + if (currentPiece.y < 0) { + isGameOver = true; + endGame(); + return; + } + + // Set the new falling piece + currentPiece = nextPiece; + nextPiece = getRandomPiece(); + drawNextPiece(); + + // Check for an immediate collision for the new piece (Game Over) + if (checkCollision(currentPiece.matrix, currentPiece.x, currentPiece.y)) { + isGameOver = true; + endGame(); + } +} + +/** + * Moves the current piece horizontally (left or right). + * @param {number} direction - -1 for left, 1 for right. + */ +function movePiece(direction) { + if (isGameOver) return; + currentPiece.x += direction; + if (checkCollision(currentPiece.matrix, currentPiece.x, currentPiece.y)) { + // Revert move if collision occurs + currentPiece.x -= direction; + } +} + +/** + * Rotates the piece's matrix 90 degrees clockwise and handles wall kicks/collision. + */ +function rotatePiece() { + if (isGameOver) return; + const oldMatrix = currentPiece.matrix; + const p = currentPiece; + + // 1. Matrix Transposition (swap rows and columns) + for (let y = 0; y < p.matrix.length; ++y) { + for (let x = 0; x < y; ++x) { + [p.matrix[x][y], p.matrix[y][x]] = [p.matrix[y][x], p.matrix[x][y]]; + } + } + + // 2. Reverse each row (to complete the 90-degree rotation) + p.matrix.forEach(row => row.reverse()); + + // 3. Collision check and Wall Kick + // Try to nudge the piece away from a collision (basic wall kick) + let offset = 1; + while (checkCollision(p.matrix, p.x, p.y)) { + p.x += offset; + offset = -(offset + (offset > 0 ? 1 : -1)); // Try +1, -2, +3, -4... + if (offset > p.matrix[0].length) { + // Revert rotation if wall kick fails + p.matrix = oldMatrix; + p.x = p.x - (offset - 1); // Revert last successful offset + return; + } + } +} + +/** + * Handles the automatic or forced downward movement of the piece. + * @param {number} [speed=1] - Multiplier for score calculation on soft drops. + */ +function dropPiece(speed = 1) { + if (isGameOver) return; + currentPiece.y++; + + if (checkCollision(currentPiece.matrix, currentPiece.x, currentPiece.y)) { + currentPiece.y--; // Revert move + mergePiece(); + checkLines(); + } else if (speed > 1) { + // Add score for soft drop (player pushing down) + score += speed; + updateScore(); + } + dropCounter = 0; // Reset drop counter after a successful or failed drop +} + +// --- Line Clearance and Scoring --- + +/** + * Checks for and clears any completed lines, then updates the score. + */ +function checkLines() { + let linesCleared = 0; + + // Iterate from the bottom row up + outer: for (let y = ROWS - 1; y >= 0; --y) { + // Check if the row is full (no 0s) + for (let x = 0; x < COLS; ++x) { + if (board[y][x] === 0) { + continue outer; // Not a full line, move to the next row up + } + } + + // Line is full: Clear the line and shift everything above it down + const row = board.splice(y, 1)[0].fill(0); // Remove the full row + board.unshift(row); // Add a new empty row to the top + y++; // Re-check the current row index, as it now contains the row that was just above the cleared one + + linesCleared++; + } + + if (linesCleared > 0) { + lines += linesCleared; + + // Basic Tetris scoring system: 100 * level * linesCleared^2 + const points = [0, 40, 100, 300, 1200]; + score += points[linesCleared] * level; + + updateScore(); + updateLevel(); + } +} + +/** + * Updates the score and line count displays. + */ +function updateScore() { + scoreElement.textContent = score; + linesElement.textContent = lines; +} + +/** + * Updates the game level and drop speed based on lines cleared. + */ +function updateLevel() { + const newLevel = Math.floor(lines / 10) + 1; + if (newLevel > level) { + level = newLevel; + // Increase drop speed: e.g., 50ms faster per level + dropInterval = Math.max(100, 1000 - (level - 1) * 50); + console.log(`Level Up! Level: ${level}, Drop Interval: ${dropInterval}ms`); + } +} + +/** + * Game over handler. + */ +function endGame() { + cancelAnimationFrame(lastTime); + finalScoreSpan.textContent = score; + gameOverMessage.classList.remove('hidden'); + // Remove controls listeners + document.removeEventListener('keydown', handleKeyPress); +} + +// --- Game Initialization and Loop --- + +/** + * Resets the game state and starts the main loop. + */ +function startGame() { + board = createBoard(); + currentPiece = getRandomPiece(); + nextPiece = getRandomPiece(); + score = 0; + lines = 0; + level = 1; + dropInterval = 1000; + isGameOver = false; + updateScore(); + drawNextPiece(); + gameOverMessage.classList.add('hidden'); + startButton.classList.add('hidden'); // Hide start button + + // Add controls listeners + document.addEventListener('keydown', handleKeyPress); + + // Start the game loop + requestAnimationFrame(update); +} + +/** + * The main update loop for falling blocks and timing. + * @param {DOMHighResTimeStamp} time - The current time provided by rAF. + */ +function update(time = 0) { + if (isGameOver) return; + const deltaTime = time - lastTime; + lastTime = time; + + dropCounter += deltaTime; + + // Check if it's time for the automatic drop + if (dropCounter > dropInterval) { + dropPiece(); + } + + draw(); + requestAnimationFrame(update); +} + +// --- Event Handlers (Controls) --- + +function handleKeyPress(e) { + if (isGameOver) return; + + switch (e.key) { + case 'ArrowLeft': + e.preventDefault(); + movePiece(-1); + break; + case 'ArrowRight': + e.preventDefault(); + movePiece(1); + break; + case 'ArrowDown': + e.preventDefault(); + // Soft drop: move down faster and get some points + dropPiece(5); + break; + case 'ArrowUp': + case 'x': + case 'X': + e.preventDefault(); + rotatePiece(); + break; + case 'z': + case 'Z': + e.preventDefault(); + // Reverse rotation (optional, but good practice) + // For simplicity, we'll keep it as rotate for now, or you could implement a dedicated reverse rotation here. + rotatePiece(); + break; + } +} + +// --- Initial Setup and Button Listeners --- +startButton.addEventListener('click', startGame); +restartButton.addEventListener('click', startGame); + +// Draw the initial board (empty) +document.addEventListener('DOMContentLoaded', () => { + board = createBoard(); + // Use an empty piece for initial draw before game starts + currentPiece = new Piece(1); + currentPiece.matrix = [[0]]; + nextPiece = new Piece(1); + drawNextPiece(); + draw(); +}); \ No newline at end of file diff --git a/games/terris-clone/style.css b/games/terris-clone/style.css new file mode 100644 index 00000000..27ab7416 --- /dev/null +++ b/games/terris-clone/style.css @@ -0,0 +1,104 @@ +:root { + --primary-color: #8B0000; /* Dark Red */ + --accent-color: #FFD700; /* Gold */ + --bg-color: #f0f4f8; +} + +body { + font-family: 'Arial', sans-serif; + background-color: var(--bg-color); + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + user-select: none; +} + +.game-container { + background: white; + padding: 20px; + border-radius: 10px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + text-align: center; +} + +h1 { + color: var(--primary-color); + margin-bottom: 20px; +} + +.game-area { + display: flex; + gap: 20px; + align-items: flex-start; +} + +/* --- Canvas Styling --- */ + +#gameCanvas { + border: 5px solid var(--primary-color); + background-color: #111; /* Dark well background */ + box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); + border-radius: 5px; +} + +/* --- Side Panel Styling --- */ + +.side-panel { + width: 150px; + text-align: left; +} + +.side-panel h2, .side-panel h3 { + margin: 10px 0; + color: #333; +} + +#nextPieceCanvas { + border: 2px solid #ccc; + background-color: #eee; + margin-bottom: 15px; +} + +.controls-info { + font-size: 0.9em; + color: #555; + margin-bottom: 15px; +} + +/* --- Buttons and Game Over --- */ + +button { + padding: 10px 20px; + font-size: 1.1em; + cursor: pointer; + background-color: var(--accent-color); + color: var(--primary-color); + border: none; + border-radius: 5px; + font-weight: bold; + transition: background-color 0.3s; +} + +button:hover { + background-color: #FFCC00; +} + +.hidden { + display: none !important; +} + +#game-over-message { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(255, 255, 255, 0.95); + padding: 40px; + border-radius: 10px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); + z-index: 10; + text-align: center; + border: 5px solid var(--primary-color); +} \ No newline at end of file diff --git a/games/tetris/index.html b/games/tetris/index.html new file mode 100644 index 00000000..80c11a36 --- /dev/null +++ b/games/tetris/index.html @@ -0,0 +1,33 @@ + + + + + + Tetris | Mini JS Games Hub + + + +
      +
      +

      TETRIS

      +
      +

      Score: 0

      +

      Level: 1

      +

      Lines: 0

      +
      +
      +

      Next

      +
      +
      +
      +

      Hold

      +
      +
      + + +
      +
      +
      + + + diff --git a/games/tetris/script.js b/games/tetris/script.js new file mode 100644 index 00000000..4a36612e --- /dev/null +++ b/games/tetris/script.js @@ -0,0 +1,229 @@ +document.addEventListener('DOMContentLoaded', () => { + const grid = document.getElementById('tetris-grid'); + const width = 10; + const height = 20; + const cells = []; + const scoreDisplay = document.getElementById('score'); + const levelDisplay = document.getElementById('level'); + const linesDisplay = document.getElementById('lines'); + const nextGrid = document.getElementById('next-grid'); + const holdGrid = document.getElementById('hold-grid'); + const startBtn = document.getElementById('start-btn'); + const pauseBtn = document.getElementById('pause-btn'); + + let timerId; + let score = 0; + let level = 1; + let lines = 0; + let currentPosition = 4; + let currentRotation = 0; + let current; + let nextRandom = 0; + let hold = null; + let canHold = true; + let gamePaused = false; + + // create grid + for (let i = 0; i < width * height; i++) { + const cell = document.createElement('div'); + cell.classList.add('cell'); + grid.appendChild(cell); + cells.push(cell); + } + + // Tetrominoes + const lTetromino = [ + [1, width+1, width*2+1, 2], + [width, width+1, width+2, width*2+2], + [1, width+1, width*2+1, width*2], + [width, width*2, width*2+1, width*2+2] + ]; + + const zTetromino = [ + [0,width,width+1,width*2+1], + [width+1,width+2,width*2,width*2+1], + [0,width,width+1,width*2+1], + [width+1,width+2,width*2,width*2+1] + ]; + + const tTetromino = [ + [1,width,width+1,width+2], + [1,width+1,width+2,width*2+1], + [width,width+1,width+2,width*2+1], + [1,width,width+1,width*2+1] + ]; + + const oTetromino = [ + [0,1,width,width+1], + [0,1,width,width+1], + [0,1,width,width+1], + [0,1,width,width+1] + ]; + + const iTetromino = [ + [1,width+1,width*2+1,width*3+1], + [width,width+1,width+2,width+3], + [1,width+1,width*2+1,width*3+1], + [width,width+1,width+2,width+3] + ]; + + const tetrominoes = [lTetromino, zTetromino, tTetromino, oTetromino, iTetromino]; + + // random tetromino + function randomTetromino() { + const rand = Math.floor(Math.random() * tetrominoes.length); + return rand; + } + + function draw() { + current.forEach(index => { + cells[currentPosition + index].classList.add('active'); + }); + } + + function undraw() { + current.forEach(index => { + cells[currentPosition + index].classList.remove('active'); + }); + } + + function moveDown() { + if (!gamePaused) { + undraw(); + currentPosition += width; + draw(); + freeze(); + } + } + + function moveLeft() { + undraw(); + const isAtLeftEdge = current.some(index => (currentPosition + index) % width === 0); + if (!isAtLeftEdge) currentPosition -=1; + if (current.some(index => cells[currentPosition + index].classList.contains('taken'))) currentPosition +=1; + draw(); + } + + function moveRight() { + undraw(); + const isAtRightEdge = current.some(index => (currentPosition + index) % width === width-1); + if (!isAtRightEdge) currentPosition +=1; + if (current.some(index => cells[currentPosition + index].classList.contains('taken'))) currentPosition -=1; + draw(); + } + + function rotate() { + undraw(); + currentRotation++; + if (currentRotation === current.length) currentRotation = 0; + current = tetrominoes[random][currentRotation]; + draw(); + } + + function freeze() { + if (current.some(index => cells[currentPosition + index + width].classList.contains('taken'))) { + current.forEach(index => cells[currentPosition + index].classList.add('taken')); + // start new tetromino + random = nextRandom; + nextRandom = randomTetromino(); + current = tetrominoes[random][currentRotation]; + currentPosition = 4; + draw(); + displayNext(); + addScore(); + gameOver(); + canHold = true; + } + } + + function displayNext() { + nextGrid.innerHTML = ''; + for (let i=0;i<16;i++){ + const cell = document.createElement('div'); + cell.classList.add('cell'); + nextGrid.appendChild(cell); + } + const next = tetrominoes[nextRandom][0]; + next.forEach(index => nextGrid.querySelectorAll('.cell')[index].classList.add('active')); + } + + function holdTetromino() { + if (!canHold) return; + undraw(); + if (hold === null) { + hold = random; + random = nextRandom; + nextRandom = randomTetromino(); + } else { + [hold, random] = [random, hold]; + } + current = tetrominoes[random][currentRotation]; + currentPosition = 4; + draw(); + displayNext(); + canHold = false; + } + + function addScore() { + for (let i = 0; i < 199; i += width) { + const row = Array.from({length: width}, (_, k) => i + k); + if (row.every(index => cells[index].classList.contains('taken'))) { + score += 10; + lines += 1; + scoreDisplay.textContent = score; + linesDisplay.textContent = lines; + row.forEach(index => { + cells[index].classList.remove('taken'); + cells[index].classList.remove('active'); + }); + const removed = cells.splice(i, width); + cells.unshift(...removed); + cells.forEach(cell => grid.appendChild(cell)); + } + } + if (lines % 10 === 0 && lines !== 0) { + level += 1; + levelDisplay.textContent = level; + clearInterval(timerId); + timerId = setInterval(moveDown, 1000 - (level*100)); + } + } + + function gameOver() { + if (current.some(index => cells[currentPosition + index].classList.contains('taken'))) { + clearInterval(timerId); + alert("Game Over! Score: " + score); + } + } + + // Controls + document.addEventListener('keydown', e => { + if (!gamePaused) { + if (e.key === 'ArrowLeft') moveLeft(); + if (e.key === 'ArrowRight') moveRight(); + if (e.key === 'ArrowDown') moveDown(); + if (e.key === 'ArrowUp' || e.key === ' ') rotate(); + if (e.key.toLowerCase() === 'c') holdTetromino(); + } + }); + + startBtn.addEventListener('click', () => { + if (timerId) clearInterval(timerId); + random = randomTetromino(); + nextRandom = randomTetromino(); + current = tetrominoes[random][currentRotation]; + draw(); + displayNext(); + timerId = setInterval(moveDown, 1000); + score = 0; lines = 0; level = 1; + scoreDisplay.textContent = score; + linesDisplay.textContent = lines; + levelDisplay.textContent = level; + }); + + pauseBtn.addEventListener('click', () => { + gamePaused = !gamePaused; + if (!gamePaused) timerId = setInterval(moveDown, 1000 - (level*100)); + else clearInterval(timerId); + }); +}); diff --git a/games/tetris/style.css b/games/tetris/style.css new file mode 100644 index 00000000..eff53c1e --- /dev/null +++ b/games/tetris/style.css @@ -0,0 +1,83 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: linear-gradient(to bottom, #1e1e2f, #11111f); + color: #fff; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; +} + +.tetris-container { + display: flex; + gap: 20px; +} + +.game-info { + display: flex; + flex-direction: column; + align-items: center; + width: 200px; +} + +.game-info h1 { + font-size: 2em; + margin-bottom: 20px; +} + +.score-board p { + margin: 5px 0; +} + +.mini-grid { + width: 80px; + height: 80px; + display: grid; + grid-template-columns: repeat(4, 20px); + grid-template-rows: repeat(4, 20px); + gap: 2px; + background-color: #222; + margin-bottom: 10px; + border-radius: 5px; +} + +button { + margin-top: 10px; + padding: 6px 12px; + font-size: 14px; + cursor: pointer; + border: none; + border-radius: 5px; + background-color: #ff6600; + color: #fff; + transition: 0.2s; +} + +button:hover { + background-color: #ff4500; +} + +.tetris-grid { + width: 200px; + height: 400px; + display: grid; + grid-template-columns: repeat(10, 20px); + grid-template-rows: repeat(20, 20px); + gap: 1px; + background-color: #111; + border: 2px solid #555; + border-radius: 5px; +} + +.cell { + width: 20px; + height: 20px; + background-color: #222; + border-radius: 2px; + transition: background 0.1s; +} + +.cell.active { + background-color: #ff6600; +} diff --git a/games/the-dodge-zone/index.html b/games/the-dodge-zone/index.html new file mode 100644 index 00000000..4c931b6a --- /dev/null +++ b/games/the-dodge-zone/index.html @@ -0,0 +1,20 @@ + + + + + + Dodge the Blocks + + + +

      Dodge the Blocks

      +
      +
      +
      +
      +
      Score: 0
      +
      Time: 0:00
      +
      + + + diff --git a/games/the-dodge-zone/script.js b/games/the-dodge-zone/script.js new file mode 100644 index 00000000..0c01c896 --- /dev/null +++ b/games/the-dodge-zone/script.js @@ -0,0 +1,187 @@ +const gameContainer = document.getElementById('gameContainer'); +const player = document.getElementById('player'); +const scoreDisplay = document.getElementById('score'); +const message = document.getElementById('message'); +const timerDisplay = document.getElementById('timer'); + +let playerX = 180; +const playerSpeed = 30; // increased for snappier keyboard movement +const gameWidth = 400; +const gameHeight = 500; +const groundHeight = 6; // must match CSS #ground height +let score = 0; +let blocks = []; +let gameRunning = true; +let blockSpeed = 3; + +// Timer +let elapsedSeconds = 0; +let timerInterval = null; + +function formatTime(s) { + const m = Math.floor(s / 60); + const sec = s % 60; + return `${m}:${sec.toString().padStart(2, '0')}`; +} + +function startTimer() { + elapsedSeconds = 0; + if (timerDisplay) timerDisplay.textContent = 'Time: ' + formatTime(elapsedSeconds); + timerInterval = setInterval(() => { + elapsedSeconds++; + if (timerDisplay) timerDisplay.textContent = 'Time: ' + formatTime(elapsedSeconds); + }, 1000); +} + +function stopTimer() { + if (timerInterval) { + clearInterval(timerInterval); + timerInterval = null; + } +} + +// Helper to update player position and clamp inside game container +function updatePlayerPosition(x) { + // clamp between 0 and (gameWidth - player width) + const maxX = gameWidth - 40; // player width = 40 + playerX = Math.max(0, Math.min(maxX, Math.round(x))); + player.style.left = playerX + 'px'; +} + +// Player movement +document.addEventListener('keydown', (e) => { + if (!gameRunning) return; + + if (e.key === 'ArrowLeft' || e.key === 'a') { + playerX = Math.max(0, playerX - playerSpeed); + } else if (e.key === 'ArrowRight' || e.key === 'd') { + playerX = Math.min(gameWidth - 40, playerX + playerSpeed); + } + player.style.left = playerX + 'px'; +}); + +// Pointer (mouse/touch) control: move the player horizontally with pointer +gameContainer.addEventListener('pointermove', (e) => { + if (!gameRunning) return; + // get x relative to game container + const rect = gameContainer.getBoundingClientRect(); + const relativeX = e.clientX - rect.left - 20; // center the 40px player + updatePlayerPosition(relativeX); +}); + +// Also allow direct clicks/taps to move player to that position +gameContainer.addEventListener('pointerdown', (e) => { + if (!gameRunning) return; + const rect = gameContainer.getBoundingClientRect(); + const relativeX = e.clientX - rect.left - 20; + updatePlayerPosition(relativeX); +}); + +// Create blocks +function createBlock() { + const block = document.createElement('div'); + block.classList.add('block'); + block.style.left = Math.floor(Math.random() * (gameWidth - 50)) + 'px'; + block.style.top = '-60px'; + gameContainer.appendChild(block); + blocks.push(block); +} + +// Move blocks +function moveBlocks() { + if (!gameRunning) return; + + blocks.forEach((block, index) => { + const currentTop = parseInt(block.style.top); + block.style.top = currentTop + blockSpeed + 'px'; + + // Collision detection + const playerRect = player.getBoundingClientRect(); + const blockRect = block.getBoundingClientRect(); + if ( + playerRect.left < blockRect.right && + playerRect.right > blockRect.left && + playerRect.top < blockRect.bottom && + playerRect.bottom > blockRect.top + ) { + gameOver(); + } + + // Remove off-screen blocks before they cover the ground (account for block height and ground) + // block height is 50, so remove when top > (gameHeight - blockHeight - groundHeight) + if (currentTop > gameHeight - 50 - groundHeight) { + block.remove(); + blocks.splice(index, 1); + score++; + scoreDisplay.textContent = 'Score: ' + score; + } + }); + + requestAnimationFrame(moveBlocks); +} + +// Increase speed gradually +setInterval(() => { + if (gameRunning) { + blockSpeed += 0.3; + createBlock(); + } +}, 1000); + +// Level up message +setInterval(() => { + if (gameRunning) { + message.textContent = 'โšก Level Up!'; + setTimeout(() => (message.textContent = ''), 1000); + } +}, 10000); + +// Game over +function gameOver() { + gameRunning = false; + stopTimer(); + message.textContent = '๐Ÿ’ฅ Game Over! Final Score: ' + score + ' | Time: ' + formatTime(elapsedSeconds); + blocks.forEach((block) => block.remove()); + blocks = []; + + setTimeout(() => { + message.textContent = 'Press Enter to Restart'; + }, 2000); +} + +// Restart the game without reloading the page +function restartGame() { + // clear any remaining blocks + blocks.forEach((b) => b.remove()); + blocks = []; + + // reset values + score = 0; + scoreDisplay.textContent = 'Score: ' + score; + blockSpeed = 3; + elapsedSeconds = 0; + if (timerDisplay) timerDisplay.textContent = 'Time: ' + formatTime(elapsedSeconds); + + // reset player + playerX = 180; + updatePlayerPosition(playerX); + + // clear message and restart loops + message.textContent = ''; + gameRunning = true; + createBlock(); + startTimer(); + moveBlocks(); +} + +// Allow pressing Enter to restart after game over +document.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !gameRunning) { + restartGame(); + } +}); + +// Start +createBlock(); +moveBlocks(); +startTimer(); diff --git a/games/the-dodge-zone/style.css b/games/the-dodge-zone/style.css new file mode 100644 index 00000000..32fa2f5f --- /dev/null +++ b/games/the-dodge-zone/style.css @@ -0,0 +1,93 @@ +body { + margin: 0; + background: linear-gradient(135deg, #1e1e2f, #2e2e4f); + color: white; + font-family: 'Poppins', sans-serif; + text-align: center; + overflow: hidden; +} + +h1 { + margin-top: 10px; + font-size: 32px; + color: #ffce00; + letter-spacing: 1px; +} + +#gameContainer { + position: relative; + margin: 30px auto; + width: 400px; + height: 500px; + background: rgba(255, 255, 255, 0.1); + border: 3px solid #fff; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 0 20px rgba(255, 255, 255, 0.2); +} + +#player { + position: absolute; + /* sit slightly above the ground line */ + bottom: 18px; + left: 180px; + width: 40px; + height: 40px; + background: #00e6ff; + border-radius: 5px; + /* remove transition so player moves immediately (no lag) */ + transition: none; + z-index: 20; /* stay above ground and blocks */ +} + +/* visible ground line at the bottom of the game area + Raised a bit so the container border and rounding don't cover it. */ +#ground { + position: absolute; + left: 8px; /* inset to avoid rounded corners clipping */ + right: 8px; + bottom: 3px; /* lift above the inner edge of the container border */ + height: 6px; + background: rgba(255,255,255,0.36); + box-shadow: 0 -1px 6px rgba(0,0,0,0.25) inset; + z-index: 5; /* put ground above falling blocks */ + pointer-events: none; /* allow clicks to pass through */ + border-radius: 4px; +} + +.block { + position: absolute; + top: 0; + width: 50px; + height: 50px; + background: #ff4d4d; + border-radius: 6px; + z-index: 2; /* behind ground and player */ +} + +#score { + font-size: 20px; + margin-top: -10px; +} + +/* timer shown inside the game container at top-right */ +/* timer fixed to the page upper-right */ +#timer { + position: fixed; + top: 12px; + right: 16px; + font-size: 16px; + color: #bfefff; + background: rgba(0,0,0,0.18); + padding: 6px 10px; + border-radius: 8px; + z-index: 9999; + pointer-events: none; +} + +#message { + margin-top: 10px; + font-size: 22px; + font-weight: bold; + color: #ffce00; +} diff --git a/games/the-floor-is-lava/index.html b/games/the-floor-is-lava/index.html new file mode 100644 index 00000000..286abd21 --- /dev/null +++ b/games/the-floor-is-lava/index.html @@ -0,0 +1,29 @@ + + + + + + The Floor is Lava + + + +
      +

      The Floor is Lava

      +

      Click an adjacent safe tile to move. Survive as long as you can!

      +
      + +
      +
      +
      Score: 0
      +
      Best: 0
      +
      + + +
      + + + \ No newline at end of file diff --git a/games/the-floor-is-lava/script.js b/games/the-floor-is-lava/script.js new file mode 100644 index 00000000..41e8d672 --- /dev/null +++ b/games/the-floor-is-lava/script.js @@ -0,0 +1,208 @@ +// --- DOM Elements --- +const gameBoard = document.getElementById('game-board'); +const currentScoreEl = document.getElementById('current-score'); +const highScoreEl = document.getElementById('high-score'); +const gameOverOverlay = document.getElementById('game-over-overlay'); +const finalScoreEl = document.getElementById('final-score'); +const restartBtn = document.getElementById('restart-btn'); + +// --- Game Constants & State --- +const GRID_SIZE = 8; +const LAVA_COOLDOWN = 5000; +const MAX_NEW_WARNINGS = 5; // REBALANCE: Cap the number of new warnings +let grid = []; +let playerPosition = { row: 0, col: 0 }; +let score = 0, highScore = 0; +let gameInterval; +let gameSpeed = 1000; +let gameState = 'playing'; + +// --- Sound Effects --- +const sounds = { move: new Audio(''), warn: new Audio(''), sizzle: new Audio(''), gameover: new Audio('') }; +function playSound(sound) { try { sounds[sound].currentTime = 0; sounds[sound].play(); } catch (e) {} } + +// --- Game Setup --- +function createGrid() { + gameBoard.innerHTML = ''; grid = []; + for (let r = 0; r < GRID_SIZE; r++) { + const row = []; + for (let c = 0; c < GRID_SIZE; c++) { + const cell = document.createElement('div'); + cell.classList.add('grid-cell'); + cell.dataset.row = r; cell.dataset.col = c; + cell.addEventListener('click', handleCellClick); + gameBoard.appendChild(cell); + row.push({ state: 'safe', element: cell, lavaTimestamp: 0 }); + } + grid.push(row); + } +} + +function startGame() { + gameState = 'playing'; + score = 0; gameSpeed = 1000; + gameOverOverlay.classList.add('hidden'); + currentScoreEl.textContent = score; + + for (let r = 0; r < GRID_SIZE; r++) { + for (let c = 0; c < GRID_SIZE; c++) { + grid[r][c].state = 'safe'; + grid[r][c].lavaTimestamp = 0; + } + } + + playerPosition = { row: Math.floor(GRID_SIZE / 2), col: Math.floor(GRID_SIZE / 2) }; + render(); + if (gameInterval) clearInterval(gameInterval); + gameInterval = setInterval(gameTick, gameSpeed); +} + +// --- Main Game Tick --- +function gameTick() { + score++; + currentScoreEl.textContent = score; + + updateLavaStates(); + + if (checkGameOver()) { + gameOver(); + return; + } + + generateNewWarnings(); + render(); + adjustGameSpeed(); +} + +// --- REFACTORED: Game Logic Functions --- +function updateLavaStates() { + const now = Date.now(); + for (let r = 0; r < GRID_SIZE; r++) { + for (let c = 0; c < GRID_SIZE; c++) { + const cell = grid[r][c]; + if (cell.state === 'lava' && now - cell.lavaTimestamp > LAVA_COOLDOWN) { + cell.state = 'safe'; + } + if (cell.state === 'warning') { + cell.state = 'lava'; + cell.lavaTimestamp = now; + playSound('sizzle'); + } + } + } +} + +function checkGameOver() { + const playerIsOnLava = grid[playerPosition.row][playerPosition.col].state === 'lava'; + const playerIsTrapped = !hasValidMoves(playerPosition); + return playerIsOnLava || playerIsTrapped; +} + +function generateNewWarnings() { + // ENHANCEMENT: Player's tile is no longer safe from warnings. + const safeTiles = []; + for (let r = 0; r < GRID_SIZE; r++) { + for (let c = 0; c < GRID_SIZE; c++) { + if (grid[r][c].state === 'safe') { + safeTiles.push({ r, c }); + } + } + } + safeTiles.sort(() => Math.random() - 0.5); + + // REBALANCE: Apply the cap to the number of new warnings. + const baseWarnings = 1 + Math.floor(score / 10); + const numNewWarnings = Math.min(safeTiles.length, baseWarnings, MAX_NEW_WARNINGS); + + if (numNewWarnings > 0) playSound('warn'); + for (let i = 0; i < numNewWarnings; i++) { + const tileToWarn = safeTiles[i]; + grid[tileToWarn.r][tileToWarn.c].state = 'warning'; + } +} + +function adjustGameSpeed() { + if (score % 10 === 0 && gameSpeed > 300) { + gameSpeed -= 50; + clearInterval(gameInterval); + gameInterval = setInterval(gameTick, gameSpeed); + } +} + +function handleCellClick(event) { + if (gameState !== 'playing') return; + const row = parseInt(event.target.dataset.row); + const col = parseInt(event.target.dataset.col); + if (isValidMove(row, col)) { + playerPosition = { row, col }; + playSound('move'); + render(); + } +} + +// --- Helper & UI Functions --- +function isValidMove(row, col) { + const isAdjacent = Math.abs(row - playerPosition.row) <= 1 && Math.abs(col - playerPosition.col) <= 1; + const isSelf = row === playerPosition.row && col === playerPosition.col; + return !isSelf && isAdjacent && grid[row][col].state === 'safe'; +} + +function hasValidMoves(position) { + for (let r = -1; r <= 1; r++) { + for (let c = -1; c <= 1; c++) { + if (r === 0 && c === 0) continue; + const newRow = position.row + r; + const newCol = position.col + c; + if (newRow >= 0 && newRow < GRID_SIZE && newCol >= 0 && newCol < GRID_SIZE) { + if (grid[newRow][newCol].state === 'safe') return true; + } + } + } + return false; +} + +function render() { + const now = Date.now(); + for (let r = 0; r < GRID_SIZE; r++) { + for (let c = 0; c < GRID_SIZE; c++) { + const cell = grid[r][c]; + cell.element.className = 'grid-cell'; + cell.element.classList.add(cell.state); + if (gameState === 'playing' && isValidMove(r, c)) { + cell.element.classList.add('movable'); + } + if (cell.state === 'lava' && now - cell.lavaTimestamp > LAVA_COOLDOWN - 2000) { + cell.element.classList.add('cooling'); + } + } + } + if (gameState === 'playing') { + grid[playerPosition.row][playerPosition.col].element.classList.add('player'); + } +} + +function gameOver() { + gameState = 'game_over'; + clearInterval(gameInterval); + playSound('gameover'); + + const playerCell = grid[playerPosition.row][playerPosition.col].element; + playerCell.classList.add('game-over'); + + if (score > highScore) { highScore = score; saveGame(); highScoreEl.textContent = highScore; } + finalScoreEl.textContent = score; + render(); + setTimeout(() => { gameOverOverlay.classList.remove('hidden'); }, 500); +} + +// --- Save/Load & Initialization --- +function saveGame() { localStorage.setItem('floorIsLava_highScore', highScore); } +function loadGame() { + highScore = parseInt(localStorage.getItem('floorIsLava_highScore')) || 0; + highScoreEl.textContent = highScore; +} + +restartBtn.addEventListener('click', startGame); +createGrid(); +loadGame(); +startGame(); \ No newline at end of file diff --git a/games/the-floor-is-lava/style.css b/games/the-floor-is-lava/style.css new file mode 100644 index 00000000..ed397069 --- /dev/null +++ b/games/the-floor-is-lava/style.css @@ -0,0 +1,79 @@ +/* --- Core Responsive Layout --- */ +html, body { height: 100%; margin: 0; overflow: hidden; } +body { + font-family: 'Segoe UI', sans-serif; + background-color: #111; + color: #f1f1f1; + display: flex; + align-items: center; + justify-content: center; + text-align: center; +} +#main-content { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + max-width: 100%; + max-height: 100%; +} + +/* --- UI Elements --- */ +h1 { font-size: 2.5em; margin-bottom: 10px; color: #ff6b6b; } +p { font-size: 1.2em; color: #aaa; margin-top: 0; } +#score-display { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 600px; + margin-top: 15px; + font-size: 1.5em; + font-weight: bold; +} +#score-display div:last-child { color: #f1c40f; } + +/* --- Game Board --- */ +#game-board { + display: grid; + grid-template-columns: repeat(8, 1fr); + gap: 5px; + width: clamp(320px, 80vmin, 600px); + height: clamp(320px, 80vmin, 600px); + background-color: #000; + border: 5px solid #444; + padding: 5px; +} +.grid-cell { + background-color: #333; + border-radius: 5px; + transition: background-color 0.2s ease, transform 0.1s ease; + display: flex; + align-items: center; + justify-content: center; + font-size: clamp(1rem, 5vmin, 2rem); + position: relative; +} +.grid-cell:not(.movable) { cursor: not-allowed; } + +/* --- Tile States & Animations --- */ +.grid-cell.player::after { content: '๐Ÿ™‚'; position: absolute; animation: hop-in 0.3s ease-out; } +.grid-cell.player.game-over::after { animation: sink 0.5s ease-in forwards; } +.grid-cell.safe { background-color: #333; } +.grid-cell.movable { background-color: #4a4a4a; cursor: pointer; } +.grid-cell.movable:hover { background-color: #555; transform: scale(1.05); box-shadow: 0 0 10px #3498db; } +.grid-cell.warning { background-color: #e74c3c; animation: pulse-warning 1s infinite; } +.grid-cell.lava { background-color: #f39c12; } +.grid-cell.cooling { animation: pulse-cooling 1s infinite; } + +@keyframes hop-in { from { transform: scale(0.5); opacity: 0; } to { transform: scale(1); opacity: 1; } } +@keyframes sink { from { transform: scale(1); opacity: 1; } to { transform: scale(0); opacity: 0; } } +@keyframes pulse-warning { 50% { opacity: 0.6; } } +@keyframes pulse-cooling { 50% { background-color: #b1700c; } } + +/* --- Game Over Overlay --- */ +#game-over-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.85); display: flex; flex-direction: column; align-items: center; justify-content: center; color: #fff; font-size: 2em; animation: fade-in 0.5s; z-index: 20;} +@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } +#game-over-overlay p { font-size: 0.8em; } +#restart-btn { margin-top: 20px; padding: 15px 30px; font-size: 0.8em; border: none; border-radius: 10px; background-color: #ff6b6b; color: white; cursor: pointer; transition: background-color 0.2s; } +#restart-btn:hover { background-color: #e74c3c; } +.hidden { display: none !important; } \ No newline at end of file diff --git a/games/tile-memory-flip/index.html b/games/tile-memory-flip/index.html new file mode 100644 index 00000000..2f6bdcf5 --- /dev/null +++ b/games/tile-memory-flip/index.html @@ -0,0 +1,33 @@ + + + + + + Tile Memory Flip | Mini JS Games Hub + + + +
      +

      Tile Memory Flip

      +

      Moves: 0

      +

      Time: 0s

      + +
      + +
      + +
      + + + +
      + + + + + +
      + + + + diff --git a/games/tile-memory-flip/script.js b/games/tile-memory-flip/script.js new file mode 100644 index 00000000..874483e4 --- /dev/null +++ b/games/tile-memory-flip/script.js @@ -0,0 +1,128 @@ +const tileLine = document.getElementById("tile-line"); +const movesEl = document.getElementById("moves"); +const timeEl = document.getElementById("time"); + +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const restartBtn = document.getElementById("restart-btn"); + +const flipSound = document.getElementById("flip-sound"); +const matchSound = document.getElementById("match-sound"); +const winSound = document.getElementById("win-sound"); +const wrongSound = document.getElementById("wrong-sound"); + +let tiles = []; +let firstTile = null; +let secondTile = null; +let moves = 0; +let time = 0; +let timerInterval = null; +let isPaused = false; + +// Online images (emoji icons) +const icons = ["๐ŸŸฆ","๐ŸŸฉ","๐ŸŸฅ","๐ŸŸจ","๐ŸŸช","๐ŸŸง","๐ŸŸซ","โฌ›","โฌœ","๐Ÿ”ต","๐Ÿ”ด","๐ŸŸข"]; +let gameTiles = []; + +// Initialize game +function initGame() { + tileLine.innerHTML = ""; + moves = 0; + time = 0; + movesEl.textContent = moves; + timeEl.textContent = time; + firstTile = null; + secondTile = null; + isPaused = false; + clearInterval(timerInterval); + + // Prepare pairs + gameTiles = [...icons, ...icons].slice(0, 12); // 12 pairs = 24 tiles + shuffle(gameTiles); + + gameTiles.forEach(icon => { + const tile = document.createElement("div"); + tile.classList.add("tile"); + tile.innerHTML = `
      ?
      ${icon}
      `; + tile.addEventListener("click", flipTile); + tileLine.appendChild(tile); + }); +} + +function flipTile(e) { + if (isPaused) return; + const tile = e.currentTarget; + if (tile.classList.contains("flipped")) return; + + tile.classList.add("flipped"); + flipSound.play(); + + if (!firstTile) { + firstTile = tile; + } else { + secondTile = tile; + moves++; + movesEl.textContent = moves; + + const firstIcon = firstTile.querySelector(".back").textContent; + const secondIcon = secondTile.querySelector(".back").textContent; + + if (firstIcon === secondIcon) { + matchSound.play(); + firstTile = null; + secondTile = null; + checkWin(); + } else { + wrongSound.play(); + setTimeout(() => { + firstTile.classList.remove("flipped"); + secondTile.classList.remove("flipped"); + firstTile = null; + secondTile = null; + }, 800); + } + } +} + +function checkWin() { + const allFlipped = [...document.querySelectorAll(".tile")].every(t => t.classList.contains("flipped")); + if (allFlipped) { + clearInterval(timerInterval); + winSound.play(); + alert(`๐ŸŽ‰ You won in ${moves} moves and ${time} seconds!`); + } +} + +function shuffle(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } +} + +function startTimer() { + clearInterval(timerInterval); + timerInterval = setInterval(() => { + if (!isPaused) { + time++; + timeEl.textContent = time; + } + }, 1000); +} + +// Buttons +startBtn.addEventListener("click", () => { + isPaused = false; + startTimer(); +}); + +pauseBtn.addEventListener("click", () => { + isPaused = true; +}); + +restartBtn.addEventListener("click", () => { + initGame(); + startTimer(); +}); + +// Initialize at load +initGame(); diff --git a/games/tile-memory-flip/style.css b/games/tile-memory-flip/style.css new file mode 100644 index 00000000..3fd8add8 --- /dev/null +++ b/games/tile-memory-flip/style.css @@ -0,0 +1,94 @@ +body { + font-family: 'Arial', sans-serif; + background: #111; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; +} + +.game-container { + text-align: center; + padding: 20px; + width: 95%; + max-width: 1000px; +} + +h1 { + font-size: 2.2rem; + margin-bottom: 10px; + color: #fffa; +} + +.tile-line { + display: flex; + justify-content: center; + gap: 15px; + margin: 20px 0; + overflow-x: auto; + padding-bottom: 10px; +} + +.tile { + width: 80px; + height: 120px; + background: #222; + border-radius: 12px; + cursor: pointer; + perspective: 1000px; + position: relative; + box-shadow: 0 0 10px #0ff; + transition: transform 0.5s; +} + +.tile.flipped { + transform: rotateY(180deg); +} + +.tile .front, .tile .back { + width: 100%; + height: 100%; + border-radius: 12px; + position: absolute; + backface-visibility: hidden; + display: flex; + justify-content: center; + align-items: center; + font-size: 2rem; + font-weight: bold; +} + +.tile .front { + background: #0ff3; + color: #000; +} + +.tile .back { + background: #222; + color: #0ff; + transform: rotateY(180deg); +} + +.controls button { + padding: 8px 15px; + margin: 5px; + font-size: 16px; + border-radius: 6px; + border: none; + cursor: pointer; + background: linear-gradient(45deg, #0ff, #0f0); + color: #000; + font-weight: bold; + box-shadow: 0 0 10px #0ff; + transition: 0.3s; +} + +.controls button:hover { + box-shadow: 0 0 20px #0ff, 0 0 40px #0f0; +} + +.score, .timer { + font-size: 1.2rem; + margin: 5px; +} diff --git a/games/tile-slide/index.html b/games/tile-slide/index.html new file mode 100644 index 00000000..cce629f5 --- /dev/null +++ b/games/tile-slide/index.html @@ -0,0 +1,22 @@ + + + + + + Tile Slide Game + + + +
      +

      Tile Slide

      +

      Arrange the tiles in order from 1 to 15. Click a tile to move it into the empty space.

      +
      +
      Time: 30
      + +
      + +
      +
      + + + \ No newline at end of file diff --git a/games/tile-slide/script.js b/games/tile-slide/script.js new file mode 100644 index 00000000..ae0b0de2 --- /dev/null +++ b/games/tile-slide/script.js @@ -0,0 +1,136 @@ +// Tile Slide Game Script +// Simple sliding puzzle game + +var canvas = document.getElementById('gameCanvas'); +var ctx = canvas.getContext('2d'); +var timerDisplay = document.getElementById('timer'); +var restartBtn = document.getElementById('restart'); +var messageDiv = document.getElementById('message'); + +var gridSize = 4; +var tileSize = canvas.width / gridSize; +var tiles = []; +var emptyPos = { x: 3, y: 3 }; +var timeLeft = 30; +var timerInterval; +var gameWon = false; + +// Initialize the game +function initGame() { + tiles = []; + for (var i = 0; i < gridSize * gridSize - 1; i++) { + tiles.push(i + 1); + } + tiles.push(0); // 0 represents empty + emptyPos = { x: 3, y: 3 }; + shuffleTiles(); + timeLeft = 30; + gameWon = false; + messageDiv.textContent = ''; + startTimer(); + drawBoard(); +} + +// Shuffle the tiles +function shuffleTiles() { + for (var i = tiles.length - 1; i > 0; i--) { + var j = Math.floor(Math.random() * (i + 1)); + var temp = tiles[i]; + tiles[i] = tiles[j]; + tiles[j] = temp; + } + // Find empty position after shuffle + for (var y = 0; y < gridSize; y++) { + for (var x = 0; x < gridSize; x++) { + if (tiles[y * gridSize + x] === 0) { + emptyPos = { x: x, y: y }; + } + } + } +} + +// Draw the board +function drawBoard() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + for (var y = 0; y < gridSize; y++) { + for (var x = 0; x < gridSize; x++) { + var tile = tiles[y * gridSize + x]; + if (tile !== 0) { + ctx.fillStyle = '#4CAF50'; + ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize); + ctx.strokeStyle = '#333'; + ctx.strokeRect(x * tileSize, y * tileSize, tileSize, tileSize); + ctx.fillStyle = '#fff'; + ctx.font = '24px Arial'; + ctx.textAlign = 'center'; + ctx.fillText(tile, x * tileSize + tileSize / 2, y * tileSize + tileSize / 2 + 8); + } else { + ctx.fillStyle = '#fff'; + ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize); + ctx.strokeStyle = '#333'; + ctx.strokeRect(x * tileSize, y * tileSize, tileSize, tileSize); + } + } + } +} + +// Check if the game is won +function checkWin() { + for (var i = 0; i < tiles.length - 1; i++) { + if (tiles[i] !== i + 1) { + return false; + } + } + return tiles[tiles.length - 1] === 0; +} + +// Move tile if possible +function moveTile(x, y) { + if (gameWon) return; + var dx = Math.abs(x - emptyPos.x); + var dy = Math.abs(y - emptyPos.y); + if ((dx === 1 && dy === 0) || (dx === 0 && dy === 1)) { + var index = y * gridSize + x; + var emptyIndex = emptyPos.y * gridSize + emptyPos.x; + tiles[emptyIndex] = tiles[index]; + tiles[index] = 0; + emptyPos = { x: x, y: y }; + drawBoard(); + if (checkWin()) { + gameWon = true; + clearInterval(timerInterval); + messageDiv.textContent = 'Congratulations! You won!'; + messageDiv.style.color = 'green'; + } + } +} + +// Handle canvas click +canvas.addEventListener('click', function(event) { + var rect = canvas.getBoundingClientRect(); + var x = Math.floor((event.clientX - rect.left) / tileSize); + var y = Math.floor((event.clientY - rect.top) / tileSize); + moveTile(x, y); +}); + +// Start the timer +function startTimer() { + clearInterval(timerInterval); + timerInterval = setInterval(function() { + timeLeft--; + timerDisplay.textContent = 'Time: ' + timeLeft; + if (timeLeft <= 0) { + clearInterval(timerInterval); + if (!gameWon) { + messageDiv.textContent = 'Time\'s up! Game over.'; + messageDiv.style.color = 'red'; + } + } + }, 1000); +} + +// Restart button +restartBtn.addEventListener('click', initGame); + +// Start the game +initGame(); \ No newline at end of file diff --git a/games/tile-slide/style.css b/games/tile-slide/style.css new file mode 100644 index 00000000..23cd3653 --- /dev/null +++ b/games/tile-slide/style.css @@ -0,0 +1,50 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #f0f0f0; +} + +.container { + text-align: center; +} + +h1 { + color: #333; +} + +.game-info { + margin-bottom: 20px; +} + +#timer { + font-size: 24px; + margin-bottom: 10px; +} + +#restart { + padding: 10px 20px; + font-size: 16px; + background-color: #4CAF50; + color: white; + border: none; + cursor: pointer; +} + +#restart:hover { + background-color: #45a049; +} + +canvas { + border: 2px solid #333; + background-color: #fff; +} + +#message { + margin-top: 20px; + font-size: 18px; + font-weight: bold; +} \ No newline at end of file diff --git a/games/tileman/index.html b/games/tileman/index.html new file mode 100644 index 00000000..ad813dc5 --- /dev/null +++ b/games/tileman/index.html @@ -0,0 +1,30 @@ + + + + + + TileMan.io | Mini JS Games Hub + + + +
      +
      +

      ๐ŸŸฉ TileMan.io

      +

      Claim as many tiles as possible โ€” but avoid your opponents!

      +
      + Score: 0 + Tiles Claimed: 0 +
      + +
      + + + +
      +

      Use WASD or Arrow Keys to move

      +
      +
      + + + + diff --git a/games/tileman/script.js b/games/tileman/script.js new file mode 100644 index 00000000..65b6434b --- /dev/null +++ b/games/tileman/script.js @@ -0,0 +1,139 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); +const scoreEl = document.getElementById("score"); +const tilesEl = document.getElementById("tiles"); +const restartBtn = document.getElementById("restartBtn"); + +const gridSize = 20; +const rows = canvas.height / gridSize; +const cols = canvas.width / gridSize; + +let player, enemies, grid, score; + +function initGame() { + grid = Array(rows) + .fill() + .map(() => Array(cols).fill(null)); + + player = { x: 10, y: 10, color: "#4ade80", direction: "RIGHT" }; + enemies = [ + { x: 20, y: 15, color: "#f87171", direction: "LEFT" }, + { x: 5, y: 25, color: "#60a5fa", direction: "DOWN" }, + ]; + + score = 0; + updateStats(); +} + +function updateStats() { + scoreEl.textContent = score; + tilesEl.textContent = countClaimedTiles(); +} + +function countClaimedTiles() { + return grid.flat().filter((cell) => cell === player.color).length; +} + +function drawGrid() { + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + ctx.fillStyle = grid[y][x] || "#1f2937"; + ctx.fillRect(x * gridSize, y * gridSize, gridSize - 1, gridSize - 1); + } + } +} + +function moveEntity(entity) { + switch (entity.direction) { + case "UP": + entity.y--; + break; + case "DOWN": + entity.y++; + break; + case "LEFT": + entity.x--; + break; + case "RIGHT": + entity.x++; + break; + } + + // Wrap around edges + if (entity.x < 0) entity.x = cols - 1; + if (entity.y < 0) entity.y = rows - 1; + if (entity.x >= cols) entity.x = 0; + if (entity.y >= rows) entity.y = 0; +} + +function handleInput(e) { + const key = e.key.toLowerCase(); + if (key === "w" || e.key === "ArrowUp") player.direction = "UP"; + else if (key === "s" || e.key === "ArrowDown") player.direction = "DOWN"; + else if (key === "a" || e.key === "ArrowLeft") player.direction = "LEFT"; + else if (key === "d" || e.key === "ArrowRight") player.direction = "RIGHT"; +} + +document.addEventListener("keydown", handleInput); + +function update() { + moveEntity(player); + grid[player.y][player.x] = player.color; + score++; + + enemies.forEach((enemy) => { + moveEntity(enemy); + grid[enemy.y][enemy.x] = enemy.color; + // Collision detection + if (enemy.x === player.x && enemy.y === player.y) { + gameOver(); + } + }); + + updateStats(); +} + +function render() { + drawGrid(); + ctx.fillStyle = player.color; + ctx.fillRect( + player.x * gridSize, + player.y * gridSize, + gridSize - 1, + gridSize - 1 + ); + + enemies.forEach((enemy) => { + ctx.fillStyle = enemy.color; + ctx.fillRect( + enemy.x * gridSize, + enemy.y * gridSize, + gridSize - 1, + gridSize - 1 + ); + }); +} + +let gameLoop; +function startGame() { + initGame(); + clearInterval(gameLoop); + gameLoop = setInterval(() => { + update(); + render(); + }, 120); +} + +function gameOver() { + clearInterval(gameLoop); + ctx.fillStyle = "rgba(0,0,0,0.7)"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "#fff"; + ctx.font = "28px Poppins"; + ctx.textAlign = "center"; + ctx.fillText("๐Ÿ’ฅ Game Over! ๐Ÿ’ฅ", canvas.width / 2, canvas.height / 2 - 20); + ctx.fillText(`Score: ${score}`, canvas.width / 2, canvas.height / 2 + 20); +} + +restartBtn.addEventListener("click", startGame); +startGame(); diff --git a/games/tileman/style.css b/games/tileman/style.css new file mode 100644 index 00000000..07ed0b73 --- /dev/null +++ b/games/tileman/style.css @@ -0,0 +1,58 @@ +body { + margin: 0; + background: linear-gradient(135deg, #1d1f21, #292d32); + font-family: "Poppins", sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + color: #f0f0f0; +} + +.game-container { + text-align: center; +} + +.game-header { + margin-bottom: 10px; +} + +.game-header h1 { + font-size: 2rem; + color: #4ade80; + text-shadow: 0 0 5px #22c55e; +} + +.stats { + display: flex; + justify-content: center; + gap: 30px; + margin: 10px 0; +} + +button { + padding: 8px 18px; + background-color: #22c55e; + border: none; + color: #fff; + border-radius: 6px; + font-weight: bold; + cursor: pointer; + transition: background-color 0.3s; +} + +button:hover { + background-color: #16a34a; +} + +canvas { + background-color: #111; + border: 2px solid #333; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.7); +} + +.game-footer { + margin-top: 8px; + font-size: 0.9rem; + color: #bbb; +} diff --git a/games/time-traveler/index.html b/games/time-traveler/index.html new file mode 100644 index 00000000..67d9fdc8 --- /dev/null +++ b/games/time-traveler/index.html @@ -0,0 +1,72 @@ + + + + + + Time Traveler #996 - Mini JS Games Hub + + + + +
      +
      +
      Era: Prehistoric
      +
      Artifacts: 0/5
      +
      Time Energy: 100
      +
      Score: 0
      +
      + + + +
      +
      +
      + +
      +
      +

      Time Traveler #996

      +

      Jump through historical eras, collect artifacts, and solve time-based puzzles while avoiding paradoxes!

      + +

      How to Play:

      +
        +
      • Move: Arrow keys or WASD
      • +
      • Jump: Spacebar
      • +
      • Time Jump: Hold mouse button to charge, release to jump through time
      • +
      • Collect: Touch artifacts to collect them
      • +
      • Avoid: Paradoxes (red swirling portals) and obstacles
      • +
      • Goal: Collect all artifacts in each era and reach the time portal
      • +
      + +

      Eras:

      +
        +
      • Prehistoric - Dinosaurs and ancient artifacts
      • +
      • Ancient Egypt - Pyramids and hieroglyphs
      • +
      • Medieval - Castles and knights
      • +
      • Industrial Revolution - Factories and steam engines
      • +
      • Future - High-tech cities and robots
      • +
      + + +
      +
      + + + + +
      + + + + \ No newline at end of file diff --git a/games/time-traveler/script.js b/games/time-traveler/script.js new file mode 100644 index 00000000..352fe9ad --- /dev/null +++ b/games/time-traveler/script.js @@ -0,0 +1,497 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const startButton = document.getElementById('startButton'); +const restartButton = document.getElementById('restartButton'); +const nextEraButton = document.getElementById('nextEraButton'); +const instructionsOverlay = document.getElementById('instructions-overlay'); +const gameOverOverlay = document.getElementById('game-over-overlay'); +const levelCompleteOverlay = document.getElementById('level-complete-overlay'); +const eraDisplay = document.getElementById('era-display'); +const artifactsDisplay = document.getElementById('artifacts-collected'); +const timeEnergyDisplay = document.getElementById('time-energy'); +const scoreDisplay = document.getElementById('score'); +const powerBar = document.getElementById('power-bar'); + +canvas.width = 800; +canvas.height = 600; + +let gameRunning = false; +let gameOver = false; +let levelComplete = false; +let player; +let platforms = []; +let artifacts = []; +let paradoxes = []; +let timePortal; +let keys = {}; +let mouseDown = false; +let timeJumpCharge = 0; +let maxTimeJumpCharge = 100; +let currentEra = 0; +let score = 0; +let timeEnergy = 100; + +const eras = [ + { + name: 'Prehistoric', + bgClass: 'prehistoric', + artifacts: 5, + description: 'Dinosaurs and ancient artifacts' + }, + { + name: 'Ancient Egypt', + bgClass: 'ancient-egypt', + artifacts: 6, + description: 'Pyramids and hieroglyphs' + }, + { + name: 'Medieval', + bgClass: 'medieval', + artifacts: 7, + description: 'Castles and knights' + }, + { + name: 'Industrial Revolution', + bgClass: 'industrial', + artifacts: 8, + description: 'Factories and steam engines' + }, + { + name: 'Future', + bgClass: 'future', + artifacts: 10, + description: 'High-tech cities and robots' + } +]; + +// Player class +class Player { + constructor(x, y) { + this.x = x; + this.y = y; + this.width = 20; + this.height = 30; + this.vx = 0; + this.vy = 0; + this.speed = 5; + this.jumpPower = -12; + this.onGround = false; + this.color = '#00ffff'; + this.glowColor = '#00ffff'; + this.timeTraveling = false; + this.timeTravelTimer = 0; + } + + update() { + // Horizontal movement + if (keys.ArrowLeft || keys.KeyA) { + this.vx = -this.speed; + } else if (keys.ArrowRight || keys.KeyD) { + this.vx = this.speed; + } else { + this.vx *= 0.8; // Friction + } + + // Jumping + if ((keys.Space || keys.ArrowUp || keys.KeyW) && this.onGround) { + this.vy = this.jumpPower; + this.onGround = false; + } + + // Time travel effect + if (this.timeTraveling) { + this.timeTravelTimer++; + if (this.timeTravelTimer > 30) { + this.timeTraveling = false; + this.timeTravelTimer = 0; + } + } + + // Apply gravity + this.vy += 0.5; + + // Update position + this.x += this.vx; + this.y += this.vy; + + // Ground collision + if (this.y + this.height >= canvas.height - 50) { + this.y = canvas.height - 50 - this.height; + this.vy = 0; + this.onGround = true; + } + + // Platform collisions + this.onGround = false; + platforms.forEach(platform => { + if (this.x < platform.x + platform.width && + this.x + this.width > platform.x && + this.y < platform.y + platform.height && + this.y + this.height > platform.y) { + + // Landing on top of platform + if (this.vy > 0 && this.y < platform.y) { + this.y = platform.y - this.height; + this.vy = 0; + this.onGround = true; + } + // Hitting platform from below + else if (this.vy < 0 && this.y > platform.y) { + this.y = platform.y + platform.height; + this.vy = 0; + } + // Side collisions + else if (this.vx > 0 && this.x < platform.x) { + this.x = platform.x - this.width; + this.vx = 0; + } else if (this.vx < 0 && this.x > platform.x) { + this.x = platform.x + platform.width; + this.vx = 0; + } + } + }); + + // Keep player in bounds + if (this.x < 0) this.x = 0; + if (this.x + this.width > canvas.width) this.x = canvas.width - this.width; + + // Check artifact collection + artifacts.forEach((artifact, index) => { + if (!artifact.collected && + this.x < artifact.x + artifact.width && + this.x + this.width > artifact.x && + this.y < artifact.y + artifact.height && + this.y + this.height > artifact.y) { + artifact.collected = true; + score += 100; + updateUI(); + } + }); + + // Check paradox collision + paradoxes.forEach(paradox => { + const dx = this.x + this.width/2 - paradox.x; + const dy = this.y + this.height/2 - paradox.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < paradox.radius + 15) { + gameOver = true; + showGameOver(); + } + }); + + // Check time portal collision + if (timePortal) { + const dx = this.x + this.width/2 - timePortal.x; + const dy = this.y + this.height/2 - timePortal.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < timePortal.radius + 15) { + levelComplete = true; + showLevelComplete(); + } + } + } + + draw() { + ctx.save(); + + // Time travel effect + if (this.timeTraveling) { + ctx.globalAlpha = 0.7 + Math.sin(this.timeTravelTimer * 0.3) * 0.3; + ctx.shadowColor = this.glowColor; + ctx.shadowBlur = 20; + } + + // Draw player + ctx.fillStyle = this.color; + ctx.fillRect(this.x, this.y, this.width, this.height); + + // Draw glow effect + ctx.shadowColor = this.glowColor; + ctx.shadowBlur = 10; + ctx.strokeStyle = this.glowColor; + ctx.lineWidth = 2; + ctx.strokeRect(this.x, this.y, this.width, this.height); + + ctx.restore(); + } + + timeJump() { + this.timeTraveling = true; + this.timeTravelTimer = 0; + timeEnergy = Math.max(0, timeEnergy - 10); + updateUI(); + } +} + +// Initialize level +function initLevel() { + const era = eras[currentEra]; + + // Set background + canvas.className = era.bgClass; + + // Create player + player = new Player(50, canvas.height - 100); + + // Create platforms + platforms = []; + const numPlatforms = 5 + currentEra; + for (let i = 0; i < numPlatforms; i++) { + platforms.push({ + x: Math.random() * (canvas.width - 100), + y: 150 + i * 80 + Math.random() * 50, + width: 80 + Math.random() * 50, + height: 20 + }); + } + + // Create artifacts + artifacts = []; + for (let i = 0; i < era.artifacts; i++) { + artifacts.push({ + x: 100 + i * 120 + Math.random() * 50, + y: canvas.height - 120 - Math.random() * 300, + width: 15, + height: 15, + collected: false, + glow: Math.random() * Math.PI * 2 + }); + } + + // Create paradoxes + paradoxes = []; + const numParadoxes = 2 + currentEra; + for (let i = 0; i < numParadoxes; i++) { + paradoxes.push({ + x: 200 + i * 150 + Math.random() * 100, + y: canvas.height - 100 - Math.random() * 400, + radius: 20, + rotation: 0 + }); + } + + // Create time portal + timePortal = { + x: canvas.width - 80, + y: canvas.height - 120, + radius: 25, + rotation: 0 + }; + + timeEnergy = 100; + updateUI(); +} + +// Show game over +function showGameOver() { + gameRunning = false; + gameOverOverlay.style.display = 'flex'; + document.getElementById('final-score').textContent = `Final Score: ${score}`; +} + +// Show level complete +function showLevelComplete() { + gameRunning = false; + levelCompleteOverlay.style.display = 'flex'; + const bonus = 500 + currentEra * 100; + score += bonus; + document.getElementById('era-bonus').textContent = `Time Jump Bonus: +${bonus}`; + updateUI(); +} + +// Next era +function nextEra() { + currentEra = (currentEra + 1) % eras.length; + levelComplete = false; + levelCompleteOverlay.style.display = 'none'; + initLevel(); + gameRunning = true; + gameLoop(); +} + +// Update UI +function updateUI() { + const era = eras[currentEra]; + eraDisplay.textContent = `Era: ${era.name}`; + artifactsDisplay.textContent = `Artifacts: ${artifacts.filter(a => a.collected).length}/${era.artifacts}`; + timeEnergyDisplay.textContent = `Time Energy: ${timeEnergy}`; + scoreDisplay.textContent = `Score: ${score}`; +} + +// Draw everything +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw ground + ctx.fillStyle = '#4a4a4a'; + ctx.fillRect(0, canvas.height - 50, canvas.width, 50); + + // Draw platforms + ctx.fillStyle = '#666666'; + platforms.forEach(platform => { + ctx.fillRect(platform.x, platform.y, platform.width, platform.height); + }); + + // Draw artifacts + artifacts.forEach(artifact => { + if (!artifact.collected) { + ctx.save(); + artifact.glow += 0.1; + const glowIntensity = 0.5 + Math.sin(artifact.glow) * 0.5; + + ctx.shadowColor = '#ffff00'; + ctx.shadowBlur = 15 * glowIntensity; + ctx.fillStyle = '#ffff00'; + ctx.fillRect(artifact.x, artifact.y, artifact.width, artifact.height); + + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 2; + ctx.strokeRect(artifact.x, artifact.y, artifact.width, artifact.height); + ctx.restore(); + } + }); + + // Draw paradoxes + paradoxes.forEach(paradox => { + paradox.rotation += 0.1; + ctx.save(); + ctx.translate(paradox.x, paradox.y); + ctx.rotate(paradox.rotation); + + ctx.shadowColor = '#ff0000'; + ctx.shadowBlur = 20; + ctx.fillStyle = '#ff0000'; + ctx.beginPath(); + ctx.arc(0, 0, paradox.radius, 0, Math.PI * 2); + ctx.fill(); + + // Swirling effect + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 3; + ctx.beginPath(); + for (let i = 0; i < 8; i++) { + const angle = (i / 8) * Math.PI * 2 + paradox.rotation; + const x1 = Math.cos(angle) * (paradox.radius - 5); + const y1 = Math.sin(angle) * (paradox.radius - 5); + const x2 = Math.cos(angle) * paradox.radius; + const y2 = Math.sin(angle) * paradox.radius; + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + } + ctx.stroke(); + ctx.restore(); + }); + + // Draw time portal + if (timePortal) { + timePortal.rotation += 0.05; + ctx.save(); + ctx.translate(timePortal.x, timePortal.y); + ctx.rotate(timePortal.rotation); + + ctx.shadowColor = '#00ffff'; + ctx.shadowBlur = 25; + ctx.fillStyle = '#00ffff'; + ctx.beginPath(); + ctx.arc(0, 0, timePortal.radius, 0, Math.PI * 2); + ctx.fill(); + + // Portal rings + for (let i = 1; i <= 3; i++) { + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(0, 0, timePortal.radius * (1 - i * 0.2), 0, Math.PI * 2); + ctx.stroke(); + } + ctx.restore(); + } + + // Draw player + player.draw(); + + // Draw time jump charge effect + if (mouseDown && timeEnergy > 0) { + timeJumpCharge = Math.min(timeJumpCharge + 2, maxTimeJumpCharge); + powerBar.style.width = `${(timeJumpCharge / maxTimeJumpCharge) * 100}%`; + + // Draw charge effect around player + ctx.save(); + ctx.strokeStyle = '#00ffff'; + ctx.lineWidth = 3; + ctx.shadowColor = '#00ffff'; + ctx.shadowBlur = 15; + ctx.beginPath(); + ctx.arc(player.x + player.width/2, player.y + player.height/2, + 30 + timeJumpCharge * 0.5, 0, Math.PI * 2); + ctx.stroke(); + ctx.restore(); + } else { + timeJumpCharge = 0; + powerBar.style.width = '0%'; + } +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + player.update(); + draw(); + + requestAnimationFrame(gameLoop); +} + +// Event listeners +startButton.addEventListener('click', () => { + instructionsOverlay.style.display = 'none'; + gameRunning = true; + initLevel(); + gameLoop(); +}); + +restartButton.addEventListener('click', () => { + gameOver = false; + gameOverOverlay.style.display = 'none'; + currentEra = 0; + score = 0; + initLevel(); + gameRunning = true; + gameLoop(); +}); + +nextEraButton.addEventListener('click', nextEra); + +// Keyboard events +document.addEventListener('keydown', (e) => { + keys[e.code] = true; +}); + +document.addEventListener('keyup', (e) => { + keys[e.code] = false; +}); + +// Mouse events for time jumping +canvas.addEventListener('mousedown', (e) => { + if (!gameRunning || timeEnergy <= 0) return; + mouseDown = true; +}); + +canvas.addEventListener('mouseup', (e) => { + if (!gameRunning) return; + + if (mouseDown && timeJumpCharge > 50) { + player.timeJump(); + // Time jump effect - teleport player slightly + player.x += 50 + Math.random() * 100; + if (player.x > canvas.width - player.width) player.x = canvas.width - player.width; + } + + mouseDown = false; + timeJumpCharge = 0; +}); + +// Initialize +updateUI(); \ No newline at end of file diff --git a/games/time-traveler/style.css b/games/time-traveler/style.css new file mode 100644 index 00000000..d5d2fd4c --- /dev/null +++ b/games/time-traveler/style.css @@ -0,0 +1,214 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Poppins', sans-serif; + background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #16213e 100%); + color: #ffffff; + overflow: hidden; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +} + +#game-container { + position: relative; + width: 900px; + height: 700px; + border-radius: 15px; + overflow: hidden; + box-shadow: 0 0 30px rgba(0, 255, 255, 0.3); + border: 2px solid rgba(0, 255, 255, 0.5); +} + +#ui-panel { + position: absolute; + top: 10px; + left: 10px; + z-index: 10; + background: rgba(0, 0, 0, 0.8); + padding: 15px; + border-radius: 10px; + border: 1px solid rgba(0, 255, 255, 0.3); + font-family: 'Orbitron', monospace; + font-size: 14px; + min-width: 200px; +} + +#ui-panel div { + margin-bottom: 8px; + color: #00ffff; + text-shadow: 0 0 5px #00ffff; +} + +#power-bar-container { + position: absolute; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + width: 300px; + height: 20px; + background: rgba(0, 0, 0, 0.8); + border-radius: 10px; + border: 2px solid rgba(0, 255, 255, 0.5); + overflow: hidden; + z-index: 10; +} + +#power-bar { + height: 100%; + width: 0%; + background: linear-gradient(90deg, #00ff00, #ffff00, #ff0000); + transition: width 0.1s ease; + box-shadow: 0 0 10px rgba(255, 255, 0, 0.5); +} + +#gameCanvas { + display: block; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f0f23 100%); + border-radius: 15px; +} + +#instructions-overlay, +#game-over-overlay, +#level-complete-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.9); + display: flex; + justify-content: center; + align-items: center; + z-index: 100; + backdrop-filter: blur(5px); +} + +.instructions-content, +.game-over-content, +.level-complete-content { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + padding: 30px; + border-radius: 15px; + border: 2px solid rgba(0, 255, 255, 0.5); + box-shadow: 0 0 30px rgba(0, 255, 255, 0.3); + max-width: 600px; + text-align: center; + font-family: 'Poppins', sans-serif; +} + +.instructions-content h1 { + font-family: 'Orbitron', monospace; + font-size: 2.5em; + margin-bottom: 20px; + color: #00ffff; + text-shadow: 0 0 15px #00ffff; +} + +.instructions-content h3 { + color: #ffaa00; + margin: 20px 0 10px 0; + font-size: 1.2em; +} + +.instructions-content ul { + text-align: left; + margin: 15px 0; + padding-left: 20px; +} + +.instructions-content li { + margin-bottom: 8px; + line-height: 1.4; +} + +.instructions-content strong { + color: #00ff88; +} + +button { + background: linear-gradient(135deg, #00ffff, #0088ff); + border: none; + color: white; + padding: 12px 24px; + font-size: 16px; + font-weight: 600; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; + margin: 10px; + font-family: 'Poppins', sans-serif; + text-transform: uppercase; + letter-spacing: 1px; + box-shadow: 0 4px 15px rgba(0, 255, 255, 0.3); +} + +button:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 255, 255, 0.5); + background: linear-gradient(135deg, #33ffff, #3399ff); +} + +.game-over-content h2, +.level-complete-content h2 { + font-family: 'Orbitron', monospace; + font-size: 2em; + color: #ff4444; + margin-bottom: 20px; + text-shadow: 0 0 10px #ff4444; +} + +.level-complete-content h2 { + color: #44ff44; + text-shadow: 0 0 10px #44ff44; +} + +#final-score, +#era-bonus { + font-size: 1.2em; + margin: 15px 0; + color: #ffff88; +} + +/* Era-specific backgrounds */ +.prehistoric { + background: linear-gradient(135deg, #2d5016 0%, #4a7c24 50%, #1a2e0f 100%); +} + +.ancient-egypt { + background: linear-gradient(135deg, #daa520 0%, #b8860b 50%, #8b6914 100%); +} + +.medieval { + background: linear-gradient(135deg, #4a4a4a 0%, #696969 50%, #2f2f2f 100%); +} + +.industrial { + background: linear-gradient(135deg, #2c2c2c 0%, #4a4a4a 50%, #1a1a1a 100%); +} + +.future { + background: linear-gradient(135deg, #1a0033 0%, #330066 50%, #000033 100%); +} + +/* Glow effects */ +.glow-blue { + box-shadow: 0 0 20px rgba(0, 255, 255, 0.5); +} + +.glow-red { + box-shadow: 0 0 20px rgba(255, 0, 0, 0.5); +} + +.glow-green { + box-shadow: 0 0 20px rgba(0, 255, 0, 0.5); +} + +.glow-yellow { + box-shadow: 0 0 20px rgba(255, 255, 0, 0.5); +} \ No newline at end of file diff --git a/games/time_keeper/index.html b/games/time_keeper/index.html new file mode 100644 index 00000000..c9653dc1 --- /dev/null +++ b/games/time_keeper/index.html @@ -0,0 +1,35 @@ + + + + + + The Time Keeper Reaction Game + + + + +
      +

      ๐ŸŽฏ The Time Keeper

      + +
      + Score: 0 | Last Score: --ms +
      + +
      +
      HIT ZONE
      + +
      +
      + +
      +

      Click the button when the target is inside the HIT ZONE!

      +
      + +
      + +
      +
      + + + + \ No newline at end of file diff --git a/games/time_keeper/script.js b/games/time_keeper/script.js new file mode 100644 index 00000000..2d7dc3b7 --- /dev/null +++ b/games/time_keeper/script.js @@ -0,0 +1,145 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. DOM Elements and Constants --- + const movingTarget = document.getElementById('moving-target'); + const actionButton = document.getElementById('action-button'); + const scoreSpan = document.getElementById('score'); + const lastScoreSpan = document.getElementById('last-score'); + const feedbackMessage = document.getElementById('feedback-message'); + const trackContainer = document.getElementById('track-container'); + + // Get track dimensions (must match CSS) + const TRACK_WIDTH = trackContainer.clientWidth; + const TARGET_SIZE = movingTarget.clientWidth; + const HIT_ZONE_WIDTH = 50; // Must match CSS :root variable + + // Define the boundaries of the hit zone's *center* + const HIT_ZONE_CENTER_START = (TRACK_WIDTH / 2) - (HIT_ZONE_WIDTH / 2); + const HIT_ZONE_CENTER_END = (TRACK_WIDTH / 2) + (HIT_ZONE_WIDTH / 2); + + // Define the boundaries of the hit zone relative to the *target's left edge* + const HIT_ZONE_START = HIT_ZONE_CENTER_START - (TARGET_SIZE / 2); + const HIT_ZONE_END = HIT_ZONE_CENTER_END + (TARGET_SIZE / 2); + + const TRACK_END = TRACK_WIDTH - TARGET_SIZE; // Max valid 'left' position + + // --- 2. Game State Variables --- + let score = 0; + let position = 0; // Current 'left' position of the target + let direction = 1; // 1 = right, -1 = left + let speed = 2.5; // Pixels per frame + let animationFrameId = null; // ID for requestAnimationFrame + let gameActive = false; + let lastTime = 0; + + // --- 3. Core Logic Functions --- + + /** + * The main animation loop for movement, using requestAnimationFrame for smoothness. + * @param {number} timestamp - Time provided by rAF (not used for movement here, but good practice) + */ + function moveTarget(timestamp) { + if (!gameActive) return; + + // Calculate the next position + position += direction * speed; + + // --- Boundary Check --- + if (position >= TRACK_END) { + // Hit the right wall, reverse direction + direction = -1; + position = TRACK_END; + // Optionally, increase speed slightly for difficulty + speed += 0.05; + } else if (position <= 0) { + // Hit the left wall, reverse direction + direction = 1; + position = 0; + // Optionally, increase speed slightly + speed += 0.05; + } + + // Apply position to the DOM + movingTarget.style.left = `${position}px`; + + // Request the next frame + animationFrameId = requestAnimationFrame(moveTarget); + } + + /** + * Initializes and starts the game. + */ + function startGame() { + if (gameActive) return; + + // Reset state + gameActive = true; + position = 0; + direction = 1; + speed = 2.5; + + actionButton.textContent = 'CLICK!'; + actionButton.disabled = false; + feedbackMessage.textContent = 'Wait for the perfect moment...'; + + // Start the animation loop + lastTime = performance.now(); // Record start time for reaction calculation + animationFrameId = requestAnimationFrame(moveTarget); + } + + /** + * Handles the button click to check if the target is in the hit zone. + */ + function handleAction() { + if (!gameActive) return; + + // Stop movement immediately + cancelAnimationFrame(animationFrameId); + gameActive = false; + actionButton.disabled = true; + + // Check if the current position is within the target zone + if (position >= HIT_ZONE_START && position <= HIT_ZONE_END) { + score++; + scoreSpan.textContent = score; + + // Calculate score based on proximity to the exact center + const centerPosition = (TRACK_WIDTH / 2) - (TARGET_SIZE / 2); + const deviation = Math.abs(position - centerPosition); + + // Score in ms (smaller is better). Max score for perfect hit is 1ms. + // Normalize deviation to a range (e.g., 0 to 100) and scale it up. + const maxDeviation = HIT_ZONE_END - centerPosition; + const reactionScore = Math.round(100 * (deviation / maxDeviation)) + 1; // 1ms is perfect + + lastScoreSpan.textContent = `${reactionScore}ms`; + + feedbackMessage.innerHTML = `โœ… **HIT!** Precision Score: ${reactionScore}ms.`; + feedbackMessage.style.color = '#2ecc71'; + + } else { + feedbackMessage.innerHTML = `โŒ **MISS!** You were outside the zone.`; + feedbackMessage.style.color = '#e74c3c'; + } + + // Prepare for next round + actionButton.textContent = 'RETRY'; + actionButton.disabled = false; + } + + // --- 4. Event Listeners --- + actionButton.addEventListener('click', () => { + if (actionButton.textContent === 'START' || actionButton.textContent === 'RETRY') { + startGame(); + } else if (actionButton.textContent === 'CLICK!') { + handleAction(); + } + }); + + // Allow spacebar to act as the action button for quicker reaction time + document.addEventListener('keydown', (e) => { + if (e.code === 'Space' && !actionButton.disabled) { + e.preventDefault(); // Prevents default browser scrolling + handleAction(); + } + }); +}); \ No newline at end of file diff --git a/games/time_keeper/style.css b/games/time_keeper/style.css new file mode 100644 index 00000000..e69de29b diff --git a/games/timer_game_new/index.html b/games/timer_game_new/index.html new file mode 100644 index 00000000..c9b7a74c --- /dev/null +++ b/games/timer_game_new/index.html @@ -0,0 +1,19 @@ + + + + + + Reaction Time Test โšก + + + +
      +

      Click to Start

      +

      When the screen turns **GREEN**, click or tap as fast as you can!

      +

      +

      Best Time: -- ms

      +
      + + + + \ No newline at end of file diff --git a/games/timer_game_new/script.js b/games/timer_game_new/script.js new file mode 100644 index 00000000..d19ac1a2 --- /dev/null +++ b/games/timer_game_new/script.js @@ -0,0 +1,122 @@ +// --- 1. DOM Element References --- +const testArea = document.getElementById('test-area'); +const headline = document.getElementById('headline'); +const instruction = document.getElementById('instruction'); +const resultDisplay = document.getElementById('result-display'); +const bestTimeDisplay = document.getElementById('best-time'); + +// --- 2. Game State Variables --- +let testPhase = 'initial'; // 'initial', 'ready', 'go' +let timeoutID; // Stores the ID for the setTimeout that controls the random delay +let startTime; // Time the screen turned green (using Date.now()) +let bestTime = localStorage.getItem('bestReactionTime') ? + parseInt(localStorage.getItem('bestReactionTime')) : + Infinity; + +// Update best time display immediately on load +updateBestTimeDisplay(); + +// --- 3. Core Game Functions --- + +// Starts the waiting phase (Screen turns RED) +function startWaitingPhase() { + testPhase = 'ready'; + + // 1. Update UI to RED/Ready state + testArea.classList.remove('initial'); + testArea.classList.add('ready'); + headline.textContent = 'Wait for Green...'; + instruction.textContent = 'Clicking early will restart the test!'; + resultDisplay.textContent = ''; + resultDisplay.classList.remove('critical'); + + // 2. Set random delay (e.g., 1.5 to 4.5 seconds) + const randomDelay = Math.random() * 3000 + 1500; + + // 3. Set a timeout to switch to the GO phase + timeoutID = setTimeout(startGoPhase, randomDelay); +} + +// Starts the GO phase (Screen turns GREEN) +function startGoPhase() { + testPhase = 'go'; + + // 1. Record the start time with high precision + startTime = Date.now(); + + // 2. Update UI to GREEN/Go state + testArea.classList.remove('ready'); + testArea.classList.add('go'); + headline.textContent = 'CLICK NOW!'; + instruction.textContent = 'Measure your speed.'; +} + +// Handles player interaction (click or key press) +function handleAction() { + if (testPhase === 'initial') { + // Start the test + startWaitingPhase(); + } + else if (testPhase === 'ready') { + // Player clicked too early! + clearTimeout(timeoutID); // Stop the pending green switch + endTest('Too soon! You must wait for GREEN.', 0); + } + else if (testPhase === 'go') { + // Player clicked at the right time. Measure reaction time. + const endTime = Date.now(); + const reactionTime = endTime - startTime; + + // Update best time if current time is better + if (reactionTime < bestTime) { + bestTime = reactionTime; + localStorage.setItem('bestReactionTime', bestTime); + updateBestTimeDisplay(); + resultDisplay.classList.add('critical'); + } + + endTest(`Your time: ${reactionTime} ms`, reactionTime); + } +} + +// Ends the current test round +function endTest(message, time) { + testPhase = 'initial'; // Reset state to initial + + // Update UI back to Blue/Initial state + testArea.classList.remove('ready', 'go'); + testArea.classList.add('initial'); + headline.textContent = 'Click to Start Again'; + + // Display result message + instruction.textContent = 'Ready for another round?'; + resultDisplay.textContent = message; +} + +// Updates the display for the best recorded time +function updateBestTimeDisplay() { + if (bestTime !== Infinity) { + bestTimeDisplay.textContent = `Best Time: ${bestTime} ms`; + } else { + bestTimeDisplay.textContent = 'Best Time: -- ms'; + } +} + +// --- 4. Event Listeners --- + +// Main listener for clicks/taps on the test area +testArea.addEventListener('click', handleAction); + +// Listener for keyboard events (useful for dedicated users) +document.addEventListener('keydown', (e) => { + // Only respond to keydown if we are in the GO phase (to prevent accidental early clicks) + if (e.code === 'Space' || e.code === 'Enter') { + // Prevent default action (like page scroll or button click) + e.preventDefault(); + + // Treat key press like a click if in the GO phase + if (testPhase === 'go') { + handleAction(); + } + } +}); \ No newline at end of file diff --git a/games/timer_game_new/style.css b/games/timer_game_new/style.css new file mode 100644 index 00000000..f7008de8 --- /dev/null +++ b/games/timer_game_new/style.css @@ -0,0 +1,60 @@ +body, html { + margin: 0; + padding: 0; + height: 100%; + width: 100%; + overflow: hidden; /* Hide scrollbars */ +} + +#test-area { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100vh; + width: 100vw; + text-align: center; + color: white; + transition: background-color 0.1s ease; /* Smooth color transition */ + cursor: pointer; /* Indicate it's clickable */ +} + +/* --- State Styles --- */ + +/* Initial State: Ready to start */ +#test-area.initial { + background-color: #3498db; /* Blue */ +} + +/* Ready State: Waiting for the change (RED) */ +#test-area.ready { + background-color: #e74c3c; /* Red */ +} + +/* Go State: Click now! (GREEN) */ +#test-area.go { + background-color: #2ecc71; /* Green */ +} + +/* --- Text Styles --- */ + +#headline { + font-size: 3em; + margin: 10px; +} + +#instruction { + font-size: 1.5em; + margin-bottom: 30px; +} + +#result-display { + font-size: 2.5em; + font-weight: bold; + color: #ffcc00; /* Yellow for results */ +} + +#best-time { + font-size: 1.2em; + margin-top: 15px; +} \ No newline at end of file diff --git a/games/timing_game/index.html b/games/timing_game/index.html new file mode 100644 index 00000000..f081c12f --- /dev/null +++ b/games/timing_game/index.html @@ -0,0 +1,14 @@ + + + + + Rhythm Game Engine + + + +
      + +
      + + + \ No newline at end of file diff --git a/games/timing_game/script.js b/games/timing_game/script.js new file mode 100644 index 00000000..eac1660e --- /dev/null +++ b/games/timing_game/script.js @@ -0,0 +1,28 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const CANVAS_WIDTH = canvas.width; +const CANVAS_HEIGHT = canvas.height; + +// --- Timing & Synchronization Constants --- +const BPM = 120; // Beats Per Minute +const MS_PER_BEAT = 60000 / BPM; // Milliseconds per beat (e.g., 500ms for 120 BPM) +const NOTE_TRAVEL_DURATION = 2000; // 2 seconds for a note to travel from top to hit line + +// Visual positions +const HIT_LINE_Y = CANVAS_HEIGHT * 0.8; // Target line is 80% down the screen +const START_LINE_Y = 0; + +// Calculated speed (pixels per millisecond) +// Note must travel CANVAS_HEIGHT * 0.8 in NOTE_TRAVEL_DURATION time +const NOTE_SPEED_PIXELS_PER_MS = HIT_LINE_Y / NOTE_TRAVEL_DURATION; + +// Hit tolerance windows (in milliseconds) +const PERFECT_WINDOW = 50; // +/- 50ms from target time +const GOOD_WINDOW = 150; // +/- 150ms from target time + +// --- Game State Variables --- +let score = 0; +let lastBeatTime = performance.now(); +let notes = []; +let keys = {}; +let noteCounter = 0; // Used to determine which note pattern to spawn \ No newline at end of file diff --git a/games/timing_game/style.css b/games/timing_game/style.css new file mode 100644 index 00000000..a187f79b --- /dev/null +++ b/games/timing_game/style.css @@ -0,0 +1,14 @@ +/* style.css */ +body { + background-color: #1a1a2e; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} +#gameCanvas { + border: 3px solid #64ffda; /* Neon green/teal border */ + background-color: #000; + display: block; +} \ No newline at end of file diff --git a/games/tiny-fishing/index.html b/games/tiny-fishing/index.html new file mode 100644 index 00000000..9c8384a5 --- /dev/null +++ b/games/tiny-fishing/index.html @@ -0,0 +1,28 @@ + + + + + + Tiny Fishing ๐ŸŽฃ + + + +
      +

      Tiny Fishing ๐ŸŽฃ

      +
      +

      ๐Ÿ’ฐ Coins: 0

      +

      โฌ‡๏ธ Depth: 100m

      +

      ๐Ÿช Hooks: 1

      +
      + +
      + + +
      +
      + + + + + + diff --git a/games/tiny-fishing/script.js b/games/tiny-fishing/script.js new file mode 100644 index 00000000..8b7011b9 --- /dev/null +++ b/games/tiny-fishing/script.js @@ -0,0 +1,179 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); + +canvas.width = window.innerWidth * 0.9; +canvas.height = window.innerHeight * 0.7; + +const startBtn = document.getElementById("startBtn"); +const upgradeDepth = document.getElementById("upgradeDepth"); +const upgradeHooks = document.getElementById("upgradeHooks"); + +const coinsEl = document.getElementById("coins"); +const depthEl = document.getElementById("depth"); +const hooksEl = document.getElementById("hooks"); + +let coins = 0; +let depth = 100; +let hooks = 1; +let fishing = false; +let lineY = 0; +let direction = "down"; +let fishes = []; + +class Fish { + constructor(x, y, size, speed, value, color) { + this.x = x; + this.y = y; + this.size = size; + this.speed = speed; + this.value = value; + this.color = color; + } + + draw() { + ctx.fillStyle = this.color; + ctx.beginPath(); + ctx.ellipse(this.x, this.y, this.size * 1.5, this.size, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.moveTo(this.x - this.size, this.y); + ctx.lineTo(this.x - this.size - 10, this.y - 5); + ctx.lineTo(this.x - this.size - 10, this.y + 5); + ctx.closePath(); + ctx.fill(); + } + + update() { + this.x += this.speed; + if (this.x > canvas.width + 50) this.x = -50; + if (this.x < -50) this.x = canvas.width + 50; + } +} + +function generateFish() { + fishes = []; + for (let i = 0; i < 10; i++) { + const size = Math.random() * 15 + 10; + const y = Math.random() * canvas.height; + const x = Math.random() * canvas.width; + const speed = (Math.random() - 0.5) * 2; + const value = Math.floor(size); + const colors = ["#ffadad", "#ffd6a5", "#fdffb6", "#caffbf", "#9bf6ff"]; + const color = colors[Math.floor(Math.random() * colors.length)]; + fishes.push(new Fish(x, y, size, speed, value, color)); + } +} + +let hookX = canvas.width / 2; +let hookY = 0; +let caughtFish = []; + +function drawLine() { + ctx.strokeStyle = "#fff"; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(canvas.width / 2, 0); + ctx.lineTo(hookX, hookY); + ctx.stroke(); +} + +function drawHook() { + ctx.fillStyle = "#ffd166"; + ctx.beginPath(); + ctx.arc(hookX, hookY, 5, 0, Math.PI * 2); + ctx.fill(); +} + +function detectCatch() { + fishes.forEach((fish, index) => { + const dx = hookX - fish.x; + const dy = hookY - fish.y; + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance < fish.size + 5 && caughtFish.length < hooks) { + caughtFish.push(fish); + fishes.splice(index, 1); + } + }); +} + +function updateGame() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Background water waves + const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height); + gradient.addColorStop(0, "#4ec0ca"); + gradient.addColorStop(1, "#023047"); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + fishes.forEach((fish) => { + fish.update(); + fish.draw(); + }); + + drawLine(); + drawHook(); + + if (fishing) { + if (direction === "down") { + hookY += 5; + if (hookY > canvas.height - depth) { + direction = "up"; + } + detectCatch(); + } else { + hookY -= 5; + caughtFish.forEach((fish) => { + fish.y = hookY + 10; + fish.x = hookX; + fish.draw(); + }); + if (hookY <= 0) { + fishing = false; + coins += caughtFish.reduce((sum, f) => sum + f.value, 0); + coinsEl.textContent = coins; + caughtFish = []; + startBtn.disabled = false; + } + } + } + + requestAnimationFrame(updateGame); +} + +startBtn.addEventListener("click", () => { + if (!fishing) { + fishing = true; + direction = "down"; + hookY = 0; + caughtFish = []; + startBtn.disabled = true; + } +}); + +upgradeDepth.addEventListener("click", () => { + if (coins >= 50) { + coins -= 50; + depth += 50; + coinsEl.textContent = coins; + depthEl.textContent = depth; + } +}); + +upgradeHooks.addEventListener("click", () => { + if (coins >= 100) { + coins -= 100; + hooks += 1; + coinsEl.textContent = coins; + hooksEl.textContent = hooks; + } +}); + +generateFish(); +updateGame(); + +window.addEventListener("mousemove", (e) => { + if (fishing) { + hookX = e.clientX - canvas.getBoundingClientRect().left; + } +}); diff --git a/games/tiny-fishing/style.css b/games/tiny-fishing/style.css new file mode 100644 index 00000000..ce6e1840 --- /dev/null +++ b/games/tiny-fishing/style.css @@ -0,0 +1,45 @@ +body { + margin: 0; + overflow: hidden; + font-family: 'Poppins', sans-serif; + background: linear-gradient(#4ec0ca, #034078); + color: #fff; + display: flex; + flex-direction: column; + align-items: center; +} + +.ui { + position: absolute; + top: 20px; + text-align: center; + z-index: 10; +} + +button { + padding: 8px 14px; + background-color: #ffb703; + border: none; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + margin: 5px; +} + +button:hover { + background-color: #ffd166; +} + +canvas { + background: linear-gradient(#4ec0ca, #023047); + display: block; + border-top: 2px solid #fff; + margin-top: 120px; +} + +.stats { + display: flex; + gap: 20px; + justify-content: center; + margin-bottom: 10px; +} diff --git a/games/tiny-tower-tactics/index.html b/games/tiny-tower-tactics/index.html new file mode 100644 index 00000000..f6a57837 --- /dev/null +++ b/games/tiny-tower-tactics/index.html @@ -0,0 +1,28 @@ + + + + + + Tiny Tower Tactics - Mini JS Games Hub + + + +
      +

      Tiny Tower Tactics

      +
      + +
      +
      +
      + + +
      +

      Drag to position, tap to place blocks. Build the tallest tower!

      +

      Height: 0

      +

      Blocks: 0

      +
      +
      Made for Mini JS Games Hub
      +
      + + + \ No newline at end of file diff --git a/games/tiny-tower-tactics/screenshot.png b/games/tiny-tower-tactics/screenshot.png new file mode 100644 index 00000000..c48703ed --- /dev/null +++ b/games/tiny-tower-tactics/screenshot.png @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/games/tiny-tower-tactics/script.js b/games/tiny-tower-tactics/script.js new file mode 100644 index 00000000..ea686f05 --- /dev/null +++ b/games/tiny-tower-tactics/script.js @@ -0,0 +1,286 @@ +// Tiny Tower Tactics Game +const canvas = document.getElementById('game'); +const ctx = canvas.getContext('2d'); +const BASE_W = 400, BASE_H = 600, ASPECT = BASE_H / BASE_W; +let DPR = window.devicePixelRatio || 1; +let W = BASE_W, H = BASE_H; + +let frame = 0; +let gameState = 'menu'; // 'menu' | 'play' | 'paused' | 'over' +let score = 0; +let blocksPlaced = 0; +let tower = []; +let currentBlock = null; +let wobble = 0; +let wobbleDir = 1; +let isDragging = false; + +canvas.setAttribute('role', 'application'); +canvas.setAttribute('aria-label', 'Tiny Tower Tactics game canvas'); +canvas.tabIndex = 0; + +function resizeCanvas() { + DPR = window.devicePixelRatio || 1; + const container = canvas.parentElement || document.body; + const maxWidth = Math.min(window.innerWidth - 40, 450); + const cssWidth = Math.min(container.clientWidth - 24 || BASE_W, maxWidth); + const cssHeight = Math.round(cssWidth * ASPECT); + + canvas.style.width = cssWidth + 'px'; + canvas.style.height = cssHeight + 'px'; + + canvas.width = Math.round(cssWidth * DPR); + canvas.height = Math.round(cssHeight * DPR); + ctx.setTransform(DPR, 0, 0, DPR, 0, 0); + + W = cssWidth; + H = cssHeight; +} + +window.addEventListener('resize', resizeCanvas); +resizeCanvas(); + +const shapes = [ + {name: 'rect', points: [[0,0],[60,0],[60,20],[0,20]], color: '#3498db'}, + {name: 'L', points: [[0,0],[50,0],[50,10],[10,10],[10,20],[0,20]], color: '#e74c3c'}, + {name: 'T', points: [[0,0],[60,0],[60,10],[35,10],[35,20],[25,20],[25,10],[0,10]], color: '#2ecc71'}, + {name: 'Z', points: [[0,0],[20,0],[20,10],[40,10],[40,20],[20,20],[20,30],[0,30]], color: '#f39c12'} +]; + +class Block { + constructor(shape, x, y) { + this.shape = shape; + this.x = x; + this.y = y; + this.rotation = 0; + this.wobble = 0; + this.falling = false; + } + draw() { + ctx.save(); + ctx.translate(this.x + this.wobble, this.y); + ctx.fillStyle = this.shape.color; + ctx.strokeStyle = '#000'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(this.shape.points[0][0], this.shape.points[0][1]); + for (let i = 1; i < this.shape.points.length; i++) { + ctx.lineTo(this.shape.points[i][0], this.shape.points[i][1]); + } + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + ctx.restore(); + } + getBounds() { + let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; + for (const [px, py] of this.shape.points) { + minX = Math.min(minX, px + this.x); + maxX = Math.max(maxX, px + this.x); + minY = Math.min(minY, py + this.y); + maxY = Math.max(maxY, py + this.y); + } + return {minX, maxX, minY, maxY}; + } +} + +function reset() { + frame = 0; + score = 0; + blocksPlaced = 0; + tower = []; + wobble = 0; + wobbleDir = 1; + spawnBlock(); + gameState = 'play'; + document.getElementById('score').textContent = 'Height: 0'; + document.getElementById('blocks').textContent = 'Blocks: 0'; +} + +function spawnBlock() { + const shape = shapes[Math.floor(Math.random() * shapes.length)]; + currentBlock = new Block(shape, W / 2 - 30, 50); +} + +function update() { + if (gameState === 'play') { + frame++; + if (currentBlock) { + if (!isDragging) { + currentBlock.y += 2; + if (currentBlock.y > H - 100) { + placeBlock(); + } + } + } + // Update wobble + if (wobble > 0) { + wobble += wobbleDir * 0.1; + if (Math.abs(wobble) > 5) { + wobbleDir *= -1; + } + wobble *= 0.98; + if (Math.abs(wobble) < 0.1) wobble = 0; + } + // Check stability + if (tower.length > 0 && !checkStability()) { + wobble = 2; + if (Math.random() < 0.01) { // Chance to fall + gameState = 'over'; + } + } + } +} + +function checkStability() { + if (tower.length === 0) return true; + let totalX = 0, totalWeight = 0; + for (const block of tower) { + const bounds = block.getBounds(); + const weight = bounds.maxX - bounds.minX; + totalX += (bounds.minX + bounds.maxX) / 2 * weight; + totalWeight += weight; + } + const centerX = totalX / totalWeight; + const baseWidth = 100; // Assume base width + return centerX > W/2 - baseWidth/2 && centerX < W/2 + baseWidth/2; +} + +function placeBlock() { + if (currentBlock) { + tower.push(currentBlock); + blocksPlaced++; + score = Math.max(score, H - currentBlock.y); + currentBlock = null; + spawnBlock(); + document.getElementById('score').textContent = 'Height: ' + Math.floor(score / 10); + document.getElementById('blocks').textContent = 'Blocks: ' + blocksPlaced; + } +} + +function draw() { + ctx.clearRect(0, 0, W, H); + ctx.fillStyle = '#f0f0f0'; + ctx.fillRect(0, 0, W, H); + + // Draw ground + ctx.fillStyle = '#8B4513'; + ctx.fillRect(0, H - 50, W, 50); + + // Draw tower + for (const block of tower) { + block.wobble = wobble; + block.draw(); + } + + // Draw current block + if (currentBlock) { + currentBlock.draw(); + } + + if (gameState === 'menu') { + ctx.fillStyle = 'rgba(0,0,0,0.5)'; + ctx.fillRect(0, 0, W, H); + ctx.fillStyle = '#fff'; + ctx.font = '24px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Tap or press Space to start', W / 2, H / 2); + } + if (gameState === 'over') { + ctx.fillStyle = 'rgba(0,0,0,0.7)'; + ctx.fillRect(20, H / 2 - 60, W - 40, 120); + ctx.fillStyle = '#fff'; + ctx.font = '28px sans-serif'; + ctx.fillText('Tower Fell!', W / 2, H / 2 - 20); + ctx.font = '20px sans-serif'; + ctx.fillText('Height: ' + Math.floor(score / 10), W / 2, H / 2 + 10); + ctx.fillText('Blocks: ' + blocksPlaced, W / 2, H / 2 + 35); + } +} + +function loop() { + update(); + draw(); + requestAnimationFrame(loop); +} + +// Input +let mouseX = W / 2; +canvas.addEventListener('mousedown', e => { + if (gameState === 'menu') reset(); + else if (gameState === 'over') reset(); + else if (currentBlock) { + isDragging = true; + mouseX = e.offsetX; + } +}); + +canvas.addEventListener('mousemove', e => { + if (isDragging && currentBlock) { + mouseX = e.offsetX; + currentBlock.x = mouseX - 30; + currentBlock.x = Math.max(0, Math.min(W - 60, currentBlock.x)); + } +}); + +canvas.addEventListener('mouseup', () => { + if (isDragging) { + isDragging = false; + placeBlock(); + } +}); + +canvas.addEventListener('touchstart', e => { + e.preventDefault(); + if (gameState === 'menu') reset(); + else if (gameState === 'over') reset(); + else if (currentBlock) { + isDragging = true; + mouseX = e.touches[0].clientX - canvas.getBoundingClientRect().left; + } +}); + +canvas.addEventListener('touchmove', e => { + e.preventDefault(); + if (isDragging && currentBlock) { + mouseX = e.touches[0].clientX - canvas.getBoundingClientRect().left; + currentBlock.x = mouseX - 30; + currentBlock.x = Math.max(0, Math.min(W - 60, currentBlock.x)); + } +}); + +canvas.addEventListener('touchend', e => { + e.preventDefault(); + if (isDragging) { + isDragging = false; + placeBlock(); + } +}); + +canvas.addEventListener('keydown', e => { + if (e.code === 'Space') { + e.preventDefault(); + if (gameState === 'menu') reset(); + else if (gameState === 'over') reset(); + else placeBlock(); + } +}); + +// Buttons +document.getElementById('startBtn').addEventListener('click', () => { + if (gameState === 'menu' || gameState === 'over') reset(); +}); + +document.getElementById('pauseBtn').addEventListener('click', () => { + if (gameState === 'play') { + gameState = 'paused'; + document.getElementById('pauseBtn').setAttribute('aria-pressed', 'true'); + document.getElementById('pauseBtn').textContent = 'Resume'; + } else if (gameState === 'paused') { + gameState = 'play'; + document.getElementById('pauseBtn').setAttribute('aria-pressed', 'false'); + document.getElementById('pauseBtn').textContent = 'Pause'; + } +}); + +loop(); \ No newline at end of file diff --git a/games/tiny-tower-tactics/style.css b/games/tiny-tower-tactics/style.css new file mode 100644 index 00000000..a99841b7 --- /dev/null +++ b/games/tiny-tower-tactics/style.css @@ -0,0 +1,14 @@ +*{box-sizing:border-box;margin:0;padding:0} +body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;background:#f0f0f0;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px} +.game-wrap{background:#fff;border-radius:15px;padding:20px;text-align:center;box-shadow:0 10px 30px rgba(0,0,0,0.2);max-width:450px;width:100%} +h1{color:#333;margin-bottom:15px;font-size:1.8em} +canvas{background:#e8f4f8;display:block;margin:0 auto;border-radius:10px;max-width:100%;height:auto;border:2px solid #ccc} +.info{color:#333;margin-top:15px} +.controls{display:flex;gap:10px;justify-content:center;margin-bottom:10px} +button{padding:8px 16px;border:none;border-radius:8px;background:#007bff;color:#fff;font-size:14px;cursor:pointer;transition:all 0.3s ease} +button:hover{background:#0056b3} +footer{font-size:12px;color:#666;margin-top:15px} +@media (max-width: 600px) { + .game-wrap{padding:15px} + canvas{width:100%;height:auto} +} \ No newline at end of file diff --git a/games/todolist/index.html b/games/todolist/index.html new file mode 100644 index 00000000..86661856 --- /dev/null +++ b/games/todolist/index.html @@ -0,0 +1,54 @@ + + + + + + Chore RPG: Gamified To-Do List โš”๏ธ + + + +
      +

      Your Quest Log ๐Ÿ“œ

      + +
      +
      + Level: 1 +
      +
      + XP: 0 / 100 + +
      +
      + Gold: 0 ๐Ÿ’ฐ +
      +
      + +
      + +
      + + + +
      + +
        +
      + +
      + +
      +

      The General Store

      +

      + + +
      + +
      +

      Your Gear:

      +

      None

      +
      +
      + + + + \ No newline at end of file diff --git a/games/todolist/script.js b/games/todolist/script.js new file mode 100644 index 00000000..ae9ebf24 --- /dev/null +++ b/games/todolist/script.js @@ -0,0 +1,203 @@ +// --- 1. Game State and Configuration --- +const XP_BASE = 100; // Base XP needed for Level 2 +const XP_MULTIPLIER = 1.5; // XP needed for next level: Current XP * Multiplier + +let player = { + level: 1, + xp: 0, + gold: 0, + inventory: [] +}; + +let tasks = []; + +// --- 2. DOM Element References --- +const levelDisplay = document.getElementById('player-level'); +const xpDisplay = document.getElementById('player-xp'); +const xpToLevelDisplay = document.getElementById('xp-to-level'); +const xpProgress = document.getElementById('xp-progress'); +const goldDisplay = document.getElementById('player-gold'); +const taskList = document.getElementById('task-list'); + +const newTaskText = document.getElementById('new-task-text'); +const newTaskXP = document.getElementById('new-task-xp'); +const addTaskButton = document.getElementById('add-task-button'); + +const shopMessage = document.getElementById('shop-message'); +const inventoryDisplay = document.getElementById('inventory-display'); + +// --- 3. Persistence (Local Storage) Functions --- + +function loadGame() { + const savedPlayer = localStorage.getItem('choreRpgPlayer'); + const savedTasks = localStorage.getItem('choreRpgTasks'); + + if (savedPlayer) { + player = JSON.parse(savedPlayer); + } + if (savedTasks) { + tasks = JSON.parse(savedTasks); + } + + updateStatsUI(); + renderTasks(); +} + +function saveGame() { + localStorage.setItem('choreRpgPlayer', JSON.stringify(player)); + localStorage.setItem('choreRpgTasks', JSON.stringify(tasks)); +} + +// --- 4. Game Logic Functions --- + +function getXpNeeded(level) { + return Math.floor(XP_BASE * Math.pow(XP_MULTIPLIER, level - 1)); +} + +function checkLevelUp() { + let xpNeeded = getXpNeeded(player.level); + + while (player.xp >= xpNeeded) { + player.level++; + player.xp -= xpNeeded; // Carry over excess XP + xpNeeded = getXpNeeded(player.level); // Calculate XP for the *new* level + + shopMessage.textContent = `**LEVEL UP!** You are now Level ${player.level}!`; + // Optional: Grant bonus gold for leveling up + player.gold += player.level * 10; + } + updateStatsUI(); +} + +function updateStatsUI() { + const xpNeeded = getXpNeeded(player.level); + + levelDisplay.textContent = player.level; + xpDisplay.textContent = player.xp; + goldDisplay.textContent = player.gold; + xpToLevelDisplay.textContent = xpNeeded; + + xpProgress.max = xpNeeded; + xpProgress.value = player.xp; + + inventoryDisplay.textContent = player.inventory.length > 0 + ? player.inventory.join(', ') + : "None"; + + saveGame(); // Save state after every stat change +} + +function completeTask(taskIndex) { + const task = tasks[taskIndex]; + + // 1. Grant Rewards + player.xp += task.xp; + player.gold += task.gold; + + // 2. Check for Level Up + checkLevelUp(); + + // 3. Provide Feedback + shopMessage.textContent = `Quest Complete! You earned ${task.xp} XP and ${task.gold} Gold.`; + + // 4. Remove Task + tasks.splice(taskIndex, 1); + + // 5. Update UI + renderTasks(); + updateStatsUI(); +} + +// --- 5. DOM Manipulation and Rendering --- + +function addTask() { + const text = newTaskText.value.trim(); + const xpValue = parseInt(newTaskXP.value); + + if (text && xpValue > 0) { + const newTask = { + id: Date.now(), // Unique ID + text: text, + xp: xpValue, + gold: Math.ceil(xpValue / 2) // Gold is half the XP value + }; + + tasks.push(newTask); + newTaskText.value = ''; + renderTasks(); + saveGame(); + } +} + +function renderTasks() { + taskList.innerHTML = ''; // Clear the list + + if (tasks.length === 0) { + taskList.innerHTML = '

      Your quest log is empty! Time to add some tasks.

      '; + return; + } + + tasks.forEach((task, index) => { + const listItem = document.createElement('li'); + listItem.classList.add('task-item'); + + // Task Text + const textSpan = document.createElement('span'); + textSpan.classList.add('task-text'); + textSpan.textContent = task.text; + + // Reward Display + const rewardSpan = document.createElement('span'); + rewardSpan.classList.add('task-reward'); + rewardSpan.textContent = `(XP: ${task.xp}, G: ${task.gold})`; + + // Complete Button + const completeBtn = document.createElement('button'); + completeBtn.classList.add('complete-btn'); + completeBtn.textContent = 'Complete'; + completeBtn.onclick = () => completeTask(index); + + listItem.appendChild(textSpan); + listItem.appendChild(rewardSpan); + listItem.appendChild(completeBtn); + taskList.appendChild(listItem); + }); +} + +// --- 6. Shop Logic --- + +function buyUpgrade(event) { + const button = event.target; + const cost = parseInt(button.dataset.cost); + const itemName = button.textContent.split('(')[0].trim(); + + if (player.gold >= cost) { + if (!player.inventory.includes(itemName)) { + player.gold -= cost; + player.inventory.push(itemName); + shopMessage.textContent = `You bought a ${itemName}! It looks great!`; + } else { + shopMessage.textContent = `You already own the ${itemName}!`; + return; + } + } else { + shopMessage.textContent = `Not enough Gold! You need ${cost - player.gold} more.`; + return; + } + updateStatsUI(); // Re-render gold and inventory +} + + +// --- 7. Event Listeners and Initialization --- + +addTaskButton.addEventListener('click', addTask); +newTaskText.addEventListener('keypress', (e) => { + if (e.key === 'Enter') addTask(); +}); + +document.getElementById('buy-hat-button').addEventListener('click', buyUpgrade); +document.getElementById('buy-cloak-button').addEventListener('click', buyUpgrade); + + +// Initial load +loadGame(); \ No newline at end of file diff --git a/games/todolist/style.css b/games/todolist/style.css new file mode 100644 index 00000000..31724a52 --- /dev/null +++ b/games/todolist/style.css @@ -0,0 +1,187 @@ +body { + font-family: 'Georgia', serif; + background-color: #3b3a3d; /* Dark, textured background */ + color: #eee; + display: flex; + justify-content: center; + align-items: flex-start; + min-height: 100vh; + padding: 30px 15px; +} + +.app-container { + background: #555458; /* Slightly lighter gray for container */ + padding: 30px; + border-radius: 10px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + width: 90%; + max-width: 650px; + border: 3px solid #8d7350; /* Wood/leather trim */ +} + +h1, h2 { + color: #ffd700; /* Gold color for titles */ + text-shadow: 1px 1px 2px #000; + text-align: center; + margin-bottom: 20px; +} + +hr { + border: 0; + height: 1px; + background-image: linear-gradient(to right, rgba(255, 255, 255, 0), #8d7350, rgba(255, 255, 255, 0)); + margin: 25px 0; +} + +/* --- Stats Panel --- */ +.stats-panel { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0; + margin-bottom: 20px; + border-bottom: 1px dashed #777; +} + +.stat { + text-align: center; + font-weight: bold; + font-size: 1.1em; +} + +.level-stat #player-level { + color: #f00; /* Red for power */ + font-size: 1.5em; +} + +.gold-stat #player-gold { + color: #ffd700; + font-size: 1.2em; +} + +.xp-stat { + flex-grow: 1; + margin: 0 20px; + text-align: left; +} + +#xp-progress { + width: 100%; + height: 10px; + background: #333; + border: none; + border-radius: 5px; + margin-top: 5px; +} + +/* Style the progress bar fill */ +#xp-progress::-webkit-progress-value { background-color: #00ff00; } +#xp-progress::-moz-progress-bar { background-color: #00ff00; } + + +/* --- Task Input --- */ +.task-input-area { + display: flex; + gap: 10px; + margin-bottom: 25px; +} + +.task-input-area input { + padding: 10px; + border-radius: 5px; + border: 1px solid #777; + background-color: #444; + color: #eee; +} + +#new-task-text { + flex-grow: 1; +} + +#new-task-xp { + width: 80px; +} + +button { + padding: 10px 15px; + border: none; + border-radius: 5px; + background-color: #3f6e91; /* Blue/Steel color */ + color: white; + cursor: pointer; + transition: background-color 0.2s; + font-weight: bold; +} + +button:hover { + background-color: #305672; +} + + +/* --- Task List --- */ +#task-list { + list-style: none; + padding: 0; +} + +.task-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 15px; + margin-bottom: 8px; + background-color: #666; + border-radius: 6px; + border-left: 5px solid #00ff00; /* Highlight */ +} + +.task-text { + flex-grow: 1; + text-align: left; +} + +.task-reward { + color: #ffd700; + font-weight: bold; + margin-right: 15px; +} + +.complete-btn { + background-color: #00ff00; + color: #333; + padding: 5px 10px; + font-size: 0.9em; +} + +.complete-btn:hover { + background-color: #00cc00; +} + + +/* --- Shop and Inventory --- */ +.shop-section button { + margin: 5px; + background-color: #8d7350; + border: 1px solid #6c5843; +} +.shop-section button:hover { + background-color: #6c5843; +} + +#shop-message { + color: #ffd700; + font-style: italic; + margin-bottom: 10px; +} + +.inventory { + margin-top: 20px; + text-align: center; + padding: 15px; + background-color: #444; + border-radius: 6px; +} +#inventory-display { + color: #00ff00; + font-weight: bold; +} \ No newline at end of file diff --git a/games/tower-defense/index.html b/games/tower-defense/index.html new file mode 100644 index 00000000..cd10230d --- /dev/null +++ b/games/tower-defense/index.html @@ -0,0 +1,1179 @@ + + + + + + Neon Defense V3 - Extreme + + + + + + + +
      + + +
      +

      CORE SYSTEM

      + + +
      +

      HP: 20

      +

      CREDITS: 300

      +

      WAVE: 0 / 5

      +

      SCORE: 0

      +

      User ID: Loading...

      +
      + + +

      GLOBAL UPGRADES

      +
      + + + + +
      + + +

      TOWER DEPLOYMENT

      +
      + + + +
      + + +
      + + +
      + + + +
      + + +
      + +
      + +
      + + + + + + + + + + diff --git a/games/tower-of-hanoi/index.html b/games/tower-of-hanoi/index.html new file mode 100644 index 00000000..fe1331d2 --- /dev/null +++ b/games/tower-of-hanoi/index.html @@ -0,0 +1,53 @@ + + + + + + Tower of Hanoi Visualizer + + + +
      +
      +

      Tower of Hanoi Visualizer

      +

      Learn recursion & algorithms interactively

      +
      + +
      + + + +
      + + + + + + +
      + + + +

      Moves: 0 | Optimal: 0

      +
      + +
      +
      +

      Source

      +
      +
      +

      Auxiliary

      +
      +
      +

      Target

      +
      +
      + +
      +

      Tip: Use Auto Solve to watch the recursive solution, or drag disks manually (click top disk, then click target peg).

      +
      +
      + + + + diff --git a/games/tower-of-hanoi/script.js b/games/tower-of-hanoi/script.js new file mode 100644 index 00000000..eb55344d --- /dev/null +++ b/games/tower-of-hanoi/script.js @@ -0,0 +1,163 @@ +const pegs = [[], [], []]; +let diskCount = 3; +let moveCounter = 0; +let moves = []; +let moveIndex = 0; +let animationInterval; +let speed = 500; +let selectedDisk = null; + +const pegElems = [document.getElementById("peg-0"), document.getElementById("peg-1"), document.getElementById("peg-2")]; +const moveCounterElem = document.getElementById("move-counter"); +const optimalMovesElem = document.getElementById("optimal-moves"); +const diskNumberElem = document.getElementById("disk-number"); +const speedInput = document.getElementById("speed"); + +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const resumeBtn = document.getElementById("resume-btn"); +const resetBtn = document.getElementById("reset-btn"); +const stepForwardBtn = document.getElementById("step-forward-btn"); +const stepBackBtn = document.getElementById("step-back-btn"); + +function initGame() { + // Clear pegs + pegs.forEach((peg, i) => peg.length = 0); + + for (let i = diskCount; i >= 1; i--) { + pegs[0].push(i); + } + + moveCounter = 0; + moves = []; + moveIndex = 0; + updateOptimalMoves(); + renderPegs(); +} + +function updateOptimalMoves() { + optimalMovesElem.textContent = Math.pow(2, diskCount) - 1; +} + +function renderPegs() { + pegElems.forEach((pegElem, i) => { + pegElem.querySelectorAll(".disk").forEach(d => d.remove()); + pegs[i].forEach((diskSize, index) => { + const disk = document.createElement("div"); + disk.className = "disk"; + disk.style.width = `${20 + diskSize * 20}px`; + disk.style.left = `${(200 - (20 + diskSize * 20)) / 2}px`; + disk.style.bottom = `${index * 22}px`; + disk.style.backgroundColor = `hsl(${diskSize * 50}, 70%, 50%)`; + disk.dataset.size = diskSize; + disk.dataset.peg = i; + pegElem.appendChild(disk); + + disk.addEventListener("click", () => { + if (selectedDisk === null && pegs[i][pegs[i].length-1] === diskSize) { + selectedDisk = { size: diskSize, from: i }; + disk.style.transform = "translateY(-30px)"; + } + }); + }); + }); +} + +function moveDisk(from, to) { + if (pegs[from].length === 0) return false; + const disk = pegs[from][pegs[from].length-1]; + if (pegs[to].length === 0 || pegs[to][pegs[to].length-1] > disk) { + pegs[to].push(pegs[from].pop()); + moveCounter++; + moveCounterElem.textContent = moveCounter; + renderPegs(); + return true; + } + return false; +} + +function recordMoves(n, from, to, aux) { + if (n === 0) return; + recordMoves(n - 1, from, aux, to); + moves.push({from, to}); + recordMoves(n - 1, aux, to, from); +} + +function playMoves() { + if (moveIndex >= moves.length) { + clearInterval(animationInterval); + return; + } + const {from, to} = moves[moveIndex]; + moveDisk(from, to); + moveIndex++; +} + +startBtn.addEventListener("click", () => { + recordMoves(diskCount, 0, 2, 1); + startBtn.disabled = true; + pauseBtn.disabled = false; + stepForwardBtn.disabled = false; + stepBackBtn.disabled = false; + animationInterval = setInterval(playMoves, speed); +}); + +pauseBtn.addEventListener("click", () => { + clearInterval(animationInterval); + pauseBtn.disabled = true; + resumeBtn.disabled = false; +}); + +resumeBtn.addEventListener("click", () => { + animationInterval = setInterval(playMoves, speed); + pauseBtn.disabled = false; + resumeBtn.disabled = true; +}); + +resetBtn.addEventListener("click", () => { + clearInterval(animationInterval); + startBtn.disabled = false; + pauseBtn.disabled = true; + resumeBtn.disabled = true; + stepForwardBtn.disabled = true; + stepBackBtn.disabled = true; + initGame(); +}); + +stepForwardBtn.addEventListener("click", () => { + if (moveIndex < moves.length) { + const {from, to} = moves[moveIndex]; + moveDisk(from, to); + moveIndex++; + } +}); + +stepBackBtn.addEventListener("click", () => { + if (moveIndex > 0) { + moveIndex--; + const {from, to} = moves[moveIndex]; + // Reverse move + pegs[from].push(pegs[to].pop()); + moveCounter--; + moveCounterElem.textContent = moveCounter; + renderPegs(); + } +}); + +diskNumberElem.textContent = diskCount; +document.getElementById("disk-count").addEventListener("input", (e) => { + diskCount = parseInt(e.target.value); + diskNumberElem.textContent = diskCount; + initGame(); +}); + +speedInput.addEventListener("input", (e) => { + speed = parseInt(e.target.value); + if (animationInterval) { + clearInterval(animationInterval); + animationInterval = setInterval(playMoves, speed); + } +}); + +// Initialize +initGame(); diff --git a/games/tower-of-hanoi/style.css b/games/tower-of-hanoi/style.css new file mode 100644 index 00000000..329dfc1b --- /dev/null +++ b/games/tower-of-hanoi/style.css @@ -0,0 +1,101 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #1e3c72, #2a5298); + color: #fff; + margin: 0; + padding: 0; +} + +.container { + max-width: 1000px; + margin: 0 auto; + padding: 20px; + text-align: center; +} + +header h1 { + font-size: 2rem; + margin-bottom: 0.2rem; +} + +header p { + font-size: 1rem; + margin-top: 0; +} + +.controls { + margin: 20px 0; +} + +.controls label { + margin-right: 10px; +} + +.controls input[type="range"] { + margin: 5px; +} + +.buttons { + margin: 10px 0; +} + +button { + margin: 3px; + padding: 8px 14px; + border-radius: 5px; + border: none; + background-color: #ff7f50; + color: #fff; + cursor: pointer; + transition: background 0.2s ease; +} + +button:disabled { + background-color: #aaa; + cursor: not-allowed; +} + +button:hover:not(:disabled) { + background-color: #ff4500; +} + +.pegs { + display: flex; + justify-content: space-around; + margin-top: 30px; + height: 300px; + align-items: flex-end; +} + +.peg { + background-color: rgba(255,255,255,0.1); + width: 200px; + height: 100%; + position: relative; + border-radius: 5px 5px 0 0; + padding-top: 20px; +} + +.peg h3 { + position: absolute; + top: 0; + width: 100%; + text-align: center; + margin: 0; +} + +.disk { + position: absolute; + bottom: 0; + height: 20px; + margin-bottom: 2px; + border-radius: 5px; + cursor: pointer; + transition: all 0.3s ease; +} + +.notes { + margin-top: 30px; + font-size: 0.9rem; + color: #eee; +} diff --git a/games/traffic-light-controller/index.html b/games/traffic-light-controller/index.html new file mode 100644 index 00000000..1fee5383 --- /dev/null +++ b/games/traffic-light-controller/index.html @@ -0,0 +1,107 @@ + + + + + + Traffic Light Controller โ€” Mini JS Games Hub + + + + + +
      +
      +
      + traffic light +

      Traffic Light Controller

      +
      +
      +
      Score: 0
      +
      Collisions: 0
      +
      +
      + +
      +
      +
      + + + +
      + +
      + + + +
      + +
      + Tip: Click a traffic light to cycle Red โ†’ Green โ†’ Yellow. Use timing to avoid jams. +
      + +
      + + +
      +
      + +
      + +
      + +
      + +
      +
      +
      +
      + + +
      +
      +
      +
      + + +
      +
      +
      +
      +
      + +
      +
      +
      +
      +
      + + +
      + + + +
      +
      +
      + +
      +
      Made with โ™ฅ โ€” Advanced demo. Uses online assets & sounds.
      +
      Controls: Click lights, use Start/Pause/Restart, tune speed & difficulty.
      +
      +
      + + + + diff --git a/games/traffic-light-controller/script.js b/games/traffic-light-controller/script.js new file mode 100644 index 00000000..3c70c46e --- /dev/null +++ b/games/traffic-light-controller/script.js @@ -0,0 +1,375 @@ +/* Traffic Light Controller โ€” advanced demo + Place under games/traffic-light-controller/script.js + Uses online sounds and an advanced car-spawn + movement + collision system. +*/ + +(() => { + // ---- Config & assets (online) ---- + const SOUND_CLICK = "https://www.soundjay.com/button/sounds/button-16.mp3"; + const SOUND_HORN = "https://www.soundjay.com/transportation/sounds/car-horn-1.mp3"; + const SOUND_CRASH = "https://www.soundjay.com/transportation/sounds/car-crash-1.mp3"; + + // DOM + const startBtn = document.getElementById("startBtn"); + const pauseBtn = document.getElementById("pauseBtn"); + const restartBtn = document.getElementById("restartBtn"); + const addCarBtn = document.getElementById("addCar"); + const muteBtn = document.getElementById("muteBtn"); + const helpBtn = document.getElementById("helpBtn"); + + const speedRange = document.getElementById("speedRange"); + const difficultySelect = document.getElementById("difficulty"); + + const scoreEl = document.getElementById("score"); + const collisionsEl = document.getElementById("collisions"); + const carsContainer = document.getElementById("cars"); + + const overlay = document.getElementById("overlay"); + const overlayTitle = document.getElementById("overlayTitle"); + const overlayText = document.getElementById("overlayText"); + const overlayClose = document.getElementById("overlayClose"); + + const tlVertical = document.querySelector(".tl-vertical"); + const tlHorizontal = document.querySelector(".tl-horizontal"); + + // State + let running = false; + let paused = false; + let lastTimestamp = 0; + let spawnTimer = 0; + let spawnInterval = 1500; // ms + let gameSpeed = 1; + let difficulty = "normal"; + let muted = false; + + // Logical representation of lights + // 0 = red, 1 = green, 2 = yellow + const lights = { + vertical: 1, // initial green vertical + horizontal: 0, // initial red horizontal + }; + + // game stats + let score = 0; + let collisions = 0; + + // car list + const cars = []; + + // helper audio + function playSound(url, vol = 0.9) { + if (muted) return; + const a = new Audio(url); + a.volume = vol; + a.play().catch(() => {}); + } + + // Setup initial lights UI + function updateLightsUI() { + // vertical + const v = lights.vertical; + const h = lights.horizontal; + + // helper to set classes + const setBulbs = (el, state) => { + el.querySelectorAll(".bulb").forEach(b => b.classList.remove("on")); + if (state === 0) el.querySelector(".bulb.red").classList.add("on"); + if (state === 1) el.querySelector(".bulb.green").classList.add("on"); + if (state === 2) el.querySelector(".bulb.yellow").classList.add("on"); + }; + + setBulbs(tlVertical, v); + setBulbs(tlHorizontal, h); + } + + // Cycle a light (click handler). Also auto-set opposite (mutually exclusive) when green is set. + function cycleLight(orientation) { + playSound(SOUND_CLICK, 0.5); + lights[orientation] = (lights[orientation] + 1) % 3; + // safety: if set green, opposite must be red (no crossing green) + if (lights[orientation] === 1) { + const opp = orientation === "vertical" ? "horizontal" : "vertical"; + lights[opp] = 0; + } + updateLightsUI(); + } + + // add click listeners on lights (user can click) + tlVertical.addEventListener("click", () => cycleLight("vertical")); + tlHorizontal.addEventListener("click", () => cycleLight("horizontal")); + + // Car generation: cars have direction, position, speed, and DOM element + // Directions: 'down' (from top to center), 'up' (from bottom), 'right' (from left), 'left' (from right) + const spawnPositions = { + down: { x: "calc(50% - 26px)", y: "-60px", vx: 0, vy: 1, rotate: 0 }, + up: { x: "calc(50% - 26px)", y: "calc(100% + 60px)", vx: 0, vy: -1, rotate: 180 }, + right: { x: "-80px", y: "calc(50% - 15px)", vx: 1, vy: 0, rotate: 90 }, + left: { x: "calc(100% + 80px)", y: "calc(50% - 15px)", vx: -1, vy: 0, rotate: -90 } + }; + const directions = Object.keys(spawnPositions); + + function randomChoice(arr) { return arr[Math.floor(Math.random()*arr.length)]; } + function randomInt(min, max) { return Math.floor(Math.random()*(max-min+1))+min; } + + function createCar(manualDir) { + const dir = manualDir || randomChoice(directions); + const pos = spawnPositions[dir]; + + const carEl = document.createElement("div"); + carEl.className = "car type-" + randomChoice(["a","b","c","d"]); + carEl.style.left = pos.x; + carEl.style.top = pos.y; + carEl.style.transform = `rotate(${pos.rotate}deg)`; + carEl.innerHTML = `
      ${randomInt(1,99)}
      +
      `; + + carsContainer.appendChild(carEl); + + // compute numeric position (pixels) + const rect = carsContainer.getBoundingClientRect(); + let x = 0, y = 0; + // parse positions: better to set based on viewport dims + if (dir === "down") { x = rect.width/2 - 26; y = -80; } + if (dir === "up") { x = rect.width/2 - 26; y = rect.height + 80; } + if (dir === "right") { x = -100; y = rect.height/2 - 15; } + if (dir === "left") { x = rect.width + 100; y = rect.height/2 - 15; } + + // base speed depends on difficulty + let base = { easy: 80, normal: 120, hard: 170 }[difficulty]; + base *= (1 + (Math.random()*0.25 - 0.12)); // slight variation + + const car = { + el: carEl, + dir, + x, y, + vx: pos.vx, + vy: pos.vy, + speed: base * gameSpeed, + width: 52, + height: 30, + state: "moving", // 'moving', 'stopped', 'crossing' + id: Date.now() + Math.random() + }; + cars.push(car); + } + + // remove car + function removeCar(car) { + try { car.el.remove(); } catch(e){} + const i = cars.indexOf(car); + if (i >= 0) cars.splice(i,1); + } + + // check if a car should stop before intersection based on lights and orientation + function shouldStop(car) { + // define a region near center where cars must stop if light is red or yellow + // We'll compute distance from center depending on dir + const rect = carsContainer.getBoundingClientRect(); + const cx = rect.width/2, cy = rect.height/2; + const margin = 120 + (car.speed / 30); // stopping margin adjusts with speed + + if (car.dir === "down") { + // vertical green controls down/up + if (lights.vertical === 1) return false; + // if red or yellow, stop before center y - margin + if (car.y + car.height >= cy - margin && car.y < cy + margin) return true; + } + if (car.dir === "up") { + if (lights.vertical === 1) return false; + if (car.y <= cy + margin && car.y > cy - margin) return true; + } + if (car.dir === "right") { + if (lights.horizontal === 1) return false; + if (car.x + car.width >= cx - margin && car.x < cx + margin) return true; + } + if (car.dir === "left") { + if (lights.horizontal === 1) return false; + if (car.x <= cx + margin && car.x > cx - margin) return true; + } + return false; + } + + // collision detection between cars (simple AABB) + function detectCollisions() { + for (let i=0;i b.x && a.y < b.y + b.height && a.y + a.height > b.y) { + // collision! + handleCollision(a,b); + } + } + } + } + + function handleCollision(a,b) { + // remove both cars with crash effect + playSound(SOUND_CRASH, 0.6); + collisions++; + collisionsEl.textContent = collisions; + // visual flash + [a,b].forEach(c => { + if (!c) return; + c.el.style.transition = "transform .2s ease-out, opacity .4s"; + c.el.style.transform += " scale(.2) rotate(20deg)"; + c.el.style.opacity = 0; + setTimeout(()=> removeCar(c), 420); + }); + // small penalty to score + score = Math.max(0, score - 20); + scoreEl.textContent = score; + } + + // main loop + function tick(ts) { + if (!running || paused) { lastTimestamp = ts; requestAnimationFrame(tick); return; } + if(!lastTimestamp) lastTimestamp = ts; + const dt = (ts - lastTimestamp) / 1000; // seconds + lastTimestamp = ts; + + // spawn cars + spawnTimer += (ts - (lastTimestamp - dt*1000)); + // simplified spawn: use spawnInterval adjusted by difficulty & speed + spawnInterval = Math.max(700, 1500 / gameSpeed * (difficulty === "hard" ? 0.7 : difficulty === "easy" ? 1.3 : 1)); + if (Math.random() < (dt * (1.2 * gameSpeed)) * (difficulty === "hard" ? 1.5 : difficulty === "easy" ? 0.7 : 1)) { + createCar(); + } + + // update cars + const rect = carsContainer.getBoundingClientRect(); + const cx = rect.width/2, cy = rect.height/2; + + for (let i=cars.length-1;i>=0;i--){ + const car = cars[i]; + // update car speed (sync with slider & difficulty) + car.speed = (car.speed / gameSpeed) * gameSpeed; // keep base but mod by gameSpeed (no-op but safe) + // decide if should stop + const stop = shouldStop(car); + if (stop && car.state !== "stopped") { + car.state = "stopped"; + car.el.style.opacity = 0.9; + } else if (!stop && car.state === "stopped") { + // begin crossing + car.state = "crossing"; + playSound(SOUND_HORN, 0.12); + } else if (car.state === "crossing") { + // if crossing area passed, revert to moving + // crossing threshold depending on dir + if (car.dir === "down" && car.y > cy + 90) car.state = "moving"; + if (car.dir === "up" && car.y < cy - 90) car.state = "moving"; + if (car.dir === "right" && car.x > cx + 90) car.state = "moving"; + if (car.dir === "left" && car.x < cx - 90) car.state = "moving"; + } + + if (car.state !== "stopped") { + // velocity scaled by dt and gameSpeed + const spd = (car.speed / 100) * (gameSpeed); + car.x += car.vx * spd * dt * 60; + car.y += car.vy * spd * dt * 60; + } + + // update DOM + car.el.style.left = car.x + "px"; + car.el.style.top = car.y + "px"; + + // off-screen remove and reward + if (car.x < -160 || car.x > rect.width + 160 || car.y < -160 || car.y > rect.height + 160) { + // successful pass without collision -> +points + score += 10; + scoreEl.textContent = score; + removeCar(car); + } + } + + // collisions + detectCollisions(); + + requestAnimationFrame(tick); + } + + // ---- Controls ---- + startBtn.addEventListener("click", () => { + if (!running) { + running = true; paused = false; lastTimestamp = 0; + overlay.classList.add("hidden"); + startBtn.textContent = "Running"; + startBtn.classList.add("primary"); + requestAnimationFrame(tick); + } else if (paused) { + paused = false; overlay.classList.add("hidden"); + } + }); + + pauseBtn.addEventListener("click", () => { + paused = !paused; + overlay.classList.toggle("hidden", !paused); + overlayTitle.textContent = paused ? "Paused" : "Running"; + overlayText.textContent = paused ? "Game is paused. Click Start to resume." : "Running"; + }); + + restartBtn.addEventListener("click", () => { + playSound(SOUND_CLICK, 0.6); + // clear cars + cars.slice().forEach(c => removeCar(c)); + score = 0; collisions = 0; + scoreEl.textContent = score; collisionsEl.textContent = collisions; + running = false; paused = false; lastTimestamp = 0; + startBtn.textContent = "Start"; startBtn.classList.remove("primary"); + // reset lights + lights.vertical = 1; lights.horizontal = 0; updateLightsUI(); + overlay.classList.remove("hidden"); overlayTitle.textContent = "Restarted"; overlayText.textContent = "Press Start to begin a fresh session"; + }); + + addCarBtn.addEventListener("click", () => { + createCar(); playSound(SOUND_HORN, 0.12); + }); + + muteBtn.addEventListener("click", () => { + muted = !muted; + muteBtn.textContent = muted ? "๐Ÿ”‡ Muted" : "๐Ÿ”Š Mute"; + }); + + helpBtn.addEventListener("click", () => { + overlay.classList.remove("hidden"); + overlayTitle.textContent = "How to Play"; + overlayText.textContent = "Click on either traffic light to cycle it. Green lets the traffic through. Use timing to prevent jams and collisions. Use Speed & Difficulty to tune the challenge."; + }); + + overlayClose.addEventListener("click", () => { + overlay.classList.add("hidden"); + }); + + speedRange.addEventListener("input", (e) => { + gameSpeed = parseFloat(e.target.value); + }); + + difficultySelect.addEventListener("change", (e) => { + difficulty = e.target.value; + }); + + // save plays for Pro Badges when user hits Play (this page is the game; main hub tracks .play-button clicks) + // but we can also push an event to localStorage for tracking + function trackPlay() { + try { + const playData = JSON.parse(localStorage.getItem("gamePlays") || "{}"); + const name = "Traffic Light Controller"; + if (!playData[name]) playData[name] = { plays: 0, success: 0 }; + playData[name].plays += 1; + localStorage.setItem("gamePlays", JSON.stringify(playData)); + } catch (e) {} + } + + // start auto (for quick demo) track play + trackPlay(); + + // initialize UI + updateLightsUI(); + overlay.classList.remove("hidden"); + overlayTitle.textContent = "Welcome"; + overlayText.textContent = "Press Start to run the simulation. Tip: vertical starts green."; + + // initial spawn to show some activity + for(let i=0;i<2;i++) createCar(); + +})(); diff --git a/games/traffic-light-controller/style.css b/games/traffic-light-controller/style.css new file mode 100644 index 00000000..856c3a17 --- /dev/null +++ b/games/traffic-light-controller/style.css @@ -0,0 +1,97 @@ +:root{ + --bg:#0f1724; + --card:#0b1220; + --muted:#9aa4b2; + --accent:#00e676; + --glass: rgba(255,255,255,0.04); + --glow-red: rgba(255,80,80,0.9); + --glow-yellow: rgba(255,210,80,0.9); + --glow-green: rgba(80,255,120,0.9); + --glass-2: rgba(255,255,255,0.02); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; +} + +*{box-sizing:border-box} +html,body{height:100%;margin:0;background:linear-gradient(180deg,#071127 0%, #0b1420 100%);color:#dbe7f7} +.page{min-height:100vh;display:flex;flex-direction:column} +.topbar{ + display:flex;align-items:center;justify-content:space-between;padding:14px 22px;background:linear-gradient(90deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border-bottom:1px solid rgba(255,255,255,0.03); +} +.branding{display:flex;align-items:center;gap:12px} +.branding img{width:42px;height:42px;border-radius:8px} +.branding h1{margin:0;font-size:18px;letter-spacing:0.2px} +.meta{display:flex;gap:18px;align-items:center;font-weight:600} +.meta .score,.meta .lives{background:var(--glass);padding:8px 12px;border-radius:10px;color:var(--muted);font-weight:700} + +.game-area{display:flex;gap:18px;padding:18px;flex:1;align-items:flex-start;justify-content:center} +.controls{ + width:360px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border-radius:12px;padding:16px;box-shadow:0 8px 30px rgba(2,6,23,0.6);border:1px solid rgba(255,255,255,0.03) +} +.controls-row{display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap} +.controls-row.small{font-size:13px;color:var(--muted)} +.control{background:var(--glass-2);border:1px solid rgba(255,255,255,0.03);padding:8px 12px;border-radius:8px;cursor:pointer;color:#e6f2ff;font-weight:600} +.control.primary{background:linear-gradient(90deg,#00e676,#00bfa5);color:#022a16;border:none;box-shadow:0 6px 18px rgba(0,230,118,0.12),0 2px 6px rgba(0,0,0,0.3)} +.control:hover{transform:translateY(-1px)} +.controls label{display:flex;align-items:center;gap:8px;color:var(--muted);font-weight:600} + +.intersection-wrap{position:relative;width:920px;height:640px;border-radius:12px;overflow:hidden;box-shadow:0 20px 50px rgba(2,6,23,0.6);border:1px solid rgba(255,255,255,0.03)} +.city-bg{position:absolute;inset:0;background-size:cover;background-position:center;filter:grayscale(20%) blur(0.6px) brightness(.36);transform:scale(1.05)} +.intersection{position:relative;inset:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;pointer-events:auto} + +/* roads */ +.road{position:absolute;background:linear-gradient(#1f2937,#111827);box-shadow:inset 0 0 30px rgba(0,0,0,0.6);border-radius:4px} +.road.vertical{width:220px;height:100%;z-index:2;display:flex;flex-direction:column;justify-content:center;align-items:center} +.road.horizontal{height:220px;width:100%;z-index:2;display:flex;flex-direction:row;justify-content:center;align-items:center} +.lane{width:100%;height:45%;border-top:2px dashed rgba(255,255,255,0.06);border-bottom:2px dashed rgba(255,255,255,0.06);} + +/* intersection center mark */ +.intersection::after{ + content:'';position:absolute;width:220px;height:220px;border-radius:6px;z-index:3;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + box-shadow:inset 0 0 40px rgba(0,0,0,0.5) +} + +/* traffic lights */ +.traffic-light{ + position:absolute;width:60px;background:rgba(7,10,16,0.85);padding:8px;border-radius:12px;display:flex;flex-direction:column;gap:8px;align-items:center;justify-content:center;z-index:40;border:1px solid rgba(255,255,255,0.04); + cursor:pointer;transition:transform .18s; +} +.traffic-light:hover{transform:scale(1.04)} +.tl-vertical{right:calc(50% - 110px);top:calc(50% - 220px)} +.tl-horizontal{left:calc(50% - 220px);bottom:calc(50% - 110px);transform:rotate(90deg)} +.bulb{width:36px;height:36px;border-radius:50%;background:#111;box-shadow:inset 0 -6px 18px rgba(0,0,0,0.6);transition:box-shadow .18s, filter .18s, background .18s} +.bulb.red.on{background:radial-gradient(circle at 30% 25%, #ff8a8a, #ff2b2b);box-shadow:0 0 18px var(--glow-red)} +.bulb.yellow.on{background:radial-gradient(circle at 30% 25%, #ffe6a6, #ffd14f);box-shadow:0 0 18px var(--glow-yellow)} +.bulb.green.on{background:radial-gradient(circle at 30% 25%, #bffbc1, #3fff73);box-shadow:0 0 18px var(--glow-green)} + +/* cars */ +#cars{position:absolute;inset:0;pointer-events:none;z-index:10} +.car{ + position:absolute;width:52px;height:30px;border-radius:6px;display:flex;align-items:center;justify-content:center;color:#021018;font-weight:700;font-size:12px; + box-shadow:0 10px 18px rgba(2,6,23,0.5), inset 0 -6px 10px rgba(0,0,0,0.2); + transition:transform .06s linear; +} +.car .wheel{position:absolute;width:8px;height:8px;border-radius:50%;bottom:-5px;background:#0b0b0b;left:6px;box-shadow:0 2px 2px rgba(0,0,0,0.5)} +.car .wheel.r{left:auto;right:6px} +.car.type-a{background:linear-gradient(180deg,#ffd54a,#ffb300)} +.car.type-b{background:linear-gradient(180deg,#90caf9,#42a5f5)} +.car.type-c{background:linear-gradient(180deg,#ef9a9a,#e57373)} +.car.type-d{background:linear-gradient(180deg,#b39ddb,#9575cd)} + +/* overlay */ +.overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;z-index:80} +.overlay.hidden{display:none} +.overlay .overlay-card{background:rgba(2,6,23,0.8);padding:24px;border-radius:12px;border:1px solid rgba(255,255,255,0.04);box-shadow:0 12px 30px rgba(0,0,0,0.6);color:#eaf6ff} +.footer{padding:14px 22px;color:var(--muted);display:flex;justify-content:space-between;align-items:center;font-size:13px} + +/* responsive */ +@media (max-width:1100px){ + .intersection-wrap{width:92vw;height:56vh} + .controls{width:320px} +} +@media (max-width:800px){ + .game-area{flex-direction:column;align-items:center} + .controls{width:92%} + .intersection-wrap{width:92%;height:60vh} +} diff --git a/games/treasure-seeker/index.html b/games/treasure-seeker/index.html new file mode 100644 index 00000000..b6447eb3 --- /dev/null +++ b/games/treasure-seeker/index.html @@ -0,0 +1,45 @@ + + + + + + Treasure Seeker + + + +
      +

      ๐Ÿ’ฐ Treasure Seeker

      +

      Dig for treasure in the grid! Click cells to reveal what's hidden.

      + +
      +
      Level: 1
      +
      Score: 0
      +
      Treasures Left: 0
      +
      Time: 00:00
      +
      + +
      + +
      + + + +
      + +
      + +
      +

      How to Play:

      +
        +
      • Click on dirt cells to dig and reveal what's underneath
      • +
      • Find all the treasure chests to complete the level
      • +
      • Avoid the bombs - they end your digging!
      • +
      • Use shovels to reveal larger areas (costs points)
      • +
      • Complete levels quickly for bonus points
      • +
      +
      +
      + + + + \ No newline at end of file diff --git a/games/treasure-seeker/script.js b/games/treasure-seeker/script.js new file mode 100644 index 00000000..80d78494 --- /dev/null +++ b/games/treasure-seeker/script.js @@ -0,0 +1,253 @@ +// Treasure Seeker Game +// Dig through the grid to find hidden treasures while avoiding bombs + +// DOM elements +const gameBoard = document.getElementById('game-board'); +const levelEl = document.getElementById('current-level'); +const scoreEl = document.getElementById('current-score'); +const treasuresLeftEl = document.getElementById('remaining-treasures'); +const timerEl = document.getElementById('time-display'); +const messageEl = document.getElementById('message'); +const startBtn = document.getElementById('start-btn'); +const resetBtn = document.getElementById('reset-btn'); +const hintBtn = document.getElementById('hint-btn'); + +// Game constants +const GRID_SIZE = 10; +const TOTAL_CELLS = GRID_SIZE * GRID_SIZE; + +// Game variables +let level = 1; +let score = 0; +let gameGrid = []; +let revealedCells = 0; +let treasuresFound = 0; +let totalTreasures = 0; +let gameRunning = false; +let startTime; +let timerInterval; + +// Initialize game +function initGame() { + createGrid(); + placeItems(); + updateDisplay(); +} + +// Create the grid HTML +function createGrid() { + gameBoard.innerHTML = ''; + for (let i = 0; i < TOTAL_CELLS; i++) { + const cell = document.createElement('div'); + cell.className = 'cell'; + cell.dataset.index = i; + cell.addEventListener('click', () => revealCell(i)); + gameBoard.appendChild(cell); + } +} + +// Place treasures and bombs randomly +function placeItems() { + gameGrid = new Array(TOTAL_CELLS).fill('empty'); + + // Calculate number of items based on level + const numTreasures = Math.min(5 + level, 12); + const numBombs = Math.min(3 + Math.floor(level / 2), 8); + + totalTreasures = numTreasures; + + // Place treasures + let placed = 0; + while (placed < numTreasures) { + const index = Math.floor(Math.random() * TOTAL_CELLS); + if (gameGrid[index] === 'empty') { + gameGrid[index] = 'treasure'; + placed++; + } + } + + // Place bombs + placed = 0; + while (placed < numBombs) { + const index = Math.floor(Math.random() * TOTAL_CELLS); + if (gameGrid[index] === 'empty') { + gameGrid[index] = 'bomb'; + placed++; + } + } + + console.log(`Level ${level}: ${numTreasures} treasures, ${numBombs} bombs placed`); +} + +// Reveal a cell +function revealCell(index) { + if (!gameRunning) return; + + const cell = gameBoard.children[index]; + if (cell.classList.contains('revealed')) return; + + cell.classList.add('revealed'); + revealedCells++; + + const item = gameGrid[index]; + cell.textContent = getCellSymbol(item); + cell.classList.add(item); + + if (item === 'treasure') { + treasuresFound++; + score += 100 * level; + updateDisplay(); + + if (treasuresFound === totalTreasures) { + levelComplete(); + } + } else if (item === 'bomb') { + gameOver(); + } +} + +// Get symbol for cell content +function getCellSymbol(item) { + switch (item) { + case 'treasure': return '๐Ÿ’ฐ'; + case 'bomb': return '๐Ÿ’ฃ'; + default: return ''; + } +} + +// Use shovel hint +function useShovel() { + if (!gameRunning || score < 50) return; + + score -= 50; + scoreEl.textContent = score; + + // Find a random unrevealed cell + const unrevealed = []; + for (let i = 0; i < TOTAL_CELLS; i++) { + if (!gameBoard.children[i].classList.contains('revealed')) { + unrevealed.push(i); + } + } + + if (unrevealed.length === 0) return; + + const randomIndex = unrevealed[Math.floor(Math.random() * unrevealed.length)]; + revealCell(randomIndex); + + // Also reveal adjacent cells + const row = Math.floor(randomIndex / GRID_SIZE); + const col = randomIndex % GRID_SIZE; + + for (let dr = -1; dr <= 1; dr++) { + for (let dc = -1; dc <= 1; dc++) { + const newRow = row + dr; + const newCol = col + dc; + if (newRow >= 0 && newRow < GRID_SIZE && newCol >= 0 && newCol < GRID_SIZE) { + const adjacentIndex = newRow * GRID_SIZE + newCol; + if (!gameBoard.children[adjacentIndex].classList.contains('revealed')) { + gameBoard.children[adjacentIndex].classList.add('shovel-hint'); + setTimeout(() => { + gameBoard.children[adjacentIndex].classList.remove('shovel-hint'); + }, 1000); + } + } + } + } +} + +// Start the game +function startGame() { + level = 1; + score = 0; + treasuresFound = 0; + revealedCells = 0; + gameRunning = true; + startTime = Date.now(); + + initGame(); + startTimer(); + + startBtn.style.display = 'none'; + resetBtn.style.display = 'inline-block'; + + messageEl.textContent = 'Dig for treasure!'; +} + +// Level completed +function levelComplete() { + gameRunning = false; + clearInterval(timerInterval); + + const timeBonus = Math.max(0, 300 - Math.floor((Date.now() - startTime) / 1000)); + score += timeBonus; + + messageEl.textContent = `Level ${level} Complete! Time bonus: +${timeBonus}`; + + level++; + levelEl.textContent = level; + + setTimeout(() => { + messageEl.textContent = 'Get ready for next level...'; + setTimeout(() => { + startGame(); + }, 2000); + }, 3000); +} + +// Game over +function gameOver() { + gameRunning = false; + clearInterval(timerInterval); + + messageEl.textContent = '๐Ÿ’ฃ Boom! Game Over!'; + + // Reveal all bombs + for (let i = 0; i < TOTAL_CELLS; i++) { + if (gameGrid[i] === 'bomb') { + gameBoard.children[i].classList.add('revealed', 'bomb'); + gameBoard.children[i].textContent = '๐Ÿ’ฃ'; + } + } + + resetBtn.style.display = 'inline-block'; +} + +// Reset game +function resetGame() { + gameRunning = false; + clearInterval(timerInterval); + timerEl.textContent = '00:00'; + messageEl.textContent = ''; + startBtn.style.display = 'inline-block'; + resetBtn.style.display = 'none'; + initGame(); +} + +// Update display +function updateDisplay() { + scoreEl.textContent = score; + treasuresLeftEl.textContent = totalTreasures - treasuresFound; +} + +// Start timer +function startTimer() { + timerInterval = setInterval(() => { + const elapsed = Math.floor((Date.now() - startTime) / 1000); + const minutes = Math.floor(elapsed / 60).toString().padStart(2, '0'); + const seconds = (elapsed % 60).toString().padStart(2, '0'); + timerEl.textContent = `${minutes}:${seconds}`; + }, 1000); +} + +// Event listeners +startBtn.addEventListener('click', startGame); +resetBtn.addEventListener('click', resetGame); +hintBtn.addEventListener('click', useShovel); + +// Initialize on load +initGame(); + +// I had fun making this grid-based game +// The shovel hint feature adds a nice strategic element +// Maybe I'll add different shovel types later \ No newline at end of file diff --git a/games/treasure-seeker/style.css b/games/treasure-seeker/style.css new file mode 100644 index 00000000..54c6cc15 --- /dev/null +++ b/games/treasure-seeker/style.css @@ -0,0 +1,179 @@ +/* Treasure Seeker Game Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #8e44ad, #3498db); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + color: white; +} + +.container { + text-align: center; + max-width: 800px; + padding: 20px; +} + +h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +p { + font-size: 1.1em; + margin-bottom: 20px; + opacity: 0.9; +} + +.game-info { + display: flex; + justify-content: space-around; + flex-wrap: wrap; + margin: 20px 0; + font-size: 1.1em; + font-weight: bold; + background: rgba(255, 255, 255, 0.1); + padding: 15px; + border-radius: 10px; +} + +#game-board { + display: grid; + grid-template-columns: repeat(10, 1fr); + gap: 2px; + margin: 20px auto; + max-width: 500px; + background: #2c3e50; + padding: 10px; + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0,0,0,0.3); +} + +.cell { + width: 40px; + height: 40px; + background: #8B4513; + border: 1px solid #654321; + border-radius: 3px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + font-weight: bold; + transition: all 0.2s; + user-select: none; +} + +.cell:hover { + background: #A0522D; + transform: scale(1.05); +} + +.cell.revealed { + background: #DEB887; + cursor: default; +} + +.cell.treasure { + background: #FFD700; + color: #B8860B; +} + +.cell.bomb { + background: #DC143C; + color: white; +} + +.cell.shovel-hint { + background: #90EE90; + color: #228B22; +} + +.controls { + margin: 20px 0; +} + +button { + background: #27ae60; + color: white; + border: none; + padding: 12px 24px; + font-size: 1em; + border-radius: 5px; + cursor: pointer; + margin: 0 10px; + transition: background 0.3s; +} + +button:hover { + background: #229954; +} + +#hint-btn { + background: #f39c12; +} + +#hint-btn:hover { + background: #e67e22; +} + +#message { + font-size: 1.2em; + margin: 20px 0; + min-height: 30px; + font-weight: bold; +} + +.instructions { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + margin-top: 20px; + text-align: left; + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.instructions h3 { + margin-bottom: 10px; + color: #ffeaa7; +} + +.instructions ul { + list-style-type: disc; + padding-left: 20px; +} + +.instructions li { + margin: 5px 0; + line-height: 1.4; +} + +/* Responsive design */ +@media (max-width: 600px) { + #game-board { + grid-template-columns: repeat(8, 1fr); + max-width: 400px; + } + + .cell { + width: 35px; + height: 35px; + font-size: 16px; + } + + .game-info { + font-size: 1em; + } +} \ No newline at end of file diff --git a/games/trivia-master/index.html b/games/trivia-master/index.html new file mode 100644 index 00000000..620ecec9 --- /dev/null +++ b/games/trivia-master/index.html @@ -0,0 +1,90 @@ + + + + + + Trivia Master + + + +
      +

      ๐Ÿง  Trivia Master

      +

      Test your knowledge and become a trivia champion!

      + +
      +
      Score: 0
      +
      Question: 0/10
      +
      Time: 30s
      +
      Streak: 0
      +
      + +
      +
      +
      General Knowledge
      +
      Easy
      +
      + Click "Start Game" to begin your trivia challenge! +
      +
      +
      +
      +
      + +
      + + + + +
      +
      + +
      + + + + +
      + +
      + + + +
      +

      How to Play:

      +
        +
      • Answer trivia questions as quickly as possible
      • +
      • Each correct answer earns points based on speed and difficulty
      • +
      • Build a streak for bonus points
      • +
      • Use hints or skip questions (costs points)
      • +
      • You have 30 seconds per question
      • +
      • Complete all 10 questions to finish the game
      • +
      +
      +
      + + + + \ No newline at end of file diff --git a/games/trivia-master/script.js b/games/trivia-master/script.js new file mode 100644 index 00000000..96bd6e29 --- /dev/null +++ b/games/trivia-master/script.js @@ -0,0 +1,491 @@ +// Trivia Master Game +// Test your knowledge with timed trivia questions + +// DOM elements +const scoreEl = document.getElementById('current-score'); +const questionCountEl = document.getElementById('current-question'); +const totalQuestionsEl = document.getElementById('total-questions'); +const timerEl = document.getElementById('time-left'); +const streakEl = document.getElementById('current-streak'); +const categoryEl = document.getElementById('category'); +const difficultyEl = document.getElementById('difficulty'); +const questionTextEl = document.getElementById('question-text'); +const timerFillEl = document.getElementById('timer-fill'); +const answerBtns = document.querySelectorAll('.answer-btn'); +const startBtn = document.getElementById('start-btn'); +const hintBtn = document.getElementById('hint-btn'); +const skipBtn = document.getElementById('skip-btn'); +const quitBtn = document.getElementById('quit-btn'); +const messageEl = document.getElementById('message'); +const resultsEl = document.getElementById('results'); +const finalScoreEl = document.getElementById('final-score'); +const questionsAnsweredEl = document.getElementById('questions-answered'); +const correctAnswersEl = document.getElementById('correct-answers'); +const accuracyEl = document.getElementById('accuracy'); +const bestStreakEl = document.getElementById('best-streak'); +const gradeEl = document.getElementById('grade'); +const playAgainBtn = document.getElementById('play-again-btn'); + +// Game variables +let currentQuestionIndex = 0; +let score = 0; +let streak = 0; +let bestStreak = 0; +let timeLeft = 30; +let timerInterval = null; +let gameActive = false; +let questions = []; +let currentQuestion = null; +let hintUsed = false; + +// Trivia questions database +const triviaQuestions = [ + { + question: "What is the capital of France?", + answers: ["London", "Berlin", "Paris", "Madrid"], + correct: 2, + category: "Geography", + difficulty: "easy" + }, + { + question: "Which planet is known as the Red Planet?", + answers: ["Venus", "Mars", "Jupiter", "Saturn"], + correct: 1, + category: "Science", + difficulty: "easy" + }, + { + question: "Who painted the Mona Lisa?", + answers: ["Vincent van Gogh", "Pablo Picasso", "Leonardo da Vinci", "Michelangelo"], + correct: 2, + category: "Art", + difficulty: "medium" + }, + { + question: "What is the largest ocean on Earth?", + answers: ["Atlantic Ocean", "Indian Ocean", "Arctic Ocean", "Pacific Ocean"], + correct: 3, + category: "Geography", + difficulty: "easy" + }, + { + question: "In which year did World War II end?", + answers: ["1944", "1945", "1946", "1947"], + correct: 1, + category: "History", + difficulty: "medium" + }, + { + question: "What is the chemical symbol for gold?", + answers: ["Go", "Gd", "Au", "Ag"], + correct: 2, + category: "Science", + difficulty: "medium" + }, + { + question: "Which country is known as the Land of the Rising Sun?", + answers: ["China", "Japan", "Thailand", "South Korea"], + correct: 1, + category: "Geography", + difficulty: "medium" + }, + { + question: "Who wrote 'Romeo and Juliet'?", + answers: ["Charles Dickens", "William Shakespeare", "Jane Austen", "Mark Twain"], + correct: 1, + category: "Literature", + difficulty: "medium" + }, + { + question: "What is the smallest prime number?", + answers: ["0", "1", "2", "3"], + correct: 2, + category: "Mathematics", + difficulty: "easy" + }, + { + question: "Which element has the atomic number 1?", + answers: ["Helium", "Hydrogen", "Lithium", "Beryllium"], + correct: 1, + category: "Science", + difficulty: "easy" + }, + { + question: "What is the longest river in the world?", + answers: ["Amazon River", "Nile River", "Yangtze River", "Mississippi River"], + correct: 1, + category: "Geography", + difficulty: "hard" + }, + { + question: "Who was the first president of the United States?", + answers: ["Thomas Jefferson", "John Adams", "George Washington", "Benjamin Franklin"], + correct: 2, + category: "History", + difficulty: "easy" + }, + { + question: "What is the square root of 144?", + answers: ["10", "11", "12", "13"], + correct: 2, + category: "Mathematics", + difficulty: "easy" + }, + { + question: "Which planet is closest to the Sun?", + answers: ["Venus", "Earth", "Mercury", "Mars"], + correct: 2, + category: "Science", + difficulty: "easy" + }, + { + question: "In which year was the first iPhone released?", + answers: ["2006", "2007", "2008", "2009"], + correct: 1, + category: "Technology", + difficulty: "hard" + }, + { + question: "What is the largest mammal in the world?", + answers: ["African Elephant", "Blue Whale", "Giraffe", "Polar Bear"], + correct: 1, + category: "Science", + difficulty: "medium" + }, + { + question: "Who directed the movie 'Inception'?", + answers: ["Steven Spielberg", "Christopher Nolan", "Martin Scorsese", "Quentin Tarantino"], + correct: 1, + category: "Entertainment", + difficulty: "hard" + }, + { + question: "What is the capital of Australia?", + answers: ["Sydney", "Melbourne", "Canberra", "Brisbane"], + correct: 2, + category: "Geography", + difficulty: "medium" + }, + { + question: "Which programming language was created by Guido van Rossum?", + answers: ["Java", "C++", "Python", "JavaScript"], + correct: 2, + category: "Technology", + difficulty: "hard" + }, + { + question: "What is the hardest natural substance on Earth?", + answers: ["Gold", "Iron", "Diamond", "Platinum"], + correct: 2, + category: "Science", + difficulty: "medium" + } +]; + +// Initialize game +function initGame() { + shuffleQuestions(); + setupEventListeners(); + updateDisplay(); +} + +// Shuffle questions for random order +function shuffleQuestions() { + questions = [...triviaQuestions]; + for (let i = questions.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [questions[i], questions[j]] = [questions[j], questions[i]]; + } + // Take first 10 questions + questions = questions.slice(0, 10); +} + +// Setup event listeners +function setupEventListeners() { + startBtn.addEventListener('click', startGame); + hintBtn.addEventListener('click', useHint); + skipBtn.addEventListener('click', skipQuestion); + quitBtn.addEventListener('click', endGame); + playAgainBtn.addEventListener('click', resetGame); + + answerBtns.forEach(btn => { + btn.addEventListener('click', () => selectAnswer(btn)); + }); +} + +// Start the game +function startGame() { + gameActive = true; + currentQuestionIndex = 0; + score = 0; + streak = 0; + bestStreak = 0; + + startBtn.style.display = 'none'; + quitBtn.style.display = 'inline-block'; + hintBtn.disabled = false; + skipBtn.disabled = false; + + resultsEl.style.display = 'none'; + messageEl.textContent = ''; + + loadQuestion(); +} + +// Load current question +function loadQuestion() { + if (currentQuestionIndex >= questions.length) { + endGame(); + return; + } + + currentQuestion = questions[currentQuestionIndex]; + hintUsed = false; + + // Update UI + questionTextEl.textContent = currentQuestion.question; + categoryEl.textContent = currentQuestion.category; + difficultyEl.textContent = currentQuestion.difficulty.charAt(0).toUpperCase() + currentQuestion.difficulty.slice(1); + + // Set difficulty color + difficultyEl.className = 'difficulty-badge ' + currentQuestion.difficulty; + + // Update answers + answerBtns.forEach((btn, index) => { + const answerText = btn.querySelector('.answer-text'); + answerText.textContent = currentQuestion.answers[index]; + btn.classList.remove('selected', 'correct', 'incorrect'); + btn.style.display = 'flex'; + }); + + // Reset timer + timeLeft = 30; + timerEl.textContent = timeLeft; + timerFillEl.style.width = '100%'; + timerFillEl.classList.remove('warning'); + + // Start timer + startTimer(); + + updateDisplay(); +} + +// Start question timer +function startTimer() { + if (timerInterval) clearInterval(timerInterval); + + timerInterval = setInterval(() => { + timeLeft--; + timerEl.textContent = timeLeft; + timerFillEl.style.width = (timeLeft / 30) * 100 + '%'; + + if (timeLeft <= 10) { + timerFillEl.classList.add('warning'); + } + + if (timeLeft <= 0) { + clearInterval(timerInterval); + timeUp(); + } + }, 1000); +} + +// Handle time up +function timeUp() { + messageEl.textContent = 'Time\'s up! The correct answer was: ' + getCorrectAnswerText(); + showCorrectAnswer(); + streak = 0; + setTimeout(nextQuestion, 3000); +} + +// Select answer +function selectAnswer(btn) { + if (!gameActive) return; + + clearInterval(timerInterval); + + const selectedIndex = parseInt(btn.dataset.answer.charCodeAt(0) - 65); // A=0, B=1, etc. + const correctIndex = currentQuestion.correct; + + // Mark selected answer + btn.classList.add('selected'); + + if (selectedIndex === correctIndex) { + // Correct answer + btn.classList.add('correct'); + correctAnswer(); + } else { + // Incorrect answer + btn.classList.add('incorrect'); + showCorrectAnswer(); + incorrectAnswer(); + } + + setTimeout(nextQuestion, 2000); +} + +// Handle correct answer +function correctAnswer() { + streak++; + if (streak > bestStreak) bestStreak = streak; + + // Calculate points based on time and difficulty + let points = 10; // Base points + + // Time bonus + if (timeLeft >= 25) points += 10; // Very fast + else if (timeLeft >= 20) points += 5; // Fast + else if (timeLeft >= 10) points += 2; // Decent + + // Difficulty bonus + if (currentQuestion.difficulty === 'hard') points *= 2; + else if (currentQuestion.difficulty === 'medium') points *= 1.5; + + // Streak bonus + if (streak >= 3) points += streak * 2; + + score += Math.round(points); + + messageEl.textContent = `Correct! +${Math.round(points)} points (Streak: ${streak})`; +} + +// Handle incorrect answer +function incorrectAnswer() { + streak = 0; + messageEl.textContent = 'Incorrect! The correct answer was: ' + getCorrectAnswerText(); +} + +// Show correct answer +function showCorrectAnswer() { + const correctBtn = answerBtns[currentQuestion.correct]; + correctBtn.classList.add('correct'); +} + +// Get correct answer text +function getCorrectAnswerText() { + return currentQuestion.answers[currentQuestion.correct]; +} + +// Use hint +function useHint() { + if (!gameActive || hintUsed || score < 50) return; + + if (score < 50) { + messageEl.textContent = 'Not enough points for hint!'; + setTimeout(() => messageEl.textContent = '', 2000); + return; + } + + score -= 50; + hintUsed = true; + + // Remove two incorrect answers + const incorrectIndices = []; + for (let i = 0; i < 4; i++) { + if (i !== currentQuestion.correct) { + incorrectIndices.push(i); + } + } + + // Shuffle and remove two + incorrectIndices.sort(() => Math.random() - 0.5); + for (let i = 0; i < 2; i++) { + answerBtns[incorrectIndices[i]].style.display = 'none'; + } + + updateDisplay(); + messageEl.textContent = 'Hint used! Two incorrect answers removed.'; +} + +// Skip question +function skipQuestion() { + if (!gameActive || score < 25) return; + + if (score < 25) { + messageEl.textContent = 'Not enough points to skip!'; + setTimeout(() => messageEl.textContent = '', 2000); + return; + } + + clearInterval(timerInterval); + score -= 25; + streak = 0; + + updateDisplay(); + messageEl.textContent = 'Question skipped!'; + setTimeout(nextQuestion, 1500); +} + +// Next question +function nextQuestion() { + currentQuestionIndex++; + loadQuestion(); +} + +// End game +function endGame() { + gameActive = false; + clearInterval(timerInterval); + + // Show results + showResults(); +} + +// Show final results +function showResults() { + const correctAnswers = questions.slice(0, currentQuestionIndex).filter((q, index) => { + // This is a simplified check - in a real game you'd track answers + return true; // Placeholder + }).length; + + const accuracy = currentQuestionIndex > 0 ? Math.round((correctAnswers / currentQuestionIndex) * 100) : 0; + + finalScoreEl.textContent = score.toLocaleString(); + questionsAnsweredEl.textContent = currentQuestionIndex; + correctAnswersEl.textContent = correctAnswers; + accuracyEl.textContent = accuracy + '%'; + bestStreakEl.textContent = bestStreak; + + // Calculate grade + let grade = 'F'; + if (accuracy >= 90) grade = 'A'; + else if (accuracy >= 80) grade = 'B'; + else if (accuracy >= 70) grade = 'C'; + else if (accuracy >= 60) grade = 'D'; + + gradeEl.textContent = 'Grade: ' + grade; + gradeEl.className = 'grade ' + grade; + + resultsEl.style.display = 'block'; + startBtn.style.display = 'none'; + quitBtn.style.display = 'none'; + hintBtn.disabled = true; + skipBtn.disabled = true; +} + +// Reset game +function resetGame() { + resultsEl.style.display = 'none'; + startBtn.style.display = 'inline-block'; + quitBtn.style.display = 'none'; + hintBtn.disabled = true; + skipBtn.disabled = true; + + shuffleQuestions(); + updateDisplay(); + messageEl.textContent = 'Ready for another round?'; +} + +// Update display elements +function updateDisplay() { + scoreEl.textContent = score.toLocaleString(); + questionCountEl.textContent = currentQuestionIndex + 1; + streakEl.textContent = streak; +} + +// Start the game +initGame(); + +// This trivia game includes timing, hints, and scoring +// Questions are randomized and cover multiple categories +// Players can use lifelines and track their progress \ No newline at end of file diff --git a/games/trivia-master/style.css b/games/trivia-master/style.css new file mode 100644 index 00000000..ac3cc920 --- /dev/null +++ b/games/trivia-master/style.css @@ -0,0 +1,385 @@ +/* Trivia Master Game Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #667eea, #764ba2); + min-height: 100vh; + color: white; +} + +.container { + max-width: 900px; + margin: 0 auto; + padding: 20px; +} + +h1 { + font-size: 2.5em; + text-align: center; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); + color: #ffd700; +} + +p { + text-align: center; + font-size: 1.1em; + margin-bottom: 20px; + opacity: 0.9; +} + +.game-stats { + display: flex; + justify-content: space-around; + flex-wrap: wrap; + margin: 20px 0; + font-size: 1.1em; + font-weight: bold; + background: rgba(255, 215, 0, 0.1); + padding: 15px; + border-radius: 10px; + border: 2px solid #ffd700; +} + +.game-area { + margin: 20px 0; +} + +.question-section { + background: rgba(255, 255, 255, 0.1); + border-radius: 15px; + padding: 25px; + margin-bottom: 20px; + border: 2px solid #ffd700; + position: relative; +} + +.category-badge, .difficulty-badge { + position: absolute; + top: 15px; + padding: 5px 12px; + border-radius: 20px; + font-size: 0.8em; + font-weight: bold; + text-transform: uppercase; +} + +.category-badge { + left: 15px; + background: #3498db; + color: white; +} + +.difficulty-badge { + right: 15px; + color: white; +} + +.difficulty-badge.easy { background: #27ae60; } +.difficulty-badge.medium { background: #f39c12; } +.difficulty-badge.hard { background: #e74c3c; } + +.question { + font-size: 1.4em; + line-height: 1.4; + margin: 40px 0 20px 0; + text-align: center; + min-height: 80px; + display: flex; + align-items: center; + justify-content: center; +} + +.timer-bar { + width: 100%; + height: 8px; + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; + overflow: hidden; + margin-top: 15px; +} + +.timer-fill { + height: 100%; + background: linear-gradient(90deg, #27ae60, #ffd700, #e74c3c); + width: 100%; + transition: width 1s linear; +} + +.timer-fill.warning { + background: linear-gradient(90deg, #e74c3c, #c0392b); +} + +.answers-section { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; +} + +.answer-btn { + background: rgba(255, 255, 255, 0.1); + border: 2px solid #ddd; + border-radius: 10px; + padding: 15px 20px; + font-size: 1.1em; + cursor: pointer; + transition: all 0.3s; + display: flex; + align-items: center; + gap: 15px; + text-align: left; +} + +.answer-btn:hover { + background: rgba(255, 255, 255, 0.2); + border-color: #ffd700; + transform: translateY(-2px); +} + +.answer-btn.selected { + background: #3498db; + border-color: #3498db; + color: white; +} + +.answer-btn.correct { + background: #27ae60; + border-color: #27ae60; + color: white; + animation: correctAnswer 0.6s ease-out; +} + +.answer-btn.incorrect { + background: #e74c3c; + border-color: #e74c3c; + color: white; + animation: incorrectAnswer 0.6s ease-out; +} + +.answer-letter { + background: rgba(255, 255, 255, 0.2); + width: 30px; + height: 30px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + flex-shrink: 0; +} + +.answer-text { + flex: 1; +} + +.controls { + display: flex; + justify-content: center; + gap: 15px; + margin: 20px 0; + flex-wrap: wrap; +} + +button { + background: #3498db; + color: white; + border: none; + padding: 12px 24px; + font-size: 1em; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s; + font-weight: bold; +} + +button:hover { + background: #2980b9; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.3); +} + +button:disabled { + background: #666; + cursor: not-allowed; + transform: none; +} + +#start-btn { + background: #27ae60; + font-size: 1.2em; + padding: 15px 30px; +} + +#start-btn:hover { + background: #229954; +} + +#hint-btn { + background: #f39c12; +} + +#hint-btn:hover { + background: #e67e22; +} + +#skip-btn { + background: #9b59b6; +} + +#skip-btn:hover { + background: #8e44ad; +} + +#quit-btn { + background: #e74c3c; +} + +#quit-btn:hover { + background: #c0392b; +} + +#message { + font-size: 1.2em; + margin: 20px 0; + min-height: 30px; + font-weight: bold; + color: #ffd700; + text-align: center; +} + +.game-results { + background: rgba(255, 255, 255, 0.1); + border-radius: 15px; + padding: 30px; + text-align: center; + border: 2px solid #ffd700; + margin: 20px 0; +} + +.game-results h2 { + color: #ffd700; + margin-bottom: 20px; +} + +.final-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + margin-bottom: 20px; +} + +.stat { + background: rgba(255, 255, 255, 0.1); + padding: 10px; + border-radius: 8px; + border: 1px solid #ffd700; +} + +.grade { + font-size: 1.5em; + font-weight: bold; + margin: 20px 0; + padding: 10px; + border-radius: 8px; + display: inline-block; +} + +.grade.A { background: #27ae60; color: white; } +.grade.B { background: #f39c12; color: white; } +.grade.C { background: #e67e22; color: white; } +.grade.D { background: #e74c3c; color: white; } +.grade.F { background: #c0392b; color: white; } + +#play-again-btn { + background: #3498db; + font-size: 1.1em; + padding: 12px 30px; +} + +#play-again-btn:hover { + background: #2980b9; +} + +.instructions { + background: rgba(255, 255, 255, 0.1); + padding: 20px; + border-radius: 10px; + margin-top: 20px; + text-align: left; + max-width: 800px; + margin-left: auto; + margin-right: auto; +} + +.instructions h3 { + margin-bottom: 10px; + color: #ffd700; + text-align: center; +} + +.instructions ul { + list-style-type: disc; + padding-left: 20px; +} + +.instructions li { + margin: 5px 0; + line-height: 1.4; +} + +/* Animations */ +@keyframes correctAnswer { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +@keyframes incorrectAnswer { + 0% { transform: scale(1); background: #e74c3c; } + 25% { transform: translateX(-5px); } + 50% { transform: translateX(5px); } + 75% { transform: translateX(-5px); } + 100% { transform: translateX(0); } +} + +/* Responsive design */ +@media (max-width: 768px) { + .answers-section { + grid-template-columns: 1fr; + } + + .answer-btn { + padding: 12px 15px; + font-size: 1em; + } + + .question { + font-size: 1.2em; + min-height: 60px; + } + + .controls { + flex-direction: column; + align-items: center; + } + + button { + width: 100%; + max-width: 200px; + } + + .final-stats { + grid-template-columns: 1fr; + } + + .game-stats { + font-size: 0.9em; + padding: 10px; + } +} \ No newline at end of file diff --git a/games/trivia-showdown/index.html b/games/trivia-showdown/index.html new file mode 100644 index 00000000..5281f714 --- /dev/null +++ b/games/trivia-showdown/index.html @@ -0,0 +1,51 @@ + + + + + + Trivia Showdown | Mini JS Games Hub + + + +
      +

      Trivia Showdown

      + +
      + + + +
      + + +
      + + + + diff --git a/games/trivia-showdown/script.js b/games/trivia-showdown/script.js new file mode 100644 index 00000000..88260e4b --- /dev/null +++ b/games/trivia-showdown/script.js @@ -0,0 +1,132 @@ +const startBtn = document.getElementById("start-btn"); +const categorySelect = document.getElementById("category"); +const gameSection = document.querySelector(".game-section"); +const questionEl = document.getElementById("question"); +const answersEl = document.getElementById("answers"); +const nextBtn = document.getElementById("next-btn"); +const restartBtn = document.getElementById("restart-btn"); +const playerNameEl = document.getElementById("player-name"); +const playerScoreEl = document.getElementById("player-score"); +const timerEl = document.getElementById("timer"); +const resultEl = document.getElementById("result"); +const finalScoreEl = document.getElementById("final-score"); + +let questions = []; +let currentQuestionIndex = 0; +let score = 0; +let timeLeft = 15; +let timerInterval; + +const questionBank = { + general: [ + {q:"What is the capital of France?", a:["Paris","London","Berlin","Rome"], correct:"Paris"}, + {q:"Which ocean is the largest?", a:["Atlantic","Indian","Pacific","Arctic"], correct:"Pacific"} + ], + science: [ + {q:"What planet is known as the Red Planet?", a:["Mars","Earth","Jupiter","Venus"], correct:"Mars"}, + {q:"What gas do plants absorb?", a:["Oxygen","Carbon Dioxide","Nitrogen","Helium"], correct:"Carbon Dioxide"} + ], + history: [ + {q:"Who was the first President of the USA?", a:["George Washington","Abraham Lincoln","Thomas Jefferson","John Adams"], correct:"George Washington"}, + {q:"In which year did World War II end?", a:["1945","1939","1918","1965"], correct:"1945"} + ], + movies: [ + {q:"Who directed 'Jurassic Park'?", a:["Steven Spielberg","James Cameron","Christopher Nolan","Peter Jackson"], correct:"Steven Spielberg"}, + {q:"Which movie won Best Picture in 2020?", a:["Parasite","1917","Joker","Ford v Ferrari"], correct:"Parasite"} + ], + sports: [ + {q:"How many players in a soccer team?", a:["11","9","10","12"], correct:"11"}, + {q:"Which country won FIFA World Cup 2018?", a:["France","Croatia","Brazil","Germany"], correct:"France"} + ] +}; + +function startGame() { + const selectedCategory = categorySelect.value; + questions = [...questionBank[selectedCategory]]; + currentQuestionIndex = 0; + score = 0; + timeLeft = 15; + gameSection.hidden = false; + resultEl.hidden = true; + updateScore(); + displayQuestion(); +} + +function displayQuestion() { + resetState(); + if(currentQuestionIndex >= questions.length){ + endGame(); + return; + } + const currentQuestion = questions[currentQuestionIndex]; + questionEl.textContent = currentQuestion.q; + currentQuestion.a.forEach(answer => { + const button = document.createElement("button"); + button.textContent = answer; + button.addEventListener("click", selectAnswer); + answersEl.appendChild(button); + }); + startTimer(); +} + +function resetState(){ + clearInterval(timerInterval); + nextBtn.disabled = true; + answersEl.innerHTML = ""; + timeLeft = 15; + timerEl.textContent = timeLeft; +} + +function startTimer(){ + timerInterval = setInterval(()=>{ + timeLeft--; + timerEl.textContent = timeLeft; + if(timeLeft<=0){ + clearInterval(timerInterval); + nextBtn.disabled = false; + Array.from(answersEl.children).forEach(btn=>{ + btn.disabled = true; + if(btn.textContent === questions[currentQuestionIndex].correct){ + btn.style.backgroundColor = "#74ebd5"; + } + }); + } + },1000); +} + +function selectAnswer(e){ + clearInterval(timerInterval); + const selectedBtn = e.target; + const correct = questions[currentQuestionIndex].correct; + if(selectedBtn.textContent === correct){ + score++; + selectedBtn.style.backgroundColor = "#74ebd5"; + } else { + selectedBtn.style.backgroundColor = "red"; + Array.from(answersEl.children).forEach(btn=>{ + if(btn.textContent===correct){ + btn.style.backgroundColor = "#74ebd5"; + } + }); + } + updateScore(); + Array.from(answersEl.children).forEach(btn=>btn.disabled=true); + nextBtn.disabled = false; +} + +function updateScore(){ + playerScoreEl.textContent = score; +} + +function endGame(){ + gameSection.hidden = true; + resultEl.hidden = false; + finalScoreEl.textContent = `Your final score is: ${score} / ${questions.length}`; +} + +startBtn.addEventListener("click", startGame); +nextBtn.addEventListener("click", ()=>{ + currentQuestionIndex++; + displayQuestion(); +}); +restartBtn.addEventListener("click", startGame); diff --git a/games/trivia-showdown/style.css b/games/trivia-showdown/style.css new file mode 100644 index 00000000..42d5cf5d --- /dev/null +++ b/games/trivia-showdown/style.css @@ -0,0 +1,92 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(to right, #74ebd5, #ACB6E5); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.trivia-container { + background: #fff; + padding: 30px 50px; + border-radius: 12px; + width: 90%; + max-width: 600px; + text-align: center; + box-shadow: 0 10px 25px rgba(0,0,0,0.2); +} + +h1 { + margin-bottom: 20px; +} + +.category-select { + margin-bottom: 20px; +} + +.category-select select, .category-select button { + padding: 8px 12px; + margin-left: 10px; + font-size: 16px; + border-radius: 6px; + border: 1px solid #ccc; +} + +.player-info { + display: flex; + justify-content: space-around; + margin-bottom: 20px; + font-weight: bold; +} + +.question-section h2 { + margin-bottom: 20px; +} + +.answers-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; +} + +.answers-grid button { + padding: 12px; + font-size: 16px; + border-radius: 8px; + border: none; + cursor: pointer; + background-color: #e0e0e0; + transition: background 0.3s; +} + +.answers-grid button:hover { + background-color: #74ebd5; +} + +.controls { + margin-top: 20px; +} + +.controls button { + padding: 10px 20px; + font-size: 16px; + margin: 0 5px; + border-radius: 6px; + border: none; + cursor: pointer; + background-color: #ACB6E5; + color: #fff; + transition: background 0.3s; +} + +.controls button:hover { + background-color: #74ebd5; +} + +.result { + margin-top: 20px; + font-size: 18px; + font-weight: bold; +} diff --git a/games/tug-of-war/index.html b/games/tug-of-war/index.html new file mode 100644 index 00000000..245b6f64 --- /dev/null +++ b/games/tug-of-war/index.html @@ -0,0 +1,67 @@ + + + + + + Tug of War โ€” Mini JS Games Hub + + + +
      +
      +

      ๐Ÿณ๏ธ Tug of War

      +
      + + + + +
      +
      + +
      +
      +
      Left
      +
      Tap โ—€ (A)
      +
      +
      +
      +
      + +
      +
      +
      +
      +
      +
      +
      +
      0
      +
      0
      +
      +
      + +
      +
      Right
      +
      Tap โ–ถ (L)
      +
      +
      +
      +
      +
      + +
      +

      Controls โ€” Left: press A or click left zone. Right: press L or click right zone.

      +

      First to move the marker fully into opponent zone wins.

      +
      + + + + +
      + + + + diff --git a/games/tug-of-war/script.js b/games/tug-of-war/script.js new file mode 100644 index 00000000..388207c9 --- /dev/null +++ b/games/tug-of-war/script.js @@ -0,0 +1,222 @@ +// Tug of War โ€” script.js +// Two-player local tapping or keyboard controls (A for Left, L for Right). +// Features: start/pause/restart, sound toggle, visual glow, and win handling. + +// DOM +const startBtn = document.getElementById('startBtn'); +const pauseBtn = document.getElementById('pauseBtn'); +const restartBtn = document.getElementById('restartBtn'); +const leftTap = document.getElementById('leftTap'); +const rightTap = document.getElementById('rightTap'); +const rope = document.getElementById('rope'); +const marker = document.getElementById('marker'); +const leftScoreEl = document.getElementById('leftScore'); +const rightScoreEl = document.getElementById('rightScore'); +const cheerAudio = document.getElementById('cheer-audio'); +const battleAudio = document.getElementById('battle-audio'); +const soundToggle = document.getElementById('soundToggle'); + +let gameRunning = false; +let paused = false; +let leftPower = 0; +let rightPower = 0; +let markerPos = 0.5; // normalized 0 to 1 (0 = full left, 1 = full right) +let lastMove = null; +let leftScore = 0; +let rightScore = 0; +let lastTapTime = 0; +let tickInterval = null; +const TICK_MS = 16; +const TAP_BOOST = 0.015; // per tap movement influence +const DECAY = 0.995; // slight decay so rapid taps required +const WIN_THRESHOLD = 0.05; // how close to edge to win +const MAX_SCORE = 1; // unused but helps logic + +// small in-browser tap sound using WebAudio (so user doesn't need to download) +const AudioCtx = window.AudioContext || window.webkitAudioContext; +let audioCtx = null; +function playTapSound() { + if (!soundToggle.checked) return; + if (!audioCtx) audioCtx = new AudioCtx(); + const o = audioCtx.createOscillator(); + const g = audioCtx.createGain(); + o.type = 'sine'; + o.frequency.value = 550 + Math.random()*80; + g.gain.value = 0.08; + o.connect(g); g.connect(audioCtx.destination); + o.start(); + setTimeout(()=>{ g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime+0.06); }, 20); + setTimeout(()=> o.stop(), 120); +} +function playWinSound() { + if (!soundToggle.checked) return; + // crowd cheer via hosted file + try { cheerAudio.currentTime = 0; cheerAudio.play(); } catch(e){} + try { battleAudio.currentTime = 0; battleAudio.play(); } catch(e){} +} + +// update UI +function updateMarker() { + const trackWidth = rope.clientWidth; + const leftPct = markerPos * 100; + marker.style.left = leftPct + '%'; + // update glow meters + const leftGlow = document.querySelector('.meter-left .glow'); + const rightGlow = document.querySelector('.meter-right .glow'); + if (leftGlow) leftGlow.style.width = Math.max(8, (0.5 + (0.5 - markerPos)) * 100) + '%'; + if (rightGlow) rightGlow.style.width = Math.max(8, (0.5 + (markerPos - 0.5)) * 100) + '%'; + + // marker glow based on last move + marker.classList.remove('marker-glow-left','marker-glow-right'); + if (lastMove === 'left') marker.classList.add('marker-glow-left'); + if (lastMove === 'right') marker.classList.add('marker-glow-right'); +} + +function updateScores() { + leftScoreEl.textContent = leftScore; + rightScoreEl.textContent = rightScore; +} + +// main tick: move marker according to powers +function tick() { + if (!gameRunning || paused) return; + // combine powers + // amplify short rapid taps by decay+boost pattern + leftPower *= DECAY; + rightPower *= DECAY; + const net = (leftPower - rightPower); + // move marker + markerPos += net; + markerPos = Math.max(0, Math.min(1, markerPos)); + updateMarker(); + + // check win + if (markerPos <= WIN_THRESHOLD) { + // left wins + leftScore += 1; + finishRound('left'); + } else if (markerPos >= 1 - WIN_THRESHOLD) { + rightScore += 1; + finishRound('right'); + } +} + +// round end +function finishRound(winner) { + gameRunning = false; + paused = false; + // visual banner + const shell = document.querySelector('.game-shell'); + shell.classList.remove('win-left','win-right'); + if (winner === 'left') { + shell.classList.add('win-left'); + renderBanner('LEFT WINS!'); + } else { + shell.classList.add('win-right'); + renderBanner('RIGHT WINS!'); + } + updateScores(); + playWinSound(); + clearInterval(tickInterval); + tickInterval = null; +} + +// show temporary banner +function renderBanner(text) { + // remove existing + const existing = document.querySelector('.win-banner'); + if (existing) existing.remove(); + const banner = document.createElement('div'); + banner.className = 'win-banner'; + banner.textContent = text; + document.querySelector('.rope-track').appendChild(banner); + setTimeout(()=> banner.remove(), 2500); +} + +// game controls +function startGame() { + if (gameRunning) return; + // reset powers but keep marker at center + leftPower = 0; rightPower = 0; + lastMove = null; + gameRunning = true; + paused = false; + if (!tickInterval) tickInterval = setInterval(tick, TICK_MS); +} + +function pauseGame() { + paused = !paused; + pauseBtn.textContent = paused ? 'Resume' : 'Pause'; +} + +function restartGame(fullReset = false) { + // clear visual classes + const shell = document.querySelector('.game-shell'); + shell.classList.remove('win-left','win-right'); + // reset + markerPos = 0.5; + leftPower = 0; rightPower = 0; + lastMove = null; + gameRunning = false; + paused = false; + pauseBtn.textContent = 'Pause'; + if (tickInterval) { clearInterval(tickInterval); tickInterval = null; } + updateMarker(); + if (fullReset) { leftScore = 0; rightScore = 0; updateScores(); } +} + +// tapping handlers +function handleLeftTap() { + if (!gameRunning) return; + playTapSound(); + leftPower += TAP_BOOST + Math.random()*0.01; + lastMove = 'left'; +} +function handleRightTap() { + if (!gameRunning) return; + playTapSound(); + rightPower += TAP_BOOST + Math.random()*0.01; + lastMove = 'right'; +} + +// key controls +document.addEventListener('keydown', (e) => { + if (e.key.toLowerCase() === 'a') { + handleLeftTap(); + } else if (e.key.toLowerCase() === 'l') { + handleRightTap(); + } else if (e.key === ' ') { + // space toggles start/pause quickly + if (!gameRunning) startGame(); + else pauseGame(); + } +}); + +// click/touch zones +leftTap.addEventListener('pointerdown', ()=> { + handleLeftTap(); +}); +rightTap.addEventListener('pointerdown', ()=> { + handleRightTap(); +}); + +// buttons +startBtn.addEventListener('click', ()=> startGame()); +pauseBtn.addEventListener('click', ()=> pauseGame()); +restartBtn.addEventListener('click', ()=> restartGame(false)); + +// initial render +updateMarker(); +updateScores(); + +// export a small API to parent window for hub tracking (if desired) +window.tugOfWarGame = { + start: startGame, + pause: pauseGame, + restart: restartGame +}; + +// when page unload, stop audio contexts +window.addEventListener('beforeunload', ()=> { + if (audioCtx && audioCtx.close) audioCtx.close(); +}); diff --git a/games/tug-of-war/style.css b/games/tug-of-war/style.css new file mode 100644 index 00000000..7a46fc97 --- /dev/null +++ b/games/tug-of-war/style.css @@ -0,0 +1,198 @@ +:root{ + --bg:#0f1724; + --card:#0b1220; + --accent-left:#3bd1ff; + --accent-right:#ff7b7b; + --muted:#9aa7bf; + --glass: rgba(255,255,255,0.03); + --glow-left: 0 0 30px rgba(59,209,255,0.25), 0 0 8px rgba(59,209,255,0.12) inset; + --glow-right:0 0 30px rgba(255,123,123,0.22), 0 0 8px rgba(255,123,123,0.12) inset; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; +} + +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + background: radial-gradient(circle at 10% 20%, rgba(59,209,255,0.03), transparent 120px), + radial-gradient(circle at 90% 80%, rgba(255,123,123,0.02), transparent 120px), + var(--bg); + color:#e6eef8; + display:flex; + align-items:center; + justify-content:center; + padding:28px; +} + +/* game shell */ +.game-shell{ + width:980px; + max-width:calc(100% - 40px); + background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border:1px solid rgba(255,255,255,0.04); + border-radius:14px; + padding:18px; + box-shadow: 0 10px 30px rgba(2,6,23,0.6); + overflow:hidden; +} + +/* header */ +.game-header{ + display:flex; + align-items:center; + justify-content:space-between; + gap:16px; + margin-bottom:18px; +} +.game-header h1{margin:0;font-size:20px} +.controls{display:flex;gap:10px;align-items:center} +.btn{ + background:var(--glass); + border:1px solid rgba(255,255,255,0.04); + padding:8px 12px; + border-radius:8px; + color:var(--muted); + cursor:pointer; + font-weight:600; + transition:transform .08s ease, box-shadow .12s ease; +} +.btn:hover{transform:translateY(-2px)} +.btn.primary{ + color:#061025; + background:linear-gradient(90deg,var(--accent-left),var(--accent-right)); + box-shadow: 0 6px 18px rgba(12,20,30,0.5), 0 0 20px rgba(255,255,255,0.03) inset; +} + +/* toggle */ +.switch{display:flex;align-items:center;gap:8px;margin-left:6px} +.switch input{display:none} +.switch .slider{ + width:40px;height:22px;background:rgba(255,255,255,0.06);border-radius:30px;position:relative;cursor:pointer; + box-shadow:inset 0 -3px 6px rgba(0,0,0,0.4); +} +.switch input:checked + .slider{background:linear-gradient(90deg,var(--accent-left),var(--accent-right))} +.switch .slider::after{ + content:"";position:absolute;left:3px;top:3px;width:16px;height:16px;border-radius:50%;background:#fff;transition:left .14s ease; +} +.switch input:checked + .slider::after{left:21px} +.switch .label-text{font-size:13px;color:var(--muted);margin-left:6px} + +/* arena */ +.arena{ + display:grid; + grid-template-columns: 1fr 520px 1fr; + gap:16px; + align-items:stretch; + padding:6px; +} + +/* team panels */ +.team{ + background:linear-gradient(180deg, rgba(255,255,255,0.01), rgba(255,255,255,0.0)); + border-radius:10px; + padding:14px; + display:flex; + flex-direction:column; + align-items:center; + gap:14px; + border:1px solid rgba(255,255,255,0.03); +} +.team__name{font-weight:700;color:var(--muted)} +.tap-zone{ + width:100%; + background:rgba(255,255,255,0.02); + border-radius:8px; + padding:16px; + text-align:center; + font-weight:700; + cursor:pointer; + user-select:none; + transition:transform .06s ease, box-shadow .08s ease; +} +.tap-zone:active{transform:translateY(1px)} +.meter{ + width:100%; + height:12px; + background:linear-gradient(90deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border-radius:8px; + overflow:hidden; + position:relative; +} +.meter .glow{ + position:absolute;left:0;top:0;bottom:0;width:50%;background:linear-gradient(90deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + transform-origin:left center; +} + +/* right/left specific */ +.team-left .meter .glow{background:linear-gradient(90deg,var(--accent-left), rgba(59,209,255,0.12))} +.team-right .meter .glow{background:linear-gradient(90deg,var(--accent-right), rgba(255,123,123,0.12)); transform-origin:right; right:0; left:auto} + +/* rope area */ +.rope-area{ + display:flex; + flex-direction:column; + gap:10px; + align-items:center; + justify-content:center; + padding:6px; +} +.rope-track{ + width:100%; + height:120px; + background:linear-gradient(180deg, rgba(255,255,255,0.01), rgba(255,255,255,0.02)); + border-radius:12px; + display:flex; + align-items:center; + justify-content:center; + position:relative; + border:1px solid rgba(255,255,255,0.03); + overflow:hidden; +} + +/* rope visuals */ +.rope{ + width:78%; + height:12px; + background: repeating-linear-gradient(90deg, #cfae7a 0 8px, #8b6e4b 8px 16px); + border-radius:8px; + position:relative; + box-shadow: 0 8px 20px rgba(0,0,0,0.4), 0 0 40px rgba(0,0,0,0.15) inset; +} +.center-marker{ + position:absolute; + top:50%; + left:50%; + transform:translate(-50%,-50%); + width:26px;height:26px;border-radius:50%; + background:linear-gradient(180deg,#fff,#e6eef8); + box-shadow: 0 6px 18px rgba(2,6,23,0.5); + border:3px solid rgba(0,0,0,0.14); + z-index:4; + transition:left .07s linear, transform .12s ease; +} + +/* scoreboard */ +.scoreboard{display:flex;gap:12px;margin-top:8px} +.score{padding:6px 12px;border-radius:8px;background:rgba(255,255,255,0.02);color:var(--muted);font-weight:700} + +/* footer */ +.game-footer{margin-top:12px;display:flex;flex-direction:column;gap:6px} +.hint{margin:0;color:var(--muted);font-size:13px} + +/* marker glow when moving */ +.marker-glow-left{box-shadow: var(--glow-left);} +.marker-glow-right{box-shadow: var(--glow-right);} + +/* winners visual */ +.win-left .rope{box-shadow: 0 0 60px rgba(59,209,255,0.18), 0 6px 30px rgba(6,23,37,0.7) inset} +.win-right .rope{box-shadow: 0 0 60px rgba(255,123,123,0.12), 0 6px 30px rgba(6,23,37,0.7) inset} +.win-banner{ + position:absolute;left:50%;top:14px;transform:translateX(-50%);padding:8px 16px;border-radius:999px;background:linear-gradient(90deg,var(--accent-left),var(--accent-right));font-weight:800;color:#061025;box-shadow:0 6px 24px rgba(0,0,0,0.55) +} + +/* responsive */ +@media (max-width:880px){ + .arena{grid-template-columns:1fr;grid-auto-rows:auto} + .rope-track{height:90px} + .game-shell{padding:14px} +} diff --git a/games/two_zero_four_eight/index.html b/games/two_zero_four_eight/index.html new file mode 100644 index 00000000..51a6a53a --- /dev/null +++ b/games/two_zero_four_eight/index.html @@ -0,0 +1,15 @@ + + + + + + 2048 Mini + + + +

      ๐Ÿ”ข 2048 Mini

      +
      +

      + + + diff --git a/games/two_zero_four_eight/readme.md b/games/two_zero_four_eight/readme.md new file mode 100644 index 00000000..ddf42a15 --- /dev/null +++ b/games/two_zero_four_eight/readme.md @@ -0,0 +1,49 @@ +# 2048 Mini + +## Game Details +**Name:** +2048 Mini + +**Description:** +A simplified web-based version of the popular 2048 puzzle game! +Combine matching numbered tiles by pressing arrow keys (โฌ†๏ธโฌ‡๏ธโฌ…๏ธโžก๏ธ). +When two tiles with the same number touch, they merge into one with double the value. +Your goal? **Reach the 2048 tile!** ๐Ÿ† + +--- + +## Files Included +- [x] `index.html` โ€“ The game layout and grid setup. +- [x] `style.css` โ€“ Handles tile styling, grid colors, and animations. +- [x] `script.js` โ€“ Core logic for tile movement, merging, random number generation, and win/loss detection. + +--- + +## Additional Notes +- Use your **arrow keys** to move tiles up, down, left, or right. +- After every move, a new tile (2 or 4) appears at a random empty spot. +- The game ends when there are **no possible moves** left. +- You can customize: + - Tile size and color in `style.css`. + - Starting tiles or merge rules in `script.js`. +- Tip ๐Ÿ’ก: To restart, simply **refresh the page**! + +--- + +## Features +- Simple responsive 4x4 grid design. +- Smooth merging animations. +- Scoreboard tracking your current score. +- Minimal, clean, and fast โ€” perfect for browsers. + +--- + +## How to Play +1. Open `index.html` in your browser. +2. Use arrow keys to slide tiles. +3. Merge tiles strategically to reach **2048**. +4. Donโ€™t get stuck โ€” plan ahead! + +--- + +_Built with HTML, CSS, and JavaScript by Vidhi Rohira_ diff --git a/games/two_zero_four_eight/script.js b/games/two_zero_four_eight/script.js new file mode 100644 index 00000000..4f28414d --- /dev/null +++ b/games/two_zero_four_eight/script.js @@ -0,0 +1,99 @@ +const boardSize = 4; +let board = []; +const boardContainer = document.getElementById('game-board'); +const statusText = document.getElementById('status'); + +function initBoard() { + board = Array.from({ length: boardSize }, () => Array(boardSize).fill(0)); + addRandomTile(); + addRandomTile(); + drawBoard(); +} + +function drawBoard() { + boardContainer.innerHTML = ''; + board.flat().forEach(value => { + const tile = document.createElement('div'); + tile.className = 'tile'; + if (value !== 0) { + tile.textContent = value; + tile.dataset.value = value; + } + boardContainer.appendChild(tile); + }); +} + +function addRandomTile() { + const empty = []; + for (let r = 0; r < boardSize; r++) { + for (let c = 0; c < boardSize; c++) { + if (board[r][c] === 0) empty.push({ r, c }); + } + } + if (empty.length === 0) return; + const { r, c } = empty[Math.floor(Math.random() * empty.length)]; + board[r][c] = Math.random() < 0.9 ? 2 : 4; +} + +function move(direction) { + let rotated = false; + let moved = false; + + if (direction === 'up') board = rotateLeft(board); + if (direction === 'down') board = rotateRight(board); + if (direction === 'right') board = board.map(row => row.reverse()); + + const newBoard = board.map(row => { + const filtered = row.filter(v => v); + for (let i = 0; i < filtered.length - 1; i++) { + if (filtered[i] === filtered[i + 1]) { + filtered[i] *= 2; + filtered[i + 1] = 0; + } + } + const compacted = filtered.filter(v => v); + while (compacted.length < boardSize) compacted.push(0); + if (JSON.stringify(compacted) !== JSON.stringify(row)) moved = true; + return compacted; + }); + + board = newBoard; + + if (direction === 'up') board = rotateRight(board); + if (direction === 'down') board = rotateLeft(board); + if (direction === 'right') board = board.map(row => row.reverse()); + + if (moved) { + addRandomTile(); + drawBoard(); + checkGameOver(); + } +} + +function rotateLeft(matrix) { + return matrix[0].map((_, i) => matrix.map(row => row[row.length - 1 - i])); +} + +function rotateRight(matrix) { + return matrix[0].map((_, i) => matrix.map(row => row[i])).reverse(); +} + +function checkGameOver() { + for (let r = 0; r < boardSize; r++) { + for (let c = 0; c < boardSize; c++) { + if (board[r][c] === 0) return; + if (c < boardSize - 1 && board[r][c] === board[r][c + 1]) return; + if (r < boardSize - 1 && board[r][c] === board[r + 1][c]) return; + } + } + statusText.textContent = 'Game Over ๐Ÿ˜ญ'; +} + +document.addEventListener('keydown', e => { + if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) { + e.preventDefault(); + move(e.key.replace('Arrow', '').toLowerCase()); + } +}); + +initBoard(); diff --git a/games/two_zero_four_eight/style.css b/games/two_zero_four_eight/style.css new file mode 100644 index 00000000..edf6dc1c --- /dev/null +++ b/games/two_zero_four_eight/style.css @@ -0,0 +1,55 @@ +body { + background: linear-gradient(135deg, #ff9966, #ff5e62); + color: #fff; + font-family: 'Poppins', sans-serif; + text-align: center; + margin: 0; + padding: 0; +} + +h1 { + margin: 20px 0; +} + +#game-board { + width: 320px; + height: 320px; + background: #bbada0; + margin: 30px auto; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; + padding: 10px; + border-radius: 10px; +} + +.tile { + width: 70px; + height: 70px; + border-radius: 8px; + background: #cdc1b4; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + font-weight: bold; + color: #776e65; + transition: all 0.2s; +} + +.tile[data-value="2"] { background: #eee4da; } +.tile[data-value="4"] { background: #ede0c8; } +.tile[data-value="8"] { background: #f2b179; color: #f9f6f2; } +.tile[data-value="16"] { background: #f59563; color: #f9f6f2; } +.tile[data-value="32"] { background: #f67c5f; color: #f9f6f2; } +.tile[data-value="64"] { background: #f65e3b; color: #f9f6f2; } +.tile[data-value="128"] { background: #edcf72; color: #f9f6f2; } +.tile[data-value="256"] { background: #edcc61; color: #f9f6f2; } +.tile[data-value="512"] { background: #edc850; color: #f9f6f2; } +.tile[data-value="1024"] { background: #edc53f; color: #f9f6f2; } +.tile[data-value="2048"] { background: #edc22e; color: #f9f6f2; } + +#status { + margin-top: 20px; + font-size: 1.2rem; +} diff --git a/games/tykoon/index.html b/games/tykoon/index.html new file mode 100644 index 00000000..dcc5f957 --- /dev/null +++ b/games/tykoon/index.html @@ -0,0 +1,37 @@ + + + + + Factory Builder + + + + +
      + +
      +

      ๐Ÿ’ฐ Resources

      +
      Money: $500.00
      +
      Ore: 0.0
      +
      Ingots: 0.0
      +
      Products Sold: 0
      +
      + +
      +

      ๐Ÿ› ๏ธ Build Menu

      + + + + + +

      Current Tool: MINE

      +
      + +
      +
      + +
      + + + + \ No newline at end of file diff --git a/games/tykoon/script.js b/games/tykoon/script.js new file mode 100644 index 00000000..207e5c16 --- /dev/null +++ b/games/tykoon/script.js @@ -0,0 +1,86 @@ +// game.js (Add these constants and functions) + +// --- DOM Element References --- +const gameContainer = document.getElementById('game-container'); +const currentToolDisplay = document.getElementById('current-tool'); +const buildButtons = document.querySelectorAll('.build-button'); + +// Resource Display Elements +const moneyDisplay = document.getElementById('money'); +const oreDisplay = document.getElementById('ore'); +const ingotsDisplay = document.getElementById('ingots'); +const productDisplay = document.getElementById('product'); + +// Initial State (as defined in the previous response) +const GRID_SIZE = 10; +const TILE_WIDTH = 50; +const ENTITIES = { /* ... definitions ... */ }; // Need to include your ENTITIES object here +let resources = { money: 500, ore: 0, ingots: 0, product: 0 }; +let gameMap = []; // ... initialization ... +let selectedBuilding = 'MINE'; // Default selected tool + +// --- UI Update Function --- +function updateUI() { + // 1. Update Resource Dashboard + moneyDisplay.textContent = resources.money.toFixed(2); + oreDisplay.textContent = resources.ore.toFixed(1); + ingotsDisplay.textContent = resources.ingots.toFixed(1); + productDisplay.textContent = resources.product; + + // 2. Update Tool Display + currentToolDisplay.textContent = selectedBuilding; + + // 3. Update all tile visuals (necessary after production or placement) + updateTileVisuals(); +} + +function updateTileVisuals() { + // A slightly optimized way to update visuals without recreating the whole grid + document.querySelectorAll('.grid-tile').forEach(tileEl => { + const x = parseInt(tileEl.dataset.x); + const y = parseInt(tileEl.dataset.y); + const tile = gameMap[y][x]; + + if (tile.type !== 'empty') { + const entityDef = ENTITIES[tile.type]; + tileEl.style.backgroundColor = entityDef.color; + tileEl.title = `${entityDef.name}\nOre: ${tile.state.storage.ore.toFixed(2)}`; + } else { + tileEl.style.backgroundColor = '#333333'; + tileEl.title = 'Empty Tile'; + } + }); +} + +// --- Building Selection Logic --- +buildButtons.forEach(button => { + button.addEventListener('click', () => { + const newTool = button.dataset.type; + selectedBuilding = newTool; + + // Visual feedback for the active button + buildButtons.forEach(btn => btn.classList.remove('active-tool')); + button.classList.add('active-tool'); + + updateUI(); + }); +}); + +// --- Final Call --- +// Replace the previous renderGrid call with this setup: +function initializeGame() { + // 1. Setup the 2D array gameMap (from previous blueprint) + // [CODE HERE] + + // 2. Render the initial DOM grid + // Ensure renderGrid function is in your game.js and working correctly + renderGrid(); + + // 3. Start the production loop + // setInterval(runProductionCycle, 100); + + // 4. Update UI with initial state + updateUI(); +} + +// initializeGame(); \ No newline at end of file diff --git a/games/tykoon/style.css b/games/tykoon/style.css new file mode 100644 index 00000000..898f9277 --- /dev/null +++ b/games/tykoon/style.css @@ -0,0 +1,87 @@ +/* General Styling */ +body { + background-color: #212529; /* Dark background */ + color: #f8f9fa; + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; +} + +#game-wrapper { + display: grid; + grid-template-areas: + "menu dashboard" + "menu game-area"; + grid-template-columns: 200px 1fr; /* Menu fixed width, game area flexible */ + grid-gap: 20px; +} + +/* --- UI Panels --- */ +#dashboard, #building-menu { + background-color: #343a40; + padding: 15px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5); +} + +#dashboard { + grid-area: dashboard; +} + +#building-menu { + grid-area: menu; +} + +.resource-item { + padding: 5px 0; + border-bottom: 1px dashed #495057; +} + +/* Button Styling */ +.build-button { + display: block; + width: 100%; + padding: 10px; + margin-bottom: 10px; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; +} + +.build-button:hover { + background-color: #0056b3; +} + +.build-button.active-tool { + background-color: #28a745; /* Highlight the currently selected tool */ +} + + +/* --- Game Grid Area --- */ +#game-container { + grid-area: game-area; + display: flex; + flex-wrap: wrap; /* Allows tiles to form a continuous grid */ + border: 3px solid #6c757d; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.7); +} + +/* Grid Tile Styling */ +.grid-tile { + /* Dimensions set by JavaScript based on TILE_WIDTH */ + border: 1px solid #495057; + box-sizing: border-box; /* Ensures padding/border don't affect width/height */ + position: relative; + cursor: pointer; + transition: background-color 0.1s; +} + +.grid-tile:hover { + border: 1px solid #ffffff; /* Highlight on hover */ +} \ No newline at end of file diff --git a/games/type_master/index.html b/games/type_master/index.html new file mode 100644 index 00000000..1b144536 --- /dev/null +++ b/games/type_master/index.html @@ -0,0 +1,39 @@ + + + + + + Typing Speed Test โฑ๏ธ + + + +
      +

      Typing Speed Test

      + +
      +

      Click "Start Test" to load a paragraph and begin your typing challenge!

      +
      + + + +
      + + + +
      +

      Timer: 0:00

      +

      WPM: 0

      +

      Accuracy: 100%

      +
      +
      + + +
      + + + + \ No newline at end of file diff --git a/games/type_master/script.js b/games/type_master/script.js new file mode 100644 index 00000000..839f739c --- /dev/null +++ b/games/type_master/script.js @@ -0,0 +1,175 @@ +// --- 1. Game Data & State --- +const testParagraphs = [ + "The quick brown fox jumps over the lazy dog.", + "Jinxed wizards pluck ivy from the big quilt.", + "Waltz, bad nymph, for quick jigs vex him.", + "Pack my box with five dozen liquor jugs.", + "How vexingly quick daft zebras jump!" +]; + +let startTime; +let timerInterval; +let timerRunning = false; +let currentParagraph = ""; +let totalCharacters = 0; +let correctCharacters = 0; + +// --- 2. DOM Element References --- +const textDisplay = document.getElementById('text-display'); +const textInput = document.getElementById('text-input'); +const startButton = document.getElementById('start-button'); +const resetButton = document.getElementById('reset-button'); +const timerDisplay = document.getElementById('timer'); +const wpmDisplay = document.getElementById('wpm'); +const accuracyDisplay = document.getElementById('accuracy'); +const resultsFeedback = document.getElementById('results-feedback'); + +// --- 3. Utility Functions --- + +// Loads a random paragraph and prepares the display area +function loadNewTest() { + // 1. Reset State + clearInterval(timerInterval); + timerRunning = false; + currentParagraph = testParagraphs[Math.floor(Math.random() * testParagraphs.length)]; + totalCharacters = currentParagraph.length; + correctCharacters = 0; + + // 2. Reset UI + timerDisplay.textContent = '0:00'; + wpmDisplay.textContent = '0'; + accuracyDisplay.textContent = '100%'; + textInput.value = ''; + textInput.disabled = true; + startButton.textContent = 'Start Test'; + startButton.disabled = false; + resetButton.classList.add('hidden'); + resultsFeedback.classList.add('hidden'); + + // 3. Prepare Display Text + textDisplay.innerHTML = `

      ${currentParagraph.split('').map(char => `${char}`).join('')}

      `; +} + +// Starts the timer +function startTimer() { + if (timerRunning) return; + + startTime = new Date().getTime(); + timerRunning = true; + + timerInterval = setInterval(() => { + const elapsedTime = (new Date().getTime() - startTime) / 1000; + const minutes = Math.floor(elapsedTime / 60); + const seconds = Math.floor(elapsedTime % 60); + + // Format time as M:SS + timerDisplay.textContent = `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; + + // Update WPM in real-time + updateWPM(elapsedTime); + + }, 1000); +} + +// Stops the timer and ends the test +function endTest() { + clearInterval(timerInterval); + timerRunning = false; + textInput.disabled = true; + startButton.classList.add('hidden'); + + // Calculate final results + const finalTimeSeconds = (new Date().getTime() - startTime) / 1000; + const finalWPM = calculateWPM(finalTimeSeconds); + const finalAccuracy = calculateAccuracy(); + + // Display final results + resultsFeedback.querySelector('.final-wpm-display span').textContent = finalWPM; + resultsFeedback.querySelector('.final-accuracy-display span').textContent = finalAccuracy; + resultsFeedback.classList.remove('hidden'); +} + +// --- 4. Calculation Functions --- + +function calculateWPM(timeInSeconds) { + // Formula: (Total Characters / 5) / (Time in Minutes) + // 5 characters is the standard definition of a "Word" + const totalWords = correctCharacters / 5; + const timeInMinutes = timeInSeconds / 60; + + if (timeInMinutes === 0) return 0; + + return Math.round(totalWords / timeInMinutes); +} + +function calculateAccuracy() { + if (totalCharacters === 0) return '100%'; + const accuracy = (correctCharacters / totalCharacters) * 100; + return `${Math.max(0, accuracy).toFixed(1)}%`; +} + +function updateWPM(elapsedTime) { + const wpm = calculateWPM(elapsedTime); + wpmDisplay.textContent = wpm; +} + +// --- 5. Event Handlers --- + +function handleInput() { + const typedText = textInput.value; + const targetChars = textDisplay.querySelectorAll('span'); + correctCharacters = 0; + let testFinished = true; + + // Iterate through the typed characters and compare them to the target + for (let i = 0; i < totalCharacters; i++) { + const targetChar = currentParagraph[i]; + const typedChar = typedText[i]; + const charElement = targetChars[i]; + + if (typedChar == null) { + // Character not yet typed (game continues) + charElement.className = ''; + testFinished = false; + } else if (typedChar === targetChar) { + // Correct character + charElement.className = 'correct'; + correctCharacters++; + } else { + // Incorrect character + charElement.className = 'incorrect'; + testFinished = false; + } + } + + // Update accuracy display in real-time + accuracyDisplay.textContent = calculateAccuracy(); + + // Check for test completion + if (typedText.length >= totalCharacters && testFinished) { + endTest(); + } +} + +// --- 6. Event Listeners and Initialization --- + +startButton.addEventListener('click', () => { + // 1. Enable input and focus + textInput.disabled = false; + textInput.focus(); + + // 2. Start the timer logic + startTimer(); + + // 3. Update buttons + startButton.disabled = true; + startButton.textContent = 'Typing...'; + resetButton.classList.remove('hidden'); +}); + +textInput.addEventListener('input', handleInput); + +resetButton.addEventListener('click', loadNewTest); + +// Initialize the game on load +loadNewTest(); \ No newline at end of file diff --git a/games/type_master/style.css b/games/type_master/style.css new file mode 100644 index 00000000..7c2d0ea2 --- /dev/null +++ b/games/type_master/style.css @@ -0,0 +1,141 @@ +body { + font-family: Arial, sans-serif; + background-color: #f0f0f5; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; +} + +.test-container { + background: #ffffff; + padding: 40px; + border-radius: 10px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); + width: 90%; + max-width: 800px; + text-align: center; +} + +h1 { + color: #333; + margin-bottom: 30px; +} + +/* --- Text Display Area --- */ +.text-to-type { + min-height: 100px; + padding: 15px; + margin-bottom: 20px; + border: 2px solid #ddd; + border-radius: 8px; + text-align: left; + line-height: 1.6; + font-size: 1.2em; + color: #555; + background-color: #fafafa; +} + +/* Styling for correct and incorrect characters */ +.correct { + color: #28a745; /* Green */ +} + +.incorrect { + color: #dc3545; /* Red */ + text-decoration: underline wavy #dc3545 1px; +} + +/* --- Input Area --- */ +#text-input { + width: 100%; + height: 100px; + padding: 15px; + font-size: 1.2em; + border: 2px solid #007bff; + border-radius: 8px; + resize: none; + box-sizing: border-box; + margin-bottom: 20px; +} + +#text-input:disabled { + cursor: not-allowed; + background-color: #eee; + border-color: #ccc; +} + +/* --- Controls and Stats --- */ +.controls-and-results { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 10px; +} + +.stats-area p { + display: inline-block; + margin: 0 15px; + font-size: 1.1em; + font-weight: bold; + color: #333; +} + +.stats-area span { + color: #007bff; /* Blue for stats */ +} + +button { + padding: 10px 20px; + font-size: 1.1em; + border: none; + border-radius: 5px; + cursor: pointer; + background-color: #007bff; + color: white; + transition: background-color 0.3s; +} + +button:hover { + background-color: #0056b3; +} + +#reset-button { + background-color: #6c757d; +} + +#reset-button:hover { + background-color: #5a6268; +} + +/* --- Results Feedback --- */ +#results-feedback { + margin-top: 30px; + padding: 20px; + border: 2px solid #28a745; + border-radius: 8px; + background-color: #d4edda; + text-align: center; +} + +#results-feedback h3 { + color: #155724; + margin-top: 0; +} + +.final-wpm-display span { + font-size: 2em; + color: #007bff; + font-weight: bold; +} + +.final-accuracy-display span { + font-size: 1.5em; + color: #28a745; + font-weight: bold; +} + +.hidden { + display: none; +} \ No newline at end of file diff --git a/games/typing-ghost/index.html b/games/typing-ghost/index.html new file mode 100644 index 00000000..4a6c083a --- /dev/null +++ b/games/typing-ghost/index.html @@ -0,0 +1,41 @@ + + + + + + Typing Ghost ๐Ÿ‘ป | Mini JS Games Hub + + + + +
      +

      ๐Ÿ‘ป Typing Ghost Challenge

      +

      Race against your previous ghost! Type as fast & accurately as possible.

      + +
      + โฑ๏ธ WPM: 0 + ๐ŸŽฏ Accuracy: 100% + ๐Ÿ‘ป Ghost: Ready +
      + +
      +

      +

      +
      + + + +
      + + + +
      + + + + +
      + + + + diff --git a/games/typing-ghost/script.js b/games/typing-ghost/script.js new file mode 100644 index 00000000..d5af4174 --- /dev/null +++ b/games/typing-ghost/script.js @@ -0,0 +1,111 @@ +const displayText = document.getElementById("display-text"); +const typingArea = document.getElementById("typing-area"); +const ghostProgress = document.getElementById("ghost-progress"); +const wpmDisplay = document.getElementById("wpm"); +const accuracyDisplay = document.getElementById("accuracy"); +const ghostStatus = document.getElementById("ghost-status"); +const pauseBtn = document.getElementById("pause-btn"); +const resumeBtn = document.getElementById("resume-btn"); +const restartBtn = document.getElementById("restart-btn"); +const typeSound = document.getElementById("type-sound"); +const successSound = document.getElementById("success-sound"); +const failSound = document.getElementById("fail-sound"); + +const sentences = [ + "The quick brown fox jumps over the lazy dog.", + "JavaScript makes web pages alive and interactive.", + "Coding is fun when creativity meets logic.", + "Practice typing to improve your speed and accuracy." +]; + +let currentSentence = ""; +let startTime, ghostData; +let timer, paused = false; + +function startGame() { + currentSentence = sentences[Math.floor(Math.random() * sentences.length)]; + displayText.textContent = currentSentence; + typingArea.value = ""; + wpmDisplay.textContent = "0"; + accuracyDisplay.textContent = "100%"; + ghostStatus.textContent = "Ready"; + ghostProgress.style.width = "0%"; + paused = false; +} + +function calculateStats() { + const typed = typingArea.value; + const elapsed = (Date.now() - startTime) / 1000 / 60; + const words = typed.trim().split(/\s+/).length; + const wpm = Math.round(words / elapsed || 0); + const correctChars = typed.split("").filter((ch, i) => ch === currentSentence[i]).length; + const accuracy = Math.round((correctChars / currentSentence.length) * 100); + wpmDisplay.textContent = wpm; + accuracyDisplay.textContent = `${accuracy}%`; +} + +function runGhost() { + if (!ghostData) return; + const duration = ghostData.time; + const start = Date.now(); + const ghostTimer = setInterval(() => { + if (paused) return; + const elapsed = Date.now() - start; + const progress = Math.min((elapsed / duration) * 100, 100); + ghostProgress.style.width = `${progress}%`; + if (progress >= 100) clearInterval(ghostTimer); + }, 100); +} + +typingArea.addEventListener("input", () => { + if (!startTime) { + startTime = Date.now(); + runGhost(); + } + typeSound.currentTime = 0; + typeSound.play(); + calculateStats(); + if (typingArea.value === currentSentence) endGame(); +}); + +pauseBtn.addEventListener("click", () => { + paused = true; + typingArea.disabled = true; + ghostStatus.textContent = "Paused"; + pauseBtn.disabled = true; + resumeBtn.disabled = false; +}); + +resumeBtn.addEventListener("click", () => { + paused = false; + typingArea.disabled = false; + ghostStatus.textContent = "Racing!"; + pauseBtn.disabled = false; + resumeBtn.disabled = true; +}); + +restartBtn.addEventListener("click", () => { + startGame(); + ghostStatus.textContent = "Restarted"; + startTime = null; +}); + +function endGame() { + const totalTime = Date.now() - startTime; + ghostStatus.textContent = "Finished!"; + successSound.play(); + + const previous = JSON.parse(localStorage.getItem("ghostData") || "null"); + if (!previous || totalTime < previous.time) { + localStorage.setItem("ghostData", JSON.stringify({ time: totalTime })); + ghostStatus.textContent = "๐Ÿ† New Record!"; + } + + ghostData = JSON.parse(localStorage.getItem("ghostData")); + setTimeout(startGame, 3000); +} + +window.addEventListener("load", () => { + ghostData = JSON.parse(localStorage.getItem("ghostData")); + startGame(); +}); diff --git a/games/typing-ghost/style.css b/games/typing-ghost/style.css new file mode 100644 index 00000000..3c7fc359 --- /dev/null +++ b/games/typing-ghost/style.css @@ -0,0 +1,102 @@ +body { + background: radial-gradient(circle at top, #121212, #000); + color: #fff; + font-family: "Poppins", sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + flex-direction: column; + overflow: hidden; +} + +h1 { + font-size: 2.5rem; + text-shadow: 0 0 15px #0ff; + margin-bottom: 10px; +} + +.subtext { + font-size: 1rem; + opacity: 0.8; + margin-bottom: 20px; +} + +.stats { + display: flex; + justify-content: center; + gap: 20px; + font-size: 1.1rem; + margin-bottom: 20px; + text-shadow: 0 0 8px cyan; +} + +.text-box { + border: 2px solid cyan; + border-radius: 12px; + padding: 20px; + width: 80%; + height: 120px; + margin-bottom: 20px; + background-color: rgba(0,0,0,0.6); + position: relative; +} + +#display-text { + color: #ddd; + letter-spacing: 1px; +} + +#ghost-progress { + position: absolute; + bottom: 5px; + left: 0; + height: 4px; + background: linear-gradient(90deg, cyan, purple); + border-radius: 2px; + width: 0%; + transition: width 0.2s linear; +} + +#typing-area { + width: 80%; + height: 100px; + font-size: 1.1rem; + border-radius: 10px; + border: none; + padding: 10px; + outline: none; + resize: none; + background-color: rgba(255,255,255,0.1); + color: #fff; + text-shadow: 0 0 8px #0ff; + box-shadow: 0 0 10px cyan; +} + +.buttons { + display: flex; + gap: 10px; + margin-top: 15px; +} + +button { + background: linear-gradient(90deg, #00f5ff, #8a2be2); + border: none; + padding: 10px 20px; + border-radius: 8px; + font-size: 1rem; + color: #fff; + cursor: pointer; + box-shadow: 0 0 10px cyan; + transition: 0.3s; +} + +button:hover { + box-shadow: 0 0 20px #0ff, 0 0 30px #8a2be2; +} + +button:disabled { + opacity: 0.6; + cursor: not-allowed; +} diff --git a/games/typing-speed-challenge/index.html b/games/typing-speed-challenge/index.html new file mode 100644 index 00000000..5612fbeb --- /dev/null +++ b/games/typing-speed-challenge/index.html @@ -0,0 +1,22 @@ + + + + + + Typing Speed Challenge + + + +

      Typing Speed Challenge

      +

      Type the words that appear on the screen as fast as possible. Spelling and speed challenge.

      +
      + +
      + +
      Time: 30
      +
      Score: 0
      +
      +
      + + + \ No newline at end of file diff --git a/games/typing-speed-challenge/script.js b/games/typing-speed-challenge/script.js new file mode 100644 index 00000000..4f920aee --- /dev/null +++ b/games/typing-speed-challenge/script.js @@ -0,0 +1,66 @@ +const canvas = document.getElementById('game-canvas'); +const ctx = canvas.getContext('2d'); +const input = document.getElementById('word-input'); +const timerDisplay = document.getElementById('timer'); +const scoreDisplay = document.getElementById('score'); + +const words = ['apple', 'banana', 'cat', 'dog', 'elephant', 'fish', 'grape', 'house', 'ice', 'jungle', 'kite', 'lemon', 'mouse', 'nest', 'orange', 'penguin', 'queen', 'rabbit', 'sun', 'tree', 'umbrella', 'violet', 'water', 'xylophone', 'yellow', 'zebra']; + +let currentWords = []; +let score = 0; +let time = 30; +let gameRunning = true; + +function addWord() { + const word = words[Math.floor(Math.random() * words.length)]; + const x = Math.random() * (canvas.width - 100); + const y = Math.random() * (canvas.height - 50) + 50; + currentWords.push({ word, x, y }); +} + +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.font = '20px Arial'; + ctx.fillStyle = 'black'; + currentWords.forEach(w => { + ctx.fillText(w.word, w.x, w.y); + }); +} + +function updateTimer() { + timerDisplay.textContent = `Time: ${time}`; + if (time <= 0) { + gameRunning = false; + input.disabled = true; + alert(`Time's up! Final score: ${score}`); + } else { + time--; + } +} + +function checkInput() { + const typed = input.value.trim().toLowerCase(); + const index = currentWords.findIndex(w => w.word.toLowerCase() === typed); + if (index !== -1) { + currentWords.splice(index, 1); + score++; + scoreDisplay.textContent = `Score: ${score}`; + addWord(); + input.value = ''; + } +} + +input.addEventListener('input', checkInput); + +// Initialize +for (let i = 0; i < 5; i++) { + addWord(); +} +draw(); + +setInterval(() => { + if (gameRunning) { + updateTimer(); + draw(); + } +}, 1000); \ No newline at end of file diff --git a/games/typing-speed-challenge/style.css b/games/typing-speed-challenge/style.css new file mode 100644 index 00000000..4c910828 --- /dev/null +++ b/games/typing-speed-challenge/style.css @@ -0,0 +1,46 @@ +body { + font-family: Arial, sans-serif; + text-align: center; + background-color: #f0f0f0; + margin: 0; + padding: 20px; +} + +h1 { + color: #333; +} + +p { + color: #666; + margin-bottom: 20px; +} + +#game-container { + display: inline-block; + border: 2px solid #333; + background-color: white; + padding: 20px; + border-radius: 10px; +} + +#game-canvas { + border: 1px solid #ccc; + background-color: #fafafa; +} + +#input-area { + margin-top: 20px; +} + +#word-input { + font-size: 18px; + padding: 10px; + width: 200px; + text-align: center; +} + +#timer, #score { + font-size: 20px; + margin: 10px; + color: #333; +} \ No newline at end of file diff --git a/games/ufo-escape/index.html b/games/ufo-escape/index.html new file mode 100644 index 00000000..672d7978 --- /dev/null +++ b/games/ufo-escape/index.html @@ -0,0 +1,57 @@ + + + + + + UFO Escape โ€” Mini JS Games Hub + + + + +
      +
      +
      + ๐Ÿ›ธ +

      UFO Escape

      +
      +
      + + + + + +
      +
      + +
      + + +
      + + +
      +
      + +
      +

      Use โ† โ†’ or A D to move. On mobile, touch left/right halves to move. Collect energy orbs and avoid saucers.

      +
      +
      + + + + diff --git a/games/ufo-escape/script.js b/games/ufo-escape/script.js new file mode 100644 index 00000000..198efe48 --- /dev/null +++ b/games/ufo-escape/script.js @@ -0,0 +1,545 @@ +/* UFO Escape - script.js + Features: + - Canvas rendering with glow + - Player UFO movement (keyboard + mobile touch) + - Enemies (saucers) spawn from top and move down + - Energy orbs spawn (score & powerups) + - Pause / Resume / Start / Restart / Mute + - Difficulty ramp by level/elapsed time + - LocalStorage highscore +*/ + +/* ---------------------- + SOUND ASSET LINKS (online) + ---------------------- + Using Google Actions sounds (publicly accessible): + - collectSound: orb pickup + - hitSound: collision / explosion + - shootSound: optional + - bgMusic: light background loop (kept short) +*/ +const SOUNDS = { + collect: 'https://actions.google.com/sounds/v1/cartoon/clang_and_wobble.ogg', + hit: 'https://actions.google.com/sounds/v1/impacts/metal_crash.ogg', + explosion: 'https://actions.google.com/sounds/v1/explosions/explosion_crash.ogg', + bg: 'https://actions.google.com/sounds/v1/alarms/medium_bell_ring.ogg' // short loop-like bell +}; + +// small helper to load audio +function createAudio(url, loop = false, vol = 0.6) { + const a = new Audio(url); + a.loop = loop; + a.volume = vol; + a.preload = 'auto'; + return a; +} + +/* ---------------------- + DOM +*/ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const startBtn = document.getElementById('startBtn'); +const pauseBtn = document.getElementById('pauseBtn'); +const resumeBtn = document.getElementById('resumeBtn'); +const restartBtn = document.getElementById('restartBtn'); +const muteBtn = document.getElementById('muteBtn'); +const overlay = document.getElementById('overlay'); +const overlayTitle = document.getElementById('overlay-title'); +const overlayText = document.getElementById('overlay-text'); +const overlayStart = document.getElementById('overlayStart'); +const overlayResume = document.getElementById('overlayResume'); + +const scoreEl = document.getElementById('score'); +const livesEl = document.getElementById('lives'); +const energyEl = document.getElementById('energy'); +const levelEl = document.getElementById('level'); +const highscoreEl = document.getElementById('highscore'); + +let W = canvas.width = canvas.clientWidth; +let H = canvas.height = canvas.clientHeight; + +/* ---------------------- + Game State +*/ +let running = false; +let paused = false; +let muted = false; +let lastTime = 0; +let spawnTimer = 0; +let orbTimer = 0; +let difficultyTimer = 0; +let elapsed = 0; + +let score = 0; +let lives = 3; +let energy = 0; +let level = 1; +const highscoreKey = 'ufoEscapeHighscore'; +let highscore = parseInt(localStorage.getItem(highscoreKey) || '0', 10); + +/* sounds */ +const sndCollect = createAudio(SOUNDS.collect, false, 0.5); +const sndHit = createAudio(SOUNDS.hit, false, 0.6); +const sndExplode = createAudio(SOUNDS.explosion, false, 0.7); +const sndBg = createAudio(SOUNDS.bg, true, 0.25); + +/* Entities */ +const player = { + x: W / 2, + y: H - 80, + w: 68, + h: 26, + speed: 6, + vx: 0, + glow: 18 +}; + +let enemies = []; // saucers +let orbs = []; // energy orbs + +/* tuning */ +let spawnInterval = 1100; // ms initial +let orbInterval = 2200; +let enemySpeedBase = 1.2; +let maxEnemies = 6; + +/* input */ +const keys = {}; +let touchX = null; + +/* helpers */ +function rand(min, max) { return Math.random() * (max - min) + min; } +function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); } + +/* resize handling */ +function resize() { + W = canvas.width = canvas.clientWidth; + H = canvas.height = canvas.clientHeight; + player.y = H - 80; +} +window.addEventListener('resize', () => { resize(); }); + +/* ---------------------- + Entity constructors +*/ +function spawnEnemy() { + const size = rand(36, 70); + const x = rand(size / 2, W - size / 2); + const speed = enemySpeedBase + rand(0, 1.4) + (level - 1) * 0.25; + enemies.push({ + x, y: -rand(20, 120), size, speed, + wobble: rand(0.6, 1.6), + rotation: rand(-0.6, 0.6) + }); + // cap + if (enemies.length > maxEnemies) enemies.splice(0, enemies.length - maxEnemies); +} + +function spawnOrb() { + const r = rand(8, 16); + const x = rand(r, W - r); + const speed = 1 + rand(0, 0.8) + (level - 1) * 0.12; + orbs.push({ x, y: -rand(10, 200), r, speed, wob: rand(0.4, 1.4) }); +} + +/* ---------------------- + Game start / stop / reset +*/ +function resetGame() { + enemies = []; orbs = []; + running = false; paused = false; + lastTime = performance.now(); + spawnTimer = orbTimer = difficultyTimer = elapsed = 0; + score = 0; lives = 3; energy = 0; level = 1; + spawnInterval = 1100; orbInterval = 2200; enemySpeedBase = 1.2; maxEnemies = 6; + player.x = W / 2; + updateHUD(); + overlayTitle.textContent = 'UFO Escape'; + overlayText.textContent = 'Press Start to begin'; + overlay.classList.remove('hidden'); + pauseBtn.disabled = true; resumeBtn.disabled = true; restartBtn.disabled = true; + startBtn.disabled = false; + if (!muted) { sndBg.pause(); sndBg.currentTime = 0; } +} + +function startGame() { + running = true; paused = false; lastTime = performance.now(); + overlay.classList.add('hidden'); + pauseBtn.disabled = false; resumeBtn.disabled = true; restartBtn.disabled = false; + startBtn.disabled = true; + if (!muted) { + sndBg.currentTime = 0; + sndBg.play().catch(() => {}); + } + window.requestAnimationFrame(loop); +} + +function pauseGame() { + if (!running || paused) return; + paused = true; resumeBtn.disabled = false; pauseBtn.disabled = true; + overlay.classList.remove('hidden'); + overlayTitle.textContent = 'Paused'; + overlayText.textContent = 'Game is paused'; + overlayResume.style.display = 'inline-block'; + if (!muted) sndBg.pause(); +} + +function resumeGame() { + if (!running || !paused) return; + paused = false; pauseBtn.disabled = false; resumeBtn.disabled = true; + overlay.classList.add('hidden'); + if (!muted) sndBg.play().catch(() => {}); + lastTime = performance.now(); + window.requestAnimationFrame(loop); +} + +function gameOver() { + running = false; + paused = false; + overlay.classList.remove('hidden'); + overlayTitle.textContent = 'Game Over'; + overlayText.textContent = `Score: ${score} โ€” Press Restart to play again`; + restartBtn.disabled = false; pauseBtn.disabled = true; resumeBtn.disabled = true; + startBtn.disabled = true; + if (score > highscore) { + highscore = score; + localStorage.setItem(highscoreKey, String(highscore)); + } + updateHUD(); + if (!muted) { sndBg.pause(); sndExplode.play().catch(()=>{}); } +} + +/* ---------------------- + Collision util +*/ +function rectCircleCollision(px, py, pr, cx, cy, cw, ch) { + // not used directly; use circle-based checks below + return false; +} + +function circleHit(ax, ay, ar, bx, by, br) { + const dx = ax - bx, dy = ay - by; + return dx * dx + dy * dy < (ar + br) * (ar + br); +} + +/* ---------------------- + Update / game loop +*/ +function update(dt) { + elapsed += dt; + spawnTimer += dt; + orbTimer += dt; + difficultyTimer += dt; + + // increase difficulty over time + if (difficultyTimer > 8000) { + difficultyTimer = 0; + level += 1; + spawnInterval = Math.max(420, spawnInterval - 80); + orbInterval = Math.max(900, orbInterval - 90); + enemySpeedBase += 0.18; + maxEnemies = Math.min(12, maxEnemies + 1); + } + + // spawn enemies & orbs + if (spawnTimer > spawnInterval) { + spawnTimer = 0; + spawnEnemy(); + } + if (orbTimer > orbInterval) { + orbTimer = 0; + spawnOrb(); + } + + // player horizontal movement + if (keys['ArrowLeft'] || keys['a'] || (touchX !== null && touchX < W/2)) { + player.vx = -player.speed; + } else if (keys['ArrowRight'] || keys['d'] || (touchX !== null && touchX > W/2)) { + player.vx = player.speed; + } else player.vx = 0; + + player.x += player.vx; + player.x = clamp(player.x, player.w / 2 + 6, W - player.w / 2 - 6); + + // update enemies + for (let i = enemies.length - 1; i >= 0; i--) { + const e = enemies[i]; + e.y += e.speed * dt / 16 * 1.0; + e.x += Math.sin((elapsed / 300) * e.wobble) * 0.9; // wobble + if (e.y - e.size > H + 40) enemies.splice(i, 1); + + // collision: enemy vs player (use circular approximations) + const ex = e.x, ey = e.y, er = e.size * 0.5; + const px = player.x, py = player.y - 6, pr = Math.max(player.w, player.h) * 0.4; + if (circleHit(px, py, pr, ex, ey, er)) { + // hit + enemies.splice(i, 1); + lives -= 1; + if (!muted) sndHit.play().catch(()=>{}); + if (lives <= 0) { + gameOver(); + return; + } + updateHUD(); + // small screen shake: store for draw + screenShake = 12; + } + } + + // update orbs + for (let i = orbs.length - 1; i >= 0; i--) { + const o = orbs[i]; + o.y += o.speed * dt / 16 * 1.0; + o.x += Math.sin((elapsed / 190) * o.wob) * 0.9; + if (o.y - o.r > H + 20) orbs.splice(i, 1); + + // collect? + if (circleHit(player.x, player.y - 6, Math.max(player.w, player.h) * 0.4, o.x, o.y, o.r)) { + orbs.splice(i, 1); + score += 10 + Math.floor(level * 2); + energy = Math.min(100, energy + 14); + if (!muted) sndCollect.play().catch(()=>{}); + updateHUD(); + } + } + + // rewards based on energy + if (energy >= 100) { + energy = 0; + score += 80 + level * 8; + lives = Math.min(6, lives + 1); // small extra life + if (!muted) sndCollect.play().catch(()=>{}); + updateHUD(); + } + + // incremental score over time + score += Math.floor(dt / 800); + updateHUD(); +} + +/* ---------------------- + Drawing +*/ +let screenShake = 0; + +function draw() { + // clear + ctx.clearRect(0, 0, W, H); + + // background stars + ctx.save(); + ctx.fillStyle = '#010214'; + ctx.fillRect(0, 0, W, H); + // subtle starfield + for (let i = 0; i < 60; i++) { + const sx = (i * 97 + (elapsed/6) % W) % W; + const sy = (i * 37 + (elapsed/9)) % H; + ctx.fillStyle = 'rgba(255,255,255,' + (0.03 + (i%7)/70) + ')'; + ctx.fillRect(sx, sy, 1, 1); + } + ctx.restore(); + + // parallax nebula glow + ctx.save(); + const grad = ctx.createRadialGradient(W*0.15, H*0.12, 20, W*0.15, H*0.12, W*0.9); + grad.addColorStop(0, 'rgba(138, 92, 246, 0.12)'); + grad.addColorStop(1, 'rgba(1, 2, 8, 0.0)'); + ctx.fillStyle = grad; + ctx.fillRect(0, 0, W, H); + ctx.restore(); + + // screen shake transform + if (screenShake > 0) { + const s = (Math.random() - 0.5) * screenShake; + ctx.translate(s, s * 0.6); + screenShake *= 0.85; + if (screenShake < 0.1) screenShake = 0; + } + + // draw orbs (glowing) + for (const o of orbs) { + ctx.save(); + ctx.beginPath(); + ctx.shadowBlur = 20; + ctx.shadowColor = 'rgba(122,255,255,0.85)'; + const gradO = ctx.createRadialGradient(o.x, o.y, 1, o.x, o.y, o.r*3); + gradO.addColorStop(0, 'rgba(122,255,255,0.95)'); + gradO.addColorStop(0.4, 'rgba(122,255,255,0.25)'); + gradO.addColorStop(1, 'rgba(122,255,255,0)'); + ctx.fillStyle = gradO; + ctx.arc(o.x, o.y, o.r*1.8, 0, Math.PI*2); + ctx.fill(); + ctx.restore(); + } + + // draw enemies (UFO saucer style) + for (const e of enemies) { + ctx.save(); + ctx.translate(e.x, e.y); + // glow + ctx.beginPath(); + ctx.shadowBlur = 30; + ctx.shadowColor = 'rgba(255, 120, 120, 0.18)'; + ctx.fillStyle = 'rgba(255,255,255,0.02)'; + ctx.ellipse(0, 0, e.size*0.9, e.size*0.35, 0, 0, Math.PI*2); + ctx.fill(); + + // saucer body + ctx.beginPath(); + ctx.shadowBlur = 18; + ctx.shadowColor = 'rgba(255, 120, 120, 0.35)'; + const bodyGrad = ctx.createLinearGradient(-e.size*0.7, 0, e.size*0.7, 0); + bodyGrad.addColorStop(0, '#ff8899'); + bodyGrad.addColorStop(1, '#ff5c8a'); + ctx.fillStyle = bodyGrad; + ctx.ellipse(0, 0, e.size*0.6, e.size*0.26, 0, 0, Math.PI*2); + ctx.fill(); + + // glass dome + ctx.beginPath(); + ctx.shadowBlur = 6; + ctx.shadowColor = 'rgba(255,255,255,0.12)'; + ctx.fillStyle = 'rgba(14, 21, 35, 0.7)'; + ctx.ellipse(0, -e.size*0.15, e.size*0.28, e.size*0.18, 0, 0, Math.PI*2); + ctx.fill(); + + // little lights + for (let i = -2; i <= 2; i++) { + ctx.beginPath(); + ctx.shadowBlur = 12; + ctx.shadowColor = 'rgba(255,255,170,0.9)'; + ctx.fillStyle = `rgba(255,${200 - i*12}, ${60 + i*10}, 0.98)`; + ctx.arc(i * (e.size*0.16), e.size*0.04, Math.max(2, e.size*0.04), 0, Math.PI*2); + ctx.fill(); + } + ctx.restore(); + } + + // draw player UFO + ctx.save(); + ctx.translate(player.x, player.y); + // glow ring + ctx.beginPath(); + ctx.shadowBlur = player.glow + 10; + ctx.shadowColor = 'rgba(122,255,255,0.22)'; + ctx.fillStyle = 'rgba(122,255,255,0.02)'; + ctx.ellipse(0, 0, player.w*1.6, player.h*2.2, 0, 0, Math.PI*2); + ctx.fill(); + + // main hull + ctx.beginPath(); + ctx.shadowBlur = 18; + ctx.shadowColor = 'rgba(122,255,255,0.35)'; + const hullGrad = ctx.createLinearGradient(-player.w/2, 0, player.w/2, 0); + hullGrad.addColorStop(0, '#8bf3ff'); + hullGrad.addColorStop(1, '#7afcff'); + ctx.fillStyle = hullGrad; + ctx.ellipse(0, 0, player.w, player.h, 0, 0, Math.PI*2); + ctx.fill(); + + // cockpit dome + ctx.beginPath(); + ctx.shadowBlur = 6; + ctx.shadowColor = 'rgba(255,255,255,0.12)'; + ctx.fillStyle = 'rgba(5,16,26,0.7)'; + ctx.ellipse(0, -8, player.w*0.36, player.h*0.5, 0, 0, Math.PI*2); + ctx.fill(); + + // thruster glow + ctx.beginPath(); + ctx.shadowBlur = 28; + ctx.shadowColor = 'rgba(255, 200, 90, 0.18)'; + ctx.fillStyle = 'rgba(255,200,90,0.06)'; + ctx.ellipse(0, player.h*0.9, player.w*0.55, player.h*0.4, 0, 0, Math.PI*2); + ctx.fill(); + + ctx.restore(); +} + +/* ---------------------- + HUD update +*/ +function updateHUD() { + scoreEl.textContent = Math.max(0, Math.floor(score)); + livesEl.textContent = lives; + energyEl.textContent = Math.floor(energy); + levelEl.textContent = level; + highscoreEl.textContent = highscore; +} + +/* ---------------------- + Main loop +*/ +function loop(ts) { + if (!running || paused) return; + const dt = ts - lastTime; + lastTime = ts; + + update(dt); + draw(); + + if (running && !paused) window.requestAnimationFrame(loop); +} + +/* ---------------------- + Input handlers +*/ +window.addEventListener('keydown', (e) => { + keys[e.key] = true; + if (e.key === 'p') pauseGame(); + if (e.key === 'r') { if (!running) startGame(); else { resetGame(); startGame(); } } +}); +window.addEventListener('keyup', (e) => { keys[e.key] = false; }); + +canvas.addEventListener('touchstart', (ev) => { + ev.preventDefault(); + const t = ev.changedTouches[0]; + touchX = t.clientX - canvas.getBoundingClientRect().left; +}); +canvas.addEventListener('touchmove', (ev) => { + ev.preventDefault(); + const t = ev.changedTouches[0]; + touchX = t.clientX - canvas.getBoundingClientRect().left; +}); +canvas.addEventListener('touchend', (ev) => { touchX = null; }); + +/* mouse optional steering */ +canvas.addEventListener('mousemove', (ev) => { + // small subtle follow when mouse moves (desktop players may like it) + // not enabling continuous follow; keyboard/touch is primary +}); + +/* ---------------------- + Buttons +*/ +startBtn.addEventListener('click', () => { + if (!running) startGame(); +}); +pauseBtn.addEventListener('click', () => pauseGame()); +resumeBtn.addEventListener('click', () => resumeGame()); +restartBtn.addEventListener('click', () => { + resetGame(); + startGame(); +}); +overlayStart.addEventListener('click', () => { startGame(); }); +overlayResume.addEventListener('click', () => { resumeGame(); }); + +muteBtn.addEventListener('click', () => { + muted = !muted; + muteBtn.textContent = muted ? '๐Ÿ”‡' : '๐Ÿ”Š'; + if (muted) { + sndBg.pause(); + sndCollect.muted = sndHit.muted = sndExplode.muted = true; + } else { + sndCollect.muted = sndHit.muted = sndExplode.muted = false; + if (running && !paused) sndBg.play().catch(()=>{}); + } +}); + +/* ---------------------- + Start initial state +*/ +resetGame(); +updateHUD(); +draw(); diff --git a/games/ufo-escape/style.css b/games/ufo-escape/style.css new file mode 100644 index 00000000..88561a3b --- /dev/null +++ b/games/ufo-escape/style.css @@ -0,0 +1,121 @@ +:root{ + --bg:#060617; + --card:#0f1724; + --accent:#7afcff; + --accent-2:#8b5cf6; + --muted:#9aa4b2; + --glass: rgba(255,255,255,0.04); +} + +*{box-sizing:border-box;margin:0;padding:0;font-family:Inter,ui-sans-serif,system-ui,Segoe UI,Roboto,"Helvetica Neue",Arial;} +html,body,#gameCanvas{height:100%} +body{ + background: radial-gradient(1000px 400px at 10% 10%, rgba(138,92,246,0.08), transparent), + radial-gradient(800px 300px at 90% 90%, rgba(122,255,255,0.03), transparent), + var(--bg); + color:#e6eef8; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + padding:22px; +} + +.page{ + max-width:1160px; + margin:0 auto; + display:flex; + flex-direction:column; + gap:18px; +} + +.topbar{ + display:flex; + align-items:center; + justify-content:space-between; + gap:16px; +} + +.title{display:flex;align-items:center;gap:12px} +.title .icon{font-size:28px; filter: drop-shadow(0 6px 12px rgba(138,92,246,0.25));} +.title h1{font-size:20px;letter-spacing:0.3px} + +.controls-top .btn{margin-left:8px} + +.btn{ + background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + border:1px solid rgba(255,255,255,0.06); + padding:8px 12px; + border-radius:10px; + color:var(--accent); + cursor:pointer; + font-weight:600; + box-shadow: 0 6px 18px rgba(11,12,17,0.6); + transition:transform .12s ease, box-shadow .12s ease; +} +.btn:hover{transform:translateY(-2px)} +.btn.primary{ + background:linear-gradient(90deg,var(--accent),var(--accent-2)); + color:#021224; + border: none; + box-shadow:0 6px 30px rgba(123,100,248,0.18), 0 0 40px rgba(122,255,255,0.06) inset; +} + +.game-area{display:flex;gap:18px;align-items:flex-start} +.hud{ + width:200px; + background:var(--card); + border-radius:14px; + padding:14px; + box-shadow:0 10px 30px rgba(2,6,23,0.6); + border:1px solid rgba(255,255,255,0.03); +} +.hud-row{display:flex;justify-content:space-between;padding:10px 6px;border-bottom:1px dashed rgba(255,255,255,0.02);color:var(--muted)} +.hud-row strong{color:#fff;font-weight:700} + +.canvas-wrap{ + flex:1; + background: linear-gradient(180deg, rgba(255,255,255,0.02), transparent); + border-radius:14px; + padding:12px; + position:relative; + box-shadow:0 10px 30px rgba(2,6,23,0.6); + border:1px solid rgba(255,255,255,0.04); +} + +canvas{ + width:100%; + height:540px; + display:block; + border-radius:10px; + background: linear-gradient(180deg,#02020a -10%, rgba(0,0,0,0.35) 70%), url('data:image/svg+xml;utf8,'); + box-shadow: + 0 20px 60px rgba(0,0,0,0.6), + inset 0 0 80px rgba(122,92,246,0.06); + border: 1px solid rgba(255,255,255,0.02); +} + +/* overlay */ +.overlay{ + position:absolute;inset:12px;display:flex;align-items:center;justify-content:center; +} +.overlay.hidden{display:none} +.overlay-card{ + background:linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.02)); + padding:22px;border-radius:14px;border:1px solid rgba(255,255,255,0.04); + box-shadow: 0 20px 60px rgba(2,6,23,0.6); + text-align:center; + width:360px; +} +.overlay-card h2{font-size:20px;margin-bottom:6px} +.overlay-card p{color:var(--muted);margin-bottom:12px} +.overlay-actions .btn{margin:6px} + +/* Footer */ +.footer{color:var(--muted);font-size:13px} + +/* small responsive */ +@media (max-width:980px){ + .page{padding:12px} + .game-area{flex-direction:column} + .hud{width:100%} + canvas{height:420px} +} diff --git a/games/un-tangle/index.html b/games/un-tangle/index.html new file mode 100644 index 00000000..5f4eac43 --- /dev/null +++ b/games/un-tangle/index.html @@ -0,0 +1,38 @@ + + + + + + Un-tangle + + + +
      +

      Un-tangle

      +

      Click and drag the nodes until no lines are crossing.

      +
      + + +
      +
      +
      Level: 1
      +
      + Moves: 0 + Time: 0s +
      +
      + + +
      +
      +
      + + + + \ No newline at end of file diff --git a/games/un-tangle/script.js b/games/un-tangle/script.js new file mode 100644 index 00000000..17495d55 --- /dev/null +++ b/games/un-tangle/script.js @@ -0,0 +1,189 @@ +// --- DOM Elements --- +const canvas = document.getElementById('game-canvas'); +const ctx = canvas.getContext('2d'); +const winOverlay = document.getElementById('win-overlay'); +const nextLevelBtn = document.getElementById('next-level-btn'); +const levelDisplay = document.getElementById('level-display'); +const movesCountEl = document.getElementById('moves-count'); +const timerDisplayEl = document.getElementById('timer-display'); +const winStatsEl = document.getElementById('win-stats'); +const hintBtn = document.getElementById('hint-btn'); +const restartBtn = document.getElementById('restart-btn'); + +// --- Game Constants --- +const NODE_RADIUS = 15; +const NODE_COLOR = '#3498db'; +const NODE_DRAG_COLOR = '#f1c40f'; +const LINE_COLOR = '#ecf0f1'; +const LINE_INTERSECT_COLOR = '#e74c3c'; +const PADDING = 40; + +// --- Game State --- +let nodes = []; +let lines = []; +let selectedNodeIndex = -1; +let isDragging = false; +let offsetX = 0; +let offsetY = 0; +let currentLevel = 0; +let moveCount = 0; +let timerInterval; +let startTime; +let hintNodeIndex = -1; + +// --- Level Generation & Game Flow --- +function generateLevel(levelNumber) { + nodes = []; + lines = []; + const numNodes = Math.min(15, 4 + levelNumber); + const numLines = Math.floor(numNodes * (1.2 + levelNumber * 0.1)); + for (let i = 0; i < numNodes; i++) { + nodes.push({ x: PADDING + Math.random() * (canvas.width - PADDING * 2), y: PADDING + Math.random() * (canvas.height - PADDING * 2) }); + } + const connectedNodes = new Set(); + for (let i = 0; i < numNodes - 1; i++) { lines.push([i, i + 1]); connectedNodes.add(`${i}-${i+1}`); } + while (lines.length < numLines) { + const nodeA = Math.floor(Math.random() * numNodes); + const nodeB = Math.floor(Math.random() * numNodes); + const connection1 = `${nodeA}-${nodeB}`; const connection2 = `${nodeB}-${nodeA}`; + if (nodeA !== nodeB && !connectedNodes.has(connection1) && !connectedNodes.has(connection2)) { + lines.push([nodeA, nodeB]); connectedNodes.add(connection1); + } + } +} + +function loadLevel(levelIndex) { + currentLevel = levelIndex; + moveCount = 0; + movesCountEl.textContent = moveCount; + generateLevel(currentLevel); + + winOverlay.classList.add('hidden'); + levelDisplay.textContent = `Level: ${currentLevel + 1}`; + startTimer(); + draw(); +} + +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + const intersectingLines = findIntersectingLines(); + + lines.forEach((line, index) => { + const [startNode, endNode] = [nodes[line[0]], nodes[line[1]]]; + ctx.beginPath(); + ctx.moveTo(startNode.x, startNode.y); + ctx.lineTo(endNode.x, endNode.y); + ctx.lineWidth = 4; + ctx.strokeStyle = intersectingLines.has(index) ? LINE_INTERSECT_COLOR : LINE_COLOR; + ctx.stroke(); + }); + + nodes.forEach((node, index) => { + ctx.beginPath(); + ctx.arc(node.x, node.y, NODE_RADIUS, 0, Math.PI * 2); + ctx.fillStyle = (index === selectedNodeIndex) ? NODE_DRAG_COLOR : NODE_COLOR; + ctx.fill(); + if (index === hintNodeIndex) { ctx.strokeStyle = NODE_DRAG_COLOR; ctx.lineWidth = 3; ctx.stroke(); } + }); + + if (intersectingLines.size === 0 && lines.length > 0) { + levelComplete(); + } +} + +function levelComplete() { + stopTimer(); + const timeTaken = Math.floor((Date.now() - startTime) / 1000); + winStatsEl.innerHTML = `Moves: ${moveCount}   |   Time: ${timeTaken}s`; + winOverlay.classList.remove('hidden'); + saveGame(); +} + +// --- Scoring and Timer --- +function startTimer() { + startTime = Date.now(); + if (timerInterval) clearInterval(timerInterval); + timerInterval = setInterval(() => { + const seconds = Math.floor((Date.now() - startTime) / 1000); + timerDisplayEl.textContent = `${seconds}s`; + }, 1000); +} +function stopTimer() { clearInterval(timerInterval); } + +// --- Hint System --- +function getHint() { + const intersectingLines = findIntersectingLines(); + if (intersectingLines.size === 0) return; + const nodeIntersectionCount = new Array(nodes.length).fill(0); + intersectingLines.forEach(lineIndex => { const [nodeA, nodeB] = lines[lineIndex]; nodeIntersectionCount[nodeA]++; nodeIntersectionCount[nodeB]++; }); + let maxIntersections = -1; + let mostTangledNode = -1; + for (let i = 0; i < nodeIntersectionCount.length; i++) { if (nodeIntersectionCount[i] > maxIntersections) { maxIntersections = nodeIntersectionCount[i]; mostTangledNode = i; } } + if (mostTangledNode !== -1) { + hintNodeIndex = mostTangledNode; + draw(); + hintBtn.disabled = true; + setTimeout(() => { hintNodeIndex = -1; hintBtn.disabled = false; draw(); }, 1500); + } +} + +// --- Event Handlers --- +canvas.addEventListener('mousedown', (e) => { + const mousePos = getMousePos(e); + for (let i = nodes.length - 1; i >= 0; i--) { + const node = nodes[i]; + const dist = Math.sqrt((mousePos.x - node.x) ** 2 + (mousePos.y - node.y) ** 2); + if (dist < NODE_RADIUS) { isDragging = true; selectedNodeIndex = i; offsetX = mousePos.x - node.x; offsetY = mousePos.y - node.y; break; } + } +}); +canvas.addEventListener('mousemove', (e) => { + if (isDragging) { const mousePos = getMousePos(e); nodes[selectedNodeIndex].x = mousePos.x - offsetX; nodes[selectedNodeIndex].y = mousePos.y - offsetY; draw(); } +}); +canvas.addEventListener('mouseup', () => { if (isDragging) { moveCount++; movesCountEl.textContent = moveCount; } isDragging = false; selectedNodeIndex = -1; draw(); }); +canvas.addEventListener('mouseout', () => { if (isDragging) { moveCount++; movesCountEl.textContent = moveCount; } isDragging = false; selectedNodeIndex = -1; draw(); }); +nextLevelBtn.addEventListener('click', () => { loadLevel(currentLevel + 1); }); +hintBtn.addEventListener('click', getHint); +restartBtn.addEventListener('click', restartGame); + +// --- Game Reset Functionality --- +function restartGame() { + if (confirm("Are you sure you want to restart from Level 1? Your saved progress will be lost.")) { + localStorage.removeItem('untangle_level'); + loadLevel(0); // Soft reset without reloading page + } +} + +// --- Save/Load Progress --- +function saveGame() { localStorage.setItem('untangle_level', currentLevel + 1); } +function loadGame() { + const savedLevel = localStorage.getItem('untangle_level'); + currentLevel = savedLevel ? parseInt(savedLevel) : 0; + loadLevel(currentLevel); +} + +// --- Core Intersection Logic (Unchanged) --- +function findIntersectingLines() { + const intersecting = new Set(); + for (let i = 0; i < lines.length; i++) { + for (let j = i + 1; j < lines.length; j++) { + const l1 = lines[i], l2 = lines[j]; + const p1 = nodes[l1[0]], p2 = nodes[l1[1]], p3 = nodes[l2[0]], p4 = nodes[l2[1]]; + if (l1[0] === l2[0] || l1[0] === l2[1] || l1[1] === l2[0] || l1[1] === l2[1]) continue; + if (doLinesIntersect(p1, p2, p3, p4)) { intersecting.add(i); intersecting.add(j); } + } + } + return intersecting; +} +function doLinesIntersect(p1,p2,p3,p4){function o(p,q,r){const v=(q.y-p.y)*(r.x-q.x)-(q.x-p.x)*(r.y-q.y);if(v===0)return 0;return(v>0)?1:2}function on(p,q,r){return(q.x<=Math.max(p.x,r.x)&&q.x>=Math.min(p.x,r.x)&&q.y<=Math.max(p.y,r.y)&&q.y>=Math.min(p.y,r.y))}const o1=o(p1,p2,p3),o2=o(p1,p2,p4),o3=o(p3,p4,p1),o4=o(p3,p4,p2);if(o1!==o2&&o3!==o4)return true;if(o1===0&&on(p1,p3,p2))return true;if(o2===0&&on(p1,p4,p2))return true;if(o3===0&&on(p3,p1,p4))return true;if(o4===0&&on(p3,p2,p4))return true;return false} + +// --- REFACTORED: Utility for Responsive Canvas --- +function getMousePos(evt) { + const rect = canvas.getBoundingClientRect(); + // Translate mouse coordinates from screen space to canvas's internal 600x600 space + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + return { x: (evt.clientX - rect.left) * scaleX, y: (evt.clientY - rect.top) * scaleY }; +} + +// --- Start Game --- +loadGame(); \ No newline at end of file diff --git a/games/un-tangle/style.css b/games/un-tangle/style.css new file mode 100644 index 00000000..50435695 --- /dev/null +++ b/games/un-tangle/style.css @@ -0,0 +1,100 @@ +/* --- Core Responsive Layout (Fixes Overflow) --- */ +html, body { + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; /* Prevent all scrolling */ +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + background-color: #2c3e50; + color: #ecf0f1; + display: flex; + align-items: center; + justify-content: center; + text-align: center; +} + +#main-content { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + max-width: 100%; + max-height: 100%; +} + +/* --- Typography & UI --- */ +h1 { font-size: 2.5em; margin-bottom: 10px; color: white; } +p { font-size: 1.2em; color: #bdc3c7; margin-top: 0; } + +/* REBUILT: Fully responsive game container and canvas */ +#game-container { + position: relative; + width: clamp(300px, 80vmin, 600px); /* Responsive width: min 300px, max 600px, ideally 80% of viewport smaller dimension */ + height: clamp(300px, 80vmin, 600px); /* Responsive height */ + margin: 15px auto; +} + +#game-canvas { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: #34495e; + border-radius: 15px; + box-shadow: 0 5px 20px rgba(0,0,0,0.4); +} + +/* REBUILT: Dashboard layout with perfect alignment */ +#game-info { + width: 100%; + max-width: 600px; + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0; + font-size: clamp(1rem, 2.5vmin, 1.5rem); /* Responsive font size */ + font-weight: bold; +} + +#stats-display { + display: flex; + gap: 20px; +} +#action-buttons { + display: flex; + gap: 10px; +} +#hint-btn, #restart-btn { + padding: 10px 20px; + font-size: 0.9em; + border: none; + border-radius: 10px; + color: white; + cursor: pointer; + transition: all 0.2s; +} +#hint-btn { background-color: #3498db; } +#restart-btn { background-color: #e74c3c; } +#hint-btn:hover, #restart-btn:hover { transform: translateY(-2px); box-shadow: 0 2px 5px rgba(0,0,0,0.2); } +#hint-btn:disabled { background-color: #7f8c8d; cursor: not-allowed; transform: none; box-shadow: none; } + +/* --- Win Overlay (Unchanged) --- */ +#win-overlay { + position: absolute; top: 0; left: 0; width: 100%; height: 100%; + background-color: rgba(44, 62, 80, 0.95); border-radius: 15px; + display: flex; flex-direction: column; align-items: center; justify-content: center; + color: #f1c40f; font-size: 2em; animation: fade-in 0.5s; +} +@keyframes fade-in { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } } +#win-stats { font-size: 0.6em; color: #ecf0f1; margin-top: -10px; margin-bottom: 20px; } +#next-level-btn { + padding: 15px 30px; font-size: 1em; border: none; border-radius: 10px; + background-color: #f1c40f; color: #2c3e50; cursor: pointer; transition: all 0.2s; +} +#next-level-btn:hover { background-color: #f39c12; transform: translateY(-3px); box-shadow: 0 4px 10px rgba(0,0,0,0.3); } + +.hidden { display: none !important; } \ No newline at end of file diff --git a/games/underwater-diver/index.html b/games/underwater-diver/index.html new file mode 100644 index 00000000..8587f50b --- /dev/null +++ b/games/underwater-diver/index.html @@ -0,0 +1,27 @@ + + + + + + Underwater Diver + + + +
      +
      Score: 0
      +
      Oxygen: 100%
      +
      Lives: 3
      +
      Power-up: None
      + + +
      + + + + + + + + + + diff --git a/games/underwater-diver/script.js b/games/underwater-diver/script.js new file mode 100644 index 00000000..ae92455f --- /dev/null +++ b/games/underwater-diver/script.js @@ -0,0 +1,160 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); +canvas.width = window.innerWidth; +canvas.height = window.innerHeight; + +let score = 0; +let oxygen = 100; +let lives = 3; +let powerup = "None"; +let isPaused = false; + +// HUD elements +const scoreEl = document.getElementById("score"); +const oxygenEl = document.getElementById("oxygen"); +const livesEl = document.getElementById("lives"); +const powerupEl = document.getElementById("powerup"); + +// Sounds +const collectSound = document.getElementById("collectSound"); +const hitSound = document.getElementById("hitSound"); +const powerupSound = document.getElementById("powerupSound"); +const bgMusic = document.getElementById("bgMusic"); + +// Buttons +document.getElementById("pauseBtn").addEventListener("click", () => { + isPaused = !isPaused; +}); +document.getElementById("restartBtn").addEventListener("click", () => { + restartGame(); +}); + +// Diver +const diver = { + x: 100, + y: canvas.height / 2, + width: 60, + height: 60, + vy: 0, + color: "#FFD700", + thrust: -0.7, + gravity: 0.3 +}; + +// Obstacles +const obstacles = []; +const pearls = []; + +// Control +let up = false; +window.addEventListener("keydown", (e) => { + if(e.key === "w" || e.key === "ArrowUp") up = true; +}); +window.addEventListener("keyup", (e) => { + if(e.key === "w" || e.key === "ArrowUp") up = false; +}); +canvas.addEventListener("touchstart", ()=> up = true); +canvas.addEventListener("touchend", ()=> up = false); + +function spawnObstacle() { + const height = Math.random() * 150 + 50; + obstacles.push({x: canvas.width, y: Math.random() * (canvas.height - height), width: 50, height, color: "#FF4500"}); +} +function spawnPearl() { + const y = Math.random() * (canvas.height - 30); + pearls.push({x: canvas.width, y, radius: 15, color: "#00FFFF"}); +} + +let frame = 0; +function update() { + if(isPaused) return; + frame++; + ctx.clearRect(0,0,canvas.width,canvas.height); + + // Diver movement + if(up) diver.vy += diver.thrust; + diver.vy += diver.gravity; + diver.y += diver.vy; + diver.vy *= 0.9; // damping + if(diver.y < 0) diver.y = 0; + if(diver.y + diver.height > canvas.height) diver.y = canvas.height - diver.height; + + // Spawn obstacles + if(frame % 150 === 0) spawnObstacle(); + if(frame % 200 === 0) spawnPearl(); + + // Move obstacles + obstacles.forEach((ob, i) => { + ob.x -= 5; + ctx.fillStyle = ob.color; + ctx.fillRect(ob.x, ob.y, ob.width, ob.height); + + // Collision + if(diver.x < ob.x + ob.width && + diver.x + diver.width > ob.x && + diver.y < ob.y + ob.height && + diver.y + diver.height > ob.y) { + hitSound.play(); + lives--; + obstacles.splice(i,1); + } + + if(ob.x + ob.width < 0) obstacles.splice(i,1); + }); + + // Move pearls + pearls.forEach((p, i) => { + p.x -= 5; + ctx.beginPath(); + ctx.arc(p.x, p.y, p.radius, 0, Math.PI*2); + ctx.fillStyle = p.color; + ctx.shadowColor = "#00FFFF"; + ctx.shadowBlur = 15; + ctx.fill(); + ctx.shadowBlur = 0; + + // Collect + if(diver.x < p.x + p.radius && + diver.x + diver.width > p.x - p.radius && + diver.y < p.y + p.radius && + diver.y + diver.height > p.y - p.radius) { + collectSound.play(); + score += 10; + pearls.splice(i,1); + } + + if(p.x + p.radius < 0) pearls.splice(i,1); + }); + + // Oxygen decreases + if(frame % 60 === 0) oxygen -= 1; + if(oxygen <= 0 || lives <= 0) { + alert("Game Over! Score: " + score); + restartGame(); + } + + // Draw diver + ctx.fillStyle = diver.color; + ctx.fillRect(diver.x, diver.y, diver.width, diver.height); + + // Update HUD + scoreEl.textContent = score; + oxygenEl.textContent = oxygen; + livesEl.textContent = lives; + powerupEl.textContent = powerup; + + requestAnimationFrame(update); +} + +function restartGame() { + score = 0; + oxygen = 100; + lives = 3; + frame = 0; + diver.y = canvas.height / 2; + obstacles.length = 0; + pearls.length = 0; + isPaused = false; +} + +update(); diff --git a/games/underwater-diver/style.css b/games/underwater-diver/style.css new file mode 100644 index 00000000..29e28580 --- /dev/null +++ b/games/underwater-diver/style.css @@ -0,0 +1,44 @@ +body { + margin: 0; + padding: 0; + overflow: hidden; + font-family: 'Arial', sans-serif; + background: linear-gradient(to bottom, #0077be, #004466); + color: #fff; +} + +canvas { + display: block; + background: linear-gradient(to bottom, #0077be, #004466); +} + +.hud { + position: absolute; + top: 10px; + left: 10px; + right: 10px; + display: flex; + justify-content: space-between; + align-items: center; + z-index: 100; + font-size: 16px; + background: rgba(0,0,0,0.3); + padding: 8px 12px; + border-radius: 8px; +} + +button { + background: #00f9ff; + border: none; + color: #000; + font-weight: bold; + padding: 6px 10px; + border-radius: 5px; + cursor: pointer; + transition: 0.2s; +} + +button:hover { + background: #00ffff; + box-shadow: 0 0 10px #00ffff; +} diff --git a/games/virus-defender/index.html b/games/virus-defender/index.html new file mode 100644 index 00000000..29708b3a --- /dev/null +++ b/games/virus-defender/index.html @@ -0,0 +1,69 @@ + + + + + + Virus Defender โ€” Mini JS Games Hub + + + + + +
      +
      +
      + ๐Ÿฆ  +

      Virus Defender

      + Defend the core โ€” click or tap viruses before they reach it +
      + +
      +
      +
      0
      +
      5
      +
      1
      +
      + +
      + + + +
      +
      +
      + +
      + + + + + +
      + +
      +

      How to Play

      +
        +
      • Click or tap a virus before it reaches the glowing core.
      • +
      • Each destroyed virus gives points. Missed viruses reduce core health.
      • +
      • Difficulty ramps up: spawn rate and speed increase over time.
      • +
      • Use Pause / Restart / Mute controls. Works on mobile and desktop.
      • +
      +
      + +
      + Built with HTML โ€ข CSS โ€ข JS โ€” assets from Mixkit & SoundJay (direct links). +
      +
      + + + + diff --git a/games/virus-defender/script.js b/games/virus-defender/script.js new file mode 100644 index 00000000..2718a0ec --- /dev/null +++ b/games/virus-defender/script.js @@ -0,0 +1,543 @@ +/* Virus Defender - script.js + - Place in games/virus-defender/script.js + - Click/tap viruses to destroy them + - Progressive difficulty, pause/restart/mute + - Uses online sound links (Mixkit / SoundJay) +*/ + +// -------------------- Configuration -------------------- +const CONFIG = { + canvasWidth: 1200, + canvasHeight: 700, + initialSpawnInterval: 1400, // ms + minSpawnInterval: 380, + spawnAcceleration: 0.985, // multiply spawnInterval each wave + initialVirusSpeed: 0.6, // base speed pixel/frame + speedAcceleration: 1.02, // multiply virus speed each wave + health: 5, + pointsPerVirus: 10, + hitRadiusPadding: 6, // extra hitbox for fingers + assets: { + // sounds (online) + sfxPop: "https://assets.mixkit.co/sfx/preview/mixkit-quick-jump-arcade-239.wav", + sfxHit: "https://assets.mixkit.co/sfx/preview/mixkit-sci-fi-burst-718.wav", + sfxCoreHit: "https://www.soundjay.com/misc/sounds/bell-ringing-01.mp3", + bgm: "https://assets.mixkit.co/music/preview/mixkit-energetic-electronic-1177.mp3" + } +}; + +// -------------------- DOM Elements -------------------- +const canvas = document.getElementById("gameCanvas"); +const scoreEl = document.getElementById("score"); +const healthEl = document.getElementById("health"); +const levelEl = document.getElementById("level"); +const pauseBtn = document.getElementById("pauseBtn"); +const restartBtn = document.getElementById("restartBtn"); +const muteBtn = document.getElementById("muteBtn"); +const overlay = document.getElementById("overlay"); +const overlayTitle = document.getElementById("overlay-title"); +const overlaySub = document.getElementById("overlay-sub"); +const resumeBtn = document.getElementById("resumeBtn"); +const overlayRestart = document.getElementById("overlayRestart"); + +// high-dpi scaling +function setupCanvas(c, w, h) { + const dpr = Math.min(window.devicePixelRatio || 1, 2); + c.width = w * dpr; + c.height = h * dpr; + c.style.width = Math.min(w, window.innerWidth - 80) + "px"; + c.style.height = (h * (c.style.width.replace("px", "")/w)) + "px"; + c.getContext("2d").scale(dpr, dpr); +} +setupCanvas(canvas, CONFIG.canvasWidth, CONFIG.canvasHeight); +const ctx = canvas.getContext("2d"); + +// -------------------- Audio -------------------- +let muted = false; +const audio = { + pop: new Audio(CONFIG.assets.sfxPop), + hit: new Audio(CONFIG.assets.sfxHit), + core: new Audio(CONFIG.assets.sfxCoreHit), + bgm: new Audio(CONFIG.assets.bgm), +}; +audio.bgm.loop = true; +audio.bgm.volume = 0.18; +audio.pop.volume = 0.75; +audio.hit.volume = 0.8; +audio.core.volume = 0.8; + +function setMuted(v) { + muted = v; + audio.bgm.muted = v; + audio.pop.muted = v; + audio.hit.muted = v; + audio.core.muted = v; + muteBtn.textContent = v ? "๐Ÿ”‡" : "๐Ÿ”Š"; +} +setMuted(false); + +// -------------------- Game State -------------------- +let state = { + running: true, + score: 0, + health: CONFIG.health, + spawnInterval: CONFIG.initialSpawnInterval, + lastSpawn: 0, + viruses: [], + virusSpeed: CONFIG.initialVirusSpeed, + level: 1, + lastFrame: 0, + paused: false, + gameOver: false, +}; + +// -------------------- Utility -------------------- +function rand(min, max) { return Math.random() * (max - min) + min; } +function dist(a, b, c, d) { return Math.hypot(a - c, b - d); } +function lightColor(hex, a=0.55) { return hex + Math.floor(a*255).toString(16).padStart(2,'0'); } + +// -------------------- Virus class -------------------- +class Virus { + constructor() { + // spawn from random edge + const side = Math.floor(Math.random() * 4); + // spawn area padding + const pad = 30; + if (side === 0) { // top + this.x = rand(pad, CONFIG.canvasWidth - pad); + this.y = -30; + } else if (side === 1) { // right + this.x = CONFIG.canvasWidth + 30; + this.y = rand(pad, CONFIG.canvasHeight - pad); + } else if (side === 2) { // bottom + this.x = rand(pad, CONFIG.canvasWidth - pad); + this.y = CONFIG.canvasHeight + 30; + } else { // left + this.x = -30; + this.y = rand(pad, CONFIG.canvasHeight - pad); + } + + // target: core at center + this.targetX = CONFIG.canvasWidth / 2; + this.targetY = CONFIG.canvasHeight / 2; + + // size & speed + this.baseRadius = rand(18, 38); + this.radius = this.baseRadius; + // velocity towards center (normalized) + const dx = this.targetX - this.x; + const dy = this.targetY - this.y; + const len = Math.hypot(dx, dy) || 1; + this.vx = (dx / len) * (state.virusSpeed * (0.9 + Math.random()*0.8)); + this.vy = (dy / len) * (state.virusSpeed * (0.9 + Math.random()*0.8)); + + // wobble & animation + this.angle = Math.random() * Math.PI * 2; + this.spin = rand(-0.03, 0.03); + this.hit = false; + this.id = Math.random().toString(36).slice(2,9); + this.color = `hsl(${rand(250, 360)}, ${rand(65,90)}%, ${rand(45,65)}%)`; + this.spawnTime = performance.now(); + } + + update(dt) { + // move + this.x += this.vx * dt; + this.y += this.vy * dt; + + // slight oscillation + this.angle += this.spin * dt; + this.radius = this.baseRadius + Math.sin((performance.now() - this.spawnTime) / 180) * 3; + } + + draw(ctx) { + // glow + ctx.save(); + ctx.beginPath(); + ctx.shadowColor = this.color; + ctx.shadowBlur = 26; + ctx.fillStyle = this.color; + // spiky body by drawing multiple circles around + ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); + ctx.fill(); + ctx.closePath(); + + // spikes + const spikes = 8; + for (let i=0;i { + handleClick(getCanvasMouse(e)); +}); +canvas.addEventListener("touchstart", (e) => { + e.preventDefault(); + handleClick(getCanvasMouse(e)); +}, {passive:false}); + +function handleClick(pos) { + if (state.paused || state.gameOver) return; + // reverse iterate to hit front viruses first + for (let i = state.viruses.length - 1; i >= 0; i--) { + const v = state.viruses[i]; + if (v.isHit(pos.x, pos.y)) { + // kill virus + audio.pop.currentTime = 0; + if (!muted) audio.pop.play().catch(()=>{}); + // points & explosion effect + state.score += CONFIG.pointsPerVirus; + spawnHitBurst(v.x, v.y, v.color); + // remove virus + state.viruses.splice(i, 1); + updateHUD(); + return; + } + } +} + +// visual burst particles on kill +const bursts = []; +function spawnHitBurst(x,y,color){ + audio.hit.currentTime = 0; + if (!muted) audio.hit.play().catch(()=>{}); + for (let i=0;i<12;i++){ + bursts.push({ + x, y, + vx: rand(-2.6,2.6), + vy: rand(-2.6,2.6), + t: 0, ttl: rand(400,900), + c: color, + r: rand(2,6), + }); + } +} + +// -------------------- Game Loop & Update -------------------- +function updateHUD() { + scoreEl.textContent = state.score; + healthEl.textContent = state.health; + levelEl.textContent = state.level; +} + +// remove viruses that reached core +function checkCoreCollisions() { + for (let i = state.viruses.length -1; i >= 0; i--) { + const v = state.viruses[i]; + if (v.reachedCore()) { + // play core hit + audio.core.currentTime = 0; + if (!muted) audio.core.play().catch(()=>{}); + state.viruses.splice(i,1); + state.health -= 1; + spawnCoreBurst(core.x + rand(-20,20), core.y + rand(-20,20)); + updateHUD(); + if (state.health <= 0) { + endGame(); + } + } + } +} + +function spawnCoreBurst(x,y){ + for (let i=0;i<28;i++){ + bursts.push({ + x, y, + vx: rand(-6,6), + vy: rand(-6,6), + t: 0, ttl: rand(500,1300), + c: "#ff6b6b", + r: rand(3,8), + }); + } +} + +// update & draw particles +function updateBursts(dt){ + for (let i = bursts.length -1; i>=0; i--){ + const p = bursts[i]; + p.t += dt; + p.x += p.vx * (dt/16); + p.y += p.vy * (dt/16); + if (p.t > p.ttl) bursts.splice(i,1); + } +} + +// main loop +function loop(ts) { + if (!state.lastFrame) state.lastFrame = ts; + const dt = Math.min(40, ts - state.lastFrame); // clamp dt + state.lastFrame = ts; + + if (!state.paused && !state.gameOver) { + // spawn logic + if (ts - state.lastSpawn > state.spawnInterval) { + spawnWave(); + state.lastSpawn = ts; + } + + // update virus speed & difficulty gradually + // every 10s increment level + if (Math.floor(ts / 10000) + 1 > state.level) { + state.level = Math.floor(ts / 10000) + 1; + // accelerate spawn & speed + state.spawnInterval = Math.max(CONFIG.minSpawnInterval, state.spawnInterval * CONFIG.spawnAcceleration); + state.virusSpeed *= CONFIG.speedAcceleration; + } + + // update viruses + state.viruses.forEach(v => v.update(dt)); + // check collisions with core + checkCoreCollisions(); + // update bursts + updateBursts(dt); + } + + // draw + drawScene(ts); + + // schedule next + if (!state.gameOver) requestAnimationFrame(loop); +} + +// draw everything +function drawScene(ts){ + // clear + ctx.clearRect(0,0,CONFIG.canvasWidth, CONFIG.canvasHeight); + + // background subtle grid + ctx.save(); + ctx.globalAlpha = 0.08; + ctx.fillStyle = "#ffffff"; + for (let i=0;i<50;i++){ + ctx.beginPath(); + ctx.arc(40 + i*24, 40 + Math.sin((i*18 + ts/80) * 0.02) * 6, 1, 0, Math.PI*2); + ctx.fill(); + } + ctx.restore(); + + // draw core + core.draw(ctx, ts); + + // draw viruses + state.viruses.forEach(v => v.draw(ctx)); + + // draw bursts + bursts.forEach(p => { + ctx.save(); + ctx.globalAlpha = 1 - (p.t / p.ttl); + ctx.beginPath(); + ctx.fillStyle = p.c; + ctx.arc(p.x, p.y, p.r, 0, Math.PI*2); + ctx.fill(); + ctx.restore(); + }); + + // HUD subtle overlay on canvas + ctx.save(); + ctx.globalAlpha = 0.06; + ctx.fillStyle = "#ffffff"; + ctx.fillRect(12,12,200,60); + ctx.restore(); + + // If paused overlay + if (state.paused && !overlay.classList.contains("hidden")) { + // overlay handled by DOM + } +} + +// -------------------- End / Restart -------------------- +function endGame() { + state.gameOver = true; + overlayTitle.textContent = "Game Over"; + overlaySub.textContent = `Score: ${state.score}. Tap Restart to try again.`; + overlay.classList.remove("hidden"); + // stop background music + try { audio.bgm.pause(); } catch(e){} +} + +// restart +function restartGame() { + // reset + state.running = true; + state.score = 0; + state.health = CONFIG.health; + state.spawnInterval = CONFIG.initialSpawnInterval; + state.viruses = []; + state.virusSpeed = CONFIG.initialVirusSpeed; + state.level = 1; + state.lastSpawn = performance.now(); + state.gameOver = false; + state.paused = false; + state.lastFrame = 0; + bursts.length = 0; + updateHUD(); + overlay.classList.add("hidden"); + if (!muted) audio.bgm.play().catch(()=>{}); + requestAnimationFrame(loop); +} + +// pause/resume +function openPause(text="Paused") { + state.paused = true; + overlayTitle.textContent = text; + overlaySub.textContent = "Tap Resume to continue or Restart to start over."; + overlay.classList.remove("hidden"); +} +function closePause(){ + state.paused = false; + overlay.classList.add("hidden"); +} + +// -------------------- Event Listeners -------------------- +pauseBtn.addEventListener("click", ()=>{ + if (state.paused) { + closePause(); + } else { + openPause("Paused"); + } +}); + +resumeBtn.addEventListener("click", ()=>{ + closePause(); +}); + +restartBtn.addEventListener("click", ()=> { + startClickFeedback(); // small haptic-ish effect on mobile (no-op fallback) + restartGame(); +}); + +overlayRestart.addEventListener("click", ()=> { + restartGame(); +}); + +muteBtn.addEventListener("click", ()=>{ + setMuted(!muted); +}); + +// keyboard shortcuts +window.addEventListener("keydown", (e)=>{ + if (e.key === " " || e.key.toLowerCase() === "p") { + e.preventDefault(); + pauseBtn.click(); + } else if (e.key.toLowerCase() === "m") { + muteBtn.click(); + } else if (e.key.toLowerCase() === "r") { + restartBtn.click(); + } +}); + +// tiny vibration emulator (only if supported) +function startClickFeedback(){ + if (navigator.vibrate) navigator.vibrate(10); +} + +// -------------------- Init -------------------- +function init(){ + updateHUD(); + state.lastSpawn = performance.now(); + // play bg music after user gesture (modern browsers restrict autoplay) + document.addEventListener("click", function startMusicOnce(){ + if (!muted) { + audio.bgm.play().catch(()=>{}); + } + document.removeEventListener("click", startMusicOnce); + }); + + // start loop + requestAnimationFrame(loop); +} + +// start +init(); + +// ensure canvas resizes on window change +window.addEventListener("resize", () => { + setupCanvas(canvas, CONFIG.canvasWidth, CONFIG.canvasHeight); +}); diff --git a/games/virus-defender/style.css b/games/virus-defender/style.css new file mode 100644 index 00000000..70cda83f --- /dev/null +++ b/games/virus-defender/style.css @@ -0,0 +1,122 @@ +:root{ + --bg:#080812; + --panel:#0f1220; + --accent:#7c4dff; + --glow: rgba(124,77,255,0.45); + --danger:#ff4d6d; + --muted: #9aa0c7; + --glass: rgba(255,255,255,0.04); + --glass-2: rgba(255,255,255,0.02); + --radius: 14px; + --card-pad: 18px; + --shadow: 0 8px 30px rgba(3,5,15,0.6); +} + +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + font-family: Inter, "Segoe UI", Roboto, system-ui, -apple-system, "Helvetica Neue", Arial; + background: radial-gradient(circle at 10% 10%, rgba(124,77,255,0.06), transparent 6%), + linear-gradient(180deg, #05050a 0%, #0a0b10 100%); + color:#e9eef8; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + padding:28px; + display:flex; + align-items:center; + justify-content:center; +} + +.game-shell{ + width:100%; + max-width:1100px; + border-radius:18px; + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + padding:20px; + box-shadow: var(--shadow); + overflow:hidden; +} + +/* Header */ +.game-header{ + display:flex; + align-items:center; + justify-content:space-between; + gap:20px; + margin-bottom:14px; +} + +.title{ + display:flex; + gap:14px; + align-items:center; +} +.title .icon{ + font-size:34px; + display:inline-grid; + place-items:center; + width:66px; + height:66px; + border-radius:12px; + background: linear-gradient(135deg,var(--accent), #4dc3ff); + box-shadow: 0 6px 28px var(--glow), inset 0 -6px 20px rgba(0,0,0,0.25); +} +.title h1{ + margin:0;font-size:20px; +} +.title small{display:block;color:var(--muted);margin-top:2px;font-size:12px} + +/* HUD & Buttons */ +.controls{display:flex;gap:12px; align-items:center} +.hud{display:flex;gap:10px} +.hud-item{background:var(--glass); padding:8px 12px; border-radius:12px; min-width:68px; text-align:center} +.hud-item label{display:block;font-size:11px;color:var(--muted); margin-bottom:6px} +.hud-item div{font-weight:700; font-size:16px} + +.buttons{display:flex;gap:8px} +.btn{background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); border:1px solid rgba(255,255,255,0.03); color: #eaf0ff; padding:8px 12px; border-radius:10px; cursor:pointer; font-weight:600} +.btn:hover{transform:translateY(-2px); box-shadow:0 10px 30px rgba(0,0,0,0.45)} +.btn.big{padding:12px 18px; font-size:16px} + +/* Game area */ +.game-area{position:relative; display:flex; align-items:center; justify-content:center; margin:10px 0; background: linear-gradient(180deg, rgba(255,255,255,0.01), rgba(255,255,255,0.005)); border-radius:12px; padding:12px} +#gameCanvas{ + display:block; + width:100%; + max-width:980px; + height:520px; + border-radius:12px; + background: radial-gradient(circle at center, rgba(124,77,255,0.06) 0%, rgba(0,0,0,0.35) 40%), + linear-gradient(180deg, #061025 0%, #03122a 100%); + box-shadow: 0 8px 40px rgba(2,6,20,0.6), inset 0 0 40px rgba(255,255,255,0.02); + border:1px solid rgba(255,255,255,0.03); +} + +/* Overlay */ +.overlay{ + position:absolute; inset:0; display:flex; align-items:center; justify-content:center; backdrop-filter: blur(6px); +} +.overlay-card{background:linear-gradient(180deg, rgba(12,12,20,0.85), rgba(10,10,16,0.75)); padding:26px; border-radius:12px; text-align:center; width:320px; box-shadow:0 20px 60px rgba(0,0,0,0.6); border:1px solid rgba(255,255,255,0.03)} +.overlay-card h2{margin:0 0 8px} +.overlay-card p{margin:0 0 18px; color:var(--muted)} +.overlay-actions{display:flex;gap:12px;justify-content:center} + +/* Instructions */ +.instructions{margin-top:12px; color:var(--muted); padding:14px; border-radius:10px; background:var(--glass-2)} +.instructions ul{margin:6px 0 0; padding:0 0 0 18px} +.instructions li{margin:6px 0} + +/* Footer */ +.game-footer{margin-top:10px; color:var(--muted); font-size:12px; text-align:center} + +/* utility */ +.hidden{display:none} + +/* Responsive adjustments */ +@media (max-width:760px){ + .game-header{flex-direction:column; align-items:flex-start; gap:8px} + .hud{order:2} + .buttons{order:1} + #gameCanvas{height:420px} +} diff --git a/games/vortex-jump/index.html b/games/vortex-jump/index.html new file mode 100644 index 00000000..388acd5a --- /dev/null +++ b/games/vortex-jump/index.html @@ -0,0 +1,31 @@ + + + + + + Vortex Jump | Mini JS Games Hub + + + +
      +

      Vortex Jump ๐ŸŒ€

      + +
      + + + + +
      +
      + Score: 0 +
      +
      + + + + + + + + + diff --git a/games/vortex-jump/script.js b/games/vortex-jump/script.js new file mode 100644 index 00000000..ef9a735e --- /dev/null +++ b/games/vortex-jump/script.js @@ -0,0 +1,152 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); +canvas.width = 400; +canvas.height = 600; + +const jumpSound = document.getElementById("jumpSound"); +const hitSound = document.getElementById("hitSound"); +const levelUpSound = document.getElementById("levelUpSound"); + +let gameInterval; +let obstacles = []; +let bulbs = []; +let score = 0; +let gravity = 0.6; +let player = { x: 200, y: 500, radius: 15, dy: 0, color: "#0ff" }; +let gameRunning = false; + +// Controls +document.getElementById("startBtn").addEventListener("click", startGame); +document.getElementById("pauseBtn").addEventListener("click", pauseGame); +document.getElementById("resumeBtn").addEventListener("click", resumeGame); +document.getElementById("restartBtn").addEventListener("click", restartGame); +canvas.addEventListener("click", jump); + +function jump() { + if (!gameRunning) return; + player.dy = -10; + jumpSound.play(); +} + +function startGame() { + if (gameRunning) return; + gameRunning = true; + obstacles = []; + bulbs = []; + score = 0; + player.y = 500; + player.dy = 0; + gameInterval = requestAnimationFrame(gameLoop); +} + +function pauseGame() { + gameRunning = false; + cancelAnimationFrame(gameInterval); +} + +function resumeGame() { + if (!gameRunning) { + gameRunning = true; + gameInterval = requestAnimationFrame(gameLoop); + } +} + +function restartGame() { + pauseGame(); + startGame(); +} + +function spawnObstacle() { + const x = Math.random() * 350 + 25; + const size = Math.random() * 80 + 30; + obstacles.push({ x, y: -size, width: size, height: 20, color: "#ff0" }); +} + +function spawnBulb() { + const x = Math.random() * 350 + 25; + const y = -20; + bulbs.push({ x, y, radius: 8, color: "#0f0" }); +} + +function drawPlayer() { + ctx.beginPath(); + ctx.arc(player.x, player.y, player.radius, 0, Math.PI * 2); + ctx.fillStyle = player.color; + ctx.shadowColor = "#0ff"; + ctx.shadowBlur = 20; + ctx.fill(); + ctx.closePath(); +} + +function drawObstacles() { + obstacles.forEach((o) => { + ctx.beginPath(); + ctx.fillStyle = o.color; + ctx.shadowColor = "#ff0"; + ctx.shadowBlur = 15; + ctx.fillRect(o.x, o.y, o.width, o.height); + ctx.closePath(); + }); +} + +function drawBulbs() { + bulbs.forEach((b) => { + ctx.beginPath(); + ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2); + ctx.fillStyle = b.color; + ctx.shadowColor = "#0f0"; + ctx.shadowBlur = 20; + ctx.fill(); + ctx.closePath(); + }); +} + +function checkCollision() { + // Obstacles + for (let o of obstacles) { + if ( + player.x + player.radius > o.x && + player.x - player.radius < o.x + o.width && + player.y + player.radius > o.y && + player.y - player.radius < o.y + o.height + ) { + hitSound.play(); + pauseGame(); + alert("Game Over! Score: " + score); + return true; + } + } + // Bulbs + bulbs.forEach((b, index) => { + let dist = Math.hypot(player.x - b.x, player.y - b.y); + if (dist < player.radius + b.radius) { + bulbs.splice(index, 1); + score += 1; + levelUpSound.play(); + } + }); + return false; +} + +function gameLoop() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + player.dy += gravity; + player.y += player.dy; + + // Spawn obstacles and bulbs + if (Math.random() < 0.02) spawnObstacle(); + if (Math.random() < 0.03) spawnBulb(); + + obstacles.forEach((o) => o.y += 2 + score * 0.05); + bulbs.forEach((b) => b.y += 2 + score * 0.05); + + drawPlayer(); + drawObstacles(); + drawBulbs(); + + if (checkCollision()) return; + + document.getElementById("score").textContent = score; + gameInterval = requestAnimationFrame(gameLoop); +} diff --git a/games/vortex-jump/style.css b/games/vortex-jump/style.css new file mode 100644 index 00000000..fd5c4332 --- /dev/null +++ b/games/vortex-jump/style.css @@ -0,0 +1,51 @@ +body { + margin: 0; + font-family: 'Arial', sans-serif; + background: radial-gradient(circle, #0f2027, #203a43, #2c5364); + color: #fff; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; +} + +.game-container { + text-align: center; +} + +canvas { + background: #111; + border-radius: 20px; + box-shadow: 0 0 40px rgba(0,255,255,0.5); + margin-top: 20px; +} + +.controls { + margin-top: 15px; +} + +button { + padding: 10px 15px; + margin: 0 5px; + font-size: 16px; + border-radius: 8px; + border: none; + background: #0ff; + color: #000; + font-weight: bold; + cursor: pointer; + box-shadow: 0 0 10px #0ff; + transition: 0.2s; +} + +button:hover { + transform: scale(1.1); + box-shadow: 0 0 20px #0ff, 0 0 30px #0ff inset; +} + +.score-display { + margin-top: 10px; + font-size: 20px; + font-weight: bold; +} diff --git a/games/vortex-vault/index.html b/games/vortex-vault/index.html new file mode 100644 index 00000000..e63b3b30 --- /dev/null +++ b/games/vortex-vault/index.html @@ -0,0 +1,18 @@ + + + + + + Vortex Vault Game + + + +
      +

      Vortex Vault

      + +
      Treasures: 0
      +
      Use arrow keys to swim. Press SPACE to jump out of vortices. Collect treasures, avoid traps!
      +
      + + + \ No newline at end of file diff --git a/games/vortex-vault/script.js b/games/vortex-vault/script.js new file mode 100644 index 00000000..cc8776c3 --- /dev/null +++ b/games/vortex-vault/script.js @@ -0,0 +1,167 @@ +// Vortex Vault Game Script +// Dive into swirling vortices to collect treasures while avoiding suction traps. + +const canvas = document.getElementById('game-canvas'); +const ctx = canvas.getContext('2d'); +const scoreElement = document.getElementById('score'); + +// Game variables +let player = { x: 400, y: 300, vx: 0, vy: 0, speed: 2 }; +let vortices = []; +let treasures = []; +let traps = []; +let score = 0; +let gameRunning = true; +let inVortex = false; + +// Constants +const friction = 0.95; + +// Initialize game +function init() { + // Create vortices + vortices.push({ x: 200, y: 200, radius: 80, strength: 0.5 }); + vortices.push({ x: 600, y: 400, radius: 80, strength: 0.5 }); + + // Create treasures + treasures.push({ x: 150, y: 150 }); + treasures.push({ x: 650, y: 150 }); + treasures.push({ x: 150, y: 450 }); + treasures.push({ x: 650, y: 450 }); + + // Create traps + traps.push({ x: 400, y: 100, width: 50, height: 50 }); + traps.push({ x: 400, y: 500, width: 50, height: 50 }); + + // Start game loop + requestAnimationFrame(gameLoop); +} + +// Game loop +function gameLoop() { + if (!gameRunning) return; + + update(); + draw(); + + requestAnimationFrame(gameLoop); +} + +// Update game state +function update() { + // Handle input + if (keys.ArrowUp) player.vy -= player.speed * 0.1; + if (keys.ArrowDown) player.vy += player.speed * 0.1; + if (keys.ArrowLeft) player.vx -= player.speed * 0.1; + if (keys.ArrowRight) player.vx += player.speed * 0.1; + + // Apply vortex forces + vortices.forEach(vortex => { + const dx = vortex.x - player.x; + const dy = vortex.y - player.y; + const dist = Math.sqrt(dx*dx + dy*dy); + if (dist < vortex.radius) { + inVortex = true; + const force = vortex.strength / dist; + player.vx += (dx / dist) * force; + player.vy += (dy / dist) * force; + } + }); + + // Apply friction + player.vx *= friction; + player.vy *= friction; + + // Move player + player.x += player.vx; + player.y += player.vy; + + // Keep in bounds + if (player.x < 0) player.x = canvas.width; + if (player.x > canvas.width) player.x = 0; + if (player.y < 0) player.y = canvas.height; + if (player.y > canvas.height) player.y = 0; + + // Check treasure collection + treasures.forEach((treasure, i) => { + if (Math.abs(player.x - treasure.x) < 20 && Math.abs(player.y - treasure.y) < 20) { + treasures.splice(i, 1); + score++; + if (treasures.length === 0) { + gameRunning = false; + alert('All treasures collected! You win!'); + } + } + }); + + // Check trap collision + traps.forEach(trap => { + if (player.x > trap.x && player.x < trap.x + trap.width && + player.y > trap.y && player.y < trap.y + trap.height) { + gameRunning = false; + alert('Sucked into a trap! Game Over. Treasures: ' + score); + } + }); +} + +// Draw everything +function draw() { + // Clear canvas with water effect + ctx.fillStyle = '#000022'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw vortices + vortices.forEach(vortex => { + ctx.strokeStyle = '#0080ff'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.arc(vortex.x, vortex.y, vortex.radius, 0, Math.PI * 2); + ctx.stroke(); + // Swirl effect + ctx.beginPath(); + ctx.arc(vortex.x, vortex.y, vortex.radius * 0.5, 0, Math.PI * 2); + ctx.stroke(); + }); + + // Draw treasures + ctx.fillStyle = '#ffff00'; + treasures.forEach(treasure => { + ctx.beginPath(); + ctx.arc(treasure.x, treasure.y, 10, 0, Math.PI * 2); + ctx.fill(); + }); + + // Draw traps + ctx.fillStyle = '#ff0000'; + traps.forEach(trap => ctx.fillRect(trap.x, trap.y, trap.width, trap.height)); + + // Draw player + ctx.fillStyle = '#00ffff'; + ctx.beginPath(); + ctx.arc(player.x, player.y, 15, 0, Math.PI * 2); + ctx.fill(); + + // Update score + scoreElement.textContent = 'Treasures: ' + score; +} + +// Handle input +let keys = {}; +document.addEventListener('keydown', e => { + keys[e.code] = true; + if (e.code === 'Space' && inVortex) { + // Jump out: teleport to random edge + const side = Math.floor(Math.random() * 4); + if (side === 0) { player.x = 0; player.y = Math.random() * canvas.height; } + else if (side === 1) { player.x = canvas.width; player.y = Math.random() * canvas.height; } + else if (side === 2) { player.x = Math.random() * canvas.width; player.y = 0; } + else { player.x = Math.random() * canvas.width; player.y = canvas.height; } + player.vx = 0; + player.vy = 0; + inVortex = false; + } +}); +document.addEventListener('keyup', e => keys[e.code] = false); + +// Start the game +init(); \ No newline at end of file diff --git a/games/vortex-vault/style.css b/games/vortex-vault/style.css new file mode 100644 index 00000000..791ab7d0 --- /dev/null +++ b/games/vortex-vault/style.css @@ -0,0 +1,38 @@ +body { + font-family: Arial, sans-serif; + background-color: #000; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +#game-container { + text-align: center; +} + +h1 { + font-size: 2.5em; + margin-bottom: 20px; + text-shadow: 0 0 10px #0080ff; +} + +#game-canvas { + border: 2px solid #0080ff; + background-color: #000022; + box-shadow: 0 0 20px #0080ff; +} + +#score { + font-size: 1.2em; + margin: 10px 0; + color: #ffff00; +} + +#instructions { + font-size: 1em; + margin-top: 10px; + color: #cccccc; +} \ No newline at end of file diff --git a/games/voyger_viewpoint/index.html b/games/voyger_viewpoint/index.html new file mode 100644 index 00000000..f29420a0 --- /dev/null +++ b/games/voyger_viewpoint/index.html @@ -0,0 +1,36 @@ + + + + + + The Viewport Voyager ๐Ÿ“ + + + + +
      +

      Viewport Voyager

      +

      Use your mouse to **resize the browser window** to manipulate the level geometry.

      +
      + Width: 0px | Height: 0px +
      +
      Current Level: 1
      +
      Resize the window until a platform aligns with the player!
      +
      + +
      +
      + +
      + +
      +
      + +
      + +
      +
      + + + + \ No newline at end of file diff --git a/games/voyger_viewpoint/script.js b/games/voyger_viewpoint/script.js new file mode 100644 index 00000000..9bc99ee1 --- /dev/null +++ b/games/voyger_viewpoint/script.js @@ -0,0 +1,233 @@ +// --- 1. Game Constants and State --- +const PLAYER_SIZE = 20; +const LEVEL_CONFIG = [ + { + id: 1, + goalW: 800, // Target window width to solve the puzzle + goalH: 600, // Target window height + message: "Resize to 800x600 to align the hidden platform and reach the Goal!", + platforms: { + 'platform-a': { w: 200, h: 20, relX: -100, relY: 100 }, // Positioned relative to center + 'platform-b': { w: 100, h: 20, relX: 300, relY: -50 }, + 'hidden-platform': { w: 150, h: 20, relX: 0, relY: 150, revealW: 790, revealH: 590 } + }, + goal: { w: 30, h: 30, relX: 350, relY: -100 } + } +]; + +let currentLevelIndex = 0; +let playerOffsetX = 0; // Player offset from the center (for WASD) +let playerOffsetY = 0; +let isGrounded = false; + +// --- 2. DOM Elements --- +const D = (id) => document.getElementById(id); +const $ = { + player: D('player'), + dimW: D('dim-w'), + dimH: D('dim-h'), + gameMessage: D('game-message'), + levelStatus: D('level-status'), + goal: D('goal'), + platformA: D('platform-a'), + platformB: D('platform-b'), + hiddenPlatform: D('hidden-platform') +}; + +// --- 3. Core Geometry and Resize Functions --- + +/** + * Updates the position and size of all level elements based on the current viewport. + */ +function updateLevelGeometry() { + const levelData = LEVEL_CONFIG[currentLevelIndex]; + const width = window.innerWidth; + const height = window.innerHeight; + + // UI Update + $.dimW.textContent = width; + $.dimH.textContent = height; + + // Calculate center of the viewport (which is the player's fixed point) + const centerX = width / 2; + const centerY = height / 2; + + // --- Dynamic Positioning --- + + // Platforms: Positioned relative to the viewport's center + // We use a combination of screen size and the relative coordinates defined in LEVEL_CONFIG. + + for (const key in levelData.platforms) { + const platformElement = D(key); + const p = levelData.platforms[key]; + + // 1. Position based on center offset + let finalX = centerX + p.relX; + let finalY = centerY + p.relY; + + // 2. Add an effect based on screen dimension (e.g., platforms move closer to the center as screen shrinks) + // Example: Shift the platform more negatively (left/up) as width/height increase + finalX -= (width - levelData.goalW) * 0.5; // Platform A shifts left if window is wider than goalW + + platformElement.style.width = `${p.w}px`; + platformElement.style.height = `${p.h}px`; + platformElement.style.left = `${finalX}px`; + platformElement.style.top = `${finalY}px`; + + // Hidden Platform Reveal Logic + if (p.revealW && p.revealH) { + const distW = Math.abs(width - p.revealW); + const distH = Math.abs(height - p.revealH); + // When both dimensions are within a small range (e.g., 20px) + if (distW < 20 && distH < 20) { + platformElement.style.opacity = 1; + } else { + platformElement.style.opacity = 0.1; + } + } + } + + // Goal Positioning (Similar to platforms) + $.goal.style.left = `${centerX + levelData.goal.relX - (width - levelData.goalW) * 0.5}px`; + $.goal.style.top = `${centerY + levelData.goal.relY}px`; + $.goal.textContent = levelData.id; + + // Recalculate everything after moving the world + checkCollisions(); + checkPuzzleState(); +} + +/** + * Checks if the player is touching any platform or the goal. + */ +function checkCollisions() { + isGrounded = false; + + const playerRect = $.player.getBoundingClientRect(); + const playerBottom = playerRect.top + playerRect.height; + + // Collect all elements to check (Platforms + Goal) + const elements = document.querySelectorAll('.platform, .goal'); + + for (const element of elements) { + const elementRect = element.getBoundingClientRect(); + + // Basic AABB Collision Check + const overlapX = playerRect.left < elementRect.right && playerRect.right > elementRect.left; + const overlapY = playerRect.top < elementRect.bottom && playerRect.bottom > elementRect.top; + + if (overlapX && overlapY) { + // Collision occurred! + if (element.classList.contains('goal')) { + // Goal Collision + handleGoalReached(); + return; + } + + // Simple Grounding check (for vertical movement later) + // If the player's bottom is within the platform's top edge + if (playerBottom > elementRect.top && playerBottom < elementRect.top + 5) { + isGrounded = true; + // Optional: Snap player to the platform top (removes small gaps) + // playerOffsetY = elementRect.top - centerY - PLAYER_SIZE / 2; + // $.player.style.marginTop = `${playerOffsetY}px`; + } + } + } +} + +/** + * Checks if the current viewport dimensions meet the level's specific requirements. + */ +function checkPuzzleState() { + const levelData = LEVEL_CONFIG[currentLevelIndex]; + const width = window.innerWidth; + const height = window.innerHeight; + + const W_TOLERANCE = 10; // Pixels of tolerance + const H_TOLERANCE = 10; + + // Check if the window is near the solution size + const nearW = Math.abs(width - levelData.goalW) < W_TOLERANCE; + const nearH = Math.abs(height - levelData.goalH) < H_TOLERANCE; + + if (nearW && nearH) { + // Subtle feedback when the player hits the sweet spot + $.gameMessage.textContent = "๐ŸŽฏ Perfect dimensions! Check your alignment now."; + document.body.style.backgroundColor = 'var(--world-bg)'; + } else { + $.gameMessage.textContent = levelData.message; + // Subtle visual cue for distance (e.g., world darkens/brightens) + const totalDistance = Math.abs(width - levelData.goalW) + Math.abs(height - levelData.goalH); + const darkness = Math.min(0.8, totalDistance / 1000); + document.body.style.backgroundColor = `hsl(240, 10%, ${10 + darkness * 10}%)`; + } +} + +/** + * Handles what happens when the goal is reached. + */ +function handleGoalReached() { + alert(`Level ${LEVEL_CONFIG[currentLevelIndex].id} Complete!`); + currentLevelIndex++; + if (currentLevelIndex < LEVEL_CONFIG.length) { + loadLevel(); + } else { + $.gameMessage.textContent = "Game Complete! You are a master Viewport Voyager!"; + alert("You beat the game!"); + } +} + +/** + * Initializes the game for the current level. + */ +function loadLevel() { + const levelData = LEVEL_CONFIG[currentLevelIndex]; + + // Reset player position (center) + playerOffsetX = 0; + playerOffsetY = 0; + $.player.style.marginLeft = '0px'; + $.player.style.marginTop = '0px'; + + // Update UI + $.levelStatus.textContent = `Current Level: ${levelData.id}`; + $.gameMessage.textContent = levelData.message; + + // Immediately update geometry based on current window size + updateLevelGeometry(); +} + + +// --- 4. Event Listeners and Initialization --- + +// CRITICAL: Rerun the geometry calculation and collision check on every resize +window.addEventListener('resize', updateLevelGeometry); + +// Optional: WASD/Arrow Key player movement for minor adjustments +window.addEventListener('keydown', (e) => { + const moveSpeed = 5; // Pixels per key press + + if (e.key === 'w' || e.key === 'ArrowUp') { + playerOffsetY -= moveSpeed; + } else if (e.key === 's' || e.key === 'ArrowDown') { + // Only allow falling if not grounded (simple gravity simulation) + // For simplicity in this version, we'll allow full movement: + playerOffsetY += moveSpeed; + } else if (e.key === 'a' || e.key === 'ArrowLeft') { + playerOffsetX -= moveSpeed; + } else if (e.key === 'd' || e.key === 'ArrowRight') { + playerOffsetX += moveSpeed; + } + + // Apply movement offset + $.player.style.marginLeft = `${playerOffsetX}px`; + $.player.style.marginTop = `${playerOffsetY}px`; + + // Check collisions after movement + checkCollisions(); +}); + + +// Start the game! +loadLevel(); \ No newline at end of file diff --git a/games/voyger_viewpoint/style.css b/games/voyger_viewpoint/style.css new file mode 100644 index 00000000..084af7ec --- /dev/null +++ b/games/voyger_viewpoint/style.css @@ -0,0 +1,90 @@ +:root { + --player-size: 20px; + --world-bg: #282a36; /* Dark background */ + --platform-color: #50fa7b; /* Neon Green */ + --obstacle-color: #ff5555; /* Red */ + --goal-color: #bd93f9; /* Purple */ +} + +/* Global Reset and Background */ +body, html { + margin: 0; + padding: 0; + overflow: hidden; /* Prevent scrolls bars from interfering with dimensions */ + background-color: var(--world-bg); +} + +/* --- UI Overlay (Fixed) --- */ +#ui-overlay { + position: fixed; + top: 10px; + left: 10px; + padding: 10px 20px; + background-color: rgba(0, 0, 0, 0.7); + color: #f8f8f2; + font-family: sans-serif; + border-radius: 5px; + z-index: 100; +} + +#dimensions { + font-weight: bold; + color: #ffb86c; +} + +#game-message { + margin-top: 10px; + color: var(--platform-color); +} + +/* --- Player Element (Fixed in the center) --- */ +#player { + position: fixed; + width: var(--player-size); + height: var(--player-size); + background-color: #f8f8f2; + border-radius: 50%; + /* Key to keeping the player fixed in the center */ + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 50; + transition: margin-left 0.05s, margin-top 0.05s; /* For WASD movement */ +} + +/* --- Game World Elements (Positioned dynamically by JS) --- */ +.level-element { + position: absolute; + box-sizing: border-box; + transition: width 0.1s, height 0.1s, top 0.1s, left 0.1s, opacity 0.3s; /* Smooth visual changes */ + z-index: 10; +} + +.platform { + background-color: var(--platform-color); + height: 20px; + border-radius: 3px; +} + +.obstacle { + background-color: var(--obstacle-color); + border: 1px solid #444; +} + +.goal { + background-color: var(--goal-color); + width: 30px; + height: 30px; + border-radius: 5px; + display: flex; + justify-content: center; + align-items: center; + color: var(--world-bg); + font-weight: bold; + font-size: 10px; +} + +.hidden-platform { + background-color: #44475a; + opacity: 0.1; /* Barely visible hint */ +} \ No newline at end of file diff --git a/games/weight-balance/index.html b/games/weight-balance/index.html new file mode 100644 index 00000000..912c597a --- /dev/null +++ b/games/weight-balance/index.html @@ -0,0 +1,38 @@ + + + + + + Weight Balance | Mini JS Games Hub + + + +
      +

      โš–๏ธ Weight Balance Game

      +
      +
      +
      +
      +
      + + + + + +
      +

      Score: 0

      +

      +
      + + + +
      +
      + + + + + + + + diff --git a/games/weight-balance/script.js b/games/weight-balance/script.js new file mode 100644 index 00000000..394e3751 --- /dev/null +++ b/games/weight-balance/script.js @@ -0,0 +1,84 @@ +let score = 0; +let gameInterval; +let isPaused = false; + +const leftPan = document.getElementById("left-pan"); +const rightPan = document.getElementById("right-pan"); +const bulbs = document.querySelectorAll(".bulb"); +const scoreEl = document.getElementById("score"); +const messageEl = document.getElementById("message"); +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const restartBtn = document.getElementById("restart-btn"); + +const successSound = document.getElementById("success-sound"); +const failSound = document.getElementById("fail-sound"); + +function lightBulbs(count) { + bulbs.forEach((bulb, idx) => { + bulb.classList.toggle("active", idx < count); + }); +} + +function addObstacle() { + const weight = document.createElement("div"); + weight.className = "weight"; + weight.style.width = `${Math.random() * 40 + 20}px`; + weight.style.height = `${Math.random() * 40 + 20}px`; + weight.style.backgroundColor = "red"; + weight.style.position = "absolute"; + weight.style.left = `${Math.random() * 180}px`; + weight.style.top = "-50px"; + document.body.appendChild(weight); + + let pos = -50; + const fallInterval = setInterval(() => { + if (!isPaused) { + pos += 5; + weight.style.top = pos + "px"; + + const rect = weight.getBoundingClientRect(); + const panRect = leftPan.getBoundingClientRect(); + if ( + rect.top + rect.height >= panRect.top && + rect.left + rect.width > panRect.left && + rect.right < panRect.right + ) { + score++; + scoreEl.textContent = "Score: " + score; + successSound.play(); + lightBulbs(score % 6); + weight.remove(); + clearInterval(fallInterval); + } else if (pos > window.innerHeight) { + failSound.play(); + weight.remove(); + clearInterval(fallInterval); + } + } + }, 30); +} + +function startGame() { + if (gameInterval) clearInterval(gameInterval); + gameInterval = setInterval(addObstacle, 1000); + messageEl.textContent = "Game Started!"; +} + +function pauseGame() { + isPaused = !isPaused; + pauseBtn.textContent = isPaused ? "Resume" : "Pause"; + messageEl.textContent = isPaused ? "Game Paused" : "Game Resumed"; +} + +function restartGame() { + isPaused = false; + score = 0; + scoreEl.textContent = "Score: " + score; + lightBulbs(0); + messageEl.textContent = "Game Restarted!"; +} + +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", pauseGame); +restartBtn.addEventListener("click", restartGame); diff --git a/games/weight-balance/style.css b/games/weight-balance/style.css new file mode 100644 index 00000000..a55a5bee --- /dev/null +++ b/games/weight-balance/style.css @@ -0,0 +1,66 @@ +body { + margin: 0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: #0e0e0e; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + text-align: center; +} + +.scale { + display: flex; + justify-content: space-between; + width: 500px; + margin: 20px auto; +} + +.pan { + width: 200px; + height: 30px; + background-color: #444; + border-radius: 5px; + position: relative; +} + +.bulbs { + display: flex; + justify-content: center; + margin: 20px; + gap: 10px; +} + +.bulb { + width: 30px; + height: 30px; + background-color: #222; + border-radius: 50%; + box-shadow: 0 0 10px #000; + transition: 0.3s; +} + +.bulb.active { + background-color: yellow; + box-shadow: 0 0 20px yellow, 0 0 40px yellow; +} + +.controls button { + margin: 10px; + padding: 8px 16px; + font-size: 16px; + cursor: pointer; + border: none; + border-radius: 5px; + background-color: #333; + color: #fff; + transition: 0.2s; +} + +.controls button:hover { + background-color: #555; +} diff --git a/games/whack-a-mole-game/index.html b/games/whack-a-mole-game/index.html new file mode 100644 index 00000000..ac1419fc --- /dev/null +++ b/games/whack-a-mole-game/index.html @@ -0,0 +1,27 @@ + + + + + + Whack-A-Mole + + + +
      +

      Whack-A-Mole!

      + +
      +

      Score: 0

      +

      Time Left: 30s

      +
      + +
      +
      + + +

      Hit the moles before they disappear!

      +
      + + + + \ No newline at end of file diff --git a/games/whack-a-mole-game/script.js b/games/whack-a-mole-game/script.js new file mode 100644 index 00000000..9818c607 --- /dev/null +++ b/games/whack-a-mole-game/script.js @@ -0,0 +1,220 @@ +// --- Game Constants --- +const GAME_DURATION = 30; // seconds +const HOLE_COUNT = 9; +const MIN_MOLE_UP_TIME = 800; // milliseconds +const MAX_MOLE_UP_TIME = 1500; // milliseconds +const MIN_MOLE_DELAY = 500; // milliseconds before mole pops up next +const MAX_MOLE_DELAY = 2000; + +// --- DOM Elements --- +const gameBoardEl = document.getElementById('game-board'); +const scoreEl = document.getElementById('score'); +const timerEl = document.getElementById('timer'); +const startButton = document.getElementById('start-button'); +const messageEl = document.getElementById('message'); +const gameContainer = document.getElementById('game-container'); + +// --- Game State Variables --- +let score = 0; +let timeLeft = GAME_DURATION; +let gameInterval; // Interval for the main timer +let moleTimers = []; // Array to hold individual mole interval/timeouts +let holes = []; // Array of DOM hole elements + +// --- Utility Functions --- + +/** + * Generates a random integer within a range. + * @param {number} min - The minimum value (inclusive). + * @param {number} max - The maximum value (inclusive). + * @returns {number} A random integer. + */ +function randomRange(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +/** + * Finds a random hole that is currently NOT active (mole is down). + * @returns {number} The index of a random, available hole. + */ +function pickRandomHole() { + // Filter for holes that are not 'up' + const availableIndices = holes + .map((hole, index) => hole.classList.contains('up') ? -1 : index) + .filter(index => index !== -1); + + if (availableIndices.length === 0) { + return -1; // No holes available + } + + const randomIndex = randomRange(0, availableIndices.length - 1); + return availableIndices[randomIndex]; +} + +// --- Game Logic --- + +/** + * Makes a mole pop up in a specified hole for a set duration. + * @param {number} index - The index of the hole to use. + */ +function popUpMole(index) { + const hole = holes[index]; + const mole = hole.querySelector('.mole'); + + // 1. Mole pops up + hole.classList.add('up'); + + // 2. Set timer for mole to go down (moleUpTime) + const moleUpTime = randomRange(MIN_MOLE_UP_TIME, MAX_MOLE_UP_TIME); + + const downTimer = setTimeout(() => { + // Mole goes down if it wasn't whacked + hole.classList.remove('up'); + mole.classList.remove('whacked'); // Ensure whacked class is gone + + // 3. Set timer for the next time a mole should pop up in this hole (moleDelay) + const moleDelay = randomRange(MIN_MOLE_DELAY, MAX_MOLE_DELAY); + moleTimers[index] = setTimeout(() => { + if (gameInterval) { // Only continue spawning if the game is still running + popUpMole(index); + } + }, moleDelay); + + }, moleUpTime); + + // Store the timeout ID so we can clear it if the mole is whacked + moleTimers[index] = downTimer; +} + +/** + * Spawns moles across the board by initializing the timers for all holes. + */ +function startMoleSpawning() { + // Start initial mole pop-ups for all holes with a random delay + for (let i = 0; i < HOLE_COUNT; i++) { + const initialDelay = randomRange(500, 3000); + moleTimers[i] = setTimeout(() => { + if (gameInterval) { + popUpMole(i); + } + }, initialDelay); + } +} + + +/** + * Handles the click event on a mole hole. + * @param {number} index - The index of the hole clicked. + */ +function whack(index) { + if (!gameInterval) return; // Ignore clicks if game hasn't started or is over + + const hole = holes[index]; + const mole = hole.querySelector('.mole'); + + if (hole.classList.contains('up')) { + // Successful whack! + score++; + scoreEl.textContent = score; + + // Stop the mole's 'down' timer (it was whacked early) + clearTimeout(moleTimers[index]); + + // Visual feedback + mole.classList.add('whacked'); + hole.classList.remove('up'); + + // Restart the spawning cycle for this mole after a brief pause + const restartDelay = 500; + moleTimers[index] = setTimeout(() => { + mole.classList.remove('whacked'); + if (gameInterval) { + popUpMole(index); + } + }, restartDelay); + + } else { + // Missed, optional penalty or visual feedback can go here + messageEl.textContent = "Missed!"; + setTimeout(() => messageEl.textContent = "Hit the moles before they disappear!", 500); + } +} + +/** + * Manages the main game timer countdown. + */ +function startTimer() { + gameInterval = setInterval(() => { + timeLeft--; + timerEl.textContent = timeLeft; + + if (timeLeft <= 0) { + clearInterval(gameInterval); + gameInterval = null; + gameOver(); + } + }, 1000); +} + +/** + * Initializes the game state and starts the action. + */ +function startGame() { + if (gameInterval) return; // Already running + + // Reset State + score = 0; + timeLeft = GAME_DURATION; + scoreEl.textContent = score; + timerEl.textContent = timeLeft; + gameContainer.classList.remove('game-stopped'); + startButton.textContent = "Whack!"; + messageEl.textContent = "Hit the moles before they disappear!"; + + // Clear any previous timers + moleTimers.forEach(timer => clearTimeout(timer)); + moleTimers = []; + + startTimer(); + startMoleSpawning(); +} + +/** + * Ends the game and displays the results. + */ +function gameOver() { + // Stop all mole spawning/despawning cycles + moleTimers.forEach(timer => clearTimeout(timer)); + moleTimers = []; + + gameContainer.classList.add('game-stopped'); + startButton.textContent = "Play Again"; + messageEl.textContent = `Time's Up! Final Score: ${score}!`; +} + +/** + * Initial setup: creates the hole DOM elements. + */ +function createBoard() { + for (let i = 0; i < HOLE_COUNT; i++) { + const hole = document.createElement('div'); + hole.classList.add('hole'); + hole.dataset.index = i; + + const mole = document.createElement('div'); + mole.classList.add('mole'); + + hole.appendChild(mole); + gameBoardEl.appendChild(hole); + + // Add click listener + hole.addEventListener('click', () => whack(i)); + + holes.push(hole); + } + gameContainer.classList.add('game-stopped'); // Initially stop interaction +} + +// --- Initialization and Event Listeners --- +startButton.addEventListener('click', startGame); +createBoard(); \ No newline at end of file diff --git a/games/whack-a-mole-game/style.css b/games/whack-a-mole-game/style.css new file mode 100644 index 00000000..a097be65 --- /dev/null +++ b/games/whack-a-mole-game/style.css @@ -0,0 +1,114 @@ +:root { + --hole-color: #583f2a; /* Brown dirt */ + --mole-color: #3b3b3b; /* Dark mole */ + --game-time: 30s; +} + +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #b8c9d9; /* Light blue background */ +} + +#game-container { + background-color: #fff; + padding: 30px; + border-radius: 15px; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3); + text-align: center; + width: 90%; + max-width: 450px; +} + +h1 { + color: #e74c3c; + margin-bottom: 10px; +} + +#header-info { + display: flex; + justify-content: space-around; + font-size: 1.4em; + font-weight: bold; + margin-bottom: 20px; +} + +/* --- Game Board and Holes --- */ +#game-board { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(3, 1fr); + gap: 15px; + padding: 15px; + background-color: #90ee90; /* Grass green board */ + border-radius: 10px; +} + +.hole { + width: 100%; + height: 100px; + position: relative; + overflow: hidden; /* Crucial: Hides the mole when it's down */ + border-radius: 50%; + background-color: var(--hole-color); + box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5); + cursor: pointer; +} + +/* --- Mole Styling and Animation --- */ +.mole { + width: 100%; + height: 100%; + position: absolute; + bottom: -100px; /* Hidden position */ + background-color: var(--mole-color); + border-radius: 50%; + border-bottom: 5px solid #222; + transition: bottom 0.2s ease-out; /* Smooth pop-up transition */ +} + +/* Class added when the mole should be visible */ +.hole.up .mole { + bottom: 0; +} + +/* Class added when the mole is successfully hit */ +.mole.whacked { + background-color: #f39c12; /* Flash color */ + transform: scale(0.9); + opacity: 0.5; + transition: transform 0.1s; +} + +/* --- Controls and Messages --- */ +#start-button { + padding: 12px 25px; + background-color: #3498db; + color: white; + font-size: 1.2em; + font-weight: bold; + border: none; + border-radius: 8px; + margin-top: 20px; + cursor: pointer; + transition: background-color 0.2s; +} + +#start-button:hover { + background-color: #2980b9; +} + +#message { + margin-top: 10px; + font-size: 1em; + color: #555; +} + +/* Disable interaction when game is not running */ +.game-stopped .hole { + pointer-events: none; +} \ No newline at end of file diff --git a/games/wind-blower/index.html b/games/wind-blower/index.html new file mode 100644 index 00000000..d7933ed0 --- /dev/null +++ b/games/wind-blower/index.html @@ -0,0 +1,38 @@ + + + + + + Wind Blower ๐ŸŽˆ + + + +
      +

      ๐Ÿ’จ Wind Blower ๐ŸŽˆ

      +
      + Balloon +
      + +
      + + + +
      + +
      +

      ๐ŸŽฏ Size: 1x

      +

      โฑ๏ธ Time Left: 30s

      +
      + + + + + +
      + + + + diff --git a/games/wind-blower/script.js b/games/wind-blower/script.js new file mode 100644 index 00000000..57362e86 --- /dev/null +++ b/games/wind-blower/script.js @@ -0,0 +1,90 @@ +const balloon = document.getElementById("balloon"); +const sizeDisplay = document.getElementById("sizeDisplay"); +const timerDisplay = document.getElementById("timer"); +const inflateSound = document.getElementById("inflateSound"); +const popSound = document.getElementById("popSound"); +const startBtn = document.getElementById("startBtn"); +const pauseBtn = document.getElementById("pauseBtn"); +const restartBtn = document.getElementById("restartBtn"); +const gameOverScreen = document.getElementById("gameOver"); +const gameOverText = document.getElementById("gameOverText"); +const playAgain = document.getElementById("playAgain"); + +let size = 1; +let isRunning = false; +let timer = 30; +let timerInterval; + +// ๐Ÿชถ Glow effect on balloon +function glow() { + balloon.style.filter = `drop-shadow(0 0 ${10 + size * 5}px #00e5ff)`; +} + +// ๐ŸŽˆ Inflate on mouse movement +window.addEventListener("mousemove", () => { + if (isRunning) { + inflateBalloon(); + } +}); + +function inflateBalloon() { + if (size >= 5) { + popBalloon(); + return; + } + size += 0.05; + balloon.style.transform = `scale(${size})`; + sizeDisplay.textContent = `${size.toFixed(1)}x`; + glow(); + inflateSound.currentTime = 0; + inflateSound.play(); +} + +function popBalloon() { + isRunning = false; + balloon.src = "https://pngimg.com/uploads/explosion/explosion_PNG156.png"; + popSound.play(); + endGame("๐Ÿ’ฅ The balloon popped! Game Over!"); +} + +function startGame() { + isRunning = true; + timer = 30; + size = 1; + balloon.src = "https://pngimg.com/uploads/balloon/balloon_PNG4963.png"; + balloon.style.transform = "scale(1)"; + sizeDisplay.textContent = "1x"; + glow(); + gameOverScreen.classList.add("hidden"); + + timerInterval = setInterval(() => { + if (timer > 0) { + timer--; + timerDisplay.textContent = timer; + } else { + endGame("โฐ Timeโ€™s up!"); + } + }, 1000); +} + +function pauseGame() { + isRunning = false; + clearInterval(timerInterval); +} + +function restartGame() { + clearInterval(timerInterval); + startGame(); +} + +function endGame(message) { + isRunning = false; + clearInterval(timerInterval); + gameOverText.textContent = message; + gameOverScreen.classList.remove("hidden"); +} + +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", pauseGame); +restartBtn.addEventListener("click", restartGame); +playAgain.addEventListener("click", startGame); diff --git a/games/wind-blower/style.css b/games/wind-blower/style.css new file mode 100644 index 00000000..d4e17041 --- /dev/null +++ b/games/wind-blower/style.css @@ -0,0 +1,85 @@ +body { + margin: 0; + font-family: 'Poppins', sans-serif; + background: radial-gradient(circle at center, #e0f7fa, #006064); + color: white; + text-align: center; + height: 100vh; + overflow: hidden; +} + +h1 { + margin-top: 20px; + font-size: 2.5em; + text-shadow: 0 0 20px #fff, 0 0 40px #00e5ff; +} + +.game-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; +} + +.balloon-container { + margin-top: 30px; +} + +#balloon { + width: 150px; + transition: all 0.2s ease-in-out; + filter: drop-shadow(0 0 15px #00e5ff); +} + +.controls { + margin-top: 20px; +} + +button { + background: linear-gradient(135deg, #00bcd4, #006064); + border: none; + color: white; + padding: 10px 20px; + margin: 5px; + border-radius: 8px; + font-size: 1em; + cursor: pointer; + transition: 0.3s; + box-shadow: 0 0 10px #00e5ff; +} + +button:hover { + transform: scale(1.05); + box-shadow: 0 0 20px #00e5ff; +} + +.stats { + margin-top: 15px; + font-size: 1.2em; + text-shadow: 0 0 10px #00e5ff; +} + +#gameOver { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.85); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.hidden { + display: none; +} + +#gameOverText { + font-size: 2em; + margin-bottom: 20px; + color: #ffeb3b; + text-shadow: 0 0 20px #ff5722; +} diff --git a/games/wind-petals/index.html b/games/wind-petals/index.html new file mode 100644 index 00000000..c56a970d --- /dev/null +++ b/games/wind-petals/index.html @@ -0,0 +1,24 @@ + + + + + +Wind Petals | Mini JS Games Hub + + + +
      +

      ๐ŸŒธ Wind Petals ๐ŸŒธ

      + +
      + + + + Score: 0 +
      + + +
      + + + diff --git a/games/wind-petals/script.js b/games/wind-petals/script.js new file mode 100644 index 00000000..91e87ff2 --- /dev/null +++ b/games/wind-petals/script.js @@ -0,0 +1,143 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); +canvas.width = 600; +canvas.height = 800; + +let petals = []; +let obstacles = []; +let score = 0; +let animationId; +let gameRunning = false; + +// Audio +const windSound = document.getElementById("windSound"); +const collectSound = document.getElementById("collectSound"); + +// Petal class +class Petal { + constructor() { + this.x = Math.random() * canvas.width; + this.y = -20; + this.radius = 10 + Math.random() * 10; + this.speed = 1 + Math.random() * 2; + this.angle = Math.random() * Math.PI * 2; + } + draw() { + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); + ctx.fillStyle = `rgba(255,182,193,0.8)`; + ctx.shadowBlur = 15; + ctx.shadowColor = "#ff69b4"; + ctx.fill(); + ctx.closePath(); + } + update() { + this.y += this.speed; + this.x += Math.sin(this.angle) * 1.5; + this.angle += 0.05; + } +} + +// Obstacle class +class Obstacle { + constructor(x, y, w, h) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + } + draw() { + ctx.fillStyle = "rgba(255, 105, 180, 0.7)"; + ctx.shadowBlur = 10; + ctx.shadowColor = "#ff1493"; + ctx.fillRect(this.x, this.y, this.w, this.h); + } +} + +// Initialize obstacles +function initObstacles() { + obstacles = [ + new Obstacle(100, 300, 150, 20), + new Obstacle(350, 500, 200, 20), + new Obstacle(200, 650, 180, 20) + ]; +} + +// Spawn petals +function spawnPetals() { + if(petals.length < 15) { + petals.push(new Petal()); + } +} + +// Draw everything +function draw() { + ctx.clearRect(0,0,canvas.width,canvas.height); + petals.forEach((p, i) => { + p.update(); + p.draw(); + // Check collision with obstacles + obstacles.forEach(obs => { + if(p.x > obs.x && p.x < obs.x + obs.w && p.y + p.radius > obs.y && p.y - p.radius < obs.y + obs.h) { + p.y = obs.y - p.radius; // simple collision response + } + }); + // Collect at bottom + if(p.y > canvas.height) { + score += 1; + document.getElementById("score").textContent = score; + collectSound.play(); + petals.splice(i,1); + } + }); + obstacles.forEach(obs => obs.draw()); +} + +// Animation loop +function animate() { + draw(); + if(gameRunning) animationId = requestAnimationFrame(animate); +} + +// Control buttons +document.getElementById("startBtn").addEventListener("click", () => { + if(!gameRunning){ + gameRunning = true; + windSound.play(); + animate(); + } +}); + +document.getElementById("pauseBtn").addEventListener("click", () => { + gameRunning = false; + windSound.pause(); + cancelAnimationFrame(animationId); +}); + +document.getElementById("restartBtn").addEventListener("click", () => { + petals = []; + score = 0; + document.getElementById("score").textContent = score; + gameRunning = false; + windSound.pause(); + cancelAnimationFrame(animationId); + initObstacles(); +}); + +// Wind control (mouse drag) +canvas.addEventListener("mousemove", e => { + petals.forEach(p => { + if(e.buttons > 0){ + let dx = e.offsetX - p.x; + let dy = e.offsetY - p.y; + p.x += dx * 0.01; + p.y += dy * 0.01; + } + }); +}); + +// Spawn petals every 0.5 seconds +setInterval(spawnPetals, 500); + +// Initialize +initObstacles(); diff --git a/games/wind-petals/style.css b/games/wind-petals/style.css new file mode 100644 index 00000000..a4e03f81 --- /dev/null +++ b/games/wind-petals/style.css @@ -0,0 +1,50 @@ +body { + margin: 0; + padding: 0; + background: linear-gradient(to bottom, #a8edea, #fed6e3); + font-family: 'Segoe UI', sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + text-align: center; + position: relative; +} + +canvas { + background: rgba(255,255,255,0.1); + border: 2px solid #fff; + border-radius: 15px; + display: block; + margin: 0 auto; + box-shadow: 0 0 20px #ff69b4, 0 0 40px #ffb6c1; +} + +.controls { + margin-top: 10px; +} + +button { + padding: 10px 20px; + margin: 5px; + border: none; + background: #ff69b4; + color: #fff; + font-size: 16px; + cursor: pointer; + border-radius: 10px; + box-shadow: 0 0 10px #ff69b4, 0 0 20px #ffb6c1; + transition: 0.3s; +} + +button:hover { + box-shadow: 0 0 20px #ff69b4, 0 0 40px #ffb6c1; +} + +.score { + font-size: 18px; + margin-left: 20px; +} diff --git a/games/word-chain-puzzle/index.html b/games/word-chain-puzzle/index.html new file mode 100644 index 00000000..4460db49 --- /dev/null +++ b/games/word-chain-puzzle/index.html @@ -0,0 +1,67 @@ + + + + + + Word Chain Puzzle + + + +
      +
      +

      Word Chain Puzzle

      +
      +
      Score: 0
      +
      Time: 60s
      +
      +
      + +
      +
      + + + + + +
      High Score: 0
      +
      + + +
      + +
      +

      Build a chain of words in the chosen category. Each word must start with the last letter of the previous word. Score points for each valid word added!

      +
      +
      + + + + \ No newline at end of file diff --git a/games/word-chain-puzzle/script.js b/games/word-chain-puzzle/script.js new file mode 100644 index 00000000..ed8266b2 --- /dev/null +++ b/games/word-chain-puzzle/script.js @@ -0,0 +1,229 @@ +// Simple sound effects using Web Audio API +function playSound(frequency, duration, type = 'sine') { + try { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.frequency.value = frequency; + oscillator.type = type; + + gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration); + + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + duration); + } catch (e) { + // Silently fail if Web Audio API not supported + } +} + +const wordLists = { + animals: { + easy: ['cat', 'dog', 'bird', 'fish', 'cow', 'pig', 'duck', 'bee', 'ant', 'fox'], + medium: ['elephant', 'giraffe', 'kangaroo', 'penguin', 'dolphin', 'turtle', 'rabbit', 'squirrel', 'butterfly', 'octopus'], + hard: ['hippopotamus', 'rhinoceros', 'chimpanzee', 'crocodile', 'porcupine', 'armadillo', 'platypus', 'quokka', 'axolotl', 'narwhal'] + }, + food: { + easy: ['apple', 'bread', 'cheese', 'egg', 'milk', 'rice', 'soup', 'tea', 'cake', 'fish'], + medium: ['sandwich', 'pasta', 'salad', 'pizza', 'burger', 'cookie', 'yogurt', 'cereal', 'smoothie', 'taco'], + hard: ['spaghetti', 'lasagna', 'quiche', 'souffle', 'casserole', 'ratatouille', 'bouillabaisse', 'tiramisu', 'pavlova', 'baklava'] + }, + technology: { + easy: ['phone', 'mouse', 'screen', 'code', 'web', 'app', 'data', 'file', 'net', 'chip'], + medium: ['computer', 'keyboard', 'software', 'internet', 'browser', 'database', 'network', 'program', 'device', 'system'], + hard: ['algorithm', 'encryption', 'microprocessor', 'cybersecurity', 'virtualization', 'quantumcomputing', 'blockchain', 'artificialintelligence', 'machinelearning', 'neuralnetwork'] + }, + colors: { + easy: ['red', 'blue', 'green', 'yellow', 'pink', 'black', 'white', 'gray', 'brown', 'orange'], + medium: ['purple', 'violet', 'indigo', 'turquoise', 'magenta', 'crimson', 'azure', 'emerald', 'amber', 'scarlet'], + hard: ['chartreuse', 'cerulean', 'vermillion', 'ultramarine', 'saffron', 'cobalt', 'maroon', 'taupe', 'ecru', 'fuchsia'] + }, + countries: { + easy: ['usa', 'china', 'india', 'brazil', 'russia', 'japan', 'germany', 'france', 'uk', 'italy'], + medium: ['canada', 'australia', 'mexico', 'spain', 'southkorea', 'indonesia', 'netherlands', 'turkey', 'saudiarabia', 'switzerland'], + hard: ['argentina', 'kazakhstan', 'algeria', 'uzbekistan', 'mozambique', 'ecuador', 'azerbaijan', 'belarus', 'panama', 'uruguay'] + }, + sports: { + easy: ['run', 'jump', 'swim', 'ball', 'game', 'play', 'win', 'team', 'goal', 'race'], + medium: ['football', 'basketball', 'tennis', 'soccer', 'baseball', 'hockey', 'golf', 'boxing', 'cycling', 'skiing'], + hard: ['volleyball', 'cricket', 'rugby', 'badminton', 'squash', 'fencing', 'archery', 'wrestling', 'judo', 'taekwondo'] + }, + music: { + easy: ['song', 'note', 'beat', 'sing', 'play', 'band', 'rock', 'pop', 'jazz', 'rap'], + medium: ['guitar', 'piano', 'drums', 'violin', 'flute', 'trumpet', 'saxophone', 'microphone', 'headphones', 'speaker'], + hard: ['symphony', 'orchestra', 'concerto', 'sonata', 'rhapsody', 'ballad', 'etude', 'prelude', 'fugue', 'cantata'] + } +}; + +let currentCategory = ''; +let currentDifficulty = ''; +let chain = []; +let usedWords = new Set(); +let score = 0; +let timeLeft = 60; +let timerInterval; +let highScore = localStorage.getItem('wordChainHighScore') || 0; + +const setupDiv = document.getElementById('setup'); +const gameDiv = document.getElementById('game'); +const categorySelect = document.getElementById('category'); +const difficultySelect = document.getElementById('difficulty'); +const startBtn = document.getElementById('start-btn'); +const chainDiv = document.getElementById('chain'); +const wordInput = document.getElementById('word-input'); +const submitBtn = document.getElementById('submit-btn'); +const hintBtn = document.getElementById('hint-btn'); +const lastLetterSpan = document.getElementById('last-letter'); +const scoreSpan = document.getElementById('score'); +const timerSpan = document.getElementById('timer'); +const messageDiv = document.getElementById('message'); +const highScoreSpan = document.getElementById('high-score'); + +startBtn.addEventListener('click', startGame); +submitBtn.addEventListener('click', submitWord); +hintBtn.addEventListener('click', giveHint); +wordInput.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + submitWord(); + } +}); + +// Initialize high score display +highScoreSpan.textContent = highScore; + +function startGame() { + currentCategory = categorySelect.value; + currentDifficulty = difficultySelect.value; + const words = wordLists[currentCategory][currentDifficulty]; + const startWord = words[Math.floor(Math.random() * words.length)]; + + chain = [startWord]; + usedWords = new Set([startWord.toLowerCase()]); + score = 0; + timeLeft = 60; + + updateDisplay(); + setupDiv.style.display = 'none'; + gameDiv.style.display = 'block'; + + startTimer(); + wordInput.focus(); +} + +function startTimer() { + timerInterval = setInterval(() => { + timeLeft--; + timerSpan.textContent = timeLeft; + if (timeLeft <= 0) { + endGame(); + } + }, 1000); +} + +function submitWord() { + const word = wordInput.value.trim().toLowerCase(); + if (!word) return; + + const lastWord = chain[chain.length - 1]; + const lastLetter = lastWord.slice(-1).toLowerCase(); + + if (word[0] !== lastLetter) { + playSound(200, 0.3, 'sawtooth'); // Error sound + showMessage('Word must start with "' + lastLetter.toUpperCase() + '"', 'error'); + return; + } + + if (usedWords.has(word)) { + playSound(200, 0.3, 'sawtooth'); + showMessage('Word already used!', 'error'); + return; + } + + if (!wordLists[currentCategory][currentDifficulty].includes(word)) { + playSound(200, 0.3, 'sawtooth'); + showMessage('Word not in ' + currentCategory + ' (' + currentDifficulty + ') category!', 'error'); + return; + } + + // Valid word + chain.push(word); + usedWords.add(word); + score += 10; // 10 points per word + updateDisplay(); + wordInput.value = ''; + playSound(800, 0.2); // Success sound + showMessage('Good! Next word starts with "' + word.slice(-1).toUpperCase() + '"', 'success'); +} + +function updateDisplay() { + chainDiv.innerHTML = ''; + chain.forEach(word => { + const span = document.createElement('span'); + span.textContent = word; + chainDiv.appendChild(span); + }); + + const lastWord = chain[chain.length - 1]; + lastLetterSpan.textContent = lastWord.slice(-1).toUpperCase(); + + scoreSpan.textContent = score; +} + +function giveHint() { + const lastWord = chain[chain.length - 1]; + const lastLetter = lastWord.slice(-1).toLowerCase(); + const availableWords = wordLists[currentCategory][currentDifficulty].filter(word => + word[0].toLowerCase() === lastLetter && !usedWords.has(word.toLowerCase()) + ); + + if (availableWords.length === 0) { + playSound(200, 0.3, 'sawtooth'); + showMessage('No more words available starting with "' + lastLetter.toUpperCase() + '"!', 'error'); + return; + } + + const hintWord = availableWords[Math.floor(Math.random() * availableWords.length)]; + playSound(600, 0.2); + showMessage('Try: ' + hintWord.charAt(0).toUpperCase() + hintWord.slice(1), 'info'); + score = Math.max(0, score - 5); // Penalty for hint + scoreSpan.textContent = score; +} + +function endGame() { + clearInterval(timerInterval); + + if (score > highScore) { + highScore = score; + localStorage.setItem('wordChainHighScore', highScore); + highScoreSpan.textContent = highScore; + showMessage('New High Score! Final score: ' + score, 'success'); + } else { + showMessage('Time\'s up! Final score: ' + score, 'info'); + } + + submitBtn.disabled = true; + hintBtn.disabled = true; + wordInput.disabled = true; + + // Allow restart + setTimeout(() => { + setupDiv.style.display = 'block'; + gameDiv.style.display = 'none'; + submitBtn.disabled = false; + hintBtn.disabled = false; + wordInput.disabled = false; + }, 5000); +} + +function showMessage(text, type) { + messageDiv.textContent = text; + messageDiv.className = 'message ' + type; + setTimeout(() => { + messageDiv.textContent = ''; + messageDiv.className = 'message'; + }, 3000); +} \ No newline at end of file diff --git a/games/word-chain-puzzle/style.css b/games/word-chain-puzzle/style.css new file mode 100644 index 00000000..291e6353 --- /dev/null +++ b/games/word-chain-puzzle/style.css @@ -0,0 +1,177 @@ +body { + font-family: Arial, sans-serif; + background-color: #f0f0f0; + margin: 0; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; +} + +.container { + background-color: white; + border-radius: 10px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + padding: 20px; + max-width: 600px; + width: 100%; +} + +header { + text-align: center; + margin-bottom: 20px; +} + +h1 { + color: #333; + margin: 0; +} + +.stats { + display: flex; + justify-content: space-around; + margin-top: 10px; +} + +.score, .timer { + background-color: #4CAF50; + color: white; + padding: 5px 10px; + border-radius: 5px; + font-weight: bold; +} + +main { + margin-bottom: 20px; +} + +.setup { + text-align: center; +} + +#category, #difficulty { + padding: 5px; + margin: 10px; +} + +#start-btn { + background-color: #2196F3; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + font-size: 16px; +} + +#start-btn:hover { + background-color: #0b7dda; +} + +.high-score { + margin-top: 10px; + font-weight: bold; +} + +.game { + text-align: center; +} + +.chain-display { + margin-bottom: 20px; +} + +.chain { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 10px; + min-height: 50px; + padding: 10px; + background-color: #f9f9f9; + border-radius: 5px; +} + +.chain span { + background-color: #e0e0e0; + padding: 5px 10px; + border-radius: 5px; + display: inline-block; + margin: 2px; + animation: fadeIn 0.5s ease-in; +} + +@keyframes fadeIn { + from { opacity: 0; transform: scale(0.8); } + to { opacity: 1; transform: scale(1); } +} + +.input-section { + margin-bottom: 20px; +} + +#word-input { + padding: 10px; + font-size: 16px; + width: 200px; + margin: 10px; +} + +.buttons { + display: flex; + gap: 10px; + justify-content: center; +} + +#submit-btn { + background-color: #4CAF50; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + font-size: 16px; +} + +#submit-btn:hover { + background-color: #45a049; +} + +#hint-btn { + background-color: #FF9800; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + font-size: 16px; +} + +#hint-btn:hover { + background-color: #e68900; +} + +.message { + margin-top: 20px; + font-weight: bold; + min-height: 20px; +} + +.message.error { + color: red; +} + +.message.success { + color: green; +} + +.message.info { + color: blue; +} + +footer { + text-align: center; + font-size: 14px; + color: #666; +} \ No newline at end of file diff --git a/games/word-scramble/index.html b/games/word-scramble/index.html new file mode 100644 index 00000000..b39d3697 --- /dev/null +++ b/games/word-scramble/index.html @@ -0,0 +1,45 @@ + + + + + + Word Scramble | Mini JS Games Hub + + + +
      +

      ๐Ÿง  Word Scramble

      + +
      + + + +
      + + + + +
      + + + + diff --git a/games/word-scramble/script.js b/games/word-scramble/script.js new file mode 100644 index 00000000..0f9648b5 --- /dev/null +++ b/games/word-scramble/script.js @@ -0,0 +1,118 @@ +const words = { + easy: ["cat", "sun", "book", "tree", "milk", "fish"], + medium: ["planet", "jumble", "orange", "rocket", "garden", "silver"], + hard: ["scramble", "awareness", "algorithm", "javascript", "university", "framework"] +}; + +const difficultySelect = document.getElementById("difficulty"); +const startBtn = document.getElementById("start-btn"); +const gameSection = document.querySelector(".game"); +const resultSection = document.getElementById("result"); +const scrambledWordEl = document.getElementById("scrambled-word"); +const userInput = document.getElementById("user-input"); +const submitBtn = document.getElementById("submit-btn"); +const nextBtn = document.getElementById("next-btn"); +const hintBtn = document.getElementById("hint-btn"); +const hintText = document.getElementById("hint-text"); +const timerEl = document.getElementById("time"); +const scoreEl = document.getElementById("score"); +const finalScoreEl = document.getElementById("final-score"); +const resultMessage = document.getElementById("result-message"); +const restartBtn = document.getElementById("restart-btn"); + +let currentWord = ""; +let scrambled = ""; +let score = 0; +let timer; +let timeLeft; +let difficulty = "medium"; +let usedHint = false; +let gameStarted = false; + +startBtn.addEventListener("click", startGame); +submitBtn.addEventListener("click", checkAnswer); +nextBtn.addEventListener("click", nextRound); +hintBtn.addEventListener("click", showHint); +restartBtn.addEventListener("click", restartGame); +userInput.addEventListener("keypress", (e) => { + if (e.key === "Enter") { + checkAnswer(); + } +}); + +function startGame() { + difficulty = difficultySelect.value; + document.querySelector(".settings").classList.add("hidden"); + gameSection.classList.remove("hidden"); + score = 0; + scoreEl.textContent = score; + timeLeft = 60; // Overall game time + timerEl.textContent = timeLeft; + gameStarted = true; + startRound(); + timer = setInterval(() => { + timeLeft--; + timerEl.textContent = timeLeft; + if (timeLeft <= 0) { + clearInterval(timer); + gameOver(); + } + }, 1000); +} + +function startRound() { + const list = words[difficulty]; + currentWord = list[Math.floor(Math.random() * list.length)]; + scrambled = shuffleWord(currentWord); + scrambledWordEl.textContent = scrambled; + userInput.value = ""; + hintText.textContent = ""; + usedHint = false; + nextBtn.classList.add("hidden"); + submitBtn.disabled = false; +} + +function shuffleWord(word) { + return word.split("").sort(() => 0.5 - Math.random()).join(""); +} + +function checkAnswer() { + const answer = userInput.value.trim().toLowerCase(); + if (!answer) return; + if (answer === currentWord) { + score += 10; + if (usedHint) score -= 5; + scoreEl.textContent = score; + scrambledWordEl.textContent = "โœ… Correct!"; + submitBtn.disabled = true; + nextBtn.classList.remove("hidden"); + } else { + hintText.textContent = "โŒ Incorrect! Try again."; + } +} + +function nextRound() { + startRound(); +} + +function showHint() { + if (usedHint) return; + usedHint = true; + const randomIndex = Math.floor(Math.random() * currentWord.length); + const hintChar = currentWord[randomIndex]; + hintText.textContent = `๐Ÿ’ก Hint: Letter '${hintChar.toUpperCase()}' is in the word.`; +} + +function restartGame() { + resultSection.classList.add("hidden"); + document.querySelector(".settings").classList.remove("hidden"); + gameStarted = false; + clearInterval(timer); +} + +function gameOver() { + gameSection.classList.add("hidden"); + resultSection.classList.remove("hidden"); + finalScoreEl.textContent = score; + resultMessage.textContent = score >= 100 ? "๐ŸŽ‰ Amazing!" : score >= 50 ? "๐Ÿ‘ Good job!" : "Keep practicing!"; +} diff --git a/games/word-scramble/style.css b/games/word-scramble/style.css new file mode 100644 index 00000000..e4757924 --- /dev/null +++ b/games/word-scramble/style.css @@ -0,0 +1,90 @@ +* { + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: linear-gradient(135deg, #8e44ad, #3498db); + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + margin: 0; + color: #fff; +} + +.container { + background: rgba(0, 0, 0, 0.3); + padding: 30px; + border-radius: 20px; + text-align: center; + width: 350px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4); +} + +h1 { + margin-bottom: 15px; + font-size: 2rem; + color: #fff; + text-shadow: 1px 1px 3px rgba(0,0,0,0.5); +} + +.settings { + margin-bottom: 20px; +} + +select, button, input { + border: none; + border-radius: 8px; + padding: 8px 12px; + font-size: 16px; + margin: 5px; +} + +button { + background: #2ecc71; + color: #fff; + cursor: pointer; + transition: background 0.3s; +} + +button:hover { + background: #27ae60; +} + +.scrambled-word { + font-size: 2rem; + margin: 20px 0; + letter-spacing: 5px; + font-weight: bold; +} + +input { + width: 80%; + text-align: center; +} + +.timer, .score { + font-weight: 600; +} + +.hint-text { + margin: 10px 0; + font-style: italic; +} + +.hidden { + display: none; +} + +.result h2 { + margin-bottom: 10px; +} + +.result button { + background: #e67e22; +} + +.result button:hover { + background: #d35400; +} diff --git a/games/word-screen/index.html b/games/word-screen/index.html new file mode 100644 index 00000000..78a66d42 --- /dev/null +++ b/games/word-screen/index.html @@ -0,0 +1,24 @@ + + + + + + Word Screen Game + + + +
      +

      Word Screen

      +

      Type the words that appear as fast as you can!

      +
      +
      Time: 30
      +
      Words: 0
      + +
      +
      + +
      +
      + + + \ No newline at end of file diff --git a/games/word-screen/script.js b/games/word-screen/script.js new file mode 100644 index 00000000..d0fb8ab6 --- /dev/null +++ b/games/word-screen/script.js @@ -0,0 +1,78 @@ +// Word Screen Game Script +// Type words as fast as possible + +var wordDisplay = document.getElementById('word-display'); +var inputField = document.getElementById('input-field'); +var timerDisplay = document.getElementById('timer'); +var scoreDisplay = document.getElementById('score'); +var restartBtn = document.getElementById('restart'); +var messageDiv = document.getElementById('message'); + +var words = ['apple', 'banana', 'cherry', 'date', 'elderberry', 'fig', 'grape', 'honeydew', 'kiwi', 'lemon', 'mango', 'nectarine', 'orange', 'peach', 'quince', 'raspberry', 'strawberry', 'tangerine', 'ugli', 'vanilla', 'watermelon', 'xigua', 'yam', 'zucchini', 'algorithm', 'binary', 'cache', 'debug', 'encryption', 'firewall', 'gateway', 'hash', 'interface', 'javascript', 'kernel', 'lambda', 'middleware', 'namespace', 'object', 'protocol', 'query', 'router', 'server', 'token', 'unicode', 'variable', 'websocket', 'xml', 'yaml', 'zip']; +var currentWord = ''; +var score = 0; +var timeLeft = 30; +var timerInterval; +var gameRunning = true; + +// Initialize the game +function initGame() { + score = 0; + timeLeft = 30; + gameRunning = true; + messageDiv.textContent = ''; + scoreDisplay.textContent = 'Words: ' + score; + inputField.value = ''; + inputField.disabled = false; + inputField.focus(); + generateWord(); + startTimer(); +} + +// Generate a random word +function generateWord() { + currentWord = words[Math.floor(Math.random() * words.length)]; + wordDisplay.textContent = currentWord; +} + +// Check input +function checkInput() { + if (!gameRunning) return; + var input = inputField.value.trim(); + if (input === currentWord) { + score++; + scoreDisplay.textContent = 'Words: ' + score; + inputField.value = ''; + generateWord(); + messageDiv.textContent = 'Correct!'; + messageDiv.style.color = 'green'; + setTimeout(function() { + messageDiv.textContent = ''; + }, 500); + } +} + +// Handle input +inputField.addEventListener('input', checkInput); + +// Start the timer +function startTimer() { + clearInterval(timerInterval); + timerInterval = setInterval(function() { + timeLeft--; + timerDisplay.textContent = 'Time: ' + timeLeft; + if (timeLeft <= 0) { + clearInterval(timerInterval); + gameRunning = false; + inputField.disabled = true; + messageDiv.textContent = 'Time\'s up! Final Score: ' + score; + messageDiv.style.color = 'blue'; + } + }, 1000); +} + +// Restart button +restartBtn.addEventListener('click', initGame); + +// Start the game +initGame(); \ No newline at end of file diff --git a/games/word-screen/style.css b/games/word-screen/style.css new file mode 100644 index 00000000..55ab7ca1 --- /dev/null +++ b/games/word-screen/style.css @@ -0,0 +1,75 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #f5f5f5; +} + +.container { + text-align: center; + max-width: 600px; + padding: 20px; +} + +h1 { + color: #3f51b5; +} + +.game-info { + margin-bottom: 20px; + display: flex; + justify-content: center; + gap: 20px; +} + +#timer, #score { + font-size: 24px; + font-weight: bold; +} + +#restart { + padding: 10px 20px; + font-size: 16px; + background-color: #3f51b5; + color: white; + border: none; + cursor: pointer; +} + +#restart:hover { + background-color: #303f9f; +} + +#word-display { + font-size: 48px; + font-weight: bold; + color: #000; + margin: 40px 0; + min-height: 60px; + display: flex; + align-items: center; + justify-content: center; +} + +#input-field { + font-size: 24px; + padding: 10px; + width: 300px; + text-align: center; + border: 2px solid #3f51b5; + border-radius: 5px; +} + +#input-field:focus { + outline: none; + border-color: #303f9f; +} + +#message { + margin-top: 20px; + font-size: 18px; + font-weight: bold; +} \ No newline at end of file diff --git a/games/word-scrumble/index.html b/games/word-scrumble/index.html new file mode 100644 index 00000000..37e6d372 --- /dev/null +++ b/games/word-scrumble/index.html @@ -0,0 +1,39 @@ + + + + + + Word Scramble Game + + + +
      +

      Word Scramble

      + +
      +

      Time Left: 60s

      +

      Score: 0

      +
      + +
      +
      + +
      + +
      + + +
      + +

      + +
      +

      Words Found (0):

      +
        +
      +
      +
      + + + + \ No newline at end of file diff --git a/games/word-scrumble/script.js b/games/word-scrumble/script.js new file mode 100644 index 00000000..1744a884 --- /dev/null +++ b/games/word-scrumble/script.js @@ -0,0 +1,256 @@ +// --- DICTIONARY (Simple, small word list for demonstration) --- +// Note: In a real game, this would be much larger and stored externally. +const DICTIONARY = [ + "word", "draw", "road", "drow", "code", "rock", "core", "cook", + "scramble", "able", "bale", "blame", "lame", "male", "ramble", + "cat", "act", "rat", "art", "tar", "star", "rats", "arts", + "time", "emit", "mite", "item", "met", "tie", "eat", "tea", "ate", + "play", "lap", "lay", "pay", "pal", "yap", "alp", + "read", "dear", "dare", "red", "ear", "era", "are" +]; + +// --- DOM Elements --- +const jumbledLettersEl = document.getElementById('jumbled-letters'); +const wordInput = document.getElementById('word-input'); +const submitBtn = document.getElementById('submit-btn'); +const scrambleBtn = document.getElementById('scramble-btn'); +const timerEl = document.getElementById('timer'); +const scoreEl = document.getElementById('score'); +const feedbackMsg = document.getElementById('feedback-msg'); +const foundWordsList = document.getElementById('found-words-list'); +const foundCountEl = document.getElementById('found-count'); +const gameContainer = document.getElementById('game-container'); + + +// --- Game State Variables --- +let currentPuzzle = ""; +let availableWords = []; // Dictionary words that can be formed from the puzzle +let foundWords = new Set(); +let score = 0; +let timeLeft = 60; +let timerInterval; + +// --- Utility Functions --- + +/** + * Creates a frequency map (letter count) for a given string. + * @param {string} str - The string to analyze. + * @returns {Map} A Map where keys are letters and values are counts. + */ +function getLetterCount(str) { + const counts = new Map(); + for (const char of str.toLowerCase()) { + counts.set(char, (counts.get(char) || 0) + 1); + } + return counts; +} + +/** + * Checks if a candidate word can be formed using only the letters in the puzzle string. + * This is the core anagram validation logic. + * @param {string} candidate - The word submitted by the player. + * @param {string} puzzle - The jumbled letters available. + * @returns {boolean} True if the word is a valid anagram of the puzzle's letters. + */ +function isValidAnagram(candidate, puzzle) { + if (candidate.length === 0) return false; + + const puzzleCounts = getLetterCount(puzzle); + const candidateCounts = getLetterCount(candidate); + + for (const [char, count] of candidateCounts) { + // If the puzzle doesn't have the letter, or doesn't have enough of that letter + if ((puzzleCounts.get(char) || 0) < count) { + return false; + } + } + return true; +} + +/** + * Shuffles a string (used to jumble the letters). + * @param {string} str - The string to shuffle. + * @returns {string} The shuffled string. + */ +function shuffleString(str) { + const arr = str.split(''); + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + return arr.join(''); +} + + +// --- Game Logic --- + +/** + * Selects a base word and generates a new puzzle. + */ +function generatePuzzle() { + // 1. Select a random long word from the dictionary as the base + const baseWords = DICTIONARY.filter(w => w.length >= 6 && w.length <= 8); + if (baseWords.length === 0) { + alert("Dictionary too small to generate a puzzle!"); + return; + } + const baseWord = baseWords[Math.floor(Math.random() * baseWords.length)]; + + // 2. Set the current puzzle and jumble the letters for display + currentPuzzle = baseWord.toUpperCase(); + jumbledLettersEl.textContent = shuffleString(currentPuzzle); + + // 3. Find all possible words from this puzzle's letters + availableWords = DICTIONARY.filter(word => + word.length >= 3 && isValidAnagram(word, currentPuzzle) + ); + + // 4. Reset game state + score = 0; + scoreEl.textContent = score; + foundWords.clear(); + foundWordsList.innerHTML = ''; + foundCountEl.textContent = 0; + feedbackMsg.textContent = "Find as many words as you can!"; +} + +/** + * Updates the score based on the length of the submitted word. + * Longer words yield more points. + * @param {string} word - The valid submitted word. + */ +function updateScore(word) { + let wordScore = 0; + const len = word.length; + + if (len >= 6) wordScore = 15; + else if (len === 5) wordScore = 8; + else if (len === 4) wordScore = 4; + else if (len === 3) wordScore = 2; + + score += wordScore; + scoreEl.textContent = score; +} + +/** + * Handles the submission of a word by the player. + */ +function handleSubmit() { + if (timeLeft <= 0) return; + + const word = wordInput.value.trim().toLowerCase(); + wordInput.value = ''; // Clear input field immediately + + if (word.length < 3) { + showFeedback("Word must be at least 3 letters long.", 'red'); + return; + } + + if (foundWords.has(word)) { + showFeedback("You already found that word!", 'orange'); + return; + } + + // 1. Check if the word can be formed from the puzzle letters (Anagram check) + if (!isValidAnagram(word, currentPuzzle)) { + showFeedback("This word cannot be formed from the letters.", 'red'); + return; + } + + // 2. Check if the word is in the dictionary (Available words check) + if (availableWords.includes(word)) { + // SUCCESS! + foundWords.add(word); + updateScore(word); + + // Add word to the list + const listItem = document.createElement('li'); + listItem.textContent = word; + foundWordsList.appendChild(listItem); + foundCountEl.textContent = foundWords.size; + + showFeedback(`+${scoreEl.textContent - score} points!`, 'green'); + + } else { + showFeedback("Not a valid word.", 'red'); + } +} + +/** + * Displays feedback to the player. + * @param {string} message - The feedback text. + * @param {string} color - The text color. + */ +function showFeedback(message, color) { + feedbackMsg.textContent = message; + feedbackMsg.style.color = color; + + // Clear feedback after 2 seconds + clearTimeout(feedbackMsg.timeoutId); + feedbackMsg.timeoutId = setTimeout(() => { + feedbackMsg.textContent = ""; + }, 2000); +} + +/** + * Manages the game timer countdown. + */ +function startTimer() { + clearInterval(timerInterval); + timeLeft = 60; + timerEl.textContent = timeLeft; + gameContainer.classList.remove('disabled'); + wordInput.focus(); + + timerInterval = setInterval(() => { + timeLeft--; + timerEl.textContent = timeLeft; + + if (timeLeft <= 10) { + timerEl.style.color = 'red'; + } else { + timerEl.style.color = 'black'; + } + + if (timeLeft <= 0) { + clearInterval(timerInterval); + gameOver(); + } + }, 1000); +} + +/** + * Ends the game and displays the final score. + */ +function gameOver() { + gameContainer.classList.add('disabled'); + feedbackMsg.style.color = '#3498db'; + feedbackMsg.textContent = `Time's Up! Final Score: ${score}.`; + timerEl.textContent = '0'; + wordInput.blur(); // Remove focus + + // Optional: Show the words the player missed + const missedWords = availableWords.filter(word => !foundWords.has(word)); + // console.log(`Missed words: ${missedWords.join(', ')}`); +} + + +// --- Event Listeners --- + +// Handle Enter key press on the input field +wordInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + handleSubmit(); + } +}); + +submitBtn.addEventListener('click', handleSubmit); +scrambleBtn.addEventListener('click', () => { + generatePuzzle(); + startTimer(); +}); + +// --- Initialization --- +generatePuzzle(); // Generate initial puzzle +// Don't start the timer immediately; wait for the player to click 'Scramble' or 'Submit' +feedbackMsg.textContent = "Click 'Scramble' to start the timer!"; \ No newline at end of file diff --git a/games/word-scrumble/style.css b/games/word-scrumble/style.css new file mode 100644 index 00000000..67184a16 --- /dev/null +++ b/games/word-scrumble/style.css @@ -0,0 +1,128 @@ +body { + font-family: 'Arial', sans-serif; + display: flex; + justify-content: center; + align-items: flex-start; + min-height: 100vh; + padding-top: 50px; + margin: 0; + background-color: #f0f4f8; +} + +#game-container { + background-color: #fff; + padding: 30px; + border-radius: 12px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + width: 90%; + max-width: 500px; + text-align: center; +} + +h1 { + color: #3498db; + margin-bottom: 20px; +} + +#header-info { + display: flex; + justify-content: space-around; + font-size: 1.2em; + margin-bottom: 20px; + padding: 10px; + background-color: #ecf0f1; + border-radius: 6px; +} + +/* --- Puzzle Area --- */ +#puzzle-area { + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + margin-bottom: 20px; +} + +#jumbled-letters { + font-size: 3em; + font-weight: bold; + letter-spacing: 5px; + padding: 10px 20px; + background-color: #e74c3c; /* Red background for letters */ + color: white; + border-radius: 8px; + min-width: 200px; +} + +#scramble-btn { + padding: 10px 15px; + background-color: #f39c12; /* Orange */ + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; +} + +#scramble-btn:hover { + background-color: #e67e22; +} + +/* --- Input Area --- */ +#input-area { + display: flex; + gap: 10px; + margin-bottom: 15px; +} + +#word-input { + flex-grow: 1; + padding: 10px; + font-size: 1.1em; + border: 2px solid #bdc3c7; + border-radius: 5px; +} + +#submit-btn { + padding: 10px 20px; + background-color: #2ecc71; /* Green */ + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; +} + +#submit-btn:hover { + background-color: #27ae60; +} + +/* --- Feedback and Results --- */ +#feedback-msg { + min-height: 20px; + font-weight: bold; + margin-bottom: 20px; +} + +#found-words-list { + list-style: none; + padding: 0; + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px; +} + +#found-words-list li { + background-color: #3498db; + color: white; + padding: 5px 10px; + border-radius: 4px; + font-size: 0.9em; +} + +/* Game Over / Disabled State */ +.disabled { + opacity: 0.6; + pointer-events: none; +} \ No newline at end of file diff --git a/games/word_length_game/index.html b/games/word_length_game/index.html new file mode 100644 index 00000000..8bed43cf --- /dev/null +++ b/games/word_length_game/index.html @@ -0,0 +1,39 @@ + + + + + + Word Length Puzzle + + + + +
      +

      โ“ Word Length Puzzle

      + +
      + Score: 0 / 0 +
      + +
      +

      Definition:

      +

      Press START to get a definition!

      +
      + +
      + + +
      + +
      +
      + +
      + + +
      +
      + + + + \ No newline at end of file diff --git a/games/word_length_game/script.js b/games/word_length_game/script.js new file mode 100644 index 00000000..043aca8e --- /dev/null +++ b/games/word_length_game/script.js @@ -0,0 +1,155 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. GAME DATA --- + const wordDefinitions = [ + { word: "Nebula", definition: "A cloud of gas and dust in outer space, visible in the night sky." }, + { word: "Eloquent", definition: "Fluent or persuasive in speaking or writing." }, + { word: "Ephemeral", definition: "Lasting for a very short time." }, + { word: "Synergy", definition: "The interaction or cooperation of two or more agents to produce a combined effect greater than the sum of their separate effects." }, + { word: "Ambiguous", definition: "Open to more than one interpretation; having a double meaning." }, + { word: "Curious", definition: "Eager to know or learn something." } + ]; + + // --- 2. GAME STATE VARIABLES --- + let currentRounds = []; // Shuffled array for the current game + let currentRoundIndex = 0; + let score = 0; + let gameActive = false; + + // --- 3. DOM Elements --- + const definitionDisplay = document.getElementById('definition-display'); + const lengthInput = document.getElementById('length-input'); + const submitButton = document.getElementById('submit-button'); + const feedbackMessage = document.getElementById('feedback-message'); + const scoreSpan = document.getElementById('score'); + const totalRoundsSpan = document.getElementById('total-rounds'); + const startButton = document.getElementById('start-button'); + const nextButton = document.getElementById('next-button'); + + // --- 4. UTILITY FUNCTIONS --- + + /** + * Shuffles an array in place (Fisher-Yates). + */ + function shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + } + + // --- 5. CORE GAME FUNCTIONS --- + + /** + * Initializes the game. + */ + function startGame() { + gameActive = true; + shuffleArray(wordDefinitions); + currentRounds = wordDefinitions; // Use all definitions + totalRoundsSpan.textContent = currentRounds.length; + + currentRoundIndex = 0; + score = 0; + scoreSpan.textContent = score; + + startButton.style.display = 'none'; + nextButton.style.display = 'none'; + loadRound(); + } + + /** + * Loads the next word definition onto the screen. + */ + function loadRound() { + if (currentRoundIndex >= currentRounds.length) { + endGame(); + return; + } + + const roundData = currentRounds[currentRoundIndex]; + + // Update display + definitionDisplay.textContent = roundData.definition; + feedbackMessage.textContent = 'Guess the number of letters in the word!'; + feedbackMessage.style.color = '#34495e'; + + // Enable input + lengthInput.value = ''; + lengthInput.disabled = false; + submitButton.disabled = false; + lengthInput.focus(); + nextButton.style.display = 'none'; + } + + /** + * Checks the player's number guess against the actual word length. + */ + function checkGuess() { + const roundData = currentRounds[currentRoundIndex]; + const correctWord = roundData.word; + const correctLength = correctWord.length; + const playerGuess = parseInt(lengthInput.value); + + // Disable input after submission + lengthInput.disabled = true; + submitButton.disabled = true; + + if (isNaN(playerGuess)) { + feedbackMessage.textContent = `Please enter a valid number. The word was **${correctWord}** (${correctLength} letters).`; + feedbackMessage.style.color = '#e74c3c'; + } else if (playerGuess === correctLength) { + score++; + scoreSpan.textContent = score; + feedbackMessage.textContent = `๐ŸŽ‰ CORRECT! The word **${correctWord}** has ${correctLength} letters.`; + feedbackMessage.style.color = '#2ecc71'; + } else { + feedbackMessage.textContent = `โŒ INCORRECT. The word **${correctWord}** has ${correctLength} letters (your guess: ${playerGuess}).`; + feedbackMessage.style.color = '#e74c3c'; + } + + // Prepare for next round + nextButton.style.display = 'block'; + } + + /** + * Moves the game to the next round. + */ + function nextRound() { + currentRoundIndex++; + loadRound(); + } + + /** + * Ends the game and shows the final score. + */ + function endGame() { + gameActive = false; + definitionDisplay.textContent = 'GAME OVER!'; + feedbackMessage.textContent = `Final Score: ${score} / ${currentRounds.length}.`; + feedbackMessage.style.color = '#1abc9c'; + nextButton.style.display = 'none'; + + startButton.textContent = 'PLAY AGAIN'; + startButton.style.display = 'block'; + } + + // --- 6. EVENT LISTENERS --- + + startButton.addEventListener('click', startGame); + nextButton.addEventListener('click', nextRound); + submitButton.addEventListener('click', checkGuess); + + // Allow 'Enter' key to submit the guess + lengthInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter' && !submitButton.disabled) { + checkGuess(); + } + }); + + // Initial setup: check if the user clicks 'Enter' on start button + startButton.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + startGame(); + } + }); +}); \ No newline at end of file diff --git a/games/word_length_game/style.css b/games/word_length_game/style.css new file mode 100644 index 00000000..70f15bcc --- /dev/null +++ b/games/word_length_game/style.css @@ -0,0 +1,125 @@ +body { + font-family: 'Georgia', serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #e8f8f5; /* Pale teal background */ + color: #34495e; +} + +#game-container { + background-color: white; + padding: 30px; + border-radius: 12px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 550px; + width: 90%; +} + +h1 { + color: #1abc9c; /* Teal/Turquoise color */ + margin-bottom: 20px; +} + +#score-area { + font-size: 1.1em; + font-weight: bold; + margin-bottom: 25px; +} + +/* --- Puzzle Box --- */ +#puzzle-box { + background-color: #f0fdfa; + padding: 20px; + border: 2px solid #1abc9c; + border-radius: 8px; + margin-bottom: 30px; +} + +#puzzle-box h2 { + margin-top: 0; + color: #2c3e50; + font-size: 1.2em; +} + +#definition-display { + font-size: 1.5em; + font-style: italic; + font-weight: 500; +} + +/* --- Input Area --- */ +#input-area { + display: flex; + justify-content: center; + gap: 10px; + margin-bottom: 20px; +} + +#length-input { + padding: 10px; + font-size: 1.1em; + border: 2px solid #bdc3c7; + border-radius: 5px; + width: 180px; + text-align: center; +} + +#submit-button { + padding: 10px 20px; + font-size: 1.1em; + background-color: #3498db; /* Blue */ + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; +} + +#submit-button:hover:not(:disabled) { + background-color: #2980b9; +} + +#submit-button:disabled { + background-color: #95a5a6; + cursor: not-allowed; +} + +/* --- Feedback and Controls --- */ +#feedback-message { + min-height: 1.5em; + font-weight: bold; + font-size: 1.1em; + margin-bottom: 20px; +} + +#controls button { + padding: 10px 20px; + font-size: 1em; + font-weight: bold; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; +} + +#start-button { + background-color: #2ecc71; /* Green */ + color: white; +} + +#start-button:hover { + background-color: #27ae60; +} + +#next-button { + background-color: #9b59b6; /* Purple */ + color: white; +} + +#next-button:hover { + background-color: #8e44ad; +} \ No newline at end of file diff --git a/games/word_racing/index.html b/games/word_racing/index.html new file mode 100644 index 00000000..56e92dc3 --- /dev/null +++ b/games/word_racing/index.html @@ -0,0 +1,39 @@ + + + + + + Word Racer Typing Game + + + + +
      +

      ๐ŸŽ๏ธ Word Racer

      + +
      +
      ๐Ÿ‘ค Player
      + +
      ๐Ÿค– Opponent
      +
      + +
      +

      Press START to load the race text!

      +
      + +
      + +
      + +
      +

      Click **START** to begin the race!

      +
      + +
      + +
      +
      + + + + \ No newline at end of file diff --git a/games/word_racing/script.js b/games/word_racing/script.js new file mode 100644 index 00000000..87beb1ac --- /dev/null +++ b/games/word_racing/script.js @@ -0,0 +1,167 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- 1. GAME DATA --- + const raceTexts = [ + "The quick brown fox jumps over the lazy dog and runs away from the yellow cat.", + "Programming is the art of telling another human being what one wants the computer to do.", + "Every great design starts with an even better story. Consistency is key to user experience.", + "Jaded wizards pluck ivy from the big quilt. Pack my box with five dozen liquor jugs." + ]; + + // --- 2. DOM Elements --- + const targetTextElement = document.getElementById('target-text'); + const typingInput = document.getElementById('typing-input'); + const playerRacer = document.getElementById('player-racer'); + const opponentRacer = document.getElementById('opponent-racer'); + const messageElement = document.getElementById('message'); + const startButton = document.getElementById('start-button'); + + // --- 3. GAME STATE VARIABLES --- + let targetText = ''; + let targetLength = 0; + let currentIndex = 0; + let gameActive = false; + let raceInterval = null; + + // --- 4. COMPUTER OPPONENT VARIABLES --- + const OPPONENT_WPM = 40; // Target words per minute for opponent + let opponentInterval = null; + let opponentProgress = 0; + let opponentSpeed = 0; // Calculated in initGame + + // --- 5. CORE FUNCTIONS --- + + /** + * Initializes the game state and UI. + */ + function initGame() { + // 1. Pick and set new race text + const randomIndex = Math.floor(Math.random() * raceTexts.length); + targetText = raceTexts[randomIndex]; + targetLength = targetText.length; + + // 2. Calculate Opponent Speed (in characters per interval) + // Avg Word Length: 5 chars + 1 space = 6 chars/word + const charsPerMinute = OPPONENT_WPM * 6; + // The opponent will take 60 seconds (600 intervals of 100ms) to type 60 WPM + const timeToFinishMS = targetLength / (charsPerMinute / 60000); // Time in ms to type the whole text + opponentSpeed = (100 / targetLength) / (timeToFinishMS / 100); // Percentage increase per 100ms interval + + // Reset state + currentIndex = 0; + opponentProgress = 0; + typingInput.value = ''; + typingInput.disabled = false; + playerRacer.style.left = '0%'; + opponentRacer.style.left = '0%'; + gameActive = true; + + // Render initial text (all characters are default color) + targetTextElement.innerHTML = targetText; + messageElement.textContent = 'Race started! Type carefully...'; + startButton.textContent = 'RESTART RACE'; + + typingInput.focus(); + + // Start opponent movement + opponentInterval = setInterval(moveOpponent, 100); + } + + /** + * Handles key input and checks it against the target text. + */ + function handleTypingInput() { + if (!gameActive) return; + + const typedValue = typingInput.value; + const currentLength = typedValue.length; + + // Prevent moving backwards past the current check point + if (currentLength < currentIndex) { + // User deleted text, allow it, but don't re-render movement. + return; + } + + // Check the newly typed character + const charToCheck = typedValue[currentLength - 1]; + const targetChar = targetText[currentLength - 1]; + + if (charToCheck === targetChar) { + currentIndex = currentLength; + updateTextDisplay(); + updateRacerPosition(); + } else { + // Incorrect character, do not advance currentIndex, highlight error + typingInput.classList.add('error'); + setTimeout(() => typingInput.classList.remove('error'), 100); + } + + // Check for win condition + if (currentIndex === targetLength) { + endGame('player'); + } + } + + /** + * Updates the text display with green (correct) and red (incorrect) highlighting. + */ + function updateTextDisplay() { + const correctPart = `${targetText.substring(0, currentIndex)}`; + const rest = targetText.substring(currentIndex); + targetTextElement.innerHTML = correctPart + rest; + } + + /** + * Moves the player's racer based on typing progress. + */ + function updateRacerPosition() { + const progressPercent = (currentIndex / targetLength) * 100; + // Subtract a small buffer (e.g., 5%) so the racer doesn't run off the right edge. + playerRacer.style.left = Math.min(progressPercent, 95) + '%'; + } + + /** + * Controls the opponent's smooth, automatic movement. + */ + function moveOpponent() { + if (!gameActive) { + clearInterval(opponentInterval); + return; + } + + opponentProgress += opponentSpeed; + + // Subtract a small buffer (e.g., 5%) + opponentRacer.style.left = Math.min(opponentProgress, 95) + '%'; + + if (opponentProgress >= 100) { + endGame('opponent'); + } + } + + /** + * Stops the game and declares the winner. + */ + function endGame(winner) { + if (!gameActive) return; + + gameActive = false; + clearInterval(opponentInterval); + typingInput.disabled = true; + + if (winner === 'player') { + messageElement.innerHTML = '๐Ÿ† **YOU WIN!** You beat the computer!'; + messageElement.style.color = '#2ecc71'; + } else if (winner === 'opponent') { + messageElement.innerHTML = '๐Ÿ˜ญ **YOU LOSE.** The computer beat you!'; + messageElement.style.color = '#e74c3c'; + } + } + + // --- 6. EVENT LISTENERS --- + + startButton.addEventListener('click', initGame); + typingInput.addEventListener('input', handleTypingInput); + + // Load initial text when script loads + targetTextElement.innerHTML = 'Ready to race? Click Start!'; +}); \ No newline at end of file diff --git a/games/word_racing/style.css b/games/word_racing/style.css new file mode 100644 index 00000000..2cfb5a7e --- /dev/null +++ b/games/word_racing/style.css @@ -0,0 +1,126 @@ +body { + font-family: 'Consolas', monospace; /* Monospace font is good for typing games */ + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #34495e; /* Dark background */ + color: #ecf0f1; +} + +#game-container { + background-color: #2c3e50; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4); + text-align: center; + max-width: 800px; + width: 90%; +} + +h1 { + color: #f1c40f; /* Yellow */ + margin-bottom: 20px; +} + +/* --- Race Track Area --- */ +#race-track { + width: 100%; + height: 120px; + background-color: #7f8c8d; /* Grey track */ + border: 3px solid #f1c40f; + border-radius: 8px; + margin-bottom: 20px; + position: relative; /* Necessary for absolute positioning of racers */ + overflow: hidden; +} + +.racer { + position: absolute; + padding: 5px 10px; + border-radius: 4px; + font-weight: bold; + color: white; + transition: left 0.5s ease; /* Smooth movement transition */ + min-width: 100px; /* Consistent size */ + text-align: left; + left: 0%; /* Initial position */ +} + +#player-racer { + top: 5px; + background-color: #2980b9; /* Blue for player */ + z-index: 10; +} + +#opponent-racer { + bottom: 5px; + background-color: #e74c3c; /* Red for opponent */ +} + +/* --- Text Display and Input --- */ +#text-display-box { + background-color: #ecf0f1; + color: #333; + padding: 15px; + border-radius: 8px; + margin-bottom: 15px; + text-align: left; + min-height: 80px; + box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2); +} + +#target-text { + margin: 0; + line-height: 1.5; + font-size: 1.1em; + word-wrap: break-word; +} + +/* Styles for the text being typed */ +.correct { + color: #27ae60; /* Green */ +} + +.incorrect { + background-color: #e74c3c; /* Red highlight */ + color: white; +} + +#typing-input { + width: 98%; + padding: 10px; + font-size: 1.2em; + border: 2px solid #3498db; + border-radius: 5px; + box-sizing: border-box; + margin-bottom: 15px; +} + +/* --- Status and Controls --- */ +#status-area { + min-height: 20px; + margin-bottom: 20px; +} + +#message { + font-weight: 500; + color: #bdc3c7; +} + +#start-button { + padding: 12px 25px; + font-size: 1.2em; + font-weight: bold; + background-color: #3498db; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +#start-button:hover { + background-color: #2980b9; +} \ No newline at end of file diff --git a/games/word_scrumble/index.html b/games/word_scrumble/index.html new file mode 100644 index 00000000..c5ea819a --- /dev/null +++ b/games/word_scrumble/index.html @@ -0,0 +1,39 @@ + + + + + + Word Scramble Challenge ๐Ÿง  + + + +
      +

      Word Scramble

      + +
      + Score: 0 + Time: 30 +
      + +
      +

      Unscramble this word:

      +

      +
      + +
      + + +
      + +

      + + +
      + + + + \ No newline at end of file diff --git a/games/word_scrumble/script.js b/games/word_scrumble/script.js new file mode 100644 index 00000000..b9b63764 --- /dev/null +++ b/games/word_scrumble/script.js @@ -0,0 +1,156 @@ +// --- 1. Game Data and Configuration --- +const wordList = [ + "apple", "banana", "orange", "grape", "kiwi", + "coding", "javascript", "developer", "browser", "algorithm", + "mountain", "ocean", "forest", "desert", "river" +]; + +const START_TIME_SECONDS = 30; // Game duration in seconds + +// --- 2. Game State Variables --- +let currentWord = ""; // The correct, unscrambled word +let score = 0; +let timeLeft = START_TIME_SECONDS; +let timerInterval; +let gameActive = false; + +// --- 3. DOM Element References --- +const scoreDisplay = document.getElementById('score-display'); +const timerDisplay = document.getElementById('timer-display'); +const scrambledWordDisplay = document.getElementById('scrambled-word'); +const guessInput = document.getElementById('guess-input'); +const submitButton = document.getElementById('submit-button'); +const feedbackElement = document.getElementById('feedback'); +const gameOverScreen = document.getElementById('game-over-screen'); +const finalScoreDisplay = document.getElementById('final-score'); +const restartButton = document.getElementById('restart-button'); +const gameContainer = document.querySelector('.game-container'); // Need to show/hide quiz elements + +// --- 4. Core Utility Functions --- + +// Function to shuffle the letters of a word +function shuffleWord(word) { + // Convert string to array, shuffle, then join back to string + let letters = word.split(''); + for (let i = letters.length - 1; i > 0; i--) { + // Fisher-Yates shuffle algorithm + const j = Math.floor(Math.random() * (i + 1)); + [letters[i], letters[j]] = [letters[j], letters[i]]; + } + const scrambled = letters.join(''); + + // Ensure the scrambled word is not the same as the original word + if (scrambled === word && word.length > 1) { + // If it accidentally didn't shuffle, shuffle it again + return shuffleWord(word); + } + + return scrambled.toUpperCase(); +} + +// Loads a new random word and displays it +function loadNewWord() { + // 1. Pick a random word + const randomIndex = Math.floor(Math.random() * wordList.length); + currentWord = wordList[randomIndex]; + + // 2. Scramble and display + const scrambled = shuffleWord(currentWord); + scrambledWordDisplay.textContent = scrambled; + + // 3. Reset input/feedback + guessInput.value = ''; + feedbackElement.textContent = ''; + feedbackElement.classList.remove('correct', 'incorrect'); + guessInput.focus(); +} + +// --- 5. Game Loop and Timer --- + +function updateTimer() { + timeLeft--; + timerDisplay.textContent = `Time: ${timeLeft}`; + + if (timeLeft <= 0) { + clearInterval(timerInterval); + endGame(); + } +} + +function startGame() { + // Reset state + score = 0; + timeLeft = START_TIME_SECONDS; + gameActive = true; + + // Reset UI + scoreDisplay.textContent = `Score: 0`; + timerDisplay.textContent = `Time: ${START_TIME_SECONDS}`; + gameOverScreen.classList.add('hidden'); + gameContainer.querySelector('.info-bar').classList.remove('hidden'); + gameContainer.querySelector('.scramble-area').classList.remove('hidden'); + gameContainer.querySelector('.input-area').classList.remove('hidden'); + feedbackElement.textContent = ''; + + // Start timer + timerInterval = setInterval(updateTimer, 1000); + + // Load first word + loadNewWord(); +} + +function endGame() { + gameActive = false; + + // Hide game elements + gameContainer.querySelector('.info-bar').classList.add('hidden'); + gameContainer.querySelector('.scramble-area').classList.add('hidden'); + gameContainer.querySelector('.input-area').classList.add('hidden'); + feedbackElement.textContent = ''; + + // Show game over screen + finalScoreDisplay.textContent = `Your final score is: ${score} points!`; + gameOverScreen.classList.remove('hidden'); +} + +// --- 6. Event Handlers --- + +function handleSubmit() { + if (!gameActive) return; + + const playerGuess = guessInput.value.toLowerCase().trim(); + + if (playerGuess === currentWord) { + // Correct Guess + score += 10; + scoreDisplay.textContent = `Score: ${score}`; + feedbackElement.textContent = 'โœ… Correct! +10 points!'; + feedbackElement.className = 'message correct'; + + // Load the next word quickly + setTimeout(loadNewWord, 500); + } else { + // Incorrect Guess + feedbackElement.textContent = 'โŒ Incorrect. Try again!'; + feedbackElement.className = 'message incorrect'; + + // Clear input for another attempt + guessInput.value = ''; + guessInput.focus(); + } +} + +// --- 7. Event Listeners --- +submitButton.addEventListener('click', handleSubmit); +restartButton.addEventListener('click', startGame); + +// Allow pressing 'Enter' key in the input field to submit the guess +guessInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + handleSubmit(); + } +}); + +// --- 8. Initialization --- +// Start the game immediately upon loading the script +startGame(); \ No newline at end of file diff --git a/games/word_scrumble/stle.css b/games/word_scrumble/stle.css new file mode 100644 index 00000000..872579ca --- /dev/null +++ b/games/word_scrumble/stle.css @@ -0,0 +1,129 @@ +body { + font-family: 'Verdana', sans-serif; + background-color: #e9f7ef; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; +} + +.game-container { + background: #ffffff; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + text-align: center; + width: 90%; + max-width: 500px; +} + +h1 { + color: #38761d; /* Dark Green */ + margin-bottom: 20px; +} + +/* Info Bar (Score and Timer) */ +.info-bar { + display: flex; + justify-content: space-between; + margin-bottom: 30px; + padding: 10px; + background-color: #f0f0f0; + border-radius: 8px; + font-weight: bold; + font-size: 1.1em; +} + +#timer-display { + color: #ff0000; /* Red for urgency */ +} + +/* Scrambled Word Display */ +.scramble-area p { + color: #666; + margin-bottom: 5px; +} + +#scrambled-word { + font-size: 3em; + letter-spacing: 5px; + color: #007bff; /* Blue accent */ + margin-top: 5px; + margin-bottom: 30px; +} + +/* Input Area */ +.input-area { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +#guess-input { + flex-grow: 1; + padding: 12px; + font-size: 1.1em; + border: 2px solid #ccc; + border-radius: 6px; +} + +#submit-button { + padding: 12px 20px; + font-size: 1.1em; + border: none; + border-radius: 6px; + background-color: #38761d; + color: white; + cursor: pointer; + transition: background-color 0.2s; +} + +#submit-button:hover { + background-color: #275214; +} + +/* Feedback Message */ +.message { + padding: 10px; + margin-top: 10px; + border-radius: 5px; + font-weight: bold; +} + +.correct { + color: #28a745; /* Green */ +} + +.incorrect { + color: #dc3545; /* Red */ +} + +/* Game Over Screen */ +#game-over-screen { + background-color: #fff3cd; + border: 1px solid #ffeeba; + padding: 20px; + border-radius: 8px; + margin-top: 20px; +} + +#final-score { + font-size: 1.5em; + font-weight: bold; + color: #856404; +} + +#restart-button { + margin-top: 15px; + background-color: #ffc107; + color: #333; +} + +#restart-button:hover { + background-color: #e0a800; +} + +.hidden { + display: none; +} \ No newline at end of file diff --git a/games/words-of-wonders/index.html b/games/words-of-wonders/index.html new file mode 100644 index 00000000..e3f2cfed --- /dev/null +++ b/games/words-of-wonders/index.html @@ -0,0 +1,34 @@ + + + + + + Words of Wonders | Mini JS Games Hub + + + +
      +
      +

      ๐Ÿงฉ Words of Wonders

      +

      Form words, fill the puzzle, and test your vocabulary!

      +
      + +
      + +
      + +
      + + + +
      + +
      +

      Words Found: 0 / 0

      +

      +
      +
      + + + + diff --git a/games/words-of-wonders/script.js b/games/words-of-wonders/script.js new file mode 100644 index 00000000..c5de08ff --- /dev/null +++ b/games/words-of-wonders/script.js @@ -0,0 +1,122 @@ +// Words of Wonders Game Logic +const words = ["CODE", "NODE", "CONE", "DONE", "ONCE"]; +const letters = ["C", "O", "D", "E", "N"]; +const totalWords = words.length; + +let foundWords = []; +let currentWord = ""; + +const grid = document.getElementById("grid"); +const lettersContainer = document.getElementById("letters-container"); +const wordsFoundEl = document.getElementById("words-found"); +const totalWordsEl = document.getElementById("total-words"); +const messageEl = document.getElementById("message"); +const shuffleBtn = document.getElementById("shuffle-btn"); +const hintBtn = document.getElementById("hint-btn"); +const resetBtn = document.getElementById("reset-btn"); + +totalWordsEl.textContent = totalWords; + +// Initialize crossword grid (placeholder 5x5) +function createGrid() { + grid.innerHTML = ""; + for (let i = 0; i < 25; i++) { + const cell = document.createElement("div"); + cell.classList.add("cell"); + grid.appendChild(cell); + } +} + +// Render draggable letters +function renderLetters() { + lettersContainer.innerHTML = ""; + letters.forEach((ltr) => { + const letterEl = document.createElement("div"); + letterEl.classList.add("letter"); + letterEl.textContent = ltr; + letterEl.addEventListener("click", () => selectLetter(ltr)); + lettersContainer.appendChild(letterEl); + }); +} + +function selectLetter(ltr) { + currentWord += ltr; + messageEl.textContent = currentWord; + if (currentWord.length >= 3) checkWord(); +} + +function checkWord() { + if (words.includes(currentWord)) { + if (!foundWords.includes(currentWord)) { + foundWords.push(currentWord); + updateGrid(); + wordsFoundEl.textContent = foundWords.length; + showMessage(`โœ… Found: ${currentWord}`, "#a3be8c"); + } else { + showMessage("โš ๏ธ Already found!", "#d08770"); + } + } + if (foundWords.length === totalWords) { + showMessage("๐ŸŽ‰ All words found! You win!", "#a3be8c"); + } + currentWord = ""; +} + +function showMessage(text, color = "#2e3440") { + messageEl.textContent = text; + messageEl.style.color = color; + setTimeout(() => (messageEl.textContent = ""), 2000); +} + +function updateGrid() { + const cells = document.querySelectorAll(".cell"); + for (let i = 0; i < foundWords.length; i++) { + cells[i].classList.add("filled"); + cells[i].textContent = foundWords[i][0]; + } +} + +function shuffleLetters() { + letters.sort(() => Math.random() - 0.5); + renderLetters(); +} + +function showHint() { + const remaining = words.filter((w) => !foundWords.includes(w)); + if (remaining.length > 0) { + const hintWord = remaining[0]; + showMessage(`๐Ÿ’ก Hint: starts with "${hintWord[0]}"`, "#ebcb8b"); + highlightHint(hintWord[0]); + } else { + showMessage("All words found!", "#a3be8c"); + } +} + +function highlightHint(char) { + const cells = document.querySelectorAll(".cell"); + cells.forEach((cell) => { + if (cell.textContent === "") cell.classList.remove("hint"); + }); + const emptyCell = Array.from(cells).find((c) => !c.textContent); + if (emptyCell) { + emptyCell.classList.add("hint"); + emptyCell.textContent = char; + } +} + +function resetGame() { + foundWords = []; + currentWord = ""; + createGrid(); + renderLetters(); + wordsFoundEl.textContent = "0"; + showMessage("๐Ÿ” Game reset!", "#5e81ac"); +} + +shuffleBtn.addEventListener("click", shuffleLetters); +hintBtn.addEventListener("click", showHint); +resetBtn.addEventListener("click", resetGame); + +// Initialize +createGrid(); +renderLetters(); diff --git a/games/words-of-wonders/style.css b/games/words-of-wonders/style.css new file mode 100644 index 00000000..49843863 --- /dev/null +++ b/games/words-of-wonders/style.css @@ -0,0 +1,116 @@ +body { + font-family: "Poppins", sans-serif; + background: linear-gradient(135deg, #88c0d0, #81a1c1); + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + margin: 0; + color: #2e3440; +} + +.game-container { + background: #eceff4; + border-radius: 20px; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2); + padding: 30px; + width: 95%; + max-width: 600px; + text-align: center; +} + +header h1 { + margin: 0; + font-size: 2rem; + color: #3b4252; +} + +.subtitle { + font-size: 1rem; + color: #4c566a; + margin-bottom: 20px; +} + +.crossword-grid { + display: grid; + grid-template-columns: repeat(5, 60px); + grid-gap: 8px; + justify-content: center; + margin-bottom: 25px; +} + +.cell { + width: 60px; + height: 60px; + background-color: #d8dee9; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.4rem; + font-weight: bold; + transition: all 0.3s ease; +} + +.cell.filled { + background-color: #a3be8c; + color: #fff; +} + +.cell.hint { + background-color: #ebcb8b; + color: #2e3440; +} + +.letters-container { + display: flex; + justify-content: center; + gap: 15px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.letter { + background-color: #5e81ac; + color: white; + font-size: 1.5rem; + padding: 15px 18px; + border-radius: 50%; + cursor: pointer; + transition: transform 0.2s, background-color 0.2s; + user-select: none; +} + +.letter:hover { + transform: scale(1.1); + background-color: #81a1c1; +} + +.controls { + margin-bottom: 15px; +} + +button { + padding: 10px 16px; + border: none; + border-radius: 8px; + cursor: pointer; + background-color: #8fbcbb; + color: #2e3440; + font-weight: 600; + margin: 5px; + transition: all 0.2s ease; +} + +button:hover { + background-color: #88c0d0; +} + +.status-bar { + font-size: 1rem; +} + +#message { + margin-top: 10px; + font-weight: bold; +} diff --git a/games/worlds-easiest-game/index.html b/games/worlds-easiest-game/index.html new file mode 100644 index 00000000..ca2ec875 --- /dev/null +++ b/games/worlds-easiest-game/index.html @@ -0,0 +1,23 @@ + + + + + + The World's Easiest Game + + + +
      +

      The World's Easiest Game ๐ŸŽ‰

      +

      Click the button below to win instantly!

      + + + + + + +
      + + + + diff --git a/games/worlds-easiest-game/script.js b/games/worlds-easiest-game/script.js new file mode 100644 index 00000000..7665d99a --- /dev/null +++ b/games/worlds-easiest-game/script.js @@ -0,0 +1,34 @@ +const easyBtn = document.getElementById("easy-btn"); +const feedback = document.getElementById("feedback"); +const resetBtn = document.getElementById("reset-btn"); + +const successMessages = [ + "๐ŸŽ‰ Congrats! Youโ€™re a genius!", + "โœจ That was too easy, right?", + "๐Ÿ† Youโ€™ve won! Incredible!", + "๐Ÿ˜ You nailed it instantly!", + "๐Ÿš€ Success! That was lightning fast!" +]; + +const failureMessages = [ + "๐Ÿ˜… Try again, it's really easy!", + "๐Ÿค” Almost! But not quite...", + "๐Ÿ˜œ Donโ€™t overthink it!", +]; + +// Handle click +easyBtn.addEventListener("click", () => { + const randomIndex = Math.floor(Math.random() * successMessages.length); + feedback.textContent = successMessages[randomIndex]; + + // Hide main button, show reset + easyBtn.classList.add("hidden"); + resetBtn.classList.remove("hidden"); +}); + +// Reset game +resetBtn.addEventListener("click", () => { + feedback.textContent = ""; + easyBtn.classList.remove("hidden"); + resetBtn.classList.add("hidden"); +}); diff --git a/games/worlds-easiest-game/style.css b/games/worlds-easiest-game/style.css new file mode 100644 index 00000000..5102443e --- /dev/null +++ b/games/worlds-easiest-game/style.css @@ -0,0 +1,66 @@ +/* Basic Reset */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +body { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background: linear-gradient(135deg, #ffecd2, #fcb69f); +} + +.game-container { + background-color: #fff; + padding: 40px 60px; + border-radius: 20px; + box-shadow: 0 10px 30px rgba(0,0,0,0.2); + text-align: center; + width: 320px; +} + +h1 { + margin-bottom: 15px; + font-size: 1.8rem; + color: #ff6b6b; +} + +.instructions { + margin-bottom: 25px; + color: #333; +} + +button { + padding: 15px 25px; + font-size: 1rem; + border: none; + border-radius: 10px; + cursor: pointer; + transition: all 0.3s ease; + background-color: #ff6b6b; + color: #fff; +} + +button:hover { + background-color: #ff8787; + transform: translateY(-3px); +} + +button:active { + transform: translateY(1px); +} + +.hidden { + display: none; +} + +.feedback { + margin-top: 20px; + font-size: 1.2rem; + color: #2b8a3e; + min-height: 30px; +} diff --git a/games/zombie-tapper/index.html b/games/zombie-tapper/index.html new file mode 100644 index 00000000..856811f2 --- /dev/null +++ b/games/zombie-tapper/index.html @@ -0,0 +1,32 @@ + + + + + + Zombie Tapper | Mini JS Games Hub + + + +
      +

      ๐ŸงŸโ€โ™‚๏ธ Zombie Tapper

      +
      +

      Score: 0

      +

      Lives: 3

      +

      Time: 60s

      +
      +
      +
      + + + +
      +

      +
      + + + + + + + + diff --git a/games/zombie-tapper/script.js b/games/zombie-tapper/script.js new file mode 100644 index 00000000..854ba41a --- /dev/null +++ b/games/zombie-tapper/script.js @@ -0,0 +1,112 @@ +const gameArea = document.getElementById("game-area"); +const scoreDisplay = document.getElementById("score"); +const livesDisplay = document.getElementById("lives"); +const timerDisplay = document.getElementById("timer"); +const message = document.getElementById("message"); + +const startBtn = document.getElementById("start-btn"); +const pauseBtn = document.getElementById("pause-btn"); +const restartBtn = document.getElementById("restart-btn"); + +const zombieSound = document.getElementById("zombie-hit"); +const humanSound = document.getElementById("human-hit"); +const bgMusic = document.getElementById("bg-music"); + +let score = 0; +let lives = 3; +let timeLeft = 60; +let gameInterval; +let spawnInterval; +let paused = false; + +function randomPosition(max) { + return Math.floor(Math.random() * (max - 70)); +} + +function spawnCharacter() { + if (paused) return; + const char = document.createElement("div"); + const isZombie = Math.random() < 0.7; + char.classList.add("character", isZombie ? "zombie" : "human"); + + char.style.top = `${randomPosition(gameArea.offsetHeight)}px`; + char.style.left = `${randomPosition(gameArea.offsetWidth)}px`; + + gameArea.appendChild(char); + + char.addEventListener("click", () => { + if (isZombie) { + score += 10; + zombieSound.play(); + } else { + lives -= 1; + humanSound.play(); + } + updateHUD(); + char.remove(); + }); + + setTimeout(() => char.remove(), 1500); +} + +function updateHUD() { + scoreDisplay.textContent = score; + livesDisplay.textContent = lives; + if (lives <= 0) endGame("๐Ÿ’€ Game Over!"); +} + +function gameTimer() { + if (paused) return; + timeLeft--; + timerDisplay.textContent = timeLeft; + if (timeLeft <= 0) endGame("โฐ Timeโ€™s Up!"); +} + +function startGame() { + if (gameInterval) return; + bgMusic.volume = 0.3; + bgMusic.play(); + message.textContent = ""; + paused = false; + gameInterval = setInterval(gameTimer, 1000); + spawnInterval = setInterval(spawnCharacter, 800); +} + +function pauseGame() { + paused = !paused; + if (paused) { + message.textContent = "โธ๏ธ Paused"; + bgMusic.pause(); + } else { + message.textContent = ""; + bgMusic.play(); + } +} + +function restartGame() { + clearInterval(gameInterval); + clearInterval(spawnInterval); + score = 0; + lives = 3; + timeLeft = 60; + paused = false; + gameArea.innerHTML = ""; + updateHUD(); + message.textContent = ""; + gameInterval = null; + startGame(); +} + +function endGame(text) { + clearInterval(gameInterval); + clearInterval(spawnInterval); + bgMusic.pause(); + message.textContent = text; + const characters = document.querySelectorAll(".character"); + characters.forEach(c => c.remove()); + gameInterval = null; +} + +startBtn.addEventListener("click", startGame); +pauseBtn.addEventListener("click", pauseGame); +restartBtn.addEventListener("click", restartGame); diff --git a/games/zombie-tapper/style.css b/games/zombie-tapper/style.css new file mode 100644 index 00000000..899e6604 --- /dev/null +++ b/games/zombie-tapper/style.css @@ -0,0 +1,88 @@ +body { + margin: 0; + font-family: 'Poppins', sans-serif; + background: radial-gradient(circle at center, #111, #000); + color: white; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.game-container { + text-align: center; +} + +h1 { + text-shadow: 0 0 15px limegreen, 0 0 30px greenyellow; +} + +.hud { + display: flex; + justify-content: space-around; + font-size: 18px; + margin-bottom: 10px; +} + +.game-area { + position: relative; + width: 600px; + height: 400px; + border: 3px solid limegreen; + background: rgba(30, 30, 30, 0.9); + overflow: hidden; + box-shadow: 0 0 20px limegreen, inset 0 0 20px #0f0; +} + +.character { + position: absolute; + width: 70px; + height: 70px; + background-size: cover; + cursor: pointer; + transition: transform 0.2s ease; +} + +.character.zombie { + background-image: url('https://i.ibb.co/3StQXcD/zombie.png'); + filter: drop-shadow(0 0 10px limegreen); +} + +.character.human { + background-image: url('https://i.ibb.co/vX3qCZ2/human.png'); + filter: drop-shadow(0 0 10px red); +} + +.character:hover { + transform: scale(1.1); +} + +.controls { + margin-top: 10px; +} + +button { + margin: 5px; + padding: 10px 15px; + font-size: 16px; + border: none; + border-radius: 8px; + background-color: #0f0; + color: black; + font-weight: bold; + cursor: pointer; + box-shadow: 0 0 15px #0f0; + transition: all 0.2s; +} + +button:hover { + background-color: #8fff8f; + box-shadow: 0 0 30px #0f0; +} + +#message { + margin-top: 10px; + font-size: 20px; + text-shadow: 0 0 10px yellow; +} diff --git a/index.html b/index.html index cc63d030..104bfece 100644 --- a/index.html +++ b/index.html @@ -1,110 +1,253 @@ + + + + + Mini JS Games Hub ๐ŸŽฎ + + + - - - - Mini JS Games Hub ๐ŸŽฎ - - - - - - -
      -
      - - -
      -
      -

      Open-source arcade for the web

      -

      Play. Learn. Ship your next mini-game.

      -

      Explore hand-crafted JavaScript games, peek under the hood, and add your own creations. - Everything runs right in the browser.

      - -

      Click "Play now" on any card to open the game in the same tab. Use "Open in new tab โ†’" to - keep this hub open.

      -
      -
      -
      Total Games
      -
      0 and counting
      + + + +
      +
      + + +
      +
      +

      Open-source arcade for the web

      +

      Play. Learn. Ship your next mini-game.

      +

      + Explore hand-crafted JavaScript games, peek under the hood, and + add your own creations โ€” all right in your browser. +

      + + + +

      + Click "Play now" to open a game in this tab, or "Open in new tab + โ†’" to keep this hub open. +

      + +
      +
      +
      Total Games
      +
      0 and counting
      +
      +
      +
      Latest Launch
      +
      --
      +
      +
      +
      Stack
      +
      HTML โ€ข CSS โ€ข JS
      +
      +
      -
      -
      Stack
      -
      HTML โ€ข CSS โ€ข JS
      + + -
      -
      - -
      + +
      +
      +

      Your Pro Player Badges โญ

      +
      +
      -
      - โšก Instant Load - ๐Ÿ“ฆ No Build Tools - ๐ŸŒ Browser Ready +
      + +
      + -
      - + +
      + 0 games + Latest: -- +
      + + +
      + +
      +
      +
      + + + - - -
      -
      - -
      - 0 games - Latest: -- -
      -
      - - - -
      -
      - - - - - - - - \ No newline at end of file + + + + + + + + + + diff --git a/login.css b/login.css new file mode 100644 index 00000000..6be76d72 --- /dev/null +++ b/login.css @@ -0,0 +1,587 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Poppins', sans-serif; +} + +body { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background: linear-gradient(135deg, #000000, #000000, #000000); + overflow: hidden; + perspective: 1000px; +} + +#particles-js { + position: absolute; + width: 100%; + height: 100%; + z-index: 1; +} + +.container { + width: 100%; + max-width: 360px; + padding: 20px; + position: relative; + z-index: 10; + perspective: 1000px; +} + +.form-box { + background: rgba(0, 0, 0, 0.7); + border-radius: 24px; + padding: 40px; + box-shadow: 0 8px 24px rgba(0, 212, 255, 0.4), inset 0 0 16px rgba(0, 212, 255, 0.2); + + border: 1px solid rgba(0, 212, 255, 0.3); + transform-style: preserve-3d; + animation: formEntrance 1.5s cubic-bezier(0.68, -0.55, 0.27, 1.55) forwards; + opacity: 0; + transform: rotateX(20deg) translateY(100px); +} + +@keyframes formEntrance { + 0% { + opacity: 0; + transform: rotateX(20deg) translateY(100px); + } + 50% { + opacity: 1; + } + 100% { + opacity: 1; + transform: rotateX(0deg) translateY(0); + } +} + +h2 { + color: #00d4ff; + text-align: center; + margin-bottom: 10px; + font-family: 'Cinzel', serif; + font-weight: 700; + letter-spacing: 3px; + text-shadow: 0 0 10px rgba(0, 212, 255, 0.5); + animation: glow 2s infinite alternate; + font-size: 24px; +} + +@keyframes glow { + from { + text-shadow: 0 0 5px rgba(0, 212, 255, 0.5); + } + to { + text-shadow: 0 0 15px rgba(0, 212, 255, 0.8); + } +} + +/* Form Styles */ +.login-form { + width: 100%; +} + +.input-container { + margin-bottom: 25px; +} + +.input-wrapper { + position: relative; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 107, 107, 0.3); + border-radius: 10px; + overflow: hidden; + transition: all 0.3s ease; +} + +.input-wrapper:hover { + border-color: #ff6b6b; + box-shadow: 0 0 20px rgba(255, 107, 107, 0.3); +} + +.input-wrapper.focused { + border-color: #4ecdc4; + box-shadow: 0 0 25px rgba(78, 205, 196, 0.4); +} + +.input-wrapper input { + width: 100%; + padding: 15px 20px; + background: transparent; + border: none; + outline: none; + color: #fff; + font-size: 1rem; + font-family: 'Rajdhani', sans-serif; + position: relative; + z-index: 2; +} + +.input-wrapper label { + position: absolute; + top: 15px; + left: 20px; + color: rgba(255, 107, 107, 0.7); + font-size: 1rem; + pointer-events: none; + transition: all 0.3s ease; + z-index: 1; +} + +.input-wrapper.focused label, +.input-wrapper input:valid + label { + top: -10px; + left: 15px; + font-size: 0.8rem; + color: #4ecdc4; + background: #1a1a2e; + padding: 0 5px; +} + +.scan-line { + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 2px; + background: linear-gradient(90deg, transparent, #ff6b6b, transparent); + transition: left 0.5s ease; +} + +.input-wrapper.focused .scan-line { + animation: scanLine 2s ease-in-out infinite; +} + +@keyframes scanLine { + 0% { left: -100%; } + 50% { left: 100%; } + 100% { left: -100%; } +} + +/* Options */ +.options { + display: flex; + justify-content: space-between; + align-items: center; + margin: 25px 0; + font-size: 0.9rem; +} + +.checkbox-container { + display: flex; + align-items: center; + cursor: pointer; + color: rgba(255, 255, 255, 0.8); +} + +.checkbox-container input { + display: none; +} + +.checkmark { + width: 18px; + height: 18px; + border: 2px solid #ff6b6b; + border-radius: 3px; + margin-right: 10px; + position: relative; + transition: all 0.3s ease; +} + +.checkbox-container input:checked + .checkmark { + background: #ff6b6b; + box-shadow: 0 0 15px rgba(255, 107, 107, 0.6); +} + +.checkmark::after { + content: ''; + position: absolute; + display: none; + left: 5px; + top: 2px; + width: 6px; + height: 10px; + border: solid #1a1a2e; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +.checkbox-container input:checked + .checkmark::after { + display: block; +} + +.forgot-link { + color: #4ecdc4; + text-decoration: none; + transition: all 0.3s ease; +} + +.forgot-link:hover { + color: #ff6b6b; + text-shadow: 0 0 10px rgba(255, 107, 107, 0.8); +} + +p { + color: rgba(0, 212, 255, 0.7); + text-align: center; + margin-bottom: 25px; + font-size: 14px; + letter-spacing: 1px; +} + +.input-group { + position: relative; + margin-bottom: 25px; + transform: translateZ(10px); +} + +.input-field { + width: 100%; + padding: 12px 0; + font-size: 15px; + color: #00d4ff; + background: transparent; + border: none; + border-bottom: 1px solid rgba(0, 212, 255, 0.3); + outline: none; + transition: 0.3s; + z-index: 1; + position: relative; +} + +.input-field:focus { + border-bottom-color: transparent; +} + +.glow-line { + position: absolute; + bottom: 0; + left: 0; + width: 0; + height: 2px; + background: linear-gradient(90deg, + transparent, + rgba(0, 212, 255, 0.5), + #00d4ff, + rgba(0, 212, 255, 0.5), + transparent); + transition: 0.4s; + z-index: 0; +} + +.input-field:focus ~ .glow-line { + width: 100%; + box-shadow: 0 0 10px rgba(0, 212, 255, 0.7); + animation: shimmer 2s infinite; +} + +@keyframes shimmer { + 0% { + background-position: -100% 0; + } + 100% { + background-position: 200% 0; + } +} + +.input-group label { + position: absolute; + top: 12px; + left: 0; + color: rgba(0, 212, 255, 0.7); + font-size: 15px; + pointer-events: none; + transition: 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55); +} + +.input-field:focus ~ label, +.input-field:valid ~ label { + top: -12px; + font-size: 12px; + color: #00d4ff; + text-shadow: 0 0 5px rgba(0, 212, 255, 0.5); + letter-spacing: 1px; +} + +.remember-forgot { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 25px; + font-size: 13px; +} + +.remember { + display: flex; + align-items: center; +} + +.remember input { + margin-right: 5px; + accent-color: #00d4ff; +} + +.remember label, .forgot { + color: rgba(0, 212, 255, 0.8); + font-size: 13px; +} + +.forgot { + text-decoration: none; + transition: 0.3s; + position: relative; +} + +.forgot:hover { + color: #00d4ff; + text-shadow: 0 0 5px rgba(0, 212, 255, 0.5); +} + +.forgot::after { + content: ''; + position: absolute; + width: 0; + height: 1px; + bottom: -2px; + left: 0; + background-color: #00d4ff; + transition: 0.3s; +} + +.forgot:hover::after { + width: 100%; + box-shadow: 0 0 5px rgba(0, 212, 255, 0.7); +} + +.login-btn { + width: 100%; + padding: 12px 0; + background: #000; + border: 1px solid #00d4ff; + border-radius: 25px; + color: #00d4ff; + font-size: 15px; + font-weight: 500; + letter-spacing: 2px; + cursor: pointer; + transition: all 0.5s; + position: relative; + overflow: hidden; + transform: translateZ(20px); + font-family: 'Cinzel', serif; + margin-bottom: 10px; +} + +.login-btn span { + position: relative; + z-index: 1; + transition: 0.3s; +} + +.btn-glow { + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, + transparent, + rgba(0, 212, 255, 0.2), + rgba(0, 212, 255, 0.3), + rgba(0, 212, 255, 0.2), + transparent); + transition: 0.5s; +} + +.login-btn:hover .btn-glow { + left: 100%; +} + +.login-btn:hover { + background: rgba(0, 212, 255, 0.1); + box-shadow: 0 0 20px rgba(0, 212, 255, 0.5), + inset 0 0 10px rgba(0, 212, 255, 0.3); + transform: translateZ(20px) scale(1.05); +} + +.login-btn:hover span { + text-shadow: 0 0 5px rgba(0, 212, 255, 0.7); +} + +.login-btn:active { + transform: translateZ(20px) scale(0.98); +} + +.signup-link { + text-align: center; + margin-top: 20px; + color: rgba(0, 212, 255, 0.7); + font-size: 13px; +} + +.signup-link a { + color: #00d4ff; + text-decoration: none; + font-weight: 500; + transition: 0.3s; + position: relative; +} + +.signup-link a:hover { + text-shadow: 0 0 5px rgba(0, 212, 255, 0.7); +} + +.signup-link a::after { + content: ''; + position: absolute; + width: 0; + height: 1px; + bottom: -2px; + left: 0; + background-color: #00d4ff; + transition: 0.3s; +} + +.signup-link a:hover::after { + width: 100%; + box-shadow: 0 0 5px rgba(0, 212, 255, 0.7); +} + +/* Responsive Design */ +@media (max-width: 1200px) { + .login-container { + flex-direction: column; + justify-content: center; + gap: 40px; + padding: 40px; + } + + .info-panel { + max-width: 420px; + } +} + +@media (max-width: 768px) { + .login-container { + padding: 20px; + gap: 30px; + } + + .login-box { + padding: 32px 24px; + max-width: 100%; + } + + .info-panel { + padding: 32px 24px; + max-width: 100%; + } + + .logo h1 { + font-size: 24px; + } + + .info-panel h3 { + font-size: 20px; + } +} + +@media (max-width: 480px) { + .login-container { + padding: 16px; + } + + .login-box { + padding: 24px 20px; + } + + .info-panel { + padding: 24px 20px; + } + + input[type="text"], + input[type="password"] { + padding: 14px 14px 14px 44px; + } + + .input-icon { + left: 14px; + } + + label { + left: 44px; + } + + .input-wrapper.focused label, + input:focus + label, + input:valid + label { + left: 36px; + } +} + +@media (max-width: 768px) { + .login-box { + padding: 30px 20px; + margin: 10px; + } + + .header h1 { + font-size: 2rem; + } + + .status-panel { + position: relative; + top: auto; + right: auto; + margin-top: 20px; + margin-left: auto; + margin-right: auto; + max-width: 300px; + } +} + +/* 3D hover effect for the entire form */ +.form-box:hover { + transform: rotateX(5deg) rotateY(5deg); + transition: transform 0.5s ease; +} + +/* Additional animations for form elements */ +.input-group, .remember-forgot, .login-btn, .signup-link { + animation: fadeInUp 0.5s forwards; + opacity: 0; +} + +.input-group:nth-child(1) { + animation-delay: 0.6s; +} + +.input-group:nth-child(2) { + animation-delay: 0.8s; +} + +.remember-forgot { + animation-delay: 1s; +} + +.login-btn { + animation-delay: 1.2s; +} + +.signup-link { + animation-delay: 1.4s; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/login.html b/login.html new file mode 100644 index 00000000..44a4ceef --- /dev/null +++ b/login.html @@ -0,0 +1,164 @@ + + + + + + Gamehub login + + + + + +
      + +
      +
      +

      Login

      +

      Welcome Back!😊

      + +
      +
      + + +
      +
      + +
      + + +
      +
      + +
      +
      + + +
      + Forgot Password? +
      + + + + +
      +
      +
      + + + + + + diff --git a/login.js b/login.js new file mode 100644 index 00000000..33369ac2 --- /dev/null +++ b/login.js @@ -0,0 +1,26 @@ +document.addEventListener('DOMContentLoaded', function () { + const form = document.getElementById('loginForm'); + if (!form) return; + + form.addEventListener('submit', function (e) { + e.preventDefault(); + const email = document.getElementById('loginEmail').value.trim(); + const password = document.getElementById('loginPassword').value; + + if (!email || !password) { + alert('Please fill all fields.'); + return; + } + + const users = JSON.parse(localStorage.getItem('users') || '[]'); + const user = users.find(u => u.email === email && u.password === password); + + if (user) { + // On successful login + localStorage.setItem('loggedInUser', email); + window.location.href = 'index.html'; + } else { + alert('Invalid email or password.'); + } + }); +}); \ No newline at end of file diff --git a/register.css b/register.css new file mode 100644 index 00000000..6be76d72 --- /dev/null +++ b/register.css @@ -0,0 +1,587 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Poppins', sans-serif; +} + +body { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background: linear-gradient(135deg, #000000, #000000, #000000); + overflow: hidden; + perspective: 1000px; +} + +#particles-js { + position: absolute; + width: 100%; + height: 100%; + z-index: 1; +} + +.container { + width: 100%; + max-width: 360px; + padding: 20px; + position: relative; + z-index: 10; + perspective: 1000px; +} + +.form-box { + background: rgba(0, 0, 0, 0.7); + border-radius: 24px; + padding: 40px; + box-shadow: 0 8px 24px rgba(0, 212, 255, 0.4), inset 0 0 16px rgba(0, 212, 255, 0.2); + + border: 1px solid rgba(0, 212, 255, 0.3); + transform-style: preserve-3d; + animation: formEntrance 1.5s cubic-bezier(0.68, -0.55, 0.27, 1.55) forwards; + opacity: 0; + transform: rotateX(20deg) translateY(100px); +} + +@keyframes formEntrance { + 0% { + opacity: 0; + transform: rotateX(20deg) translateY(100px); + } + 50% { + opacity: 1; + } + 100% { + opacity: 1; + transform: rotateX(0deg) translateY(0); + } +} + +h2 { + color: #00d4ff; + text-align: center; + margin-bottom: 10px; + font-family: 'Cinzel', serif; + font-weight: 700; + letter-spacing: 3px; + text-shadow: 0 0 10px rgba(0, 212, 255, 0.5); + animation: glow 2s infinite alternate; + font-size: 24px; +} + +@keyframes glow { + from { + text-shadow: 0 0 5px rgba(0, 212, 255, 0.5); + } + to { + text-shadow: 0 0 15px rgba(0, 212, 255, 0.8); + } +} + +/* Form Styles */ +.login-form { + width: 100%; +} + +.input-container { + margin-bottom: 25px; +} + +.input-wrapper { + position: relative; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 107, 107, 0.3); + border-radius: 10px; + overflow: hidden; + transition: all 0.3s ease; +} + +.input-wrapper:hover { + border-color: #ff6b6b; + box-shadow: 0 0 20px rgba(255, 107, 107, 0.3); +} + +.input-wrapper.focused { + border-color: #4ecdc4; + box-shadow: 0 0 25px rgba(78, 205, 196, 0.4); +} + +.input-wrapper input { + width: 100%; + padding: 15px 20px; + background: transparent; + border: none; + outline: none; + color: #fff; + font-size: 1rem; + font-family: 'Rajdhani', sans-serif; + position: relative; + z-index: 2; +} + +.input-wrapper label { + position: absolute; + top: 15px; + left: 20px; + color: rgba(255, 107, 107, 0.7); + font-size: 1rem; + pointer-events: none; + transition: all 0.3s ease; + z-index: 1; +} + +.input-wrapper.focused label, +.input-wrapper input:valid + label { + top: -10px; + left: 15px; + font-size: 0.8rem; + color: #4ecdc4; + background: #1a1a2e; + padding: 0 5px; +} + +.scan-line { + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 2px; + background: linear-gradient(90deg, transparent, #ff6b6b, transparent); + transition: left 0.5s ease; +} + +.input-wrapper.focused .scan-line { + animation: scanLine 2s ease-in-out infinite; +} + +@keyframes scanLine { + 0% { left: -100%; } + 50% { left: 100%; } + 100% { left: -100%; } +} + +/* Options */ +.options { + display: flex; + justify-content: space-between; + align-items: center; + margin: 25px 0; + font-size: 0.9rem; +} + +.checkbox-container { + display: flex; + align-items: center; + cursor: pointer; + color: rgba(255, 255, 255, 0.8); +} + +.checkbox-container input { + display: none; +} + +.checkmark { + width: 18px; + height: 18px; + border: 2px solid #ff6b6b; + border-radius: 3px; + margin-right: 10px; + position: relative; + transition: all 0.3s ease; +} + +.checkbox-container input:checked + .checkmark { + background: #ff6b6b; + box-shadow: 0 0 15px rgba(255, 107, 107, 0.6); +} + +.checkmark::after { + content: ''; + position: absolute; + display: none; + left: 5px; + top: 2px; + width: 6px; + height: 10px; + border: solid #1a1a2e; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +.checkbox-container input:checked + .checkmark::after { + display: block; +} + +.forgot-link { + color: #4ecdc4; + text-decoration: none; + transition: all 0.3s ease; +} + +.forgot-link:hover { + color: #ff6b6b; + text-shadow: 0 0 10px rgba(255, 107, 107, 0.8); +} + +p { + color: rgba(0, 212, 255, 0.7); + text-align: center; + margin-bottom: 25px; + font-size: 14px; + letter-spacing: 1px; +} + +.input-group { + position: relative; + margin-bottom: 25px; + transform: translateZ(10px); +} + +.input-field { + width: 100%; + padding: 12px 0; + font-size: 15px; + color: #00d4ff; + background: transparent; + border: none; + border-bottom: 1px solid rgba(0, 212, 255, 0.3); + outline: none; + transition: 0.3s; + z-index: 1; + position: relative; +} + +.input-field:focus { + border-bottom-color: transparent; +} + +.glow-line { + position: absolute; + bottom: 0; + left: 0; + width: 0; + height: 2px; + background: linear-gradient(90deg, + transparent, + rgba(0, 212, 255, 0.5), + #00d4ff, + rgba(0, 212, 255, 0.5), + transparent); + transition: 0.4s; + z-index: 0; +} + +.input-field:focus ~ .glow-line { + width: 100%; + box-shadow: 0 0 10px rgba(0, 212, 255, 0.7); + animation: shimmer 2s infinite; +} + +@keyframes shimmer { + 0% { + background-position: -100% 0; + } + 100% { + background-position: 200% 0; + } +} + +.input-group label { + position: absolute; + top: 12px; + left: 0; + color: rgba(0, 212, 255, 0.7); + font-size: 15px; + pointer-events: none; + transition: 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55); +} + +.input-field:focus ~ label, +.input-field:valid ~ label { + top: -12px; + font-size: 12px; + color: #00d4ff; + text-shadow: 0 0 5px rgba(0, 212, 255, 0.5); + letter-spacing: 1px; +} + +.remember-forgot { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 25px; + font-size: 13px; +} + +.remember { + display: flex; + align-items: center; +} + +.remember input { + margin-right: 5px; + accent-color: #00d4ff; +} + +.remember label, .forgot { + color: rgba(0, 212, 255, 0.8); + font-size: 13px; +} + +.forgot { + text-decoration: none; + transition: 0.3s; + position: relative; +} + +.forgot:hover { + color: #00d4ff; + text-shadow: 0 0 5px rgba(0, 212, 255, 0.5); +} + +.forgot::after { + content: ''; + position: absolute; + width: 0; + height: 1px; + bottom: -2px; + left: 0; + background-color: #00d4ff; + transition: 0.3s; +} + +.forgot:hover::after { + width: 100%; + box-shadow: 0 0 5px rgba(0, 212, 255, 0.7); +} + +.login-btn { + width: 100%; + padding: 12px 0; + background: #000; + border: 1px solid #00d4ff; + border-radius: 25px; + color: #00d4ff; + font-size: 15px; + font-weight: 500; + letter-spacing: 2px; + cursor: pointer; + transition: all 0.5s; + position: relative; + overflow: hidden; + transform: translateZ(20px); + font-family: 'Cinzel', serif; + margin-bottom: 10px; +} + +.login-btn span { + position: relative; + z-index: 1; + transition: 0.3s; +} + +.btn-glow { + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, + transparent, + rgba(0, 212, 255, 0.2), + rgba(0, 212, 255, 0.3), + rgba(0, 212, 255, 0.2), + transparent); + transition: 0.5s; +} + +.login-btn:hover .btn-glow { + left: 100%; +} + +.login-btn:hover { + background: rgba(0, 212, 255, 0.1); + box-shadow: 0 0 20px rgba(0, 212, 255, 0.5), + inset 0 0 10px rgba(0, 212, 255, 0.3); + transform: translateZ(20px) scale(1.05); +} + +.login-btn:hover span { + text-shadow: 0 0 5px rgba(0, 212, 255, 0.7); +} + +.login-btn:active { + transform: translateZ(20px) scale(0.98); +} + +.signup-link { + text-align: center; + margin-top: 20px; + color: rgba(0, 212, 255, 0.7); + font-size: 13px; +} + +.signup-link a { + color: #00d4ff; + text-decoration: none; + font-weight: 500; + transition: 0.3s; + position: relative; +} + +.signup-link a:hover { + text-shadow: 0 0 5px rgba(0, 212, 255, 0.7); +} + +.signup-link a::after { + content: ''; + position: absolute; + width: 0; + height: 1px; + bottom: -2px; + left: 0; + background-color: #00d4ff; + transition: 0.3s; +} + +.signup-link a:hover::after { + width: 100%; + box-shadow: 0 0 5px rgba(0, 212, 255, 0.7); +} + +/* Responsive Design */ +@media (max-width: 1200px) { + .login-container { + flex-direction: column; + justify-content: center; + gap: 40px; + padding: 40px; + } + + .info-panel { + max-width: 420px; + } +} + +@media (max-width: 768px) { + .login-container { + padding: 20px; + gap: 30px; + } + + .login-box { + padding: 32px 24px; + max-width: 100%; + } + + .info-panel { + padding: 32px 24px; + max-width: 100%; + } + + .logo h1 { + font-size: 24px; + } + + .info-panel h3 { + font-size: 20px; + } +} + +@media (max-width: 480px) { + .login-container { + padding: 16px; + } + + .login-box { + padding: 24px 20px; + } + + .info-panel { + padding: 24px 20px; + } + + input[type="text"], + input[type="password"] { + padding: 14px 14px 14px 44px; + } + + .input-icon { + left: 14px; + } + + label { + left: 44px; + } + + .input-wrapper.focused label, + input:focus + label, + input:valid + label { + left: 36px; + } +} + +@media (max-width: 768px) { + .login-box { + padding: 30px 20px; + margin: 10px; + } + + .header h1 { + font-size: 2rem; + } + + .status-panel { + position: relative; + top: auto; + right: auto; + margin-top: 20px; + margin-left: auto; + margin-right: auto; + max-width: 300px; + } +} + +/* 3D hover effect for the entire form */ +.form-box:hover { + transform: rotateX(5deg) rotateY(5deg); + transition: transform 0.5s ease; +} + +/* Additional animations for form elements */ +.input-group, .remember-forgot, .login-btn, .signup-link { + animation: fadeInUp 0.5s forwards; + opacity: 0; +} + +.input-group:nth-child(1) { + animation-delay: 0.6s; +} + +.input-group:nth-child(2) { + animation-delay: 0.8s; +} + +.remember-forgot { + animation-delay: 1s; +} + +.login-btn { + animation-delay: 1.2s; +} + +.signup-link { + animation-delay: 1.4s; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/register.html b/register.html new file mode 100644 index 00000000..7812c636 --- /dev/null +++ b/register.html @@ -0,0 +1,169 @@ + + + + + + Gamehub register + + + + + +
      + +
      +
      +

      Register

      +

      Namaste!!👋

      + +
      +
      + + +
      +
      +
      + + +
      +
      +
      + + +
      +
      +
      + + +
      +
      +
      +
      + + +
      +
      + + +
      +
      +
      + + + + + + diff --git a/register.js b/register.js new file mode 100644 index 00000000..134b92e2 --- /dev/null +++ b/register.js @@ -0,0 +1,26 @@ +document.addEventListener('DOMContentLoaded', function () { + const form = document.getElementById('registerForm'); + form.addEventListener('submit', function (e) { + e.preventDefault(); + const name = document.getElementById('registerUsername').value.trim(); + const email = document.getElementById('registerEmail').value.trim(); + const password = document.getElementById('registerPassword').value; + + if (!name || !email || !password) { + alert('Please fill all fields.'); + return; + } + + let users = JSON.parse(localStorage.getItem('users') || '[]'); + if (users.find(u => u.email === email)) { + alert('Email already registered.'); + return; + } + users.push({ name, email, password }); + localStorage.setItem('users', JSON.stringify(users)); + // On successful registration + alert('Signup successful! You are now logged in.'); + localStorage.setItem('loggedInUser', email); + window.location.href = 'index.html'; + }); +}); \ No newline at end of file diff --git a/script.js b/script.js index 67f23519..3877a6c2 100644 --- a/script.js +++ b/script.js @@ -1,4 +1,21 @@ const games = [ + { + name: "Simon Says", + path: "games/Simon-Says-Game/index.html", + icon: "๐Ÿง ", + description: + "Challenge your memory and reflexes in this fast-paced color sequence game! Each round adds a new twistโ€”can you keep up as the pattern grows? Perfect for puzzle lovers and focus masters seeking a brain-boosting thrill.", + category: "Memory", + duration: "Progressive", + tags: [ + "memory", + "focus", + "puzzle", + "challenge", + "reflex", + "brain-training", + ], + }, { name: "Tic Tac Toe", path: "games/tictactoe/index.html", @@ -9,6 +26,66 @@ const games = [ duration: "2 min rounds", tags: ["2 players", "grid", "classic"], }, + { + name: "Tap Challenge", + path: "games/tap-challenge/index.html", + icon: "๐Ÿ–ฑ๏ธ", + description: + "A fast-paced reaction game where you tap the button as many times as possible before time runs out. Test your speed and reflexes!", + category: "Arcade", + duration: "20 seconds", + tags: ["arcade", "reaction", "reflex", "clicker", "timed"], + }, + { + name: "Emoji Battle Arena", + path: "games/emoji-battle-arena/index.html", + icon: "๐Ÿช„", + description: + "Fast-paced multiplayer arena where players control emoji characters, move around, and attack opponents. Last emoji standing wins!", + category: "Arcade", + duration: "Unlimited", + tags: ["multiplayer", "arcade", "emoji", "action", "battle"], + }, + { + name: "Quick Math Battle Quiz", + path: "games/quick-math-battle-quiz/index.html", + icon: "๐Ÿงฎ", + description: + "A fast-paced math quiz game. Solve arithmetic questions (addition, subtraction, multiplication, division) before the timer runs out. Tracks score and speed for a fun and educational challenge.", + category: "Puzzle", + duration: "Timed", + tags: ["puzzle", "quiz", "math", "logic", "speed"], + }, + { + name: "Reaction Duel", + path: "games/reaction-duel/index.html", + icon: "โšก", + description: + "A fast-paced two-player reaction game. Press your key as fast as possible after the signal appears!", + category: "Arcade", + duration: "Unlimited", + tags: ["arcade", "reflex", "two-player", "keyboard"], + }, + { + name: "Lights Out", + path: "games/lights-out/index.html", + icon: "๐Ÿ’ก", + description: "Turn off all lights by toggling your moves wisely!", + category: "Puzzle", + duration: "Unlimited", + tags: ["puzzle", "logic", "brain", "strategy"], + }, + { + name: "Brick Breaker", + path: "games/brick-breaker/index.html", + icon: "๐Ÿงฑ", + description: + "A classic arcade game where the player controls a paddle to deflect a ball and destroy a wall of colored bricks.", + category: "Arcade", + duration: "Short/Medium", + tags: ["arcade", "classic", "physics", "canvas"], + }, + { name: "Snake Game", path: "games/snake/index.html", @@ -19,6 +96,22 @@ const games = [ duration: "Endless", tags: ["arcade", "retro", "keyboard"], }, + { + name: "Simon color memory game", + path: "games/simon_game/index.html", + icon: "๐Ÿง ", + description: "Test your memory and quick thinking in this dynamic color pattern challenge! Each round adds a new twist to the sequence. Ideal for puzzle enthusiasts and sharp minds craving a stimulating mental workout.", + category: "Memory", + duration: "Progressive", + tags: [ + "memory", + "focus", + "puzzle", + "challenge", + "reflex", + "brain-training", + ], + }, { name: "Memory Game", path: "games/memory/index.html", @@ -39,6 +132,17 @@ const games = [ duration: "30 sec", tags: ["reflex", "timed", "mouse"], }, + { + name: "Falling Leaves Collector", + path: "games/falling-leaves-collector/index.html", + icon: "๐Ÿ‚", + description: + "Collect falling leaves before they hit the ground. Test your reflexes and hand-eye coordination, and try to get the highest score!", + category: "Arcade", + duration: "Unlimited", + tags: ["arcade", "reflex", "clicker", "catch", "reaction"], + }, + { name: "Reaction Timer", path: "games/reaction-timer/index.html", @@ -49,6 +153,17 @@ const games = [ duration: "Quick burst", tags: ["speed", "focus", "solo"], }, + { + name: "Math Challenge", + path: "games/math-challenge/index.html", + icon: "๐Ÿงฎ", + description: + "Test your arithmetic skills with addition, subtraction, multiplication, and division against the clock!", + category: "Puzzle", + duration: "60 seconds", + tags: ["math", "puzzle", "arithmetic", "timed", "logic"], + }, + { name: "Space Shooter", path: "games/space-shooter/index.html", @@ -69,6 +184,17 @@ const games = [ duration: "10-20 min", tags: ["puzzle", "singleplayer", "numbers"], }, + { + name: "Spot the Difference", + path: "games/spot-the-difference/index.html", + icon: "๐Ÿ”", + description: + "Find all the differences between two images before time runs out! Test your observation and attention to detail.", + category: "Puzzle", + duration: "60 seconds", + tags: ["puzzle", "observation", "attention", "clicker", "challenge"], + }, + { name: "15 Puzzle", path: "games/15-puzzle/index.html", @@ -79,6 +205,17 @@ const games = [ duration: "5-15 min", tags: ["puzzle", "tiles", "spatial"], }, + { + name: "Endless Runner", + path: "games/endless-runner/index.html", + icon: "๐Ÿƒโ€โ™‚๏ธ", + description: + "Run endlessly, dodge obstacles, and survive as the game speeds up!", + category: "Arcade", + duration: "Endless", + tags: ["arcade", "runner", "reflex", "jump", "dodge"], + }, + { name: "Pong", path: "games/pong/index.html", @@ -90,14 +227,68 @@ const games = [ tags: ["arcade", "retro", "multiplayer", "cpu"], }, { - name: "Simon Says", + name: "Avoid the Blocks", + path: "games/avoid-the-blocks/index.html", + icon: "โฌ›", + description: + "A fast-paced obstacle-avoidance game. Dodge falling blocks as long as possible. Tests reflexes and timing with increasing difficulty.", + category: "Arcade", + duration: "Unlimited", + tags: ["arcade", "reflex", "dodging", "timing"], + }, + { + name: "Emoji Match Game", + path: "games/Emoji Match Game/index.html", // Updated path + icon: "๐Ÿงฉ", + description: + "A fun and addictive memory game! Flip cards to reveal emojis and match pairs. Track your moves and timeโ€”can you finish with the fewest moves?", + category: "Memory", + duration: "Unlimited", + tags: ["memory", "puzzle", "matching", "logic", "brain-training", "fun"], + }, + + { + name: "The Godzilla Fights game", // Updated name based on folder + path: "games/The Godzilla Fights game(html,css,js)/index.html", + icon: "🦍", + description: + "A exciting fighting game where two cartoon gorillas stand on opposite rooftops in a cityscape at sunset. The player (on the left) aims and throws a bomb at the computer opponent by dragging to set the angle and velocity", + category: "Fighting", + duration: "Endless", + tags: ["Fighting", "Special", "multiplayer", "computer"], + }, + { + name: "Color Switch Challenge", + path: "games/color-switch-challenge/index.html", + icon: "๐ŸŽจ", + description: + "A fast-paced game where you navigate a ball through rotating obstacles, matching its color. Test your reflexes and timing!", + category: "Puzzle", + duration: "Unlimited", + tags: ["puzzle", "reflex", "timing", "color", "challenge"], + }, + + { + name: "SimonSays", // Kept folder name due to duplicate name path: "games/SimonSays/index.html", icon: "๐Ÿง ", - description: "A fun memory game where players repeat an increasingly complex sequence of colors.", + description: + "A fun memory game where players repeat an increasingly complex sequence of colors.", category: "Memory", duration: "Progressive", - tags: ["memory", "focus", "puzzle", "challenge"], + tags: ["memory", "focus", "puzzle", "challenge"], + }, + { + name: "Color Sequence Memory", + path: "games/Color Sequence Memory/index.html", // Updated path + icon: "๐ŸŽจ", + description: + "Test your memory by following the color sequence shown on the screen. Repeat the pattern correctly to advance through increasingly challenging levels.", + category: "Puzzle", + duration: "Unlimited", + tags: ["puzzle", "memory", "sequence", "logic", "challenge"], }, + { name: "Typing Test", path: "games/typing-test/index.html", @@ -112,13 +303,890 @@ const games = [ name: "Balloon Pop", path: "games/balloon-pop/index.html", icon: "๐ŸŽˆ", - description: "Click the balloons before they float away! Pop as many as you can.", + description: + "Click the balloons before they float away! Pop as many as you can.", + category: "Arcade", + duration: "30 seconds", + tags: ["arcade", "reflex", "clicker"], + }, + { + name: "Minesweeper", + path: "games/minesweeper/index.html", + icon: "๐Ÿ’ฃ", + description: + "A classic Minesweeper game. Clear the grid without detonating mines. Numbers indicate how many mines are adjacent to a square.", + category: "Puzzle", + duration: "Unlimited", + tags: ["puzzle", "logic", "grid", "strategy"], + }, + + { + name: "Catch The Dot", // Updated name based on folder + path: "games/Catch_The_Dot/index.html", + icon: "โšซ", + description: + "Test your reflexes! Click the moving dot as many times as you can before time runs out.", + category: "Reflex / Skill", + duration: "30 seconds per round", + tags: ["single player", "reaction", "fast-paced", "matte UI"], + }, + { + name: "Emoji Pop Quiz", + path: "games/Emoji Pop Quiz/index.html", // Updated path + icon: "๐Ÿ“", + description: + "A fun and interactive quiz game. Guess the word, phrase, movie, or song from emoji combinations. Challenges your emoji interpretation skills and provides instant feedback.", + category: "Puzzle", + duration: "Unlimited", + tags: ["puzzle", "quiz", "emoji", "brain-teaser", "logic"], + }, + + { + name: "Rock Paper Scissors", + path: "games/rock-paper-scissors/index.html", + icon: "โœŠ๐Ÿ“„โœ‚๏ธ", + description: + "Classic hand game โ€” challenge the computer in a best-of-three Rock, Paper, Scissors match.", + category: "Strategy / Fun", + duration: "1โ€“2 min", + tags: ["fun", "strategy", "classic", "singleplayer"], + }, + + { + name: "Meme Generator", // Updated name based on folder + path: "games/meme_generator/index.html", + icon: "๐Ÿ˜‚", + description: + "Get your daily dose of memes! Fetch random memes dynamically from the API.", + category: "Fun / Entertainment", + duration: "Unlimited", + tags: ["single player", "dynamic content", "API-driven", "fun"], + }, + { + name: "Find the Hidden Object", + path: "games/find-hidden-object/index.html", + icon: "๐Ÿ”", + description: + "Spot and click hidden items in cluttered scenes before time runs out!", + category: "Puzzle", + duration: "60 seconds", + tags: ["puzzle", "hidden", "seek", "timed", "casual"], + }, + + { + name: "Color Guessing Game", + path: "games/color-guessing-game/index.html", + icon: "๐ŸŽจ", + description: + "Guess the correct color based on the RGB value shown โ€” test your eyes and reflexes!", + category: "Puzzle", + duration: "30 seconds", + tags: ["puzzle", "color", "rgb", "reflex", "visual"], + }, + + { + name: "Click Combo Quiz", // Updated name based on folder + path: "games/click-combo-quiz/index.html", // Updated path (was click_combo_game_quiz) + icon: "โšก", + description: + "Speed + knowledge challenge! Click the correct answers to build combos and score high.", + category: "Arcade / Quiz", + duration: "Timed", + tags: ["quiz", "combo", "reaction", "clicker", "fast"], + }, + + { + name: "Number Gussing Game", // Updated name based on folder + path: "games/Number_Gussing_game/NGG.html", + icon: "๐Ÿค“", + description: "Guess the number in lowest time", + category: "Fun / Entertainment", + duration: "Unlimited", + tags: ["single player", "Solo", "Numbers", "fun"], + }, + { + name: "Word Scramble", + path: "games/word-scramble/index.html", + icon: "๐Ÿ”ค", + description: "Unscramble letters to form words before time runs out!", + category: "Puzzle", + duration: "Variable", + tags: ["puzzle", "word", "timer", "logic"], + }, + { + name: "Link Game", + path: "games/link-game/index.html", + icon: "๐Ÿ”—", + description: + "Connect matching tiles before you run out of moves! A fun logic puzzle for quick thinkers.", + category: "Puzzle", + duration: "Unlimited", + tags: ["arcade", "reflex", "clicker", "speed"], + }, + { + name: "Number Guessing Game", // Kept specific name due to folder name difference + path: "games/Number_Guessing_Game/index.html", // Updated path + icon: "๐Ÿค“", + description: "Guess the secret number in the lowest number of tries!", + category: "Fun / Entertainment", + duration: "Unlimited", + tags: ["numbers", "solo", "fun"], + }, + { + name: "Sudoku", // Updated name + path: "games/sudoku/index.html", + icon: "๐Ÿคฏ", + description: "Use logic to fill the grid and solve the puzzle!", + category: "Classic / Skill", + duration: "Unlimited", + tags: ["singleplayer", "numbers", "logic", "brain"], + }, + { + name: "Coin Toss Simulator", + path: "games/coin_toss_simulator/index.html", + icon: "๐Ÿช™", + description: "A simple coin toss simulator. Will it be heads or tails?", + category: "Fun / Simulation", + duration: "Unlimited", + tags: ["single player", "fun", "simulation"], + }, + { + name: "Fruit Slicer", + path: "games/fruit-slicer/index.html", + icon: "๐ŸŽ", + description: + "Form a line of four of your own coloured discs - Outsmart your opponent", + category: "Strategy", + duration: "5-10 min", + tags: ["two-player", "grid", "classic"], + }, + { + name: "Hangman", + path: "games/hangman/index.html", + icon: "๐Ÿ—๏ธ", + description: + "Guess the word before you run out of attempts! Can you save the stickman?", + category: "Puzzle", + duration: "Unlimited", + tags: ["puzzle", "word", "logic", "guessing"], + }, + { + name: "Frogger", + path: "games/Frogger/index.html", // Updated path + icon: "๐Ÿธ", + description: + "Classic arcade game where you guide a frog across roads and rivers, avoiding obstacles and reaching safe zones.", + category: "Arcade", + duration: "Unlimited", + tags: ["arcade", "reaction", "strategy", "reflex"], + }, + { + name: "8 Ball Pool", // Updated name + path: "games/8-ball-pool/index.html", + icon: "๐ŸŽฑ", + description: + "Realistic local 2-player 8-ball pool with cue aiming, power meter and physics using Canvas.", + category: "Arcade", + duration: "5-15 minutes", + tags: ["arcade", "multiplayer", "physics", "canvas"], + }, + { + name: "Tiny Fishing", + path: "games/tiny-fishing/index.html", + icon: "๐ŸŽฃ", + description: + "Cast your line, catch fish, and upgrade your gear! A relaxing fishing challenge built with Canvas.", category: "Arcade", duration: "Endless", - tags: ["arcade", "click", "fun", "score"], - } -]; + tags: ["arcade", "fishing", "canvas", "upgrade", "relaxing"], + }, + { + name: "Grass Defense", + path: "games/grass-defense/index.html", + icon: "๐ŸŒฟ", + description: + "Strategic tower defense! Place plants to defend your garden from pests.", + category: "Strategy", + duration: "Wave-based", + tags: ["strategy", "defense", "canvas", "logic"], + }, + { + name: "Quote Generator", + path: "games/quote/index.html", + icon: "๐Ÿ—ƒ๏ธ", + description: "Generate your random quote", + category: "Simple", + duration: "Unlimited", + tags: ["single-player", "quote", "classic"], + }, + { + name: "Color Clicker", + path: "games/color-clicker/index.html", + icon: "๐ŸŽจ", + description: + "Click the color box as fast as you can to score points! Every click changes the color, testing your speed and focus.", + category: "Arcade / Reflex", + duration: "Endless", + tags: ["reflex", "clicker", "solo", "color"], + }, + { + name: "Odd One Out", + path: "games/odd-one-out/index.html", + icon: "๐Ÿ”", + description: + "Find the odd emoji/ odd-coloured tile out from a group of similar ones!", + category: "Puzzle", + duration: "1 min", + tags: ["single player", "puzzle", "emoji", "fun"], + }, + { + name: "Tap the Bubble", + path: "games/tap-the-bubble/index.html", + icon: "๐Ÿซง", + description: + "Tap the bubbles as they appear to score points! How many can you pop?", + category: "Arcade / Reflex", + duration: "Endless", + tags: ["reflex", "clicker", "solo", "bubble"], + }, + { + name: "Pixel Art Creator", + path: "games/pixel-art-creator/index.html", + icon: "๐ŸŽจ", + description: + "Create beautiful pixel art on a 16x16 grid! Choose colors, draw, save your creations, and export as images.", + category: "Creative", + duration: "Unlimited", + tags: ["art", "creative", "pixel", "drawing", "solo"], + }, + { + name: "Word Chain Puzzle", + path: "games/word-chain-puzzle/index.html", + icon: "๐Ÿ”—", + description: + "Build chains of related words in different categories with difficulty levels! Start with a word and find the next one starting with the last letter. Features hints, sound effects, and high score tracking.", + category: "Puzzle", + duration: "1 min rounds", + tags: [ + "puzzle", + "words", + "vocabulary", + "timed", + "brain-teaser", + "difficulty", + "hints", + ], + }, + { + name: "Starry Night Sky", + path: "games/starry-night-sky/index.html", + icon: "๐ŸŒŒ", + description: + "A relaxing meditation game where you connect stars to form constellations. Features ambient music, breathing guide, and achievement system for mindful star gazing.", + category: "Relaxation", + duration: "Unlimited", + tags: [ + "relaxation", + "meditation", + "stars", + "constellations", + "mindfulness", + "ambient", + "breathing", + ], + }, + { + name: "Precision Archer", + path: "games/precision-archer/index.html", + icon: "๐Ÿน", + description: + "Test your aiming skills in this physics-based archery game! Adjust power and angle, account for wind effects, and hit moving targets for maximum points.", + category: "Skill", + duration: "10-15 min", + tags: [ + "archery", + "physics", + "precision", + "aiming", + "wind", + "targets", + "skill-based", + ], + }, + { + name: "Color Switch", + path: "games/color-switch/index.html", + icon: "๐ŸŽจ", + description: + "A fast-paced color-matching platformer! Switch the ball's color to match rotating platforms and survive as long as possible. Features smooth animations and increasing difficulty.", + category: "Arcade", + duration: "Endless", + tags: [ + "color-matching", + "platformer", + "timing", + "reflexes", + "endless", + "mobile-friendly", + ], + }, + { + name: "ShadowShift", + path: "games/shadowshift/index.html", + icon: "๐Ÿ•ถ๏ธ", + description: + "Stealth puzzle: drag blocker panels to reshape shadows and sneak past lights to the goal.", + category: "Puzzle", + duration: "3-5 min", + tags: ["single player", "stealth", "puzzle", "logic", "drag-and-drop"], + }, + // --- Added Games Below --- + { + name: "Peglinko", + path: "games/peglinko/index.html", // Corrected path from ' peglinko' + icon: "๐ŸŽฏ", + description: + "Drop balls and watch them bounce through pegs to score points.", + category: "Arcade", + duration: "Endless", + tags: ["physics", "luck", "casual"], + }, + { + name: "Asteroids", + path: "games/asteroids/index.html", + icon: "โ˜„๏ธ", + description: + "Blast asteroids and avoid collisions in this classic space shooter.", + category: "Arcade", + duration: "Endless", + tags: ["space", "shooter", "retro"], + }, + { + name: "Breakout", + path: "games/breakout/index.html", + icon: "๐Ÿงฑ", + description: "Use a paddle to break bricks with a bouncing ball.", + category: "Arcade", + duration: "Level-based", + tags: ["classic", "paddle", "retro"], + }, + { + name: "Bubble Shooter", + path: "games/bubble-shooter/index.html", + icon: "๐Ÿซง", + description: "Shoot bubbles to match colors and clear the board.", + category: "Puzzle", + duration: "Level-based", + tags: ["matching", "shooter", "puzzle"], + }, + { + name: "Color Squid Puzzle", + path: "games/color-squid-puzzle/index.html", + icon: "๐Ÿฆ‘", + description: "Solve color-based puzzles in sequence.", + category: "Puzzle", + duration: "Level-based", + tags: ["color", "sequence", "logic"], + }, + { + name: "Cozy Blocks", + path: "games/cozy-blocks/index.html", + icon: "๐Ÿงฑ", + description: "Stack blocks as high as you can.", + category: "Arcade", + duration: "Endless", + tags: ["stacking", "timing", "casual"], + }, + { + name: "Dodge the Blocks", + path: "games/Dodge-the-blocks/index.html", + icon: "๐Ÿงฑ", + description: "Move left and right to dodge falling blocks.", + category: "Arcade", + duration: "Endless", + tags: ["reflex", "dodging", "simple"], + }, + { + name: "Doodle Jump", + path: "games/doodle-jump/index.html", + icon: "โฌ†๏ธ", + description: "Jump up platforms endlessly.", + category: "Arcade", + duration: "Endless", + tags: ["jumping", "platformer", "casual"], + }, + { + name: "Fruit Catcher", + path: "games/fruit_catcher/index.html", + icon: "๐ŸŽ", + description: "Catch falling fruits in your basket.", + category: "Arcade", + duration: "Lives-based", + tags: ["catching", "reflex", "casual"], + }, + { + name: "Fruits", + path: "games/fruits/index.html", + icon: "๐Ÿ“", + description: "Control a basket to catch falling fruits.", + category: "Arcade", + duration: "Lives-based", + tags: ["catching", "arrows", "simple"], + }, + { + name: "Island Survival", + path: "games/island-survival/index.html", + icon: "๐Ÿ๏ธ", + description: "Make choices to survive on a deserted island.", + category: "Simulation", + duration: "Turns", + tags: ["survival", "text-based", "choices"], + }, + { + name: "Line Game", + path: "games/line-game/index.html", + icon: "โšก", + description: "Avoid falling obstacles while moving your line.", + category: "Arcade", + duration: "Endless", + tags: ["dodging", "reflex", "minimalist"], + }, + { + name: "Maiolike Block Puzzle", + path: "games/maiolike-block-puzzle/index.html", + icon: "๐Ÿงฉ", + description: "Fit Tetris-like blocks onto a grid.", + category: "Puzzle", + duration: "Endless", + tags: ["puzzle", "blocks", "grid"], + }, + { + name: "Merge Lab", + path: "games/merge-lab/index.html", + icon: "๐Ÿงช", + description: "Merge items of the same level to create higher levels.", + category: "Puzzle", + duration: "Endless", + tags: ["merge", "puzzle", "casual"], + }, + { + name: "Minesweeper Clone", + path: "games/minesweeper-clone/index.html", + icon: "๐Ÿ’ฃ", + description: "A clone of the classic Minesweeper game.", + category: "Puzzle", + duration: "Variable", + tags: ["logic", "classic", "grid"], + }, + { + name: "Mole Whacking", // Used folder name + path: "games/mole whacking/index.html", + icon: "๐Ÿ”จ", + description: "Whack moles as they pop out of holes.", + category: "Arcade", + duration: "Timed", + tags: ["reflex", "timed", "clicker"], + }, + { + name: "Rock Scissor Paper", // Used folder name + path: "games/Rock_Scissor_Paper/index.html", + icon: "โœŠโœ‹โœŒ๏ธ", + description: "Play Rock Paper Scissors against the computer.", + category: "Strategy / Fun", + duration: "Rounds", + tags: ["classic", "luck", "simple"], + }, + { + name: "Shadow Catcher", + path: "games/shadow-catcher/index.html", + icon: "๐Ÿ‘ป", + description: "Identify the object casting the moving shadow.", + category: "Puzzle", + duration: "Rounds", + tags: ["visual", "puzzle", "timed"], + }, + { + name: "Stack Tower", + path: "games/stack-tower/index.html", + icon: "๐Ÿ—ผ", + description: "Stack blocks perfectly to build the tallest tower.", + category: "Arcade", + duration: "Endless", + tags: ["stacking", "timing", "precision"], + }, + { + name: "Tetris", + path: "games/tetris/index.html", + icon: "๐Ÿงฑ", + description: "Classic Tetris block stacking game.", + category: "Puzzle", + duration: "Endless", + tags: ["classic", "blocks", "puzzle"], + }, + { + name: "TileMan.io", + path: "games/tileman/index.html", + icon: "๐ŸŸฉ", + description: "Claim tiles by moving around the grid, avoid enemies.", + category: "Arcade", + duration: "Endless", + tags: ["io-game", "territory", "multiplayer-sim"], + }, + { + name: "Tower Defense", + path: "games/tower-defense/index.html", + icon: "๐Ÿฐ", + description: "Place towers to defend against waves of enemies.", + category: "Strategy", + duration: "Wave-based", + tags: ["strategy", "defense", "towers"], + }, + { + name: "Two Zero Four Eight", // Used folder name + path: "games/two_zero_four_eight/index.html", + icon: "๐Ÿ”ข", + description: "Slide and merge tiles to reach the 2048 tile.", + category: "Puzzle", + duration: "Variable", + tags: ["puzzle", "numbers", "logic"], + }, + { + name: "Words of Wonders", + path: "games/words-of-wonders/index.html", + icon: "๐Ÿงฉ", + description: "Form words from given letters to fill a crossword.", + category: "Puzzle", + duration: "Level-based", + tags: ["word", "puzzle", "crossword"], + }, + { + name: "Worlds Easiest Game", + path: "games/worlds-easiest-game/index.html", + icon: "๐Ÿ˜œ", + description: "A deceptively simple (or maybe just simple?) game.", + category: "Fun", + duration: "Instant", + tags: ["simple", "fun", "casual"], + }, + { + name: "Tap Reveal", + path: "games/tap-reveal/index.html", + icon: "โ“", + description: "Tap cards to reveal and match pairs.", + category: "Memory", + duration: "Timed", + tags: ["memory", "matching", "puzzle"], + }, + // Newly added games from the folder list + { + name: "Blink Catch", + path: "games/blink-catch/index.html", + icon: "โšก", + description: "Click the blinking icon as fast as you can before it moves!", + category: "Reflex", + duration: "Endless", + tags: ["reflex", "speed", "clicker"], + }, + { + name: "Boom", + path: "games/boom/index.html", + icon: "๐Ÿ’ฃ", + description: "Click the bombs before they explode!", + category: "Arcade", + duration: "Level-based", + tags: ["arcade", "clicker", "reflex"], + }, + { + name: "Burger Builder", + path: "games/burger-builder/index.html", + icon: "๐Ÿ”", + description: "Assemble the burger ingredients in the correct order.", + category: "Puzzle", + duration: "Quick", + tags: ["puzzle", "food", "sequence"], + }, + { + name: "Catch the Ball", + path: "games/catch-the-ball/index.html", + icon: "๐ŸŽฏ", + description: "Click the moving ball to score points.", + category: "Arcade", + duration: "Timed", + tags: ["arcade", "reflex", "clicker"], + }, + { + name: "Catch the Falling Emoji", + path: "games/catch-the-falling-emoji/index.html", + icon: "๐ŸŽฏ", + description: "Catch the target emoji falling from the sky.", + category: "Arcade", + duration: "Lives-based", + tags: ["arcade", "catching", "reflex", "emoji"], + }, + { + name: "Catch the Stars", + path: "games/catch-the-stars/index.html", + icon: "๐ŸŒŸ", + description: "Control a catcher to collect falling stars.", + category: "Arcade", + duration: "Lives-based", + tags: ["arcade", "catching", "reflex", "stars"], + }, + { + name: "Clicker Farmer", + path: "games/Clicker Farmer/index.html", + icon: "๐Ÿง‘โ€๐ŸŒพ", + description: "Plant crops, harvest them, and expand your farm.", + category: "Simulation", + duration: "Endless", + tags: ["clicker", "farming", "simulation", "idle"], + }, + { + name: "Color Catch", + path: "games/color catch/index.html", + icon: "๐ŸŽฏ", + description: "Click circles matching the target color.", + category: "Arcade", + duration: "Timed", + tags: ["arcade", "reflex", "color", "clicker"], + }, + { + name: "Color Merge", + path: "games/color-merge/index.html", + icon: "๐ŸŽจ", + description: "Merge blocks of the same color to reach the target.", + category: "Puzzle", + duration: "Endless", + tags: ["puzzle", "merge", "color", "casual"], + }, + { + name: "Connect Four", + path: "games/Connect-four/index.html", + icon: "๐Ÿ”ด๐ŸŸก", + description: "Drop discs to connect four in a row.", + category: "Strategy", + duration: "Variable", + tags: ["strategy", "two-player", "classic", "grid"], + }, + { + name: "Emoji Connect", + path: "games/emoji-connect/index.html", + icon: "๐Ÿงฉ", + description: "Connect matching emojis without overlapping lines.", + category: "Puzzle", + duration: "Variable", + tags: ["puzzle", "emoji", "connecting", "logic"], + }, + { + name: "Fast Finger Maze", + path: "games/fast-finger-maze/index.html", + icon: "๐Ÿ‘‰", + description: "Navigate a maze using arrow keys before time runs out.", + category: "Arcade", + duration: "Timed", + tags: ["maze", "arcade", "speed", "keyboard"], + }, + { + name: "Logic Grid", + path: "games/logic-grid/index.html", + icon: "๐Ÿงฉ", + description: "Fill the grid based on logical clues.", + category: "Puzzle", + duration: "Variable", + tags: ["puzzle", "logic", "grid", "brain-teaser"], + }, + { + name: "Memory Flip Game", + path: "games/Memory-Game/index.html", + icon: "๐Ÿง ", + description: "Flip cards to find matching emoji pairs.", + category: "Memory", + duration: "Variable", + tags: ["memory", "puzzle", "matching", "emoji"], + }, + { + name: "Number Pop", + path: "games/Number-Pop/index.html", + icon: "#๏ธโƒฃ", + description: "Click only the even numbers before they disappear.", + category: "Arcade", + duration: "Timed", + tags: ["arcade", "reflex", "numbers", "clicker"], + }, + { + name: "Painting Rush", + path: "games/painting-rush/index.html", + icon: "๐ŸŽจ", + description: "Quickly paint the targets before time runs out.", + category: "Arcade", + duration: "Timed", + tags: ["arcade", "speed", "painting", "clicker"], + }, + { + name: "Shape Rotation Puzzle", + path: "games/shape-rotation-puzzle/index.html", + icon: "๐Ÿ”„", + description: "Rotate and place falling shapes (like Tetris).", + category: "Puzzle", + duration: "Endless", + tags: ["puzzle", "blocks", "rotation", "classic"], + }, + { + name: "Star Jump", + path: "games/star-jump/index.html", + icon: "โญ", + description: "Jump between stars, avoid black holes.", + category: "Arcade", + duration: "Endless", + tags: ["arcade", "jumping", "platformer", "space"], + }, + { + name: "Tap Pop Clouds", + path: "games/tap-pop-clouds/index.html", + icon: "โ˜๏ธ", + description: "Tap the rising clouds before they float away.", + category: "Arcade", + duration: "Lives-based", + tags: ["arcade", "clicker", "reflex", "casual"], + }, + { + name: "Tower of Hanoi", + path: "games/tower-of-hanoi/index.html", + icon: "๐Ÿ—ผ", + description: "Move disks between pegs following the rules.", + category: "Puzzle", + duration: "Variable", + tags: ["puzzle", "logic", "strategy", "classic"], + }, + { + name: "Trivia Showdown", + path: "games/trivia-showdown/index.html", + icon: "โ“", + description: "Answer trivia questions from various categories.", + category: "Quiz", + duration: "Variable", + tags: ["quiz", "trivia", "knowledge"], + }, + { + name: "Underwater Diver", + path: "games/underwater-diver/index.html", + icon: "๐Ÿคฟ", + description: "Swim through an underwater world, avoid obstacles, collect pearls, and survive as long as possible.", + category: "Arcade", + duration: "Unlimited", + tags: ["arcade","reflex","action","underwater","diver"], + }, + { + name: "Knife Thrower", + path: "games/knife-thrower/index.html", + icon: "๐Ÿ”ช", + description: "Throw knives at a rotating board. Avoid hitting stuck knives and survive as long as possible!", + category: "Arcade", + duration: "Unlimited", + tags: ["arcade", "reflex", "action", "skill"], + }, + { + name: "Light Orb Quest", + path: "games/light-orb-quest/index.html", + icon: "๐Ÿ”†", + description: "Navigate a glowing orb through dark ruins โ€” light reveals only nearby tiles. Find treasures, avoid traps and solve light-based puzzles.", + category: "Puzzle", + duration: "Varies", + tags: ["puzzle","exploration","stealth","light","arcade"], + }, + { + name: "Mirror Math", + path: "games/mirror-math/index.html", + icon: "๐Ÿชž", + description: "Decode mirrored/rotated arithmetic equations before time runs out. Practice visual rotation, pattern recognition, and quick math.", + category: "Puzzle", + duration: "Varies", + tags: ["puzzle", "math", "visual", "rotation", "brain-train"], +}, +{ + name: "Math Rush โ€” Addition Challenge", + path: "games/math-rush/index.html", + icon: "โž•", + description: "Solve quick addition problems before time runs out! Test your reflexes and accuracy.", + category: "Math", + duration: "10 seconds per round", + tags: ["math", "addition", "speed", "challenge", "fun"], +}, +{ + name: "Blackjack", + path: "games/blackjack/index.html", + description: "A classic card game where the goal is to get closer to 21 than the dealer. Features betting, doubling down, and a polished UI.", + icon: "๐Ÿƒ", + category: "Card Game", + duration: "Endless", + tags: ["Card", "Casino", "Strategy", "Classic"] +}, + { + name: "Bloom Catch", + path: "games/bloom-catch/index.html", + icon: "๐ŸŒธ", + description: "A calm arcade game where soft petals drift down a pastel-gradient sky. Tilt or drag a vase to catch them, collect combos to grow your vase into a blooming plant. Relaxing pace with subtle sound.", + category: "Arcade", + duration: "Unlimited", + tags: ["arcade", "relaxing", "mobile", "canvas", "petals", "combo"], + }, + // Add these objects to the end of the "games" array in the main script.js file + { + name: "Follow the Path", + path: "games/follow-the-path/index.html", + description: "A sleek memory game where players must repeat a progressively longer path on a grid. Features infinite levels and high-score saving.", + icon: "๐Ÿง ", + category: "Puzzle", + duration: "Endless", + tags: ["Memory", "Puzzle", "Logic", "Pattern"] + }, + { + name: "Perfect Aim", + path: "games/perfect-aim/index.html", + description: "A minimalist arcade game of pure timing. Stop a rotating pointer inside a target sector that shrinks with each hit. Features a combo multiplier and 'perfect hit' bonuses.", + icon: "๐ŸŽฏ", + category: "Arcade", + duration: "Endless", + tags: ["Timing", "Reflex", "Arcade", "Skill"] + }, + { + name: "Un-tangle", + path: "games/un-tangle/index.html", + description: "A relaxing yet challenging logic puzzle. Drag the nodes to untangle a web of intersecting lines. Features procedurally generated levels of increasing difficulty.", + icon: "โžฐ", + category: "Puzzle", + duration: "Endless", + tags: ["Puzzle", "Logic", "Relaxing", "Lines"] + }, + { + name: "The Floor is Lava", + path: "games/the-floor-is-lava/index.html", + description: "A fast-paced survival game where you must hop between safe tiles to avoid the rising lava. Features endless gameplay with a unique lava cooldown mechanic.", + icon: "๐Ÿ”ฅ", + category: "Arcade", + duration: "Endless", + tags: ["Survival", "Arcade", "Fast-Paced", "Endless"] + }, + { + name: "Star Collector", + path: "games/star-collector/index.html", + icon: "โญ", + description: "Navigate through space collecting stars while avoiding asteroids. Use arrow keys to move your spaceship and collect as many stars as possible!", + category: "Arcade", + duration: "Endless", + tags: ["space", "arcade", "keyboard", "avoider", "stars"], + }, + { + name: "Bubble Pop Adventure", + path: "games/bubble-pop-adventure/index.html", + icon: "๐Ÿซง", + description: "Pop bubbles of different colors to score points in this relaxing game. Click on floating bubbles to burst them and earn points based on their color!", + category: "Arcade", + duration: "Endless", + tags: ["relaxing", "clicker", "colorful", "casual", "bubble"] + }, +]; const container = document.getElementById("games-container"); const searchInput = document.getElementById("game-search"); @@ -127,6 +1195,15 @@ const clearSearchButton = document.getElementById("clear-search"); const countTargets = document.querySelectorAll("[data-games-count]"); const latestTargets = document.querySelectorAll("[data-latest-game]"); const previewCount = document.querySelector("[data-preview-count]"); +const seeMoreContainer = document.getElementById("see-more-container"); + +//Sorting the game array alphabetically for consistent order +games.sort((a, b) => a.name.localeCompare(b.name)); + +const INIT_GAMES_LIMIT = 9; +const SEE_MORE_INCREMENT = 15; +let displayedGamesCount = 0; +let currGameList = games; const observer = new IntersectionObserver( (entries) => { @@ -136,7 +1213,7 @@ const observer = new IntersectionObserver( observer.unobserve(entry.target); }); }, - { threshold: 0.4 } + { threshold: 0.1 } ); const latestGameName = games.length ? games[games.length - 1].name : "--"; @@ -171,19 +1248,25 @@ if (clearSearchButton) { function renderGames(list) { container.innerHTML = ""; - if (!list.length) { + if (!list.length && displayedGamesCount === 0) { if (emptyState) emptyState.hidden = false; + if (seeMoreContainer) seeMoreContainer.innerHTML = ""; return; } if (emptyState) emptyState.hidden = true; + const fragment = document.createDocumentFragment(); + list.forEach((game, index) => { const card = document.createElement("article"); card.className = "game-card"; card.tabIndex = 0; card.dataset.name = game.name.toLowerCase(); - card.style.setProperty("--stagger", `${index * 60}ms`); + card.style.setProperty( + "--stagger", + `${(displayedGamesCount + index) * 30}ms` + ); card.innerHTML = `
      @@ -197,7 +1280,9 @@ function renderGames(list) {
      `; @@ -207,25 +1292,76 @@ function renderGames(list) { card.addEventListener("keydown", (event) => { if (event.key !== "Enter" && event.key !== " ") return; event.preventDefault(); + const gameName = card + .querySelector(".card-title") + ?.textContent.replace(game.icon, "") + .trim(); + trackGamePlay(gameName); window.open(game.path, "_blank", "noopener,noreferrer"); }); + card.querySelector(".play-button")?.addEventListener("click", () => { + const gameName = card + .querySelector(".card-title") + ?.textContent.replace(game.icon, "") + .trim(); + trackGamePlay(gameName); + }); - container.appendChild(card); + fragment.appendChild(card); observer.observe(card); }); + container.appendChild(fragment); + + displayedGamesCount += list.length; + updateSeeMore(list); +} + +function displayGames(reset = false) { + if (reset) { + container.innerHTML = ""; + displayedGamesCount = 0; + } + const nextGames = currGameList.slice( + displayedGamesCount, + displayedGamesCount === 0 + ? INIT_GAMES_LIMIT + : displayedGamesCount + SEE_MORE_INCREMENT + ); + renderGames(nextGames); +} + +function updateSeeMore() { + if (seeMoreContainer) { + seeMoreContainer.innerHTML = ""; + if (displayedGamesCount < currGameList.length) { + const btn = document.createElement("button"); + btn.className = "cta-button see-more-button"; + btn.textContent = "See More Games"; + btn.id = "see-more-btn"; + btn.addEventListener("click", () => { + displayGames(false); + }); + seeMoreContainer.appendChild(btn); + } + } +} + +if (searchInput) { + searchInput.addEventListener("input", () => { + currGameList = filterGames(searchInput.value); + displayGames(true); + }); } +currGameList = games; +displayGames(true); + function filterGames(rawTerm) { const term = rawTerm.trim().toLowerCase(); if (!term) return games; return games.filter((game) => { - const haystack = [ - game.name, - game.category, - game.description, - ...game.tags, - ] + const haystack = [game.name, game.category, game.description, ...game.tags] .join(" ") .toLowerCase(); return haystack.includes(term); @@ -268,3 +1404,66 @@ function animateCount(node, target, duration) { function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); } + +// Theme Toggle Logic +const themeToggleBtn = document.getElementById("themeToggle"); +const appBody = document.body; +const themeKey = "theme-preference"; + +function applyTheme(theme) { + appBody.classList.toggle("light-theme", theme === "light"); + + if (themeToggleBtn) { + themeToggleBtn.innerHTML = theme === "light" ? "โ˜€๏ธ" : "๐ŸŒ™"; + } + + try { + localStorage.setItem(themeKey, theme); + } catch (e) { + console.warn("Could not save theme preference to localStorage."); + } +} +const savedTheme = localStorage.getItem(themeKey) || "dark"; // Default to dark +applyTheme(savedTheme); + +if (themeToggleBtn) { + themeToggleBtn.addEventListener("click", () => { + const isLight = appBody.classList.contains("light-theme"); + applyTheme(isLight ? "dark" : "light"); + }); +} + +// Scroll Button Logic +const scrollTopBtn = document.getElementById("scroll-top"); +const scrollBottomBtn = document.getElementById("scroll-bottom"); +let lastScrollTop = 0; + +window.addEventListener( + "scroll", + () => { + const st = window.pageYOffset || document.documentElement.scrollTop; + const scrollHeight = document.documentElement.scrollHeight; + const clientHeight = document.documentElement.clientHeight; + + // Show/Hide buttons based on scroll position + if (scrollTopBtn) scrollTopBtn.style.display = st > 100 ? "block" : "none"; + if (scrollBottomBtn) + scrollBottomBtn.style.display = + st + clientHeight < scrollHeight - 100 ? "block" : "none"; + + lastScrollTop = st <= 0 ? 0 : st; // For Mobile or negative scrolling + }, + false +); + +if (scrollTopBtn) { + scrollTopBtn.addEventListener("click", () => { + window.scrollTo({ top: 0, behavior: "smooth" }); + }); +} + +if (scrollBottomBtn) { + scrollBottomBtn.addEventListener("click", () => { + window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }); + }); +} diff --git a/style.css b/style.css index de153053..f2ed7f66 100644 --- a/style.css +++ b/style.css @@ -12,12 +12,116 @@ --text-300: #c9d1d9; --text-500: #94a3b8; --glass-border: rgba(148, 163, 184, 0.12); + --ui-border: rgba(255,255,255,0.22); /* stronger white border in dark mode */ --radius-lg: 24px; --radius-md: 16px; --radius-sm: 10px; --shadow-soft: 0 18px 40px rgba(3, 5, 12, 0.38); --font-base: 'Poppins', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } +body.light-theme { + --bg-900: #f9fafb; + --bg-800: #ffffff; + --bg-700: #f1f5f9; + --bg-600: #e2e8f0; + --primary: #2563eb; + --primary-soft: rgba(37, 99, 235, 0.18); + --accent: #9333ea; + --accent-soft: rgba(147, 51, 234, 0.14); + --text-100: #0f172a; + --text-300: #334155; + --text-500: #64748b; + --glass-border: rgba(148, 163, 184, 0.25); + --ui-border: rgba(10,10,10,0.12); + background: radial-gradient(circle at top left, #f1f5f9, #e2e8f0); + --border-color: rgba(37, 99, 235, 0.3); + color-scheme: light; +} +/* =========================== + ๐ŸŽฎ GAME & PREVIEW CARDS +=========================== */ + +.game-card, +.preview-card, +.hero__stats div { + background: rgba(25, 28, 45, 0.85); /* deep bluish-gray */ + backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); + transition: background 0.3s ease, box-shadow 0.3s ease; +} + +/* Slight hover glow for interaction */ +.game-card:hover, +.preview-card:hover { + background: rgba(35, 40, 65, 0.9); + box-shadow: 0 12px 28px rgba(0, 0, 0, 0.35); +} +body.light-theme .game-card, +body.light-theme .hero__nav, +body.light-theme .preview-card, +body.light-theme .hero__stats div, +body.light-theme .logo, +body.light-theme .pro-badges, +body.light-theme .hub__controls, +body.light-theme .search{ + background: rgba(255, 255, 255, 0.85); + border: 1px solid rgba(0, 0, 0, 0.65); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.18); +} + +body.light-theme .game-card:hover, +body.light-theme .hero__nav:hover, +body.light-theme .preview-card:hover { + background: rgba(250, 250, 255, 1); + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12); +} + +.see-more-container { + display: flex; + justify-content: center; +} + +/* =========================== + ๐Ÿ” SEARCH BAR + EMPTY CARD +=========================== */ + +/* =========================== + ๐ŸŒค๏ธ LIGHT FROSTED CARDS +=========================== */ + +.search, +.hub__controls, +.empty{ + background: rgba(255, 255, 255, 0.75); /* soft white glass */ + border: 1px solid rgba(200, 210, 230, 0.4); /* light border */ + backdrop-filter: blur(25px); + border-radius: 20px; + box-shadow: 0 6px 16px rgba(200, 200, 255, 0.15); /* subtle bluish shadow */ + transition: background 0.3s ease, box-shadow 0.3s ease; +} + +.search:hover, +.hub__controls:hover, +.empty:hover { + background: rgba(255, 255, 255, 0.9); /* lighter on hover */ + box-shadow: 0 10px 24px rgba(190, 200, 255, 0.25); +} + +body:not(.light-theme) .search, +body:not(.light-theme) .hub__controls, +body:not(.light-theme) .empty { + background: rgba(245, 247, 255, 0.1); /* very light bluish glass */ + border: 1px solid rgba(255, 255, 255, 0.05); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25); +} + +body:not(.light-theme) .search:hover, +body:not(.light-theme) .hub__controls:hover, +body:not(.light-theme) .empty:hover { + background: rgba(255, 255, 255, 0.15); +} + * { box-sizing: border-box; @@ -194,7 +298,10 @@ a { margin-top: 0.5rem; } -.hero__eyebrow { +.hero__label { + border-width: 1px; + border-style: solid; + border-color: var(--ui-border); /* Fix applied here by splitting border */ font-size: 0.95rem; font-weight: 600; color: var(--primary); @@ -315,6 +422,12 @@ a { padding: 0.3rem 0.6rem; border-radius: 999px; box-shadow: 0 6px 16px rgba(3,6,18,0.28); + border: 1px solid var(--ui-border); + transition: border-color 200ms ease, box-shadow 200ms ease; +} + +.hero__nav:hover { + border-color: rgba(255,255,255,0.36); } .hub { @@ -349,9 +462,15 @@ a { border-radius: 999px; border: 1px solid rgba(88, 166, 255, 0.18); background: rgba(14, 20, 33, 0.85); - color: var(--text-100); + color: var(--text-100); /* This correctly toggles the text color */ font-size: 1rem; - transition: border-color 0.25s ease, box-shadow 0.25s ease; + transition: border-color 0.25s ease, box-shadow 0.25s ease, background 0.3s ease, color 0.3s ease; +} + +body.light-theme .search input { + background: rgba(255, 255, 255, 0.85); /* Light background */ + border-color: rgba(0, 0, 0, 0.1); /* Light border */ + /* color: var(--text-100) is inherited and works perfectly */ } .search input:focus-visible { @@ -647,5 +766,244 @@ a { .hub__meta { width: 100%; justify-content: space-between; + + inset: 0; + z-index: -2; + animation: bg-move 18s linear infinite alternate; + pointer-events: none; +} +} + +@keyframes bg-move { + 0% { background-position: 0% 50%; } + 100% { background-position: 100% 50%; } +} + +/* Glassmorphism for cards and controls */ +.game-card, +.hub__controls, +.empty { + backdrop-filter: blur(18px) saturate(1.2); + background: linear-gradient(135deg, rgba(18,30,52,0.92) 60%, rgba(88,166,255,0.10) 100%); + border: 1.5px solid rgba(88,166,255,0.18); + box-shadow: 0 8px 32px rgba(88,166,255,0.10), var(--shadow-soft); +} + +/* Card hover: scale up and glow */ +.game-card:hover, +.game-card:focus-within { + transform: perspective(900px) rotateX(var(--tiltX, 0deg)) rotateY(var(--tiltY, 0deg)) scale(1.04); + box-shadow: 0 0 0 3px var(--primary), 0 26px 44px rgba(27,46,86,0.5); + border-color: var(--primary); +} + +/* CTA button: vibrant accent and glow */ +.cta-button { + background: linear-gradient(135deg, #58a6ff 0%, #a855f7 100%); + color: #fff; + box-shadow: 0 0 0 3px rgba(168,85,247,0.18), 0 14px 30px rgba(88,166,255,0.25); + border: none; +} + +.cta-button:hover { + box-shadow: 0 0 0 6px rgba(168,85,247,0.22), 0 18px 36px rgba(88,166,255,0.35); + filter: brightness(1.08) saturate(1.2); +} + +/* Play button: animated border glow */ +.play-button { + position: relative; + z-index: 1; + background: linear-gradient(135deg, #2dd4bf 80%, #3b82f6 100%); + color: #fff; + box-shadow: 0 0 0 3px rgba(45,212,191,0.18), 0 14px 32px rgba(45,212,191,0.22); + border: none; + overflow: hidden; +} + +.play-button::after { + content: ""; + position: absolute; + inset: -4px; + border-radius: 999px; + background: linear-gradient(90deg, #58a6ff, #a855f7, #2dd4bf, #3b82f6, #58a6ff); + opacity: 0.7; + z-index: -1; + filter: blur(8px); + animation: border-glow 3s linear infinite; +} + +@keyframes border-glow { + 0% { background-position: 0% 50%; } + 100% { background-position: 100% 50%; } +} + +/* Logo: glowing border on hover */ +.logo { + box-shadow: 0 8px 24px rgba(3,7,18,0.4), 0 0 0 0px var(--primary); + transition: box-shadow 0.3s; +} + +.logo:hover { + box-shadow: 0 12px 28px rgba(6,11,25,0.5), 0 0 0 4px var(--primary); +} + +/* Search input: pop focus ring */ +.search input:focus-visible { + background: linear-gradient(90deg, rgba(88,166,255,0.18), rgba(168,85,247,0.14)); + color: #a855f7; + border-color: #a855f7; +} + +/* Subtle text glow for h1 */ +.hero h1 { + text-shadow: 0 2px 16px rgba(88,166,255,0.18), 0 1px 0 #222; +} +/* ===================== + ๐ŸŒ— THEME TOGGLE BUTTON +===================== */ +.theme-toggle { + background: rgba(14, 20, 33, 0.85); + border: 1px solid rgba(88, 166, 255, 0.3); + color: var(--text-100); + font-size: 1.2rem; + border-radius: 50%; + width: 42px; + height: 42px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); +} +body.light-theme .theme-toggle { + background: rgba(245, 247, 255, 0.85); + border: 1px solid rgba(37, 99, 235, 0.3); + color: var(--text-100); +} + +.theme-toggle:hover { + opacity: 0.85; + background: rgb(23, 34, 58, 0.85); + color: #020305; + transform: scale(1.05); + transform: translateY(-3px); +} + +/* =========================== + ๐ŸŒŸ PRO PLAYER BADGES +=========================== */ +.pro-badges { + margin-bottom: 2rem; + padding: 1.5rem; + border-radius: var(--radius-lg); + background: rgba(16, 23, 39, 0.72); + border: 1px solid var(--glass-border); + box-shadow: var(--shadow-soft); + backdrop-filter: blur(18px) saturate(1.2); +} + +.pro-badges h2 { + font-size: 1.5rem; + margin-bottom: 1rem; + color: var(--primary); + text-shadow: 0 1px 6px rgba(88, 166, 255, 0.25); +} + +.pro-badges-container { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +.pro-badge { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.8rem; + border-radius: var(--radius-md); + background: linear-gradient(145deg, rgba(88, 166, 255, 0.18), rgba(168, 85, 247, 0.14)); + border: 1px solid rgba(88, 166, 255, 0.25); + color: var(--text-100); + font-weight: 600; + font-size: 0.95rem; + box-shadow: 0 4px 12px rgba(88, 166, 255, 0.2); + transition: transform 0.25s ease, box-shadow 0.25s ease; + cursor: default; +} + +.pro-badge:hover { + transform: translateY(-2px) scale(1.02); + box-shadow: 0 6px 18px rgba(88, 166, 255, 0.35); +} + +.pro-badge__icon { + font-size: 1.2rem; + color: #ffd700; /* gold star */ +} + +.pro-badge__name { + font-weight: 600; +} + +.pro-badge__count { + font-size: 0.85rem; + color: var(--text-300); + background: rgba(88, 166, 255, 0.12); + padding: 0.15rem 0.4rem; + border-radius: 999px; +} + +/* Responsive badges */ +@media (max-width: 640px) { + .pro-badges-container { + flex-direction: column; + gap: 0.75rem; } -} \ No newline at end of file +} + +/* Scroll to Top/Bottom Buttons */ +.scroll-btn { + display: none; /* Hidden by default, JS will show when needed */ + position: fixed; + right: 2rem; + width: 2.7rem; + height: 2.7rem; + border-radius: 50%; + border: 1px solid var(--ui-border); + background: linear-gradient(135deg, rgba(88,166,255,0.92), rgba(168,85,247,0.85)); + color: #fff; + font-size: 1.7rem; + box-shadow: 0 4px 16px rgba(0,0,0,0.18); + cursor: pointer; + z-index: 100; + opacity: 0.85; + transition: opacity 0.2s, transform 0.2s; +} +.scroll-btn:hover { + opacity: 1; + transform: scale(1.08); +} +.scroll-btn:focus-visible { + outline: none; + box-shadow: 0 0 0 4px rgba(88,166,255,0.12); + border-color: rgba(255,255,255,0.6); +} +#scroll-top { + bottom: 5.5rem; +} +#scroll-bottom { + bottom: 2rem; +} +@media (max-width: 600px) { + .scroll-btn { + right: 1rem; + width: 2.2rem; + height: 2.2rem; + font-size: 1.2rem; + } + #scroll-top { bottom: 4.5rem; } + #scroll-bottom { bottom: 1rem; } +} +