diff --git a/.env b/.env
new file mode 100644
index 0000000..8e3e86c
--- /dev/null
+++ b/.env
@@ -0,0 +1,5 @@
+# Domain for the icebreaker site
+NGINX_HOST=icebreaker.timeobossuet.tech
+
+# Email for Let's Encrypt SSL certificates
+LETSENCRYPT_EMAIL=contact@timeobossuet.tech
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..5b96d39
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,13 @@
+.PHONY: up down logs restart
+
+up:
+ docker compose up -d
+
+down:
+ docker compose down
+
+logs:
+ docker compose logs -f
+
+restart:
+ docker compose restart
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..11b5bf6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,51 @@
+# Icebreaker
+
+A simple HTML/CSS/JS website served via Nginx.
+
+## Setup
+
+1. Copy the environment file and configure it:
+ ```bash
+ cp .env.example .env
+ ```
+
+2. Edit `.env` and set your domain and email:
+ ```
+ NGINX_HOST=icebreaker.yourdomain.com
+ LETSENCRYPT_EMAIL=your-email@example.com
+ ```
+
+3. Make sure the proxy is running:
+ ```bash
+ cd ../proxy && docker compose up -d
+ ```
+
+4. Start the service:
+ ```bash
+ make up
+ ```
+
+## Commands
+
+- `make up` - Start the container
+- `make down` - Stop the container
+- `make logs` - View logs
+- `make restart` - Restart the container
+
+## Structure
+
+```
+icebreaker/
+├── docker-compose.yml
+├── Makefile
+├── README.md
+├── .env.example
+└── src/
+ ├── index.html
+ ├── css/
+ │ └── style.css
+ └── js/
+ └── main.js
+```
+
+Edit files in `src/` - changes are reflected immediately (no rebuild needed).
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..a36c9b1
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,17 @@
+services:
+ nginx:
+ image: nginx:alpine
+ container_name: icebreaker-nginx
+ restart: always
+ volumes:
+ - ./src:/usr/share/nginx/html:ro
+ environment:
+ - VIRTUAL_HOST=${NGINX_HOST}
+ - LETSENCRYPT_HOST=${NGINX_HOST}
+ - LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
+ networks:
+ - webnet
+
+networks:
+ webnet:
+ external: true
diff --git a/src/assets/css/checkpoint.css b/src/assets/css/checkpoint.css
new file mode 100644
index 0000000..4c74604
--- /dev/null
+++ b/src/assets/css/checkpoint.css
@@ -0,0 +1,308 @@
+:root {
+ --success-green: #4ade80;
+ --font-display: 'Orbitron', sans-serif;
+}
+
+body {
+ background: linear-gradient(180deg,
+ var(--arctic-dark) 0%,
+ var(--arctic-deep) 50%,
+ var(--arctic-medium) 100%);
+ display: flex;
+ flex-direction: column;
+ overflow-x: hidden;
+}
+
+#snow-container {
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ z-index: 100;
+}
+
+.snowflake {
+ position: absolute;
+ top: -20px;
+ color: white;
+ animation: snowfall linear infinite;
+ text-shadow: 0 0 5px rgba(255, 255, 255, 0.5);
+}
+
+@keyframes snowfall {
+ 0% {
+ transform: translateY(0) rotate(0deg);
+ opacity: 0;
+ }
+
+ 10% {
+ opacity: 1;
+ }
+
+ 90% {
+ opacity: 1;
+ }
+
+ 100% {
+ transform: translateY(100vh) rotate(360deg);
+ opacity: 0;
+ }
+}
+
+#checkpoint-main {
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 2rem;
+}
+
+.checkpoint-card {
+ background: var(--glass-bg);
+ backdrop-filter: blur(25px);
+ -webkit-backdrop-filter: blur(25px);
+ border: 1px solid var(--glass-border);
+ border-radius: 24px;
+ padding: 2.5rem;
+ width: 100%;
+ max-width: 420px;
+ box-shadow:
+ 0 8px 32px rgba(0, 0, 0, 0.3),
+ 0 0 60px rgba(135, 206, 235, 0.15);
+ animation: cardAppear 0.6s ease-out;
+}
+
+@keyframes cardAppear {
+ from {
+ opacity: 0;
+ transform: translateY(20px) scale(0.95);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+.card-header {
+ text-align: center;
+ margin-bottom: 2rem;
+}
+
+.success-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 70px;
+ height: 70px;
+ background: linear-gradient(135deg, var(--success-green), #22c55e);
+ border-radius: 50%;
+ font-size: 2rem;
+ color: white;
+ margin-bottom: 1rem;
+ box-shadow: 0 4px 20px rgba(74, 222, 128, 0.4);
+ animation: pulseSuccIceBreakeress 2s ease-in-out infinite;
+}
+
+@keyframes pulseSuccess {
+
+ 0%,
+ 100% {
+ box-shadow: 0 4px 20px rgba(74, 222, 128, 0.4);
+ }
+
+ 50% {
+ box-shadow: 0 4px 35px rgba(74, 222, 128, 0.6);
+ }
+}
+
+.card-header h1 {
+ font-family: var(--font-display);
+ font-size: 1.6rem;
+ background: linear-gradient(135deg, var(--ice-white), var(--ice-cyan));
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ margin-bottom: 0.5rem;
+ letter-spacing: 1px;
+}
+
+.subtitle {
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 0.95rem;
+}
+
+#captain-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-top: 1rem;
+ padding: 0.5rem 1rem;
+ background: rgba(255, 215, 0, 0.1);
+ border: 1px solid rgba(255, 215, 0, 0.3);
+ border-radius: 20px;
+ font-size: 0.85rem;
+ color: var(--gold-accent);
+}
+
+#captain-badge:empty {
+ display: none;
+}
+
+.badge-icon {
+ font-size: 1rem;
+}
+
+.scores-section {
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 16px;
+ padding: 1.5rem;
+ margin-bottom: 1.5rem;
+}
+
+.score-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.75rem 0;
+}
+
+.score-row:not(:last-child) {
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.score-label {
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 0.9rem;
+}
+
+.score-value {
+ font-family: var(--font-display);
+ font-weight: 600;
+ font-size: 1rem;
+ color: var(--ice-white);
+}
+
+.score-row.bonus .score-value {
+ color: var(--success-green);
+}
+
+.separator {
+ height: 2px;
+ background: linear-gradient(90deg, transparent, var(--ice-cyan), transparent);
+ margin: 0.5rem 0;
+ border: none;
+}
+
+.score-row.total {
+ padding-top: 1rem;
+}
+
+.score-row.total .score-label {
+ font-weight: 600;
+ color: var(--gold-accent);
+}
+
+.score-row.total .score-value {
+ font-size: 1.5rem;
+ color: var(--gold-accent);
+ text-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
+ animation: glowPulse 2s ease-in-out infinite;
+}
+
+@keyframes glowPulse {
+
+ 0%,
+ 100% {
+ text-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
+ }
+
+ 50% {
+ text-shadow: 0 0 30px rgba(255, 215, 0, 0.8);
+ }
+}
+
+.actions {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.75rem;
+ padding: 1rem 1.5rem;
+ border: none;
+ border-radius: 12px;
+ font-family: var(--font-body);
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.btn-icon {
+ font-size: 1.2rem;
+}
+
+.btn-primary {
+ background: linear-gradient(135deg, var(--ice-cyan), var(--arctic-light));
+ color: var(--arctic-dark);
+ box-shadow: 0 4px 15px rgba(0, 212, 255, 0.3);
+}
+
+.btn-primary:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 25px rgba(0, 212, 255, 0.5);
+}
+
+.btn-primary:active {
+ transform: translateY(0);
+}
+
+.btn-secondary {
+ background: transparent;
+ color: var(--ice-white);
+ border: 2px solid var(--glass-border);
+}
+
+.btn-secondary:hover {
+ background: var(--glass-bg);
+ border-color: var(--ice-cyan);
+ color: var(--ice-cyan);
+}
+
+@media (max-width: 480px) {
+ #checkpoint-main {
+ padding: 1rem;
+ }
+
+ .checkpoint-card {
+ padding: 1.5rem;
+ border-radius: 20px;
+ }
+
+ .card-header h1 {
+ font-size: 1.3rem;
+ }
+
+ .success-icon {
+ width: 60px;
+ height: 60px;
+ font-size: 1.6rem;
+ }
+
+ .score-row.total .score-value {
+ font-size: 1.3rem;
+ }
+
+ .btn {
+ padding: 0.875rem 1.25rem;
+ font-size: 0.9rem;
+ }
+
+ .footer-nav {
+ gap: 1.5rem;
+ }
+}
\ No newline at end of file
diff --git a/src/assets/css/cruise.css b/src/assets/css/cruise.css
new file mode 100644
index 0000000..961e624
--- /dev/null
+++ b/src/assets/css/cruise.css
@@ -0,0 +1,612 @@
+@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&family=Roboto:wght@400;700&display=swap');
+
+
+
+:root {
+ --bg-ocean-deep: #0a1929;
+ --bg-ocean: #0d2137;
+ --bg-ocean-light: #1565c0;
+ --bg-ocean-surface: #1976d2;
+ --color-ice: #b3e5fc;
+ --color-ice-glow: rgba(179, 229, 252, 0.4);
+ --text-main: #e3f2fd;
+ --text-muted: #90a4ae;
+ --ui-glass: rgba(10, 25, 41, 0.85);
+ --ui-border: rgba(179, 229, 252, 0.2);
+ --container-blue: #4fc3f7;
+ --container-red: #ff8a80;
+ --container-yellow: #ffd54f;
+ --color-primary: #4fc3f7;
+ --color-accent: #ffb74d;
+ --color-danger: #ff5252;
+ --color-success: #69f0ae;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ height: 100vh;
+ overflow: hidden;
+ background: linear-gradient(180deg,
+ var(--bg-ocean-deep) 0%,
+ var(--bg-ocean) 30%,
+ var(--bg-ocean-light) 70%,
+ var(--bg-ocean-surface) 100%);
+ font-family: 'Roboto', sans-serif;
+ user-select: none;
+}
+
+#score-panel {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ width: 240px;
+ padding: 15px;
+ background: var(--ui-glass);
+ backdrop-filter: blur(15px);
+ -webkit-backdrop-filter: blur(15px);
+ border: 1px solid var(--ui-border);
+ border-radius: 16px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 0 20px var(--color-ice-glow);
+ color: var(--text-main);
+ z-index: 100;
+}
+
+
+.hud-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid var(--ui-border);
+}
+
+.hud-title {
+ font-family: 'Poppins', sans-serif;
+ font-weight: 700;
+ font-size: 1rem;
+ color: var(--color-ice);
+ letter-spacing: 1px;
+}
+
+.captain-badge {
+ font-size: 0.75rem;
+ color: var(--color-accent);
+ background: rgba(255, 183, 77, 0.15);
+ padding: 3px 8px;
+ border-radius: 10px;
+}
+
+.score-grid {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.score-box {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 8px 10px;
+ background: rgba(255, 255, 255, 0.03);
+ border-radius: 10px;
+ transition: background 0.2s ease;
+}
+
+.score-box.highlight {
+ background: rgba(79, 195, 247, 0.1);
+ border: 1px solid rgba(79, 195, 247, 0.3);
+}
+
+.score-icon {
+ width: 28px;
+ height: 28px;
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4));
+}
+
+.score-info {
+ display: flex;
+ flex-direction: column;
+}
+
+.score-label {
+ font-size: 0.7rem;
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.score-value {
+ font-weight: 700;
+ font-size: 1.1rem;
+ color: var(--text-main);
+}
+
+.score-box.highlight .score-value {
+ color: var(--color-primary);
+ font-size: 1.3rem;
+}
+
+.game-audio-panel {
+ position: fixed;
+ top: 20px;
+ left: 20px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 12px;
+ background: var(--ui-glass);
+ backdrop-filter: blur(10px);
+ border: 1px solid var(--ui-border);
+ border-radius: 25px;
+ z-index: 100;
+ opacity: 0.7;
+ transition: opacity 0.3s ease;
+}
+
+.game-audio-panel:hover {
+ opacity: 1;
+}
+
+.audio-btn-game {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.2s ease;
+}
+
+.audio-btn-game:hover {
+ background: rgba(79, 195, 247, 0.2);
+}
+
+.audio-icon {
+ font-size: 1.2rem;
+}
+
+.volume-slider {
+ width: 80px;
+ height: 4px;
+ -webkit-appearance: none;
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 2px;
+ outline: none;
+}
+
+.volume-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: var(--color-primary);
+ cursor: pointer;
+}
+
+#game-world {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ transform: translateZ(0);
+ will-change: contents;
+}
+
+#iceberg-layer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: 5;
+ transform: translateZ(0);
+ contain: layout style;
+}
+
+.iceberg {
+ position: absolute;
+ top: -150px;
+ will-change: transform;
+ transform: translate3d(0, 0, 0);
+ backface-visibility: hidden;
+ -webkit-backface-visibility: hidden;
+}
+
+.boat {
+ position: absolute;
+ bottom: 20px;
+ left: 0;
+ width: 50px;
+ height: auto;
+ z-index: 10;
+ transform-origin: center bottom;
+ will-change: transform, bottom;
+ backface-visibility: hidden;
+ -webkit-backface-visibility: hidden;
+}
+
+.boat img {
+ width: 100%;
+ display: block;
+ pointer-events: none;
+}
+
+.boat .overlay {
+ position: absolute;
+ top: 13%;
+ left: 7%;
+ width: 84%;
+ height: 73%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+}
+
+.container-row {
+ display: flex;
+ justify-content: space-between;
+ height: 18%;
+}
+
+.container {
+ width: 22%;
+ height: 100%;
+ border-radius: 2px;
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.3), 0 1px 3px rgba(0, 0, 0, 0.2);
+ position: relative;
+ will-change: transform, opacity;
+ transform: translateZ(0);
+}
+
+.container.dropped-left {
+ animation: dropLeft 1s ease-in forwards;
+ z-index: 100;
+}
+
+.container.dropped-right {
+ animation: dropRight 1s ease-in forwards;
+ z-index: 100;
+}
+
+@keyframes dropLeft {
+ 0% {
+ transform: translate3d(0, 0, 0) rotate(0);
+ opacity: 1;
+ }
+
+ 100% {
+ transform: translate3d(-150px, 200px, 0) rotate(-90deg);
+ opacity: 0;
+ }
+}
+
+@keyframes dropRight {
+ 0% {
+ transform: translate3d(0, 0, 0) rotate(0);
+ opacity: 1;
+ }
+
+ 100% {
+ transform: translate3d(150px, 200px, 0) rotate(90deg);
+ opacity: 0;
+ }
+}
+
+.hint {
+ position: fixed;
+ bottom: 15px;
+ width: 100%;
+ text-align: center;
+ color: var(--text-muted);
+ font-size: 0.85rem;
+ z-index: 200;
+ opacity: 0.6;
+ transition: opacity 0.3s ease;
+}
+
+.hint:hover {
+ opacity: 1;
+}
+
+.hint-keys {
+ background: rgba(255, 255, 255, 0.1);
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-family: monospace;
+ font-size: 0.9rem;
+}
+
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(10, 25, 41, 0.95);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+ opacity: 1;
+ transition: opacity 0.5s ease;
+}
+
+.modal-content {
+ background: var(--ui-glass);
+ backdrop-filter: blur(20px);
+ border: 1px solid var(--ui-border);
+ border-radius: 20px;
+ padding: 40px;
+ width: 90%;
+ max-width: 420px;
+ text-align: center;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 40px var(--color-ice-glow);
+ color: var(--text-main);
+}
+
+.modal-icon {
+ font-size: 3rem;
+ margin-bottom: 15px;
+ animation: pulse 2s ease-in-out infinite;
+}
+
+@keyframes pulse {
+
+ 0%,
+ 100% {
+ transform: scale(1);
+ }
+
+ 50% {
+ transform: scale(1.1);
+ }
+}
+
+.modal-content h3 {
+ font-family: 'Poppins', sans-serif;
+ font-size: 1.5rem;
+ margin: 0 0 15px 0;
+ color: var(--color-ice);
+}
+
+.modal-content p {
+ color: var(--text-muted);
+ line-height: 1.6;
+ margin: 0 0 25px 0;
+}
+
+
+
+
+.modal-button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ width: 100%;
+ padding: 15px 25px;
+ background: linear-gradient(135deg, var(--color-primary) 0%, #29b6f6 100%);
+ color: var(--bg-ocean-deep);
+ border: none;
+ border-radius: 12px;
+ cursor: pointer;
+ font-size: 1rem;
+ font-weight: 700;
+ font-family: inherit;
+ transition: all 0.2s ease;
+}
+
+.modal-button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(79, 195, 247, 0.4);
+}
+
+.btn-wave {
+ animation: wave-icon 1s ease-in-out infinite;
+}
+
+@keyframes wave-icon {
+
+ 0%,
+ 100% {
+ transform: rotate(-10deg);
+ }
+
+ 50% {
+ transform: rotate(10deg);
+ }
+}
+
+.modal-button-secondary {
+ display: block;
+ width: 100%;
+ margin-top: 12px;
+ padding: 12px;
+ background: transparent;
+ color: var(--text-muted);
+ border: 1px solid var(--ui-border);
+ border-radius: 10px;
+ cursor: pointer;
+ font-size: 0.9rem;
+ font-family: inherit;
+ transition: all 0.2s ease;
+}
+
+.modal-button-secondary:hover {
+ color: var(--text-main);
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+.modal-overlay.hidden {
+ opacity: 0;
+ pointer-events: none;
+}
+
+.gameover-content {
+ animation: gameoverAppear 0.5s ease-out;
+}
+
+@keyframes gameoverAppear {
+ 0% {
+ transform: scale(0.8);
+ opacity: 0;
+ }
+
+ 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+.gameover-icon {
+ font-size: 4rem;
+ margin-bottom: 10px;
+ animation: shake 0.6s ease-in-out;
+}
+
+@keyframes shake {
+
+ 0%,
+ 100% {
+ transform: translateX(0);
+ }
+
+ 10%,
+ 30%,
+ 50%,
+ 70%,
+ 90% {
+ transform: translateX(-5px);
+ }
+
+ 20%,
+ 40%,
+ 60%,
+ 80% {
+ transform: translateX(5px);
+ }
+}
+
+.gameover-title {
+ font-family: 'Poppins', sans-serif;
+ font-size: 2rem;
+ font-weight: 700;
+ color: var(--color-danger);
+ letter-spacing: 3px;
+ margin-bottom: 8px;
+ text-shadow: 0 0 20px rgba(255, 82, 82, 0.5);
+}
+
+.gameover-subtitle {
+ color: var(--text-muted);
+ font-size: 0.95rem;
+ margin: 0 0 25px 0;
+}
+
+.gameover-stats {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ margin-bottom: 25px;
+ padding: 15px;
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 12px;
+ border: 1px solid var(--ui-border);
+}
+
+.gameover-stat {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 8px 10px;
+ background: rgba(255, 255, 255, 0.03);
+ border-radius: 8px;
+}
+
+.stat-icon {
+ font-size: 1.5rem;
+ width: 40px;
+ text-align: center;
+}
+
+.stat-info {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+}
+
+.stat-label {
+ font-size: 0.7rem;
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.stat-value {
+ font-weight: 700;
+ font-size: 1.2rem;
+ color: var(--text-main);
+}
+
+.gameover-btn {
+ background: linear-gradient(135deg, var(--color-danger) 0%, #ff1744 100%) !important;
+ box-shadow: 0 4px 15px rgba(255, 82, 82, 0.3);
+}
+
+.gameover-btn:hover {
+ box-shadow: 0 8px 25px rgba(255, 82, 82, 0.5) !important;
+}
+
+@media (max-width: 600px) {
+ #score-panel {
+ width: calc(100% - 40px);
+ top: auto;
+ bottom: 60px;
+ right: 20px;
+ }
+
+ .score-grid {
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+
+ .score-box {
+ flex: 1;
+ min-width: 80px;
+ }
+
+ .game-audio-panel {
+ top: 10px;
+ left: 10px;
+ }
+
+ .volume-slider {
+ width: 60px;
+ }
+
+ .gameover-title {
+ font-size: 1.5rem;
+ }
+
+ .gameover-stats {
+ padding: 10px;
+ }
+
+ .stat-icon {
+ font-size: 1.2rem;
+ width: 30px;
+ }
+
+ .stat-value {
+ font-size: 1rem;
+ }
+}
\ No newline at end of file
diff --git a/src/assets/css/index.css b/src/assets/css/index.css
new file mode 100644
index 0000000..165acf5
--- /dev/null
+++ b/src/assets/css/index.css
@@ -0,0 +1,670 @@
+:root {
+ --color-ice: #b3e5fc;
+ --color-ice-light: #e1f5fe;
+ --color-ice-glow: rgba(179, 229, 252, 0.4);
+ --color-primary: #4fc3f7;
+ --color-primary-bright: #81d4fa;
+ --color-accent-warm: #ffb74d;
+ --color-text-secondary: #90a4ae;
+ --color-text-muted: #607d8b;
+ --shadow-glass: 0 8px 32px rgba(13, 33, 55, 0.5);
+ --shadow-glow: 0 0 30px var(--color-ice-glow);
+ --transition-fast: 0.15s ease;
+ --transition-normal: 0.3s ease;
+}
+
+body#indexBody {
+ background: linear-gradient(180deg, var(--arctic-dark) 0%, var(--arctic-deep) 50%, var(--arctic-medium) 100%);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ overflow-x: hidden;
+}
+
+#snow-container {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: 1;
+ overflow: hidden;
+}
+
+.snowflake {
+ position: absolute;
+ top: -20px;
+ color: var(--ice-white);
+ animation: snowfall linear infinite;
+ text-shadow: 0 0 5px rgba(255, 255, 255, 0.5);
+}
+
+@keyframes snowfall {
+ 0% {
+ transform: translateY(-10px) rotate(0deg);
+ }
+
+ 100% {
+ transform: translateY(100vh) rotate(360deg);
+ }
+}
+
+#arctic-background {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+ overflow: hidden;
+}
+
+#arctic-aurora {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 40%;
+ background: linear-gradient(135deg,
+ transparent 30%,
+ rgba(79, 195, 247, 0.1) 40%,
+ rgba(129, 212, 250, 0.08) 50%,
+ rgba(100, 255, 218, 0.06) 60%,
+ transparent 70%);
+ animation: aurora 15s ease-in-out infinite alternate;
+}
+
+@keyframes aurora {
+ 0% {
+ opacity: 0.3;
+ transform: translateX(-5%) skewX(-5deg);
+ }
+
+ 50% {
+ opacity: 0.6;
+ }
+
+ 100% {
+ opacity: 0.4;
+ transform: translateX(5%) skewX(5deg);
+ }
+}
+
+#arctic-wave,
+#arctic-wave-2 {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 200%;
+ height: 200px;
+ background: linear-gradient(to top,
+ rgba(13, 71, 161, 0.6) 0%,
+ rgba(21, 101, 192, 0.4) 50%,
+ transparent 100%);
+}
+
+#arctic-wave {
+ clip-path: polygon(0 70%, 5% 65%, 10% 68%, 15% 62%, 20% 66%, 25% 60%,
+ 30% 64%, 35% 58%, 40% 62%, 45% 56%, 50% 60%,
+ 55% 54%, 60% 58%, 65% 52%, 70% 56%, 75% 50%,
+ 80% 54%, 85% 48%, 90% 52%, 95% 46%, 100% 50%,
+ 100% 100%, 0 100%);
+ animation: wave 8s ease-in-out infinite;
+}
+
+#arctic-wave-2 {
+ height: 180px;
+ background: linear-gradient(to top,
+ rgba(25, 118, 210, 0.5) 0%,
+ rgba(66, 165, 245, 0.3) 50%,
+ transparent 100%);
+ clip-path: polygon(0 75%, 5% 70%, 10% 73%, 15% 67%, 20% 71%, 25% 65%,
+ 30% 69%, 35% 63%, 40% 67%, 45% 61%, 50% 65%,
+ 55% 59%, 60% 63%, 65% 57%, 70% 61%, 75% 55%,
+ 80% 59%, 85% 53%, 90% 57%, 95% 51%, 100% 55%,
+ 100% 100%, 0 100%);
+ animation: wave 10s ease-in-out infinite reverse;
+ animation-delay: -2s;
+}
+
+@keyframes wave {
+
+ 0%,
+ 100% {
+ transform: translateX(0);
+ }
+
+ 50% {
+ transform: translateX(-25%);
+ }
+}
+
+@keyframes float {
+
+ 0%,
+ 100% {
+ transform: translateY(0) rotate(-2deg);
+ }
+
+ 50% {
+ transform: translateY(-15px) rotate(2deg);
+ }
+}
+
+.main-content {
+ position: relative;
+ z-index: 10;
+ width: 95%;
+ max-width: 800px;
+ padding: 40px 20px;
+ animation: fadeInUp 0.8s ease-out;
+}
+
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY(30px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.hub-container {
+ background: rgba(13, 33, 55, 0.6);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border-radius: 24px;
+ border: 1px solid rgba(179, 229, 252, 0.01);
+ box-shadow: var(--shadow-glass), var(--shadow-glow);
+ padding: 40px;
+ transition: transform var(--transition-normal), box-shadow var(--transition-normal);
+}
+
+header {
+ text-align: center;
+ margin-bottom: 35px;
+}
+
+.logo-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 15px;
+ margin-bottom: 15px;
+}
+
+.logo-icon {
+ font-size: 3rem;
+ filter: drop-shadow(0 0 10px var(--color-ice-glow));
+ animation: bobLogo 3s ease-in-out infinite;
+}
+
+@keyframes bobLogo {
+
+ 0%,
+ 100% {
+ transform: translateY(0) rotate(-3deg);
+ }
+
+ 50% {
+ transform: translateY(-5px) rotate(3deg);
+ }
+}
+
+#main-title {
+ font-size: clamp(2rem, 6vw, 3.5rem);
+ font-weight: 700;
+ background: linear-gradient(135deg, var(--color-ice-light) 0%, var(--color-primary) 50%, var(--color-ice) 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ text-shadow: none;
+ letter-spacing: 3px;
+ margin: 0;
+}
+
+.welcome-text {
+ color: var(--color-text-secondary);
+ font-size: 1.1rem;
+ font-weight: 300;
+ margin: 0;
+ line-height: 1.6;
+}
+
+#captain-section {
+ margin-bottom: 30px;
+ padding: 25px;
+ background: rgba(79, 195, 247, 0.08);
+ border-radius: 16px;
+ border: 1px solid rgba(79, 195, 247, 0.2);
+}
+
+#captain-section.hidden {
+ display: none;
+}
+
+.captain-welcome h2 {
+ color: var(--color-primary);
+ font-size: 1.4rem;
+ margin: 0 0 10px 0;
+}
+
+.captain-welcome h2 .icon {
+ margin-right: 8px;
+}
+
+.captain-welcome p {
+ color: var(--color-text-secondary);
+ margin-bottom: 20px;
+}
+
+.input-group {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+#captain-name-input {
+ flex: 1;
+ min-width: 200px;
+ padding: 14px 20px;
+ border: 2px solid rgba(79, 195, 247, 0.3);
+ border-radius: 12px;
+ background: rgba(10, 25, 41, 0.6);
+ color: var(--color-text-primary);
+ font-size: 1rem;
+ font-family: inherit;
+ transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
+}
+
+#captain-name-input:focus {
+ outline: none;
+ border-color: var(--color-primary);
+ box-shadow: 0 0 0 3px rgba(79, 195, 247, 0.2);
+}
+
+#captain-name-input.error {
+ animation: shake 0.5s ease;
+ border-color: var(--color-danger);
+}
+
+@keyframes shake {
+
+ 0%,
+ 100% {
+ transform: translateX(0);
+ }
+
+ 25% {
+ transform: translateX(-5px);
+ }
+
+ 75% {
+ transform: translateX(5px);
+ }
+}
+
+#captain-name-input::placeholder {
+ color: var(--color-text-muted);
+}
+
+.btn-primary {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 14px 28px;
+ background: linear-gradient(135deg, var(--color-primary) 0%, #29b6f6 100%);
+ color: var(--color-bg-deep);
+ border: none;
+ border-radius: 12px;
+ font-size: 1rem;
+ font-weight: 600;
+ font-family: inherit;
+ cursor: pointer;
+ transition: all var(--transition-fast);
+}
+
+.btn-primary:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(79, 195, 247, 0.4);
+}
+
+.btn-primary:active {
+ transform: translateY(0);
+}
+
+.btn-icon {
+ font-size: 1.2rem;
+ transition: transform var(--transition-fast);
+}
+
+.btn-primary:hover .btn-icon {
+ transform: translateX(3px);
+}
+
+#difficulty-selection {
+ margin-bottom: 30px;
+}
+
+#difficulty-selection h2 {
+ color: var(--color-text-primary);
+ font-size: 1.3rem;
+ margin: 0 0 8px 0;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.greeting-text {
+ color: var(--color-accent-warm);
+ font-size: 0.95rem;
+ font-style: italic;
+ margin: 0 0 25px 0;
+ min-height: 1.5em;
+}
+
+.difficulty-options {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 15px;
+}
+
+.diff-btn {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ padding: 20px 15px;
+ background: rgba(255, 255, 255, 0.03);
+ border: 2px solid rgba(179, 229, 252, 0.15);
+ border-radius: 16px;
+ color: var(--color-text-primary);
+ cursor: pointer;
+ transition: all var(--transition-normal);
+ font-family: inherit;
+}
+
+.diff-btn:hover {
+ background: rgba(79, 195, 247, 0.1);
+ border-color: var(--color-primary);
+ transform: translateY(-5px);
+ box-shadow: 0 10px 30px rgba(79, 195, 247, 0.2);
+}
+
+.diff-btn:active {
+ transform: translateY(-2px);
+}
+
+.diff-icon {
+ font-size: 2rem;
+}
+
+.diff-name {
+ font-weight: 600;
+ font-size: 1rem;
+}
+
+.diff-desc {
+ font-size: 0.8rem;
+ color: var(--color-text-muted);
+}
+
+#leaderboard {
+ margin-bottom: 20px;
+}
+
+#leaderboard h2 {
+ color: var(--color-accent-warm);
+ font-size: 1.3rem;
+ margin: 0 0 20px 0;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding-top: 20px;
+ border-top: 1px solid rgba(179, 229, 252, 0.1);
+}
+
+.leaderboard-content {
+ overflow-x: auto;
+}
+
+#results-table {
+ width: 100%;
+ border-collapse: collapse;
+ border-radius: 12px;
+ overflow: hidden;
+}
+
+#results-table th,
+#results-table td {
+ padding: 14px 12px;
+ text-align: center;
+}
+
+#results-table th {
+ background: rgba(79, 195, 247, 0.15);
+ color: var(--color-ice);
+ font-weight: 600;
+ font-size: 0.85rem;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+#results-table tbody tr {
+ border-bottom: 1px solid rgba(179, 229, 252, 0.08);
+ transition: background var(--transition-fast);
+}
+
+#results-table tbody tr:hover {
+ background: rgba(79, 195, 247, 0.05);
+}
+
+#results-table tbody tr.top-score {
+ background: rgba(105, 240, 174, 0.08);
+}
+
+#results-table tbody tr.top-score td {
+ color: var(--color-success);
+ font-weight: 600;
+}
+
+#results-table .rank {
+ font-size: 1.2rem;
+}
+
+#results-table .score {
+ font-weight: 700;
+ color: var(--color-primary-bright);
+}
+
+#results-table .captain {
+ color: var(--color-text-primary);
+}
+
+#results-table .no-data {
+ color: var(--color-text-muted);
+ font-style: italic;
+ padding: 30px;
+}
+
+.audio-panel {
+ position: fixed;
+ bottom: 100px;
+ right: 20px;
+ z-index: 100;
+}
+
+.audio-btn {
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: rgba(13, 33, 55, 0.8);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(179, 229, 252, 0.2);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.audio-btn:hover {
+ background: rgba(79, 195, 247, 0.2);
+ transform: scale(1.1);
+}
+
+.audio-icon {
+ font-size: 1.4rem;
+}
+
+.audio-slider-wrap {
+ position: absolute;
+ bottom: 60px;
+ right: 0;
+ background: rgba(13, 33, 55, 0.95);
+ backdrop-filter: blur(15px);
+ padding: 15px 20px;
+ border-radius: 12px;
+ border: 1px solid rgba(179, 229, 252, 0.2);
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ min-width: 150px;
+}
+
+.audio-slider-wrap.hidden {
+ display: none;
+}
+
+.audio-slider-wrap label {
+ font-size: 0.85rem;
+ color: var(--color-text-secondary);
+}
+
+#master-volume {
+ -webkit-appearance: none;
+ width: 100%;
+ height: 6px;
+ border-radius: 3px;
+ background: rgba(255, 255, 255, 0.1);
+ outline: none;
+}
+
+#master-volume::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ background: var(--color-primary);
+ cursor: pointer;
+ transition: transform var(--transition-fast);
+}
+
+#master-volume::-webkit-slider-thumb:hover {
+ transform: scale(1.2);
+}
+
+#volume-value {
+ font-size: 0.9rem;
+ color: var(--color-primary);
+ text-align: center;
+}
+
+#main-footer {
+ position: relative;
+ z-index: 10;
+ width: 100%;
+ margin-top: auto;
+ padding: 20px;
+ background: rgba(10, 25, 41, 0.8);
+ backdrop-filter: blur(10px);
+ border-top: 1px solid rgba(179, 229, 252, 0.1);
+}
+
+.footer-nav {
+ display: flex;
+ justify-content: center;
+ gap: 40px;
+ margin-bottom: 15px;
+}
+
+.footer-link {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 5px;
+ color: var(--color-text-secondary);
+ text-decoration: none;
+ font-size: 0.9rem;
+ transition: color var(--transition-fast);
+}
+
+.footer-link:hover,
+.footer-link.active {
+ color: var(--color-primary);
+}
+
+.nav-icon {
+ font-size: 1.3rem;
+}
+
+.copyright {
+ text-align: center;
+ color: var(--color-text-muted);
+ font-size: 0.8rem;
+ margin: 0;
+}
+
+.hidden {
+ display: none !important;
+}
+
+.fade-in {
+ animation: fadeInUp 0.5s ease-out;
+}
+
+@media (max-width: 600px) {
+ .hub-container {
+ padding: 25px 20px;
+ }
+
+ #main-title {
+ font-size: 2rem;
+ }
+
+ .logo-icon {
+ font-size: 2.5rem;
+ }
+
+ .difficulty-options {
+ grid-template-columns: 1fr;
+ }
+
+ .input-group {
+ flex-direction: column;
+ }
+
+ #captain-name-input {
+ min-width: 100%;
+ }
+
+ .btn-primary {
+ width: 100%;
+ justify-content: center;
+ }
+
+ #results-table th,
+ #results-table td {
+ padding: 10px 8px;
+ font-size: 0.85rem;
+ }
+
+ .audio-panel {
+ bottom: 90px;
+ right: 15px;
+ }
+}
\ No newline at end of file
diff --git a/src/assets/css/main.css b/src/assets/css/main.css
new file mode 100644
index 0000000..16d316c
--- /dev/null
+++ b/src/assets/css/main.css
@@ -0,0 +1,55 @@
+@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700&family=Poppins:wght@300;400;500;600;700&display=swap');
+
+:root {
+ --arctic-dark: #0a1628;
+ --arctic-deep: #0d2137;
+ --arctic-medium: #1a3a5c;
+ --arctic-light: #2d5a87;
+ --arctic-pale: #4a7eb3;
+ --ice-white: #f0f8ff;
+ --ice-blue: #87ceeb;
+ --ice-cyan: #00d4ff;
+ --ice-glow: rgba(135, 206, 235, 0.4);
+ --container-blue: #4fc3f7;
+ --container-red: #ff8a80;
+ --container-yellow: #ffd54f;
+ --gold-accent: #ffd700;
+ --success: #4ade80;
+ --danger: #ef4444;
+ --text-main: var(--ice-white);
+ --text-muted: rgba(255, 255, 255, 0.6);
+ --glass-bg: rgba(255, 255, 255, 0.05);
+ --glass-border: rgba(255, 255, 255, 0.15);
+ --font-title: 'Orbitron', 'Segoe UI', sans-serif;
+ --font-body: 'Poppins', 'Segoe UI', sans-serif;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ margin: 0;
+ padding: 0;
+ font-family: var(--font-body);
+ color: var(--text-main);
+ font-size: 16px;
+ min-height: 100vh;
+}
+
+h1,
+h2,
+h3 {
+ font-family: var(--font-title);
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--text-main);
+ margin: 0;
+}
+
+.hidden {
+ display: none !important;
+}
\ No newline at end of file
diff --git a/src/assets/img/cargoship.png b/src/assets/img/cargoship.png
new file mode 100644
index 0000000..3e8cc9d
Binary files /dev/null and b/src/assets/img/cargoship.png differ
diff --git a/src/assets/img/container.png b/src/assets/img/container.png
new file mode 100644
index 0000000..b67305b
Binary files /dev/null and b/src/assets/img/container.png differ
diff --git a/src/assets/img/icebergs/iceberg-1.png b/src/assets/img/icebergs/iceberg-1.png
new file mode 100644
index 0000000..409cf12
Binary files /dev/null and b/src/assets/img/icebergs/iceberg-1.png differ
diff --git a/src/assets/img/icebergs/iceberg-10.png b/src/assets/img/icebergs/iceberg-10.png
new file mode 100644
index 0000000..28f4355
Binary files /dev/null and b/src/assets/img/icebergs/iceberg-10.png differ
diff --git a/src/assets/img/icebergs/iceberg-11.png b/src/assets/img/icebergs/iceberg-11.png
new file mode 100644
index 0000000..3393e01
Binary files /dev/null and b/src/assets/img/icebergs/iceberg-11.png differ
diff --git a/src/assets/img/icebergs/iceberg-12.png b/src/assets/img/icebergs/iceberg-12.png
new file mode 100644
index 0000000..1c3fb43
Binary files /dev/null and b/src/assets/img/icebergs/iceberg-12.png differ
diff --git a/src/assets/img/icebergs/iceberg-2.png b/src/assets/img/icebergs/iceberg-2.png
new file mode 100644
index 0000000..7f52096
Binary files /dev/null and b/src/assets/img/icebergs/iceberg-2.png differ
diff --git a/src/assets/img/icebergs/iceberg-3.png b/src/assets/img/icebergs/iceberg-3.png
new file mode 100644
index 0000000..da501e7
Binary files /dev/null and b/src/assets/img/icebergs/iceberg-3.png differ
diff --git a/src/assets/img/icebergs/iceberg-4.png b/src/assets/img/icebergs/iceberg-4.png
new file mode 100644
index 0000000..fb765fb
Binary files /dev/null and b/src/assets/img/icebergs/iceberg-4.png differ
diff --git a/src/assets/img/icebergs/iceberg-5.png b/src/assets/img/icebergs/iceberg-5.png
new file mode 100644
index 0000000..a663278
Binary files /dev/null and b/src/assets/img/icebergs/iceberg-5.png differ
diff --git a/src/assets/img/icebergs/iceberg-6.png b/src/assets/img/icebergs/iceberg-6.png
new file mode 100644
index 0000000..67218c1
Binary files /dev/null and b/src/assets/img/icebergs/iceberg-6.png differ
diff --git a/src/assets/img/icebergs/iceberg-7.png b/src/assets/img/icebergs/iceberg-7.png
new file mode 100644
index 0000000..3c7b320
Binary files /dev/null and b/src/assets/img/icebergs/iceberg-7.png differ
diff --git a/src/assets/img/icebergs/iceberg-8.png b/src/assets/img/icebergs/iceberg-8.png
new file mode 100644
index 0000000..8a2f966
Binary files /dev/null and b/src/assets/img/icebergs/iceberg-8.png differ
diff --git a/src/assets/img/icebergs/iceberg-9.png b/src/assets/img/icebergs/iceberg-9.png
new file mode 100644
index 0000000..0585fbf
Binary files /dev/null and b/src/assets/img/icebergs/iceberg-9.png differ
diff --git a/src/assets/img/kilometer.png b/src/assets/img/kilometer.png
new file mode 100644
index 0000000..6724b13
Binary files /dev/null and b/src/assets/img/kilometer.png differ
diff --git a/src/assets/js/audio-manager.js b/src/assets/js/audio-manager.js
new file mode 100644
index 0000000..c58019e
--- /dev/null
+++ b/src/assets/js/audio-manager.js
@@ -0,0 +1,215 @@
+/*
+ * Audio - sons et musique du jeu
+ */
+
+const AudioManager = (function () {
+ 'use strict';
+
+ let isInitialized = false;
+ let audioContext = null;
+
+ // sons
+ const sounds = {
+ bgm: null,
+ drop: null,
+ alert: null,
+ break: null
+ };
+
+ // par défaut
+ let settings = {
+ masterVolume: 0.8,
+ sfxVolume: 1.0,
+ musicVolume: 0.6,
+ sfxEnabled: true,
+ musicEnabled: true
+ };
+
+ // init
+ function init(audioElements = {}) {
+ // Charger les paramètres depuis le storage
+ if (typeof IceBreakerStorage !== 'undefined') {
+ settings = { ...settings, ...IceBreakerStorage.getSettings() };
+ }
+
+ // Assigner les éléments audio
+ sounds.bgm = audioElements.bgm || document.getElementById('background-audio');
+ sounds.drop = audioElements.drop || document.getElementById('sfx-drop');
+ sounds.alert = audioElements.alert || document.getElementById('sfx-alert');
+ sounds.break = audioElements.break || document.getElementById('sfx-break');
+
+ // Appliquer les volumes initiaux
+ applyVolumes();
+
+ // Différer la création du contexte audio jusqu'à l'interaction utilisateur
+ const createAudioContext = () => {
+ if (audioContext) return;
+ try {
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
+ } catch (e) {
+ console.warn('AudioContext non supporté');
+ }
+ document.removeEventListener('click', createAudioContext);
+ document.removeEventListener('keydown', createAudioContext);
+ };
+
+ document.addEventListener('click', createAudioContext, { once: true });
+ document.addEventListener('keydown', createAudioContext, { once: true });
+
+ isInitialized = true;
+ }
+
+ // précharger les sons
+ function preload() {
+ if (!isInitialized) return;
+
+ Object.entries(sounds).forEach(([name, audio]) => {
+ if (audio && name !== 'bgm') {
+ audio.volume = 0;
+ audio.play().then(() => {
+ audio.pause();
+ audio.currentTime = 0;
+ applyVolumes();
+ }).catch(() => { });
+ }
+ });
+ }
+
+ // mettre à jour les volumes
+ function applyVolumes() {
+ const master = settings.masterVolume;
+
+ if (sounds.bgm) {
+ sounds.bgm.volume = settings.musicEnabled ? master * settings.musicVolume : 0;
+ }
+
+ ['drop', 'alert', 'break'].forEach(name => {
+ if (sounds[name]) {
+ sounds[name].volume = settings.sfxEnabled ? master * settings.sfxVolume : 0;
+ }
+ });
+ }
+
+ // jouer un son
+ function playSfx(name, volumeMultiplier = 1.0) {
+ if (!settings.sfxEnabled || !sounds[name]) return;
+
+ const audio = sounds[name];
+ audio.currentTime = 0;
+ audio.volume = settings.masterVolume * settings.sfxVolume * volumeMultiplier;
+ audio.play().catch(() => { });
+ }
+
+ // musique de fond
+ function playMusic() {
+ if (!settings.musicEnabled || !sounds.bgm) return Promise.resolve();
+
+ sounds.bgm.volume = settings.masterVolume * settings.musicVolume;
+ return sounds.bgm.play().catch(e => {
+ console.warn('Impossible de jouer la musique:', e);
+ });
+ }
+
+ // stop musique
+ function stopMusic() {
+ if (sounds.bgm) {
+ sounds.bgm.pause();
+ sounds.bgm.currentTime = 0;
+ }
+ }
+
+ // pause
+ function pauseMusic() {
+ if (sounds.bgm) {
+ sounds.bgm.pause();
+ }
+ }
+
+ // changer un param
+ function setSetting(key, value) {
+ if (key in settings) {
+ settings[key] = value;
+ applyVolumes();
+
+ // Sauvegarder
+ if (typeof IceBreakerStorage !== 'undefined') {
+ IceBreakerStorage.saveSettings(settings);
+ }
+ }
+ }
+
+ // plusieurs params d'un coup
+ function setSettings(newSettings) {
+ settings = { ...settings, ...newSettings };
+ applyVolumes();
+
+ if (typeof IceBreakerStorage !== 'undefined') {
+ IceBreakerStorage.saveSettings(settings);
+ }
+ }
+
+ // lire params
+ function getSettings() {
+ return { ...settings };
+ }
+
+ // volume principal
+ function getMasterVolume() {
+ return settings.masterVolume;
+ }
+
+ // modifier volume
+ function setMasterVolume(value) {
+ setSetting('masterVolume', Math.max(0, Math.min(1, value)));
+ }
+
+ // on/off musique
+ function toggleMusic() {
+ settings.musicEnabled = !settings.musicEnabled;
+ applyVolumes();
+
+ if (settings.musicEnabled && sounds.bgm) {
+ sounds.bgm.play().catch(() => { });
+ } else if (sounds.bgm) {
+ sounds.bgm.pause();
+ }
+
+ if (typeof IceBreakerStorage !== 'undefined') {
+ IceBreakerStorage.saveSettings(settings);
+ }
+
+ return settings.musicEnabled;
+ }
+
+ // on/off sfx
+ function toggleSfx() {
+ settings.sfxEnabled = !settings.sfxEnabled;
+ applyVolumes();
+
+ if (typeof IceBreakerStorage !== 'undefined') {
+ IceBreakerStorage.saveSettings(settings);
+ }
+
+ return settings.sfxEnabled;
+ }
+
+ return {
+ init,
+ preload,
+ playSfx,
+ playMusic,
+ stopMusic,
+ pauseMusic,
+ setSetting,
+ setSettings,
+ getSettings,
+ getMasterVolume,
+ setMasterVolume,
+ toggleMusic,
+ toggleSfx
+ };
+})();
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = AudioManager;
+}
diff --git a/src/assets/js/checkpoint.js b/src/assets/js/checkpoint.js
new file mode 100644
index 0000000..8729f9e
--- /dev/null
+++ b/src/assets/js/checkpoint.js
@@ -0,0 +1,174 @@
+/*
+ * Page checkpoint - calcul des scores et affichage
+ */
+
+(function () {
+ 'use strict';
+
+ // params URL
+ const params = new URLSearchParams(window.location.search);
+ const difficulty = parseFloat(params.get('difficulty')) || 1;
+ const previousScore = parseInt(params.get('score')) || 0;
+ const containersRemaining = parseInt(params.get('containers')) || 0;
+
+ // cargaison cumulée sur tous les niveaux
+ const totalDeliveredPrev = parseInt(params.get('totalDelivered')) || 0;
+ const totalPossiblePrev = parseInt(params.get('totalPossible')) || 0;
+ const checkpointsPrev = parseInt(params.get('checkpoints')) || 0;
+
+ // 20 containers par niveau
+ const containersThisLevel = 20;
+
+ // Mise à jour des totaux cumulatifs
+ const totalDelivered = totalDeliveredPrev + containersRemaining;
+ const totalPossible = totalPossiblePrev + containersThisLevel;
+ const checkpoints = checkpointsPrev + 1;
+
+ // Calcul des scores
+ let totalScore = 0;
+
+ document.addEventListener('DOMContentLoaded', init);
+
+ function init() {
+ calculateAndDisplayScores();
+ displayCaptainName();
+ initSnowEffect();
+ bindEvents();
+ renderContainersOnBoat();
+ initAudio();
+ }
+
+ function initAudio() {
+ // volume
+ let masterVolume = 0.8;
+ if (typeof IceBreakerStorage !== 'undefined') {
+ const settings = IceBreakerStorage.getSettings();
+ masterVolume = settings.masterVolume || 0.8;
+ }
+
+ // son victoire
+ const successSound = document.getElementById('checkpoint-success');
+ if (successSound) {
+ successSound.volume = masterVolume * 0.8;
+ successSound.play().catch(() => { });
+ }
+
+ // ambiance port (mouettes etc)
+ const portAmbiance = document.getElementById('port-ambiance');
+ if (portAmbiance) {
+ portAmbiance.volume = masterVolume * 0.4;
+ setTimeout(() => {
+ portAmbiance.play().catch(() => { });
+ }, 1500);
+ }
+ }
+
+ function calculateAndDisplayScores() {
+ // bonus niveau terminé
+ const levelBonus = Math.round(125 * difficulty);
+
+ // bonus containers restants
+ const containerBonus = Math.round(containersRemaining * difficulty * 30);
+
+ // Score total
+ totalScore = previousScore + levelBonus + containerBonus;
+
+ // Affichage
+ const scoreprec = document.getElementById('scoreprec');
+ const scoreniv = document.getElementById('scoreniv');
+ const contbonus = document.getElementById('contbonus');
+ const scoretot = document.getElementById('scoretot');
+
+ if (scoreprec) scoreprec.textContent = previousScore.toLocaleString();
+ if (scoreniv) scoreniv.textContent = `+${levelBonus.toLocaleString()}`;
+ if (contbonus) contbonus.textContent = `+${containerBonus.toLocaleString()}`;
+ if (scoretot) scoretot.textContent = totalScore.toLocaleString();
+
+ // anim score
+ if (scoretot) {
+ scoretot.classList.add('animate-score');
+ }
+ }
+
+ function displayCaptainName() {
+ const badge = document.getElementById('captain-badge');
+ if (badge && typeof IceBreakerStorage !== 'undefined') {
+ const name = IceBreakerStorage.getCaptainName();
+ if (name) {
+ badge.innerHTML = `⚓ Capitaine ${name}`;
+ }
+ }
+ }
+
+ function initSnowEffect() {
+ const container = document.getElementById('snow-container');
+ if (!container) return;
+
+ const snowflakeCount = 25;
+ const fragment = document.createDocumentFragment();
+
+ for (let i = 0; i < snowflakeCount; i++) {
+ const flake = document.createElement('div');
+ flake.className = 'snowflake';
+ flake.style.cssText = `
+ left: ${Math.random() * 100}%;
+ animation-delay: ${Math.random() * 10}s;
+ animation-duration: ${10 + Math.random() * 10}s;
+ opacity: ${0.2 + Math.random() * 0.5};
+ font-size: ${6 + Math.random() * 10}px;
+ `;
+ flake.textContent = '❄';
+ fragment.appendChild(flake);
+ }
+
+ container.appendChild(fragment);
+ }
+
+ function renderContainersOnBoat() {
+ const deck = document.getElementById('container-deck');
+ if (!deck) return;
+
+ deck.innerHTML = '';
+ const colors = ['#4fc3f7', '#ffd54f', '#ff8a80'];
+ let remaining = containersRemaining;
+
+ for (let r = 0; r < 5; r++) {
+ const row = document.createElement('div');
+ row.className = 'container-row';
+
+ for (let c = 0; c < 4; c++) {
+ const cont = document.createElement('div');
+ cont.className = 'container';
+
+ if (remaining > 0) {
+ cont.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
+ remaining--;
+ } else {
+ cont.style.opacity = '0';
+ }
+
+ row.appendChild(cont);
+ }
+ deck.appendChild(row);
+ }
+ }
+
+ function bindEvents() {
+ const continueBtn = document.getElementById('continue-btn');
+ const lobbyBtn = document.getElementById('lobby-btn');
+
+ continueBtn?.addEventListener('click', continueGame);
+ lobbyBtn?.addEventListener('click', returnToLobby);
+ }
+
+ function continueGame() {
+ const nextDifficulty = difficulty + 0.5;
+ // on passe tout au niveau suivant
+ window.location.href = `cruise.html?difficulty=${nextDifficulty}&score=${totalScore}&totalDelivered=${totalDelivered}&totalPossible=${totalPossible}&checkpoints=${checkpoints}`;
+ }
+
+ function returnToLobby() {
+ // retour accueil avec le score
+ window.location.href = `index.html?finalScore=${totalScore}&finalDifficulty=${difficulty}&totalDelivered=${totalDelivered}&totalPossible=${totalPossible}&checkpoints=${checkpoints}`;
+ }
+})();
diff --git a/src/assets/js/cruise.js b/src/assets/js/cruise.js
new file mode 100644
index 0000000..f9bfe64
--- /dev/null
+++ b/src/assets/js/cruise.js
@@ -0,0 +1,1010 @@
+/*
+ * Le jeu principal
+ */
+
+'use strict';
+
+const CONFIG = {
+ // gameplay
+ targetDistance: 300,
+ containerRows: 5,
+ containerCols: 4,
+ colors: ['#4fc3f7', '#ffd54f', '#ff8a80'],
+
+ // physique (pour 60fps)
+ acceleration: 0.6,
+ friction: 0.94,
+ maxSpeed: 10,
+ targetFPS: 60,
+ frameTime: 1000 / 60,
+
+ // difficulté
+ stabilityThreshold: 0.8,
+ proximityRange: 100,
+ collisionRange: 25,
+
+ // perf
+ baseMaxIcebergs: 6,
+ collisionCheckInterval: 3,
+};
+
+// dimensions fenêtre (cache pour éviter les reflows)
+let viewportWidth = window.innerWidth;
+let viewportHeight = window.innerHeight;
+window.addEventListener('resize', () => {
+ viewportWidth = window.innerWidth;
+ viewportHeight = window.innerHeight;
+}, { passive: true });
+
+class Boat {
+ constructor(elementId, sfxCallback) {
+ this.el = document.getElementById(elementId);
+ this.deck = document.getElementById('container-deck');
+ this.sfxCallback = sfxCallback;
+
+ // dimensions du bateau
+ this.width = 50;
+ this.height = 220;
+ this.cachedBounds = null;
+ this.boundsNeedUpdate = true;
+
+ // position et vitesse
+ this.x = window.innerWidth / 2 - 60;
+ this.y = 20;
+ this.vx = 0;
+ this.lastVx = 0;
+
+ // détection zigzag (pour pénaliser les changements brusques)
+ this.lastDirection = 0;
+ this.lastChangeTime = 0;
+ this.ZIGZAG_TIME_WINDOW = 800;
+
+ // Containers
+ this.containers = [];
+ this.activeContainerCount = CONFIG.containerRows * CONFIG.containerCols;
+ this.initDeck();
+
+ // touches
+ this.keys = { up: false, down: false, left: false, right: false };
+ this.bindControls();
+
+ // mesurer une fois au début
+ requestAnimationFrame(() => {
+ if (this.el) {
+ this.width = this.el.offsetWidth || 50;
+ this.height = this.el.offsetHeight || 220;
+ }
+ });
+ }
+
+ initDeck() {
+ this.deck.innerHTML = '';
+ const fragment = document.createDocumentFragment();
+
+ for (let r = 0; r < CONFIG.containerRows; r++) {
+ const row = document.createElement('div');
+ row.className = 'container-row';
+
+ for (let c = 0; c < CONFIG.containerCols; c++) {
+ const cont = document.createElement('div');
+ cont.className = 'container';
+ cont.style.backgroundColor = CONFIG.colors[Math.floor(Math.random() * CONFIG.colors.length)];
+ this.containers.push({ el: cont, active: true });
+ row.appendChild(cont);
+ }
+ fragment.appendChild(row);
+ }
+ this.deck.appendChild(fragment);
+ }
+
+ bindControls() {
+ const handleKey = (e, isPressed) => {
+ const k = e.key.toLowerCase();
+ if (['arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(k)) {
+ e.preventDefault();
+ }
+
+ if (k === 'z' || k === 'arrowup') this.keys.up = isPressed;
+ if (k === 's' || k === 'arrowdown') this.keys.down = isPressed;
+ if (k === 'q' || k === 'arrowleft') this.keys.left = isPressed;
+ if (k === 'd' || k === 'arrowright') this.keys.right = isPressed;
+ };
+
+ window.addEventListener('keydown', (e) => handleKey(e, true));
+ window.addEventListener('keyup', (e) => handleKey(e, false));
+ }
+
+ checkZigzagConflict() {
+ const now = performance.now(); // Plus précis que Date.now()
+ let currentDirection = 0;
+
+ if (this.keys.left && !this.keys.right) currentDirection = -1;
+ else if (this.keys.right && !this.keys.left) currentDirection = 1;
+
+ if (currentDirection !== 0 && currentDirection !== this.lastDirection && this.lastDirection !== 0) {
+ if (now - this.lastChangeTime < this.ZIGZAG_TIME_WINDOW) {
+ this.loseContainer('input_conflict');
+ this.vx *= 0.3;
+ this.lastDirection = 0;
+ this.lastChangeTime = 0;
+ return true;
+ }
+ }
+
+ if (currentDirection !== 0 && currentDirection !== this.lastDirection) {
+ this.lastDirection = currentDirection;
+ this.lastChangeTime = now;
+ }
+
+ return false;
+ }
+
+ updatePhysics(deltaTime = 1) {
+ if (this.checkZigzagConflict()) {
+ const shake = 5 * Math.sin(performance.now() / 50);
+ this.el.style.transform = `translateX(${this.x}px) rotate(${shake}deg)`;
+ this.el.style.bottom = `${this.y}px`;
+ this.boundsNeedUpdate = true;
+ return;
+ }
+
+ this.lastVx = this.vx;
+
+ // Accélération (normalisée par deltaTime)
+ const accel = CONFIG.acceleration * deltaTime;
+ if (this.keys.right) this.vx = Math.min(this.vx + accel, CONFIG.maxSpeed);
+ if (this.keys.left) this.vx = Math.max(this.vx - accel, -CONFIG.maxSpeed);
+
+ // Mouvement vertical (normalisé par deltaTime)
+ const vStep = 5 * deltaTime;
+ if (this.keys.up) this.y = Math.min(this.y + vStep, viewportHeight - 200);
+ if (this.keys.down) this.y = Math.max(this.y - vStep, 0);
+
+ // Friction (ajustée pour deltaTime)
+ const frictionFactor = Math.pow(CONFIG.friction, deltaTime);
+ this.vx *= frictionFactor;
+
+ // Position
+ this.x += this.vx * deltaTime;
+
+ // Limites
+ const maxX = viewportWidth - this.width;
+ if (this.x < 0) { this.x = 0; this.vx *= -0.5; }
+ if (this.x > maxX) { this.x = maxX; this.vx *= -0.5; }
+
+ // Rotation visuelle - utiliser transform3d pour GPU acceleration
+ const tilt = this.vx * 2;
+ this.el.style.transform = `translate3d(${this.x}px, 0, 0) rotate(${tilt}deg)`;
+ this.el.style.bottom = `${this.y}px`;
+
+ this.boundsNeedUpdate = true;
+ }
+
+ checkStability() {
+ const gForce = Math.abs(this.vx - this.lastVx);
+
+ if (gForce > CONFIG.stabilityThreshold) {
+ const dropChance = (gForce - CONFIG.stabilityThreshold) * 0.15;
+ if (Math.random() < dropChance) {
+ const direction = (this.vx - this.lastVx) > 0 ? 'left' : 'right';
+ this.loseContainer(direction);
+ }
+ }
+ }
+
+ loseContainer(reason) {
+ if (this.activeContainerCount === 0) return false;
+
+ // Trouver le dernier container actif sans filter()
+ let target = null;
+ for (let i = this.containers.length - 1; i >= 0; i--) {
+ if (this.containers[i].active) {
+ target = this.containers[i];
+ break;
+ }
+ }
+ if (!target) return false;
+
+ target.active = false;
+ this.activeContainerCount--;
+
+ let dropClass;
+ if (reason === 'input_conflict') {
+ dropClass = Math.random() > 0.5 ? 'dropped-left' : 'dropped-right';
+ } else {
+ dropClass = reason === 'left' ? 'dropped-left' : 'dropped-right';
+ }
+
+ target.el.classList.add(dropClass);
+
+ if (this.sfxCallback) {
+ this.sfxCallback('drop', 0.9);
+ }
+
+ return true;
+ }
+
+ getBounds() {
+ // Utiliser le cache si disponible et valide
+ if (!this.boundsNeedUpdate && this.cachedBounds) {
+ return this.cachedBounds;
+ }
+
+ // Calculer les bounds basés sur la position connue
+ // On utilise les dimensions ORIGINALES du bateau (sans rotation)
+ // pour que la hitbox reste cohérente pendant les virages
+ const top = viewportHeight - this.y - this.height;
+
+ this.cachedBounds = {
+ left: this.x,
+ top: top,
+ right: this.x + this.width,
+ bottom: top + this.height,
+ width: this.width,
+ height: this.height,
+ centerX: this.x + this.width / 2,
+ centerY: top + this.height / 2
+ };
+
+ this.boundsNeedUpdate = false;
+ return this.cachedBounds;
+ }
+
+ getContainerCount() {
+ return this.activeContainerCount;
+ }
+}
+
+// images d'icebergs
+const ICEBERG_IMAGE_COUNT = 12;
+
+// précharger pour éviter le lag
+const ICEBERG_IMAGES = [];
+let icebergImagesLoaded = false;
+
+function preloadIcebergImages() {
+ let loaded = 0;
+ for (let i = 1; i <= ICEBERG_IMAGE_COUNT; i++) {
+ const img = new Image();
+ img.src = `./assets/img/icebergs/iceberg-${i}.png`;
+ img.onload = () => {
+ loaded++;
+ if (loaded === ICEBERG_IMAGE_COUNT) {
+ icebergImagesLoaded = true;
+ console.log('images icebergs ok');
+ }
+ };
+ ICEBERG_IMAGES.push(img);
+ }
+}
+
+// Lancer le préchargement
+preloadIcebergImages();
+
+class IcebergGenerator {
+ constructor(containerId, difficulty = 1) {
+ this.container = document.getElementById(containerId);
+ this.list = [];
+ this.activeCount = 0;
+ this.pooledElements = [];
+ // max icebergs à l'écran (6 à 15)
+ this.maxIcebergs = Math.min(Math.floor(CONFIG.baseMaxIcebergs + difficulty * 2), 15);
+ }
+
+ // Récupérer ou créer un élément image
+ getPooledElement(size, imageIndex) {
+ // Chercher un élément dans le pool
+ const pooled = this.pooledElements.pop();
+ if (pooled) {
+ pooled.style.width = `${size}px`;
+ pooled.style.height = `${size}px`;
+ pooled.style.display = 'block';
+ pooled.src = ICEBERG_IMAGES[imageIndex % ICEBERG_IMAGE_COUNT].src;
+ return pooled;
+ }
+
+ // Créer un nouvel élément image
+ const el = document.createElement('img');
+ el.className = 'iceberg';
+ el.src = ICEBERG_IMAGES[imageIndex % ICEBERG_IMAGE_COUNT].src;
+ el.alt = 'Iceberg';
+ el.draggable = false;
+ return el;
+ }
+
+ // Recycler un élément au lieu de le supprimer
+ recycleElement(el) {
+ el.style.display = 'none';
+ if (this.pooledElements.length < 20) { // Limite du pool
+ this.pooledElements.push(el);
+ } else {
+ el.remove();
+ }
+ }
+
+ spawn(difficulty) {
+ // pas trop d'icebergs en même temps
+ if (this.activeCount >= this.maxIcebergs) return;
+
+ // attendre que les images soient là
+ if (!icebergImagesLoaded) return;
+
+ // taille selon difficulté
+ const baseSize = 70 + difficulty * 10;
+ const sizeVariation = 100 + difficulty * 20;
+ const size = Math.min(baseSize + Math.random() * sizeVariation, 300);
+
+ // trouver une position libre
+ const posX = this.findSafePosition(size);
+ if (posX === null) return;
+
+ // image aléatoire
+ const imageIndex = Math.floor(Math.random() * ICEBERG_IMAGE_COUNT);
+
+ const el = this.getPooledElement(size, imageIndex);
+ el.style.width = `${size}px`;
+ el.style.height = `${size}px`;
+ el.style.left = `${posX}px`;
+ el.style.top = '-150px';
+
+ if (!el.parentNode) {
+ this.container.appendChild(el);
+ }
+
+ this.activeCount++;
+ this.list.push({
+ el,
+ x: posX,
+ y: -150,
+ size,
+ speed: (2 + Math.random() * 2) * difficulty,
+ active: true,
+ inProximity: false
+ });
+ }
+
+ findSafePosition(newSize) {
+ const margin = 30;
+ const maxAttempts = 5;
+
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
+ const posX = Math.random() * (viewportWidth - newSize);
+ let isSafe = true;
+
+ // check collision avec les autres icebergs en haut
+ for (const ice of this.list) {
+ if (!ice.active) continue;
+
+ // Ne vérifier que les icebergs qui sont encore en haut de l'écran
+ if (ice.y > 200) continue;
+
+ // check horizontal
+ const iceLeft = ice.x;
+ const iceRight = ice.x + ice.size;
+ const newLeft = posX;
+ const newRight = posX + newSize;
+
+ // overlap avec marge?
+ if (!(newRight + margin < iceLeft || newLeft - margin > iceRight)) {
+ isSafe = false;
+ break;
+ }
+ }
+
+ if (isSafe) return posX;
+ }
+
+ return null; // Aucune position sûre trouvée
+ }
+
+ update(deltaTime = 1) {
+ let needsCleanup = false;
+ const list = this.list;
+ const len = list.length;
+
+ // Une seule boucle optimisée
+ for (let i = 0; i < len; i++) {
+ const ice = list[i];
+ if (!ice.active) continue;
+
+ // Mouvement normalisé par deltaTime
+ ice.y += ice.speed * deltaTime;
+
+ if (ice.y > viewportHeight) {
+ ice.active = false;
+ ice.el.style.display = 'none';
+ this.activeCount--;
+ needsCleanup = true;
+ } else {
+ // Mise à jour visuelle directe
+ ice.el.style.transform = `translate3d(0, ${ice.y + 150}px, 0)`;
+ }
+ }
+
+ // nettoyage si besoin
+ if (needsCleanup) {
+ // Recyclage des éléments inactifs
+ for (let i = 0; i < len; i++) {
+ const ice = list[i];
+ if (!ice.active && ice.el.style.display === 'none') {
+ if (this.pooledElements.length < 15) {
+ this.pooledElements.push(ice.el);
+ }
+ }
+ }
+ this.list = list.filter(i => i.active);
+ }
+ }
+
+ checkIcebergOverlap(ice1, ice2, margin = 0) {
+ // Collision rectangulaire avec marge
+ const left1 = ice1.x - margin;
+ const right1 = ice1.x + ice1.size + margin;
+ const top1 = ice1.y - margin;
+ const bottom1 = ice1.y + ice1.size + margin;
+
+ const left2 = ice2.x;
+ const right2 = ice2.x + ice2.size;
+ const top2 = ice2.y;
+ const bottom2 = ice2.y + ice2.size;
+
+ return !(right1 < left2 || left1 > right2 || bottom1 < top2 || top1 > bottom2);
+ }
+}
+
+class Game {
+ constructor() {
+ const params = new URLSearchParams(window.location.search);
+ this.difficulty = parseFloat(params.get('difficulty')) || 1;
+ this.score = parseInt(params.get('score')) || 0;
+ this.distance = CONFIG.targetDistance;
+ this.isRunning = false;
+ this.debugMode = false; // touche H pour activer
+
+ // suivi cargaison sur tous les niveaux
+ this.totalDelivered = parseInt(params.get('totalDelivered')) || 0;
+ this.totalPossible = parseInt(params.get('totalPossible')) || 0;
+ this.checkpoints = parseInt(params.get('checkpoints')) || 0;
+ this.initialContainers = CONFIG.containerRows * CONFIG.containerCols; // 20
+
+ // toggle debug
+ this.bindDebugControls();
+
+ // sons
+ this.sfx = {
+ drop: document.getElementById('sfx-drop'),
+ alert: document.getElementById('sfx-alert'),
+ break: document.getElementById('sfx-break'),
+ bgm: document.getElementById('background-audio')
+ };
+
+ // Volume settings
+ this.masterVolume = 0.8;
+ this.loadVolumeSettings();
+
+ // le bateau
+ this.boat = new Boat('player-boat', this.playSound.bind(this));
+ this.icebergs = new IcebergGenerator('iceberg-layer', this.difficulty);
+
+ this.lastSpawn = 0;
+ this.spawnRate = 1000 / this.difficulty;
+ this.damageCooldown = 0;
+
+ // deltaTime
+ this.lastFrameTime = 0;
+ this.frameCount = 0;
+ this.accumulatedTime = 0;
+
+ // UI
+ this.ui = {
+ dist: document.getElementById('dist-display'),
+ score: document.getElementById('score-display'),
+ cont: document.getElementById('cont-display'),
+ captainName: document.getElementById('captain-name-display')
+ };
+
+ // limiter les updates UI
+ this.lastUIUpdate = 0;
+ this.uiUpdateInterval = 200;
+
+ this.distanceInterval = null;
+ this.loop = this.loop.bind(this);
+
+ // nom capitaine
+ this.displayCaptainName();
+
+ // audio en jeu
+ this.initGameAudioControls();
+ }
+
+ loadVolumeSettings() {
+ if (typeof IceBreakerStorage !== 'undefined') {
+ const settings = IceBreakerStorage.getSettings();
+ this.masterVolume = settings.masterVolume || 0.8;
+ }
+ }
+
+ displayCaptainName() {
+ if (this.ui.captainName && typeof IceBreakerStorage !== 'undefined') {
+ const name = IceBreakerStorage.getCaptainName();
+ if (name) {
+ this.ui.captainName.textContent = `Cap. ${name}`;
+ }
+ }
+ }
+
+ initGameAudioControls() {
+ const toggleBtn = document.getElementById('audio-toggle-game');
+ const volumeSlider = document.getElementById('volume-slider-game');
+
+ if (volumeSlider) {
+ volumeSlider.value = Math.round(this.masterVolume * 100);
+
+ volumeSlider.addEventListener('input', (e) => {
+ this.masterVolume = parseInt(e.target.value) / 100;
+ this.applyVolume();
+
+ if (typeof IceBreakerStorage !== 'undefined') {
+ IceBreakerStorage.saveSettings({ masterVolume: this.masterVolume });
+ }
+ });
+ }
+
+ if (toggleBtn) {
+ toggleBtn.addEventListener('click', () => {
+ if (this.sfx.bgm) {
+ if (this.sfx.bgm.paused) {
+ this.sfx.bgm.play();
+ toggleBtn.querySelector('.audio-icon').textContent = '🔊';
+ } else {
+ this.sfx.bgm.pause();
+ toggleBtn.querySelector('.audio-icon').textContent = '🔇';
+ }
+ }
+ });
+ }
+ }
+
+ applyVolume() {
+ if (this.sfx.bgm) {
+ this.sfx.bgm.volume = this.masterVolume * 0.9;
+ }
+ }
+
+ preloadSfx() {
+ ['drop', 'alert', 'break'].forEach(name => {
+ const audio = this.sfx[name];
+ if (audio) {
+ audio.volume = 0;
+ audio.play().then(() => {
+ audio.pause();
+ audio.currentTime = 0;
+ }).catch(() => { });
+ }
+ });
+ }
+
+ playSound(sfxName, volume = 1.0) {
+ const audio = this.sfx[sfxName];
+ if (audio && this.isRunning) {
+ audio.currentTime = 0;
+ audio.volume = this.masterVolume * volume;
+ audio.play().catch(() => { });
+ }
+ }
+
+ startGame() {
+ if (this.isRunning) return;
+
+ this.isRunning = true;
+ this.preloadSfx();
+ this.applyVolume();
+ this.startGameLoop();
+ this.startDistanceInterval();
+ }
+
+ startGameLoop() {
+ requestAnimationFrame(this.loop);
+ }
+
+ startDistanceInterval() {
+ // toutes les 200ms
+ this.distanceInterval = setInterval(() => {
+ if (this.isRunning && this.distance > 0) {
+ this.distance--;
+ this.score += Math.ceil(this.difficulty);
+ // UI update géré dans la boucle principale
+
+ if (this.distance <= 0) this.win();
+ }
+ }, 200);
+ }
+
+ loop(timestamp) {
+ if (!this.isRunning) return;
+
+ // deltaTime (normalisé 60fps)
+ if (this.lastFrameTime === 0) {
+ this.lastFrameTime = timestamp;
+ requestAnimationFrame(this.loop);
+ return;
+ }
+
+ const rawDelta = timestamp - this.lastFrameTime;
+ this.lastFrameTime = timestamp;
+
+ // limiter pour éviter les sauts quand on change d'onglet
+ const clampedDelta = Math.min(rawDelta, 33);
+ const deltaTime = clampedDelta / CONFIG.frameTime;
+
+ this.frameCount++;
+
+ // physique
+ this.boat.updatePhysics(deltaTime);
+
+ // stabilité (pas à chaque frame)
+ if (this.frameCount % 4 === 0) {
+ this.boat.checkStability();
+ }
+
+ // spawn icebergs
+ if (timestamp - this.lastSpawn > this.spawnRate) {
+ this.icebergs.spawn(this.difficulty);
+ this.lastSpawn = timestamp;
+ this.spawnRate = (1000 + Math.random() * 500) / this.difficulty;
+ }
+
+ // update icebergs
+ this.icebergs.update(deltaTime);
+
+ // collisions (toutes les 3 frames)
+ if (this.frameCount % 3 === 0) {
+ this.checkCollisions();
+ }
+
+ // UI (pas trop souvent)
+ if (timestamp - this.lastUIUpdate > this.uiUpdateInterval) {
+ this.updateUI();
+ this.lastUIUpdate = timestamp;
+ }
+
+ // debug hitboxes (coûte cher)
+ if (this.debugMode && this.frameCount % 6 === 0) {
+ this.updateDebugVisuals();
+ }
+
+ requestAnimationFrame(this.loop);
+ }
+
+ // --- Debug (touche H) ---
+ bindDebugControls() {
+ window.addEventListener('keydown', (e) => {
+ if (e.key === 'h' || e.key === 'H') {
+ this.debugMode = !this.debugMode;
+ console.log('Debug mode:', this.debugMode ? 'ON' : 'OFF');
+
+ if (!this.debugMode) {
+ // Supprimer toutes les hitboxes
+ document.querySelectorAll('.debug-hitbox').forEach(el => el.remove());
+ }
+ }
+ });
+ }
+
+ updateDebugVisuals() {
+ // virer les anciens
+ document.querySelectorAll('.debug-hitbox').forEach(el => el.remove());
+
+ // hitbox bateau
+ this.drawBoatHitbox();
+
+ // hitbox icebergs
+ for (const ice of this.icebergs.list) {
+ if (ice.active) {
+ this.drawIcebergHitbox(ice);
+ }
+ }
+ }
+
+ drawBoatHitbox() {
+ const boatRect = this.boat.getBounds();
+ const turbulenceMargin = 30;
+ const tilt = this.boat.vx * 2; // Même rotation que le bateau visuel
+
+ // Zone de collision (rectangle = taille du bateau)
+ const collisionHitbox = document.createElement('div');
+ collisionHitbox.className = 'debug-hitbox';
+ collisionHitbox.style.cssText = `
+ position: fixed;
+ left: ${boatRect.left}px;
+ top: ${boatRect.top}px;
+ width: ${boatRect.width}px;
+ height: ${boatRect.height}px;
+ border: 3px solid #00ff00;
+ background: rgba(0, 255, 0, 0.15);
+ pointer-events: none;
+ z-index: 9999;
+ box-sizing: border-box;
+ transform: rotate(${tilt}deg);
+ transform-origin: center bottom;
+ `;
+ document.body.appendChild(collisionHitbox);
+
+ // Zone de turbulence (rectangle 30px plus large)
+ const turbulenceHitbox = document.createElement('div');
+ turbulenceHitbox.className = 'debug-hitbox';
+ turbulenceHitbox.style.cssText = `
+ position: fixed;
+ left: ${boatRect.left - turbulenceMargin}px;
+ top: ${boatRect.top - turbulenceMargin}px;
+ width: ${boatRect.width + turbulenceMargin * 2}px;
+ height: ${boatRect.height + turbulenceMargin * 2}px;
+ border: 2px dashed #ffff00;
+ background: rgba(255, 255, 0, 0.05);
+ pointer-events: none;
+ z-index: 9998;
+ box-sizing: border-box;
+ transform: rotate(${tilt}deg);
+ transform-origin: center bottom;
+ `;
+ document.body.appendChild(turbulenceHitbox);
+
+ // Label
+ const label = document.createElement('div');
+ label.className = 'debug-hitbox';
+ label.style.cssText = `
+ position: fixed;
+ left: ${boatRect.right + 10}px;
+ top: ${boatRect.top}px;
+ color: #00ff00;
+ font-size: 12px;
+ font-family: monospace;
+ background: rgba(0,0,0,0.7);
+ padding: 2px 6px;
+ pointer-events: none;
+ z-index: 10000;
+ `;
+ label.textContent = `BOAT ${Math.round(boatRect.width)}x${Math.round(boatRect.height)}`;
+ document.body.appendChild(label);
+ }
+
+ drawIcebergHitbox(ice) {
+ const centerX = ice.x + ice.size / 2;
+ const centerY = ice.y + ice.size / 2;
+ const radius = ice.size / 2;
+
+ // Hitbox de l'iceberg (cercle)
+ const hitbox = document.createElement('div');
+ hitbox.className = 'debug-hitbox';
+ hitbox.style.cssText = `
+ position: fixed;
+ left: ${centerX - radius}px;
+ top: ${centerY - radius}px;
+ width: ${radius * 2}px;
+ height: ${radius * 2}px;
+ border: 2px solid ${ice.inProximity ? '#ff0000' : '#ff6600'};
+ border-radius: 50%;
+ background: rgba(255, ${ice.inProximity ? '0' : '102'}, 0, 0.15);
+ pointer-events: none;
+ z-index: 9997;
+ box-sizing: border-box;
+ `;
+ document.body.appendChild(hitbox);
+ }
+
+ checkCollisions() {
+ const boatRect = this.boat.getBounds();
+ const turbulenceMargin = 30;
+ const now = performance.now();
+ const icebergList = this.icebergs.list;
+ const listLength = icebergList.length;
+
+ if (listLength === 0) return;
+
+ // valeurs bateau (une fois)
+ const boatLeft = boatRect.left;
+ const boatTop = boatRect.top;
+ const boatRight = boatRect.right;
+ const boatBottom = boatRect.bottom;
+ const boatCenterX = boatRect.centerX;
+
+ // zone turbulence
+ const turbLeft = boatLeft - turbulenceMargin;
+ const turbTop = boatTop - turbulenceMargin;
+ const turbRight = boatRight + turbulenceMargin;
+ const turbBottom = boatBottom + turbulenceMargin;
+
+ for (let i = 0; i < listLength; i++) {
+ const ice = icebergList[i];
+ if (!ice.active) continue;
+
+ // Early exit: ignorer les icebergs trop loin verticalement
+ const iceBottom = ice.y + ice.size;
+ if (iceBottom < turbTop - 50 || ice.y > turbBottom + 50) continue;
+
+ // Pré-calcul des valeurs iceberg
+ const halfSize = ice.size * 0.5;
+ const iceCenterX = ice.x + halfSize;
+ const iceCenterY = ice.y + halfSize;
+
+ // Early exit: ignorer les icebergs trop loin horizontalement
+ if (iceCenterX + halfSize < turbLeft - 20 || iceCenterX - halfSize > turbRight + 20) continue;
+
+ // Collision avec le bateau (test simplifié AABB d'abord)
+ if (iceCenterX + halfSize > boatLeft && iceCenterX - halfSize < boatRight &&
+ iceCenterY + halfSize > boatTop && iceCenterY - halfSize < boatBottom) {
+ this.playSound('break', 1.0);
+ this.gameOver();
+ return;
+ }
+
+ // Zone de turbulence (test simplifié AABB)
+ if (iceCenterX + halfSize > turbLeft && iceCenterX - halfSize < turbRight &&
+ iceCenterY + halfSize > turbTop && iceCenterY - halfSize < turbBottom) {
+
+ if (!ice.inProximity) {
+ this.playSound('alert', 0.4);
+ ice.inProximity = true;
+ }
+
+ if (now - this.damageCooldown > 500) {
+ const dropDirection = (iceCenterX < boatCenterX) ? 'right' : 'left';
+ if (this.boat.loseContainer(dropDirection)) {
+ this.damageCooldown = now;
+ }
+ }
+ } else {
+ ice.inProximity = false;
+ }
+ }
+ }
+
+ // Collision entre un rectangle et un cercle
+ checkRectCircleCollision(rectX, rectY, rectW, rectH, circleX, circleY, circleR) {
+ // Trouver le point le plus proche du cercle sur le rectangle
+ const closestX = Math.max(rectX, Math.min(circleX, rectX + rectW));
+ const closestY = Math.max(rectY, Math.min(circleY, rectY + rectH));
+
+ // Calculer la distance entre ce point et le centre du cercle
+ const distX = circleX - closestX;
+ const distY = circleY - closestY;
+ const distanceSquared = distX * distX + distY * distY;
+
+ return distanceSquared < (circleR * circleR);
+ }
+
+ updateUI() {
+ if (this.ui.dist) this.ui.dist.textContent = `${this.distance} km`;
+ if (this.ui.score) this.ui.score.textContent = this.score.toLocaleString();
+ if (this.ui.cont) this.ui.cont.textContent = this.boat.getContainerCount();
+ }
+
+ gameOver() {
+ this.isRunning = false;
+ if (this.distanceInterval) clearInterval(this.distanceInterval);
+
+ // Calculer les totaux pour ce game over (0 containers livrés ce niveau)
+ const containersThisLevel = 0;
+ const totalDelivered = this.totalDelivered + containersThisLevel;
+ const totalPossible = this.totalPossible + this.initialContainers;
+ const distanceTraveled = CONFIG.targetDistance - this.distance;
+
+ setTimeout(() => {
+ this.showGameOverModal(this.score, totalDelivered, totalPossible, distanceTraveled);
+ }, 300);
+ }
+
+ showGameOverModal(score, totalDelivered, totalPossible, distance) {
+ const modal = document.getElementById('gameover-modal');
+ const deliveryRate = totalPossible > 0 ? Math.round((totalDelivered / totalPossible) * 100) : 0;
+
+ if (!modal) {
+ // Fallback si la modal n'existe pas
+ alert("💥 GAME OVER!\n\nScore final: " + score.toLocaleString());
+ window.location.href = `index.html?finalScore=${score}&finalDifficulty=${this.difficulty}&totalDelivered=${totalDelivered}&totalPossible=${totalPossible}&checkpoints=${this.checkpoints}`;
+ return;
+ }
+
+ // Mettre à jour les statistiques
+ const scoreEl = document.getElementById('gameover-score');
+ const containersEl = document.getElementById('gameover-containers');
+ const distanceEl = document.getElementById('gameover-distance');
+
+ if (scoreEl) scoreEl.textContent = score.toLocaleString();
+ if (containersEl) containersEl.textContent = `${deliveryRate}% livré`;
+ if (distanceEl) distanceEl.textContent = `${distance} km`;
+
+ // Afficher la modal
+ modal.classList.remove('hidden');
+
+ // Gérer le bouton retour
+ const homeBtn = document.getElementById('gameover-home-btn');
+ if (homeBtn) {
+ homeBtn.onclick = () => {
+ window.location.href = `index.html?finalScore=${score}&finalDifficulty=${this.difficulty}&totalDelivered=${totalDelivered}&totalPossible=${totalPossible}&checkpoints=${this.checkpoints}`;
+ };
+ }
+ }
+
+ win() {
+ this.isRunning = false;
+ if (this.distanceInterval) clearInterval(this.distanceInterval);
+
+ const containers = this.boat.getContainerCount();
+ // Passer les données cumulatives au checkpoint
+ window.location.href = `checkpoint.html?score=${this.score}&difficulty=${this.difficulty}&containers=${containers}&totalDelivered=${this.totalDelivered}&totalPossible=${this.totalPossible}&checkpoints=${this.checkpoints}`;
+ }
+}
+
+// --- Démarrage ---
+function setupAudioPrompt(gameInstance) {
+ const modal = document.getElementById('audio-prompt-modal');
+ const allowBtn = document.getElementById('allow-audio-button');
+ const skipBtn = document.getElementById('skip-audio-button');
+
+ if (!modal) {
+ gameInstance.startGame();
+ return;
+ }
+
+ // vérifier si déjà choisi
+ const settings = typeof IceBreakerStorage !== 'undefined'
+ ? IceBreakerStorage.getSettings()
+ : null;
+
+ // si déjà choisi, on applique direct
+ if (settings && typeof settings.audioPromptAnswered !== 'undefined') {
+ modal.classList.add('hidden');
+
+ if (settings.musicEnabled) {
+ const bgm = gameInstance.sfx.bgm;
+ if (bgm) {
+ bgm.volume = gameInstance.masterVolume * 0.6;
+ bgm.play().catch(() => { });
+ }
+ }
+
+ gameInstance.startGame();
+ return;
+ }
+
+ const startWithAudio = () => {
+ // sauver le choix
+ if (typeof IceBreakerStorage !== 'undefined') {
+ IceBreakerStorage.saveSettings({
+ audioPromptAnswered: true,
+ musicEnabled: true
+ });
+ }
+
+ const bgm = gameInstance.sfx.bgm;
+ if (bgm) {
+ bgm.volume = gameInstance.masterVolume * 0.6;
+ bgm.play().catch(() => { });
+ }
+ modal.classList.add('hidden');
+ gameInstance.startGame();
+ };
+
+ const startWithoutAudio = () => {
+ // sauver le choix
+ if (typeof IceBreakerStorage !== 'undefined') {
+ IceBreakerStorage.saveSettings({
+ audioPromptAnswered: true,
+ musicEnabled: false
+ });
+ }
+
+ modal.classList.add('hidden');
+ gameInstance.startGame();
+ };
+
+ allowBtn?.addEventListener('click', startWithAudio);
+ skipBtn?.addEventListener('click', startWithoutAudio);
+}
+
+// Démarrage
+window.addEventListener('DOMContentLoaded', () => {
+ const game = new Game();
+ setupAudioPrompt(game);
+});
\ No newline at end of file
diff --git a/src/assets/js/index.js b/src/assets/js/index.js
new file mode 100644
index 0000000..59b7bd1
--- /dev/null
+++ b/src/assets/js/index.js
@@ -0,0 +1,469 @@
+/*
+ * Page d'accueil - gestion capitaine + leaderboard
+ */
+
+(function () {
+ 'use strict';
+
+ const MAX_SCORES_DISPLAY = 5;
+
+ // refs DOM (on les cache au début)
+ const DOM = {
+ captainSection: null,
+ captainInput: null,
+ saveCaptainBtn: null,
+ difficultySection: null,
+ captainGreeting: null,
+ subTitle: null,
+ resultsTableBody: null,
+ audioToggle: null,
+ audioSliderContainer: null,
+ masterVolumeSlider: null,
+ volumeValue: null,
+ snowContainer: null
+ };
+
+ // textes selon si le joueur est nouveau ou pas
+ const TEXTS = {
+ new: {
+ subtitle: "Traversez les eaux glacées arctiques et livrez votre précieuse cargaison.",
+ greeting: "Prêt pour votre première traversée, Capitaine ?",
+ noScores: "Votre journal de bord est vide. Partez en mer pour écrire votre légende !"
+ },
+ returning: {
+ subtitle: "Content de vous revoir ! Prêt à battre vos records ?",
+ greetings: [
+ "De retour aux commandes, Capitaine {name} ! La mer vous attend.",
+ "Les icebergs n'attendent que vous, Capitaine {name}.",
+ "Capitaine {name}, votre équipage est prêt à lever l'ancre !",
+ "Ahoy Capitaine {name} ! Une nouvelle tempête se profile.",
+ "Bienvenue à bord, Capitaine {name}. Le cargo est chargé."
+ ],
+ noScores: "Hmm, votre journal semble vide. Reprenez la mer !"
+ },
+ veteran: {
+ subtitle: "Légende des mers glacées ! Votre réputation vous précède.",
+ greetings: [
+ "La légende revient ! Capitaine {name}, montrez-leur de quel bois vous êtes fait.",
+ "Capitaine {name}, vos exploits sont chantés dans tous les ports arctiques !",
+ "Les icebergs tremblent à votre approche, Capitaine {name} !"
+ ]
+ }
+ };
+
+ // --- Init ---
+ document.addEventListener('DOMContentLoaded', init);
+
+ function init() {
+ try {
+ // Vérifier que le storage est disponible
+ if (typeof IceBreakerStorage === 'undefined') {
+ console.error('[IceBreaker] IceBreakerStorage not loaded!');
+ return;
+ }
+
+ cacheDOMElements();
+ initSnowEffect();
+ initAudioControls();
+
+ // Vérifier et sauvegarder un nouveau score depuis l'URL
+ checkAndSaveNewScore();
+
+ // Configurer l'interface selon le statut du joueur
+ setupPlayerExperience();
+
+ // Charger le leaderboard
+ loadLeaderboard();
+
+ // Attacher les événements
+ bindEvents();
+
+ console.log('[IceBreaker] Init complete');
+ } catch (e) {
+ console.error('[IceBreaker] Init error:', e);
+ // Assurer que le leaderboard est mis à jour même en cas d'erreur partielle
+ if (DOM.resultsTableBody) {
+ DOM.resultsTableBody.innerHTML = `
| Erreur de chargement. Rafraîchissez la page. |
`;
+ }
+ }
+ }
+
+ function cacheDOMElements() {
+ DOM.captainSection = document.getElementById('captain-section');
+ DOM.captainInput = document.getElementById('captain-name-input');
+ DOM.saveCaptainBtn = document.getElementById('save-captain-btn');
+ DOM.difficultySection = document.getElementById('difficulty-selection');
+ DOM.captainGreeting = document.getElementById('captain-greeting');
+ DOM.subTitle = document.getElementById('sub-title');
+ DOM.resultsTableBody = document.getElementById('results-table-body');
+ DOM.audioToggle = document.getElementById('audio-toggle');
+ DOM.audioSliderContainer = document.getElementById('audio-slider-container');
+ DOM.masterVolumeSlider = document.getElementById('master-volume');
+ DOM.volumeValue = document.getElementById('volume-value');
+ DOM.snowContainer = document.getElementById('snow-container');
+ }
+
+ // flocons de neige
+ function initSnowEffect() {
+ if (!DOM.snowContainer) return;
+
+ const snowflakeCount = 50;
+ const fragment = document.createDocumentFragment();
+
+ for (let i = 0; i < snowflakeCount; i++) {
+ const flake = document.createElement('div');
+ flake.className = 'snowflake';
+ flake.style.cssText = `
+ left: ${Math.random() * 100}%;
+ animation-delay: ${Math.random() * 10}s;
+ animation-duration: ${8 + Math.random() * 7}s;
+ opacity: ${0.3 + Math.random() * 0.7};
+ font-size: ${8 + Math.random() * 12}px;
+ `;
+ flake.textContent = '❄';
+ fragment.appendChild(flake);
+ }
+
+ DOM.snowContainer.appendChild(fragment);
+ }
+
+ // --- Audio ---
+ function initAudioControls() {
+ if (!DOM.audioToggle) return;
+
+ // Initialiser le gestionnaire audio
+ if (typeof AudioManager !== 'undefined') {
+ AudioManager.init();
+ }
+
+ // Charger le volume sauvegardé
+ const settings = typeof IceBreakerStorage !== 'undefined'
+ ? IceBreakerStorage.getSettings()
+ : { masterVolume: 0.8 };
+
+ // S'assurer que masterVolume est un nombre valide
+ const masterVolume = typeof settings.masterVolume === 'number' && isFinite(settings.masterVolume)
+ ? settings.masterVolume
+ : 0.8;
+
+ if (DOM.masterVolumeSlider) {
+ DOM.masterVolumeSlider.value = Math.round(masterVolume * 100);
+ updateVolumeDisplay(masterVolume * 100);
+ }
+
+ // Initialiser la musique d'ambiance
+ initBackgroundMusic(masterVolume);
+
+ // Toggle du panneau audio
+ DOM.audioToggle.addEventListener('click', () => {
+ DOM.audioSliderContainer.classList.toggle('hidden');
+ });
+
+ // Changement de volume
+ if (DOM.masterVolumeSlider) {
+ DOM.masterVolumeSlider.addEventListener('input', (e) => {
+ const value = parseInt(e.target.value);
+ updateVolumeDisplay(value);
+
+ if (typeof AudioManager !== 'undefined') {
+ AudioManager.setMasterVolume(value / 100);
+ }
+ });
+ }
+
+ // Fermer le panneau en cliquant ailleurs
+ document.addEventListener('click', (e) => {
+ if (!e.target.closest('#audio-controls')) {
+ DOM.audioSliderContainer?.classList.add('hidden');
+ }
+ });
+ }
+
+ function initBackgroundMusic(masterVolume) {
+ const bgm = document.getElementById('background-audio');
+ if (!bgm) return;
+
+ // S'assurer que le volume est un nombre fini entre 0 et 1
+ const safeVolume = typeof masterVolume === 'number' && isFinite(masterVolume)
+ ? Math.max(0, Math.min(1, masterVolume))
+ : 0.8;
+
+ bgm.volume = safeVolume * 0.5;
+
+ // Démarrer la musique au premier clic utilisateur (politique autoplay)
+ const startMusic = () => {
+ bgm.play().catch(() => { });
+ document.removeEventListener('click', startMusic);
+ document.removeEventListener('keydown', startMusic);
+ };
+
+ document.addEventListener('click', startMusic);
+ document.addEventListener('keydown', startMusic);
+
+ // Mettre à jour le volume quand le slider change
+ if (DOM.masterVolumeSlider) {
+ DOM.masterVolumeSlider.addEventListener('input', (e) => {
+ bgm.volume = (parseInt(e.target.value) / 100) * 0.5;
+ });
+ }
+ }
+
+ function updateVolumeDisplay(value) {
+ if (DOM.volumeValue) {
+ DOM.volumeValue.textContent = `${Math.round(value)}%`;
+ }
+
+ // Mettre à jour l'icône
+ const icon = DOM.audioToggle?.querySelector('.audio-icon');
+ if (icon) {
+ if (value === 0) {
+ icon.textContent = '🔇';
+ } else if (value < 50) {
+ icon.textContent = '🔉';
+ } else {
+ icon.textContent = '🔊';
+ }
+ }
+ }
+
+ // config interface selon le joueur
+ function setupPlayerExperience() {
+ const captainName = IceBreakerStorage.getCaptainName();
+ const playCount = IceBreakerStorage.getPlayCount();
+ const isReturning = IceBreakerStorage.isReturningPlayer();
+
+ if (!captainName) {
+ // Nouveau joueur : afficher le formulaire
+ showCaptainForm();
+ } else {
+ // Joueur existant : personnaliser l'interface
+ hideCaptainForm();
+ personalizeInterface(captainName, playCount);
+ }
+ }
+
+ function showCaptainForm() {
+ DOM.captainSection?.classList.remove('hidden');
+ DOM.subTitle.textContent = TEXTS.new.subtitle;
+
+ // Focus sur l'input après un délai pour l'animation
+ setTimeout(() => {
+ DOM.captainInput?.focus();
+ }, 500);
+ }
+
+ function hideCaptainForm() {
+ DOM.captainSection?.classList.add('hidden');
+ }
+
+ function personalizeInterface(captainName, playCount) {
+ let textSet;
+
+ if (playCount >= 10) {
+ textSet = TEXTS.veteran;
+ } else if (playCount > 0) {
+ textSet = TEXTS.returning;
+ } else {
+ textSet = TEXTS.new;
+ }
+
+ // Subtitle
+ DOM.subTitle.textContent = textSet.subtitle;
+
+ // Greeting personnalisé
+ if (textSet.greetings) {
+ const greeting = textSet.greetings[Math.floor(Math.random() * textSet.greetings.length)];
+ DOM.captainGreeting.textContent = greeting.replace('{name}', captainName);
+ } else {
+ DOM.captainGreeting.textContent = textSet.greeting || '';
+ }
+ }
+
+ // events
+ function bindEvents() {
+ // Sauvegarder le nom du capitaine
+ DOM.saveCaptainBtn?.addEventListener('click', saveCaptainName);
+ DOM.captainInput?.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') saveCaptainName();
+ });
+
+ // Boutons de difficulté
+ document.querySelectorAll('.diff-btn').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ const difficulty = e.currentTarget.getAttribute('data-difficulty');
+ startGame(difficulty);
+ });
+ });
+ }
+
+ function saveCaptainName() {
+ const name = DOM.captainInput?.value.trim();
+ const MIN_LEN = 3;
+ const MAX_LEN = 20;
+ const validRe = /^[A-Za-z0-9 _-]+$/;
+
+ if (!name) {
+ showInputError('Veuillez entrer un pseudo.');
+ return;
+ }
+ // Validation du pseudo comme demandé
+ if (name.length < MIN_LEN || name.length > MAX_LEN || !validRe.test(name)) {
+ showInputError(`Pseudo invalide — ${MIN_LEN}-${MAX_LEN} caractères, lettres/chiffres/_/- seulement.`);
+ return;
+ }
+
+ IceBreakerStorage.saveCaptainName(name);
+ hideCaptainForm();
+ personalizeInterface(name, 0);
+ DOM.difficultySection?.classList.add('fade-in');
+ }
+
+ // démarrer le jeu
+ function startGame(difficulty) {
+ // Incrémenter le compteur de parties
+ IceBreakerStorage.incrementPlayCount();
+
+ // Rediriger vers le jeu
+ window.location.href = `cruise.html?score=0&difficulty=${difficulty}`;
+ }
+
+ // vérifier si on revient avec un score
+ function checkAndSaveNewScore() {
+ try {
+ const params = new URLSearchParams(window.location.search);
+
+ // Format unifié pour tous les scores (checkpoint et game over)
+ if (params.get('finalScore') !== null) {
+ const totalDelivered = parseInt(params.get('totalDelivered')) || 0;
+ const totalPossible = parseInt(params.get('totalPossible')) || 1;
+ const cargoDeliveryRate = Math.round((totalDelivered / totalPossible) * 100);
+
+ const scoreData = {
+ score: parseInt(params.get('finalScore')) || 0,
+ difficulty: parseFloat(params.get('finalDifficulty')) || 1,
+ cargoDeliveryRate: cargoDeliveryRate,
+ checkpoints: parseInt(params.get('checkpoints')) || 0,
+ totalDelivered: totalDelivered,
+ totalPossible: totalPossible
+ };
+
+ console.log('[IceBreaker] Saving score:', scoreData);
+ IceBreakerStorage.saveScore(scoreData);
+ // Nettoyer l'URL
+ window.history.replaceState({}, document.title, window.location.pathname);
+ }
+ } catch (e) {
+ console.error('[IceBreaker] Error saving score:', e);
+ }
+ }
+
+ function loadLeaderboard() {
+ if (!DOM.resultsTableBody) {
+ console.error('[IceBreaker] resultsTableBody element not found');
+ return;
+ }
+
+ let scores = [];
+ try {
+ scores = IceBreakerStorage.getLeaderboard() || [];
+ } catch (e) {
+ console.error('[IceBreaker] Error loading leaderboard:', e);
+ scores = [];
+ }
+
+ DOM.resultsTableBody.innerHTML = '';
+
+ if (!Array.isArray(scores) || scores.length === 0) {
+ const isReturning = typeof IceBreakerStorage !== 'undefined' && IceBreakerStorage.isReturningPlayer();
+ const noDataText = isReturning ? TEXTS.returning.noScores : TEXTS.new.noScores;
+ DOM.resultsTableBody.innerHTML = `| ${noDataText} |
`;
+ return;
+ }
+
+ const fragment = document.createDocumentFragment();
+
+ scores.slice(0, MAX_SCORES_DISPLAY).forEach((result, index) => {
+ const row = document.createElement('tr');
+ row.className = index === 0 ? 'top-score' : '';
+
+ const rankIcon = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : `${index + 1}`;
+
+ // Afficher le pourcentage de livraison ou fallback pour anciens scores
+ let deliveryDisplay;
+ if (typeof result.cargoDeliveryRate === 'number') {
+ deliveryDisplay = `${result.cargoDeliveryRate}%`;
+ } else if (result.containers !== undefined) {
+ // Fallback pour anciens scores
+ const rate = Math.round((result.containers / (result.initialContainers || 20)) * 100);
+ deliveryDisplay = `${rate}%`;
+ } else {
+ deliveryDisplay = '-';
+ }
+
+ row.innerHTML = `
+ ${rankIcon} |
+ ${escapeHtml(result.captain || 'Anonyme')} |
+ ${result.score.toLocaleString()} |
+ ${deliveryDisplay} |
+ ${mapDifficulty(result.difficulty)} |
+ `;
+
+ fragment.appendChild(row);
+ });
+
+ DOM.resultsTableBody.appendChild(fragment);
+ }
+
+ // helpers
+ function mapDifficulty(diff) {
+ if (diff <= 1.1) return '❄️ Calme';
+ if (diff <= 1.6) return '🌊 Normal';
+ if (diff <= 2.6) return '⚡ Tempête';
+ return `🔥 Expert`;
+ }
+
+ function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+
+ // messages d'erreur input
+ function showInputMessage(msg, type = 'error') {
+ if (!DOM.captainInput) return;
+
+ let msgEl = document.getElementById('captain-input-message');
+
+ if (!msgEl) {
+ msgEl = document.createElement('div');
+ msgEl.id = 'captain-input-message';
+ msgEl.className = 'input-message';
+ DOM.captainInput.parentNode?.insertBefore(msgEl, DOM.captainInput.nextSibling);
+ }
+
+ msgEl.textContent = msg;
+ msgEl.classList.remove('error', 'success', 'visible');
+ msgEl.classList.add(type);
+
+ // Force reflow then show for CSS transitions
+ void msgEl.offsetWidth;
+ msgEl.classList.add('visible');
+
+ // Animation visuelle sur l'input
+ DOM.captainInput.classList.remove('shake');
+ void DOM.captainInput.offsetWidth;
+ DOM.captainInput.classList.add('shake');
+
+ DOM.captainInput.focus();
+
+ // Nettoyage automatique
+ clearTimeout(showInputMessage._timeout);
+ showInputMessage._timeout = setTimeout(() => {
+ msgEl.classList.remove('visible');
+ DOM.captainInput.classList.remove('shake');
+ }, 3000);
+ }
+
+ function showInputError(msg) { showInputMessage(msg, 'error'); }
+ function showInputSuccess(msg) { showInputMessage(msg, 'success'); }
+})();
\ No newline at end of file
diff --git a/src/assets/js/storage.js b/src/assets/js/storage.js
new file mode 100644
index 0000000..b1d8a42
--- /dev/null
+++ b/src/assets/js/storage.js
@@ -0,0 +1,193 @@
+/*
+ * Gestion du stockage local - pseudo, scores, préférences
+ * localStorage + cookies en fallback
+ */
+
+const IceBreakerStorage = (function () {
+ 'use strict';
+
+ // clés localStorage
+ const KEYS = {
+ CAPTAIN_NAME: 'icebreaker_captain',
+ LEADERBOARD: 'icebreaker_scores',
+ SETTINGS: 'icebreaker_settings',
+ PLAY_COUNT: 'icebreaker_plays',
+ LAST_PLAYED: 'icebreaker_lastplayed'
+ };
+
+ const MAX_SCORES = 10;
+
+ // --- Cookies (vieux mais ça marche) ---
+ function setCookie(name, value, days = 365) {
+ const expires = new Date(Date.now() + days * 864e5).toUTCString();
+ document.cookie = `${name}=${encodeURIComponent(JSON.stringify(value))}; expires=${expires}; path=/; SameSite=Lax`;
+ }
+
+ function getCookie(name) {
+ const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
+ if (match) {
+ try {
+ return JSON.parse(decodeURIComponent(match[2]));
+ } catch {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ function deleteCookie(name) {
+ document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
+ }
+
+ // --- Wrapper localStorage (avec fallback cookies au cas où) ---
+ function saveData(key, data) {
+ try {
+ localStorage.setItem(key, JSON.stringify(data));
+ } catch {
+ setCookie(key, data);
+ }
+ }
+
+ function loadData(key, defaultValue = null) {
+ try {
+ const data = localStorage.getItem(key);
+ return data ? JSON.parse(data) : getCookie(key) || defaultValue;
+ } catch {
+ return getCookie(key) || defaultValue;
+ }
+ }
+
+ // --- Fonctions publiques ---
+
+ // sauvegarde le nom du capitaine
+ function saveCaptainName(name) {
+ if (!name || typeof name !== 'string') return false;
+ const sanitized = name.trim().slice(0, 30);
+ saveData(KEYS.CAPTAIN_NAME, sanitized);
+ return true;
+ }
+
+ // récup le nom
+ function getCaptainName() {
+ return loadData(KEYS.CAPTAIN_NAME, null);
+ }
+
+ // le joueur est-il déjà venu ?
+ function isReturningPlayer() {
+ return loadData(KEYS.PLAY_COUNT, 0) > 0;
+ }
+
+ // +1 partie jouée
+ function incrementPlayCount() {
+ const count = loadData(KEYS.PLAY_COUNT, 0) + 1;
+ saveData(KEYS.PLAY_COUNT, count);
+ saveData(KEYS.LAST_PLAYED, Date.now());
+ return count;
+ }
+
+ // combien de parties ?
+ function getPlayCount() {
+ return loadData(KEYS.PLAY_COUNT, 0);
+ }
+
+ // enregistre un score
+ function saveScore(scoreData) {
+ let scores = loadData(KEYS.LEADERBOARD, []);
+
+ // S'assurer que scores est bien un tableau
+ if (!Array.isArray(scores)) {
+ console.warn('[IceBreakerStorage] Leaderboard was not an array, resetting');
+ scores = [];
+ }
+
+ // Calculer le pourcentage de livraison si non fourni
+ let deliveryRate = scoreData.cargoDeliveryRate;
+ if (typeof deliveryRate !== 'number') {
+ const totalDelivered = parseInt(scoreData.totalDelivered) || 0;
+ const totalPossible = parseInt(scoreData.totalPossible) || 1;
+ deliveryRate = Math.round((totalDelivered / totalPossible) * 100);
+ }
+
+ const entry = {
+ score: parseInt(scoreData.score) || 0,
+ difficulty: parseFloat(scoreData.difficulty) || 1,
+ cargoDeliveryRate: deliveryRate, // Pourcentage de marchandise livrée
+ checkpoints: parseInt(scoreData.checkpoints) || 0, // Nombre de checkpoints atteints
+ captain: getCaptainName() || 'Anonyme',
+ timestamp: Date.now()
+ };
+
+ scores.push(entry);
+
+ // Trier et limiter
+ scores.sort((a, b) => b.score - a.score);
+ const trimmed = scores.slice(0, MAX_SCORES);
+
+ saveData(KEYS.LEADERBOARD, trimmed);
+ console.log('[IceBreakerStorage] Score saved, total entries:', trimmed.length);
+ return entry;
+ }
+
+ // tous les scores
+ function getLeaderboard() {
+ const scores = loadData(KEYS.LEADERBOARD, []);
+ // S'assurer qu'on retourne toujours un tableau
+ return Array.isArray(scores) ? scores : [];
+ }
+
+ // meilleur score
+ function getBestScore() {
+ const scores = getLeaderboard();
+ return scores.length > 0 ? scores[0] : null;
+ }
+
+ // tout effacer (reset)
+ function clearAllData() {
+ Object.values(KEYS).forEach(key => {
+ try {
+ localStorage.removeItem(key);
+ } catch { }
+ deleteCookie(key);
+ });
+ }
+
+ // --- Audio ---
+
+ function saveSettings(settings) {
+ const current = loadData(KEYS.SETTINGS, {});
+ const merged = { ...current, ...settings };
+ saveData(KEYS.SETTINGS, merged);
+ return merged;
+ }
+
+ function getSettings() {
+ return loadData(KEYS.SETTINGS, {
+ masterVolume: 0.8,
+ sfxVolume: 1.0,
+ musicVolume: 0.6,
+ sfxEnabled: true,
+ musicEnabled: true
+ });
+ }
+
+ // Exposer l'API
+ return {
+ saveCaptainName,
+ getCaptainName,
+ isReturningPlayer,
+ incrementPlayCount,
+ getPlayCount,
+ saveScore,
+ getLeaderboard,
+ getBestScore,
+ clearAllData,
+ saveSettings,
+ getSettings,
+ KEYS
+ };
+})();
+
+// Export pour utilisation en modules si nécessaire
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = IceBreakerStorage;
+}
diff --git a/src/assets/sounds/break.mp3 b/src/assets/sounds/break.mp3
new file mode 100644
index 0000000..e926294
Binary files /dev/null and b/src/assets/sounds/break.mp3 differ
diff --git a/src/assets/sounds/cargoship-sound.mp3 b/src/assets/sounds/cargoship-sound.mp3
new file mode 100644
index 0000000..7a451d1
Binary files /dev/null and b/src/assets/sounds/cargoship-sound.mp3 differ
diff --git a/src/assets/sounds/checkpoint-success.mp3 b/src/assets/sounds/checkpoint-success.mp3
new file mode 100644
index 0000000..e84fe7e
Binary files /dev/null and b/src/assets/sounds/checkpoint-success.mp3 differ
diff --git a/src/assets/sounds/fall.mp3 b/src/assets/sounds/fall.mp3
new file mode 100644
index 0000000..3b987f9
Binary files /dev/null and b/src/assets/sounds/fall.mp3 differ
diff --git a/src/assets/sounds/hui.mp3 b/src/assets/sounds/hui.mp3
new file mode 100644
index 0000000..1d6eacc
Binary files /dev/null and b/src/assets/sounds/hui.mp3 differ
diff --git a/src/assets/sounds/menu-ambiance.mp3 b/src/assets/sounds/menu-ambiance.mp3
new file mode 100644
index 0000000..fdfa06e
Binary files /dev/null and b/src/assets/sounds/menu-ambiance.mp3 differ
diff --git a/src/assets/sounds/port-ambiance.mp3 b/src/assets/sounds/port-ambiance.mp3
new file mode 100644
index 0000000..f811b8f
Binary files /dev/null and b/src/assets/sounds/port-ambiance.mp3 differ
diff --git a/src/checkpoint.html b/src/checkpoint.html
new file mode 100644
index 0000000..2d7dcde
--- /dev/null
+++ b/src/checkpoint.html
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+ IceBreaker - Checkpoint
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Score précédent
+ 0
+
+
+ Bonus niveau
+ +0
+
+
+ Bonus cargaison
+ +0
+
+
+
+ SCORE TOTAL
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/cruise.html b/src/cruise.html
new file mode 100644
index 0000000..63d6cc7
--- /dev/null
+++ b/src/cruise.html
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+ IceBreaker - En Mer
+
+
+
+
+
+
+
+
+
+
+
🎧
+
Immersion Arctique
+
Activez le son pour une expérience complète dans les eaux glaciales. L'ambiance sonore et les alertes
+ amélioreront votre navigation.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+ Distance
+ 300 km
+
+
+
+

+
+ Cargaison
+ 20
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+ Z/Q/S/D ou ↑←↓→ pour naviguer • Attention aux
+ virages brusques !
+
+
+
+
+
+
💥
+
NAUFRAGE
+
La coque a été percée par un iceberg
+
+
+
+
🏆
+
+ Score Final
+ 0
+
+
+
+
📦
+
+ Marchandise Livrée
+ 0%
+
+
+
+
📏
+
+ Distance Parcourue
+ 0 km
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..5ed241b
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+ IceBreaker - Défi Arctique
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
⚓ Bienvenue à bord, Matelot !
+
Avant de prendre la mer, présentez-vous. Comment devons-nous vous appeler, Capitaine ?
+
+
+
+
+
+
+
+
+
+ Nouvelle Traversée
+
+
+
+
+
+
+
+
+
+
+ 🏆 Journal de Bord
+
+
+
+
+ | Rang |
+ Capitaine |
+ Score |
+ Livraison |
+ Difficulté |
+
+
+
+
+ | Chargement des archives... |
+
+
+
+
+
+
+
+
+
+
+
+
+ 80%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file