├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── sketch.js └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .history 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 torin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stsmapgen 2 | 3 | The map generator inspired by [Slay the Spire](https://store.steampowered.com/app/646570). 4 | 5 | **I do not authorize the use of anything generated by this project for the selling of NFTs.** 6 | 7 | ![](https://user-images.githubusercontent.com/59264002/75630736-89e6a080-5c30-11ea-86ed-3f00e3631e48.gif) 8 | 9 | ## Theory 10 | 11 | 1. Set the start point and the end point. 12 | 2. Prepare points with Poisson disk sampling. 13 | 3. Generate links with Delaunay triangulation. 14 | 4. Find the path from the start point to the end point with A*. 15 | 5. Exclude random points on the path. 16 | 6. Repeat steps 4 and 5 several times. 17 | 7. Complete! 18 | 19 | ## License 20 | 21 | stsmapgen is licensed under the MIT license. See the [LICENSE](https://github.com/yurkth/stsmapgen/blob/master/LICENSE) for more information. 22 | 23 | Libraries: 24 | 25 | - [poisson-disk-sampling](https://github.com/kchapelier/poisson-disk-sampling) from [kchapelier](https://github.com/kchapelier) is licensed under the MIT license. 26 | - [Delaunator](https://github.com/mapbox/delaunator.git) from [mapbox](https://github.com/mapbox) is licensed under the ISC license. 27 | - [ngraph.path](https://github.com/anvaka/ngraph.path) from [anvaka](https://github.com/anvaka) is licensed under the MIT license. 28 | - [ngraph.graph](https://github.com/anvaka/ngraph.graph) from [anvaka](https://github.com/anvaka) is licensed under the BSD 3-Clause license. 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Procedural Map Generator 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |

Procedural Map Generator

24 |
25 |
26 |
27 | 28 |
29 |
30 |

31 | The map generator inspired by 32 | Slay the Spire. 33 |

34 | Refresh 35 | View 36 | on GitHub 37 |
38 |
39 | 40 |
41 | 42 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /sketch.js: -------------------------------------------------------------------------------- 1 | let canvasSize 2 | const noiseScale = 0.02 3 | 4 | let startPoint 5 | let endPoint 6 | let graph = createGraph() 7 | 8 | function setup() { 9 | canvasSize = min(500, windowWidth) 10 | let canvas = createCanvas(canvasSize, canvasSize) 11 | canvas.parent("canvas") 12 | colorMode(HSB) 13 | noLoop() 14 | 15 | // Poisson Disk Sampling 16 | let pdsObj = new PoissonDiskSampling({ 17 | shape: [canvasSize * 0.9, canvasSize * 0.9], 18 | minDistance: 40, 19 | maxDistance: 80, 20 | tries: 20 21 | }, random) 22 | startPoint = [canvasSize * 0.45, canvasSize * 0.9] 23 | endPoint = [canvasSize * 0.45, 0] 24 | pdsObj.addPoint(startPoint) 25 | pdsObj.addPoint(endPoint) 26 | let points = pdsObj.fill().filter(p => { 27 | return dist(...p, canvasSize * 0.45, canvasSize * 0.45) <= canvasSize * 0.45 28 | }) 29 | 30 | // Delaunay 31 | let delaunay = Delaunator.from(points).triangles 32 | let triangles = [] 33 | for (let i = 0; i < delaunay.length; i += 3) { 34 | triangles.push([ 35 | points[delaunay[i]], 36 | points[delaunay[i + 1]], 37 | points[delaunay[i + 2]] 38 | ]) 39 | } 40 | for (let t of triangles) { 41 | graph.addLink(t[0], t[1], { 42 | weight: dist(...t[0], ...t[1]) 43 | }) 44 | graph.addLink(t[1], t[2], { 45 | weight: dist(...t[1], ...t[2]) 46 | }) 47 | graph.addLink(t[2], t[0], { 48 | weight: dist(...t[2], ...t[0]) 49 | }) 50 | } 51 | } 52 | 53 | function draw() { 54 | noStroke() 55 | fill(40, 50, 60) 56 | rect(0, 0, canvasSize, canvasSize) 57 | push() 58 | strokeWeight(10) 59 | stroke(40, 80, 20) 60 | fill(0, 0) 61 | square(0, 0, canvasSize) 62 | pop() 63 | 64 | push() 65 | translate(canvasSize * 0.05, canvasSize * 0.05) 66 | // Lines 67 | let activePoints = [] 68 | for (let i = 0; i < canvasSize / 50; i++) { 69 | const pathFinder = ngraphPath.aStar(graph, { 70 | distance(fromNode, toNode, link) { 71 | return link.data.weight 72 | } 73 | }) 74 | const foundPath = pathFinder.find(startPoint, endPoint) 75 | if (foundPath.length === 0) { 76 | break 77 | } 78 | activePoints.push(...foundPath.map(obj => obj.id)) 79 | 80 | stroke(40, 80, 20) 81 | fill(40, 80, 20) 82 | for (let j = 1; j < foundPath.length; j++) { 83 | arrow(...foundPath[j].id, ...foundPath[j - 1].id) 84 | } 85 | 86 | const idx = floor(random(1, foundPath.length - 1)) 87 | graph.removeNode(foundPath[idx].id) 88 | } 89 | 90 | // Points 91 | stroke(0) 92 | textSize(16) 93 | textAlign(CENTER, CENTER) 94 | for (const p of new Set(activePoints)) { 95 | const pJSON = JSON.stringify(p) 96 | switch (pJSON) { 97 | case JSON.stringify(startPoint): 98 | text("😀", ...p) 99 | break 100 | case JSON.stringify(endPoint): 101 | text("😈", ...p) 102 | break 103 | default: 104 | text(random(Array.from("💀💀💀💰❓")), ...p) 105 | } 106 | } 107 | pop() 108 | 109 | noStroke() 110 | fill(40, 50, 60, 0.3) 111 | rect(0, 0, canvasSize, canvasSize) 112 | } 113 | 114 | function arrow(x1, y1, x2, y2, arrowSize = 6) { 115 | let vec = createVector(x2 - x1, y2 - y1) 116 | const len = vec.mag() 117 | vec.mult((len - 10) / len) 118 | push() 119 | translate(x1, y1) 120 | dottedLine(0, 0, vec.x, vec.y) 121 | rotate(vec.heading()) 122 | translate(vec.mag() - arrowSize, 0) 123 | triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0) 124 | pop() 125 | } 126 | 127 | function dottedLine(x1, y1, x2, y2, fragment = 5) { 128 | let vec = createVector(x2 - x1, y2 - y1) 129 | const len = vec.mag() 130 | push() 131 | translate(x1, y1) 132 | for (let i = floor(len * 0.5 / fragment); i >= 0; i--) { 133 | if (i == 0 && floor(len / fragment) % 2 == 0) { 134 | vec.normalize().mult(len % fragment) 135 | } else { 136 | vec.normalize().mult(fragment) 137 | } 138 | line(0, 0, vec.x, vec.y) 139 | vec.mult(2) 140 | translate(vec.x, vec.y) 141 | } 142 | pop() 143 | } 144 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | .button { 7 | margin-top: 10px; 8 | margin-left: 5px; 9 | margin-right: 5px; 10 | } 11 | 12 | canvas { 13 | margin: auto; 14 | display: block; 15 | } 16 | 17 | footer { 18 | margin-top: 48px; 19 | padding-bottom: 0px; 20 | } 21 | --------------------------------------------------------------------------------