OispaHallaV2TaiJtn / src / Main.svelte
Main.svelte
Raw
<script lang="ts">
    import { onMount } from 'svelte';
    import { fade, blur, fly, slide, scale } from "svelte/transition";
    import { flip } from "svelte/animate";
    import { quadInOut } from "svelte/easing";

    export let engine;

    // import components
    import RestartButton from './components/RestartButton.svelte';
    import Controls from './components/Controls.svelte';
    import GridBackground from './components/GridBackground.svelte';
    import Analyzer from './components/Analyzer.svelte';
    import type Tile from './components/tile.ts';

    // Game variables
    const start_board = '[[{"x":0,"y":0,"value":2,"merged":false,"id":1},{"x":1,"y":0,"value":0,"merged":false,"id":2},{"x":2,"y":0,"value":2,"merged":false,"id":3},{"x":3,"y":0,"value":0,"merged":false,"id":4},null,null],[{"x":0,"y":1,"value":0,"merged":false,"id":5},{"x":1,"y":1,"value":0,"merged":false,"id":6},{"x":2,"y":1,"value":0,"merged":false,"id":7},{"x":3,"y":1,"value":0,"merged":false,"id":8},null,null],[{"x":0,"y":2,"value":0,"merged":false,"id":9},{"x":1,"y":2,"value":0,"merged":false,"id":10},{"x":2,"y":2,"value":0,"merged":false,"id":11},{"x":3,"y":2,"value":0,"merged":false,"id":12},null,null],[{"x":0,"y":3,"value":0,"merged":false,"id":13},{"x":1,"y":3,"value":0,"merged":false,"id":14},{"x":2,"y":3,"value":0,"merged":false,"id":15},{"x":3,"y":3,"value":0,"merged":false,"id":16},null,null],[null,null,null,null,null,null],[null,null,null,null,null,null]]';
    const start_board_3x3 = '[[{"x":0,"y":0,"value":2,"merged":false,"id":1},{"x":1,"y":0,"value":0,"merged":false,"id":2},{"x":2,"y":0,"value":2,"merged":false,"id":3},null,null,null],[{"x":0,"y":1,"value":0,"merged":false,"id":4},{"x":1,"y":1,"value":0,"merged":false,"id":5},{"x":2,"y":1,"value":0,"merged":false,"id":6},null,null,null],[{"x":0,"y":2,"value":0,"merged":false,"id":7},{"x":1,"y":2,"value":0,"merged":false,"id":8},{"x":2,"y":2,"value":0,"merged":false,"id":9},null,null,null],[null,null,null,null,null,null],[null,null,null,null,null,null],[null,null,null,null,null,null]]'
    let score = 0;
    let hiscore = 0;
    $: if(score > hiscore){hiscore = score};
    let width = 4;
    let height = 4;
    let width_max = 6;
    let height_max = 6;
    let board: Array<Array<Tile>> = JSON.parse(start_board);

    function restart(size){
        width = size;
        height = size;
        board = Array(width_max).fill(null).map(() => Array(height_max).fill(null));
        for(let x = 0; x < width_max; x++){
            for(let y = 0; y < height_max; y++){
                board[x][y] = {x: x, y: y, value: 0, merged: false, id: x + y * width_max + 99};
            }
        }
        board = JSON.parse( engine.add_random(JSON.stringify([width, height, board])) );
        board = JSON.parse( engine.add_random(JSON.stringify([width, height, board])) );
        score = 0;
    }
    let game_over = false;
    function move(direction: number){
      try{
          let result = JSON.parse(engine.apply_move(JSON.stringify([width, height, board]), direction, true));
          let newboard = result[0];
          let possible = result[1];
          let scoreGain = result[2];
          switch(direction){
            case 0: // Up
                latestEvent = "up";
                latestX = 0;
                latestY =-1;
                break;
            case 1: // Right
                latestEvent = "right";
                latestX = 1;
                latestY = 0;
                break;
            case 2: // Down
                latestEvent = "down";
                latestX = 0;
                latestY = 1;
                break;
            case 3: // Left
                latestEvent = "left";
                latestX =-1;
                latestY = 0;
                break;
            default:
                break;
          }
          if(possible === true){
              board = newboard;
              score += scoreGain;
          }
          let nextMovesPossible = 0;
          // Check if game is over
          for(let dir = 0; dir < 4; dir++){
              try{
                let next_result = JSON.parse(engine.apply_move(JSON.stringify([width, height, board]), dir, true));
                console.log(next_result);
                if(next_result[1] == true){
                    nextMovesPossible += 1;
                    console.log("Move to " + dir + " possible!");
                }
              }
              catch(e){
                  console.log("Error trying to check possible moves: ", e);
              }
          }
          console.log(nextMovesPossible);
          if(nextMovesPossible < 1){
              game_over = true;
              alert("Game Over!");
          }
      }
      catch(e){
          console.log("E", e);
      }
    }
    let latestEvent = "";
    let latestX = 0;
    let latestY = 0;
    function handleKeydown(event){
        let key = event.key;
        switch(key){
            case "ArrowUp":
            case "w":
                move(0);
                break;
            case "ArrowDown":
            case "s":
                move(2);
                break;
            case "ArrowLeft":
            case "a":
                move(3);
                break;
            case "ArrowRight":
            case "d":
                move(1);
                break;
            default:
                break;
        }
    }
    let packName = "OispaHalla";
    let packSubtitle = "Yhdistä opettajat ja saavuta Halla!";
    let imagePackPath = "../img";
    let usingCustomPack = false;
    $: usingCustomPack = imagePackPath !== "../img";
    let theme = 0;
    const IMAGENAME_MAX = 15;
    let imageNames = [];
    $: imageNames = Array.from(Array(IMAGENAME_MAX).keys()).map(i => `${imagePackPath}/theme-${theme}/${2**(i+1)}.png`);
    // $: if(theme != null){
    //   imageNames = [];
    //   for(let i = 1; i < IMAGENAME_MAX; i++){
    //       imageNames.push(`../img/theme-${theme}/${Math.pow(2, i)}.png`);
    //   }
    //   console.log("imageNames", imageNames);
    // }

    let devmode = true;

    onMount(async () => {
      if(window.location.href.includes("?pack=")){
        try{
          let pack = window.location.href.split("?pack=")[1];
          imagePackPath = atob(pack);
          fetch(imagePackPath + "/manifest.json").then(res => res.json()).then(res => {
            packName = res.name;
            packSubtitle = res.subtitle;
          })
        }
        catch(e){
          alert("Invalid pack! (Error in devconsole)");
          console.log("Invalid pack!", e)
        }
      }
    });
</script>

<svelte:head>
    <title>Oispa Halla</title>

    <link rel="manifest" href="./manifest.json">
    <link rel="icon" type="image/png" href="./favicon.png"/>
  
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
  
    <meta name="HandheldFriendly" content="True">
    <meta name="MobileOptimized" content="320">
    <meta name="viewport" content="width=device-width, initial-scale=1">
  
    <meta name="author" content="Hallabois">
    <meta name="description" content="Oispa Halla: Yhdistä opettajat ja saavuta Halla!">
    <meta name="keywords" content="Oispahalla, halla, ksyk, 2048, Lauri Halla, hallabois, oispa pelit">
  
    <meta name="twitter:card" content="Yhdistä opettajat ja saavuta Halla!">
    <meta name="theme-color" content="#000000"/>
    <script>window.sa_event=window.sa_event||function(){var a=[].slice.call(arguments);window.sa_event.q?window.sa_event.q.push(a):window.sa_event.q=[a]};</script>
</svelte:head>

<svelte:window on:keydown={handleKeydown}/>

<main>
	<noscript>
        <div class="noscript">
          <p>
            <strong>JavaScript ei ole päällä tai selaimesi ei tue sitä.</strong>
            <br>
            Sivuston toiminta edellyttää JavaScriptin toimintaa.
          </p>
        </div>
      </noscript>
      <div class="container">
        <div class="new-above-game">
          <div class="above-game-left">
            <a href="https://hallabois.github.io/invite/" target="_blank">
              <h1 class="title">{packName}</h1>
            </a>
            <p class="game-intro">{packSubtitle}</p>
          </div>
          <div class="above-game-right">
            <div class="HAC-container" title="HAC:n tila" style="display:none;">
              <div class="HAC-status">...</div>
            </div>
            <div class="score-container">{score}</div>
            <div class="best-container">{hiscore}</div>
            <RestartButton callback={restart} />
          </div>
        </div>
        <!-- <div class="heading">
          <a href="https://discord.gg/7x25Jxrkvr" target="_blank">
            <h1 class="title">Oispa Halla</h1>
          </a>
          <div class="scores-container">
              <div class="HAC-container" title="HAC:n tila" style="display:none;">
              <div class="HAC-status">...</div>
            </div>
            <div class="score-container">0</div>
            <div class="best-container">0</div>
          </div>
        </div>
    
        <div class="above-game">
          <p class="game-intro">Yhdistä opettajat ja saavuta <strong>Halla!</strong></p>
          <a class="restart-button">
            <div class="uusi-jakso">Uusi Jakso</div>
            <div class="size-selector">
              <button>&lt;</button>
              <button class="restart-3x3">3x3</button>
              <button class="restart-4x4">4x4</button>
            </div>
          </a>
        </div> -->
        <Controls callback={move} />
        <p>Latest move: {latestEvent}, X: {latestX}, Y: {latestY}</p>
        <div class="game-container" style="--grid-size: {Math.min(width, height)};">
          <div class="kurin-palautus-viesti"></div>
          <div class="game-message">
            <p class="tilanne"></p>
            <p class="kurinpalautukset"></p>
            <div class="lower">
                <a class="keep-playing-button">Jatka pelaamista</a>
              <a class="retry-button">Yritä uudelleen</a>
            </div>
          </div>
    
          <GridBackground game_width={width_max} game_height={height_max} />
    
          <div class="tile-container">
            {#if board}
                {#each board.flat().filter(t => t != null) as tile, key (tile.id)}
                        <div class="tile tile-position-{tile.x+1}-{tile.y+1} tile-{tile.value}" style="--x:{tile.x+1};--y:{tile.y+1};--value:{tile.value == 0 ? "" : Math.log2(tile.value)};" out:scale in:fly="{{ y: latestY*-20, x: latestX*-20, duration: 99, easing: quadInOut }}" animate:flip="{{duration: 99, easing: quadInOut}}">
                            <!-- {tile.value == 0 ? "" : tile.value} -->
                            <!-- {#if tile && tile.value && tile.value != 0} -->
                                <div class="tile-inner" style="--img-path:url({tile.value == 0 ? "" : imageNames[Math.log2(tile.value)-1]});"></div> <!-- Why do we need a logarithm? Good question. -->
                            <!-- {/if} -->
                        </div>
                {/each} <!-- && board.flat().filter(t2 => t2 && t2.id == t.id).length < 2 -->
            {/if}
          </div>
        </div>
        <div class="underbar-container">
          <div class="kurin-palautus-container">
            <button class="kurin-palautus kurin-palautus-color">
              <a class="parin-kulautus" title="Vai parin kulautus? Lahjot opettajia pois ruudulta, mutta menetät arvosanojasi! Voit lahjoa opettajia vain kolme kertaa ennen kun Halla saa kuulla tilanteesta.">KURINPALAUTUS</a>
            </button>
          </div>
          <div class="button-container">
            <div class="event-container">
              <a class="event-button" title="Toggle Hallatunturin Joulu">
                <img src="https://d33wubrfki0l68.cloudfront.net/96e5314f432b063a6e5f1078466585810b92d733/50db0/img/no_snow.svg" id="event-icon">
              </a>
            </div>
          </div>
        </div>
        <div class="pwa-container" style="width: 100%;z-index: 500;display: flex;justify-content: center;margin-top: 30px;margin-bottom: -50px;">
          <button class="pwa-add-button" style="display: none;border: none;margin: .5em;cursor: pointer;">Asenna sovelluksena</button>
        </div>
        <div class="disclaimer">
          <p>
            <strong>HUOMIO:</strong> Pelin lista opettajista on tehty täysin sattumanvaraisesti, eikä opettajia ole laitettu minkäänlaiseen paremmuusjärjestykseen. Rakastamme kaikkia opettajia sekä arvostamme kaikkien heidän työtänsä yhtä paljon ❤️.
          </p>
          <p>
            Alkuperäisen projektin <a href="https://github.com/gabrielecirulli/2048" target="_blank">2048</a> on tehnyt <a href="http://gabrielecirulli.com" target="_blank">Gabriele Cirulli.</a>
          </p>
          <p>
            <a href="https://simpleanalytics.com/oispahalla.com" target="_blank">Simpleanalytics</a>
          </p>
        </div>
        {#if devmode}
          <div class="dev">
            <p>
              <strong>DEV MODE</strong>
            </p>
            <label for="dev_theme">Theme:</label>
            <input id="dev_theme" type="number" min="0" bind:value={theme} />
          </div>
        {/if}
        <div class="preload-container">
          {#each imageNames as imageName}
            <img src="{usingCustomPack ? "" : "./build/"}{imageName}" alt="" />
          {/each}
        </div>
      </div>
      <!-- <Analyzer engine={engine} /> -->
</main>

<style lang="scss">
    @import "style/helpers";
    div.container {
        width: var(--field-width);
        margin: 0 auto;
        padding: 40px 0;
    }
    h1.title {
        font-size: 45px;
        font-weight: bold;
        margin: 0;
        display: block;
        float: left;
        transition: 0.25s color;
    }
    .new-above-game{
        display: -webkit-flex;
        display: flex;
        gap: 1em;
    }
    .above-game-left{
        display: -webkit-flex;
        display: flex;
        flex-direction: column;
        flex: 1;
    }
    .above-game-right{
        display: -webkit-flex;
        display: flex;
        flex-direction: row;
        gap: .5em;
        flex: 1;
        flex-wrap: wrap;
        align-content: end;
    }
    .game-intro {
        float: left;
        line-height: 42px;
        margin-bottom: 0;
        font-size: smaller;
    }
    .scores-container {
        float: right;
        text-align: right;
        margin-bottom: 20px;
        min-width: 20px;
        max-width: 50%;
    }
    .score-container, .best-container {
        $height: 25px;

        position: relative;
        background: var(--game-container-background);
        padding: 15px 20px;
        font-size: $height - 5px;
        height: $height;
        line-height: $height + 22px;
        font-weight: bold;
        border-radius: 3px;
        color: white;
        text-align: center;
        flex: 1; // mf's browsers don't support flex: content???
        min-width: 50px;

        &:after {
            position: absolute;
            width: 100%;
            top: 10px;
            left: 0;
            text-transform: uppercase;
            font-size: 13px;
            line-height: 13px;
            text-align: center;
            color: var(--tile-color);
        }

        .score-addition {
            position: absolute;
            right: 30px;
            color: red;
            font-size: $height;
            line-height: $height;
            font-weight: bold;
            color: rgba(var(--text-color), .9);
            z-index: 100;
            @include animation(move-up 600ms ease-in);
            @include animation-fill-mode(both);
        }
    }
    .score-container:after {
        content: "Arvosana";
    }

    .best-container:after {
        content: "Paras Halla";
    }

    .game-container{
      --tile-size: calc(calc(var(--field-width) - calc(var(--grid-gap) * calc(var(--grid-size) + 1))) / var(--grid-size));
    }

    .game-container .kurin-palautus-viesti {
        width: 94%;
        position: absolute;
        top: 25px;

        display: -webkit-flex;
        display: flex;
        justify-content: center;

        z-index: 1000;
        pointer-events: none;

        & img {
            width: 300px;

            @include animation(fade-in-out 4000ms ease-out);
            @include animation-fill-mode(both);
        }
    }
    .underbar-container {
        display: -webkit-flex;
        display: flex;
        width: 100%;

        > * {
            margin-top: 15px;
        }

        .button-container {
            > * {
                display: -webkit-flex;
                display: flex;
                width: 50%;
                
                img {
                    width: 25px;
                }
            }
        }
    }
    .kurin-palautus-container {
        justify-content: center;
        flex: 1; // mf's browsers don't support flex: content???
        display: -webkit-flex;
        display: flex;
        text-align: center;
        margin-left: 25px;
        min-height: 30px;
    }

    .kurin-palautus {
        padding: 0 5px;
        background-color: #c00;
        cursor: pointer;
        width: 25%;
        border-radius: 3px;
        transition: 1s background, 1s color;
        display: -webkit-flex;
        display: flex;
        align-items: center;
        justify-content: center; 
        border: var(--game-background) 1px solid;

        &:hover {
            border: var(--game-background-dark) 1px solid;
        }
    }


    .HAC-container {
        position: relative;
        background: #bbada0;
        padding: 15px 15px;
        font-size: 25px;
        height: 25px;
        line-height: 47px;
        font-weight: bold;
        border-radius: 3px;
        color: white;
        text-align: center;
    }
    .HAC-container::after{
        content: "HAC";
        position: absolute;
        width: 100%;
        top: 10px;
        left: 0;
        text-transform: uppercase;
        font-size: 13px;
        line-height: 13px;
        text-align: center;
        color: #eee4da;
    }
    .HAC-status{
        cursor: default;
    }

    .parin-kulautus {
        text-decoration: none;
        color: #eee;
        font-size: 11px;
        transition: 1s background, 1s color;
    }

    .disclaimer {
        margin-top: 60px;
        font-size: small;
        text-align: center;
    }

    .disclaimer p {
        margin-bottom: 20px;
    }

    .discord {
        margin-top: 40px;
        font-size: small;
        text-align: center;
    }
    @import "style/game-field";
    @include game-field;

    $kiillot: (128: (-4px, #a8fa8a), 
          256: (-4px, #FFFB62), 
          512: (-3px, #ffa600), 
          1024: (-1px, #5000ff), 
          2048: (0px, #FF4646), 
          4096: (0px, #FF9B00), 
          8192: (0px, #00FF13), 
          16384: (1px, #FFC600), 
          32768: (2px, #0B00FF));

    .tile .tile-inner{
        background-size: cover;
    }
    .tile {
        position: absolute;
        -webkit-transition: 100ms ease-in-out;
        -moz-transition: 100ms ease-in-out;
        transition: 100ms ease-in-out;
        transition-property: all;
        -webkit-transition-property: -webkit-transform;
        -moz-transition-property: -moz-transform;
        transition-property: transform;
    }
</style>