diff --git a/README.md b/README.md index 5f69707..c8e3850 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Un jeu multijoueur **Pierre-Feuille-Ciseaux** en 2D premium avec paris sur la bl | API & DB | Express + Prisma ORM + PostgreSQL 17 | | Cache | Redis | | Blockchain | Hardhat + Solidity + Ethers.js v6 | -| Audio | Web Audio API (procédural) | +| Audio | Howler.js | | CI/CD | Forgejo Actions | | Reverse Proxy | Nginx Proxy Manager | @@ -32,21 +32,9 @@ rps-royale/ │ └── contracts/ # Hardhat + Solidity ├── packages/ │ └── shared/ # Types et constantes partagés -├── docs/ # Documentation complète └── docker-compose.yml ``` -## Fonctionnalités - -- **Matchmaking temps réel** : appariement automatique par montant de mise -- **Mode IA** : joue contre un adversaire IA si personne n'est disponible -- **Sans wallet** : génération automatique d'une adresse guest -- **Anti-triche** : pattern commit-reveal via hash keccak256 -- **Audio procédural** : musique et SFX générés en temps réel (pas de fichiers audio) -- **Graphismes procéduraux** : sprites générés par Phaser (pas d'assets externes) -- **Animations premium** : zoom, shake, particules, clash cinématique -- **Smart contract** : 7/7 tests passants, déployé sur Hardhat local - ## Développement local ### Prérequis @@ -97,14 +85,6 @@ Le jeu repose sur un **pattern commit-reveal** blockchain pour garantir l'intég Voir [docs/GAME_DESIGN.md](docs/GAME_DESIGN.md) pour la documentation complète. -## Documentation - -- [Architecture](docs/ARCHITECTURE.md) -- [API Reference](docs/API_REFERENCE.md) -- [Smart Contract](docs/SMART_CONTRACT.md) -- [Déploiement](docs/DEPLOYMENT.md) -- [Game Design](docs/GAME_DESIGN.md) - ## Licence MIT diff --git a/apps/server/package.json b/apps/server/package.json index 039e4de..d1206ab 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -17,7 +17,6 @@ "cors": "^2.8.5", "ethers": "^6.13.0", "express": "^4.21.0", - "express-rate-limit": "^8.5.2", "ioredis": "^5.4.0", "socket.io": "^4.8.0" }, diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 4e899e4..50f7499 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -2,7 +2,6 @@ import express from 'express'; import { createServer as createHttpServer } from 'http'; import { Server } from 'socket.io'; import cors from 'cors'; -import rateLimit from 'express-rate-limit'; import { setupSocketHandlers } from './socket/handlers.js'; import { apiRoutes } from './api/routes.js'; import { listenToContractEvents } from './blockchain/service.js'; @@ -11,16 +10,6 @@ export function createServer(port: number | string) { const app = express(); app.use(cors({ origin: process.env.CORS_ORIGIN || '*' })); app.use(express.json()); - - // Rate limiting for REST API - const apiLimiter = rateLimit({ - windowMs: 60 * 1000, // 1 minute - max: 120, - standardHeaders: true, - legacyHeaders: false, - message: { error: 'Too many requests, please try again later.' }, - }); - app.use('/api', apiLimiter); app.use('/api', apiRoutes); const httpServer = createHttpServer(app); diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 3120da1..2a84bb7 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -5,16 +5,6 @@ RUN npm install -g pnpm@9 COPY . . RUN pnpm install --frozen-lockfile RUN pnpm --filter @rps-royale/shared build - -ARG NEXT_PUBLIC_API_URL -ARG NEXT_PUBLIC_CONTRACT_ADDRESS -ARG NEXT_PUBLIC_CHAIN_ID -ARG NEXT_PUBLIC_HARDHAT_RPC_URL -ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} -ENV NEXT_PUBLIC_CONTRACT_ADDRESS=${NEXT_PUBLIC_CONTRACT_ADDRESS} -ENV NEXT_PUBLIC_CHAIN_ID=${NEXT_PUBLIC_CHAIN_ID} -ENV NEXT_PUBLIC_HARDHAT_RPC_URL=${NEXT_PUBLIC_HARDHAT_RPC_URL} - RUN pnpm --filter @rps-royale/web build ENV NODE_ENV=production EXPOSE 3000 diff --git a/apps/web/src/app/play/PlayClient.tsx b/apps/web/src/app/play/PlayClient.tsx new file mode 100644 index 0000000..8593b42 --- /dev/null +++ b/apps/web/src/app/play/PlayClient.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { useEffect, useRef } from 'react'; +import { initGame } from '@/phaser/Game'; + +export default function PlayClient() { + const containerRef = useRef(null); + + useEffect(() => { + if (!containerRef.current) return; + const game = initGame('phaser-container'); + return () => { + game.destroy(true); + }; + }, []); + + return ( +
+
+
+ ); +} diff --git a/apps/web/src/app/play/page.tsx b/apps/web/src/app/play/page.tsx index 8a707ae..60631a1 100644 --- a/apps/web/src/app/play/page.tsx +++ b/apps/web/src/app/play/page.tsx @@ -1,63 +1,9 @@ 'use client'; -import { useEffect, useRef, useState } from 'react'; +import dynamic from 'next/dynamic'; + +const PlayClient = dynamic(() => import('./PlayClient'), { ssr: false }); export default function PlayPage() { - const containerRef = useRef(null); - const [error, setError] = useState(null); - const [loaded, setLoaded] = useState(false); - const gameRef = useRef(null); - - useEffect(() => { - let mounted = true; - const init = async () => { - try { - const { initGame } = await import('@/phaser/Game'); - if (!mounted || !containerRef.current) return; - await new Promise((r) => setTimeout(r, 200)); - if (!mounted || !containerRef.current) return; - const game = initGame('phaser-container'); - gameRef.current = game; - setLoaded(true); - } catch (err: any) { - console.error('Failed to init Phaser game:', err); - setError(err?.message || 'Erreur de chargement du jeu'); - } - }; - init(); - return () => { - mounted = false; - if (gameRef.current) { - gameRef.current.destroy(true); - gameRef.current = null; - } - }; - }, []); - - if (error) { - return ( -
-

Erreur de chargement

-

{error}

-
- ); - } - - return ( -
- {!loaded && ( -
-

- Chargement de l'arène... -

-
- )} -
- ); + return ; } diff --git a/apps/web/src/phaser/audio/AudioManager.ts b/apps/web/src/phaser/audio/AudioManager.ts deleted file mode 100644 index ddc4757..0000000 --- a/apps/web/src/phaser/audio/AudioManager.ts +++ /dev/null @@ -1,222 +0,0 @@ -// Procedural audio using Web Audio API — no external files needed -class AudioManager { - private ctx: AudioContext | null = null; - private masterGain: GainNode | null = null; - private ambienceGain: GainNode | null = null; - private sfxGain: GainNode | null = null; - private ambienceOsc: OscillatorNode | null = null; - private ambienceLFO: OscillatorNode | null = null; - - private getCtx(): AudioContext { - if (!this.ctx) { - this.ctx = new AudioContext(); - this.masterGain = this.ctx.createGain(); - this.masterGain.gain.value = 0.6; - this.masterGain.connect(this.ctx.destination); - - this.ambienceGain = this.ctx.createGain(); - this.ambienceGain.gain.value = 0; - this.ambienceGain.connect(this.masterGain); - - this.sfxGain = this.ctx.createGain(); - this.sfxGain.gain.value = 1.0; - this.sfxGain.connect(this.masterGain); - } - return this.ctx; - } - - resume() { - const ctx = this.getCtx(); - if (ctx.state === 'suspended') ctx.resume(); - } - - // Low drone for lobby background - startLobbyAmbience() { - const ctx = this.getCtx(); - if (this.ambienceOsc) return; - this.ambienceOsc = ctx.createOscillator(); - this.ambienceOsc.type = 'sine'; - this.ambienceOsc.frequency.value = 55; - this.ambienceLFO = ctx.createOscillator(); - this.ambienceLFO.type = 'sine'; - this.ambienceLFO.frequency.value = 0.2; - const lfoGain = ctx.createGain(); - lfoGain.gain.value = 8; - this.ambienceLFO.connect(lfoGain); - lfoGain.connect(this.ambienceOsc.frequency); - this.ambienceOsc.connect(this.ambienceGain!); - this.ambienceOsc.start(); - this.ambienceLFO.start(); - this.ambienceGain!.gain.setTargetAtTime(0.25, ctx.currentTime, 0.5); - } - - stopAmbience() { - if (!this.ctx || !this.ambienceGain) return; - this.ambienceGain.gain.setTargetAtTime(0, this.ctx.currentTime, 0.3); - setTimeout(() => { - this.ambienceOsc?.stop(); - this.ambienceLFO?.stop(); - this.ambienceOsc = null; - this.ambienceLFO = null; - }, 500); - } - - // Tension riser for suspense phase - playSuspenseRiser() { - const ctx = this.getCtx(); - const osc = ctx.createOscillator(); - const gain = ctx.createGain(); - osc.type = 'sawtooth'; - osc.frequency.setValueAtTime(80, ctx.currentTime); - osc.frequency.exponentialRampToValueAtTime(400, ctx.currentTime + 3.5); - gain.gain.setValueAtTime(0, ctx.currentTime); - gain.gain.linearRampToValueAtTime(0.15, ctx.currentTime + 1); - gain.gain.linearRampToValueAtTime(0, ctx.currentTime + 3.8); - osc.connect(gain); - gain.connect(this.sfxGain!); - osc.start(); - osc.stop(ctx.currentTime + 4); - - // Heartbeat layer - this.playHeartbeat(3.5, 0.12); - } - - playHeartbeat(durationSec: number, intensity: number) { - const ctx = this.getCtx(); - const start = ctx.currentTime; - const count = Math.floor(durationSec / 0.45); - for (let i = 0; i < count; i++) { - const t = start + i * 0.45; - const osc = ctx.createOscillator(); - const gain = ctx.createGain(); - osc.type = 'sine'; - osc.frequency.value = 60; - gain.gain.setValueAtTime(0, t); - gain.gain.linearRampToValueAtTime(intensity, t + 0.05); - gain.gain.exponentialRampToValueAtTime(0.001, t + 0.2); - osc.connect(gain); - gain.connect(this.sfxGain!); - osc.start(t); - osc.stop(t + 0.25); - } - } - - // UI click sound - playClick() { - const ctx = this.getCtx(); - const osc = ctx.createOscillator(); - const gain = ctx.createGain(); - osc.type = 'triangle'; - osc.frequency.setValueAtTime(800, ctx.currentTime); - osc.frequency.exponentialRampToValueAtTime(1200, ctx.currentTime + 0.05); - gain.gain.setValueAtTime(0.2, ctx.currentTime); - gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.08); - osc.connect(gain); - gain.connect(this.sfxGain!); - osc.start(); - osc.stop(ctx.currentTime + 0.1); - } - - // Commit confirmation sound - playCommit() { - const ctx = this.getCtx(); - const osc = ctx.createOscillator(); - const gain = ctx.createGain(); - osc.type = 'square'; - osc.frequency.setValueAtTime(200, ctx.currentTime); - osc.frequency.exponentialRampToValueAtTime(600, ctx.currentTime + 0.15); - gain.gain.setValueAtTime(0.15, ctx.currentTime); - gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.25); - osc.connect(gain); - gain.connect(this.sfxGain!); - osc.start(); - osc.stop(ctx.currentTime + 0.3); - } - - // Victory fanfare - playVictory() { - const ctx = this.getCtx(); - const notes = [523.25, 659.25, 783.99, 1046.5]; - notes.forEach((freq, i) => { - const t = ctx.currentTime + i * 0.12; - const osc = ctx.createOscillator(); - const gain = ctx.createGain(); - osc.type = 'triangle'; - osc.frequency.value = freq; - gain.gain.setValueAtTime(0, t); - gain.gain.linearRampToValueAtTime(0.18, t + 0.02); - gain.gain.exponentialRampToValueAtTime(0.001, t + 0.35); - osc.connect(gain); - gain.connect(this.sfxGain!); - osc.start(t); - osc.stop(t + 0.4); - }); - - // Noise burst - const bufferSize = ctx.sampleRate * 0.3; - const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); - const data = buffer.getChannelData(0); - for (let i = 0; i < bufferSize; i++) { - data[i] = Math.random() * 2 - 1; - } - const noise = ctx.createBufferSource(); - noise.buffer = buffer; - const noiseGain = ctx.createGain(); - noiseGain.gain.setValueAtTime(0.1, ctx.currentTime); - noiseGain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3); - noise.connect(noiseGain); - noiseGain.connect(this.sfxGain!); - noise.start(); - } - - // Defeat sound - playDefeat() { - const ctx = this.getCtx(); - const osc = ctx.createOscillator(); - const gain = ctx.createGain(); - osc.type = 'sawtooth'; - osc.frequency.setValueAtTime(300, ctx.currentTime); - osc.frequency.exponentialRampToValueAtTime(80, ctx.currentTime + 0.6); - gain.gain.setValueAtTime(0.18, ctx.currentTime); - gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.7); - osc.connect(gain); - gain.connect(this.sfxGain!); - osc.start(); - osc.stop(ctx.currentTime + 0.8); - } - - // Draw sound - playDraw() { - const ctx = this.getCtx(); - const osc = ctx.createOscillator(); - const gain = ctx.createGain(); - osc.type = 'sine'; - osc.frequency.value = 440; - gain.gain.setValueAtTime(0.12, ctx.currentTime); - gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4); - osc.connect(gain); - gain.connect(this.sfxGain!); - osc.start(); - osc.stop(ctx.currentTime + 0.5); - } - - // Impact sound when result is shown - playImpact() { - const ctx = this.getCtx(); - const bufferSize = ctx.sampleRate * 0.2; - const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); - const data = buffer.getChannelData(0); - for (let i = 0; i < bufferSize; i++) { - data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (ctx.sampleRate * 0.03)); - } - const src = ctx.createBufferSource(); - src.buffer = buffer; - const gain = ctx.createGain(); - gain.gain.value = 0.25; - src.connect(gain); - gain.connect(this.sfxGain!); - src.start(); - } -} - -export const audio = new AudioManager(); diff --git a/apps/web/src/phaser/objects/AssetLoader.ts b/apps/web/src/phaser/objects/AssetLoader.ts deleted file mode 100644 index 9d13c5b..0000000 --- a/apps/web/src/phaser/objects/AssetLoader.ts +++ /dev/null @@ -1,106 +0,0 @@ -import * as Phaser from 'phaser'; - -export function generateAssets(scene: Phaser.Scene) { - const g = scene.make.graphics({ x: 0, y: 0 }); - - // Rock — golem-like hexagon - g.fillStyle(0x64748b, 1); - g.fillCircle(64, 64, 56); - g.fillStyle(0x475569, 1); - g.fillCircle(64, 64, 44); - g.fillStyle(0x94a3b8, 1); - g.fillCircle(48, 48, 12); - g.fillCircle(80, 48, 12); - g.fillStyle(0x1e293b, 1); - g.fillCircle(48, 48, 6); - g.fillCircle(80, 48, 6); - g.generateTexture('rock', 128, 128); - g.clear(); - - // Paper — flowing scroll / leaf shape - g.fillStyle(0x22c55e, 1); - g.beginPath(); - g.moveTo(64, 8); - g.lineTo(120, 40); - g.lineTo(100, 120); - g.lineTo(28, 120); - g.lineTo(8, 40); - g.closePath(); - g.fillPath(); - g.fillStyle(0x15803d, 1); - g.beginPath(); - g.moveTo(64, 24); - g.lineTo(100, 48); - g.lineTo(88, 104); - g.lineTo(40, 104); - g.lineTo(28, 48); - g.closePath(); - g.fillPath(); - g.generateTexture('paper', 128, 128); - g.clear(); - - // Scissors — blade crossed shape - g.lineStyle(8, 0xef4444, 1); - g.beginPath(); - g.moveTo(20, 20); - g.lineTo(108, 108); - g.strokePath(); - g.beginPath(); - g.moveTo(108, 20); - g.lineTo(20, 108); - g.strokePath(); - g.fillStyle(0xfca5a5, 1); - g.fillCircle(64, 64, 18); - g.fillStyle(0xef4444, 1); - g.fillCircle(64, 64, 10); - g.generateTexture('scissors', 128, 128); - g.clear(); - - // Avatar placeholder (glowing orb) - g.fillStyle(0x06b6d4, 1); - g.fillCircle(64, 64, 60); - g.fillStyle(0x22d3ee, 1); - g.fillCircle(64, 64, 48); - g.fillStyle(0x0f172a, 1); - g.fillCircle(64, 64, 36); - g.generateTexture('avatar', 128, 128); - g.clear(); - - // AI avatar (purple orb) - g.fillStyle(0xa855f7, 1); - g.fillCircle(64, 64, 60); - g.fillStyle(0xc084fc, 1); - g.fillCircle(64, 64, 48); - g.fillStyle(0x0f172a, 1); - g.fillCircle(64, 64, 36); - g.generateTexture('avatarAI', 128, 128); - g.clear(); - - // VS badge - g.fillStyle(0xf59e0b, 1); - g.fillCircle(32, 32, 28); - g.fillStyle(0x78350f, 1); - g.fillCircle(32, 32, 22); - g.generateTexture('vs', 64, 64); - g.clear(); - - // Spark particle - g.fillStyle(0xffffff, 1); - g.fillCircle(4, 4, 4); - g.generateTexture('spark', 8, 8); - g.clear(); - - // Flare particle - g.fillStyle(0xffffff, 1); - g.fillCircle(8, 8, 8); - g.generateTexture('flare', 16, 16); - g.clear(); - - // Ring glow - g.lineStyle(4, 0x22d3ee, 1); - g.strokeCircle(64, 64, 60); - g.generateTexture('ring', 128, 128); - g.clear(); - - g.destroy(); -} diff --git a/apps/web/src/phaser/scenes/ArenaScene.ts b/apps/web/src/phaser/scenes/ArenaScene.ts index 2e175d8..c62d423 100644 --- a/apps/web/src/phaser/scenes/ArenaScene.ts +++ b/apps/web/src/phaser/scenes/ArenaScene.ts @@ -3,7 +3,6 @@ import { UIButton } from '../objects/UIButton'; import { socket } from '@/lib/socket'; import { keccak256, solidityPacked, parseEther, BrowserProvider, Contract } from 'ethers'; import { Choice, MatchStatus, RPSArenaAbi } from '@rps-royale/shared'; -import { audio } from '../audio/AudioManager'; interface ArenaData { match: { @@ -27,14 +26,8 @@ export class ArenaScene extends Phaser.Scene { private infoText!: Phaser.GameObjects.Text; private choiceButtons: UIButton[] = []; private actionButton!: UIButton; + private particles!: any; private phase: 'commit' | 'reveal' = 'commit'; - private p1Avatar!: Phaser.GameObjects.Image; - private p2Avatar!: Phaser.GameObjects.Image; - private p1Label!: Phaser.GameObjects.Text; - private p2Label!: Phaser.GameObjects.Text; - private p1Lock!: Phaser.GameObjects.Text; - private p2Lock!: Phaser.GameObjects.Text; - private waitingIndicator!: Phaser.GameObjects.Text; constructor() { super({ key: 'ArenaScene' }); @@ -61,50 +54,14 @@ export class ArenaScene extends Phaser.Scene { return this.matchId.startsWith('ai_'); } - createCyberpunkBackground(width: number, height: number) { - this.add.rectangle(width / 2, height / 2, width, height, 0x0f172a); - - const g = this.add.graphics(); - g.lineStyle(1, 0x1e293b, 0.6); - for (let i = 0; i < 12; i++) { - const y = height / 2 + i * 32; - g.lineBetween(0, y, width, y); - } - const cx = width / 2; - for (let i = -10; i <= 10; i++) { - const x = cx + i * 80; - g.lineStyle(1, 0x1e293b, 0.4); - g.lineBetween(x, height / 2, cx + i * 20, height); - } - g.lineStyle(2, 0x06b6d4, 0.8); - g.lineBetween(0, height / 2, width, height / 2); - - const colColors = [0x06b6d4, 0xa855f7, 0xec4899]; - for (let i = 0; i < 6; i++) { - const x = (i + 0.5) * (width / 6); - const color = colColors[i % 3]; - this.add.rectangle(x, height / 2 - 100, 4, height / 2 - 80, color).setAlpha(0.3); - } - - this.add.particles(0, 0, 'spark', { - x: { min: 0, max: width }, - y: { min: 0, max: height }, - quantity: 1, - frequency: 180, - lifespan: 2000, - alpha: { start: 0.25, end: 0 }, - scale: { start: 0.4, end: 0 }, - tint: [0x06b6d4, 0xa855f7], - }); - } - create() { const { width, height } = this.scale; - this.createCyberpunkBackground(width, height); + // Background gradient feel via dark rects + this.add.rectangle(width / 2, height / 2, width, height, 0x0f172a); this.add - .text(width / 2, 36, `TABLE #${this.matchId.slice(-6)}`, { + .text(width / 2, 40, `TABLE #${this.matchId.slice(-6)}`, { fontSize: '28px', fontFamily: 'ui-sans-serif, system-ui, sans-serif', color: '#f8fafc', @@ -113,97 +70,48 @@ export class ArenaScene extends Phaser.Scene { .setOrigin(0.5); this.infoText = this.add - .text(width / 2, 76, `Mise: ${this.betAmount} ETH | En attente du commit`, { + .text(width / 2, 80, `Mise: 0.01 ETH | En attente du commit`, { fontSize: '16px', color: '#94a3b8', }) .setOrigin(0.5); this.statusText = this.add - .text(width / 2, height / 2 - 60, 'CHOISIS TON SIGNE', { - fontSize: '36px', + .text(width / 2, height / 2, 'CHOISIS TON SIGNE', { + fontSize: '32px', color: '#22d3ee', fontStyle: 'bold', }) .setOrigin(0.5); - // Player avatars with lock indicators - const p1Name = this.isPlayer1 ? 'Toi' : (this.isAIMatch ? 'IA' : 'Adversaire'); - const p2Name = this.isPlayer1 ? (this.isAIMatch ? 'IA' : 'Adversaire') : 'Toi'; - - this.p1Avatar = this.add.image(140, height / 2 - 40, 'avatar').setScale(1.0); - this.p2Avatar = this.add.image(width - 140, height / 2 - 40, this.isAIMatch ? 'avatarAI' : 'avatar').setScale(1.0); - - this.add.image(140, height / 2 - 40, 'ring').setScale(1.1).setAlpha(0.6); - this.add.image(width - 140, height / 2 - 40, 'ring').setScale(1.1).setAlpha(0.6); - - this.p1Label = this.add.text(140, height / 2 + 50, p1Name, { fontSize: '16px', color: '#cbd5e1' }).setOrigin(0.5); - this.p2Label = this.add.text(width - 140, height / 2 + 50, p2Name, { fontSize: '16px', color: '#cbd5e1' }).setOrigin(0.5); - - this.p1Lock = this.add.text(140, height / 2 - 90, '', { fontSize: '24px', color: '#22c55e' }).setOrigin(0.5); - this.p2Lock = this.add.text(width - 140, height / 2 - 90, '', { fontSize: '24px', color: '#22c55e' }).setOrigin(0.5); - - this.add.image(width / 2, height / 2 - 40, 'vs').setScale(1.0).setAlpha(0.8); - - // Waiting for opponent text (initially hidden) - this.waitingIndicator = this.add - .text(width / 2, height / 2 + 10, '', { fontSize: '14px', color: '#f59e0b' }) - .setOrigin(0.5) - .setAlpha(0); - - // Choice buttons with icons + // Choice buttons const choices = [ - { label: 'PIERRE', value: Choice.Rock, tex: 'rock' }, - { label: 'FEUILLE', value: Choice.Paper, tex: 'paper' }, - { label: 'CISEAUX', value: Choice.Scissors, tex: 'scissors' }, + { label: 'PIERRE', value: Choice.Rock }, + { label: 'FEUILLE', value: Choice.Paper }, + { label: 'CISEAUX', value: Choice.Scissors }, ]; choices.forEach((c, i) => { - const x = width / 2 - 280 + i * 280; - const y = height / 2 + 100; - const icon = this.add.image(x, y - 50, c.tex).setScale(0.6).setAlpha(0.8); - const btn = new UIButton(this, x, y, c.label, () => { - audio.playClick(); + const btn = new UIButton(this, width / 2 - 280 + i * 280, height / 2 + 60, c.label, () => { this.selectChoice(c.value); - choices.forEach((cc, ii) => { - (this.children.getByName(`choiceIcon_${ii}`) as Phaser.GameObjects.Image)?.setAlpha(ii === i ? 1 : 0.3); - }); }); - icon.setName(`choiceIcon_${i}`); this.choiceButtons.push(btn); }); - this.actionButton = new UIButton(this, width / 2, height / 2 + 220, 'Valider le Commit', () => { - if (this.phase === 'commit') { - audio.playCommit(); - this.sendCommit(); - } else if (this.phase === 'reveal') { - audio.playCommit(); - this.sendReveal(); - } - }, 280); + // Action button (commit / reveal) + this.actionButton = new UIButton(this, width / 2, height / 2 + 160, 'Valider le Commit', () => { + if (this.phase === 'commit') this.sendCommit(); + else if (this.phase === 'reveal') this.sendReveal(); + }); this.actionButton.setDisabled(true); - socket.on('match:commitReceived', (data: { matchId: string; playerAddress: string }) => { + // Particle emitter for atmosphere (initially off) + this.createParticles(); + + // Socket listeners + socket.on('match:commitReceived', (data: { matchId: string }) => { if (data.matchId !== this.matchId) return; - const isMe = data.playerAddress.toLowerCase() === this.playerAddress.toLowerCase(); - if (isMe) { - this.p1Lock.setText('🔒'); - this.tweens.add({ targets: this.p1Lock, scaleX: 1.5, scaleY: 1.5, duration: 200, yoyo: true }); - } else { - this.p2Lock.setText('🔒'); - this.tweens.add({ targets: this.p2Lock, scaleX: 1.5, scaleY: 1.5, duration: 200, yoyo: true }); - } this.infoText.setText('Un joueur a commité... en attente du second'); - this.waitingIndicator.setText('En attente de l\'adversaire...').setAlpha(1); - this.tweens.add({ - targets: this.waitingIndicator, - alpha: 0.4, - duration: 800, - yoyo: true, - repeat: -1, - ease: 'Sine.easeInOut', - }); }); socket.on('match:suspenseStart', (matchId: string) => { @@ -294,44 +202,40 @@ export class ArenaScene extends Phaser.Scene { const { width, height } = this.scale; this.statusText.setText('SUSPENSE').setColor('#a855f7'); this.infoText.setText('Les deux joueurs ont commité. Prépare-toi...'); - this.waitingIndicator.setAlpha(0); - this.tweens.killTweensOf(this.waitingIndicator); - audio.playSuspenseRiser(); + // Camera zoom and shake + this.cameras.main.zoomTo(1.1, 2000, 'Sine.easeInOut'); + this.cameras.main.shake(3000, 0.005); - this.cameras.main.zoomTo(1.15, 2000, 'Sine.easeInOut'); - this.cameras.main.shake(3000, 0.008); - - // Both avatars pulse with energy - this.tweens.add({ targets: this.p1Avatar, scaleX: 1.3, scaleY: 1.3, duration: 500, yoyo: true, repeat: 3 }); - this.tweens.add({ targets: this.p2Avatar, scaleX: 1.3, scaleY: 1.3, duration: 500, yoyo: true, repeat: 3 }); + // Intense particles + this.particles.setFrequency(30); + this.particles.setLifespan(2000); + // Flash effect const flash = this.add.rectangle(width / 2, height / 2, width, height, 0xffffff).setAlpha(0); this.tweens.add({ targets: flash, - alpha: 0.12, + alpha: 0.15, duration: 200, yoyo: true, repeat: 6, onComplete: () => flash.destroy(), }); + // Countdown text let countdown = 4; const countdownText = this.add - .text(width / 2, height / 2 - 120, `${countdown}`, { - fontSize: '120px', + .text(width / 2, height / 2 - 100, `${countdown}`, { + fontSize: '96px', color: '#f8fafc', fontStyle: '900', }) .setOrigin(0.5) - .setAlpha(0) - .setScale(0.8); + .setAlpha(0); this.tweens.add({ targets: countdownText, alpha: 1, - scaleX: 1.2, - scaleY: 1.2, duration: 500, repeat: 3, yoyo: true, @@ -376,6 +280,25 @@ export class ArenaScene extends Phaser.Scene { this.statusText.setText('Révélation envoyée...'); } + createParticles() { + const graphics = this.make.graphics({ x: 0, y: 0 }); + graphics.fillStyle(0xffffff, 1); + graphics.fillCircle(4, 4, 4); + graphics.generateTexture('spark', 8, 8); + graphics.destroy(); + + this.particles = this.add.particles(0, 0, 'spark', { + x: { min: 0, max: this.scale.width }, + y: { min: 0, max: this.scale.height }, + quantity: 1, + frequency: 200, + lifespan: 2000, + alpha: { start: 0.2, end: 0 }, + scale: { start: 0.4, end: 0 }, + tint: [0x06b6d4, 0xa855f7], + }); + } + shutdown() { socket.off('match:commitReceived'); socket.off('match:suspenseStart'); diff --git a/apps/web/src/phaser/scenes/BootScene.ts b/apps/web/src/phaser/scenes/BootScene.ts index 3141a75..1b980b5 100644 --- a/apps/web/src/phaser/scenes/BootScene.ts +++ b/apps/web/src/phaser/scenes/BootScene.ts @@ -1,81 +1,55 @@ import * as Phaser from 'phaser'; import { UIButton } from '../objects/UIButton'; -import { generateAssets } from '../objects/AssetLoader'; -import { audio } from '../audio/AudioManager'; export class BootScene extends Phaser.Scene { constructor() { super({ key: 'BootScene' }); } - preload() { - generateAssets(this); - } - create() { const { width, height } = this.scale; - this.add.rectangle(width / 2, height / 2, width, height, 0x0f172a); - - // Animated background stars/particles - this.add.particles(0, 0, 'flare', { - x: { min: 0, max: width }, - y: { min: 0, max: height }, - quantity: 1, - frequency: 80, - lifespan: 4000, - alpha: { start: 0.25, end: 0 }, - scale: { start: 0.6, end: 0 }, - tint: [0x06b6d4, 0xa855f7, 0xec4899], - }); - - // Pulsing glow behind title - const glow = this.add.circle(width / 2, height / 2 - 120, 180, 0x06b6d4, 0.15); - this.tweens.add({ - targets: glow, - scaleX: 1.3, - scaleY: 1.3, - alpha: 0.05, - duration: 2000, - yoyo: true, - repeat: -1, - ease: 'Sine.easeInOut', - }); - this.add - .text(width / 2, height / 2 - 120, 'THE ARENA', { - fontSize: '80px', + .text(width / 2, height / 2 - 80, 'THE ARENA', { + fontSize: '64px', fontFamily: 'ui-sans-serif, system-ui, sans-serif', color: '#f8fafc', fontStyle: '900', }) - .setOrigin(0.5) - .setShadow(0, 0, '#06b6d4', 20, true, true); + .setOrigin(0.5); this.add - .text(width / 2, height / 2 - 40, 'RPS ROYALE', { - fontSize: '28px', + .text(width / 2, height / 2 - 20, 'RPS Royale', { + fontSize: '24px', fontFamily: 'ui-sans-serif, system-ui, sans-serif', color: '#94a3b8', - letterSpacing: 8, }) .setOrigin(0.5); - const leftAvatar = this.add.image(width / 2 - 220, height / 2 + 40, 'avatar').setScale(0.9); - const rightAvatar = this.add.image(width / 2 + 220, height / 2 + 40, 'avatarAI').setScale(0.9); - this.tweens.add({ targets: leftAvatar, angle: 360, duration: 20000, repeat: -1, ease: 'Linear' }); - this.tweens.add({ targets: rightAvatar, angle: -360, duration: 20000, repeat: -1, ease: 'Linear' }); - - this.add.image(width / 2, height / 2 + 40, 'vs').setScale(1.2); - - new UIButton(this, width / 2, height / 2 + 180, 'Entrer dans\'Arène', () => { - audio.resume(); - audio.playClick(); - audio.startLobbyAmbience(); + new UIButton(this, width / 2, height / 2 + 80, 'Entrer dans l\'Arène', () => { this.cameras.main.fadeOut(400, 0, 0, 0); this.time.delayedCall(400, () => { this.scene.start('LobbyScene'); }); }); + + // Simple particle effect for atmosphere + const particles = this.add.particles(0, 0, 'flare', { + x: { min: 0, max: width }, + y: { min: 0, max: height }, + quantity: 1, + frequency: 100, + lifespan: 3000, + alpha: { start: 0.3, end: 0 }, + scale: { start: 0.5, end: 0 }, + tint: [0x06b6d4, 0xa855f7, 0xec4899], + }); + + // Create a simple flare texture procedurally + const graphics = this.make.graphics({ x: 0, y: 0, }); + graphics.fillStyle(0xffffff, 1); + graphics.fillCircle(8, 8, 8); + graphics.generateTexture('flare', 16, 16); + graphics.destroy(); } } diff --git a/apps/web/src/phaser/scenes/LobbyScene.ts b/apps/web/src/phaser/scenes/LobbyScene.ts index 2a060fc..2441094 100644 --- a/apps/web/src/phaser/scenes/LobbyScene.ts +++ b/apps/web/src/phaser/scenes/LobbyScene.ts @@ -1,7 +1,6 @@ import * as Phaser from 'phaser'; import { UIButton } from '../objects/UIButton'; import { socket } from '@/lib/socket'; -import { audio } from '../audio/AudioManager'; export class LobbyScene extends Phaser.Scene { private tablesText: Phaser.GameObjects.Text[] = []; @@ -14,32 +13,14 @@ export class LobbyScene extends Phaser.Scene { create() { const { width, height } = this.scale; - this.add.rectangle(width / 2, height / 2, width, height, 0x0f172a); - - this.add.particles(0, 0, 'spark', { - x: { min: 0, max: width }, - y: { min: 0, max: height }, - quantity: 1, - frequency: 120, - lifespan: 3000, - alpha: { start: 0.2, end: 0 }, - scale: { start: 0.3, end: 0 }, - tint: [0x06b6d4, 0xa855f7], - }); - this.add - .text(width / 2, 50, 'SALLES DE JEU', { - fontSize: '44px', + .text(width / 2, 60, 'LOBBY', { + fontSize: '40px', fontFamily: 'ui-sans-serif, system-ui, sans-serif', color: '#f8fafc', fontStyle: '900', }) - .setOrigin(0.5) - .setShadow(0, 0, '#22d3ee', 10, true, true); - - this.add.image(width / 2 - 260, 50, 'rock').setScale(0.35); - this.add.image(width / 2, 50, 'paper').setScale(0.35); - this.add.image(width / 2 + 260, 50, 'scissors').setScale(0.35); + .setOrigin(0.5); this.statusText = this.add .text(width / 2, 120, '', { @@ -48,21 +29,17 @@ export class LobbyScene extends Phaser.Scene { }) .setOrigin(0.5); - new UIButton(this, width / 2 - 200, height - 180, 'Matchmaking 0.01 ETH', () => { - audio.playClick(); + new UIButton(this, width / 2 - 140, height / 2 + 80, 'Matchmaking (0.01 ETH)', () => { this.requestMatch('0.01'); - }, 280); + }); - new UIButton(this, width / 2 + 200, height - 180, 'Matchmaking 0.05 ETH', () => { - audio.playClick(); + new UIButton(this, width / 2 + 140, height / 2 + 80, 'Matchmaking (0.05 ETH)', () => { this.requestMatch('0.05'); - }, 280); + }); - const aiBtn = new UIButton(this, width / 2, height - 90, 'Jouer contre l\'IA', () => { - audio.playClick(); + new UIButton(this, width / 2, height / 2 + 160, 'Jouer contre l\'IA', () => { this.requestAIMatch('0.01'); - }, 320, 64); - aiBtn.setLabel('Jouer contre l\'IA'); + }); socket.emit('lobby:join'); @@ -71,7 +48,6 @@ export class LobbyScene extends Phaser.Scene { }); socket.on('match:found', (match) => { - audio.stopAmbience(); this.cameras.main.fadeOut(400, 0, 0, 0); this.time.delayedCall(400, () => { this.scene.start('ArenaScene', { match }); @@ -82,6 +58,7 @@ export class LobbyScene extends Phaser.Scene { getAddress(): string { const eth = (window as any).ethereum; if (eth?.selectedAddress) return eth.selectedAddress; + // Generate a guest address if no wallet let guest = (window as any).__guestAddress; if (!guest) { guest = '0x' + Array.from({ length: 40 }, () => Math.floor(Math.random() * 16).toString(16)).join(''); @@ -103,13 +80,14 @@ export class LobbyScene extends Phaser.Scene { } renderTables(tables: any[]) { + // Clear previous this.tablesText.forEach((t) => t.destroy()); this.tablesText = []; const { width } = this.scale; if (tables.length === 0) { const t = this.add - .text(width / 2, 200, 'Aucune table active. Créez-en une !', { fontSize: '16px', color: '#64748b' }) + .text(width / 2, 220, 'Aucune table active. Créez-en une !', { fontSize: '16px', color: '#64748b' }) .setOrigin(0.5); this.tablesText.push(t); return; @@ -119,7 +97,7 @@ export class LobbyScene extends Phaser.Scene { const t = this.add .text( width / 2, - 180 + i * 44, + 200 + i * 40, `Table #${table.id.slice(0, 6)} — Mise: ${table.betAmount} ETH — Joueurs: ${table.players}/2`, { fontSize: '16px', color: '#cbd5e1' } ) diff --git a/apps/web/src/phaser/scenes/ResultScene.ts b/apps/web/src/phaser/scenes/ResultScene.ts index a641c46..8f55682 100644 --- a/apps/web/src/phaser/scenes/ResultScene.ts +++ b/apps/web/src/phaser/scenes/ResultScene.ts @@ -1,6 +1,5 @@ import * as Phaser from 'phaser'; import { UIButton } from '../objects/UIButton'; -import { audio } from '../audio/AudioManager'; interface ResultData { matchId: string; @@ -13,7 +12,6 @@ interface ResultData { } const choiceLabels = ['Pierre', 'Feuille', 'Ciseaux']; -const choiceTextures = ['rock', 'paper', 'scissors']; export class ResultScene extends Phaser.Scene { constructor() { @@ -41,89 +39,19 @@ export class ResultScene extends Phaser.Scene { color = '#ef4444'; } - // Pulsing glow behind result - const glow = this.add.circle(width / 2, height / 2 - 180, 200, parseInt(color.replace('#', '0x')), 0.12); - this.tweens.add({ - targets: glow, - scaleX: 1.4, - scaleY: 1.4, - alpha: 0.04, - duration: 1500, - yoyo: true, - repeat: -1, - ease: 'Sine.easeInOut', - }); - this.add - .text(width / 2, height / 2 - 180, resultText, { + .text(width / 2, height / 2 - 140, resultText, { fontSize: '72px', fontFamily: 'ui-sans-serif, system-ui, sans-serif', color, fontStyle: '900', }) - .setOrigin(0.5) - .setShadow(0, 0, color, 20, true, true); - - // Animated clash: icons start far apart and smash together - const p1Tex = choiceTextures[data.p1Choice]; - const p2Tex = choiceTextures[data.p2Choice]; - - const leftIcon = this.add.image(-80, height / 2 - 40, p1Tex).setScale(0.2).setAlpha(0); - const rightIcon = this.add.image(width + 80, height / 2 - 40, p2Tex).setScale(0.2).setAlpha(0); - - // Animate icons sliding in from sides and crashing - this.tweens.add({ - targets: leftIcon, - x: width / 2 - 100, - scaleX: 1.4, - scaleY: 1.4, - alpha: 1, - duration: 600, - ease: 'Back.out', - delay: 200, - }); - this.tweens.add({ - targets: rightIcon, - x: width / 2 + 100, - scaleX: 1.4, - scaleY: 1.4, - alpha: 1, - duration: 600, - ease: 'Back.out', - delay: 200, - }); - - // Crash effect after they meet - this.time.delayedCall(800, () => { - this.tweens.add({ - targets: [leftIcon, rightIcon], - scaleX: 1.6, - scaleY: 1.6, - duration: 150, - yoyo: true, - ease: 'Bounce.easeOut', - }); - - // Impact explosion at center - const explosion = this.add.particles(width / 2, height / 2 - 40, 'spark', { - speed: { min: 150, max: 500 }, - angle: { min: 0, max: 360 }, - quantity: 60, - lifespan: 1200, - alpha: { start: 1, end: 0 }, - scale: { start: 1.2, end: 0 }, - tint: isDraw ? 0x94a3b8 : isWinner ? 0x22c55e : 0xef4444, - }); - explosion?.explode(60); - this.time.delayedCall(1300, () => explosion?.destroy()); - }); - - this.add.image(width / 2, height / 2 - 40, 'vs').setScale(1.5).setAlpha(0.8); + .setOrigin(0.5); this.add .text( width / 2, - height / 2 + 60, + height / 2 - 60, `Toi: ${choiceLabels[data.isPlayer1 ? data.p1Choice : data.p2Choice]} vs Adversaire: ${ choiceLabels[data.isPlayer1 ? data.p2Choice : data.p1Choice] }`, @@ -135,41 +63,40 @@ export class ResultScene extends Phaser.Scene { this.add .text( width / 2, - height / 2 + 110, + height / 2, isWinner ? `Gain: ${data.payout} ETH` : 'Mise perdue', { fontSize: '28px', color: isWinner ? '#22c55e' : '#ef4444', fontStyle: 'bold' } ) .setOrigin(0.5); } - // Secondary lingering particles - const emitter = this.add.particles(width / 2, height / 2 - 40, 'spark', { - speed: { min: 50, max: 200 }, + // Impact particles + const graphics = this.make.graphics({ x: 0, y: 0, }); + graphics.fillStyle(0xffffff, 1); + graphics.fillCircle(4, 4, 4); + graphics.generateTexture('resultSpark', 8, 8); + graphics.destroy(); + + this.add.particles(width / 2, height / 2 - 60, 'resultSpark', { + speed: { min: 100, max: 300 }, angle: { min: 0, max: 360 }, - quantity: 20, - lifespan: 2000, - alpha: { start: 0.6, end: 0 }, - scale: { start: 0.6, end: 0 }, + quantity: 40, + lifespan: 1500, + alpha: { start: 0.8, end: 0 }, + scale: { start: 0.8, end: 0 }, tint: isDraw ? 0x94a3b8 : isWinner ? 0x22c55e : 0xef4444, }); - emitter?.explode(30); - new UIButton(this, width / 2, height / 2 + 220, 'Retour au Lobby', () => { - audio.playClick(); - audio.startLobbyAmbience(); + new UIButton(this, width / 2, height / 2 + 140, 'Retour au Lobby', () => { this.cameras.main.fadeOut(400, 0, 0, 0); this.time.delayedCall(400, () => { this.scene.start('LobbyScene'); }); }); - audio.playImpact(); - if (isDraw) audio.playDraw(); - else if (isWinner) audio.playVictory(); - else audio.playDefeat(); - + // Camera shake on win if (isWinner) { - this.cameras.main.shake(600, 0.015); + this.cameras.main.shake(500, 0.01); this.cameras.main.flash(300, 34, 197, 94); } } diff --git a/docker-compose.yml b/docker-compose.yml index 0cdc27a..b772cd3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -74,11 +74,6 @@ services: build: context: . dockerfile: apps/web/Dockerfile - args: - NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-https://api.jeu.cosmolan.fr} - NEXT_PUBLIC_CONTRACT_ADDRESS: ${NEXT_PUBLIC_CONTRACT_ADDRESS} - NEXT_PUBLIC_CHAIN_ID: ${NEXT_PUBLIC_CHAIN_ID:-31337} - NEXT_PUBLIC_HARDHAT_RPC_URL: ${NEXT_PUBLIC_HARDHAT_RPC_URL} container_name: rps-web restart: unless-stopped environment: diff --git a/docs/GAME_DESIGN.md b/docs/GAME_DESIGN.md deleted file mode 100644 index 57f921d..0000000 --- a/docs/GAME_DESIGN.md +++ /dev/null @@ -1,84 +0,0 @@ -# Game Design — The Arena RPS Royale - -## Concept - -The Arena RPS Royale est un jeu multijoueur **Pierre-Feuille-Ciseaux** en 2D premium avec paris basés sur la blockchain. Le gameplay repose sur un pattern **commit-reveal** garantissant l'impossibilité de tricher. - -## Mécaniques de jeu - -### 1. Commit (Phase de verrouillage) -- Le joueur choisit son signe (Pierre, Feuille ou Ciseaux) -- Le frontend calcule un hash : `keccak256(choice + nonce)` -- Le joueur envoie le hash au serveur et, si wallet connecté, à la blockchain -- Le choix reste secret : impossible de le deviner à partir du hash - -### 2. Suspense (Phase de tension) -- Quand les deux joueurs ont commité, une phase de suspense démarre -- Zoom caméra, shake, particules intenses -- Compte à rebours de 4 secondes -- Audio : riser montant + heartbeat - -### 3. Reveal (Phase de révélation) -- Les joueurs révèlent leur choix + nonce -- Le serveur (et le smart contract) vérifient que le hash correspond -- Le gagnant est déterminé selon les règles classiques - -### 4. Résultat (Cinématique) -- Les icônes s'animent depuis les côtés et se heurtent au centre -- Explosion de particules colorées selon le résultat -- Affichage du gain ou de la perte -- Audio : fanfare de victoire, son de défaite, ou son de match nul - -## Économie - -- Mise minimale : 0.001 ETH (configurable) -- Frais plateforme : 3% -- Gain : 97% du pot total au gagnant -- Match nul : remboursement 100% - -## Modes de jeu - -### Matchmaking (multijoueur) -- Queue automatique par montant de mise -- Appariement des deux joueurs -- Match enregistré sur la blockchain - -### Contre l'IA (solo) -- Si aucun adversaire n'est disponible, l'IA simule un adversaire -- Délai de réponse aléatoire (1–3s) pour le commit et le reveal -- Pas de transaction blockchain nécessaire -- Adresse IA : `0xA1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1` - -### Sans wallet (invité) -- Le jeu génère automatiquement une adresse guest si MetaMask n'est pas détecté -- L'invité peut jouer en mode IA sans connecter de wallet - -## Expérience visuelle - -### Scènes Phaser - -| Scène | Description | -|-------|-------------| -| BootScene | Écran titre avec particules flottantes, avatars tournants, glow pulsant | -| LobbyScene | Liste des tables, boutons de matchmaking, mode IA | -| ArenaScene | Arène cyberpunk avec grille perspective, colonnes néons, avatars, VS badge | -| ResultScene | Cinématique de clash des icônes avec explosion de particules | - -### Effets visuels -- **Particules** : étincelles flottantes, explosions d'impact -- **Camera** : zoom, shake, flash -- **Tweening** : pulsations, déplacements, apparitions -- **Glow** : halos néons autour des textes et avatars - -### Audio procédural -- **Lobby** : drone basse ambiant -- **Clic** : son triangulaire court -- **Commit** : montée de fréquence carrée -- **Suspense** : riser sawtooth + heartbeat -- **Victoire** : fanfare arpégée + bruit blanc -- **Défaite** : descente de fréquence sawtooth -- **Impact** : bruit blanc filtré exponentiellement - -## Contrôles -- Souris : survol, clic sur les boutons et icônes -- MetaMask : connexion wallet (optionnel) diff --git a/e2e/ai-match.spec.ts b/e2e/ai-match.spec.ts deleted file mode 100644 index 85d84e8..0000000 --- a/e2e/ai-match.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('The Arena — RPS Royale', () => { - test('play page loads with Phaser canvas', async ({ page }) => { - await page.goto('/play'); - - // Wait for Phaser canvas to appear - const canvas = page.locator('canvas'); - await canvas.waitFor({ state: 'visible', timeout: 10000 }); - - // BootScene title should be visible in the canvas (we can't easily read canvas text, - // but we can verify the canvas has a non-zero size) - const box = await canvas.boundingBox(); - expect(box).not.toBeNull(); - expect(box!.width).toBeGreaterThan(0); - expect(box!.height).toBeGreaterThan(0); - }); - - test('health endpoint responds', async ({ request }) => { - const response = await request.get('http://127.0.0.1:3051/api/health'); - expect(response.status()).toBe(200); - const body = await response.json(); - expect(body.status).toBe('ok'); - }); - - test('leaderboard page loads', async ({ page }) => { - const response = await page.goto('/leaderboard'); - expect(response?.status()).toBe(200); - }); - - test('home page loads', async ({ page }) => { - const response = await page.goto('/'); - expect(response?.status()).toBe(200); - await expect(page.locator('h1', { hasText: 'The Arena' })).toBeVisible(); - }); -}); diff --git a/package.json b/package.json index 905e73c..257cbb2 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,6 @@ "dev": "pnpm --parallel dev", "lint": "pnpm -r lint", "test": "pnpm -r test", - "test:e2e": "playwright test", "typecheck": "pnpm -r typecheck", "db:migrate": "pnpm --filter server db:migrate", "db:generate": "pnpm --filter server db:generate", @@ -18,8 +17,6 @@ "contract:deploy:sepolia": "pnpm --filter contracts deploy:sepolia" }, "devDependencies": { - "@playwright/test": "^1.60.0", - "playwright": "^1.60.0", "typescript": "^5.6.0" }, "packageManager": "pnpm@9.0.0" diff --git a/playwright.config.ts b/playwright.config.ts deleted file mode 100644 index 5d3de1d..0000000 --- a/playwright.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { defineConfig, devices } from '@playwright/test'; - -export default defineConfig({ - testDir: './e2e', - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, - reporter: 'list', - use: { - baseURL: 'http://127.0.0.1:3050', - trace: 'on-first-retry', - }, - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - ], -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6f7a15..51091d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,6 @@ importers: .: devDependencies: - '@playwright/test': - specifier: ^1.60.0 - version: 1.60.0 - playwright: - specifier: ^1.60.0 - version: 1.60.0 typescript: specifier: ^5.6.0 version: 5.9.3 @@ -47,9 +41,6 @@ importers: express: specifier: ^4.21.0 version: 4.22.2 - express-rate-limit: - specifier: ^8.5.2 - version: 8.5.2(express@4.22.2) ioredis: specifier: ^5.4.0 version: 5.10.1 @@ -89,7 +80,7 @@ importers: version: 2.2.4 next: specifier: ^15.1.0 - version: 15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 15.5.18(react-dom@19.2.6(react@19.2.6))(react@19.2.6) phaser: specifier: ^3.86.0 version: 3.90.0 @@ -803,11 +794,6 @@ packages: resolution: {integrity: sha512-q4n32/FNKIhQ3zQGGw5CvPF6GTvDCpYwIf7bEY/dZTZbgfDsHyjJwURxUJf3VQuuJj+fDIFl4+KkBVbw4Ef6jA==} engines: {node: '>= 12'} - '@playwright/test@1.60.0': - resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} - engines: {node: '>=18'} - hasBin: true - '@prisma/client@5.22.0': resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} engines: {node: '>=16.13'} @@ -1650,12 +1636,6 @@ packages: evp_bytestokey@1.0.3: resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} - express-rate-limit@8.5.2: - resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} - engines: {node: '>= 16'} - peerDependencies: - express: '>= 4.11' - express@4.22.2: resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==} engines: {node: '>= 0.10.0'} @@ -1762,11 +1742,6 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1965,10 +1940,6 @@ packages: resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} engines: {node: '>=12.22.0'} - ip-address@10.2.0: - resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} - engines: {node: '>= 12'} - ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -2409,16 +2380,6 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - playwright-core@1.60.0: - resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} - engines: {node: '>=18'} - hasBin: true - - playwright@1.60.0: - resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} - engines: {node: '>=18'} - hasBin: true - possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -3873,10 +3834,6 @@ snapshots: '@nomicfoundation/solidity-analyzer-linux-x64-musl': 0.1.2 '@nomicfoundation/solidity-analyzer-win32-x64-msvc': 0.1.2 - '@playwright/test@1.60.0': - dependencies: - playwright: 1.60.0 - '@prisma/client@5.22.0(prisma@5.22.0)': optionalDependencies: prisma: 5.22.0 @@ -4866,11 +4823,6 @@ snapshots: md5.js: 1.3.5 safe-buffer: 5.2.1 - express-rate-limit@8.5.2(express@4.22.2): - dependencies: - express: 4.22.2 - ip-address: 10.2.0 - express@4.22.2: dependencies: accepts: 1.3.8 @@ -5018,9 +4970,6 @@ snapshots: fs.realpath@1.0.0: {} - fsevents@2.3.2: - optional: true - fsevents@2.3.3: optional: true @@ -5304,8 +5253,6 @@ snapshots: transitivePeerDependencies: - supports-color - ip-address@10.2.0: {} - ipaddr.js@1.9.1: {} is-binary-path@2.1.0: @@ -5551,7 +5498,7 @@ snapshots: neo-async@2.6.2: {} - next@15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + next@15.5.18(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@next/env': 15.5.18 '@swc/helpers': 0.5.15 @@ -5569,7 +5516,6 @@ snapshots: '@next/swc-linux-x64-musl': 15.5.18 '@next/swc-win32-arm64-msvc': 15.5.18 '@next/swc-win32-x64-msvc': 15.5.18 - '@playwright/test': 1.60.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -5682,14 +5628,6 @@ snapshots: pirates@4.0.7: {} - playwright-core@1.60.0: {} - - playwright@1.60.0: - dependencies: - playwright-core: 1.60.0 - optionalDependencies: - fsevents: 2.3.2 - possible-typed-array-names@1.1.0: {} postcss-import@15.1.0(postcss@8.5.15): diff --git a/test-results/.last-run.json b/test-results/.last-run.json deleted file mode 100644 index cbcc1fb..0000000 --- a/test-results/.last-run.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "status": "passed", - "failedTests": [] -} \ No newline at end of file