malc-game-engine 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/malc.js +2739 -0
  2. package/package.json +16 -0
package/malc.js ADDED
@@ -0,0 +1,2739 @@
1
+ /**
2
+ * MALC Game Engine Library
3
+ * Version: 1.0.0
4
+ * Description: A comprehensive 2D game engine for p5.js
5
+ */
6
+
7
+ (function(root, factory) {
8
+ if (typeof define === 'function' && define.amd) {
9
+ // AMD. Register as an anonymous module
10
+ define(['p5'], factory);
11
+ } else if (typeof module === 'object' && module.exports) {
12
+ // Node. Does not work with strict CommonJS
13
+ module.exports = factory(require('p5'));
14
+ } else {
15
+ // Browser globals (root is window)
16
+ root.MALC = factory(root.p5);
17
+ }
18
+ }(typeof self !== 'undefined' ? self : this, function(p5) {
19
+
20
+ // ========== GLOBAL ARRAYS ==========
21
+ const MALCgameObjects = [];
22
+ const MALCbuttons = [];
23
+ const MALCScene = [];
24
+ var UIPlanes = [];
25
+ var buttonsToggled = true;
26
+
27
+ // ========== GRAVITY CONSTANTS ==========
28
+ const GRAVITY = 0.5;
29
+ const TERMINAL_VELOCITY = 20;
30
+
31
+ // ========== HELPER FUNCTIONS ==========
32
+ function getTimestamp() {
33
+ return new Date().getTime();
34
+ }
35
+
36
+ function generateId(prefix) {
37
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
38
+ }
39
+
40
+ // ========== GAME OBJECT CLASS WITH GRAVITY ==========
41
+ class gameObject {
42
+ static objects = [];
43
+ static started = false;
44
+ static gravity = GRAVITY;
45
+ static terminalVelocity = TERMINAL_VELOCITY;
46
+
47
+ static render() {
48
+ MALCgameObjects.forEach(o => {
49
+ if (o.active) o.render();
50
+ });
51
+ }
52
+
53
+ static update() {
54
+ this.started = true;
55
+ this.objects = MALCgameObjects;
56
+
57
+ // Update all active objects
58
+ MALCgameObjects.forEach(o => {
59
+ if (o.active) o.update();
60
+ });
61
+ }
62
+
63
+ static initialize() {
64
+ console.log("MALC gameObjects initialized");
65
+
66
+ // Add objects to their scenes
67
+ MALCgameObjects.forEach(o => {
68
+ o.scenes.forEach(sceneId => {
69
+ let scene = Scene.getSceneById(sceneId);
70
+ if (scene && !scene.objects.includes(o)) {
71
+ scene.objects.push(o);
72
+ }
73
+ });
74
+ });
75
+ }
76
+
77
+ static getObjectByIndex(index) {
78
+ return MALCgameObjects[index] || null;
79
+ }
80
+
81
+ static getActiveObjects() {
82
+ return MALCgameObjects.filter(o => o.active);
83
+ }
84
+
85
+ static getObjectsInScene(sceneId) {
86
+ let scene = Scene.getSceneById(sceneId);
87
+ return scene ? scene.objects : [];
88
+ }
89
+
90
+ static setGlobalGravity(value) {
91
+ this.gravity = value;
92
+ }
93
+
94
+ static getGlobalGravity() {
95
+ return this.gravity;
96
+ }
97
+
98
+ constructor(x = 0, y = 0, w = 20, h = 20, ...scenes) {
99
+ this.id = generateId('gameObject');
100
+ this.x = x;
101
+ this.y = y;
102
+ this.width = w;
103
+ this.height = h;
104
+ this.rotation = 0;
105
+ this.rotationMode = "degrees";
106
+ this.velocity = [0, 0];
107
+ this.velocityMatrix = [0, 0];
108
+ this.velocityMode = "polar";
109
+ this.rvm = "unlinked";
110
+
111
+ // Gravity properties
112
+ this.gravity = {
113
+ enabled: false,
114
+ velocity: 0,
115
+ grounded: false,
116
+ groundTolerance: 1, // pixels
117
+ mass: 1,
118
+ bounce: 0, // 0 = no bounce, 1 = full bounce
119
+ friction: 0.1 // ground friction
120
+ };
121
+
122
+ this.formatting = {
123
+ outline: [false, 0, "black"],
124
+ color: "white",
125
+ };
126
+
127
+ this.collition = true;
128
+
129
+ this.scripts = [];
130
+ this.scenes = scenes.length < 1 ? ["blank"] : [...new Set(scenes)];
131
+ this.active = false;
132
+ this.visible = true;
133
+ this.parentScene = null;
134
+
135
+ this.debug = false;
136
+ this.hitbox = {
137
+ x: 0,
138
+ y: 0,
139
+ width: 0,
140
+ height: 0,
141
+ rotation: 0,
142
+ parts: null,
143
+ outline: 1,
144
+ };
145
+
146
+ this.objectInstance = MALCgameObjects.length;
147
+ this.lastGroundY = y;
148
+
149
+ MALCgameObjects.push(this);
150
+ }
151
+
152
+ // Enable gravity for this object
153
+ enableGravity() {
154
+ this.gravity.enabled = true;
155
+ return this;
156
+ }
157
+
158
+ // Disable gravity for this object
159
+ disableGravity() {
160
+ this.gravity.enabled = false;
161
+ this.gravity.velocity = 0;
162
+ return this;
163
+ }
164
+
165
+ // Set gravity parameters
166
+ setGravity(options = {}) {
167
+ if (options.enabled !== undefined) this.gravity.enabled = options.enabled;
168
+ if (options.mass !== undefined) this.gravity.mass = Math.max(0.1, options.mass);
169
+ if (options.bounce !== undefined) this.gravity.bounce = Math.min(1, Math.max(0, options.bounce));
170
+ if (options.friction !== undefined) this.gravity.friction = Math.min(1, Math.max(0, options.friction));
171
+ if (options.groundTolerance !== undefined) this.gravity.groundTolerance = options.groundTolerance;
172
+ return this;
173
+ }
174
+
175
+ // Apply gravity to this object
176
+ applyGravity() {
177
+ if (!this.gravity.enabled) return;
178
+
179
+ // Apply gravity acceleration (scaled by mass)
180
+ this.gravity.velocity += gameObject.gravity * this.gravity.mass;
181
+
182
+ // Limit to terminal velocity
183
+ this.gravity.velocity = Math.min(this.gravity.velocity, gameObject.terminalVelocity);
184
+
185
+ // Store last position before moving
186
+ let lastY = this.y;
187
+
188
+ // Apply vertical movement
189
+ this.y += this.gravity.velocity;
190
+
191
+ // Check for ground collision with other objects
192
+ this.checkGroundCollision();
193
+
194
+ // If we just landed, stop downward velocity
195
+ if (this.gravity.grounded) {
196
+ this.gravity.velocity = 0;
197
+
198
+ // Apply ground friction to horizontal movement
199
+ if (this.gravity.friction > 0 && this.velocityMode === "polar") {
200
+ this.velocity[0] *= (1 - this.gravity.friction);
201
+ if (Math.abs(this.velocity[0]) < 0.01) this.velocity[0] = 0;
202
+ }
203
+ }
204
+ }
205
+
206
+ // Check if this object is standing on another object
207
+ checkGroundCollision() {
208
+ if (!this.collition || !this.gravity.enabled) return;
209
+
210
+ let wasGrounded = this.gravity.grounded;
211
+ this.gravity.grounded = false;
212
+
213
+ // Check collision with other objects in the same scene
214
+ if (this.parentScene && this.parentScene.objects) {
215
+ this.parentScene.objects.forEach(other => {
216
+ // Skip self and inactive objects
217
+ if (other.id === this.id || !other.active) return;
218
+
219
+ // Only check if gravity is enabled on this object and we're moving downward
220
+ if (this.gravity.velocity <= 0) return;
221
+
222
+ // Check if other object is below this one
223
+ let verticalDistance = (other.y - other.height/2) - (this.y + this.height/2);
224
+
225
+ // If within ground tolerance and horizontally overlapping
226
+ if (Math.abs(verticalDistance) <= this.gravity.groundTolerance &&
227
+ this.x + this.width/2 > other.x - other.width/2 &&
228
+ this.x - this.width/2 < other.x + other.width/2) {
229
+
230
+ this.gravity.grounded = true;
231
+ this.lastGroundY = other.y - other.height/2 - this.height/2;
232
+
233
+ // Apply bounce if enabled
234
+ if (this.gravity.bounce > 0 && wasGrounded === false) {
235
+ this.gravity.velocity = -this.gravity.velocity * this.gravity.bounce;
236
+
237
+ // If bounce velocity is very small, just set to zero
238
+ if (Math.abs(this.gravity.velocity) < 0.1) {
239
+ this.gravity.velocity = 0;
240
+ }
241
+ } else {
242
+ // Position exactly on ground
243
+ this.y = this.lastGroundY;
244
+ }
245
+ }
246
+ });
247
+ }
248
+ }
249
+
250
+ update() {
251
+ if (!this.active) return;
252
+
253
+ // Apply gravity if enabled
254
+ this.applyGravity();
255
+
256
+ let vel = this.velocity[0];
257
+ let angle = this.velocity[1];
258
+
259
+ if (this.velocityMode == "polar") {
260
+ let linked = false;
261
+ if (!/unlinked/i.test(this.rvm) && /linked/i.test(this.rvm)) {
262
+ this.velocity[1] = this.rotation;
263
+ linked = true;
264
+ }
265
+
266
+ let rot = linked ?
267
+ (this.rotationMode == "degrees" ? (this.rotation) : radians(this.rotation)) :
268
+ (this.rotationMode == "degrees" ? (this.velocity[1]) : (this.velocity[1]));
269
+
270
+ if(isNaN(rot)){
271
+ vel = 0;
272
+ rot = 0;
273
+ }
274
+
275
+ let vx = vel * cos(rot);
276
+ let vy = vel * sin(rot);
277
+
278
+ this.velocityMatrix = [vx, vy];
279
+
280
+ // Don't apply horizontal movement if gravity is enabled and we're grounded with friction
281
+ if (!(this.gravity.enabled && this.gravity.grounded && this.gravity.friction > 0)) {
282
+ this.x += vx;
283
+ }
284
+
285
+ // Vertical movement is handled by gravity when enabled
286
+ if (!this.gravity.enabled) {
287
+ this.y += vy;
288
+ }
289
+ } else {
290
+ // Cartesian velocity mode
291
+ if (!(this.gravity.enabled && this.gravity.grounded && this.gravity.friction > 0)) {
292
+ this.x += vel;
293
+ }
294
+ if (!this.gravity.enabled) {
295
+ this.y += angle;
296
+ }
297
+ }
298
+
299
+ // Update parent scene reference
300
+ this.updateParentScene();
301
+
302
+ MALCgameObjects[this.objectInstance] = this;
303
+ }
304
+
305
+ render() {
306
+ if (!this.active) return;
307
+
308
+ this.scripts.forEach(s => {
309
+ if(typeof s == "function")s(this);
310
+ });
311
+
312
+ let outline = this.formatting.outline;
313
+ let hb = this.hitbox;
314
+
315
+ // Draw debug hitbox if enabled
316
+ if (this.debug) {
317
+ push();
318
+ translate(this.x, this.y);
319
+ rectMode(CENTER);
320
+ if (this.rotationMode == "degrees") angleMode(DEGREES);
321
+ rotate(this.rotation + hb.rotation);
322
+
323
+ stroke("#00FF27");
324
+ strokeWeight(hb.outline);
325
+ noFill();
326
+ rect(hb.x, hb.y, this.width + hb.width, this.height + hb.height);
327
+
328
+ // Draw gravity indicator if enabled
329
+ if (this.gravity.enabled) {
330
+ stroke(0, 255, 0, 100);
331
+ line(0, 0, 0, this.gravity.velocity * 5);
332
+ }
333
+
334
+ pop();
335
+ }
336
+
337
+ if (!this.visible) return;
338
+
339
+ push();
340
+ translate(this.x, this.y);
341
+ rectMode(CENTER);
342
+ if (this.rotationMode == "degrees") angleMode(DEGREES);
343
+ rotate(this.rotation);
344
+
345
+ if (outline[0]) {
346
+ strokeWeight(outline[1]);
347
+ stroke(outline[2]);
348
+ } else {
349
+ noStroke();
350
+ }
351
+
352
+ fill(this.formatting.color);
353
+ rect(0, 0, this.width, this.height);
354
+ pop();
355
+ }
356
+
357
+ // ========== HELPFUL METHODS ==========
358
+
359
+ belongsToScene(sceneId) {
360
+ return this.scenes.includes(sceneId);
361
+ }
362
+
363
+ addToScene(sceneId) {
364
+ if (!this.scenes.includes(sceneId)) {
365
+ this.scenes.push(sceneId);
366
+ let scene = Scene.getSceneById(sceneId);
367
+ if (scene && !scene.objects.includes(this)) {
368
+ scene.objects.push(this);
369
+ }
370
+ }
371
+ return this;
372
+ }
373
+
374
+ removeFromScene(sceneId) {
375
+ this.scenes = this.scenes.filter(id => id != sceneId);
376
+ let scene = Scene.getSceneById(sceneId);
377
+ if (scene) {
378
+ scene.objects = scene.objects.filter(obj => obj != this);
379
+ }
380
+ return this;
381
+ }
382
+
383
+ removeFromAllScenes() {
384
+ this.scenes.forEach(sceneId => {
385
+ let scene = Scene.getSceneById(sceneId);
386
+ if (scene) {
387
+ scene.objects = scene.objects.filter(obj => obj != this);
388
+ }
389
+ });
390
+ this.scenes = [];
391
+ return this;
392
+ }
393
+
394
+ updateParentScene() {
395
+ if (Scene.activeScene) {
396
+ let activeScene = Scene.getSceneById(Scene.activeScene);
397
+ if (activeScene && this.belongsToScene(activeScene.id)) {
398
+ this.parentScene = activeScene;
399
+ }
400
+ }
401
+ }
402
+
403
+ distanceTo(target) {
404
+ let dx = target.x !== undefined ? target.x - this.x : target[0] - this.x;
405
+ let dy = target.y !== undefined ? target.y - this.y : target[1] - this.y;
406
+ return Math.sqrt(dx * dx + dy * dy);
407
+ }
408
+
409
+ collidesWith(other) {
410
+ return (this.x < other.x + other.width &&
411
+ this.x + this.width > other.x &&
412
+ this.y < other.y + other.height &&
413
+ this.y + this.height > other.y);
414
+ }
415
+
416
+ directionTo(x, y, err = 0.5) {
417
+ let pa = [x - this.x, y - this.y];
418
+
419
+ let angle = (x && y) ? atan(pa[1]/pa[0]) : this.rotation;
420
+ var quads = [
421
+ pa[0] < -err && pa[1] > err,
422
+ pa[0] < -err && pa[1] < -err,
423
+ pa[0] > err && pa[1] > err,
424
+ pa[0] > err && pa[1] < -err,
425
+ (pa[0] < err && pa[0] > -err),
426
+ (pa[1] < err && pa[1] > -err),
427
+ ];
428
+
429
+ let da = (Math.atan(pa[1]/pa[0])*180)/Math.PI;
430
+
431
+ if((pa[1] > -err && pa[1] < err) && (pa[0] > -err && pa[0] < err)){
432
+ angle = NaN;
433
+ } else if(quads[0]){
434
+ angle = da+180;
435
+ } else if(quads[1]){
436
+ angle = da+180;
437
+ } else if(quads[2]){
438
+ angle = da;
439
+ } else if(quads[3]){
440
+ angle = da;
441
+ } else if(quads[4]){
442
+ if(pa[1] < -err) {
443
+ angle = -90;
444
+ } else if(pa[1] > err) {
445
+ angle = 90;
446
+ }
447
+ } else if(quads[5]){
448
+ if(pa[0] < -err) {
449
+ angle = 180;
450
+ } else if(pa[0] > err) {
451
+ angle = 0;
452
+ }
453
+ }
454
+
455
+ return angle;
456
+ }
457
+
458
+ pointTo(target) {
459
+ this.rotation = this.directionTo(target);
460
+ return this;
461
+ }
462
+
463
+ setPosition(x, y) {
464
+ this.x = x;
465
+ this.y = y;
466
+ return this;
467
+ }
468
+
469
+ move(dx, dy) {
470
+ this.x += dx;
471
+ this.y += dy;
472
+ return this;
473
+ }
474
+
475
+ setVelocity(speed, x, y, err = 0.5){
476
+ let angle = this.directionTo(x,y,err);
477
+
478
+ if(isNaN(angle)){
479
+ angle = 0;
480
+ speed = 0;
481
+ }
482
+
483
+ this.velocity = [speed, angle];
484
+ return this.velocity;
485
+ }
486
+
487
+ destroy() {
488
+ this.removeFromAllScenes();
489
+ let index = MALCgameObjects.indexOf(this);
490
+ if (index > -1) {
491
+ MALCgameObjects.splice(index, 1);
492
+ }
493
+ }
494
+
495
+ clone() {
496
+ let clone = new gameObject(this.x, this.y, this.width, this.height, ...this.scenes);
497
+ clone.rotation = this.rotation;
498
+ clone.rotationMode = this.rotationMode;
499
+ clone.velocity = [...this.velocity];
500
+ clone.velocityMode = this.velocityMode;
501
+ clone.rvm = this.rvm;
502
+ clone.formatting = JSON.parse(JSON.stringify(this.formatting));
503
+ clone.gravity = JSON.parse(JSON.stringify(this.gravity));
504
+ clone.debug = this.debug;
505
+ clone.hitbox = JSON.parse(JSON.stringify(this.hitbox));
506
+ return clone;
507
+ }
508
+
509
+ screenToWorld(screenX, screenY) {
510
+ if (window.camera && typeof camera.screenToWorld == "function") {
511
+ return camera.screenToWorld(screenX, screenY);
512
+ }
513
+ return { x: screenX, y: screenY };
514
+ }
515
+
516
+ isOnScreen() {
517
+ if (!window.camera) return true;
518
+
519
+ let cameraPos = camera.getOrientation();
520
+ let screenRight = cameraPos[0] + camera.width;
521
+ let screenBottom = cameraPos[1] + camera.height;
522
+
523
+ return (this.x + this.width/2 > cameraPos[0] &&
524
+ this.x - this.width/2 < screenRight &&
525
+ this.y + this.height/2 > cameraPos[1] &&
526
+ this.y - this.height/2 < screenBottom);
527
+ }
528
+ }
529
+
530
+ // ========== BUTTON CLASS ==========
531
+ class Button extends gameObject {
532
+ static buttons = [];
533
+
534
+ static updateButton() {
535
+ this.startedButtons = true;
536
+ this.buttons = MALCbuttons;
537
+
538
+ this.buttons.forEach(b => {
539
+ if (!b.active) return;
540
+
541
+ b.isHovered = b.events.hover();
542
+
543
+ if (MALCbuttons.every(b => !b.events.hover())) {
544
+ cursor();
545
+ } else if (b.isHovered) {
546
+ cursor(b.cursor);
547
+ }
548
+ });
549
+ }
550
+
551
+ static getButtonByIndex(index) {
552
+ return MALCbuttons[index] || null;
553
+ }
554
+
555
+ static getHoveredButton() {
556
+ return MALCbuttons.find(b => b.active && b.events.hover());
557
+ }
558
+
559
+ static getPressedButton() {
560
+ return MALCbuttons.find(b => b.active && b.events.pressed());
561
+ }
562
+
563
+ constructor(x = 0, y = 0, w = 20, h = 20, displayText = "Button", ...scenes) {
564
+ super(x, y, w, h, ...scenes);
565
+
566
+ this.formatting.button = {
567
+ hover: 220,
568
+ clicked: 190,
569
+ text: {
570
+ color: 0,
571
+ size: 14,
572
+ style:"normal",
573
+ display: displayText,
574
+ },
575
+ colors: {
576
+ normal: 255,
577
+ hover: 220,
578
+ pressed: 190,
579
+ disabled: 150
580
+ }
581
+ };
582
+
583
+ this.cursor = "pointer";
584
+ this.isHovered = false;
585
+ this.isPressed = false;
586
+ this.isDisabled = false;
587
+ this.clickCooldown = 100;
588
+ this.lastClickTime = 0;
589
+ this.cooldownActive = false;
590
+
591
+ this.events = {
592
+ hover: (err = 0) => {
593
+ return !this.isDisabled && (
594
+ mouse.x < this.x + this.width / 2 + err &&
595
+ mouse.x > this.x - (this.width / 2 + err) &&
596
+ mouse.y < this.y + this.height / 2 + err &&
597
+ mouse.y > this.y - (this.height / 2 + err)
598
+ );
599
+ },
600
+ pressed: () => {
601
+ return this.events.hover() && mouse.down;
602
+ },
603
+ clicked: () => {
604
+ let wasPressed = this.wasPressed;
605
+ let isHovering = this.events.hover();
606
+ let mouseReleased = !mouse.down && wasPressed;
607
+
608
+ this.wasPressed = mouse.down && isHovering;
609
+
610
+ return mouseReleased && isHovering;
611
+ }
612
+ };
613
+
614
+ this.wasPressed = false;
615
+ this.onClick = null;
616
+
617
+ this.buttonIndex = MALCbuttons.length;
618
+ MALCbuttons.push(this);
619
+ }
620
+
621
+ update(boolean) {
622
+ super.update(boolean);
623
+
624
+ if (this.cooldownActive) {
625
+ let currentTime = Date.now();
626
+ if (currentTime - this.lastClickTime >= this.clickCooldown) {
627
+ this.cooldownActive = false;
628
+ }
629
+ }
630
+
631
+ this.isHovered = this.events.hover();
632
+ this.isPressed = this.events.pressed();
633
+
634
+ if (this.events.clicked() && this.onClick && !this.isDisabled && !this.cooldownActive) {
635
+ this.onClick(this);
636
+ this.lastClickTime = Date.now();
637
+ this.cooldownActive = true;
638
+ }
639
+
640
+ MALCbuttons[this.buttonIndex] = this;
641
+ }
642
+
643
+ render() {
644
+ if (!this.active) return;
645
+
646
+ let btnFormat = this.formatting.button;
647
+ let buttonColor;
648
+
649
+ if (this.isDisabled) {
650
+ buttonColor = btnFormat.colors.disabled;
651
+ } else if (this.isPressed) {
652
+ buttonColor = btnFormat.colors.pressed;
653
+ } else if (this.isHovered) {
654
+ buttonColor = btnFormat.colors.hover;
655
+ } else {
656
+ buttonColor = btnFormat.colors.normal;
657
+ }
658
+
659
+ let originalColor = this.formatting.color;
660
+ this.formatting.color = buttonColor;
661
+
662
+ super.render();
663
+
664
+ if(!this.visible) return;
665
+
666
+ push();
667
+ translate(this.x, this.y);
668
+ if (this.rotationMode == "degrees") angleMode(DEGREES);
669
+ rotate(this.rotation);
670
+
671
+ textStyle(btnFormat.text.style);
672
+ textSize(btnFormat.text.size);
673
+ fill(btnFormat.text.color);
674
+ coloredText(btnFormat.text.display, 0, 0, CENTER, CENTER);
675
+ pop();
676
+
677
+ this.formatting.color = originalColor;
678
+ }
679
+
680
+ // ========== BUTTON-SPECIFIC HELPER METHODS ==========
681
+
682
+ setText(text) {
683
+ this.formatting.button.text.display = text;
684
+ return this;
685
+ }
686
+
687
+ getRGBFromColor(colorInput) {
688
+ let c = color(colorInput);
689
+ return [red(c), green(c), blue(c)];
690
+ }
691
+
692
+ getBrightness(colorInput) {
693
+ let rgb = this.getRGBFromColor(colorInput);
694
+ return 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2];
695
+ }
696
+
697
+ scaleColor(baseColor, scaleFactor) {
698
+ let rgb = this.getRGBFromColor(baseColor);
699
+ let scaledRGB = rgb.map(val => constrain(val * scaleFactor, 0, 255));
700
+ return color(scaledRGB);
701
+ }
702
+
703
+ setColors(normal, hover = null, pressed = null, disabled = null) {
704
+ if (hover === null && pressed === null) {
705
+ let originalNormal = this.formatting.button.colors.normal;
706
+ let originalHover = this.formatting.button.colors.hover;
707
+ let originalPressed = this.formatting.button.colors.pressed;
708
+
709
+ let normalBrightness = this.getBrightness(originalNormal);
710
+ let hoverBrightness = this.getBrightness(originalHover);
711
+ let pressedBrightness = this.getBrightness(originalPressed);
712
+
713
+ let hoverScale = normalBrightness !== 0 ? hoverBrightness / normalBrightness : 0.86;
714
+ let pressedScale = normalBrightness !== 0 ? pressedBrightness / normalBrightness : 0.75;
715
+
716
+ hover = this.scaleColor(normal, hoverScale);
717
+ pressed = this.scaleColor(normal, pressedScale);
718
+ } else {
719
+ hover = color(hover);
720
+ pressed = color(pressed);
721
+ }
722
+
723
+ let normalColor = color(normal);
724
+ let disabledColor = disabled !== null ? color(disabled) : null;
725
+
726
+ this.formatting.button.colors.normal = normalColor;
727
+ this.formatting.button.colors.hover = hover;
728
+ this.formatting.button.colors.pressed = pressed;
729
+ if (disabledColor !== null) {
730
+ this.formatting.button.colors.disabled = disabledColor;
731
+ }
732
+
733
+ return this;
734
+ }
735
+
736
+ textStyle(color, size) {
737
+ if (color !== undefined) this.formatting.button.text.color = color;
738
+ if (size !== undefined) this.formatting.button.text.size = size;
739
+ return this;
740
+ }
741
+
742
+ Disable(disabled = true) {
743
+ this.isDisabled = disabled;
744
+ return this;
745
+ }
746
+
747
+ click(call) {
748
+ if(typeof call == "function" && this.events.pressed()){
749
+ call();
750
+ }
751
+ return this.events.clicked();
752
+ }
753
+ }
754
+
755
+ // ========== SCENE CLASS ==========
756
+ class Scene {
757
+ static scenes = [];
758
+ static activeScene = "blank";
759
+ static started = false;
760
+ static sceneHistory = [];
761
+ static historyLimit = 10;
762
+
763
+ static update() {
764
+ this.started = true;
765
+ this.scenes = MALCScene;
766
+
767
+ let activeSceneFound = false;
768
+
769
+ this.scenes.forEach(S => {
770
+ if (S.id == this.activeScene) {
771
+ activeSceneFound = true;
772
+
773
+ this.scenes.forEach(s => {
774
+ if (s != S) {
775
+ s._active = false;
776
+ s.active = false;
777
+
778
+ s.objects.forEach(o => {
779
+ if (o && typeof o.active !== 'undefined') o.active = false;
780
+ });
781
+ }
782
+ });
783
+
784
+ S.active = true;
785
+
786
+ if (!S._active) {
787
+ S.activated = MALC.time.getTime();
788
+ if (typeof S.onActivate == 'function') S.onActivate();
789
+ }
790
+
791
+ S._active = true;
792
+ S.timeActive = MALC.time.getTime() - S.activated;
793
+
794
+ S.objects.forEach(o => {
795
+ if (o && typeof o.active !== 'undefined') o.active = true;
796
+ });
797
+
798
+ push();
799
+ if (window.camera && typeof camera.render == 'function') {
800
+ camera.render();
801
+ }
802
+
803
+ S.render();
804
+
805
+ pop();
806
+ }
807
+ });
808
+
809
+ if (!activeSceneFound && this.activeScene != "blank") {
810
+ console.warn(`Scene "${this.activeScene}" not found, switching to blank`);
811
+ this.activeScene = "blank";
812
+ }
813
+ }
814
+
815
+ static getSceneById(id) {
816
+ return MALCScene.find(scene => scene.id == id) || null;
817
+ }
818
+
819
+ static getActiveScene() {
820
+ return this.getSceneById(this.activeScene);
821
+ }
822
+
823
+ static switchToScene(id, addToHistory = true) {
824
+ let scene = this.getSceneById(id);
825
+ if (scene) {
826
+ if (addToHistory && this.activeScene) {
827
+ this.sceneHistory.push(this.activeScene);
828
+ if (this.sceneHistory.length > this.historyLimit) {
829
+ this.sceneHistory.shift();
830
+ }
831
+ }
832
+ this.activeScene = id;
833
+ } else {
834
+ console.error(`Cannot switch to scene "${id}" - not found`);
835
+ }
836
+ }
837
+
838
+ static goBack() {
839
+ if (this.sceneHistory.length > 0) {
840
+ let previousScene = this.sceneHistory.pop();
841
+ this.switchToScene(previousScene, false);
842
+ return true;
843
+ }
844
+ return false;
845
+ }
846
+
847
+ static getAllScenes() {
848
+ return [...MALCScene];
849
+ }
850
+
851
+ static getScenesWithObject(object) {
852
+ return MALCScene.filter(scene => scene.objects.includes(object));
853
+ }
854
+
855
+ static getScenesByTag(tag) {
856
+ return MALCScene.filter(scene => scene.hasTag(tag));
857
+ }
858
+
859
+ constructor(id, backgroundColor, ...scripts) {
860
+ MALCScene.forEach(s => {
861
+ if (s.id == id || typeof id != "string") {
862
+ throw new Error(`Scenes must have unique IDs and be strings. Duplicate/Invalid ID: "${id}"`);
863
+ }
864
+ });
865
+
866
+ this.id = id;
867
+ this.backColor = backgroundColor;
868
+ this.scripts = scripts;
869
+
870
+ this.objects = [];
871
+ this.uiPlanes = [];
872
+
873
+ this.active = false;
874
+ this._active = false;
875
+ this.activated = 0;
876
+ this.timeActive = -1;
877
+
878
+ this.tags = [];
879
+ this.paused = false;
880
+ this.transition = null;
881
+ this.onActivateCallbacks = [];
882
+ this.onDeactivateCallbacks = [];
883
+ this.onUpdateCallbacks = [];
884
+
885
+ this.sceneInstance = MALCScene.length;
886
+ MALCScene.push(this);
887
+ }
888
+
889
+ render() {
890
+ if (this.paused) return;
891
+
892
+ if (this.transition) {
893
+ this.applyTransition();
894
+ }
895
+
896
+ background(this.backColor);
897
+
898
+ this.scripts.forEach(exe => {
899
+ if (typeof exe == "function") exe(this);
900
+ });
901
+
902
+ this.onUpdateCallbacks.forEach(cb => {
903
+ if (typeof cb == "function") cb(this);
904
+ });
905
+
906
+ this.objects.forEach(o => {
907
+ if (o && typeof o.update == "function") {
908
+ o.update(true);
909
+ }
910
+ if (o && typeof o.render == "function") {
911
+ o.render();
912
+ }
913
+ });
914
+
915
+ if (typeof UIPlanes !== 'undefined' && UIPlanes.length > 0) {
916
+ UIPlanes.forEach(ui => {
917
+ if (ui && typeof ui.belongsToScene == "function" && ui.belongsToScene(this.id)) {
918
+ ui.render();
919
+ }
920
+ });
921
+ }
922
+
923
+ this.uiPlanes.forEach(ui => {
924
+ if (ui && typeof ui.render == "function") {
925
+ ui.render();
926
+ }
927
+ });
928
+
929
+ MALCScene[this.sceneInstance] = this;
930
+ }
931
+
932
+ applyTransition() {
933
+ if (!this.transition || !this.transition.active) return;
934
+
935
+ this.transition.progress += 1/60;
936
+
937
+ if (this.transition.progress >= this.transition.duration) {
938
+ this.transition.active = false;
939
+ this.transition = null;
940
+ return;
941
+ }
942
+
943
+ let t = this.transition.progress / this.transition.duration;
944
+
945
+ push();
946
+ switch(this.transition.type) {
947
+ case "fade":
948
+ fill(0, 255 * (1 - t));
949
+ rect(0, 0, width, height);
950
+ break;
951
+ case "slide":
952
+ translate(width * (1 - t), 0);
953
+ break;
954
+ }
955
+ pop();
956
+ }
957
+
958
+ addObject(object) {
959
+ if (object && !this.objects.includes(object)) {
960
+ this.objects.push(object);
961
+ if (typeof object.addToScene == "function") {
962
+ object.addToScene(this.id);
963
+ }
964
+ }
965
+ return this;
966
+ }
967
+
968
+ addObjects(objects) {
969
+ objects.forEach(obj => this.addObject(obj));
970
+ return this;
971
+ }
972
+
973
+ removeObject(object) {
974
+ this.objects = this.objects.filter(obj => obj != object);
975
+ if (object && typeof object.removeFromScene == "function") {
976
+ object.removeFromScene(this.id);
977
+ }
978
+ return this;
979
+ }
980
+
981
+ clearObjects() {
982
+ this.objects.forEach(obj => {
983
+ if (obj && typeof obj.removeFromScene == "function") {
984
+ obj.removeFromScene(this.id);
985
+ }
986
+ });
987
+ this.objects = [];
988
+ return this;
989
+ }
990
+
991
+ getObjects(filter) {
992
+ if (typeof filter == "function") {
993
+ return this.objects.filter(filter);
994
+ } else if (filter == "button") {
995
+ return this.objects.filter(obj => obj instanceof Button);
996
+ } else if (filter == "gameObject") {
997
+ return this.objects.filter(obj => obj instanceof gameObject);
998
+ }
999
+ return this.objects;
1000
+ }
1001
+
1002
+ getObjectById(id) {
1003
+ return this.objects.find(obj => obj && obj.id == id);
1004
+ }
1005
+
1006
+ addUIPlane(uiPlane) {
1007
+ if (uiPlane && !this.uiPlanes.includes(uiPlane)) {
1008
+ this.uiPlanes.push(uiPlane);
1009
+ if (typeof uiPlane.addToScene == "function") {
1010
+ uiPlane.addToScene(this.id);
1011
+ }
1012
+ }
1013
+ return this;
1014
+ }
1015
+
1016
+ removeUIPlane(uiPlane) {
1017
+ this.uiPlanes = this.uiPlanes.filter(ui => ui != uiPlane);
1018
+ if (uiPlane && typeof uiPlane.removeFromScene == "function") {
1019
+ uiPlane.removeFromScene(this.id);
1020
+ }
1021
+ return this;
1022
+ }
1023
+
1024
+ clearUIPlanes() {
1025
+ this.uiPlanes.forEach(ui => {
1026
+ if (ui && typeof ui.removeFromScene == "function") {
1027
+ ui.removeFromScene(this.id);
1028
+ }
1029
+ });
1030
+ this.uiPlanes = [];
1031
+ return this;
1032
+ }
1033
+
1034
+ addScript(script) {
1035
+ if (typeof script == "function" && !this.scripts.includes(script)) {
1036
+ this.scripts.push(script);
1037
+ }
1038
+ return this;
1039
+ }
1040
+
1041
+ removeScript(script) {
1042
+ this.scripts = this.scripts.filter(s => s != script);
1043
+ return this;
1044
+ }
1045
+
1046
+ clearScripts() {
1047
+ this.scripts = [];
1048
+ return this;
1049
+ }
1050
+
1051
+ onActivate(callback) {
1052
+ if (typeof callback == "function") {
1053
+ this.onActivateCallbacks.push(callback);
1054
+ }
1055
+ return this;
1056
+ }
1057
+
1058
+ onDeactivate(callback) {
1059
+ if (typeof callback == "function") {
1060
+ this.onDeactivateCallbacks.push(callback);
1061
+ }
1062
+ return this;
1063
+ }
1064
+
1065
+ onUpdate(callback) {
1066
+ if (typeof callback == "function") {
1067
+ this.onUpdateCallbacks.push(callback);
1068
+ }
1069
+ return this;
1070
+ }
1071
+
1072
+ pause() {
1073
+ this.paused = true;
1074
+ return this;
1075
+ }
1076
+
1077
+ resume() {
1078
+ this.paused = false;
1079
+ return this;
1080
+ }
1081
+
1082
+ setTransition(type, duration = 1.0) {
1083
+ this.transition = {
1084
+ type: type,
1085
+ duration: duration,
1086
+ progress: 0,
1087
+ active: true
1088
+ };
1089
+ return this;
1090
+ }
1091
+
1092
+ addTag(tag) {
1093
+ if (!this.tags.includes(tag)) {
1094
+ this.tags.push(tag);
1095
+ }
1096
+ return this;
1097
+ }
1098
+
1099
+ removeTag(tag) {
1100
+ this.tags = this.tags.filter(t => t != tag);
1101
+ return this;
1102
+ }
1103
+
1104
+ hasTag(tag) {
1105
+ return this.tags.includes(tag);
1106
+ }
1107
+
1108
+ reset() {
1109
+ this.clearObjects();
1110
+ this.clearUIPlanes();
1111
+ this.clearScripts();
1112
+ this.onActivateCallbacks = [];
1113
+ this.onDeactivateCallbacks = [];
1114
+ this.onUpdateCallbacks = [];
1115
+ this.tags = [];
1116
+ this.paused = false;
1117
+ this.transition = null;
1118
+ this.timeActive = 0;
1119
+ return this;
1120
+ }
1121
+
1122
+ destroy() {
1123
+ let index = MALCScene.indexOf(this);
1124
+ if (index > -1) {
1125
+ MALCScene.splice(index, 1);
1126
+ }
1127
+
1128
+ this.clearObjects();
1129
+ this.clearUIPlanes();
1130
+
1131
+ if (Scene.activeScene == this.id) {
1132
+ Scene.activeScene = "blank";
1133
+ }
1134
+ }
1135
+
1136
+ clone(newId) {
1137
+ let clone = new Scene(newId || this.id + "_copy", this.backColor, ...this.scripts);
1138
+ clone.objects = [...this.objects];
1139
+ clone.uiPlanes = [...this.uiPlanes];
1140
+ clone.tags = [...this.tags];
1141
+ return clone;
1142
+ }
1143
+
1144
+ getInfo() {
1145
+ return {
1146
+ id: this.id,
1147
+ active: this.active,
1148
+ timeActive: this.timeActive,
1149
+ objectCount: this.objects.length,
1150
+ uiPlaneCount: this.uiPlanes.length,
1151
+ scriptCount: this.scripts.length,
1152
+ tags: this.tags,
1153
+ paused: this.paused
1154
+ };
1155
+ }
1156
+ }
1157
+
1158
+ // ========== UIPLANE CLASS ==========
1159
+ class UIPlane {
1160
+ constructor(executable, formatting = [], ...scenes) {
1161
+ this.executable = executable;
1162
+
1163
+ this.formatting = {
1164
+ txt: {
1165
+ title: 24,
1166
+ heading: 22,
1167
+ subtitle: 20,
1168
+ base: 16,
1169
+ color: 255,
1170
+ },
1171
+ objectScale: 1,
1172
+ orientation: ["camera", 0, 0],
1173
+ };
1174
+
1175
+ this._formatting = {
1176
+ txt: {
1177
+ title: 24,
1178
+ heading: 22,
1179
+ subtitle: 20,
1180
+ base: 16,
1181
+ color: 255,
1182
+ },
1183
+ objectScale: 1,
1184
+ orientation: ["camera", 0, 0],
1185
+ };
1186
+
1187
+ if (formatting.length > 0) {
1188
+ this.applyFormatting(formatting);
1189
+ }
1190
+
1191
+ this.scenes = (scenes.length < 1) ? ["blank"] : [...new Set(scenes)];
1192
+ this.active = false;
1193
+ this.id = generateId('uiPlane');
1194
+
1195
+ this.uiIndex = UIPlanes.length;
1196
+
1197
+ this.scenes.forEach(s => {
1198
+ let scene = Scene.getSceneById(s);
1199
+ if (scene) scene.addUIPlane(this);
1200
+ });
1201
+
1202
+ UIPlanes.push(this);
1203
+ }
1204
+
1205
+ applyFormatting(formattingArray) {
1206
+ if (!Array.isArray(formattingArray)) {
1207
+ console.log(new Error("Formatting is invalid format. Input must be an array!"));
1208
+ return;
1209
+ }
1210
+
1211
+ formattingArray.forEach(f => {
1212
+ if (typeof f == "string") {
1213
+ this.parseStringFormat(f);
1214
+ } else if (typeof f == "number") {
1215
+ this.parseNumberFormat(f, formattingArray);
1216
+ }
1217
+ });
1218
+ }
1219
+
1220
+ parseStringFormat(formatString) {
1221
+ let parts = formatString.split("!");
1222
+ if (parts.length < 2) return;
1223
+
1224
+ let leftParts = parts[0].split(":");
1225
+ let valueType = leftParts[0].toLowerCase();
1226
+ let valueKey = leftParts.length > 1 ? leftParts[1].toLowerCase() : null;
1227
+
1228
+ let rightParts = parts[1].split("|");
1229
+ let operator = rightParts[0].toLowerCase();
1230
+ let value = rightParts.length > 1 ? rightParts[1] : null;
1231
+
1232
+ if (valueType === "txt") {
1233
+ if (valueKey && this.formatting.txt.hasOwnProperty(valueKey)) {
1234
+ let numValue = Number(value);
1235
+ if (isNaN(numValue)) return;
1236
+
1237
+ if (operator === "set") {
1238
+ this.formatting.txt[valueKey] = numValue;
1239
+ } else if (operator === "add") {
1240
+ this.formatting.txt[valueKey] += numValue;
1241
+ } else if (operator === "scale") {
1242
+ this.formatting.txt[valueKey] *= numValue;
1243
+ } else if (operator === "default") {
1244
+ this.formatting.txt[valueKey] = this._formatting.txt[valueKey];
1245
+ }
1246
+ } else if (valueKey === "all") {
1247
+ let numValue = Number(value);
1248
+ if (isNaN(numValue)) return;
1249
+
1250
+ Object.keys(this.formatting.txt).forEach(key => {
1251
+ if (operator === "set") {
1252
+ this.formatting.txt[key] = numValue;
1253
+ } else if (operator === "add") {
1254
+ this.formatting.txt[key] += numValue;
1255
+ } else if (operator === "scale") {
1256
+ this.formatting.txt[key] *= numValue;
1257
+ }
1258
+ });
1259
+ }
1260
+ } else if (valueType === "orientation") {
1261
+ if (!value) return;
1262
+
1263
+ try {
1264
+ let values = JSON.parse(`[${value}]`);
1265
+
1266
+ if (operator === "set") {
1267
+ if (values.length >= 1 && typeof values[0] == "string") {
1268
+ this.formatting.orientation[0] = values[0];
1269
+ }
1270
+ if (values.length >= 2 && typeof values[1] == "number") {
1271
+ this.formatting.orientation[1] = values[1];
1272
+ }
1273
+ if (values.length >= 3 && typeof values[2] == "number") {
1274
+ this.formatting.orientation[2] = values[2];
1275
+ }
1276
+ } else if (operator === "add") {
1277
+ if (values.length >= 2) this.formatting.orientation[1] += values[1];
1278
+ if (values.length >= 3) this.formatting.orientation[2] += values[2];
1279
+ } else if (operator === "scale") {
1280
+ if (values.length >= 2) this.formatting.orientation[1] *= values[1];
1281
+ if (values.length >= 3) this.formatting.orientation[2] *= values[2];
1282
+ }
1283
+ } catch (e) {
1284
+ console.log(new Error("Formatting Error: Invalid orientation format"));
1285
+ }
1286
+ } else if (valueType === "scale") {
1287
+ let numValue = Number(value);
1288
+ if (isNaN(numValue)) return;
1289
+
1290
+ if (operator === "set") {
1291
+ this.formatting.objectScale = numValue;
1292
+ } else if (operator === "add") {
1293
+ this.formatting.objectScale += numValue;
1294
+ } else if (operator === "mult") {
1295
+ this.formatting.objectScale *= numValue;
1296
+ }
1297
+ } else if (valueType === "color" && valueKey) {
1298
+ const colorMap = {
1299
+ "red": 0xFF0000,
1300
+ "green": 0x00FF00,
1301
+ "blue": 0x0000FF,
1302
+ "white": 0xFFFFFF,
1303
+ "black": 0x000000,
1304
+ "yellow": 0xFFFF00,
1305
+ "cyan": 0x00FFFF,
1306
+ "magenta": 0xFF00FF
1307
+ };
1308
+
1309
+ let colorValue;
1310
+ if (colorMap.hasOwnProperty(value)) {
1311
+ colorValue = colorMap[value];
1312
+ } else {
1313
+ colorValue = Number(value);
1314
+ if (isNaN(colorValue)) return;
1315
+ }
1316
+
1317
+ if (operator === "set") {
1318
+ if (valueKey === "txt") {
1319
+ this.formatting.txt.color = colorValue;
1320
+ } else if (valueKey === "bg") {
1321
+ if (!this.formatting.bg) this.formatting.bg = {};
1322
+ this.formatting.bg.color = colorValue;
1323
+ }
1324
+ }
1325
+ }
1326
+ }
1327
+
1328
+ parseNumberFormat(number, fullArray) {
1329
+ let index = fullArray.indexOf(number);
1330
+
1331
+ if (index < 5) {
1332
+ let keys = ["title", "heading", "subtitle", "base", "color"];
1333
+ if (keys[index]) {
1334
+ this.formatting.txt[keys[index]] = number;
1335
+ }
1336
+ } else if (index == 5) {
1337
+ this.formatting.objectScale = number;
1338
+ } else if (index == 6) {
1339
+ if (typeof fullArray[index] == "string") {
1340
+ this.formatting.orientation[0] = fullArray[index];
1341
+ }
1342
+ } else if (index == 7) {
1343
+ if (typeof fullArray[index] == "number") {
1344
+ this.formatting.orientation[1] = number;
1345
+ }
1346
+ } else if (index == 8) {
1347
+ if (typeof fullArray[index] == "number") {
1348
+ this.formatting.orientation[2] = number;
1349
+ }
1350
+ }
1351
+ }
1352
+
1353
+ render() {
1354
+ push();
1355
+
1356
+ this.applyOrientation();
1357
+ this.applyTextFormatting();
1358
+
1359
+ if (this.formatting.objectScale !== 1) {
1360
+ scale(this.formatting.objectScale);
1361
+ }
1362
+
1363
+ if (typeof this.executable == "function") {
1364
+ this.executable(this);
1365
+ }
1366
+
1367
+ pop();
1368
+ }
1369
+
1370
+ applyOrientation() {
1371
+ let [mode, offsetX, offsetY] = this.formatting.orientation;
1372
+
1373
+ if (mode.toLowerCase() == "camera") {
1374
+ if (window.camera) {
1375
+ let cameraPos;
1376
+ if (typeof window.camera.getOrientation == "function") {
1377
+ cameraPos = window.camera.getOrientation();
1378
+ } else {
1379
+ cameraPos = [window.camera.x || 0, window.camera.y || 0];
1380
+ }
1381
+ translate(cameraPos[0] + offsetX, cameraPos[1] + offsetY);
1382
+ } else {
1383
+ translate(offsetX, offsetY);
1384
+ }
1385
+ } else if (mode.toLowerCase() == "screen") {
1386
+ translate(offsetX, offsetY);
1387
+ } else if (mode.includes(",")) {
1388
+ try {
1389
+ let coords = mode.split(",").map(Number);
1390
+ if (coords.length >= 2) {
1391
+ if (window.camera && typeof window.camera.worldToScreen == "function") {
1392
+ let screenPos = window.camera.worldToScreen(coords[0], coords[1]);
1393
+ translate(screenPos.x + offsetX, screenPos.y + offsetY);
1394
+ } else {
1395
+ translate(coords[0] + offsetX, coords[1] + offsetY);
1396
+ }
1397
+ }
1398
+ } catch (e) {
1399
+ console.log(new Error("Formatting Error: Invalid orientation coordinates"));
1400
+ }
1401
+ }
1402
+ }
1403
+
1404
+ applyTextFormatting() {
1405
+ textSize(this.formatting.txt.base);
1406
+ fill(this.formatting.txt.color);
1407
+ textAlign(LEFT, TOP);
1408
+ }
1409
+
1410
+ drawText(str, x, y, hAlign = LEFT, vAlign = TOP) {
1411
+ push();
1412
+
1413
+ if (str.startsWith("[title]")) {
1414
+ textSize(this.formatting.txt.title);
1415
+ str = str.replace("[title]", "");
1416
+ } else if (str.startsWith("[heading]")) {
1417
+ textSize(this.formatting.txt.heading);
1418
+ str = str.replace("[heading]", "");
1419
+ } else if (str.startsWith("[subtitle]")) {
1420
+ textSize(this.formatting.txt.subtitle);
1421
+ str = str.replace("[subtitle]", "");
1422
+ } else {
1423
+ textSize(this.formatting.txt.base);
1424
+ }
1425
+
1426
+ fill(this.formatting.txt.color);
1427
+ textAlign(hAlign, vAlign);
1428
+ text(str, x, y);
1429
+
1430
+ pop();
1431
+ }
1432
+
1433
+ drawButton(button, x, y) {
1434
+ push();
1435
+
1436
+ if (this.formatting.objectScale !== 1) {
1437
+ scale(this.formatting.objectScale);
1438
+ }
1439
+
1440
+ if (button && typeof button.render == "function") {
1441
+ button.render();
1442
+ }
1443
+
1444
+ pop();
1445
+ }
1446
+
1447
+ belongsToScene(sceneId) {
1448
+ return this.scenes.includes(sceneId) || this.scenes.includes("blank");
1449
+ }
1450
+
1451
+ addToScene(sceneId) {
1452
+ if (!this.scenes.includes(sceneId)) {
1453
+ this.scenes.push(sceneId);
1454
+ }
1455
+ return this;
1456
+ }
1457
+
1458
+ removeFromScene(sceneId) {
1459
+ this.scenes = this.scenes.filter(id => id != sceneId);
1460
+ return this;
1461
+ }
1462
+
1463
+ addToScenes(sceneIds) {
1464
+ sceneIds.forEach(id => this.addToScene(id));
1465
+ return this;
1466
+ }
1467
+
1468
+ removeFromAllScenes() {
1469
+ this.scenes = [];
1470
+ return this;
1471
+ }
1472
+
1473
+ resetFormatting() {
1474
+ this.formatting = JSON.parse(JSON.stringify(this._formatting));
1475
+ return this;
1476
+ }
1477
+
1478
+ setTextStyle(property, value) {
1479
+ if (this.formatting.txt.hasOwnProperty(property)) {
1480
+ this.formatting.txt[property] = value;
1481
+ }
1482
+ return this;
1483
+ }
1484
+
1485
+ setOrientation(mode, offsetX = 0, offsetY = 0) {
1486
+ this.formatting.orientation = [mode, offsetX, offsetY];
1487
+ return this;
1488
+ }
1489
+
1490
+ setScale(scale) {
1491
+ this.formatting.objectScale = scale;
1492
+ return this;
1493
+ }
1494
+
1495
+ destroy() {
1496
+ this.removeFromAllScenes();
1497
+ let index = UIPlanes.indexOf(this);
1498
+ if (index > -1) {
1499
+ UIPlanes.splice(index, 1);
1500
+ }
1501
+ }
1502
+
1503
+ clone() {
1504
+ let clone = new UIPlane(
1505
+ this.executable,
1506
+ [...this.scenes],
1507
+ ...this.scenes
1508
+ );
1509
+ clone.formatting = JSON.parse(JSON.stringify(this.formatting));
1510
+ return clone;
1511
+ }
1512
+ }
1513
+
1514
+ // ========== CAMERA CLASS ==========
1515
+ class Camera {
1516
+ constructor(canvasX, canvasY) {
1517
+ this.x = canvasX/2;
1518
+ this.y = canvasY/2;
1519
+ this.position = [CENTER, CENTER];
1520
+ this.width = canvasX;
1521
+ this.height = canvasY;
1522
+ this.offsetX = 0;
1523
+ this.offsetY = 0;
1524
+ this.targetObject = null;
1525
+ }
1526
+
1527
+ getOrientation() {
1528
+ let topLeftX = this.x;
1529
+ let topLeftY = this.y;
1530
+
1531
+ if (this.position[0] == CENTER) {
1532
+ topLeftX = this.x - this.width / 2;
1533
+ } else if (this.position[0] == RIGHT) {
1534
+ topLeftX = this.x - this.width;
1535
+ }
1536
+
1537
+ if (this.position[1] == CENTER) {
1538
+ topLeftY = this.y - this.height / 2;
1539
+ } else if (this.position[1] == BOTTOM) {
1540
+ topLeftY = this.y - this.height;
1541
+ }
1542
+
1543
+ return [topLeftX, topLeftY];
1544
+ }
1545
+
1546
+ link(object, offsetX = 0, offsetY = 0) {
1547
+ if (!Number(object.x) && !Number(object.y)) {
1548
+ console.log(new Error(`TypeError: camera.link parameter must be an object that contains x, y values!`));
1549
+ return;
1550
+ }
1551
+
1552
+ this.targetObject = object;
1553
+ this.offsetX = offsetX;
1554
+ this.offsetY = offsetY;
1555
+
1556
+ this.updatePosition();
1557
+ }
1558
+
1559
+ updatePosition() {
1560
+ if (!this.targetObject) return;
1561
+
1562
+ let objX = this.targetObject.x;
1563
+ let objY = this.targetObject.y;
1564
+
1565
+ if (this.position[0] == LEFT) {
1566
+ this.x = objX - this.offsetX - this.width/2;
1567
+ } else if (this.position[0] == CENTER) {
1568
+ this.x = objX - this.offsetX;
1569
+ } else if (this.position[0] == RIGHT) {
1570
+ this.x = objX - this.offsetX + this.width/2;
1571
+ }
1572
+
1573
+ if (this.position[1] == TOP) {
1574
+ this.y = objY - this.offsetY - this.height/2;
1575
+ } else if (this.position[1] == CENTER) {
1576
+ this.y = objY - this.offsetY;
1577
+ } else if (this.position[1] == BOTTOM) {
1578
+ this.y = objY - this.offsetY + this.height/2;
1579
+ }
1580
+ }
1581
+
1582
+ setOffset(offsetX, offsetY) {
1583
+ this.offsetX = offsetX;
1584
+ this.offsetY = offsetY;
1585
+ this.updatePosition();
1586
+ }
1587
+
1588
+ render() {
1589
+ if (this.targetObject) {
1590
+ this.updatePosition();
1591
+ }
1592
+
1593
+ let [translateX, translateY] = this.getOrientation();
1594
+ translate(-translateX, -translateY);
1595
+ }
1596
+
1597
+ unlink() {
1598
+ this.targetObject = null;
1599
+ this.offsetX = 0;
1600
+ this.offsetY = 0;
1601
+ }
1602
+
1603
+ worldToScreen(worldX, worldY) {
1604
+ let [topLeftX, topLeftY] = this.getOrientation();
1605
+ return {
1606
+ x: worldX - topLeftX,
1607
+ y: worldY - topLeftY
1608
+ };
1609
+ }
1610
+
1611
+ screenToWorld(screenX, screenY) {
1612
+ let [topLeftX, topLeftY] = this.getOrientation();
1613
+ return {
1614
+ x: screenX + topLeftX,
1615
+ y: screenY + topLeftY
1616
+ };
1617
+ }
1618
+ }
1619
+
1620
+ // ========== MOUSE HANDLER ==========
1621
+ class MouseHandler {
1622
+ constructor() {
1623
+ this.mouse = {
1624
+ x: 0,
1625
+ y: 0,
1626
+ rawX:0,
1627
+ rawY:0,
1628
+ pressed: false,
1629
+ button: null,
1630
+ clicked: false,
1631
+ doubleClicked: false,
1632
+ lastClickTime: 0,
1633
+ clickCount: 0,
1634
+ pressedButtons: new Set(),
1635
+ dragStart: { x: 0, y: 0, active: false },
1636
+ dragDelta: { dx: 0, dy: 0 },
1637
+ wheel: { deltaX: 0, deltaY: 0, deltaZ: 0 },
1638
+ insideCanvas: false
1639
+ };
1640
+
1641
+ this.buttonMap = {
1642
+ 0: 'left',
1643
+ 1: 'middle',
1644
+ 2: 'right'
1645
+ };
1646
+
1647
+ this.setupEventListeners();
1648
+ }
1649
+
1650
+ setupEventListeners() {
1651
+ window.addEventListener('mousemove', (e) => this.handleMouseMove(e));
1652
+ window.addEventListener('mousedown', (e) => this.handleMouseDown(e));
1653
+ window.addEventListener('mouseup', (e) => this.handleMouseUp(e));
1654
+ window.addEventListener('click', (e) => this.handleClick(e));
1655
+ window.addEventListener('dblclick', (e) => this.handleDoubleClick(e));
1656
+ window.addEventListener('contextmenu', (e) => e.preventDefault());
1657
+ window.addEventListener('wheel', (e) => this.handleWheel(e));
1658
+ window.addEventListener('mouseenter', (e) => this.handleMouseEnter(e));
1659
+ window.addEventListener('mouseleave', (e) => this.handleMouseLeave(e));
1660
+ }
1661
+
1662
+ handleMouseMove(e) {
1663
+ this.mouse.x = e.clientX;
1664
+ this.mouse.y = e.clientY;
1665
+
1666
+ if (this.mouse.pressed) {
1667
+ this.mouse.dragStart.active = true;
1668
+ this.mouse.dragDelta = {
1669
+ dx: this.mouse.x - this.mouse.dragStart.x,
1670
+ dy: this.mouse.y - this.mouse.dragStart.y
1671
+ };
1672
+ }
1673
+
1674
+ if (window.p5 && p5.instance) {
1675
+ p5.instance._onmousemove(e);
1676
+ }
1677
+ }
1678
+
1679
+ handleMouseDown(e) {
1680
+ this.mouse.pressed = true;
1681
+ this.mouse.button = e.button;
1682
+ this.mouse.pressedButtons.add(e.button);
1683
+ this.mouse.dragStart = {
1684
+ x: this.mouse.x,
1685
+ y: this.mouse.y,
1686
+ active: false
1687
+ };
1688
+ this.mouse.dragDelta = { dx: 0, dy: 0 };
1689
+
1690
+ if (window.p5 && p5.instance) {
1691
+ p5.instance._onmousedown(e);
1692
+ }
1693
+ }
1694
+
1695
+ handleMouseUp(e) {
1696
+ this.mouse.pressed = false;
1697
+ this.mouse.pressedButtons.delete(e.button);
1698
+
1699
+ if (this.mouse.dragStart.active) {
1700
+ this.mouse.dragStart.active = false;
1701
+ }
1702
+
1703
+ if (window.p5 && p5.instance) {
1704
+ p5.instance._onmouseup(e);
1705
+ }
1706
+ }
1707
+
1708
+ handleClick(e) {
1709
+ this.mouse.clicked = true;
1710
+ this.mouse.clickCount++;
1711
+
1712
+ const currentTime = Date.now();
1713
+ if (currentTime - this.mouse.lastClickTime < 300) {
1714
+ this.mouse.doubleClicked = true;
1715
+ }
1716
+ this.mouse.lastClickTime = currentTime;
1717
+
1718
+ setTimeout(() => {
1719
+ this.mouse.clicked = false;
1720
+ this.mouse.doubleClicked = false;
1721
+ }, 100);
1722
+
1723
+ if (window.p5 && p5.instance) {
1724
+ p5.instance._onclick(e);
1725
+ }
1726
+ }
1727
+
1728
+ handleDoubleClick(e) {
1729
+ this.mouse.doubleClicked = true;
1730
+
1731
+ if (window.p5 && p5.instance) {
1732
+ p5.instance._ondblclick(e);
1733
+ }
1734
+ }
1735
+
1736
+ handleWheel(e) {
1737
+ this.mouse.wheel = {
1738
+ deltaX: e.deltaX,
1739
+ deltaY: e.deltaY,
1740
+ deltaZ: e.deltaZ
1741
+ };
1742
+
1743
+ if (window.p5 && p5.instance) {
1744
+ p5.instance._onwheel(e);
1745
+ }
1746
+ }
1747
+
1748
+ handleMouseEnter(e) {
1749
+ this.mouse.insideCanvas = true;
1750
+ }
1751
+
1752
+ handleMouseLeave(e) {
1753
+ this.mouse.insideCanvas = false;
1754
+
1755
+ if (this.mouse.dragStart.active) {
1756
+ this.mouse.dragStart.active = false;
1757
+ this.mouse.dragDelta = { dx: 0, dy: 0 };
1758
+ }
1759
+ }
1760
+
1761
+ getMousePosition() {
1762
+ return { x: this.mouse.x, y: this.mouse.y };
1763
+ }
1764
+
1765
+ isInsideCanvas() {
1766
+ return this.mouse.insideCanvas;
1767
+ }
1768
+
1769
+ isMousePressed(button = null) {
1770
+ if (button !== null) {
1771
+ if (typeof button === 'string') {
1772
+ const buttonIndex = this.getButtonIndex(button);
1773
+ return this.mouse.pressedButtons.has(buttonIndex);
1774
+ }
1775
+ return this.mouse.pressedButtons.has(button);
1776
+ }
1777
+ return this.mouse.pressed;
1778
+ }
1779
+
1780
+ getPressedButton() {
1781
+ if (this.mouse.button !== null) {
1782
+ return {
1783
+ index: this.mouse.button,
1784
+ name: this.buttonMap[this.mouse.button] || 'unknown'
1785
+ };
1786
+ }
1787
+ return null;
1788
+ }
1789
+
1790
+ getPressedButtons() {
1791
+ const buttons = [];
1792
+ this.mouse.pressedButtons.forEach(buttonIndex => {
1793
+ buttons.push({
1794
+ index: buttonIndex,
1795
+ name: this.buttonMap[buttonIndex] || 'unknown'
1796
+ });
1797
+ });
1798
+ return buttons;
1799
+ }
1800
+
1801
+ wasMouseClicked() {
1802
+ return this.mouse.clicked;
1803
+ }
1804
+
1805
+ wasMouseDoubleClicked() {
1806
+ return this.mouse.doubleClicked;
1807
+ }
1808
+
1809
+ getClickCount() {
1810
+ return this.mouse.clickCount;
1811
+ }
1812
+
1813
+ isDragging() {
1814
+ return this.mouse.dragStart.active && this.mouse.pressed;
1815
+ }
1816
+
1817
+ getDragDelta() {
1818
+ return { ...this.mouse.dragDelta };
1819
+ }
1820
+
1821
+ getDragStartPosition() {
1822
+ if (this.mouse.dragStart.active) {
1823
+ return {
1824
+ x: this.mouse.dragStart.x,
1825
+ y: this.mouse.dragStart.y
1826
+ };
1827
+ }
1828
+ return null;
1829
+ }
1830
+
1831
+ getWheelDelta() {
1832
+ return { ...this.mouse.wheel };
1833
+ }
1834
+
1835
+ wasWheelScrolled() {
1836
+ return this.mouse.wheel.deltaY !== 0 ||
1837
+ this.mouse.wheel.deltaX !== 0 ||
1838
+ this.mouse.wheel.deltaZ !== 0;
1839
+ }
1840
+
1841
+ getMouseVelocity() {
1842
+ if (!this.mouse.prevX) {
1843
+ this.mouse.prevX = this.mouse.x;
1844
+ this.mouse.prevY = this.mouse.y;
1845
+ return { vx: 0, vy: 0 };
1846
+ }
1847
+
1848
+ const velocity = {
1849
+ vx: this.mouse.x - this.mouse.prevX,
1850
+ vy: this.mouse.y - this.mouse.prevY
1851
+ };
1852
+
1853
+ this.mouse.prevX = this.mouse.x;
1854
+ this.mouse.prevY = this.mouse.y;
1855
+
1856
+ return velocity;
1857
+ }
1858
+
1859
+ getButtonIndex(buttonName) {
1860
+ const buttonMap = {
1861
+ 'left': 0,
1862
+ 'middle': 1,
1863
+ 'right': 2,
1864
+ 'back': 3,
1865
+ 'forward': 4
1866
+ };
1867
+ return buttonMap[buttonName.toLowerCase()] ?? -1;
1868
+ }
1869
+
1870
+ reset() {
1871
+ this.mouse.pressed = false;
1872
+ this.mouse.button = null;
1873
+ this.mouse.clicked = false;
1874
+ this.mouse.doubleClicked = false;
1875
+ this.mouse.pressedButtons.clear();
1876
+ this.mouse.dragStart.active = false;
1877
+ this.mouse.dragDelta = { dx: 0, dy: 0 };
1878
+ this.mouse.wheel = { deltaX: 0, deltaY: 0, deltaZ: 0 };
1879
+ }
1880
+ }
1881
+
1882
+ // ========== KEYBOARD HANDLER ==========
1883
+ class KeyboardHandler {
1884
+ constructor(){
1885
+ this.keys = {};
1886
+ this.modifiers = {
1887
+ shift: false,
1888
+ ctrl: false,
1889
+ alt: false,
1890
+ meta: false
1891
+ };
1892
+
1893
+ this.keyTyped = "";
1894
+ this.typedBuffer = "";
1895
+
1896
+ this.shiftSymbolMap = {
1897
+ '`': '~', '1': '!', '2': '@', '3': '#', '4': '$', '5': '%',
1898
+ '6': '^', '7': '&', '8': '*', '9': '(', '0': ')', '-': '_',
1899
+ '=': '+', '[': '{', ']': '}', '\\': '|', ';': ':', "'": '"',
1900
+ ',': '<', '.': '>', '/': '?'
1901
+ };
1902
+
1903
+ this.nonTypingKeys = [
1904
+ 'shift', 'control', 'alt', 'meta', 'escape', 'tab', 'capslock',
1905
+ 'arrowup', 'arrowdown', 'arrowleft', 'arrowright', 'enter', 'backspace',
1906
+ 'delete', 'home', 'end', 'pageup', 'pagedown', 'insert', 'f1', 'f2',
1907
+ 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12'
1908
+ ];
1909
+ }
1910
+
1911
+ keyPressed(){
1912
+ let returner = [];
1913
+ let keys = arguments;
1914
+
1915
+ let keysLength = Object.keys(this.keys).length;
1916
+
1917
+ if(keysLength < 1) return false;
1918
+
1919
+ if(arguments.length == 0){
1920
+ for (const [key, value] of Object.entries(this.keys)) {
1921
+ if(value.pressed) returner.push(value);
1922
+ }
1923
+ } else if(arguments.length == 1){
1924
+ returner = (this.keys[arguments[0]] != undefined) ? this.keys[arguments[0]].pressed : false;
1925
+ } else {
1926
+ for(let a in arguments){
1927
+ let arg = arguments[a];
1928
+
1929
+ if(typeof this.keys[arg] == "object") returner.push(this.keys[arg].pressed);
1930
+ else returner.push(undefined);
1931
+ }
1932
+
1933
+ returner = returner.every(k => k == true);
1934
+ }
1935
+
1936
+ return returner;
1937
+ }
1938
+
1939
+ getKey(k){
1940
+ if(!this.keys[k]) return;
1941
+ return this.keys[k];
1942
+ }
1943
+
1944
+ held(k){
1945
+ if(!this.keys[k]) return;
1946
+ let key = this.keys[k];
1947
+ if(key.timeStamp.pressed > key.timeStamp.released) {
1948
+ return (new Date().getTime() - key.timeStamp.pressed)/1000;
1949
+ }
1950
+ return (key.timeStamp.released - key.timeStamp.pressed)/1000;
1951
+ }
1952
+
1953
+ getTypedChar(key) {
1954
+ if (!key || key.length === 0) return '';
1955
+
1956
+ const lowerKey = key.toLowerCase();
1957
+
1958
+ if (this.nonTypingKeys.includes(lowerKey)) {
1959
+ return '';
1960
+ }
1961
+
1962
+ if (lowerKey.match(/[a-z]/)) {
1963
+ const shiftPressed = this.isShiftPressed();
1964
+ return shiftPressed ? key.toUpperCase() : key.toLowerCase();
1965
+ }
1966
+
1967
+ if (this.isShiftPressed() && this.shiftSymbolMap[key]) {
1968
+ return this.shiftSymbolMap[key];
1969
+ }
1970
+
1971
+ return key;
1972
+ }
1973
+
1974
+ isShiftPressed() {
1975
+ for (const [keyCode, keyData] of Object.entries(this.keys)) {
1976
+ if (keyData.modifiers && keyData.modifiers.shift) {
1977
+ return true;
1978
+ }
1979
+ }
1980
+ return false;
1981
+ }
1982
+
1983
+ typed(key = null, clearAfter = true) {
1984
+ if (key !== null) {
1985
+ const typedChar = this.getTypedChar(key);
1986
+ if (typedChar) {
1987
+ this.typedBuffer += typedChar;
1988
+ }
1989
+ return typedChar;
1990
+ }
1991
+
1992
+ const buffer = this.typedBuffer;
1993
+ if (clearAfter) {
1994
+ this.clearTypedBuffer();
1995
+ }
1996
+ return buffer;
1997
+ }
1998
+
1999
+ clearTypedBuffer() {
2000
+ this.typedBuffer = "";
2001
+ }
2002
+
2003
+ backspace() {
2004
+ this.typedBuffer = this.typedBuffer.slice(0, -1);
2005
+ }
2006
+
2007
+ enter() {
2008
+ this.typedBuffer += '\n';
2009
+ }
2010
+
2011
+ getTypedBuffer() {
2012
+ return this.typedBuffer;
2013
+ }
2014
+ }
2015
+
2016
+ // ========== KEYBOARD EVENT LISTENERS ==========
2017
+ const keyboard = new KeyboardHandler();
2018
+
2019
+ window.addEventListener('keydown', (e) => {
2020
+ const key = e.key;
2021
+ const lowerKey = key.toLowerCase();
2022
+
2023
+ let ck = keyboard.keys[lowerKey];
2024
+ let keyObject;
2025
+
2026
+ if(ck != undefined){
2027
+ if(!ck.pressed) ck.timeStamp.pressed = new Date().getTime();
2028
+ ck.pressed = true;
2029
+ ck.modifiers = {
2030
+ shift: e.shiftKey,
2031
+ ctrl: e.ctrlKey,
2032
+ alt: e.altKey,
2033
+ meta: e.metaKey,
2034
+ };
2035
+ keyObject = ck;
2036
+ } else {
2037
+ keyObject = {
2038
+ key: lowerKey,
2039
+ originalKey: key,
2040
+ modifiers:{
2041
+ shift: e.shiftKey,
2042
+ ctrl: e.ctrlKey,
2043
+ alt: e.altKey,
2044
+ meta: e.metaKey,
2045
+ },
2046
+ held: e.repeat,
2047
+ pressed: true,
2048
+ timeStamp:{
2049
+ pressed: new Date().getTime(),
2050
+ released: -1,
2051
+ },
2052
+ };
2053
+ }
2054
+
2055
+ keyboard.keys[lowerKey] = keyObject;
2056
+
2057
+ if (!e.ctrlKey && !e.altKey && !e.metaKey && key.length === 1) {
2058
+ keyboard.typed(key, false);
2059
+ } else {
2060
+ if (lowerKey === 'backspace') {
2061
+ keyboard.backspace();
2062
+ } else if (lowerKey === 'enter') {
2063
+ keyboard.enter();
2064
+ }
2065
+ }
2066
+ });
2067
+
2068
+ window.addEventListener('keyup', (e) => {
2069
+ const lowerKey = e.key.toLowerCase();
2070
+
2071
+ let ck = keyboard.keys[lowerKey];
2072
+ let keyObject;
2073
+
2074
+ if(ck != undefined){
2075
+ if(ck.pressed) ck.timeStamp.released = new Date().getTime();
2076
+ ck.pressed = false;
2077
+ ck.modifiers = {
2078
+ shift: false,
2079
+ ctrl: false,
2080
+ alt: false,
2081
+ meta: false,
2082
+ };
2083
+ keyObject = ck;
2084
+ } else {
2085
+ keyObject = {
2086
+ key: lowerKey,
2087
+ modifiers:{
2088
+ shift: e.shiftKey,
2089
+ ctrl: e.ctrlKey,
2090
+ alt: e.altKey,
2091
+ meta: e.metaKey,
2092
+ },
2093
+ held: e.repeat,
2094
+ pressed: false,
2095
+ timeStamp:{
2096
+ pressed: -1,
2097
+ released: new Date().getTime(),
2098
+ },
2099
+ };
2100
+ }
2101
+
2102
+ keyboard.keys[lowerKey] = keyObject;
2103
+ });
2104
+
2105
+ window.addEventListener('blur', (e) => {
2106
+ for(let K in keyboard.keys){
2107
+ let k = keyboard.keys[K];
2108
+ k.pressed = false;
2109
+ k.timeStamp.released = new Date().getTime();
2110
+ k.modifiers = {
2111
+ shift: false,
2112
+ ctrl: false,
2113
+ alt: false,
2114
+ meta: false,
2115
+ };
2116
+ }
2117
+ });
2118
+
2119
+ // ========== CONTROLLER HANDLER ==========
2120
+ class GameController {
2121
+ constructor() {
2122
+ this.index = 0;
2123
+ this.buttons = [];
2124
+ this.axes = [];
2125
+ this.connected = false;
2126
+ this.id = "";
2127
+
2128
+ this.binds = {
2129
+ select: null,
2130
+ back: null,
2131
+ primary: null,
2132
+ secondary: null,
2133
+ leftBumber: null,
2134
+ rightBumber: null,
2135
+ leftTrigger: null,
2136
+ rightTrigger: null,
2137
+ view: null,
2138
+ menu: null,
2139
+ leftStick: null,
2140
+ rightStick: null,
2141
+ up: null,
2142
+ down: null,
2143
+ left: null,
2144
+ right: null,
2145
+ home: null,
2146
+ };
2147
+
2148
+ this.setupListeners();
2149
+ }
2150
+
2151
+ setupListeners() {
2152
+ window.addEventListener("gamepadconnected", (e) => {
2153
+ console.log("Controller connected:", e.gamepad);
2154
+ this.updateController(e.gamepad);
2155
+ });
2156
+
2157
+ window.addEventListener("gamepaddisconnected", (e) => {
2158
+ console.log("Controller disconnected");
2159
+ this.connected = false;
2160
+ });
2161
+ }
2162
+
2163
+ update() {
2164
+ const gamepads = navigator.getGamepads();
2165
+ if (gamepads[this.index]) {
2166
+ this.updateController(gamepads[this.index]);
2167
+ }
2168
+
2169
+ for(let b in this.binds){
2170
+ let tester = this.getButton(b);
2171
+
2172
+ if(tester != false && tester.value != 0 && typeof this.binds[b] == "function") {
2173
+ this.binds[b]();
2174
+ }
2175
+ }
2176
+ }
2177
+
2178
+ updateController(gp) {
2179
+ this.connected = true;
2180
+ this.id = gp.id;
2181
+ this.buttons = gp.buttons.map(b => ({
2182
+ pressed: b.pressed,
2183
+ value: b.value
2184
+ }));
2185
+ this.axes = [...gp.axes];
2186
+ }
2187
+
2188
+ isButtonPressed(buttonIndex) {
2189
+ return this.buttons[buttonIndex]?.pressed || false;
2190
+ }
2191
+
2192
+ getButtonValue(buttonIndex) {
2193
+ return this.buttons[buttonIndex]?.value || 0;
2194
+ }
2195
+
2196
+ getAxis(axisIndex) {
2197
+ if(typeof axisIndex == "number") {
2198
+ return this.axes[axisIndex] || 0;
2199
+ } else if(typeof axisIndex == "string"){
2200
+ if(/left/gi.test(axisIndex)) {
2201
+ return {x: this.axes[0], y: this.axes[1]};
2202
+ }
2203
+ if(/right/gi.test(axisIndex)) {
2204
+ return {x: this.axes[2], y: this.axes[3]};
2205
+ }
2206
+ }
2207
+ }
2208
+
2209
+ getButton(name){
2210
+ name = name.toLowerCase();
2211
+ let btn = {
2212
+ select: 0,
2213
+ back: 1,
2214
+ primary: 2,
2215
+ secondary: 3,
2216
+ lefttumber: 4,
2217
+ righttumber: 5,
2218
+ lefttrigger: 6,
2219
+ righttrigger: 7,
2220
+ view: 8,
2221
+ menu: 9,
2222
+ leftstick: 10,
2223
+ rightstick: 11,
2224
+ up: 12,
2225
+ down: 13,
2226
+ left: 14,
2227
+ right: 15,
2228
+ home: 16,
2229
+ }[name];
2230
+
2231
+ if(btn == undefined) throw new Error(`Controller|TypeError: "${name}" isn't a valid button mapping getter!`);
2232
+
2233
+ return this.buttons[btn]?.pressed || false;
2234
+ }
2235
+ }
2236
+
2237
+ const controller = new GameController();
2238
+
2239
+ // ========== FPS TRACKER ==========
2240
+ const fpsTimes = [];
2241
+ let fps;
2242
+
2243
+ function refreshLoop() {
2244
+ window.requestAnimationFrame(() => {
2245
+ const now = performance.now();
2246
+ while (fpsTimes.length > 0 && fpsTimes[0] <= now - 1000) {
2247
+ fpsTimes.shift();
2248
+ }
2249
+ fpsTimes.push(now);
2250
+ fps = fpsTimes.length;
2251
+ refreshLoop();
2252
+ });
2253
+ }
2254
+
2255
+ // ========== COLORED TEXT FUNCTION ==========
2256
+ p5.prototype._parseColoredText = function(str) {
2257
+ const lines = str.split('\n');
2258
+ const result = [];
2259
+
2260
+ for (let i = 0; i < lines.length; i++) {
2261
+ const line = lines[i];
2262
+ const parts = this._parseColoredLine(line);
2263
+
2264
+ result.push(...parts);
2265
+
2266
+ if (i < lines.length - 1) {
2267
+ result.push({
2268
+ text: '\n',
2269
+ color: null,
2270
+ isNewline: true
2271
+ });
2272
+ }
2273
+ }
2274
+
2275
+ return result;
2276
+ };
2277
+
2278
+ p5.prototype._parseColoredLine = function(str) {
2279
+ const regex = /\\([^|\\\n]+)\|([^|]+)\|/g;
2280
+ const parts = [];
2281
+ let lastIndex = 0;
2282
+ let match;
2283
+
2284
+ while ((match = regex.exec(str)) !== null) {
2285
+ if (match.index > lastIndex) {
2286
+ parts.push({
2287
+ text: str.substring(lastIndex, match.index),
2288
+ color: null
2289
+ });
2290
+ }
2291
+
2292
+ parts.push({
2293
+ text: match[2],
2294
+ color: match[1]
2295
+ });
2296
+
2297
+ lastIndex = match.index + match[0].length;
2298
+ }
2299
+
2300
+ if (lastIndex < str.length) {
2301
+ parts.push({
2302
+ text: str.substring(lastIndex),
2303
+ color: null
2304
+ });
2305
+ }
2306
+
2307
+ return parts.length ? parts : [{ text: str, color: null }];
2308
+ };
2309
+
2310
+ p5.prototype.coloredText = function(str, x, y, horizontal = LEFT, vertical = BASELINE, maxWidth) {
2311
+ const parts = this._parseColoredText(str);
2312
+ let currentX = x;
2313
+ let currentY = y;
2314
+
2315
+ const originalFill = this.drawingContext.fillStyle;
2316
+ const originalAlign = this.drawingContext.textAlign;
2317
+ const originalBaseline = this.drawingContext.textBaseline;
2318
+
2319
+ this.textAlign(horizontal, vertical);
2320
+
2321
+ for (const part of parts) {
2322
+ if (part.isNewline) {
2323
+ currentX = x;
2324
+ currentY += this.textLeading() || this.textSize() * 1.2;
2325
+ continue;
2326
+ }
2327
+
2328
+ if (part.color) {
2329
+ try {
2330
+ this.fill(part.color);
2331
+ } catch (e) {
2332
+ this.fill(originalFill);
2333
+ }
2334
+ }
2335
+
2336
+ this.text(part.text, currentX, currentY, maxWidth);
2337
+ currentX += this.textWidth(part.text);
2338
+ }
2339
+
2340
+ this.fill(originalFill);
2341
+ if (originalAlign && originalBaseline) {
2342
+ this.drawingContext.textAlign = originalAlign;
2343
+ this.drawingContext.textBaseline = originalBaseline;
2344
+ }
2345
+
2346
+ return this;
2347
+ };
2348
+
2349
+ // ========== FPS ACCESSOR ==========
2350
+ function getFPS() {
2351
+ return fps;
2352
+ }
2353
+
2354
+ // ========== HELP SYSTEM ==========
2355
+ const helpDocs = {
2356
+ // Game Engine Overview
2357
+ overview: `
2358
+ MALC Game Engine - A comprehensive 2D game engine for p5.js
2359
+ Version: 1.0.0
2360
+
2361
+ Core Features:
2362
+ - Scene management system
2363
+ - GameObject class with physics (position, velocity, rotation)
2364
+ - Gravity system with collision detection
2365
+ - Interactive Button class
2366
+ - UI Plane system for HUD elements
2367
+ - Camera system with object tracking
2368
+ - Mouse, Keyboard, and Gamepad input handlers
2369
+ - Colored text rendering
2370
+ - FPS tracking
2371
+ `,
2372
+
2373
+ // Classes
2374
+ classes: {
2375
+ gameObject: {
2376
+ description: "Base class for all game objects with position, velocity, and gravity properties",
2377
+ constructor: "new gameObject(x, y, width, height, ...scenes)",
2378
+ properties: {
2379
+ x: "X position of the object",
2380
+ y: "Y position of the object",
2381
+ width: "Width of the object",
2382
+ height: "Height of the object",
2383
+ rotation: "Rotation angle in degrees",
2384
+ velocity: "[speed, angle] for polar mode or [vx, vy] for cartesian",
2385
+ gravity: "Object containing gravity settings (enabled, velocity, grounded, etc.)",
2386
+ active: "Whether the object is active",
2387
+ visible: "Whether the object is visible",
2388
+ debug: "Toggle debug visualization"
2389
+ },
2390
+ methods: {
2391
+ enableGravity: "Enable gravity for this object",
2392
+ disableGravity: "Disable gravity for this object",
2393
+ setGravity: "Configure gravity settings: {enabled, mass, bounce, friction, groundTolerance}",
2394
+ setVelocity: "Set velocity towards a point: setVelocity(speed, x, y, error)",
2395
+ pointTo: "Rotate to face a target",
2396
+ distanceTo: "Get distance to another object",
2397
+ collidesWith: "Check collision with another object",
2398
+ addToScene: "Add object to a scene",
2399
+ removeFromScene: "Remove object from a scene",
2400
+ destroy: "Remove object from game",
2401
+ clone: "Create a copy of the object"
2402
+ },
2403
+ staticMethods: {
2404
+ setGlobalGravity: "Set global gravity strength",
2405
+ getGlobalGravity: "Get current gravity value",
2406
+ getActiveObjects: "Get all active objects",
2407
+ getObjectsInScene: "Get objects in a specific scene"
2408
+ }
2409
+ },
2410
+
2411
+ Button: {
2412
+ description: "Interactive button class that extends gameObject",
2413
+ constructor: "new Button(x, y, width, height, displayText, ...scenes)",
2414
+ properties: {
2415
+ onClick: "Callback function when button is clicked",
2416
+ isHovered: "Whether mouse is over button",
2417
+ isPressed: "Whether button is being pressed",
2418
+ isDisabled: "Whether button is disabled"
2419
+ },
2420
+ methods: {
2421
+ setText: "Set button text",
2422
+ setColors: "Set button colors for different states",
2423
+ textStyle: "Set text color and size",
2424
+ Disable: "Enable/disable the button",
2425
+ click: "Simulate a button click"
2426
+ }
2427
+ },
2428
+
2429
+ Scene: {
2430
+ description: "Scene management system for organizing game states",
2431
+ constructor: "new Scene(id, backgroundColor, ...scripts)",
2432
+ staticMethods: {
2433
+ switchToScene: "Switch to a different scene",
2434
+ getActiveScene: "Get the currently active scene",
2435
+ getSceneById: "Find a scene by ID",
2436
+ goBack: "Go back to previous scene"
2437
+ },
2438
+ methods: {
2439
+ addObject: "Add an object to the scene",
2440
+ addObjects: "Add multiple objects to the scene",
2441
+ removeObject: "Remove an object from the scene",
2442
+ clearObjects: "Remove all objects",
2443
+ pause: "Pause scene updates",
2444
+ resume: "Resume scene updates",
2445
+ setTransition: "Set scene transition effect"
2446
+ }
2447
+ },
2448
+
2449
+ UIPlane: {
2450
+ description: "UI element for HUD and interface elements",
2451
+ constructor: "new UIPlane(executableFunction, formattingArray, ...scenes)",
2452
+ formatting: {
2453
+ txt: "Text styling: {title, heading, subtitle, base, color}",
2454
+ orientation: "Positioning: ['camera', offsetX, offsetY] or ['screen', x, y]",
2455
+ objectScale: "Scale factor for the UI plane"
2456
+ },
2457
+ methods: {
2458
+ drawText: "Draw formatted text on the UI plane",
2459
+ drawButton: "Draw a button on the UI plane",
2460
+ setOrientation: "Set position mode and offsets",
2461
+ setScale: "Set scale factor",
2462
+ addToScene: "Add UI plane to a scene"
2463
+ }
2464
+ },
2465
+
2466
+ Camera: {
2467
+ description: "Camera system for following game objects",
2468
+ constructor: "new Camera(canvasWidth, canvasHeight)",
2469
+ methods: {
2470
+ link: "Make camera follow an object: link(object, offsetX, offsetY)",
2471
+ unlink: "Stop following",
2472
+ worldToScreen: "Convert world coordinates to screen coordinates",
2473
+ screenToWorld: "Convert screen coordinates to world coordinates",
2474
+ getOrientation: "Get camera position in world space"
2475
+ }
2476
+ }
2477
+ },
2478
+
2479
+ // Input Handlers
2480
+ input: {
2481
+ mouse: {
2482
+ description: "Global mouse object for input detection",
2483
+ methods: {
2484
+ getMousePosition: "Get current mouse position {x, y}",
2485
+ isMousePressed: "Check if mouse button is pressed",
2486
+ wasMouseClicked: "Check if mouse was clicked this frame",
2487
+ isDragging: "Check if mouse is dragging",
2488
+ getDragDelta: "Get drag movement since drag started",
2489
+ getWheelDelta: "Get mouse wheel scroll amount"
2490
+ }
2491
+ },
2492
+
2493
+ keyboard: {
2494
+ description: "Global keyboard object for input detection",
2495
+ methods: {
2496
+ keyPressed: "Check if specific keys are pressed",
2497
+ getKey: "Get key data object",
2498
+ held: "Get how long a key has been held (seconds)",
2499
+ typed: "Get typed characters with shift handling",
2500
+ getTypedBuffer: "Get current typed input without clearing"
2501
+ }
2502
+ },
2503
+
2504
+ controller: {
2505
+ description: "Global game controller object",
2506
+ methods: {
2507
+ update: "Update controller state (call in draw)",
2508
+ getButton: "Check if a button is pressed by name",
2509
+ getAxis: "Get axis values (left/right stick)",
2510
+ isButtonPressed: "Check button by index",
2511
+ getButtonValue: "Get analog button value"
2512
+ },
2513
+ buttonNames: ["select", "back", "primary", "secondary", "leftBumber", "rightBumber",
2514
+ "leftTrigger", "rightTrigger", "view", "menu", "leftStick", "rightStick",
2515
+ "up", "down", "left", "right", "home"]
2516
+ }
2517
+ },
2518
+
2519
+ // Utility Functions
2520
+ utilities: {
2521
+ coloredText: "Render text with color tags: coloredText('\\red|Hello| \\blue|World|', x, y)",
2522
+ getFPS: "Get current frames per second",
2523
+ generateId: "Generate unique ID with prefix",
2524
+ getTimestamp: "Get current timestamp in milliseconds"
2525
+ },
2526
+
2527
+ // Getting Started
2528
+ quickStart: `
2529
+ // 1. Initialize the engine in setup()
2530
+ function setup() {
2531
+ MALC.init(800, 600); // Initialize with canvas size
2532
+ }
2533
+
2534
+ // 2. Create a scene
2535
+ let gameScene = new Scene("game", 220);
2536
+
2537
+ // 3. Create a game object with gravity
2538
+ let player = new gameObject(100, 100, 50, 50, "game")
2539
+ .enableGravity()
2540
+ .setGravity({ mass: 1, bounce: 0.3 });
2541
+
2542
+ // 4. Add update logic in draw()
2543
+ function draw() {
2544
+ MALC.update(); // Updates all MALC systems
2545
+ }
2546
+ `
2547
+ };
2548
+
2549
+ // ========== MALC MAIN OBJECT ==========
2550
+ const MALC = {
2551
+ version: "1.0.0",
2552
+
2553
+ // Core classes
2554
+ gameObject: gameObject,
2555
+ Button: Button,
2556
+ Scene: Scene,
2557
+ UIPlane: UIPlane,
2558
+ Camera: Camera,
2559
+
2560
+ // Input handlers
2561
+ mouse: null,
2562
+ keyboard: keyboard,
2563
+ controller: controller,
2564
+
2565
+ // FPS
2566
+ fps: fps,
2567
+ getFPS: getFPS,
2568
+
2569
+ // Time tracking
2570
+ time: new Date(),
2571
+ startTime: new Date().getTime(),
2572
+ timer: 0,
2573
+
2574
+ // Utility functions
2575
+ generateId: generateId,
2576
+ getTimestamp: getTimestamp,
2577
+
2578
+ // Gravity constants
2579
+ GRAVITY: GRAVITY,
2580
+ TERMINAL_VELOCITY: TERMINAL_VELOCITY,
2581
+
2582
+ // Help system
2583
+ help: function(topic = "overview") {
2584
+ if (topic === "overview") {
2585
+ console.log(helpDocs.overview);
2586
+ return helpDocs.overview;
2587
+ }
2588
+
2589
+ // Check classes
2590
+ if (helpDocs.classes[topic]) {
2591
+ console.log(`=== ${topic.toUpperCase()} ===`);
2592
+ console.log(helpDocs.classes[topic].description);
2593
+ console.log("\nConstructor:", helpDocs.classes[topic].constructor);
2594
+
2595
+ if (helpDocs.classes[topic].properties) {
2596
+ console.log("\nProperties:");
2597
+ Object.entries(helpDocs.classes[topic].properties).forEach(([prop, desc]) => {
2598
+ console.log(` ${prop}: ${desc}`);
2599
+ });
2600
+ }
2601
+
2602
+ if (helpDocs.classes[topic].methods) {
2603
+ console.log("\nMethods:");
2604
+ Object.entries(helpDocs.classes[topic].methods).forEach(([method, desc]) => {
2605
+ console.log(` ${method}: ${desc}`);
2606
+ });
2607
+ }
2608
+
2609
+ if (helpDocs.classes[topic].staticMethods) {
2610
+ console.log("\nStatic Methods:");
2611
+ Object.entries(helpDocs.classes[topic].staticMethods).forEach(([method, desc]) => {
2612
+ console.log(` static ${method}: ${desc}`);
2613
+ });
2614
+ }
2615
+
2616
+ return helpDocs.classes[topic];
2617
+ }
2618
+
2619
+ // Check input
2620
+ if (helpDocs.input[topic]) {
2621
+ console.log(`=== ${topic.toUpperCase()} ===`);
2622
+ console.log(helpDocs.input[topic].description);
2623
+ console.log("\nMethods:");
2624
+ Object.entries(helpDocs.input[topic].methods).forEach(([method, desc]) => {
2625
+ console.log(` ${method}: ${desc}`);
2626
+ });
2627
+
2628
+ if (topic === "controller" && helpDocs.input.controller.buttonNames) {
2629
+ console.log("\nButton Names:", helpDocs.input.controller.buttonNames.join(", "));
2630
+ }
2631
+
2632
+ return helpDocs.input[topic];
2633
+ }
2634
+
2635
+ // Check utilities
2636
+ if (helpDocs.utilities[topic]) {
2637
+ console.log(`=== ${topic.toUpperCase()} ===`);
2638
+ console.log(helpDocs.utilities[topic]);
2639
+ return helpDocs.utilities[topic];
2640
+ }
2641
+
2642
+ // Quick start
2643
+ if (topic === "quickStart" || topic === "start") {
2644
+ console.log(helpDocs.quickStart);
2645
+ return helpDocs.quickStart;
2646
+ }
2647
+
2648
+ // Not found
2649
+ console.log(`Help topic "${topic}" not found. Try: overview, classes (gameObject, Button, Scene, UIPlane, Camera), input (mouse, keyboard, controller), utilities (coloredText, getFPS), quickStart`);
2650
+ return null;
2651
+ },
2652
+
2653
+ // List all available help topics
2654
+ helpTopics: function() {
2655
+ const topics = [
2656
+ "overview",
2657
+ "classes: " + Object.keys(helpDocs.classes).join(", "),
2658
+ "input: " + Object.keys(helpDocs.input).join(", "),
2659
+ "utilities: " + Object.keys(helpDocs.utilities).join(", "),
2660
+ "quickStart"
2661
+ ];
2662
+ console.log("Available help topics:\n" + topics.join("\n"));
2663
+ return topics;
2664
+ },
2665
+
2666
+ // Initialize the engine
2667
+ init: function(canvasX, canvasY) {
2668
+ createCanvas(canvasX, canvasY);
2669
+
2670
+ this.time = new Date();
2671
+ this.startTime = this.time.getTime();
2672
+
2673
+ // Initialize camera
2674
+ window.camera = new Camera(canvasX, canvasY);
2675
+
2676
+ // Initialize mouse handler
2677
+ this.mouse = new MouseHandler();
2678
+ window.mouse = this.mouse;
2679
+
2680
+ // Start FPS tracking
2681
+ refreshLoop();
2682
+
2683
+ // Create default scenes
2684
+ new Scene("blank", 70);
2685
+ new Scene("loading", 50, function(self) {
2686
+ textSize(24);
2687
+ let timed = (self.timeActive / 250 % 4);
2688
+ let dots = "";
2689
+
2690
+ if (timed < 1) dots = ".";
2691
+ else if (timed < 2) dots = "..";
2692
+ else if (timed < 3) dots = "...";
2693
+
2694
+ coloredText(`\\lime|Loading Game${dots}| `, 120, 200, LEFT, CENTER);
2695
+ textSize(16);
2696
+
2697
+ let num = (Math.floor(self.timeActive / 100) / 10);
2698
+ coloredText(`\\red|${ Math.round((10 - num) * 10) / 10 + ((num + "").length < 2 ? ".0" : "")}|`, 200, 225, CENTER, CENTER);
2699
+ });
2700
+
2701
+ Scene.activeScene = "loading";
2702
+
2703
+ console.log("MALC Game Engine initialized v" + this.version);
2704
+ console.log("Type MALC.help() for documentation");
2705
+ },
2706
+
2707
+ // Update all systems (call in draw)
2708
+ update: function() {
2709
+ this.time = new Date();
2710
+ this.timer = this.time - this.startTime;
2711
+
2712
+ if (this.mouse) {
2713
+ this.mouse.rawX = mouseX;
2714
+ this.mouse.rawY = mouseY;
2715
+ this.mouse.x = this.mouse.rawX + camera.getOrientation()[0];
2716
+ this.mouse.y = this.mouse.rawY + camera.getOrientation()[1];
2717
+ this.mouse.down = mouseIsPressed;
2718
+ }
2719
+
2720
+ controller.update();
2721
+
2722
+ gameObject.update();
2723
+ Button.updateButton();
2724
+
2725
+ this.fps = fps;
2726
+
2727
+ if (typeof camera.render == "function") {
2728
+ camera.render();
2729
+ }
2730
+ Scene.update();
2731
+ }
2732
+ };
2733
+
2734
+ // Initialize mouse and keyboard handlers
2735
+ MALC.mouse = new MouseHandler();
2736
+
2737
+ return MALC;
2738
+
2739
+ }));