<div id="game" class="game">
</div>
<div class="title">DUCKY<br />FOG</div>
<div id="level-text" class="level-text">0</div>
<div id="water-level" class="water-level"></div>
<div id="controls" class="controls">
<div id="top" class="controls__arrow controls__arrow--top"></div>
<div id="left" class="controls__arrow controls__arrow--left"></div>
<div id="right" class="controls__arrow controls__arrow--right"></div>
<div id="bottom" class="controls__arrow controls__arrow--bottom"></div>
</div>
<div id="fullscreen" class="fullscreen"><svg fill="#FFF" width="1.3rem" height="1.3rem" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M14 3.414L9.414 8 14 12.586v-2.583h2V16h-6v-1.996h2.59L8 9.414l-4.59 4.59H6V16H0v-5.997h2v2.583L6.586 8 2 3.414v2.588H0V0h16v6.002h-2V3.414zm-1.415-1.413H10V0H6v2H3.415L8 6.586 12.585 2z" fill-rule="evenodd" />
</svg></div>
<div id="refresh" class="refresh"><svg fill="#fff" width="2rem" height="2rem" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M27.1 14.313V5.396L24.158 8.34c-2.33-2.325-5.033-3.503-8.11-3.503C9.902 4.837 4.901 9.847 4.899 16c.001 6.152 5.003 11.158 11.15 11.16 4.276 0 9.369-2.227 10.836-8.478l.028-.122h-3.23l-.022.068c-1.078 3.242-4.138 5.421-7.613 5.421a8 8 0 0 1-5.691-2.359A7.993 7.993 0 0 1 8 16.001c0-4.438 3.611-8.049 8.05-8.049 2.069 0 3.638.58 5.924 2.573l-3.792 3.789H27.1z" />
</svg></div>
<div id="over-text" class="over-text"></div>
<div id="level-selector" class="level-selector"></div>
<div id="intro" class="intro">
<span class="intro__earth">DUCKY<br /></span>
<span class="intro__title">DUCKY<br />FOG</span>
<span class="intro__subtitle">around the world</span>
<span class="intro__button">Play</span>
<svg class="intro__player" height="800px" width="800px xmlns=" http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" xml:space="preserve">
<g>
<path style="fill:#ff7d66;" d="M90.465,186.912c0.086-7.581-5.26-27.49,2.771-43.781c8.025-16.291-42.226-20.546-67.682,0.367
c-24.51,20.137,1.764,37.468,17.938,33.657C76.267,169.428,90.299,202.261,90.465,186.912z" />
<path style="fill:#ff7d66;" d="M73.315,96.009c0.079,7.574-5.26,27.483,2.764,43.782c8.025,16.277-46.799,25.105-67.674-0.375
c-20.878-25.48,1.76-37.461,17.941-33.65C59.102,113.485,73.137,80.654,73.315,96.009z" />
<path class="game__player-body" style="fill:#fff07f;" d="M76.45,264.211c18.183-21.406,33.092-31.038,20.821-50.433
c-23.402-23.408-34.309-48.743-34.309-84.458c0-35.709,14.476-68.035,37.876-91.436C124.243,14.483,156.563,0,192.278,0
s68.035,14.483,91.443,37.884c23.401,23.402,37.87,55.728,37.87,91.436c0,52.789-42.222,95.011-42.222,121.4
c-2.64,10.561,15.835,23.761,39.582,23.761c23.761,0,102.932,18.467,160.988-58.063c15.28-21.122,33.755-9.238,31.675,21.107
C509.618,266.609,493.133,512,318.951,512c-50.135,0-43.788,0-139.874,0c-40.074,0-76.36-16.25-102.627-42.513
c-26.273-26.27-42.52-62.554-42.52-102.649C33.93,326.765,50.176,290.474,76.45,264.211" />
<path class="game__player-wing" style="fill:#dccc49;" d="M219.533,340.458h193.216c21.108,0,26.388,36.941-7.92,58.063c0,26.38-13.201,65.97-52.782,65.97
c-39.596,0-87.092,0-121.4,0c-34.316,0-58.064-39.59-58.064-63.337C172.584,377.399,195.777,340.458,219.533,340.458z" />
<path class="game__player-wing" style="fill:#F5FCCD;" d="M206.331,324.616h193.218c21.108,0,26.388,36.949-7.92,58.064c0,26.395-13.194,65.983-52.776,65.983
c-39.589,0-87.097,0-121.406,0s-58.057-39.589-58.057-63.344C159.39,361.565,182.583,324.616,206.331,324.616z" />
<path class="game__player-eye" style="fill:#FFFFFF;" d="M159.39,109.522c0,16.77-13.596,30.359-30.348,30.359c-16.763,0-30.355-13.589-30.355-30.359
c0-16.748,13.592-30.344,30.355-30.344C145.794,79.177,159.39,92.773,159.39,109.522z" />
<path class="game__player-eye" style="fill:#000038;" d="M143.556,109.522c0,8.025-6.503,14.525-14.514,14.525c-8.018,0-14.518-6.5-14.518-14.525
c0-8.003,6.5-14.503,14.518-14.503C137.052,95.018,143.556,101.518,143.556,109.522z" />
</g>
</svg>
</div>
<div class="drawer" style="display:none;">
<svg id="player" class="game__player" height="800px" width="800px xmlns=" http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" xml:space="preserve">
<g>
<path style="fill:#ff7d66;" d="M90.465,186.912c0.086-7.581-5.26-27.49,2.771-43.781c8.025-16.291-42.226-20.546-67.682,0.367
c-24.51,20.137,1.764,37.468,17.938,33.657C76.267,169.428,90.299,202.261,90.465,186.912z" />
<path style="fill:#ff7d66;" d="M73.315,96.009c0.079,7.574-5.26,27.483,2.764,43.782c8.025,16.277-46.799,25.105-67.674-0.375
c-20.878-25.48,1.76-37.461,17.941-33.65C59.102,113.485,73.137,80.654,73.315,96.009z" />
<path class="game__player-body" style="fill:#fff07f;" d="M76.45,264.211c18.183-21.406,33.092-31.038,20.821-50.433
c-23.402-23.408-34.309-48.743-34.309-84.458c0-35.709,14.476-68.035,37.876-91.436C124.243,14.483,156.563,0,192.278,0
s68.035,14.483,91.443,37.884c23.401,23.402,37.87,55.728,37.87,91.436c0,52.789-42.222,95.011-42.222,121.4
c-2.64,10.561,15.835,23.761,39.582,23.761c23.761,0,102.932,18.467,160.988-58.063c15.28-21.122,33.755-9.238,31.675,21.107
C509.618,266.609,493.133,512,318.951,512c-50.135,0-43.788,0-139.874,0c-40.074,0-76.36-16.25-102.627-42.513
c-26.273-26.27-42.52-62.554-42.52-102.649C33.93,326.765,50.176,290.474,76.45,264.211" />
<path class="game__player-wing" style="fill:#dccc49;" d="M219.533,340.458h193.216c21.108,0,26.388,36.941-7.92,58.063c0,26.38-13.201,65.97-52.782,65.97
c-39.596,0-87.092,0-121.4,0c-34.316,0-58.064-39.59-58.064-63.337C172.584,377.399,195.777,340.458,219.533,340.458z" />
<path class="game__player-wing" style="fill:#F5FCCD;" d="M206.331,324.616h193.218c21.108,0,26.388,36.949-7.92,58.064c0,26.395-13.194,65.983-52.776,65.983
c-39.589,0-87.097,0-121.406,0s-58.057-39.589-58.057-63.344C159.39,361.565,182.583,324.616,206.331,324.616z" />
<path class="game__player-eye" style="fill:#FFFFFF;" d="M159.39,109.522c0,16.77-13.596,30.359-30.348,30.359c-16.763,0-30.355-13.589-30.355-30.359
c0-16.748,13.592-30.344,30.355-30.344C145.794,79.177,159.39,92.773,159.39,109.522z" />
<path class="game__player-eye" style="fill:#000038;" d="M143.556,109.522c0,8.025-6.503,14.525-14.514,14.525c-8.018,0-14.518-6.5-14.518-14.525
c0-8.003,6.5-14.503,14.518-14.503C137.052,95.018,143.556,101.518,143.556,109.522z" />
</g>
</svg>
<div id="water" class="game__water game__water--0">
<div class="game__water--bottom"></div>
<div class="game__water--left"></div>
<div class="game__water--right"></div>
<div class="game__water--top"></div>
<div class="game__water--front"></div>
</div>
<div id="rock" class="game__cell game__cell--rock">
<div class="game__cell--rock-face game__cell--rock-left"></div>
<div class="game__cell--rock-face game__cell--rock-right"></div>
<div class="game__cell--rock-face game__cell--rock-top"></div>
<div class="game__cell--rock-face game__cell--rock-bottom"></div>
<div class="game__cell--rock-face game__cell--rock-front"></div>
</div>
</div>
@import url("https://fonts.googleapis.com/css2?family=Titan+One&display=swap");
$green: #80c0a1;
$yellow: #f5fccd;
$high-yellow: #fff07f;
$red: #ff7d66;
$black: #000038;
$white: #fff;
$pink: #eb80b1;
$light-blue: #78d6c6;
$blue: #419197;
$dark-blue: #12486b;
$orange: orange;
$grey: #919ea3;
$dark-grey: #66777f;
html {
font-size: 3vh;
@media (orientation: portrait) {
font-size: 2.9vw;
}
}
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
color: $white;
background: radial-gradient($blue, $blue 60%, $white 300%);
overflow: hidden;
font-family: "Titan One", sans-serif;
font-weight: 700;
font-size: 1.5rem;
}
.game {
$component-class: &;
position: relative;
display: flex;
flex-wrap: wrap;
width: 29.03rem; // .03 for errors when summing cells on Mozilla
transition: transform 0.5s ease;
border: 1px solid $blue;
overflow: hidden;
perspective: 20rem;
transform-style: preserve-3d;
transform-origin: center center 0;
border-radius: 0.5rem;
background-color: $light-blue;
&:before,
&:after {
content: "";
display: flex;
position: absolute;
z-index: -1;
top: 0;
width: var(--cell);
height: 100%;
background: red;
transform-style: preserve-3d;
transform-origin: center center 0;
background-color: rgba($blue, 0.5);
outline: 1px solid $white;
}
&:before {
left: 0;
transform: translateZ(calc(-0.5 * var(--cell)))
translateX(calc(-0.5 * var(--cell))) rotate3d(0, 1, 0, 90deg);
}
&:after {
right: 0;
transform: translateZ(calc(-0.5 * var(--cell)))
translateX(calc(0.5 * var(--cell))) rotate3d(0, 1, 0, 90deg);
}
&__player {
position: absolute;
z-index: 999;
width: calc(var(--cell) * 0.8);
height: calc(var(--cell) * 0.8);
top: calc((var(--positionTop) + 0.1) * var(--cell));
left: calc((var(--positionLeft) + 0.1) * var(--cell));
transition-property: top, left, transform;
transition-duration: 0.2s;
transition-timing-function: ease;
transform: var(--rotation) translateZ(0);
&-eye {
transform-origin: 50% 20%;
@keyframes blink {
0%,
40%,
60%,
100% {
transform: scaleY(1);
}
50% {
transform: scaleY(0.2);
}
}
animation: blink;
animation-duration: 2s;
animation-iteration-count: infinite;
}
@media (max-width: 767px) {
transition-duration: 0.1s;
}
}
&__cell {
position: relative;
z-index: -1;
width: var(--cell);
height: var(--cell);
background: linear-gradient(135deg, $light-blue, $white 200%);
outline: 1px solid $white;
transform: translateZ(calc(-1 * var(--cell)));
&--rock {
$rock: &;
position: relative;
transform: translateZ(0);
transform-style: preserve-3d;
transform-origin: center center 0;
background: none;
outline: none;
&-face {
position: absolute;
display: flex;
background: linear-gradient(135deg, $grey, $white 200%);
border-radius: 0.1rem;
outline: 1px solid $grey;
}
&-lava #{$rock}-face {
background: linear-gradient(45deg, $orange, $red 200%);
background-size: 100% 200%;
@keyframes lava {
0%,
100% {
background-position: 0 0;
}
50% {
background-position: 0 100%;
}
}
animation: lava;
animation-duration: 2s;
animation-iteration-count: infinite;
outline: 1px solid $red;
}
&-front {
top: 0;
left: 0;
width: 100%;
height: 100%;
}
&-top,
&-bottom,
&-left,
&-right {
width: 100%;
height: var(--cell);
}
&-top {
top: 0;
left: 0;
transform: translateZ(calc(-0.5 * var(--cell)))
translateY(calc(-0.5 * var(--cell))) rotate3d(1, 0, 0, 90deg);
}
&-bottom {
bottom: 0;
left: 0;
transform: translateZ(calc(-0.5 * var(--cell)))
translateY(calc(0.5 * var(--cell))) rotate3d(1, 0, 0, 90deg);
}
&-left {
top: 0;
left: 0;
transform: translateZ(calc(-0.5 * var(--cell)))
translateY(calc(-0.5 * var(--cell)))
translateX(calc(var(--cell) / -2)) rotate3d(1, 0, 0, 90deg)
rotate3d(0, 1, 0, 90deg) translateX(calc(var(--cell) / 2));
}
&-right {
top: 0;
left: 0;
transform: translateZ(calc(-0.5 * var(--cell)))
translateY(calc(-0.5 * var(--cell)))
translateX(calc(var(--cell) / -2)) rotate3d(1, 0, 0, 90deg)
rotate3d(0, 1, 0, 90deg) translateX(calc(var(--cell) / 2))
translateZ(var(--cell));
}
}
&--horizontal {
z-index: 0;
transform: translateZ(calc(var(--cell) * -0.8));
background: linear-gradient(135deg, $light-blue, $pink 200%);
&:before,
&:after {
content: "";
position: absolute;
width: 0;
height: 0;
border-top: calc(var(--cell) / 4) solid transparent;
border-bottom: calc(var(--cell) / 4) solid transparent;
}
&:before {
bottom: calc(var(--cell) / 6);
left: calc(var(--cell) / 6);
border-left: calc(var(--cell) / 3) solid $white;
}
&:after {
top: calc(var(--cell) / 6);
right: calc(var(--cell) / 6);
border-right: calc(var(--cell) / 3) solid $white;
}
}
&--vertical {
z-index: 0;
transform: translateZ(calc(var(--cell) * -0.8));
background: linear-gradient(135deg, $light-blue, $orange 200%);
&:before,
&:after {
content: "";
position: absolute;
width: 0;
height: 0;
border-left: calc(var(--cell) / 4) solid transparent;
border-right: calc(var(--cell) / 4) solid transparent;
}
&:before {
bottom: calc(var(--cell) / 6);
left: calc(var(--cell) / 6);
border-bottom: calc(var(--cell) / 3) solid $white;
}
&:after {
top: calc(var(--cell) / 6);
right: calc(var(--cell) / 6);
border-top: calc(var(--cell) / 3) solid $white;
}
}
&--end {
background-color: $white;
background-image: linear-gradient(
45deg,
$black 25%,
transparent 25%,
transparent 74%,
$black 75%,
$black
),
linear-gradient(
45deg,
$black 25%,
transparent 25%,
transparent 74%,
$black 75%,
$black
);
background-size: calc(var(--cell) / 2) calc(var(--cell) / 2);
background-position: 0 0, calc(var(--cell) / 4) calc(var(--cell) / 4);
}
}
&__wrapper {
position: relative;
}
&__water {
$water: &;
position: absolute;
transition-property: width, height;
transition-duration: 0.2s;
transition-timing-function: ease;
transform-style: preserve-3d;
&--0,
&--2 {
width: 100%;
height: calc(var(--depth) * var(--cell));
}
&--1,
&--3 {
height: 100%;
width: calc(var(--depth) * var(--cell));
}
&--0,
&--3 {
bottom: 0;
left: 0;
}
&--1 {
bottom: 0;
right: 0;
}
&--2 {
top: 0;
left: 0;
}
&--front {
position: relative;
z-index: 2;
width: 100%;
height: 100%;
background-color: rgba($blue, 0.6);
transform: translateZ(0);
}
&--top,
&--left,
&--right,
&--bottom {
position: absolute;
display: flex;
background: linear-gradient(45deg, rgba($blue, 0.5), $white 200%);
}
&--top,
&--bottom {
width: 100%;
height: var(--cell);
transform-origin: center 0 0;
}
&--left,
&--right {
width: var(--cell);
height: 100%;
transform-origin: center center 0;
}
&--top {
top: 0;
left: 0;
transform: translateZ(calc(-1 * var(--cell))) rotate3d(1, 0, 0, 90deg);
}
&--left {
top: 0;
left: 0;
transform: translateZ(calc(-0.5 * var(--cell)))
translateX(calc(-0.5 * var(--cell))) rotate3d(0, 1, 0, 90deg);
}
&--right {
top: 0;
right: 0;
transform: translateZ(calc(-0.5 * var(--cell)))
translateX(calc(0.5 * var(--cell))) rotate3d(0, 1, 0, 90deg);
}
&--bottom {
bottom: 0;
left: 0;
transform: translateZ(calc(-1 * var(--cell)))
translateY(calc(var(--cell))) rotate3d(1, 0, 0, 90deg);
}
}
}
.title {
position: absolute;
top: 1rem;
left: 1rem;
color: $high-yellow;
//font-size: 1.2rem;
}
.level-text {
position: absolute;
top: 1rem;
right: 1rem;
&:before {
content: "\0062\0079\ \0050\0045\0044\0052\004F\ \004F\004E\0044\0049\0056\0049\0045\004C\0041";
position: absolute;
right: 0;
bottom: calc(4rem - 100vh);
font-size: 0.5rem;
text-align: right;
}
}
.water-level {
position: absolute;
top: calc(50vh - var(--height) * var(--cell) * 0.5);
right: calc(50vw - var(--width) * var(--cell) * 0.5 - 0.15rem);
display: flex;
width: 0.3rem;
height: calc(var(--height) * var(--cell));
background: linear-gradient($light-blue 50%, transparent 50%);
background-size: 100% 200%;
background-position: 0 calc(100% + (var(--depth) / var(--height) * 100%));
transition: all 0.2s ease;
&:before,
&:after {
content: "";
position: absolute;
left: -0.15rem;
width: 5rem;
color: $white;
font-size: 1rem;
}
&:before {
content: "min -";
top: calc(100% - (var(--min-depth) / var(--height) * 100%) - 0.7rem);
left: -2.35rem;
}
&:after {
content: "- max";
top: calc(100% - (var(--max-depth) / var(--height) * 100%) - 0.7rem);
}
&--hidden {
display: none;
}
}
.over-text {
$overText: &;
position: absolute;
top: 0;
left: 0;
display: none;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
color: $white;
background-color: rgba($blue, 0.5);
text-align: center;
text-transform: uppercase;
&--display {
display: flex;
}
&--lose {
color: $black;
background-color: rgba($red, 0.5);
}
&--win {
display: flex;
flex-direction: column;
font-size: 10vw;
color: $dark-blue;
background-color: rgba($yellow, 0.5);
#{$overText}__timer {
font-size: 5vw;
}
}
&--tutorial {
color: $dark-blue;
background: rgba(white, 0.3);
top: unset;
bottom: 0;
height: 4rem;
pointer-events: none;
}
}
.controls {
position: absolute;
top: 0;
left: 0;
display: flex;
flex-wrap: wrap;
width: 100%;
height: 100%;
&__arrow {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
&:after {
content: "";
width: 0;
height: 0;
margin: 1rem;
opacity: 0.3;
}
&--top,
&--bottom {
width: 100%;
height: 40vh;
}
&--left,
&--right {
flex-direction: column;
width: 50%;
height: 20vh;
}
&--top,
&--left {
align-items: flex-start;
}
&--bottom,
&--right {
align-items: flex-end;
}
&--top:after {
border-left: 1rem solid transparent;
border-right: 1rem solid transparent;
border-bottom: 1rem solid $white;
}
&--left:after {
border-left: 1rem solid transparent;
border-right: 1rem solid transparent;
border-top: 1rem solid $white;
transform: rotate(45deg);
}
&--right:after {
border-left: 1rem solid transparent;
border-right: 1rem solid transparent;
border-top: 1rem solid $white;
transform: rotate(-45deg);
}
&--bottom:after {
border-left: 1rem solid transparent;
border-right: 1rem solid transparent;
border-top: 1rem solid $white;
}
}
}
.intro {
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
padding: 1rem;
color: $high-yellow;
background-color: $blue;
cursor: pointer;
&__title {
font-size: 4rem;
}
&__subtitle {
color: $yellow;
}
&__explanation {
position: absolute;
bottom: 3rem;
left: 1rem;
width: calc(100vw - 2rem);
font-size: 1.5rem;
text-align: right;
color: $white;
}
&__player {
position: absolute;
top: 10rem;
right: 0;
width: 40vw;
height: 40vh;
transform: rotate(10deg);
}
&__button {
position: absolute;
bottom: 30vh;
left: 40vw;
display: inline-flex;
justify-content: center;
width: 20vw;
padding: 0.5rem 1rem;
border-radius: 2rem;
color: $dark-blue;
background: $high-yellow;
outline: 2px solid $dark-blue;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.2);
}
}
&__earth {
position: absolute;
top: 30vh;
left: 15vw;
width: 70vw;
height: 70vw;
border-radius: 50%;
background-color: $dark-blue;
transform: rotate(10deg);
overflow: hidden;
@media (max-width: 767px) {
width: 120vw;
height: 120vw;
left: -10vw;
}
&:before {
content: "";
display: block;
width: 100%;
height: 100%;
background: url("https://www.pedro-ondiviela.com/assets/map.svg");
background-size: cover;
transition: background-position 0.5s ease;
@keyframes globeSpinning {
0% {
background-position: 0 0;
}
100% {
background-position: 288% 0;
}
}
animation: globeSpinning;
animation-duration: 300s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
}
&--hidden {
display: none;
}
}
.fullscreen {
position: absolute;
top: 1.2rem;
right: 7rem;
cursor: pointer;
}
.refresh {
position: absolute;
top: 1rem;
right: 4rem;
cursor: pointer;
}
.level-selector {
position: absolute;
top: 0;
left: 0;
width: 80vw;
height: 100vh;
padding: 0 10vw;
color: $white;
background-color: rgba($blue, 0.5);
font-size: 2rem;
display: none;
@media (min-height: 600px) {
font-size: 1.5rem;
}
&--active {
display: flex;
justify-content: center;
align-items: center;
align-content: center;
flex-wrap: wrap;
}
&__button {
padding: 1rem;
border-radius: 0.5rem;
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
background-color: rgba($black, 0.2);
}
&--current {
background-color: $dark-blue;
}
&--disabled {
opacity: 0.5;
cursor: default;
pointer-events: none;
}
}
}
View Compiled
const game = document.getElementById("game");
const player = document.getElementById("player");
const water = document.getElementById("water");
const rock = document.getElementById("rock");
const overText = document.getElementById("over-text");
const levelText = document.getElementById("level-text");
const waterLevel = document.getElementById("water-level");
const intro = document.getElementById("intro");
const topArrow = document.getElementById("top");
const leftArrow = document.getElementById("left");
const rightArrow = document.getElementById("right");
const bottomArrow = document.getElementById("bottom");
const refresh = document.getElementById("refresh");
const fullscreen = document.getElementById("fullscreen");
const levelSelector = document.getElementById("level-selector");
let level = 0;
let width = 29;
const puzzles = [
{
initialDepth: 1,
minDepth: 1,
maxDepth: 2,
level: ["-----", "-----", "--e--", "-ox--", "--x--"]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 3,
level: ["------", "------", "---e--", "--xx--", "-o-x--", "---x--"]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 2,
level: ["-----", "-----", "-e---", "---xx", "---ox", "----x"]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 2,
level: ["--xxx-", "-----x", "-x-x--", "---xex", "---oxx", "----xx"]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 4,
level: [
"----xxx-",
"-------x",
"---x-x--",
"-xx--xex",
"xx---oxx",
"xx----xx"
]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 6,
level: [
"----xxe",
"----xx-",
"xx-----",
"----xx-",
"----xx-",
"-o--xx-",
"----xx-"
]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 2,
level: [
"x---xxx",
"x-----x",
"x-xxx-x",
"xe--x-x",
"x-x-x-x",
"xox---x",
"x-xxxxx"
]
},
{
initialDepth: 0,
minDepth: 0,
maxDepth: 2,
level: [
"x--exxx",
"x-xx--x",
"x-x---x",
"x---x-x",
"xxxxx-x",
"x-----x",
"xoxxxxx"
]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 2,
level: ["x--exxx", "xo----x", "x-xxxxx"]
},
{
initialDepth: 0,
minDepth: 0,
maxDepth: 1,
level: ["-x-", "---", "oxe"]
},
{
initialDepth: 1,
minDepth: 0,
maxDepth: 3,
level: ["-x--x--", "-------", "-x--x--", "---ox--", "-x--xe-"]
},
{
initialDepth: 3,
minDepth: 0,
maxDepth: 3,
level: [
"-x--x--",
"-------",
"-x--x--",
"---ox--",
"-x--xx-",
"-x--x--",
"-x--xe-"
]
},
{
initialDepth: 1,
minDepth: 0,
maxDepth: 1,
level: [
"-xxx--x--",
"x-------x",
"xx-xx-x--",
"----xx---",
"-x--x--xx",
"-x-ox-x--",
"-x--x--e-"
]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 6,
level: [
"xxx--x--xx",
"--------xx",
"x-xx-xx---",
"---xx-xx--",
"x--xxx---x",
"x-oxex-xx-",
"x--x----xx"
]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 2,
level: [
"x-----x",
"-x---x-",
"-------",
"-------",
"--x-x--",
"oxe----",
"x-----x"
]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 2,
level: [
"x--xx---x",
"-x-xx--x-",
"---------",
"---xx----",
"--xxx-x--",
"oxe------",
"x---xx--x"
]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 1,
level: [
"x-----x",
"--x--x-",
"-e-----",
"----x--",
"-x-x---",
"ox----x",
"----xxx"
]
},
{
initialDepth: 1,
minDepth: 0,
maxDepth: 1,
level: ["-----", "---e-", "-xxx-", "--o--", "-----"]
},
{
initialDepth: 3,
minDepth: 3,
maxDepth: 4,
level: ["------", "----x-", "--o---", "---x--", "----e-", "-x----"]
},
{
initialDepth: 0,
minDepth: 0,
maxDepth: 0,
level: ["xx---x", "----x-", "-xx---", "---xx-", "e-----", "ox-x--"]
},
{
initialDepth: 0,
minDepth: 0,
maxDepth: 0,
level: ["xx---x", "------", "-xx---", "-e-x-x", "x-----", "-xox--"]
},
{
initialDepth: 6,
minDepth: 3,
maxDepth: 6,
level: [
"-------x",
"-x----xo",
"--------",
"---x----",
"x---x---",
"--x----x",
"--e--xxx"
]
},
{
initialDepth: 3,
minDepth: 1,
maxDepth: 3,
level: [
"x--x--x",
"-x---x-",
"---x---",
"---x--o",
"x------",
"-xx--ex",
"----x-x"
]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 2,
level: [
"x-x--x--x",
"-x--x--x-",
"-----x---",
"--x--x---",
"x--------",
"-xx-x--ex",
"----x-x-x",
"xxo-x-x-x",
"------x-x"
]
},
{
initialDepth: 6,
minDepth: 5,
maxDepth: 6,
level: [
"x-x--x--x",
"-x--x--x-",
"---o-x---",
"--x--x---",
"x--------",
"-xx-x---x",
"----x-e-x",
"xx--x-x-x",
"------x-x"
]
},
{
initialDepth: 2,
minDepth: 2,
maxDepth: 2,
level: ["-------", "--x----", "--oe---", "-------"]
},
{
initialDepth: 6,
minDepth: 6,
maxDepth: 6,
level: [
"---e----",
"--------",
"--x----x",
"--x-x---",
"----o---",
"--x-----",
"----x---"
]
},
{
initialDepth: 5,
minDepth: 5,
maxDepth: 5,
level: [
"xxx--x",
"xx---x",
"x--x-x",
"x---ex",
"x-x--x",
"xx---x",
"xo-xxx"
]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 4,
level: ["-----", "-----", "-l-e-", "-o---", "-----"]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 2,
level: ["-l---", "l--e-", "--l--", "--ol-", "---l-"]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 2,
level: ["xxxl-x", "xxxl-x", "xlll-x", "x---ex", "xo-lxx", "x--lxx"]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 2,
level: ["--l--", "--l--", "-l-e-", "-o-l-", "---l-"]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 3,
level: [
"l---lll",
"l-----l",
"l-lll-l",
"l-lel-l",
"l-l-l-l",
"l-l-l-l",
"lol---l",
"l-lllll"
]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 3,
level: [
"l---lll",
"l-l---l",
"l-lll-l",
"l-e-l-l",
"l-l-l-l",
"lol---l",
"l-lllll"
]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 2,
level: ["l---lll", "l-l---l", "l----ll", "l-l---l", "lol-lel", "l-lllll"]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 4,
level: [
"l-ll---ll",
"l----l--l",
"l-l----ll",
"l-l--l--l",
"l-l-ll--l",
"lo--le--l",
"l---lllll"
]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 2,
level: ["l-l----ll", "lo--l--el", "l------ll"]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 4,
level: [
"l-l-lllll",
"l-----lll",
"l---l--ll",
"l--lll-ll",
"l-o--l-el",
"l---ll-ll"
]
},
{
initialDepth: 6,
minDepth: 6,
maxDepth: 6,
level: [
"xxxxxlll",
"oxe---xl",
"--lx---l",
"x---ll--",
"x-------",
"xxxlllll"
]
},
{
initialDepth: 3,
minDepth: 2,
maxDepth: 3,
level: ["x---x-ol", "-x----xl", "--l--l--", "x-e-----", "xxxlllll"]
},
{
initialDepth: 2,
minDepth: 1,
maxDepth: 2,
level: ["llllll", "xe-xxx", "ll--ll", "xxx--x", "llll-o", "xxxxx-"]
},
{
initialDepth: 2,
minDepth: 1,
maxDepth: 2,
level: [
"llllllll",
"xx----xx",
"ll-ll-ll",
"xxexx-xx",
"llll---o",
"xxxxxxx-"
]
},
{
initialDepth: 2,
minDepth: 0,
maxDepth: 4,
level: [
"le--------l",
"x-llll---xx",
"l--lll-l-ll",
"xx-----x-xx",
"llll-l-l--o",
"xxxxxlllxx-"
]
},
{
initialDepth: 8,
minDepth: 8,
maxDepth: 8,
level: [
"lx------elx",
"ll--xxlllll",
"ll----lllll",
"llxxx-xxxxl",
"xl-------xx",
"l--ll-ll-ll",
"xx-----x-xx",
"ll----xl--o",
"xxxxxlllxx-"
]
},
{
initialDepth: 0,
minDepth: 0,
maxDepth: 2,
level: ["-e-", "hhv", "-o-"]
},
{
initialDepth: 0,
minDepth: 0,
maxDepth: 2,
level: ["hhv--", "-ev--", "hhv--", "-o---"]
},
{
initialDepth: 0,
minDepth: 0,
maxDepth: 2,
level: ["hhvvv", "hevhh", "hhhvv", "-ohhv"]
},
{
initialDepth: 0,
minDepth: 0,
maxDepth: 2,
level: ["hvhhvvv", "hevhhhh", "vvhhhvv", "-hhohhv"]
},
{
initialDepth: 0,
minDepth: 0,
maxDepth: 2,
level: ["e-hh-", "vhvhv", "-l-hl", "hvhvh", "--o--"]
},
{
initialDepth: 1,
minDepth: 0,
maxDepth: 1,
level: ["hhvheh", "hvhvhv", "vvvvvv", "hhhohh", "------"]
},
{
initialDepth: 1,
minDepth: 0,
maxDepth: 2,
level: ["xxllhev", "xvx--lv", "xvh--lv", "xh-l-hh", "x--llo-", "xvhh--l"]
},
{
initialDepth: 0,
minDepth: 0,
maxDepth: 2,
level: ["hvhh", "vvvh", "vhvh", "-ove"]
},
{
initialDepth: 1,
minDepth: 0,
maxDepth: 1,
level: ["vhhvlh", "vv-hhh", "vhvllv", "-ohlev"]
},
{
initialDepth: 2,
minDepth: 0,
maxDepth: 2,
level: [
"vh--hhxh",
"vv-hl--h",
"vlv--llv",
"-oh--lev",
"vv-hl--h",
"vv-hh--h"
]
},
{
initialDepth: 1,
minDepth: 1,
maxDepth: 1,
level: ["xlohhev"]
}
];
let rotation = 0;
let rotationPosition = 0;
let depth = 1;
let initialPosition = [];
let position = [];
let end = [];
let rocks = [];
let startTimer;
const getLevelInfo = () => {
const currentLevel = puzzles[level];
const rawWidth = puzzles[level].level[0].length;
const rawHeight = puzzles[level].level.length;
return {
rawWidth,
rawHeight,
levelWidth: rawWidth - 1,
levelHeight: rawHeight - 1,
currentLevel
};
};
const changeLevelStyle = () => {
const { rawWidth, rawHeight, currentLevel } = getLevelInfo();
game.style.transform = `rotate(${90 * rotation}deg)`;
water.classList = `game__water game__water--${rotationPosition}`;
water.style = `--depth: ${depth};`;
waterLevel.style = `--depth: ${depth}; --width: ${
rotationPosition % 2 === 0 ? rawWidth : rawHeight
}; --height: ${
rotationPosition % 2 === 0 ? rawHeight : rawWidth
}; --min-depth: ${currentLevel.minDepth}; --max-depth: ${
currentLevel.maxDepth
};`;
};
const topFunction = () => changeWater(true);
const leftFunction = () => rotateLevel(false);
const rightFunction = () => rotateLevel(true);
const bottomFunction = () => changeWater(false);
const registerArrows = () => {
topArrow.addEventListener("click", topFunction);
leftArrow.addEventListener("click", leftFunction);
rightArrow.addEventListener("click", rightFunction);
bottomArrow.addEventListener("click", bottomFunction);
};
const removeArrowsEvents = () => {
topArrow.removeEventListener("click", topFunction);
leftArrow.removeEventListener("click", leftFunction);
rightArrow.removeEventListener("click", rightFunction);
bottomArrow.removeEventListener("click", bottomFunction);
};
const buildLevel = () => {
if (level === 0) {
startTimer = Date.now();
}
const {
rawWidth,
rawHeight,
levelWidth,
levelHeight,
currentLevel
} = getLevelInfo();
// Adapt cell size to level
document.body.style = `--cell: ${width / rawWidth}rem`;
// Reset rocks
rocks = [];
currentLevel.level.forEach((row, rowIndex) => {
const cells = row.split("");
cells.forEach((cell, cellIndex) => {
const newDiv = document.createElement("div");
newDiv.classList.add("game__cell");
if (cell === "o") {
initialPosition = [cellIndex, rowIndex];
position = [cellIndex, rowIndex];
game.appendChild(newDiv);
} else if (cell === "x" || cell === "l") {
rocks.push([cellIndex, rowIndex, cell === "x" ? "rock" : "lava"]);
const rockClone = rock.cloneNode(true);
rockClone.id = "";
if (cell === "l") {
rockClone.classList.add("game__cell--rock-lava");
}
const xFactor =
(cellIndex > levelWidth / 2 ? levelWidth - cellIndex : cellIndex) + 1;
const yFactor =
(rowIndex > levelHeight / 2 ? levelHeight - rowIndex : rowIndex) + 1;
rockClone.style.zIndex = Math.round(xFactor * yFactor) + 3;
game.appendChild(rockClone);
} else if (cell === "h") {
rocks.push([cellIndex, rowIndex, "horizontal"]);
newDiv.classList.add("game__cell--horizontal");
game.appendChild(newDiv);
} else if (cell === "v") {
rocks.push([cellIndex, rowIndex, "vertical"]);
newDiv.classList.add("game__cell--vertical");
game.appendChild(newDiv);
} else if (cell === "e") {
end = [cellIndex, rowIndex];
newDiv.classList.add("game__cell--end");
game.appendChild(newDiv);
} else {
game.appendChild(newDiv);
}
});
});
rotation = 0;
rotationPosition = 0;
levelText.innerHTML = level;
depth = currentLevel.initialDepth;
game.appendChild(water);
changeLevelStyle();
game.appendChild(player);
};
const positionPlayer = () => {
player.style = `--positionLeft: ${position[0]}; --positionTop: ${
position[1]
}; --rotation: rotate(${-90 * rotation}deg)`;
};
const removeEvent = (e) => {
e.preventDefault();
e.stopPropagation();
};
const winLevel = () => {
overText.classList.add("over-text--display");
const winTexts = [
"Well done! Next",
"Ducktastic!",
"You win!",
"Amazing!",
"Go go duck!",
"You've got this!",
"Keep going!",
"Stay duck!",
"Rotating to the next level...",
"Best duck!",
"You rock, duck!"
];
overText.innerHTML = winTexts[Math.floor(Math.random() * winTexts.length)];
document.addEventListener("keydown", removeEvent);
removeArrowsEvents();
setTimeout(() => {
game.innerHTML = "";
overText.classList.remove("over-text--display");
level = level + 1;
localStorage.setItem("ducky-fog", level);
const maxLevel = localStorage.getItem("ducky-fog-max", level);
localStorage.setItem("ducky-fog-max", maxLevel > level ? maxLevel : level);
buildLevel();
positionPlayer();
document.removeEventListener("keydown", removeEvent);
registerArrows();
}, 500);
};
const loseLevel = () => {
overText.classList.add("over-text--display");
overText.classList.add("over-text--lose");
const loseTexts = [
"You lose! Repeat",
"Oh no",
"Uuuh",
"Better duck next time",
"You died",
"Mamma mia",
"Looosing",
"That hurted",
"Roasted duck!",
"Burned",
"Toasted"
];
overText.innerHTML = loseTexts[Math.floor(Math.random() * loseTexts.length)];
document.addEventListener("keydown", removeEvent);
removeArrowsEvents();
setTimeout(() => {
game.innerHTML = "";
overText.classList.remove("over-text--display");
overText.classList.remove("over-text--lose");
buildLevel();
positionPlayer();
document.removeEventListener("keydown", removeEvent);
registerArrows();
}, 500);
};
const nextLevel = () => {
if (position[0] === end[0] && position[1] === end[1]) {
if (level === puzzles.length - 1) {
// If this was the last level: win the game
overText.classList.add("over-text--display");
overText.classList.add("over-text--win");
if (startTimer) {
const s = (Date.now() - startTimer) / 1000;
overText.innerHTML = `The end!<br/><span class="over-text__timer">${parseInt(
s / 60 / 60
)}h ${parseInt((s / 60) % 60)}m ${parseInt(s % 60)}s</span>`;
} else {
overText.innerHTML = "The end!";
}
document.addEventListener("keydown", removeEvent);
removeArrowsEvents();
} else {
winLevel();
}
}
};
const findRocks = (axis, perpendicularAxis, movingForward) => {
const relevantRocks = rocks
.filter((rock) => rock[axis] === position[axis]) // Filtering rocks in this axis
.filter((rock) =>
movingForward
? rock[perpendicularAxis] > position[perpendicularAxis]
: rock[perpendicularAxis] < position[perpendicularAxis]
); // Filtering rocks that are in the desired direction
const filteredRelevantRocks = relevantRocks.filter((rock) => {
if (axis === 0 && rock[2] !== "vertical") {
return rock;
} else if (axis === 1 && rock[2] !== "horizontal") {
return rock;
}
});
if (filteredRelevantRocks.length) {
// If there are rocks in this path
const rockPosition = movingForward
? Math.min(
...filteredRelevantRocks.map((rock) => rock[perpendicularAxis])
)
: Math.max(
...filteredRelevantRocks.map((rock) => rock[perpendicularAxis])
);
const relevantRock = filteredRelevantRocks.filter(
(rock) => rock[perpendicularAxis] === rockPosition
)[0];
const maxmin = movingForward ? rockPosition - 1 : rockPosition + 1;
return { maxmin, rockPosition, relevantRock };
}
return { maxmin: undefined, rockPostion: undefined, relevantRock: undefined };
};
// true = right, false = left
const rotateLevel = (direction) => {
const { levelWidth, levelHeight } = getLevelInfo();
const y1 = position[1];
const y2 = levelHeight - y1;
const x1 = position[0];
const x2 = levelWidth - x1;
const next = [...position];
const maxminCondition = (position, maxmin, next) => {
return (
maxmin !== undefined &&
((position >= maxmin && maxmin >= next) ||
(position <= maxmin && maxmin <= next))
);
};
if (rotationPosition === 0) {
const next = direction ? levelWidth - depth : depth;
const { maxmin, rockPosition, relevantRock } = findRocks(
1,
0,
next - position[0] > 0
);
if (maxminCondition(position[0], maxmin, next)) {
if (relevantRock[2] === "lava" && next !== maxmin) {
position[0] = rockPosition;
loseLevel();
} else {
position[0] = maxmin;
}
} else {
position[0] = next;
}
} else if (rotationPosition === 1) {
const next = direction ? depth : levelHeight - depth;
const { maxmin, rockPosition, relevantRock } = findRocks(
0,
1,
next - position[1] > 0
);
if (maxminCondition(position[1], maxmin, next)) {
if (relevantRock[2] === "lava" && next !== maxmin) {
position[1] = rockPosition;
loseLevel();
} else {
position[1] = maxmin;
}
} else {
position[1] = next;
}
} else if (rotationPosition === 2) {
const next = direction ? depth : levelWidth - depth;
const { maxmin, rockPosition, relevantRock } = findRocks(
1,
0,
next - position[0] > 0
);
if (maxminCondition(position[0], maxmin, next)) {
if (relevantRock[2] === "lava" && next !== maxmin) {
position[0] = rockPosition;
loseLevel();
} else {
position[0] = maxmin;
}
} else {
position[0] = next;
}
} else {
const next = direction ? levelHeight - depth : depth;
const { maxmin, rockPosition, relevantRock } = findRocks(
0,
1,
next - position[1] > 0
);
if (maxminCondition(position[1], maxmin, next)) {
if (relevantRock[2] === "lava" && next !== maxmin) {
position[1] = rockPosition;
loseLevel();
} else {
position[1] = maxmin;
}
} else {
position[1] = next;
}
}
if (direction) {
rotationPosition = (rotationPosition + 1) % 4;
} else {
rotationPosition = rotationPosition === 0 ? 3 : rotationPosition - 1;
}
waterLevel.classList.add("game__water-level--hidden");
setTimeout(
() => waterLevel.classList.remove("game__water-level--hidden"),
300
);
rotation = direction ? rotation + 1 : rotation - 1;
changeLevelStyle();
positionPlayer();
nextLevel();
};
// true = top, false = bottom
const changeWater = (direction) => {
const { levelWidth, levelHeight, currentLevel } = getLevelInfo();
// Avoid change if depth is max or min
if (
(direction && depth === currentLevel.maxDepth) ||
(!direction && depth === currentLevel.minDepth)
) {
return;
}
const y1 = position[1];
const y2 = levelHeight - y1;
const x1 = position[0];
const x2 = levelWidth - x1;
let playerOutWater = false;
// Avoid change if depth doesnt match player
playerOutWater =
(rotationPosition === 0 && depth !== y2) ||
(rotationPosition === 1 && depth !== x2) ||
(rotationPosition === 2 && depth !== y1) ||
(rotationPosition === 3 && depth !== x1);
depth = direction ? depth + 1 : depth - 1;
changeLevelStyle();
if (playerOutWater) {
return;
}
const next = [...position];
if (rotationPosition === 0) {
next[1] = direction ? position[1] - 1 : position[1] + 1;
} else if (rotationPosition === 1) {
next[0] = direction ? position[0] - 1 : position[0] + 1;
} else if (rotationPosition === 2) {
next[1] = direction ? position[1] + 1 : position[1] - 1;
} else {
next[0] = direction ? position[0] + 1 : position[0] - 1;
}
const relevantRock = rocks.find(
(rock) => rock[0] === next[0] && rock[1] === next[1]
);
if (!relevantRock) {
position = next;
} else if (relevantRock[2] === "lava") {
position = next;
loseLevel();
} else if (
rotationPosition % 2 === 0
? relevantRock[2] === "vertical"
: relevantRock[2] === "horizontal"
) {
position = next;
}
positionPlayer();
nextLevel();
};
const removeTutorial = () => {
overText.classList.remove("over-text--display");
overText.classList.remove("over-text--tutorial");
};
const buildLevelSelector = () => {
const maxLevel = localStorage.getItem("ducky-fog-max", level) ?? level;
puzzles.forEach((puzzle, index) => {
const newButton = document.createElement("div");
newButton.innerHTML = index;
newButton.classList.add("level-selector__button");
if (index === level) {
newButton.classList.add("level-selector__button--current");
}
if (index <= maxLevel) {
newButton.addEventListener("click", () => {
level = index;
localStorage.setItem("ducky-fog", level);
levelSelector.classList.remove("level-selector--active");
game.innerHTML = "";
buildLevel();
positionPlayer();
});
} else {
newButton.classList.add("level-selector__button--disabled");
}
levelSelector.appendChild(newButton);
});
levelSelector.classList.add("level-selector--active");
};
document.addEventListener(
"DOMContentLoaded",
() => {
const storedLevel = localStorage.getItem("ducky-fog");
if (storedLevel) {
level = Number(storedLevel);
buildLevelSelector();
} else {
overText.classList.add("over-text--display");
overText.classList.add("over-text--tutorial");
overText.addEventListener("click", removeTutorial);
overText.innerHTML =
"Use arrow keys or buttons to rotate and control water";
}
// Init
buildLevel();
positionPlayer();
registerArrows();
},
false
);
intro.addEventListener("click", () => {
intro.classList.add("intro--hidden");
setTimeout(() => {
removeTutorial();
overText.removeEventListener("click", removeTutorial);
}, 4000);
});
refresh.addEventListener("click", () => {
game.innerHTML = "";
buildLevel();
positionPlayer();
});
fullscreen.addEventListener("click", () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else if (document.exitFullscreen) {
document.exitFullscreen();
}
});
window.addEventListener("keydown", (event) => {
if (event.key === "ArrowRight") {
rotateLevel(true);
} else if (event.key === "ArrowLeft") {
rotateLevel(false);
} else if (event.key === "ArrowDown") {
changeWater(false);
} else if (event.key === "ArrowUp") {
changeWater(true);
}
});
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.