puzzle-scene.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. class PuzzleScene {
  2. constructor() {
  3. this.board = {
  4. width: 7,
  5. height: 8,
  6. offset: { x: 100, y: 100 },
  7. tileSize: { width: 50, height: 50 },
  8. };
  9. this.tiles = [];
  10. this.pathTiles = [];
  11. this.isWon = false;
  12. this.splashText = null;
  13. this.splashTextTweenId = -1;
  14. }
  15. init() {
  16. this.isWon = false;
  17. this.tiles = [];
  18. let maxWidth = Math.floor(canvas.width / (this.board.tileSize.width + 3)) - 3;
  19. let maxHeight = Math.floor(canvas.height / (this.board.tileSize.height + 3)) - 3;
  20. this.board = {
  21. width: Math.floor(maxWidth * Math.random()) + 2,
  22. height: Math.floor(maxHeight * Math.random()) + 2,
  23. offset: { x: 100, y: 100 },
  24. tileSize: { width: 50, height: 50 },
  25. };
  26. this.board.width = maxWidth;
  27. this.board.height = maxHeight;
  28. if (puzzleRules.width > 0) {
  29. this.board.width = puzzleRules.width;
  30. }
  31. if (puzzleRules.height > 0) {
  32. this.board.height = puzzleRules.height;
  33. }
  34. this.board.offset = { x: (canvas.width / 2) - (this.board.width * this.board.tileSize.width) / 2, y: (canvas.height / 2) - (this.board.height * this.board.tileSize.height) / 2 };
  35. for (let y = 0; y < this.board.height; y++) {
  36. for (let x = 0; x <= this.board.width; x++) {
  37. this.tiles.push(new Tile(this.board, x, y));
  38. }
  39. }
  40. this.buildPath();
  41. this.splashText = new SplashText("Connected!", - canvas.width - 200, canvas.height / 2);
  42. this.cancelPuzzle = new Button(canvas.width - 100, 50, "Back", () => {
  43. changeState(1);
  44. });
  45. this.cancelPuzzle.width = 100;
  46. this.cancelPuzzle.height = 40;
  47. this.cancelPuzzle.fontSize = 18;
  48. this.cancelPuzzle.buttonColor = Color.LightGray;
  49. this.cancelPuzzle.buttonHoverColor = Color.DarkGray;
  50. this.cancelPuzzle.textColor = Color.DarkGray;
  51. this.cancelPuzzle.textHoverColor = Color.White;
  52. }
  53. buildPath() {
  54. this.pathTiles = [];
  55. let validStartTiles = [];
  56. if (this.board.width >= this.board.height) {
  57. validStartTiles = this.tiles.filter(tile => tile.x == 0);
  58. } else {
  59. validStartTiles = this.tiles.filter(tile => tile.y == 0);
  60. }
  61. let currentTile = validStartTiles[Math.floor(validStartTiles.length * Math.random())];
  62. this.pathTiles.push(currentTile);
  63. let invalidTiles = [];
  64. while (this.isEndingConditionMet(currentTile)) {
  65. let validPathChoices = this.tiles.filter(tile => this.isValidNearby(tile, currentTile, invalidTiles));
  66. if (validPathChoices.length == 0) {
  67. this.pathTiles.splice(this.pathTiles.findIndex(tile => tile == currentTile), 1);
  68. invalidTiles.push(currentTile);
  69. currentTile = this.pathTiles[this.pathTiles.length - 1];
  70. continue;
  71. }
  72. let randomTile = validPathChoices[Math.floor(validPathChoices.length * Math.random())];
  73. currentTile = randomTile;
  74. this.pathTiles.push(currentTile);
  75. }
  76. let startTile = this.pathTiles[0];
  77. let nextTile = this.pathTiles[1];
  78. startTile.piece = TileType.Node;
  79. startTile.isPowered = true;
  80. if (startTile.x < nextTile.x && startTile.y == nextTile.y) {
  81. startTile.goal = [90];
  82. } else if (startTile.x == nextTile.x && startTile.y < nextTile.y) {
  83. startTile.goal = [180];
  84. } else if (startTile.x == nextTile.x && startTile.y > nextTile.y) {
  85. startTile.goal = [0];
  86. } else if (startTile.x > nextTile.x && startTile.y == nextTile.y) {
  87. startTile.goal = [270];
  88. }
  89. startTile.targetRadians = startTile.goal[0] * (Math.PI / 180);
  90. for (let i = 1; i < this.pathTiles.length - 1; i++) {
  91. let previousTile = this.pathTiles[i - 1];
  92. let currentTile = this.pathTiles[i];
  93. let nextTile = this.pathTiles[i + 1];
  94. currentTile.piece = TileType.Straight;
  95. if (previousTile.x == nextTile.x && previousTile.y != nextTile.y) {
  96. currentTile.goal = [0, 180];
  97. } else if (previousTile.x != nextTile.x && previousTile.y == nextTile.y) {
  98. currentTile.goal = [90, 270];
  99. } else if (previousTile.x != nextTile.x && previousTile.y != nextTile.y) {
  100. currentTile.piece = TileType.Corner;
  101. let prev = [previousTile.x - currentTile.x, previousTile.y - currentTile.y].join(', ');
  102. let next = [nextTile.x - currentTile.x, nextTile.y - currentTile.y].join(', ');
  103. if (
  104. prev == "0, -1" && next == "-1, 0" ||
  105. next == "0, -1" && prev == "-1, 0") {
  106. currentTile.goal = [0];
  107. } else if (
  108. prev == "0, -1" && next == "1, 0" ||
  109. next == "0, -1" && prev == "1, 0") {
  110. currentTile.goal = [90];
  111. } else if (
  112. prev == "1, 0" && next == "0, 1" ||
  113. next == "1, 0" && prev == "0, 1") {
  114. currentTile.goal = [180];
  115. } else if (
  116. prev == "-1, 0" && next == "0, 1" ||
  117. next == "-1, 0" && prev == "0, 1") {
  118. currentTile.goal = [270];
  119. }
  120. }
  121. }
  122. let pentultimateTile = this.pathTiles[this.pathTiles.length - 2];
  123. let lastTile = this.pathTiles[this.pathTiles.length - 1];
  124. lastTile.piece = TileType.Node;
  125. if (lastTile.x > pentultimateTile.x && lastTile.y == pentultimateTile.y) {
  126. lastTile.goal = [270];
  127. } else if (lastTile.x == pentultimateTile.x && lastTile.y < pentultimateTile.y) {
  128. lastTile.goal = [180];
  129. } else if (lastTile.x == pentultimateTile.x && lastTile.y > pentultimateTile.y) {
  130. lastTile.goal = [0];
  131. } else if (lastTile.x < pentultimateTile.x && lastTile.y == pentultimateTile.y) {
  132. lastTile.goal = [90];
  133. }
  134. }
  135. isEndingConditionMet(currentTile) {
  136. if (this.board.width >= this.board.height) {
  137. return currentTile.x <= this.board.width - 1;
  138. }
  139. return currentTile.y < this.board.height - 1;
  140. }
  141. isValidNearby(tile, currentTile, invalidTiles) {
  142. if (invalidTiles.includes(tile)) {
  143. return false;
  144. }
  145. if (this.pathTiles.includes(tile)) {
  146. return false;
  147. }
  148. //TOP
  149. if (tile.x == currentTile.x && tile.y == currentTile.y - 1) {
  150. return true;
  151. }
  152. //LEFT
  153. if (tile.x == currentTile.x - 1 && tile.y == currentTile.y) {
  154. return true;
  155. }
  156. //BOTTOM
  157. if (tile.x == currentTile.x && tile.y == currentTile.y + 1) {
  158. return true;
  159. }
  160. //RIGHT
  161. if (tile.x == currentTile.x + 1 && tile.y == currentTile.y) {
  162. return true;
  163. }
  164. return false;
  165. }
  166. update(delta) {
  167. Tween.update();
  168. for (let tileIndex in this.tiles) {
  169. this.tiles[tileIndex].update(delta);
  170. }
  171. if (keys[KeyCode.Esc]) {
  172. changeState(1);
  173. }
  174. if (this.isWon) {
  175. this.splashText.update(delta);
  176. if (keys[KeyCode.Enter]) {
  177. Tween.cancel(this.splashTextTweenId);
  178. changeState(1);
  179. }
  180. return;
  181. }
  182. this.clearPower();
  183. this.calculatePower(this.pathTiles[0]);
  184. if (this.pathTiles[this.pathTiles.length - 1].isPowered) {
  185. this.isWon = true;
  186. saveData.player.rail = puzzleRules.successRail;
  187. saveData.player.railnode = puzzleRules.successNode;
  188. saveGame();
  189. sound.playPingSequence(["E6", "C6", "C6", "C6", "C7"], 75);
  190. this.splashTextTweenId = Tween.create(this.splashText, { x: canvas.width / 2 }, 2000, Tween.Easing.Elastic.EaseOut, () => {
  191. setTimeout(() => {
  192. changeState(1);
  193. }, 750);
  194. });
  195. }
  196. this.cancelPuzzle.update(delta);
  197. }
  198. draw(context) {
  199. this.drawBackground(context);
  200. for (let tileIndex in this.tiles) {
  201. this.tiles[tileIndex].draw(context);
  202. }
  203. if (this.isWon) {
  204. this.splashText.draw(context);
  205. } else {
  206. this.cancelPuzzle.draw(context);
  207. }
  208. }
  209. drawBackground(context) {
  210. context.fillStyle = Color.VeryDarkBlue;
  211. for (let y = 0; y < canvas.height / 128; y++) {
  212. for (let x = 0; x < canvas.width / 128; x++) {
  213. let wobble = (Math.sin((x * 45) + new Date().getTime() / 500));
  214. context.save();
  215. context.translate(x * 128 + (10 * wobble), y * 128 + (20 * wobble));
  216. context.rotate(45 * (Math.PI / 180));
  217. context.beginPath();
  218. context.rect(-(wobble) - 4, -(wobble) - 4, 2 * wobble + 8, 2 * wobble + 8);
  219. context.fill();
  220. context.restore();
  221. }
  222. }
  223. }
  224. clearPower() {
  225. for (let i = 0; i < this.tiles.length; i++) {
  226. this.tiles[i].isPowered = false;
  227. }
  228. }
  229. calculatePower(startingTile, source) {
  230. if (!startingTile.canReceivePower(source)) {
  231. return;
  232. }
  233. startingTile.isPowered = true;
  234. let possibleCoords = startingTile.getPoweringTileCoordinates();
  235. for (let i = 0; i < possibleCoords.length; i++) {
  236. let foundTile = this.tiles.find(tile => tile.x == possibleCoords[i][0] + startingTile.x && tile.y == possibleCoords[i][1] + startingTile.y && !tile.isPowered);
  237. if (foundTile) {
  238. this.calculatePower(foundTile, startingTile);
  239. }
  240. }
  241. }
  242. onResize() {
  243. this.board.offset = { x: (canvas.width / 2) - (this.board.width * this.board.tileSize.width) / 2, y: (canvas.height / 2) - (this.board.height * this.board.tileSize.height) / 2 };
  244. }
  245. onKeyUp(event) {
  246. if (keys[KeyCode.W]) {
  247. this.pathTiles.forEach(tile => { tile.targetRadians = tile.goal[0] * (Math.PI / 180); tile.targetRadians %= 360 * (Math.PI / 180) });
  248. }
  249. }
  250. onMouseUp(event) {
  251. if (this.isWon) {
  252. Tween.cancel(this.splashTextTweenId);
  253. changeState(1);
  254. return;
  255. }
  256. let clickedTile = this.tiles.find(tile => tile.isMouseOver(event));
  257. if (clickedTile) {
  258. this.tiles.splice(this.tiles.findIndex(tile => tile == clickedTile), 1);
  259. this.tiles.push(clickedTile);
  260. clickedTile.targetRadians += 90 * (Math.PI / 180);
  261. clickedTile.targetRadians %= (360 * (Math.PI / 180));
  262. sound.playPing("C6", 0, 0.1);
  263. }
  264. if (this.cancelPuzzle.isMouseOver(event)) {
  265. this.cancelPuzzle.triggerHandler();
  266. document.body.style.cursor = "default";
  267. }
  268. }
  269. onMouseMove(event) {
  270. if (this.isWon) {
  271. return;
  272. }
  273. for (let tileIndex in this.tiles) {
  274. this.tiles[tileIndex].isHovered = false;
  275. }
  276. this.cancelPuzzle.isHovered = false;
  277. document.body.style.cursor = "default";
  278. if (this.cancelPuzzle.isMouseOver(event)) {
  279. this.cancelPuzzle.isHovered = true;
  280. document.body.style.cursor = "pointer";
  281. }
  282. let hoveredTile = this.tiles.find(tile => tile.isMouseOver(event));
  283. if (hoveredTile) {
  284. hoveredTile.isHovered = true;
  285. }
  286. }
  287. onTouchEnd(event) {
  288. if (this.cancelPuzzle.isTouchOver(event)) {
  289. event.preventDefault();
  290. this.cancelPuzzle.triggerHandler();
  291. document.body.style.cursor = "default";
  292. }
  293. }
  294. onRightClick(event) {
  295. event.preventDefault();
  296. }
  297. }
  298. var TileType = {};
  299. TileType.Blank = 0;
  300. TileType.Straight = 1;
  301. TileType.Corner = 2;
  302. TileType.Tee = 3;
  303. TileType.Cross = 4;
  304. TileType.Node = 5;
  305. class Tile {
  306. constructor(board, x, y) {
  307. this.x = x;
  308. this.y = y;
  309. this.width = board.tileSize.width;
  310. this.height = board.tileSize.height;
  311. this.center = { x: board.tileSize.width / 2, y: board.tileSize.height / 2 };
  312. this.radians = (90 * Math.floor(4 * Math.random())) * (Math.PI / 180);
  313. this.targetRadians = (90 * Math.floor(4 * Math.random())) * (Math.PI / 180);
  314. this.goal = [];
  315. this.piece = Math.floor(5 * Math.random());
  316. this.board = board;
  317. this.isHovered = false;
  318. this.isPowered = false;
  319. this.powerPellets = [];
  320. }
  321. update(delta) {
  322. if (this.targetRadians != this.radians) {
  323. this.radians += (9 * (Math.PI / 180));
  324. this.radians %= (360 * (Math.PI / 180));
  325. }
  326. //for (let i = 0; i < this.powerPellets.length; i++) {
  327. // let pellet = this.powerPellets[i];
  328. // pellet.lifetime -= delta * 10;
  329. //}
  330. //if (this.powerPellets.length == 0) {
  331. // this.powerPellets.push({ lifetime: 500 });
  332. //}
  333. //this.powerPellets = this.powerPellets.filter(pellet => pellet.lifetime > 0);
  334. }
  335. draw(context) {
  336. context.fillStyle = Color.DarkGray;
  337. context.strokeStyle = Color.DarkGray;
  338. if (this.isHovered) {
  339. context.strokeStyle = Color.LightGray;
  340. }
  341. context.save();
  342. context.translate((this.x * this.width) + this.board.offset.x + (this.x * 3), (this.y * this.height) + this.board.offset.y + (this.y * 3));
  343. context.rotate(this.radians);
  344. context.beginPath();
  345. context.rect(-this.center.x, -this.center.y, this.width, this.height);
  346. context.fill();
  347. context.stroke();
  348. context.fillStyle = Color.DarkBlue;
  349. context.strokeStyle = Color.DarkBlue;
  350. context.lineWidth = 4;
  351. if (this.isPowered) {
  352. context.fillStyle = Color.LightBlue;
  353. context.strokeStyle = Color.LightBlue;
  354. }
  355. switch (this.piece) {
  356. case TileType.Straight:
  357. context.beginPath();
  358. context.rect(- 2, -this.center.y, 4, this.height);
  359. context.fill();
  360. break;
  361. case TileType.Corner:
  362. context.beginPath();
  363. context.rect(- 2, -this.center.y, 4, 2 + this.height / 2);
  364. context.fill();
  365. context.beginPath();
  366. context.rect(-this.center.x, -2, 2 + this.width / 2, 4);
  367. context.fill();
  368. break;
  369. case TileType.Tee:
  370. context.beginPath();
  371. context.rect(- 2, -this.center.y, 4, this.height);
  372. context.fill();
  373. context.beginPath();
  374. context.rect(-this.center.x, -2, 2 + this.width / 2, 4);
  375. context.fill();
  376. break;
  377. case TileType.Cross:
  378. context.beginPath();
  379. context.rect(- 2, -this.center.y, 4, this.height);
  380. context.fill();
  381. context.beginPath();
  382. context.rect(- this.center.x, -2, this.width, 4);
  383. context.fill();
  384. break;
  385. case TileType.Node:
  386. context.beginPath();
  387. context.rect(- 2, -this.center.y, 4, 4 + this.height / 4);
  388. context.fill();
  389. context.beginPath();
  390. context.arc(0, 0, 8, 0, 2 * Math.PI);
  391. context.stroke();
  392. break;
  393. case TileType.Blank:
  394. default:
  395. break;
  396. }
  397. //if (this.isPowered) {
  398. // context.fillStyle = Color.White;
  399. // for (let i = 0; i < this.powerPellets.length; i++) {
  400. // let pellet = this.powerPellets[i];
  401. // if (this.piece == TileType.Straight) {
  402. // context.beginPath();
  403. // context.arc(0, ((1 - pellet.lifetime / 500) * -(2 * this.center.y) + this.center.y), 2, 0, 2 * Math.PI);
  404. // context.fill();
  405. // }
  406. // }
  407. //}
  408. context.restore();
  409. }
  410. isMouseOver(event) {
  411. let boardOffsetX = event.clientX - this.board.offset.x - (this.x * 3) + this.center.x;
  412. let boardOffsetY = event.clientY - this.board.offset.y - (this.y * 3) + this.center.y;
  413. let x = Math.floor(boardOffsetX / this.width);
  414. let y = Math.floor(boardOffsetY / this.height);
  415. return this.x == x && this.y == y;
  416. }
  417. getPoweringTileCoordinates() {
  418. let possibleCoordinates = [];
  419. switch (this.piece) {
  420. case TileType.Straight:
  421. switch (this.radians * (180 / Math.PI)) {
  422. case 0:
  423. case 180:
  424. possibleCoordinates.push([0, -1]);
  425. possibleCoordinates.push([0, 1]);
  426. break;
  427. case 90:
  428. case 270:
  429. possibleCoordinates.push([-1, 0]);
  430. possibleCoordinates.push([1, 0]);
  431. break;
  432. }
  433. break;
  434. case TileType.Corner:
  435. switch (this.radians * (180 / Math.PI)) {
  436. case 0:
  437. possibleCoordinates.push([-1, 0]);
  438. possibleCoordinates.push([0, -1]);
  439. break;
  440. case 90:
  441. possibleCoordinates.push([0, -1]);
  442. possibleCoordinates.push([1, 0]);
  443. break;
  444. case 180:
  445. possibleCoordinates.push([1, 0]);
  446. possibleCoordinates.push([0, 1]);
  447. break;
  448. case 270:
  449. possibleCoordinates.push([-1, 0]);
  450. possibleCoordinates.push([0, 1]);
  451. break;
  452. }
  453. break;
  454. case TileType.Tee:
  455. switch (this.radians * (180 / Math.PI)) {
  456. case 0:
  457. possibleCoordinates.push([0, 1]);
  458. possibleCoordinates.push([-1, 0]);
  459. possibleCoordinates.push([0, -1]);
  460. break;
  461. case 90:
  462. possibleCoordinates.push([-1, 0]);
  463. possibleCoordinates.push([0, -1]);
  464. possibleCoordinates.push([1, 0]);
  465. break;
  466. case 180:
  467. possibleCoordinates.push([0, -1]);
  468. possibleCoordinates.push([1, 0]);
  469. possibleCoordinates.push([0, 1]);
  470. break;
  471. case 270:
  472. possibleCoordinates.push([1, 0]);
  473. possibleCoordinates.push([0, 1]);
  474. possibleCoordinates.push([-1, 0]);
  475. break;
  476. }
  477. break;
  478. case TileType.Cross:
  479. possibleCoordinates.push([0, -1]);
  480. possibleCoordinates.push([0, 1]);
  481. possibleCoordinates.push([-1, 0]);
  482. possibleCoordinates.push([1, 0]);
  483. break;
  484. case TileType.Node:
  485. switch (this.radians * (180 / Math.PI)) {
  486. case 0:
  487. possibleCoordinates.push([0, -1]);
  488. break;
  489. case 90:
  490. possibleCoordinates.push([1, 0]);
  491. break;
  492. case 180:
  493. possibleCoordinates.push([0, 1]);
  494. break;
  495. case 270:
  496. possibleCoordinates.push([-1, 0]);
  497. break;
  498. }
  499. break;
  500. case TileType.Blank:
  501. default:
  502. break;
  503. }
  504. return possibleCoordinates;
  505. }
  506. canReceivePower(source) {
  507. if (!source) {
  508. return true;
  509. }
  510. let possibleHookups = this.getPoweringTileCoordinates();
  511. for (let i = 0; i < possibleHookups.length; i++) {
  512. if (source.x == (possibleHookups[i][0]) + this.x && source.y == (possibleHookups[i][1]) + this.y) {
  513. return true;
  514. }
  515. }
  516. return false;
  517. }
  518. }