diff options
Diffstat (limited to 'app/assets/javascripts/trace2.js')
-rw-r--r-- | app/assets/javascripts/trace2.js | 1055 |
1 files changed, 1055 insertions, 0 deletions
diff --git a/app/assets/javascripts/trace2.js b/app/assets/javascripts/trace2.js new file mode 100644 index 0000000..000e60b --- /dev/null +++ b/app/assets/javascripts/trace2.js | |||
@@ -0,0 +1,1055 @@ | |||
1 | namespace(function() { | ||
2 | |||
3 | var BBOX_DEBUG = false | ||
4 | |||
5 | function clamp(value, min, max) { | ||
6 | return value < min ? min : value > max ? max : value | ||
7 | } | ||
8 | |||
9 | class BoundingBox { | ||
10 | constructor(x1, x2, y1, y2, sym=false) { | ||
11 | this.raw = {'x1':x1, 'x2':x2, 'y1':y1, 'y2':y2} | ||
12 | this.sym = sym | ||
13 | if (BBOX_DEBUG === true) { | ||
14 | this.debug = createElement('rect') | ||
15 | data.svg.appendChild(this.debug) | ||
16 | this.debug.setAttribute('opacity', 0.5) | ||
17 | this.debug.setAttribute('style', 'pointer-events: none;') | ||
18 | if (data.puzzle.symType == SYM_TYPE_NONE) { | ||
19 | this.debug.setAttribute('fill', 'white') | ||
20 | } else { | ||
21 | if (this.sym !== true) { | ||
22 | this.debug.setAttribute('fill', 'blue') | ||
23 | } else { | ||
24 | this.debug.setAttribute('fill', 'orange') | ||
25 | } | ||
26 | } | ||
27 | } | ||
28 | this._update() | ||
29 | } | ||
30 | |||
31 | shift(dir, pixels) { | ||
32 | if (dir === 'left') { | ||
33 | this.raw.x2 = this.raw.x1 | ||
34 | this.raw.x1 -= pixels | ||
35 | } else if (dir === 'right') { | ||
36 | this.raw.x1 = this.raw.x2 | ||
37 | this.raw.x2 += pixels | ||
38 | } else if (dir === 'top') { | ||
39 | this.raw.y2 = this.raw.y1 | ||
40 | this.raw.y1 -= pixels | ||
41 | } else if (dir === 'bottom') { | ||
42 | this.raw.y1 = this.raw.y2 | ||
43 | this.raw.y2 += pixels | ||
44 | } | ||
45 | this._update() | ||
46 | } | ||
47 | |||
48 | inMain(x, y) { | ||
49 | var inMainBox = | ||
50 | (this.x1 < x && x < this.x2) && | ||
51 | (this.y1 < y && y < this.y2) | ||
52 | var inRawBox = | ||
53 | (this.raw.x1 < x && x < this.raw.x2) && | ||
54 | (this.raw.y1 < y && y < this.raw.y2) | ||
55 | |||
56 | return inMainBox && !inRawBox | ||
57 | } | ||
58 | |||
59 | _update() { | ||
60 | this.x1 = this.raw.x1 | ||
61 | this.x2 = this.raw.x2 | ||
62 | this.y1 = this.raw.y1 | ||
63 | this.y2 = this.raw.y2 | ||
64 | |||
65 | // Check for endpoint adjustment. | ||
66 | // Pretend it's not an endpoint if the sym cell isn't an endpoint. | ||
67 | if (data.puzzle.symType != SYM_TYPE_NONE) { | ||
68 | var cell1 = data.puzzle.getCell(data.pos.x, data.pos.y) | ||
69 | var cell2 = data.puzzle.getSymmetricalCell(data.pos.x, data.pos.y) | ||
70 | |||
71 | if ((cell1.end == null) != (cell2.end == null)) { | ||
72 | var cell = {'end': 'none'} | ||
73 | } else if (this.sym !== true) { | ||
74 | var cell = cell1 | ||
75 | } else { | ||
76 | var cell = cell2 | ||
77 | } | ||
78 | } else { | ||
79 | var cell = data.puzzle.getCell(data.pos.x, data.pos.y) | ||
80 | } | ||
81 | if (cell.end === 'left') { | ||
82 | this.x1 -= 24 | ||
83 | } else if (cell.end === 'right') { | ||
84 | this.x2 += 24 | ||
85 | } else if (cell.end === 'top') { | ||
86 | this.y1 -= 24 | ||
87 | } else if (cell.end === 'bottom') { | ||
88 | this.y2 += 24 | ||
89 | } | ||
90 | |||
91 | this.middle = { // Note: Middle of the raw object | ||
92 | 'x':(this.raw.x1 + this.raw.x2)/2, | ||
93 | 'y':(this.raw.y1 + this.raw.y2)/2 | ||
94 | } | ||
95 | |||
96 | if (this.debug != null) { | ||
97 | this.debug.setAttribute('x', this.x1) | ||
98 | this.debug.setAttribute('y', this.y1) | ||
99 | this.debug.setAttribute('width', this.x2 - this.x1) | ||
100 | this.debug.setAttribute('height', this.y2 - this.y1) | ||
101 | } | ||
102 | } | ||
103 | } | ||
104 | |||
105 | class PathSegment { | ||
106 | constructor(dir) { | ||
107 | this.poly1 = createElement('polygon') | ||
108 | this.circ = createElement('circle') | ||
109 | this.poly2 = createElement('polygon') | ||
110 | this.pillarCirc = createElement('circle') | ||
111 | this.dir = dir | ||
112 | data.svg.insertBefore(this.circ, data.cursor) | ||
113 | data.svg.insertBefore(this.poly2, data.cursor) | ||
114 | data.svg.insertBefore(this.pillarCirc, data.cursor) | ||
115 | this.circ.setAttribute('cx', data.bbox.middle.x) | ||
116 | this.circ.setAttribute('cy', data.bbox.middle.y) | ||
117 | |||
118 | if (data.puzzle.pillar === true) { | ||
119 | // cx/cy are updated in redraw(), since pillarCirc tracks the cursor | ||
120 | this.pillarCirc.setAttribute('cy', data.bbox.middle.y) | ||
121 | this.pillarCirc.setAttribute('r', 12) | ||
122 | if (data.pos.x === 0 && this.dir === MOVE_RIGHT) { | ||
123 | this.pillarCirc.setAttribute('cx', data.bbox.x1) | ||
124 | this.pillarCirc.setAttribute('static', true) | ||
125 | } else if (data.pos.x === data.puzzle.width - 1 && this.dir === MOVE_LEFT) { | ||
126 | this.pillarCirc.setAttribute('cx', data.bbox.x2) | ||
127 | this.pillarCirc.setAttribute('static', true) | ||
128 | } else { | ||
129 | this.pillarCirc.setAttribute('cx', data.bbox.middle.x) | ||
130 | } | ||
131 | } | ||
132 | |||
133 | if (data.puzzle.symType == SYM_TYPE_NONE) { | ||
134 | this.poly1.setAttribute('class', 'line-1 ' + data.svg.id) | ||
135 | this.circ.setAttribute('class', 'line-1 ' + data.svg.id) | ||
136 | this.poly2.setAttribute('class', 'line-1 ' + data.svg.id) | ||
137 | this.pillarCirc.setAttribute('class', 'line-1 ' + data.svg.id) | ||
138 | } else { | ||
139 | this.poly1.setAttribute('class', 'line-2 ' + data.svg.id) | ||
140 | this.circ.setAttribute('class', 'line-2 ' + data.svg.id) | ||
141 | this.poly2.setAttribute('class', 'line-2 ' + data.svg.id) | ||
142 | this.pillarCirc.setAttribute('class', 'line-2 ' + data.svg.id) | ||
143 | |||
144 | this.symPoly1 = createElement('polygon') | ||
145 | this.symCirc = createElement('circle') | ||
146 | this.symPoly2 = createElement('polygon') | ||
147 | this.symPillarCirc = createElement('circle') | ||
148 | data.svg.insertBefore(this.symCirc, data.cursor) | ||
149 | data.svg.insertBefore(this.symPoly2, data.cursor) | ||
150 | data.svg.insertBefore(this.symPillarCirc, data.cursor) | ||
151 | |||
152 | if (data.puzzle.settings.INVISIBLE_SYMMETRY) { | ||
153 | this.symPoly1.setAttribute('class', 'line-4 ' + data.svg.id) | ||
154 | this.symCirc.setAttribute('class', 'line-4 ' + data.svg.id) | ||
155 | this.symPoly2.setAttribute('class', 'line-4 ' + data.svg.id) | ||
156 | this.symPillarCirc.setAttribute('class', 'line-4 ' + data.svg.id) | ||
157 | } else { | ||
158 | this.symPoly1.setAttribute('class', 'line-3 ' + data.svg.id) | ||
159 | this.symCirc.setAttribute('class', 'line-3 ' + data.svg.id) | ||
160 | this.symPoly2.setAttribute('class', 'line-3 ' + data.svg.id) | ||
161 | this.symPillarCirc.setAttribute('class', 'line-3 ' + data.svg.id) | ||
162 | } | ||
163 | |||
164 | this.symCirc.setAttribute('cx', data.symbbox.middle.x) | ||
165 | this.symCirc.setAttribute('cy', data.symbbox.middle.y) | ||
166 | |||
167 | if (data.puzzle.pillar === true) { | ||
168 | // cx/cy are updated in redraw(), since symPillarCirc tracks the cursor | ||
169 | this.symPillarCirc.setAttribute('cy', data.symbbox.middle.y) | ||
170 | this.symPillarCirc.setAttribute('r', 12) | ||
171 | var symmetricalDir = getSymmetricalDir(data.puzzle, this.dir) | ||
172 | if (data.sym.x === 0 && symmetricalDir === MOVE_RIGHT) { | ||
173 | this.symPillarCirc.setAttribute('cx', data.symbbox.x1) | ||
174 | this.symPillarCirc.setAttribute('static', true) | ||
175 | } else if (data.sym.x === data.puzzle.width - 1 && symmetricalDir === MOVE_LEFT) { | ||
176 | this.symPillarCirc.setAttribute('cx', data.symbbox.x2) | ||
177 | this.symPillarCirc.setAttribute('static', true) | ||
178 | } else { | ||
179 | this.symPillarCirc.setAttribute('cx', data.symbbox.middle.x) | ||
180 | } | ||
181 | } | ||
182 | } | ||
183 | |||
184 | if (this.dir === MOVE_NONE) { // Start point | ||
185 | this.circ.setAttribute('r', 24) | ||
186 | this.circ.setAttribute('class', this.circ.getAttribute('class') + ' start') | ||
187 | if (data.puzzle.symType != SYM_TYPE_NONE) { | ||
188 | this.symCirc.setAttribute('r', 24) | ||
189 | this.symCirc.setAttribute('class', this.symCirc.getAttribute('class') + ' start') | ||
190 | } | ||
191 | } else { | ||
192 | // Only insert poly1 in non-startpoints | ||
193 | data.svg.insertBefore(this.poly1, data.cursor) | ||
194 | this.circ.setAttribute('r', 12) | ||
195 | if (data.puzzle.symType != SYM_TYPE_NONE) { | ||
196 | data.svg.insertBefore(this.symPoly1, data.cursor) | ||
197 | this.symCirc.setAttribute('r', 12) | ||
198 | } | ||
199 | } | ||
200 | } | ||
201 | |||
202 | destroy() { | ||
203 | data.svg.removeChild(this.poly1) | ||
204 | data.svg.removeChild(this.circ) | ||
205 | data.svg.removeChild(this.poly2) | ||
206 | data.svg.removeChild(this.pillarCirc) | ||
207 | if (data.puzzle.symType != SYM_TYPE_NONE) { | ||
208 | data.svg.removeChild(this.symPoly1) | ||
209 | data.svg.removeChild(this.symCirc) | ||
210 | data.svg.removeChild(this.symPoly2) | ||
211 | data.svg.removeChild(this.symPillarCirc) | ||
212 | } | ||
213 | } | ||
214 | |||
215 | redraw() { // Uses raw bbox because of endpoints | ||
216 | // Move the cursor and related objects | ||
217 | var x = clamp(data.x, data.bbox.x1, data.bbox.x2) | ||
218 | var y = clamp(data.y, data.bbox.y1, data.bbox.y2) | ||
219 | data.cursor.setAttribute('cx', x) | ||
220 | data.cursor.setAttribute('cy', y) | ||
221 | if (data.puzzle.symType != SYM_TYPE_NONE) { | ||
222 | data.symcursor.setAttribute('cx', this._reflX(x,y)) | ||
223 | data.symcursor.setAttribute('cy', this._reflY(x,y)) | ||
224 | } | ||
225 | if (data.puzzle.pillar === true) { | ||
226 | if (this.pillarCirc.getAttribute('static') == null) { | ||
227 | this.pillarCirc.setAttribute('cx', x) | ||
228 | this.pillarCirc.setAttribute('cy', y) | ||
229 | } | ||
230 | if (data.puzzle.symType != SYM_TYPE_NONE) { | ||
231 | if (this.symPillarCirc.getAttribute('static') == null) { | ||
232 | this.symPillarCirc.setAttribute('cx', this._reflX(x,y)) | ||
233 | this.symPillarCirc.setAttribute('cy', this._reflY(x,y)) | ||
234 | } | ||
235 | } | ||
236 | } | ||
237 | |||
238 | // Draw the first-half box | ||
239 | var points1 = JSON.parse(JSON.stringify(data.bbox.raw)) | ||
240 | if (this.dir === MOVE_LEFT) { | ||
241 | points1.x1 = clamp(data.x, data.bbox.middle.x, data.bbox.x2) | ||
242 | } else if (this.dir === MOVE_RIGHT) { | ||
243 | points1.x2 = clamp(data.x, data.bbox.x1, data.bbox.middle.x) | ||
244 | } else if (this.dir === MOVE_TOP) { | ||
245 | points1.y1 = clamp(data.y, data.bbox.middle.y, data.bbox.y2) | ||
246 | } else if (this.dir === MOVE_BOTTOM) { | ||
247 | points1.y2 = clamp(data.y, data.bbox.y1, data.bbox.middle.y) | ||
248 | } | ||
249 | this.poly1.setAttribute('points', | ||
250 | points1.x1 + ' ' + points1.y1 + ',' + | ||
251 | points1.x1 + ' ' + points1.y2 + ',' + | ||
252 | points1.x2 + ' ' + points1.y2 + ',' + | ||
253 | points1.x2 + ' ' + points1.y1 | ||
254 | ) | ||
255 | |||
256 | var firstHalf = false | ||
257 | var isEnd = (data.puzzle.grid[data.pos.x][data.pos.y].end != null) | ||
258 | // The second half of the line uses the raw so that it can enter the endpoint properly. | ||
259 | var points2 = JSON.parse(JSON.stringify(data.bbox.raw)) | ||
260 | if (data.x < data.bbox.middle.x && this.dir !== MOVE_RIGHT) { | ||
261 | points2.x1 = clamp(data.x, data.bbox.x1, data.bbox.middle.x) | ||
262 | points2.x2 = data.bbox.middle.x | ||
263 | if (isEnd && data.pos.x%2 === 0 && data.pos.y%2 === 1) { | ||
264 | points2.y1 += 17 | ||
265 | points2.y2 -= 17 | ||
266 | } | ||
267 | } else if (data.x > data.bbox.middle.x && this.dir !== MOVE_LEFT) { | ||
268 | points2.x1 = data.bbox.middle.x | ||
269 | points2.x2 = clamp(data.x, data.bbox.middle.x, data.bbox.x2) | ||
270 | if (isEnd && data.pos.x%2 === 0 && data.pos.y%2 === 1) { | ||
271 | points2.y1 += 17 | ||
272 | points2.y2 -= 17 | ||
273 | } | ||
274 | } else if (data.y < data.bbox.middle.y && this.dir !== MOVE_BOTTOM) { | ||
275 | points2.y1 = clamp(data.y, data.bbox.y1, data.bbox.middle.y) | ||
276 | points2.y2 = data.bbox.middle.y | ||
277 | if (isEnd && data.pos.x%2 === 1 && data.pos.y%2 === 0) { | ||
278 | points2.x1 += 17 | ||
279 | points2.x2 -= 17 | ||
280 | } | ||
281 | } else if (data.y > data.bbox.middle.y && this.dir !== MOVE_TOP) { | ||
282 | points2.y1 = data.bbox.middle.y | ||
283 | points2.y2 = clamp(data.y, data.bbox.middle.y, data.bbox.y2) | ||
284 | if (isEnd && data.pos.x%2 === 1 && data.pos.y%2 === 0) { | ||
285 | points2.x1 += 17 | ||
286 | points2.x2 -= 17 | ||
287 | } | ||
288 | } else { | ||
289 | firstHalf = true | ||
290 | } | ||
291 | |||
292 | this.poly2.setAttribute('points', | ||
293 | points2.x1 + ' ' + points2.y1 + ',' + | ||
294 | points2.x1 + ' ' + points2.y2 + ',' + | ||
295 | points2.x2 + ' ' + points2.y2 + ',' + | ||
296 | points2.x2 + ' ' + points2.y1 | ||
297 | ) | ||
298 | |||
299 | // Show the second poly only in the second half of the cell | ||
300 | this.poly2.setAttribute('opacity', (firstHalf ? 0 : 1)) | ||
301 | // Show the circle in the second half of the cell AND in the start | ||
302 | if (firstHalf && this.dir !== MOVE_NONE) { | ||
303 | this.circ.setAttribute('opacity', 0) | ||
304 | } else { | ||
305 | this.circ.setAttribute('opacity', 1) | ||
306 | } | ||
307 | |||
308 | // Draw the symmetrical path based on the original one | ||
309 | if (data.puzzle.symType != SYM_TYPE_NONE) { | ||
310 | this.symPoly1.setAttribute('points', | ||
311 | this._reflX(points1.x2, points1.y2) + ' ' + this._reflY(points1.x2, points1.y2) + ',' + | ||
312 | this._reflX(points1.x2, points1.y2) + ' ' + this._reflY(points1.x1, points1.y1) + ',' + | ||
313 | this._reflX(points1.x1, points1.y1) + ' ' + this._reflY(points1.x1, points1.y1) + ',' + | ||
314 | this._reflX(points1.x1, points1.y1) + ' ' + this._reflY(points1.x2, points1.y2) | ||
315 | ) | ||
316 | |||
317 | this.symPoly2.setAttribute('points', | ||
318 | this._reflX(points2.x2, points2.y2) + ' ' + this._reflY(points2.x2, points2.y2) + ',' + | ||
319 | this._reflX(points2.x2, points2.y2) + ' ' + this._reflY(points2.x1, points2.y1) + ',' + | ||
320 | this._reflX(points2.x1, points2.y1) + ' ' + this._reflY(points2.x1, points2.y1) + ',' + | ||
321 | this._reflX(points2.x1, points2.y1) + ' ' + this._reflY(points2.x2, points2.y2) | ||
322 | ) | ||
323 | |||
324 | this.symCirc.setAttribute('opacity', this.circ.getAttribute('opacity')) | ||
325 | this.symPoly2.setAttribute('opacity', this.poly2.getAttribute('opacity')) | ||
326 | } | ||
327 | } | ||
328 | |||
329 | _reflX(x,y) { | ||
330 | if (data.puzzle.symType == SYM_TYPE_NONE) return x | ||
331 | |||
332 | if (data.puzzle.symType == SYM_TYPE_VERTICAL || data.puzzle.symType == SYM_TYPE_ROTATIONAL || data.puzzle.symType == SYM_TYPE_PARALLEL_H_FLIP) { | ||
333 | // Mirror position inside the bounding box | ||
334 | return (data.bbox.middle.x - x) + data.symbbox.middle.x | ||
335 | } | ||
336 | if (data.puzzle.symType == SYM_TYPE_HORIZONTAL || data.puzzle.symType == SYM_TYPE_PARALLEL_H || data.puzzle.symType == SYM_TYPE_PARALLEL_V || data.puzzle.symType == SYM_TYPE_PARALLEL_V_FLIP) { | ||
337 | // Copy position inside the bounding box | ||
338 | return (x - data.bbox.middle.x) + data.symbbox.middle.x | ||
339 | } | ||
340 | if (data.puzzle.symType == SYM_TYPE_ROTATE_LEFT || data.puzzle.symType == SYM_TYPE_FLIP_XY) { | ||
341 | // Rotate position left inside the bounding box | ||
342 | return (y - data.bbox.middle.y) + data.symbbox.middle.x | ||
343 | } | ||
344 | if (data.puzzle.symType == SYM_TYPE_ROTATE_RIGHT || data.puzzle.symType == SYM_TYPE_FLIP_NEG_XY) { | ||
345 | // Rotate position right inside the bounding box | ||
346 | return (data.bbox.middle.y - y) + data.symbbox.middle.x | ||
347 | } | ||
348 | } | ||
349 | |||
350 | _reflY(x,y) { | ||
351 | if (data.puzzle.symType == SYM_TYPE_NONE) return y | ||
352 | |||
353 | if (data.puzzle.symType == SYM_TYPE_HORIZONTAL || data.puzzle.symType == SYM_TYPE_ROTATIONAL || data.puzzle.symType == SYM_TYPE_PARALLEL_V_FLIP) { | ||
354 | // Mirror position inside the bounding box | ||
355 | return (data.bbox.middle.y - y) + data.symbbox.middle.y | ||
356 | } | ||
357 | if (data.puzzle.symType == SYM_TYPE_VERTICAL || data.puzzle.symType == SYM_TYPE_PARALLEL_V || data.puzzle.symType == SYM_TYPE_PARALLEL_H || data.puzzle.symType == SYM_TYPE_PARALLEL_H_FLIP) { | ||
358 | // Copy position inside the bounding box | ||
359 | return (y - data.bbox.middle.y) + data.symbbox.middle.y | ||
360 | } | ||
361 | if (data.puzzle.symType == SYM_TYPE_ROTATE_RIGHT || data.puzzle.symType == SYM_TYPE_FLIP_XY) { | ||
362 | // Rotate position left inside the bounding box | ||
363 | return (x - data.bbox.middle.x) + data.symbbox.middle.y | ||
364 | } | ||
365 | if (data.puzzle.symType == SYM_TYPE_ROTATE_LEFT || data.puzzle.symType == SYM_TYPE_FLIP_NEG_XY) { | ||
366 | // Rotate position right inside the bounding box | ||
367 | return (data.bbox.middle.x - x) + data.symbbox.middle.y | ||
368 | } | ||
369 | } | ||
370 | } | ||
371 | |||
372 | var data = {} | ||
373 | |||
374 | function clearGrid(svg, puzzle) { | ||
375 | if (data.bbox != null && data.bbox.debug != null) { | ||
376 | data.svg.removeChild(data.bbox.debug) | ||
377 | data.bbox = null | ||
378 | } | ||
379 | if (data.symbbox != null && data.symbbox.debug != null) { | ||
380 | data.svg.removeChild(data.symbbox.debug) | ||
381 | data.symbbox = null | ||
382 | } | ||
383 | |||
384 | window.deleteElementsByClassName(svg, 'cursor') | ||
385 | window.deleteElementsByClassName(svg, 'line-1') | ||
386 | window.deleteElementsByClassName(svg, 'line-2') | ||
387 | window.deleteElementsByClassName(svg, 'line-3') | ||
388 | window.deleteElementsByClassName(svg, 'line-4') | ||
389 | puzzle.clearLines() | ||
390 | } | ||
391 | |||
392 | // This copy is an exact copy of puzzle.getSymmetricalDir, except that it uses MOVE_* values instead of strings | ||
393 | function getSymmetricalDir(puzzle, dir) { | ||
394 | if (puzzle.symType == SYM_TYPE_VERTICAL || puzzle.symType == SYM_TYPE_ROTATIONAL || puzzle.symType == SYM_TYPE_PARALLEL_H_FLIP) { | ||
395 | if (dir === MOVE_LEFT) return MOVE_RIGHT | ||
396 | if (dir === MOVE_RIGHT) return MOVE_LEFT | ||
397 | } | ||
398 | if (puzzle.symType == SYM_TYPE_HORIZONTAL || puzzle.symType == SYM_TYPE_ROTATIONAL || puzzle.symType == SYM_TYPE_PARALLEL_V_FLIP) { | ||
399 | if (dir === MOVE_TOP) return MOVE_BOTTOM | ||
400 | if (dir === MOVE_BOTTOM) return MOVE_TOP | ||
401 | } | ||
402 | if (puzzle.symType == SYM_TYPE_ROTATE_LEFT || puzzle.symType == SYM_TYPE_FLIP_NEG_XY) { | ||
403 | if (dir === MOVE_LEFT) return MOVE_BOTTOM | ||
404 | if (dir === MOVE_RIGHT) return MOVE_TOP | ||
405 | } | ||
406 | if (puzzle.symType == SYM_TYPE_ROTATE_RIGHT || puzzle.symType == SYM_TYPE_FLIP_NEG_XY) { | ||
407 | if (dir === MOVE_TOP) return MOVE_RIGHT | ||
408 | if (dir === MOVE_BOTTOM) return MOVE_LEFT | ||
409 | } | ||
410 | if (puzzle.symType == SYM_TYPE_ROTATE_LEFT || puzzle.symType == SYM_TYPE_FLIP_XY) { | ||
411 | if (dir === MOVE_TOP) return MOVE_LEFT | ||
412 | if (dir === MOVE_BOTTOM) return MOVE_RIGHT | ||
413 | } | ||
414 | if (puzzle.symType == SYM_TYPE_ROTATE_RIGHT || puzzle.symType == SYM_TYPE_FLIP_XY) { | ||
415 | if (dir === MOVE_RIGHT) return MOVE_BOTTOM | ||
416 | if (dir === MOVE_LEFT) return MOVE_TOP | ||
417 | } | ||
418 | return dir | ||
419 | } | ||
420 | |||
421 | window.trace = function(event, puzzle, pos, start, symStart=null) { | ||
422 | /*if (data.start == null) {*/ | ||
423 | if (data.tracing !== true) { // could be undefined or false | ||
424 | var svg = start.parentElement | ||
425 | data.tracing = true | ||
426 | window.PLAY_SOUND('start') | ||
427 | // Cleans drawn lines & puzzle state | ||
428 | clearGrid(svg, puzzle) | ||
429 | onTraceStart(puzzle, pos, svg, start, symStart) | ||
430 | data.animations.insertRule('.' + svg.id + '.start {animation: 150ms 1 forwards start-grow}\n') | ||
431 | |||
432 | hookMovementEvents(start) | ||
433 | } else { | ||
434 | event.stopPropagation() | ||
435 | // Signal the onMouseMove to stop accepting input (race condition) | ||
436 | data.tracing = false | ||
437 | |||
438 | // At endpoint and in main box | ||
439 | var cell = puzzle.getCell(data.pos.x, data.pos.y) | ||
440 | if (cell.end != null && data.bbox.inMain(data.x, data.y)) { | ||
441 | data.cursor.onpointerdown = null | ||
442 | setTimeout(function() { // Run validation asynchronously so we can free the pointer immediately. | ||
443 | puzzle.endPoint = data.pos | ||
444 | var puzzleData = window.validate(puzzle, false) // We want all invalid elements so we can show the user. | ||
445 | |||
446 | for (var negation of puzzleData.negations) { | ||
447 | console.debug('Rendering negation', negation) | ||
448 | data.animations.insertRule('.' + data.svg.id + '_' + negation.source.x + '_' + negation.source.y + ' {animation: 0.75s 1 forwards fade}\n') | ||
449 | data.animations.insertRule('.' + data.svg.id + '_' + negation.target.x + '_' + negation.target.y + ' {animation: 0.75s 1 forwards fade}\n') | ||
450 | } | ||
451 | |||
452 | if (puzzleData.valid()) { | ||
453 | window.PLAY_SOUND('success') | ||
454 | // !important to override the child animation | ||
455 | data.animations.insertRule('.' + data.svg.id + ' {animation: 1s 1 forwards line-success !important}\n') | ||
456 | |||
457 | // Convert the traced path into something suitable for solve.drawPath (for publishing purposes) | ||
458 | var rawPath = [puzzle.startPoint] | ||
459 | for (var i=1; i<data.path.length; i++) rawPath.push(data.path[i].dir) | ||
460 | rawPath.push(0) | ||
461 | |||
462 | if (window.TRACE_COMPLETION_FUNC) window.TRACE_COMPLETION_FUNC(puzzle, rawPath) | ||
463 | } else { | ||
464 | window.PLAY_SOUND('fail') | ||
465 | data.animations.insertRule('.' + data.svg.id + ' {animation: 1s 1 forwards line-fail !important}\n') | ||
466 | // Get list of invalid elements | ||
467 | if (puzzle.settings.FLASH_FOR_ERRORS) { | ||
468 | for (var invalidElement of puzzleData.invalidElements) { | ||
469 | data.animations.insertRule('.' + data.svg.id + '_' + invalidElement.x + '_' + invalidElement.y + ' {animation: 0.4s 20 alternate-reverse error}\n') | ||
470 | } | ||
471 | } | ||
472 | } | ||
473 | }, 1) | ||
474 | |||
475 | // Right-clicked (or double-tapped) and not at the end: Clear puzzle | ||
476 | } else if (event.isRightClick()) { | ||
477 | window.PLAY_SOUND('abort') | ||
478 | clearGrid(data.svg, puzzle) | ||
479 | } else { // Exit lock but allow resuming from the cursor (Desktop only) | ||
480 | data.cursor.onpointerdown = function() { | ||
481 | if (start.parentElement !== data.svg) return // Another puzzle is live, so data is gone | ||
482 | data.tracing = true | ||
483 | hookMovementEvents(start) | ||
484 | } | ||
485 | } | ||
486 | |||
487 | unhookMovementEvents() | ||
488 | } | ||
489 | } | ||
490 | |||
491 | window.clearAnimations = function() { | ||
492 | if (data.animations == null) return | ||
493 | for (var i=0; i<data.animations.cssRules.length; i++) { | ||
494 | var rule = data.animations.cssRules[i] | ||
495 | if (rule.selectorText != null && rule.selectorText.startsWith('.' + data.svg.id)) { | ||
496 | data.animations.deleteRule(i--) | ||
497 | } | ||
498 | } | ||
499 | } | ||
500 | |||
501 | window.onTraceStart = function(puzzle, pos, svg, start, symStart=null) { | ||
502 | var x = parseFloat(start.getAttribute('cx')) | ||
503 | var y = parseFloat(start.getAttribute('cy')) | ||
504 | |||
505 | var cursor = createElement('circle') | ||
506 | cursor.setAttribute('r', 12) | ||
507 | cursor.setAttribute('fill', window.CURSOR) | ||
508 | cursor.setAttribute('stroke', 'black') | ||
509 | cursor.setAttribute('stroke-width', '2px') | ||
510 | cursor.setAttribute('stroke-opacity', '0.4') | ||
511 | cursor.setAttribute('class', 'cursor') | ||
512 | cursor.setAttribute('cx', x) | ||
513 | cursor.setAttribute('cy', y) | ||
514 | svg.insertBefore(cursor, svg.getElementById('cursorPos')) | ||
515 | |||
516 | data.svg = svg | ||
517 | data.cursor = cursor | ||
518 | data.x = x | ||
519 | data.y = y | ||
520 | data.pos = pos | ||
521 | data.sym = puzzle.getSymmetricalPos(pos.x, pos.y) | ||
522 | data.puzzle = puzzle | ||
523 | data.path = [] | ||
524 | puzzle.startPoint = {'x': pos.x, 'y': pos.y} | ||
525 | |||
526 | if (pos.x % 2 === 1) { // Start point is on a horizontal segment | ||
527 | data.bbox = new BoundingBox(x - 29, x + 29, y - 12, y + 12) | ||
528 | } else if (pos.y % 2 === 1) { // Start point is on a vertical segment | ||
529 | data.bbox = new BoundingBox(x - 12, x + 12, y - 29, y + 29) | ||
530 | } else { // Start point is at an intersection | ||
531 | data.bbox = new BoundingBox(x - 12, x + 12, y - 12, y + 12) | ||
532 | } | ||
533 | |||
534 | for (var styleSheet of document.styleSheets) { | ||
535 | if (styleSheet.title === 'animations') { | ||
536 | data.animations = styleSheet | ||
537 | break | ||
538 | } | ||
539 | } | ||
540 | |||
541 | clearAnimations() | ||
542 | |||
543 | // Add initial line segments + secondary symmetry cursor, if needed | ||
544 | if (puzzle.symType == SYM_TYPE_NONE) { | ||
545 | data.puzzle.updateCell2(data.pos.x, data.pos.y, 'type', 'line') | ||
546 | data.puzzle.updateCell2(data.pos.x, data.pos.y, 'line', window.LINE_BLACK) | ||
547 | } else { | ||
548 | data.puzzle.updateCell2(data.pos.x, data.pos.y, 'type', 'line') | ||
549 | data.puzzle.updateCell2(data.pos.x, data.pos.y, 'line', window.LINE_BLUE) | ||
550 | data.puzzle.updateCell2(data.sym.x, data.sym.y, 'type', 'line') | ||
551 | data.puzzle.updateCell2(data.sym.x, data.sym.y, 'line', window.LINE_YELLOW) | ||
552 | |||
553 | var dx = parseFloat(symStart.getAttribute('cx')) - data.x | ||
554 | var dy = parseFloat(symStart.getAttribute('cy')) - data.y | ||
555 | data.symbbox = new BoundingBox( | ||
556 | data.bbox.raw.x1 + dx, | ||
557 | data.bbox.raw.x2 + dx, | ||
558 | data.bbox.raw.y1 + dy, | ||
559 | data.bbox.raw.y2 + dy, | ||
560 | sym = true) | ||
561 | |||
562 | data.symcursor = createElement('circle') | ||
563 | svg.appendChild(data.symcursor) | ||
564 | if (data.puzzle.settings.INVISIBLE_SYMMETRY) { | ||
565 | data.symcursor.setAttribute('class', 'line-4 ' + data.svg.id) | ||
566 | } else { | ||
567 | data.symcursor.setAttribute('class', 'line-3 ' + data.svg.id) | ||
568 | } | ||
569 | data.symcursor.setAttribute('cx', symStart.getAttribute('cx')) | ||
570 | data.symcursor.setAttribute('cy', symStart.getAttribute('cy')) | ||
571 | data.symcursor.setAttribute('r', 12) | ||
572 | } | ||
573 | |||
574 | // Fixup: Mark out of bounds cells as null, setting inbounds cells as {} | ||
575 | // This allows tracing to correctly identify inbounds cells (and thus interior walls) and correctly handle exterior walls for oddly shaped puzzles. | ||
576 | { | ||
577 | var savedGrid = data.puzzle.switchToMaskedGrid() | ||
578 | var maskedGrid = data.puzzle.grid | ||
579 | data.puzzle.grid = savedGrid | ||
580 | |||
581 | for (var x=1; x<data.puzzle.width; x+=2) { | ||
582 | for (var y=1; y<data.puzzle.height; y+=2) { | ||
583 | if (maskedGrid[x][y] == null) { // null == MASKED_OOB | ||
584 | data.puzzle.grid[x][y] = null | ||
585 | } else if (data.puzzle.grid[x][y] == null) { | ||
586 | data.puzzle.grid[x][y] = {'type':'nonce'} | ||
587 | } | ||
588 | } | ||
589 | } | ||
590 | } | ||
591 | data.path.push(new PathSegment(MOVE_NONE)) // Must be created after initializing data.symbbox | ||
592 | } | ||
593 | |||
594 | // In case the user exit the pointer lock via another means (clicking outside the window, hitting esc, etc) | ||
595 | // we still need to disengage our tracing hooks. | ||
596 | document.onpointerlockchange = function() { | ||
597 | if (document.pointerLockElement == null) unhookMovementEvents() | ||
598 | } | ||
599 | |||
600 | function unhookMovementEvents() { | ||
601 | data.start = null | ||
602 | document.onmousemove = null | ||
603 | document.ontouchstart = null | ||
604 | document.ontouchmove = null | ||
605 | document.ontouchend = null | ||
606 | if (document.exitPointerLock != null) document.exitPointerLock() | ||
607 | if (document.mozExitPointerLock != null) document.mozExitPointerLock() | ||
608 | } | ||
609 | |||
610 | function hookMovementEvents(start) { | ||
611 | data.start = start | ||
612 | if (start.requestPointerLock != null) start.requestPointerLock() | ||
613 | if (start.mozRequestPointerLock != null) start.mozRequestPointerLock() | ||
614 | |||
615 | var sens = parseFloat(document.getElementById('sens').value) | ||
616 | document.onmousemove = function(event) { | ||
617 | // Working around a race condition where movement events fire after the handler is removed. | ||
618 | if (data.tracing !== true) return | ||
619 | // Prevent accidental fires on mobile platforms (ios and android). They will be handled via ontouchmove instead. | ||
620 | if (event.movementX == null) return | ||
621 | onMove(sens * event.movementX, sens * event.movementY) | ||
622 | } | ||
623 | document.ontouchstart = function(event) { | ||
624 | if (event.touches.length > 1) { | ||
625 | // Stop tracing for two+ finger touches (the equivalent of a right click on desktop) | ||
626 | window.trace(event, data.puzzle, null, null, null) | ||
627 | return | ||
628 | } | ||
629 | data.lastTouchPos = event.position | ||
630 | } | ||
631 | document.ontouchmove = function(event) { | ||
632 | if (data.tracing !== true) return | ||
633 | |||
634 | var eventIsWithinPuzzle = false | ||
635 | for (var node = event.target; node != null; node = node.parentElement) { | ||
636 | if (node == data.svg) { | ||
637 | eventIsWithinPuzzle = true | ||
638 | break | ||
639 | } | ||
640 | } | ||
641 | if (!eventIsWithinPuzzle) return // Ignore drag events that aren't within the puzzle | ||
642 | event.preventDefault() // Prevent accidental scrolling if the touch event is within the puzzle. | ||
643 | |||
644 | var newPos = event.position | ||
645 | onMove(newPos.x - data.lastTouchPos.x, newPos.y - data.lastTouchPos.y) | ||
646 | data.lastTouchPos = newPos | ||
647 | } | ||
648 | document.ontouchend = function(event) { | ||
649 | data.lastTouchPos = null | ||
650 | // Only call window.trace (to stop tracing) if we're really in an endpoint. | ||
651 | var cell = data.puzzle.getCell(data.pos.x, data.pos.y) | ||
652 | if (cell.end != null && data.bbox.inMain(data.x, data.y)) { | ||
653 | window.trace(event, data.puzzle, null, null, null) | ||
654 | } | ||
655 | } | ||
656 | } | ||
657 | |||
658 | // @Volatile -- must match order of PATH_* in solve | ||
659 | var MOVE_NONE = 0 | ||
660 | var MOVE_LEFT = 1 | ||
661 | var MOVE_RIGHT = 2 | ||
662 | var MOVE_TOP = 3 | ||
663 | var MOVE_BOTTOM = 4 | ||
664 | |||
665 | window.onMove = function(dx, dy) { | ||
666 | { | ||
667 | // Also handles some collision | ||
668 | var collidedWith = pushCursor(dx, dy) | ||
669 | console.spam('Collided with', collidedWith) | ||
670 | } | ||
671 | |||
672 | while (true) { | ||
673 | hardCollision() | ||
674 | |||
675 | // Potentially move the location to a new cell, and make absolute boundary checks | ||
676 | var moveDir = move() | ||
677 | data.path[data.path.length - 1].redraw() | ||
678 | if (moveDir === MOVE_NONE) break | ||
679 | console.debug('Moved', ['none', 'left', 'right', 'top', 'bottom'][moveDir]) | ||
680 | |||
681 | // Potentially adjust data.x/data.y if our position went around a pillar | ||
682 | if (data.puzzle.pillar === true) pillarWrap(moveDir) | ||
683 | |||
684 | var lastDir = data.path[data.path.length - 1].dir | ||
685 | var backedUp = ((moveDir === MOVE_LEFT && lastDir === MOVE_RIGHT) | ||
686 | || (moveDir === MOVE_RIGHT && lastDir === MOVE_LEFT) | ||
687 | || (moveDir === MOVE_TOP && lastDir === MOVE_BOTTOM) | ||
688 | || (moveDir === MOVE_BOTTOM && lastDir === MOVE_TOP)) | ||
689 | |||
690 | if (data.puzzle.symType != SYM_TYPE_NONE) { | ||
691 | var symMoveDir = getSymmetricalDir(data.puzzle, moveDir) | ||
692 | } | ||
693 | |||
694 | // If we backed up, remove a path segment and mark the old cell as unvisited | ||
695 | if (backedUp) { | ||
696 | data.path.pop().destroy() | ||
697 | data.puzzle.updateCell2(data.pos.x, data.pos.y, 'line', window.LINE_NONE) | ||
698 | if (data.puzzle.symType != SYM_TYPE_NONE) { | ||
699 | if (data.puzzle.getLine(data.sym.x, data.sym.y) == window.LINE_OVERLAP) { | ||
700 | data.puzzle.updateCell2(data.sym.x, data.sym.y, 'line', window.LINE_BLUE) | ||
701 | } else { | ||
702 | data.puzzle.updateCell2(data.sym.x, data.sym.y, 'line', window.LINE_NONE) | ||
703 | } | ||
704 | } | ||
705 | } | ||
706 | |||
707 | // Move to the next cell | ||
708 | changePos(data.bbox, data.pos, moveDir) | ||
709 | if (data.puzzle.symType != SYM_TYPE_NONE) { | ||
710 | changePos(data.symbbox, data.sym, symMoveDir) | ||
711 | } | ||
712 | |||
713 | // If we didn't back up, add a path segment and mark the new cell as visited | ||
714 | if (!backedUp) { | ||
715 | data.path.push(new PathSegment(moveDir)) | ||
716 | if (data.puzzle.symType == SYM_TYPE_NONE) { | ||
717 | data.puzzle.updateCell2(data.pos.x, data.pos.y, 'line', window.LINE_BLACK) | ||
718 | } else { | ||
719 | data.puzzle.updateCell2(data.pos.x, data.pos.y, 'line', window.LINE_BLUE) | ||
720 | if (data.puzzle.getLine(data.sym.x, data.sym.y) == window.LINE_BLUE) { | ||
721 | data.puzzle.updateCell2(data.sym.x, data.sym.y, 'line', window.LINE_OVERLAP) | ||
722 | } else { | ||
723 | data.puzzle.updateCell2(data.sym.x, data.sym.y, 'line', window.LINE_YELLOW) | ||
724 | } | ||
725 | } | ||
726 | } | ||
727 | } | ||
728 | } | ||
729 | |||
730 | // Helper function for pushCursor. Used to determine the direction and magnitude of redirection. | ||
731 | function push(dx, dy, dir, targetDir) { | ||
732 | // Fraction of movement to redirect in the other direction | ||
733 | var movementRatio = null | ||
734 | if (targetDir === 'left' || targetDir === 'top') { | ||
735 | movementRatio = -3 | ||
736 | } else if (targetDir === 'right' || targetDir === 'bottom') { | ||
737 | movementRatio = 3 | ||
738 | } | ||
739 | if (window.settings.disablePushing === true) movementRatio *= 1000 | ||
740 | |||
741 | if (dir === 'left') { | ||
742 | var overshoot = data.bbox.x1 - (data.x + dx) + 12 | ||
743 | if (overshoot > 0) { | ||
744 | data.y += dy + overshoot / movementRatio | ||
745 | data.x = data.bbox.x1 + 12 | ||
746 | return true | ||
747 | } | ||
748 | } else if (dir === 'right') { | ||
749 | var overshoot = (data.x + dx) - data.bbox.x2 + 12 | ||
750 | if (overshoot > 0) { | ||
751 | data.y += dy + overshoot / movementRatio | ||
752 | data.x = data.bbox.x2 - 12 | ||
753 | return true | ||
754 | } | ||
755 | } else if (dir === 'leftright') { | ||
756 | data.y += dy + Math.abs(dx) / movementRatio | ||
757 | return true | ||
758 | } else if (dir === 'top') { | ||
759 | var overshoot = data.bbox.y1 - (data.y + dy) + 12 | ||
760 | if (overshoot > 0) { | ||
761 | data.x += dx + overshoot / movementRatio | ||
762 | data.y = data.bbox.y1 + 12 | ||
763 | return true | ||
764 | } | ||
765 | } else if (dir === 'bottom') { | ||
766 | var overshoot = (data.y + dy) - data.bbox.y2 + 12 | ||
767 | if (overshoot > 0) { | ||
768 | data.x += dx + overshoot / movementRatio | ||
769 | data.y = data.bbox.y2 - 12 | ||
770 | return true | ||
771 | } | ||
772 | } else if (dir === 'topbottom') { | ||
773 | data.x += dx + Math.abs(dy) / movementRatio | ||
774 | return true | ||
775 | } | ||
776 | return false | ||
777 | } | ||
778 | |||
779 | // Redirect momentum from pushing against walls, so that all further moment steps | ||
780 | // will be strictly linear. Returns a string for logging purposes only. | ||
781 | function pushCursor(dx, dy) { | ||
782 | // Outer wall collision | ||
783 | var cell = data.puzzle.getCell(data.pos.x, data.pos.y) | ||
784 | if (cell == null) return 'nothing' | ||
785 | |||
786 | // Only consider non-endpoints or endpoints which are parallel | ||
787 | if ([undefined, 'top', 'bottom'].includes(cell.end)) { | ||
788 | var leftCell = data.puzzle.getCell(data.pos.x - 1, data.pos.y) | ||
789 | if (leftCell == null || leftCell.gap === window.GAP_FULL) { | ||
790 | if (push(dx, dy, 'left', 'top')) return 'left outer wall' | ||
791 | } | ||
792 | var rightCell = data.puzzle.getCell(data.pos.x + 1, data.pos.y) | ||
793 | if (rightCell == null || rightCell.gap === window.GAP_FULL) { | ||
794 | if (push(dx, dy, 'right', 'top')) return 'right outer wall' | ||
795 | } | ||
796 | } | ||
797 | // Only consider non-endpoints or endpoints which are parallel | ||
798 | if ([undefined, 'left', 'right'].includes(cell.end)) { | ||
799 | var topCell = data.puzzle.getCell(data.pos.x, data.pos.y - 1) | ||
800 | if (topCell == null || topCell.gap === window.GAP_FULL) { | ||
801 | if (push(dx, dy, 'top', 'right')) return 'top outer wall' | ||
802 | } | ||
803 | var bottomCell = data.puzzle.getCell(data.pos.x, data.pos.y + 1) | ||
804 | if (bottomCell == null || bottomCell.gap === window.GAP_FULL) { | ||
805 | if (push(dx, dy, 'bottom', 'right')) return 'bottom outer wall' | ||
806 | } | ||
807 | } | ||
808 | |||
809 | // Inner wall collision | ||
810 | if (cell.end == null) { | ||
811 | if (data.pos.x%2 === 1 && data.pos.y%2 === 0) { // Horizontal cell | ||
812 | if (data.x < data.bbox.middle.x) { | ||
813 | push(dx, dy, 'topbottom', 'left') | ||
814 | return 'topbottom inner wall, moved left' | ||
815 | } else { | ||
816 | push(dx, dy, 'topbottom', 'right') | ||
817 | return 'topbottom inner wall, moved right' | ||
818 | } | ||
819 | } else if (data.pos.x%2 === 0 && data.pos.y%2 === 1) { // Vertical cell | ||
820 | if (data.y < data.bbox.middle.y) { | ||
821 | push(dx, dy, 'leftright', 'top') | ||
822 | return 'leftright inner wall, moved up' | ||
823 | } else { | ||
824 | push(dx, dy, 'leftright', 'bottom') | ||
825 | return 'leftright inner wall, moved down' | ||
826 | } | ||
827 | } | ||
828 | } | ||
829 | |||
830 | // Intersection & endpoint collision | ||
831 | // Ratio of movement to be considered turning at an intersection | ||
832 | var turnMod = 2 | ||
833 | if ((data.pos.x%2 === 0 && data.pos.y%2 === 0) || cell.end != null) { | ||
834 | if (data.x < data.bbox.middle.x) { | ||
835 | push(dx, dy, 'topbottom', 'right') | ||
836 | // Overshot the intersection and appears to be trying to turn | ||
837 | if (data.x > data.bbox.middle.x && Math.abs(dy) * turnMod > Math.abs(dx)) { | ||
838 | data.y += Math.sign(dy) * (data.x - data.bbox.middle.x) | ||
839 | data.x = data.bbox.middle.x | ||
840 | return 'overshot moving right' | ||
841 | } | ||
842 | return 'intersection moving right' | ||
843 | } else if (data.x > data.bbox.middle.x) { | ||
844 | push(dx, dy, 'topbottom', 'left') | ||
845 | // Overshot the intersection and appears to be trying to turn | ||
846 | if (data.x < data.bbox.middle.x && Math.abs(dy) * turnMod > Math.abs(dx)) { | ||
847 | data.y += Math.sign(dy) * (data.bbox.middle.x - data.x) | ||
848 | data.x = data.bbox.middle.x | ||
849 | return 'overshot moving left' | ||
850 | } | ||
851 | return 'intersection moving left' | ||
852 | } | ||
853 | if (data.y < data.bbox.middle.y) { | ||
854 | push(dx, dy, 'leftright', 'bottom') | ||
855 | // Overshot the intersection and appears to be trying to turn | ||
856 | if (data.y > data.bbox.middle.y && Math.abs(dx) * turnMod > Math.abs(dy)) { | ||
857 | data.x += Math.sign(dx) * (data.y - data.bbox.middle.y) | ||
858 | data.y = data.bbox.middle.y | ||
859 | return 'overshot moving down' | ||
860 | } | ||
861 | return 'intersection moving down' | ||
862 | } else if (data.y > data.bbox.middle.y) { | ||
863 | push(dx, dy, 'leftright', 'top') | ||
864 | // Overshot the intersection and appears to be trying to turn | ||
865 | if (data.y < data.bbox.middle.y && Math.abs(dx) * turnMod > Math.abs(dy)) { | ||
866 | data.x += Math.sign(dx) * (data.bbox.middle.y - data.y) | ||
867 | data.y = data.bbox.middle.y | ||
868 | return 'overshot moving up' | ||
869 | } | ||
870 | return 'intersection moving up' | ||
871 | } | ||
872 | } | ||
873 | |||
874 | // No collision, limit movement to X or Y only to prevent out-of-bounds | ||
875 | if (Math.abs(dx) > Math.abs(dy)) { | ||
876 | data.x += dx | ||
877 | return 'nothing, x' | ||
878 | } else { | ||
879 | data.y += dy | ||
880 | return 'nothing, y' | ||
881 | } | ||
882 | } | ||
883 | |||
884 | // Check to see if we collided with any gaps, or with a symmetrical line, or a startpoint. | ||
885 | // In any case, abruptly zero momentum. | ||
886 | function hardCollision() { | ||
887 | var lastDir = data.path[data.path.length - 1].dir | ||
888 | var cell = data.puzzle.getCell(data.pos.x, data.pos.y) | ||
889 | if (cell == null) return | ||
890 | |||
891 | var gapSize = 0 | ||
892 | if (cell.gap === window.GAP_BREAK) { | ||
893 | console.spam('Collided with a gap') | ||
894 | gapSize = 21 | ||
895 | } else { | ||
896 | var nextCell = null | ||
897 | if (lastDir === MOVE_LEFT) nextCell = data.puzzle.getCell(data.pos.x - 1, data.pos.y) | ||
898 | if (lastDir === MOVE_RIGHT) nextCell = data.puzzle.getCell(data.pos.x + 1, data.pos.y) | ||
899 | if (lastDir === MOVE_TOP) nextCell = data.puzzle.getCell(data.pos.x, data.pos.y - 1) | ||
900 | if (lastDir === MOVE_BOTTOM) nextCell = data.puzzle.getCell(data.pos.x, data.pos.y + 1) | ||
901 | if (nextCell != null && nextCell.start === true && nextCell.line > window.LINE_NONE) { | ||
902 | gapSize = -5 | ||
903 | } | ||
904 | } | ||
905 | |||
906 | if (data.puzzle.symType != SYM_TYPE_NONE) { | ||
907 | if (data.sym.x === data.pos.x && data.sym.y === data.pos.y) { | ||
908 | console.spam('Collided with our symmetrical line') | ||
909 | gapSize = 13 | ||
910 | } else if (data.puzzle.getCell(data.sym.x, data.sym.y).gap === window.GAP_BREAK) { | ||
911 | console.spam('Symmetrical line hit a gap') | ||
912 | gapSize = 21 | ||
913 | } | ||
914 | } | ||
915 | if (gapSize === 0) return // Didn't collide with anything | ||
916 | |||
917 | if (lastDir === MOVE_LEFT) { | ||
918 | data.x = Math.max(data.bbox.middle.x + gapSize, data.x) | ||
919 | } else if (lastDir === MOVE_RIGHT) { | ||
920 | data.x = Math.min(data.x, data.bbox.middle.x - gapSize) | ||
921 | } else if (lastDir === MOVE_TOP) { | ||
922 | data.y = Math.max(data.bbox.middle.y + gapSize, data.y) | ||
923 | } else if (lastDir === MOVE_BOTTOM) { | ||
924 | data.y = Math.min(data.y, data.bbox.middle.y - gapSize) | ||
925 | } | ||
926 | } | ||
927 | |||
928 | // Check to see if we've gone beyond the edge of puzzle cell, and if the next cell is safe, | ||
929 | // i.e. not out of bounds. Reports the direction we are going to move (or none), | ||
930 | // but does not actually change data.pos | ||
931 | function move() { | ||
932 | var lastDir = data.path[data.path.length - 1].dir | ||
933 | |||
934 | if (data.x < data.bbox.x1 + 12) { // Moving left | ||
935 | var cell = data.puzzle.getCell(data.pos.x - 1, data.pos.y) | ||
936 | if (cell == null || cell.type !== 'line' || cell.gap === window.GAP_FULL) { | ||
937 | console.spam('Collided with outside / gap-2', cell) | ||
938 | data.x = data.bbox.x1 + 12 | ||
939 | } else if (cell.line > window.LINE_NONE && lastDir !== MOVE_RIGHT) { | ||
940 | console.spam('Collided with other line', cell.line) | ||
941 | data.x = data.bbox.x1 + 12 | ||
942 | } else if (data.puzzle.symType != SYM_TYPE_NONE) { | ||
943 | var symCell = data.puzzle.getSymmetricalCell(data.pos.x - 1, data.pos.y) | ||
944 | if (symCell == null || symCell.type !== 'line' || symCell.gap === window.GAP_FULL) { | ||
945 | console.spam('Collided with symmetrical outside / gap-2', cell) | ||
946 | data.x = data.bbox.x1 + 12 | ||
947 | } | ||
948 | } | ||
949 | if (data.x < data.bbox.x1) { | ||
950 | return MOVE_LEFT | ||
951 | } | ||
952 | } else if (data.x > data.bbox.x2 - 12) { // Moving right | ||
953 | var cell = data.puzzle.getCell(data.pos.x + 1, data.pos.y) | ||
954 | if (cell == null || cell.type !== 'line' || cell.gap === window.GAP_FULL) { | ||
955 | console.spam('Collided with outside / gap-2', cell) | ||
956 | data.x = data.bbox.x2 - 12 | ||
957 | } else if (cell.line > window.LINE_NONE && lastDir !== MOVE_LEFT) { | ||
958 | console.spam('Collided with other line', cell.line) | ||
959 | data.x = data.bbox.x2 - 12 | ||
960 | } else if (data.puzzle.symType != SYM_TYPE_NONE) { | ||
961 | var symCell = data.puzzle.getSymmetricalCell(data.pos.x + 1, data.pos.y) | ||
962 | if (symCell == null || symCell.type !== 'line' || symCell.gap === window.GAP_FULL) { | ||
963 | console.spam('Collided with symmetrical outside / gap-2', cell) | ||
964 | data.x = data.bbox.x2 - 12 | ||
965 | } | ||
966 | } | ||
967 | if (data.x > data.bbox.x2) { | ||
968 | return MOVE_RIGHT | ||
969 | } | ||
970 | } else if (data.y < data.bbox.y1 + 12) { // Moving up | ||
971 | var cell = data.puzzle.getCell(data.pos.x, data.pos.y - 1) | ||
972 | if (cell == null || cell.type !== 'line' || cell.gap === window.GAP_FULL) { | ||
973 | console.spam('Collided with outside / gap-2', cell) | ||
974 | data.y = data.bbox.y1 + 12 | ||
975 | } else if (cell.line > window.LINE_NONE && lastDir !== MOVE_BOTTOM) { | ||
976 | console.spam('Collided with other line', cell.line) | ||
977 | data.y = data.bbox.y1 + 12 | ||
978 | } else if (data.puzzle.symType != SYM_TYPE_NONE) { | ||
979 | var symCell = data.puzzle.getSymmetricalCell(data.pos.x, data.pos.y - 1) | ||
980 | if (symCell == null || symCell.type !== 'line' || symCell.gap === window.GAP_FULL) { | ||
981 | console.spam('Collided with symmetrical outside / gap-2', cell) | ||
982 | data.y = data.bbox.y1 + 12 | ||
983 | } | ||
984 | } | ||
985 | if (data.y < data.bbox.y1) { | ||
986 | return MOVE_TOP | ||
987 | } | ||
988 | } else if (data.y > data.bbox.y2 - 12) { // Moving down | ||
989 | var cell = data.puzzle.getCell(data.pos.x, data.pos.y + 1) | ||
990 | if (cell == null || cell.type !== 'line' || cell.gap === window.GAP_FULL) { | ||
991 | console.spam('Collided with outside / gap-2') | ||
992 | data.y = data.bbox.y2 - 12 | ||
993 | } else if (cell.line > window.LINE_NONE && lastDir !== MOVE_TOP) { | ||
994 | console.spam('Collided with other line', cell.line) | ||
995 | data.y = data.bbox.y2 - 12 | ||
996 | } else if (data.puzzle.symType != SYM_TYPE_NONE) { | ||
997 | var symCell = data.puzzle.getSymmetricalCell(data.pos.x, data.pos.y + 1) | ||
998 | if (symCell == null || symCell.type !== 'line' || symCell.gap === window.GAP_FULL) { | ||
999 | console.spam('Collided with symmetrical outside / gap-2', cell) | ||
1000 | data.y = data.bbox.y2 - 12 | ||
1001 | } | ||
1002 | } | ||
1003 | if (data.y > data.bbox.y2) { | ||
1004 | return MOVE_BOTTOM | ||
1005 | } | ||
1006 | } | ||
1007 | return MOVE_NONE | ||
1008 | } | ||
1009 | |||
1010 | // Check to see if you moved beyond the edge of a pillar. | ||
1011 | // If so, wrap the cursor x to preserve momentum. | ||
1012 | // Note that this still does not change the position. | ||
1013 | function pillarWrap(moveDir) { | ||
1014 | if (moveDir === MOVE_LEFT && data.pos.x === 0) { | ||
1015 | data.x += data.puzzle.width * 41 | ||
1016 | } | ||
1017 | if (moveDir === MOVE_RIGHT && data.pos.x === data.puzzle.width - 1) { | ||
1018 | data.x -= data.puzzle.width * 41 | ||
1019 | } | ||
1020 | } | ||
1021 | |||
1022 | // Actually change the data position. (Note that this takes in pos to allow easier symmetry). | ||
1023 | // Note that this doesn't zero the momentum, so that we can adjust appropriately on further loops. | ||
1024 | // This function also shifts the bounding box that we use to determine the bounds of the cell. | ||
1025 | function changePos(bbox, pos, moveDir) { | ||
1026 | if (moveDir === MOVE_LEFT) { | ||
1027 | pos.x-- | ||
1028 | // Wrap around the left | ||
1029 | if (data.puzzle.pillar === true && pos.x < 0) { | ||
1030 | pos.x += data.puzzle.width | ||
1031 | bbox.shift('right', data.puzzle.width * 41 - 82) | ||
1032 | bbox.shift('right', 58) | ||
1033 | } else { | ||
1034 | bbox.shift('left', (pos.x%2 === 0 ? 24 : 58)) | ||
1035 | } | ||
1036 | } else if (moveDir === MOVE_RIGHT) { | ||
1037 | pos.x++ | ||
1038 | // Wrap around to the right | ||
1039 | if (data.puzzle.pillar === true && pos.x >= data.puzzle.width) { | ||
1040 | pos.x -= data.puzzle.width | ||
1041 | bbox.shift('left', data.puzzle.width * 41 - 82) | ||
1042 | bbox.shift('left', 24) | ||
1043 | } else { | ||
1044 | bbox.shift('right', (pos.x%2 === 0 ? 24 : 58)) | ||
1045 | } | ||
1046 | } else if (moveDir === MOVE_TOP) { | ||
1047 | pos.y-- | ||
1048 | bbox.shift('top', (pos.y%2 === 0 ? 24 : 58)) | ||
1049 | } else if (moveDir === MOVE_BOTTOM) { | ||
1050 | pos.y++ | ||
1051 | bbox.shift('bottom', (pos.y%2 === 0 ? 24 : 58)) | ||
1052 | } | ||
1053 | } | ||
1054 | |||
1055 | }) | ||