UNPKG

pxt-common-packages

Version:
862 lines (737 loc) 29 kB
enum TileScale { //% block="4x4" Four = 2, //% block="8x8" Eight = 3, //% block="16x16" Sixteen = 4, //% block="32x32" ThirtyTwo = 5 } namespace tiles { /** * A (col, row) location in the tilemap **/ //% blockNamespace=scene color="#401255" export class Location { protected _row: number; protected _col: number; protected _originalMap: TileMap; constructor(col: number, row: number, map: TileMap) { this._col = col; this._row = row; this._originalMap = map; } get tileMap() { return game.currentScene().tileMap; } //% group="Locations" blockSetVariable="location" //% blockCombine block="column" //% weight=100 blockCombineGetHelp=tiles/location get column() { return this._col; } //% group="Locations" blockSetVariable="location" //% blockCombine block="row" //% weight=100 blockCombineGetHelp=tiles/location get row() { return this._row; } //% group="Locations" blockSetVariable="location" //% blockCombine block="x" //% weight=100 blockCombineGetHelp=tiles/location get x(): number { const scale = this.tileMap.scale; return (this._col << scale) + (1 << (scale - 1)); } //% group="Locations" blockSetVariable="location" //% blockCombine block="y" //% weight=100 blockCombineGetHelp=tiles/location get y(): number { const scale = this.tileMap.scale; return (this._row << scale) + (1 << (scale - 1)); } //% group="Locations" blockSetVariable="location" //% blockCombine block="left" //% weight=100 blockCombineGetHelp=tiles/location get left(): number { return (this._col << this.tileMap.scale); } //% group="Locations" blockSetVariable="location" //% blockCombine block="top" //% weight=100 blockCombineGetHelp=tiles/location get top(): number { return (this._row << this.tileMap.scale); } //% group="Locations" blockSetVariable="location" //% blockCombine block="right" //% weight=100 blockCombineGetHelp=tiles/location get right(): number { return this.left + (1 << this.tileMap.scale); } //% group="Locations" blockSetVariable="location" //% blockCombine block="bottom" //% weight=100 blockCombineGetHelp=tiles/location get bottom(): number { return this.top + (1 << this.tileMap.scale); } get tileSet(): number { return this.tileMap.getTileIndex(this._col, this._row); } // deprecated get col() { return this.column; } public isWall(): boolean { return this.tileMap.isObstacle(this._col, this._row); } public getImage(): Image { return this.tileMap.getTileImage(this.tileSet); } _getOriginalImage(): Image { return this._originalMap.getTileImage(this._originalMap.getTileIndex(this._col, this._row)); } /** * Returns the neighboring location in a specifc direction from a location in a tilemap * @param direction The direction to fetch the location in */ //% blockId=tiles_location_get_neighboring_location //% block="tilemap location $direction of $this" //% this.defl=location //% this.shadow=variables_get //% group="Locations" blockGap=8 //% weight=10 help=tiles/get-neighboring-location public getNeighboringLocation(direction: CollisionDirection): Location { switch (direction) { case CollisionDirection.Top: return this.tileMap.getTile(this._col, this._row - 1); case CollisionDirection.Right: return this.tileMap.getTile(this._col + 1, this._row); case CollisionDirection.Bottom: return this.tileMap.getTile(this._col, this._row + 1); case CollisionDirection.Left: return this.tileMap.getTile(this._col - 1, this._row); } } /** * Center the given sprite on this tile * @param sprite */ place(mySprite: Sprite): void { if (!mySprite) return; mySprite.setPosition(this.x, this.y); } // ## LEGACY: DO NOT USE ## _toTile(): Tile { return new Tile(this._col, this._row, this.tileMap); } } /** * DEPRECATED: a tile in the tilemap **/ //% blockNamespace=scene color="#401255" blockGap=8 export class Tile { protected _row: number; protected _col: number; protected tileMap: TileMap; constructor(col: number, row: number, map: TileMap) { this._col = col; this._row = row; this.tileMap = map; } get x(): number { const scale = this.tileMap.scale; return (this._col << scale) + (1 << (scale - 1)); } get y(): number { const scale = this.tileMap.scale; return (this._row << scale) + (1 << (scale - 1)); } get tileSet(): number { return this.tileMap.getTileIndex(this._col, this._row); } /** * Center the given sprite on this tile * @param sprite */ //% blockId=gameplaceontile block="on top of %tile(myTile) place %sprite=variables_get(mySprite)" //% blockNamespace="scene" group="Tilemap Operations" //% weight=25 //% help=tiles/place //% deprecated=1 place(mySprite: Sprite): void { if (!mySprite) return; mySprite.setPosition(this.x, this.y); } } const TM_DATA_PREFIX_LENGTH = 4; const TM_WALL = 2; //% snippet='tilemap` `' //% pySnippet='tilemap(""" """)' export class TileMapData { // The tile data for the map (indices into tileset) protected data: Buffer; // The metadata layers for the map. Currently only 1 is used for walls protected layers: Image; protected tileset: Image[]; protected cachedTileView: Image[]; protected _scale: TileScale; protected _width: number; protected _height: number; constructor(data: Buffer, layers: Image, tileset: Image[], scale: TileScale) { this.data = data; this.layers = layers; this.tileset = tileset; this.scale = scale; this._width = data.getNumber(NumberFormat.UInt16LE, 0); this._height = data.getNumber(NumberFormat.UInt16LE, 2); } get width(): number { return this._width; } get height(): number { return this._height; } get scale(): TileScale { return this._scale; } set scale(s: TileScale) { this._scale = s; this.cachedTileView = []; } getTile(col: number, row: number) { if (this.isOutsideMap(col, row)) return 0; return this.data.getUint8(TM_DATA_PREFIX_LENGTH + (col | 0) + (row | 0) * this.width); } setTile(col: number, row: number, tile: number) { if (this.isOutsideMap(col, row)) return; if (this.data.isReadOnly()) { this.data = this.data.slice(); } this.data.setUint8(TM_DATA_PREFIX_LENGTH + (col | 0) + (row | 0) * this.width, tile); } getTileset() { return this.tileset; } getTileImage(index: number) { const size = 1 << this.scale; let cachedImage = this.cachedTileView[index]; if (!cachedImage) { const originalImage = this.tileset[index]; if (originalImage) { if (originalImage.width <= size && originalImage.height <= size) { cachedImage = originalImage; } else { cachedImage = image.create(size, size); cachedImage.drawImage(originalImage, 0, 0); } this.cachedTileView[index] = cachedImage; } } return cachedImage; } setWall(col: number, row: number, on: boolean) { return this.layers.setPixel(col, row, on ? TM_WALL : 0); } isWall(col: number, row: number) { return this.layers.getPixel(col, row) === TM_WALL; } isOutsideMap(col: number, row: number) { return col < 0 || col >= this.width || row < 0 || row >= this.height; } } export enum TileMapEvent { Loaded, Unloaded } export class TileMapEventHandler { constructor(public event: TileMapEvent, public callback: (data: TileMapData) => void) {} } export class TileMap { protected _scale: TileScale; protected _layer: number; protected _map: TileMapData; renderable: scene.Renderable; protected handlerState: TileMapEventHandler[]; constructor(scale: TileScale = TileScale.Sixteen) { this._layer = 1; this.scale = scale; this.renderable = scene.createRenderable( scene.TILE_MAP_Z, (t, c) => this.draw(t, c) ); } get scale() { return this._scale; } set scale(s: TileScale) { this._scale = s; if (this._map) { this._map.scale = s; } } get data(): TileMapData { return this._map; } 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._map ? (this._map.width << this.scale) : 0; } areaHeight() { return this._map ? (this._map.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._map; } setData(map: TileMapData) { const previous = this._map; if (this.handlerState && previous !== map && previous) { for (const eventHandler of this.handlerState) { if (eventHandler.event === TileMapEvent.Unloaded) { eventHandler.callback(previous); } } } this._map = map; if (map) { this._scale = map.scale; } if (this.handlerState && previous !== map && map) { for (const eventHandler of this.handlerState) { if (eventHandler.event === TileMapEvent.Loaded) { eventHandler.callback(map); } } } } public getTile(col: number, row: number): Location { return new Location(col, row, this); } public getTileIndex(col: number, row: number) { return this.data.getTile(col, row); } public setTileAt(col: number, row: number, index: number): void { if (!this._map.isOutsideMap(col, row) && !this.isInvalidIndex(index)) this._map.setTile(col, row, index); } public getImageType(im: Image): number { const tileset = this._map.getTileset(); for (let i = 0; i < tileset.length; i++) if (tileset[i].equals(im)) return i; // not found; append to the tileset if there are spots left. const newIndex = tileset.length; if (!this.isInvalidIndex(newIndex)) { tileset.push(im); return newIndex; } return -1; } public setWallAt(col: number, row: number, on: boolean): void { if (!this._map.isOutsideMap(col, row)) this._map.setWall(col, row, on); } public getTilesByType(index: number): Location[] { if (this.isInvalidIndex(index) || !this.enabled) return []; let output: Location[] = []; for (let col = 0; col < this._map.width; ++col) { for (let row = 0; row < this._map.height; ++row) { let currTile = this._map.getTile(col, row); if (currTile === index) { output.push(new Location(col, row, this)); } } } return output; } public sampleTilesByType(index: number, maxCount: number): Location[] { if (this.isInvalidIndex(index) || !this.enabled || maxCount <= 0) return []; let count = 0; const reservoir: Location[] = []; for (let col = 0; col < this._map.width; ++col) { for (let row = 0; row < this._map.height; ++row) { let currTile = this._map.getTile(col, row); if (currTile === index) { // first **maxCount** elements just enqueue if (count < maxCount) { reservoir.push(new Location(col, row, this)); } else { const potentialIndex = randint(0, count); if (potentialIndex < maxCount) { reservoir[potentialIndex] = new Location(col, row, this); } } ++count; } } } return reservoir; } protected isInvalidIndex(index: number): boolean { return index < 0 || index > 0xff; } 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._map.width, ((camera.drawOffsetX + target.width) >> this.scale) + 1); const y0 = Math.max(0, camera.drawOffsetY >> this.scale); const yn = Math.min(this._map.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._map.getTile(x, y); const tile = this._map.getTileImage(index); if (tile) { target.drawTransparentImage( tile, ((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._map.isOutsideMap(col, row)) return true; return this._map.isWall(col, row); } public getObstacle(col: number, row: number) { const index = this._map.isOutsideMap(col, row) ? 0 : this._map.getTile(col, row); const tile = this._map.getTileImage(index); return new sprites.StaticObstacle( tile, row << this.scale, col << this.scale, this.layer, index ); } public isOnWall(s: Sprite) { 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 getTileImage(index: number) { return this.data.getTileImage(index); } public addEventListener(event: TileMapEvent, handler: (data: TileMapData) => void) { if (!this.handlerState) this.handlerState = []; for (const eventHandler of this.handlerState) { if (eventHandler.event === event && eventHandler.callback === handler) return; } this.handlerState.push(new TileMapEventHandler(event, handler)); } public removeEventListener(event: TileMapEvent, handler: (data: TileMapData) => void) { if (!this.handlerState) return; for (let i = 0; i < this.handlerState.length; i++) { if (this.handlerState[i].event === event && this.handlerState[i].callback === handler) { this.handlerState.splice(i, 1) return; } } } } function mkColorTile(index: number, scale: TileScale): Image { const size = 1 << scale const i = image.create(size, size); i.fill(index); return i; } //% scale.defl="TileScale.Sixteen" export function createTilemap(data: Buffer, layer: Image, tiles: Image[], scale: TileScale): TileMapData { return new TileMapData(data, layer, tiles, scale) } //% blockId=tilemap_editor block="set tilemap to $tilemap" //% weight=200 blockGap=8 //% tilemap.fieldEditor="tilemap" //% tilemap.fieldOptions.decompileArgumentAsString="true" //% tilemap.fieldOptions.filter="tile" //% tilemap.fieldOptions.taggedTemplate="tilemap" //% blockNamespace="scene" duplicateShadowOnDrag //% help=tiles/set-tilemap //% deprecated=1 export function setTilemap(tilemap: TileMapData) { setCurrentTilemap(tilemap); } /** * Sets the given tilemap to be the current active tilemap in the game * * @param tilemap The tilemap to set as the current tilemap */ //% blockId=set_current_tilemap block="set tilemap to $tilemap" //% weight=201 blockGap=8 //% tilemap.shadow=tiles_tilemap_editor //% blockNamespace="scene" group="Tilemaps" duplicateShadowOnDrag //% help=tiles/set-current-tilemap export function setCurrentTilemap(tilemap: TileMapData) { scene.setTileMapLevel(tilemap); } /** * Set a location in the map (column, row) to a tile * @param loc * @param tile */ //% blockId=mapsettileat block="set $tile at $loc=mapgettile" //% tile.shadow=tileset_tile_picker //% tile.decompileIndirectFixedInstances=true //% blockNamespace="scene" group="Tilemap Operations" blockGap=8 //% help=tiles/set-tile-at //% weight=70 export function setTileAt(loc: Location, tile: Image): void { const scene = game.currentScene(); if (!loc || !tile || !scene.tileMap) return null; const scale = scene.tileMap.scale; const index = scene.tileMap.getImageType(tile); scene.tileMap.setTileAt(loc.x >> scale, loc.y >> scale, index); } /** * Set or unset a wall at a location in the map (column, row) * @param loc * @param on */ //% blockId=mapsetwallat block="set wall $on at $loc" //% on.shadow=toggleOnOff loc.shadow=mapgettile //% blockNamespace="scene" group="Tilemap Operations" //% help=tiles/set-wall-at //% weight=60 export function setWallAt(loc: Location, on: boolean): void { const scene = game.currentScene(); if (!loc || !scene.tileMap) return null; const scale = scene.tileMap.scale; scene.tileMap.setWallAt(loc.x >> scale, loc.y >> scale, on); } /** * Get the tile position given a column and row in the tilemap * @param col * @param row */ //% blockId=mapgettile block="tilemap col $col row $row" //% blockNamespace="scene" group="Locations" //% duplicateShadowOnDrag //% weight=100 blockGap=8 //% help=tiles/get-tile-location export function getTileLocation(col: number, row: number): Location { const scene = game.currentScene(); if (col == undefined || row == undefined || !scene.tileMap) return null; return scene.tileMap.getTile(col, row); } /** * Get the image of a tile, given a location in the tilemap * @param loc */ export function getTileImage(loc: Location): Image { const scene = game.currentScene(); if (!loc || !scene.tileMap) return img``; return scene.tileMap.getTileImage(loc.tileSet); } /** * Get the image of a tile, given a (column, row) in the tilemap * @param loc */ export function getTileAt(col: number, row: number): Image { const scene = game.currentScene(); if (col == undefined || row == undefined || !scene.tileMap) return img``; return scene.tileMap.getTileImage(tiles.getTileLocation(col, row).tileSet); } /** * Returns true if the tile at the given location is the same as the given tile; * otherwise returns false * @param location * @param tile */ //% blockId=maplocationistile block="tile at $location is $tile" //% location.shadow=mapgettile //% tile.shadow=tileset_tile_picker tile.decompileIndirectFixedInstances=true //% blockNamespace="scene" group="Locations" blockGap=8 //% weight=40 help=tiles/tile-at-location-equals export function tileAtLocationEquals(location: Location, tile: Image): boolean { const scene = game.currentScene(); if (!location || !tile || !scene.tileMap) return false; return location.tileSet === scene.tileMap.getImageType(tile); } /** * Returns true if the tile at the given location is a wall in the current tilemap; * otherwise returns false * @param location The location to check for a wall */ //% blockId=tiles_tile_at_location_is_wall //% block="tile at $location is wall" //% location.shadow=mapgettile //% blockNamespace="scene" group="Locations" blockGap=8 //% weight=30 help=tiles/tile-at-location-is-wall export function tileAtLocationIsWall(location: Location): boolean { if (!location || !location.tileMap) return false; return location.isWall(); } /** * Returns the image of the tile at the given location in the current tilemap * * @param location The location of the image to fetch */ //% blockId=tiles_image_at_location //% block="tile image at $location" //% location.shadow=mapgettile //% weight=0 help=tiles/tile-image-at-location //% blockNamespace="scene" group="Locations" export function tileImageAtLocation(location: Location): Image { const scene = game.currentScene(); if (!location || !scene.tileMap) return img``; return location.getImage(); } /** * Center the given sprite on a given location * @param sprite * @param loc */ //% blockId=mapplaceontile block="place $sprite=variables_get(mySprite) on top of $loc" //% sprite.defl=mySprite //% loc.shadow=mapgettile //% blockNamespace="scene" group="Tilemap Operations" blockGap=8 //% help=tiles/place-on-tile //% weight=100 export function placeOnTile(sprite: Sprite, loc: Location): void { if (!sprite || !loc || !loc.tileMap) return; loc.place(sprite); } /** * Center the given sprite on a random location that is the given type (image) * @param sprite * @param tile */ //% blockId=mapplaceonrandomtile block="place $sprite=variables_get(mySprite) on top of random $tile" //% sprite.defl=mySprite //% tile.shadow=tileset_tile_picker //% tile.decompileIndirectFixedInstances=true //% blockNamespace="scene" group="Tilemap Operations" //% help=tiles/place-on-random-tile //% weight=90 export function placeOnRandomTile(sprite: Sprite, tile: Image): void { if (!sprite || !game.currentScene().tileMap) return; const loc = getRandomTileByType(tile); if (loc) loc.place(sprite); } /** * Get all tiles in the tilemap with the given type (image). * @param tile */ //% blockId=mapgettilestype block="array of all $tile locations" //% tile.shadow=tileset_tile_picker //% tile.decompileIndirectFixedInstances=true //% blockNamespace="scene" group="Locations" blockGap=8 //% help=tiles/get-tiles-by-type //% weight=10 export function getTilesByType(tile: Image): Location[] { const scene = game.currentScene(); if (!tile || !scene.tileMap) return []; const index = scene.tileMap.getImageType(tile); return scene.tileMap.getTilesByType(index); } /** * Get a random tile of the given type * @param tile the type of tile to get a random selection of */ export function getRandomTileByType(tile: Image): Location { const scene = game.currentScene(); if (!tile || !scene.tileMap) return undefined; const index = scene.tileMap.getImageType(tile); const sample = scene.tileMap.sampleTilesByType(index, 1); return sample[0]; } /** * A tilemap */ //% blockId=tiles_tilemap_editor shim=TD_ID //% weight=200 blockGap=8 //% block="tilemap $tilemap" //% tilemap.fieldEditor="tilemap" //% tilemap.fieldOptions.decompileArgumentAsString="true" //% tilemap.fieldOptions.filter="tile" //% tilemap.fieldOptions.taggedTemplate="tilemap" //% blockNamespace="scene" group="Tilemaps" duplicateShadowOnDrag //% help=tiles/tilemap export function _tilemapEditor(tilemap: TileMapData): TileMapData { return tilemap; } /** * Adds an event handler that will fire whenever the specified event * is triggered. Unloaded tilemap events will fire before the new tilemap * is set and loaded events will fire afterwards. The same handler can * not be added for the same event more than once. * * @param event The event to subscribe to * @param handler The code to run when the event triggers */ export function addEventListener(event: TileMapEvent, callback: (data: TileMapData) => void) { const scene = game.currentScene(); if (!scene.tileMap) { scene.tileMap = new TileMap(); } scene.tileMap.addEventListener(event, callback); } /** * Removes an event handler registered with addEventListener. * * @param event The event that the handler was registered for * @param handler The handler to remove */ export function removeEventListener(event: TileMapEvent, callback: (data: TileMapData) => void) { const scene = game.currentScene(); if (!scene.tileMap) return; scene.tileMap.removeEventListener(event, callback); } }