Simple Map Editor - Knockout Version

A simple map editor for grid-based systems. The assumes that you're doing it yourself and isn't using anything like ROT for instance.

by Sam Fereday

HTML

<div id="preview" data-bind="css: { foundationEnabled: !propMode(), foundationDisabled: propMode() }">
  <span data-bind="foreach: grid">
    <div class="sq" data-bind="style: positionCSS, css: { occupied: occupied() }, click: !$parent.propMode() ? clicked : null, text: decorType()"></div>
  </span>
</div>

<div id="prop-preview" data-bind="css: { foundationEnabled: propMode(), foundationDisabled: !propMode() }">
  <span data-bind="foreach: propGrid">
    <div class="sq" data-bind="style: positionCSS, css: { occupied: occupied() }, click: $parent.propMode() ? clicked : null, text: decorType()"></div>
  </span>
</div>

<div id="ui">

  <form id="entities" data-bind="css: { enabled: !propMode() }">
    <p><strong>Foundation Tile Types</strong></p>
    <span>
    <input type="radio" name="entity" value="block" data-bind="checked: tileGraphic, enable: !propMode()"> Block
    </span>
  </form>

  <p><strong>Placement mode:</strong>
    <input type="checkbox" data-bind="checked: propMode, click: changeDefault" />
  </p>
  <form id="entities" data-bind="css: { enabled: propMode() }">
    <p><strong>Entity Tile Types</strong></p>
    <span>
    <input type="radio" name="entity" value="enemy" data-bind="checked: tileGraphic, enable: propMode"> Enemy
  </span>
    <span>
    <input type="radio" name="entity" value="chest" data-bind="checked: tileGraphic, enable: propMode"> Chest
  </span>
  </form>

  <p>
    <strong>Export data:</strong>
  </p>
  <button data-bind="click: exportData">Export</button>
  <p>
    <strong>Exported data output:</strong>
  </p>
  <div id="output" contenteditable="true" data-bind="text: exportedData"></div>

</div>

SCSS

$tileWidth: 32;
$tileHeight: 32;
$tileNum: 6;
body {
  font: 75%/1.4em arial;
}

.sq {
  width: $tileWidth + 'px';
  height: $tileHeight + 'px';
  position: absolute;
  background: #333;
  cursor: pointer;
  text-align: center;
}

.sq:hover {
  background: #555;
}

.sq.occupied {
  background: green;
}

#ui {
  width: 100%;
  clear: both;
  padding-top: $tileHeight * $tileNum + 32 + 'px';
}

form {
  opacity: 0.5;
}

.enabled {
  opacity: 1;
}

form span {
  display: block;
  clear: both;
  width: 100%;
}

#preview,
#prop-preview {
  // width: 100%;
  height: $tileHeight * $tileNum + 'px';
  position: absolute;
  top: 2em;
  left: 2em;
}

#prop-preview {
  .sq {
    background: #EEE;
  }
}

// Needs sorting a bit
.foundationDisabled {
  opacity: 0.5;
  z-index: 0;
}

.foundationEnabled {
  opacity: 0.5;
  z-index: 999;
}

#output {
  max-height: 200px;
  border: 1px solid #ccc;
  padding: 1em;
  overflow-y: auto;
}

JavaScript

// Tile graphical types
var tileTypes = {
	unset: 0,
  block: 1,
  enemy: 2,
  chest: 3
};

// Tile type data (contains all graphical info, etc) - Expects each eType to always differ!
var tileStaticData = [{
  name: "block",
  eType: tileTypes.block, // Corresponds to enums below.
  asset: "../blockimg.jpg"
},
{
  name: "enemy",
  eType: tileTypes.enemy, // Corresponds to enums below.
  asset: "../enemyimg.jpg"
}];

// Helpers, for when there's nowhere else to put these functions
var helpers = {
  guid: function() {
    function s4() {
      return Math.floor((1 + Math.random()) * 0x10000)
        .toString(16)
        .substring(1);
    }
    return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
      s4() + '-' + s4() + s4() + s4();
  },
  getDataTileName: function(eType) {

    var found = tileStaticData.find(function(data) {
      return data.eType === eType;
    });
    
    return found ? found.name : null;

  }
}

// Components / models for mapper to use
var TileModel = function(x, y, occupied, d, parent) {
  this.w = 32;
  this.h = 32;
  this.x = ko.observable(x);
  this.y = ko.observable(y);
  this.occupied = ko.observable(occupied);
  this.decorType = ko.observable(d);
  this.guid = ko.observable(helpers.guid());
  this.parent = parent;
  this.layer = null;

  // No idea how this works.
  // http://stackoverflow.com/questions/18560815/can-you-add-knockoutjs-style-binding-specifying-propertyvalue-together
  this.positionCSS = ko.computed(function() {
    return {
      "left": this.x() * this.w + "px",
      "top": this.y() * this.h + "px"
    };
  }, this);

};

TileModel.prototype.clicked = function(data, event) {

  // Not entirely sure if this is allowed or if it buggers the memory...
  this.occupied(!this.occupied());

  // This should be pre-determined by what you select from the menu. If you're not in placement mode, it should revert to using the default block graphic (or set of)
  this.decorType(tileTypes[data.parent.tileGraphic()]);

}

//...