This commit is contained in:
2025-12-14 19:15:48 +01:00
parent 8462027d1d
commit 51d0fcc601
42 changed files with 1202 additions and 279 deletions

468
package-lock.json generated
View File

@@ -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
}
}
}

View File

@@ -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"
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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;

View File

@@ -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;
}
/**

View File

@@ -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
*/

View File

@@ -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({

View File

@@ -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
*/

View File

@@ -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,

View File

@@ -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');
}
}

View File

@@ -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();
}
}
}

306
src/scenes/IntroScene.ts Normal file
View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -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;