Simon пре 4 година
родитељ
комит
547884332c
100 измењених фајлова са 1654 додато и 0 уклоњено
  1. 21 0
      client/LICENSE
  2. 366 0
      client/base.css
  3. 137 0
      client/index.html
  4. 1128 0
      client/main.js
  5. BIN
      client/resources/background-2.jpg
  6. BIN
      client/resources/background-3.png
  7. BIN
      client/resources/background.jpg
  8. BIN
      client/resources/characters/guard.glb
  9. BIN
      client/resources/characters/paladin.glb
  10. 1 0
      client/resources/characters/readme.txt
  11. BIN
      client/resources/characters/sorceror.glb
  12. BIN
      client/resources/characters/warrok.glb
  13. BIN
      client/resources/characters/zombie-guy.glb
  14. BIN
      client/resources/characters/zombie.glb
  15. 1 0
      client/resources/icons/readme.txt
  16. BIN
      client/resources/icons/ui/backpack.png
  17. BIN
      client/resources/icons/ui/health-bar.png
  18. BIN
      client/resources/icons/ui/inventory-character.png
  19. BIN
      client/resources/icons/ui/skills.png
  20. BIN
      client/resources/icons/ui/tied-scroll.png
  21. BIN
      client/resources/icons/weapons/hammer-64.png
  22. BIN
      client/resources/icons/weapons/pointy-sword-64.png
  23. BIN
      client/resources/icons/weapons/pointy-sword.png
  24. BIN
      client/resources/icons/weapons/thor-hammer.png
  25. BIN
      client/resources/icons/weapons/war-axe-64.png
  26. BIN
      client/resources/icons/weapons/war-axe.png
  27. BIN
      client/resources/nature/Blends/BirchTree_1.blend
  28. BIN
      client/resources/nature/Blends/BirchTree_2.blend
  29. BIN
      client/resources/nature/Blends/BirchTree_3.blend
  30. BIN
      client/resources/nature/Blends/BirchTree_4.blend
  31. BIN
      client/resources/nature/Blends/BirchTree_5.blend
  32. BIN
      client/resources/nature/Blends/BirchTree_Autumn_1.blend
  33. BIN
      client/resources/nature/Blends/BirchTree_Autumn_2.blend
  34. BIN
      client/resources/nature/Blends/BirchTree_Autumn_3.blend
  35. BIN
      client/resources/nature/Blends/BirchTree_Autumn_4.blend
  36. BIN
      client/resources/nature/Blends/BirchTree_Autumn_5.blend
  37. BIN
      client/resources/nature/Blends/BirchTree_Dead_1.blend
  38. BIN
      client/resources/nature/Blends/BirchTree_Dead_2.blend
  39. BIN
      client/resources/nature/Blends/BirchTree_Dead_3.blend
  40. BIN
      client/resources/nature/Blends/BirchTree_Dead_4.blend
  41. BIN
      client/resources/nature/Blends/BirchTree_Dead_5.blend
  42. BIN
      client/resources/nature/Blends/BirchTree_Dead_Snow_1.blend
  43. BIN
      client/resources/nature/Blends/BirchTree_Dead_Snow_2.blend
  44. BIN
      client/resources/nature/Blends/BirchTree_Dead_Snow_3.blend
  45. BIN
      client/resources/nature/Blends/BirchTree_Dead_Snow_4.blend
  46. BIN
      client/resources/nature/Blends/BirchTree_Dead_Snow_5.blend
  47. BIN
      client/resources/nature/Blends/BirchTree_Snow_1.blend
  48. BIN
      client/resources/nature/Blends/BirchTree_Snow_2.blend
  49. BIN
      client/resources/nature/Blends/BirchTree_Snow_3.blend
  50. BIN
      client/resources/nature/Blends/BirchTree_Snow_4.blend
  51. BIN
      client/resources/nature/Blends/BirchTree_Snow_5.blend
  52. BIN
      client/resources/nature/Blends/BushBerries_1.blend
  53. BIN
      client/resources/nature/Blends/BushBerries_2.blend
  54. BIN
      client/resources/nature/Blends/Bush_1.blend
  55. BIN
      client/resources/nature/Blends/Bush_2.blend
  56. BIN
      client/resources/nature/Blends/Bush_Snow_1.blend
  57. BIN
      client/resources/nature/Blends/Bush_Snow_2.blend
  58. BIN
      client/resources/nature/Blends/CactusFlower_1.blend
  59. BIN
      client/resources/nature/Blends/CactusFlowers_2.blend
  60. BIN
      client/resources/nature/Blends/CactusFlowers_3.blend
  61. BIN
      client/resources/nature/Blends/CactusFlowers_4.blend
  62. BIN
      client/resources/nature/Blends/CactusFlowers_5.blend
  63. BIN
      client/resources/nature/Blends/Cactus_1.blend
  64. BIN
      client/resources/nature/Blends/Cactus_2.blend
  65. BIN
      client/resources/nature/Blends/Cactus_3.blend
  66. BIN
      client/resources/nature/Blends/Cactus_4.blend
  67. BIN
      client/resources/nature/Blends/Cactus_5.blend
  68. BIN
      client/resources/nature/Blends/CommonTree_1.blend
  69. BIN
      client/resources/nature/Blends/CommonTree_2.blend
  70. BIN
      client/resources/nature/Blends/CommonTree_3.blend
  71. BIN
      client/resources/nature/Blends/CommonTree_4.blend
  72. BIN
      client/resources/nature/Blends/CommonTree_5.blend
  73. BIN
      client/resources/nature/Blends/CommonTree_Autumn_1.blend
  74. BIN
      client/resources/nature/Blends/CommonTree_Autumn_2.blend
  75. BIN
      client/resources/nature/Blends/CommonTree_Autumn_3.blend
  76. BIN
      client/resources/nature/Blends/CommonTree_Autumn_4.blend
  77. BIN
      client/resources/nature/Blends/CommonTree_Autumn_5.blend
  78. BIN
      client/resources/nature/Blends/CommonTree_Dead_1.blend
  79. BIN
      client/resources/nature/Blends/CommonTree_Dead_2.blend
  80. BIN
      client/resources/nature/Blends/CommonTree_Dead_3.blend
  81. BIN
      client/resources/nature/Blends/CommonTree_Dead_4.blend
  82. BIN
      client/resources/nature/Blends/CommonTree_Dead_5.blend
  83. BIN
      client/resources/nature/Blends/CommonTree_Dead_Snow_1.blend
  84. BIN
      client/resources/nature/Blends/CommonTree_Dead_Snow_2.blend
  85. BIN
      client/resources/nature/Blends/CommonTree_Dead_Snow_3.blend
  86. BIN
      client/resources/nature/Blends/CommonTree_Dead_Snow_4.blend
  87. BIN
      client/resources/nature/Blends/CommonTree_Dead_Snow_5.blend
  88. BIN
      client/resources/nature/Blends/CommonTree_Snow_1.blend
  89. BIN
      client/resources/nature/Blends/CommonTree_Snow_2.blend
  90. BIN
      client/resources/nature/Blends/CommonTree_Snow_3.blend
  91. BIN
      client/resources/nature/Blends/CommonTree_Snow_4.blend
  92. BIN
      client/resources/nature/Blends/CommonTree_Snow_5.blend
  93. BIN
      client/resources/nature/Blends/Corn_1.blend
  94. BIN
      client/resources/nature/Blends/Corn_2.blend
  95. BIN
      client/resources/nature/Blends/Flowers.blend
  96. BIN
      client/resources/nature/Blends/Grass.blend
  97. BIN
      client/resources/nature/Blends/Grass_2.blend
  98. BIN
      client/resources/nature/Blends/Grass_Short.blend
  99. BIN
      client/resources/nature/Blends/Lilypad.blend
  100. BIN
      client/resources/nature/Blends/PalmTree_1.blend

+ 21 - 0
client/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 simondevyoutube
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 366 - 0
client/base.css

@@ -0,0 +1,366 @@
+body {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  background: #000000;
+  margin: 0;
+  padding: 0;
+  overscroll-behavior: none;
+}
+
+.container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+}
+
+.ui {
+  width: 100%;
+  height: 100%;            
+  position: absolute;
+  top: 0;
+  left: 0;
+  font-family: 'IM Fell French Canon', serif;
+}
+
+#game-ui {
+  visibility: hidden;
+}
+
+#login-ui {
+  visibility: hidden;
+}
+
+.login-screen {
+  position: absolute;
+  width: 100%;
+  height: 100%;            
+  top: 0;
+  left: 0;
+  font-family: 'IM Fell French Canon', serif;
+  background-image: url('./resources/background-3.png');
+  background-size: cover;
+}
+
+.ui.fadeOut {
+  opacity: 1.0;
+  animation: fadeOut 1s ease-in-out forwards;
+}
+
+
+
+@keyframes fadeOut {
+  from {
+    opacity: 1.0;
+    visibility: visible;
+  }
+  to {
+    opacity: 0.0;
+    visibility: hidden;
+  }
+}
+
+.login-screen-layout {
+  display: flex;
+  height: 100%;
+  width: 100%;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+}
+
+.login-screen-layout.window {
+  height: 400px;
+  width: 500px;
+  border-radius: 5px;
+  border-color: black;
+  border-style: solid;
+  background: rgba(0, 0, 0, 0.75);
+}
+
+.login-button {
+  height: 50px;
+  width: 200px;
+  border-radius: 5px;
+  box-sizing: border-box;
+  text-align: center;
+  background-color: rgba(130, 130, 130, 0.5);
+  display: inline-block;
+  font-size: 1.5em;
+  text-shadow: 2px 2px 5px black;
+  color: white;
+  margin: 15px;
+  transition: all 0.2s;
+}
+
+.login-button:hover {
+  color: #000000;
+  background-color: rgba(255, 255, 255, 0.5);
+}
+
+.login-input {
+  background: rgba(0, 0, 0, 0);
+  width: 300px;
+  font-size: 1.5em;
+  text-shadow: 2px 2px 5px black;
+  color: white;
+  margin: 15px;
+}
+
+.login-text {
+  font-size: 2.0em;
+  text-shadow: 2px 2px 5px black;
+  color: white;
+}
+
+
+.chat-ui {
+  position: absolute;
+  left: 10px;
+  bottom: 10px; 
+  background: rgba(1.0, 1.0, 1.0, 0.0);
+  width: 400px;
+  height: 200px;
+  padding: 10px 10px;
+  border-radius: 10px;
+}
+
+.chat-ui-text-area {
+  display: flex;
+  height: 100%;
+  width: 100%;
+  flex-direction: column;
+  justify-content: flex-end;
+}
+
+.chat-text {
+  font-size: .75em;
+  text-shadow: 2px 2px 5px black;
+  color: white;
+}
+
+.chat-text-server {
+  color: rgb(116, 235, 87);
+}
+
+.chat-text-action {
+  color: rgb(247, 20, 20);
+}
+
+.chat-input {
+  background: rgba(0, 0, 0, 0);
+  width: 280px;
+  margin-top: 5px;
+}
+
+
+.quest-ui-layout {
+  display: flex;
+  height: 100%;
+  width: 100%;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+}
+
+.quest-ui {
+  background: rgba(1.0, 1.0, 1.0, 0.75);
+  padding: 20px 20px;
+  width: 700px;
+}
+
+.quest-title {
+  font-size: 3em;
+  color: white;
+  text-shadow: 4px 4px black;
+}
+
+.quest-text-title {
+  font-size: 3em;
+  color: white;
+  padding-bottom: 10px;
+}
+
+.quest-text {
+  font-size: 1em;
+  color: white;
+}
+
+.quest-journal {
+  position: absolute;
+  right: 0px;
+  display: flex;
+  flex-direction: column;
+  background: rgba(1.0, 1.0, 1.0, 0.75);
+  margin: 30px;
+  padding: 20px 20px;
+  padding-top: 5px;
+  width: 300px;
+  height: 600px;
+  z-index: 1;
+}
+
+.quest-entry {
+  font-size: 2em;
+  color: white;
+  border: black;
+  border-style: solid;
+  border-width: thick;
+  padding: 5px;
+}
+
+.icon-bar {
+  position: absolute;
+  bottom: 0px;
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  margin-bottom: 10px;
+}
+
+.icon-bar-item {
+  background-size: cover;
+  width: 75px;
+  height: 75px;
+  margin: 2px;
+}
+
+.health-ui {
+  position: absolute;
+  top: 0px;
+  left: 0px;
+  background-image: url('./resources/icons/ui/health-bar.png');
+  width: 500px;
+  height: 300px;
+  z-index: 1;
+}
+
+.health-bar {
+  background: greenyellow;
+  width: 200px;
+  max-width: 200px;
+  height: 40px;
+  position: relative;
+  top: 215px;
+  left: 260px;
+  border-style: solid;
+  border-width: 2px;
+  border-color: black;
+  border-radius: 5px;
+}
+
+.stats-tooltip {
+  position: relative;
+  display: inline-block;
+  z-index: 100;
+}
+
+.stats-tooltip .stats-tooltiptext {
+  visibility: hidden;
+  width: 200px;
+  background-color: black;
+  opacity: 0.75;
+  text-align: center;
+  padding: 1em;
+  border-radius: 6px;
+  color: white;
+  font-size: medium;
+  position: absolute;
+  z-index: 1;
+  top: -5px;
+  right: 105%;
+}
+
+.stats-tooltip:hover .stats-tooltiptext {
+  visibility: visible;
+}
+
+.stats-title {
+  font-size: 3em;
+  color: white;
+  text-shadow: 4px 4px black;
+}
+
+.stats {
+  position: absolute;
+  right: 0px;
+  top: 0px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  margin: 30px;
+  z-index: 1;
+}
+
+.stats-inner {
+  display: flex;
+  flex-direction: column;
+  background: rgba(1.0, 1.0, 1.0, 0.75);
+  padding: 20px 20px;
+  width: 250px;
+  padding-top: 5px;
+}
+
+.stats-row {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  font-size: 2em;
+  color: white;
+  text-shadow: 4px 4px black;
+}
+
+.inventory-title {
+  font-size: 3em;
+  color: white;
+  text-shadow: 4px 4px black;
+}
+
+.inventory {
+  position: absolute;
+  top: 0px;
+  right: 0px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  margin: 30px;
+  z-index: 1;
+}
+
+.inventory-inner {
+  display: flex;
+  flex-direction: column;
+  background: rgba(1.0, 1.0, 1.0, 0.75);
+  padding: 20px 20px;
+  padding-top: 5px;
+}
+
+.inventory-row {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-evenly;
+}
+
+.inventory-column {
+  display: flex;
+  flex-direction: column;
+  justify-content: space-evenly;
+}
+
+.inventory-character {
+  background-image: url('./resources/icons/ui/inventory-character.png');
+  background-size: cover;
+  width: 200px;
+  height: 350px;
+}
+
+.inventory-item {
+  border: black;
+  border-style: solid;
+  border-radius: 10%;
+  background-color: black;
+  width: 50px;
+  height: 50px;
+  margin: 2px;
+  background-size: cover;
+}

+ 137 - 0
client/index.html

@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>SimonDev Crappy MMO</title>
+  <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+  <link rel="stylesheet" type="text/css" href="base.css">
+  <link href="https://fonts.googleapis.com/css2?family=IM+Fell+English&display=swap" rel="stylesheet">
+  <link href="https://fonts.googleapis.com/css2?family=IM+Fell+French+Canon+SC&display=swap" rel="stylesheet">
+</head>
+<body>
+  <script src="./src/main.js" type="module">
+  </script>
+  <div class="container" id="container">
+    <div class="ui" id="login-ui">
+      <div class="login-screen">
+        <div class="login-screen-layout">
+          <div class="login-screen-layout window">
+            <div class="login-text">Account Name</div>
+            <input class="login-input" id="login-input" maxlength="64" type="text"></input>
+            <button class="login-button" id="login-button">Login</button>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="ui" id="game-ui">
+      <div class="icon-bar" id="icon-bar">
+        <div class="icon-bar-item" id="icon-bar-stats" style="background-image: url('./resources/icons/ui/skills.png');"></div>
+        <div class="icon-bar-item" id="icon-bar-inventory" style="background-image: url('./resources/icons/ui/backpack.png');"></div>
+        <div class="icon-bar-item" id="icon-bar-quests" style="background-image: url('./resources/icons/ui/tied-scroll.png');"></div>
+      </div>
+      <div class="health-ui">
+        <div class="health-bar" id="health-bar"></div>
+      </div>
+      <div class="quest-journal" id="quest-journal"></div>
+      <div class="quest-ui-layout" id="quest-ui">
+        <div class="quest-ui">
+          <div class="quest-text-title" id="quest-text-title"></div>
+          <div class="quest-text" id="quest-text"></div>
+        </div>
+      </div>
+
+      <div class="stats" id="stats">
+        <div class="stats-inner">
+          <div class="stats-title">Statistics</div>
+          <div class="stats-row">
+            <div class="stats-tooltip">Strength
+              <div class="stats-tooltiptext">How strong you are, affects how much damage you do. So blah blah if you're doing stuff then its stronger or whatever, the damage is up. This is text to show the tooltip.</div>
+            </div>
+            <div id="stats-strength">0</div>
+          </div>
+          <div class="stats-row">
+            <div class="stats-tooltip">Wisdomness
+              <div class="stats-tooltiptext">Wisdom is the guage of something to do with wisdom in the game because wisdom is important and wisdom is wise to wisdoming.</div>
+            </div>
+            <div id="stats-wisdomness">0</div>
+          </div>
+          <div class="stats-row">
+            <div class="stats-tooltip">Benchpress
+              <div class="stats-tooltiptext">Gotta have a good bench to be a warrior or something.</div>
+            </div>
+            <div id="stats-benchpress">0</div>
+          </div>
+          <div class="stats-row">
+            <div class="stats-tooltip">Curl
+              <div class="stats-tooltiptext">The ultimate expression of strength, this affects literally everything in your life.</div>
+            </div>
+            <div id="stats-curl">0</div>
+          </div>
+          <div class="stats-row">
+            <div class="stats-tooltip">XP
+              <div class="stats-tooltiptext">How much xp you've accumulated by killing things for xp. Get enough and you'll gain a level or something.</div>
+            </div>
+            <div id="stats-experience">0</div>
+          </div>
+        </div>
+      </div>
+
+      <div class="inventory" id="inventory">
+        <div class="inventory-inner">
+          <div class="inventory-title">Inventory</div>
+          <div class="inventory-row">
+            <div class="inventory-column">
+              <div class="inventory-item" id="inventory-equip-1" draggable="true"></div>
+              <div class="inventory-item" id="inventory-equip-2" draggable="true"></div>
+              <div class="inventory-item" id="inventory-equip-3" draggable="true"></div>
+              <div class="inventory-item" id="inventory-equip-4" draggable="true"></div>
+            </div>
+            <div class="inventory-character"></div>
+            <div class="inventory-column">
+              <div class="inventory-item" id="inventory-equip-5" draggable="true"></div>
+              <div class="inventory-item" id="inventory-equip-6" draggable="true"></div>
+              <div class="inventory-item" id="inventory-equip-7" draggable="true"></div>
+              <div class="inventory-item" id="inventory-equip-8" draggable="true"></div>
+            </div>
+          </div>
+          <div class="inventory-row">
+            <div class="inventory-item" id="inventory-1" draggable="true"></div>
+            <div class="inventory-item" id="inventory-2" draggable="true"></div>
+            <div class="inventory-item" id="inventory-3" draggable="true"></div>
+            <div class="inventory-item" id="inventory-4" draggable="true"></div>
+            <div class="inventory-item" id="inventory-5" draggable="true"></div>
+            <div class="inventory-item" id="inventory-6" draggable="true"></div>
+            <div class="inventory-item" id="inventory-7" draggable="true"></div>
+            <div class="inventory-item" id="inventory-8" draggable="true"></div>
+          </div>
+          <div class="inventory-row">
+            <div class="inventory-item" id="inventory-9" draggable="true"></div>
+            <div class="inventory-item" id="inventory-10" draggable="true"></div>
+            <div class="inventory-item" id="inventory-11" draggable="true"></div>
+            <div class="inventory-item" id="inventory-12" draggable="true"></div>
+            <div class="inventory-item" id="inventory-13" draggable="true"></div>
+            <div class="inventory-item" id="inventory-14" draggable="true"></div>
+            <div class="inventory-item" id="inventory-15" draggable="true"></div>
+            <div class="inventory-item" id="inventory-16" draggable="true"></div>
+          </div>
+          <div class="inventory-row">
+            <div class="inventory-item" id="inventory-17" draggable="true"></div>
+            <div class="inventory-item" id="inventory-18" draggable="true"></div>
+            <div class="inventory-item" id="inventory-19" draggable="true"></div>
+            <div class="inventory-item" id="inventory-20" draggable="true"></div>
+            <div class="inventory-item" id="inventory-21" draggable="true"></div>
+            <div class="inventory-item" id="inventory-22" draggable="true"></div>
+            <div class="inventory-item" id="inventory-23" draggable="true"></div>
+            <div class="inventory-item" id="inventory-24" draggable="true"></div>
+          </div>
+        </div>
+      </div>
+
+      <div class="chat-ui">
+        <div class="chat-ui-text-area" id="chat-ui-text-area">
+          <input class="chat-text chat-input" id="chat-input" maxLength="64" type="text"></input>
+        </div>
+      </div>
+    </div>
+  </div>
+</body>
+</html>

+ 1128 - 0
client/main.js

@@ -0,0 +1,1128 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import {OrbitControls} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/controls/OrbitControls.js';
+import {FBXLoader} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/FBXLoader.js';
+
+import 'https://cdn.jsdelivr.net/npm/[email protected]/dist/socket.io.js';
+
+
+const _CHARACTER_MODELS = {
+  zombie: {
+    base: 'mremireh_o_desbiens.fbx',
+    path: './resources/characters/zombie/',
+    animations: {
+      idle: 'idle.fbx',
+      walk: 'walk.fbx',
+      run: 'run.fbx',
+      dance: 'dance.fbx',
+    },
+    nameOffset: 25,
+  },
+  guard: {
+    base: 'castle_guard_01.fbx',
+    path: './resources/characters/guard/',
+    animations: {
+      idle: 'Sword And Shield Idle.fbx',
+      walk: 'Sword And Shield Walk.fbx',
+      run: 'Sword And Shield Run.fbx',
+      dance: 'Macarena Dance.fbx',
+    },
+    nameOffset: 20,
+  }
+}
+
+
+class FloatingName {
+  constructor(params) {
+    this.params_ = params;
+    this.Init_();
+  }
+
+  Destroy() {
+    this.element_ = null;
+  }
+
+  Init_() {
+    const modelData = _CHARACTER_MODELS[this.params_.desc.character.class];
+
+    this.element_ = document.createElement('canvas');
+    this.context2d_ = this.element_.getContext('2d');
+    this.context2d_.canvas.width = 256;
+    this.context2d_.canvas.height = 128;
+    this.context2d_.fillStyle = '#FFF';
+    this.context2d_.font = "18pt Helvetica";
+    this.context2d_.shadowOffsetX = 3;
+    this.context2d_.shadowOffsetY = 3;
+    this.context2d_.shadowColor = "rgba(0,0,0,0.3)";
+    this.context2d_.shadowBlur = 4;
+    this.context2d_.textAlign = 'center';
+    this.context2d_.fillText(this.params_.desc.account.name, 128, 64);
+
+    const map = new THREE.CanvasTexture(this.context2d_.canvas);
+
+    this.sprite_ = new THREE.Sprite(
+        new THREE.SpriteMaterial({map: map, color: 0xffffff}));
+    this.sprite_.scale.set(20, 10, 1)
+    this.sprite_.position.y += modelData.nameOffset;
+    this.params_.parent.add(this.sprite_);
+  }
+};
+
+
+class OurLoadingManager {
+  constructor(loader) {
+    this.loader_ = loader;
+    this.files_ = new Set();
+    this.onLoad = () => {};
+  }
+
+  load(file, cb) {
+    this.files_.add(file);
+
+    this.loader_.load(file, (result) => {
+      this.files_.delete(file);
+      cb(result);
+
+      if (this.files_.size == 0) {
+        this.onLoad();
+      }
+    });
+  }
+};
+
+
+class BasicCharacterControllerProxy {
+  constructor(animations) {
+    this.animations_ = animations;
+  }
+
+  get animations() {
+    return this.animations_;
+  }
+};
+
+
+class AnimatedMesh {
+  constructor(params) {
+    this.params_ = params;
+    this.group_ = new THREE.Group();
+    this.params_.scene.add(this.group_);
+    this.target_ = null;
+    this.animations_ = {};
+    this.mixer_ = null;
+    this.onLoad = () => {};
+    this.Load_();
+  }
+
+  Destroy() {
+    if (this.target_) {
+      this.target_.traverse(c => {
+        if (c.material) {
+          c.material.dispose();
+        }
+        if (c.geometry) {
+          c.geometry.dispose();
+        }
+      });
+    }
+    this.params_.scene.remove(this.group_);
+  }
+
+  get position() {
+    return this.group_.position;
+  }
+
+  get quaternion() {
+    return this.group_.quaternion;
+  }
+
+  Load_() {
+    const modelData = _CHARACTER_MODELS[this.params_.desc.character.class];
+    const loader = new FBXLoader();
+    loader.setPath(modelData.path);
+    loader.load(modelData.base, (fbx) => {
+      fbx.scale.setScalar(0.1);
+      fbx.traverse(c => {
+        c.castShadow = true;
+      });
+
+      this.target_ = fbx;
+      this.group_.add(this.target_);
+
+      this.mixer_ = new THREE.AnimationMixer(this.target_);
+
+      const _OnLoad = (animName, anim) => {
+        const clip = anim.animations[0];
+        const action = this.mixer_.clipAction(clip);
+  
+        this.animations_[animName] = {
+          clip: clip,
+          action: action,
+        };
+      };
+
+      // LoadingManager seems to be broken when you attempt to load multiple
+      // resources multiple times, only first onLoad is called.
+      // So roll our own.
+      const loader = new FBXLoader();
+      loader.setPath(modelData.path);
+
+      this.manager_ = new OurLoadingManager(loader);
+      this.manager_.load(
+          modelData.animations.idle,
+          (a) => { _OnLoad('idle', a); });
+      this.manager_.load(
+          modelData.animations.walk,
+          (a) => { _OnLoad('walk', a); });
+      this.manager_.load(
+          modelData.animations.run,
+          (a) => { _OnLoad('run', a); });
+      this.manager_.load(
+          modelData.animations.dance,
+          (a) => { _OnLoad('dance', a); });
+      this.manager_.onLoad = () => {
+        this.onLoad();
+      };
+    });
+  }
+
+  Update(timeElapsed) {
+    if (this.mixer_) {
+      this.mixer_.update(timeElapsed);
+    }
+  }
+};
+
+
+class BasicCharacterController {
+  constructor(params) {
+    this._Init(params);
+  }
+
+  _Init(params) {
+    this.params_ = params;
+    this.decceleration_ = new THREE.Vector3(-0.0005, -0.0001, -5.0);
+    this.acceleration_ = new THREE.Vector3(1, 0.25, 50.0);
+    this.velocity_ = new THREE.Vector3(0, 0, 0);
+    this.position_ = new THREE.Vector3();
+    this.quaternion_ = new THREE.Quaternion();
+    this.loaded_ = false;
+
+    this._input = new BasicCharacterControllerInput();
+
+    this.target_ = new AnimatedMesh({
+        scene: params.scene,
+        desc: params.desc,
+    });
+    this.target_.onLoad = () => {
+      this.loaded_ = true;
+      this.stateMachine_.SetState('idle');
+    }
+    this.stateMachine_ = new CharacterFSM(
+        new BasicCharacterControllerProxy(this.target_.animations_));
+  }
+
+  get IsLoaded() {
+    return this.loaded_;
+  }
+
+  get Position() {
+    return this.position_;
+  }
+
+  get Rotation() {
+    return this.quaternion_;
+  }
+
+  SetTransform(p, q) {
+    this.position_.copy(p);
+    this.quaternion_.copy(q);
+    this.target_.group_.position.copy(this.position_);
+    this.target_.group_.quaternion.copy(this.quaternion_);
+  }
+
+  CreateTransformPacket() {
+    return [
+        this.stateMachine_.currentState_.Name,
+        this.position_.toArray(),
+        this.quaternion_.toArray(),
+    ];
+  }
+
+  Update(timeInSeconds) {
+    if (!this.stateMachine_.currentState_) {
+      return;
+    }
+
+    this.stateMachine_.Update(timeInSeconds, this._input);
+
+    const velocity = this.velocity_;
+    const frameDecceleration = new THREE.Vector3(
+        velocity.x * this.decceleration_.x,
+        velocity.y * this.decceleration_.y,
+        velocity.z * this.decceleration_.z
+    );
+    frameDecceleration.multiplyScalar(timeInSeconds);
+    frameDecceleration.z = Math.sign(frameDecceleration.z) * Math.min(
+        Math.abs(frameDecceleration.z), Math.abs(velocity.z));
+
+    velocity.add(frameDecceleration);
+
+    const controlObject = this.target_;
+    const _Q = new THREE.Quaternion();
+    const _A = new THREE.Vector3();
+    const _R = controlObject.quaternion.clone();
+
+    const acc = this.acceleration_.clone();
+    if (this._input._keys.shift) {
+      acc.multiplyScalar(2.0);
+    }
+
+    if (this.stateMachine_.currentState_.Name == 'dance') {
+      acc.multiplyScalar(0.0);
+    }
+
+    if (this._input._keys.forward) {
+      velocity.z += acc.z * timeInSeconds;
+    }
+    if (this._input._keys.backward) {
+      velocity.z -= acc.z * timeInSeconds;
+    }
+    if (this._input._keys.left) {
+      _A.set(0, 1, 0);
+      _Q.setFromAxisAngle(_A, 4.0 * Math.PI * timeInSeconds * this.acceleration_.y);
+      _R.multiply(_Q);
+    }
+    if (this._input._keys.right) {
+      _A.set(0, 1, 0);
+      _Q.setFromAxisAngle(_A, 4.0 * -Math.PI * timeInSeconds * this.acceleration_.y);
+      _R.multiply(_Q);
+    }
+
+    controlObject.quaternion.copy(_R);
+
+    const oldPosition = new THREE.Vector3();
+    oldPosition.copy(controlObject.position);
+
+    const forward = new THREE.Vector3(0, 0, 1);
+    forward.applyQuaternion(controlObject.quaternion);
+    forward.normalize();
+
+    const sideways = new THREE.Vector3(1, 0, 0);
+    sideways.applyQuaternion(controlObject.quaternion);
+    sideways.normalize();
+
+    sideways.multiplyScalar(velocity.x * timeInSeconds);
+    forward.multiplyScalar(velocity.z * timeInSeconds);
+
+    controlObject.position.add(forward);
+    controlObject.position.add(sideways);
+
+    this.position_.copy(controlObject.position);
+    this.quaternion_.copy(controlObject.quaternion);
+
+    this.target_.Update(timeInSeconds);
+  }
+};
+
+class BasicCharacterControllerInput {
+  constructor() {
+    this._Init();    
+  }
+
+  _Init() {
+    this._keys = {
+      forward: false,
+      backward: false,
+      left: false,
+      right: false,
+      space: false,
+      shift: false,
+    };
+    document.addEventListener('keydown', (e) => this.OnKeyDown_(e), false);
+    document.addEventListener('keyup', (e) => this._onKeyUp(e), false);
+  }
+
+  OnKeyDown_(event) {
+    if (event.currentTarget.activeElement != document.body) {
+      return;
+    }
+    switch (event.keyCode) {
+      case 87: // w
+        this._keys.forward = true;
+        break;
+      case 65: // a
+        this._keys.left = true;
+        break;
+      case 83: // s
+        this._keys.backward = true;
+        break;
+      case 68: // d
+        this._keys.right = true;
+        break;
+      case 32: // SPACE
+        this._keys.space = true;
+        break;
+      case 16: // SHIFT
+        this._keys.shift = true;
+        break;
+    }
+  }
+
+  _onKeyUp(event) {
+    if (event.currentTarget.activeElement != document.body) {
+      return;
+    }
+    switch(event.keyCode) {
+      case 87: // w
+        this._keys.forward = false;
+        break;
+      case 65: // a
+        this._keys.left = false;
+        break;
+      case 83: // s
+        this._keys.backward = false;
+        break;
+      case 68: // d
+        this._keys.right = false;
+        break;
+      case 32: // SPACE
+        this._keys.space = false;
+        break;
+      case 16: // SHIFT
+        this._keys.shift = false;
+        break;
+    }
+  }
+};
+
+
+class FiniteStateMachine {
+  constructor() {
+    this._states = {};
+    this.currentState_ = null;
+  }
+
+  _AddState(name, type) {
+    this._states[name] = type;
+  }
+
+  SetState(name) {
+    const prevState = this.currentState_;
+    
+    if (prevState) {
+      if (prevState.Name == name) {
+        return;
+      }
+      prevState.Exit();
+    }
+
+    const state = new this._states[name](this);
+
+    this.currentState_ = state;
+    state.Enter(prevState);
+  }
+
+  Update(timeElapsed, input) {
+    if (this.currentState_) {
+      this.currentState_.Update(timeElapsed, input);
+    }
+  }
+};
+
+
+class CharacterFSM extends FiniteStateMachine {
+  constructor(proxy) {
+    super();
+    this._proxy = proxy;
+    this._Init();
+  }
+
+  _Init() {
+    this._AddState('idle', IdleState);
+    this._AddState('walk', WalkState);
+    this._AddState('run', RunState);
+    this._AddState('dance', DanceState);
+  }
+};
+
+
+class State {
+  constructor(parent) {
+    this._parent = parent;
+  }
+
+  Enter() {}
+  Exit() {}
+  Update() {}
+};
+
+
+class DanceState extends State {
+  constructor(parent) {
+    super(parent);
+
+    this._FinishedCallback = () => {
+      this._Finished();
+    }
+  }
+
+  get Name() {
+    return 'dance';
+  }
+
+  Enter(prevState) {
+    const curAction = this._parent._proxy.animations_['dance'].action;
+    const mixer = curAction.getMixer();
+    mixer.addEventListener('finished', this._FinishedCallback);
+
+    if (prevState) {
+      const prevAction = this._parent._proxy.animations_[prevState.Name].action;
+
+      curAction.reset();  
+      curAction.setLoop(THREE.LoopOnce, 1);
+      curAction.clampWhenFinished = true;
+      curAction.crossFadeFrom(prevAction, 0.2, true);
+      curAction.play();
+    } else {
+      curAction.play();
+    }
+  }
+
+  _Finished() {
+    this._Cleanup();
+    this._parent.SetState('idle');
+  }
+
+  _Cleanup() {
+    const action = this._parent._proxy.animations_['dance'].action;
+    
+    action.getMixer().removeEventListener('finished', this._CleanupCallback);
+  }
+
+  Exit() {
+    this._Cleanup();
+  }
+
+  Update(_) {
+  }
+};
+
+
+class WalkState extends State {
+  constructor(parent) {
+    super(parent);
+  }
+
+  get Name() {
+    return 'walk';
+  }
+
+  Enter(prevState) {
+    const curAction = this._parent._proxy.animations_['walk'].action;
+    if (prevState) {
+      const prevAction = this._parent._proxy.animations_[prevState.Name].action;
+
+      curAction.enabled = true;
+
+      if (prevState.Name == 'run') {
+        const ratio = curAction.getClip().duration / prevAction.getClip().duration;
+        curAction.time = prevAction.time * ratio;
+      } else {
+        curAction.time = 0.0;
+        curAction.setEffectiveTimeScale(1.0);
+        curAction.setEffectiveWeight(1.0);
+      }
+
+      curAction.crossFadeFrom(prevAction, 0.5, true);
+      curAction.play();
+    } else {
+      curAction.play();
+    }
+  }
+
+  Exit() {
+  }
+
+  Update(timeElapsed, input) {
+    if (!input) {
+      return;
+    }
+
+    if (input._keys.forward || input._keys.backward) {
+      if (input._keys.shift) {
+        this._parent.SetState('run');
+      }
+      return;
+    }
+
+    this._parent.SetState('idle');
+  }
+};
+
+
+class RunState extends State {
+  constructor(parent) {
+    super(parent);
+  }
+
+  get Name() {
+    return 'run';
+  }
+
+  Enter(prevState) {
+    const curAction = this._parent._proxy.animations_['run'].action;
+    if (prevState) {
+      const prevAction = this._parent._proxy.animations_[prevState.Name].action;
+
+      curAction.enabled = true;
+
+      if (prevState.Name == 'walk') {
+        const ratio = curAction.getClip().duration / prevAction.getClip().duration;
+        curAction.time = prevAction.time * ratio;
+      } else {
+        curAction.time = 0.0;
+        curAction.setEffectiveTimeScale(1.0);
+        curAction.setEffectiveWeight(1.0);
+      }
+
+      curAction.crossFadeFrom(prevAction, 0.5, true);
+      curAction.play();
+    } else {
+      curAction.play();
+    }
+  }
+
+  Exit() {
+  }
+
+  Update(timeElapsed, input) {
+    if (!input) {
+      return;
+    }
+
+    if (input._keys.forward || input._keys.backward) {
+      if (!input._keys.shift) {
+        this._parent.SetState('walk');
+      }
+      return;
+    }
+
+    this._parent.SetState('idle');
+  }
+};
+
+
+class IdleState extends State {
+  constructor(parent) {
+    super(parent);
+  }
+
+  get Name() {
+    return 'idle';
+  }
+
+  Enter(prevState) {
+    const idleAction = this._parent._proxy.animations_['idle'].action;
+    if (prevState) {
+      const prevAction = this._parent._proxy.animations_[prevState.Name].action;
+      idleAction.time = 0.0;
+      idleAction.enabled = true;
+      idleAction.setEffectiveTimeScale(1.0);
+      idleAction.setEffectiveWeight(1.0);
+      idleAction.crossFadeFrom(prevAction, 0.5, true);
+      idleAction.play();
+    } else {
+      idleAction.play();
+    }
+  }
+
+  Exit() {
+  }
+
+  Update(_, input) {
+    if (!input) {
+      return;
+    }
+    if (input._keys.forward || input._keys.backward) {
+      this._parent.SetState('walk');
+    } else if (input._keys.space) {
+      this._parent.SetState('dance');
+    }
+  }
+};
+
+
+class ThirdPersonCamera {
+  constructor(params) {
+    this.params_ = params;
+    this._camera = params.camera;
+
+    this._currentPosition = new THREE.Vector3();
+    this._currentLookat = new THREE.Vector3();
+  }
+
+  _CalculateIdealOffset() {
+    const idealOffset = new THREE.Vector3(-15, 20, -30);
+    idealOffset.applyQuaternion(this.params_.target.Rotation);
+    idealOffset.add(this.params_.target.Position);
+    return idealOffset;
+  }
+
+  _CalculateIdealLookat() {
+    const idealLookat = new THREE.Vector3(0, 10, 50);
+    idealLookat.applyQuaternion(this.params_.target.Rotation);
+    idealLookat.add(this.params_.target.Position);
+    return idealLookat;
+  }
+
+  Update(timeElapsed) {
+    const idealOffset = this._CalculateIdealOffset();
+    const idealLookat = this._CalculateIdealLookat();
+
+    // const t = 0.05;
+    // const t = 4.0 * timeElapsed;
+    const t = 1.0 - Math.pow(0.001, timeElapsed);
+
+    this._currentPosition.lerp(idealOffset, t);
+    this._currentLookat.lerp(idealLookat, t);
+
+    this._camera.position.copy(this._currentPosition);
+    this._camera.lookAt(this._currentLookat);
+  }
+}
+
+
+class PlayerEntity {
+  constructor(params) {
+    this.params_ = params;
+    this.Init_();
+  }
+
+  Init_() {
+  }
+
+  CreateFromDesc(desc) {
+    const params = {
+      camera: this.params_.camera,
+      scene: this.params_.scene,
+      desc: desc,
+    };
+    this.controls_ = new BasicCharacterController(params);
+
+    this.thirdPersonCamera_ = new ThirdPersonCamera({
+      camera: this.params_.camera,
+      target: this.controls_,
+    });
+
+    this.updateTimer_ = 0.0;
+  }
+
+  UpdateTransform(data) {
+    const s = data[0];
+    const p = data[1];
+    const q = data[2];
+    this.controls_.SetTransform(
+        new THREE.Vector3(...p),
+        new THREE.Quaternion(...q)
+    );
+  }
+
+  Update(timeElapsed) {
+    this.controls_.Update(timeElapsed);
+    this.thirdPersonCamera_.Update(timeElapsed);
+    this.SendTransform_(timeElapsed);
+  }
+
+  SendTransform_(timeElapsed) {
+    this.updateTimer_ -= timeElapsed;
+    if (this.updateTimer_ <= 0.0 && this.controls_.IsLoaded) {
+      this.updateTimer_ = 0.1;
+      this.params_.socket.emit(
+        'world.update',
+        this.controls_.CreateTransformPacket(),
+      );
+    }
+  }
+};
+
+
+class NetworkCharacterController {
+  constructor(params) {
+    this._Init(params);
+  }
+
+  Destroy() {
+    this.name_.Destroy();
+    this.name_ = null;
+    this.target_.Destroy();
+    this.target_ = null;
+  }
+
+  _Init(params) {
+    this.params_ = params;
+
+    this.target_ = new AnimatedMesh({
+        scene: params.scene,
+        desc: params.desc,
+    });
+    this.target_.onLoad = () => {
+      this.stateMachine_ = new CharacterFSM(
+          new BasicCharacterControllerProxy(this.target_.animations_));
+      this.stateMachine_.SetState('idle');
+    }
+
+    this.name_ = new FloatingName({
+        parent: this.target_.group_,
+        desc: params.desc,
+    });
+  }
+
+  get position() {
+    return this.target_.position;
+  }
+
+  get quaternion() {
+    return this.target_.quaternion;
+  }
+
+  SetState(s) {
+    if (!this.stateMachine_) {
+      return;
+    }
+    this.stateMachine_.SetState(s);
+  }
+
+  Update(timeInSeconds) {
+    if (!this.stateMachine_) {
+      return;
+    }
+
+    this.stateMachine_.Update(timeInSeconds, null);
+    this.target_.Update(timeInSeconds);
+  }
+};
+
+
+class NetworkEntity {
+  constructor(params) {
+    this.params_ = params;
+    this.transformUpdates_ = [];
+    this.targetFrame_ = null;
+    this.lastFrame_ = null;
+    this.Init_();
+  }
+
+  Destroy() {
+    this.controller_.Destroy();
+  }
+
+  Init_() {
+  }
+
+  CreateFromDesc(desc, transform) {
+    this.controller_ = new NetworkCharacterController({
+        scene: this.params_.scene,
+        desc: desc,
+    });
+    this.controller_.position.set(...transform[1]);
+    this.controller_.quaternion.set(...transform[2]);
+    this.targetFrame_ = {time: 0.1, transform: transform};
+}
+
+  UpdateTransform(data) {
+    this.transformUpdates_.push({time: 0.1, transform: data});
+  }
+
+  Update(timeElapsed) {
+    this.controller_.Update(timeElapsed);
+
+    this.ApplyLCT_(timeElapsed);
+  }
+
+  ApplyLCT_(timeElapsed) {    
+    if (this.transformUpdates_.length == 0) {
+      return;
+    }
+
+    for (let i = 0; i < this.transformUpdates_.length; ++i) {
+      this.transformUpdates_[i].time -= timeElapsed;
+    }
+
+    while (this.transformUpdates_.length > 0 &&
+        this.transformUpdates_[0].time <= 0.0) {
+      this.lastFrame_ = {
+        transform: [
+          this.targetFrame_.transform[0],
+          this.controller_.position.toArray(),
+          this.controller_.quaternion.toArray()
+        ]
+      };
+      this.targetFrame_ = this.transformUpdates_.shift();
+      this.targetFrame_.time = 0.0;
+    }
+
+    if (this.targetFrame_ && this.lastFrame_) {
+      this.targetFrame_.time += timeElapsed;
+      const p1 = new THREE.Vector3(...this.lastFrame_.transform[1]);
+      const p2 = new THREE.Vector3(...this.targetFrame_.transform[1]);
+      const q1 = new THREE.Quaternion(...this.lastFrame_.transform[2]);
+      const q2 = new THREE.Quaternion(...this.targetFrame_.transform[2]);
+      this.controller_.position.copy(p1);
+      this.controller_.quaternion.copy(q1);
+      const t = Math.max(Math.min(this.targetFrame_.time / 0.1, 1.0), 0.0);
+      this.controller_.position.lerp(p2, t);
+      this.controller_.quaternion.slerp(q2, t);
+      this.controller_.SetState(this.lastFrame_.transform[0]);
+    }
+  }
+}
+
+
+class Chatbox {
+  constructor(params) {
+    this.params_ = params;
+    this.OnChat = () => {};
+    this.Init_(); 
+  }
+
+  Init_() {
+    this.element_ = document.getElementById('chat-input');
+    this.element_.addEventListener(
+        'keydown', (e) => this.OnKeyDown_(e), false);
+  }
+
+  OnKeyDown_(evt) {
+    if (evt.keyCode === 13) {
+      evt.preventDefault();
+      const msg = this.element_.value;
+      if (msg != '') {
+        this.OnChat(msg);
+      }
+      this.element_.value = '';
+    }
+  }
+
+  AddMessage(msg) {
+    const e = document.createElement('div');
+    e.className = 'chat-text';
+    e.innerText = '[' + msg.name + ']: ' + msg.text;
+    const chatElement = document.getElementById('chat-ui-text-area');
+    chatElement.insertBefore(e, document.getElementById('chat-input'));
+  }
+};
+
+
+class BasicMMODemo {
+  constructor() {
+    this._Initialize();
+  }
+
+  _Initialize() {
+    this.threejs_ = new THREE.WebGLRenderer({
+      antialias: true,
+    });
+    this.threejs_.outputEncoding = THREE.sRGBEncoding;
+    this.threejs_.gammaFactor = 2.2;
+    this.threejs_.shadowMap.enabled = true;
+    this.threejs_.shadowMap.type = THREE.PCFSoftShadowMap;
+    this.threejs_.setPixelRatio(window.devicePixelRatio);
+    this.threejs_.setSize(window.innerWidth, window.innerHeight);
+
+    document.getElementById('container').appendChild(
+        this.threejs_.domElement);
+
+    window.addEventListener('resize', () => {
+      this.OnWindowResize_();
+    }, false);
+
+    const fov = 60;
+    const aspect = 1920 / 1080;
+    const near = 1.0;
+    const far = 1000.0;
+    this.camera_ = new THREE.PerspectiveCamera(fov, aspect, near, far);
+    this.camera_.position.set(75, 20, 0);
+
+    this.scene_ = new THREE.Scene();
+
+    let light = new THREE.DirectionalLight(0xFFFFFF, 1.0);
+    light.position.set(20, 100, 10);
+    light.target.position.set(0, 0, 0);
+    light.castShadow = true;
+    light.shadow.bias = -0.001;
+    light.shadow.mapSize.width = 2048;
+    light.shadow.mapSize.height = 2048;
+    light.shadow.camera.near = 0.1;
+    light.shadow.camera.far = 500.0;
+    light.shadow.camera.near = 0.5;
+    light.shadow.camera.far = 500.0;
+    light.shadow.camera.left = 100;
+    light.shadow.camera.right = -100;
+    light.shadow.camera.top = 100;
+    light.shadow.camera.bottom = -100;
+    this.scene_.add(light);
+
+    light = new THREE.AmbientLight(0x101010);
+    this.scene_.add(light);
+
+    const controls = new OrbitControls(
+      this.camera_, this.threejs_.domElement);
+    controls.target.set(0, 20, 0);
+    controls.update();
+
+    const loader = new THREE.CubeTextureLoader();
+    const texture = loader.load([
+        './resources/posx.jpg',
+        './resources/negx.jpg',
+        './resources/posy.jpg',
+        './resources/negy.jpg',
+        './resources/posz.jpg',
+        './resources/negz.jpg',
+    ]);
+    texture.encoding = THREE.sRGBEncoding;
+    this.scene_.background = texture;
+
+    const plane = new THREE.Mesh(
+        new THREE.PlaneGeometry(100, 100, 10, 10),
+        new THREE.MeshStandardMaterial({
+            color: 0x808080,
+          }));
+    plane.castShadow = false;
+    plane.receiveShadow = true;
+    plane.rotation.x = -Math.PI / 2;
+    this.scene_.add(plane);
+
+    this.SetupSocket_();
+
+    this.entities_ = {};
+
+    this.chatbox_ = new Chatbox();
+    this.chatbox_.OnChat = (txt) => { this.OnChat_(txt); };
+
+    this.previousRAF_ = null;
+    this.RAF_();
+  }
+
+  GenerateRandomName_() {
+    const names1 = [
+        'Aspiring', 'Nameless', 'Cautionary', 'Excited',
+        'Modest', 'Maniacal', 'Caffeinated', 'Sleepy',
+        'Passionate', 'Masochistic', 'Aging', 'Pedantic',
+        'Talkative',
+    ];
+    const names2 = [
+        'Coder', 'Mute', 'Giraffe', 'Snowman',
+        'Machinist', 'Fondler', 'Typist',
+        'Noodler', 'Arborist', 'Peeper', 'Ghost',
+    ];
+    const n1 = names1[
+        Math.floor(Math.random() * names1.length)];
+    const n2 = names2[
+        Math.floor(Math.random() * names2.length)];
+    return n1 + ' ' + n2;
+  }
+
+  SetupSocket_() {
+    this.socket_ = io('ws://localhost:3000', {
+        reconnection: false,
+        transports: ['websocket'],
+    });
+
+    this.socket_.on("connect", () => {
+      console.log(this.socket_.id);
+      const randomName = this.GenerateRandomName_();
+      this.socket_.emit('login.commit', randomName);
+    });
+
+    this.socket_.on("disconnect", () => {
+      console.log('DISCONNECTED: ' + this.socket_.id); // undefined
+    });
+
+    this.socket_.onAny((e, d) => {
+      this.OnMessage_(e, d);
+    });
+  }
+
+  OnChat_(txt) {
+    this.socket_.emit('chat.msg', txt);
+  }
+
+  OnMessage_(e, d) {
+    if (e == 'world.player') {
+      this.playerID_ = d.id;
+      const e = new PlayerEntity({
+          scene: this.scene_,
+          camera: this.camera_,
+          socket: this.socket_
+      });
+      e.CreateFromDesc(d.desc);
+      e.UpdateTransform(d.transform);
+      this.entities_[d.id] = e;
+      console.log('entering world: ' + d.id);
+    } else if (e == 'world.update') {
+      const updates = d;
+      const alive = {};
+
+      alive[this.playerID_] = this.entities_[this.playerID_];
+
+      for (let u of updates) {
+        if ('desc' in u) {
+          const e = new NetworkEntity({scene: this.scene_});
+          e.CreateFromDesc(u.desc, u.transform);
+          this.entities_[u.id] = e;
+        } else {
+          this.entities_[u.id].UpdateTransform(u.transform);
+        }
+
+        alive[u.id] = this.entities_[u.id];
+      }
+
+      const dead = [];
+      for (let k in this.entities_) {
+        if (!(k in alive)) {
+          dead.push(this.entities_[k]);
+        }
+      }
+
+      this.entities_ = alive;
+
+      for (let i = 0; i < dead.length; ++i) {
+        dead[i].Destroy();
+      }
+    } else if (e == 'chat.message') {
+      this.chatbox_.AddMessage(d);
+    }
+  }
+
+  OnWindowResize_() {
+    this.camera_.aspect = window.innerWidth / window.innerHeight;
+    this.camera_.updateProjectionMatrix();
+    this.threejs_.setSize(window.innerWidth, window.innerHeight);
+  }
+
+  RAF_() {
+    requestAnimationFrame((t) => {
+      if (this.previousRAF_ == null) {
+        this.previousRAF_ = t;
+      }
+
+      this.Update_((t - this.previousRAF_) * 0.001);
+      this.threejs_.render(this.scene_, this.camera_);
+      this.previousRAF_ = t;
+      this.RAF_();
+    });
+  }
+
+  Update_(timeElapsed) {
+    for (let k in this.entities_) {
+      this.entities_[k].Update(timeElapsed);
+    }
+  }
+}
+
+
+let _APP = null;
+
+window.addEventListener('DOMContentLoaded', () => {
+  _APP = new BasicMMODemo();
+});

BIN
client/resources/background-2.jpg


BIN
client/resources/background-3.png


BIN
client/resources/background.jpg


BIN
client/resources/characters/guard.glb


BIN
client/resources/characters/paladin.glb


+ 1 - 0
client/resources/characters/readme.txt

@@ -0,0 +1 @@
+Free asset from https://www.mixamo.com/.

BIN
client/resources/characters/sorceror.glb


BIN
client/resources/characters/warrok.glb


BIN
client/resources/characters/zombie-guy.glb


BIN
client/resources/characters/zombie.glb


+ 1 - 0
client/resources/icons/readme.txt

@@ -0,0 +1 @@
+Free asset from https://game-icons.net/

BIN
client/resources/icons/ui/backpack.png


BIN
client/resources/icons/ui/health-bar.png


BIN
client/resources/icons/ui/inventory-character.png


BIN
client/resources/icons/ui/skills.png


BIN
client/resources/icons/ui/tied-scroll.png


BIN
client/resources/icons/weapons/hammer-64.png


BIN
client/resources/icons/weapons/pointy-sword-64.png


BIN
client/resources/icons/weapons/pointy-sword.png


BIN
client/resources/icons/weapons/thor-hammer.png


BIN
client/resources/icons/weapons/war-axe-64.png


BIN
client/resources/icons/weapons/war-axe.png


BIN
client/resources/nature/Blends/BirchTree_1.blend


BIN
client/resources/nature/Blends/BirchTree_2.blend


BIN
client/resources/nature/Blends/BirchTree_3.blend


BIN
client/resources/nature/Blends/BirchTree_4.blend


BIN
client/resources/nature/Blends/BirchTree_5.blend


BIN
client/resources/nature/Blends/BirchTree_Autumn_1.blend


BIN
client/resources/nature/Blends/BirchTree_Autumn_2.blend


BIN
client/resources/nature/Blends/BirchTree_Autumn_3.blend


BIN
client/resources/nature/Blends/BirchTree_Autumn_4.blend


BIN
client/resources/nature/Blends/BirchTree_Autumn_5.blend


BIN
client/resources/nature/Blends/BirchTree_Dead_1.blend


BIN
client/resources/nature/Blends/BirchTree_Dead_2.blend


BIN
client/resources/nature/Blends/BirchTree_Dead_3.blend


BIN
client/resources/nature/Blends/BirchTree_Dead_4.blend


BIN
client/resources/nature/Blends/BirchTree_Dead_5.blend


BIN
client/resources/nature/Blends/BirchTree_Dead_Snow_1.blend


BIN
client/resources/nature/Blends/BirchTree_Dead_Snow_2.blend


BIN
client/resources/nature/Blends/BirchTree_Dead_Snow_3.blend


BIN
client/resources/nature/Blends/BirchTree_Dead_Snow_4.blend


BIN
client/resources/nature/Blends/BirchTree_Dead_Snow_5.blend


BIN
client/resources/nature/Blends/BirchTree_Snow_1.blend


BIN
client/resources/nature/Blends/BirchTree_Snow_2.blend


BIN
client/resources/nature/Blends/BirchTree_Snow_3.blend


BIN
client/resources/nature/Blends/BirchTree_Snow_4.blend


BIN
client/resources/nature/Blends/BirchTree_Snow_5.blend


BIN
client/resources/nature/Blends/BushBerries_1.blend


BIN
client/resources/nature/Blends/BushBerries_2.blend


BIN
client/resources/nature/Blends/Bush_1.blend


BIN
client/resources/nature/Blends/Bush_2.blend


BIN
client/resources/nature/Blends/Bush_Snow_1.blend


BIN
client/resources/nature/Blends/Bush_Snow_2.blend


BIN
client/resources/nature/Blends/CactusFlower_1.blend


BIN
client/resources/nature/Blends/CactusFlowers_2.blend


BIN
client/resources/nature/Blends/CactusFlowers_3.blend


BIN
client/resources/nature/Blends/CactusFlowers_4.blend


BIN
client/resources/nature/Blends/CactusFlowers_5.blend


BIN
client/resources/nature/Blends/Cactus_1.blend


BIN
client/resources/nature/Blends/Cactus_2.blend


BIN
client/resources/nature/Blends/Cactus_3.blend


BIN
client/resources/nature/Blends/Cactus_4.blend


BIN
client/resources/nature/Blends/Cactus_5.blend


BIN
client/resources/nature/Blends/CommonTree_1.blend


BIN
client/resources/nature/Blends/CommonTree_2.blend


BIN
client/resources/nature/Blends/CommonTree_3.blend


BIN
client/resources/nature/Blends/CommonTree_4.blend


BIN
client/resources/nature/Blends/CommonTree_5.blend


BIN
client/resources/nature/Blends/CommonTree_Autumn_1.blend


BIN
client/resources/nature/Blends/CommonTree_Autumn_2.blend


BIN
client/resources/nature/Blends/CommonTree_Autumn_3.blend


BIN
client/resources/nature/Blends/CommonTree_Autumn_4.blend


BIN
client/resources/nature/Blends/CommonTree_Autumn_5.blend


BIN
client/resources/nature/Blends/CommonTree_Dead_1.blend


BIN
client/resources/nature/Blends/CommonTree_Dead_2.blend


BIN
client/resources/nature/Blends/CommonTree_Dead_3.blend


BIN
client/resources/nature/Blends/CommonTree_Dead_4.blend


BIN
client/resources/nature/Blends/CommonTree_Dead_5.blend


BIN
client/resources/nature/Blends/CommonTree_Dead_Snow_1.blend


BIN
client/resources/nature/Blends/CommonTree_Dead_Snow_2.blend


BIN
client/resources/nature/Blends/CommonTree_Dead_Snow_3.blend


BIN
client/resources/nature/Blends/CommonTree_Dead_Snow_4.blend


BIN
client/resources/nature/Blends/CommonTree_Dead_Snow_5.blend


BIN
client/resources/nature/Blends/CommonTree_Snow_1.blend


BIN
client/resources/nature/Blends/CommonTree_Snow_2.blend


BIN
client/resources/nature/Blends/CommonTree_Snow_3.blend


BIN
client/resources/nature/Blends/CommonTree_Snow_4.blend


BIN
client/resources/nature/Blends/CommonTree_Snow_5.blend


BIN
client/resources/nature/Blends/Corn_1.blend


BIN
client/resources/nature/Blends/Corn_2.blend


BIN
client/resources/nature/Blends/Flowers.blend


BIN
client/resources/nature/Blends/Grass.blend


BIN
client/resources/nature/Blends/Grass_2.blend


BIN
client/resources/nature/Blends/Grass_Short.blend


BIN
client/resources/nature/Blends/Lilypad.blend


BIN
client/resources/nature/Blends/PalmTree_1.blend


Неке датотеке нису приказане због велике количине промена