hexcraft
ARCHIVED - browser-based 3D hexagonal tile editor built with Three.js
git clone https://git.vogt.world/hexcraft.git
Log | Files | README.md | LICENSE
← All files
name: js/main.js
-rw-r--r--
16817
  1//Scene variables
  2var camera, scene, renderer;
  3var mesh, light;
  4var dirLight, hemiLight;
  5var controls;
  6var delta;
  7var projector, raycaster, intersects;
  8var mouse = new THREE.Vector2(), INTERSECTED;
  9var clock = new THREE.Clock();
 10
 11//Hexagon constants
 12var SIZE = 12;
 13var THICKNESS = 5;
 14var HEIGHT = SIZE*2;
 15var WIDTH = (Math.sqrt(3)/2) * HEIGHT;
 16
 17//Board Constants
 18var xCount = 40;
 19var yCount = 40;
 20var zCount = 40;
 21var board;
 22
 23//Tools
 24var TOOLS = {
 25  add: 0,
 26  remove: 1
 27};
 28var currentTool = TOOLS.add;
 29var currentHexagonPosition = {x:0, y:0, z:0};
 30var phantomHexagon;
 31var hexcount = 0;
 32var currentGame;
 33
 34//Game element to stamp out in the DOM
 35var gameElement = $('.modal-body.list').html();
 36
 37//Colors
 38var COLORS = [0xFFFFFF, 0xFAFAFA, 0xF6F6F6, 0xF1F1F1, 0xEDEDED, 0xE8E8E8];
 39var currentColor = 0xE32818;
 40var sky = 0xBFE6FF;
 41var ground = 0x333333;
 42
 43//Set the draw div to be the height of the window
 44$('#draw').css('height', window.innerWidth - $('.nav').height());
 45
 46init();
 47hud();
 48animate();
 49firstLoad();
 50
 51//Places the heads up display
 52function hud() {
 53  var hud = $('.hud');
 54  hud.css('left', ((window.innerWidth/2) - (hud.width()/2)));
 55  hud.find('.center').css('width', hud.width()-24)
 56  hud.show();
 57}
 58
 59function firstLoad() {
 60  //If this is a first time user, trigger the welcome modal.
 61  if (localStorage.hex == undefined) {
 62    $('#welcomeModal').modal('toggle');
 63  }
 64  //Find which game we'll be saving to
 65  for (var i = 1; i < 30; i++) {
 66    var name = 'game' + i;
 67    if (!localStorage.hasOwnProperty(name)) {
 68      currentGame = name;
 69      break;
 70    }
 71  }
 72}
 73
 74function init() {
 75  //WebGL detection and redirection
 76  if (!Detector.webgl) {
 77    window.location = "http://get.webgl.org";
 78  }
 79
 80  //Camera initialization
 81  camera = new THREE.PerspectiveCamera( 39, window.innerWidth / window.innerHeight, 1, 2000 );
 82  camera.eulerOrder = "YXZ"
 83  camera.position.z = 200;
 84  camera.position.y = 40;
 85  camera.position.x = 100;
 86  camera.lookAt(getHexagonPositionFromLocation(Math.floor(xCount/2), 0, Math.floor(zCount/2)));
 87
 88  //Controls
 89  controls = new THREE.FlyControls(camera);
 90  controls.movementSpeed = 70;
 91  controls.domElement = draw;
 92  controls.rollSpeed = Math.PI / 7;
 93  controls.autoForward = false;
 94  controls.dragToLook = false;
 95
 96  //Scene
 97  scene = new THREE.Scene();
 98  scene.fog = new THREE.Fog(0xffffff, 500, 1200);
 99
100  //Lights
101  hemiLight = new THREE.HemisphereLight( 0xffffff, 0xffffff, 0.6 );
102  hemiLight.color.setHSL( 0.6, 1, 0.6 );
103  hemiLight.groundColor.setHSL( 0.095, 1, 0.75 );
104  hemiLight.position.set( 0, 500, 0 );
105  scene.add( hemiLight );
106  dirLight = new THREE.DirectionalLight( 0xffffff, 1 );
107  dirLight.color.setHSL( 0.1, 1, 0.95 );
108  dirLight.position.set( -1, 1.75, 1 );
109  dirLight.position.multiplyScalar( 50 );
110  scene.add( dirLight );
111  dirLight.castShadow = true;
112  dirLight.shadowMapWidth = 2048;
113  dirLight.shadowMapHeight = 2048;
114  var d = 50;
115  dirLight.shadowCameraLeft = -d;
116  dirLight.shadowCameraRight = d;
117  dirLight.shadowCameraTop = d;
118  dirLight.shadowCameraBottom = -d;
119  dirLight.shadowCameraFar = 3500;
120  dirLight.shadowBias = -0.0001;
121  dirLight.shadowDarkness = 0.35;
122
123
124  //Ground
125  var ground = new THREE.Mesh(new THREE.PlaneGeometry(50000, 50000), new THREE.MeshPhongMaterial({ambient: ground, color: ground, specular: ground}) );
126  ground.name = 'ground';
127  ground.rotation.x = -Math.PI/2;
128  ground.position.y = 0;
129  scene.add( ground );
130  ground.receiveShadow = true;
131
132  //Board to store hexagon colors for loading, saving
133  board = new Board(xCount, zCount, yCount);
134
135  //Initializing floor hexagons
136  for (var x = 0; x < board.width; x++) {
137    for (var z = 0; z < board.depth; z++) {
138      var hexagon = newFloorHexagon(SIZE, THICKNESS);
139      var mesh = new THREE.Mesh(hexagon, new THREE.MeshLambertMaterial(
140        { color: COLORS[Math.floor(Math.random()*6)], shading: THREE.FlatShading, ambient: 0xffffff, wireframe: false }));
141      mesh.castShadow = true;
142      mesh.receiveShadow = true;
143      mesh.boardLocation = {x: x, z: z, y: 0};
144      mesh.name = "floor";
145      board.setHexagon({x: x, z: z, y: 0}, mesh.material.color);
146      scene.add(mesh);
147      mesh.position = getHexagonPositionFromLocation(x, 0, z);
148    }
149  }
150  
151  //Phantom hexagon to act as selector for adding and removing.
152  var hexagon = newHexagon(SIZE, THICKNESS);
153  phantomHexagon = new THREE.Mesh(hexagon, new THREE.MeshNormalMaterial( { color: currentColor, emissive: currentColor, ambient: currentColor, transparent: true, opacity: 0 } ));
154  scene.add(phantomHexagon);
155  phantomHexagon.name = 'phantomHexagon';
156  
157  //Projector and raycaster for instersection with mouse location
158  projector = new THREE.Projector();
159  raycaster = new THREE.Raycaster();
160  
161  //Bind events
162  bindEvents();
163  
164  //Engage the rendered
165  renderer = new THREE.WebGLRenderer( { antialias: false } );
166  renderer.setSize( window.innerWidth, window.innerHeight );
167  draw.appendChild( renderer.domElement );
168  renderer.setClearColor( sky, 1 );
169  renderer.gammaInput = true;
170  renderer.gammaOutput = true;
171  renderer.physicallyBasedShading = true;
172  renderer.shadowMapEnabled = false;
173  renderer.shadowMapCullFace = THREE.CullFaceBack;
174  document.body.appendChild( renderer.domElement );
175}
176
177//Binding common mouse events
178function bindEvents() {
179  //Handle apsect ratios when the user resizes
180  window.addEventListener('resize', onWindowResize, false);
181  //Change location of phantom hexagon on mouse move
182  $('#draw').on('mousemove', onDocumentMouseMove);
183  //Handle mouse clicks
184  $('#draw').on('click', onDocumentMouseClick);
185  
186  //Select 'add hexagon' tool
187  $('.button.add').on('click', function(e) {
188    //remove is not active
189    var button = $('.button.remove');
190    var classes = button.attr('class');
191    classes = classes.replace(' active', '');
192    button.attr('class', classes);
193    //add is active
194    button = $('.button.add');
195    var classes = button.attr('class');
196    if (classes.search(' active') == -1) {
197      classes = classes + ' active';
198      button.attr('class', classes);
199    }
200    currentTool = TOOLS.add;
201  });
202  
203  //Select 'remove hexagon' tool
204  $('.button.remove').on('click', function(e) {
205    //add is not active
206    var button = $('.button.add');
207    var classes = button.attr('class');
208    classes = classes.replace(' active', '');
209    button.attr('class', classes);
210    //remove is active
211    button = $('.button.remove');
212    var classes = button.attr('class');
213    if (classes.search('active') == -1) {
214      classes = classes + ' active';
215      button.attr('class', classes);
216    }
217    currentTool = TOOLS.remove;
218  });
219  
220  //Save the current scene to local storage
221  $('.button.save').on('click', function(e) {
222    localStorage.hex = true;
223    var game = {tiles: board.getActiveTiles(), date: moment().format('MMMM Do YYYY, h:mm a')};
224    localStorage[currentGame] = JSON.stringify(game);
225  });
226  
227  //Trigger the load modal, and populated it with saved games
228  $('.button.load').on('click', function(e) {
229    $('.modal-body.list').empty();
230    for (var i = 1; i < 30; i++) {
231      var name = 'game' + i.toString();
232      if (localStorage.hasOwnProperty(name)) {
233        $('.modal-body.list').append(gameElement);
234        var game = JSON.parse(localStorage[name]);
235        var single = $('.modal-body.list').children().last();
236        single.find('.how-many').text(game.tiles.length.toString() + ' hexagons');
237        single.find('.date').text(game.date);
238        single.find('.loadme a').attr('id', name).on('click', function(event) {
239          currentGame = event.target.id;
240          var game = JSON.parse(localStorage[currentGame]);
241          loadGame(game.tiles);
242          $('#loadModal').modal('hide');
243        });
244      } else {
245        break;
246      }
247    }
248    $('#loadModal').modal('toggle');
249  });
250  
251  //Wipe the current game, start a new localStorage game
252  $('.button.new').on('click', function(e) {
253    clearScene();
254    resetScene();
255    for (var i = 1; i < 30; i++) {
256      var name = 'game' + i;
257      if (!localStorage.hasOwnProperty(name)) {
258        currentGame = name;
259        break;
260      }
261    }
262  });
263  
264  //Show welcome modal for more information
265  $('.button.info').on('click', function(e) {
266    $('#welcomeModal').modal('show');
267  });
268  
269  //Load demo game
270  $('.demo').on('click', function(e) {
271    $('#welcomeModal').modal('hide');
272    clearScene();
273    resetScene();
274    loadGame(demos[event.target.id].tiles);
275  });
276  
277  //Bind mouse wheel for zoom/perspective control
278  $("#draw").bind('mousewheel', function (e) { 
279   if(e.originalEvent.wheelDelta /120 > 0) {
280     if (camera.fov < 60)
281       camera.projectionMatrix.makePerspective( camera.fov += 2, window.innerWidth / window.innerHeight, 1, 2000 );
282   }
283   else {
284     if (camera.fov > 30)
285       camera.projectionMatrix.makePerspective( camera.fov -= 2, window.innerWidth / window.innerHeight, 1, 2000 );
286   }
287  });
288  
289  //Binding the enter key to add a hexagon
290  $(document).on('keypress', function (event) {
291    if (event.keyCode == 13) {
292      switch (currentTool) {
293        case TOOLS.remove:
294          removeHexagon();
295          break;
296        case TOOLS.add:
297          addHexagon();
298          break;
299      }
300    }
301  });
302  
303  //Binding the collor picker
304  $('.picker').colorpicker({color: '#0066cc'})
305  .on('changeColor', function(ev) {
306    currentColor = parseInt(ev.color.toHex().replace('#', '0x'));
307    $('.picker polygon.color').css('fill', currentColor.toString(16));
308    $('.picker polygon.drop').css('fill', currentColor.toString(16));
309  });
310
311
312  //Tool tips
313  $('[data-toggle="tooltip"]').tooltip({
314      'placement': 'bottom'
315  });
316}
317
318//Handling mouse moves
319function onDocumentMouseMove( event ) {
320  //When the mouse is over the game, and not the color picker, hide color picker
321  $('.picker').colorpicker('hide');
322  event.preventDefault();
323  //Capture mouse location
324  mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
325  mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
326}
327
328//Routing mouse click events based on tool
329function onDocumentMouseClick() {
330  switch (currentTool) {
331    case TOOLS.remove:
332      removeHexagon();
333      break;
334    case TOOLS.add:
335      addHexagon();
336      break;
337  }
338}
339
340//Reset the 'floor' hexagons for a new scene
341function resetScene() {
342  for (var x = 0; x < board.width; x++) {
343    for (var z = 0; z < board.depth; z++) {
344      var hexagon = newFloorHexagon(SIZE, THICKNESS);
345      var mesh = new THREE.Mesh(hexagon, new THREE.MeshLambertMaterial(
346        { color: COLORS[Math.floor(Math.random()*6)], shading: THREE.FlatShading, ambient: 0xffffff, wireframe: false }));
347      mesh.castShadow = true;
348      mesh.receiveShadow = true;
349      mesh.boardLocation = {x: x, z: z, y: 0};
350      mesh.name = "floor";
351      board.setHexagon({x: x, z: z, y: 0}, mesh.material.color);
352      scene.add(mesh);
353      mesh.position = getHexagonPositionFromLocation(x, 0, z);
354    }
355  }
356}
357
358//Load a saved game
359function loadGame(tiles) {
360  clearScene();
361  resetScene();
362
363  //Add the hexagons one by one
364  for (var i = 0; i < tiles.length; i++) {
365    var hexagon = newHexagon(SIZE, THICKNESS);
366    var mesh = new THREE.Mesh(hexagon, new THREE.MeshLambertMaterial(
367      { color: tiles[i].color, shading: THREE.FlatShading, ambient: 0xffffff, wireframe: false }));
368    mesh.castShadow = true;
369    mesh.receiveShadow = true;
370    scene.add(mesh);
371    mesh.name = 'tile';
372    mesh.position = getHexagonPositionFromLocation(tiles[i].x, tiles[i].y, tiles[i].z);
373    mesh.boardLocation = {x: tiles[i].x, y: tiles[i].y, z:tiles[i].z};
374    board.setHexagon({x: tiles[i].x, y: tiles[i].y, z:tiles[i].z}, tiles[i].color);
375    
376    updateHexcount(hexcount+1);
377  }
378}
379
380//Clears an entire scene, including the floor.
381function clearScene() {
382  board = new Board(xCount, zCount, yCount);
383  var removeList = [];
384  for (var i = 0; i < scene.children.length; i++) {
385    if (scene.children[i].name == 'tile' || scene.children[i].name == 'floor') {
386      removeList.push(scene.children[i])
387    }
388  }
389  for (var i = 0; i < removeList.length; i++) {
390    scene.remove(removeList[i]);
391  }
392  updateHexcount(0);
393}
394
395//Push the hexagon count to the HUD
396function updateHexcount(number) {
397  hexcount = number;
398  $('.hexcount').text(hexcount + ' hexagons');
399}
400
401//Remove the selected hexagon
402function removeHexagon() {
403  if ((INTERSECTED.name != 'ground' && INTERSECTED.name != 'floor') && intersects[1].object.name !== 'phantomHexagon') {
404    scene.remove(intersects[1].object);
405    phantomHexagon.material.opacity = 0;
406    phantomHexagon.position = new THREE.Vector3(-100, -100, -100);
407    board.unsetHexagon({x: currentHexagonPosition.x, y: currentHexagonPosition.y, z:currentHexagonPosition.z});
408    updateHexcount(hexcount-1);
409  }
410}
411
412//Add a hexagon at the appropriate location
413function addHexagon() {
414  if (INTERSECTED.name != 'ground') {
415    var hexagon = newHexagon(SIZE, THICKNESS);
416    var mesh = new THREE.Mesh(hexagon, new THREE.MeshLambertMaterial(
417      { color: currentColor, shading: THREE.FlatShading, ambient: 0xffffff, wireframe: false }));
418    mesh.castShadow = true;
419    mesh.receiveShadow = true;
420    scene.add(mesh);
421    mesh.name = 'tile';
422    mesh.position = getHexagonPositionFromLocation(currentHexagonPosition.x, currentHexagonPosition.y, currentHexagonPosition.z);
423    mesh.boardLocation = {x: currentHexagonPosition.x, y: currentHexagonPosition.y, z:currentHexagonPosition.z};
424    board.setHexagon({x: currentHexagonPosition.x, y: currentHexagonPosition.y, z:currentHexagonPosition.z}, currentColor);
425    phantomHexagon.position.y += THICKNESS;
426    currentHexagonPosition.y++;
427    updateHexcount(hexcount+1);
428  }
429}
430
431//Places the phantomHexagon at the appropriate location, and keeps track of the foremost intersected hexagon
432function selector() {
433  var mouseVector = new THREE.Vector3(mouse.x, mouse.y, 1);
434  projector.unprojectVector(mouseVector, camera);
435  raycaster.set(camera.position, mouseVector.sub(camera.position).normalize());
436  intersects = raycaster.intersectObjects(scene.children);
437  if (intersects.length > 0) {
438    INTERSECTED = intersects[0].object;
439    if (INTERSECTED.name != 'ground' && INTERSECTED.name != 'phantomHexagon') {
440      switch (currentTool) {
441        case TOOLS.remove:
442          phantomHexagon.material.opacity = 1;
443          setPhantomPosition(INTERSECTED.position.x, INTERSECTED.position.y, INTERSECTED.position.z);
444          currentHexagonPosition = INTERSECTED.boardLocation;
445          break;
446        case TOOLS.add:
447          var topY = board.topMostHexagon({x: INTERSECTED.boardLocation.x, y: INTERSECTED.boardLocation.y, z: INTERSECTED.boardLocation.z});
448          phantomHexagon.material.opacity = 0.5;
449          phantomHexagon.position = getHexagonPositionFromLocation(INTERSECTED.boardLocation.x, topY, INTERSECTED.boardLocation.z);
450          currentHexagonPosition = {x: INTERSECTED.boardLocation.x, y: topY, z:INTERSECTED.boardLocation.z};
451          break;
452      }
453    }
454    if (INTERSECTED.name == 'ground') {
455      phantomHexagon.material.opacity = 0;
456    }
457  } else {
458    INTERSECTED = null;
459    phantomHexagon.material.opacity = 0;
460  }
461}
462
463//Set the phantomHexagons board location
464function setPhantomPosition(x, y, z) {
465  phantomHexagon.position.x = x;
466  phantomHexagon.position.y = y;
467  phantomHexagon.position.z = z;
468}
469
470//Get a hexagons real-world position based upon its board location
471function getHexagonPositionFromLocation(x, y, z) {
472  var position = new THREE.Vector3();
473  if (z % 2 == 0) {
474    position.x = x*WIDTH;
475    position.z = z*0.75*HEIGHT;
476  } else {
477    position.x = x*WIDTH + 0.5*WIDTH;
478    position.z = z*0.75*HEIGHT;
479  }
480  position.y = y*THICKNESS;
481  return position;
482}
483
484//Ensures the user doesn't fly the camera below the board, or too far in any direction.
485function checkCameraBoundries() {
486  // //Y Boundries.
487  if (camera.position.y < 10) {
488    camera.position.y = 10;
489  } else {
490    if (camera.position.y > 720) {
491      camera.position.y = 720;
492    }
493  }
494  //X Boundries
495  if (camera.position.x < -350) {
496    camera.position.x = -350;
497  } else {
498    if (camera.position.x > 1300) {
499      camera.position.x = 1300;
500    }
501  }
502  //Z Boundries
503  if (camera.position.z < -350) {
504    camera.position.z = -350;
505  } else {
506    if (camera.position.z > 1300) {
507      camera.position.z = 1300;
508    }
509  }
510}
511
512//When the user resizes the window, we handle the camera's aspect ratio
513function onWindowResize() {
514  camera.aspect = window.innerWidth / window.innerHeight;
515  camera.updateProjectionMatrix();
516  renderer.setSize( window.innerWidth, window.innerHeight );
517}
518
519//Perform selector update, control update, camera boundry checks, and render
520function animate() {
521  selector();
522  delta = clock.getDelta();
523  controls.update(delta);
524  checkCameraBoundries();
525  requestAnimationFrame(animate);
526  render();
527}
528
529//Render
530function render() {
531  renderer.render(scene, camera);
532}