diff --git a/package-lock.json b/package-lock.json index e9bc658..a553298 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,376 +12,450 @@ }, "devDependencies": { "@types/node": "^20.10.0", - "typescript": "^5.3.3", - "vite": "^5.0.8" + "typescript": "^5.9.3", + "vite": "^7.2.7" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ - "x64" + "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -392,6 +466,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -405,6 +480,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -418,6 +494,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -431,6 +508,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -444,6 +522,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -457,6 +536,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -470,6 +550,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -483,6 +564,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -496,6 +578,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -509,6 +592,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -522,6 +606,7 @@ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -535,6 +620,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -548,6 +634,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -561,6 +648,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -574,6 +662,7 @@ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -587,6 +676,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -600,6 +690,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -613,6 +704,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openharmony" @@ -626,6 +718,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -639,6 +732,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -652,6 +746,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -665,6 +760,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -674,59 +770,84 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", - "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "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", @@ -734,6 +855,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -753,6 +875,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -764,6 +887,7 @@ "version": "3.90.0", "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.90.0.tgz", "integrity": "sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==", + "license": "MIT", "dependencies": { "eventemitter3": "^5.0.1" } @@ -772,7 +896,21 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, "node_modules/postcss": { "version": "8.5.6", @@ -793,6 +931,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -807,6 +946,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "1.0.8" }, @@ -848,15 +988,34 @@ "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.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -869,23 +1028,28 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "7.2.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz", + "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", "dev": true, + "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -894,19 +1058,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.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 }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -927,6 +1097,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } } diff --git a/package.json b/package.json index 26f12cc..1710e41 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "devDependencies": { "@types/node": "^20.10.0", - "typescript": "^5.3.3", - "vite": "^5.0.8" + "typescript": "^5.9.3", + "vite": "^7.2.7" } } diff --git a/public/assets/audio/01. Ground Theme.mp3 b/public/assets/audio/01. Ground Theme.mp3 new file mode 100644 index 0000000..6c30c6b Binary files /dev/null and b/public/assets/audio/01. Ground Theme.mp3 differ diff --git a/public/assets/audio/1-up.mp3 b/public/assets/audio/1-up.mp3 new file mode 100644 index 0000000..535c142 Binary files /dev/null and b/public/assets/audio/1-up.mp3 differ diff --git a/public/assets/audio/champignon.mp3 b/public/assets/audio/champignon.mp3 new file mode 100644 index 0000000..e3fc54b Binary files /dev/null and b/public/assets/audio/champignon.mp3 differ diff --git a/public/assets/audio/game-over.mp3 b/public/assets/audio/game-over.mp3 new file mode 100644 index 0000000..b70d975 Binary files /dev/null and b/public/assets/audio/game-over.mp3 differ diff --git a/public/assets/audio/hurry-up.mp3 b/public/assets/audio/hurry-up.mp3 new file mode 100644 index 0000000..6d8d6d0 Binary files /dev/null and b/public/assets/audio/hurry-up.mp3 differ diff --git a/public/assets/audio/monde-termine.mp3 b/public/assets/audio/monde-termine.mp3 new file mode 100644 index 0000000..8fa0fe4 Binary files /dev/null and b/public/assets/audio/monde-termine.mp3 differ diff --git a/public/assets/audio/niveau-termine.mp3 b/public/assets/audio/niveau-termine.mp3 new file mode 100644 index 0000000..8a0c7dd Binary files /dev/null and b/public/assets/audio/niveau-termine.mp3 differ diff --git a/public/assets/audio/piece.mp3 b/public/assets/audio/piece.mp3 new file mode 100644 index 0000000..5570f4d Binary files /dev/null and b/public/assets/audio/piece.mp3 differ diff --git a/public/assets/audio/power-up.mp3 b/public/assets/audio/power-up.mp3 new file mode 100644 index 0000000..4e65734 Binary files /dev/null and b/public/assets/audio/power-up.mp3 differ diff --git a/public/assets/audio/saut.mp3 b/public/assets/audio/saut.mp3 new file mode 100644 index 0000000..ba901a8 Binary files /dev/null and b/public/assets/audio/saut.mp3 differ diff --git a/public/assets/audio/saute_champi.mp3 b/public/assets/audio/saute_champi.mp3 new file mode 100644 index 0000000..c4cd683 Binary files /dev/null and b/public/assets/audio/saute_champi.mp3 differ diff --git a/public/assets/audio/super_tresor.mp3 b/public/assets/audio/super_tresor.mp3 new file mode 100644 index 0000000..e6c4aad Binary files /dev/null and b/public/assets/audio/super_tresor.mp3 differ diff --git a/public/assets/audio/tuyau.mp3 b/public/assets/audio/tuyau.mp3 new file mode 100644 index 0000000..443497e Binary files /dev/null and b/public/assets/audio/tuyau.mp3 differ diff --git a/public/assets/sprites/champignon.png b/public/assets/sprites/champignon.png new file mode 100644 index 0000000..0a9f2d2 Binary files /dev/null and b/public/assets/sprites/champignon.png differ diff --git a/public/assets/sprites/champignon_ecrasé.png b/public/assets/sprites/champignon_ecrasé.png new file mode 100644 index 0000000..afa2c7b Binary files /dev/null and b/public/assets/sprites/champignon_ecrasé.png differ diff --git a/public/assets/sprites/jump_1.png b/public/assets/sprites/jump_1.png new file mode 100644 index 0000000..6b20bad Binary files /dev/null and b/public/assets/sprites/jump_1.png differ diff --git a/public/assets/sprites/jump_2.png b/public/assets/sprites/jump_2.png new file mode 100644 index 0000000..ab0f823 Binary files /dev/null and b/public/assets/sprites/jump_2.png differ diff --git a/public/assets/sprites/jump_3.png b/public/assets/sprites/jump_3.png new file mode 100644 index 0000000..2c7df30 Binary files /dev/null and b/public/assets/sprites/jump_3.png differ diff --git a/public/assets/sprites/jump_4.png b/public/assets/sprites/jump_4.png new file mode 100644 index 0000000..a5b9324 Binary files /dev/null and b/public/assets/sprites/jump_4.png differ diff --git a/public/assets/sprites/jump_5.png b/public/assets/sprites/jump_5.png new file mode 100644 index 0000000..555d7b6 Binary files /dev/null and b/public/assets/sprites/jump_5.png differ diff --git a/public/assets/sprites/jumping.png b/public/assets/sprites/jumping.png new file mode 100644 index 0000000..c564c24 Binary files /dev/null and b/public/assets/sprites/jumping.png differ diff --git a/public/assets/sprites/player_spritesheet.png b/public/assets/sprites/player_spritesheet.png new file mode 100644 index 0000000..5825731 Binary files /dev/null and b/public/assets/sprites/player_spritesheet.png differ diff --git a/public/assets/sprites/walk_1.png b/public/assets/sprites/walk_1.png new file mode 100644 index 0000000..3d3e423 Binary files /dev/null and b/public/assets/sprites/walk_1.png differ diff --git a/public/assets/sprites/walk_2.png b/public/assets/sprites/walk_2.png new file mode 100644 index 0000000..917a3bf Binary files /dev/null and b/public/assets/sprites/walk_2.png differ diff --git a/public/assets/sprites/walk_3.png b/public/assets/sprites/walk_3.png new file mode 100644 index 0000000..f1b9fd8 Binary files /dev/null and b/public/assets/sprites/walk_3.png differ diff --git a/public/assets/sprites/walk_4.png b/public/assets/sprites/walk_4.png new file mode 100644 index 0000000..f976cf3 Binary files /dev/null and b/public/assets/sprites/walk_4.png differ diff --git a/public/assets/video/intro (Copie 2).mp4 b/public/assets/video/intro (Copie 2).mp4 new file mode 100644 index 0000000..8a6633c Binary files /dev/null and b/public/assets/video/intro (Copie 2).mp4 differ diff --git a/public/assets/video/intro (Copie).mp4 b/public/assets/video/intro (Copie).mp4 new file mode 100644 index 0000000..caf8d21 Binary files /dev/null and b/public/assets/video/intro (Copie).mp4 differ diff --git a/public/assets/video/intro.mp4 b/public/assets/video/intro.mp4 new file mode 100644 index 0000000..6e25cab Binary files /dev/null and b/public/assets/video/intro.mp4 differ diff --git a/src/controls/DirectionalButtons.ts b/src/controls/DirectionalButtons.ts index 80d8ae3..593cc25 100644 --- a/src/controls/DirectionalButtons.ts +++ b/src/controls/DirectionalButtons.ts @@ -13,17 +13,33 @@ export class DirectionalButtons { private isLeftPressed: boolean = false; private isRightPressed: boolean = false; + private isMobile: boolean = false; constructor(scene: Phaser.Scene) { this.scene = scene; - this.createButtons(); + this.detectDevice(); + + // Ne créer les boutons QUE si on n'est PAS sur mobile + if (!this.isMobile) { + this.createButtons(); + } else { + console.log('📱 Smartphone détecté → boutons directionnels désactivés (gyroscope utilisé)'); + } + } + + /** + * Détecte si on est sur mobile + */ + private detectDevice(): void { + const userAgent = navigator.userAgent.toLowerCase(); + this.isMobile = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent); + console.log('[DirectionalButtons] Mobile détecté?', this.isMobile); } /** * Crée les boutons gauche et droite */ private createButtons(): void { - const width = this.scene.cameras.main.width; const height = this.scene.cameras.main.height; const buttonSize = 80; const padding = 30; diff --git a/src/controls/GyroControl.ts b/src/controls/GyroControl.ts index 17a9267..277c779 100644 --- a/src/controls/GyroControl.ts +++ b/src/controls/GyroControl.ts @@ -7,8 +7,6 @@ import { GYRO_DEADZONE, GYRO_MAX_TILT, GYRO_SENSITIVITY } from '../utils/constan export class GyroControl { private tiltValue: number = 0; private isActive: boolean = false; - private baseOrientation: number | null = null; - private calibrationMode: boolean = false; constructor() { this.setupGyroscope(); @@ -46,17 +44,20 @@ export class GyroControl { private handleOrientation(event: DeviceOrientationEvent): void { if (!this.isActive) return; - // Utiliser gamma (inclinaison gauche/droite) - // gamma: -90 à 90 degrés - let gamma = event.gamma || 0; + // Déterminer l'axe horizontal en fonction de l'orientation écran + const angle = (window.screen.orientation?.angle ?? (window as any).orientation ?? 0) as number; + const beta = event.beta ?? 0; // inclinaison avant/arrière + const gamma = event.gamma ?? 0; // inclinaison gauche/droite - // Calibration : définir l'orientation de base au premier appel - if (this.baseOrientation === null && !this.calibrationMode) { - this.baseOrientation = gamma; + // En paysage, la gauche/droite correspond à beta; en portrait, à gamma + let horizontalTiltDeg = gamma; + if (angle === 90) { + horizontalTiltDeg = -beta; + } else if (angle === -90 || angle === 270) { + horizontalTiltDeg = beta; } - // Calculer l'inclinaison relative à l'orientation de base - let relativeTilt = gamma - (this.baseOrientation || 0); + const relativeTilt = horizontalTiltDeg; // Appliquer la deadzone if (Math.abs(relativeTilt) < GYRO_DEADZONE) { @@ -70,7 +71,8 @@ export class GyroControl { // Clamper entre -1 et 1 normalizedTilt = Math.max(-1, Math.min(1, normalizedTilt)); - this.tiltValue = normalizedTilt; + // Inversion gauche/droite (plus naturel selon retour) + this.tiltValue = -normalizedTilt; } /** @@ -91,11 +93,8 @@ export class GyroControl { * Calibre le gyroscope (définit l'orientation actuelle comme neutre) */ public calibrate(): void { - this.calibrationMode = true; - this.baseOrientation = null; - setTimeout(() => { - this.calibrationMode = false; - }, 100); + // Calibration simplifiée : pas de base dynamique, on recentre juste à 0 + this.tiltValue = 0; } /** diff --git a/src/entities/Player.ts b/src/entities/Player.ts index 3af65a4..7cf542e 100644 --- a/src/entities/Player.ts +++ b/src/entities/Player.ts @@ -13,11 +13,12 @@ import { * Gère le mouvement, les animations, et les collisions */ export class Player extends Phaser.Physics.Arcade.Sprite { - private isJumping: boolean = false; private velocityX: number = 0; private jumpCount: number = 0; // Compteur de sauts (pour double saut) private isInvincible: boolean = false; // Invincibilité temporaire après respawn private invincibilityTimer?: Phaser.Time.TimerEvent; + private animationsCreated: boolean = false; + private wasAir: boolean = false; constructor(scene: Phaser.Scene, x: number, y: number) { // Pour l'instant, utiliser un sprite simple @@ -35,6 +36,8 @@ export class Player extends Phaser.Physics.Arcade.Sprite { body.onWorldBounds = true; // Active les événements de collision body.setSize(40, 70); // Hitbox body.setMaxVelocity(PLAYER_MAX_SPEED, 1000); + // Aligner la hitbox en bas du sprite (80x169 par défaut) + body.setOffset((this.width - 40) / 2, (this.height || 169) - 70); // Temporaire : créer un rectangle coloré si pas de texture if (!scene.textures.exists('player')) { @@ -42,6 +45,10 @@ export class Player extends Phaser.Physics.Arcade.Sprite { } this.setOrigin(0.5, 1); // Origine en bas au centre + body.setOffset((this.width - 40) / 2, (this.height || 169) - 70); // aligner la hitbox en bas + + this.ensureAnimations(); + this.setTexture('player_walk_1'); // frame par défaut } /** @@ -108,7 +115,15 @@ export class Player extends Phaser.Physics.Arcade.Sprite { if (this.jumpCount < PLAYER_MAX_JUMPS) { body.setVelocityY(PLAYER_JUMP_VELOCITY); this.jumpCount++; - this.isJumping = true; + + // Déclencher l'anim de saut immédiatement si disponible + if (this.animationsCreated && this.anims.get('player-jump')) { + this.anims.play('player-jump', true); + } + + // Effet sonore de saut (volume global SFX depuis le registry) + const sfxVolume = (this.scene.registry.get('sfxVolume') as number | undefined) ?? 1; + this.scene.sound.play('sfx_jump', { volume: 0.5 * sfxVolume }); // TODO: Jouer son de saut (différent pour double saut) console.log(`Saut ${this.jumpCount}/${PLAYER_MAX_JUMPS}`); @@ -123,14 +138,39 @@ export class Player extends Phaser.Physics.Arcade.Sprite { // Réinitialiser le flag de saut et le compteur si on touche le sol if (body.touching.down) { - this.isJumping = false; this.jumpCount = 0; } // TODO: Jouer les animations appropriées - // - idle si velocityX proche de 0 et au sol - // - walk/run si velocityX > 0 et au sol - // - jump si isJumping + if (!this.animationsCreated) return; + + const isMoving = Math.abs(this.velocityX) > 10; + const bodyState = this.body as Phaser.Physics.Arcade.Body; + const isAir = !bodyState.touching.down; + + // Transition sol -> air : jouer l'anim jump une fois + if (isAir && !this.wasAir) { + const jumpAnim = this.anims.get('player-jump'); + if (jumpAnim) { + this.anims.play('player-jump', true); + } + } + + // Sol : choisir walk ou idle + if (!isAir) { + if (isMoving) { + this.anims.play('player-walk', true); + } else { + this.anims.play('player-idle', true); + } + } else { + // En l'air : si pas d'anim jump, forcer dernière frame jump + if (this.anims.currentAnim?.key !== 'player-jump' && this.anims.get('player-jump')) { + this.anims.play('player-jump', true); + } + } + + this.wasAir = isAir; } /** @@ -191,6 +231,48 @@ export class Player extends Phaser.Physics.Arcade.Sprite { // Logique supplémentaire si nécessaire } + /** + * Création des animations (walk + idle) + */ + private ensureAnimations(): void { + if (this.animationsCreated) return; + + const scene = this.scene; + const walkFrames = ['player_walk_1', 'player_walk_2', 'player_walk_3', 'player_walk_4'] + .filter((key) => scene.textures.exists(key)) + .map((key) => ({ key })); + const jumpFrames = ['player_jump_1', 'player_jump_2', 'player_jump_3', 'player_jump_4', 'player_jump_5'] + .filter((key) => scene.textures.exists(key)) + .map((key) => ({ key })); + + if (walkFrames.length >= 2) { + scene.anims.create({ + key: 'player-walk', + frames: walkFrames, + frameRate: 10, + repeat: -1, + }); + + scene.anims.create({ + key: 'player-idle', + frames: [{ key: walkFrames[0].key }], + frameRate: 1, + repeat: -1, + }); + } + + if (jumpFrames.length >= 2) { + scene.anims.create({ + key: 'player-jump', + frames: jumpFrames, + frameRate: 12, + repeat: 0, + }); + } + + this.animationsCreated = walkFrames.length >= 2 || jumpFrames.length >= 2; + } + /** * Nettoie les ressources */ diff --git a/src/entities/SuperTreasure.ts b/src/entities/SuperTreasure.ts index 1d92035..df8a0ce 100644 --- a/src/entities/SuperTreasure.ts +++ b/src/entities/SuperTreasure.ts @@ -112,7 +112,6 @@ export class SuperTreasure extends Phaser.Physics.Arcade.Sprite { const star = scene.add.circle(x, y, 3, 0xFFFFFF, 0.8); star.setDepth(this.depth - 1); - const angle = (i * 120) * (Math.PI / 180); const radius = 40; scene.tweens.add({ diff --git a/src/entities/TreasureChest.ts b/src/entities/TreasureChest.ts index 3a0a61c..7a80e22 100644 --- a/src/entities/TreasureChest.ts +++ b/src/entities/TreasureChest.ts @@ -8,7 +8,6 @@ import Phaser from 'phaser'; export class TreasureChest extends Phaser.Physics.Arcade.Sprite { private isOpen: boolean = false; private requiredGifts: number; - private particles?: Phaser.GameObjects.Particles.ParticleEmitter; constructor(scene: Phaser.Scene, x: number, y: number, requiredGifts: number = 15) { super(scene, x, y, 'chest'); @@ -220,19 +219,6 @@ export class TreasureChest extends Phaser.Physics.Arcade.Sprite { } } - /** - * Met à jour le texte du requirement - */ - public updateRequirementText(scene: Phaser.Scene, giftsCollected: number): void { - if (this.isOpen) return; - - // Trouver le texte et le mettre à jour - const remaining = this.requiredGifts - giftsCollected; - if (remaining > 0) { - // Le texte sera mis à jour par GameScene - } - } - /** * Vérifie si le coffre est ouvert */ diff --git a/src/game.ts b/src/game.ts index b982c39..dec357f 100644 --- a/src/game.ts +++ b/src/game.ts @@ -3,6 +3,7 @@ import { GAME_WIDTH, GAME_HEIGHT } from './utils/constants'; import { BootScene } from './scenes/BootScene'; import { MenuScene } from './scenes/MenuScene'; import { GameScene } from './scenes/GameScene'; +import { IntroScene } from './scenes/IntroScene'; // Configuration Phaser const config: Phaser.Types.Core.GameConfig = { @@ -21,7 +22,7 @@ const config: Phaser.Types.Core.GameConfig = { debug: false, // Mettre à true pour voir les hitboxes }, }, - scene: [BootScene, MenuScene, GameScene], + scene: [BootScene, IntroScene, MenuScene, GameScene], backgroundColor: '#87CEEB', render: { pixelArt: false, diff --git a/src/scenes/BootScene.ts b/src/scenes/BootScene.ts index 6b21b94..5a809c0 100644 --- a/src/scenes/BootScene.ts +++ b/src/scenes/BootScene.ts @@ -42,14 +42,49 @@ export class BootScene extends Phaser.Scene { loadingText.destroy(); }); - // Charger les assets de base ici - // Exemple : this.load.image('logo', 'assets/logo.png'); + // Sprites du joueur (80x169, 1 frame pour l'instant) + this.load.spritesheet('player', 'assets/sprites/player_spritesheet.png', { + frameWidth: 80, + frameHeight: 169, + }); + // Frames de marche (sprite individuel) + this.load.image('player_walk_1', 'assets/sprites/walk_1.png'); + this.load.image('player_walk_2', 'assets/sprites/walk_2.png'); + this.load.image('player_walk_3', 'assets/sprites/walk_3.png'); + this.load.image('player_walk_4', 'assets/sprites/walk_4.png'); + this.load.image('player_jump_1', 'assets/sprites/jump_1.png'); + this.load.image('player_jump_2', 'assets/sprites/jump_2.png'); + this.load.image('player_jump_3', 'assets/sprites/jump_3.png'); + this.load.image('player_jump_4', 'assets/sprites/jump_4.png'); + this.load.image('player_jump_5', 'assets/sprites/jump_5.png'); - // TODO: Charger sprites, backgrounds, sons, etc. + // Musique de fond + this.load.audio('bgm', 'assets/audio/01. Ground Theme.mp3'); + + // Effets sonores + this.load.audio('sfx_jump', 'assets/audio/saut.mp3'); + this.load.audio('sfx_piece', 'assets/audio/piece.mp3'); + this.load.audio('sfx_powerup', 'assets/audio/power-up.mp3'); + this.load.audio('sfx_gameover', 'assets/audio/game-over.mp3'); + this.load.audio('sfx_levelcomplete', 'assets/audio/niveau-termine.mp3'); + this.load.audio('sfx_tuyau', 'assets/audio/tuyau.mp3'); + // Charger en priorité le MP3, mais accepter AIFF en fallback si présent + this.load.audio('sfx_hit', ['assets/audio/champignon.mp3', 'assets/audio/champignon.aiff']); + this.load.audio('sfx_super', 'assets/audio/super_tresor.mp3'); + this.load.audio('sfx_saute_champi', 'assets/audio/saute_champi.mp3'); + + // Sprites obstacles + this.load.image('obstacle_mushroom', 'assets/sprites/champignon.png'); + + // Vidéo d'intro (mp4 uniquement) + // Le 3e paramètre 'noAudio' est à false pour garder l'audio si présent + this.load.video('intro', 'assets/video/intro.mp4', false); + + // TODO: Charger d'autres sprites, backgrounds, sons, etc. } create(): void { - // Passer à la scène Menu - this.scene.start('MenuScene'); + // Passer par l'intro vidéo puis le menu + this.scene.start('IntroScene'); } } diff --git a/src/scenes/GameScene.ts b/src/scenes/GameScene.ts index 7d4a8fb..b0f76d2 100644 --- a/src/scenes/GameScene.ts +++ b/src/scenes/GameScene.ts @@ -3,6 +3,7 @@ import { LEVEL_DURATION, PLAYER_STARTING_LIVES, CHEST_REQUIRED_GIFTS } from '../ import { Player } from '../entities/Player'; import { GyroControl } from '../controls/GyroControl'; import { JumpButton } from '../controls/JumpButton'; +import { DirectionalButtons } from '../controls/DirectionalButtons'; import { SuperTreasure } from '../entities/SuperTreasure'; import { TreasureChest } from '../entities/TreasureChest'; @@ -15,6 +16,7 @@ export class GameScene extends Phaser.Scene { private cursors?: Phaser.Types.Input.Keyboard.CursorKeys; private gyroControl?: GyroControl; private jumpButton?: JumpButton; + private directionalButtons?: DirectionalButtons; // Plateformes et groupes private platforms?: Phaser.Physics.Arcade.StaticGroup; @@ -22,6 +24,8 @@ export class GameScene extends Phaser.Scene { private gifts?: Phaser.Physics.Arcade.Group; private superTreasures?: Phaser.Physics.Arcade.Group; private treasureChest?: TreasureChest; + private bgMusic?: Phaser.Sound.BaseSound; + private platformRects: { x: number; y: number; w: number; h: number }[] = []; // Background private background?: Phaser.GameObjects.TileSprite; @@ -32,6 +36,8 @@ export class GameScene extends Phaser.Scene { private controlInfoText?: Phaser.GameObjects.Text; private livesText?: Phaser.GameObjects.Text; private giftsCollectedText?: Phaser.GameObjects.Text; + private volumeText?: Phaser.GameObjects.Text; + private sfxVolumeText?: Phaser.GameObjects.Text; // Game state private score: number = 0; @@ -41,6 +47,11 @@ export class GameScene extends Phaser.Scene { private lives: number = PLAYER_STARTING_LIVES; private giftsCollected: number = 0; private lastCheckpointX: number = 200; // Position du dernier checkpoint + private musicVolume: number = 0.5; + private sfxVolume: number = 0.6; + private snowPileTimer?: Phaser.Time.TimerEvent; + private snowHeights: number[] = []; + private snowPileGraphics?: Phaser.GameObjects.Graphics; constructor() { super({ key: 'GameScene' }); @@ -50,17 +61,24 @@ export class GameScene extends Phaser.Scene { const width = this.cameras.main.width; const height = this.cameras.main.height; + // Réinitialiser l'état de partie + this.score = 0; + this.giftsCollected = 0; + this.lives = PLAYER_STARTING_LIVES; + this.lastCheckpointX = 200; + this.gameStartTime = this.time.now; // Détecter si mobile this.isMobile = this.sys.game.device.os.android || this.sys.game.device.os.iOS; // Configurer les limites du monde physique (IMPORTANT pour permettre mouvement infini) - const levelWidth = width * 6; // Niveau 6x plus grand + // Étendre un peu plus pour inclure la plateforme finale et le coffre + const levelWidth = Math.max(width * 7, 8000); this.physics.world.setBounds(0, 0, levelWidth, height); // Créer le background qui défile - this.createBackground(); + this.createBackground(levelWidth); // Créer les plateformes this.createPlatforms(); @@ -89,6 +107,15 @@ export class GameScene extends Phaser.Scene { // UI this.createUI(); + // Effet neige + this.createSnow(); + + // Musique + this.setupMusic(); + + // Volume SFX initial + this.registry.set('sfxVolume', this.sfxVolume); + // Générer quelques obstacles et cadeaux de test this.spawnTestObjects(); @@ -98,7 +125,7 @@ export class GameScene extends Phaser.Scene { /** * Crée le background qui défile */ - private createBackground(): void { + private createBackground(levelWidth: number): void { const width = this.cameras.main.width; const height = this.cameras.main.height; @@ -122,7 +149,7 @@ export class GameScene extends Phaser.Scene { graphics.generateTexture('sky', width, height); graphics.destroy(); - this.background = this.add.tileSprite(0, 0, width * 6, height, 'sky'); + this.background = this.add.tileSprite(0, 0, levelWidth, height, 'sky'); this.background.setOrigin(0, 0); this.background.setScrollFactor(0.3); // Effet parallaxe } @@ -141,6 +168,7 @@ export class GameScene extends Phaser.Scene { const ground = this.add.rectangle(groundWidth / 2, height - 25, groundWidth, 50, 0x8B4513); this.physics.add.existing(ground, true); this.platforms.add(ground); + this.platformRects.push({ x: groundWidth / 2, y: height - 25, w: groundWidth, h: 50 }); // BEAUCOUP plus de plateformes réparties sur toute la longueur const platformPositions = [ @@ -187,6 +215,7 @@ export class GameScene extends Phaser.Scene { const platform = this.add.rectangle(pos.x, pos.y, pos.w, pos.h, 0x6B8E23); this.physics.add.existing(platform, true); this.platforms!.add(platform); + this.platformRects.push({ x: pos.x, y: pos.y, w: pos.w, h: pos.h }); }); console.log(`${platformPositions.length} plateformes créées sur ${groundWidth}px`); @@ -254,9 +283,77 @@ export class GameScene extends Phaser.Scene { 6800, 7100, 7400, ]; - obstaclePositions.forEach((x) => { - const obstacle = this.add.rectangle(x, height - 80, 40, 60, 0xF44336); - this.physics.add.existing(obstacle); + // Obstacles sur plateformes (x, y) + const obstaclePlatforms = [ + { x: 700, y: height - 280 }, + { x: 1300, y: height - 330 }, + { x: 1900, y: height - 350 }, + { x: 2500, y: height - 410 }, + { x: 3350, y: height - 380 }, + { x: 3900, y: height - 430 }, + { x: 4800, y: height - 360 }, + { x: 5400, y: height - 440 }, + { x: 6250, y: height - 400 }, + { x: 6800, y: height - 500 }, + ]; + + // Ajouter quelques champignons directement sur les plateformes existantes + const platformPlaced: Array<{ x: number; y: number }> = []; + this.platformRects + .filter((rect) => rect.y < height - 80 && rect.w >= 120) // ignorer le sol et les très petites plateformes + .forEach((rect, idx) => { + // Placer un champignon sur ~1 plateforme sur 2 pour ne pas surcharger + if (idx % 2 !== 0) return; + const x = rect.x + Phaser.Math.Between(Math.round(-rect.w / 4), Math.round(rect.w / 4)); + const y = rect.y - rect.h / 2; + platformPlaced.push({ x, y }); + }); + + obstaclePositions.filter((_, idx) => idx % 2 === 0).forEach((x) => { + const obstacle = this.physics.add.sprite(x, height - 50, 'obstacle_mushroom'); + obstacle.setOrigin(0.5, 1); // ancré sur les pieds + obstacle.setImmovable(true); + obstacle.setPushable(false); + obstacle.setScale(0.9); + + const body = obstacle.body as Phaser.Physics.Arcade.Body; + body.setAllowGravity(false); + body.setSize(45, 81); // hitbox 10% plus petite avec le scale + body.setOffset((obstacle.displayWidth - 45) / 2, obstacle.displayHeight - 81); + + this.obstacles!.add(obstacle); + }); + + obstaclePlatforms.filter((_, idx) => idx % 2 === 0).forEach((pos) => { + // Trouver la plateforme la plus proche à cet x + const target = this.platformRects.find((rect) => Math.abs(pos.x - rect.x) <= rect.w / 2); + const topY = target ? target.y - target.h / 2 : pos.y; + const obstacle = this.physics.add.sprite(pos.x, topY, 'obstacle_mushroom'); + obstacle.setOrigin(0.5, 1); + obstacle.setImmovable(true); + obstacle.setPushable(false); + obstacle.setScale(0.9); + + const body = obstacle.body as Phaser.Physics.Arcade.Body; + body.setAllowGravity(false); + body.setSize(45, 81); + body.setOffset((obstacle.displayWidth - 45) / 2, obstacle.displayHeight - 81); + + this.obstacles!.add(obstacle); + }); + + platformPlaced.filter((_, idx) => idx % 2 === 0).forEach((pos) => { + const obstacle = this.physics.add.sprite(pos.x, pos.y, 'obstacle_mushroom'); + obstacle.setOrigin(0.5, 1); + obstacle.setImmovable(true); + obstacle.setPushable(false); + obstacle.setScale(0.9); + + const body = obstacle.body as Phaser.Physics.Arcade.Body; + body.setAllowGravity(false); + body.setSize(45, 81); + body.setOffset((obstacle.displayWidth - 45) / 2, obstacle.displayHeight - 81); + this.obstacles!.add(obstacle); }); @@ -276,7 +373,7 @@ export class GameScene extends Phaser.Scene { }); // COFFRE FINAL au bout du niveau - this.treasureChest = new TreasureChest(this, 7600, height - 300, CHEST_REQUIRED_GIFTS); + this.treasureChest = new TreasureChest(this, 7700, height - 300, CHEST_REQUIRED_GIFTS); this.physics.add.overlap(this.player!, this.treasureChest, this.openChest, undefined, this); console.log(`${giftPositions.length} cadeaux, ${obstaclePositions.length} obstacles, ${superTreasurePositions.length} SUPER TRÉSORS et 1 COFFRE FINAL créés`); @@ -293,6 +390,9 @@ export class GameScene extends Phaser.Scene { this.jumpButton = new JumpButton(this, () => { this.player?.jump(); }); + + // Boutons gauche/droite (en bas à gauche) + this.directionalButtons = new DirectionalButtons(this); } /** @@ -375,6 +475,36 @@ export class GameScene extends Phaser.Scene { this.cleanup(); this.scene.start('MenuScene'); }); + + // Volume musique + this.volumeText = this.add.text(this.cameras.main.width - 20, 60, this.getVolumeLabel(), { + fontSize: '18px', + color: '#ffffff', + backgroundColor: '#000000', + padding: { x: 8, y: 4 }, + }); + this.volumeText.setOrigin(1, 0); + this.volumeText.setScrollFactor(0); + this.volumeText.setDepth(100); + this.volumeText.setInteractive({ useHandCursor: true }); + this.volumeText.on('pointerdown', () => { + this.cycleMusicVolume(); + }); + + // Volume SFX + this.sfxVolumeText = this.add.text(this.cameras.main.width - 20, 90, this.getSfxVolumeLabel(), { + fontSize: '18px', + color: '#ffffff', + backgroundColor: '#000000', + padding: { x: 8, y: 4 }, + }); + this.sfxVolumeText.setOrigin(1, 0); + this.sfxVolumeText.setScrollFactor(0); + this.sfxVolumeText.setDepth(100); + this.sfxVolumeText.setInteractive({ useHandCursor: true }); + this.sfxVolumeText.on('pointerdown', () => { + this.cycleSfxVolume(); + }); } update(time: number): void { @@ -405,10 +535,15 @@ export class GameScene extends Phaser.Scene { } } - // Mobile : Gyroscope - if (this.gyroControl && this.isMobile) { - const tiltValue = this.gyroControl.getTiltValue(); - direction = tiltValue; // -1 à 1 + // Mobile : boutons directionnels priment, sinon gyroscope + if (this.isMobile) { + const dirButtons = this.directionalButtons?.getDirection() ?? 0; + if (dirButtons !== 0) { + direction = dirButtons; + } else if (this.gyroControl) { + const tiltValue = this.gyroControl.getTiltValue(); + direction = tiltValue; // -1 à 1 + } } // Déplacer le joueur @@ -460,6 +595,7 @@ export class GameScene extends Phaser.Scene { gift.destroy(); this.giftsCollected++; this.addScore(100); + this.playSfx('sfx_powerup', 0.5); // Mettre à jour l'UI this.giftsCollectedText?.setText(`🎁 Cadeaux: ${this.giftsCollected}/${CHEST_REQUIRED_GIFTS}`); @@ -509,6 +645,7 @@ export class GameScene extends Phaser.Scene { obstacle.destroy(); this.addScore(50); // Bonus pour avoir sauté dessus player.jump(); // Petit rebond + this.playSfx('sfx_saute_champi', 0.6); // Effet visuel const explosion = this.add.circle(obstacleBody.x, obstacleBody.y, 20, 0x00FF00, 0.5); @@ -530,6 +667,7 @@ export class GameScene extends Phaser.Scene { return; } + this.playSfx('sfx_hit', 0.6); this.loseLife(); } } @@ -546,6 +684,7 @@ export class GameScene extends Phaser.Scene { // GROS BONUS de points! this.addScore(500); + this.playSfx('sfx_super', 0.6); // Message spécial const bonusText = this.add.text( @@ -583,18 +722,40 @@ export class GameScene extends Phaser.Scene { /** * Ouvre le coffre au trésor final */ - private openChest(_player: any, _chest: any): void { - if (!this.treasureChest) return; - - // Vérifier si on peut ouvrir - if (this.treasureChest.canOpen(this.giftsCollected)) { - const megaBonus = this.treasureChest.open(this); - this.addScore(megaBonus); + private openChest(_player: any, chest: any): void { + if (chest.canOpen(this.giftsCollected)) { + const bonus = chest.open(this); + this.addScore(bonus); // VICTOIRE ! Lancer l'animation de fin this.time.delayedCall(2000, () => { this.levelComplete(); }); + } else if (!chest.getIsOpen()) { + // Pas assez de cadeaux + const remaining = chest.getRequiredGifts() - this.giftsCollected; + + const warning = this.add.text( + this.cameras.main.scrollX + this.cameras.main.width / 2, + this.cameras.main.height / 2, + `❌ Encore ${remaining} cadeaux nécessaires! ❌`, + { + fontSize: '28px', + color: '#FF0000', + stroke: '#000000', + strokeThickness: 4, + } + ); + warning.setOrigin(0.5); + warning.setScrollFactor(0); + warning.setDepth(1000); + + this.tweens.add({ + targets: warning, + alpha: 0, + duration: 2000, + onComplete: () => warning.destroy(), + }); } } @@ -606,6 +767,7 @@ export class GameScene extends Phaser.Scene { // Arrêter la physique this.physics.pause(); + this.playSfx('sfx_levelcomplete', 0.6); // Flash doré géant this.cameras.main.flash(1000, 255, 215, 0, true); @@ -737,21 +899,18 @@ export class GameScene extends Phaser.Scene { * Fait perdre une vie au joueur */ private loseLife(): void { - // Flash rouge pour indiquer les dégâts - this.cameras.main.flash(200, 255, 0, 0, true); - - // Décrémenter les vies this.lives--; this.livesText?.setText(`❤️ Vies: ${this.lives}`); - // Effet sonore (TODO: ajouter un vrai son) - console.log(`💔 Vie perdue ! Vies restantes: ${this.lives}`); + // Flash rouge + this.cameras.main.flash(200, 255, 0, 0, true); + this.cameras.main.shake(200, 0.01); + + console.log(`💔 Vie perdue! Vies restantes: ${this.lives}`); if (this.lives <= 0) { - // Game Over this.gameOver(); } else { - // Respawn au dernier checkpoint this.respawnPlayer(); } } @@ -762,55 +921,81 @@ export class GameScene extends Phaser.Scene { private respawnPlayer(): void { if (!this.player) return; - // Téléporter au checkpoint - this.player.setPosition(this.lastCheckpointX, this.cameras.main.height - 150); + // Téléporter au dernier checkpoint + this.player.setPosition(this.lastCheckpointX, this.cameras.main.height - 200); this.player.setVelocity(0, 0); - // Activer l'invincibilité temporaire + // Activer l'invincibilité this.player.makeInvincible(this); - console.log(`🔄 Respawn au checkpoint x=${this.lastCheckpointX}`); + // Message + const respawnText = this.add.text( + this.cameras.main.scrollX + this.cameras.main.width / 2, + this.cameras.main.height / 2, + `💫 RESPAWN! ${this.lives} ❤️ restantes`, + { + fontSize: '36px', + color: '#00FF00', + stroke: '#000000', + strokeThickness: 6, + } + ); + respawnText.setOrigin(0.5); + respawnText.setScrollFactor(0); + respawnText.setDepth(1000); + + this.tweens.add({ + targets: respawnText, + alpha: 0, + duration: 2000, + onComplete: () => respawnText.destroy(), + }); } /** * Game Over - plus de vies */ private gameOver(): void { - console.log('💀 GAME OVER - Plus de vies!'); + console.log('💀 GAME OVER'); // Arrêter la physique this.physics.pause(); + this.playSfx('sfx_gameover', 0.6); - // Message Game Over + // Écran de game over const gameOverText = this.add.text( this.cameras.main.scrollX + this.cameras.main.width / 2, - this.cameras.main.height / 2, - `GAME OVER\n\nScore Final: ${this.score}\n\nRetour au menu dans 5s...`, + this.cameras.main.height / 2 - 50, + 'GAME OVER', { - fontSize: '48px', + fontSize: '72px', color: '#FF0000', stroke: '#000000', strokeThickness: 8, fontStyle: 'bold', - align: 'center', } ); gameOverText.setOrigin(0.5); gameOverText.setScrollFactor(0); gameOverText.setDepth(2000); - // Animation du texte - this.tweens.add({ - targets: gameOverText, - scaleX: 1.1, - scaleY: 1.1, - duration: 500, - yoyo: true, - repeat: -1, - }); + const scoreText = this.add.text( + this.cameras.main.scrollX + this.cameras.main.width / 2, + this.cameras.main.height / 2 + 50, + `Score Final: ${this.score}`, + { + fontSize: '36px', + color: '#FFFFFF', + stroke: '#000000', + strokeThickness: 4, + } + ); + scoreText.setOrigin(0.5); + scoreText.setScrollFactor(0); + scoreText.setDepth(2000); - // Retour au menu après 5 secondes - this.time.delayedCall(5000, () => { + // Retour au menu après 3 secondes + this.time.delayedCall(3000, () => { this.cleanup(); this.scene.start('MenuScene'); }); @@ -851,6 +1036,163 @@ export class GameScene extends Phaser.Scene { }); } + /** + * Configure et lance la musique de fond + */ + private setupMusic(): void { + if (this.bgMusic && this.bgMusic.isPlaying) { + return; + } + + this.bgMusic = this.sound.add('bgm', { + loop: true, + volume: this.musicVolume, + }); + this.bgMusic.play(); + } + + /** + * Met à jour le volume musique (cycle 0%, 20%, 40%, 60%, 80%, 100%) + */ + private cycleMusicVolume(): void { + const steps = [0, 0.2, 0.4, 0.6, 0.8, 1]; + const currentIndex = steps.findIndex((v) => v === this.musicVolume); + const nextIndex = (currentIndex + 1) % steps.length; + this.musicVolume = steps[nextIndex]; + + if (this.bgMusic) { + const webAudio = this.bgMusic as Phaser.Sound.WebAudioSound; + const htmlAudio = this.bgMusic as Phaser.Sound.HTML5AudioSound; + if (typeof webAudio.setVolume === 'function') { + webAudio.setVolume(this.musicVolume); + } else if (typeof htmlAudio.setVolume === 'function') { + htmlAudio.setVolume(this.musicVolume); + } + if (this.musicVolume === 0 && this.bgMusic.isPlaying) { + this.bgMusic.pause(); + } else if (this.musicVolume > 0 && this.bgMusic.isPaused) { + this.bgMusic.resume(); + } else if (this.musicVolume > 0 && !this.bgMusic.isPlaying) { + this.bgMusic.play(); + } + } + + this.volumeText?.setText(this.getVolumeLabel()); + } + + private getVolumeLabel(): string { + return `🔊 Volume: ${Math.round(this.musicVolume * 100)}% (clic)`; + } + + /** + * Met à jour le volume des SFX (cycle 0%, 20%, 40%, 60%, 80%, 100%) + */ + private cycleSfxVolume(): void { + const steps = [0, 0.2, 0.4, 0.6, 0.8, 1]; + const currentIndex = steps.findIndex((v) => v === this.sfxVolume); + const nextIndex = (currentIndex + 1) % steps.length; + this.sfxVolume = steps[nextIndex]; + this.registry.set('sfxVolume', this.sfxVolume); + this.sfxVolumeText?.setText(this.getSfxVolumeLabel()); + } + + private getSfxVolumeLabel(): string { + return `🎚️ Bruitages: ${Math.round(this.sfxVolume * 100)}% (clic)`; + } + + /** + * Lecture SFX avec volume global appliqué + */ + private playSfx(key: string, baseVolume: number = 0.5): void { + const globalSfxVolume = (this.registry.get('sfxVolume') as number | undefined) ?? this.sfxVolume; + // Vérifier que l'asset audio existe pour éviter un blocage si le fichier est absent + const audioCache = this.cache.audio; + if (!audioCache.exists(key)) { + console.warn(`SFX manquant: ${key}`); + return; + } + this.sound.play(key, { volume: baseVolume * globalSfxVolume }); + } + + /** + * Effet de neige qui tombe en overlay + */ + private createSnow(): void { + // Texture flocon si absente + if (!this.textures.exists('snowflake')) { + const g = this.add.graphics(); + g.fillStyle(0xffffff, 1); + g.fillCircle(3, 3, 3); + g.generateTexture('snowflake', 6, 6); + g.destroy(); + } + + // Emetteur unique attaché à un manager en overlay + const emitterManager = this.add.particles(0, 0, 'snowflake', { + x: { min: 0, max: this.cameras.main.width }, + y: -10, + lifespan: 8000, + speedY: { min: 50, max: 100 }, + speedX: { min: -20, max: 20 }, + scale: { min: 0.5, max: 1 }, + quantity: 4, + frequency: 80, + alpha: { start: 0.9, end: 0.15 }, + rotate: { min: -30, max: 30 }, + blendMode: 'ADD', + emitZone: { type: 'random', source: new Phaser.Geom.Rectangle(0, 0, this.cameras.main.width, 1) } as any, + }); + emitterManager.setScrollFactor(0); + emitterManager.setDepth(2000); + + // Neige qui s'accumule au sol (colonne par colonne) + const initSnowBuffer = () => { + const cols = Math.max(10, Math.ceil(this.cameras.main.width / 8)); + this.snowHeights = new Array(cols).fill(0); + if (!this.snowPileGraphics) { + this.snowPileGraphics = this.add.graphics(); + this.snowPileGraphics.setScrollFactor(0); + this.snowPileGraphics.setDepth(5); + } + this.renderSnowPile(); + }; + + initSnowBuffer(); + this.scale.on('resize', initSnowBuffer); + + const maxHeight = 160; + this.snowPileTimer = this.time.addEvent({ + delay: 120, + loop: true, + callback: () => { + if (this.snowHeights.length === 0) return; + const idx = Phaser.Math.Between(0, this.snowHeights.length - 1); + const add = Phaser.Math.Between(2, 6); + this.snowHeights[idx] = Math.min(maxHeight, this.snowHeights[idx] + add); + this.renderSnowPile(); + }, + }); + } + + /** + * Dessine l'accumulation de neige au sol + */ + private renderSnowPile(): void { + if (!this.snowPileGraphics || this.snowHeights.length === 0) return; + const gfx = this.snowPileGraphics; + const colWidth = this.cameras.main.width / this.snowHeights.length; + const baseY = this.cameras.main.height; + + gfx.clear(); + gfx.fillStyle(0xffffff, 0.9); + + this.snowHeights.forEach((h, i) => { + if (h <= 0) return; + const x = i * colWidth; + gfx.fillRect(x, baseY - h, colWidth + 1, h); + }); + } + /** * Nettoyage avant de quitter la scène */ @@ -861,5 +1203,15 @@ export class GameScene extends Phaser.Scene { if (this.jumpButton) { this.jumpButton.destroy(); } + if (this.directionalButtons) { + this.directionalButtons.destroy(); + } + if (this.bgMusic) { + this.bgMusic.stop(); + this.bgMusic.destroy(); + } + if (this.snowPileTimer) { + this.snowPileTimer.destroy(); + } } } diff --git a/src/scenes/IntroScene.ts b/src/scenes/IntroScene.ts new file mode 100644 index 0000000..1ffee68 --- /dev/null +++ b/src/scenes/IntroScene.ts @@ -0,0 +1,306 @@ +import Phaser from 'phaser'; + +/** + * Scène d'introduction : lit une vidéo en mode paysage puis passe au menu + */ +export class IntroScene extends Phaser.Scene { + private video?: Phaser.GameObjects.Video; + private hasFinished: boolean = false; + private rotationText?: Phaser.GameObjects.Text; + private playButton?: Phaser.GameObjects.Container; + + constructor() { + super({ key: 'IntroScene' }); + } + + create(): void { + const { width, height } = this.cameras.main; + this.cameras.main.setBackgroundColor('#000000'); + + console.log('[IntroScene] Création de la scène d\'intro'); + console.log('[IntroScene] Dimensions:', width, 'x', height); + + // Vérifier si on est en mode paysage + const isLandscape = window.innerWidth > window.innerHeight; + + if (!isLandscape) { + // Mode portrait → demander de tourner en paysage + console.log('[IntroScene] Mode portrait détecté → demande de rotation paysage'); + this.showRotateToLandscapeScreen(); + return; + } + + // Mode paysage → afficher le bouton Play + console.log('[IntroScene] Mode paysage → affichage du bouton Play'); + this.showPlayButton(); + } + + /** + * Affiche le bouton Play pour démarrer la vidéo + */ + private showPlayButton(): void { + const { width, height } = this.cameras.main; + + // Créer un container pour le bouton + this.playButton = this.add.container(width / 2, height / 2); + + // Cercle du bouton + const circle = this.add.circle(0, 0, 60, 0x4CAF50, 1); + circle.setStrokeStyle(4, 0xFFFFFF); + + // Triangle "Play" + const triangle = this.add.triangle( + 5, // Décalé légèrement à droite pour centrer visuellement + 0, + -15, -20, // Point gauche haut + -15, 20, // Point gauche bas + 20, 0, // Point droit + 0xFFFFFF + ); + + // Texte "Cliquez pour démarrer" + const text = this.add.text(0, 100, 'Cliquez pour démarrer la vidéo', { + fontSize: '24px', + color: '#ffffff', + align: 'center', + fontFamily: 'Arial', + }); + text.setOrigin(0.5); + + this.playButton.add([circle, triangle, text]); + this.playButton.setDepth(1000); + + // Rendre le bouton interactif + circle.setInteractive({ useHandCursor: true }); + + // Animation hover + circle.on('pointerover', () => { + circle.setFillStyle(0x66BB6A); + this.tweens.add({ + targets: this.playButton, + scale: 1.1, + duration: 200, + ease: 'Power2', + }); + }); + + circle.on('pointerout', () => { + circle.setFillStyle(0x4CAF50); + this.tweens.add({ + targets: this.playButton, + scale: 1.0, + duration: 200, + ease: 'Power2', + }); + }); + + // Clic sur le bouton + circle.on('pointerdown', () => { + console.log('[IntroScene] Bouton Play cliqué → démarrage vidéo'); + + // Effet de clic + circle.setFillStyle(0x2E7D32); + this.tweens.add({ + targets: this.playButton, + scale: 0.9, + duration: 100, + yoyo: true, + onComplete: () => { + // Détruire le bouton + this.playButton?.destroy(); + this.playButton = undefined; + + // Lancer la vidéo + this.playIntroVideo(); + }, + }); + }); + + console.log('[IntroScene] Bouton Play affiché'); + } + + /** + * Affiche l'écran demandant de tourner en paysage + */ + private showRotateToLandscapeScreen(): void { + const { width, height } = this.cameras.main; + + this.rotationText = this.add.text(width / 2, height / 2, '🔄\n\nTournez votre téléphone\nen mode paysage\npour voir la vidéo', { + fontSize: '32px', + color: '#ffffff', + align: 'center', + fontFamily: 'Arial', + }); + this.rotationText.setOrigin(0.5); + + // Surveiller l'orientation + this.listenForLandscapeOrientation(); + } + + /** + * Écoute les changements d'orientation + */ + private listenForLandscapeOrientation(): void { + // Méthode 1: Utiliser l'événement resize + this.scale.on('resize', this.checkOrientationForVideo, this); + + // Méthode 2: orientationchange event + window.addEventListener('orientationchange', () => { + console.log('[IntroScene] orientationchange event détecté'); + setTimeout(() => this.checkOrientationForVideo(), 300); + }); + + // Méthode 3: polling manuel toutes les 500ms + const checkInterval = setInterval(() => { + this.checkOrientationForVideo(); + if (this.video || this.hasFinished) { + clearInterval(checkInterval); + } + }, 500); + + // Vérifier immédiatement + this.checkOrientationForVideo(); + } + + /** + * Vérifie l'orientation pour afficher le bouton Play + */ + private checkOrientationForVideo(): void { + const isNowLandscape = window.innerWidth > window.innerHeight; + + console.log('[IntroScene] Vérification orientation - Paysage?', isNowLandscape, 'Dimensions:', window.innerWidth, 'x', window.innerHeight); + + if (isNowLandscape && !this.playButton && !this.video) { + console.log('[IntroScene] Paysage détecté → affichage bouton Play'); + + // Effacer le message + if (this.rotationText) { + this.rotationText.destroy(); + this.rotationText = undefined; + } + + this.scale.off('resize', this.checkOrientationForVideo, this); + this.showPlayButton(); + } + } + + /** + * Lit la vidéo d'intro + */ + private playIntroVideo(): void { + const { width, height } = this.cameras.main; + + console.log('[IntroScene] Vidéo dans cache?', this.cache.video.exists('intro')); + + if (!this.cache.video.exists('intro')) { + console.warn('[IntroScene] Vidéo intro non trouvée → passage au menu.'); + this.gotoMenu(); + return; + } + + console.log('[IntroScene] Création de l\'objet vidéo'); + this.video = this.add.video(width / 2, height / 2, 'intro'); + this.video.setOrigin(0.5); + this.video.setDepth(1000); + + // Attendre que les métadonnées soient chargées + this.video.on('metadata', () => { + if (!this.video) return; + + const videoWidth = this.video.video?.videoWidth || 324; + const videoHeight = this.video.video?.videoHeight || 720; + + console.log('[IntroScene] Métadonnées vidéo chargées:', videoWidth, 'x', videoHeight); + + this.video.setSize(videoWidth, videoHeight); + this.updateVideoSize(); + }); + + // Forcer mute pour éviter les blocages autoplay + this.video.setMute(true); + this.video.setLoop(false); + + console.log('[IntroScene] Démarrage de la lecture'); + const started = this.video.play(false); + console.log('[IntroScene] Lecture démarrée?', started); + + if (!started) { + console.warn('[IntroScene] Lecture vidéo bloquée → passage au menu.'); + this.gotoMenu(); + return; + } + + this.video.once('complete', () => { + console.log('[IntroScene] Vidéo terminée → passage au menu'); + this.gotoMenu(); + }); + + this.video.once('error', (err: any) => { + console.error('[IntroScene] Erreur lecture vidéo:', err); + this.gotoMenu(); + }); + + // Ajuster si resize + this.scale.on('resize', (gameSize: Phaser.Structs.Size) => { + if (this.video && this.video.isPlaying()) { + this.updateVideoSize(gameSize.width, gameSize.height); + } + }); + } + + /** + * Ajuste la vidéo à l'écran en mode paysage en respectant le ratio + * Vidéo 324×720 (portrait) affichée en paysage → mode "contain" pour tout voir + */ + private updateVideoSize(targetW?: number, targetH?: number): void { + if (!this.video) return; + const w = targetW ?? this.cameras.main.width; + const h = targetH ?? this.cameras.main.height; + + // Dimensions natives de la vidéo (324×720 portrait) + const nativeW = this.video.video?.videoWidth || 324; + const nativeH = this.video.video?.videoHeight || 720; + + console.log('[IntroScene] updateVideoSize - Écran:', w, 'x', h); + console.log('[IntroScene] updateVideoSize - Vidéo native:', nativeW, 'x', nativeH); + + // Ratio vidéo = 324/720 = 0.45 (portrait étroit) + // Ratio écran paysage = 1280/720 = 1.78 (paysage large) + const videoRatio = nativeW / nativeH; + const screenRatio = w / h; + + let scale: number; + + // Mode "contain" : adapter pour que toute la vidéo soit visible + if (screenRatio > videoRatio) { + // L'écran est plus large que la vidéo → adapter à la HAUTEUR + scale = h / nativeH; + console.log('[IntroScene] Adaptation à la hauteur'); + } else { + // L'écran est plus étroit que la vidéo → adapter à la LARGEUR + scale = w / nativeW; + console.log('[IntroScene] Adaptation à la largeur'); + } + + const finalWidth = nativeW * scale; + const finalHeight = nativeH * scale; + + console.log('[IntroScene] updateVideoSize - Ratio vidéo:', videoRatio.toFixed(2), '- Ratio écran:', screenRatio.toFixed(2)); + console.log('[IntroScene] updateVideoSize - Scale:', scale.toFixed(2)); + console.log('[IntroScene] updateVideoSize - Taille finale:', Math.round(finalWidth), 'x', Math.round(finalHeight)); + + this.video.setScale(scale); + this.video.setPosition(w / 2, h / 2); + } + + /** + * Passe au menu + */ + private gotoMenu(): void { + if (this.hasFinished) return; + this.hasFinished = true; + this.video?.stop(); + this.video?.destroy(); + this.scene.start('MenuScene'); + } +} diff --git a/src/scenes/MenuScene.ts b/src/scenes/MenuScene.ts index e00cdb1..61d9f1d 100644 --- a/src/scenes/MenuScene.ts +++ b/src/scenes/MenuScene.ts @@ -6,6 +6,7 @@ import Phaser from 'phaser'; export class MenuScene extends Phaser.Scene { private startButton?: Phaser.GameObjects.Text; private gyroStatus?: Phaser.GameObjects.Text; + private hasStarted: boolean = false; constructor() { super({ key: 'MenuScene' }); @@ -79,47 +80,17 @@ export class MenuScene extends Phaser.Scene { * Demande la permission gyroscope (iOS) et lance le jeu */ private requestGyroPermission(): void { - let debugInfo = '🔍 Vérification...\n'; - debugInfo += `DeviceOrientation: ${!!window.DeviceOrientationEvent}\n`; - debugInfo += `requestPermission: ${typeof (DeviceOrientationEvent as any).requestPermission}\n`; - debugInfo += `HTTPS: ${window.location.protocol === 'https:'}\n`; - debugInfo += `UserAgent: ${navigator.userAgent.substring(0, 50)}...`; - - this.gyroStatus?.setText(debugInfo); - - // iOS 13+ nécessite une permission explicite - if (typeof (DeviceOrientationEvent as any).requestPermission === 'function') { - setTimeout(() => { - this.gyroStatus?.setText('📱 iOS détecté\nDemande permission...'); - }, 1000); - - (DeviceOrientationEvent as any).requestPermission() - .then((response: string) => { - if (response === 'granted') { - this.gyroStatus?.setText('✅ Permission accordée\nLancement du jeu...'); - setTimeout(() => this.startGame(), 500); - } else { - this.gyroStatus?.setText(`❌ Permission: ${response}\nLancement quand même...`); - setTimeout(() => this.startGame(), 2000); - } - }) - .catch((error: Error) => { - this.gyroStatus?.setText(`❌ Erreur:\n${error.message}\n${error.name}\nLancement quand même...`); - setTimeout(() => this.startGame(), 3000); - }); - } else { - // Android ou navigateurs sans permission requise - setTimeout(() => { - this.gyroStatus?.setText('✅ Android/Desktop\nGyroscope disponible\nLancement...'); - setTimeout(() => this.startGame(), 500); - }, 1000); - } + // Simplifié : on part immédiatement en jeu, sans attendre la permission. + this.gyroStatus?.setText('Lancement du jeu...'); + this.startGame(); } /** * Lance la scène de jeu */ private startGame(): void { + if (this.hasStarted || this.scene.isActive('GameScene')) return; + this.hasStarted = true; this.scene.start('GameScene'); } } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 6980ca5..63eab4a 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -12,8 +12,8 @@ export const PLAYER_ACCELERATION = 50; export const PLAYER_MAX_JUMPS = 2; // Nombre de sauts autorisés (double saut) // Gyroscope -export const GYRO_DEADZONE = 5; // Degrés de zone morte -export const GYRO_MAX_TILT = 30; // Tilt maximum pris en compte (degrés) +export const GYRO_DEADZONE = 10; // Degrés de zone morte (plus large pour éviter les faux mouvements) +export const GYRO_MAX_TILT = 35; // Tilt max pris en compte (pense à incliner franchement) export const GYRO_SENSITIVITY = 10; // Multiplicateur de sensibilité // Niveau @@ -25,7 +25,7 @@ export const PLAYER_STARTING_LIVES = 3; // Nombre de vies au départ export const RESPAWN_INVINCIBILITY_TIME = 2000; // Temps d'invincibilité après respawn (ms) // Coffre final -export const CHEST_REQUIRED_GIFTS = 5; // Nombre de cadeaux requis pour ouvrir le coffre +export const CHEST_REQUIRED_GIFTS = 15; // Nombre de cadeaux requis pour ouvrir le coffre // Couleurs export const COLOR_PRIMARY = 0x4CAF50;