Initial commit

This commit is contained in:
Timeo Bossuet 2025-12-21 13:10:23 +01:00
commit 150db3d0c9
34 changed files with 4003 additions and 0 deletions

308
assets/css/checkpoint.css Normal file
View file

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

612
assets/css/cruise.css Normal file
View file

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

670
assets/css/index.css Normal file
View file

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

55
assets/css/main.css Normal file
View file

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

BIN
assets/img/cargoship.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 KiB

BIN
assets/img/container.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
assets/img/kilometer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

207
assets/js/audio-manager.js Normal file
View file

@ -0,0 +1,207 @@
/*
* 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();
// Créer le contexte audio pour le préchargement
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
} catch (e) {
console.warn('AudioContext non supporté');
}
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;
}

174
assets/js/checkpoint.js Normal file
View file

@ -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 = `<span class="badge-icon">⚓</span> 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}`;
}
})();

1010
assets/js/cruise.js Normal file

File diff suppressed because it is too large Load diff

431
assets/js/index.js Normal file
View file

@ -0,0 +1,431 @@
/*
* 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() {
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();
}
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 };
if (DOM.masterVolumeSlider) {
DOM.masterVolumeSlider.value = Math.round(settings.masterVolume * 100);
updateVolumeDisplay(settings.masterVolume * 100);
}
// Initialiser la musique d'ambiance
initBackgroundMusic(settings.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;
bgm.volume = masterVolume * 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() {
const params = new URLSearchParams(window.location.search);
// Format unifié pour tous les scores (checkpoint et game over)
if (params.get('finalScore')) {
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')),
difficulty: parseFloat(params.get('finalDifficulty') || 1),
cargoDeliveryRate: cargoDeliveryRate,
checkpoints: parseInt(params.get('checkpoints') || 0),
totalDelivered: totalDelivered,
totalPossible: totalPossible
};
if (!isNaN(scoreData.score)) {
IceBreakerStorage.saveScore(scoreData);
// Nettoyer l'URL
window.history.replaceState({}, document.title, window.location.pathname);
}
}
}
function loadLeaderboard() {
const scores = IceBreakerStorage.getLeaderboard();
if (!DOM.resultsTableBody) return;
DOM.resultsTableBody.innerHTML = '';
if (scores.length === 0) {
const isReturning = IceBreakerStorage.isReturningPlayer();
const noDataText = isReturning ? TEXTS.returning.noScores : TEXTS.new.noScores;
DOM.resultsTableBody.innerHTML = `<tr><td colspan="5" class="no-data">${noDataText}</td></tr>`;
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 = `
<td class="rank">${rankIcon}</td>
<td class="captain">${escapeHtml(result.captain || 'Anonyme')}</td>
<td class="score">${result.score.toLocaleString()}</td>
<td class="cargo">${deliveryDisplay}</td>
<td class="difficulty">${mapDifficulty(result.difficulty)}</td>
`;
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'); }
})();

184
assets/js/storage.js Normal file
View file

@ -0,0 +1,184 @@
/*
* 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) {
const scores = loadData(KEYS.LEADERBOARD, []);
// 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);
return entry;
}
// tous les scores
function getLeaderboard() {
return loadData(KEYS.LEADERBOARD, []);
}
// 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;
}

BIN
assets/sounds/break.mp3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/sounds/fall.mp3 Normal file

Binary file not shown.

BIN
assets/sounds/hui.mp3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

81
checkpoint.html Normal file
View file

@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="IceBreaker - Point de ravitaillement atteint !">
<title>IceBreaker - Checkpoint</title>
<link rel="stylesheet" href="./assets/css/main.css">
<link rel="stylesheet" href="./assets/css/checkpoint.css">
<link
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700&family=Poppins:wght@300;400;600;700&display=swap"
rel="stylesheet">
</head>
<body>
<!-- Particules de neige -->
<div id="snow-container" aria-hidden="true"></div>
<!-- Contenu principal centré -->
<main id="checkpoint-main">
<div class="checkpoint-card">
<!-- Header de la carte -->
<div class="card-header">
<span class="success-icon"></span>
<h1>Checkpoint Atteint !</h1>
<p class="subtitle">Traversée réussie, Capitaine</p>
<div id="captain-badge"></div>
</div>
<!-- Scores -->
<div class="scores-section">
<div class="score-row">
<span class="score-label">Score précédent</span>
<span id="scoreprec" class="score-value">0</span>
</div>
<div class="score-row bonus">
<span class="score-label">Bonus niveau</span>
<span id="scoreniv" class="score-value">+0</span>
</div>
<div class="score-row bonus">
<span class="score-label">Bonus cargaison</span>
<span id="contbonus" class="score-value">+0</span>
</div>
<hr class="separator">
<div class="score-row total">
<span class="score-label">SCORE TOTAL</span>
<span id="scoretot" class="score-value">0</span>
</div>
</div>
<!-- Boutons -->
<div class="actions">
<button id="continue-btn" class="btn btn-primary">
<span class="btn-icon">🚀</span>
Continuer l'aventure
</button>
<button id="lobby-btn" class="btn btn-secondary">
<span class="btn-icon">🏠</span>
Retour à l'accueil
</button>
</div>
</div>
</main>
<!-- Audio -->
<audio id="checkpoint-success" preload="auto">
<source src="./assets/sounds/checkpoint-success.mp3" type="audio/mpeg">
</audio>
<audio id="port-ambiance" loop preload="auto">
<source src="./assets/sounds/port-ambiance.mp3" type="audio/mpeg">
</audio>
<script src="./assets/js/storage.js"></script>
<script src="./assets/js/checkpoint.js"></script>
</body>
</html>

143
cruise.html Normal file
View file

@ -0,0 +1,143 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="IceBreaker - Naviguez à travers les icebergs et protégez votre cargaison !">
<title>IceBreaker - En Mer</title>
<link rel="stylesheet" href="./assets/css/cruise.css">
</head>
<body>
<audio id="background-audio" loop>
<source src="./assets/sounds/cargoship-sound.mp3" type="audio/mpeg">
</audio>
<!-- Modal d'activation audio -->
<div id="audio-prompt-modal" class="modal-overlay">
<div class="modal-content">
<div class="modal-icon">🎧</div>
<h3>Immersion Arctique</h3>
<p>Activez le son pour une expérience complète dans les eaux glaciales. L'ambiance sonore et les alertes
amélioreront votre navigation.</p>
<button id="allow-audio-button" class="modal-button">
<span>Activer le Son</span>
<span class="btn-wave">🔊</span>
</button>
<button id="skip-audio-button" class="modal-button-secondary">
Continuer sans son
</button>
</div>
</div>
<!-- Contrôles audio discrets -->
<div id="audio-controls" class="game-audio-panel">
<button id="audio-toggle-game" class="audio-btn-game" title="Son">
<span class="audio-icon">🔊</span>
</button>
<input type="range" id="volume-slider-game" class="volume-slider" min="0" max="100" value="80">
</div>
<!-- HUD / Tableau de bord -->
<div id="score-panel">
<div class="hud-header">
<span class="hud-title">IceBreaker</span>
<span id="captain-name-display" class="captain-badge"></span>
</div>
<div class="score-grid">
<div class="score-box">
<img class="score-icon" src="./assets/img/kilometer.png" alt="Distance">
<div class="score-info">
<span class="score-label">Distance</span>
<span id="dist-display" class="score-value">300 km</span>
</div>
</div>
<div class="score-box">
<img class="score-icon" src="./assets/img/container.png" alt="Cargo">
<div class="score-info">
<span class="score-label">Cargaison</span>
<span id="cont-display" class="score-value">20</span>
</div>
</div>
<div class="score-box highlight">
<div class="score-info">
<span class="score-label">Score</span>
<span id="score-display" class="score-value">0</span>
</div>
</div>
</div>
</div>
<!-- Zone de jeu -->
<div id="game-world">
<div id="iceberg-layer"></div>
<div class="boat" id="player-boat">
<img src="./assets/img/cargoship.png" alt="Cargo Ship">
<div class="overlay" id="container-deck"></div>
</div>
</div>
<!-- Indication des contrôles -->
<div class="hint">
<span class="hint-keys">Z/Q/S/D</span> ou <span class="hint-keys">↑←↓→</span> pour naviguer • Attention aux
virages brusques !
</div>
<!-- Modal Game Over -->
<div id="gameover-modal" class="modal-overlay hidden">
<div class="modal-content gameover-content">
<div class="gameover-icon">💥</div>
<div class="gameover-title">NAUFRAGE</div>
<p class="gameover-subtitle">La coque a été percée par un iceberg</p>
<div class="gameover-stats">
<div class="gameover-stat">
<span class="stat-icon">🏆</span>
<div class="stat-info">
<span class="stat-label">Score Final</span>
<span id="gameover-score" class="stat-value">0</span>
</div>
</div>
<div class="gameover-stat">
<span class="stat-icon">📦</span>
<div class="stat-info">
<span class="stat-label">Marchandise Livrée</span>
<span id="gameover-containers" class="stat-value">0%</span>
</div>
</div>
<div class="gameover-stat">
<span class="stat-icon">📏</span>
<div class="stat-info">
<span class="stat-label">Distance Parcourue</span>
<span id="gameover-distance" class="stat-value">0 km</span>
</div>
</div>
</div>
<button id="gameover-home-btn" class="modal-button gameover-btn">
<span>🏠</span>
<span>Retour au Port</span>
</button>
</div>
</div>
<!-- Effets sonores -->
<audio id="sfx-drop" preload="auto">
<source src="./assets/sounds/fall.mp3" type="audio/mpeg">
</audio>
<audio id="sfx-alert" preload="auto">
<source src="./assets/sounds/hui.mp3" type="audio/mpeg">
</audio>
<audio id="sfx-break" preload="auto">
<source src="./assets/sounds/break.mp3" type="audio/mpeg">
</audio>
<script src="./assets/js/storage.js"></script>
<script src="./assets/js/audio-manager.js"></script>
<script src="./assets/js/cruise.js"></script>
</body>
</html>

128
index.html Normal file
View file

@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description"
content="IceBreaker - Le jeu de navigation arctique. Pilotez votre cargo à travers les eaux glaciales et évitez les icebergs !">
<meta name="theme-color" content="#0a1929">
<title>IceBreaker - Défi Arctique</title>
<link rel="stylesheet" href="./assets/css/main.css">
<link rel="stylesheet" href="./assets/css/index.css">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap" rel="stylesheet">
</head>
<body id="indexBody">
<!-- Particules de neige -->
<div id="snow-container" aria-hidden="true"></div>
<!-- Background arctique -->
<div id="arctic-background">
<div id="arctic-aurora"></div>
<div id="arctic-wave"></div>
<div id="arctic-wave-2"></div>
</div>
<main class="main-content">
<div class="hub-container glassmorphism-panel">
<!-- Header avec logo -->
<header>
<div class="logo-container">
<span class="logo-icon">🚢</span>
<h1 id="main-title">IceBreaker</h1>
</div>
<p id="sub-title" class="welcome-text">Traversez les eaux glacées arctiques et livrez votre précieuse
cargaison.</p>
</header>
<!-- Section Capitaine (nouveau joueur) -->
<section id="captain-section" class="hidden">
<div class="captain-welcome">
<h2><span class="icon"></span> Bienvenue à bord, Matelot !</h2>
<p>Avant de prendre la mer, présentez-vous. Comment devons-nous vous appeler, Capitaine ?</p>
<div class="input-group">
<input type="text" id="captain-name-input" placeholder="Entrez votre nom de capitaine..."
maxlength="30" autocomplete="off">
<button id="save-captain-btn" class="btn-primary">
<span>Embarquer</span>
<span class="btn-icon"></span>
</button>
</div>
</div>
</section>
<!-- Section Sélection de difficulté -->
<section id="difficulty-selection">
<h2> Nouvelle Traversée</h2>
<p id="captain-greeting" class="greeting-text"></p>
<div class="difficulty-options">
<button class="diff-btn" data-difficulty="1">
<span class="diff-icon">❄️</span>
<span class="diff-name">Eaux Calmes</span>
<span class="diff-desc">Pour les nouveaux marins</span>
</button>
<button class="diff-btn" data-difficulty="2">
<span class="diff-icon">🌊</span>
<span class="diff-name">Mer de Béring</span>
<span class="diff-desc">Navigateur confirmé</span>
</button>
<button class="diff-btn" data-difficulty="3">
<span class="diff-icon"></span>
<span class="diff-name">Tempête Polaire</span>
<span class="diff-desc">Pour les plus téméraires</span>
</button>
</div>
</section>
<!-- Section Leaderboard -->
<section id="leaderboard">
<h2><span class="icon">🏆</span> Journal de Bord</h2>
<div class="leaderboard-content">
<table id="results-table">
<thead>
<tr>
<th>Rang</th>
<th>Capitaine</th>
<th>Score</th>
<th>Livraison</th>
<th>Difficulté</th>
</tr>
</thead>
<tbody id="results-table-body">
<tr>
<td colspan="5" class="no-data">Chargement des archives...</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- Contrôle Audio discret -->
<div id="audio-controls" class="audio-panel">
<button id="audio-toggle" class="audio-btn" title="Réglages audio">
<span class="audio-icon">🔊</span>
</button>
<div id="audio-slider-container" class="audio-slider-wrap hidden">
<label for="master-volume">Volume</label>
<input type="range" id="master-volume" min="0" max="100" value="80">
<span id="volume-value">80%</span>
</div>
</div>
</div>
</main>
<!-- Audio -->
<audio id="background-audio" loop preload="auto">
<source src="./assets/sounds/menu-ambiance.mp3" type="audio/mpeg">
</audio>
<script src="./assets/js/storage.js"></script>
<script src="./assets/js/audio-manager.js"></script>
<script src="./assets/js/index.js"></script>
</body>
</html>