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.
- package/malc.js +2739 -0
- 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
|
+
}));
|