UNPKG

pxt-common-packages

Version:
410 lines (355 loc) 14.6 kB
//% blockGap=8 namespace scene { /** * Package for color-coded tilemap blocks, to support existing curriculum. */ /** * Set the map for placing tiles in the scene * @param map * @param scale */ //% blockId=gamesettilemap block="set tile map to %map=tilemap_image_picker || with %scale pixel tiles" //% scale.defl=TileScale.Sixteen //% group="Color-coded Tilemap" //% help=color-coded-tilemap/set-tile-map export function setTileMap(map: Image, scale = TileScale.Sixteen) { const scene = game.currentScene(); if (!scene.tileMap || !(scene.tileMap as tiles.legacy.LegacyTilemap).isLegacy) { scene.tileMap = new tiles.legacy.LegacyTilemap(); } (scene.tileMap as tiles.legacy.LegacyTilemap).setMap(map); scene.tileMap.scale = scale; } /** * Set an image as a tile at the given index. Tiles should be a 16x16 image * @param index * @param img */ //% blockId=gamesettile block="set tile %index=colorindexpicker to %img=tile_image_picker with wall %wall=toggleOnOff" //% group="Color-coded Tilemap" //% help=color-coded-tilemap/set-tile export function setTile(index: number, img: Image, wall?: boolean) { const scene = game.currentScene(); if (!scene.tileMap || !(scene.tileMap as tiles.legacy.LegacyTilemap).isLegacy) { scene.tileMap = new tiles.legacy.LegacyTilemap(); } (scene.tileMap as tiles.legacy.LegacyTilemap).setTile(index, img, !!wall); } /** * Get the tile at a position in the tile map * @param col * @param row */ //% blockId=gamegettile block="tile col %col row %row" //% group="Color-coded Tilemap" blockSetVariable="myTile" //% help=color-coded-tilemap/get-tile export function getTile(col: number, row: number): tiles.Tile { const scene = game.currentScene(); if (!scene.tileMap || !(scene.tileMap as tiles.legacy.LegacyTilemap).isLegacy) { scene.tileMap = new tiles.legacy.LegacyTilemap(); } return (scene.tileMap as tiles.legacy.LegacyTilemap).getTileLegacy(col, row); } /** * Get all tiles in the tile map with the given index. * @param index */ //% blockId=gamegettilestype block="array of all %index=colorindexpicker tiles" //% group="Color-coded Tilemap" blockSetVariable="tile list" //% help=color-coded-tilemap/get-tiles-by-type export function getTilesByType(index: number): tiles.Tile[] { const scene = game.currentScene(); if (!scene.tileMap || !(scene.tileMap as tiles.legacy.LegacyTilemap).isLegacy) { scene.tileMap = new tiles.legacy.LegacyTilemap(); } return (scene.tileMap as tiles.legacy.LegacyTilemap).getTilesByTypeLegacy(index); } /** * Center the given sprite on a random tile that is the given color * @param sprite * @param color */ //% blockId=gameplaceonrandomtile block="place %sprite=variables_get(mySprite) on top of random $color tile" //% group="Color-coded Tilemap" //% color.shadow="colorindexpicker" //% help=color-coded-tilemap/place-on-random-tile export function placeOnRandomTile(sprite: Sprite, color: number): void { if (!sprite || !game.currentScene().tileMap) return; const tiles = getTilesByType(color); if (tiles.length > 0) Math.pickRandom(tiles).place(sprite); } /** * Set a tile at the given index * @param tile * @param index */ //% blockId=gamesettileat block="set %tile=gamegettile to %index=colorindexpicker" //% group="Color-coded Tilemap" //% help=color-coded-tilemap/set-tile-at export function setTileAt(tile: tiles.Tile, index: number) { const scene = game.currentScene(); if (!scene.tileMap || !(scene.tileMap as tiles.legacy.LegacyTilemap).isLegacy) { scene.tileMap = new tiles.legacy.LegacyTilemap(); } const tm: tiles.legacy.LegacyTilemap = scene.tileMap as tiles.legacy.LegacyTilemap; const scale = tm.scale; tm.setTileAt(tile.x >> scale, tile.y >> scale, index); } /** * Center the given sprite on this tile * @param sprite */ //% blockId=legacyplaceontile block="on top of %tile=variables_get(myTile) place %sprite=variables_get(mySprite)" //% group="Color-coded Tilemap" //% help=tiles/place export function place(tile: tiles.Tile, mySprite: Sprite): void { if (!tile) return; tile.place(mySprite); } /** * Run code when a certain kind of sprite hits a tile * @param direction * @param tile * @param handler */ //% group="Color-coded Tilemap" //% draggableParameters="reporter" //% blockId=spritesollisions block="on $sprite of kind $kind=spritekind hits wall $tile=colorindexpicker" //% help=color-coded-tilemap/on-hit-tile export function onHitTile(kind: number, tile: number, handler: (sprite: Sprite) => void) { if (kind == undefined || tile < 0 || tile > 0xF || !handler) return; const collisionHandlers = game.currentScene().collisionHandlers; if (!collisionHandlers[tile]) { collisionHandlers[tile] = []; } collisionHandlers[tile].push( new scene.SpriteHandler( kind, handler ) ); } /** * Get the obstacle sprite in a given direction if any * @param direction */ //% blockId=legacyspriteobstacle block="%sprite=variables_get(mySprite) wall hit on %direction" //% group="Color-coded Tilemap" //% help=sprites/sprite/tile-hit-from export function tileHitFrom(sprite: Sprite, direction: CollisionDirection): number { if (!sprite) return 0; return sprite.tileHitFrom(direction); } } namespace tiles.legacy { class TileSet { obstacle: boolean; private map: TileMap; private originalImage: Image; private cachedImage: Image; constructor(image: Image, collisions: boolean, map: tiles.TileMap) { this.originalImage = image; this.obstacle = collisions; this.map = map; } get image(): Image { const size = 1 << this.map.scale; if (!this.cachedImage || this.cachedImage.width != size || this.cachedImage.height != size) { if (this.originalImage.width == size && this.originalImage.height == size) { this.cachedImage = this.originalImage; } else { this.cachedImage = image.create(size, size); this.cachedImage.drawImage(this.originalImage, 0, 0); } } return this.cachedImage; } } export class LegacyTilemap extends tiles.TileMap { private _mapImage: Image; private _tileSets: TileSet[]; public isLegacy: boolean; constructor(scale: TileScale = TileScale.Sixteen) { super(scale); this._tileSets = []; this.isLegacy = true; } get data(): TileMapData { return null; } get image(): Image { return this._mapImage; } offsetX(value: number) { return Math.clamp(0, Math.max(this.areaWidth() - screen.width, 0), value); } offsetY(value: number) { return Math.clamp(0, Math.max(this.areaHeight() - screen.height, 0), value); } areaWidth() { return this._mapImage ? (this._mapImage.width << this.scale) : 0; } areaHeight() { return this._mapImage ? (this._mapImage.height << this.scale) : 0; } get layer(): number { return this._layer; } set layer(value: number) { if (this._layer != value) { this._layer = value; } } get enabled(): boolean { return !!this._mapImage; } setTile(index: number, img: Image, collisions?: boolean) { if (this.isInvalidIndex(index)) return; this._tileSets[index] = new TileSet(img, collisions, this); } setMap(map: Image) { this._mapImage = map; } public getTileLegacy(col: number, row: number): Tile { return new Tile(col, row, this); } public getTile(col: number, row: number): Location { return new Location(col, row, this); } public setTileAt(col: number, row: number, index: number): void { if (!this.isOutsideMap(col, row) && !this.isInvalidIndex(index)) this._mapImage.setPixel(col, row, index); } public getTilesByType(index: number): Location[] { if (this.isInvalidIndex(index) || !this.enabled) return []; let output: Location[] = []; for (let col = 0; col < this._mapImage.width; ++col) { for (let row = 0; row < this._mapImage.height; ++row) { let currTile = this._mapImage.getPixel(col, row); if (currTile === index) { output.push(new Location(col, row, this)); } } } return output; } public getTilesByTypeLegacy(index: number): Tile[] { if (this.isInvalidIndex(index) || !this.enabled) return []; let output: Tile[] = []; for (let col = 0; col < this._mapImage.width; ++col) { for (let row = 0; row < this._mapImage.height; ++row) { let currTile = this._mapImage.getPixel(col, row); if (currTile === index) { output.push(new Tile(col, row, this)); } } } return output; } private generateTile(index: number): TileSet { const size = 1 << this.scale const i = image.create(size, size); i.fill(index); return this._tileSets[index] = new TileSet(i, false, this); } private isOutsideMap(col: number, row: number): boolean { return !this.enabled || col < 0 || col >= this._mapImage.width || row < 0 || row >= this._mapImage.height; } protected isInvalidIndex(index: number): boolean { return index < 0 || index > 0xf; } protected draw(target: Image, camera: scene.Camera) { if (!this.enabled) return; // render tile map const bitmask = (0x1 << this.scale) - 1; const offsetX = camera.drawOffsetX & bitmask; const offsetY = camera.drawOffsetY & bitmask; const x0 = Math.max(0, camera.drawOffsetX >> this.scale); const xn = Math.min(this._mapImage.width, ((camera.drawOffsetX + target.width) >> this.scale) + 1); const y0 = Math.max(0, camera.drawOffsetY >> this.scale); const yn = Math.min(this._mapImage.height, ((camera.drawOffsetY + target.height) >> this.scale) + 1); for (let x = x0; x <= xn; ++x) { for (let y = y0; y <= yn; ++y) { const index = this._mapImage.getPixel(x, y); const tile = this._tileSets[index] || this.generateTile(index); if (tile) { target.drawTransparentImage( tile.image, ((x - x0) << this.scale) - offsetX, ((y - y0) << this.scale) - offsetY ); } } } if (game.debug) { // render debug grid overlay for (let x = x0; x <= xn; ++x) { const xLine = ((x - x0) << this.scale) - offsetX; if (xLine >= 0 && xLine <= screen.width) { target.drawLine( xLine, 0, xLine, target.height, 1 ); } } for (let y = y0; y <= yn; ++y) { const yLine = ((y - y0) << this.scale) - offsetY; if (yLine >= 0 && yLine <= screen.height) { target.drawLine( 0, yLine, target.width, yLine, 1 ); } } } } public isObstacle(col: number, row: number) { if (!this.enabled) return false; if (this.isOutsideMap(col, row)) return true; let t = this._tileSets[this._mapImage.getPixel(col, row)]; return t && t.obstacle; } public getObstacle(col: number, row: number) { const index = this.isOutsideMap(col, row) ? 0 : this._mapImage.getPixel(col, row); const tile = this._tileSets[index] || this.generateTile(index); return new sprites.StaticObstacle( tile.image, row << this.scale, col << this.scale, this.layer, index ); } public isOnWall(s: Sprite) { if (!s.isStatic()) s.setHitbox(); const hbox = s._hitbox const left = Fx.toIntShifted(hbox.left, this.scale); const right = Fx.toIntShifted(hbox.right, this.scale); const top = Fx.toIntShifted(hbox.top, this.scale); const bottom = Fx.toIntShifted(hbox.bottom, this.scale); for (let col = left; col <= right; ++col) { for (let row = top; row <= bottom; ++row) { if (this.isObstacle(col, row)) { return true; } } } return false; } public getTileIndex(col: number, row: number) { return this._mapImage.getPixel(col, row); } public getTileImage(index: number) { if (!this._tileSets[index]) this.generateTile(index); return this._tileSets[index].image; } } }