The JavaScript below implements a simple engine for allowing an animated character to move around a map made of tiles. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.
This code was originally intended to be the first steps in implementing the exploration engine for a role-playing game.
<html> <body> <div id="divMain"></div> <script type="text/javascript"> // main function main() { var display = new Display(new Coords(200, 200)); var mapCellSizeInPixels = new Coords(16, 16); var world = World.demo(display.sizeInPixels, mapCellSizeInPixels); var universe = new Universe ( 10, // timerTicksPerSecond display, world ); universe.start(); } // extensions function ArrayExtensions() { // Extension class. } { Array.prototype.addLookups = function(keyName) { for (var i = 0; i < this.length; i++) { var element = this[i]; var key = element[keyName]; this[key] = element; } return this; } Array.prototype.clone = function() { var returnValues = []; for (var i = 0; i < this.length; i++) { var element = this[i]; var elementCloned = element.clone(); returnValues.push(elementCloned); } return returnValues; } Array.prototype.remove = function(element) { var elementIndex = this.indexOf(element); if (elementIndex >= 0) { this.splice(elementIndex, 1); } return this; } } // classes function Activity(defnName, target) { this.defnName = defnName; this.target = target; } { Activity.prototype.defn = function(world) { return world.activityDefns[this.defnName]; } Activity.prototype.perform = function(universe, world, venue, actor) { this.defn(world).perform(universe, world, venue, actor, this); } } function ActivityDefn(name, perform) { this.name = name; this.perform = perform; } function Camera(viewSize, pos) { this.viewSize = viewSize; this.pos = pos; this.viewSizeHalf = this.viewSize.clone().half(); } function Color(name, code, componentsRGBA) { this.name = name; this.code = code; this.componentsRGBA = componentsRGBA; this.systemColor = "rgba(" + Math.floor(this.componentsRGBA[0] * Color.ComponentMax) + "," + Math.floor(this.componentsRGBA[1] * Color.ComponentMax) + "," + Math.floor(this.componentsRGBA[2] * Color.ComponentMax) + "," + this.componentsRGBA[3] // ? + ")"; } { Color.ComponentMax = 255; Color.Instances = function() { if (Color._instances == null) { Color._instances = new Color_Instances(); } return Color._instances; } function Color_Instances() { this._Transparent = new Color("Transparent", ".", [0, 0, 0, 0]); this.Black = new Color("Black", "k", [0, 0, 0, 1]); this.Blue = new Color("Blue", "b", [0, 0, 1, 1]); this.Cyan = new Color("Cyan", "c", [0, 1, 1, 1]); this.Gray = new Color("Gray", "a", [.5, .5, .5, 1]); this.GrayDark = new Color("GrayDark", "A", [.25, .25, .25, 1]); this.GrayLight = new Color("GrayLight", "-", [.75, .75, .75, 1]); this.Green = new Color("Green", "g", [0, 1, 0, 1]); this.Orange = new Color("Orange", "o", [1, .5, 0, 1]); this.Red = new Color("Red", "r", [1, 0, 0, 1]); this.Violet = new Color("Violet", "v", [1, 0, 1, 1]); this.White = new Color("White", "w", [1, 1, 1, 1]); this.Yellow = new Color("Yellow", "y", [1, 1, 0, 1]); this._All = [ this._Transparent, this.Black, this.Blue, this.Cyan, this.Gray, this.GrayDark, this.GrayLight, this.Green, this.Orange, this.Red, this.Violet, this.White, this.Yellow, ].addLookups("code"); } } function Constraint_Follow(target) { this.target = target; } { Constraint_Follow.prototype.apply = function(constrainable) { constrainable.pos.overwriteWith(this.target.pos); } } function Coords(x, y) { this.x = x; this.y = y; } { Coords.prototype.add = function(other) { this.x += other.x; this.y += other.y; return this; } Coords.prototype.addXY = function(x, y) { this.x += x; this.y += y; return this; } Coords.prototype.ceiling = function() { this.x = Math.ceil(this.x); this.y = Math.ceil(this.y); return this; } Coords.prototype.clear = function() { this.x = 0; this.y = 0; return this; } Coords.prototype.clone = function() { return new Coords(this.x, this.y); } Coords.prototype.divide = function(other) { this.x /= other.x; this.y /= other.y; return this; } Coords.prototype.divideScalar = function(scalar) { this.x /= scalar; this.y /= scalar; return this; } Coords.prototype.floor = function() { this.x = Math.floor(this.x); this.y = Math.floor(this.y); return this; } Coords.prototype.half = function() { return this.divideScalar(2); } Coords.prototype.isInRangeMax = function(max) { var returnValue = ( this.x >= 0 && this.x <= max.x && this.y >= 0 && this.y <= max.y ); return returnValue; } Coords.prototype.magnitude = function() { return Math.sqrt(this.x * this.x + this.y * this.y); } Coords.prototype.multiply = function(other) { this.x *= other.x; this.y *= other.y; return this; } Coords.prototype.multiplyScalar = function(scalar) { this.x *= scalar; this.y *= scalar; return this; } Coords.prototype.overwriteWith = function(other) { this.x = other.x; this.y = other.y; return this; } Coords.prototype.overwriteWithXY = function(x, y) { this.x = x; this.y = y; return this; } Coords.prototype.round = function() { this.x = Math.round(this.x); this.y = Math.round(this.y); return this; } Coords.prototype.subtract = function(other) { this.x -= other.x; this.y -= other.y; return this; } Coords.prototype.trimToRangeMax = function(max) { if (this.x < 0) { this.x = 0; } else if (this.x > max.x) { this.x = max.x; } if (this.y < 0) { this.y = 0; } else if (this.y > max.y) { this.y = max.y; } return this; } } function Display(sizeInPixels) { this.sizeInPixels = sizeInPixels; this.fontSizeInPixels = Math.floor(this.sizeInPixels.y / 32); this.sizeInPixelsHalf = this.sizeInPixels.clone().half(); this.drawPos = new Coords(); } { Display.prototype.initialize = function() { this.canvas = document.createElement("canvas"); this.canvas.width = this.sizeInPixels.x; this.canvas.height = this.sizeInPixels.y; this.graphics = this.canvas.getContext("2d"); this.graphics.font = this.fontSizeInPixels + "px sans-serif"; return this; } Display.prototype.toImage = function(name) { var dataURL = this.canvas.toDataURL(); var systemImage = document.createElement("img"); systemImage.src = dataURL; var returnValue = new Image(name, this.sizeInPixels, systemImage); return returnValue; } // drawing Display.prototype.clear = function() { this.graphics.fillStyle = "White"; this.graphics.fillRect(0, 0, this.sizeInPixels.x, this.sizeInPixels.y); this.graphics.strokeStyle = "Gray"; this.graphics.strokeRect(0, 0, this.sizeInPixels.x, this.sizeInPixels.y); return this; } Display.prototype.clearRectangle = function(pos, size) { this.graphics.clearRect(pos.x, pos.y, size.x, size.y); return this; } Display.prototype.drawCircle = function(center, radius, colorFill, colorBorder) { this.graphics.beginPath(); this.graphics.arc(center.x, center.y, radius, 0, Polar.RadiansPerTurn); if (colorFill != null) { this.graphics.fillStyle = colorFill; this.graphics.fill(); } if (colorBorder != null) { this.graphics.strokeStyle = colorBorder; this.graphics.stroke(); } return this; } Display.prototype.drawImage = function(image, pos) { this.graphics.drawImage(image.systemImage, pos.x, pos.y); return this; } Display.prototype.drawImageRegion = function(image, sourcePos, sourceSize, targetPos) { var targetSize = sourceSize; this.graphics.drawImage ( image.systemImage, sourcePos.x, sourcePos.y, sourceSize.x, sourceSize.y, targetPos.x, targetPos.y, targetSize.x, targetSize.y ); return this; } Display.prototype.drawPolygon = function(vertices, colorFill, colorBorder) { this.graphics.beginPath(); var vertex = vertices[0]; this.graphics.moveTo(vertex.x, vertex.y); for (var i = 1; i < vertices.length; i++) { vertex = vertices[i]; this.graphics.lineTo(vertex.x, vertex.y); } this.graphics.closePath(); if (colorFill != null) { this.graphics.fillStyle = colorFill; this.graphics.fill(); } if (colorBorder != null) { this.graphics.strokeStyle = colorBorder; this.graphics.stroke(); } return this; } Display.prototype.drawRectangle = function(pos, size, colorFill, colorBorder) { if (colorFill != null) { this.graphics.fillStyle = colorFill; this.graphics.fillRect(pos.x, pos.y, size.x, size.y); } if (colorBorder != null) { this.graphics.strokeStyle = colorBorder; this.graphics.strokeRect(pos.x, pos.y, size.x, size.y); } return this; } Display.prototype.drawText = function(text, pos, colorFill, colorBorder) { if (colorBorder != null) { this.graphics.strokeStyle = color; this.graphics.strokeString(text, pos.x, pos.y); } if (colorFill != null) { this.graphics.fillStyle = color; this.graphics.fillString(text, pos.x, pos.y); } return this; } } function Image(name, size, systemImage) { this.name = name; this.size = size; this.systemImage = systemImage; this.sizeHalf = this.size.clone().half(); } { Image.fromStrings = function(name, colors, pixelsAsStrings) { var size = new Coords ( pixelsAsStrings[0].length, pixelsAsStrings.length ); var canvas = document.createElement("canvas"); canvas.width = size.x; canvas.height = size.y; var graphics = canvas.getContext("2d"); for (var y = 0; y < size.y; y++) { var pixelRowAsString = pixelsAsStrings[y]; for (var x = 0; x < size.x; x++) { var pixelColorCode = pixelRowAsString[x]; var pixelColor = colors[pixelColorCode]; graphics.fillStyle = pixelColor.systemColor; graphics.fillRect(x, y, 1, 1); } } var systemImage = document.createElement("img"); systemImage.src = canvas.toDataURL(); var returnValue = new Image ( name, size, systemImage ); return returnValue; } Image.prototype.toDisplay = function() { return new Display(this.size).initialize().drawImage(this, new Coords(0, 0)); } } function InputHelper() { this.inputsPressed = []; this.inputsActive = []; } { InputHelper.prototype.initialize = function() { document.body.onkeydown = this.handleEventKeyDown.bind(this); document.body.onkeyup = this.handleEventKeyUp.bind(this); } InputHelper.prototype.inputActivate = function(input) { if (this.inputsActive[input] == null) { this.inputsActive[input] = input; this.inputsActive.push(input); } } InputHelper.prototype.inputAdd = function(input) { if (this.inputsPressed[input] == null) { this.inputsPressed[input] = input; this.inputsPressed.push(input); this.inputActivate(input); } } InputHelper.prototype.inputInactivate = function(input) { if (this.inputsActive[input] != null) { delete this.inputsActive[input]; this.inputsActive.remove(input); } } InputHelper.prototype.inputRemove = function(input) { this.inputInactivate(input); if (this.inputsPressed[input] != null) { delete this.inputsPressed[input]; this.inputsPressed.remove(input); } } // events InputHelper.prototype.handleEventKeyDown = function(event) { this.inputAdd(event.key); } InputHelper.prototype.handleEventKeyUp = function(event) { this.inputRemove(event.key); } } function Map(cellSizeInPixels, terrains, cellsAsStrings) { this.cellSizeInPixels = cellSizeInPixels; this.terrains = terrains.addLookups("code"); this.cellsAsStrings = cellsAsStrings; this.sizeInCells = new Coords ( cellsAsStrings[0].length, cellsAsStrings.length ); this.sizeInCellsMinusOnes = this.sizeInCells.clone().addXY ( -1, -1 ); this.cellPos = new Coords(); this.drawPos = new Coords(); } { Map.prototype.terrainAtPosInCells = function(posInCells) { var terrainCode = this.cellsAsStrings[posInCells.y][posInCells.x]; var terrain = this.terrains[terrainCode]; return terrain; } // drawable Map.prototype.draw = function(universe, world, display, visualCamera) { var cellPos = this.cellPos; var drawPos = this.drawPos; var cell = {}; cell.pos = drawPos; cell.velInCellsPerTick = new Coords(0, 0); // hack var halves = new Coords(.5, .5); var ones = new Coords(1, 1); var camera = visualCamera.camera; var cameraPos = camera.pos; var cameraViewSizeHalf = camera.viewSizeHalf; var cellPosMin = cameraPos.clone().subtract ( cameraViewSizeHalf ).divide ( this.cellSizeInPixels ).floor().trimToRangeMax ( this.sizeInCellsMinusOnes ); var cellPosMax = cameraPos.clone().add ( cameraViewSizeHalf ).divide ( this.cellSizeInPixels ).ceiling().trimToRangeMax ( this.sizeInCellsMinusOnes ); for (var y = cellPosMin.y; y <= cellPosMax.y; y++) { cellPos.y = y; for (var x = cellPosMin.x; x <= cellPosMax.x; x++) { cellPos.x = x; drawPos.overwriteWith ( cellPos ).add(halves).multiply ( this.cellSizeInPixels ); var terrain = this.terrainAtPosInCells(cellPos); var terrainVisual = terrain.visual; visualCamera.child = terrainVisual; visualCamera.draw ( universe, world, display, cell ); } } var sizeDiminished = this.sizeInCellsMinusOnes.clone().addXY(-1, -1); var cornerPosMin = cellPosMin.clone().addXY(-1, -1).trimToRangeMax(this.sizeInCells); var cornerPosMax = cellPosMax.trimToRangeMax(sizeDiminished); var cornerPos = new Coords(); var neighborOffsets = [ new Coords(0, 0), new Coords(1, 0), new Coords(0, 1), new Coords(1, 1), ]; var neighborPos = new Coords(); var neighborTerrains = []; for (var y = cornerPosMin.y; y <= cornerPosMax.y; y++) { cornerPos.y = y; for (var x = cornerPosMin.x; x <= cornerPosMax.x; x++) { cornerPos.x = x; var neighborOffset = neighborOffsets[0]; neighborPos.overwriteWith(cornerPos).add(neighborOffset); var terrainHighestSoFar = this.terrainAtPosInCells(neighborPos); for (var n = 1; n < neighborOffsets.length; n++) { var neighborOffset = neighborOffsets[n]; neighborPos.overwriteWith(cornerPos).add(neighborOffset); var neighborTerrain = this.terrainAtPosInCells(neighborPos); var zLevelDifference = neighborTerrain.zLevelForOverlays - terrainHighestSoFar.zLevelForOverlays; if (zLevelDifference > 0) { terrainHighestSoFar = neighborTerrain; } } var terrainHighest = terrainHighestSoFar; var visualChildIndexSoFar = 0; for (var n = 0; n < neighborOffsets.length; n++) { var neighborOffset = neighborOffsets[n]; neighborPos.overwriteWith(cornerPos).add(neighborOffset); var neighborTerrain = this.terrainAtPosInCells(neighborPos); if (neighborTerrain != terrainHighest) { visualChildIndexSoFar |= (1 << n); } } if (visualChildIndexSoFar > 0) { drawPos.overwriteWith ( cornerPos ).add(ones).multiply ( this.cellSizeInPixels ); var terrainVisual = terrainHighest.visual.children[visualChildIndexSoFar]; if (terrainVisual != null) // hack { visualCamera.child = terrainVisual; visualCamera.draw ( universe, world, display, cell ); } } } } } } function MapTerrain(name, code, blocksMovement, zLevelForOverlays, visual) { this.name = name; this.code = code; this.blocksMovement = blocksMovement; this.zLevelForOverlays = zLevelForOverlays; this.visual = visual; } function MapTerrainVisual(children) { this.children = children; var childNames = MapTerrainVisual.ChildNames; for (var i = 0; i < childNames.length; i++) { var childName = childNames[i]; var child = this.children[i]; this.children[childName] = child; } } { MapTerrainVisual.TestInstance = function() { // Helpful for debugging. var radius = 3; var size = new Coords(5, 5); return new MapTerrainVisual ( [ new VisualRectangle(size, null, "Black"), // 0000 - center new VisualCircle(radius, "Red", "Black"), // 0001 - inside se new VisualCircle(radius, "Orange", "Black"), // 0010 - inside sw new VisualCircle(radius, "Yellow","Black"), // 0011 - edge n new VisualCircle(radius, "Green", "Black"), // 0100 - inside ne new VisualCircle(radius, "Blue", "Black"), // 0101 - edge w new VisualCircle(radius, "Violet", "Black"), // 0110 - diagonal new VisualCircle(radius, "Gray", "Black"), // 0111 - outside se new VisualRectangle(size, "Red", "Black"), // 1000 - inside nw new VisualRectangle(size, "Orange", "Black"), // 1001 - diagonal? new VisualRectangle(size, "Yellow", "Black"), // 1010 - edge e new VisualRectangle(size, "Green", "Black"), // 1011 - outside sw new VisualRectangle(size, "Blue", "Black"), // 1100 - edge s new VisualRectangle(size, "Violet", "Black"), // 1101 - outside ne new VisualRectangle(size, "Gray", "Black"), // 1110 - outside nw new VisualRectangle(size, null, "Red"), // 1111 // Never ] ); } MapTerrainVisual.ChildNames = [ "Center", "InsideSE", "InsideSW", "EdgeN", "InsideNE", "EdgeW", "DiagonalSlash", "OutsideSE", "InsideNW", "DiagonalBackslash", "EdgeE", "OutsideSW", "EdgeS", "OutsideNE", "OutsideNW" ] MapTerrainVisual.prototype.draw = function(universe, world, display, drawable) { this.children["Center"].draw(universe, world, display, drawable); } } function Mover(name, visual, activity, posInCells) { this.name = name; this.visual = visual; this.activity = activity; this.posInCells = posInCells; this.pos = new Coords(); this.posInCellsNext = new Coords(); this.posInCellsNextFloor = new Coords(); this.velInCellsPerTick = new Coords(0, 0); } { Mover.prototype.updateForTimerTick = function(universe, world, venue) { this.posInCellsNext.overwriteWith ( this.posInCells ).add ( this.velInCellsPerTick ); var map = venue.map; this.posInCellsNextFloor.overwriteWith(this.posInCellsNext).floor(); var mapTerrain = map.terrainAtPosInCells(this.posInCellsNextFloor); if (mapTerrain.blocksMovement == false) { this.posInCells.overwriteWith(this.posInCellsNext); } this.pos.overwriteWith ( this.posInCells ).multiply ( map.cellSizeInPixels ); this.activity.perform(universe, world, venue, this); } // drawable Mover.prototype.draw = function(universe, world, visualCamera) { visualCamera.child = this.visual; visualCamera.draw(universe, world, universe.display, this); } } function Polar(azimuthInTurns, radius) { this.azimuthInTurns = azimuthInTurns; this.radius = radius; } { Polar.RadiansPerTurn = Math.PI * 2.0; Polar.prototype.fromCoords = function(coords) { var azimuthInRadians = Math.atan2(coords.y, coords.x); var azimuthInTurns = azimuthInRadians / Polar.RadiansPerTurn; if (azimuthInTurns < 0) { azimuthInTurns += 1; } this.azimuthInTurns = azimuthInTurns; this.radius = coords.magnitude(); return this; } } function Portal(defnName, posInCells, destinationVenueName, destinationPosInCells) { this.defnName = defnName; this.posInCells = posInCells.addXY(.5, .5); this.destinationVenueName = destinationVenueName; this.destinationPosInCells = destinationPosInCells; this.pos = new Coords(); } { Portal.prototype.defn = function(world) { return world.portalDefns[this.defnName]; } Portal.prototype.activate = function(universe, world, venue, actor) { var mover = actor; venue.moversToRemove.push(mover); var venueNext = world.venues[this.destinationVenueName]; venueNext.movers.splice(0, 0, mover); mover.posInCells.overwriteWith(this.destinationPosInCells); world.venueNext = venueNext; } Portal.prototype.updateForTimerTick = function(universe, world, venue) { this.pos.overwriteWith(this.posInCells).multiply(venue.map.cellSizeInPixels); } // drawable Portal.prototype.draw = function(universe, world, visualCamera) { var defn = this.defn(world); var visual = defn.visual; visualCamera.child = visual; visualCamera.draw(universe, world, universe.display, this); } } function PortalDefn(name, visual) { this.name = name; this.visual = visual; } function Universe(timerTicksPerSecond, display, world) { this.timerTicksPerSecond = timerTicksPerSecond; this.display = display; this.world = world; this.secondsPerTimerTick = 1 / this.timerTicksPerSecond; } { Universe.prototype.start = function() { var divMain = document.getElementById("divMain"); divMain.appendChild(this.display.initialize().canvas); var timerTicksPerSecond = 10; var msPerSecond = 1000; var msPerTimerTick = Math.floor(msPerSecond / timerTicksPerSecond); this.timer = setInterval ( this.updateForTimerTick.bind(this), msPerTimerTick ); this.inputHelper = new InputHelper(); this.inputHelper.initialize(); this.world.initialize(this); } Universe.prototype.updateForTimerTick = function() { this.world.draw(this); this.world.updateForTimerTick(this); } } function VisualAnimation(framesPerSecond, frames) { this.framesPerSecond = framesPerSecond; this.frames = frames; this.durationInSeconds = this.frames.length / this.framesPerSecond; } { VisualAnimation.prototype.draw = function(universe, world, display, drawable) { if (drawable.secondsSinceAnimationStarted == null) { drawable.secondsSinceAnimationStarted = 0; } var frameIndexCurrent = Math.floor ( drawable.secondsSinceAnimationStarted * this.framesPerSecond ); var frameCurrent = this.frames[frameIndexCurrent]; frameCurrent.draw(universe, world, display, drawable); drawable.secondsSinceAnimationStarted += universe.secondsPerTimerTick; if (drawable.secondsSinceAnimationStarted >= this.durationInSeconds) { drawable.secondsSinceAnimationStarted -= this.durationInSeconds; } } } function VisualCamera(camera, child) { this.camera = camera; this.child = child; this.drawablePosOriginal = new Coords(); } { VisualCamera.prototype.draw = function(universe, world, display, drawable) { this.drawablePosOriginal.overwriteWith(drawable.pos); drawable.pos.subtract ( this.camera.pos ).add ( display.sizeInPixelsHalf ); this.child.draw(universe, world, display, drawable); drawable.pos.overwriteWith(this.drawablePosOriginal); } } function VisualCircle(radius, colorFill, colorBorder) { this.radius = radius; this.colorFill = colorFill; this.colorBorder = colorBorder; } { VisualCircle.prototype.draw = function(universe, world, display, drawable) { display.drawCircle(drawable.pos, this.radius, this.colorFill, this.colorBorder); } } function VisualDirectional(visualAtRest, visualsForDirections) { this.visualAtRest = visualAtRest; this.visualsForDirections = visualsForDirections; this.polar = new Polar(); } { VisualDirectional.prototype.draw = function(universe, world, display, drawable) { var visualToDraw = null; var vel = drawable.velInCellsPerTick; if (vel.magnitude() == 0) { visualToDraw = this.visualAtRest; } else { this.polar.fromCoords(vel); var azimuthInTurns = this.polar.azimuthInTurns; var directionIndex = Math.floor(azimuthInTurns * this.visualsForDirections.length); visualToDraw = this.visualsForDirections[directionIndex]; } visualToDraw.draw(universe, world, display, drawable); } } function VisualGroup(children) { this.children = children; } { VisualGroup.prototype.draw = function(universe, world, display, drawable) { for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; child.draw(universe, world, display, drawable); } } } function VisualImage(image, size) { this.image = image; this.size = (size == null ? this.image.size : size); this.sizeHalf = this.size.clone().half(); this.drawPos = new Coords(); } { VisualImage.manyFromImages = function(images) { var returnValues = []; for (var i = 0; i < images.length; i++) { var image = images[i]; var visual = (image == null ? null : new VisualImage(image)); returnValues.push(visual); } return returnValues; } VisualImage.prototype.draw = function(universe, world, display, drawable) { var drawPos = this.drawPos.overwriteWith ( drawable.pos ).subtract ( this.sizeHalf ); display.drawImage(this.image, drawPos); } } function VisualImageRegion(image, offset, size) { this.image = image; this.offset = offset; this.size = size; this.sizeHalf = this.size.clone().half(); this.drawPos = new Coords(); } { VisualImageRegion.prototype.draw = function(universe, world, display, drawable) { var drawPos = this.drawPos.overwriteWith ( drawable.pos ).subtract ( this.sizeHalf ); display.drawImageRegion(this.image, this.offset, this.size, drawPos); } } function VisualOffset(offset, child) { this.offset = offset; this.child = child; this.drawablePosOriginal = new Coords(); } { VisualOffset.prototype.draw = function(universe, world, display, drawable) { this.drawablePosOriginal.overwriteWith(drawable.pos); drawable.pos.add(this.offset); this.child.draw(universe, world, display, drawable); drawable.pos.overwriteWith(this.drawablePosOriginal); } } function VisualPolygon(vertices, colorFill, colorBorder) { this.vertices = vertices; this.colorFill = colorFill; this.colorBorder = colorBorder; this.verticesTransformed = this.vertices.clone(); } { VisualPolygon.prototype.draw = function(universe, world, display, drawable) { for (var i = 0; i < this.vertices.length; i++) { var vertex = this.vertices[i]; var vertexTransformed = this.verticesTransformed[i]; vertexTransformed.overwriteWith ( vertex ).add ( drawable.pos ); } display.drawPolygon(this.verticesTransformed, this.colorFill, this.colorBorder); } } function VisualRectangle(size, colorFill, colorBorder) { this.size = size; this.colorFill = colorFill; this.colorBorder = colorBorder; this.sizeHalf = this.size.clone().half(); this.drawPos = new Coords(); } { VisualRectangle.prototype.draw = function(universe, world, display, drawable) { var drawPos = this.drawPos.overwriteWith ( drawable.pos ).subtract ( this.sizeHalf ); display.drawRectangle(drawPos, this.size, this.colorFill, this.colorBorder); } } function VisualText(text) { this.text = text; } { VisualText.prototype.draw = function(universe, world, display, drawable) { display.drawText(this.text, drawable.pos); } } function Venue(name, camera, map, portals, movers) { this.name = name; this.camera = camera; this.map = map; this.portals = portals; this.movers = movers; this.moversToRemove = []; } { Venue.prototype.initialize = function(universe, world) { this.constraintCameraFollowPlayer = new Constraint_Follow ( this.movers[0] ); this.updateForTimerTick(universe, world); } Venue.prototype.updateForTimerTick = function(universe, world) { for (var i = 0; i < this.portals.length; i++) { var portal = this.portals[i]; portal.updateForTimerTick(universe, world, this); } for (var i = 0; i < this.movers.length; i++) { var mover = this.movers[i]; mover.updateForTimerTick(universe, world, this); } for (var i = 0; i < this.moversToRemove.length; i++) { var mover = this.moversToRemove[i]; if (mover == this.constraintCameraFollowPlayer.target) { this.constraintCameraFollowPlayer.target = this.camera; } this.movers.remove(mover); } this.moversToRemove.length = 0; } // drawable Venue.prototype.draw = function(universe, world) { this.constraintCameraFollowPlayer.apply(this.camera); universe.display.clear(); var visualCamera = new VisualCamera(this.camera); this.map.draw(universe, this, universe.display, visualCamera); for (var i = 0; i < this.portals.length; i++) { var portal = this.portals[i]; portal.draw(universe, world, visualCamera); } for (var i = 0; i < this.movers.length; i++) { var mover = this.movers[i]; mover.draw(universe, world, visualCamera); } } } function World(name, portalDefns, activityDefns, venues) { this.name = name; this.portalDefns = portalDefns.addLookups("name"); this.activityDefns = activityDefns.addLookups("name"); this.venues = venues.addLookups("name"); this.venueNext = this.venues[0]; } { World.demo = function(displaySizeInPixels, cellSizeInPixels) { var portalSize = cellSizeInPixels.clone().multiplyScalar(.75);; var portalDefns = [ new PortalDefn ( "PortalTown", new VisualPolygon ( [ new Coords(-.5, 0).multiply(portalSize), new Coords(.5, 0).multiply(portalSize), new Coords(.5, -.5).multiply(portalSize), new Coords(0, -1).multiply(portalSize), new Coords(-.5, -.5).multiply(portalSize), ], "LightGreen", "Green" ) ), new PortalDefn ( "PortalExit", new VisualPolygon ( [ new Coords(-.5, 0).multiply(portalSize), new Coords(0, -.5).multiply(portalSize), new Coords(0, -.25).multiply(portalSize), new Coords(.5, -.25).multiply(portalSize), new Coords(.5, .25).multiply(portalSize), new Coords(0, .25).multiply(portalSize), new Coords(0, .5).multiply(portalSize), ], "LightGreen", "Green" ) ) ]; var activityDefns = [ new ActivityDefn ( "DoNothing", function perform(universe, world, venue, actor, activity) { // Do nothing. } ), new ActivityDefn ( "MoveRandomly", function perform(universe, world, venue, actor, activity) { while (activity.target == null) { actor.posInCells.round(); var directionToMove = new Coords(); var heading = Math.floor(4 * Math.random()); if (heading == 0) { directionToMove.overwriteWithXY(0, 1); } else if (heading == 1) { directionToMove.overwriteWithXY(-1, 0); } else if (heading == 2) { directionToMove.overwriteWithXY(1, 0); } else if (heading == 3) { directionToMove.overwriteWithXY(0, -1); } var target = actor.posInCells.clone().add ( directionToMove ); if (target.isInRangeMax(venue.map.sizeInCells) == true) { var terrainAtTarget = venue.map.terrainAtPosInCells(target); if (terrainAtTarget.blocksMovement == false) { activity.target = target; } } } var target = activity.target; var displacementToTarget = target.clone().subtract ( actor.posInCells ); var distanceToTarget = displacementToTarget.magnitude(); var speedInCellsPerTick = 0.1; if (distanceToTarget <= speedInCellsPerTick) { actor.posInCells.overwriteWith(target); activity.target = null; } else { var directionToTarget = displacementToTarget.divideScalar ( distanceToTarget ); actor.velInCellsPerTick.overwriteWith ( directionToTarget ).multiplyScalar ( speedInCellsPerTick ); } } ), new ActivityDefn ( "UserInputAccept", function perform(universe, world, venue, actor, activity) { var actorVel = actor.velInCellsPerTick; actorVel.clear(); var inputHelper = universe.inputHelper; var inputsActive = inputHelper.inputsActive; for (var i = 0; i < inputsActive.length; i++) { var input = inputsActive[i]; if (input == null) { // do nothing } else if (input.startsWith("Arrow") == true) { if (input == "ArrowDown") { actorVel.overwriteWithXY(0, 1); } else if (input == "ArrowLeft") { actorVel.overwriteWithXY(-1, 0); } else if (input == "ArrowRight") { actorVel.overwriteWithXY(1, 0); } else if (input == "ArrowUp") { actorVel.overwriteWithXY(0, -1); } var speedInCellsPerTick = 0.1; actorVel.multiplyScalar(speedInCellsPerTick); } else if (input == "Enter") { inputHelper.inputInactivate(input); var displacement = new Coords(); var portals = venue.portals; for (var i = 0; i < portals.length; i++) { var portal = portals[i]; var distance = displacement.overwriteWith ( portal.pos ).subtract ( actor.pos ).magnitude(); var distanceMax = venue.map.cellSizeInPixels.x; if (distance <= distanceMax) { portal.activate(universe, world, venue, actor); } } } } } ), ]; var colors = Color.Instances()._All; var mapTerrainVisualDesert = new MapTerrainVisual(VisualImage.manyFromImages ([ Image.fromStrings ( "DesertCenter", colors, [ "yywwyywwyywwyyww", "ywwyywwyywwyywwy", "wwyywwyywwyywwyy", "wyywwyywwyywwyyw", "yywwyywwyywwyyww", "ywwyywwyywwyywwy", "wwyywwyywwyywwyy", "wyywwyywwyywwyyw", "yywwyywwyywwyyww", "ywwyywwyywwyywwy", "wwyywwyywwyywwyy", "wyywwyywwyywwyyw", "yywwyywwyywwyyww", "ywwyywwyywwyywwy", "wwyywwyywwyywwyy", "wyywwyywwyywwyyw", ] ), Image.fromStrings ( "DesertInsideSE", colors, [ "...ayyww........", "...aywwy........", "...awwyy........", "aaaawyyw........", "yywwyyww........", "ywwyywwy........", "wwyywwyy........", "wyywwyyw........", "................", "................", "................", "................", "................", "................", "................", "................", ] ), Image.fromStrings ( "DesertInsideSW", colors, [ "........yywwa...", "........ywwya...", "........wwyya...", "........wyywaaaa", "........yywwyyww", "........ywwyywwy", "........wwyywwyy", "........wyywwyyw", "................", "................", "................", "................", "................", "................", "................", "................", ] ), Image.fromStrings ( "DesertEdgeN", colors, [ "................", "................", "................", "aaaaaaaaaaaaaaaa", "yywwyywwyywwyyww", "ywwyywwyywwyywwy", "wwyywwyywwyywwyy", "wyywwyywwyywwyyw", "................", "................", "................", "................", "................", "................", "................", "................", ] ), Image.fromStrings ( "DesertInsideNE", colors, [ "................", "................", "................", "................", "................", "................", "................", "................", "yywwyyww........", "ywwyywwy........", "wwyywwyy........", "wyywwyyw........", "aaaayyww........", "...aywwy........", "...awwyy........", "...awyyw........", ] ), Image.fromStrings ( "DesertEdgeW", colors, [ "...ayyww........", "...aywwy........", "...awwyy........", "...awyyw........", "...ayyww........", "...aywwy........", "...awwyy........", "...awyyw........", "...ayyww........", "...aywwy........", "...awwyy........", "...awyyw........", "...ayyww........", "...aywwy........", "...awwyy........", "...awyyw........", ] ), Image.fromStrings ( "DesertDiagonalBackslash", colors, [ "........yywwa...", "........ywwya...", "........wwyya...", "........wyywaaaa", "........yywwyyww", "........ywwyywwy", "........wwyywwyy", "........wyywwyyw", "yywwyyww........", "ywwyywwy........", "wwyywwyy........", "wyywwyyw........", "aaaayyww........", "...aywwy........", "...awwyy........", "...awyyw........", ] ), Image.fromStrings ( "DesertOutsideNW", colors, [ "................", "................", "................", "...aaaaaaaaaaaaa", "...ayywwyywwyyww", "...aywwyywwyywwy", "...awwyywwyywwyy", "...awyywwyywwyyw", "...ayywwy.......", "...aywwyy.......", "...awwyyw.......", "...awyyww.......", "...ayywwy.......", "...aywwyy.......", "...awwyyw.......", "...awyyww.......", ] ), Image.fromStrings ( "DesertInsideNW", colors, [ "................", "................", "................", "................", "................", "................", "................", "................", "........yywwyyww", "........ywwyywwy", "........wwyywwyy", "........wyywwyyw", "........yywwaaaa", "........ywwya...", "........wwyya...", "........wyywa...", ] ), Image.fromStrings ( "DesertDiagonalSlash", colors, [ "...ayyww........", "...aywwy........", "...awwyy........", "aaaawyyw........", "yywwyyww........", "ywwyywwy........", "wwyywwyy........", "wyywwyyw........", "........yywwyyww", "........ywwyywwy", "........wwyywwyy", "........wyywwyyw", "........yywwaaaa", "........ywwya...", "........wwyya...", "........wyywa...", ] ), Image.fromStrings ( "DesertEdgeE", colors, [ "........yywwa...", "........ywwya...", "........wwyya...", "........wyywa...", "........yywwa...", "........ywwya...", "........wwyya...", "........wyywa...", "........yywwa...", "........ywwya...", "........wwyya...", "........wyywa...", "........yywwa...", "........ywwya...", "........wwyya...", "........wyywa...", ] ), Image.fromStrings ( "DesertOutsideNE", colors, [ "................", "................", "................", "aaaaaaaaaaaaa...", "yywwyywwyywwa...", "ywwyywwyywwya...", "wwyywwyywwyya...", "wyywwyywwyywa...", "........yywwa...", "........ywwya...", "........wwyya...", "........wyywa...", "........yywwa...", "........ywwya...", "........wwyya...", "........wyywa...", ] ), Image.fromStrings ( "DesertEdgeS", colors, [ "................", "................", "................", "................", "................", "................", "................", "................", "yywwyywwyywwyyww", "ywwyywwyywwyywwy", "wwyywwyywwyywwyy", "wyywwyywwyywwyyw", "aaaaaaaaaaaaaaaa", "................", "................", "................", ] ), Image.fromStrings ( "DesertOutsideSW", colors, [ "...ayyww........", "...aywwy........", "...awwyy........", "...awyyw........", "...ayyww........", "...aywwy........", "...awwyy........", "...awyyw........", "...ayywwyywwyyww", "...aywwyywwyywwy", "...awwyywwyywwyy", "...awyywwyywwyyw", "...aaaaaaaaaaaaa", "................", "................", "................", ] ), Image.fromStrings ( "DesertOutsideSE", colors, [ "........yywwa...", "........ywwya...", "........wwyya...", "........wyywa...", "........yywwa...", "........ywwya...", "........wwyya...", "........wyywa...", "yywwyywwyywwa...", "ywwyywwyywwya...", "wwyywwyywwyya...", "wyywwyywwyywa...", "aaaaaaaaaaaaa...", "................", "................", "................", ] ), ])); var mapTerrainVisualRock = new MapTerrainVisual(VisualImage.manyFromImages ([ Image.fromStrings ( "RockCenter", colors, [ "--AA--AA--AA--AA", "-AA--AA--AA--AA-", "AA--AA--AA--AA--", "A--AA--AA--AA--A", "--AA--AA--AA--AA", "-AA--AA--AA--AA-", "AA--AA--AA--AA--", "A--AA--AA--AA--A", "--AA--AA--AA--AA", "-AA--AA--AA--AA-", "AA--AA--AA--AA--", "A--AA--AA--AA--A", "--AA--AA--AA--AA", "-AA--AA--AA--AA-", "AA--AA--AA--AA--", "A--AA--AA--AA--A", ] ), Image.fromStrings ( "RockInsideSE", colors, [ "...a--AA........", "...a-AA-........", "...aAA--........", "aaaaA--A........", "--AA--AA........", "-AA--AA-........", "AA--AA--........", "A--AA--A........", "................", "................", "................", "................", "................", "................", "................", "................", ] ), Image.fromStrings ( "RockInsideSW", colors, [ "........--AAa...", "........-AA-a...", "........AA--a...", "........A--Aaaaa", "........--AA--AA", "........-AA--AA-", "........AA--AA--", "........A--AA--A", "................", "................", "................", "................", "................", "................", "................", "................", ] ), Image.fromStrings ( "RockEdgeN", colors, [ "................", "................", "................", "aaaaaaaaaaaaaaaa", "--AA--AA--AA--AA", "-AA--AA--AA--AA-", "AA--AA--AA--AA--", "A--AA--AA--AA--A", "................", "................", "................", "................", "................", "................", "................", "................", ] ), Image.fromStrings ( "RockInsideNE", colors, [ "................", "................", "................", "................", "................", "................", "................", "................", "--AA--AA........", "-AA--AA-........", "AA--AA--........", "A--AA--A........", "aaaa--AA........", "...a-AA-........", "...aAA--........", "...aA--A........", ] ), Image.fromStrings ( "RockEdgeW", colors, [ "...a--AA........", "...a-AA-........", "...aAA--........", "...aA--A........", "...a--AA........", "...a-AA-........", "...aAA--........", "...aA--A........", "...a--AA........", "...a-AA-........", "...aAA--........", "...aA--A........", "...a--AA........", "...a-AA-........", "...aAA--........", "...aA--A........", ] ), Image.fromStrings ( "RockDiagonalBackslash", colors, [ "........--AAa...", "........-AA-a...", "........AA--a...", "........A--Aaaaa", "........--AA--AA", "........-AA--AA-", "........AA--AA--", "........A--AA--A", "--AA--AA........", "-AA--AA-........", "AA--AA--........", "A--AA--A........", "aaaa--AA........", "...a-AA-........", "...aAA--........", "...aA--A........", ] ), Image.fromStrings ( "RockOutsideNW", colors, [ "................", "................", "................", "...aaaaaaaaaaaaa", "...a--AA--AA--AA", "...a-AA--AA--AA-", "...aAA--AA--AA--", "...aA--AA--AA--A", "...a--AA-.......", "...a-AA--.......", "...aAA--A.......", "...aA--AA.......", "...a--AA-.......", "...a-AA--.......", "...aAA--A.......", "...aA--AA.......", ] ), Image.fromStrings ( "RockInsideNW", colors, [ "................", "................", "................", "................", "................", "................", "................", "................", "........--AA--AA", "........-AA--AA-", "........AA--AA--", "........A--AA--A", "........--AAaaaa", "........-AA-a...", "........AA--a...", "........A--Aa...", ] ), Image.fromStrings ( "RockDiagonalSlash", colors, [ "...a--AA........", "...a-AA-........", "...aAA--........", "aaaaA--A........", "--AA--AA........", "-AA--AA-........", "AA--AA--........", "A--AA--A........", "........--AA--AA", "........-AA--AA-", "........AA--AA--", "........A--AA--A", "........--AAaaaa", "........-AA-a...", "........AA--a...", "........A--Aa...", ] ), Image.fromStrings ( "RockEdgeE", colors, [ "........--AAa...", "........-AA-a...", "........AA--a...", "........A--Aa...", "........--AAa...", "........-AA-a...", "........AA--a...", "........A--Aa...", "........--AAa...", "........-AA-a...", "........AA--a...", "........A--Aa...", "........--AAa...", "........-AA-a...", "........AA--a...", "........A--Aa...", ] ), Image.fromStrings ( "RockOutsideNE", colors, [ "................", "................", "................", "aaaaaaaaaaaaa...", "--AA--AA--AAa...", "-AA--AA--AA-a...", "AA--AA--AA--a...", "A--AA--AA--Aa...", "........--AAa...", "........-AA-a...", "........AA--a...", "........A--Aa...", "........--AAa...", "........-AA-a...", "........AA--a...", "........A--Aa...", ] ), Image.fromStrings ( "RockEdgeS", colors, [ "................", "................", "................", "................", "................", "................", "................", "................", "--AA--AA--AA--AA", "-AA--AA--AA--AA-", "AA--AA--AA--AA--", "A--AA--AA--AA--A", "aaaaaaaaaaaaaaaa", "................", "................", "................", ] ), Image.fromStrings ( "RockOutsideSW", colors, [ "...a--AA........", "...a-AA-........", "...aAA--........", "...aA--A........", "...a--AA........", "...a-AA-........", "...aAA--........", "...aA--A........", "...a--AA--AA--AA", "...a-AA--AA--AA-", "...aAA--AA--AA--", "...aA--AA--AA--A", "...aaaaaaaaaaaaa", "................", "................", "................", ] ), Image.fromStrings ( "RockOutsideSE", colors, [ "........--AAa...", "........-AA-a...", "........AA--a...", "........A--Aa...", "........--AAa...", "........-AA-a...", "........AA--a...", "........A--Aa...", "--AA--AA--AAa...", "-AA--AA--AA-a...", "AA--AA--AA--a...", "A--AA--AA--Aa...", "aaaaaaaaaaaaa...", "................", "................", "................", ] ), ])); //mapTerrainVisualDesert = MapTerrainVisual.TestInstance(); var mapTerrains = [ new MapTerrain ( "Desert", ".", false, // blocksMovement 1, // zLevelForOverlays mapTerrainVisualDesert ), new MapTerrain ( "Rocks", "x", true, // blocksMovement 2, // zLevelForOverlays mapTerrainVisualRock ), new MapTerrain ( "Water", "~", true, // blocksMovement 0, // zLevelForOverlays new MapTerrainVisual([new VisualImage ( Image.fromStrings ( "Water", colors, [ "cccccccccccccccc", "cccccccccccccccc", "cccccccccccccccc", "cccccccccccccccc", "cccccccccccccccc", "cccccccccccccccc", "cccccccccccccccc", "cccccccccccccccc", "cccccccccccccccc", "cccccccccccccccc", "cccccccccccccccc", "cccccccccccccccc", "cccccccccccccccc", "cccccccccccccccc", "cccccccccccccccc", "cccccccccccccccc", ] ) )]) ) ]; var moverVisual = new VisualDirectional ( // visualAtRest new VisualGroup ([ new VisualPolygon ( [ new Coords(0, -1).multiply(cellSizeInPixels), new Coords(-.5, 0).multiply(cellSizeInPixels), new Coords(.5, 0).multiply(cellSizeInPixels), ], "Gray", null ), new VisualOffset ( new Coords(0, -cellSizeInPixels.y / 2), new VisualCircle(cellSizeInPixels.x / 4, "Tan", null) ) ]), // visualsForDirections [ // east new VisualAnimation ( 4, // framesPerSecond [ new VisualPolygon ( [ new Coords(.4, -1).multiply(cellSizeInPixels), new Coords(-.5, 0).multiply(cellSizeInPixels), new Coords(.5, 0).multiply(cellSizeInPixels), ], "Gray", null ), new VisualPolygon ( [ new Coords(.5, -.9).multiply(cellSizeInPixels), new Coords(-.5, 0).multiply(cellSizeInPixels), new Coords(.5, 0).multiply(cellSizeInPixels), ], "Gray", null ), ] ), // south new VisualAnimation ( 4, // framesPerSecond [ new VisualGroup ([ new VisualPolygon ( [ new Coords(0, -1).multiply(cellSizeInPixels), new Coords(-.5, 0).multiply(cellSizeInPixels), new Coords(.5, 0).multiply(cellSizeInPixels), ], "Gray", null ), new VisualOffset ( new Coords(0, -cellSizeInPixels.y * .5), new VisualCircle(cellSizeInPixels.x / 4, "Tan", null) ) ]), new VisualGroup ([ new VisualPolygon ( [ new Coords(0, -.9).multiply(cellSizeInPixels), new Coords(-.5, 0).multiply(cellSizeInPixels), new Coords(.5, 0).multiply(cellSizeInPixels), ], "Gray", null ), new VisualOffset ( new Coords(0, -cellSizeInPixels.y * .4), new VisualCircle(cellSizeInPixels.x / 4, "Tan", null) ) ]), ] ), // west new VisualAnimation ( 4, // framesPerSecond [ new VisualPolygon ( [ new Coords(-.4, -1).multiply(cellSizeInPixels), new Coords(-.5, 0).multiply(cellSizeInPixels), new Coords(.5, 0).multiply(cellSizeInPixels), ], "Gray", null ), new VisualPolygon ( [ new Coords(-.5, -.9).multiply(cellSizeInPixels), new Coords(-.5, 0).multiply(cellSizeInPixels), new Coords(.5, 0).multiply(cellSizeInPixels), ], "Gray", null ), ] ), // north new VisualAnimation ( 4, // framesPerSecond [ new VisualPolygon ( [ new Coords(0, -1).multiply(cellSizeInPixels), new Coords(-.5, 0).multiply(cellSizeInPixels), new Coords(.5, 0).multiply(cellSizeInPixels), ], "Gray", null ), new VisualPolygon ( [ new Coords(0, -.9).multiply(cellSizeInPixels), new Coords(-.5, 0).multiply(cellSizeInPixels), new Coords(.5, 0).multiply(cellSizeInPixels), ], "Gray", null ), ] ), ] ); var venues = [ new Venue ( "Overworld", new Camera(displaySizeInPixels, new Coords(0, 0)), new Map ( cellSizeInPixels, mapTerrains, [ "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", "~....x.........................~", "~..........~~..................~", "~.........~~~~.................~", "~.........~~~~...xx............~", "~..........~~....x...xx........~", "~..............xxxx.x.x........~", "~................x.............~", "~.........~.~.......x..........~", "~..........~...................~", "~..............................~", "~..............................~", "~..............................~", "~..............................~", "~..............................~", "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", ] ), [ new Portal ( "PortalTown", new Coords(17, 9), "Lonelytown", // destinationVenueName new Coords(1, 4) // destinationPosInCells ) ], [ new Mover ( "Player", moverVisual, new Activity("UserInputAccept", null), new Coords(16, 8) // posInCells ), ] ), new Venue ( "Lonelytown", new Camera(displaySizeInPixels, new Coords(0, 0)), new Map ( cellSizeInPixels, mapTerrains, [ "xxxxxxxxxxxxxxxx", "x..............x", "x..............x", "x..............x", "x..............x", "x..............x", "x..............x", "xxxxxxxxxxxxxxxx", ] ), [ new Portal ( "PortalExit", new Coords(1, 4), // posInCells "Overworld", // destinationVenueName new Coords(17, 9) // destinationPosInCells ) ], [ new Mover ( "Stranger", moverVisual, new Activity("MoveRandomly", null), new Coords(4, 4) // posInCells ), ] ), ]; var returnValue = new World ( "WorldDemo", portalDefns, activityDefns, venues ); return returnValue; } // instance methods World.prototype.initialize = function(universe) { this.updateForTimerTick(universe); } World.prototype.updateForTimerTick = function(universe) { if (this.venueNext != null) { this.venueCurrent = this.venueNext; this.venueCurrent.initialize(universe, this); this.venueNext = null; } this.venueCurrent.updateForTimerTick(universe, this); } World.prototype.draw = function(universe) { this.venueCurrent.draw(universe, this, universe.display); } } // run main(); </script> </body> </html>
Reblogged this on Site Title.