~starkingdoms/starkingdoms

92cc6c3c2850dd7cd59021aba2df3ccc963dad87 — core 1 year, 11 months ago d434e6c
client save editor
M starkingdoms-client/package.json => starkingdoms-client/package.json +1 -0
@@ 26,6 26,7 @@
    "vite": "^5.0.0"
  },
  "dependencies": {
    "@msgpack/msgpack": "^3.0.0-beta2",
    "debug": "^4.3.4",
    "pixi.js": "^7.3.2"
  }

M starkingdoms-client/src/components/ui/Button.svelte => starkingdoms-client/src/components/ui/Button.svelte +22 -4
@@ 4,25 4,43 @@
  export let id: string = "";
  export let disabled = false;
  export let style = "";
  export let variant = "normal";
</script>

<button {id} class="btn {clazz}" {disabled} on:click on:focus {style}>
<button {id} class="btn-{variant} {clazz}" {disabled} on:click on:focus {style}>
  <slot />
</button>

<style lang="scss">
  .btn {
  %standard {
    appearance: none;
    background: transparent;
    color: var(--text);
    padding: 0.675em 1em;
    border-radius: 0.25rem;
    cursor: text;
    border: 2px solid var(--links);
    transition: 0.1s ease-in-out;
  }
  .btn:hover {

  %standard-hover {
    cursor: pointer;
  }

  .btn-normal {
    @extend %standard;
    border: 2px solid var(--links);
  }
  .btn-normal:hover {
    @extend %standard-hover;
    background-color: var(--links-transparent);
  }

  .btn-danger {
    @extend %standard;
    border: 2px solid var(--error);
  }
  .btn-danger:hover {
    @extend %standard-hover;
    background-color: var(--error-transparent);
  }
</style>

M starkingdoms-client/src/css/themes/catppuccin-mocha.scss => starkingdoms-client/src/css/themes/catppuccin-mocha.scss +1 -0
@@ 53,6 53,7 @@
  --success: rgb(var(--green));
  --warning: rgb(var(--yellow));
  --error: rgb(var(--red));
  --error-transparent: rgba(var(--red), 0.35);
  --tag: rgb(var(--blue));
  --pill: rgb(var(--blue));
  --sel-bg: rgba(var(--surface2), 0.4);

M starkingdoms-client/src/pages/Home.svelte => starkingdoms-client/src/pages/Home.svelte +8 -6
@@ 117,12 117,14 @@
  </form>
</Popup>

<span
  style="position: absolute; right: 0.5rem; top: 0.5rem; background-color: var(--bg-secondary-1);">
  <a href="/shipeditor/" style="color: var(--text)">
    <Button>Ship Editor</Button>
  </a>
</span>
{#if window.localStorage.getItem("save") !== null}
  <span
    style="position: absolute; right: 0.5rem; top: 0.5rem; background-color: var(--bg-secondary-1);">
    <a href="/shipeditor/" style="color: var(--text)">
      <Button>Ship Editor</Button>
    </a>
  </span>
{/if}

<span class="footer-left">
  StarKingdoms Client {APP_VERSION} ({COMMIT_HASH})

M starkingdoms-client/src/pages/ShipEditor.svelte => starkingdoms-client/src/pages/ShipEditor.svelte +312 -25
@@ 3,14 3,13 @@
  import createDebug from "debug";
  import "../css/themes/catppuccin-mocha.scss";
  import "../css/style.scss";
  import { parseJwt } from "../jwt.ts";
  import HeartIcon from "../icons/HeartIcon.svelte";
  import Popup from "../components/ui/Popup.svelte";
  import { unpack_save } from "../save.ts";
  import { PartType } from "../protocol.ts";
  import { onMount } from "svelte";
  import { part_texture_url } from "../textures.ts";
  import Button from "../components/ui/Button.svelte";
  import TextInput from "../components/ui/TextInput.svelte";
  import PasswordInput from "../components/ui/PasswordInput.svelte";
  import tex_hearty from "../assets/hearty.svg";
  import tex_hub from "../assets/hub_off.svg";

  let config = DEFAULT_CONFIG;
  // Top-level await. Sets the default config, and overwrites it when the new config is avail. Thanks reactivity!


@@ 23,29 22,290 @@
    `Hello, world! StarKingdoms ${APP_VERSION} (${COMMIT_HASH}) at your service!`,
  );
  logger("Current view: ShipEditor.svelte");

  let canvas: HTMLCanvasElement;

  let x = window.innerWidth / 2 - 24;
  let y = window.innerHeight / 2 - 24;

  let grid_x = 0;
  let grid_y = 0;

  let scale = 1.0;

  $: grid_size = 48 * scale;

  function handleWheel(e: WheelEvent) {
    let delta = e.shiftKey ? 0.01 : 0.1;
    if (e.deltaY < 0) {
      scale += delta;
    } else {
      scale -= delta;
    }
    if (scale > 5) {
      scale = 5;
    } else if (scale < 0.1) {
      scale = 0.1;
    }
  }

  let isdragging = false;

  let last_cx = -1;
  let last_cy = -1;

  function mousedown(e: MouseEvent) {
    isdragging = true;
    last_cx = e.clientX;
    last_cy = e.clientY;
  }
  function mouseup(e) {
    isdragging = false;
  }

  function mousemove(e: MouseEvent) {
    if (isdragging) {
      if (last_cx != -1) {
        x += e.clientX - last_cx;
      }
      last_cx = e.clientX;
      if (last_cy != -1) {
        y += e.clientY - last_cy;
      }
      last_cy = e.clientY;
    } else {
      grid_x = Math.round((e.clientX - x) / grid_size - 0.5);
      grid_y = Math.round((e.clientY - y) / grid_size - 0.5);
    }
  }

  function goto(nx: number, ny: number) {
    x = nx + (window.innerWidth / 2 - 24);
    y = ny + (window.innerHeight / 2 - 24);
  }

  if (window.localStorage.getItem("save") === null) {
    window.location.href = "/";
  }

  let save = unpack_save(window.localStorage.getItem("save")!);
  console.log(save);

  let grid: Map<number, Map<number, PartType>> = new Map();

  function placePart(x: number, y: number, part: PartType) {
    if (!grid.has(x)) {
      grid.set(x, new Map());
    }

    grid.get(x)?.set(y, part);
  }

  let textures: Map<PartType, HTMLImageElement> = new Map();
  let context: CanvasRenderingContext2D | null = null;

  function render(
    ctx: CanvasRenderingContext2D | null,
    x: number,
    y: number,
    scale: number,
    grid: Map<number, Map<number, PartType>>,
    grid_x: number,
    grid_y: number,
  ) {
    if (ctx === null) {
      return;
    }

    // clear out the canvas
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    grid.forEach((row, x_coord) => {
      row.forEach(async (part_type, y_coord) => {
        // draw the image
        if (!textures.has(part_type)) {
          let promise = new Promise((resolve, reject) => {
            let img = new Image(grid_size, grid_size);
            img.onload = () => resolve(img);
            img.onerror = reject;
            img.src = part_texture_url(part_type, true);
          });

          let img = await promise;

          textures.set(part_type, <HTMLImageElement>img);
        }

        let img = textures.get(part_type)!;

        let canvas_x = x_coord * grid_size + x;
        let canvas_y = y_coord * grid_size + y;

        ctx.drawImage(img, canvas_x, canvas_y, grid_size, grid_size);
      });
    });

    ctx.beginPath();
    ctx.strokeStyle = "rgb(166, 227, 161)";
    ctx.lineWidth = 2;
    ctx.strokeRect(
      grid_x * grid_size + x,
      grid_y * grid_size + y,
      grid_size,
      grid_size,
    );
    ctx.stroke();
  }

  $: {
    render(context, x, y, scale, grid, grid_x, grid_y);
  }

  onMount(() => {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    context = canvas.getContext("2d");
    render(context, x, y, scale, grid, grid_x, grid_y);
  });

  window.onresize = () => {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
  };

  let confirm_reload_save = false;
  let confirm_save = false;

  let part_counts: Map<PartType, { used: number; available: number }> =
    new Map();

  function place(x: number, y: number, data: any) {
    if (data === null) {
      return;
    }

    let [type, children] = data;

    placePart(x, y, type);

    let existing_part_count =
      part_counts.get(<PartType>type) === undefined
        ? { used: 0, available: 0 }
        : part_counts.get(<PartType>type)!;

    part_counts.set(<PartType>type, {
      used: existing_part_count.used + 1,
      available: existing_part_count.available + 1,
    });

    if (children[0] !== null) {
      place(x, y + 1, children[0]);
    }
    if (children[1] !== null) {
      place(x + 1, y, children[1]);
    }
    if (children[2] !== null) {
      place(x, y - 1, children[2]);
    }
    if (children[3] !== null) {
      place(x - 1, y, children[3]);
    }
  }

  function load_save_data(save: any) {
    grid = new Map();
    place(0, 0, [PartType.Hearty, save[0]]);
  }

  $: {
    load_save_data(save);
  }

  function reload_btn() {
    if (confirm_reload_save) {
      save = unpack_save(window.localStorage.getItem("save")!);
    }
    confirm_reload_save = !confirm_reload_save;
    setTimeout(() => {
      confirm_reload_save = false;
    }, 5000);
  }

  function save_btn() {
    if (confirm_save) {
      // todo: @ghostly you need to turn this back into a savefile and then call pack_partial on it
      window.location.href = "/";
    }
    confirm_save = !confirm_save;
    setTimeout(() => {
      confirm_save = false;
    }, 5000);
  }
</script>

<span
  style="position: absolute; left: 0px; top: 0px; min-width: 20px; height: 100%; width: 250px; background-color: var(--bg-secondary-2)">
  <ul
    style="list-style-type: none; width: 100%; padding: 0px; display: inline-block;
                text-align: center; font-size: 48px; margin-block-start: 0px; margin-block-end: 0px;">
    <li style="padding-top: 20px;">
      <img src={tex_hearty} style="width: 50%" />
    </li>
    <li style="padding-top: 20px; vertical-align: middle; text-align: center">
      <span
        style="position: relative; display: inline-block; height: 2em; line-height: 2em">
        2
      </span>
      <img src={tex_hub} style="width: 2em" />
    </li>
  </ul>
</span>
<span
  style="position: absolute; bottom: 0px; right: 0px; width: calc(100% - 250px); height: 20%; background-color: var(--bg-secondary-1)">
<Popup title="Parts" id="parts" draggable minimizable style="width: 15em;">
  {#each part_counts.entries() as [type, counts]}
    {#if type !== PartType.Hearty}
      <div>
        <img
          style="vertical-align: middle;"
          src={part_texture_url(type, true)}
          width="24"
          height="24"
          alt={type} />
        <span>{type} - {counts.used} used of {counts.available}</span>
      </div>
    {/if}
  {/each}
</Popup>

<Popup
  title="Tools"
  id="tools"
  draggable
  minimizable
  style="width: min-content;">
  <Button
    style="width: 16em; margin-top: 5px;"
    on:click={() => {
      goto(0, 0);
      scale = 1.0;
    }}>
    Reset Viewport
  </Button>
  <Button
    on:click={reload_btn}
    variant="danger"
    style="min-width: 16em; margin-top: 5px;">
    {#if confirm_reload_save}
      Are you sure? (again to confirm, resets in 5s)
    {:else}
      Reload Save
    {/if}
  </Button>
  <Button
    on:click={save_btn}
    variant="danger"
    style="min-width: 16em; margin-top: 5px;">
    {#if confirm_save}
      Are you sure? Overwrites old save! (again to confirm, resets in 5s)
    {:else}
      Save Changes
    {/if}
  </Button>
</Popup>

<canvas
  on:mousedown|preventDefault={mousedown}
  on:mouseup|preventDefault={mouseup}
  on:mousemove|preventDefault={mousemove}
  on:wheel|preventDefault={handleWheel}
  style="background-size: {grid_size}px {grid_size}px; background-position: {x}px {y}px;"
  bind:this={canvas} />

<span style="position: absolute; top: 10px; right: 10px;">
  ({grid_x}, {grid_y}) {scale.toFixed(2)}x
</span>
<canvas id="shipeditor" />

<span class="footer-left">
  StarKingdoms Client {APP_VERSION} ({COMMIT_HASH})


@@ 53,3 313,30 @@
<span class="footer-right">
  Made with <HeartIcon class="footer-icon" /> by the StarKingdoms team
</span>

<style>
  :global(#tools) {
    position: absolute;
    bottom: 0.5em;
    left: 0.5em;
  }

  :global(#parts) {
    position: absolute;
    top: 0.5em;
    left: 0.5em;
  }

  canvas {
    width: 100vw;
    height: 100vh;
    position: absolute;
    top: 0;
    left: 0;
    padding: 0;
    margin: 0;

    background-image: linear-gradient(to right, #adadad20 1px, transparent 1px),
      linear-gradient(to bottom, #adadad20 1px, transparent 1px);
  }
</style>

A starkingdoms-client/src/save.ts => starkingdoms-client/src/save.ts +21 -0
@@ 0,0 1,21 @@
import { decode, encode } from "@msgpack/msgpack";

function base64ToBytes(base64: string) {
  const binString = atob(base64);
  // @ts-ignore
  return Uint8Array.from(binString, (m) => m.codePointAt(0));
}

function bytesToBase64(bytes: any) {
  const binString = String.fromCodePoint(...bytes);
  return btoa(binString);
}

export function unpack_save(data: string): any {
  // @ts-ignore
  return decode(decode(base64ToBytes(data))[0]);
}

export function pack_partial(data: any): string {
  return bytesToBase64(encode(data));
}

M starkingdoms-client/yarn.lock => starkingdoms-client/yarn.lock +5 -0
@@ 152,6 152,11 @@
    "@jridgewell/resolve-uri" "^3.1.0"
    "@jridgewell/sourcemap-codec" "^1.4.14"

"@msgpack/msgpack@^3.0.0-beta2":
  version "3.0.0-beta2"
  resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-3.0.0-beta2.tgz#5bccee30f84df220b33905e3d8249ba96deca0b7"
  integrity sha512-y+l1PNV0XDyY8sM3YtuMLK5vE3/hkfId+Do8pLo/OPxfxuFAUwcGz3oiiUuV46/aBpwTzZ+mRWVMtlSKbradhw==

"@nodelib/fs.scandir@2.1.5":
  version "2.1.5"
  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"