From fa2b7e9b18977422eceefaa10c0de6df47669a1b Mon Sep 17 00:00:00 2001 From: oliver Date: Thu, 22 Jan 2026 22:23:08 +0100 Subject: [PATCH 1/2] feat: refactor AudioManager and ColliderManager to use globalThis for singleton pattern and enhance TgSpriteMap with collider registration and rendering modes --- package.json | 2 +- src/components.d.ts | 46 ++- src/components/engine/tg-collider/readme.md | 4 +- .../engine/tg-collider/tg-collider.tsx | 26 +- src/components/engine/tg-sprite-map/readme.md | 26 +- .../tg-sprite-map/test/tg-sprite-map.spec.tsx | 353 ++++++++++-------- .../engine/tg-sprite-map/tg-sprite-map.scss | 26 ++ .../engine/tg-sprite-map/tg-sprite-map.tsx | 328 +++++++++++++--- .../entities/entity-player/entity-player.tsx | 16 +- .../example/example-page/example-page.tsx | 14 +- src/index.ts | 3 +- src/interfaces/ITile.ts | 6 +- src/manager/audio.manager.ts | 11 +- src/manager/collider.manager.ts | 97 ++++- 14 files changed, 693 insertions(+), 265 deletions(-) diff --git a/package.json b/package.json index 288e93d..8e8b3bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tg-engine", - "version": "0.1.7", + "version": "0.1.8", "description": "a small sprite game engine for typescript, build with stenciljs, ALPHA VERSION", "main": "dist/index.cjs.js", "module": "dist/index.js", diff --git a/src/components.d.ts b/src/components.d.ts index ac5082f..0faa0e3 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -6,15 +6,11 @@ */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; import { IVector2 } from "./interfaces/IVector2"; -import { ICollider } from "./interfaces/ICollider"; -import { IParticleConfig } from "./interfaces"; +import { ICollider, IParticleConfig, ITile, IVector2 as IVector21 } from "./interfaces"; import { IAnimation } from "./interfaces/IAnimation"; -import { ITile } from "./interfaces/ITile"; export { IVector2 } from "./interfaces/IVector2"; -export { ICollider } from "./interfaces/ICollider"; -export { IParticleConfig } from "./interfaces"; +export { ICollider, IParticleConfig, ITile, IVector2 as IVector21 } from "./interfaces"; export { IAnimation } from "./interfaces/IAnimation"; -export { ITile } from "./interfaces/ITile"; export namespace Components { interface ExampleAnimator { } @@ -48,7 +44,7 @@ export namespace Components { /** * @default new Vector2(0, 0) */ - "vector": IVector2; + "vector": IVector21; } interface ExampleEntityWall { /** @@ -83,7 +79,7 @@ export namespace Components { "width": number; } interface TgCollider { - "checkCollisionOnPosition": (x: number, y: number, width: number, height: number) => Promise; + "checkCollisionOnPosition": (x: number, y: number, width: number, height: number) => Promise; /** * @default false */ @@ -275,6 +271,14 @@ export namespace Components { "state": 'running' | 'paused'; } interface TgSpriteMap { + /** + * @default '' + */ + "colliderMapId": string; + /** + * @default false + */ + "debugColliders": boolean; /** * @default 1 */ @@ -283,6 +287,14 @@ export namespace Components { * @default 5 */ "height": number; + /** + * @default false + */ + "registerColliders": boolean; + /** + * @default 'canvas' + */ + "renderMode": 'dom' | 'canvas'; /** * @default 1 */ @@ -594,7 +606,7 @@ declare namespace LocalJSX { /** * @default new Vector2(0, 0) */ - "vector"?: IVector2; + "vector"?: IVector21; } interface ExampleEntityWall { /** @@ -830,6 +842,14 @@ declare namespace LocalJSX { "state"?: 'running' | 'paused'; } interface TgSpriteMap { + /** + * @default '' + */ + "colliderMapId"?: string; + /** + * @default false + */ + "debugColliders"?: boolean; /** * @default 1 */ @@ -838,6 +858,14 @@ declare namespace LocalJSX { * @default 5 */ "height"?: number; + /** + * @default false + */ + "registerColliders"?: boolean; + /** + * @default 'canvas' + */ + "renderMode"?: 'dom' | 'canvas'; /** * @default 1 */ diff --git a/src/components/engine/tg-collider/readme.md b/src/components/engine/tg-collider/readme.md index 1ba709f..39139ee 100644 --- a/src/components/engine/tg-collider/readme.md +++ b/src/components/engine/tg-collider/readme.md @@ -31,7 +31,7 @@ ## Methods -### `checkCollisionOnPosition(x: number, y: number, width: number, height: number) => Promise` +### `checkCollisionOnPosition(x: number, y: number, width: number, height: number) => Promise` @@ -46,7 +46,7 @@ #### Returns -Type: `Promise` +Type: `Promise` diff --git a/src/components/engine/tg-collider/tg-collider.tsx b/src/components/engine/tg-collider/tg-collider.tsx index f8fb040..fcc6986 100644 --- a/src/components/engine/tg-collider/tg-collider.tsx +++ b/src/components/engine/tg-collider/tg-collider.tsx @@ -1,6 +1,6 @@ import { Component, Element, h, Method,Event, EventEmitter, Host, Prop, ComponentInterface} from '@stencil/core'; import { ColliderManager } from '../../../manager/collider.manager'; -import { ICollider } from '../../../interfaces/ICollider'; +import { ICollider } from '../../../interfaces'; @Component({ tag: 'tg-collider', @@ -28,17 +28,17 @@ export class TgCollider implements ComponentInterface { } disconnectedCallback() { - this.manager.removeCollider(this); + this.manager.removeCollider(this.el); } - async checkCollisionOnName(name:string):Promise { - const a = this.el; - const colliders = this.manager.getColliders().filter(collider => collider.name === name); + async checkCollisionOnName(name: string): Promise { + const a = this.el; + const colliders = this.manager.getColliders().filter(collider => collider.name === name); - for(let i = 0; i < colliders.length; i++){ + for (let i = 0; i < colliders.length; i++) { const b = colliders[i]; - if(a.x < b.x + b.width && a.x + a.width > b.x && - a.y < b.y + b.height && a.y + a.height > b.y){ + if (a.x < b.x + b.width && a.x + a.width > b.x && + a.y < b.y + b.height && a.y + a.height > b.y) { return b; } } @@ -46,12 +46,12 @@ export class TgCollider implements ComponentInterface { } @Method() - async checkCollisionOnPosition(x:number,y:number,width:number,height:number):Promise { + async checkCollisionOnPosition(x: number, y: number, width: number, height: number): Promise { const a = this.el; - if(a.x < x + width && a.x + a.width > x && - a.y < y + height && a.y + a.height > y){ - return this; - }else{ + if (a.x < x + width && a.x + a.width > x && + a.y < y + height && a.y + a.height > y) { + return this.el; + } else { return null; } } diff --git a/src/components/engine/tg-sprite-map/readme.md b/src/components/engine/tg-sprite-map/readme.md index a452929..1dbadaf 100644 --- a/src/components/engine/tg-sprite-map/readme.md +++ b/src/components/engine/tg-sprite-map/readme.md @@ -7,17 +7,21 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ------------ | ------------- | ----------- | --------- | ------- | -| `hFrames` | `h-frames` | | `number` | `1` | -| `height` | `height` | | `number` | `5` | -| `scale` | `scale` | | `number` | `1` | -| `src` | `src` | | `string` | `''` | -| `tileHeight` | `tile-height` | | `number` | `16` | -| `tileWidth` | `tile-width` | | `number` | `16` | -| `tiles` | -- | | `ITile[]` | `[]` | -| `vFrames` | `v-frames` | | `number` | `1` | -| `width` | `width` | | `number` | `5` | +| Property | Attribute | Description | Type | Default | +| ------------------- | -------------------- | ----------- | ------------------- | ---------- | +| `colliderMapId` | `collider-map-id` | | `string` | `''` | +| `debugColliders` | `debug-colliders` | | `boolean` | `false` | +| `hFrames` | `h-frames` | | `number` | `1` | +| `height` | `height` | | `number` | `5` | +| `registerColliders` | `register-colliders` | | `boolean` | `false` | +| `renderMode` | `render-mode` | | `"canvas" \| "dom"` | `'canvas'` | +| `scale` | `scale` | | `number` | `1` | +| `src` | `src` | | `string` | `''` | +| `tileHeight` | `tile-height` | | `number` | `16` | +| `tileWidth` | `tile-width` | | `number` | `16` | +| `tiles` | -- | | `ITile[]` | `[]` | +| `vFrames` | `v-frames` | | `number` | `1` | +| `width` | `width` | | `number` | `5` | ## Dependencies diff --git a/src/components/engine/tg-sprite-map/test/tg-sprite-map.spec.tsx b/src/components/engine/tg-sprite-map/test/tg-sprite-map.spec.tsx index 69bbdb9..75883b9 100644 --- a/src/components/engine/tg-sprite-map/test/tg-sprite-map.spec.tsx +++ b/src/components/engine/tg-sprite-map/test/tg-sprite-map.spec.tsx @@ -5,186 +5,215 @@ import { TgSpriteAnimator } from '../../tg-sprite-animator/tg-sprite-animator'; import { h } from '@stencil/core'; describe('tg-sprite-map', () => { - it('renders with default props', async () => { - const page = await newSpecPage({ - components: [TgSpriteMap], - html: ``, + describe('canvas mode (default)', () => { + it('renders with default props', async () => { + const page = await newSpecPage({ + components: [TgSpriteMap], + html: ``, + }); + expect(page.root).toEqualHtml(` + + + + + + + `); }); - expect(page.root).toEqualHtml(` - - - - - `); - }); - it('renders with custom dimensions', async () => { - const page = await newSpecPage({ - components: [TgSpriteMap], - template: () => ( - - ), + it('renders with custom dimensions', async () => { + const page = await newSpecPage({ + components: [TgSpriteMap], + template: () => ( + + ), + }); + expect(page.root).toEqualHtml(` + + + + + + + `); }); - expect(page.root).toEqualHtml(` - - - - - `); - }); - it('renders with scale', async () => { - const page = await newSpecPage({ - components: [TgSpriteMap], - template: () => ( - - ), + it('renders with scale', async () => { + const page = await newSpecPage({ + components: [TgSpriteMap], + template: () => ( + + ), + }); + expect(page.root).toEqualHtml(` + + + + + + + `); }); - expect(page.root).toEqualHtml(` - - - - - `); }); - it('renders static tiles', async () => { - const tiles = [ - { x: 0, y: 0, frames: [0] }, - { x: 1, y: 0, frames: [1] }, - { x: 0, y: 1, frames: [2] } - ]; - - const page = await newSpecPage({ - components: [TgSpriteMap, TgSprite], - template: () => ( - - ), + describe('dom mode', () => { + it('renders with default props', async () => { + const page = await newSpecPage({ + components: [TgSpriteMap], + template: () => ( + + ), + }); + expect(page.root).toEqualHtml(` + + + + + `); }); - await page.waitForChanges(); - - const sprites = page.root.shadowRoot.querySelectorAll('tg-sprite'); - expect(sprites.length).toBe(3); - expect(sprites[0].style.position).toBe('absolute'); - expect(sprites[0].style.top).toBe('0px'); - expect(sprites[0].style.left).toBe('0px'); - }); - - it('renders animated tiles', async () => { - const tiles = [ - { - x: 0, - y: 0, - frames: [0, 1, 2], - duration: 100 - } - ]; - - const page = await newSpecPage({ - components: [TgSpriteMap, TgSprite, TgSpriteAnimator], - template: () => ( - - ), + it('renders static tiles', async () => { + const tiles = [ + { x: 0, y: 0, frames: [0] }, + { x: 1, y: 0, frames: [1] }, + { x: 0, y: 1, frames: [2] } + ]; + + const page = await newSpecPage({ + components: [TgSpriteMap, TgSprite], + template: () => ( + + ), + }); + + await page.waitForChanges(); + + const sprites = page.root.shadowRoot.querySelectorAll('tg-sprite'); + expect(sprites.length).toBe(3); + expect(sprites[0].style.position).toBe('absolute'); + expect(sprites[0].style.top).toBe('0px'); + expect(sprites[0].style.left).toBe('0px'); }); - await page.waitForChanges(); - - const animators = page.root.shadowRoot.querySelectorAll('tg-sprite-animator'); - expect(animators.length).toBe(1); - expect(animators[0].style.position).toBe('absolute'); - expect(animators[0].style.top).toBe('0px'); - expect(animators[0].style.left).toBe('0px'); - }); - - it('handles empty tiles array', async () => { - const page = await newSpecPage({ - components: [TgSpriteMap], - template: () => ( - - ), + it('renders animated tiles', async () => { + const tiles = [ + { + x: 0, + y: 0, + frames: [0, 1, 2], + duration: 100 + } + ]; + + const page = await newSpecPage({ + components: [TgSpriteMap, TgSprite, TgSpriteAnimator], + template: () => ( + + ), + }); + + await page.waitForChanges(); + + const animators = page.root.shadowRoot.querySelectorAll('tg-sprite-animator'); + expect(animators.length).toBe(1); + expect(animators[0].style.position).toBe('absolute'); + expect(animators[0].style.top).toBe('0px'); + expect(animators[0].style.left).toBe('0px'); }); - await page.waitForChanges(); + it('handles empty tiles array', async () => { + const page = await newSpecPage({ + components: [TgSpriteMap], + template: () => ( + + ), + }); - const sprites = page.root.shadowRoot.querySelectorAll('tg-sprite'); - const animators = page.root.shadowRoot.querySelectorAll('tg-sprite-animator'); - expect(sprites.length).toBe(0); - expect(animators.length).toBe(0); - }); + await page.waitForChanges(); - it('handles tiles with invalid positions', async () => { - const tiles = [ - { x: -1, y: 0, frames: [0] }, - { x: 0, y: -1, frames: [1] }, - { x: 10, y: 10, frames: [2] } - ]; - - const page = await newSpecPage({ - components: [TgSpriteMap, TgSprite], - template: () => ( - - ), + const sprites = page.root.shadowRoot.querySelectorAll('tg-sprite'); + const animators = page.root.shadowRoot.querySelectorAll('tg-sprite-animator'); + expect(sprites.length).toBe(0); + expect(animators.length).toBe(0); }); - await page.waitForChanges(); - - const sprites = page.root.shadowRoot.querySelectorAll('tg-sprite'); - expect(sprites.length).toBe(3); - expect(sprites[0].style.left).toBe('-16px'); - expect(sprites[1].style.top).toBe('-16px'); - expect(sprites[2].style.left).toBe('160px'); - expect(sprites[2].style.top).toBe('160px'); - }); - - it('handles tiles with empty frames array', async () => { - const tiles = [ - { x: 0, y: 0, frames: [] } - ]; - - const page = await newSpecPage({ - components: [TgSpriteMap, TgSprite], - template: () => ( - - ), + it('handles tiles with invalid positions', async () => { + const tiles = [ + { x: -1, y: 0, frames: [0] }, + { x: 0, y: -1, frames: [1] }, + { x: 10, y: 10, frames: [2] } + ]; + + const page = await newSpecPage({ + components: [TgSpriteMap, TgSprite], + template: () => ( + + ), + }); + + await page.waitForChanges(); + + const sprites = page.root.shadowRoot.querySelectorAll('tg-sprite'); + expect(sprites.length).toBe(3); + expect(sprites[0].style.left).toBe('-16px'); + expect(sprites[1].style.top).toBe('-16px'); + expect(sprites[2].style.left).toBe('160px'); + expect(sprites[2].style.top).toBe('160px'); }); - await page.waitForChanges(); - - const sprites = page.root.shadowRoot.querySelectorAll('tg-sprite'); - expect(sprites.length).toBe(1); - expect(sprites[0].style.position).toBe('absolute'); - expect(sprites[0].style.top).toBe('0px'); - expect(sprites[0].style.left).toBe('0px'); + it('handles tiles with empty frames array', async () => { + const tiles = [ + { x: 0, y: 0, frames: [] } + ]; + + const page = await newSpecPage({ + components: [TgSpriteMap, TgSprite], + template: () => ( + + ), + }); + + await page.waitForChanges(); + + const sprites = page.root.shadowRoot.querySelectorAll('tg-sprite'); + expect(sprites.length).toBe(1); + expect(sprites[0].style.position).toBe('absolute'); + expect(sprites[0].style.top).toBe('0px'); + expect(sprites[0].style.left).toBe('0px'); + }); }); }); diff --git a/src/components/engine/tg-sprite-map/tg-sprite-map.scss b/src/components/engine/tg-sprite-map/tg-sprite-map.scss index 62e20bc..1703cdc 100644 --- a/src/components/engine/tg-sprite-map/tg-sprite-map.scss +++ b/src/components/engine/tg-sprite-map/tg-sprite-map.scss @@ -2,3 +2,29 @@ display: block; position: relative; } + +.static-layer, +.animated-layer { + position: absolute; + top: 0; + left: 0; + image-rendering: pixelated; +} + +.static-layer { + z-index: 0; +} + +.animated-layer { + z-index: 1; + pointer-events: none; +} + +.debug-collider { + position: absolute; + border: 2px solid rgba(255, 0, 0, 0.7); + background-color: rgba(255, 0, 0, 0.2); + box-sizing: border-box; + z-index: 10; + pointer-events: none; +} diff --git a/src/components/engine/tg-sprite-map/tg-sprite-map.tsx b/src/components/engine/tg-sprite-map/tg-sprite-map.tsx index 2cb3411..e476599 100644 --- a/src/components/engine/tg-sprite-map/tg-sprite-map.tsx +++ b/src/components/engine/tg-sprite-map/tg-sprite-map.tsx @@ -1,5 +1,6 @@ -import { Component, Host, h, Prop } from '@stencil/core'; -import { ITile } from '../../../interfaces/ITile'; +import { Component, Host, h, Prop, State, Element, Watch } from '@stencil/core'; +import { ITile } from '../../../interfaces'; +import { ColliderManager } from '../../../manager/collider.manager'; @Component({ tag: 'tg-sprite-map', @@ -7,6 +8,8 @@ import { ITile } from '../../../interfaces/ITile'; shadow: true, }) export class TgSpriteMap { + @Element() el: HTMLElement; + @Prop() src: string = ''; @Prop() vFrames: number = 1; @Prop() hFrames: number = 1; @@ -16,63 +19,298 @@ export class TgSpriteMap { @Prop() tileHeight: number = 16; @Prop() scale: number = 1; @Prop() tiles: ITile[] = []; + @Prop() renderMode: 'dom' | 'canvas' = 'canvas'; + @Prop() registerColliders: boolean = false; + @Prop() colliderMapId: string = ''; + @Prop() debugColliders: boolean = false; + + @State() imageLoaded: boolean = false; + + private image: HTMLImageElement; + private staticCanvas: HTMLCanvasElement; + private animatedCanvas: HTMLCanvasElement; + private animationFrameId: number; + private animatedTiles: { tile: ITile; currentFrameIndex: number; lastFrameTime: number }[] = []; + + componentWillLoad() { + if (this.renderMode === 'canvas') { + this.loadImage(); + } + } + + componentDidLoad() { + if (this.renderMode === 'canvas' && this.imageLoaded) { + this.initCanvasRendering(); + } + if (this.registerColliders) { + this.registerTileColliders(); + } + } + + disconnectedCallback() { + if (this.animationFrameId) { + cancelAnimationFrame(this.animationFrameId); + } + if (this.registerColliders && this.colliderMapId) { + ColliderManager.getInstance().unregisterTileColliders(this.colliderMapId); + } + } + + @Watch('tiles') + tilesChanged() { + if (this.renderMode === 'canvas' && this.imageLoaded) { + this.initCanvasRendering(); + } + if (this.registerColliders) { + this.registerTileColliders(); + } + } + + @Watch('registerColliders') + registerCollidersChanged(newValue: boolean) { + if (newValue) { + this.registerTileColliders(); + } else if (this.colliderMapId) { + ColliderManager.getInstance().unregisterTileColliders(this.colliderMapId); + } + } + + private loadImage(): void { + if (typeof Image === 'undefined') { + return; + } + this.image = new Image(); + this.image.onload = () => { + this.imageLoaded = true; + this.initCanvasRendering(); + }; + this.image.src = this.src; + } + + private initCanvasRendering(): void { + if (!this.image || !this.imageLoaded) return; - /** - * Get the style for the tile - * @param tile - */ - getTileStyle(tile: ITile) { + this.staticCanvas = this.el.shadowRoot.querySelector('.static-layer') as HTMLCanvasElement; + this.animatedCanvas = this.el.shadowRoot.querySelector('.animated-layer') as HTMLCanvasElement; + + if (!this.staticCanvas || !this.animatedCanvas) return; + + const { staticTiles, animatedTiles } = this.categorizeTiles(); + + this.renderStaticTiles(staticTiles); + this.setupAnimatedTiles(animatedTiles); + } + + private categorizeTiles(): { staticTiles: ITile[]; animatedTiles: ITile[] } { + const staticTiles: ITile[] = []; + const animatedTiles: ITile[] = []; + + for (const tile of this.tiles) { + if (tile.frames?.length > 1) { + animatedTiles.push(tile); + } else { + staticTiles.push(tile); + } + } + + return { staticTiles, animatedTiles }; + } + + private getFrameSourceCoords(frame: number): { sx: number; sy: number } { + const effectiveHFrames = Math.max(1, this.hFrames); + const col = frame % effectiveHFrames; + const row = Math.floor(frame / effectiveHFrames); + return { + sx: col * this.tileWidth, + sy: row * this.tileHeight, + }; + } + + private renderStaticTiles(tiles: ITile[]): void { + const ctx = this.staticCanvas.getContext('2d'); + if (!ctx) return; + + ctx.imageSmoothingEnabled = false; + ctx.clearRect(0, 0, this.staticCanvas.width, this.staticCanvas.height); + + for (const tile of tiles) { + const frame = tile.frames?.[0] ?? 0; + const { sx, sy } = this.getFrameSourceCoords(frame); + const dx = tile.x * this.tileWidth * this.scale; + const dy = tile.y * this.tileHeight * this.scale; + const dw = this.tileWidth * this.scale; + const dh = this.tileHeight * this.scale; + + ctx.drawImage( + this.image, + sx, sy, this.tileWidth, this.tileHeight, + dx, dy, dw, dh + ); + } + } + + private setupAnimatedTiles(tiles: ITile[]): void { + if (this.animationFrameId) { + cancelAnimationFrame(this.animationFrameId); + } + + this.animatedTiles = tiles.map(tile => ({ + tile, + currentFrameIndex: 0, + lastFrameTime: performance.now(), + })); + + if (this.animatedTiles.length > 0) { + this.animateLoop(); + } + } + + private animateLoop = (): void => { + const ctx = this.animatedCanvas.getContext('2d'); + if (!ctx) return; + + ctx.imageSmoothingEnabled = false; + ctx.clearRect(0, 0, this.animatedCanvas.width, this.animatedCanvas.height); + + const now = performance.now(); + + for (const animData of this.animatedTiles) { + const { tile } = animData; + const duration = tile.duration ?? 100; + const elapsed = now - animData.lastFrameTime; + + if (elapsed >= duration) { + animData.currentFrameIndex = (animData.currentFrameIndex + 1) % tile.frames.length; + animData.lastFrameTime = now; + } + + const frame = tile.frames[animData.currentFrameIndex]; + const { sx, sy } = this.getFrameSourceCoords(frame); + const dx = tile.x * this.tileWidth * this.scale; + const dy = tile.y * this.tileHeight * this.scale; + const dw = this.tileWidth * this.scale; + const dh = this.tileHeight * this.scale; + + ctx.drawImage( + this.image, + sx, sy, this.tileWidth, this.tileHeight, + dx, dy, dw, dh + ); + } + + this.animationFrameId = requestAnimationFrame(this.animateLoop); + }; + + private registerTileColliders(): void { + if (!this.colliderMapId) return; + + const colliderManager = ColliderManager.getInstance(); + colliderManager.unregisterTileColliders(this.colliderMapId); + + const collidableTiles = this.tiles.filter(tile => tile.collidable); + if (collidableTiles.length === 0) return; + + const colliders = collidableTiles.map(tile => ({ + x: tile.x * this.tileWidth * this.scale, + y: tile.y * this.tileHeight * this.scale, + width: this.tileWidth * this.scale, + height: this.tileHeight * this.scale, + type: tile.colliderType ?? 'tile', + name: tile.colliderName ?? `tile-${tile.x}-${tile.y}`, + })); + + colliderManager.registerTileColliders(this.colliderMapId, colliders); + } + + private getTileStyle(tile: ITile) { return { position: 'absolute', top: `${tile.y * this.tileHeight * this.scale}px`, - left: `${tile.x * this.tileWidth * this.scale}px` + left: `${tile.x * this.tileWidth * this.scale}px`, }; } - /** - * Render an animated tile - * @param tile - * @param style - */ - renderAnimatedTile(tile: ITile, style: { [key: string]: string }) { - return {console.log(ev)}} animations={{ - 'default': { - frames: tile.frames, - duration: tile.duration, - }, - }} play="default" style={style}> - {this.renderTile(tile, null)} - - } - - /** - * Render a tile - * @param tile - * @param style - */ - renderTile(tile: ITile, style?: { [key: string]: string }) { - return + private renderAnimatedTileDom(tile: ITile, style: { [key: string]: string }) { + return ( + + {this.renderTileDom(tile, undefined)} + + ); + } + + private renderTileDom(tile: ITile, style?: { [key: string]: string }) { + return ( + + ); + } + + private renderDebugColliders() { + if (!this.debugColliders) return null; + + const collidableTiles = this.tiles.filter(tile => tile.collidable); + return collidableTiles.map(tile => ( +
+ )); } render() { + const mapWidth = this.tileWidth * this.scale * this.width; + const mapHeight = this.tileHeight * this.scale * this.height; + + if (this.renderMode === 'canvas') { + return ( + + + + {this.renderDebugColliders()} + + ); + } + return ( - + {this.tiles.map(tile => - tile.frames?.length > 1 ? this.renderAnimatedTile(tile, this.getTileStyle(tile)) : this.renderTile(tile, this.getTileStyle(tile)), + tile.frames?.length > 1 + ? this.renderAnimatedTileDom(tile, this.getTileStyle(tile)) + : this.renderTileDom(tile, this.getTileStyle(tile)) )} + {this.renderDebugColliders()} ); } - } diff --git a/src/components/example/example-game/entities/entity-player/entity-player.tsx b/src/components/example/example-game/entities/entity-player/entity-player.tsx index 0d2db2a..e054b3a 100644 --- a/src/components/example/example-game/entities/entity-player/entity-player.tsx +++ b/src/components/example/example-game/entities/entity-player/entity-player.tsx @@ -1,7 +1,19 @@ import { Event, Component, ComponentInterface, h, Host, Method, Prop, Element, EventEmitter } from '@stencil/core'; -import { IVector2 } from '../../../../../interfaces/IVector2'; +import { IVector2 } from '../../../../../interfaces'; import { Vector2 } from '../../../../../models/vector2/vector2'; +interface AnimationKeyframe { + frames: number[]; + duration: number; +} + +interface AnimationData { + hFrames: number; + vFrames: number; + keyframes: Record; + src: string; +} + @Component({ tag: 'example-entity-player', styleUrl: 'entity-player.scss', @@ -42,7 +54,7 @@ export class EntityPlayer implements ComponentInterface { src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAtdJREFUeJztWkuWozAMlOYNnKUP0ds+zmwm5yCbzGmbhWbRiGeMkOUP9oO4Nt2ElMpSjI0lAbw50P9gACCNMAuckqitvzE2ANA3qfowIqqDyHGghf56YREPDSLHgVb6v6UvIcoBJsW41YFvIhgRSfsVa+rvAnAkzve0QYRs5HDP0v9l+ZLlXkg8dK+VvvgIWISOQERZM6C2vhiAXOQ4UFt/fQRmABwNUzS0DaWilX7UVmLZg0vs4zX1T3kRulIA1zWAiSMiPKdJNPD38Qjt4wQA8O/51HwgUGZebf3dDAD4eVmQwM+o9hY2Iqp8/s6Rjdr6m11gBkAehL8gsdGj6bcsYquyz59/BqlO4VL6iAiDd5/56hoAsD1MpKy2ufwzbJ4xptug5wN88Z4PMOBO+YDdaTAVMefx0K9UUz8qAIhoOtKehTP0zQEoIZzjwFn65gDkHnFzHThLP+oRIKLkgZTIEZyhr+YDpPN5znlcc6CV/mYG+IPgg4NFvIQDrfVd0GuaaFj+vqaJwL51EfOJaLXBn1ttNNUfAMhHzN7NfNdOCr+G/tFUIAAxw2p99q7L//r4pK+PT3fKpC7fK3+xF81L4G7seH6kGckhO8G8JP8t0PMBvnjPBxjQ8wEC3iIfcEf0ALQeQGv0APA/vT9gwdXK27n6vT/AJ+bU53l/5SPoS7AzABxyS+n7/QF/Hg++v9MX+wPm5drPn1nr8xo/pT+AA8ol79j+AJfv64uGuJbvQhu474DGP3JAcsQF27Toa3xf37Sa1qzP916Ayuj5AF+85wMM6PkAAbfIB9yx/h+COANyavil6vi19MUA3LUXQMKmU7RUn3/LHoJYO8F8gFRyPtqGchoYWumr/QH+wSK2Ps8c/9pqo7a+i7Uu79XXLVjr81yeTu0PaKZfqj7v1uhb9AdY9NX+AIBdjT2qPn9h/sbQWqePKTWX6DEQbET3GTg2knD7/oD/ksvcybtF0+IAAAAASUVORK5CYII=', }, }; - private playedAnimation: any = this.animations[this.type]; + private playedAnimation: AnimationData = this.animations[this.type]; @Method() async getCollider() { diff --git a/src/components/example/example-page/example-page.tsx b/src/components/example/example-page/example-page.tsx index 639060c..692b011 100644 --- a/src/components/example/example-page/example-page.tsx +++ b/src/components/example/example-page/example-page.tsx @@ -1,5 +1,17 @@ import { Component, h, Host, State } from '@stencil/core'; +interface AnimationKeyframe { + frames: number[]; + duration: number; +} + +interface AnimationData { + hFrames: number; + vFrames: number; + keyframes: Record; + src: string; +} + @Component({ tag: 'example-page', styleUrl: 'example-page.scss', @@ -42,7 +54,7 @@ export class ExamplePage { }, }; - private playedAnimation: any = this.exampleAnimations[this.animatorType]; + private playedAnimation: AnimationData = this.exampleAnimations[this.animatorType]; private animator: HTMLTgSpriteAnimatorElement; private sprite: HTMLTgSpriteElement; diff --git a/src/index.ts b/src/index.ts index 828f3b5..c0783d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,8 @@ * to consume components of this package as outlined in the `README.md`. */ -export { ColliderManager } from './manager/collider.manager'; +export { ColliderManager, isTileCollider } from './manager/collider.manager'; +export type { TileColliderData, ColliderResult } from './manager/collider.manager'; export { AudioManager } from './manager/audio.manager'; export * from './interfaces'; export { Vector2 } from './models/vector2/vector2'; diff --git a/src/interfaces/ITile.ts b/src/interfaces/ITile.ts index 22b6428..6005b66 100644 --- a/src/interfaces/ITile.ts +++ b/src/interfaces/ITile.ts @@ -1,4 +1,8 @@ import { IAnimation } from './IAnimation'; import { IVector2 } from './IVector2'; -export interface ITile extends IVector2,IAnimation{} +export interface ITile extends IVector2, IAnimation { + collidable?: boolean; + colliderType?: string; + colliderName?: string; +} diff --git a/src/manager/audio.manager.ts b/src/manager/audio.manager.ts index e25b2d3..175d0c2 100644 --- a/src/manager/audio.manager.ts +++ b/src/manager/audio.manager.ts @@ -1,6 +1,10 @@ // Use globalThis to ensure singleton works across different bundles const AUDIO_MANAGER_KEY = '__TG_ENGINE_AUDIO_MANAGER__'; +interface GlobalWithAudioManager { + [AUDIO_MANAGER_KEY]?: AudioManager; +} + export class AudioManager { private audioPools: Map; @@ -10,10 +14,11 @@ export class AudioManager { // Singleton-Instanz public static getInstance(): AudioManager { - if (!(globalThis as any)[AUDIO_MANAGER_KEY]) { - (globalThis as any)[AUDIO_MANAGER_KEY] = new AudioManager(); + const global = globalThis as GlobalWithAudioManager; + if (!global[AUDIO_MANAGER_KEY]) { + global[AUDIO_MANAGER_KEY] = new AudioManager(); } - return (globalThis as any)[AUDIO_MANAGER_KEY]; + return global[AUDIO_MANAGER_KEY]; } /** diff --git a/src/manager/collider.manager.ts b/src/manager/collider.manager.ts index c7b6f04..8fd4135 100644 --- a/src/manager/collider.manager.ts +++ b/src/manager/collider.manager.ts @@ -1,18 +1,47 @@ -import { TgCollider } from '../components/engine/tg-collider/tg-collider'; - // Use globalThis to ensure singleton works across different bundles const COLLIDER_MANAGER_KEY = '__TG_ENGINE_COLLIDER_MANAGER__'; +interface GlobalWithColliderManager { + [COLLIDER_MANAGER_KEY]?: ColliderManager; +} + +export interface TileColliderData { + x: number; + y: number; + width: number; + height: number; + type: string; + name: string; + mapId: string; +} + +export interface ColliderResult { + x: number; + y: number; + width: number; + height: number; + type: string; + name: string; + el?: HTMLElement; + mapId?: string; +} + +export function isTileCollider(collider: ColliderResult): collider is TileColliderData & ColliderResult { + return 'mapId' in collider && collider.mapId !== undefined; +} + export class ColliderManager { - private colliders: any[] = []; + private colliders: HTMLTgColliderElement[] = []; + private tileColliders: TileColliderData[] = []; private constructor() {} public static getInstance(): ColliderManager { - if (!(globalThis as any)[COLLIDER_MANAGER_KEY]) { - (globalThis as any)[COLLIDER_MANAGER_KEY] = new ColliderManager(); + const global = globalThis as GlobalWithColliderManager; + if (!global[COLLIDER_MANAGER_KEY]) { + global[COLLIDER_MANAGER_KEY] = new ColliderManager(); } - return (globalThis as any)[COLLIDER_MANAGER_KEY]; + return global[COLLIDER_MANAGER_KEY]; } public getColliders() { @@ -23,22 +52,62 @@ export class ColliderManager { this.colliders.push(collider); } - removeCollider(collider: any) { + removeCollider(collider: HTMLTgColliderElement) { this.colliders = this.colliders.filter(c => c !== collider); } - public async checkCollisionOnPosition(x: number, y: number, width: number, height: number): Promise { - const results:TgCollider[] = []; + public getTileColliders(): TileColliderData[] { + return this.tileColliders; + } + + public registerTileColliders(mapId: string, colliders: Omit[]): void { + const newColliders = colliders.map(c => ({ ...c, mapId })); + this.tileColliders.push(...newColliders); + } + + public unregisterTileColliders(mapId: string): void { + this.tileColliders = this.tileColliders.filter(c => c.mapId !== mapId); + } + + private checkAABBCollision( + x1: number, y1: number, w1: number, h1: number, + x2: number, y2: number, w2: number, h2: number + ): boolean { + return x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && y1 + h1 > y2; + } + + public async checkCollisionOnPosition(x: number, y: number, width: number, height: number): Promise { + const results: ColliderResult[] = []; + + // Check component colliders for (let i = 0; i < this.colliders.length; i++) { - const collision = await this.colliders[i].checkCollisionOnPosition(x, y, width, height) - if(collision !== null) { - results.push(collision); + const collider = this.colliders[i]; + const collision = await collider.checkCollisionOnPosition(x, y, width, height); + if (collision !== null) { + results.push({ + x: collider.x, + y: collider.y, + width: collider.width, + height: collider.height, + type: collider.type, + name: collider.name, + el: collider, + }); } } - if(results.length > 0) { + + // Check tile colliders + for (let i = 0; i < this.tileColliders.length; i++) { + const tc = this.tileColliders[i]; + if (this.checkAABBCollision(x, y, width, height, tc.x, tc.y, tc.width, tc.height)) { + results.push(tc); + } + } + + if (results.length > 0) { return results; } - return + return undefined; } } From 4e1bc961130255c40d117f178137b2d275b00ef2 Mon Sep 17 00:00:00 2001 From: oliver Date: Thu, 22 Jan 2026 22:27:27 +0100 Subject: [PATCH 2/2] chore: bump version to 0.1.9 in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8e8b3bc..857a96f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tg-engine", - "version": "0.1.8", + "version": "0.1.9", "description": "a small sprite game engine for typescript, build with stenciljs, ALPHA VERSION", "main": "dist/index.cjs.js", "module": "dist/index.js",