├── LICENSE ├── README.md ├── habitat-embed.js ├── images ├── Black │ ├── Other.png │ ├── Other.psd │ ├── Other@0.25x.png │ ├── Other@0.5x.png │ └── Other@0.75x.png ├── Blank.png ├── Blank.psd ├── Blank@0.25x.png ├── Blank@0.5x.png ├── Blank@0.75x.png ├── Cyan │ ├── Mad.png │ ├── Mad.psd │ ├── Mad@0.25x.png │ ├── Mad@0.5x.png │ ├── Mad@0.75x.png │ ├── Other.png │ ├── Other.psd │ ├── Other@0.25x.png │ ├── Other@0.5x.png │ └── Other@0.75x.png ├── Green │ ├── Other.png │ ├── Other.psd │ ├── Other@0.25x.png │ ├── Other@0.5x.png │ └── Other@0.75x.png ├── Purple │ ├── Other.png │ ├── Other.psd │ ├── Other@0.25x.png │ ├── Other@0.5x.png │ └── Other@0.75x.png └── Yellow │ ├── Other.png │ ├── Other.psd │ ├── Other@0.25x.png │ ├── Other@0.5x.png │ └── Other@0.75x.png ├── index.html └── source ├── atom.js ├── config.js ├── element.js ├── main.js ├── mover.js ├── multiverse.js └── world.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Luke Wilson 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # TimePond 4 | TimePond is a time-travel physics engine.
5 | It lets you play with time-travelling frogs! 6 | 7 | **I made TimePond for this video about time travel: 🌀 [Top 9 ways to make Time Travel](https://youtu.be/Z24NKn6rQRY)** 8 | 9 | My patrons voted for it at [patreon.com/todepond](https://patreon.com/todepond) 10 | 11 | Most of the time-travel experiments are hidden behind URL parameters. Sorry about that!
12 | Here are some things you can try out though: 13 | 14 | | | Simple | Right | Fling | Fall | Bump | 15 | | --- | --- | --- | --- | --- | --- | 16 | | Teleport | [Teleport + Simple](https://timepond.cool?experiment=gsimple&menu=cyan&portal=move) | [Teleport + Right](https://timepond.cool?experiment=gright&menu=cyan&portal=move) | [Teleport + Fling](https://timepond.cool?experiment=gfling&menu=cyan&portal=move) | [Teleport + Fall](https://timepond.cool?experiment=gfall&menu=cyan&portal=move) | [Teleport + Bump](https://timepond.cool?experiment=ggen&menu=cyan&portal=move) | 17 | | Future | [Future + Simple](https://timepond.cool?experiment=gsimple&menu=cyan&portal=futurenow) | [Future + Right](https://timepond.cool?experiment=gright&menu=cyan&portal=futurenow) | [Future + Fling](https://timepond.cool?experiment=gfling&menu=cyan&portal=futurenow) | [Future + Fall](https://timepond.cool?experiment=gfall&menu=cyan&portal=futurenow) | [Future + Bump](https://timepond.cool?experiment=ggen&menu=cyan&portal=futurenow) | 18 | | Past | [Past + Simple](https://timepond.cool?experiment=gsimple&menu=cyan&portal=pastnow) | [Past + Right](https://timepond.cool?experiment=gright&menu=cyan&portal=pastnow) | [Past + Fling](https://timepond.cool?experiment=gfling&menu=cyan&portal=pastnow) | [Past + Fall](https://timepond.cool?experiment=gfall&menu=cyan&portal=pastnow) | [Past + Bump](https://timepond.cool?experiment=ggen&menu=cyan&portal=pastnow) | 19 | | Rewrite | [Rewrite + Simple](https://timepond.cool?experiment=gsimple&menu=purple&portal=rewrite) | [Rewrite + Right](https://timepond.cool?experiment=gright&menu=purple&portal=rewrite) | [Rewrite + Fling](https://timepond.cool?experiment=gfling&menu=purple&portal=rewrite) | [Rewrite + Fall](https://timepond.cool?experiment=gfall&menu=purple&portal=rewrite) | [Rewrite + Bump](https://timepond.cool?experiment=ggen&menu=purple&portal=rewrite) | 20 | | Branch | [Branch + Simple](https://timepond.cool?experiment=gsimple&menu=purple&portal=pastline) | [Branch + Right](https://timepond.cool?experiment=gright&menu=purple&portal=pastline) | [Branch + Fling](https://timepond.cool?experiment=gfling&menu=purple&portal=pastline) | [Branch + Fall](https://timepond.cool?experiment=gfall&menu=purple&portal=pastline) | [Branch + Bump](https://timepond.cool?experiment=ggen&menu=purple&portal=pastline) | 21 | -------------------------------------------------------------------------------- /habitat-embed.js: -------------------------------------------------------------------------------- 1 | const Habitat = {} 2 | 3 | //=======// 4 | // Array // 5 | //=======// 6 | { 7 | 8 | const install = (global) => { 9 | 10 | Reflect.defineProperty(global.Array.prototype, "last", { 11 | get() { 12 | return this[this.length-1] 13 | }, 14 | set(value) { 15 | Reflect.defineProperty(this, "last", {value, configurable: true, writable: true, enumerable: true}) 16 | }, 17 | configurable: true, 18 | enumerable: false, 19 | }) 20 | 21 | Reflect.defineProperty(global.Array.prototype, "clone", { 22 | get() { 23 | return [...this] 24 | }, 25 | set(value) { 26 | Reflect.defineProperty(this, "clone", {value, configurable: true, writable: true, enumerable: true}) 27 | }, 28 | configurable: true, 29 | enumerable: false, 30 | }) 31 | 32 | Reflect.defineProperty(global.Array.prototype, "at", { 33 | value(position) { 34 | if (position >= 0) return this[position] 35 | return this[this.length + position] 36 | }, 37 | configurable: true, 38 | enumerable: false, 39 | writable: true, 40 | }) 41 | 42 | Reflect.defineProperty(global.Array.prototype, "shuffle", { 43 | value() { 44 | for (let i = this.length - 1; i > 0; i--) { 45 | const r = Math.floor(Math.random() * (i+1)) 46 | ;[this[i], this[r]] = [this[r], this[i]] 47 | } 48 | return this 49 | }, 50 | configurable: true, 51 | enumerable: false, 52 | writable: true, 53 | }) 54 | 55 | Reflect.defineProperty(global.Array.prototype, "trim", { 56 | value() { 57 | if (this.length == 0) return this 58 | let start = this.length - 1 59 | let end = 0 60 | for (let i = 0; i < this.length; i++) { 61 | const value = this[i] 62 | if (value !== undefined) { 63 | start = i 64 | break 65 | } 66 | } 67 | for (let i = this.length - 1; i >= 0; i--) { 68 | const value = this[i] 69 | if (value !== undefined) { 70 | end = i + 1 71 | break 72 | } 73 | } 74 | this.splice(end) 75 | this.splice(0, start) 76 | return this 77 | }, 78 | configurable: true, 79 | enumerable: false, 80 | writable: true, 81 | }) 82 | 83 | Reflect.defineProperty(global.Array.prototype, "repeat", { 84 | value(n) { 85 | if (n === 0) { 86 | this.splice(0) 87 | return this 88 | } 89 | if (n < 0) { 90 | this.reverse() 91 | n = n - n - n 92 | } 93 | const clone = [...this] 94 | for (let i = 1; i < n; i++) { 95 | this.push(...clone) 96 | } 97 | return this 98 | }, 99 | configurable: true, 100 | enumerable: false, 101 | writable: true, 102 | }) 103 | 104 | Habitat.Array.installed = true 105 | 106 | } 107 | 108 | Habitat.Array = {install} 109 | 110 | } 111 | 112 | //=======// 113 | // Async // 114 | //=======// 115 | { 116 | const sleep = (duration) => new Promise(resolve => setTimeout(resolve, duration)) 117 | const install = (global) => { 118 | global.sleep = sleep 119 | Habitat.Async.installed = true 120 | } 121 | 122 | Habitat.Async = {install, sleep} 123 | } 124 | 125 | //========// 126 | // Colour // 127 | //========// 128 | { 129 | 130 | Habitat.Colour = {} 131 | 132 | Habitat.Colour.make = (r, g, b) => { 133 | const colour = new Uint8Array([r, g, b]) 134 | const string = `rgb(${r}, ${g}, ${b})` 135 | const hex = `#${r.toString(16)}${g.toString(16)}${b.toString(16)}` 136 | colour.toString = () => string 137 | colour.toHex = () => hex 138 | colour.brightness = (r*299 + g*587 + b*114) / 1000 / 255 139 | colour.isLight = colour.brightness > 0.7 140 | colour.isDark = colour.brightness < 0.3 141 | return colour 142 | } 143 | 144 | Habitat.Colour.Black = Habitat.Colour.make(23, 29, 40) 145 | Habitat.Colour.Grey = Habitat.Colour.make(55, 67, 98) 146 | Habitat.Colour.Silver = Habitat.Colour.make(159, 174, 201) 147 | Habitat.Colour.White = Habitat.Colour.make(242, 245, 247) 148 | 149 | Habitat.Colour.Green = Habitat.Colour.make(70, 255, 128) 150 | Habitat.Colour.Red = Habitat.Colour.make(255, 70, 70) 151 | Habitat.Colour.Blue = Habitat.Colour.make(70, 128, 255) 152 | Habitat.Colour.Yellow = Habitat.Colour.make(255, 204, 70) 153 | Habitat.Colour.Orange = Habitat.Colour.make(255, 128, 70) 154 | Habitat.Colour.Pink = Habitat.Colour.make(255, 128, 128) 155 | Habitat.Colour.Cyan = Habitat.Colour.make(70, 204, 255) 156 | Habitat.Colour.Purple = Habitat.Colour.make(128, 70, 255) 157 | 158 | Habitat.Colour.install = (global) => { 159 | global.Colour = Habitat.Colour 160 | Habitat.Colour.installed = true 161 | } 162 | 163 | } 164 | 165 | //=========// 166 | // Console // 167 | //=========// 168 | { 169 | const print = console.log.bind(console) 170 | const dir = (value) => console.dir(Object(value)) 171 | 172 | let print9Counter = 0 173 | const print9 = (message) => { 174 | if (print9Counter >= 9) return 175 | print9Counter++ 176 | console.log(message) 177 | } 178 | 179 | const install = (global) => { 180 | global.print = print 181 | global.dir = dir 182 | global.print9 = print9 183 | 184 | Reflect.defineProperty(global.Object.prototype, "d", { 185 | get() { 186 | const value = this.valueOf() 187 | console.log(value) 188 | return value 189 | }, 190 | set(value) { 191 | Reflect.defineProperty(this, "d", {value, configurable: true, writable: true, enumerable: true}) 192 | }, 193 | configurable: true, 194 | enumerable: false, 195 | }) 196 | 197 | Reflect.defineProperty(global.Object.prototype, "dir", { 198 | get() { 199 | console.dir(this) 200 | return this.valueOf() 201 | }, 202 | set(value) { 203 | Reflect.defineProperty(this, "dir", {value, configurable: true, writable: true, enumerable: true}) 204 | }, 205 | configurable: true, 206 | enumerable: false, 207 | }) 208 | 209 | let d9Counter = 0 210 | Reflect.defineProperty(global.Object.prototype, "d9", { 211 | get() { 212 | const value = this.valueOf() 213 | if (d9Counter < 9) { 214 | console.log(value) 215 | d9Counter++ 216 | } 217 | return value 218 | }, 219 | set(value) { 220 | Reflect.defineProperty(this, "d9", {value, configurable: true, writable: true, enumerable: true}) 221 | }, 222 | configurable: true, 223 | enumerable: false, 224 | }) 225 | 226 | Habitat.Console.installed = true 227 | 228 | } 229 | 230 | Habitat.Console = {install, print, dir, print9} 231 | } 232 | 233 | //==========// 234 | // Document // 235 | //==========// 236 | { 237 | 238 | const $ = (...args) => document.querySelector(...args) 239 | const $$ = (...args) => document.querySelectorAll(...args) 240 | 241 | const install = (global) => { 242 | 243 | 244 | global.$ = $ 245 | global.$$ = $$ 246 | 247 | if (global.Node === undefined) return 248 | 249 | Reflect.defineProperty(global.Node.prototype, "$", { 250 | value(...args) { 251 | return this.querySelector(...args) 252 | }, 253 | configurable: true, 254 | enumerable: false, 255 | writable: true, 256 | }) 257 | 258 | Reflect.defineProperty(global.Node.prototype, "$$", { 259 | value(...args) { 260 | return this.querySelectorAll(...args) 261 | }, 262 | configurable: true, 263 | enumerable: false, 264 | writable: true, 265 | }) 266 | 267 | Habitat.Document.installed = true 268 | 269 | } 270 | 271 | Habitat.Document = {install, $, $$} 272 | 273 | } 274 | 275 | 276 | //=======// 277 | // Event // 278 | //=======// 279 | { 280 | 281 | const install = (global) => { 282 | 283 | Reflect.defineProperty(global.EventTarget.prototype, "on", { 284 | get() { 285 | return new Proxy(this, { 286 | get: (element, eventName) => (...args) => element.addEventListener(eventName, ...args), 287 | }) 288 | }, 289 | set(value) { 290 | Reflect.defineProperty(this, "on", {value, configurable: true, writable: true, enumerable: true}) 291 | }, 292 | configurable: true, 293 | enumerable: false, 294 | }) 295 | 296 | Reflect.defineProperty(global.EventTarget.prototype, "trigger", { 297 | value(name, options = {}) { 298 | const {bubbles = true, cancelable = true, ...data} = options 299 | const event = new Event(name, {bubbles, cancelable}) 300 | for (const key in data) event[key] = data[key] 301 | this.dispatchEvent(event) 302 | }, 303 | configurable: true, 304 | enumerable: false, 305 | writable: true, 306 | }) 307 | 308 | Habitat.Event.installed = true 309 | 310 | } 311 | 312 | Habitat.Event = {install} 313 | 314 | } 315 | 316 | 317 | //======// 318 | // HTML // 319 | //======// 320 | { 321 | 322 | Habitat.HTML = (...args) => { 323 | const source = String.raw(...args) 324 | const template = document.createElement("template") 325 | template.innerHTML = source 326 | return template.content 327 | } 328 | 329 | Habitat.HTML.install = (global) => { 330 | global.HTML = Habitat.HTML 331 | Habitat.HTML.installed = true 332 | } 333 | 334 | } 335 | 336 | 337 | //============// 338 | // JavaScript // 339 | //============// 340 | { 341 | 342 | Habitat.JavaScript = (...args) => { 343 | const source = String.raw(...args) 344 | const code = `return ${source}` 345 | const func = new Function(code)() 346 | return func 347 | } 348 | 349 | Habitat.JavaScript.install = (global) => { 350 | global.JavaScript = Habitat.JavaScript 351 | Habitat.JavaScript.installed = true 352 | } 353 | 354 | } 355 | 356 | 357 | //==========// 358 | // Keyboard // 359 | //==========// 360 | { 361 | 362 | const Keyboard = Habitat.Keyboard = {} 363 | Reflect.defineProperty(Keyboard, "install", { 364 | value(global) { 365 | global.Keyboard = Keyboard 366 | global.addEventListener("keydown", e => { 367 | Keyboard[e.key] = true 368 | }) 369 | 370 | global.addEventListener("keyup", e => { 371 | Keyboard[e.key] = false 372 | }) 373 | 374 | Reflect.defineProperty(Keyboard, "installed", { 375 | value: true, 376 | configurable: true, 377 | enumerable: false, 378 | writable: true, 379 | }) 380 | }, 381 | configurable: true, 382 | enumerable: false, 383 | writable: true, 384 | }) 385 | 386 | } 387 | 388 | 389 | //======// 390 | // Main // 391 | //======// 392 | Habitat.install = (global) => { 393 | 394 | if (Habitat.installed) return 395 | 396 | if (!Habitat.Array.installed) Habitat.Array.install(global) 397 | if (!Habitat.Async.installed) Habitat.Async.install(global) 398 | if (!Habitat.Colour.installed) Habitat.Colour.install(global) 399 | if (!Habitat.Console.installed) Habitat.Console.install(global) 400 | if (!Habitat.Document.installed) Habitat.Document.install(global) 401 | if (!Habitat.Event.installed) Habitat.Event.install(global) 402 | if (!Habitat.HTML.installed) Habitat.HTML.install(global) 403 | if (!Habitat.JavaScript.installed) Habitat.JavaScript.install(global) 404 | if (!Habitat.Keyboard.installed) Habitat.Keyboard.install(global) 405 | if (!Habitat.Math.installed) Habitat.Math.install(global) 406 | if (!Habitat.Mouse.installed) Habitat.Mouse.install(global) 407 | if (!Habitat.Number.installed) Habitat.Number.install(global) 408 | if (!Habitat.Object.installed) Habitat.Object.install(global) 409 | if (!Habitat.Property.installed) Habitat.Property.install(global) 410 | if (!Habitat.Random.installed) Habitat.Random.install(global) 411 | if (!Habitat.Stage.installed) Habitat.Stage.install(global) 412 | if (!Habitat.String.installed) Habitat.String.install(global) 413 | if (!Habitat.Touches.installed) Habitat.Touches.install(global) 414 | if (!Habitat.Type.installed) Habitat.Type.install(global) 415 | 416 | Habitat.installed = true 417 | 418 | } 419 | 420 | //======// 421 | // Math // 422 | //======// 423 | { 424 | 425 | const gcd = (...numbers) => { 426 | const [head, ...tail] = numbers 427 | if (numbers.length === 1) return head 428 | if (numbers.length > 2) return gcd(head, gcd(...tail)) 429 | 430 | let [a, b] = [head, ...tail] 431 | 432 | while (true) { 433 | if (b === 0) return a 434 | a = a % b 435 | if (a === 0) return b 436 | b = b % a 437 | } 438 | 439 | } 440 | 441 | const reduce = (...numbers) => { 442 | const divisor = gcd(...numbers) 443 | return numbers.map(n => n / divisor) 444 | } 445 | 446 | const install = (global) => { 447 | global.Math.gcd = Habitat.Math.gcd 448 | global.Math.reduce = Habitat.Math.reduce 449 | Habitat.Math.installed = true 450 | } 451 | 452 | 453 | Habitat.Math = {install, gcd, reduce} 454 | 455 | } 456 | 457 | 458 | //=======// 459 | // Mouse // 460 | //=======// 461 | { 462 | 463 | const Mouse = Habitat.Mouse = { 464 | position: [undefined, undefined], 465 | } 466 | 467 | const buttonMap = ["Left", "Middle", "Right", "Back", "Forward"] 468 | 469 | Reflect.defineProperty(Mouse, "install", { 470 | value(global) { 471 | global.Mouse = Mouse 472 | global.addEventListener("mousedown", e => { 473 | const buttonName = buttonMap[e.button] 474 | Mouse[buttonName] = true 475 | }) 476 | 477 | global.addEventListener("mouseup", e => { 478 | const buttonName = buttonMap[e.button] 479 | Mouse[buttonName] = false 480 | }) 481 | 482 | global.addEventListener("mousemove", e => { 483 | Mouse.position[0] = event.clientX 484 | Mouse.position[1] = event.clientY 485 | }) 486 | 487 | Reflect.defineProperty(Mouse, "installed", { 488 | value: true, 489 | configurable: true, 490 | enumerable: false, 491 | writable: true, 492 | }) 493 | }, 494 | configurable: true, 495 | enumerable: false, 496 | writable: true, 497 | }) 498 | 499 | } 500 | 501 | 502 | //========// 503 | // Number // 504 | //========// 505 | { 506 | 507 | const install = (global) => { 508 | 509 | Reflect.defineProperty(global.Number.prototype, "to", { 510 | value: function* (v) { 511 | let i = this.valueOf() 512 | if (i <= v) { 513 | while (i <= v) { 514 | yield i 515 | i++ 516 | } 517 | } 518 | else { 519 | while (i >= v) { 520 | yield i 521 | i-- 522 | } 523 | } 524 | }, 525 | configurable: true, 526 | enumerable: false, 527 | writable: true, 528 | }) 529 | 530 | const numberToString = global.Number.prototype.toString 531 | Reflect.defineProperty(global.Number.prototype, "toString", { 532 | value(base, size) { 533 | if (size === undefined) return numberToString.call(this, base) 534 | if (size <= 0) return "" 535 | const string = numberToString.call(this, base) 536 | return string.slice(-size).padStart(size, "0") 537 | }, 538 | configurable: true, 539 | enumerable: false, 540 | writable: true, 541 | }) 542 | 543 | if (global.BigInt !== undefined) { 544 | const bigIntToString = global.BigInt.prototype.toString 545 | Reflect.defineProperty(global.BigInt.prototype, "toString", { 546 | value(base, size) { 547 | if (size === undefined) return bigIntToString.call(this, base) 548 | if (size <= 0) return "" 549 | const string = bigIntToString.call(this, base) 550 | return string.slice(-size).padStart(size, "0") 551 | }, 552 | configurable: true, 553 | enumerable: false, 554 | writable: true, 555 | }) 556 | } 557 | 558 | Habitat.Number.installed = true 559 | 560 | } 561 | 562 | Habitat.Number = {install} 563 | 564 | } 565 | 566 | //========// 567 | // Object // 568 | //========// 569 | { 570 | Habitat.Object = {} 571 | Habitat.Object.install = (global) => { 572 | 573 | Reflect.defineProperty(global.Object.prototype, Symbol.iterator, { 574 | value: function*() { 575 | for (const key in this) { 576 | yield this[key] 577 | } 578 | }, 579 | configurable: true, 580 | enumerable: false, 581 | writable: true, 582 | }) 583 | 584 | Reflect.defineProperty(global.Object.prototype, "keys", { 585 | value() { 586 | return Object.keys(this) 587 | }, 588 | configurable: true, 589 | enumerable: false, 590 | writable: true, 591 | }) 592 | 593 | Reflect.defineProperty(global.Object.prototype, "values", { 594 | value() { 595 | return Object.values(this) 596 | }, 597 | configurable: true, 598 | enumerable: false, 599 | writable: true, 600 | }) 601 | 602 | Reflect.defineProperty(global.Object.prototype, "entries", { 603 | value() { 604 | return Object.entries(this) 605 | }, 606 | configurable: true, 607 | enumerable: false, 608 | writable: true, 609 | }) 610 | 611 | Habitat.Object.installed = true 612 | 613 | } 614 | 615 | } 616 | 617 | //==========// 618 | // Property // 619 | //==========// 620 | { 621 | 622 | const install = (global) => { 623 | 624 | Reflect.defineProperty(global.Object.prototype, "_", { 625 | get() { 626 | return new Proxy(this, { 627 | set(object, propertyName, descriptor) { 628 | Reflect.defineProperty(object, propertyName, descriptor) 629 | }, 630 | get(object, propertyName) { 631 | const editor = { 632 | get value() { 633 | const descriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {} 634 | const {value} = descriptor 635 | return value 636 | }, 637 | set value(value) { 638 | const {enumerable, configurable, writable} = Reflect.getOwnPropertyDescriptor(object, propertyName) || {enumerable: true, configurable: true, writable: true} 639 | const descriptor = {value, enumerable, configurable, writable} 640 | Reflect.defineProperty(object, propertyName, descriptor) 641 | }, 642 | get get() { 643 | const descriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {} 644 | const {get} = descriptor 645 | return get 646 | }, 647 | set get(get) { 648 | const {set, enumerable, configurable} = Reflect.getOwnPropertyDescriptor(object, propertyName) || {enumerable: true, configurable: true} 649 | const descriptor = {get, set, enumerable, configurable} 650 | Reflect.defineProperty(object, propertyName, descriptor) 651 | }, 652 | get set() { 653 | const descriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {} 654 | const {set} = descriptor 655 | return set 656 | }, 657 | set set(set) { 658 | const {get, enumerable, configurable} = Reflect.getOwnPropertyDescriptor(object, propertyName) || {enumerable: true, configurable: true} 659 | const descriptor = {get, set, enumerable, configurable} 660 | Reflect.defineProperty(object, propertyName, descriptor) 661 | }, 662 | get enumerable() { 663 | const descriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {} 664 | const {enumerable} = descriptor 665 | return enumerable 666 | }, 667 | set enumerable(v) { 668 | const descriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {configurable: true, writable: true} 669 | descriptor.enumerable = v 670 | Reflect.defineProperty(object, propertyName, descriptor) 671 | }, 672 | get configurable() { 673 | const descriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {} 674 | const {configurable} = descriptor 675 | return configurable 676 | }, 677 | set configurable(v) { 678 | const descriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {enumerable: true, writable: true} 679 | descriptor.configurable = v 680 | Reflect.defineProperty(object, propertyName, descriptor) 681 | }, 682 | get writable() { 683 | const descriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {} 684 | const {writable} = descriptor 685 | return writable 686 | }, 687 | set writable(v) { 688 | const oldDescriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {enumerable: true, configurable: true} 689 | const {get, set, writable, ...rest} = oldDescriptor 690 | const newDescriptor = {...rest, writable: v} 691 | Reflect.defineProperty(object, propertyName, newDescriptor) 692 | }, 693 | } 694 | return editor 695 | }, 696 | }) 697 | }, 698 | set(value) { 699 | Reflect.defineProperty(this, "_", {value, configurable: true, writable: true, enumerable: true}) 700 | }, 701 | configurable: true, 702 | enumerable: false, 703 | }) 704 | 705 | 706 | Habitat.Property.installed = true 707 | 708 | } 709 | 710 | Habitat.Property = {install} 711 | 712 | } 713 | 714 | //========// 715 | // Random // 716 | //========// 717 | { 718 | Habitat.Random = {} 719 | 720 | const maxId8 = 2 ** 16 721 | const u8s = new Uint8Array(maxId8) 722 | let id8 = maxId8 723 | const getRandomUint8 = () => { 724 | 725 | if (id8 >= maxId8) { 726 | crypto.getRandomValues(u8s) 727 | id8 = 0 728 | } 729 | 730 | const result = u8s[id8] 731 | id8++ 732 | return result 733 | } 734 | 735 | Reflect.defineProperty(Habitat.Random, "Uint8", { 736 | get: getRandomUint8, 737 | configurable: true, 738 | enumerable: true, 739 | }) 740 | 741 | const maxId32 = 2 ** 14 742 | const u32s = new Uint32Array(maxId32) 743 | let id32 = maxId32 744 | const getRandomUint32 = () => { 745 | 746 | if (id32 >= maxId32) { 747 | crypto.getRandomValues(u32s) 748 | id32 = 0 749 | } 750 | 751 | const result = u32s[id32] 752 | id32++ 753 | return result 754 | } 755 | 756 | Reflect.defineProperty(Habitat.Random, "Uint32", { 757 | get: getRandomUint32, 758 | configurable: true, 759 | enumerable: true, 760 | }) 761 | 762 | Habitat.Random.oneIn = (n) => { 763 | const result = getRandomUint32() 764 | return result % n === 0 765 | } 766 | 767 | Habitat.Random.maybe = (chance) => { 768 | return Habitat.Random.oneIn(1 / chance) 769 | } 770 | 771 | Habitat.Random.install = (global) => { 772 | global.Random = Habitat.Random 773 | global.oneIn = Habitat.Random.oneIn 774 | global.maybe = Habitat.Random.maybe 775 | Habitat.Random.installed = true 776 | } 777 | 778 | } 779 | 780 | //=======// 781 | // Stage // 782 | //=======// 783 | { 784 | 785 | Habitat.Stage = {} 786 | Habitat.Stage.make = () => { 787 | 788 | const canvas = document.createElement("canvas") 789 | const context = canvas.getContext("2d") 790 | 791 | const stage = { 792 | canvas, 793 | context, 794 | update: () => {}, 795 | draw: () => {}, 796 | tick: () => { 797 | stage.update() 798 | stage.draw() 799 | requestAnimationFrame(stage.tick) 800 | }, 801 | } 802 | 803 | requestAnimationFrame(stage.tick) 804 | return stage 805 | } 806 | 807 | Habitat.Stage.install = (global) => { 808 | global.Stage = Habitat.Stage 809 | Habitat.Stage.installed = true 810 | 811 | } 812 | 813 | } 814 | 815 | //========// 816 | // String // 817 | //========// 818 | { 819 | 820 | const install = (global) => { 821 | 822 | Reflect.defineProperty(global.String.prototype, "divide", { 823 | value(n) { 824 | const regExp = RegExp(`[^]{1,${n}}`, "g") 825 | return this.match(regExp) 826 | }, 827 | configurable: true, 828 | enumerable: false, 829 | writable: true, 830 | }) 831 | 832 | Reflect.defineProperty(global.String.prototype, "toNumber", { 833 | value(base) { 834 | return parseInt(this, base) 835 | }, 836 | configurable: true, 837 | enumerable: false, 838 | writable: true, 839 | }) 840 | 841 | Habitat.String.installed = true 842 | 843 | } 844 | 845 | Habitat.String = {install} 846 | 847 | } 848 | 849 | //=======// 850 | // Touch // 851 | //=======// 852 | { 853 | 854 | const Touches = Habitat.Touches = [] 855 | 856 | const trim = (a) => { 857 | if (a.length == 0) return a 858 | let start = a.length - 1 859 | let end = 0 860 | for (let i = 0; i < a.length; i++) { 861 | const value = a[i] 862 | if (value !== undefined) { 863 | start = i 864 | break 865 | } 866 | } 867 | for (let i = a.length - 1; i >= 0; i--) { 868 | const value = a[i] 869 | if (value !== undefined) { 870 | end = i + 1 871 | break 872 | } 873 | } 874 | a.splice(end) 875 | a.splice(0, start) 876 | return a 877 | } 878 | 879 | Reflect.defineProperty(Touches, "install", { 880 | value(global) { 881 | 882 | global.Touches = Touches 883 | global.addEventListener("touchstart", e => { 884 | for (const changedTouch of e.changedTouches) { 885 | const x = changedTouch.clientX 886 | const y = changedTouch.clientY 887 | const id = changedTouch.identifier 888 | if (Touches[id] === undefined) Touches[id] = {position: [undefined, undefined]} 889 | const touch = Touches[id] 890 | touch.position[0] = x 891 | touch.position[1] = y 892 | } 893 | }) 894 | 895 | global.addEventListener("touchmove", e => { 896 | try { 897 | for (const changedTouch of e.changedTouches) { 898 | const x = changedTouch.clientX 899 | const y = changedTouch.clientY 900 | const id = changedTouch.identifier 901 | let touch = Touches[id] 902 | if (touch == undefined) { 903 | touch = {position: [undefined, undefined]} 904 | Touches[id] = touch 905 | } 906 | 907 | touch.position[0] = x 908 | touch.position[1] = y 909 | } 910 | } 911 | catch(e) { 912 | console.error(e) 913 | } 914 | }) 915 | 916 | global.addEventListener("touchend", e => { 917 | for (const changedTouch of e.changedTouches) { 918 | const id = changedTouch.identifier 919 | Touches[id] = undefined 920 | } 921 | trim(Touches) 922 | }) 923 | 924 | Reflect.defineProperty(Touches, "installed", { 925 | value: true, 926 | configurable: true, 927 | enumerable: false, 928 | writable: true, 929 | }) 930 | }, 931 | configurable: true, 932 | enumerable: false, 933 | writable: true, 934 | }) 935 | 936 | 937 | } 938 | 939 | 940 | //======// 941 | // Type // 942 | //======// 943 | { 944 | 945 | const Int = { 946 | check: (n) => n % 1 == 0, 947 | convert: (n) => parseInt(n), 948 | } 949 | 950 | const Positive = { 951 | check: (n) => n >= 0, 952 | convert: (n) => Math.abs(n), 953 | } 954 | 955 | const Negative = { 956 | check: (n) => n <= 0, 957 | convert: (n) => -Math.abs(n), 958 | } 959 | 960 | const UInt = { 961 | check: (n) => n % 1 == 0 && n >= 0, 962 | convert: (n) => Math.abs(parseInt(n)), 963 | } 964 | 965 | const UpperCase = { 966 | check: (s) => s == s.toUpperCase(), 967 | convert: (s) => s.toUpperCase(), 968 | } 969 | 970 | const LowerCase = { 971 | check: (s) => s == s.toLowerCase(), 972 | convert: (s) => s.toLowerCase(), 973 | } 974 | 975 | const WhiteSpace = { 976 | check: (s) => /^[ | ]*$/.test(s), 977 | } 978 | 979 | const PureObject = { 980 | check: (o) => o.constructor == Object, 981 | } 982 | 983 | const Primitive = { 984 | check: p => p.is(Number) || p.is(String) || p.is(RegExp) || p.is(Symbol), 985 | } 986 | 987 | const install = (global) => { 988 | 989 | global.Int = Int 990 | global.Positive = Positive 991 | global.Negative = Negative 992 | global.UInt = UInt 993 | global.UpperCase = UpperCase 994 | global.LowerCase = LowerCase 995 | global.WhiteSpace = WhiteSpace 996 | global.PureObject = PureObject 997 | global.Primitive = Primitive 998 | 999 | Reflect.defineProperty(global.Object.prototype, "is", { 1000 | value(type) { 1001 | if ("check" in type) { 1002 | try { return type.check(this) } 1003 | catch {} 1004 | } 1005 | try { return this instanceof type } 1006 | catch { return false } 1007 | }, 1008 | configurable: true, 1009 | enumerable: false, 1010 | writable: true, 1011 | }) 1012 | 1013 | Reflect.defineProperty(global.Object.prototype, "as", { 1014 | value(type) { 1015 | if ("convert" in type) { 1016 | try { return type.convert(this) } 1017 | catch {} 1018 | } 1019 | return type(this) 1020 | }, 1021 | configurable: true, 1022 | enumerable: false, 1023 | writable: true, 1024 | }) 1025 | 1026 | Habitat.Type.installed = true 1027 | 1028 | } 1029 | 1030 | Habitat.Type = {install, Int, Positive, Negative, UInt, UpperCase, LowerCase, WhiteSpace, PureObject, Primitive} 1031 | 1032 | } -------------------------------------------------------------------------------- /images/Black/Other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Black/Other.png -------------------------------------------------------------------------------- /images/Black/Other.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Black/Other.psd -------------------------------------------------------------------------------- /images/Black/Other@0.25x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Black/Other@0.25x.png -------------------------------------------------------------------------------- /images/Black/Other@0.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Black/Other@0.5x.png -------------------------------------------------------------------------------- /images/Black/Other@0.75x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Black/Other@0.75x.png -------------------------------------------------------------------------------- /images/Blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Blank.png -------------------------------------------------------------------------------- /images/Blank.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Blank.psd -------------------------------------------------------------------------------- /images/Blank@0.25x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Blank@0.25x.png -------------------------------------------------------------------------------- /images/Blank@0.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Blank@0.5x.png -------------------------------------------------------------------------------- /images/Blank@0.75x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Blank@0.75x.png -------------------------------------------------------------------------------- /images/Cyan/Mad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Cyan/Mad.png -------------------------------------------------------------------------------- /images/Cyan/Mad.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Cyan/Mad.psd -------------------------------------------------------------------------------- /images/Cyan/Mad@0.25x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Cyan/Mad@0.25x.png -------------------------------------------------------------------------------- /images/Cyan/Mad@0.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Cyan/Mad@0.5x.png -------------------------------------------------------------------------------- /images/Cyan/Mad@0.75x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Cyan/Mad@0.75x.png -------------------------------------------------------------------------------- /images/Cyan/Other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Cyan/Other.png -------------------------------------------------------------------------------- /images/Cyan/Other.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Cyan/Other.psd -------------------------------------------------------------------------------- /images/Cyan/Other@0.25x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Cyan/Other@0.25x.png -------------------------------------------------------------------------------- /images/Cyan/Other@0.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Cyan/Other@0.5x.png -------------------------------------------------------------------------------- /images/Cyan/Other@0.75x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Cyan/Other@0.75x.png -------------------------------------------------------------------------------- /images/Green/Other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Green/Other.png -------------------------------------------------------------------------------- /images/Green/Other.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Green/Other.psd -------------------------------------------------------------------------------- /images/Green/Other@0.25x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Green/Other@0.25x.png -------------------------------------------------------------------------------- /images/Green/Other@0.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Green/Other@0.5x.png -------------------------------------------------------------------------------- /images/Green/Other@0.75x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Green/Other@0.75x.png -------------------------------------------------------------------------------- /images/Purple/Other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Purple/Other.png -------------------------------------------------------------------------------- /images/Purple/Other.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Purple/Other.psd -------------------------------------------------------------------------------- /images/Purple/Other@0.25x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Purple/Other@0.25x.png -------------------------------------------------------------------------------- /images/Purple/Other@0.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Purple/Other@0.5x.png -------------------------------------------------------------------------------- /images/Purple/Other@0.75x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Purple/Other@0.75x.png -------------------------------------------------------------------------------- /images/Yellow/Other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Yellow/Other.png -------------------------------------------------------------------------------- /images/Yellow/Other.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Yellow/Other.psd -------------------------------------------------------------------------------- /images/Yellow/Other@0.25x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Yellow/Other@0.25x.png -------------------------------------------------------------------------------- /images/Yellow/Other@0.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Yellow/Other@0.5x.png -------------------------------------------------------------------------------- /images/Yellow/Other@0.75x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/TimePond/9767e33c8fc44bf56db826b9219ca721fdd734e3/images/Yellow/Other@0.75x.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /source/atom.js: -------------------------------------------------------------------------------- 1 | //=======// 2 | // Setup // 3 | //=======// 4 | let ATOM_ID = 0 5 | const makeAtom = ({ 6 | width = 50, 7 | height = 50, 8 | x = WORLD_WIDTH/2 - width/2, 9 | y = WORLD_HEIGHT/2 - height/2, 10 | dx = 0, 11 | dy = 0, 12 | draw = DRAW_RECTANGLE, 13 | update = UPDATE_STATIC, 14 | grab = GRAB_DRAG, 15 | turns = 0, 16 | cutTop = 0, 17 | cutBottom = 0, 18 | cutRight = 0, 19 | cutLeft = 0, 20 | autoLinks = [], 21 | construct = () => {}, 22 | flipX = false, 23 | ...args 24 | } = {}, {autoTurn = true} = {}) => { 25 | const atom = { 26 | id: ATOM_ID++, 27 | width, 28 | height, 29 | cutTop, 30 | cutBottom, 31 | cutRight, 32 | cutLeft, 33 | x, 34 | y, 35 | dx, 36 | dy, 37 | nextdx: dx, 38 | nextdy: dy, 39 | turns: 0, 40 | nextturns: 0, 41 | draw, 42 | update, 43 | grab, 44 | flipX: false, 45 | portals: {top: undefined, bottom: undefined, left: undefined, right: undefined}, 46 | links: [], 47 | ...args 48 | } 49 | 50 | if (flipX) { 51 | flipAtom(atom) 52 | } 53 | 54 | for (const autoLink of autoLinks) { 55 | const latom = makeAtom(autoLink.element) 56 | linkAtom(atom, latom, autoLink.offset, autoLink.transfer) 57 | } 58 | 59 | // Band-aid for old silly arguments idea 60 | if (autoTurn) { 61 | turnAtom(atom, turns) 62 | } 63 | else { 64 | // I SHOULD do this 65 | // But at this point, I've hardcoded fixes for it everywhere else in the code 66 | // so I can't do it now 67 | // fix it in TimePond 2 68 | //atom.turns = turns 69 | } 70 | 71 | construct(atom) 72 | return atom 73 | } 74 | 75 | const cloneAtom = (atom) => { 76 | const clone = {} 77 | for (const key in atom) { 78 | clone[key] = deepishCloneAtomProperty(atom[key], key) 79 | } 80 | return makeAtom(clone, {autoTurn: false}) 81 | } 82 | 83 | 84 | 85 | const deepishCloneAtomProperty = (value, key) => { 86 | if (key === "id") return ATOM_ID++ 87 | if (key === "portals") return {top: undefined, bottom: undefined, left: undefined, right: undefined} 88 | if (key === "links") return [] 89 | if (typeof value === "undefined") return value 90 | if (typeof value === "string") return value 91 | if (typeof value === "number") return value 92 | if (typeof value === "boolean") return value 93 | if (typeof value === "function") return value //not deepcloning but i promise i wont mess around with function properties 94 | if (typeof value === "object") { 95 | /*if (value instanceof Array) { 96 | const array = [] 97 | for (const key in value) { 98 | array[key] = value[key] //not a pure deep clone 99 | } 100 | return array.d 101 | }*/ 102 | if (value instanceof Object) { 103 | const object = {} 104 | for (const key in value) { 105 | object[key] = value[key] //not a pure deep clone either cos we want the atom REFERENCES yo 106 | } 107 | return object 108 | } 109 | } 110 | console.error("Couldn't deepish-clone value", value) 111 | } 112 | 113 | //===========// 114 | // Game Loop // 115 | //===========// 116 | const updateAtom = (atom, world) => { 117 | if (atom.skipUpdate === true) { 118 | atom.skipUpdate = false 119 | } 120 | else { 121 | 122 | //atom.prevBounds = getBounds(atom) 123 | atom.update(atom, world) 124 | } 125 | updateAtomLinks(atom, world) 126 | } 127 | 128 | const updateAtomLinks = (atom) => { 129 | for (const link of atom.links) { 130 | //link.atom.prevBounds = getBounds(link.atom) 131 | for (const key of LINKED_PROPERTIES) { 132 | 133 | if (link.offset[key] !== undefined) { 134 | const them = atom[key] 135 | const me = link.atom[key] 136 | link.atom[key] = link.offset[key](them, me) 137 | } 138 | else { 139 | link.atom[key] = atom[key] 140 | } 141 | 142 | 143 | } 144 | 145 | updateAtomLinks(link.atom) 146 | } 147 | } 148 | 149 | const drawAtom = (atom, context) => { 150 | const {draw} = atom 151 | draw(atom, context) 152 | } 153 | 154 | //=========// 155 | // Usefuls // 156 | //=========// 157 | const linkAtom = (atom, latom, offset={}, transfer={}) => { 158 | if (atom === undefined) return 159 | const trans = {...transfer} 160 | for (const key of LINKED_PROPERTIES) { 161 | if (trans[key] === undefined) { 162 | trans[key] = (parent, child, key, value=child[key]) => parent[key] = value 163 | } 164 | } 165 | const link = {atom: latom, offset: {...offset}, transfer: trans} 166 | atom.links.push(link) 167 | latom.parent = atom 168 | return link 169 | } 170 | 171 | const getDescendentsAndMe = (self) => { 172 | if (self.links.length === 0) return [self] 173 | const atoms = self.links.map(link => link.atom) 174 | const descendents = atoms.map(atom => getDescendentsAndMe(atom)).flat(1) 175 | return [self, ...descendents] 176 | } 177 | 178 | const transferToParent = (child, key, value) => { 179 | const parent = child.parent 180 | if (parent === undefined) return 181 | const link = getLink(child) 182 | if (link === undefined) { 183 | print("orphan - no parent to transfer to") 184 | return 185 | } 186 | link.transfer[key](parent, child, key, value) 187 | if (parent.parent !== undefined) transferToParent(parent, key, parent[key]) 188 | } 189 | 190 | const getLink = (child) => { 191 | const parent = child.parent 192 | if (parent === undefined) return 193 | for (const link of parent.links) { 194 | if (link.atom === child) return link 195 | } 196 | //print(child.world.id) 197 | //print(parent.world.id) 198 | print("orphan in world", child.world.id) 199 | //removeAtom(child.world, child, {includingChildren: true, destroy: true}) 200 | //child.cutRight = 0 201 | //throw new Error(`[TimePond] There's an orphan! This shouldn't happen... I haven't made sure that all children have parent links properly :(`) 202 | 203 | } 204 | 205 | const moveAtom = (atom, x, y) => { 206 | atom.x = x 207 | atom.y = y 208 | updateAtomLinks(atom) 209 | } 210 | 211 | const flipAtom = (atom) => { 212 | atom.flipX = !atom.flipX 213 | const [newCutLeft, newCutRight] = [atom.cutRight, atom.cutLeft] 214 | 215 | atom.cutLeft = newCutLeft 216 | atom.cutRight = newCutRight 217 | const cutDiff = newCutLeft - newCutRight 218 | atom.x -= cutDiff 219 | 220 | //atom.cutBottom.d 221 | /*print("") 222 | print("FLIPPED") 223 | print("cutBottom", atom.cutBottom) 224 | print("cutTop", atom.cutTop) 225 | print("cutLeft", atom.cutLeft) 226 | print("cutRight", atom.cutRight)*/ 227 | } 228 | 229 | const turnAtom = (atom, turns=1, fallSafe=false, rejectIfOverlap=false, world, exceptions=[], overridePortals=false) => { 230 | if (!overridePortals) { 231 | if (atom.portals.top !== undefined) return false 232 | if (atom.portals.bottom !== undefined) return false 233 | if (atom.portals.right !== undefined) return false 234 | if (atom.portals.left !== undefined) return false 235 | } 236 | if (atom.turns === undefined) atom.turns = 0 237 | if (turns === 0) return true 238 | if (turns < 0) return turnAtom(atom, 4+turns, fallSafe, rejectIfOverlap, world, exceptions=[], overridePortals) 239 | if (turns > 1) { 240 | const result = turnAtom(atom, 1, fallSafe, rejectIfOverlap, world, exceptions=[], overridePortals) 241 | if (!result) return false 242 | return turnAtom(atom, turns-1, fallSafe, rejectIfOverlap, world, exceptions=[], overridePortals) 243 | } 244 | const old = {} 245 | const obounds = getBounds(atom) 246 | const {height, width, cutTop, cutBottom, cutRight, cutLeft} = atom 247 | old.height = height 248 | old.width = width 249 | old.cutTop = cutTop 250 | old.cutBottom = cutBottom 251 | old.cutLeft = cutLeft 252 | old.cutRight = cutRight 253 | 254 | old.y = atom.y 255 | old.x = atom.x 256 | atom.height = width 257 | atom.width = height 258 | 259 | if (!atom.flipX) { 260 | atom.cutBottom = cutRight 261 | atom.cutLeft = cutBottom 262 | atom.cutTop = cutLeft 263 | atom.cutRight = cutTop 264 | } 265 | else { 266 | atom.cutBottom = cutLeft 267 | atom.cutLeft = cutTop 268 | atom.cutTop = cutRight 269 | atom.cutRight = cutBottom 270 | } 271 | 272 | const oldLinks = new Map() 273 | for (const link of atom.links) { 274 | const oldLink = {} 275 | oldLinks.set(link, oldLink) 276 | oldLink.height = link.atom.height 277 | oldLink.width = link.atom.width 278 | oldLink.cutTop = link.atom.cutTop 279 | oldLink.cutBottom = link.atom.cutBottom 280 | oldLink.cutLeft = link.atom.cutLeft 281 | oldLink.cutRight = link.atom.cutRight 282 | 283 | oldLink.y = link.atom.y 284 | oldLink.x = link.atom.x 285 | 286 | oldLink.offset = {} 287 | oldLink.transfer = {} 288 | 289 | for (const key of LINKED_PROPERTIES) { 290 | oldLink.offset[key] = link.offset[key] 291 | oldLink.transfer[key] = link.transfer[key] 292 | } 293 | 294 | for (const linkType of ["offset", "transfer"]) { 295 | link[linkType].width = oldLink[linkType].height 296 | link[linkType].height = oldLink[linkType].width 297 | 298 | if (!link.atom.flipX) { 299 | link[linkType].cutBottom = oldLink[linkType].cutRight 300 | link[linkType].cutLeft = oldLink[linkType].cutBottom 301 | link[linkType].cutTop = oldLink[linkType].cutLeft 302 | link[linkType].cutRight = oldLink[linkType].cutTop 303 | } 304 | else { 305 | link[linkType].cutBottom = oldLink[linkType].cutLeft 306 | link[linkType].cutLeft = oldLink[linkType].cutTop 307 | link[linkType].cutTop = oldLink[linkType].cutRight 308 | link[linkType].cutRight = oldLink[linkType].cutBottom 309 | } 310 | 311 | link[linkType].x = oldLink[linkType].y 312 | link[linkType].y = oldLink[linkType].x 313 | link[linkType].dx = oldLink[linkType].dy 314 | link[linkType].dy = oldLink[linkType].dx 315 | link[linkType].nextdx = oldLink[linkType].nextdy 316 | link[linkType].nextdy = oldLink[linkType].nextdx 317 | } 318 | 319 | turnAtom(link.atom, turns, fallSafe, false, world, exceptions, overridePortals) 320 | 321 | } 322 | 323 | if (rejectIfOverlap) { 324 | 325 | const nbounds = getBounds(atom) 326 | atom.y -= nbounds.bottom-obounds.bottom + 1 327 | atom.x -= (atom.width-atom.height)/2 328 | for (const a of world.atoms) { 329 | if (a === atom) continue 330 | if (exceptions.includes(a)) continue 331 | if (atomOverlaps(atom, a)) { 332 | //const bounds = getBounds(atom) 333 | //const abounds = getBounds(a) 334 | //if (abounds.top === bounds.bottom) continue 335 | for (const key in old) { 336 | atom[key] = old[key] 337 | } 338 | for (const [link, oldLink] of oldLinks) { 339 | for (const key in oldLink) { 340 | if (key === "offset" || key === "transfer") continue 341 | link.atom[key] = oldLink[key] 342 | } 343 | for (const key in oldLink.offset) { 344 | link.offset[key] = oldLink.offset[key] 345 | } 346 | for (const key in oldLink.transfer) { 347 | link.transfer[key] = oldLink.transfer[key] 348 | } 349 | } 350 | return false 351 | } 352 | } 353 | 354 | } 355 | atom.turns++ 356 | if (atom.turns >= 4) atom.turns = 0 357 | 358 | 359 | /*print("") 360 | print("TURNED") 361 | print("cutBottom", atom.cutBottom) 362 | print("cutTop", atom.cutTop) 363 | print("cutLeft", atom.cutLeft) 364 | print("cutRight", atom.cutRight)*/ 365 | 366 | return true 367 | } 368 | 369 | const getBounds = ({x, y, width, height, cutTop=0, cutBottom=0, cutLeft=0, cutRight=0}) => { 370 | const top = y + cutTop 371 | const bottom = y + height - cutBottom 372 | const left = x + cutLeft 373 | const right = x + width - cutRight 374 | return {top, bottom, left, right} 375 | } 376 | 377 | const pointOverlaps = ({x, y}, atom) => { 378 | const {left, right, top, bottom} = getBounds(atom) 379 | return x >= left && x <= right && y >= top && y <= bottom 380 | } 381 | 382 | const getAtomAncestor = (atom) => { 383 | if (atom.parent === undefined) return atom 384 | return getAtomAncestor(atom.parent) 385 | } 386 | 387 | const atomIsDescendant = (kid, parent) => { 388 | if (kid.parent === parent) return true 389 | if (kid.parent === undefined) return false 390 | return atomIsDescendant(kid.parent, parent) 391 | } 392 | 393 | 394 | const atomOverlaps = (self, atom) => { 395 | 396 | if (atomIsDescendant(self, atom)) return false 397 | if (atomIsDescendant(atom, self)) return false 398 | 399 | for (const link of self.links) { 400 | const result = atomOverlaps(link.atom, atom) 401 | if (result) return true 402 | } 403 | 404 | const bounds = getBounds(self) 405 | const abounds = getBounds(atom) 406 | 407 | const horizAligns = aligns([bounds.left, bounds.right], [], [abounds.left, abounds.right]) 408 | const vertAligns = aligns([bounds.top, bounds.bottom], [], [abounds.top, abounds.bottom]) 409 | if (horizAligns && vertAligns) return true 410 | //if (horizAligns && bounds.top <= abounds.top && bounds.bottom >= abounds.bottom) return true 411 | //if (horizAligns && bounds.left <= abounds.left && bounds.right >= abounds.right) return true 412 | 413 | const ahorizAligns = aligns([abounds.left, abounds.right], [], [bounds.left, bounds.right]) 414 | const avertAligns = aligns([abounds.top, abounds.bottom], [], [bounds.top, bounds.bottom]) 415 | if (ahorizAligns && avertAligns) return true 416 | //if (ahorizAligns && abounds.top <= bounds.top && abounds.bottom >= bounds.bottom) return true 417 | //if (avertAligns && abounds.left <= bounds.left && abounds.right >= bounds.right) return true 418 | 419 | return false 420 | } 421 | 422 | const getPointSide = (point, [left, right]) => { 423 | if (point < left) return -1 424 | if (point > right) return 1 425 | return 0 426 | } 427 | 428 | const aligns = ([left, right], [nleft, nright], [aleft, aright=aleft]) => { 429 | const leftSide = getPointSide(left, [aleft, aright]) 430 | const rightSide = getPointSide(right, [aleft, aright]) 431 | if (leftSide === 0) return true 432 | if (rightSide === 0) return true 433 | if (leftSide*-1 == rightSide) return true 434 | 435 | // For moving things 436 | if (nleft !== undefined && nright !== undefined) { 437 | const nleftSide = getPointSide(nleft, [aleft, aright]) 438 | const nrightSide = getPointSide(nright, [aleft, aright]) 439 | if (nleftSide === 0) return true 440 | if (nrightSide === 0) return true 441 | if (leftSide*-1 == nleftSide) return true 442 | if (rightSide*-1 == nrightSide) return true 443 | } 444 | 445 | return false 446 | } -------------------------------------------------------------------------------- /source/config.js: -------------------------------------------------------------------------------- 1 | //========// 2 | // Config // 3 | //========// 4 | const WORLD_WIDTH = 500 5 | const WORLD_HEIGHT = 500 6 | let MENU_HEIGHT = 100 -------------------------------------------------------------------------------- /source/element.js: -------------------------------------------------------------------------------- 1 | //=========// 2 | // Drawers // 3 | //=========// 4 | const DRAW_RECTANGLE = (self, context) => { 5 | const {x, y, width, height, colour = Colour.Red, cutTop=0, cutBottom=0, cutLeft=0, cutRight=0} = self 6 | context.fillStyle = colour 7 | context.fillRect(x+cutLeft, y+cutTop, width-(cutRight+cutLeft), height-(cutBottom+cutTop)) 8 | } 9 | 10 | const DRAW_CIRCLE = (self, context) => { 11 | const {x, y, width, height, colour = Colour.Red, cutTop=0, cutBottom=0, cutLeft=0, cutRight=0} = self 12 | context.fillStyle = colour 13 | context.beginPath() 14 | context.arc(x+height/2, y+height/2, height/2, 0, 2*Math.PI) 15 | context.fill() 16 | } 17 | 18 | const images = {} 19 | const DRAW_IMAGE = (self, context) => { 20 | 21 | if (self === undefined) return 22 | 23 | /*if (self.previousDraw !== undefined) { 24 | let prev = self.previousDraw 25 | DRAW_IMAGE(prev, context) 26 | //self.trailCount-- 27 | 28 | } 29 | 30 | if (self.trailCount === undefined || self.trailCount < TRAIL_LENGTH) { 31 | self.previousDraw = cloneAtom(self) 32 | self.previousDraw.trailCount = self.trailCount + 1 33 | } 34 | else { 35 | self.previousDraw = undefined 36 | }*/ 37 | 38 | //self.previousDraw = cloneAtom(self) 39 | //self.previousDraw.previousDraw = undefined 40 | 41 | // Sprite 42 | if (images[self.source] === undefined) { 43 | const image = new Image() 44 | image.src = self.source 45 | images[self.source] = image 46 | } 47 | const image = images[self.source] 48 | const imageHeightRatio = image.height/self.height 49 | const imageWidthRatio = image.width/self.width 50 | 51 | // Positioning 52 | const bounds = getBounds(self) 53 | const boundsWidth = bounds.right - bounds.left 54 | const boundsHeight = bounds.bottom - bounds.top 55 | const boundsDimensionDiff = boundsWidth - boundsHeight 56 | 57 | let centerX = (bounds.right + bounds.left)/2 58 | let centerY = (bounds.bottom + bounds.top)/2 59 | const centerDiff = centerX - centerY 60 | 61 | if (self.turns === 1) { 62 | //centerY += boundsDimensionDiff/2 63 | } 64 | /* else if (self.turns === 3) { 65 | centerY -= boundsDimensionDiff/2 66 | centerX -= boundsDimensionDiff/4 67 | }*/ 68 | 69 | // Cuts 70 | let {cutRight, cutLeft, cutBottom, cutTop} = self 71 | if (!self.flipX) { 72 | for (let i = 0; i < self.turns; i++) [cutRight, cutBottom, cutLeft, cutTop] = [cutBottom, cutLeft, cutTop, cutRight] 73 | } 74 | if (self.flipX) { 75 | //;[cutLeft, cutRight] = [cutRight, cutLeft] 76 | if (self.turns % 2 !== 0) [cutLeft, cutRight] = [cutRight, cutLeft] 77 | if (self.turns % 2 === 0) [cutLeft, cutRight] = [cutRight, cutLeft] 78 | //if (self.turns % 2 !== 0) [cutTop, cutBottom] = [cutBottom, cutTop] 79 | for (let i = 0; i < self.turns; i++) [cutRight, cutBottom, cutLeft, cutTop] = [cutBottom, cutLeft, cutTop, cutRight] 80 | } 81 | //for (let i = 0; i < self.turns; i++) [cutRight, cutBottom, cutLeft, cutTop] = [cutBottom, cutLeft, cutTop, cutRight] 82 | 83 | //else if (self.flipX && self.turns % 2 === 0) [cutTop, cutBottom] = [cutBottom, cutTop] 84 | 85 | const cutWidth = cutRight + cutLeft 86 | const cutHeight = cutBottom + cutTop 87 | 88 | // Snippet 89 | const snippetX = cutLeft*imageWidthRatio 90 | const snippetY = cutTop*imageHeightRatio 91 | const snippetWidth = image.width - cutWidth*imageWidthRatio 92 | const snippetHeight = image.height - cutHeight*imageHeightRatio 93 | 94 | const flipWidthRatio = image.height/self.width 95 | const flipHeightRatio = image.width/self.height 96 | 97 | 98 | // Flips and Rotations 99 | context.save() 100 | if (self.flipX || self.turns > 0) { 101 | context.translate(centerX, centerY) 102 | if (self.flipX) context.scale(-1, 1) 103 | if (self.turns > 0) context.rotate(Math.PI/2 * self.turns) 104 | context.translate(-centerX, -centerY) 105 | } 106 | 107 | // Draw! 108 | /*if (self.turns % 2 !== 0) { 109 | //context.drawImage(image, snippetX, snippetY, snippetWidth, snippetHeight, bounds.left+boundsDimensionDiff/2, bounds.top-boundsDimensionDiff/2, boundsHeight, boundsWidth) 110 | cutLeft.d 111 | context.drawImage(image, snippetX, snippetY, snippetWidth+boundsDimensionDiff, snippetHeight-boundsDimensionDiff, bounds.left+boundsDimensionDiff/2, bounds.top-boundsDimensionDiff/2, boundsHeight, boundsWidth) 112 | } 113 | else { 114 | context.drawImage(image, snippetX, snippetY, snippetWidth, snippetHeight, bounds.left, bounds.top, boundsWidth, boundsHeight) 115 | }*/ 116 | 117 | const alpha = self.opacity !== undefined? self.opacity : 1.0 118 | context.globalAlpha = alpha 119 | //print(self.opacity) 120 | 121 | if (self.turns % 2 !== 0) { 122 | context.drawImage(image, cutLeft*flipWidthRatio, cutTop*flipHeightRatio, image.width - cutLeft*flipWidthRatio - cutRight*flipWidthRatio, image.height - cutTop*flipHeightRatio - cutBottom*flipHeightRatio, bounds.left + boundsDimensionDiff/2, bounds.top - boundsDimensionDiff/2, boundsHeight, boundsWidth) 123 | } 124 | else { 125 | context.drawImage(image, snippetX, snippetY, snippetWidth, snippetHeight, bounds.left, bounds.top, boundsWidth, boundsHeight) 126 | } 127 | 128 | //context.drawImage(image, snippetX, snippetY, snippetWidth, snippetHeight, bounds.left, bounds.top, boundsWidth, boundsHeight) 129 | context.restore() 130 | 131 | // Debug: Showing bounding box! 132 | if (self.showBounds) { 133 | context.strokeStyle = Colour.White 134 | context.strokeRect(bounds.left, bounds.top, bounds.right-bounds.left, bounds.bottom-bounds.top) 135 | } 136 | } 137 | 138 | const DRAW_SPAWNER = (self, context) => { 139 | const {spawn} = self 140 | const {draw} = spawn 141 | draw(self, context) 142 | } 143 | 144 | //==========// 145 | // Updaters // 146 | //==========// 147 | const UPDATE_STATIC = (self) => { 148 | self.dx = 0 149 | self.dy = 0 150 | self.nextdx = 0 151 | self.nextdy = 0 152 | } 153 | 154 | const UPDATE_NONE = () => {} 155 | 156 | const UPDATE_MOVER_GRAVITY = 0.5 157 | const UPDATE_MOVER_AIR_RESISTANCE = 0.99 158 | const UPDATE_MOVER_FRICTION = 0.8 159 | const UPDATE_MOVER_BEING = (self, world) => { 160 | UPDATE_MOVER(self, world) 161 | if (self.flipX && self.dx < -0.1) { 162 | flipAtom(self) 163 | } 164 | else if (!self.flipX && self.dx > 0.1) { 165 | flipAtom(self) 166 | } 167 | if (self.nextdx < 0.1 && self.nextdx > -0.1 && self.grounded) { 168 | if (self.jumpTick > 60) { 169 | if (self.turns !== 0) { 170 | turnAtom(self, -self.turns, true, true, world) 171 | self.nextdx = 1 * (self.flipX? 1 : -1) 172 | self.nextdy = -2 173 | self.jumpTick = 0 174 | } 175 | else { 176 | self.nextdx = 3 * (self.flipX? 1 : -1) 177 | self.nextdy = -10 178 | self.jumpTick = 0 179 | } 180 | } 181 | else { 182 | if (self.jumpTick === undefined) self.jumpTick = 0 183 | self.jumpTick++ 184 | } 185 | } 186 | } 187 | 188 | const Capitalised = { 189 | convert: (s) => s[0].as(UpperCase) + s.slice(1) 190 | } 191 | 192 | const makeBlink = () => ({}) 193 | const UPDATE_MOVER = (self, world) => { 194 | return moverUpdate(self, world) 195 | } 196 | 197 | const getOppositeSideName = (side) => { 198 | if (side === "top") return "bottom" 199 | if (side === "bottom") return "top" 200 | if (side === "left") return "right" 201 | if (side === "right") return "left" 202 | } 203 | 204 | //==========// 205 | // Grabbers // 206 | //==========// 207 | const GRAB_DRAG = (self) => { 208 | if (self.portals !== undefined) { 209 | if (self.portals.top !== undefined) return undefined 210 | if (self.portals.bottom !== undefined) return undefined 211 | if (self.portals.left !== undefined) return undefined 212 | if (self.portals.right !== undefined) return undefined 213 | } 214 | if (self.isPortal) { 215 | for (const atom of self.world.atoms) { 216 | if (atom.portals !== undefined) { 217 | if (atom.portals.top === self) return undefined 218 | if (atom.portals.bottom === self) return undefined 219 | if (atom.portals.left === self) return undefined 220 | if (atom.portals.right === self) return undefined 221 | } 222 | } 223 | } 224 | return self 225 | } 226 | const GRAB_STATIC = () => {} 227 | const GRAB_SPAWNER = (self, hand, world) => { 228 | const atom = makeAtom(self.spawn) 229 | addAtom(world, atom) 230 | atom.x = self.x 231 | atom.y = self.y 232 | return atom 233 | } 234 | 235 | const GRAB_SPAWNER_PORTAL = (self, hand, world) => { 236 | //if (self.tally === undefined) self.tally = 0 237 | const grabbed = GRAB_SPAWNER(self, hand, world) 238 | /*self.tally++ 239 | if (self.tally % 2 === 0) { 240 | if (self.tally/2 >= ELEMENT_PORTAL_COLOURS.length) self.tally = 0 241 | self.spawn = {...self.spawn, colour: ELEMENT_PORTAL_COLOURS[self.tally/2]} 242 | self.colour = self.spawn.colour 243 | }*/ 244 | return grabbed 245 | } 246 | 247 | const GRAB_LINKEE = (self, hand, world) => { 248 | hand.offset.x -= self.x - self.parent.x 249 | hand.offset.y -= self.y - self.parent.y 250 | return self.parent.grab(self.parent, hand, world) 251 | } 252 | 253 | //=========// 254 | // Portals // 255 | //=========// 256 | const PORTAL_VOID = { 257 | enter: ({froggy}) => { 258 | 259 | }, 260 | exit: (atom, world) => { 261 | //print("End voidal!") 262 | }, 263 | moveIn: (atom, world) => { 264 | //print("Move in voidal!") 265 | }, 266 | moveOut: (atom, world) => { 267 | //print("Move out voidal!") 268 | }, 269 | move: () => { 270 | //print("Move through voidal!") 271 | } 272 | } 273 | 274 | const PORTAL_BOUNCE = { 275 | enter: (event) => { 276 | 277 | const realWorld = event.world.realWorld 278 | if (event.world.bounceTimer !== undefined) { 279 | event.froggy.nextdy *= -0.9 280 | //event.world.bounceTimer = undefined 281 | return 282 | } 283 | 284 | if (event.world.isCrashTest) { 285 | if (event.froggy.id === event.world.crashNeededFroggy) { 286 | event.world.crashSuccess = true 287 | return "CRASH" 288 | } 289 | } 290 | 291 | //print(event.world.id) 292 | if (event.world.isProjection && event.world.isOnCatchup !== true) { 293 | //print("proj") 294 | } 295 | else { 296 | //print("bye") 297 | PORTAL_VOID.enter(event) 298 | return 299 | } 300 | 301 | if (realWorld.isProjection) { 302 | PORTAL_VOID.enter(event) 303 | return 304 | } 305 | 306 | const crashTestRealWorld = cloneWorld(event.world) 307 | const crashTestWorld = cloneWorld(realWorld) 308 | crashTestWorld.realWorld = crashTestRealWorld 309 | crashTestWorld.future_projection_skip = 30 310 | 311 | crashTestRealWorld.isCrashTest = true 312 | crashTestWorld.isCrashTest = true 313 | 314 | const crash_froggy = crashTestRealWorld.atoms.find(a => a.id === event.froggy.id) 315 | const crash_portal = crashTestWorld.atoms.find(a => a.id === event.portal.id) 316 | const crash_target = crash_portal.target 317 | 318 | crashTestRealWorld.crashNeededFroggy = event.froggy.id 319 | crashTestWorld.crashNeededFroggy = event.froggy.id 320 | 321 | PORTAL_MOVE.enter({portal:crash_portal, froggy: crash_froggy, axis: event.axis}, {target: crash_target}) 322 | for (let i = 0; i < 31; i++) { 323 | fullUpdateWorld(crashTestRealWorld) 324 | fullUpdateWorld(crashTestWorld) 325 | } 326 | 327 | if (!crashTestWorld.crashSuccess) { 328 | print("PARADOX") 329 | realWorld.bounceTimer = 30 330 | //realWorld.future_projection_skip = 30 331 | //saveFutureProjection(realWorld) 332 | return 333 | } 334 | 335 | 336 | //realWorld.futureProjection = undefined 337 | //saveFutureProjection(realWorld) 338 | const clone_world = cloneWorld(realWorld) 339 | clone_world.future_projection_skip = 30 340 | 341 | addWorld(multiverse, clone_world) 342 | 343 | const clone_portal = clone_world.atoms.find(a => a.id === event.portal.id) 344 | const clone_target = clone_portal.target 345 | PORTAL_MOVE.enter(event, {target: clone_target}) 346 | print("nowline from", clone_portal, "to", clone_target) 347 | 348 | replaceWorld(realWorld, clone_world) 349 | realWorld.pruneTimer = 30 350 | 351 | return 352 | 353 | } 354 | } 355 | 356 | const PORTAL_FADE = { 357 | enter: (event) => { 358 | 359 | //print(event.world.id) 360 | if (event.world.isProjection && event.world.isOnCatchup !== true) { 361 | //print("proj") 362 | } 363 | else { 364 | //print("bye") 365 | PORTAL_VOID.enter(event) 366 | return 367 | } 368 | 369 | const realWorld = event.world.realWorld 370 | if (realWorld.isProjection) { 371 | PORTAL_VOID.enter(event) 372 | return 373 | } 374 | 375 | //realWorld.futureProjection = undefined 376 | //saveFutureProjection(realWorld) 377 | const clone_world = cloneWorld(realWorld) 378 | //clone_world.futureProjection = undefined 379 | clone_world.future_projection_skip = 30 380 | //clone_world.projection_skip = 1 381 | //clone_world.isProjection = false 382 | 383 | addWorld(multiverse, clone_world) 384 | 385 | const clone_portal = clone_world.atoms.find(a => a.id === event.portal.id) 386 | const clone_target = clone_portal.target 387 | const variant = PORTAL_MOVE.enter(event, {target: clone_target}) 388 | const clone_froggy = clone_world.atoms.find(a => a.id === event.froggy.id) 389 | print("nowline from", clone_portal, "to", clone_target) 390 | 391 | replaceWorld(realWorld, clone_world) 392 | realWorld.pruneTimer = 30 393 | 394 | variant.fadeReliantOn = clone_froggy 395 | //variant.fadeReliantOn = clone_froggy 396 | //clone_froggy.fadeReliantOn = variant 397 | clone_world.fadeReliance = 30 398 | 399 | return 400 | 401 | } 402 | } 403 | 404 | const PORTAL_FREEZE = { 405 | enter: (event) => { 406 | 407 | //print(event.world.id) 408 | if (event.world.isProjection && event.world.isOnCatchup !== true) { 409 | //print("proj") 410 | } 411 | else { 412 | //print("bye") 413 | PORTAL_VOID.enter(event) 414 | //event.froggy.fadeReliantOn = undefined 415 | if (event.froggy.fadeRelier !== undefined) event.froggy.fadeRelier.d.fadeReliantOn = undefined 416 | event.world.fadeReliance = undefined 417 | return 418 | } 419 | 420 | const realWorld = event.world.realWorld 421 | if (realWorld.isProjection) { 422 | PORTAL_VOID.enter(event) 423 | return 424 | } 425 | 426 | //realWorld.futureProjection = undefined 427 | //saveFutureProjection(realWorld) 428 | const clone_world = cloneWorld(realWorld) 429 | //clone_world.futureProjection = undefined 430 | clone_world.future_projection_skip = 30 431 | //clone_world.projection_skip = 1 432 | //clone_world.isProjection = false 433 | 434 | addWorld(multiverse, clone_world) 435 | 436 | const clone_portal = clone_world.atoms.find(a => a.id === event.portal.id) 437 | const clone_target = clone_portal.target 438 | const variant = PORTAL_MOVE.enter(event, {target: clone_target}) 439 | const clone_froggy = clone_world.atoms.find(a => a.id === event.froggy.id) 440 | print("nowline from", clone_portal, "to", clone_target) 441 | 442 | replaceWorld(realWorld, clone_world) 443 | realWorld.pruneTimer = 30 444 | 445 | variant.fadeReliantOn = clone_froggy 446 | clone_froggy.fadeRelier = variant 447 | variant.isFreezeFadeType = true 448 | //variant.fadeReliantOn = clone_froggy 449 | //clone_froggy.fadeReliantOn = variant 450 | clone_world.fadeReliance = 30 451 | 452 | return 453 | 454 | } 455 | } 456 | 457 | const PORTAL_MAD = { 458 | enter: (event) => { 459 | 460 | //print(event.world.id) 461 | if (event.world.isProjection && event.world.isOnCatchup !== true) { 462 | //print("proj") 463 | } 464 | else { 465 | //print("bye") 466 | PORTAL_VOID.enter(event) 467 | //event.froggy.fadeReliantOn = undefined 468 | if (event.froggy.fadeRelier !== undefined) event.froggy.fadeRelier.d.fadeReliantOn = undefined 469 | event.world.fadeReliance = undefined 470 | return 471 | } 472 | 473 | const realWorld = event.world.realWorld 474 | if (realWorld.isProjection) { 475 | PORTAL_VOID.enter(event) 476 | return 477 | } 478 | 479 | //realWorld.futureProjection = undefined 480 | //saveFutureProjection(realWorld) 481 | const clone_world = cloneWorld(realWorld) 482 | //clone_world.futureProjection = undefined 483 | clone_world.future_projection_skip = 30 484 | //clone_world.projection_skip = 1 485 | //clone_world.isProjection = false 486 | 487 | addWorld(multiverse, clone_world) 488 | 489 | const clone_portal = clone_world.atoms.find(a => a.id === event.portal.id) 490 | const clone_target = clone_portal.target 491 | const variant = PORTAL_MOVE.enter(event, {target: clone_target}) 492 | const clone_froggy = clone_world.atoms.find(a => a.id === event.froggy.id) 493 | print("nowline from", clone_portal, "to", clone_target) 494 | 495 | replaceWorld(realWorld, clone_world) 496 | realWorld.pruneTimer = 30 497 | 498 | variant.fadeReliantOn = clone_froggy 499 | clone_froggy.fadeRelier = variant 500 | variant.isMadFadeType = true 501 | //variant.fadeReliantOn = clone_froggy 502 | //clone_froggy.fadeReliantOn = variant 503 | clone_world.fadeReliance = 30 504 | 505 | return 506 | 507 | } 508 | } 509 | 510 | const PORTAL_PASTNOW = { 511 | enter: (event) => { 512 | 513 | //print(event.world.id) 514 | if (event.world.isProjection && event.world.isOnCatchup !== true) { 515 | //print("proj") 516 | } 517 | else { 518 | //print("bye") 519 | PORTAL_VOID.enter(event) 520 | return 521 | } 522 | 523 | const realWorld = event.world.realWorld 524 | if (realWorld.isProjection) { 525 | PORTAL_VOID.enter(event) 526 | return 527 | } 528 | 529 | //realWorld.futureProjection = undefined 530 | //saveFutureProjection(realWorld) 531 | const clone_world = cloneWorld(realWorld) 532 | //clone_world.futureProjection = undefined 533 | clone_world.future_projection_skip = 30 534 | //clone_world.projection_skip = 1 535 | //clone_world.isProjection = false 536 | 537 | addWorld(multiverse, clone_world) 538 | 539 | const clone_portal = clone_world.atoms.find(a => a.id === event.portal.id) 540 | const clone_target = clone_portal.target 541 | PORTAL_MOVE.enter(event, {target: clone_target}) 542 | print("nowline from", clone_portal, "to", clone_target) 543 | 544 | replaceWorld(realWorld, clone_world) 545 | realWorld.pruneTimer = 30 546 | 547 | return 548 | 549 | } 550 | } 551 | 552 | const PORTAL_PASTNOWLINE = { 553 | enter: (event) => { 554 | 555 | //print(event.world.id) 556 | if (event.world.isProjection && event.world.isOnCatchup !== true) { 557 | //print("proj") 558 | } 559 | else { 560 | //print("bye") 561 | PORTAL_VOID.enter(event) 562 | return 563 | } 564 | 565 | const realWorld = event.world.realWorld 566 | if (realWorld.isProjection) { 567 | "hi".d 568 | PORTAL_VOID.enter(event) 569 | return 570 | } 571 | 572 | //realWorld.futureProjection = undefined 573 | //saveFutureProjection(realWorld) 574 | const clone_world = cloneWorld(realWorld) 575 | //clone_world.futureProjection = undefined 576 | clone_world.future_projection_skip = 30 577 | //clone_world.projection_skip = 1 578 | //clone_world.isProjection = false 579 | 580 | addWorld(multiverse, clone_world) 581 | 582 | const clone_portal = clone_world.atoms.find(a => a.id === event.portal.id) 583 | const clone_target = clone_portal.target 584 | PORTAL_MOVE.enter(event, {target: clone_target}) 585 | print("nowline from", clone_portal, "to", clone_target) 586 | 587 | return 588 | 589 | } 590 | } 591 | 592 | const PORTAL_FUTURENOW = { 593 | enter: (event) => { 594 | 595 | if (event.world.isProjection) { 596 | PORTAL_VOID.enter(event) 597 | return 598 | } 599 | 600 | const projection = event.world.futureProjection 601 | 602 | if (projection === undefined) { 603 | PORTAL_VOID.enter(event) 604 | return 605 | } 606 | 607 | const clone_world = projection 608 | projection.isProjection = false 609 | projection.futureProjection = undefined 610 | event.world.futureProjection = undefined 611 | saveFutureProjection(event.world) 612 | //savePastProjection(clone_world) 613 | /*clone_world.projection_skip = 1*/ 614 | 615 | const clone_portal = clone_world.atoms.find(a => a.atom_id === event.portal.atom_id) 616 | const clone_target = clone_portal.target 617 | 618 | print(event.world.isProjection, event.world.id) 619 | addWorld(multiverse, clone_world) 620 | clone_world.futureNowRecordings = [] 621 | clone_world.futureNowBaseWorld = event.world 622 | 623 | PORTAL_MOVE.enter(event, {target: clone_target}) 624 | 625 | const clone_froggy = clone_world.atoms.find(a => a.atom_id === event.froggy.atom_id).d 626 | 627 | saveFutureProjection(projection) 628 | //event.world.futureProjection = undefined 629 | //saveFutureProjection(event.world) 630 | //if (clone_froggy != undefined) clone_froggy.variantParent = event.froggy 631 | //else print(clone_froggy) 632 | 633 | //clone_froggy.variantParent = event.froggy 634 | 635 | //if (clone_froggy === undefined) return true 636 | //else { 637 | //clone_froggy.variantParent = event.froggy 638 | //} 639 | 640 | //clone_froggy.hh 641 | //replaceWorld(clone_world, event.world) 642 | 643 | return 644 | } 645 | } 646 | 647 | const PORTAL_FUTURELINE = { 648 | enter: (event) => { 649 | 650 | if (event.world.isProjection) { 651 | PORTAL_VOID.enter(event) 652 | return 653 | } 654 | 655 | const projection = event.world.futureProjection 656 | 657 | if (projection === undefined) { 658 | PORTAL_VOID.enter(event) 659 | return 660 | } 661 | 662 | const clone_world = projection 663 | projection.isProjection = false 664 | projection.futureProjection = undefined 665 | event.world.futureProjection = undefined 666 | saveFutureProjection(event.world) 667 | //savePastProjection(clone_world) 668 | /*clone_world.projection_skip = 1*/ 669 | 670 | const clone_portal = clone_world.atoms.find(a => a.atom_id === event.portal.atom_id) 671 | const clone_target = clone_portal.target 672 | 673 | print(event.world.isProjection, event.world.id) 674 | addWorld(multiverse, clone_world) 675 | 676 | PORTAL_MOVE.enter(event, {target: clone_target}) 677 | 678 | const clone_froggy = clone_world.atoms.find(a => a.atom_id === event.froggy.atom_id).d 679 | 680 | saveFutureProjection(projection) 681 | //event.world.futureProjection = undefined 682 | //saveFutureProjection(event.world) 683 | //if (clone_froggy != undefined) clone_froggy.variantParent = event.froggy 684 | //else print(clone_froggy) 685 | 686 | //clone_froggy.variantParent = event.froggy 687 | 688 | //if (clone_froggy === undefined) return true 689 | //else { 690 | //clone_froggy.variantParent = event.froggy 691 | //} 692 | 693 | //clone_froggy.hh 694 | 695 | return 696 | } 697 | } 698 | const PORTAL_NEXUS = { 699 | enter: (event) => { 700 | 701 | const realWorld = event.world.realWorld 702 | if (event.world.bounceTimer !== undefined) { 703 | //event.froggy.nextdy *= -0.9 704 | //event.world.bounceTimer = undefined 705 | //return 706 | print("nexus") 707 | 708 | return PORTAL_PASTLINE.enter(event) 709 | } 710 | 711 | if (event.world.isCrashTest) { 712 | if (event.froggy.id === event.world.crashNeededFroggy) { 713 | event.world.crashSuccess = true 714 | return "CRASH" 715 | } 716 | } 717 | 718 | //print(event.world.id) 719 | if (event.world.isProjection && event.world.isOnCatchup !== true) { 720 | //print("proj") 721 | } 722 | else { 723 | //print("bye") 724 | PORTAL_VOID.enter(event) 725 | return 726 | } 727 | 728 | if (realWorld.isProjection) { 729 | PORTAL_VOID.enter(event) 730 | return 731 | } 732 | 733 | const crashTestRealWorld = cloneWorld(event.world) 734 | const crashTestWorld = cloneWorld(realWorld) 735 | crashTestWorld.realWorld = crashTestRealWorld 736 | crashTestWorld.future_projection_skip = 30 737 | 738 | crashTestRealWorld.isCrashTest = true 739 | crashTestWorld.isCrashTest = true 740 | 741 | const crash_froggy = crashTestRealWorld.atoms.find(a => a.id === event.froggy.id) 742 | const crash_portal = crashTestWorld.atoms.find(a => a.id === event.portal.id) 743 | const crash_target = crash_portal.target 744 | 745 | crashTestRealWorld.crashNeededFroggy = event.froggy.id 746 | crashTestWorld.crashNeededFroggy = event.froggy.id 747 | 748 | PORTAL_MOVE.enter({portal:crash_portal, froggy: crash_froggy, axis: event.axis}, {target: crash_target}) 749 | for (let i = 0; i < 31; i++) { 750 | fullUpdateWorld(crashTestRealWorld) 751 | fullUpdateWorld(crashTestWorld) 752 | } 753 | 754 | if (!crashTestWorld.crashSuccess) { 755 | print("PARADOX") 756 | realWorld.bounceTimer = 30 757 | //realWorld.future_projection_skip = 30 758 | //saveFutureProjection(realWorld) 759 | return PORTAL_VOID.enter(event) 760 | } 761 | 762 | 763 | //realWorld.futureProjection = undefined 764 | //saveFutureProjection(realWorld) 765 | const clone_world = cloneWorld(realWorld) 766 | clone_world.future_projection_skip = 30 767 | 768 | addWorld(multiverse, clone_world) 769 | 770 | const clone_portal = clone_world.atoms.find(a => a.id === event.portal.id) 771 | const clone_target = clone_portal.target 772 | PORTAL_MOVE.enter(event, {target: clone_target}) 773 | print("nowline from", clone_portal, "to", clone_target) 774 | 775 | realWorld.isHidden = true 776 | replaceWorld(realWorld, clone_world) 777 | realWorld.pruneTimer = 31 778 | 779 | return 780 | 781 | }, 782 | exit: (event) => { 783 | const froggy = event.froggy 784 | froggy.cutTop = 0 785 | froggy.cutLeft = 0 786 | froggy.cutRight = 0 787 | froggy.cutBottom = 0 788 | } 789 | } 790 | 791 | const PORTAL_PASTLINE = { 792 | enter: (event) => { 793 | 794 | // UNCOMMENT WHEN NOT DOING NEXUS 795 | if (event.world.bounceTimer !== undefined) { 796 | return PORTAL_VOID.enter(event) 797 | } 798 | 799 | const projection = event.world.pastProjections[30] 800 | 801 | 802 | if (projection === undefined) { 803 | PORTAL_VOID.enter(event) 804 | return 805 | } 806 | 807 | //print(projection.id) 808 | 809 | const clone_world = cloneWorld(projection) 810 | savePastProjection(clone_world) 811 | clone_world.projection_skip = 1 812 | 813 | const clone_portal = clone_world.atoms[event.portal.atom_id] 814 | const clone_target = clone_portal.target 815 | const clone_froggy = clone_world.atoms[event.froggy.atom_id] 816 | 817 | if (!event.world.isProjection) { 818 | addWorld(multiverse, clone_world) 819 | } 820 | PORTAL_MOVE.enter(event, {target: clone_target}) 821 | 822 | clone_froggy.variantParent = event.froggy 823 | clone_world.bounceTimer = 31 824 | 825 | //clone_froggy.d 826 | //event.froggy.links[0].atom.d 827 | 828 | //removeAtom(event.world, variant, {includingChildren: false, destroy: false}) 829 | //addAtom(clone_world, variant, {ignoreLinks: false}) 830 | 831 | //variant.portals[event.axis.back] = clone_target 832 | 833 | /*clone_variant.parent = froggy 834 | for (const link of froggy.links) { 835 | if (link.atom === variant) { 836 | link.atom = clone_variant 837 | } 838 | }*/ 839 | 840 | //removeAtom(event.world, variant, {includingChildren: false}) 841 | //removeAtom(clone_world, clone_froggy) 842 | 843 | 844 | 845 | //moveAtomWorld(clone_variant, event.world, clone_world) 846 | 847 | 848 | 849 | return 850 | } 851 | } 852 | 853 | const PORTAL_REFROG = { 854 | enter: (event) => { 855 | const variant = PORTAL_MOVE.enter(event) 856 | if (variant !== undefined) { 857 | variant.refrogTrackPlay = true 858 | } 859 | } 860 | } 861 | 862 | const PORTAL_INVERT = { 863 | enter: (event) => { 864 | 865 | /*if (event.world.bounceTimer !== undefined) { 866 | return PORTAL_VOID.enter(event) 867 | }*/ 868 | 869 | const projection = event.world 870 | 871 | 872 | if (projection === undefined) { 873 | PORTAL_VOID.enter(event) 874 | return 875 | } 876 | 877 | //print(projection.id) 878 | 879 | const clone_world = cloneWorld(projection) 880 | savePastProjection(clone_world) 881 | //clone_world.projection_skip = 1 882 | 883 | const clone_portal = clone_world.atoms[event.portal.atom_id] 884 | const clone_target = clone_portal.target 885 | const clone_froggy = clone_world.atoms[event.froggy.atom_id] 886 | 887 | if (!event.world.isProjection) { 888 | addWorld(multiverse, clone_world) 889 | } 890 | 891 | clone_world.rewindAutoPlay = event.world.pastProjections.map(w => cloneWorld(w)) 892 | //replaceWorld(clone_world, event.world) 893 | //event.world.isHidden = true 894 | //event.world.pruneTimer = 200 895 | //PORTAL_MOVE.enter(event, {target: clone_target}) 896 | 897 | //clone_froggy.variantParent = event.froggy 898 | //clone_world.bounceTimer = 31 899 | 900 | //clone_froggy.d 901 | //event.froggy.links[0].atom.d 902 | 903 | //removeAtom(event.world, variant, {includingChildren: false, destroy: false}) 904 | //addAtom(clone_world, variant, {ignoreLinks: false}) 905 | 906 | //variant.portals[event.axis.back] = clone_target 907 | 908 | /*clone_variant.parent = froggy 909 | for (const link of froggy.links) { 910 | if (link.atom === variant) { 911 | link.atom = clone_variant 912 | } 913 | }*/ 914 | 915 | //removeAtom(event.world, variant, {includingChildren: false}) 916 | //removeAtom(clone_world, clone_froggy) 917 | 918 | 919 | 920 | //moveAtomWorld(clone_variant, event.world, clone_world) 921 | 922 | const variant = PORTAL_MOVE.enter(event) 923 | moveAtomWorld(variant, event.world, clone_world) 924 | if (clone_world.bonusAtoms === undefined) { 925 | clone_world.bonusAtoms = [] 926 | } 927 | clone_world.bonusAtoms.push(variant) 928 | 929 | 930 | return 931 | } 932 | } 933 | 934 | const PORTAL_REWIND = { 935 | enter: (event) => { 936 | 937 | /*if (event.world.bounceTimer !== undefined) { 938 | return PORTAL_VOID.enter(event) 939 | }*/ 940 | 941 | const projection = event.world 942 | 943 | 944 | if (projection === undefined) { 945 | PORTAL_VOID.enter(event) 946 | return 947 | } 948 | 949 | //print(projection.id) 950 | 951 | const clone_world = cloneWorld(projection) 952 | savePastProjection(clone_world) 953 | //clone_world.projection_skip = 1 954 | 955 | const clone_portal = clone_world.atoms[event.portal.atom_id] 956 | const clone_target = clone_portal.target 957 | const clone_froggy = clone_world.atoms[event.froggy.atom_id] 958 | 959 | if (!event.world.isProjection) { 960 | addWorld(multiverse, clone_world) 961 | } 962 | 963 | clone_world.rewindAutoPlay = event.world.pastProjections.map(w => cloneWorld(w)) 964 | 965 | //PORTAL_MOVE.enter(event, {target: clone_target}) 966 | 967 | //clone_froggy.variantParent = event.froggy 968 | //clone_world.bounceTimer = 31 969 | 970 | //clone_froggy.d 971 | //event.froggy.links[0].atom.d 972 | 973 | //removeAtom(event.world, variant, {includingChildren: false, destroy: false}) 974 | //addAtom(clone_world, variant, {ignoreLinks: false}) 975 | 976 | //variant.portals[event.axis.back] = clone_target 977 | 978 | /*clone_variant.parent = froggy 979 | for (const link of froggy.links) { 980 | if (link.atom === variant) { 981 | link.atom = clone_variant 982 | } 983 | }*/ 984 | 985 | //removeAtom(event.world, variant, {includingChildren: false}) 986 | //removeAtom(clone_world, clone_froggy) 987 | 988 | 989 | 990 | //moveAtomWorld(clone_variant, event.world, clone_world) 991 | 992 | 993 | 994 | return 995 | } 996 | } 997 | 998 | const PORTAL_REWRITE = { 999 | enter: (event) => { 1000 | 1001 | if (event.world.bounceTimer !== undefined) { 1002 | return PORTAL_VOID.enter(event) 1003 | } 1004 | 1005 | const projection = event.world.pastProjections[30] 1006 | 1007 | if (projection === undefined) { 1008 | PORTAL_VOID.enter(event) 1009 | return 1010 | } 1011 | 1012 | //print(projection.id) 1013 | 1014 | const clone_world = cloneWorld(projection) 1015 | savePastProjection(clone_world) 1016 | clone_world.projection_skip = 1 1017 | 1018 | const clone_portal = clone_world.atoms[event.portal.atom_id] 1019 | const clone_target = clone_portal.target 1020 | const clone_froggy = clone_world.atoms[event.froggy.atom_id] 1021 | 1022 | if (!event.world.isProjection) { 1023 | addWorld(multiverse, clone_world) 1024 | } 1025 | PORTAL_MOVE.enter(event, {target: clone_target}) 1026 | 1027 | clone_froggy.variantParent = event.froggy 1028 | 1029 | replaceWorld(event.world, clone_world) 1030 | 1031 | event.world.pruneTimer = 30 1032 | event.world.isHidden = true 1033 | clone_world.bounceTimer = 31 1034 | 1035 | //clone_froggy.d 1036 | //event.froggy.links[0].atom.d 1037 | 1038 | //removeAtom(event.world, variant, {includingChildren: false, destroy: false}) 1039 | //addAtom(clone_world, variant, {ignoreLinks: false}) 1040 | 1041 | //variant.portals[event.axis.back] = clone_target 1042 | 1043 | /*clone_variant.parent = froggy 1044 | for (const link of froggy.links) { 1045 | if (link.atom === variant) { 1046 | link.atom = clone_variant 1047 | } 1048 | }*/ 1049 | 1050 | //removeAtom(event.world, variant, {includingChildren: false}) 1051 | //removeAtom(clone_world, clone_froggy) 1052 | 1053 | 1054 | 1055 | //moveAtomWorld(clone_variant, event.world, clone_world) 1056 | 1057 | 1058 | 1059 | return 1060 | } 1061 | } 1062 | 1063 | const PORTAL_DIMENSION = { 1064 | enter: (event) => { 1065 | 1066 | 1067 | 1068 | if (event.world.isProjection) { 1069 | //PORTAL_VOID.enter(event) 1070 | return 1071 | } 1072 | 1073 | 1074 | const clone_world = cloneWorld(event.world) 1075 | addWorld(multiverse, clone_world) 1076 | 1077 | const clone_portal = clone_world.atoms[event.portal.atom_id] 1078 | const clone_target = clone_portal.target 1079 | 1080 | const variant = PORTAL_MOVE.enter(event, {target: clone_target}) 1081 | const clone_froggy = clone_world.atoms[event.froggy.atom_id] 1082 | const clone_variant = clone_world.atoms[variant.atom_id] 1083 | const froggy = event.froggy 1084 | 1085 | /*clone_variant.parent = froggy 1086 | for (const link of froggy.links) { 1087 | if (link.atom === variant) { 1088 | link.atom = clone_variant 1089 | } 1090 | }*/ 1091 | 1092 | //removeAtom(event.world, variant, {includingChildren: false}) 1093 | removeAtom(clone_world, clone_froggy, {includingChildren: false}) 1094 | 1095 | 1096 | 1097 | return 1098 | } 1099 | } 1100 | 1101 | const PORTAL_MOVE = { 1102 | enter: ({portal, pbounds, froggy, world, axis, blockers}, {target = portal.target} = {}) => { 1103 | if (target !== undefined) { 1104 | 1105 | // UNCOMMENT FOR SOME THINGS!?!?! 1106 | /*if (world !== undefined && world.isProjection) { 1107 | return 1108 | }*/ 1109 | 1110 | const variant = cloneAtom(froggy) 1111 | variant.fling = target.turns - portal.turns 1112 | while (variant.fling < 0) { 1113 | variant.fling += 4 1114 | } 1115 | //if (variant.fling === 3) variant.fling = 1 1116 | 1117 | const size = (variant.turns % 2 === 0)? variant[axis.sizeName] : variant[axis.otherSizeName] 1118 | variant[axis.cutBackName] = size 1119 | variant[axis.cutFrontName] = 0 1120 | 1121 | variant.links = [] 1122 | if (froggy !== undefined && froggy.world !== undefined) { 1123 | variant.update = froggy.world.atoms.includes(froggy)? UPDATE_NONE : froggy.update 1124 | } 1125 | 1126 | //variant.portals.d 1127 | //froggy.portals.d 1128 | 1129 | let displacementOther = 0 1130 | let displacement = 0 1131 | 1132 | if (variant.fling === 0) { 1133 | 1134 | variant.portals[axis.front] = undefined 1135 | variant.portals[axis.back] = target 1136 | displacement = target[axis.name] - portal[axis.name] 1137 | displacement += target[axis.sizeName] * axis.direction // Go to other side of portal 1138 | 1139 | displacementOther = target[axis.other.name] - portal[axis.other.name] 1140 | 1141 | 1142 | variant.onPromote = (self) => { 1143 | self.update = froggy.update 1144 | self.skipUpdate = true 1145 | self.fling = undefined 1146 | } 1147 | 1148 | const link = linkAtom(froggy, variant, { 1149 | [axis.other.name]: v => v + displacementOther, 1150 | [axis.name]: v => v + displacement, 1151 | ["turns"]: (them, me) => me, 1152 | ["width"]: (them, me) => me, 1153 | 1154 | 1155 | ["nextdx"]: () => froggy.dx, 1156 | ["nextdy"]: () => froggy.dy, 1157 | 1158 | ["dx"]: () => froggy.dx, 1159 | ["dy"]: () => froggy.dy, 1160 | 1161 | ["height"]: (them, me) => me, 1162 | ["flipX"]: (them, me) => me, 1163 | }) 1164 | } 1165 | else if (variant.fling === 1) { 1166 | 1167 | variant.portals[axis.flingFrontName] = undefined 1168 | variant.portals[axis.flingBackName] = target 1169 | 1170 | const variantStartingPlaceOther = target[axis.other.name] 1171 | const froggyStartingPlaceOther = portal[axis.name] - froggy[axis.sizeName] 1172 | 1173 | const variantStartingPlace = target[axis.name] + (froggy[axis.other.name] - portal[axis.other.name]) 1174 | const froggyStartingPlace = froggy[axis.other.name] 1175 | 1176 | variant.onPromote = (self) => { 1177 | self.update = froggy.update 1178 | self.skipUpdate = true 1179 | self.fling = undefined 1180 | } 1181 | 1182 | const flipXDirection = froggy.flipX? -1 : 1 1183 | const link = linkAtom(froggy, variant, { 1184 | [axis.other.name]: () => variantStartingPlaceOther - (froggy[axis.name] - froggyStartingPlaceOther) - 1, //TODO: remove need for minus one 1185 | [axis.name]: () => variantStartingPlace + (froggy[axis.other.name] - froggyStartingPlace), 1186 | ["turns"]: (them, me) => me, 1187 | ["width"]: (them, me) => me, 1188 | ["height"]: (them, me) => me, 1189 | 1190 | ["nextdx"]: () => -froggy.dy, 1191 | ["nextdy"]: () => froggy.dx, 1192 | 1193 | ["dx"]: () => -froggy.dy, 1194 | ["dy"]: () => froggy.dx, 1195 | ["flipX"]: (them, me) => me, 1196 | }) 1197 | } 1198 | else if (variant.fling === 2) { 1199 | throw new Error(`[TimePond] Unimplemented fling type ${variant.fling}`) 1200 | } 1201 | else if (variant.fling === 3) { 1202 | throw new Error(`[TimePond] Unimplemented fling type ${variant.fling}`) 1203 | } 1204 | else { 1205 | throw new Error(`[TimePond] Invalid fling type ${variant.fling}... Please tell @todepond`) 1206 | } 1207 | 1208 | const flipXDirection = froggy.flipX? -1 : 1 1209 | updateAtomLinks(froggy) 1210 | variant.turns = froggy.turns //band-aid because makeAtom doesn't do turns properly 1211 | 1212 | 1213 | addAtom(target.world, variant) 1214 | turnAtom(variant, variant.fling * flipXDirection, false, false, target.world, [], true) 1215 | //if (variant.turns % 4 === 1) flipAtom(variant) 1216 | 1217 | //variant.prevBounds = getBounds(variant) 1218 | 1219 | return variant 1220 | } 1221 | }, 1222 | end: () => {}, 1223 | moveIn: () => {}, 1224 | moveOut: () => {}, 1225 | leave: () => {}, 1226 | } 1227 | 1228 | //===========// 1229 | // Colliders // 1230 | //===========// 1231 | // TODO: stop the rotate potion from rotating portals/frogs that are currently portaling because it would tear atoms in half 1232 | const COLLIDE_POTION_ROTATE = ({self, atom, axis, world}) => { 1233 | if (self.used) return 1234 | atom = getAtomAncestor(atom) 1235 | if (!atom.isVoid && !atom.isPotion) { 1236 | world.atoms = world.atoms.filter(a => a !== self) 1237 | if (!atom.isMover) turnAtom(atom, 1, true, true, world, [self]) 1238 | else atom.nextturns++ 1239 | self.used = true 1240 | //atom.nextdx = 0 1241 | atom.nextdy = -5 1242 | atom.jumpTick = 0 1243 | return false 1244 | } 1245 | } 1246 | 1247 | // TODO: stop the rotate potion from rotating portals/frogs that are currently portaling because it would tear atoms in half 1248 | const COLLIDED_POTION_ROTATE = ({self, atom, world}) => { 1249 | if (atom.used) return 1250 | atom = getAtomAncestor(atom) 1251 | if (!self.isVoid && !self.isPotion) { 1252 | world.atoms = world.atoms.filter(a => a !== atom) 1253 | if (!self.isMover) turnAtom(self, 1, true, true, world, [atom]) 1254 | else self.nextturns++ 1255 | atom.used = true 1256 | //atom.nextdx = 0 1257 | self.nextdy = -5 1258 | self.jumpTick = 0 1259 | return false 1260 | } 1261 | } 1262 | 1263 | const COLLIDED_PORTAL = ({self, bself, atom, axis, baxis, world, bounds, nbounds, abounds, iveHitSomething}) => { 1264 | 1265 | // Only allow going through this portal in the correct axis 1266 | const portalIsHoriz = atom.turns % 2 === 0 1267 | const movementIsVert = axis.dname === "dy" 1268 | if (portalIsHoriz !== movementIsVert) { 1269 | return true 1270 | } 1271 | 1272 | //==================================================// 1273 | // BUMP edges of portal if I'm NOT going through it // 1274 | //==================================================// 1275 | /*if (bself.portals[axis.front] !== atom) { 1276 | 1277 | const reach = [bounds[axis.other.small], bounds[axis.other.big]] 1278 | const nreach = [nbounds[axis.other.small], nbounds[axis.other.big]] 1279 | 1280 | const sideBumps = { 1281 | small: aligns(reach, nreach, [abounds[axis.other.small]]), 1282 | big: aligns(reach, nreach, [abounds[axis.other.big]]), 1283 | } 1284 | 1285 | // Collide with the edges of the portal 1286 | if (sideBumps.small || sideBumps.big) { 1287 | self.slip = 0.95 1288 | return true 1289 | } 1290 | }*/ 1291 | 1292 | // Otherwise, go through... 1293 | 1294 | const portalIsNew = bself.portals[axis.front] === undefined 1295 | 1296 | let induceError = false 1297 | 1298 | // BEGIN to cut myself down to go into portal 1299 | if (portalIsNew) { 1300 | 1301 | if (iveHitSomething) return true 1302 | //const amountInPortal = axis.direction * (nbounds[axis.front] - abounds[axis.back]) 1303 | //bself[axis.cutFrontName] += amountInPortal 1304 | //const remainingSize = baxis.size - bself[axis.cutBackName] 1305 | /*if (bself[axis.cutFrontName] >= remainingSize) { 1306 | removeAtom(world, bself, {includingChildren: false, destroy: true}) 1307 | }*/ 1308 | 1309 | if (bself[axis.cutFrontName] < 0) { 1310 | bself[axis.cutFrontName] = 0 1311 | return false 1312 | } 1313 | 1314 | // Register (or re-register) that I am currently using this portal 1315 | bself.portals[axis.front] = atom 1316 | 1317 | if (atom.portal.enter !== undefined) { 1318 | const result = atom.portal.enter({pbounds: abounds, fnbounds: nbounds, portal: atom, froggy: bself, world, axis}) 1319 | if (result === true) induceError = true 1320 | } 1321 | 1322 | 1323 | } 1324 | 1325 | // CONTINUE to cut myself down to go into portal 1326 | else { 1327 | 1328 | const amountInPortal = axis.direction * (nbounds[axis.front] - abounds[axis.back]) 1329 | bself[axis.cutFrontName] += amountInPortal 1330 | const remainingSize = baxis.size/* - bself[axis.cutBackName]*/ 1331 | if (bself[axis.cutFrontName] >= remainingSize) { 1332 | removeAtom(world, bself, {includingChildren: false, destroy: true}) 1333 | } 1334 | 1335 | if (bself[axis.cutFrontName] < 0) { 1336 | bself[axis.cutFrontName] = 0 1337 | return false 1338 | } 1339 | 1340 | } 1341 | //bself.portals.d 1342 | 1343 | if (atom.portal.move !== undefined) atom.portal.move() 1344 | if (atom.portal.moveIn !== undefined) atom.portal.moveIn() 1345 | 1346 | if (bself.portals[axis.front] !== atom) { 1347 | return true 1348 | //throw new Error(`[TimePond] An atom tried to go through two portals in the same direction.`) 1349 | } 1350 | 1351 | if (induceError) return "induce" 1352 | return false 1353 | 1354 | } 1355 | 1356 | //==========// 1357 | // Elements // 1358 | //==========// 1359 | const ELEMENT_BOX = { 1360 | colour: Colour.Orange, 1361 | draw: DRAW_RECTANGLE, 1362 | update: UPDATE_MOVER, 1363 | grab: GRAB_DRAG, 1364 | isMover: true, 1365 | width: 40, 1366 | height: 40, 1367 | /*autoLinks: [ 1368 | { 1369 | element: {draw: DRAW_RECTANGLE, grab: GRAB_LINKEE, colour: Colour.Black, update: UPDATE_NONE, isVisual: true}, 1370 | offset: {width: () => 2, x: (x) => x}, 1371 | }, 1372 | { 1373 | element: {draw: DRAW_RECTANGLE, grab: GRAB_LINKEE, colour: Colour.Black, update: UPDATE_NONE, isVisual: true}, 1374 | offset: {width: () => 2, x: (x) => x+40-2}, 1375 | }, 1376 | { 1377 | element: {draw: DRAW_RECTANGLE, grab: GRAB_LINKEE, colour: Colour.Black, update: UPDATE_NONE, isVisual: true}, 1378 | offset: {height: () => 2, x: (x) => x}, 1379 | }, 1380 | ]*/ 1381 | } 1382 | 1383 | const ELEMENT_LEAF = { 1384 | colour: Colour.Green, 1385 | draw: DRAW_RECTANGLE, 1386 | update: UPDATE_MOVER, 1387 | grab: GRAB_DRAG, 1388 | isMover: true, 1389 | width: 20, 1390 | height: 10, 1391 | maxSpeed: 2, 1392 | } 1393 | 1394 | const ELEMENT_PLATFORM = { 1395 | colour: Colour.Silver, 1396 | draw: DRAW_RECTANGLE, 1397 | update: UPDATE_STATIC, 1398 | grab: GRAB_DRAG, 1399 | width: 150, 1400 | height: 10, 1401 | } 1402 | 1403 | const ELEMENT_VOID = { 1404 | colour: INVERT? Colour.Black : Colour.Black, 1405 | draw: DRAW_RECTANGLE, 1406 | update: UPDATE_STATIC, 1407 | grab: GRAB_STATIC, 1408 | height: 10, 1409 | width: WORLD_WIDTH, 1410 | isVoid: true, 1411 | y: 0, 1412 | } 1413 | 1414 | const ELEMENT_SPAWNER = { 1415 | update: UPDATE_STATIC, 1416 | draw: DRAW_SPAWNER, 1417 | grab: GRAB_SPAWNER, 1418 | spawn: ELEMENT_BOX, 1419 | } 1420 | 1421 | const ELEMENT_SPAWNER_PORTAL = { 1422 | ...ELEMENT_SPAWNER, 1423 | grab: GRAB_SPAWNER_PORTAL, 1424 | } 1425 | 1426 | const ELEMENT_PORTAL_COLOURS = [ 1427 | Colour.Purple, 1428 | Colour.Orange, 1429 | Colour.Green, 1430 | Colour.Pink, 1431 | Colour.Yellow, 1432 | Colour.Cyan, 1433 | Colour.Red, 1434 | ] 1435 | const ELEMENT_PORTAL = { 1436 | update: UPDATE_STATIC, 1437 | draw: DRAW_RECTANGLE, 1438 | grab: GRAB_DRAG, 1439 | height: 5, 1440 | width: 125, 1441 | colour: Colour.Purple, 1442 | isPortal: true, 1443 | isPortalActive: true, 1444 | preCollided: COLLIDED_PORTAL, 1445 | autoLinks: [ 1446 | /*{ 1447 | element: {...ELEMENT_PLATFORM, grab: GRAB_LINKEE, colour: Colour.Black}, 1448 | offset: {width: (w) => w, y: (y) => y-2, x: (x) => x, height: () => 2,}, 1449 | },*/ 1450 | { 1451 | element: {...ELEMENT_PLATFORM, grab: GRAB_LINKEE, colour: INVERT? Colour.White : Colour.Black, foo: "hi"}, 1452 | offset: {width: () => 2, x: (x) => x-2}, 1453 | }, 1454 | { 1455 | element: {...ELEMENT_PLATFORM, grab: GRAB_LINKEE, colour: INVERT? Colour.White : Colour.Black, foo: "hi"}, 1456 | offset: {width: () => 2, x: (x) => x+125}, 1457 | }, 1458 | /*{ 1459 | element: {...ELEMENT_PLATFORM, grab: GRAB_LINKEE, colour: Colour.Black}, 1460 | offset: {width: (w) => w+4, y: (y) => y+5, x: (x) => x-2, height: () => 2,}, 1461 | },*/ 1462 | ] 1463 | 1464 | } 1465 | 1466 | const ELEMENT_LILYPAD = { 1467 | update: UPDATE_STATIC, 1468 | draw: DRAW_RECTANGLE, 1469 | grab: GRAB_DRAG, 1470 | bounce: 15, 1471 | height: 8, 1472 | width: 80, 1473 | colour: Colour.Green, 1474 | } 1475 | 1476 | const ELEMENT_PORTAL_VOID = { 1477 | ...ELEMENT_PORTAL, 1478 | portal: PORTAL_VOID, 1479 | colour: Colour.White, 1480 | } 1481 | 1482 | const makePortalTargeter = () => { 1483 | let lonelyPortal = undefined 1484 | return (portal) => { 1485 | if (portal.isMenuItem) return 1486 | if (lonelyPortal === undefined) { 1487 | lonelyPortal = portal 1488 | return 1489 | } 1490 | 1491 | lonelyPortal.target = portal 1492 | portal.target = lonelyPortal 1493 | lonelyPortal = undefined 1494 | } 1495 | } 1496 | const ELEMENT_PORTAL_MOVE = { 1497 | ...ELEMENT_PORTAL, 1498 | portal: PORTAL_MOVE, 1499 | colour: Colour.Orange, 1500 | construct: makePortalTargeter(), 1501 | } 1502 | 1503 | const ELEMENT_PORTAL_DIMENSION = { 1504 | ...ELEMENT_PORTAL, 1505 | portal: PORTAL_DIMENSION, 1506 | colour: Colour.Blue, 1507 | construct: makePortalTargeter(), 1508 | } 1509 | 1510 | const ELEMENT_PORTAL_REWRITE = { 1511 | ...ELEMENT_PORTAL, 1512 | portal: PORTAL_REWRITE, 1513 | colour: Colour.Yellow, 1514 | construct: makePortalTargeter(), 1515 | } 1516 | 1517 | const ELEMENT_PORTAL_PASTLINE = { 1518 | ...ELEMENT_PORTAL, 1519 | portal: PORTAL_PASTLINE, 1520 | colour: Colour.Yellow, 1521 | construct: makePortalTargeter(), 1522 | } 1523 | 1524 | const ELEMENT_PORTAL_REWIND = { 1525 | ...ELEMENT_PORTAL, 1526 | portal: PORTAL_REWIND, 1527 | colour: Colour.Yellow, 1528 | construct: makePortalTargeter(), 1529 | } 1530 | 1531 | 1532 | const ELEMENT_PORTAL_NEXUS = { 1533 | ...ELEMENT_PORTAL, 1534 | portal: PORTAL_NEXUS, 1535 | colour: Colour.Yellow, 1536 | construct: makePortalTargeter(), 1537 | requiresFutureProjections: true, 1538 | } 1539 | 1540 | const ELEMENT_PORTAL_FUTURELINE = { 1541 | ...ELEMENT_PORTAL, 1542 | portal: PORTAL_FUTURELINE, 1543 | colour: Colour.Red, 1544 | construct: makePortalTargeter(), 1545 | requiresFutureProjections: true, 1546 | } 1547 | 1548 | const ELEMENT_PORTAL_PASTNOWLINE = { 1549 | ...ELEMENT_PORTAL, 1550 | portal: PORTAL_PASTNOWLINE, 1551 | colour: Colour.Purple, 1552 | construct: makePortalTargeter(), 1553 | requiresFutureProjections: true, 1554 | } 1555 | 1556 | const ELEMENT_PORTAL_PASTNOW = { 1557 | ...ELEMENT_PORTAL, 1558 | portal: PORTAL_PASTNOW, 1559 | colour: Colour.Cyan, 1560 | construct: makePortalTargeter(), 1561 | requiresFutureProjections: true, 1562 | } 1563 | 1564 | const ELEMENT_PORTAL_FUTURENOW = { 1565 | ...ELEMENT_PORTAL, 1566 | portal: PORTAL_FUTURENOW, 1567 | colour: Colour.Red, 1568 | construct: makePortalTargeter(), 1569 | requiresFutureProjections: true, 1570 | } 1571 | 1572 | const ELEMENT_PORTAL_BOUNCE = { 1573 | ...ELEMENT_PORTAL, 1574 | portal: PORTAL_BOUNCE, 1575 | colour: Colour.Cyan, 1576 | construct: makePortalTargeter(), 1577 | requiresFutureProjections: true, 1578 | } 1579 | 1580 | const ELEMENT_PORTAL_INVERT = { 1581 | ...ELEMENT_PORTAL, 1582 | portal: PORTAL_INVERT, 1583 | colour: Colour.Black, 1584 | construct: makePortalTargeter(), 1585 | requiresFutureProjections: true, 1586 | futureProjLength: 30, 1587 | requiresRefrogTracking: true, 1588 | } 1589 | 1590 | 1591 | const ELEMENT_PORTAL_FADE = { 1592 | ...ELEMENT_PORTAL, 1593 | portal: PORTAL_FADE, 1594 | colour: Colour.Cyan, 1595 | construct: makePortalTargeter(), 1596 | requiresFutureProjections: true, 1597 | } 1598 | 1599 | 1600 | const ELEMENT_PORTAL_FREEZE = { 1601 | ...ELEMENT_PORTAL, 1602 | portal: PORTAL_FREEZE, 1603 | colour: Colour.Cyan, 1604 | construct: makePortalTargeter(), 1605 | requiresFutureProjections: true, 1606 | } 1607 | 1608 | const ELEMENT_PORTAL_MAD = { 1609 | ...ELEMENT_PORTAL, 1610 | portal: PORTAL_MAD, 1611 | colour: Colour.Cyan, 1612 | construct: makePortalTargeter(), 1613 | requiresFutureProjections: true, 1614 | } 1615 | 1616 | const ELEMENT_PORTAL_REFROG = { 1617 | ...ELEMENT_PORTAL, 1618 | portal: PORTAL_REFROG, 1619 | colour: Colour.Black, 1620 | construct: makePortalTargeter(), 1621 | //requiresFutureProjections: true, 1622 | requiresRefrogTracking: true, 1623 | } 1624 | 1625 | const ELEMENT_POTION = { 1626 | colour: Colour.Purple, 1627 | draw: DRAW_CIRCLE, 1628 | update: UPDATE_MOVER, 1629 | height: 20, 1630 | width: 20, 1631 | isPotion: true, 1632 | //preCollide: COLLIDE_POTION_ROTATE, 1633 | } 1634 | 1635 | const ELEMENT_POTION_ROTATE = { 1636 | ...ELEMENT_POTION, 1637 | colour: Colour.Orange, 1638 | preCollide: COLLIDE_POTION_ROTATE, 1639 | preCollided: COLLIDED_POTION_ROTATE, 1640 | } 1641 | 1642 | const ELEMENT_FROG = { 1643 | //filter: "invert(58%) sepia(77%) saturate(5933%) hue-rotate(336deg) brightness(110%) contrast(108%)", 1644 | draw: DRAW_IMAGE, 1645 | update: UPDATE_MOVER_BEING, 1646 | grab: GRAB_DRAG, 1647 | source: "images/Blank@0.25x.png", 1648 | width: 354/6/* - 11 - 7*/, 1649 | height: 254/6, 1650 | isMover: true, 1651 | //cutTop: 10, 1652 | //cutBottom: 10, 1653 | //cutRight: 20, 1654 | //cutLeft: 20, 1655 | showBounds: FROGGY_BOUNDS, 1656 | } 1657 | 1658 | const ELEMENT_FROG_YELLOW = { 1659 | //filter: "invert(58%) sepia(77%) saturate(5933%) hue-rotate(336deg) brightness(110%) contrast(108%)", 1660 | draw: DRAW_IMAGE, 1661 | update: UPDATE_MOVER_BEING, 1662 | grab: GRAB_DRAG, 1663 | source: "images/Yellow/Other@0.25x.png", 1664 | width: 354/6/* - 11 - 7*/, 1665 | height: 254/6, 1666 | isMover: true, 1667 | //cutTop: 10, 1668 | //cutBottom: 10, 1669 | //cutRight: 20, 1670 | //cutLeft: 20, 1671 | showBounds: FROGGY_BOUNDS, 1672 | } 1673 | 1674 | const ELEMENT_FROG_CYAN = { 1675 | //filter: "invert(58%) sepia(77%) saturate(5933%) hue-rotate(336deg) brightness(110%) contrast(108%)", 1676 | draw: DRAW_IMAGE, 1677 | update: UPDATE_MOVER_BEING, 1678 | grab: GRAB_DRAG, 1679 | source: "images/Cyan/Other@0.25x.png", 1680 | width: 354/6/* - 11 - 7*/, 1681 | height: 254/6, 1682 | isMover: true, 1683 | //cutTop: 10, 1684 | //cutBottom: 10, 1685 | //cutRight: 20, 1686 | //cutLeft: 20, 1687 | showBounds: FROGGY_BOUNDS, 1688 | } 1689 | 1690 | const ELEMENT_FROG_CYAN_MAD = { 1691 | //filter: "invert(58%) sepia(77%) saturate(5933%) hue-rotate(336deg) brightness(110%) contrast(108%)", 1692 | draw: DRAW_IMAGE, 1693 | update: UPDATE_MOVER_BEING, 1694 | grab: GRAB_DRAG, 1695 | source: "images/Cyan/Mad@0.25x.png", 1696 | width: 100 * 0.75/* - 11 - 7*/, 1697 | height: 84 * 0.75, 1698 | isMover: true, 1699 | //cutTop: 10, 1700 | //cutBottom: 10, 1701 | //cutRight: 20, 1702 | //cutLeft: 20, 1703 | showBounds: FROGGY_BOUNDS, 1704 | } 1705 | 1706 | const ELEMENT_FROG_PURPLE = { 1707 | //filter: "invert(58%) sepia(77%) saturate(5933%) hue-rotate(336deg) brightness(110%) contrast(108%)", 1708 | draw: DRAW_IMAGE, 1709 | update: UPDATE_MOVER_BEING, 1710 | grab: GRAB_DRAG, 1711 | source: "images/Purple/Other@0.25x.png", 1712 | width: 354/6/* - 11 - 7*/, 1713 | height: 254/6, 1714 | isMover: true, 1715 | //cutTop: 10, 1716 | //cutBottom: 10, 1717 | //cutRight: 20, 1718 | //cutLeft: 20, 1719 | showBounds: FROGGY_BOUNDS, 1720 | } 1721 | 1722 | const ELEMENT_FROG_GREEN = { 1723 | //filter: "invert(58%) sepia(77%) saturate(5933%) hue-rotate(336deg) brightness(110%) contrast(108%)", 1724 | draw: DRAW_IMAGE, 1725 | update: UPDATE_MOVER_BEING, 1726 | grab: GRAB_DRAG, 1727 | source: "images/Green/Other@0.25x.png", 1728 | width: 354/6/* - 11 - 7*/, 1729 | height: 254/6, 1730 | isMover: true, 1731 | //cutTop: 10, 1732 | //cutBottom: 10, 1733 | //cutRight: 20, 1734 | //cutLeft: 20, 1735 | showBounds: FROGGY_BOUNDS, 1736 | } 1737 | 1738 | const ELEMENT_FROG_BLACK = { 1739 | //filter: "invert(58%) sepia(77%) saturate(5933%) hue-rotate(336deg) brightness(110%) contrast(108%)", 1740 | draw: DRAW_IMAGE, 1741 | update: UPDATE_MOVER_BEING, 1742 | grab: GRAB_DRAG, 1743 | source: "images/Black/Other@0.25x.png", 1744 | width: 354/6/* - 11 - 7*/, 1745 | height: 254/6, 1746 | isMover: true, 1747 | //cutTop: 10, 1748 | //cutBottom: 10, 1749 | //cutRight: 20, 1750 | //cutLeft: 20, 1751 | showBounds: FROGGY_BOUNDS, 1752 | } 1753 | 1754 | const ELEMENT_BOX_DOUBLE = { 1755 | ...ELEMENT_BOX, 1756 | update: UPDATE_MOVER, 1757 | isMover: false, 1758 | autoLinks: [ 1759 | //...ELEMENT_aaBOX.autoLinks, 1760 | { 1761 | element: {...ELEMENT_BOX, update: UPDATE_STATIC, grab: GRAB_LINKEE, onPromote: (self) => { 1762 | self.update = UPDATE_MOVER 1763 | self.grab = GRAB_DRAG 1764 | }}, 1765 | offset: { 1766 | x: (v) => v + 50, 1767 | }, 1768 | }, 1769 | { 1770 | element: {...ELEMENT_BOX, update: UPDATE_STATIC, grab: GRAB_LINKEE, onPromote: (self) => { 1771 | self.update = UPDATE_MOVER 1772 | self.grab = GRAB_DRAG 1773 | }}, 1774 | offset: { 1775 | x: (v) => v + 100, 1776 | y: (v) => v + 10, 1777 | }, 1778 | }, 1779 | ] 1780 | } -------------------------------------------------------------------------------- /source/main.js: -------------------------------------------------------------------------------- 1 | //=======// 2 | // Setup // 3 | //=======// 4 | const multiverse = makeMultiverse() 5 | const canvas = makeMultiverseCanvas(multiverse) 6 | 7 | on.load(() => { 8 | document.body.style["background-color"] = Colour.Black 9 | document.body.style["overflow-x"] = "hidden" 10 | document.body.style["margin"] = "0" 11 | document.body.appendChild(canvas) 12 | trigger("resize") 13 | }) 14 | 15 | on.keydown((e) => { 16 | if (e.key === " ") { 17 | PAUSED = !PAUSED 18 | e.preventDefault() 19 | } 20 | else if (e.key === "ArrowRight") { 21 | STEP = true 22 | PAUSED = true 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /source/mover.js: -------------------------------------------------------------------------------- 1 | 2 | const moverUpdate = (self, world) => { 3 | 4 | if (self.refrogTrackPlay === true) { 5 | if (!(self.refrogTrack instanceof Array) && self.refrogTrack instanceof Object) { 6 | self.refrogTrack = Array.from(self.refrogTrack) 7 | } 8 | const step = self.refrogTrack.pop() 9 | const prev = self.refrogTrackPrev 10 | if (prev !== undefined && step !== undefined) { 11 | const dx = step.x - prev.x 12 | const dy = step.y - prev.y 13 | self.dx = dx 14 | self.dy = dy 15 | self.nextdx = dx 16 | self.nextdy = dy 17 | } 18 | self.refrogTrackPrev = step 19 | 20 | if (self.refrogTrack.length === 0) { 21 | self.refrogTrackPlay = undefined 22 | self.refrogTrack = undefined 23 | self.refrogTrackPrev = undefined 24 | } 25 | 26 | } 27 | 28 | //self.prevBounds = {...getBounds(self)} 29 | const hit = moverMove(self, world, self.dx, self.dy) 30 | if (hit) { 31 | self.refrogTrackPlay = undefined 32 | self.refrogTrack = undefined 33 | self.refrogTrackPrev = undefined 34 | 35 | if (world.bonusAtoms !== undefined && world.bonusAtoms.includes(self)) { 36 | world.rewindPrev = undefined 37 | world.rewindAutoPlay = undefined 38 | } 39 | } 40 | } 41 | 42 | const moverMove = (self, world, dx, dy) => { 43 | 44 | // Reset some game state info 45 | self.grounded = false 46 | self.slip = undefined 47 | 48 | // Make generalised axes info 49 | // This help me write axis-independent and direction-independent code 50 | const axes = makeAxesInfo(self.x, self.y, dx, dy) 51 | 52 | // Make a list of atoms in this molecule that could POTENTIALLY hit something (ie: not a pure visual) 53 | // With each atom, store info about their potential movement 54 | const candidates = makeCandidates(self, axes) 55 | 56 | // Make each collision candidate emerge from portals if needed 57 | candidates.forEach(candidate => emergeCandidate(candidate, candidate.axes)) 58 | 59 | // Find the FIRST atom I would hit if I travel forever in each axis // 60 | const candidateBlockers = candidates.map(candidate => getBlockers(candidate, axes)) 61 | const blockersX = candidateBlockers.map(blocker => blocker.dx).flat(1) 62 | const blockersY = candidateBlockers.map(blocker => blocker.dy).flat(1) 63 | const blockers = {dx: blockersX, dy: blockersY} 64 | 65 | // Order the things I would hit from nearest to furthest away 66 | // If there are any draws, put portals last 67 | orderBlockers(blockers, axes) 68 | 69 | let induce = false 70 | //===================================================// 71 | // COLLIDE with the closest atoms to me in each axis // 72 | //===================================================// 73 | let iveReallyHitSomething = false 74 | for (const axis of axes) { 75 | 76 | let iveHitSomething = false 77 | const oldNew = axis.new //haha 'oldNew' 78 | 79 | for (const blocker of blockers[axis.dname]) { 80 | const {atom} = blocker 81 | if (atom === undefined) continue 82 | 83 | const bbounds = blocker.bounds 84 | const baxis = blocker.candidate.axes[axis.dname] 85 | const bself = blocker.candidate.atom 86 | 87 | // Update blocker bounds based on the decided Hit 88 | if (iveHitSomething) { 89 | const correction = oldNew - axis.new 90 | baxis.new -= correction 91 | blocker.candidate.nbounds[axis.front] -= correction 92 | blocker.candidate.nbounds[axis.back] -= correction 93 | } 94 | 95 | // Allow MODs by elements/atoms 96 | // TODO: Here is the problem 97 | // because one of these mod functions could ADD a child to me... 98 | // which I don't check for collisions properly 99 | // Can I check for children after all others? If there any unaccounted for children, also test them maybe? 100 | // Maybe not I dunno 101 | // WHO KNOWS 102 | // But what I do know, is that itll be easier if I create more generalized functions of all the stuff Ive got here TBH 103 | let modResult = true 104 | if (bself.preCollide !== undefined) { 105 | const result = bself.preCollide({self, bself, atom, axis, baxis, world, bounds: blocker.cbounds, nbounds: blocker.cnbounds, abounds: blocker.bounds, iveHitSomething, blockers: blockers[axis.dname]}) 106 | if (result === false) modResult = false 107 | if (result === "induce") modResult = result 108 | } 109 | if (atom.preCollided !== undefined) { 110 | const result = atom.preCollided({self, bself, atom, axis, baxis, world, bounds: blocker.cbounds, nbounds: blocker.cnbounds, abounds: blocker.bounds, iveHitSomething, blockers: blockers[axis.dname]}) 111 | if (result === false) modResult = false 112 | if (result === "induce") modResult = result 113 | } 114 | 115 | if (modResult === "induce") return 116 | 117 | // SNAP to the surface! 118 | const newOffset = axis.front === axis.small? -bself[baxis.cutSmallName] : -baxis.size + bself[baxis.cutBigName] 119 | baxis.new = bbounds[axis.back] + newOffset 120 | const snapMovement = baxis.new - baxis.old 121 | axis.new = self[axis.name] + snapMovement 122 | 123 | 124 | if (iveHitSomething === true || modResult === false || modResult === "induce") continue 125 | 126 | // Change ACCELERATIONS! 127 | // Moving right or left 128 | if (axis === axes.dx) { 129 | 130 | // 2-way BOUNCE! I think this is the only 2-way collision resolution. I think... 131 | atom.nextdx *= 0.5 132 | atom.nextdx += self.dx/2 133 | transferToParent(atom, "nextdx", atom.nextdx) 134 | self.nextdx *= -0.5 135 | self.nextdx += atom.dx/2 136 | 137 | // Hardcoded trampoline override 138 | if (atom.bounce !== undefined && atom.turns % 2 !== 0) { 139 | self.nextdx = atom.bounce * -axis.direction/2 140 | } 141 | } 142 | else if (axis === axes.dy) { 143 | 144 | // Moving down 145 | if (axis.direction === 1) { 146 | 147 | // I'm on the ground! 148 | self.nextdy = atom.dy 149 | if (self.slip !== undefined) self.nextdx *= self.slip 150 | else self.nextdx *= UPDATE_MOVER_FRICTION 151 | self.grounded = true 152 | atom.jumpTick = 0 153 | 154 | // Hardcoded trampoline override 155 | if (atom.bounce !== undefined && atom.turns % 2 === 0) { 156 | self.nextdy = -atom.bounce 157 | self.nextdx *= 1.8 158 | } 159 | 160 | } 161 | 162 | // Moving up 163 | else { 164 | 165 | // Hit my head on something... 166 | self.nextdy = 0 167 | self.jumpTick = 0 168 | 169 | } 170 | 171 | } 172 | 173 | // Update other blocker infos maybe? nah they can do it! 174 | iveHitSomething = true 175 | iveReallyHitSomething = true 176 | } 177 | 178 | } 179 | 180 | //if (induce) sdhjds 181 | 182 | // Apply natural forces 183 | self.nextdy += UPDATE_MOVER_GRAVITY 184 | self.nextdx *= UPDATE_MOVER_AIR_RESISTANCE 185 | 186 | if (self.maxSpeed !== undefined) { 187 | const speed = Math.hypot(self.nextdx, self.nextdy) 188 | if (speed > self.maxSpeed) { 189 | const ratio = speed / self.maxSpeed 190 | self.nextdy /= ratio 191 | self.nextdx /= ratio 192 | } 193 | } 194 | 195 | // Now that I've checked all potential collisions, and corrected myself... 196 | // MOVE to the new position! 197 | self.x = axes.dx.new 198 | self.y = axes.dy.new 199 | 200 | // Update all children? 201 | updateAtomLinks(self) 202 | 203 | // DODGY FIX! Make sure everything is cut correctly by portals 204 | // Comment out these to uncover lots of bugs 205 | candidates.forEach(candidate => emergeCandidate(candidate, candidate.axes, getBounds(candidate.atom))) 206 | candidates.forEach(candidate => remergeCandidate(candidate)) 207 | 208 | // Now that I've moved, I can safely rotate without messing anything else up! 209 | // ROTATE! (if there is enough room) 210 | if (self.nextturns !== 0) { 211 | turnAtom(self, self.nextturns, true, true, world) 212 | self.nextturns = 0 213 | } 214 | 215 | return iveReallyHitSomething 216 | } 217 | 218 | const makeAxesInfo = (x, y, dx, dy) => { 219 | 220 | const axes = { 221 | dx: {}, 222 | dy: {}, 223 | } 224 | 225 | // Variable Stuff 226 | axes.dx.new = x + dx 227 | axes.dx.value = dx 228 | 229 | axes.dy.new = y + dy 230 | axes.dy.value = dy 231 | 232 | // Static Stuff 233 | axes.dx.name = "x" 234 | axes.dx.dname = "dx" 235 | axes.dx.small = "left" 236 | axes.dx.big = "right" 237 | axes.dx.sizeName = "width" 238 | axes.dx.otherSizeName = "height" 239 | axes.dx.direction = dx >= 0? 1 : -1 240 | axes.dx.front = axes.dx.direction === 1? axes.dx.big : axes.dx.small 241 | axes.dx.back = axes.dx.front === axes.dx.big? axes.dx.small : axes.dx.big 242 | axes.dx.other = axes.dy 243 | axes.dx.cutFrontName = "cut" + axes.dx.front.as(Capitalised) 244 | axes.dx.cutBackName = "cut" + axes.dx.back.as(Capitalised) 245 | axes.dx.flingFrontName = axes.dx.direction === 1? "top" : "bottom" 246 | axes.dx.flingBackName = axes.dx.direction === 1? "bottom" : "top" 247 | 248 | axes.dy.name = "y" 249 | axes.dy.dname = "dy" 250 | axes.dy.small = "top" 251 | axes.dy.big = "bottom" 252 | axes.dy.sizeName = "height" 253 | axes.dy.otherSizeName = "width" 254 | axes.dy.direction = dy >= 0? 1 : -1 255 | axes.dy.front = axes.dy.direction === 1? axes.dy.big : axes.dy.small 256 | axes.dy.back = axes.dy.front === axes.dy.small? axes.dy.big : axes.dy.small 257 | axes.dy.other = axes.dx 258 | axes.dy.cutFrontName = "cut" + axes.dy.front.as(Capitalised) 259 | axes.dy.cutBackName = "cut" + axes.dy.back.as(Capitalised) 260 | axes.dy.flingFrontName = axes.dy.direction === 1? "left" : "right" 261 | axes.dy.flingBackName = axes.dy.direction === 1? "right" : "left" 262 | 263 | return axes 264 | } 265 | 266 | 267 | const makeCandidates = (self, axes) => { 268 | const atoms = getDescendentsAndMe(self) 269 | const candidates = atoms.map(atom => makeCandidate(atom, axes)) 270 | return candidates 271 | } 272 | 273 | // TODO: should this support changes in cuts over time? 274 | // it can still be manually edited though! woo 275 | const makeCandidate = (atom, axes) => { 276 | 277 | //print(atom.fling) 278 | 279 | // This is what the new atom WOULD be after moving (if it doesn't hit anything) 280 | const natom = { 281 | ...atom, 282 | x: atom.x + axes.dx.value, 283 | y: atom.y + axes.dy.value, 284 | } 285 | 286 | if (atom.fling === 1) { 287 | natom.x = atom.x - axes.dy.value 288 | natom.y = atom.y + axes.dx.value 289 | } 290 | 291 | // Current bounds and new bounds 292 | const bounds = getBounds(atom) 293 | const nbounds = getBounds(natom) 294 | 295 | // Some axis-independent info to help me write code that works for all directions/axes 296 | const caxes = { 297 | dx: {}, 298 | dy: {}, 299 | } 300 | 301 | caxes.dx.old = atom.x 302 | caxes.dx.new = natom.x 303 | caxes.dx.size = atom.width 304 | caxes.dx.cutSmallName = "cutLeft" 305 | caxes.dx.cutBigName = "cutRight" 306 | caxes.dx.direction = caxes.dx.new > caxes.dx.old? 1 : -1 307 | caxes.dx.back = caxes.dx.direction === 1? "left" : "right" 308 | caxes.dx.front = caxes.dx.direction === 1? "right" : "left" 309 | caxes.dx.cutBackName = "cut" + caxes.dx.back.as(Capitalised) 310 | 311 | caxes.dy.old = atom.y 312 | caxes.dy.new = natom.y 313 | caxes.dy.size = atom.height 314 | caxes.dy.cutSmallName = "cutTop" 315 | caxes.dy.cutBigName = "cutBottom" 316 | caxes.dy.direction = caxes.dy.new > caxes.dy.old? 1 : -1 317 | caxes.dy.back = caxes.dy.direction === 1? "top" : "bottom" 318 | caxes.dy.front = caxes.dy.direction === 1? "bottom" : "top" 319 | caxes.dy.cutBackName = "cut" + caxes.dy.back.as(Capitalised) 320 | 321 | // Put it all together... 322 | const candidate = { 323 | atom, 324 | bounds, 325 | nbounds, 326 | axes: caxes, 327 | } 328 | 329 | return candidate 330 | 331 | } 332 | 333 | // This cleans up all the merge bugs that exist lol 334 | /*const autoMergeCandidate = (candidate) => { 335 | const catom = candidate.atom 336 | for (const key in catom.portals) { 337 | const portal = catom.portals[key] 338 | if (portal === undefined) continue 339 | const cutName = "cut" + key.as(Capitalised) 340 | const back = key 341 | const front = getOppositeSideName(back) 342 | const cbounds = getBounds(catom) 343 | const pbounds = getBounds(portal) 344 | if (cbounds[back] === pbounds[front]) continue 345 | const gap = 346 | catom[cutName] -= cbounds[back] - pbounds[front] 347 | } 348 | }*/ 349 | 350 | const remergeCandidate = (candidate) => { 351 | const catom = candidate.atom 352 | for (const key in catom.portals) { 353 | const portal = catom.portals[key] 354 | if (portal === undefined) continue 355 | const cutName = "cut" + key.as(Capitalised) 356 | const back = key 357 | const front = getOppositeSideName(back) 358 | const cbounds = getBounds(catom) 359 | const pbounds = getBounds(portal) 360 | if (cbounds[back] === pbounds[front]) continue 361 | const gap = cbounds[back] - pbounds[front] 362 | 363 | const direction = (key === "bottom" || key === "right")? 1 : -1 364 | catom[cutName] += gap * direction 365 | } 366 | } 367 | 368 | const emergeCandidate = (candidate, axes, nbounds = candidate.bounds) => { 369 | 370 | const atom = candidate.atom 371 | // Go through each of my portals... 372 | for (const key in atom.portals) { 373 | const portal = atom.portals[key] 374 | if (portal === undefined) continue 375 | const pbounds = getBounds(portal) 376 | 377 | //print(atom.id, key) 378 | 379 | // I'm only interested in my BACK because I'm EMERGING! 380 | for (const axis of axes) { 381 | if (axis.back !== key) continue 382 | 383 | // What's the distance between me and the portal? 384 | const distanceToPortal = nbounds[axis.back] - pbounds[axis.front] 385 | 386 | // Reduce my cut so that there isn't a gap anymore! 387 | atom[axis.cutBackName] -= distanceToPortal * axis.direction 388 | 389 | // Fire moddable events :) 390 | if (portal.portal.move !== undefined) portal.portal.move() 391 | if (portal.portal.moveIn !== undefined) portal.portal.moveIn() 392 | 393 | // If I'm fully emerged, then I'm finished with this portal! 394 | if (atom[axis.cutBackName] <= 0) { 395 | atom.portals[axis.back] = undefined 396 | atom[axis.cutBackName] = 0 397 | if (portal.portal.exit !== undefined) portal.portal.exit({froggy: atom}) 398 | } 399 | 400 | } 401 | } 402 | 403 | // (manually) Update the candidate info (with the new cut)!!! 404 | const natom = { 405 | ...atom, 406 | x: candidate.axes.dx.new, 407 | y: candidate.axes.dy.new, 408 | } 409 | candidate.nbounds = getBounds(natom) 410 | 411 | } 412 | 413 | const getBlockers = (candidate, axes) => { 414 | 415 | const blockers = {dx: [], dy: []} 416 | 417 | for (const axis of axes) { 418 | 419 | if (candidate.atom.world === undefined) continue 420 | if (candidate.atom.world.atoms === undefined) continue 421 | 422 | const cself = candidate.atom 423 | for (const atom of cself.world.atoms) { 424 | 425 | if (atomIsDescendant(cself, atom)) continue 426 | if (atomIsDescendant(atom, cself)) continue 427 | if (atom.isVisual) continue 428 | if (atom === cself) continue 429 | 430 | // Check here for collisions with the inside edge of portals (the wrong way) 431 | if (cself.portals[axis.front] !== undefined) { 432 | const portal = cself.portals[axis.front] 433 | if (atomIsDescendant(atom, portal)) { 434 | continue 435 | } 436 | } 437 | 438 | const abounds = getBounds(atom) 439 | const bounds = candidate.bounds 440 | const nbounds = candidate.nbounds 441 | 442 | // Do I go PAST this atom? 443 | const startsInFront = bounds[axis.front]*axis.direction <= abounds[axis.back]*axis.direction 444 | const endsThrough = nbounds[axis.front]*axis.direction >= abounds[axis.back]*axis.direction 445 | if (!startsInFront || !endsThrough) continue 446 | 447 | // Do I actually BUMP into this atom? (ie: I don't go to the side of it) 448 | let bumps = true 449 | const otherAxes = axes.values().filter(a => a !== axis) 450 | for (const other of otherAxes) { 451 | const reach = [bounds[other.small], bounds[other.big]] 452 | const nreach = [nbounds[other.small], nbounds[other.big]] 453 | const areach = [abounds[other.small], abounds[other.big]] 454 | if (!aligns(reach, nreach, areach)) bumps = false 455 | } 456 | if (!bumps) continue 457 | 458 | // Work out the distance to this atom we would crash into 459 | // We don't care about it if we already found a NEARER one to crash into :) 460 | const distance = (abounds[axis.back] - bounds[axis.front]) * axis.direction 461 | blockers[axis.dname].push({atom, bounds: abounds, distance, cbounds: bounds, cnbounds: nbounds, candidate}) 462 | } 463 | } 464 | 465 | return blockers 466 | } 467 | 468 | // Orders blockers in-place 469 | const orderBlockers = (blockers, axes) => { 470 | 471 | // Order the blockers so that portals gets processed LAST 472 | // (because they don't really block, do they) 473 | for (const axis of axes) { 474 | 475 | const winningBlockers = new Map() 476 | 477 | for (const blocker of blockers[axis.dname]) { 478 | if (!winningBlockers.has(blocker.candidate)) { 479 | winningBlockers.set(blocker.candidate, [blocker]) 480 | continue 481 | } 482 | const winners = winningBlockers.get(blocker.candidate) 483 | if (winners[0].distance > blocker.distance) { 484 | winningBlockers.set(blocker.candidate, [blocker]) 485 | } 486 | else if (winners[0].distance === blocker.distance) { 487 | winners.push(blocker) 488 | } 489 | } 490 | 491 | blockers[axis.dname] = [...winningBlockers.values()].flat() 492 | 493 | blockers[axis.dname].sort((a, b) => { 494 | //if (a.distance < b.distance) return -1 495 | //if (a.distance > b.distance) return 1 496 | if (a.atom.isPortal && !b.atom.isPortal) return 1 497 | if (!a.atom.isPortal && b.atom.isPortal) return -1 498 | return 0 499 | }) 500 | 501 | } 502 | 503 | return blockers 504 | } -------------------------------------------------------------------------------- /source/multiverse.js: -------------------------------------------------------------------------------- 1 | const URL_QUERY = new URLSearchParams(window.location.search) 2 | let SPEED_MOD = URL_QUERY.has("speed")? parseFloat(URL_QUERY.get("speed")) : 1 3 | let PAUSED = URL_QUERY.has("paused")? URL_QUERY.get("paused").as(Boolean) : false 4 | let FROGGY_BOUNDS = URL_QUERY.has("bounds")? URL_QUERY.get("bounds").as(Boolean) : false 5 | let INVERT = URL_QUERY.has("invert")? URL_QUERY.get("invert").as(Boolean) : false 6 | let ONION_SKIN = URL_QUERY.has("onion")? parseInt(URL_QUERY.get("onion")) : 0 7 | let TRAIL_LENGTH = URL_QUERY.has("trail")? parseInt(URL_QUERY.get("trail")) : 0 8 | let EXPERIMENT_ID = URL_QUERY.has("experiment")? URL_QUERY.get("experiment") : "" 9 | let PORTAL_TYPE_ID = URL_QUERY.has("portal")? URL_QUERY.get("portal").as(UpperCase) : "MOVE" 10 | let BONUS_ID = URL_QUERY.has("bonus")? URL_QUERY.get("bonus") : "" 11 | let MENU_ID = URL_QUERY.has("menu")? URL_QUERY.get("menu") : undefined 12 | let NO_FROG_SPAWN_ID = URL_QUERY.has("nofrog")? URL_QUERY.get("nofrog").as(Boolean) : false 13 | let STEP = false 14 | 15 | //=======// 16 | // Setup // 17 | //=======// 18 | const makeMultiverse = () => { 19 | const multiverse = {} 20 | multiverse.worlds = [] 21 | multiverse.void = {atoms: []} 22 | const world = makeWorld() 23 | addWorld(multiverse, world) 24 | 25 | // Menu 26 | if (MENU_ID === undefined) { 27 | addMenuElement(ELEMENT_FROG, multiverse) 28 | addMenuElement(ELEMENT_BOX, multiverse) 29 | addMenuElement(ELEMENT_LEAF, multiverse) 30 | addMenuElement(ELEMENT_PLATFORM, multiverse) 31 | addMenuElement(ELEMENT_LILYPAD, multiverse) 32 | addMenuElement(ELEMENT_POTION_ROTATE, multiverse) 33 | addMenuElement(ELEMENT_PORTAL_MOVE, multiverse, ELEMENT_SPAWNER_PORTAL, "Portal") 34 | addMenuElement(ELEMENT_PORTAL_PASTNOWLINE, multiverse, ELEMENT_SPAWNER_PORTAL, "Pastnowlinal") 35 | addMenuElement(ELEMENT_PORTAL_PASTLINE, multiverse, ELEMENT_SPAWNER_PORTAL, "Pastlinal") 36 | addMenuElement(ELEMENT_PORTAL_FUTURELINE, multiverse, ELEMENT_SPAWNER_PORTAL, "Pastlinal") 37 | addMenuElement(ELEMENT_PORTAL_DIMENSION, multiverse, ELEMENT_SPAWNER_PORTAL, "Dimensial") 38 | addMenuElement(ELEMENT_PORTAL_PASTNOW, multiverse, ELEMENT_SPAWNER_PORTAL) 39 | //addMenuElement(ELEMENT_BOX_DOUBLE, multiverse) 40 | } 41 | else if (MENU_ID === "yellow") { 42 | addMenuElement(ELEMENT_FROG_YELLOW, multiverse) 43 | addMenuElement(ELEMENT_BOX, multiverse) 44 | addMenuElement(ELEMENT_PLATFORM, multiverse) 45 | addMenuElement(ELEMENT_LILYPAD, multiverse) 46 | addMenuElement(ELEMENT_PORTAL_MOVE, multiverse, ELEMENT_SPAWNER_PORTAL, "Portal") 47 | addMenuElement(ELEMENT_PORTAL_FUTURENOW, multiverse, ELEMENT_SPAWNER_PORTAL) 48 | addMenuElement(ELEMENT_PORTAL_PASTNOW, multiverse, ELEMENT_SPAWNER_PORTAL) 49 | } 50 | else if (MENU_ID === "cyan") { 51 | addMenuElement(ELEMENT_FROG_CYAN, multiverse) 52 | addMenuElement(ELEMENT_BOX, multiverse) 53 | addMenuElement(ELEMENT_PLATFORM, multiverse) 54 | addMenuElement(ELEMENT_LILYPAD, multiverse) 55 | } 56 | else if (MENU_ID === "purple") { 57 | addMenuElement(ELEMENT_FROG_PURPLE, multiverse) 58 | addMenuElement(ELEMENT_BOX, multiverse) 59 | addMenuElement(ELEMENT_PLATFORM, multiverse) 60 | addMenuElement(ELEMENT_LILYPAD, multiverse) 61 | } 62 | else if (MENU_ID === "green") { 63 | addMenuElement(ELEMENT_FROG_GREEN, multiverse) 64 | addMenuElement(ELEMENT_BOX, multiverse) 65 | addMenuElement(ELEMENT_PLATFORM, multiverse) 66 | addMenuElement(ELEMENT_LILYPAD, multiverse) 67 | addMenuElement(ELEMENT_PORTAL_MOVE, multiverse, ELEMENT_SPAWNER_PORTAL, "Portal") 68 | if (BONUS_ID === "slow") { 69 | const slowverse = makeWorld() 70 | slowverse.isSlow = true 71 | addWorld(multiverse, slowverse) 72 | } 73 | else if (BONUS_ID === "rewind") { 74 | const slowverse = makeWorld() 75 | slowverse.isReverse = true 76 | addWorld(multiverse, slowverse) 77 | } 78 | } 79 | else if (MENU_ID === "black") { 80 | addMenuElement(ELEMENT_FROG_BLACK, multiverse) 81 | addMenuElement(ELEMENT_PORTAL_REFROG, multiverse, ELEMENT_SPAWNER_PORTAL) 82 | //addMenuElement(ELEMENT_BOX, multiverse) 83 | //addMenuElement(ELEMENT_PLATFORM, multiverse) 84 | //addMenuElement(ELEMENT_LILYPAD, multiverse) 85 | } 86 | else if (MENU_ID === "rainbow") { 87 | addMenuElement(ELEMENT_FROG, multiverse) 88 | addMenuElement(ELEMENT_FROG_YELLOW, multiverse) 89 | addMenuElement(ELEMENT_FROG_CYAN, multiverse) 90 | addMenuElement(ELEMENT_FROG_PURPLE, multiverse) 91 | addMenuElement(ELEMENT_FROG_GREEN, multiverse) 92 | //addMenuElement(ELEMENT_FROG_BLACK, multiverse) 93 | } 94 | 95 | return multiverse 96 | } 97 | 98 | const menu = {atoms: [], x: 0} 99 | const addMenuElement = (element, multiverse, menuElement = ELEMENT_SPAWNER) => { 100 | menu.x += 25 101 | const atom = makeAtom({ 102 | ...element, 103 | ...menuElement, 104 | spawn: element, 105 | x: menu.x, 106 | isMenuItem: true, 107 | }) 108 | 109 | const remainingY = MENU_HEIGHT - atom.height 110 | atom.y = Math.round(remainingY/2) 111 | 112 | addAtom(multiverse.void, atom) 113 | 114 | menu.x += atom.width 115 | } 116 | 117 | const makeMultiverseCanvas = (multiverse) => { 118 | const stage = Stage.make() 119 | stage.tick = () => {} 120 | const {context, canvas} = stage 121 | canvas.style["background-color"] = INVERT? Colour.Grey : Colour.Black 122 | //canvas.style["image-rendering"] = "pixelated" 123 | on.resize(() => { 124 | const multiverseHeight = getMultiverseHeight(multiverse, canvas) 125 | const height = Math.max(multiverseHeight, document.body.clientHeight) 126 | canvas.height = height 127 | canvas.style["height"] = height 128 | canvas.width = document.body.clientWidth 129 | canvas.style["width"] = document.body.clientWidth 130 | requestAnimationFrame(() => drawMultiverse(multiverse, context)) 131 | }) 132 | 133 | setInterval(() => tickMultiverse(multiverse, context), 1000/SPEED_MOD / 60) 134 | return canvas 135 | } 136 | 137 | //=====// 138 | // API // 139 | //=====// 140 | let worldCount = 0 141 | const addWorld = (multiverse, world) => { 142 | multiverse.worlds.push(world) 143 | world.id = worldCount++ 144 | trigger("resize") 145 | } 146 | 147 | const removeWorld = (multiverse, world) => { 148 | const id = multiverse.worlds.indexOf(world) 149 | multiverse.worlds.splice(id, 1) 150 | trigger("resize") 151 | } 152 | 153 | 154 | const replaceWorld = (target, source) => { 155 | const tid = multiverse.worlds.indexOf(target) 156 | const sid = multiverse.worlds.indexOf(source) 157 | if (tid !== -1) multiverse.worlds[tid] = source 158 | if (sid !== -1) multiverse.worlds[sid] = target 159 | } 160 | 161 | //===========// 162 | // Game Loop // 163 | //===========// 164 | const tickMultiverse = (multiverse, context) => { 165 | if (STEP) STEP = false 166 | else if (PAUSED) { 167 | updateCursor(multiverse, context) 168 | drawMultiverse(multiverse, context) 169 | return 170 | } 171 | 172 | updateCursor(multiverse, context) 173 | updateMultiverse(multiverse) 174 | drawMultiverse(multiverse, context) 175 | } 176 | 177 | const hand = { 178 | atom: undefined, 179 | source: undefined, 180 | offset: {x: undefined, y: undefined}, 181 | previous: {x: undefined, y: undefined} 182 | } 183 | 184 | let handStarting = {x: 0, y: 0} 185 | const CURSOR_SQUEEZE_EFFORT = 100 186 | const updateCursor = (multiverse, context) => { 187 | const [cx, cy] = Mouse.position 188 | const [mx, my] = [cx + scrollX, cy + scrollY] 189 | const down = Mouse.Left 190 | const address = getAddress(mx, my, multiverse, context) 191 | const {world, x, y} = address 192 | 193 | // State: EMPTY HAND 194 | if (hand.atom === undefined) { 195 | 196 | if (down) { 197 | for (const atom of world.atoms) { 198 | if (pointOverlaps({x, y}, atom)) { 199 | const {grab} = atom 200 | hand.offset = {x: atom.x-x, y: atom.y-y} 201 | const grabbed = grab(atom, hand, world) 202 | hand.atom = grabbed 203 | if (grabbed !== undefined) { 204 | hand.source = world 205 | hand.previous = {x: mx, y: my} 206 | handStarting.x = mx 207 | handStarting.y = my 208 | } 209 | } 210 | } 211 | } 212 | 213 | } 214 | 215 | // State: HOLDING SOMETHING 216 | else { 217 | 218 | // Move it to the dragged position! 219 | //hand.atom.prevBounds = getBounds({...hand.atom}) 220 | moveAtom(hand.atom, x + hand.offset.x, y + hand.offset.y) 221 | if (hand.atom.flipX !== undefined) { 222 | const mdx = mx - hand.previous.x 223 | const oldX = hand.atom.x 224 | if (!hand.atom.flipX && mdx > 1) flipAtom(hand.atom) 225 | else if (hand.atom.flipX && mdx < -1) flipAtom(hand.atom) 226 | const newX = hand.atom.x 227 | hand.offset.x += newX-oldX 228 | } 229 | //REAL 230 | hand.atom.dx = (mx - hand.previous.x) 231 | hand.atom.dy = (my - hand.previous.y) 232 | 233 | //DEBUG 234 | /*hand.atom.dx = (mx - handStarting.x) / 10 235 | hand.atom.dy = (my - handStarting.y) / 10*/ 236 | 237 | hand.atom.nextdx = hand.atom.dx 238 | hand.atom.nextdy = hand.atom.dy 239 | hand.atom.jumpTick = 0 240 | 241 | /*for (const link of hand.atom.links) { 242 | link.atom.dx = (mx - hand.previous.x) 243 | link.atom.dy = (my - hand.previous.y) 244 | link.atom.nextdx = link.atom.dx 245 | link.atom.nextdy = link.atom.dy 246 | link.jumpTick = 0 247 | }*/ 248 | 249 | // Transfer the dragged atom to another world if needed 250 | if (world !== hand.source) { 251 | /*hand.source.atoms = hand.source.atoms.filter(atom => atom !== hand.atom) 252 | world.atoms.push(hand.atom)*/ 253 | moveAtomWorld(hand.atom, hand.source, world) 254 | hand.source = world 255 | } 256 | 257 | // Are we letting go of the atom? 258 | if (!down) { 259 | 260 | // Can we actually drop it here? 261 | let canDrop = true 262 | for (const atom of world.atoms) { 263 | if (atom === hand.atom) continue 264 | if (atomOverlaps(hand.atom, atom)) { 265 | canDrop = false 266 | break 267 | } 268 | } 269 | 270 | // If there's room, drop it! 271 | if (canDrop) { 272 | hand.atom.refrogTrack = undefined 273 | hand.atom = undefined 274 | } 275 | 276 | for (const world of multiverse.worlds) { 277 | saveFutureProjection(world) 278 | } 279 | 280 | } 281 | 282 | // Help keep track of hand speed 283 | hand.previous = {x: mx, y: my} 284 | 285 | } 286 | 287 | } 288 | 289 | const updateMultiverse = (multiverse) => { 290 | if (hand.atom !== undefined) return 291 | updateWorldLinks(multiverse.void) 292 | for (const world of multiverse.worlds) { 293 | updateWorld(world) 294 | } 295 | for (const world of multiverse.worlds) { 296 | prepWorld(world) 297 | } 298 | } 299 | 300 | const drawMultiverse = (multiverse, context) => { 301 | context.clearRect(0, 0, canvas.width, canvas.height) 302 | drawWorld(multiverse.void, context, false) 303 | 304 | let x = 0 305 | let y = 0 306 | 307 | context.translate(0, MENU_HEIGHT) 308 | for (let i = 0; i < multiverse.worlds.length; i++) { 309 | const world = multiverse.worlds[i] 310 | if (world.isHidden) continue 311 | drawWorld(world, context) 312 | if (i >= multiverse.worlds.length-1) break 313 | x += WORLD_WIDTH 314 | context.translate(WORLD_WIDTH, 0) 315 | if (x + WORLD_WIDTH >= context.canvas.width) { 316 | x = 0 317 | y += WORLD_HEIGHT 318 | context.resetTransform() 319 | context.translate(0, y + MENU_HEIGHT) 320 | } 321 | } 322 | context.resetTransform() 323 | } 324 | 325 | //=========// 326 | // Usefuls // 327 | //=========// 328 | const getAddress = (mx, my, multiverse) => { 329 | const column = Math.floor(mx / WORLD_WIDTH) 330 | const row = Math.floor((my-MENU_HEIGHT) / WORLD_HEIGHT) 331 | const world = getWorldFromGridPosition(column, row, multiverse, canvas) 332 | if (world === multiverse.void) return {world, x: mx, y: my} 333 | const x = mx - column*WORLD_WIDTH 334 | const y = my-MENU_HEIGHT - row*WORLD_HEIGHT 335 | return {world, x, y} 336 | } 337 | 338 | const getWorldFromGridPosition = (column, row, multiverse, canvas) => { 339 | const columns = getGridMaxWidth(canvas) 340 | if (column >= columns) return multiverse.void 341 | const world = multiverse.worlds[row*columns + column] 342 | if (world === undefined) return multiverse.void 343 | return world 344 | } 345 | 346 | const getGridMaxWidth = (canvas) => Math.floor(canvas.width / WORLD_WIDTH) 347 | 348 | const getMultiverseHeight = (multiverse, canvas) => { 349 | let x = 0 350 | let y = 0 351 | for (let i = 0; i < multiverse.worlds.length-1; i++) { 352 | x += WORLD_WIDTH 353 | if (x + WORLD_WIDTH >= canvas.width) { 354 | x = 0 355 | y += WORLD_HEIGHT 356 | } 357 | } 358 | return y + WORLD_HEIGHT + MENU_HEIGHT 359 | } -------------------------------------------------------------------------------- /source/world.js: -------------------------------------------------------------------------------- 1 | //=======// 2 | // Setup // 3 | //=======// 4 | let hasMadeFirstWorld = false 5 | const makeWorld = ({isProjection = false} = {}) => { 6 | const world = {} 7 | const top = makeAtom(ELEMENT_VOID) 8 | const bottom = makeAtom({...ELEMENT_VOID, y: WORLD_HEIGHT-ELEMENT_VOID.height}) 9 | const left = makeAtom({...ELEMENT_VOID, turns: 1}) 10 | const right = makeAtom({...ELEMENT_VOID, turns: 1, x: WORLD_WIDTH-ELEMENT_VOID.height}) 11 | world.atoms = [top, bottom, left, right] 12 | 13 | world.pastProjections = [] 14 | world.isProjection = isProjection 15 | 16 | if (hasMadeFirstWorld) return world 17 | hasMadeFirstWorld = true 18 | 19 | if (EXPERIMENT_ID === "empty") { 20 | 21 | } 22 | else if (EXPERIMENT_ID === "simpleport") { 23 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 100, y: 360})) 24 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 300, y: 150})) 25 | } 26 | else if (EXPERIMENT_ID === "fling") { 27 | addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 100, flipX: true})) 28 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 250, flipX: true})) 29 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 400, flipX: true})) 30 | /* 31 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 400, y: 360})) 32 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 100, y: 150, turns: 1})) 33 | */ 34 | 35 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 100, y: 460})) 36 | //addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 100, y: 360})) 37 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 400, y: 150, turns: 1})) 38 | 39 | } 40 | else if (EXPERIMENT_ID === "pastlinefling") { 41 | addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 100, flipX: true})) 42 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 250, flipX: true})) 43 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 400, flipX: true})) 44 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTLINE, x: 100, y: 460})) 45 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTLINE, x: 400, y: 150, turns: 1})) 46 | } 47 | else if (EXPERIMENT_ID === "futurelinefling") { 48 | addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 150, flipX: true})) 49 | addAtom(world, makeAtom({...ELEMENT_LEAF, x: 450, y: 100, flipX: true})) 50 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 250, flipX: true})) 51 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 400, flipX: true})) 52 | addAtom(world, makeAtom({...ELEMENT_PORTAL_FUTURELINE, x: 100, y: 460})) 53 | addAtom(world, makeAtom({...ELEMENT_PORTAL_FUTURELINE, x: 400, y: 150, turns: 1})) 54 | 55 | } 56 | else if (EXPERIMENT_ID === "pastnowlinefling") { 57 | addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 90, flipX: true})) 58 | //addAtom(world, makeAtom({...ELEMENT_LEAF, x: 450, y: 50, flipX: true})) 59 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 250, flipX: true})) 60 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 400, flipX: true})) 61 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTNOWLINE, x: 105, y: 460})) 62 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTNOWLINE, x: 450, y: 180, turns: 1})) 63 | 64 | } 65 | else if (EXPERIMENT_ID === "pastnowfling") { 66 | addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 90, flipX: true})) 67 | //addAtom(world, makeAtom({...ELEMENT_LEAF, x: 450, y: 50, flipX: true})) 68 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 250, flipX: true})) 69 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 400, flipX: true})) 70 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTNOW, x: 100, y: 460})) 71 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTNOW, x: 400, y: 150, turns: 1})) 72 | 73 | } 74 | else if (EXPERIMENT_ID === "pastnow") { 75 | addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 90, flipX: true})) 76 | //addAtom(world, makeAtom({...ELEMENT_LEAF, x: 450, y: 50, flipX: true})) 77 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 250, flipX: true})) 78 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 400, flipX: true})) 79 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTNOW, x: 100, y: 460})) 80 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTNOW, x: 310, y: 150, turns: 0})) 81 | 82 | } 83 | else if (EXPERIMENT_ID === "pastnowline") { 84 | addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 90, flipX: true})) 85 | //addAtom(world, makeAtom({...ELEMENT_LEAF, x: 450, y: 50, flipX: true})) 86 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 250, flipX: true})) 87 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 400, flipX: true})) 88 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTNOWLINE, x: 100, y: 460})) 89 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTNOWLINE, x: 310, y: 150, turns: 0})) 90 | 91 | } 92 | else if (EXPERIMENT_ID === "pastnowlinebounce") { 93 | addAtom(world, makeAtom({...ELEMENT_FROG, x: 75, y: 90, flipX: true})) 94 | //addAtom(world, makeAtom({...ELEMENT_LEAF, x: 450, y: 50, flipX: true})) 95 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 250, flipX: true})) 96 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 400, flipX: true})) 97 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTNOWLINE, x: 50, y: 460})) 98 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTNOWLINE, x: 220, y: 300, turns: 0})) 99 | addAtom(world, makeAtom({...ELEMENT_LILYPAD, x: 312, y: 475})) 100 | 101 | } 102 | else if (EXPERIMENT_ID === "simplepastline") { 103 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTLINE, x: 100, y: 360})) 104 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTLINE, x: 300, y: 150})) 105 | } 106 | else if (EXPERIMENT_ID === "simpledimension") { 107 | addAtom(world, makeAtom({...ELEMENT_PORTAL_DIMENSION, x: 100, y: 360})) 108 | addAtom(world, makeAtom({...ELEMENT_PORTAL_DIMENSION, x: 300, y: 150})) 109 | } 110 | else if (EXPERIMENT_ID === "simplefuture") { 111 | addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 100, flipX: true})) 112 | addAtom(world, makeAtom({...ELEMENT_PORTAL_FUTURELINE, x: 100, y: 360})) 113 | addAtom(world, makeAtom({...ELEMENT_PORTAL_FUTURELINE, x: 300, y: 150})) 114 | } 115 | else if (EXPERIMENT_ID === "freefall") { 116 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 300, y: 160})) 117 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 300, y: 300})) 118 | } 119 | else if (EXPERIMENT_ID === "headpoke") { 120 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 100, y: 360})) 121 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 300, y: 150})) 122 | addAtom(world, makeAtom({...ELEMENT_FROG, x: 175, y: 380, flipX: false})) 123 | } 124 | else if (EXPERIMENT_ID === "jumpright") { 125 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 182, y: 320, turns: 1})) 126 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 350, y: 320, turns: 1})) 127 | addAtom(world, makeAtom({...ELEMENT_FROG, x: 100, y: 380, flipX: true})) 128 | } 129 | else if (EXPERIMENT_ID === "dimensionright") { 130 | addAtom(world, makeAtom({...ELEMENT_PORTAL_DIMENSION, x: 178, y: 330, turns: 1})) 131 | addAtom(world, makeAtom({...ELEMENT_PORTAL_DIMENSION, x: 350, y: 330, turns: 1})) 132 | addAtom(world, makeAtom({...ELEMENT_FROG, x: 100, y: 380, flipX: true})) 133 | 134 | //addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 205, y: 125})) 135 | //addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 205, y: 250})) 136 | } 137 | 138 | else if (EXPERIMENT_ID === "pastlineright") { 139 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTLINE, x: 178, y: 330, turns: 1})) 140 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTLINE, x: 350, y: 330, turns: 1})) 141 | addAtom(world, makeAtom({...ELEMENT_FROG, x: 100, y: 380, flipX: true})) 142 | 143 | //addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 205, y: 125})) 144 | //addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 205, y: 250})) 145 | } 146 | else if (EXPERIMENT_ID === "dimensionfall") { 147 | addAtom(world, makeAtom({...ELEMENT_PORTAL_DIMENSION, x: 300, y: 160})) 148 | addAtom(world, makeAtom({...ELEMENT_PORTAL_DIMENSION, x: 300, y: 300})) 149 | } 150 | else if (EXPERIMENT_ID === "pastlinefall") { 151 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTLINE, x: 300, y: 160})) 152 | addAtom(world, makeAtom({...ELEMENT_PORTAL_PASTLINE, x: 300, y: 300})) 153 | } 154 | else if (EXPERIMENT_ID === "gfling") { 155 | const ptype = PORTAL_TYPE_ID 156 | const pelement = eval("ELEMENT_PORTAL_" + ptype) 157 | const felement = eval("ELEMENT_FROG_" + MENU_ID.as(UpperCase)) 158 | if (!NO_FROG_SPAWN_ID) addAtom(world, makeAtom({...felement, x: 130, y: 90, flipX: true})) 159 | 160 | addAtom(world, makeAtom({...pelement, x: 100, y: 460})) 161 | addAtom(world, makeAtom({...pelement, x: 400, y: 150, turns: 1})) 162 | } 163 | else if (EXPERIMENT_ID === "gfall") { 164 | const ptype = PORTAL_TYPE_ID 165 | const pelement = eval("ELEMENT_PORTAL_" + ptype) 166 | const felement = eval("ELEMENT_FROG_" + MENU_ID.as(UpperCase)) 167 | if (!NO_FROG_SPAWN_ID) addAtom(world, makeAtom({...felement, x: 130, y: 90, flipX: true})) 168 | addAtom(world, makeAtom({...pelement, x: 300, y: 160})) 169 | addAtom(world, makeAtom({...pelement, x: 300, y: 300})) 170 | } 171 | else if (EXPERIMENT_ID === "gright") { 172 | const ptype = PORTAL_TYPE_ID 173 | const pelement = eval("ELEMENT_PORTAL_" + ptype) 174 | const felement = eval("ELEMENT_FROG_" + MENU_ID.as(UpperCase)) 175 | 176 | addAtom(world, makeAtom({...pelement, x: 182, y: 320, turns: 1})) 177 | addAtom(world, makeAtom({...pelement, x: 350, y: 320, turns: 1})) 178 | if (!NO_FROG_SPAWN_ID)addAtom(world, makeAtom({...felement, x: 100, y: 380, flipX: true})) 179 | } 180 | else if (EXPERIMENT_ID === "gsimple") { 181 | const ptype = PORTAL_TYPE_ID 182 | const pelement = eval("ELEMENT_PORTAL_" + ptype) 183 | const felement = eval("ELEMENT_FROG_" + MENU_ID.as(UpperCase)) 184 | if (!NO_FROG_SPAWN_ID) addAtom(world, makeAtom({...felement, x: 130, y: 90, flipX: true})) 185 | //addAtom(world, makeAtom({...ELEMENT_LEAF, x: 20, y: 90, flipX: true})) 186 | addAtom(world, makeAtom({...pelement, x: 100, y: 360})) 187 | addAtom(world, makeAtom({...pelement, x: 300, y: 150})) 188 | } 189 | else if (EXPERIMENT_ID === "ggen") { 190 | const ptype = PORTAL_TYPE_ID 191 | const pelement = eval("ELEMENT_PORTAL_" + ptype) 192 | const felement = eval("ELEMENT_FROG_" + MENU_ID.as(UpperCase)) 193 | if (!NO_FROG_SPAWN_ID) addAtom(world, makeAtom({...felement, x: 20, y: 90, flipX: true})) 194 | //addAtom(world, makeAtom({...ELEMENT_LEAF, x: 20, y: 90, flipX: true})) 195 | addAtom(world, makeAtom({...ELEMENT_LILYPAD, x: 100, y: 470})) 196 | addAtom(world, makeAtom({...pelement, x: 210, y: 460})) 197 | addAtom(world, makeAtom({...pelement, x: 450, y: 150, turns: 1})) 198 | } 199 | else { 200 | // PORTAL FLING 1 201 | /*addAtom(world, makeAtom({...ELEMENT_FROG, x: 130, y: 200, flipX: false})) 202 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 135, y: 380, flipX: false})) 203 | addAtom(world, makeAtom({...ELEMENT_LILYPAD, x: 120, y: 475, flipX: false})) 204 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 100, y: 350, turns: 0})) 205 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 400, y: 150, turns: 1})) 206 | */ 207 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 260, y: 440, flipX: true})) 208 | //addAtom(world, makeAtom({...ELEMENT_FROG, x: 380, y: 440, flipX: false})) 209 | //addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 350, y: 320, turns: 3})) 210 | //addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 340, y: 240})) 211 | 212 | 213 | // CHAOTIC TEST 214 | /*addAtom(world, makeAtom({...ELEMENT_FROG, x: 120, y: 200, turns: 1})) 215 | //addAtom(world, makeAtom({...ELEMENT_FROG, y: 100, x: 180})) 216 | //addAtom(world, makeAtom({...ELEMENT_FROG, y: 50, x: 340})) 217 | //addAtom(world, makeAtom({...ELEMENT_FROG, y: 400})) 218 | 219 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 100, y: 400})) 220 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 300, y: 160})) 221 | 222 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 300, y: 240})) 223 | addAtom(world, makeAtom({...ELEMENT_PORTAL_MOVE, x: 450, y: 350, turns: 1})) 224 | 225 | addAtom(world, makeAtom({...ELEMENT_PLATFORM, x: 290, y: 170}))*/ 226 | 227 | } 228 | 229 | 230 | return world 231 | } 232 | 233 | //=========// 234 | // Usefuls // 235 | //=========// 236 | const LINKED_PROPERTIES = [ 237 | "width", 238 | "height", 239 | //"cutTop", 240 | //"cutBottom", 241 | //"cutRight", 242 | //"cutLeft", 243 | "x", 244 | "y", 245 | "dx", 246 | "dy", 247 | "nextdx", 248 | "nextdy", 249 | "turns", 250 | "nextturns", 251 | "flipX", 252 | ] 253 | 254 | // TODO!!! These should allow you to NOT bring over specific links. Add a parameter and/or special link properties to cater to this. 255 | const addAtom = (world, atom, {ignoreLinks = false} = {}) => { 256 | world.atoms.push(atom) 257 | atom.world = world // I give up. Lets use state... :( 258 | if (!ignoreLinks) { 259 | for (const link of atom.links) { 260 | addAtom(world, link.atom) 261 | } 262 | } 263 | //atom.prevBounds = getBounds(atom) 264 | updateAtomLinks(atom) 265 | } 266 | 267 | const removeAtom = (world, atom, {includingChildren = true, destroy = false} = {}) => { 268 | 269 | world.atoms = world.atoms.filter(a => a !== atom) 270 | 271 | if (destroy) { 272 | if (atom.parent !== undefined) { 273 | atom.parent.links = atom.parent.links.filter(link => link.atom !== atom) 274 | } 275 | 276 | for (const link of atom.links) { 277 | link.atom.parent = undefined 278 | if (link.atom.onPromote !== undefined) link.atom.onPromote(link.atom) 279 | } 280 | } 281 | 282 | if (includingChildren) { 283 | 284 | if (atom === undefined) throw new Error("needed error") 285 | for (const link of atom.links) { 286 | removeAtom(world, link.atom, {destroy}) 287 | } 288 | } 289 | } 290 | 291 | const moveAtomWorld = (atom, world, nworld) => { 292 | removeAtom(world, atom) 293 | addAtom(nworld, atom) 294 | } 295 | 296 | const numberAtoms = (atoms) => { 297 | let i = 0 298 | for (const atom of atoms) { 299 | atom.atom_id = i++ 300 | } 301 | } 302 | 303 | // THIS FUNCTION IS TOTALLY BROKEN 304 | // AVOID AT ALL COSTS 305 | // target (for portals) 306 | // parent 307 | // links link.atom 308 | // portals.top left right bottom 309 | const betterCloneAtoms = (atoms, new_world) => { 310 | 311 | // Number stuff relatively speaking 312 | numberAtoms(atoms) 313 | const cloned_atoms = [] 314 | for (const atom of atoms) { 315 | const cloned_atom = {atom_id: atom.atom_id} 316 | cloned_atoms[atom.atom_id] = cloned_atom 317 | } 318 | 319 | for (const atom of atoms) { 320 | const cloned_atom = cloned_atoms[atom.atom_id] 321 | for (const key in atom) { 322 | if (key === "atom_id") { 323 | continue 324 | } 325 | if (key === "world") { 326 | cloned_atom.world = new_world 327 | continue 328 | } 329 | if (key === "id") { 330 | cloned_atom.id = atom.id //JUST FOR DEBUGGING NOT FOR ANYTHING ELSE //HAHA I USED IT FOR SOMETHING ELSE WHAT A BAD IDEA 331 | continue 332 | } 333 | if (key === "target" || key === "parent") { 334 | if (atoms.includes(atom[key])) { 335 | cloned_atom[key] = cloned_atoms[atom[key].atom_id] 336 | continue 337 | } 338 | const a = atom[key] 339 | if (a === undefined) { 340 | cloned_atom[key] = undefined 341 | continue 342 | } 343 | if (!a.world.atoms.includes(a)) { 344 | cloned_atom[key] = undefined 345 | if (key === "parent" && cloned_atom.onPromote !== undefined) cloned_atom.onPromote(cloned_atom) 346 | continue 347 | } 348 | } 349 | if (key === "portals") { 350 | cloned_atom[key] = {} 351 | for (const subKey in atom[key]) { 352 | if (atoms.includes(atom[key][subKey])) { 353 | cloned_atom[key][subKey] = cloned_atoms[atom[key][subKey].atom_id] 354 | } 355 | else { 356 | cloned_atom[key][subKey] = atom[key][subKey] 357 | } 358 | } 359 | continue 360 | } 361 | if (key === "links") { 362 | cloned_atom.links = [] 363 | for (const link of atom.links) { 364 | const cloned_link = {} 365 | cloned_link.offset = link.offset 366 | cloned_link.transfer = link.transfer 367 | 368 | if (atoms[link.atom.atom_id] === link.atom) { 369 | cloned_link.atom = cloned_atoms[link.atom.atom_id] 370 | //cloned_link.atom = link.atom 371 | cloned_atom.links.push(cloned_link) 372 | } 373 | else { 374 | cloned_link.atom = link.atom 375 | const a = cloned_link.atom 376 | if (a.world.atoms.includes(a)) { 377 | cloned_atom.links.push(cloned_link) 378 | } 379 | } 380 | 381 | 382 | } 383 | continue 384 | } 385 | 386 | cloned_atom[key] = atom[key] 387 | 388 | } 389 | } 390 | 391 | 392 | 393 | return cloned_atoms 394 | } 395 | 396 | const cloneWorld = (world) => { 397 | const cloned_world = makeWorld() 398 | const cloned_atoms = betterCloneAtoms(world.atoms, cloned_world) 399 | cloned_world.atoms = cloned_atoms 400 | cloned_world.id = world.id 401 | return cloned_world 402 | } 403 | 404 | //===========// 405 | // Game Loop // 406 | //===========// 407 | const prepWorld = (world) => { 408 | for (const atom of world.atoms) { 409 | atom.dx = atom.nextdx 410 | atom.dy = atom.nextdy 411 | } 412 | } 413 | 414 | const savePastProjection = (world) => { 415 | const projection = cloneWorld(world) 416 | projection.isProjection = true 417 | world.pastProjections.unshift(projection) 418 | //world.pastProjections.length = 60 419 | } 420 | 421 | const saveFutureProjection = (world, num=30, autoCatchUp=true) => { 422 | //world.futureProjections = [] 423 | const projection = cloneWorld(world) 424 | projection.isProjection = true 425 | world.futureProjection = projection 426 | projection.realWorld = world 427 | if (world.futureProjLength !== undefined) { 428 | num = world.futureProjLength 429 | } 430 | for (let i = 0; i < num; i++) { 431 | //const p = cloneWorld(world.futureProjection) 432 | //p.isProjection = true 433 | const p = world.futureProjection 434 | p.isOnCatchup = i < num-1 435 | if (!autoCatchUp) p.isOnCatchup = true 436 | updateWorld(p) 437 | prepWorld(p) 438 | //world.futureProjection = p 439 | p.isOnCatchup = false 440 | } 441 | projection.isOnCatchup = false 442 | } 443 | 444 | const fullUpdateWorld = (world) => { 445 | updateWorld(world) 446 | prepWorld(world) 447 | 448 | } 449 | 450 | const killOrphans = (world) => { 451 | for (const atom of world.atoms) { 452 | if (atom.parent === undefined) continue 453 | if (!atom.parent.links.some(link => link.atom === atom)) { 454 | removeAtom(world, atom) 455 | print("ORPHAN") 456 | } 457 | } 458 | } 459 | 460 | const updateWorld = (world) => { 461 | 462 | for (const atom of world.atoms) { 463 | if (atom.futureProjLength !== undefined) { 464 | world.futureProjLength = atom.futureProjLength 465 | break 466 | } 467 | } 468 | 469 | if (world.overridePaused) return 470 | 471 | if (world.rewindAutoPlay !== undefined) { 472 | 473 | if (world.bonusAtoms !== undefined) { 474 | for (const ba of world.bonusAtoms) { 475 | for (const a of world.atoms) { 476 | if (a === ba) continue 477 | if (a.update === UPDATE_MOVER_BEING) { 478 | 479 | if (atomOverlaps(ba, a)) { 480 | world.rewindPrev = undefined 481 | world.rewindAutoPlay = undefined 482 | } 483 | } 484 | } 485 | } 486 | } 487 | 488 | //if (world.rewindI === undefined) world.rewindI = 0 489 | const next = world.rewindAutoPlay.shift() 490 | if (next !== undefined) { 491 | world.atoms = next.atoms 492 | world.rewindPrev = next 493 | if (world.bonusAtoms !== undefined) { 494 | world.atoms.push(...world.bonusAtoms) 495 | for (const a of world.bonusAtoms) { 496 | a.nextdx = a.dx 497 | a.nextdy = a.dy 498 | updateAtom(a, world) 499 | } 500 | } 501 | return 502 | } 503 | world.rewindAutoPlay = undefined 504 | savePastProjection(world) 505 | 506 | const id = multiverse.worlds.indexOf(world) 507 | multiverse.worlds[id] = cloneWorld(world.rewindPrev) 508 | } 509 | 510 | if (world.isSlow) { 511 | if (world.slowTick === undefined) { 512 | world.slowTick = true 513 | } 514 | if (world.slowTick) { 515 | world.slowTick = !world.slowTick 516 | return 517 | } 518 | world.slowTick = !world.slowTick 519 | } 520 | 521 | if (world.pruneTimer !== undefined) { 522 | if (world.pruneTimer <= 0) { 523 | return removeWorld(multiverse, world) 524 | } 525 | else { 526 | world.pruneTimer-- 527 | } 528 | 529 | } 530 | if (world.bounceTimer !== undefined) { 531 | world.bounceTimer-- 532 | if (world.bounceTimer < 0) { 533 | world.bounceTimer = undefined 534 | saveFutureProjection(world) 535 | } 536 | } 537 | 538 | let isFreeze = false 539 | const freezeExceptions = [] 540 | 541 | 542 | if (world.fadeReliance !== undefined) { 543 | if (world.fadeReliance > 0) { 544 | world.fadeReliance-- 545 | } 546 | else for (const atom of world.atoms) { 547 | 548 | if (atom.isFreezeFadeType) { 549 | isFreeze = true 550 | freezeExceptions.push(atom.fadeReliantOn) 551 | continue 552 | } 553 | 554 | else if (atom.isMadFadeType) { 555 | print("MAD") 556 | world.fadeReliance = undefined 557 | removeAtom(world, atom.fadeReliantOn, {destroy: false}) 558 | const mad = makeAtom({...ELEMENT_FROG_CYAN_MAD, x: atom.fadeReliantOn.x, y: atom.fadeReliantOn.y, flipX: atom.fadeReliantOn.flipX}) 559 | addAtom(world, mad) 560 | atom.fadeReliantOn = undefined 561 | continue 562 | } 563 | 564 | else if (atom.isNexusFadeType) { 565 | //atom.fadeReliantOn.world.pruneTimer.d 566 | //const reworld = cloneWorld(world) 567 | //const id = multiverse.worlds.indexOf(world) 568 | //multiverse.worlds[id] = reworld 569 | addWorld(multiverse, cloneWorld(atom.nexusWorldStart)) 570 | continue 571 | } 572 | 573 | if (atom.opacity === undefined) atom.opacity = 1 574 | //print(atom.fadeReliantOn) 575 | if (world.atoms.includes(atom.fadeReliantOn)) { 576 | atom.opacity -= 0.02 577 | if (atom.opacity <= 0) { 578 | removeAtom(world, atom) 579 | } 580 | } 581 | else { 582 | atom.opacity = 1.0 583 | } 584 | } 585 | } 586 | 587 | if (isFreeze) { 588 | for (const atom of freezeExceptions) { 589 | if (atom !== undefined) { 590 | atom.nextdx = atom.dx 591 | atom.nextdy = atom.dy 592 | } 593 | } 594 | 595 | for (const atom of freezeExceptions) { 596 | if (atom !== undefined) updateAtom(atom, world) 597 | } 598 | return 599 | } 600 | 601 | if (world.futureNowRecordings !== undefined) { 602 | 603 | if (world.futureNowReplay !== undefined) { 604 | 605 | if (world.futureNowRecordings[world.futureNowReplay] === undefined) { 606 | removeWorld(multiverse, world) 607 | world.futureNowBaseWorld.overridePaused = false 608 | return 609 | } 610 | 611 | replaceWorld(world.futureNowBaseWorld, world.futureNowRecordings[world.futureNowReplay]) 612 | world.futureNowRecordings[world.futureNowReplay].overridePaused = true 613 | world.futureNowBaseWorld = world.futureNowRecordings[world.futureNowReplay] 614 | world.futureNowReplay++ 615 | } 616 | else { 617 | const copy = cloneWorld(world) 618 | world.futureNowRecordings.push(copy) 619 | 620 | if (world.futureNowRecordings.length >= 29) { 621 | 622 | //removeWorld(multiverse, world.futureNowBaseWorld) 623 | //world.futureNowRecordings = undefined 624 | world.futureNowReplay = 0 625 | 626 | 627 | } 628 | } 629 | } 630 | 631 | if (!world.isProjection) { 632 | if (world.projection_skip > 0) { 633 | world.projection_skip-- 634 | } 635 | else { 636 | savePastProjection(world) 637 | } 638 | //print("update real", world.id) 639 | 640 | } 641 | 642 | if (!world.isProjection && world.atoms.some(a => a.requiresFutureProjections)) { 643 | if (world.futureProjection === undefined || !world.futureProjection.isProjection) { 644 | if (world.future_projection_skip > 0) { 645 | world.future_projection_skip-- 646 | //saveFutureProjection(world, 30, false) 647 | //print("skip", world) 648 | } 649 | else { 650 | 651 | saveFutureProjection(world) 652 | } 653 | } 654 | else { 655 | /*if (world.future_projection_skip > 0) { 656 | world.future_projection_skip-- 657 | } 658 | else { 659 | saveFutureProjection(world) 660 | }*/ 661 | world.futureProjection.isOnCatchup = false 662 | fullUpdateWorld(world.futureProjection) 663 | //print("update", world.id) 664 | } 665 | } 666 | 667 | for (const atom of world.atoms) { 668 | atom.nextdx = atom.dx 669 | atom.nextdy = atom.dy 670 | } 671 | 672 | for (const atom of world.atoms) { 673 | updateAtom(atom, world) 674 | } 675 | 676 | killOrphans(world) 677 | 678 | 679 | if (world.atoms.some(a => a.requiresRefrogTracking)) { 680 | for (const atom of world.atoms) { 681 | if (!(atom.refrogTrack instanceof Array) && atom.refrogTrack instanceof Object) { 682 | atom.refrogTrack = Array.from(atom.refrogTrack) 683 | } 684 | if (atom.refrogTrackPlay === true) continue 685 | if (atom.refrogTrack === undefined) { 686 | atom.refrogTrack = [] 687 | } 688 | atom.refrogTrack.push({x: atom.x, y: atom.y}) 689 | } 690 | } 691 | } 692 | 693 | const updateWorldLinks = (world) => { 694 | for (const atom of world.atoms) { 695 | updateAtomLinks(atom) 696 | } 697 | } 698 | 699 | const drawWorld = (world, context, colourBackground = true) => { 700 | 701 | if (world.isHidden) return 702 | 703 | 704 | if (colourBackground) { 705 | context.fillStyle = INVERT? Colour.Silver : Colour.Grey 706 | context.fillRect(0, 0, WORLD_WIDTH, WORLD_HEIGHT) 707 | } 708 | 709 | //if (world.pastProjections === undefined) return 710 | //const projection = world.pastProjections[30] 711 | const projection = world 712 | if (projection !== undefined) { 713 | for (const atom of projection.atoms) { 714 | drawAtom(atom, context) 715 | } 716 | } 717 | 718 | } 719 | --------------------------------------------------------------------------------