dino-ge 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/Camera.d.ts +37 -0
  2. package/dist/Camera.js +44 -0
  3. package/dist/Camera.js.map +1 -0
  4. package/dist/Canvas.d.ts +12 -0
  5. package/dist/Canvas.js +21 -0
  6. package/dist/Canvas.js.map +1 -0
  7. package/dist/Circle.d.ts +39 -0
  8. package/dist/Circle.js +51 -0
  9. package/dist/Circle.js.map +1 -0
  10. package/dist/Engine.d.ts +114 -0
  11. package/dist/Engine.js +321 -0
  12. package/dist/Engine.js.map +1 -0
  13. package/dist/GameObject.d.ts +41 -0
  14. package/dist/GameObject.js +35 -0
  15. package/dist/GameObject.js.map +1 -0
  16. package/dist/Input.d.ts +27 -0
  17. package/dist/Input.js +99 -0
  18. package/dist/Input.js.map +1 -0
  19. package/dist/Line.d.ts +41 -0
  20. package/dist/Line.js +40 -0
  21. package/dist/Line.js.map +1 -0
  22. package/dist/Loader.d.ts +26 -0
  23. package/dist/Loader.js +76 -0
  24. package/dist/Loader.js.map +1 -0
  25. package/dist/Physics.d.ts +16 -0
  26. package/dist/Physics.js +77 -0
  27. package/dist/Physics.js.map +1 -0
  28. package/dist/Rectangle.d.ts +41 -0
  29. package/dist/Rectangle.js +44 -0
  30. package/dist/Rectangle.js.map +1 -0
  31. package/dist/Scene.d.ts +19 -0
  32. package/dist/Scene.js +27 -0
  33. package/dist/Scene.js.map +1 -0
  34. package/dist/Sprite.d.ts +76 -0
  35. package/dist/Sprite.js +129 -0
  36. package/dist/Sprite.js.map +1 -0
  37. package/dist/Text.d.ts +80 -0
  38. package/dist/Text.js +110 -0
  39. package/dist/Text.js.map +1 -0
  40. package/dist/Tilemap.d.ts +46 -0
  41. package/dist/Tilemap.js +48 -0
  42. package/dist/Tilemap.js.map +1 -0
  43. package/dist/Vector2.d.ts +26 -0
  44. package/dist/Vector2.js +21 -0
  45. package/dist/Vector2.js.map +1 -0
  46. package/dist/index.d.ts +15 -0
  47. package/dist/index.js +16 -0
  48. package/dist/index.js.map +1 -0
  49. package/package.json +18 -0
  50. package/src/Camera.ts +47 -0
  51. package/src/Canvas.ts +24 -0
  52. package/src/Circle.ts +88 -0
  53. package/src/Engine.ts +407 -0
  54. package/src/GameObject.ts +56 -0
  55. package/src/Input.ts +117 -0
  56. package/src/Line.ts +75 -0
  57. package/src/Loader.ts +66 -0
  58. package/src/Physics.ts +77 -0
  59. package/src/Rectangle.ts +83 -0
  60. package/src/Scene.ts +30 -0
  61. package/src/Sprite.ts +194 -0
  62. package/src/Text.ts +187 -0
  63. package/src/Tilemap.ts +95 -0
  64. package/src/Vector2.ts +36 -0
  65. package/src/index.ts +15 -0
  66. package/tsconfig.json +16 -0
package/src/Input.ts ADDED
@@ -0,0 +1,117 @@
1
+ import Vector2 from './Vector2.js';
2
+ import Engine from './Engine.js';
3
+
4
+ /**
5
+ * Handles mouse and keyboard input events.
6
+ */
7
+ export default class Input {
8
+ private static mousePosition: Vector2 = new Vector2(0, 0);
9
+ private static clickListeners: Set<(pos: Vector2) => void> = new Set();
10
+ private static keys: Set<string> = new Set();
11
+ private static isInitialized = false;
12
+
13
+ /**
14
+ * Initializes input event listeners.
15
+ */
16
+ static init() {
17
+ if (this.isInitialized) return;
18
+ this.isInitialized = true;
19
+
20
+ document.addEventListener('mousemove', (event: MouseEvent) => {
21
+ this.mousePosition.x = event.clientX;
22
+ this.mousePosition.y = event.clientY;
23
+ });
24
+
25
+ document.addEventListener('click', (event: MouseEvent) => {
26
+ if ((event.target as HTMLElement).tagName !== 'CANVAS') return;
27
+
28
+ const pos = new Vector2(this.mouseX, this.mouseY);
29
+
30
+ // If debug mode is on, try to select an object
31
+ if (Engine.debug) {
32
+ const objects = Engine.currentScene ? Engine.currentScene.objects : Engine.objects;
33
+ let found = false;
34
+
35
+ // Check objects from top to bottom (zIndex)
36
+ const sorted = Array.from(objects).sort((a, b) => (b.zIndex > a.zIndex ? 1 : -1));
37
+
38
+ for (const obj of sorted) {
39
+ if (
40
+ pos.x > obj.position.x &&
41
+ pos.x < obj.position.x + obj.width &&
42
+ pos.y > obj.position.y &&
43
+ pos.y < obj.position.y + obj.height
44
+ ) {
45
+ Engine.selectedObject = obj;
46
+ found = true;
47
+ break;
48
+ }
49
+ }
50
+
51
+ if (!found) Engine.selectedObject = null;
52
+ }
53
+
54
+ if (!Engine.paused) {
55
+ this.clickListeners.forEach((listener) => listener(pos));
56
+ }
57
+ });
58
+
59
+ document.addEventListener('mousedown', (event: MouseEvent) => {
60
+ if ((event.target as HTMLElement).tagName !== 'CANVAS') return;
61
+ this.keys.add(`mouse${event.button}`);
62
+ });
63
+
64
+ document.addEventListener('mouseup', (event: MouseEvent) => {
65
+ this.keys.delete(`mouse${event.button}`);
66
+ });
67
+
68
+ document.addEventListener('keydown', (event: KeyboardEvent) => {
69
+ this.keys.add(event.key.toLowerCase());
70
+
71
+ // Toggle Pause with P
72
+ if (event.key === 'p' || event.key === 'P') {
73
+ Engine.paused = !Engine.paused;
74
+
75
+ if (Engine.paused) {
76
+ document.getElementById('canvas')!.style.cursor = 'default';
77
+ }
78
+ }
79
+ });
80
+
81
+ document.addEventListener('keyup', (event: KeyboardEvent) => {
82
+ this.keys.delete(event.key.toLowerCase());
83
+ });
84
+
85
+ window.addEventListener('blur', () => {
86
+ this.keys.clear();
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Checks if a specific key is currently held down.
92
+ * @param key The key to check (e.g., 'w', 'ArrowUp', ' ').
93
+ */
94
+ static isKeyDown(key: string): boolean {
95
+ return this.keys.has(key.toLowerCase());
96
+ }
97
+
98
+ /** Current mouse x position in world space. */
99
+ static get mouseX() {
100
+ return (this.mousePosition.x / Engine.camera.zoom) + Engine.camera.position.x;
101
+ }
102
+
103
+ /** Current mouse y position in world space. */
104
+ static get mouseY() {
105
+ return (this.mousePosition.y / Engine.camera.zoom) + Engine.camera.position.y;
106
+ }
107
+
108
+ /** Adds a global click listener. */
109
+ static addClickListener(listener: (pos: Vector2) => void) {
110
+ this.clickListeners.add(listener);
111
+ }
112
+
113
+ /** Removes a global click listener. */
114
+ static removeClickListener(listener: (pos: Vector2) => void) {
115
+ this.clickListeners.delete(listener);
116
+ }
117
+ }
package/src/Line.ts ADDED
@@ -0,0 +1,75 @@
1
+ import GameObject from './GameObject.js';
2
+ import Vector2 from './Vector2.js';
3
+
4
+ /**
5
+ * Configuration for creating a Line object.
6
+ */
7
+ export interface LineProperties {
8
+ /** Unique tag for identification. */
9
+ tag: string;
10
+ /** Stroke width of the line. */
11
+ width: number;
12
+ /** Start point of the line. */
13
+ p1: Vector2;
14
+ /** End point of the line. */
15
+ p2: Vector2;
16
+ /** Render order (lower is background). */
17
+ zIndex: number;
18
+ }
19
+
20
+ const defaultProps = {
21
+ tag: 'line',
22
+ width: 1,
23
+ zIndex: 0,
24
+ p1: new Vector2(),
25
+ p2: new Vector2(),
26
+ };
27
+
28
+ /**
29
+ * A basic line object that can be drawn between two points.
30
+ */
31
+ export default class Line extends GameObject {
32
+ /** Stroke width of the line. */
33
+ strokeWidth: number;
34
+ /** Start x coordinate. */
35
+ x1: number;
36
+ /** Start y coordinate. */
37
+ y1: number;
38
+ /** End x coordinate. */
39
+ x2: number;
40
+ /** End y coordinate. */
41
+ y2: number;
42
+
43
+ /** Gets the starting position of the line. */
44
+ get position() { return new Vector2(this.x1, this.y1); }
45
+ /** Gets the bounding box width of the line. */
46
+ get width() { return Math.abs(this.x2 - this.x1); }
47
+ /** Gets the bounding box height of the line. */
48
+ get height() { return Math.abs(this.y2 - this.y1); }
49
+
50
+ constructor(props: LineProperties) {
51
+ super(props.tag || defaultProps.tag, props.zIndex || defaultProps.zIndex);
52
+
53
+ const defaultedProps = {
54
+ ...defaultProps,
55
+ ...props,
56
+ };
57
+
58
+ this.strokeWidth = defaultedProps.width;
59
+ this.x1 = defaultedProps.p1.x;
60
+ this.y1 = defaultedProps.p1.y;
61
+ this.x2 = defaultedProps.p2.x;
62
+ this.y2 = defaultedProps.p2.y;
63
+
64
+ this.registerSelf();
65
+ }
66
+
67
+ /** Draws the line onto the provided rendering context. */
68
+ draw(ctx: CanvasRenderingContext2D) {
69
+ if (!this.visible) return;
70
+ ctx.lineWidth = this.strokeWidth;
71
+ ctx.moveTo(this.x1, this.y1);
72
+ ctx.lineTo(this.x2, this.y2);
73
+ ctx.stroke();
74
+ }
75
+ }
package/src/Loader.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Handles asynchronous loading of assets like images.
3
+ */
4
+ export default class ResourceLoader {
5
+ private static assets: Map<string, HTMLImageElement> = new Map();
6
+ private static loadingQueue: Set<string> = new Set();
7
+ private static totalToLoad: number = 0;
8
+ private static loadedCount: number = 0;
9
+
10
+ /**
11
+ * Queue an image for loading.
12
+ * @param tag Unique tag to reference the image later.
13
+ * @param src Path to the image file.
14
+ */
15
+ static queueImage(tag: string, src: string) {
16
+ if (this.assets.has(tag)) return;
17
+ this.loadingQueue.add(JSON.stringify({ tag, src }));
18
+ this.totalToLoad++;
19
+ }
20
+
21
+ /**
22
+ * Start loading all queued assets.
23
+ * @param onProgress Optional callback for loading progress updates.
24
+ */
25
+ static async loadAll(onProgress?: (percent: number) => void): Promise<void> {
26
+ if (this.loadingQueue.size === 0) return Promise.resolve();
27
+
28
+ const promises = Array.from(this.loadingQueue).map(item => {
29
+ const { tag, src } = JSON.parse(item);
30
+ return new Promise<void>((resolve, reject) => {
31
+ const img = new Image();
32
+ img.src = src;
33
+ img.onload = () => {
34
+ this.assets.set(tag, img);
35
+ this.loadedCount++;
36
+ if (onProgress) {
37
+ onProgress((this.loadedCount / this.totalToLoad) * 100);
38
+ }
39
+ resolve();
40
+ };
41
+ img.onerror = () => {
42
+ console.error(`Failed to load asset: ${src}`);
43
+ reject();
44
+ };
45
+ });
46
+ });
47
+
48
+ await Promise.all(promises);
49
+ this.loadingQueue.clear();
50
+ this.totalToLoad = 0;
51
+ this.loadedCount = 0;
52
+ }
53
+
54
+ /**
55
+ * Get a loaded asset by its tag.
56
+ * @param tag The tag used when queuing the asset.
57
+ * @returns The loaded HTMLImageElement.
58
+ */
59
+ static getImage(tag: string): HTMLImageElement {
60
+ const asset = this.assets.get(tag);
61
+ if (!asset) {
62
+ throw new Error(`Asset not found: ${tag}. Make sure it is queued and loaded.`);
63
+ }
64
+ return asset;
65
+ }
66
+ }
package/src/Physics.ts ADDED
@@ -0,0 +1,77 @@
1
+ import Vector2 from './Vector2.js';
2
+ import GameObject from './GameObject.js';
3
+ import Circle from './Circle.js';
4
+
5
+ /**
6
+ * Utility class for collision detection between game objects.
7
+ */
8
+ export default class Physics {
9
+ /**
10
+ * Checks if two game objects are colliding.
11
+ /**
12
+ * Checks if two game objects are colliding and resolves it if not static.
13
+ * @param obj1 First object.
14
+ * @param obj2 Second object.
15
+ * @returns True if the objects are colliding.
16
+ */
17
+ static checkCollision(obj1: GameObject, obj2: GameObject): boolean {
18
+ const isCircle1 = obj1 instanceof Circle;
19
+ const isCircle2 = obj2 instanceof Circle;
20
+ let isColliding = false;
21
+
22
+ if (isCircle1 && isCircle2) {
23
+ const c1 = obj1 as Circle;
24
+ const c2 = obj2 as Circle;
25
+ isColliding = Vector2.distance(c1.center, c2.center) < c1.radius + c2.radius;
26
+ } else if (isCircle1 || isCircle2) {
27
+ const circle = (isCircle1 ? obj1 : obj2) as Circle;
28
+ const rect = (isCircle1 ? obj2 : obj1);
29
+ const closestX = Math.max(rect.position.x, Math.min(circle.center.x, rect.position.x + rect.width));
30
+ const closestY = Math.max(rect.position.y, Math.min(circle.center.y, rect.position.y + rect.height));
31
+ isColliding = Vector2.distance(circle.center, new Vector2(closestX, closestY)) < circle.radius;
32
+ } else {
33
+ isColliding = (
34
+ obj1.position.x < obj2.position.x + obj2.width &&
35
+ obj1.position.x + obj1.width > obj2.position.x &&
36
+ obj1.position.y < obj2.position.y + obj2.height &&
37
+ obj1.position.y + obj1.height > obj2.position.y
38
+ );
39
+ }
40
+
41
+ if (isColliding && (!obj1.isStatic || !obj2.isStatic)) {
42
+ this.resolveCollision(obj1, obj2);
43
+ }
44
+
45
+ return isColliding;
46
+ }
47
+ private static resolveCollision(obj1: GameObject, obj2: GameObject) {
48
+ // Calculate overlap
49
+ const overlapX = Math.min(
50
+ obj1.position.x + obj1.width - obj2.position.x,
51
+ obj2.position.x + obj2.width - obj1.position.x
52
+ );
53
+ const overlapY = Math.min(
54
+ obj1.position.y + obj1.height - obj2.position.y,
55
+ obj2.position.y + obj2.height - obj1.position.y
56
+ );
57
+
58
+ // Push apart
59
+ if (overlapX < overlapY) {
60
+ if (obj1.position.x < obj2.position.x) {
61
+ if (!obj1.isStatic) obj1.position.x -= overlapX / 2;
62
+ if (!obj2.isStatic) obj2.position.x += overlapX / 2;
63
+ } else {
64
+ if (!obj1.isStatic) obj1.position.x += overlapX / 2;
65
+ if (!obj2.isStatic) obj2.position.x -= overlapX / 2;
66
+ }
67
+ } else {
68
+ if (obj1.position.y < obj2.position.y) {
69
+ if (!obj1.isStatic) obj1.position.y -= overlapY / 2;
70
+ if (!obj2.isStatic) obj2.position.y += overlapY / 2;
71
+ } else {
72
+ if (!obj1.isStatic) obj1.position.y += overlapY / 2;
73
+ if (!obj2.isStatic) obj2.position.y -= overlapY / 2;
74
+ }
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,83 @@
1
+ import Vector2 from './Vector2.js';
2
+ import GameObject from './GameObject.js';
3
+
4
+ /**
5
+ * Configuration for creating a Rectangle object.
6
+ */
7
+ export interface RectProps {
8
+ /** Unique tag for identification. */
9
+ tag: string;
10
+ /** Initial position of the top-left corner. */
11
+ position: Vector2;
12
+ /** Width of the rectangle. */
13
+ width: number;
14
+ /** Height of the rectangle. */
15
+ height: number;
16
+ /** Fill colour. */
17
+ colour: string;
18
+ /** Render order. */
19
+ zIndex: number;
20
+ }
21
+
22
+ const defaultProps = {
23
+ tag: 'rect', colour: 'black', zIndex: 0,
24
+ }
25
+
26
+ /**
27
+ * A basic rectangle shape that can be drawn to the screen.
28
+ */
29
+ export default class Rectangle extends GameObject {
30
+ private _position: Vector2;
31
+ private _width: number;
32
+ private _height: number;
33
+ /** Fill colour. */
34
+ colour: string;
35
+
36
+ /** Gets or sets the top-left position. */
37
+ get position() { return this._position; }
38
+ set position(val) { this._position = val; }
39
+ /** Gets or sets the width. */
40
+ get width() { return this._width; }
41
+ set width(val) { this._width = val; }
42
+ /** Gets or sets the height. */
43
+ get height() { return this._height; }
44
+ set height(val) { this._height = val; }
45
+
46
+ constructor(props: RectProps) {
47
+ super(props.tag || defaultProps.tag, props.zIndex || defaultProps.zIndex);
48
+
49
+ const defaultedProps = {
50
+ ...defaultProps,
51
+ ...props
52
+ }
53
+
54
+ if (!(defaultedProps.position instanceof Vector2)) {
55
+ throw new Error('"position" must be a Vector2!');
56
+ }
57
+
58
+ if (!defaultedProps.width || !defaultedProps.height) {
59
+ throw new Error('You must provide a width and height for Rectangle');
60
+ }
61
+
62
+ this.tag = defaultedProps.tag;
63
+ this._position = defaultedProps.position;
64
+ this._width = defaultedProps.width;
65
+ this._height = defaultedProps.height;
66
+ this.colour = defaultedProps.colour;
67
+ this.zIndex = defaultedProps.zIndex;
68
+
69
+ this.registerSelf();
70
+ }
71
+
72
+ /** Draws the rectangle onto the provided rendering context. */
73
+ draw(ctx: CanvasRenderingContext2D) {
74
+ if (!this.visible) return;
75
+ ctx.fillStyle = this.colour;
76
+ ctx.fillRect(
77
+ this.position.x,
78
+ this.position.y,
79
+ this.width,
80
+ this.height
81
+ );
82
+ }
83
+ }
package/src/Scene.ts ADDED
@@ -0,0 +1,30 @@
1
+ import GameObject from './GameObject.js';
2
+
3
+ /**
4
+ * Represents a separate world or level in the game.
5
+ * Scenes manage their own set of game objects and lifecycle.
6
+ */
7
+ export default abstract class Scene {
8
+ /** The set of game objects currently in the scene. */
9
+ public objects: Set<GameObject> = new Set();
10
+
11
+ /** Called when the scene is loaded. */
12
+ onLoad(): void {}
13
+ /** Called every frame to update the scene. */
14
+ update(): void {}
15
+
16
+ /** Adds an object to the scene. */
17
+ add(object: GameObject) {
18
+ this.objects.add(object);
19
+ }
20
+
21
+ /** Removes an object from the scene. */
22
+ remove(object: GameObject) {
23
+ this.objects.delete(object);
24
+ }
25
+
26
+ /** Clears all objects from the scene. */
27
+ clear() {
28
+ this.objects.clear();
29
+ }
30
+ }
package/src/Sprite.ts ADDED
@@ -0,0 +1,194 @@
1
+ import Engine from './Engine.js';
2
+ import GameObject from './GameObject.js';
3
+ import Vector2 from './Vector2.js';
4
+ import ResourceLoader from './Loader.js';
5
+
6
+ /**
7
+ * Properties for creating a new Sprite.
8
+ */
9
+ export interface SpriteProps {
10
+ /** Unique tag for the object. */
11
+ tag: string;
12
+ /** Image element or tag from ResourceLoader. */
13
+ img: HTMLImageElement | string;
14
+ /** Number of rows in the spritesheet. */
15
+ rows: number;
16
+ /** Number of columns in the spritesheet. */
17
+ cols: number;
18
+ /** Initial world position. */
19
+ position: Vector2;
20
+ /** Starting frame column for animation. */
21
+ startCol: number;
22
+ /** Ending frame column for animation. */
23
+ endCol: number;
24
+ /** Rendering order. */
25
+ zIndex: number;
26
+ }
27
+
28
+ /**
29
+ * Represents an animated sprite using a spritesheet.
30
+ */
31
+ export default class Sprite extends GameObject {
32
+ /** The source image for the sprite. */
33
+ img: HTMLImageElement;
34
+ /** Number of rows in the spritesheet. */
35
+ rows: number;
36
+ /** Number of columns in the spritesheet. */
37
+ cols: number;
38
+ /** The world position of the sprite. */
39
+ position: Vector2;
40
+ /** The starting column for the current animation loop. */
41
+ startCol: number;
42
+ /** The ending column for the current animation loop. */
43
+ endCol: number;
44
+ /** The pixel width of a single animation frame (calculated automatically). */
45
+ frameWidth: number = 0;
46
+ /** The pixel height of a single animation frame (calculated automatically). */
47
+ frameHeight: number = 0;
48
+ /** Whether the sprite is currently registered with the engine. */
49
+ registered: boolean = false;
50
+ /** The current frame index being displayed. */
51
+ currentFrame: number = 0;
52
+ /** Whether the sprite is horizontally flipped. */
53
+ flip: boolean = false;
54
+ /** Duration of each animation frame in milliseconds. */
55
+ frameDuration: number = 100;
56
+ #lastFrameUpdate: number = Date.now();
57
+
58
+ /**
59
+ * Gets the display width of the sprite (scaled by 3).
60
+ */
61
+ get width(): number {
62
+ return this.frameWidth * 3;
63
+ }
64
+
65
+ /**
66
+ * Gets the display height of the sprite (scaled by 3).
67
+ */
68
+ get height(): number {
69
+ return this.frameHeight * 3;
70
+ }
71
+
72
+ constructor(props: SpriteProps) {
73
+ if (!props.tag) {
74
+ throw new Error('You must provide a tag for a Sprite');
75
+ }
76
+
77
+ super(props.tag, props.zIndex || 1);
78
+
79
+ if (typeof props.img === 'string') {
80
+ this.img = ResourceLoader.getImage(props.img);
81
+ } else {
82
+ this.img = props.img;
83
+ }
84
+
85
+ this.rows = props.rows;
86
+ this.cols = props.cols;
87
+ this.position = props.position;
88
+ this.startCol = props.startCol;
89
+ this.endCol = props.endCol;
90
+ this.currentFrame = props.startCol || 0;
91
+
92
+ const setDimensions = () => {
93
+ this.frameWidth = this.img.width / this.cols;
94
+ this.frameHeight = this.img.height / this.rows;
95
+ };
96
+
97
+ if (this.img.complete) {
98
+ setDimensions();
99
+ } else {
100
+ this.img.addEventListener('load', setDimensions, { once: true });
101
+ }
102
+
103
+ this.registered = false;
104
+ }
105
+
106
+ /**
107
+ * Main rendering method for the sprite.
108
+ * Handles frame calculation and horizontal flipping.
109
+ * @param ctx The canvas 2D rendering context.
110
+ */
111
+ draw(ctx: CanvasRenderingContext2D) {
112
+ if (!this.visible || !this.frameWidth || !this.frameHeight) return;
113
+
114
+ const now = Date.now();
115
+ if (this.registered && now - this.#lastFrameUpdate > this.frameDuration) {
116
+ this.currentFrame += 1;
117
+ this.#lastFrameUpdate = now;
118
+ }
119
+
120
+ const {
121
+ img,
122
+ cols,
123
+ frameWidth,
124
+ frameHeight,
125
+ position,
126
+ startCol,
127
+ endCol,
128
+ } = this;
129
+
130
+ ctx.imageSmoothingEnabled = true;
131
+ ctx.imageSmoothingQuality = 'high';
132
+
133
+ const maxFrame = endCol - 1;
134
+
135
+ if (this.currentFrame < startCol) {
136
+ this.currentFrame = startCol;
137
+ }
138
+
139
+ if (this.currentFrame > maxFrame) {
140
+ this.currentFrame = startCol;
141
+ }
142
+
143
+ // Update rows and columns
144
+ const column = this.currentFrame % cols;
145
+ const row = Math.floor(this.currentFrame / cols);
146
+
147
+ ctx.save();
148
+ if (this.flip) {
149
+ ctx.translate(position.x + (frameWidth * 3), position.y);
150
+ ctx.scale(-1, 1);
151
+ ctx.drawImage(
152
+ img,
153
+ column * frameWidth,
154
+ row * frameHeight,
155
+ frameWidth,
156
+ frameHeight,
157
+ 0,
158
+ 0,
159
+ frameWidth * 3,
160
+ frameHeight * 3
161
+ );
162
+ } else {
163
+ ctx.drawImage(
164
+ img,
165
+ column * frameWidth,
166
+ row * frameHeight,
167
+ frameWidth,
168
+ frameHeight,
169
+ position.x,
170
+ position.y,
171
+ frameWidth * 3,
172
+ frameHeight * 3
173
+ );
174
+ }
175
+ ctx.restore();
176
+ }
177
+
178
+ /**
179
+ * Registers the sprite with the engine and starts its animation loop.
180
+ */
181
+ play() {
182
+ if (!this.registered) {
183
+ Engine.registerObject(this);
184
+ this.registered = true;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Stops the sprite's animation and removes it from the engine.
190
+ */
191
+ stop() {
192
+ Engine.destroyObject(this);
193
+ }
194
+ }