pxt-common-packages
Version:
Microsoft MakeCode (PXT) common packages
410 lines (355 loc) • 14.6 kB
text/typescript
//% 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=color-coded-tilemap/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=color-coded-tilemap/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;
}
}
}