~starkingdoms/starkingdoms

25ff77753f74091ca29a5fbc4ca85a189b14857c — core 2 years ago 6425279
update drone DAG names, fmt pass to make prettier happy
M .drone.yml => .drone.yml +4 -4
@@ 9,7 9,7 @@ trigger:
      - rollback

steps:
  - name: client_formatting
  - name: client_fmt
    image: node
    commands:
      - cd starkingdoms-client


@@ 17,7 17,7 @@ steps:
      - yarn prettier . --check
  - name: client_build
    depends_on:
      - client_formatting
      - client_fmt
    image: node
    commands:
      - cd starkingdoms-client


@@ 42,7 42,7 @@ steps:
      target: builds/${DRONE_COMMIT_SHA}/client.tar.xz
      path_style: true

  - name: server_formatting
  - name: server_fmt
    image: coresdev/stk_build_env
    commands:
      - cd server


@@ 63,7 63,7 @@ steps:
  - name: server_clippy
    image: coresdev/stk_build_env
    depends_on:
      - server_formatting
      - server_fmt
    commands:
      - cd server
      - cargo clippy --color always

M starkingdoms-client/.prettierrc => starkingdoms-client/.prettierrc +1 -3
@@ 1,3 1,1 @@
{

}
{}

M starkingdoms-client/index.html => starkingdoms-client/index.html +114 -96
@@ 1,109 1,127 @@
<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8"/>
        <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
        <title>StarKingdoms</title>
    </head>
    <body class="bg-grid">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>StarKingdoms</title>
  </head>
  <body class="bg-grid">
    <div id="gamewindow">
      <!-- Canvas gets added here by the game script -->
    </div>

        <div id="gamewindow">
            <!-- Canvas gets added here by the game script -->
        </div>

        <div class="popup popup-center popup-max-width-300" id="server_selector">

            <h1>StarKingdoms</h1>
            <h2>Join Game</h2>
    <div class="popup popup-center popup-max-width-300" id="server_selector">
      <h1>StarKingdoms</h1>
      <h2>Join Game</h2>

            <form id="join-fm">
                <label>Choose server</label>
      <form id="join-fm">
        <label>Choose server</label>

                <div class="fm-select">
                    <button class="fm-select-button" role="combobox" aria-labelledby="server selector"
                            aria-haspopup="listbox"
                            aria-expanded="false" aria-controls="fm-select-dropdown">
                        <span class="fm-selected-value">Loading servers list</span>
                        <span class="fm-arrow"></span>
                    </button>
                    <ul class="fm-select-dropdown" role="listbox" id="fm-select-dropdown">
                        <!-- Filled by TS -->
                    </ul>
                </div>

                <label for="username" class="username-label">Username</label>
                <input class="username-box" id="username" required autocomplete="off"/>
                <button id="launch-btn" class="launch-btn">Launch!</button>
                <span id="server-danger" class="server-danger hidden">
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="server-danger-icon">
  <path fill-rule="evenodd"
        d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
        clip-rule="evenodd"/>
</svg>
Here be dragons! You have a <b>prerelease server</b> selected. Expect bugs, and save data on this server may be wiped at any time.
        </span>
            </form>
        <div class="fm-select">
          <button
            class="fm-select-button"
            role="combobox"
            aria-labelledby="server selector"
            aria-haspopup="listbox"
            aria-expanded="false"
            aria-controls="fm-select-dropdown"
          >
            <span class="fm-selected-value">Loading servers list</span>
            <span class="fm-arrow"></span>
          </button>
          <ul class="fm-select-dropdown" role="listbox" id="fm-select-dropdown">
            <!-- Filled by TS -->
          </ul>
        </div>

        <div class="popup popup-wmin log-hidden log-container popup-max-width-300" id="packet_log">
            <h1>Packet Log</h1>
            <table class="log">
                <thead>
                    <tr class="log-wfull">
                        <th class="log-wfull log-lalign log-header log-w50">#</th>
                        <th class="log-wfull log-lalign log-header">Dir</th>
                        <th class="log-wfull log-lalign log-header log-w240">Type</th>
                        <th class="log-wfull log-lalign log-header">Delta</th>
                    </tr>
                </thead>
                <tbody id="log_body">
        <label for="username" class="username-label">Username</label>
        <input class="username-box" id="username" required autocomplete="off" />
        <button id="launch-btn" class="launch-btn">Launch!</button>
        <span id="server-danger" class="server-danger hidden">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 20 20"
            fill="currentColor"
            class="server-danger-icon"
          >
            <path
              fill-rule="evenodd"
              d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
              clip-rule="evenodd"
            />
          </svg>
          Here be dragons! You have a <b>prerelease server</b> selected. Expect
          bugs, and save data on this server may be wiped at any time.
        </span>
      </form>
    </div>

                </tbody>
            </table>
            <hr>
            <h1>Packet Explorer</h1>
            <p id="explorer_selected" class="mono">Selected: --</p>
            <table class="mono json" id="explorer_json"></table>
        </div>
    <div
      class="popup popup-wmin log-hidden log-container popup-max-width-300"
      id="packet_log"
    >
      <h1>Packet Log</h1>
      <table class="log">
        <thead>
          <tr class="log-wfull">
            <th class="log-wfull log-lalign log-header log-w50">#</th>
            <th class="log-wfull log-lalign log-header">Dir</th>
            <th class="log-wfull log-lalign log-header log-w240">Type</th>
            <th class="log-wfull log-lalign log-header">Delta</th>
          </tr>
        </thead>
        <tbody id="log_body"></tbody>
      </table>
      <hr />
      <h1>Packet Explorer</h1>
      <p id="explorer_selected" class="mono">Selected: --</p>
      <table class="mono json" id="explorer_json"></table>
    </div>

        <div class="popup chat-container hidden" id="chat">
            <h1>Chat</h1>
            <div id="chatbox" class="chat-table mono">
                <!-- Filled by script -->
            </div>
            <input placeholder="Enter message or command here..." class="chat-box" id="chatentry" required autocomplete="off"/>
        </div>
    <div class="popup chat-container hidden" id="chat">
      <h1>Chat</h1>
      <div id="chatbox" class="chat-table mono">
        <!-- Filled by script -->
      </div>
      <input
        placeholder="Enter message or command here..."
        class="chat-box"
        id="chatentry"
        required
        autocomplete="off"
      />
    </div>

        <div class="hud hidden" id="hud">
            <div class="popup" id="hud-content-wrapper">  
                <table>
                    <thead>
                        <tr>
                            <th class="hud-d">Position</th>
                            <th class="hud-d">Velocity</th>
                            <th class="hud-d">Track Angle</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                          <td id="pos">
                              <span id="pos-val-x">--</span>, <span id="pos-val-y">--</span>
                          </td>
                          <td id="velocity">
                              <span id="velocity-val">--</span>
                          </td>
                          <td id="track">
                              <span id="track-val">--</span>
                          </td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    <div class="hud hidden" id="hud">
      <div class="popup" id="hud-content-wrapper">
        <table>
          <thead>
            <tr>
              <th class="hud-d">Position</th>
              <th class="hud-d">Velocity</th>
              <th class="hud-d">Track Angle</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td id="pos">
                <span id="pos-val-x">--</span>, <span id="pos-val-y">--</span>
              </td>
              <td id="velocity">
                <span id="velocity-val">--</span>
              </td>
              <td id="track">
                <span id="track-val">--</span>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

        <span class="footer-left" id="footer-left"></span>
        <span class="footer-right" id="footer-right"></span>
    <span class="footer-left" id="footer-left"></span>
    <span class="footer-right" id="footer-right"></span>

        <script type="module" src="src/main.ts"></script>
    </body>
    <script type="module" src="src/main.ts"></script>
  </body>
</html>

M starkingdoms-client/src/chat.ts => starkingdoms-client/src/chat.ts +5 -5
@@ 1,7 1,7 @@
export function addMessage(classname: string, message: string) {
    let p = document.createElement("p");
    p.innerText = message;
    p.classList.add("message");
    p.classList.add(classname);
    document.getElementById("chatbox")!.appendChild(p);
  let p = document.createElement("p");
  p.innerText = message;
  p.classList.add("message");
  p.classList.add(classname);
  document.getElementById("chatbox")!.appendChild(p);
}

M starkingdoms-client/src/config.json => starkingdoms-client/src/config.json +1 -1
@@ 33,4 33,4 @@
      "isPrimary": false
    }
  }
}
\ No newline at end of file
}

M starkingdoms-client/src/config.ts => starkingdoms-client/src/config.ts +29 -29
@@ 6,43 6,43 @@ const logger = createDebug("config");
const CONFIG_URL = "https://configuration.starkingdoms.io";

export interface Config {
    servers: {[id: string]: ConfigServer}
  servers: { [id: string]: ConfigServer };
}
export interface ConfigServer {
    name: string;
    clientHubUrl: string;
    apiBaseUrl: string;
    isProduction: boolean;
    isDevelopment: boolean;
    isPrimary: boolean;
  name: string;
  clientHubUrl: string;
  apiBaseUrl: string;
  isProduction: boolean;
  isDevelopment: boolean;
  isPrimary: boolean;
}

async function fetchWithTimeout(resource: RequestInfo | URL, options = {}) {
    // @ts-ignore
    const { timeout = 8000 } = options;
  // @ts-ignore
  const { timeout = 8000 } = options;

    const controller = new AbortController();
    const id = setTimeout(() => controller.abort(), timeout);
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout);

    const response = await fetch(resource, {
        ...options,
        signal: controller.signal
    });
    clearTimeout(id);
  const response = await fetch(resource, {
    ...options,
    signal: controller.signal,
  });
  clearTimeout(id);

    return response;
  return response;
}

export async function loadConfig(): Promise<Config> {
    logger("loading configuration from " + CONFIG_URL);
    try {
        const response = await fetchWithTimeout(CONFIG_URL, {
            timeout: 1000
        });
        return await response.json();
    } catch (e) {
        logger(`error loading configuration: ${e}, using fallback`);
        // @ts-ignore strong types are unhelpful here
        return CONFIG;
    }
}
\ No newline at end of file
  logger("loading configuration from " + CONFIG_URL);
  try {
    const response = await fetchWithTimeout(CONFIG_URL, {
      timeout: 1000,
    });
    return await response.json();
  } catch (e) {
    logger(`error loading configuration: ${e}, using fallback`);
    // @ts-ignore strong types are unhelpful here
    return CONFIG;
  }
}

M starkingdoms-client/src/css/chat.css => starkingdoms-client/src/css/chat.css +30 -30
@@ 1,48 1,48 @@
.chat-container {
    position: absolute;
    top: 0.5em;
    right: 0.5em;
    width: 30vw;
    height: min-content;
    font-size: 0.875rem; /* 14px */
    line-height: 1.25rem; /* 20px */
    font-weight: 500;
  position: absolute;
  top: 0.5em;
  right: 0.5em;
  width: 30vw;
  height: min-content;
  font-size: 0.875rem; /* 14px */
  line-height: 1.25rem; /* 20px */
  font-weight: 500;
}
.chat-table {
    height: 23vh;
    display: block;
    overflow: auto;
  height: 23vh;
  display: block;
  overflow: auto;
}
.chat-box {
    appearance: none;
    background: transparent;
    color: var(--text);
    padding: 0.675em 1em;
    border: 1px solid var(--links);
    border-radius: 0.25rem;
    cursor: text;
    width: 100%;
    max-width: 100%;
  appearance: none;
  background: transparent;
  color: var(--text);
  padding: 0.675em 1em;
  border: 1px solid var(--links);
  border-radius: 0.25rem;
  cursor: text;
  width: 100%;
  max-width: 100%;
}
.chat-box:focus {
    outline: none;
    background-color: var(--links-ultratransparent);
  outline: none;
  background-color: var(--links-ultratransparent);
}
.message {
    padding-top: 0;
    padding-bottom: 0;
    margin-top: 1px;
    margin-bottom: 1px;
  padding-top: 0;
  padding-bottom: 0;
  margin-top: 1px;
  margin-bottom: 1px;
}
.server-message {
    color: #facb61;
  color: #facb61;
}
.server-error {
    color: #ff2222;
  color: #ff2222;
}
.global-message {
    color: #4de640;
  color: #4de640;
}
.direct-message {
    color: #599fbd;
  color: #599fbd;
}

M starkingdoms-client/src/css/footer.css => starkingdoms-client/src/css/footer.css +14 -14
@@ 1,20 1,20 @@
.footer-left {
    font-size: 0.75rem;
    line-height: 1rem;
    position: absolute;
    bottom: 1em;
    left: 1em;
  font-size: 0.75rem;
  line-height: 1rem;
  position: absolute;
  bottom: 1em;
  left: 1em;
}
.footer-right {
    font-size: 0.75rem;
    line-height: 1rem;
    position: absolute;
    bottom: 1vh;
    right: 1vw;
  font-size: 0.75rem;
  line-height: 1rem;
  position: absolute;
  bottom: 1vh;
  right: 1vw;
}
.footer-icon {
    vertical-align: middle;
    display: inline-block;
    width: 1rem;
    height: 1rem;
  vertical-align: middle;
  display: inline-block;
  width: 1rem;
  height: 1rem;
}

M starkingdoms-client/src/css/form.css => starkingdoms-client/src/css/form.css +96 -94
@@ 1,156 1,158 @@
.launch-btn {
    appearance: none;
    width: 100%;
    padding: 1em;
    margin-top: 1em;
    color: var(--text);
    background: transparent;
    border: 2px solid var(--links);
    border-radius: 5px;
    transition: 0.1s ease-in-out;
  appearance: none;
  width: 100%;
  padding: 1em;
  margin-top: 1em;
  color: var(--text);
  background: transparent;
  border: 2px solid var(--links);
  border-radius: 5px;
  transition: 0.1s ease-in-out;
}
.launch-btn:hover {
    cursor: pointer;
    background-color: var(--links-transparent);
  cursor: pointer;
  background-color: var(--links-transparent);
}

.username-label {
    margin-top: 1em;
  margin-top: 1em;
}
.fm-select {
    margin-bottom: 1em;
    position: relative;
  margin-bottom: 1em;
  position: relative;
}

.username-box {
    appearance: none;
    background: transparent;
    color: var(--text);
    padding: 0.675em 1em;
    border: 1px solid var(--links);
    border-radius: 0.25rem;
    cursor: text;
    width: 100%;
    max-width: 100%;
  appearance: none;
  background: transparent;
  color: var(--text);
  padding: 0.675em 1em;
  border: 1px solid var(--links);
  border-radius: 0.25rem;
  cursor: text;
  width: 100%;
  max-width: 100%;
}
.username-box:focus {
    outline: none;
    background-color: var(--links-ultratransparent);
  outline: none;
  background-color: var(--links-ultratransparent);
}

.fm-select-button {
    appearance: none;
    background: transparent;
    color: var(--text);
    width: 100%;
    padding: 0.675em 1em;
    border: 1px solid var(--links);
    border-radius: 0.25rem;
    cursor: pointer;
    display: flex;
    justify-content: space-between;
    align-items: center;
  appearance: none;
  background: transparent;
  color: var(--text);
  width: 100%;
  padding: 0.675em 1em;
  border: 1px solid var(--links);
  border-radius: 0.25rem;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.fm-selected-value {
    text-align: left;
  text-align: left;
}
.fm-arrow {
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-top: 6px solid var(--links);
    transition: transform ease-in-out 0.3s;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 6px solid var(--links);
  transition: transform ease-in-out 0.3s;
}
.fm-select-dropdown {
    position: absolute;
    list-style: none;
    width: 100%;
    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
    background-color: var(--bg-secondary-1);
    border: 1px solid var(--links);
    border-radius: 4px;
    padding: 10px;
    margin-top: 10px;
    max-height: 200px;
    overflow-y: auto;
    transition: 0.2s ease;
  position: absolute;
  list-style: none;
  width: 100%;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
  background-color: var(--bg-secondary-1);
  border: 1px solid var(--links);
  border-radius: 4px;
  padding: 10px;
  margin-top: 10px;
  max-height: 200px;
  overflow-y: auto;
  transition: 0.2s ease;

    /*transform: scaleY(0);*/
    opacity: 0;
    visibility: hidden;
  /*transform: scaleY(0);*/
  opacity: 0;
  visibility: hidden;
}
.fm-select-dropdown:focus-within {
    box-shadow: 0 10px 25px var(--links-ultratransparent);
  box-shadow: 0 10px 25px var(--links-ultratransparent);
}

.fm-select-dropdown li {
    position: relative;
    cursor: pointer;
    display: flex;
    gap: 1rem;
    align-items: center;
  position: relative;
  cursor: pointer;
  display: flex;
  gap: 1rem;
  align-items: center;
}

.fm-select-dropdown li label {
    width: 100%;
    padding: 8px 10px;
    cursor: pointer;
  width: 100%;
  padding: 8px 10px;
  cursor: pointer;
}

.fm-select-dropdown::-webkit-scrollbar {
    width: 7px;
  width: 7px;
}
.fm-select-dropdown::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 25px;
  background: #f1f1f1;
  border-radius: 25px;
}

.fm-select-dropdown::-webkit-scrollbar-thumb {
    background: #ccc;
    border-radius: 25px;
  background: #ccc;
  border-radius: 25px;
}

.fm-select-dropdown li + li {
    margin-top: 5px;
  margin-top: 5px;
}

.fm-select-dropdown li, .fm-select-dropdown input ~ label {
    border-radius: 5px;
.fm-select-dropdown li,
.fm-select-dropdown input ~ label {
  border-radius: 5px;
}

.fm-select-dropdown li:hover, .fm-select-dropdown input:checked ~ label {
    background-color: var(--surface-0);
.fm-select-dropdown li:hover,
.fm-select-dropdown input:checked ~ label {
  background-color: var(--surface-0);
}
.fm-select-dropdown input:focus ~ label {
    background-color: var(--surface-1);
  background-color: var(--surface-1);
}

.fm-select-dropdown input[type="radio"] {
    position: absolute;
    left: 0;
    opacity: 0;
  position: absolute;
  left: 0;
  opacity: 0;
}
.fm-select.active .fm-arrow {
    transform: rotate(180deg);
  transform: rotate(180deg);
}
.fm-select.active .fm-select-dropdown {
    opacity: 1;
    visibility: visible;
    /*transform: scaleY(1);*/
  opacity: 1;
  visibility: visible;
  /*transform: scaleY(1);*/
}
.server-danger {
    width: 100%;
    display: block;
    height: max-content;
    margin-top: 1em;
    color: var(--error);
    font-size: 0.875rem;
    line-height: 1.25rem;
  width: 100%;
  display: block;
  height: max-content;
  margin-top: 1em;
  color: var(--error);
  font-size: 0.875rem;
  line-height: 1.25rem;
}
.server-danger.hidden {
    display: none;
  display: none;
}
.server-danger-icon {
    height: 1.25rem;
    vertical-align: middle;
    display: inline-block;
  height: 1.25rem;
  vertical-align: middle;
  display: inline-block;
}

M starkingdoms-client/src/css/game.css => starkingdoms-client/src/css/game.css +10 -10
@@ 1,11 1,11 @@
.game {
    width: 100vw;
    height: 100vh;
    margin: 0;
    padding: 0;
    position: fixed;
    left: 0;
    top: 0;
    z-index: -1;
    background: url("../assets/starfield.svg");
}
\ No newline at end of file
  width: 100vw;
  height: 100vh;
  margin: 0;
  padding: 0;
  position: fixed;
  left: 0;
  top: 0;
  z-index: -1;
  background: url("../assets/starfield.svg");
}

M starkingdoms-client/src/css/globals.css => starkingdoms-client/src/css/globals.css +31 -14
@@ 1,28 1,45 @@
html {
    box-sizing: border-box;
  box-sizing: border-box;
}
*, *:before, *:after {
    box-sizing: inherit;
*,
*:before,
*:after {
  box-sizing: inherit;
}

body {
    background-color: var(--bg);
    color: var(--body);
    /* Stolen from Tailwind. Looks good in most places. */
    font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
    font-size: 1rem;
    line-height: 1.5rem;
  background-color: var(--bg);
  color: var(--body);
  /* Stolen from Tailwind. Looks good in most places. */
  font-family:
    ui-sans-serif,
    system-ui,
    -apple-system,
    BlinkMacSystemFont,
    "Segoe UI",
    Roboto,
    "Helvetica Neue",
    Arial,
    "Noto Sans",
    sans-serif,
    "Apple Color Emoji",
    "Segoe UI Emoji",
    "Segoe UI Symbol",
    "Noto Color Emoji";
  font-size: 1rem;
  line-height: 1.5rem;
}

.mono {
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
    "Liberation Mono", "Courier New", monospace;
}

h1 {
    font-size: 1.25rem;
    line-height: 1.75rem;
  font-size: 1.25rem;
  line-height: 1.75rem;
}
h2 {
    font-size: 1.125rem;
    line-height: 1.75rem;
  font-size: 1.125rem;
  line-height: 1.75rem;
}

M starkingdoms-client/src/css/grid.css => starkingdoms-client/src/css/grid.css +3 -3
@@ 1,5 1,5 @@
.bg-grid {
    background-image: linear-gradient(to right, #80808012 1px, transparent 1px),
  background-image: linear-gradient(to right, #80808012 1px, transparent 1px),
    linear-gradient(to bottom, #80808012 1px, transparent 1px);
    background-size: 24px 24px;
}
\ No newline at end of file
  background-size: 24px 24px;
}

M starkingdoms-client/src/css/hud.css => starkingdoms-client/src/css/hud.css +11 -11
@@ 1,25 1,25 @@
.hud {
    position: absolute;
    bottom: .5em;
    left: .5em;
    width: calc(100% - 1em);
  position: absolute;
  bottom: 0.5em;
  left: 0.5em;
  width: calc(100% - 1em);
}

#hud-content-wrapper {
    width: min(100vw, 620px);
    margin: 0 auto;
  width: min(100vw, 620px);
  margin: 0 auto;
}

#hud-content-wrapper > table {
    width: 100%;
    table-layout: fixed;
  width: 100%;
  table-layout: fixed;
}

#hud-content-wrapper td {
    text-align: center;
  text-align: center;
}

.hud-d {
    margin-left: 5px;
    margin-right: 5px;
  margin-left: 5px;
  margin-right: 5px;
}

M starkingdoms-client/src/css/json.css => starkingdoms-client/src/css/json.css +10 -10
@@ 1,21 1,21 @@
:root {
    --ident: #75bfff;
    --string: #ff7de9;
    --literal: #86de74;
    --indent-spacing: 16px;
  --ident: #75bfff;
  --string: #ff7de9;
  --literal: #86de74;
  --indent-spacing: 16px;
}

.json-ident {
    color: var(--ident);
  color: var(--ident);
}
.json-string {
    color: var(--string);
  color: var(--string);
}
.json-literal {
    color: var(--literal);
  color: var(--literal);
}
.json {
    max-height: 200px;
    display: block;
    overflow: auto;
  max-height: 200px;
  display: block;
  overflow: auto;
}

M starkingdoms-client/src/css/log.css => starkingdoms-client/src/css/log.css +24 -24
@@ 1,55 1,55 @@
.log-icon {
    width: 16px
  width: 16px;
}
.log-lalign {
    text-align: left;
  text-align: left;
}
.log-item {
    cursor: pointer;
  cursor: pointer;
}
.log-item:hover {
    background-color: #4a4a4f;
  background-color: #4a4a4f;
}
.log {
    border: none;
    border-collapse: collapse;
    display: block;
    max-height: 200px;
    overflow: auto;
    width: 100%;
    table-layout: fixed;
  border: none;
  border-collapse: collapse;
  display: block;
  max-height: 200px;
  overflow: auto;
  width: 100%;
  table-layout: fixed;
}
.log-selected {
    background-color: #204e8a;
    cursor: default;
  background-color: #204e8a;
  cursor: default;
}
.log-leaving {
    color: var(--error);
  color: var(--error);
}
.log-arriving {
    color: var(--success);
  color: var(--success);
}
.log > tbody > tr > td {
    padding: 2px;
  padding: 2px;
}
.log-hidden {
    display: none;
  display: none;
}
#log_body {
    width: 100%;
  width: 100%;
}
.log-container {
    width: 350px;
  width: 350px;
}
.log-header {
    padding: 5px;
  padding: 5px;
}
.log-w240 {
    width: 240px;
  width: 240px;
}
.log-w50 {
    width: 50px;
  width: 50px;
}
.log-td {
    padding: 5px;
}
\ No newline at end of file
  padding: 5px;
}

M starkingdoms-client/src/css/popup.css => starkingdoms-client/src/css/popup.css +18 -18
@@ 1,35 1,35 @@
.popup {
    padding: 1em;
  padding: 1em;

    background-color: var(--bg-secondary-1);
  background-color: var(--bg-secondary-1);

    height: min-content;
  height: min-content;

    border-radius: 5px;
    z-index: 100000;
  border-radius: 5px;
  z-index: 100000;
}
.popup-max-width-300 {
    max-width: 300px;
  max-width: 300px;
}
.popup-wmin {
    width: min-content;
  width: min-content;
}
.popup-center {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    margin: auto;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  margin: auto;
}
.popup > h1 {
    margin: 0;
  margin: 0;
}
.popup > h2 {
    margin: 0 0 0.5em;
    color: var(--sub-headline);
  margin: 0 0 0.5em;
  color: var(--sub-headline);
}
.hidden {
    display: none;
    visibility: hidden;
  display: none;
  visibility: hidden;
}

M starkingdoms-client/src/css/style.css => starkingdoms-client/src/css/style.css +1 -1
@@ 7,4 7,4 @@
@import "log.css";
@import "game.css";
@import "hud.css";
@import "chat.css";
\ No newline at end of file
@import "chat.css";

M starkingdoms-client/src/css/themes/catppuccin-common/definitions.css => starkingdoms-client/src/css/themes/catppuccin-common/definitions.css +28 -28
@@ 2,34 2,34 @@
/* This is also a good reference if you want to make your own themes, for what variables you *must* define. */

:root {
    /* Background colors */
    --bg: rgb(var(--base));
    --bg-secondary-1: rgb(var(--crust));
    --bg-secondary-2: rgb(var(--mantle));
  /* Background colors */
  --bg: rgb(var(--base));
  --bg-secondary-1: rgb(var(--crust));
  --bg-secondary-2: rgb(var(--mantle));

    --surface-0: rgb(var(--surface0));
    --surface-1: rgb(var(--surface1));
    --surface-2: rgb(var(--surface2));
  --surface-0: rgb(var(--surface0));
  --surface-1: rgb(var(--surface1));
  --surface-2: rgb(var(--surface2));

    --overlay-0: rgb(var(--overlay0));
    --overlay-1: rgb(var(--overlay1));
    --overlay-2: rgb(var(--overlay2));
  --overlay-0: rgb(var(--overlay0));
  --overlay-1: rgb(var(--overlay1));
  --overlay-2: rgb(var(--overlay2));

    /* Typography */
    --body: rgb(var(--text));
    --headline: rgb(var(--text));
    --sub-headline: rgb(var(--subtext0));
    --label: rgb(var(--subtext0));
    --subtle: rgb(var(--overlay1));
    --links: rgb(var(--blue));
    --links-transparent: rgba(var(--blue), 0.35);
    --links-ultratransparent: rgba(var(--blue), 0.15);
    --success: rgb(var(--green));
    --warning: rgb(var(--yellow));
    --error: rgb(var(--red));
    --tag: rgb(var(--blue));
    --pill: rgb(var(--blue));
    --sel-bg: rgba(var(--surface2), 0.4);
    --cursor: rgb(var(--rosewater));
    --dm: rgb(var(--teal));
}
\ No newline at end of file
  /* Typography */
  --body: rgb(var(--text));
  --headline: rgb(var(--text));
  --sub-headline: rgb(var(--subtext0));
  --label: rgb(var(--subtext0));
  --subtle: rgb(var(--overlay1));
  --links: rgb(var(--blue));
  --links-transparent: rgba(var(--blue), 0.35);
  --links-ultratransparent: rgba(var(--blue), 0.15);
  --success: rgb(var(--green));
  --warning: rgb(var(--yellow));
  --error: rgb(var(--red));
  --tag: rgb(var(--blue));
  --pill: rgb(var(--blue));
  --sel-bg: rgba(var(--surface2), 0.4);
  --cursor: rgb(var(--rosewater));
  --dm: rgb(var(--teal));
}

M starkingdoms-client/src/css/themes/catppuccin-mocha/colors.css => starkingdoms-client/src/css/themes/catppuccin-mocha/colors.css +26 -27
@@ 2,31 2,30 @@
@import "../catppuccin-common/definitions.css";

:root {
    --rosewater: 245, 224, 220;
    --flamingo: 242, 205, 205;
    --pink: 245, 194, 231;
    --mauve: 203, 166, 247;
    --red: 243, 139, 169;
    --maroon: 235, 160, 172;
    --peach: 250, 179, 135;
    --yellow: 249, 226, 175;
    --green: 166, 227, 161;
    --teal: 148, 226, 213;
    --sky: 137, 220, 235;
    --sapphire: 116, 199, 236;
    --blue: 137, 180, 250;
    --lavender: 180, 190, 254;
    --text: 205, 214, 244;
    --subtext1: 186, 194, 222;
    --subtext0: 166, 173, 200;
    --overlay2: 147, 153, 178;
    --overlay1: 127, 132, 156;
    --overlay0: 108, 112, 134;
    --surface2: 88, 91, 112;
    --surface1: 69, 71, 90;
    --surface0: 49, 50, 68;
    --base: 30, 30, 46;
    --mantle: 24, 24, 37;
    --crust: 17, 17, 27;
  --rosewater: 245, 224, 220;
  --flamingo: 242, 205, 205;
  --pink: 245, 194, 231;
  --mauve: 203, 166, 247;
  --red: 243, 139, 169;
  --maroon: 235, 160, 172;
  --peach: 250, 179, 135;
  --yellow: 249, 226, 175;
  --green: 166, 227, 161;
  --teal: 148, 226, 213;
  --sky: 137, 220, 235;
  --sapphire: 116, 199, 236;
  --blue: 137, 180, 250;
  --lavender: 180, 190, 254;
  --text: 205, 214, 244;
  --subtext1: 186, 194, 222;
  --subtext0: 166, 173, 200;
  --overlay2: 147, 153, 178;
  --overlay1: 127, 132, 156;
  --overlay0: 108, 112, 134;
  --surface2: 88, 91, 112;
  --surface1: 69, 71, 90;
  --surface0: 49, 50, 68;
  --base: 30, 30, 46;
  --mantle: 24, 24, 37;
  --crust: 17, 17, 27;
}


M starkingdoms-client/src/env.d.ts => starkingdoms-client/src/env.d.ts +1 -1
@@ 1,2 1,2 @@
declare const APP_VERSION: string;
declare const COMMIT_HASH: string;
\ No newline at end of file
declare const COMMIT_HASH: string;

M starkingdoms-client/src/hub.ts => starkingdoms-client/src/hub.ts +195 -188
@@ 1,212 1,219 @@
import createDebug from "debug";
import {
    MessagePacket,
    MessageType,
    Packet,
    PacketType,
    PartPositionsPacket,
    PlanetPositionsPacket,
    PlayerLeavePacket,
    PlayerListPacket,
    SpawnPlayerPacket,
  MessagePacket,
  MessageType,
  Packet,
  PacketType,
  PartPositionsPacket,
  PlanetPositionsPacket,
  PlayerLeavePacket,
  PlayerListPacket,
  SpawnPlayerPacket,
} from "./protocol.ts";
import {appendPacket} from "./packet_ui.ts";
import {global} from "./main.ts";
import {startRender} from "./rendering.ts";
import {addMessage} from "./chat.ts";
import { appendPacket } from "./packet_ui.ts";
import { global } from "./main.ts";
import { startRender } from "./rendering.ts";
import { addMessage } from "./chat.ts";

const logger = createDebug("hub");

export interface ClientHub {
    socket: WebSocket;
  socket: WebSocket;
}

export function sendPacket(client: ClientHub, packet: Packet) {
    client.socket.send(JSON.stringify(packet));
    appendPacket(packet);
  client.socket.send(JSON.stringify(packet));
  appendPacket(packet);
}

export async function hub_connect(url: string, username: string): Promise<ClientHub | null> {
    logger("connecting to client hub at " + url)
export async function hub_connect(
  url: string,
  username: string,
): Promise<ClientHub | null> {
  logger("connecting to client hub at " + url);

    let ws  = new WebSocket(url);
  let ws = new WebSocket(url);

    ws.onerror = (e) => {
        console.error(e);
        throw e;
    }
  ws.onerror = (e) => {
    console.error(e);
    throw e;
  };

    ws.onopen = () => {
        logger("connected to client hub, sending username and auth details");
  ws.onopen = () => {
    logger("connected to client hub, sending username and auth details");

        let client: ClientHub = {
            socket: ws
        };
    let client: ClientHub = {
      socket: ws,
    };

        let packet: Packet = {
            t: PacketType.ClientLogin,
    let packet: Packet = {
      t: PacketType.ClientLogin,
      c: {
        username,
        jwt: null,
      },
    };
    sendPacket(client, packet);

    // input
    document.onkeydown = (e) => {
      // currently, input packet is sent on any key down. fix that
      if (e.key == "ArrowUp" || e.key == "w") {
        global.up = true;
      }
      if (e.key == "ArrowDown" || e.key == "s") {
        global.down = true;
      }
      if (e.key == "ArrowLeft" || e.key == "a") {
        global.left = true;
      }
      if (e.key == "ArrowRight" || e.key == "d") {
        global.right = true;
      }
      let input_packet: Packet = {
        t: PacketType.PlayerInput,
        c: {
          up: global.up,
          down: global.down,
          left: global.left,
          right: global.right,
        },
      };
      sendPacket(client, input_packet);
    };
    document.onkeyup = (e) => {
      if (e.key == "ArrowUp" || e.key == "w") {
        global.up = false;
      }
      if (e.key == "ArrowDown" || e.key == "s") {
        global.down = false;
      }
      if (e.key == "ArrowLeft" || e.key == "a") {
        global.left = false;
      }
      if (e.key == "ArrowRight" || e.key == "d") {
        global.right = false;
      }
      let input_packet: Packet = {
        t: PacketType.PlayerInput,
        c: {
          up: global.up,
          down: global.down,
          left: global.left,
          right: global.right,
        },
      };
      sendPacket(client, input_packet);
    };

    document.getElementById("chatentry")!.onkeydown = (e) => {
      if (e.key === "Enter") {
        let value = (<HTMLInputElement>document.getElementById("chatentry")!)
          .value;

        if (value.startsWith(".msg")) {
          let args = value.split(" ");
          if (args.length < 3) {
            addMessage("server-error", "Command error");

            (<HTMLInputElement>document.getElementById("chatentry")!).value =
              "";
            return;
          }
          let target = args[1];
          let message = args.slice(2).join(" ");
          let chat_packet: Packet = {
            t: PacketType.SendMessage,
            c: {
                username,
                jwt: null
            }
        };
        sendPacket(client, packet);

        // input
        document.onkeydown = (e) => {
            // currently, input packet is sent on any key down. fix that
            if(e.key == "ArrowUp" || e.key == "w") {
                global.up = true;
            }
            if(e.key == "ArrowDown" || e.key == "s") {
                global.down = true;
            }
            if(e.key == "ArrowLeft" || e.key == "a") {
                global.left = true;
            }
            if(e.key == "ArrowRight" || e.key == "d") {
                global.right = true;
            }
            let input_packet: Packet = {
                t: PacketType.PlayerInput,
                c: {
                    up: global.up,
                    down: global.down,
                    left: global.left,
                    right: global.right,
                }
            }
            sendPacket(client, input_packet);
        }
        document.onkeyup = (e) => {
            if(e.key == "ArrowUp" || e.key == "w") {
                global.up = false;
            }
            if(e.key == "ArrowDown" || e.key == "s") {
                global.down = false;
            }
            if(e.key == "ArrowLeft" || e.key == "a") {
                global.left = false;
            }
            if(e.key == "ArrowRight" || e.key ==  "d") {
                global.right = false;
            }
            let input_packet: Packet = {
                t: PacketType.PlayerInput,
                c: {
                    up: global.up,
                    down: global.down,
                    left: global.left,
                    right: global.right,
                }
            }
            sendPacket(client, input_packet);
              target: target,
              content: message,
            },
          };
          sendPacket(client, chat_packet);

          addMessage("direct-message", `you -> ${target}: ${message}`);
        } else {
          let chat_packet: Packet = {
            t: PacketType.SendMessage,
            c: {
              target: null,
              content: value,
            },
          };
          sendPacket(client, chat_packet);
        }
        (<HTMLInputElement>document.getElementById("chatentry")!).value = "";
      }
    };

        document.getElementById("chatentry")!.onkeydown = (e) => {
            if (e.key === 'Enter') {
                let value = (<HTMLInputElement>document.getElementById("chatentry")!).value;

                if (value.startsWith(".msg")) {
                    let args = value.split(" ");
                    if (args.length < 3) {
                        addMessage("server-error", "Command error");

                        (<HTMLInputElement>document.getElementById("chatentry")!).value = "";
                        return;
                    }
                    let target = args[1];
                    let message = args.slice(2).join(" ");
                    let chat_packet: Packet = {
                        t: PacketType.SendMessage,
                        c: {
                            target: target,
                            content: message
                        }
                    };
                    sendPacket(client, chat_packet);

                    addMessage("direct-message", `you -> ${target}: ${message}`);
                } else {
                    let chat_packet: Packet = {
                        t: PacketType.SendMessage,
                        c: {
                            target: null,
                            content: value
                        }
                    };
                    sendPacket(client, chat_packet);
                }
                (<HTMLInputElement>document.getElementById("chatentry")!).value = "";
            }
    ws.onmessage = (e) => {
      let packet: Packet = JSON.parse(e.data);

      appendPacket(packet);

      if (packet.t == PacketType.SpawnPlayer) {
        let p = <SpawnPlayerPacket>packet.c;
        if (p.username === username) {
          global.me = {
            username: p.username,
            part_id: p.id,
          };
          logger(`client spawned (username=${p.username} part_id=${p.id})`);
          startRender();
        } else {
          global.players_map.set(p.id, p.username);
          global.inverse_players_map.set(p.username, p.id);
          logger(`player joined (username=${p.username} part_id=${p.id})`);
        }

        ws.onmessage = (e) => {
            let packet: Packet = JSON.parse(e.data);

            appendPacket(packet);

            if (packet.t == PacketType.SpawnPlayer) {
                let p = <SpawnPlayerPacket> packet.c;
                if (p.username === username) {
                    global.me = {
                        username: p.username,
                        part_id: p.id
                    };
                    logger(`client spawned (username=${p.username} part_id=${p.id})`);
                    startRender();
                } else {
                    global.players_map.set(p.id, p.username);
                    global.inverse_players_map.set(p.username, p.id);
                    logger(`player joined (username=${p.username} part_id=${p.id})`);
                }
            } else if (packet.t == PacketType.PlayerList) {
                let p = <PlayerListPacket> packet.c;
                for (let i = 0; i < p.players.length; i++) {
                    global.players_map.set(p.players[i][0], p.players[i][1]);
                    global.inverse_players_map.set(p.players[i][1], p.players[i][0]);
                }
                logger(`added ${p.players.length} existing players to player list`);
            } else if (packet.t == PacketType.PlanetPositions) {
                let p = <PlanetPositionsPacket> packet.c;
                for (let i = 0; i < p.planets.length; i++) {
                    global.planets_map.set(p.planets[i][0], p.planets[i][1]);
                }
            } else if (packet.t == PacketType.PartPositions) {
                let p = <PartPositionsPacket> packet.c;
                for (let i = 0; i < p.parts.length; i++) {
                    global.parts_map.set(p.parts[i][0], p.parts[i][1]);
                }
            } else if (packet.t == PacketType.PlayerLeave) {
                let p = <PlayerLeavePacket>packet.c;
                let username = global.players_map.get(p.id)!;
                global.inverse_players_map.delete(username);
                global.players_map.delete(p.id);
                logger(`player removed (id=${p.id})`);
            } else if (packet.t == PacketType.Message) {
                let p = <MessagePacket>packet.c;
                logger(`message type=${p.message_type} actor=${p.actor} content=${p.content}`);

                if (p.message_type == MessageType.Server) {
                    addMessage("server-message", `[SERVER] ${p.content}`);
                } else if (p.message_type == MessageType.Chat) {
                    addMessage("global-message", `${p.actor}: ${p.content}`);
                } else if (p.message_type == MessageType.Direct) {
                    // actor is who sent the message. destination is not included in this packet
                    if (p.actor === global.me!.username) {
                        // skip (shown above)
                    } else {
                        addMessage("direct-message", `${p.actor} -> you: ${p.content}`);
                    }
                } else {
                    addMessage("server-error", `${p.content}`);
                }
            } else {
                logger(`unrecognized packet type ${packet.t}`);
            }
      } else if (packet.t == PacketType.PlayerList) {
        let p = <PlayerListPacket>packet.c;
        for (let i = 0; i < p.players.length; i++) {
          global.players_map.set(p.players[i][0], p.players[i][1]);
          global.inverse_players_map.set(p.players[i][1], p.players[i][0]);
        }

        return client;
        logger(`added ${p.players.length} existing players to player list`);
      } else if (packet.t == PacketType.PlanetPositions) {
        let p = <PlanetPositionsPacket>packet.c;
        for (let i = 0; i < p.planets.length; i++) {
          global.planets_map.set(p.planets[i][0], p.planets[i][1]);
        }
      } else if (packet.t == PacketType.PartPositions) {
        let p = <PartPositionsPacket>packet.c;
        for (let i = 0; i < p.parts.length; i++) {
          global.parts_map.set(p.parts[i][0], p.parts[i][1]);
        }
      } else if (packet.t == PacketType.PlayerLeave) {
        let p = <PlayerLeavePacket>packet.c;
        let username = global.players_map.get(p.id)!;
        global.inverse_players_map.delete(username);
        global.players_map.delete(p.id);
        logger(`player removed (id=${p.id})`);
      } else if (packet.t == PacketType.Message) {
        let p = <MessagePacket>packet.c;
        logger(
          `message type=${p.message_type} actor=${p.actor} content=${p.content}`,
        );

        if (p.message_type == MessageType.Server) {
          addMessage("server-message", `[SERVER] ${p.content}`);
        } else if (p.message_type == MessageType.Chat) {
          addMessage("global-message", `${p.actor}: ${p.content}`);
        } else if (p.message_type == MessageType.Direct) {
          // actor is who sent the message. destination is not included in this packet
          if (p.actor === global.me!.username) {
            // skip (shown above)
          } else {
            addMessage("direct-message", `${p.actor} -> you: ${p.content}`);
          }
        } else {
          addMessage("server-error", `${p.content}`);
        }
      } else {
        logger(`unrecognized packet type ${packet.t}`);
      }
    };
    return null;

    return client;
  };
  return null;
}

M starkingdoms-client/src/main.ts => starkingdoms-client/src/main.ts +120 -96
@@ 1,166 1,190 @@
import createDebug from "debug";
import {ClientHub, hub_connect} from "./hub.ts";
import {ConfigServer, loadConfig} from "./config.ts";
import { ClientHub, hub_connect } from "./hub.ts";
import { ConfigServer, loadConfig } from "./config.ts";
import "./css/style.css";
import "./css/themes/catppuccin-mocha/colors.css";
import {Part, Planet} from "./protocol.ts";
import { Part, Planet } from "./protocol.ts";

let config = await loadConfig();

const logger = createDebug("main");
logger(`Hello, world! StarKingdoms ${APP_VERSION} (${COMMIT_HASH}) at your service!`);
logger(
  `Hello, world! StarKingdoms ${APP_VERSION} (${COMMIT_HASH}) at your service!`,
);

if (window.localStorage.getItem("stk-packet-mode") === "debug") {
    document.getElementById("packet_log")!.classList.remove("log-hidden");
  document.getElementById("packet_log")!.classList.remove("log-hidden");
} else {
    document.getElementById("packet_log")!.remove();
  document.getElementById("packet_log")!.remove();
}

export interface GlobalData {
    client: ClientHub | null,
    me: GlobalMe | null,
  client: ClientHub | null;
  me: GlobalMe | null;

    players_map: Map<number, string>,
    inverse_players_map: Map<string, number>,
  players_map: Map<number, string>;
  inverse_players_map: Map<string, number>;

    planets_map: Map<number, Planet>,
  planets_map: Map<number, Planet>;

    parts_map: Map<number, Part>,
  parts_map: Map<number, Part>;

    up: boolean,
    down: boolean,
    left: boolean,
    right: boolean,
  up: boolean;
  down: boolean;
  left: boolean;
  right: boolean;

    rendering: GlobalRendering | null
  rendering: GlobalRendering | null;
}

export interface GlobalMe {
    username: string,
    part_id: number
  username: string;
  part_id: number;
}
export interface GlobalRendering {
    canvas: HTMLCanvasElement,
    ctx: CanvasRenderingContext2D
  canvas: HTMLCanvasElement;
  ctx: CanvasRenderingContext2D;
}

export const global: GlobalData = {
    client: null,
    me: null,
    players_map: new Map(),
    inverse_players_map: new Map(),
    planets_map: new Map(),
    parts_map: new Map(),
    up: false,
    down: false,
    left: false,
    right: false,
    rendering: null
}
  client: null,
  me: null,
  players_map: new Map(),
  inverse_players_map: new Map(),
  planets_map: new Map(),
  parts_map: new Map(),
  up: false,
  down: false,
  left: false,
  right: false,
  rendering: null,
};

export function player(): Part | undefined {
    if (global.me !== null) {
        return global.parts_map.get(global.me!.part_id);
    } else {
        return undefined;
    }
  if (global.me !== null) {
    return global.parts_map.get(global.me!.part_id);
  } else {
    return undefined;
  }
}

const version_string = `StarKingdoms Client ${APP_VERSION} (${COMMIT_HASH})`;
document.getElementById("footer-left")!.innerHTML = version_string;
document.getElementById("footer-right")!.innerHTML = `Made with <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="footer-icon"><path d="M9.653 16.915l-.005-.003-.019-.01a20.759 20.759 0 01-1.162-.682 22.045 22.045 0 01-2.582-1.9C4.045 12.733 2 10.352 2 7.5a4.5 4.5 0 018-2.828A4.5 4.5 0 0118 7.5c0 2.852-2.044 5.233-3.885 6.82a22.049 22.049 0 01-3.744 2.582l-.019.01-.005.003h-.002a.739.739 0 01-.69.001l-.002-.001z" /></svg> by the StarKingdoms team`;
document.getElementById("footer-right")!.innerHTML =
  `Made with <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="footer-icon"><path d="M9.653 16.915l-.005-.003-.019-.01a20.759 20.759 0 01-1.162-.682 22.045 22.045 0 01-2.582-1.9C4.045 12.733 2 10.352 2 7.5a4.5 4.5 0 018-2.828A4.5 4.5 0 0118 7.5c0 2.852-2.044 5.233-3.885 6.82a22.049 22.049 0 01-3.744 2.582l-.019.01-.005.003h-.002a.739.739 0 01-.69.001l-.002-.001z" /></svg> by the StarKingdoms team`;

// Dropdown stuff
const custom_select = document.querySelector(".fm-select")!;
const custom_select_btn = <HTMLButtonElement>document.querySelector(".fm-select-button")!;
const custom_select_btn = <HTMLButtonElement>(
  document.querySelector(".fm-select-button")!
);

custom_select_btn.onclick = (e) => {
    e.stopPropagation();
    e.preventDefault();
    custom_select.classList.toggle("active");
    custom_select_btn.setAttribute("aria-expanded", custom_select_btn.getAttribute("aria-expanded") === "true" ? "false" : "true");
  e.stopPropagation();
  e.preventDefault();
  custom_select.classList.toggle("active");
  custom_select_btn.setAttribute(
    "aria-expanded",
    custom_select_btn.getAttribute("aria-expanded") === "true"
      ? "false"
      : "true",
  );
};

const selected_value = document.querySelector(".fm-selected-value")!;

// Populate the main page server selector


let inverse_server_lookup: {[name: string]: string} = {};
let inverse_server_lookup: { [name: string]: string } = {};

const dropdown = document.getElementById("fm-select-dropdown")!;

dropdown.innerHTML = "";

for (let server_id in config.servers) {
    let server: ConfigServer = config.servers[server_id];
    if (server.isDevelopment && window.localStorage.getItem("stk-mode") !== "debug") {
        continue;
    }
    let html_text = `
  let server: ConfigServer = config.servers[server_id];
  if (
    server.isDevelopment &&
    window.localStorage.getItem("stk-mode") !== "debug"
  ) {
    continue;
  }
  let html_text = `
    <li>
        <input type="radio" id="${server_id}" name="server" autocomplete="off" ${server.isPrimary ? "checked" : ""} />
        <input type="radio" id="${server_id}" name="server" autocomplete="off" ${
          server.isPrimary ? "checked" : ""
        } />
        <label for="${server_id}">${server.name}</label>
    </li>
    `;
    inverse_server_lookup[server.name] = server_id;
    dropdown.innerHTML += html_text;
    if (server.isPrimary) {
        selected_value.textContent = server.name;
    }
  inverse_server_lookup[server.name] = server_id;
  dropdown.innerHTML += html_text;
  if (server.isPrimary) {
    selected_value.textContent = server.name;
  }
}

const options_list = document.querySelectorAll(".fm-select-dropdown li");
options_list.forEach((option) => {
    function handler(e: Event) {
        if (e.type === "click" && (<PointerEvent>e).clientX !== 0 && (<PointerEvent>e).clientY !== 0) {
            // @ts-ignore
            selected_value.textContent = this.children[1].textContent;
            custom_select.classList.remove("active");
        }
        if (e.type === "keyup") {
            console.log((<KeyboardEvent>e).target!);
            // @ts-ignore
            selected_value.textContent = this.textContent;
            custom_select.classList.remove("active");
        }
        if (!config.servers[inverse_server_lookup[selected_value.textContent!]].isProduction) {
            document.getElementById("server-danger")!.classList.remove("hidden");
        } else {
            document.getElementById("server-danger")!.classList.add("hidden");
        }
  function handler(e: Event) {
    if (
      e.type === "click" &&
      (<PointerEvent>e).clientX !== 0 &&
      (<PointerEvent>e).clientY !== 0
    ) {
      // @ts-ignore
      selected_value.textContent = this.children[1].textContent;
      custom_select.classList.remove("active");
    }
    if (e.type === "keyup") {
      console.log((<KeyboardEvent>e).target!);
      // @ts-ignore
      selected_value.textContent = this.textContent;
      custom_select.classList.remove("active");
    }
    if (
      !config.servers[inverse_server_lookup[selected_value.textContent!]]
        .isProduction
    ) {
      document.getElementById("server-danger")!.classList.remove("hidden");
    } else {
      document.getElementById("server-danger")!.classList.add("hidden");
    }
    // @ts-ignore
    option.onkeyup = handler;
    // @ts-ignore
    option.onclick = handler;
  }
  // @ts-ignore
  option.onkeyup = handler;
  // @ts-ignore
  option.onclick = handler;
});

function setStatus(stat: string) {
    document.getElementById("launch-btn")!.textContent = stat;
  document.getElementById("launch-btn")!.textContent = stat;
}

document.getElementById("join-fm")!.onsubmit = async (e) => {
    e.preventDefault();
  e.preventDefault();

    setStatus("Connecting...");
    (<HTMLButtonElement>custom_select_btn).disabled = true;
    (<HTMLInputElement>document.getElementById("username")!).disabled = true;
  setStatus("Connecting...");
  (<HTMLButtonElement>custom_select_btn).disabled = true;
  (<HTMLInputElement>document.getElementById("username")!).disabled = true;

    try {
        let server_name = selected_value.textContent!;
        let server_id = inverse_server_lookup[server_name];
        let server: ConfigServer = config.servers[server_id];
  try {
    let server_name = selected_value.textContent!;
    let server_id = inverse_server_lookup[server_name];
    let server: ConfigServer = config.servers[server_id];

        let username = (<HTMLInputElement>document.getElementById("username")!).value;
    let username = (<HTMLInputElement>document.getElementById("username")!)
      .value;

        logger(`connecting to ${server.clientHubUrl} as ${username} with auth = none`);
    logger(
      `connecting to ${server.clientHubUrl} as ${username} with auth = none`,
    );

        global.client = await hub_connect(server.clientHubUrl, username);
    } catch (e) {
        setStatus("Connection failed!");
        console.error(e);
        (<HTMLButtonElement>custom_select_btn).disabled = false;
        (<HTMLInputElement>document.getElementById("username")!).disabled = false;
    }
    global.client = await hub_connect(server.clientHubUrl, username);
  } catch (e) {
    setStatus("Connection failed!");
    console.error(e);
    (<HTMLButtonElement>custom_select_btn).disabled = false;
    (<HTMLInputElement>document.getElementById("username")!).disabled = false;
  }
};

M starkingdoms-client/src/packet_ui.ts => starkingdoms-client/src/packet_ui.ts +98 -80
@@ 1,5 1,5 @@
// @ts-ignore
import {Direction, Packet, type_direction} from "./protocol.ts";
import { Direction, Packet, type_direction } from "./protocol.ts";
import createDebug from "debug";

const logger = createDebug("jsonview");


@@ 7,40 7,47 @@ const logger = createDebug("jsonview");
let selected = document.getElementById("explorer_selected")!;
let table = document.getElementById("explorer_json")!;

export function show_packet(maybe_packet: Packet | null, sequence_number: number) {
    logger(`selected packet ${sequence_number}`);
    if (maybe_packet === null) {
        selected.textContent = "Selected: --";
        table.innerHTML = "";
        return;
    }
    let packet = maybe_packet!;
    let direction = type_direction(packet.t);
export function show_packet(
  maybe_packet: Packet | null,
  sequence_number: number,
) {
  logger(`selected packet ${sequence_number}`);
  if (maybe_packet === null) {
    selected.textContent = "Selected: --";
    table.innerHTML = "";
    return;
  }
  let packet = maybe_packet!;
  let direction = type_direction(packet.t);

    selected.textContent = `Selected: #${sequence_number} ${direction} ${packet.t}`;
  selected.textContent = `Selected: #${sequence_number} ${direction} ${packet.t}`;

    // iterate over everything and calculate a max depth
    let max_depth = depthOf(packet.c);
    logger(`indent depth ${max_depth}`);
  // iterate over everything and calculate a max depth
  let max_depth = depthOf(packet.c);
  logger(`indent depth ${max_depth}`);

    // generate a tree
    let tree = generateTree(packet.c, 0);
  // generate a tree
  let tree = generateTree(packet.c, 0);

    logger(`generating tree of ${tree.length} items`);
  logger(`generating tree of ${tree.length} items`);

    let table_html = "";
  let table_html = "";

    for (let i = 0; i < tree.length; i++) {
        let [indent, key, value, is_text] = tree[i];
        table_html += `
  for (let i = 0; i < tree.length; i++) {
    let [indent, key, value, is_text] = tree[i];
    table_html += `
        <tr>
            <td style="padding-left: calc(var(--indent-spacing) * ${indent})" class="json-ident">${key}</td>
            <td style="padding-left: calc(var(--indent-spacing) * ${max_depth + 1})" class="${is_text ? "json-string" : "json-literal"}">${value}</td>
            <td style="padding-left: calc(var(--indent-spacing) * ${
              max_depth + 1
            })" class="${
              is_text ? "json-string" : "json-literal"
            }">${value}</td>
        </tr>
        `
    }
        `;
  }

    table.innerHTML = `
  table.innerHTML = `
<!-- Rendered tree of ${tree.length} items -->
    <thead>
    <tr></tr>


@@ 53,57 60,64 @@ export function show_packet(maybe_packet: Packet | null, sequence_number: number
}

function depthOf(object: any) {
    let level = 0;
    for(let key in object) {
        if (!object.hasOwnProperty(key)) continue;
  let level = 0;
  for (let key in object) {
    if (!object.hasOwnProperty(key)) continue;

        if(typeof object[key] == 'object'){
            let depth = depthOf(object[key]) + 1;
            level = Math.max(depth, level);
        }
    if (typeof object[key] == "object") {
      let depth = depthOf(object[key]) + 1;
      level = Math.max(depth, level);
    }
    return level;
  }
  return level;
}


function generateTree(object: any, level: number): [indent: number, key: string, value: string, value_string: boolean][] {
    let items: [indent: number, key: string, value: string, value_string: boolean][] = [];

    for (let key in object) {
        if (!object.hasOwnProperty(key)) continue;

        if (object[key] === null) {
            items.push([level, key, "null", false]);
        } else if (object[key] === undefined) {
            items.push([level, key, "undefined", false]);
        } else if (typeof object[key] === 'object') {
            if (Array.isArray(object[key])) {
                if (object[key].length == 0) {
                    items.push([level, key + ":", "[]", false]);
                } else {
                    items.push([level, key + ":", "", false]);
                }
                for (let i = 0; i < object[key].length; i++) {
                    items.push([level+1, i.toString() + ":", "", false]);
                    items = items.concat(generateTree(object[key][i], level+2));
                }
                continue;
            }
            items.push([level, key + ":", "", false]);
            items = items.concat(generateTree(object[key], level + 1));
        } else if (Array.isArray(object[key])) {
            for (let i = 0; i < object[key].length; i++) {
                items.push([level, i.toString() + ":", "", false]);
                items = items.concat(generateTree(object[key][i], level));
            }
        } else if (typeof object[key] === 'string') {
            items.push([level, key + ":", "\"" + object[key] + "\"", true]);
function generateTree(
  object: any,
  level: number,
): [indent: number, key: string, value: string, value_string: boolean][] {
  let items: [
    indent: number,
    key: string,
    value: string,
    value_string: boolean,
  ][] = [];

  for (let key in object) {
    if (!object.hasOwnProperty(key)) continue;

    if (object[key] === null) {
      items.push([level, key, "null", false]);
    } else if (object[key] === undefined) {
      items.push([level, key, "undefined", false]);
    } else if (typeof object[key] === "object") {
      if (Array.isArray(object[key])) {
        if (object[key].length == 0) {
          items.push([level, key + ":", "[]", false]);
        } else {
            items.push([level, key + ":", object[key].toString(), false]);
          items.push([level, key + ":", "", false]);
        }
        for (let i = 0; i < object[key].length; i++) {
          items.push([level + 1, i.toString() + ":", "", false]);
          items = items.concat(generateTree(object[key][i], level + 2));
        }
        continue;
      }
      items.push([level, key + ":", "", false]);
      items = items.concat(generateTree(object[key], level + 1));
    } else if (Array.isArray(object[key])) {
      for (let i = 0; i < object[key].length; i++) {
        items.push([level, i.toString() + ":", "", false]);
        items = items.concat(generateTree(object[key][i], level));
      }
    } else if (typeof object[key] === "string") {
      items.push([level, key + ":", '"' + object[key] + '"', true]);
    } else {
      items.push([level, key + ":", object[key].toString(), false]);
    }
  }

    return items;
  return items;
}

export let packets: Packet[] = [];


@@ 123,21 137,25 @@ const down_arrow = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox=
            </svg>`;

export function selectPacket(id: number) {
    console.log("selecting packet " + id);
    if (selected_packet !== null) {
        document.getElementById("packet-" + selected_packet)!.classList.remove("log-selected");
        document.getElementById("packet-" + selected_packet)!.classList.add("log-item");
    }
    document.getElementById("packet-" + id)!.classList.add("log-selected");
    document.getElementById("packet-" + id)!.classList.remove("log-item");
    selected_packet = id;
    show_packet(packets[id], id);
  console.log("selecting packet " + id);
  if (selected_packet !== null) {
    document
      .getElementById("packet-" + selected_packet)!
      .classList.remove("log-selected");
    document
      .getElementById("packet-" + selected_packet)!
      .classList.add("log-item");
  }
  document.getElementById("packet-" + id)!.classList.add("log-selected");
  document.getElementById("packet-" + id)!.classList.remove("log-item");
  selected_packet = id;
  show_packet(packets[id], id);
}

export function appendPacket(packet: Packet) {
    packets.push(packet);
    return;
/*
  packets.push(packet);
  return;
  /*
    let duration = "--";
    if (last_packet !== null) {
        duration = (new Date().getTime() - last_packet!.getTime()).toString() + "ms";

M starkingdoms-client/src/planet_colors.ts => starkingdoms-client/src/planet_colors.ts +7 -7
@@ 1,9 1,9 @@
import {PlanetType} from "./protocol.ts";
import { PlanetType } from "./protocol.ts";

export function planet_color(type: PlanetType): string {
    if (type === PlanetType.Earth) {
        return "limegreen";
    } else {
        return "white";
    }
}
\ No newline at end of file
  if (type === PlanetType.Earth) {
    return "limegreen";
  } else {
    return "white";
  }
}

M starkingdoms-client/src/protocol.ts => starkingdoms-client/src/protocol.ts +76 -56
@@ 1,99 1,119 @@
export interface ProtoTransform {
    x: number,
    y: number,
    rot: number
  x: number;
  y: number;
  rot: number;
}
export enum PlanetType {
    Earth = "Earth"
  Earth = "Earth",
}
export enum PartType {
    Hearty = "Hearty"
  Hearty = "Hearty",
}
export interface Planet {
    planet_type: PlanetType,
    transform: ProtoTransform,
    radius: number
  planet_type: PlanetType;
  transform: ProtoTransform;
  radius: number;
}
export interface Part {
    part_type: PartType,
    transform: ProtoTransform
  part_type: PartType;
  transform: ProtoTransform;
}
export interface ClientLoginPacket {
    username: string,
    jwt: string | null,
  username: string;
  jwt: string | null;
}
export interface SpawnPlayerPacket {
    id: number,
    username: string
  id: number;
  username: string;
}
export interface PlanetPositionsPacket {
    planets: [number, Planet][]
  planets: [number, Planet][];
}
export interface PartPositionsPacket {
    parts: [number, Part][]
  parts: [number, Part][];
}
export interface PlayerInputPacket {
    up: boolean,
    down: boolean,
    left: boolean,
    right: boolean,
  up: boolean;
  down: boolean;
  left: boolean;
  right: boolean;
}
export interface PlayerListPacket {
    players: [number, string][]
  players: [number, string][];
}
export interface PlayerLeavePacket {
    id: number
  id: number;
}
export interface SendMessagePacket {
    target: string | null,
    content: string
  target: string | null;
  content: string;
}
export enum MessageType {
    Server = "Server",
    Error = "Error",
    Chat = "Chat",
    Direct = "Direct"
  Server = "Server",
  Error = "Error",
  Chat = "Chat",
  Direct = "Direct",
}
export interface MessagePacket {
    message_type: MessageType,
    actor: string,
    content: string
  message_type: MessageType;
  actor: string;
  content: string;
}

export enum PacketType {
    // serverbound
    ClientLogin = "ClientLogin",
    SendMessage = "SendMessage",
    PlayerInput = "PlayerInput",
    // clientbound
    SpawnPlayer = "SpawnPlayer",
    PlayerList = "PlayerList",
    PlanetPositions = "PlanetPositions",
    PartPositions = "PartPositions",
    PlayerLeave = "PlayerLeave",
    Message = "Message"
  // serverbound
  ClientLogin = "ClientLogin",
  SendMessage = "SendMessage",
  PlayerInput = "PlayerInput",
  // clientbound
  SpawnPlayer = "SpawnPlayer",
  PlayerList = "PlayerList",
  PlanetPositions = "PlanetPositions",
  PartPositions = "PartPositions",
  PlayerLeave = "PlayerLeave",
  Message = "Message",
}

export interface Packet {
    t: PacketType,
    c: ClientLoginPacket | SpawnPlayerPacket | PlayerListPacket | PlanetPositionsPacket | PartPositionsPacket | PlayerLeavePacket | SendMessagePacket | MessagePacket | PlayerInputPacket
  t: PacketType;
  c:
    | ClientLoginPacket
    | SpawnPlayerPacket
    | PlayerListPacket
    | PlanetPositionsPacket
    | PartPositionsPacket
    | PlayerLeavePacket
    | SendMessagePacket
    | MessagePacket
    | PlayerInputPacket;
}

export const SERVERBOUND = [PacketType.ClientLogin, PacketType.SendMessage, PacketType.PlayerInput];
export const CLIENTBOUND = [PacketType.SpawnPlayer, PacketType.PlayerList, PacketType.PlanetPositions, PacketType.PartPositions, PacketType.PlayerLeave, PacketType.Message];
export const SERVERBOUND = [
  PacketType.ClientLogin,
  PacketType.SendMessage,
  PacketType.PlayerInput,
];
export const CLIENTBOUND = [
  PacketType.SpawnPlayer,
  PacketType.PlayerList,
  PacketType.PlanetPositions,
  PacketType.PartPositions,
  PacketType.PlayerLeave,
  PacketType.Message,
];

export enum Direction {
    Serverbound = "Serverbound",
    Clientbound = "Clientbound",
    InvalidType = "InvalidType"
  Serverbound = "Serverbound",
  Clientbound = "Clientbound",
  InvalidType = "InvalidType",
}

export function type_direction(type: PacketType): Direction {
    if (SERVERBOUND.includes(type)) {
        return Direction.Serverbound;
    } else if (CLIENTBOUND.includes(type)) {
        return Direction.Clientbound;
    } else {
        return Direction.InvalidType;
    }
  if (SERVERBOUND.includes(type)) {
    return Direction.Serverbound;
  } else if (CLIENTBOUND.includes(type)) {
    return Direction.Clientbound;
  } else {
    return Direction.InvalidType;
  }
}

M starkingdoms-client/src/rendering.ts => starkingdoms-client/src/rendering.ts +114 -95
@@ 1,117 1,136 @@
import {global, player} from "./main.ts";
import {part_texture, planet_texture} from "./textures.ts";
import {planet_color} from "./planet_colors.ts";
import { global, player } from "./main.ts";
import { part_texture, planet_texture } from "./textures.ts";
import { planet_color } from "./planet_colors.ts";

//let t = performance.now();
//let delta = 0.0;

export function startRender() {
    // hide the launch popup
    document.getElementById("server_selector")!.classList.add("hidden");
    // show the HUD
    document.getElementById("hud")!.classList.remove("hidden");
    // and chat
    document.getElementById("chat")!.classList.remove("hidden");
    // create the canvas
    let canvas = document.createElement("canvas");
    canvas.classList.add("game");
    // append it
    document.getElementById("gamewindow")!.appendChild(canvas);
    let ctx = canvas.getContext("2d")!;
  // hide the launch popup
  document.getElementById("server_selector")!.classList.add("hidden");
  // show the HUD
  document.getElementById("hud")!.classList.remove("hidden");
  // and chat
  document.getElementById("chat")!.classList.remove("hidden");
  // create the canvas
  let canvas = document.createElement("canvas");
  canvas.classList.add("game");
  // append it
  document.getElementById("gamewindow")!.appendChild(canvas);
  let ctx = canvas.getContext("2d")!;
  ctx.canvas.width = window.innerWidth;
  ctx.canvas.height = window.innerHeight;

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

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

    global.rendering = {
        canvas: canvas,
        ctx: ctx
    };
    //t = performance.now();
    //delta = 0.0;
    // start the render loop
    requestAnimationFrame(renderLoop);
  };

  global.rendering = {
    canvas: canvas,
    ctx: ctx,
  };
  //t = performance.now();
  //delta = 0.0;
  // start the render loop
  requestAnimationFrame(renderLoop);
}

async function renderLoop(_newT: DOMHighResTimeStamp) {
    //delta = newT - t;
    //t = newT;
  //delta = newT - t;
  //t = newT;

    let viewer_size_x = global.rendering?.canvas.width!;
    let viewer_size_y = global.rendering?.canvas.height!;
  let viewer_size_x = global.rendering?.canvas.width!;
  let viewer_size_y = global.rendering?.canvas.height!;

    global.rendering!.canvas.style.setProperty("background-position", `${player()?.transform.x!/5}px ${-player()?.transform.y!/5}px`);
  global.rendering!.canvas.style.setProperty(
    "background-position",
    `${player()?.transform.x! / 5}px ${-player()?.transform.y! / 5}px`,
  );

    global.rendering!.ctx.setTransform(1, 0, 0, 1, 0, 0);
    global.rendering!.ctx.clearRect(0, 0, viewer_size_x, viewer_size_y);
  global.rendering!.ctx.setTransform(1, 0, 0, 1, 0, 0);
  global.rendering!.ctx.clearRect(0, 0, viewer_size_x, viewer_size_y);

    // *dont* translate the camera. we're moving everything else around us. cameracentrism.
    // only translation will be to center our core module.
  // *dont* translate the camera. we're moving everything else around us. cameracentrism.
  // only translation will be to center our core module.

    global.rendering!.ctx.translate(viewer_size_x / 2, viewer_size_y / 2);
  global.rendering!.ctx.translate(viewer_size_x / 2, viewer_size_y / 2);

    /*
  /*
    todo: track indicator
     */

    for (let [_id, planet] of global.planets_map) {
        global.rendering!.ctx.drawImage(
            await planet_texture(planet.planet_type),
            (planet.transform.x - planet.radius - player()?.transform.x!), // dx
            (planet.transform.y - planet.radius - player()?.transform.y!), // dy
            planet.radius * 2, // dw
            planet.radius * 2 // dh
        );

        global.rendering!.ctx.beginPath();
        global.rendering!.ctx.strokeStyle = planet_color(planet.planet_type);
        global.rendering!.ctx.lineWidth = 5;
        global.rendering!.ctx.moveTo(player()!.transform.x - player()!.transform.x, player()!.transform.y - player()!.transform.y);
        global.rendering!.ctx.lineTo(planet.transform.x - player()!.transform.x, planet.transform.y - player()!.transform.y);
        global.rendering!.ctx.stroke();
    }

    for (let [_id, part] of global.parts_map) {
        global.rendering!.ctx.save();

        // x_{screen} = x_{world} - player_{x_{world}}
        // x_{world} = x_{screen} + player_{x_{world}}

        global.rendering!.ctx.translate(part.transform.x - player()!.transform.x, part.transform.y - player()!.transform.y);

        global.rendering!.ctx.rotate(part.transform.rot);

        global.rendering!.ctx.drawImage(
            await part_texture(part.part_type),
            -25, -25, 50, 50
        );

        global.rendering!.ctx.restore();

        // todo: clicked stuff
  for (let [_id, planet] of global.planets_map) {
    global.rendering!.ctx.drawImage(
      await planet_texture(planet.planet_type),
      planet.transform.x - planet.radius - player()?.transform.x!, // dx
      planet.transform.y - planet.radius - player()?.transform.y!, // dy
      planet.radius * 2, // dw
      planet.radius * 2, // dh
    );

    global.rendering!.ctx.beginPath();
    global.rendering!.ctx.strokeStyle = planet_color(planet.planet_type);
    global.rendering!.ctx.lineWidth = 5;
    global.rendering!.ctx.moveTo(
      player()!.transform.x - player()!.transform.x,
      player()!.transform.y - player()!.transform.y,
    );
    global.rendering!.ctx.lineTo(
      planet.transform.x - player()!.transform.x,
      planet.transform.y - player()!.transform.y,
    );
    global.rendering!.ctx.stroke();
  }

  for (let [_id, part] of global.parts_map) {
    global.rendering!.ctx.save();

    // x_{screen} = x_{world} - player_{x_{world}}
    // x_{world} = x_{screen} + player_{x_{world}}

    global.rendering!.ctx.translate(
      part.transform.x - player()!.transform.x,
      part.transform.y - player()!.transform.y,
    );

    global.rendering!.ctx.rotate(part.transform.rot);

    global.rendering!.ctx.drawImage(
      await part_texture(part.part_type),
      -25,
      -25,
      50,
      50,
    );

    global.rendering!.ctx.restore();

    // todo: clicked stuff
  }

  for (let [id, username] of global.players_map) {
    let part = global.parts_map.get(id);

    if (part !== undefined) {
      global.rendering!.ctx.save();

      global.rendering!.ctx.translate(
        part!.transform.x - player()!.transform.x,
        part!.transform.y - player()!.transform.y,
      );

      global.rendering!.ctx.textAlign = "center";
      global.rendering!.ctx.font =
        '30px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
      global.rendering!.ctx.fillStyle = "white";
      global.rendering!.ctx.fillText(username, 0, -35);

      global.rendering!.ctx.restore();
    }
  }

    for (let [id, username] of global.players_map) {
        let part = global.parts_map.get(id);

        if (part !== undefined) {
            global.rendering!.ctx.save();

            global.rendering!.ctx.translate(part!.transform.x - player()!.transform.x, part!.transform.y - player()!.transform.y);

            global.rendering!.ctx.textAlign = "center";
            global.rendering!.ctx.font = '30px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
            global.rendering!.ctx.fillStyle = "white";
            global.rendering!.ctx.fillText(username, 0, -35);
  // particles stuff

            global.rendering!.ctx.restore();
        }
    }

    // particles stuff

    requestAnimationFrame(renderLoop);
}
\ No newline at end of file
  requestAnimationFrame(renderLoop);
}

M starkingdoms-client/src/textures.ts => starkingdoms-client/src/textures.ts +39 -37
@@ 1,4 1,4 @@
import {PartType, PlanetType} from "./protocol.ts";
import { PartType, PlanetType } from "./protocol.ts";
import tex_earth from "./assets/earth.svg";
import tex_hearty from "./assets/hearty.svg";
import tex_missing from "./assets/missing.svg";


@@ 6,49 6,51 @@ import tex_missing from "./assets/missing.svg";
let planet_textures: Map<PlanetType, HTMLImageElement> = new Map();

function planet_texture_url(type: PlanetType): string {
    if (type == PlanetType.Earth) {
        return tex_earth;
    }
    return tex_missing;
  if (type == PlanetType.Earth) {
    return tex_earth;
  }
  return tex_missing;
}

export async function planet_texture(type: PlanetType): Promise<HTMLImageElement> {
    return new Promise((resolve, reject) => {
        if (planet_textures.has(type)) {
            resolve(planet_textures.get(type)!);
        } else {
            let img = new Image();
            img.onload = () => {
                planet_textures.set(type, img);
                resolve(img);
            };
            img.onerror = reject;
            img.src = planet_texture_url(type)
        }
    });
export async function planet_texture(
  type: PlanetType,
): Promise<HTMLImageElement> {
  return new Promise((resolve, reject) => {
    if (planet_textures.has(type)) {
      resolve(planet_textures.get(type)!);
    } else {
      let img = new Image();
      img.onload = () => {
        planet_textures.set(type, img);
        resolve(img);
      };
      img.onerror = reject;
      img.src = planet_texture_url(type);
    }
  });
}

let part_textures: Map<PartType, HTMLImageElement> = new Map();

function part_texture_url(type: PartType): string {
    if (type == PartType.Hearty) {
        return tex_hearty;
    }
    return tex_missing;
  if (type == PartType.Hearty) {
    return tex_hearty;
  }
  return tex_missing;
}

export async function part_texture(type: PartType): Promise<HTMLImageElement> {
    return new Promise((resolve, reject) => {
        if (part_textures.has(type)) {
            resolve(part_textures.get(type)!);
        } else {
            let img = new Image();
            img.onload = () => {
                part_textures.set(type, img);
                resolve(img);
            };
            img.onerror = reject;
            img.src = part_texture_url(type)
        }
    });
}
\ No newline at end of file
  return new Promise((resolve, reject) => {
    if (part_textures.has(type)) {
      resolve(part_textures.get(type)!);
    } else {
      let img = new Image();
      img.onload = () => {
        part_textures.set(type, img);
        resolve(img);
      };
      img.onerror = reject;
      img.src = part_texture_url(type);
    }
  });
}

M starkingdoms-client/vite.config.ts => starkingdoms-client/vite.config.ts +15 -12
@@ 1,16 1,19 @@
import {defineConfig} from "vite";
import { defineConfig } from "vite";
import * as child from "child_process";

const commitHash = child.execSync('git describe --no-match --always --abbrev=8 --dirty').toString().trim();
const commitHash = child
  .execSync("git describe --no-match --always --abbrev=8 --dirty")
  .toString()
  .trim();

export default defineConfig({
    plugins: [],
    define: {
        APP_VERSION: JSON.stringify(process.env.npm_package_version),
        COMMIT_HASH: JSON.stringify(commitHash)
    },
    build: {
        target: ['chrome89', 'edge89', 'firefox89', 'safari15'],
        cssCodeSplit: false,
    }
})
\ No newline at end of file
  plugins: [],
  define: {
    APP_VERSION: JSON.stringify(process.env.npm_package_version),
    COMMIT_HASH: JSON.stringify(commitHash),
  },
  build: {
    target: ["chrome89", "edge89", "firefox89", "safari15"],
    cssCodeSplit: false,
  },
});