Initial commit: Naddie Jump — Monad Edition
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
.vite
|
||||
*.local
|
||||
.DS_Store
|
||||
.vercel
|
||||
65
AGENTS.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Naddie Jump — Agent Notes
|
||||
|
||||
## Описание проекта
|
||||
Игра на Phaser 3 (Vite-сборка), жанр "прыгалка" (doodle-jump style). Текущее издание — Monad Edition.
|
||||
|
||||
## Технологический стек
|
||||
- **Framework:** Vite 8.x
|
||||
- **Game Engine:** Phaser 3.70.0
|
||||
- **Language:** TypeScript / JavaScript (ES Modules)
|
||||
- **Deploy target:** Vercel (Static + Vite preset)
|
||||
|
||||
## Структура `src/`
|
||||
```
|
||||
src/
|
||||
├── main.js # Точка входа
|
||||
├── config/game.config.js # Глобальные настройки игры
|
||||
├── entities/
|
||||
│ ├── Player.js # Игрок
|
||||
│ ├── Enemy.js # Враги
|
||||
│ ├── Platform.js # Платформы
|
||||
│ ├── PropellerHat.js # Апгрейд: пропеллер
|
||||
│ ├── Rocket.js # Апгрейд: ракета
|
||||
│ └── Spring.js # Апгрейд: пружина
|
||||
├── managers/
|
||||
│ ├── BlockchainManager.js # Web3/блокчейн интеграция
|
||||
│ ├── PlatformManager.js # Генерация и управление платформами
|
||||
│ └── ScoreManager.js # Учёт очков
|
||||
└── scenes/
|
||||
├── BootScene.js # Загрузка ассетов
|
||||
├── MenuScene.js # Главное меню
|
||||
├── GameScene.js # Основной игровой процесс
|
||||
└── GameOverScene.js # Экран поражения
|
||||
```
|
||||
|
||||
## Команды разработки
|
||||
```bash
|
||||
npm install # установка зависимостей
|
||||
npm run dev # локальный dev-сервер
|
||||
npm run build # production-сборка (output: dist/)
|
||||
npm run preview # превью production-сборки локально
|
||||
```
|
||||
|
||||
## Деплой на Vercel
|
||||
- **Проект в Vercel:** `anrils-projects/naddie-jump`
|
||||
- **Production URL:** https://naddie-jump.vercel.app
|
||||
- **Конфигурация:** `vercel.json` (framework: vite, outputDirectory: dist)
|
||||
- **Привязка CLI:** `.vercel/project.json` создан, `.vercel/` добавлен в `.gitignore`
|
||||
|
||||
### ⛔ ВАЖНОЕ ПРАВИЛО — ДЕПЛОЙ
|
||||
- **Текущая версия на Vercel — это prod-альфа.** Она остаётся как есть.
|
||||
- **Вся дальнейшая разработка ведётся ТОЛЬКО локально.**
|
||||
- **Деплоить новую версию на Vercel можно ТОЛЬКО по явной команде пользователя.**
|
||||
- Не деплоить самостоятельно, даже если есть токен/доступ.
|
||||
|
||||
## Git
|
||||
- **Remote:** `https://github.com/anril44/sender2.git`
|
||||
- Репозиторий `.git/` находится в корне проекта `naddie-jump/`
|
||||
|
||||
## Зависимости
|
||||
- `phaser` — игровой движок
|
||||
- `vite` — сборщик (devDependency)
|
||||
- `typescript` — типизация (devDependency)
|
||||
|
||||
## Ассеты
|
||||
Статические ресурсы (спрайты, звуки) хранятся в `public/assets/` и копируются в `dist/` при сборке.
|
||||
105
README.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# 🚀 Naddie Jump — Monad Edition
|
||||
|
||||
A vertical endless jumper inspired by Doodle Jump, themed around the **Monad Blockchain** ecosystem.
|
||||
|
||||
Play as **Naddie**, the purple Monad mascot, and jump your way up through blockchain blocks while avoiding bugs, high gas fees, and collecting power-ups like the Propeller Hat and Fast Transaction Rocket!
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🎮 Features
|
||||
|
||||
- **Classic Doodle Jump gameplay** with blockchain twist
|
||||
- **4 platform types**: Stable, Pending (moving), Reverted (breaking), Genesis (gold 2× multiplier)
|
||||
- **3 power-ups**: Spring (super jump), Propeller Hat, Rocket Ride
|
||||
- **2 enemy types**: Software Bugs and High Gas Fee traps
|
||||
- **Combo system**: Chain jumps for score multipliers up to ×3
|
||||
- **Milestones**: Visual celebration every 100 blocks ("New Epoch!")
|
||||
- **Adaptive difficulty**: Gaps widen and enemies appear more often as you climb
|
||||
- **Touch & keyboard controls**: Arrow keys, WASD, or tap left/right sides
|
||||
- **Web3-ready architecture**: `BlockchainManager` stub ready for Monad integration
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Tech Stack
|
||||
|
||||
| Layer | Tech |
|
||||
|-------|------|
|
||||
| Engine | Phaser 3.70 |
|
||||
| Build | Vite |
|
||||
| Language | JavaScript (ES6+) |
|
||||
| Future Blockchain | Monad EVM + Viem/Ethers |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open `http://localhost:5173` in your browser.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
scenes/
|
||||
BootScene.js # Asset loading & atlas generation
|
||||
MenuScene.js # Main menu & leaderboard mock
|
||||
GameScene.js # Core gameplay loop
|
||||
GameOverScene.js # Results & restart
|
||||
entities/
|
||||
Player.js # Naddie character
|
||||
Platform.js # Stable / Moving / Breaking / Genesis
|
||||
Enemy.js # Bugs & Gas Fee traps
|
||||
Spring.js # Super jump pad
|
||||
PropellerHat.js # Fly power-up
|
||||
Rocket.js # Rocket ride power-up
|
||||
managers/
|
||||
PlatformManager.js # Procedural generation
|
||||
ScoreManager.js # Gas Score & Block Height
|
||||
BlockchainManager.js # Web3 abstraction stub
|
||||
config/
|
||||
game.config.js # Balance constants
|
||||
utils/
|
||||
sprite-atlas.js # Sprite sheet coordinates
|
||||
contracts/
|
||||
NaddieJump.sol # Future on-chain leaderboard
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Blockchain Roadmap
|
||||
|
||||
### Phase 1 — Off-chain (Current)
|
||||
- Fully playable browser game
|
||||
- localStorage high scores
|
||||
|
||||
### Phase 2 — On-chain Scores
|
||||
- Deploy `NaddieJump.sol` on Monad testnet
|
||||
- Submit scores via `viem`
|
||||
- On-chain leaderboard
|
||||
|
||||
### Phase 3 — Economy
|
||||
- ERC-20 `$GWEI` token rewards
|
||||
- NFT skins for Naddie
|
||||
- Daily on-chain tournaments
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Credits
|
||||
|
||||
- Character & art concept: user-provided sprite sheet
|
||||
- Engine: [Phaser 3](https://phaser.io)
|
||||
- Font: [Press Start 2P](https://fonts.google.com/specimen/Press+Start+2P)
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT — built for the Monad ecosystem.
|
||||
84
contracts/NaddieJump.sol
Normal file
@@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
/**
|
||||
* @title NaddieJump
|
||||
* @notice On-chain leaderboard and reward contract for Naddie Jump game.
|
||||
* @dev Designed for Monad EVM compatibility.
|
||||
*/
|
||||
contract NaddieJump {
|
||||
struct Score {
|
||||
address player;
|
||||
uint256 score;
|
||||
uint256 blockHeight;
|
||||
uint256 timestamp;
|
||||
}
|
||||
|
||||
mapping(address => Score) public bestScores;
|
||||
Score[] public leaderboard;
|
||||
uint256 public constant LEADERBOARD_SIZE = 50;
|
||||
uint256 public constant REWARD_THRESHOLD = 50000;
|
||||
|
||||
event ScoreSubmitted(address indexed player, uint256 score, uint256 blockHeight);
|
||||
event RewardClaimed(address indexed player, uint256 amount);
|
||||
|
||||
function submitScore(uint256 score, uint256 blockHeight) external {
|
||||
require(score > 0, "Invalid score");
|
||||
|
||||
Score storage current = bestScores[msg.sender];
|
||||
if (score > current.score) {
|
||||
current.player = msg.sender;
|
||||
current.score = score;
|
||||
current.blockHeight = blockHeight;
|
||||
current.timestamp = block.timestamp;
|
||||
_updateLeaderboard(current);
|
||||
}
|
||||
|
||||
emit ScoreSubmitted(msg.sender, score, blockHeight);
|
||||
}
|
||||
|
||||
function _updateLeaderboard(Score memory newScore) internal {
|
||||
// Simple insertion sort approach for leaderboard
|
||||
bool exists = false;
|
||||
for (uint256 i = 0; i < leaderboard.length; i++) {
|
||||
if (leaderboard[i].player == newScore.player) {
|
||||
leaderboard[i] = newScore;
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!exists && leaderboard.length < LEADERBOARD_SIZE) {
|
||||
leaderboard.push(newScore);
|
||||
}
|
||||
// Sort descending by score (bubble sort for simplicity in prototype)
|
||||
for (uint256 i = 0; i < leaderboard.length; i++) {
|
||||
for (uint256 j = i + 1; j < leaderboard.length; j++) {
|
||||
if (leaderboard[j].score > leaderboard[i].score) {
|
||||
Score memory tmp = leaderboard[i];
|
||||
leaderboard[i] = leaderboard[j];
|
||||
leaderboard[j] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLeaderboard(uint256 count) external view returns (Score[] memory) {
|
||||
uint256 len = count > leaderboard.length ? leaderboard.length : count;
|
||||
Score[] memory result = new Score[](len);
|
||||
for (uint256 i = 0; i < len; i++) {
|
||||
result[i] = leaderboard[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getPlayerBest(address player) external view returns (Score memory) {
|
||||
return bestScores[player];
|
||||
}
|
||||
|
||||
// Placeholder for future ERC-20 reward integration
|
||||
function claimReward() external view returns (bool eligible) {
|
||||
Score storage s = bestScores[msg.sender];
|
||||
eligible = s.score >= REWARD_THRESHOLD;
|
||||
// TODO: mint/transfer ERC-20 $GWEI tokens
|
||||
}
|
||||
}
|
||||
19
index.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||
<title>Naddie Jump — Monad Edition</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { width: 100%; height: 100%; overflow: hidden; background: #0d001a; }
|
||||
#game-container { width: 100%; height: 100%; }
|
||||
canvas { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="game-container"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
905
package-lock.json
generated
Normal file
@@ -0,0 +1,905 @@
|
||||
{
|
||||
"name": "naddie-jump",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "naddie-jump",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"phaser": "^3.70.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~6.0.2",
|
||||
"vite": "^8.0.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
||||
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
|
||||
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz",
|
||||
"integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "1.9.2",
|
||||
"@emnapi/runtime": "1.9.2",
|
||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.32.0",
|
||||
"lightningcss-darwin-arm64": "1.32.0",
|
||||
"lightningcss-darwin-x64": "1.32.0",
|
||||
"lightningcss-freebsd-x64": "1.32.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||
"lightningcss-linux-x64-musl": "1.32.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
||||
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/phaser": {
|
||||
"version": "3.70.0",
|
||||
"resolved": "https://registry.npmjs.org/phaser/-/phaser-3.70.0.tgz",
|
||||
"integrity": "sha512-2g+gh+Jp9f/Ho9FOXOYbIJMGf3UZXyMbW2iLScFaLQw11e/LqVyxj/YmaBauWbHabeTnZjiWkPklDnxhesMH0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
||||
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.126.0",
|
||||
"@rolldown/pluginutils": "1.0.0-rc.16"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.0-rc.16",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.16",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.16",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.16",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.16",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.16",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.9",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz",
|
||||
"integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.10",
|
||||
"rolldown": "1.0.0-rc.16",
|
||||
"tinyglobby": "^0.2.16"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.1.0",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitejs/devtools": {
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "naddie-jump",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~6.0.2",
|
||||
"vite": "^8.0.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"phaser": "^3.70.0"
|
||||
}
|
||||
}
|
||||
BIN
preview_player_dead.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
preview_player_idle.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
preview_player_propeller.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
preview_player_rocket.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
preview_rocket_buff.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
public/assets/sprites/enemy_bug.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/assets/sprites/naddie-spritesheet.png
Normal file
|
After Width: | Height: | Size: 7.1 MiB |
BIN
public/assets/sprites/platform.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/assets/sprites/player_dead.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
public/assets/sprites/player_idle.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
public/assets/sprites/player_propeller.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
public/assets/sprites/player_rocket.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
public/assets/sprites/propeller_hat.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/assets/sprites/rocket.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
rocket_preview.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
43
src/config/game.config.js
Normal file
@@ -0,0 +1,43 @@
|
||||
export const GAME_WIDTH = 480;
|
||||
export const GAME_HEIGHT = 854;
|
||||
|
||||
export const GRAVITY = 1200;
|
||||
export const JUMP_VELOCITY = -650;
|
||||
export const SUPER_JUMP_VELOCITY = -950;
|
||||
export const ROCKET_VELOCITY = -1400;
|
||||
export const PROPELLER_VELOCITY = -950;
|
||||
|
||||
export const PLAYER_SPEED = 300;
|
||||
export const PLAYER_MAX_SPEED = 400;
|
||||
|
||||
export const PLATFORM_GAP_MIN = 60;
|
||||
export const PLATFORM_GAP_MAX = 120;
|
||||
export const PLATFORM_WIDTH = 80;
|
||||
export const PLATFORM_HEIGHT = 24;
|
||||
|
||||
export const SPAWN_RATES = {
|
||||
stable: 0.60,
|
||||
moving: 0.20,
|
||||
breaking: 0.15,
|
||||
genesis: 0.05,
|
||||
};
|
||||
|
||||
export const POWERUP_RATES = {
|
||||
none: 0.91,
|
||||
spring: 0.04,
|
||||
propeller: 0.025,
|
||||
rocket: 0.015,
|
||||
};
|
||||
|
||||
export const ENEMY_RATES = {
|
||||
none: 0.90,
|
||||
bug: 0.10,
|
||||
};
|
||||
|
||||
export const DIFFICULTY = {
|
||||
initialGap: 100,
|
||||
gapIncreasePer1000: 12,
|
||||
maxGap: 160,
|
||||
enemyIncreasePer1000: 0.015,
|
||||
maxEnemyRate: 0.25,
|
||||
};
|
||||
36
src/entities/Enemy.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Physics } from 'phaser';
|
||||
import { GAME_WIDTH } from '../config/game.config.js';
|
||||
|
||||
export class Enemy extends Physics.Arcade.Sprite {
|
||||
constructor(scene, x, y, type = 'bug') {
|
||||
super(scene, x, y, 'enemy_bug');
|
||||
scene.add.existing(this);
|
||||
scene.physics.add.existing(this);
|
||||
|
||||
this.enemyType = type;
|
||||
this.setScale(0.7);
|
||||
this.body.allowGravity = false;
|
||||
this.body.setSize(90, 80);
|
||||
this.body.setOffset(35, 30);
|
||||
|
||||
if (type === 'bug') {
|
||||
this.speed = Phaser.Math.Between(60, 140) * (Math.random() < 0.5 ? 1 : -1);
|
||||
this.startX = x;
|
||||
this.patrolRange = Phaser.Math.Between(80, 200);
|
||||
}
|
||||
}
|
||||
|
||||
preUpdate(time, delta) {
|
||||
super.preUpdate(time, delta);
|
||||
if (this.enemyType === 'bug') {
|
||||
this.x += this.speed * (delta / 1000);
|
||||
if (Math.abs(this.x - this.startX) > this.patrolRange) {
|
||||
this.speed *= -1;
|
||||
this.setFlipX(this.speed < 0);
|
||||
}
|
||||
// Wrap
|
||||
if (this.x < -60) this.x = GAME_WIDTH + 60;
|
||||
if (this.x > GAME_WIDTH + 60) this.x = -60;
|
||||
}
|
||||
}
|
||||
}
|
||||
80
src/entities/Platform.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Physics } from 'phaser';
|
||||
|
||||
export class Platform extends Physics.Arcade.Sprite {
|
||||
constructor(scene, x, y, type = 'stable') {
|
||||
super(scene, x, y, 'platform');
|
||||
scene.add.existing(this);
|
||||
scene.physics.add.existing(this, true); // static body by default
|
||||
|
||||
this.platformType = type;
|
||||
|
||||
this.breakingState = 0;
|
||||
this.moveSpeed = 0;
|
||||
this.moveRange = 0;
|
||||
this.startX = x;
|
||||
|
||||
if (type === 'moving') {
|
||||
// Convert to dynamic for movement
|
||||
this.body.destroy();
|
||||
this.body = new Phaser.Physics.Arcade.Body(scene.physics.world, this);
|
||||
this.body.allowGravity = false;
|
||||
this.body.immovable = true;
|
||||
this.moveSpeed = Phaser.Math.Between(50, 120) * (Math.random() < 0.5 ? 1 : -1);
|
||||
this.moveRange = Phaser.Math.Between(60, 160);
|
||||
} else if (type === 'breaking') {
|
||||
this.setTint(0x999999); // grey tint for broken look
|
||||
} else if (type === 'genesis') {
|
||||
this.setTint(0xffd700); // gold
|
||||
this.genesisGlow = scene.add.ellipse(x, y + 10, 100, 30, 0xffd700, 0.25)
|
||||
.setDepth(this.depth - 1);
|
||||
scene.tweens.add({
|
||||
targets: this.genesisGlow,
|
||||
scaleX: 1.4,
|
||||
scaleY: 1.4,
|
||||
alpha: 0.1,
|
||||
duration: 800,
|
||||
yoyo: true,
|
||||
repeat: -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
preUpdate(time, delta) {
|
||||
super.preUpdate(time, delta);
|
||||
if (this.platformType === 'moving' && this.body) {
|
||||
this.setVelocityX(this.moveSpeed);
|
||||
if (Math.abs(this.x - this.startX) > this.moveRange) {
|
||||
this.moveSpeed *= -1;
|
||||
}
|
||||
if (this.genesisGlow) this.genesisGlow.setPosition(this.x, this.y + 10);
|
||||
}
|
||||
}
|
||||
|
||||
onPlayerLand(player) {
|
||||
if (this.platformType === 'breaking') {
|
||||
if (this.breakingState > 0) return false;
|
||||
this.breakingState = 2;
|
||||
this.disableBody(true, false);
|
||||
this.scene.tweens.add({
|
||||
targets: this,
|
||||
alpha: 0,
|
||||
scaleX: 0.3,
|
||||
scaleY: 0.1,
|
||||
duration: 250,
|
||||
onComplete: () => this.destroy(),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
isBroken() {
|
||||
return this.breakingState >= 2;
|
||||
}
|
||||
|
||||
destroy(fromScene) {
|
||||
if (this.glowTween) this.glowTween.stop();
|
||||
if (this.genesisGlow) this.genesisGlow.destroy();
|
||||
super.destroy(fromScene);
|
||||
}
|
||||
}
|
||||
117
src/entities/Player.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Physics } from 'phaser';
|
||||
import {
|
||||
GAME_WIDTH,
|
||||
JUMP_VELOCITY,
|
||||
SUPER_JUMP_VELOCITY,
|
||||
ROCKET_VELOCITY,
|
||||
PROPELLER_VELOCITY,
|
||||
PLAYER_SPEED,
|
||||
} from '../config/game.config.js';
|
||||
|
||||
export class Player extends Physics.Arcade.Sprite {
|
||||
constructor(scene, x, y) {
|
||||
super(scene, x, y, 'player_idle');
|
||||
scene.add.existing(this);
|
||||
scene.physics.add.existing(this);
|
||||
|
||||
this.setCollideWorldBounds(false);
|
||||
this.setScale(0.45);
|
||||
this.body.setSize(70, 90);
|
||||
|
||||
this.state = 'normal'; // normal, propeller, rocket, dead
|
||||
this.propellerTimer = 0;
|
||||
this.rocketTimer = 0;
|
||||
}
|
||||
|
||||
update(cursors, wasd, touchLeft, touchRight, time, delta) {
|
||||
if (this.state === 'dead') return;
|
||||
|
||||
let velocityX = 0;
|
||||
if (cursors.left.isDown || wasd.left.isDown || touchLeft) velocityX = -PLAYER_SPEED;
|
||||
if (cursors.right.isDown || wasd.right.isDown || touchRight) velocityX = PLAYER_SPEED;
|
||||
this.setVelocityX(velocityX);
|
||||
|
||||
// Screen wrap
|
||||
if (this.x < -this.width / 2) this.x = GAME_WIDTH + this.width / 2;
|
||||
if (this.x > GAME_WIDTH + this.width / 2) this.x = -this.width / 2;
|
||||
|
||||
// Tilt sprite based on movement
|
||||
if (velocityX < 0) {
|
||||
this.setFlipX(true);
|
||||
this.setAngle(-5);
|
||||
} else if (velocityX > 0) {
|
||||
this.setFlipX(false);
|
||||
this.setAngle(5);
|
||||
} else {
|
||||
this.setAngle(0);
|
||||
}
|
||||
|
||||
// Power-up timers
|
||||
if (this.state === 'propeller') {
|
||||
this.propellerTimer -= delta;
|
||||
this.setVelocityY(PROPELLER_VELOCITY);
|
||||
if (this.propellerTimer <= 0) this.endPowerUp();
|
||||
} else if (this.state === 'rocket') {
|
||||
this.rocketTimer -= delta;
|
||||
this.setVelocityY(ROCKET_VELOCITY);
|
||||
if (this.rocketTimer <= 0) this.endPowerUp();
|
||||
}
|
||||
}
|
||||
|
||||
jump(force = JUMP_VELOCITY) {
|
||||
if (this.state === 'dead' || this.state === 'rocket' || this.state === 'propeller') return;
|
||||
this.setVelocityY(force);
|
||||
// Squash animation
|
||||
this.scene.tweens.add({
|
||||
targets: this,
|
||||
scaleX: 0.55,
|
||||
scaleY: 0.4,
|
||||
duration: 80,
|
||||
yoyo: true,
|
||||
ease: 'Quad.easeOut',
|
||||
});
|
||||
}
|
||||
|
||||
startPropeller() {
|
||||
if (this.state === 'dead') return;
|
||||
this.state = 'propeller';
|
||||
this.propellerTimer = 3500;
|
||||
const oldHeight = this.displayHeight;
|
||||
this.setTexture('player_propeller');
|
||||
this.setScale(0.45);
|
||||
this.body.setSize(70, 105);
|
||||
this.y -= (this.displayHeight - oldHeight) / 2;
|
||||
}
|
||||
|
||||
startRocket() {
|
||||
if (this.state === 'dead') return;
|
||||
this.state = 'rocket';
|
||||
this.rocketTimer = 4000;
|
||||
const oldHeight = this.displayHeight;
|
||||
this.setTexture('player_rocket');
|
||||
this.setScale(0.52);
|
||||
this.body.setSize(55, 180);
|
||||
this.y -= (this.displayHeight - oldHeight) / 2;
|
||||
}
|
||||
|
||||
endPowerUp() {
|
||||
const oldHeight = this.displayHeight;
|
||||
this.state = 'normal';
|
||||
this.setTexture('player_idle');
|
||||
this.setScale(0.45);
|
||||
this.body.setSize(70, 90);
|
||||
this.setAngle(0);
|
||||
this.y -= (this.displayHeight - oldHeight) / 2;
|
||||
}
|
||||
|
||||
die() {
|
||||
if (this.state === 'dead') return;
|
||||
this.state = 'dead';
|
||||
this.setTexture('player_dead');
|
||||
this.setScale(0.4);
|
||||
this.setAngle(0);
|
||||
this.body.setSize(70, 90);
|
||||
this.setVelocity(0, -250);
|
||||
this.body.allowGravity = true;
|
||||
}
|
||||
}
|
||||
21
src/entities/PropellerHat.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Physics } from 'phaser';
|
||||
|
||||
export class PropellerHat extends Physics.Arcade.Sprite {
|
||||
constructor(scene, x, y) {
|
||||
super(scene, x, y, 'propeller_hat');
|
||||
scene.add.existing(this);
|
||||
scene.physics.add.existing(this, true);
|
||||
|
||||
this.setScale(0.45);
|
||||
this.body.setSize(50, 40);
|
||||
this.body.setOffset(48, 43);
|
||||
|
||||
// No spin — static propeller hat
|
||||
}
|
||||
|
||||
onPlayerTouch(player) {
|
||||
this.destroy();
|
||||
player.startPropeller();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
29
src/entities/Rocket.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Physics } from 'phaser';
|
||||
|
||||
export class Rocket extends Physics.Arcade.Sprite {
|
||||
constructor(scene, x, y) {
|
||||
super(scene, x, y, 'rocket');
|
||||
scene.add.existing(this);
|
||||
scene.physics.add.existing(this, true);
|
||||
|
||||
this.setScale(0.45);
|
||||
this.body.setSize(80, 100, false);
|
||||
this.body.setOffset(59, 91);
|
||||
|
||||
// Float animation
|
||||
scene.tweens.add({
|
||||
targets: this,
|
||||
y: y - 8,
|
||||
duration: 600,
|
||||
yoyo: true,
|
||||
repeat: -1,
|
||||
ease: 'Sine.easeInOut',
|
||||
});
|
||||
}
|
||||
|
||||
onPlayerTouch(player) {
|
||||
this.destroy();
|
||||
player.startRocket();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
23
src/entities/Spring.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Physics } from 'phaser';
|
||||
import { SUPER_JUMP_VELOCITY } from '../config/game.config.js';
|
||||
|
||||
export class Spring extends Physics.Arcade.Sprite {
|
||||
constructor(scene, x, y) {
|
||||
super(scene, x, y, 'spring');
|
||||
scene.add.existing(this);
|
||||
scene.physics.add.existing(this, true);
|
||||
|
||||
this.setScale(1);
|
||||
this.body.setSize(20, 32);
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
onPlayerTouch(player) {
|
||||
if (!this.active) return false;
|
||||
this.active = false;
|
||||
this.setVisible(false);
|
||||
this.body.enable = false;
|
||||
player.jump(SUPER_JUMP_VELOCITY);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
31
src/main.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Game, AUTO } from 'phaser';
|
||||
import { BootScene } from './scenes/BootScene.js';
|
||||
import { MenuScene } from './scenes/MenuScene.js';
|
||||
import { GameScene } from './scenes/GameScene.js';
|
||||
import { GameOverScene } from './scenes/GameOverScene.js';
|
||||
import { GAME_WIDTH, GAME_HEIGHT } from './config/game.config.js';
|
||||
|
||||
const config = {
|
||||
type: AUTO,
|
||||
parent: 'game-container',
|
||||
width: GAME_WIDTH,
|
||||
height: GAME_HEIGHT,
|
||||
backgroundColor: '#0d001a',
|
||||
scale: {
|
||||
mode: Phaser.Scale.FIT,
|
||||
autoCenter: Phaser.Scale.CENTER_BOTH,
|
||||
},
|
||||
physics: {
|
||||
default: 'arcade',
|
||||
arcade: {
|
||||
gravity: { y: 1200 },
|
||||
debug: false,
|
||||
tileBias: 64,
|
||||
},
|
||||
},
|
||||
scene: [BootScene, MenuScene, GameScene, GameOverScene],
|
||||
pixelArt: false,
|
||||
antialias: true,
|
||||
};
|
||||
|
||||
window.game = new Game(config);
|
||||
52
src/managers/BlockchainManager.js
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* BlockchainManager — abstraction layer for Web3 integration.
|
||||
* Currently a stub with localStorage fallback.
|
||||
* Future: replace with viem/ethers calls to Monad.
|
||||
*/
|
||||
export class BlockchainManager {
|
||||
constructor() {
|
||||
this.connected = false;
|
||||
this.address = null;
|
||||
this.bestScore = this.loadLocalBest();
|
||||
}
|
||||
|
||||
async connect() {
|
||||
// TODO: integrate wallet (MetaMask, Rainbow, etc.) via viem
|
||||
// connect() stub
|
||||
this.connected = true;
|
||||
this.address = '0xStub...' + Math.random().toString(36).slice(2, 8);
|
||||
return this.address;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.connected = false;
|
||||
this.address = null;
|
||||
}
|
||||
|
||||
async submitScore(score) {
|
||||
// submitScore() stub
|
||||
if (score > this.bestScore) {
|
||||
this.bestScore = score;
|
||||
this.saveLocalBest(score);
|
||||
}
|
||||
return { txHash: '0x' + Math.random().toString(16).slice(2), confirmed: true };
|
||||
}
|
||||
|
||||
async getLeaderboard(limit = 10) {
|
||||
// getLeaderboard() stub
|
||||
return [
|
||||
{ rank: 1, address: '0xMonad...Dev', score: 99999 },
|
||||
{ rank: 2, address: '0xAlice...xyz', score: 87500 },
|
||||
{ rank: 3, address: '0xBob...abc', score: 74200 },
|
||||
];
|
||||
}
|
||||
|
||||
loadLocalBest() {
|
||||
const raw = localStorage.getItem('naddie_best_score');
|
||||
return raw ? parseInt(raw, 10) : 0;
|
||||
}
|
||||
|
||||
saveLocalBest(score) {
|
||||
localStorage.setItem('naddie_best_score', String(score));
|
||||
}
|
||||
}
|
||||
131
src/managers/PlatformManager.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import { Platform } from '../entities/Platform.js';
|
||||
import { Spring } from '../entities/Spring.js';
|
||||
import { PropellerHat } from '../entities/PropellerHat.js';
|
||||
import { Rocket } from '../entities/Rocket.js';
|
||||
import { Enemy } from '../entities/Enemy.js';
|
||||
import {
|
||||
GAME_WIDTH, PLATFORM_GAP_MIN, PLATFORM_GAP_MAX,
|
||||
SPAWN_RATES, POWERUP_RATES, ENEMY_RATES, DIFFICULTY,
|
||||
} from '../config/game.config.js';
|
||||
|
||||
export class PlatformManager {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.platforms = scene.add.group({ classType: Platform });
|
||||
this.enemies = scene.add.group({ classType: Enemy });
|
||||
this.powerups = scene.add.group();
|
||||
this.lastY = scene.scale.height - 80;
|
||||
this.highestY = this.lastY;
|
||||
}
|
||||
|
||||
update(difficultyLevel, killLine) {
|
||||
const camY = this.scene.cameras.main.scrollY;
|
||||
let maxPerFrame = 5;
|
||||
while (this.highestY > camY - 250 && maxPerFrame-- > 0) {
|
||||
this.spawnPlatform(difficultyLevel);
|
||||
}
|
||||
|
||||
// Cleanup below kill line — destroy when object's bottom crosses it
|
||||
this.platforms.children.iterate((p) => {
|
||||
if (p && p.y + p.displayHeight / 2 > killLine) {
|
||||
p.destroy();
|
||||
}
|
||||
});
|
||||
this.enemies.children.iterate((e) => {
|
||||
if (e && e.y + e.displayHeight / 2 > killLine) {
|
||||
e.destroy();
|
||||
}
|
||||
});
|
||||
this.powerups.children.iterate((pu) => {
|
||||
if (pu && pu.y + pu.displayHeight / 2 > killLine) {
|
||||
pu.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
spawnPlatform(difficultyLevel) {
|
||||
const gapIncrease = Math.min(
|
||||
Math.floor(difficultyLevel / 1000) * DIFFICULTY.gapIncreasePer1000,
|
||||
DIFFICULTY.maxGap - DIFFICULTY.initialGap
|
||||
);
|
||||
const gap = Phaser.Math.Between(
|
||||
PLATFORM_GAP_MIN + gapIncrease,
|
||||
Math.min(PLATFORM_GAP_MAX + gapIncrease, DIFFICULTY.maxGap)
|
||||
);
|
||||
this.highestY -= gap;
|
||||
|
||||
const x = Phaser.Math.Between(60, GAME_WIDTH - 60);
|
||||
const rand = Math.random();
|
||||
let type = 'stable';
|
||||
if (rand < SPAWN_RATES.stable) type = 'stable';
|
||||
else if (rand < SPAWN_RATES.stable + SPAWN_RATES.moving) type = 'moving';
|
||||
else if (rand < SPAWN_RATES.stable + SPAWN_RATES.moving + SPAWN_RATES.breaking) type = 'breaking';
|
||||
else type = 'genesis';
|
||||
|
||||
const platform = new Platform(this.scene, x, this.highestY, type);
|
||||
this.platforms.add(platform);
|
||||
|
||||
this.maybeSpawnPowerUp(x, this.highestY - 25);
|
||||
this.maybeSpawnEnemy(x, this.highestY, difficultyLevel);
|
||||
}
|
||||
|
||||
maybeSpawnPowerUp(x, y) {
|
||||
const rand = Math.random();
|
||||
let type = 'none';
|
||||
if (rand < POWERUP_RATES.spring) type = 'spring';
|
||||
else if (rand < POWERUP_RATES.spring + POWERUP_RATES.propeller) type = 'propeller';
|
||||
else if (rand < POWERUP_RATES.spring + POWERUP_RATES.propeller + POWERUP_RATES.rocket) type = 'rocket';
|
||||
|
||||
if (type === 'none') return;
|
||||
|
||||
const offsetX = Phaser.Math.Between(-25, 25);
|
||||
if (type === 'spring') {
|
||||
this.powerups.add(new Spring(this.scene, x + offsetX, y));
|
||||
} else if (type === 'propeller') {
|
||||
this.powerups.add(new PropellerHat(this.scene, x + offsetX, y - 35));
|
||||
} else if (type === 'rocket') {
|
||||
this.powerups.add(new Rocket(this.scene, x + offsetX, y - 45));
|
||||
}
|
||||
}
|
||||
|
||||
maybeSpawnEnemy(x, y, difficultyLevel) {
|
||||
const enemyBonus = Math.min(
|
||||
Math.floor(difficultyLevel / 1000) * DIFFICULTY.enemyIncreasePer1000,
|
||||
DIFFICULTY.maxEnemyRate
|
||||
);
|
||||
const bugRate = Math.min(ENEMY_RATES.bug + enemyBonus, 0.5);
|
||||
const rand = Math.random();
|
||||
|
||||
let type = 'none';
|
||||
if (rand < bugRate) type = 'bug';
|
||||
|
||||
if (type === 'none') return;
|
||||
|
||||
const ex = Phaser.Math.Between(50, GAME_WIDTH - 50);
|
||||
const ey = y - Phaser.Math.Between(50, 140);
|
||||
|
||||
// Ensure minimum distance from existing enemies
|
||||
const minDist = 120;
|
||||
let tooClose = false;
|
||||
this.enemies.children.iterate((e) => {
|
||||
if (e && Phaser.Math.Distance.Between(ex, ey, e.x, e.y) < minDist) {
|
||||
tooClose = true;
|
||||
}
|
||||
});
|
||||
if (tooClose) return;
|
||||
|
||||
this.enemies.add(new Enemy(this.scene, ex, ey, type));
|
||||
}
|
||||
|
||||
getPlatforms() {
|
||||
return this.platforms;
|
||||
}
|
||||
|
||||
getEnemies() {
|
||||
return this.enemies;
|
||||
}
|
||||
|
||||
getPowerups() {
|
||||
return this.powerups;
|
||||
}
|
||||
}
|
||||
137
src/managers/ScoreManager.js
Normal file
@@ -0,0 +1,137 @@
|
||||
export class ScoreManager {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.score = 0;
|
||||
this.blockHeight = 0;
|
||||
this.combo = 0;
|
||||
this.comboMultiplier = 1;
|
||||
this.lastPlatformY = null;
|
||||
this.genesisActive = false;
|
||||
this.genesisJumps = 0;
|
||||
|
||||
this.hudBg = scene.add.rectangle(0, 0, 200, 90, 0x000000, 0.5)
|
||||
.setOrigin(0)
|
||||
.setDepth(200);
|
||||
|
||||
this.hudScore = scene.add.text(16, 16, 'Gas: 0', {
|
||||
fontFamily: '"Press Start 2P", monospace',
|
||||
fontSize: '14px',
|
||||
color: '#d8b4fe',
|
||||
}).setOrigin(0).setDepth(200).setShadow(1, 1, '#000000', 2, false, true);
|
||||
|
||||
this.hudBlocks = scene.add.text(16, 42, 'Block: 0', {
|
||||
fontFamily: '"Press Start 2P", monospace',
|
||||
fontSize: '12px',
|
||||
color: '#a855f7',
|
||||
}).setOrigin(0).setDepth(200).setShadow(1, 1, '#000000', 2, false, true);
|
||||
|
||||
this.hudCombo = scene.add.text(16, 66, '', {
|
||||
fontFamily: '"Press Start 2P", monospace',
|
||||
fontSize: '10px',
|
||||
color: '#22c55e',
|
||||
}).setOrigin(0).setDepth(200).setShadow(1, 1, '#000000', 2, false, true);
|
||||
|
||||
this.hudBest = scene.add.text(scene.scale.width - 80, 16, 'Best: 0', {
|
||||
fontFamily: '"Press Start 2P", monospace',
|
||||
fontSize: '9px',
|
||||
color: '#666',
|
||||
}).setOrigin(1, 0).setDepth(200).setShadow(1, 1, '#000000', 2, false, true);
|
||||
|
||||
this.updateBestDisplay();
|
||||
}
|
||||
|
||||
update(playerY) {
|
||||
const cam = this.scene.cameras.main;
|
||||
const width = this.scene.scale.width;
|
||||
this.hudBg.setPosition(cam.scrollX + 10, cam.scrollY + 10);
|
||||
this.hudScore.setPosition(cam.scrollX + 16, cam.scrollY + 16);
|
||||
this.hudBlocks.setPosition(cam.scrollX + 16, cam.scrollY + 42);
|
||||
this.hudCombo.setPosition(cam.scrollX + 16, cam.scrollY + 66);
|
||||
this.hudBest.setPosition(cam.scrollX + width - 80, cam.scrollY + 16);
|
||||
|
||||
const blocks = Math.max(0, Math.floor((this.scene.scale.height - playerY) / 100));
|
||||
if (blocks > this.blockHeight) {
|
||||
this.blockHeight = blocks;
|
||||
this.hudBlocks.setText(`Block: ${this.blockHeight}`);
|
||||
|
||||
// Milestone effect every 100 blocks
|
||||
if (this.blockHeight % 100 === 0 && this.blockHeight > 0) {
|
||||
this.showMilestone(this.blockHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onLand(platformY, platformType) {
|
||||
this.combo += 1;
|
||||
this.comboMultiplier = Math.min(1 + (this.combo - 1) * 0.1, 3);
|
||||
|
||||
let basePoints = 10;
|
||||
if (platformType === 'genesis') {
|
||||
this.genesisActive = true;
|
||||
this.genesisJumps = 5;
|
||||
basePoints = 50;
|
||||
}
|
||||
|
||||
let multiplier = this.comboMultiplier;
|
||||
if (this.genesisActive) {
|
||||
multiplier *= 2;
|
||||
this.genesisJumps--;
|
||||
if (this.genesisJumps <= 0) this.genesisActive = false;
|
||||
}
|
||||
|
||||
this.score += Math.floor(basePoints * multiplier);
|
||||
this.hudScore.setText(`Gas: ${this.score}`);
|
||||
|
||||
if (this.combo > 1) {
|
||||
this.hudCombo.setText(`Combo x${this.comboMultiplier.toFixed(1)}`);
|
||||
this.scene.tweens.add({ targets: this.hudCombo, scale: 1.3, duration: 100, yoyo: true });
|
||||
}
|
||||
}
|
||||
|
||||
onFall() {
|
||||
this.combo = 0;
|
||||
this.comboMultiplier = 1;
|
||||
this.hudCombo.setText('');
|
||||
}
|
||||
|
||||
showMilestone(blocks) {
|
||||
const { width, height } = this.scene.scale;
|
||||
const txt = this.scene.add.text(width / 2, height * 0.35, `EPOCH ${blocks} REACHED!`, {
|
||||
fontFamily: '"Press Start 2P", monospace',
|
||||
fontSize: '18px',
|
||||
color: '#ffd700',
|
||||
}).setOrigin(0.5).setScrollFactor(0).setDepth(200);
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: txt,
|
||||
scale: 1.4,
|
||||
alpha: 0,
|
||||
duration: 1500,
|
||||
onComplete: () => txt.destroy(),
|
||||
});
|
||||
}
|
||||
|
||||
updateBestDisplay() {
|
||||
const raw = localStorage.getItem('naddie_best_score');
|
||||
const best = raw ? (parseInt(raw, 10) || 0) : 0;
|
||||
this.hudBest.setText(`Best: ${best}`);
|
||||
}
|
||||
|
||||
saveBest() {
|
||||
const raw = localStorage.getItem('naddie_best_score');
|
||||
const best = raw ? (parseInt(raw, 10) || 0) : 0;
|
||||
if (this.score > best) {
|
||||
localStorage.setItem('naddie_best_score', String(this.score));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.hudBg.destroy();
|
||||
this.hudScore.destroy();
|
||||
this.hudBlocks.destroy();
|
||||
this.hudCombo.destroy();
|
||||
this.hudBest.destroy();
|
||||
}
|
||||
}
|
||||
122
src/scenes/BootScene.js
Normal file
@@ -0,0 +1,122 @@
|
||||
import { Scene } from 'phaser';
|
||||
|
||||
export class BootScene extends Scene {
|
||||
constructor() {
|
||||
super({ key: 'BootScene' });
|
||||
}
|
||||
|
||||
preload() {
|
||||
// New individual sprites
|
||||
this.load.image('player_idle', 'assets/sprites/player_idle.png');
|
||||
this.load.image('player_dead', 'assets/sprites/player_dead.png');
|
||||
this.load.image('player_propeller', 'assets/sprites/player_propeller.png');
|
||||
this.load.image('propeller_hat', 'assets/sprites/propeller_hat.png');
|
||||
this.load.image('rocket', 'assets/sprites/rocket.png');
|
||||
this.load.image('player_rocket', 'assets/sprites/player_rocket.png');
|
||||
this.load.image('enemy_bug', 'assets/sprites/enemy_bug.png');
|
||||
}
|
||||
|
||||
create() {
|
||||
// Generate procedural textures
|
||||
this.createBackgrounds();
|
||||
this.createParticles();
|
||||
|
||||
this.scene.start('MenuScene');
|
||||
}
|
||||
|
||||
createBackgrounds() {
|
||||
const gridW = 512;
|
||||
const gridH = 512;
|
||||
const gridGraphics = this.make.graphics({ x: 0, y: 0, add: false });
|
||||
|
||||
// Deep purple background
|
||||
gridGraphics.fillStyle(0x0f001f, 1);
|
||||
gridGraphics.fillRect(0, 0, gridW, gridH);
|
||||
|
||||
// Grid lines
|
||||
gridGraphics.lineStyle(1, 0x2e0059, 0.35);
|
||||
for (let i = 0; i <= gridW; i += 32) {
|
||||
gridGraphics.moveTo(i, 0);
|
||||
gridGraphics.lineTo(i, gridH);
|
||||
}
|
||||
for (let i = 0; i <= gridH; i += 32) {
|
||||
gridGraphics.moveTo(0, i);
|
||||
gridGraphics.lineTo(gridW, i);
|
||||
}
|
||||
gridGraphics.strokePath();
|
||||
|
||||
// Some accent hexagons / blockchain nodes
|
||||
gridGraphics.lineStyle(1, 0x581c87, 0.2);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const hx = Phaser.Math.Between(20, gridW - 20);
|
||||
const hy = Phaser.Math.Between(20, gridH - 20);
|
||||
const size = Phaser.Math.Between(6, 14);
|
||||
gridGraphics.strokeCircle(hx, hy, size);
|
||||
}
|
||||
|
||||
gridGraphics.generateTexture('gridBg', gridW, gridH);
|
||||
|
||||
// Star / particle texture
|
||||
const sGfx = this.make.graphics({ x: 0, y: 0, add: false });
|
||||
sGfx.fillStyle(0xffffff, 0.9);
|
||||
sGfx.fillCircle(2, 2, 2);
|
||||
sGfx.generateTexture('star', 4, 4);
|
||||
|
||||
// Gwei particle
|
||||
const gGfx = this.make.graphics({ x: 0, y: 0, add: false });
|
||||
gGfx.fillStyle(0xa855f7, 1);
|
||||
gGfx.fillRect(0, 0, 5, 5);
|
||||
gGfx.generateTexture('gwei', 5, 5);
|
||||
|
||||
// Platform texture — cartoon 3D block inspired by concept art
|
||||
const pw = 110;
|
||||
const ph = 32;
|
||||
const pGfx = this.make.graphics({ x: 0, y: 0, add: false });
|
||||
|
||||
// Drop shadow
|
||||
pGfx.fillStyle(0x000000, 0.25);
|
||||
pGfx.fillRoundedRect(3, 4, pw, ph, 10);
|
||||
|
||||
// Bottom / side face (darker purple for 3D look)
|
||||
pGfx.fillStyle(0x5b21b6, 1);
|
||||
pGfx.fillRoundedRect(0, 4, pw, ph - 4, 10);
|
||||
|
||||
// Top face (main purple)
|
||||
pGfx.fillStyle(0x8b5cf6, 1);
|
||||
pGfx.fillRoundedRect(0, 0, pw, ph - 6, 10);
|
||||
|
||||
// Thick cartoon outline
|
||||
pGfx.lineStyle(3, 0x1e1b4b, 1);
|
||||
pGfx.strokeRoundedRect(0, 0, pw, ph - 6, 10);
|
||||
|
||||
// Highlight on top edge
|
||||
pGfx.fillStyle(0xffffff, 0.35);
|
||||
pGfx.fillRoundedRect(5, 2, pw - 10, 6, 4);
|
||||
|
||||
// Subtle edge line separating top and side faces
|
||||
pGfx.lineStyle(2, 0x6d28d9, 0.8);
|
||||
pGfx.beginPath();
|
||||
pGfx.moveTo(2, ph - 6);
|
||||
pGfx.lineTo(pw - 2, ph - 6);
|
||||
pGfx.strokePath();
|
||||
|
||||
pGfx.generateTexture('platform', pw, ph);
|
||||
|
||||
// Spring texture (procedural)
|
||||
const springGfx = this.make.graphics({ x: 0, y: 0, add: false });
|
||||
springGfx.lineStyle(3, 0xffd700, 1);
|
||||
springGfx.beginPath();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
springGfx.moveTo(5, i * 6);
|
||||
springGfx.lineTo(15, i * 6 + 3);
|
||||
springGfx.moveTo(15, i * 6 + 3);
|
||||
springGfx.lineTo(5, i * 6 + 6);
|
||||
}
|
||||
springGfx.strokePath();
|
||||
springGfx.generateTexture('spring', 20, 32);
|
||||
}
|
||||
|
||||
createParticles() {
|
||||
// Pre-create particle emitters config if needed
|
||||
}
|
||||
}
|
||||
86
src/scenes/GameOverScene.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Scene } from 'phaser';
|
||||
|
||||
export class GameOverScene extends Scene {
|
||||
constructor() {
|
||||
super({ key: 'GameOverScene' });
|
||||
}
|
||||
|
||||
init(data) {
|
||||
this.finalScore = data.score || 0;
|
||||
this.blockHeight = data.blockHeight || 0;
|
||||
this.isNewBest = data.isNewBest || false;
|
||||
}
|
||||
|
||||
create() {
|
||||
const { width, height } = this.scale;
|
||||
|
||||
this.add.tileSprite(width / 2, height / 2, width, height, 'gridBg');
|
||||
|
||||
// Dead Naddie image
|
||||
const naddie = this.add.image(width / 2, height * 0.22, 'player_dead')
|
||||
.setScale(0.32);
|
||||
this.tweens.add({
|
||||
targets: naddie,
|
||||
angle: -10,
|
||||
duration: 2000,
|
||||
yoyo: true,
|
||||
repeat: -1,
|
||||
ease: 'Sine.easeInOut',
|
||||
});
|
||||
|
||||
// Game Over text
|
||||
this.add.text(width / 2, height * 0.38, 'GAME OVER', {
|
||||
fontFamily: '"Press Start 2P", monospace',
|
||||
fontSize: '32px',
|
||||
color: '#ef4444',
|
||||
align: 'center',
|
||||
}).setOrigin(0.5).setShadow(3, 3, '#7f1d1d', 0, false, true);
|
||||
|
||||
if (this.isNewBest) {
|
||||
this.add.text(width / 2, height * 0.46, 'NEW BEST!', {
|
||||
fontFamily: '"Press Start 2P", monospace',
|
||||
fontSize: '14px',
|
||||
color: '#22c55e',
|
||||
}).setOrigin(0.5);
|
||||
}
|
||||
|
||||
this.add.text(width / 2, height * 0.54, `Block Height: ${this.blockHeight}`, {
|
||||
fontFamily: '"Press Start 2P", monospace',
|
||||
fontSize: '13px',
|
||||
color: '#d8b4fe',
|
||||
}).setOrigin(0.5);
|
||||
|
||||
this.add.text(width / 2, height * 0.61, `Gas Score: ${this.finalScore}`, {
|
||||
fontFamily: '"Press Start 2P", monospace',
|
||||
fontSize: '16px',
|
||||
color: '#a855f7',
|
||||
}).setOrigin(0.5);
|
||||
|
||||
this.createButton(width / 2, height * 0.72, 'RETRY', () => {
|
||||
this.scene.start('GameScene');
|
||||
});
|
||||
|
||||
this.createButton(width / 2, height * 0.82, 'MAIN MENU', () => {
|
||||
this.scene.start('MenuScene');
|
||||
});
|
||||
|
||||
const submitBtn = this.createButton(width / 2, height * 0.92, 'SUBMIT TO CHAIN', () => {});
|
||||
submitBtn.bg.setAlpha(0.4);
|
||||
submitBtn.label.setAlpha(0.4);
|
||||
}
|
||||
|
||||
createButton(x, y, text, callback) {
|
||||
const bg = this.add.rectangle(x, y, 260, 48, 0x581c87)
|
||||
.setStrokeStyle(2, 0xa855f7)
|
||||
.setInteractive({ useHandCursor: true });
|
||||
const label = this.add.text(x, y, text, {
|
||||
fontFamily: '"Press Start 2P", monospace',
|
||||
fontSize: '13px',
|
||||
color: '#ffffff',
|
||||
}).setOrigin(0.5);
|
||||
bg.on('pointerover', () => bg.setFillStyle(0x7e22ce));
|
||||
bg.on('pointerout', () => bg.setFillStyle(0x581c87));
|
||||
bg.on('pointerdown', callback);
|
||||
return { bg, label };
|
||||
}
|
||||
}
|
||||
409
src/scenes/GameScene.js
Normal file
@@ -0,0 +1,409 @@
|
||||
import { Scene } from 'phaser';
|
||||
import { Player } from '../entities/Player.js';
|
||||
import { Platform } from '../entities/Platform.js';
|
||||
import { PlatformManager } from '../managers/PlatformManager.js';
|
||||
import { ScoreManager } from '../managers/ScoreManager.js';
|
||||
import { GAME_WIDTH, GAME_HEIGHT, GRAVITY } from '../config/game.config.js';
|
||||
|
||||
export class GameScene extends Scene {
|
||||
constructor() {
|
||||
super({ key: 'GameScene' });
|
||||
}
|
||||
|
||||
create() {
|
||||
this.physics.world.gravity.y = GRAVITY;
|
||||
|
||||
// Background
|
||||
this.bg = this.add.tileSprite(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 'gridBg')
|
||||
.setScrollFactor(0)
|
||||
.setDepth(-10);
|
||||
|
||||
// Gwei particles
|
||||
this.createGweiParticles();
|
||||
|
||||
// Input
|
||||
this.cursors = this.input.keyboard.createCursorKeys();
|
||||
this.wasd = this.input.keyboard.addKeys({
|
||||
up: 'W', down: 'S', left: 'A', right: 'D',
|
||||
});
|
||||
this.touchLeft = false;
|
||||
this.touchRight = false;
|
||||
this.setupTouchControls();
|
||||
|
||||
// Start platform
|
||||
const startY = GAME_HEIGHT - 80;
|
||||
this.startPlatform = new Platform(this, GAME_WIDTH / 2, startY, 'stable');
|
||||
this.startPlatform.setTint(0xa78bfa);
|
||||
this.startPlatform.setAlpha(1);
|
||||
|
||||
// Player
|
||||
this.player = new Player(this, GAME_WIDTH / 2, startY - 140);
|
||||
this.lastJumpY = this.player.y;
|
||||
|
||||
// Player shadow/glow
|
||||
this.playerShadow = this.add.graphics();
|
||||
this.playerShadow.setDepth(-4);
|
||||
|
||||
// Trail container for fast movement
|
||||
this.trailPoints = [];
|
||||
|
||||
// Managers
|
||||
this.platformManager = new PlatformManager(this);
|
||||
this.scoreManager = new ScoreManager(this);
|
||||
|
||||
// Pre-spawn platforms
|
||||
for (let i = 0; i < 10; i++) {
|
||||
this.platformManager.spawnPlatform(0);
|
||||
}
|
||||
|
||||
// Collisions
|
||||
this.physics.add.collider(this.player, this.startPlatform, this.handlePlatformCollision, this.platformCollisionFilter, this);
|
||||
this.physics.add.collider(this.player, this.platformManager.getPlatforms(), this.handlePlatformCollision, this.platformCollisionFilter, this);
|
||||
this.physics.add.overlap(this.player, this.platformManager.getPowerups(), this.handlePowerup, null, this);
|
||||
this.physics.add.overlap(this.player, this.platformManager.getEnemies(), this.handleEnemy, null, this);
|
||||
|
||||
// Camera
|
||||
this.cameras.main.setBounds(0, -999999, GAME_WIDTH, 999999 + GAME_HEIGHT);
|
||||
this.cameras.main.startFollow(this.player, true, 0, 0.05, 0, 180);
|
||||
|
||||
// Debug hitbox renderer
|
||||
this.debugGraphics = this.add.graphics().setDepth(1000);
|
||||
|
||||
// Death vignette overlay
|
||||
this.deathOverlay = this.add.graphics();
|
||||
this.deathOverlay.setScrollFactor(0);
|
||||
this.deathOverlay.setDepth(100);
|
||||
this.deathOverlay.setAlpha(0);
|
||||
|
||||
this.isGameOver = false;
|
||||
this.difficultyLevel = 0;
|
||||
this.minScrollY = this.cameras.main.scrollY;
|
||||
}
|
||||
|
||||
update(time, delta) {
|
||||
if (this.isGameOver) return;
|
||||
|
||||
this.player.update(this.cursors, this.wasd, this.touchLeft, this.touchRight, time, delta);
|
||||
|
||||
// Parallax background
|
||||
this.bg.tilePositionY = this.cameras.main.scrollY * 0.3;
|
||||
|
||||
// Kill line rises with camera but never falls back down
|
||||
this.minScrollY = Math.min(this.minScrollY, this.cameras.main.scrollY);
|
||||
const killLine = this.minScrollY + GAME_HEIGHT;
|
||||
|
||||
// Update difficulty
|
||||
const height = Math.max(0, GAME_HEIGHT - this.player.y);
|
||||
this.difficultyLevel = height;
|
||||
this.platformManager.update(this.difficultyLevel, killLine);
|
||||
this.scoreManager.update(this.player.y);
|
||||
|
||||
// Check death — same fixed kill line as platform cleanup
|
||||
if (this.player.body.bottom > killLine) {
|
||||
this.gameOver();
|
||||
}
|
||||
|
||||
// Reset combo if falling too far without landing
|
||||
if (this.player.body.velocity.y > 0 && this.player.y > this.lastJumpY + 300) {
|
||||
this.scoreManager.onFall();
|
||||
this.lastJumpY = this.player.y;
|
||||
}
|
||||
|
||||
// Update player shadow
|
||||
this.updatePlayerShadow();
|
||||
|
||||
// Update trail for fast movement
|
||||
this.updateTrail();
|
||||
|
||||
// Debug disabled
|
||||
// this.drawDebug(killLine);
|
||||
}
|
||||
|
||||
updatePlayerShadow() {
|
||||
this.playerShadow.clear();
|
||||
if (!this.player.active) return;
|
||||
|
||||
const alpha = Math.max(0.05, 0.25 - (Math.abs(this.player.body.velocity.y) / 1200));
|
||||
this.playerShadow.fillStyle(0x000000, alpha);
|
||||
this.playerShadow.fillCircle(this.player.x, this.player.y + 42, 16);
|
||||
}
|
||||
|
||||
updateTrail() {
|
||||
if (!this.player.active) return;
|
||||
|
||||
const isFast = this.player.state === 'rocket' || this.player.state === 'propeller';
|
||||
if (isFast && Math.abs(this.player.body.velocity.y) > 200) {
|
||||
this.trailPoints.push({ x: this.player.x, y: this.player.y, alpha: 0.5, scale: 1 });
|
||||
if (this.trailPoints.length > 15) this.trailPoints.shift();
|
||||
}
|
||||
|
||||
for (let i = this.trailPoints.length - 1; i >= 0; i--) {
|
||||
const point = this.trailPoints[i];
|
||||
point.alpha -= 0.04;
|
||||
point.scale -= 0.03;
|
||||
if (point.alpha <= 0 || point.scale <= 0) {
|
||||
this.trailPoints.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Redraw trail
|
||||
this.trailGraphics = this.trailGraphics || this.add.graphics();
|
||||
this.trailGraphics.clear();
|
||||
this.trailGraphics.setDepth(-3);
|
||||
|
||||
for (const point of this.trailPoints) {
|
||||
const color = this.player.state === 'rocket' ? 0xff4444 : 0x44aaff;
|
||||
this.trailGraphics.fillStyle(color, point.alpha);
|
||||
this.trailGraphics.fillCircle(point.x, point.y, 8 * point.scale);
|
||||
}
|
||||
}
|
||||
|
||||
platformCollisionFilter(player, platform) {
|
||||
if (!platform || !platform.body) return false;
|
||||
if (typeof platform.isBroken === 'function' && platform.isBroken()) return false;
|
||||
// Only bounce when falling and player is above platform center
|
||||
return player.body.velocity.y > 0 && player.y < platform.y + 8;
|
||||
}
|
||||
|
||||
handlePlatformCollision(player, platform) {
|
||||
if (this.isGameOver) return;
|
||||
if (platform && typeof platform.onPlayerLand === 'function') {
|
||||
platform.onPlayerLand(player);
|
||||
this.scoreManager.onLand(platform.y, platform.platformType || 'stable');
|
||||
}
|
||||
this.lastJumpY = player.y;
|
||||
player.jump();
|
||||
this.createJumpParticles(player.x, player.y + player.displayHeight / 2 + 3);
|
||||
}
|
||||
|
||||
handlePowerup(player, powerup) {
|
||||
if (this.isGameOver) return;
|
||||
if (powerup && typeof powerup.onPlayerTouch === 'function') {
|
||||
const px = powerup.x;
|
||||
const py = powerup.y;
|
||||
powerup.onPlayerTouch(player);
|
||||
this.createPowerupParticles(px, py);
|
||||
this.flashScreen();
|
||||
}
|
||||
}
|
||||
|
||||
handleEnemy(player, enemy) {
|
||||
if (this.isGameOver) return;
|
||||
if (player.state === 'rocket') {
|
||||
this.createExplosion(enemy.x, enemy.y);
|
||||
enemy.destroy();
|
||||
return;
|
||||
}
|
||||
if (player.state === 'propeller') {
|
||||
player.endPowerUp();
|
||||
this.createExplosion(enemy.x, enemy.y);
|
||||
enemy.destroy();
|
||||
return;
|
||||
}
|
||||
// Mario-style stomp: falling onto enemy kills it
|
||||
const stompTolerance = 12;
|
||||
if (player.body.velocity.y > 0 && player.body.bottom <= enemy.body.top + stompTolerance) {
|
||||
this.createExplosion(enemy.x, enemy.y);
|
||||
enemy.destroy();
|
||||
player.jump();
|
||||
this.scoreManager.score += 25;
|
||||
this.scoreManager.hudScore.setText(`Gas: ${this.scoreManager.score}`);
|
||||
this.createJumpParticles(player.x, player.y + player.displayHeight / 2 + 3);
|
||||
return;
|
||||
}
|
||||
this.gameOver(enemy.x, enemy.y);
|
||||
}
|
||||
|
||||
gameOver(ex, ey) {
|
||||
if (this.isGameOver) return;
|
||||
this.isGameOver = true;
|
||||
this.player.die();
|
||||
this.cameras.main.shake(300, 0.012);
|
||||
// Small explosion at impact point instead of full-screen vignette
|
||||
const bx = ex ?? this.player.x;
|
||||
const by = ey ?? this.player.y;
|
||||
this.createExplosion(bx, by);
|
||||
const isNewBest = this.scoreManager.saveBest();
|
||||
const score = this.scoreManager.score;
|
||||
const blockHeight = this.scoreManager.blockHeight;
|
||||
|
||||
this.time.delayedCall(1500, () => {
|
||||
this.scoreManager.destroy();
|
||||
this.scene.start('GameOverScene', { score, blockHeight, isNewBest });
|
||||
});
|
||||
}
|
||||
|
||||
drawDebug(camBottom) {
|
||||
this.debugGraphics.clear();
|
||||
|
||||
// Kill line — magenta
|
||||
this.debugGraphics.lineStyle(2, 0xff00ff, 0.7);
|
||||
this.debugGraphics.lineBetween(0, camBottom, GAME_WIDTH, camBottom);
|
||||
|
||||
// Player — red
|
||||
if (this.player.active && this.player.body) {
|
||||
this.debugGraphics.lineStyle(2, 0xff0000, 1);
|
||||
this.debugGraphics.strokeRectShape(this.player.body);
|
||||
}
|
||||
|
||||
// Start platform — green
|
||||
if (this.startPlatform && this.startPlatform.body) {
|
||||
this.debugGraphics.lineStyle(2, 0x00ff00, 1);
|
||||
this.debugGraphics.strokeRectShape(this.startPlatform.body);
|
||||
}
|
||||
|
||||
// Platforms — green
|
||||
this.platformManager.getPlatforms().children.iterate((p) => {
|
||||
if (p && p.body) {
|
||||
this.debugGraphics.lineStyle(2, 0x00ff00, 1);
|
||||
this.debugGraphics.strokeRectShape(p.body);
|
||||
}
|
||||
});
|
||||
|
||||
// Enemies — blue
|
||||
this.platformManager.getEnemies().children.iterate((e) => {
|
||||
if (e && e.body) {
|
||||
this.debugGraphics.lineStyle(2, 0x0000ff, 1);
|
||||
this.debugGraphics.strokeRectShape(e.body);
|
||||
}
|
||||
});
|
||||
|
||||
// Powerups — yellow
|
||||
this.platformManager.getPowerups().children.iterate((pu) => {
|
||||
if (pu && pu.body) {
|
||||
this.debugGraphics.lineStyle(2, 0xffff00, 1);
|
||||
this.debugGraphics.strokeRectShape(pu.body);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showDeathVignette() {
|
||||
const cx = GAME_WIDTH / 2;
|
||||
const cy = GAME_HEIGHT / 2;
|
||||
const maxR = Math.max(GAME_WIDTH, GAME_HEIGHT);
|
||||
|
||||
this.deathOverlay.clear();
|
||||
// Draw a radial gradient-like vignette using fewer circles
|
||||
const steps = 5;
|
||||
const band = (maxR * 0.8) / steps;
|
||||
for (let i = steps; i >= 0; i--) {
|
||||
const t = i / steps;
|
||||
const r = 50 + t * (maxR * 0.8);
|
||||
const alpha = 0.05 + (1 - t) * 0.55;
|
||||
this.deathOverlay.lineStyle(band, 0x7f0000, alpha);
|
||||
this.deathOverlay.strokeCircle(cx, cy, r);
|
||||
}
|
||||
this.deathOverlay.setAlpha(0);
|
||||
|
||||
this.tweens.add({
|
||||
targets: this.deathOverlay,
|
||||
alpha: 1,
|
||||
duration: 400,
|
||||
ease: 'Quad.easeOut',
|
||||
});
|
||||
}
|
||||
|
||||
flashScreen() {
|
||||
const flash = this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0xffffff, 0.3);
|
||||
flash.setScrollFactor(0);
|
||||
flash.setDepth(90);
|
||||
this.tweens.add({
|
||||
targets: flash,
|
||||
alpha: 0,
|
||||
duration: 250,
|
||||
onComplete: () => flash.destroy(),
|
||||
});
|
||||
}
|
||||
|
||||
setupTouchControls() {
|
||||
const { width, height } = this.scale;
|
||||
this.input.on('pointerdown', (pointer) => {
|
||||
if (pointer.x < width / 2) this.touchLeft = true;
|
||||
else this.touchRight = true;
|
||||
});
|
||||
this.input.on('pointerup', () => {
|
||||
this.touchLeft = false;
|
||||
this.touchRight = false;
|
||||
});
|
||||
this.input.on('pointermove', (pointer) => {
|
||||
if (pointer.isDown) {
|
||||
this.touchLeft = pointer.x < width / 2;
|
||||
this.touchRight = pointer.x >= width / 2;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createGweiParticles() {
|
||||
for (let i = 0; i < 25; i++) {
|
||||
const p = this.add.image(
|
||||
Phaser.Math.Between(0, GAME_WIDTH),
|
||||
Phaser.Math.Between(0, GAME_HEIGHT),
|
||||
'gwei'
|
||||
).setAlpha(0.5);
|
||||
const scene = this;
|
||||
this.tweens.add({
|
||||
targets: p,
|
||||
y: p.y - Phaser.Math.Between(100, 400),
|
||||
alpha: 0,
|
||||
duration: Phaser.Math.Between(3000, 7000),
|
||||
repeat: -1,
|
||||
delay: Phaser.Math.Between(0, 4000),
|
||||
onRepeat: function() {
|
||||
p.y = scene.cameras.main.scrollY + GAME_HEIGHT + 20;
|
||||
p.x = Phaser.Math.Between(0, GAME_WIDTH);
|
||||
p.setAlpha(0.5);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createJumpParticles(x, y) {
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const size = Phaser.Math.Between(4, 7);
|
||||
const p = this.add.rectangle(x, y, size, size, 0xa855f7).setAlpha(0.9);
|
||||
this.tweens.add({
|
||||
targets: p,
|
||||
x: x + Phaser.Math.Between(-35, 35),
|
||||
y: y + Phaser.Math.Between(10, 45),
|
||||
alpha: 0,
|
||||
scale: 0,
|
||||
angle: Phaser.Math.Between(-90, 90),
|
||||
duration: 250,
|
||||
onComplete: () => p.destroy(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createPowerupParticles(x, y) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const color = [0xffd700, 0xa855f7, 0x22c55e][Phaser.Math.Between(0, 2)];
|
||||
const p = this.add.rectangle(x, y, 5, 5, color).setAlpha(1);
|
||||
this.tweens.add({
|
||||
targets: p,
|
||||
x: x + Phaser.Math.Between(-50, 50),
|
||||
y: y + Phaser.Math.Between(-50, 50),
|
||||
alpha: 0,
|
||||
scale: 0,
|
||||
duration: 600,
|
||||
onComplete: () => p.destroy(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createExplosion(x, y) {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const p = this.add.rectangle(x, y, 6, 6, 0xef4444).setAlpha(1);
|
||||
const angle = Phaser.Math.Between(0, 360);
|
||||
const dist = Phaser.Math.Between(30, 80);
|
||||
this.tweens.add({
|
||||
targets: p,
|
||||
x: x + Math.cos(angle * Math.PI / 180) * dist,
|
||||
y: y + Math.sin(angle * Math.PI / 180) * dist,
|
||||
alpha: 0,
|
||||
scale: 0,
|
||||
duration: 500,
|
||||
onComplete: () => p.destroy(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
164
src/scenes/MenuScene.js
Normal file
@@ -0,0 +1,164 @@
|
||||
import { Scene } from 'phaser';
|
||||
|
||||
export class MenuScene extends Scene {
|
||||
constructor() {
|
||||
super({ key: 'MenuScene' });
|
||||
}
|
||||
|
||||
create() {
|
||||
const { width, height } = this.scale;
|
||||
|
||||
// Background
|
||||
this.add.tileSprite(width / 2, height / 2, width, height, 'gridBg');
|
||||
|
||||
// Title
|
||||
this.add.text(width / 2, height * 0.18, 'NADDIE JUMP', {
|
||||
fontFamily: '"Press Start 2P", monospace',
|
||||
fontSize: '38px',
|
||||
color: '#d8b4fe',
|
||||
align: 'center',
|
||||
}).setOrigin(0.5).setShadow(4, 4, '#581c87', 0, false, true);
|
||||
|
||||
this.add.text(width / 2, height * 0.27, 'MONAD EDITION', {
|
||||
fontFamily: '"Press Start 2P", monospace',
|
||||
fontSize: '14px',
|
||||
color: '#a855f7',
|
||||
align: 'center',
|
||||
}).setOrigin(0.5);
|
||||
|
||||
// Floating Naddie preview
|
||||
const preview = this.add.image(width / 2, height * 0.48, 'player_idle')
|
||||
.setScale(0.55);
|
||||
this.tweens.add({
|
||||
targets: preview,
|
||||
y: height * 0.48 - 15,
|
||||
duration: 1400,
|
||||
yoyo: true,
|
||||
repeat: -1,
|
||||
ease: 'Sine.easeInOut',
|
||||
});
|
||||
this.tweens.add({
|
||||
targets: preview,
|
||||
angle: 5,
|
||||
duration: 2000,
|
||||
yoyo: true,
|
||||
repeat: -1,
|
||||
ease: 'Sine.easeInOut',
|
||||
});
|
||||
|
||||
// Start button
|
||||
this.createButton(width / 2, height * 0.68, 'START GAME', () => {
|
||||
this.scene.start('GameScene');
|
||||
});
|
||||
|
||||
this.add.text(width / 2, height * 0.74, 'Press ENTER or SPACE to start', {
|
||||
fontFamily: '"Press Start 2P", monospace',
|
||||
fontSize: '10px',
|
||||
color: '#888',
|
||||
align: 'center',
|
||||
}).setOrigin(0.5);
|
||||
|
||||
this.input.keyboard.on('keydown-ENTER', () => {
|
||||
this.scene.start('GameScene');
|
||||
});
|
||||
this.input.keyboard.on('keydown-SPACE', () => {
|
||||
this.scene.start('GameScene');
|
||||
});
|
||||
|
||||
// Leaderboard button
|
||||
this.createButton(width / 2, height * 0.78, 'LEADERBOARD', () => {
|
||||
this.showLeaderboard();
|
||||
});
|
||||
|
||||
this.add.text(width / 2, height * 0.92, 'Web3 integration coming soon', {
|
||||
fontFamily: '"Press Start 2P", monospace',
|
||||
fontSize: '9px',
|
||||
color: '#444',
|
||||
align: 'center',
|
||||
}).setOrigin(0.5);
|
||||
|
||||
this.createAmbientParticles();
|
||||
}
|
||||
|
||||
createButton(x, y, text, callback) {
|
||||
const bg = this.add.rectangle(x, y, 280, 56, 0x581c87)
|
||||
.setStrokeStyle(3, 0xa855f7)
|
||||
.setInteractive({ useHandCursor: true });
|
||||
|
||||
const label = this.add.text(x, y, text, {
|
||||
fontFamily: '"Press Start 2P", monospace',
|
||||
fontSize: '15px',
|
||||
color: '#ffffff',
|
||||
}).setOrigin(0.5);
|
||||
|
||||
bg.on('pointerover', () => {
|
||||
bg.setFillStyle(0x7e22ce);
|
||||
this.tweens.add({ targets: [bg, label], scaleX: 1.05, scaleY: 1.05, duration: 100 });
|
||||
});
|
||||
bg.on('pointerout', () => {
|
||||
bg.setFillStyle(0x581c87);
|
||||
this.tweens.add({ targets: [bg, label], scaleX: 1, scaleY: 1, duration: 100 });
|
||||
});
|
||||
bg.on('pointerdown', callback);
|
||||
|
||||
return { bg, label };
|
||||
}
|
||||
|
||||
showLeaderboard() {
|
||||
const { width, height } = this.scale;
|
||||
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.7).setDepth(100);
|
||||
const panel = this.add.rectangle(width / 2, height / 2, 400, 440, 0x1a0533).setStrokeStyle(3, 0xa855f7).setDepth(101);
|
||||
|
||||
const title = this.add.text(width / 2, height * 0.22, 'LEADERBOARD', {
|
||||
fontFamily: '"Press Start 2P", monospace',
|
||||
fontSize: '18px',
|
||||
color: '#d8b4fe',
|
||||
}).setOrigin(0.5).setDepth(102);
|
||||
|
||||
const close = this.add.text(width / 2 + 170, height * 0.22 - 80, 'X', {
|
||||
fontFamily: '"Press Start 2P", monospace',
|
||||
fontSize: '18px',
|
||||
color: '#fff',
|
||||
}).setOrigin(0.5).setDepth(102).setInteractive({ useHandCursor: true });
|
||||
|
||||
const rows = [];
|
||||
const mock = [
|
||||
{ rank: 1, addr: '0xMonad...Dev', score: 99999 },
|
||||
{ rank: 2, addr: '0xAlice...xyz', score: 87500 },
|
||||
{ rank: 3, addr: '0xBob...abc', score: 74200 },
|
||||
{ rank: 4, addr: '0xYou', score: parseInt(localStorage.getItem('naddie_best_score') || '0', 10) },
|
||||
];
|
||||
mock.forEach((entry, i) => {
|
||||
const y = height * 0.32 + i * 55;
|
||||
rows.push({
|
||||
rank: this.add.text(width / 2 - 160, y, `#${entry.rank}`, { fontFamily: '"Press Start 2P", monospace', fontSize: '12px', color: '#a855f7' }).setOrigin(0.5).setDepth(102),
|
||||
addr: this.add.text(width / 2, y, entry.addr, { fontFamily: '"Press Start 2P", monospace', fontSize: '10px', color: '#fff' }).setOrigin(0.5).setDepth(102),
|
||||
score: this.add.text(width / 2 + 150, y, String(entry.score), { fontFamily: '"Press Start 2P", monospace', fontSize: '12px', color: '#d8b4fe' }).setOrigin(0.5).setDepth(102),
|
||||
});
|
||||
});
|
||||
|
||||
close.on('pointerdown', () => {
|
||||
overlay.destroy();
|
||||
panel.destroy();
|
||||
title.destroy();
|
||||
close.destroy();
|
||||
rows.forEach(r => { r.rank.destroy(); r.addr.destroy(); r.score.destroy(); });
|
||||
});
|
||||
}
|
||||
|
||||
createAmbientParticles() {
|
||||
const { width, height } = this.scale;
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const s = this.add.image(Phaser.Math.Between(0, width), Phaser.Math.Between(0, height), 'star');
|
||||
s.setAlpha(Phaser.Math.FloatBetween(0.15, 0.7));
|
||||
this.tweens.add({
|
||||
targets: s,
|
||||
y: s.y - Phaser.Math.Between(50, 250),
|
||||
alpha: 0,
|
||||
duration: Phaser.Math.Between(2000, 6000),
|
||||
repeat: -1,
|
||||
delay: Phaser.Math.Between(0, 3000),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
72
test-game.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from playwright.sync_api import sync_playwright
|
||||
import time
|
||||
import os
|
||||
|
||||
screenshots_dir = "/home/anril/naddie-jump/test-screenshots"
|
||||
os.makedirs(screenshots_dir, exist_ok=True)
|
||||
|
||||
console_errors = []
|
||||
console_logs = []
|
||||
|
||||
def handle_console(msg):
|
||||
console_logs.append(f"[{msg.type}] {msg.text}")
|
||||
if msg.type == "error":
|
||||
console_errors.append(msg.text)
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
context = browser.new_context(viewport={"width": 480, "height": 854})
|
||||
page = context.new_page()
|
||||
page.on("console", handle_console)
|
||||
page.on("pageerror", lambda err: console_errors.append(str(err)))
|
||||
|
||||
print("Opening game...")
|
||||
page.goto("http://localhost:5173/")
|
||||
time.sleep(2)
|
||||
|
||||
# Screenshot 1: Menu
|
||||
page.screenshot(path=f"{screenshots_dir}/01-menu.png")
|
||||
print("Screenshot: menu")
|
||||
|
||||
# Click START GAME
|
||||
print("Clicking START GAME...")
|
||||
page.click("text=START GAME")
|
||||
time.sleep(1)
|
||||
|
||||
# Screenshot 2: Game start
|
||||
page.screenshot(path=f"{screenshots_dir}/02-game-start.png")
|
||||
print("Screenshot: game start")
|
||||
|
||||
# Simulate gameplay - press left/right arrows
|
||||
print("Simulating gameplay...")
|
||||
for i in range(30):
|
||||
if i % 4 < 2:
|
||||
page.keyboard.press("ArrowRight")
|
||||
else:
|
||||
page.keyboard.press("ArrowLeft")
|
||||
time.sleep(0.15)
|
||||
|
||||
# Screenshot 3: After playing
|
||||
page.screenshot(path=f"{screenshots_dir}/03-gameplay.png")
|
||||
print("Screenshot: gameplay")
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# Screenshot 4: Final state
|
||||
page.screenshot(path=f"{screenshots_dir}/04-final.png")
|
||||
print("Screenshot: final")
|
||||
|
||||
browser.close()
|
||||
|
||||
print("\n=== CONSOLE LOGS ===")
|
||||
for log in console_logs:
|
||||
print(log)
|
||||
|
||||
print("\n=== ERRORS ===")
|
||||
if console_errors:
|
||||
for err in console_errors:
|
||||
print(err)
|
||||
else:
|
||||
print("No errors!")
|
||||
|
||||
print(f"\nScreenshots saved to: {screenshots_dir}")
|
||||
BIN
test-screenshots/01-menu.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
test-screenshots/02-game-start.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
test-screenshots/03-gameplay.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
test-screenshots/04-final.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
test-screenshots/10-game-started.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
test-screenshots/11-gameplay.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
test-screenshots/12-forced.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
test-screenshots/20-game.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
test-screenshots/21-play.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
test-screenshots/30-game.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
test-screenshots/31-play.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
test-screenshots/40-game.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
test-screenshots/41-play.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
test-screenshots/50-menu.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
test-screenshots/51-game.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
test-screenshots/52-play.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
test-screenshots/53-gameover.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
test-screenshots/test-click.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
10
vercel.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": 2,
|
||||
"buildCommand": "npm run build",
|
||||
"outputDirectory": "dist",
|
||||
"installCommand": "npm install",
|
||||
"framework": "vite",
|
||||
"rewrites": [
|
||||
{ "source": "/(.*)", "destination": "/index.html" }
|
||||
]
|
||||
}
|
||||