A Pen by not important on CodePen.
Created
December 1, 2016 03:14
-
-
Save clindsey/cd5c4e18a2ed5ce88d3b21dd25d8bb70 to your computer and use it in GitHub Desktop.
worldFlow
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <canvas id="canvas" width="500" height="500"></canvas> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| seed = 19870910 | |
| rng = new Chance seed | |
| options = { | |
| width: 5 | |
| height: 5 | |
| sector: | |
| width: 5 | |
| height: 5 | |
| tile: | |
| width: 20 | |
| height: 20 | |
| } | |
| options.grid = { | |
| width: options.sector.width * options.tile.width | |
| height: options.sector.height * options.tile.height | |
| } | |
| config = {} | |
| config.generators = | |
| world: | |
| options: | |
| spritesheetOffset: 0 | |
| maxHeight: 7 | |
| # world size | |
| worldChunkWidth: options.width | |
| worldChunkHeight: options.height | |
| # zoom level | |
| chunkTileWidth: options.sector.width | |
| chunkTileHeight: options.sector.height | |
| config.worldTileWidth = config.generators.world.options.worldChunkWidth * config.generators.world.options.chunkTileWidth | |
| config.worldTileHeight = config.generators.world.options.worldChunkHeight * config.generators.world.options.chunkTileHeight | |
| config.generators.world.options.worldTileWidth = config.worldTileWidth | |
| config.generators.world.options.worldTileHeight = config.worldTileHeight | |
| class FlowField | |
| neighborDirections: [ | |
| [ 0, -1] | |
| [ 1, 0] | |
| [ 0, 1] | |
| [-1, 0] | |
| #[ 1, -1] | |
| #[ 1, 1] | |
| #[-1, 1] | |
| #[-1, -1] | |
| ] | |
| constructor: (@x, @y, @gridData) -> | |
| callback: (node) => | |
| return unless node.data.open | |
| node.visited = true | |
| node.iteration = 1 | |
| @openNodes.push node | |
| directionFlow: (vX, vY, costCallback, flowCallback) -> | |
| @reset() | |
| @openNodes = [] | |
| {width, height} = options.sector | |
| lX = if vX is -1 then 0 else width - 1 | |
| lY = if vY is -1 then 0 else height - 1 | |
| offsetX = options.sector.width * @x | |
| offsetY = options.sector.height * @y | |
| if vX is - 1 or vX is 1 | |
| for y in [0...height] | |
| node = @gridData.getNode lX + offsetX, y + offsetY | |
| @callback node | |
| if vY is -1 or vY is 1 | |
| for x in [0...width] | |
| node = @gridData.getNode x + offsetX, lY + offsetY | |
| @callback node | |
| @costMap 1, costCallback | |
| @flowMap flowCallback | |
| if vX is -1 or vX is 1 | |
| for y in [0...height] | |
| node = @gridData.getNode lX + offsetX, y + offsetY | |
| continue unless node.data.open | |
| vector = @neighborVector node, vX, 0 | |
| flowCallback lX, y, vector | |
| if vY is -1 or vY is 1 | |
| for x in [0...width] | |
| node = @gridData.getNode x + offsetX, lY + offsetY | |
| continue unless node.data.open | |
| vector = @neighborVector node, 0, vY | |
| flowCallback x, lY, vector | |
| neighborVector: (node, vX, vY) -> # bug here, sometimes vectors point towards eachother | |
| neighborNode = @gridData.getNode node.x + vX, node.y + vY | |
| if neighborNode.data.open | |
| vector = {x: vX, y: vY} | |
| else | |
| offsetX = options.sector.width * @x | |
| offsetY = options.sector.height * @y | |
| {width, height} = options.sector | |
| neighborNodes = @collectNeighbors node, (n) -> | |
| rX = n.x - offsetX | |
| rY = n.y - offsetY | |
| return false unless rX < width and rY < height | |
| return false unless rX >= 0 and rY >= 0 | |
| n.iteration >= 0 | |
| for n in neighborNodes | |
| if n.data.open | |
| vector = { | |
| x: n.x - node.x | |
| y: n.y - node.y | |
| } | |
| break | |
| vector | |
| process: (x, y, costCallback, flowCallback) -> | |
| @reset() | |
| offsetX = options.sector.width * @x | |
| offsetY = options.sector.height * @y | |
| @openNodes = [@gridData.getNode x + offsetX, y + offsetY] | |
| @costMap 0, costCallback | |
| @flowMap flowCallback | |
| flowMap: (flowCallback) -> | |
| {width, height} = options.sector | |
| offsetX = options.sector.width * @x | |
| offsetY = options.sector.height * @y | |
| for y in [0...height] | |
| for x in [0...width] | |
| node = @gridData.getNode x + offsetX, y + offsetY | |
| continue unless node.data.open | |
| continue unless node.visited | |
| neighbors = @collectNeighbors node, (n) -> | |
| rX = n.x - offsetX | |
| rY = n.y - offsetY | |
| return false unless rX < width and rY < height | |
| return false unless rX >= 0 and rY >= 0 | |
| n.iteration >= 0 | |
| lowestNode = neighbors.pop() | |
| neighbors.map (n) -> | |
| return unless n.data.open | |
| return unless n.iteration? | |
| lowestNode = n if lowestNode.iteration > n.iteration | |
| continue unless lowestNode | |
| vector = { | |
| x: lowestNode.x - node.x | |
| y: lowestNode.y - node.y | |
| } | |
| vector = {x: 0, y: 0} if node.iteration is 0 | |
| flowCallback x, y, vector | |
| reset: -> | |
| offsetX = options.sector.width * @x | |
| offsetY = options.sector.height * @y | |
| {width, height} = options.sector | |
| for y in [0...height] | |
| for x in [0...width] | |
| node = @gridData.getNode x + offsetX, y + offsetY | |
| node.visited = false | |
| floodCallback: (node) -> # doesn't feel right to have this here | |
| return false unless node.data.open | |
| return false if node.visited | |
| node.visited = true | |
| true | |
| costMap: (iteration, costCallback) -> | |
| newNodes = [] | |
| while node = @openNodes.pop() | |
| node.visited = true | |
| node.iteration = iteration | |
| {x, y} = node | |
| costCallback x, y | |
| nodes = @collectNeighbors node, @floodCallback | |
| newNodes = newNodes.concat nodes | |
| @openNodes = @openNodes.concat newNodes | |
| if @openNodes.length | |
| @costMap iteration + 1, costCallback | |
| collectNeighbors: ({x, y}, checkCallback) -> | |
| {width, height} = options.sector | |
| nodes = [] | |
| for [dX, dY] in @neighborDirections | |
| tX = x + dX | |
| tY = y + dY | |
| node = @gridData.getNode tX, tY | |
| continue unless node | |
| continue unless checkCallback node | |
| nodes.push node | |
| nodes | |
| class FlowNode | |
| constructor: (@x, @y, @iteration, @data = {}) -> | |
| class WorldData | |
| constructor: (mapData) -> | |
| @buildData mapData | |
| buildData: (mapData) -> | |
| @width = mapData[0].length | |
| @height = mapData.length | |
| @nodes = [] | |
| for y in [0...@height] | |
| @nodes.push [] | |
| for x in [0...@width] | |
| node = new FlowNode x, y, -Infinity, { open: mapData[y][x] > 1 } | |
| @nodes[y][x] = node | |
| getNode: (x, y) -> | |
| unless @nodes[y]? and @nodes[y][x]? | |
| return node = new FlowNode x, y, -Infinity, { open: false } | |
| @nodes[y][x] | |
| class SectorTile | |
| directionLookup: { | |
| '1_0': 0 | |
| '2_1': 90 | |
| '1_2': 180 | |
| '0_1': 270 | |
| '2_0': 45 | |
| '2_2': 135 | |
| '0_2': 225 | |
| '0_0': 315 | |
| '1_1': 0 | |
| } | |
| constructor: (x, y, @node) -> | |
| @el = new createjs.Container | |
| @el.x = x | |
| @el.y = y | |
| @render() | |
| render: -> | |
| @drawBackground() | |
| @drawArrow() | |
| drawBackground: -> | |
| return if @node.data.open | |
| g = new createjs.Graphics | |
| backgroundEl = new createjs.Shape g | |
| g.f('#880000').dr 0, 0, options.tile.width, options.tile.height | |
| @el.addChild backgroundEl | |
| drawArrow: -> | |
| g = new createjs.Graphics | |
| @arrowEl = new createjs.Shape g | |
| width = options.tile.width * 0.25 | |
| height = options.tile.height * 0.75 | |
| x = (options.tile.width - width) / 2 | |
| y = (options.tile.height - height) / 2 | |
| g.lf(['#333333', '#dedede'], [0, 1], 0, 0, 0, height).dr x, y, width, height | |
| @arrowEl.regX = options.tile.width / 2 | |
| @arrowEl.regY = options.tile.height / 2 | |
| @arrowEl.x = options.tile.width / 2 | |
| @arrowEl.y = options.tile.height / 2 | |
| #@arrowEl.visible = false | |
| @el.addChild @arrowEl | |
| setVector: ({x, y}) -> | |
| return unless @node.data.open | |
| return if x is 0 and y is 0 | |
| @arrowEl.visible = true | |
| @arrowEl.rotation = @directionLookup["#{x + 1}_#{y + 1}"] | |
| clearVector: -> | |
| return if @node.data.open | |
| @arrowEl.visible = false | |
| class SectorView | |
| constructor: (@worldData, @x, @y) -> | |
| @el = new createjs.Container | |
| @render() | |
| @flowField = new FlowField @x, @y, @worldData | |
| reset: -> | |
| for row, y in @sectorTiles | |
| for sectorTile, x in row | |
| sectorTile.clearVector() | |
| markCost: -> | |
| markFlow: (x, y, vector) => | |
| @sectorTiles[y][x].setVector vector | |
| render: -> | |
| @drawTiles() | |
| @el.cache 0, 0, options.grid.width, options.grid.height | |
| drawTiles: -> | |
| @sectorTiles = [] | |
| {width, height} = options.sector | |
| for y in [0...height] | |
| @sectorTiles[y] = [] | |
| for x in [0...width] | |
| pX = x * options.tile.width | |
| pY = y * options.tile.height | |
| wX = @x * options.sector.width + x | |
| wY = @y * options.sector.height + y | |
| node = @worldData.getNode wX, wY | |
| sectorTile = new SectorTile pX, pY, node | |
| @el.addChild sectorTile.el | |
| @sectorTiles[y][x] = sectorTile | |
| process: (x, y) -> | |
| @vX = @vY = undefined | |
| return unless @sectorTiles[y][x].node.data.open | |
| @flowField.process x, y, @markCost, @markFlow | |
| @sectorTiles[y][x].arrowEl.visible = false | |
| @el.updateCache() | |
| directionFlow: (vX, vY) -> | |
| return if vX is @vX and vY is @vY | |
| @vX = vX | |
| @vY = vY | |
| @flowField.directionFlow vX, vY, @markCost, @markFlow | |
| @el.updateCache() | |
| class GridView | |
| constructor: (@width, @height, @worldData) -> | |
| @el = new createjs.Container | |
| @addSectors() | |
| @addEventListeners() | |
| addSectors: -> | |
| @sectors = [] | |
| for y in [0...options.height] | |
| @sectors[y] = [] | |
| for x in [0...options.width] | |
| pX = x * options.grid.width | |
| pY = y * options.grid.height | |
| sectorView = @addSector pX, pY, x, y | |
| @el.addChild sectorView.el | |
| @sectors[y][x] = sectorView | |
| addSector: (x, y, sectorX, sectorY) -> | |
| sectorView = new SectorView @worldData, sectorX, sectorY | |
| sectorView.el.x = x | |
| sectorView.el.y = y | |
| sectorView | |
| addEventListeners: -> | |
| {width, height} = options | |
| @cX = @cY = undefined | |
| $('canvas').mousemove @onMouseMove | |
| onMouseMove: ({offsetX, offsetY}) => | |
| sectorX = ~~(offsetX / options.grid.width) | |
| sectorY = ~~(offsetY / options.grid.height) | |
| mX = offsetX % options.grid.width | |
| mY = offsetY % options.grid.height | |
| x = ~~(mX / options.tile.width) | |
| y = ~~(mY / options.tile.height) | |
| if @cX isnt x or @cY isnt y | |
| @cX = x | |
| @cY = y | |
| for sY in [0...options.height] | |
| for sX in [0...options.width] | |
| sector = @sectors[sY][sX] | |
| sector.reset() | |
| if sX is sectorX and sY is sectorY | |
| sector.process x, y | |
| else | |
| vX = 0 | |
| vY = 0 | |
| if sectorX > sX | |
| vX = 1 | |
| if sectorX < sX | |
| vX = -1 | |
| if sectorY > sY | |
| vY = 1 | |
| if sectorY < sY | |
| vY = -1 | |
| sector.directionFlow vX, vY | |
| class WorldGenerator | |
| constructor: (@randomFn, @seed, @options, cacheTiles = true) -> | |
| @tileCache = [] | |
| @chunkCache = [] | |
| @tileTypes = [] | |
| @cacheAllTiles() if cacheTiles | |
| getStartPosition: (seed) -> | |
| grassTiles = @tileTypes[2] | |
| index = Math.floor @randomFn(seed) * grassTiles.length | |
| grassTiles[index] | |
| getFinishPosition: (seed) -> | |
| grassTiles = @tileTypes[2] | |
| index = Math.floor @randomFn(seed) * grassTiles.length | |
| grassTiles[index] | |
| getPathfindingArea: (centerX, centerY) -> | |
| offsetX = (0 - ~~(@options.worldTileHeight / 2)) + centerX | |
| offsetY = (0 - ~~(@options.worldTileHeight / 2)) + centerY | |
| outputArray = [] | |
| for y in [[email protected]] | |
| outputArray[y] = [] | |
| for x in [[email protected]] | |
| worldX = x + offsetX | |
| worldY = y + offsetY | |
| cellIndex = @getCell worldX, worldY | |
| outputArray[y][x] = cellIndex | |
| outputArray | |
| getAllCells: -> | |
| @tileCache | |
| cacheAllTiles: -> | |
| for y in [[email protected]] | |
| for x in [[email protected]] | |
| chunk = @getChunk x, y | |
| for cy in [0...chunk.length] | |
| for cx in [0...chunk[cy].length] | |
| vx = x * @options.chunkTileWidth + cx | |
| vy = y * @options.chunkTileHeight + cy | |
| cellValue = @getCell vx, vy | |
| index = Math.floor cellValue / 2 | |
| @tileTypes[index] = [] if @tileTypes[index] is undefined | |
| @tileTypes[index].push {x:vx, y:vy} | |
| getCell: (worldX, worldY) -> | |
| worldX = @clamp worldX, @options.worldTileWidth | |
| worldY = @clamp worldY, @options.worldTileHeight | |
| if @tileCache[worldY] and @tileCache[worldY][worldX]? | |
| return @tileCache[worldY][worldX] | |
| worldChunkX = Math.floor worldX / @options.chunkTileWidth | |
| worldChunkY = Math.floor worldY / @options.chunkTileHeight | |
| chunkX = worldX % @options.chunkTileWidth | |
| chunkY = worldY % @options.chunkTileHeight | |
| chunk = @getChunk worldChunkX, worldChunkY | |
| cell = Math.floor chunk[chunkY][chunkX] * @options.maxHeight | |
| @tileCache[worldY] = [] unless @tileCache[worldY]? | |
| @tileCache[worldY][worldX] = cell | |
| cell | |
| getChunk: (worldChunkX, worldChunkY) -> | |
| row0 = [ | |
| @chunkEdgeIndex worldChunkX - 1, worldChunkY - 1 | |
| @chunkEdgeIndex worldChunkX, worldChunkY - 1 | |
| @chunkEdgeIndex worldChunkX + 1, worldChunkY - 1 | |
| @chunkEdgeIndex worldChunkX + 2, worldChunkY - 1 | |
| ] | |
| row1 = [ | |
| @chunkEdgeIndex worldChunkX - 1, worldChunkY | |
| @chunkEdgeIndex worldChunkX, worldChunkY | |
| @chunkEdgeIndex worldChunkX + 1, worldChunkY | |
| @chunkEdgeIndex worldChunkX + 2, worldChunkY | |
| ] | |
| row2 = [ | |
| @chunkEdgeIndex worldChunkX - 1, worldChunkY + 1 | |
| @chunkEdgeIndex worldChunkX, worldChunkY + 1 | |
| @chunkEdgeIndex worldChunkX + 1, worldChunkY + 1 | |
| @chunkEdgeIndex worldChunkX + 2, worldChunkY + 1 | |
| ] | |
| row3 = [ | |
| @chunkEdgeIndex worldChunkX - 1, worldChunkY + 2 | |
| @chunkEdgeIndex worldChunkX, worldChunkY + 2 | |
| @chunkEdgeIndex worldChunkX + 1, worldChunkY + 2 | |
| @chunkEdgeIndex worldChunkX + 2, worldChunkY + 2 | |
| ] | |
| chunkTileWidth = @options.chunkTileWidth | |
| chunkTileHeight = @options.chunkTileHeight | |
| @bilinearInterpolate chunkTileWidth, chunkTileHeight, row0, row1, row2, row3 | |
| chunkEdgeIndex: (x, y) -> | |
| width = @options.worldChunkWidth | |
| height = @options.worldChunkHeight | |
| seed = @seed | |
| x = @clamp x, width | |
| y = @clamp y, height | |
| @randomFn(y * width + x + seed) | |
| bilinearInterpolate: (width, height, row0, row1, row2, row3) -> | |
| cells = [] | |
| for y in [0...height] | |
| cells[y] = [] | |
| yFactor = y / height | |
| for x in [0...width] | |
| xFactor = x / width | |
| i0 = @terp xFactor, row0[0], row0[1], row0[2], row0[3] | |
| i1 = @terp xFactor, row1[0], row1[1], row1[2], row1[3] | |
| i2 = @terp xFactor, row2[0], row2[1], row2[2], row2[3] | |
| i3 = @terp xFactor, row3[0], row3[1], row3[2], row3[3] | |
| cells[y][x] = @terp yFactor, i0, i1, i2, i3 | |
| cells | |
| terp: (t, a, b, c, d) -> | |
| val = 0.5 * (c - a + (2.0*a - 5.0*b + 4.0*c - d + (3.0*(b - c) + d - a)*t)*t)*t + b | |
| val = Math.max 0, val | |
| val = Math.min 1, val | |
| val | |
| clamp: (index, size) -> | |
| (index + size) % size | |
| randomFn = (seed) -> | |
| (new Chance seed).floating {min: 0, max: 1} | |
| createGrid = -> | |
| generator = new WorldGenerator randomFn, seed, config.generators.world.options | |
| worldData = new WorldData generator.tileCache | |
| gridView = new GridView options.width, options.height, worldData | |
| stageEl.addChild gridView.el | |
| gridView.onMouseMove {offsetX: 0, offsetY: 0} | |
| stageEl = new createjs.Stage 'canvas' | |
| createjs.Ticker.setFPS 60 | |
| createjs.Ticker.addEventListener 'tick', -> | |
| stageEl.update() | |
| createGrid() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <script src="https://code.createjs.com/easeljs-0.8.0.min.js"></script> | |
| <script src="//cdnjs.cloudflare.com/ajax/libs/chance/0.5.6/chance.js"></script> | |
| <script src="//code.jquery.com/jquery-2.1.3.js"></script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| canvas { | |
| background-color: #dedede; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment