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 26ebd70..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,11 +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; constructor() { super({ key: 'ArenaScene' }); @@ -58,56 +54,14 @@ export class ArenaScene extends Phaser.Scene { return this.matchId.startsWith('ai_'); } - createCyberpunkBackground(width: number, height: number) { - // Dark base - this.add.rectangle(width / 2, height / 2, width, height, 0x0f172a); - - // Grid floor perspective effect using simple lines - const g = this.add.graphics(); - g.lineStyle(1, 0x1e293b, 0.6); - // Horizontal perspective lines - for (let i = 0; i < 12; i++) { - const y = height / 2 + i * 32; - g.lineBetween(0, y, width, y); - } - // Vertical vanishing lines - 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); - - // Neon columns - 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); - } - - // Ambient particles - 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', @@ -116,69 +70,45 @@ 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 - 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); - - // VS badge in center - this.add.image(width / 2, height / 2 - 40, 'vs').setScale(1.0).setAlpha(0.8); - - // 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); + // Particle emitter for atmosphere (initially off) + this.createParticles(); + + // Socket listeners socket.on('match:commitReceived', (data: { matchId: string }) => { if (data.matchId !== this.matchId) return; this.infoText.setText('Un joueur a commité... en attente du second'); @@ -273,11 +203,15 @@ export class ArenaScene extends Phaser.Scene { this.statusText.setText('SUSPENSE').setColor('#a855f7'); this.infoText.setText('Les deux joueurs ont commité. Prépare-toi...'); - audio.playSuspenseRiser(); - + // Camera zoom and shake this.cameras.main.zoomTo(1.1, 2000, 'Sine.easeInOut'); this.cameras.main.shake(3000, 0.005); + // 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, @@ -288,9 +222,10 @@ export class ArenaScene extends Phaser.Scene { onComplete: () => flash.destroy(), }); + // Countdown text let countdown = 4; const countdownText = this.add - .text(width / 2, height / 2 - 120, `${countdown}`, { + .text(width / 2, height / 2 - 100, `${countdown}`, { fontSize: '96px', color: '#f8fafc', fontStyle: '900', @@ -345,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 46f920d..1b980b5 100644 --- a/apps/web/src/phaser/scenes/BootScene.ts +++ b/apps/web/src/phaser/scenes/BootScene.ts @@ -1,68 +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); - - // Ambient 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], - }); - 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 5121293..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() { @@ -42,26 +40,18 @@ export class ResultScene extends Phaser.Scene { } 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); - - const p1Tex = choiceTextures[data.p1Choice]; - const p2Tex = choiceTextures[data.p2Choice]; - - this.add.image(width / 2 - 160, height / 2 - 40, p1Tex).setScale(1.4); - this.add.image(width / 2 + 160, height / 2 - 40, p2Tex).setScale(1.4); - this.add.image(width / 2, height / 2 - 40, 'vs').setScale(1.5); + .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] }`, @@ -73,14 +63,21 @@ 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); } - const emitter = this.add.particles(width / 2, height / 2 - 40, 'spark', { + // 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: 40, @@ -89,22 +86,15 @@ export class ResultScene extends Phaser.Scene { scale: { start: 0.8, end: 0 }, tint: isDraw ? 0x94a3b8 : isWinner ? 0x22c55e : 0xef4444, }); - emitter?.explode(40); - 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(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: