UNPKG

juno-console

Version:

Juno is a clean and lightweight 2D game framework written in TypeScript for making tiny HTML5 Games.

932 lines (851 loc) 29.7 kB
/** * @author Digitsensitive <digit.sensitivee@gmail.com> * @copyright 2018 Digitsensitive * @description Juno Core: API functions * * Here you will find the core functions of Juno. * * @license {@link https://github.com/digitsensitive/juno-console/blob/master/license.txt|MIT License} */ import { ICanvasRenderer } from "../interfaces/canvas-renderer.interface"; import { Input } from "./input"; import { ITiledMapJson, ILayer, ITileset, IObject } from "../interfaces/tiled-map-json.interface"; import { IMouse } from "../interfaces/mouse.interface"; import { KEY } from "../enums/key.enum"; export class API { private palette: string[]; private images: Map<string, HTMLImageElement> = new Map(); private mapData: ITiledMapJson[] = []; private mapAdjustments = { x: 0, y: 0 }; private spriteSize: number; private tileSize: number; private passedTicks: number; constructor(private cr: ICanvasRenderer, private inputs: Input) { this.passedTicks = 0; } /******************************************************************** * Init color palette with chain hex color string * Total 16 colors: 6 * 16 = 96 (string length) * Examples: * Juno: * 1a1c2c572956b14156ee7b58ffd079a0f07238b86e276e7b29366f405bd04fa4f786ecf8f4f4f493b6c1557185324056 * TIC-80 (DB16): * 140C1C44243430346D4E4A4F854C30346524D04648757161597DCED27D2C8595A16DAA2CD2AA996DC2CADAD45EDEEED6 * COMMODORE VIC-20 PALETTE * 000000ffffffa8734ae9b287772d26b6686285d4dcc5ffffa85fb4e99df5559e4a92df8742348b7e70cabdcc71ffffb0 * STILL LIFE PALETTE * 3f28117a2222d13b27e07f8a5d853a68c127b3e868122615513155286fb89b8bffa8e4d4cc8218c7b581000000ffffff * JAPANESE MACHINE PALETTE * 00000019102846af45a1d685453e787664fe8331299ec2e8dc534be18d79d6b97be9d8a1216c4bd365c8afaab9f5f4eb * CGARNE PALETTE * 0000005e606e2234d10c7e455c2e78b5b5b5FFFFFFffd93f7be2f98a36224c81fb44aacceb8a60aa5c3d6cd947e23d69 * PSYGNOSIA PALETTE * 0000001b1e29362747443f4152524c64647c73615077785b9ea4a7cbe8f7e08b79a2324e003308084a3c546a00516cbf * COLOR GRAPHICS ADAPTER PALETTE * 000000555555AAAAAAFFFFFF0000AA5555FF00AA0055FF5500AAAA55FFFFAA0000FF5555AA00AAFF55FFAA5500FFFF55 * EROGE COPPER PALETTE * 0d080d4f2b24825b31c59154f0bd77fbdf9bfff9e4bebbb27bb24e74adbb4180a032535f2a23497d3840c16c5be89973 * EASTER ISLAND PALETTE * f6f6bfe6d1d1868691794765f5e17aedc38dcc8d86ca657e39d4b98dbcd28184ab6860869dc0857ea788567864051625 * PICO-8 PALETTE * 0000001D2B537E255383769CAB5236008751FF004D5F574FFF77A8FFA300C2C3C700E436FFCCAA29ADFFFFEC27FFF1E8 * GRAYSCALE * 000000111111222222333333444444555555666666777777888888999999aaaaaabbbbbbccccccddddddeeeeeeffffff * * @param palette [index of the color in the palette] ********************************************************************/ public ipal(palette: string): void { this.palette = []; let fromPositionInString = 0; while (fromPositionInString < 96) { this.palette.push("#" + palette.substr(fromPositionInString, 6)); fromPositionInString += 6; } } /******************************************************************** * Clear the screen with a specified color. * @param color [index of the color in the palette] /********************************************************************/ public cls(c?: number): void { const context = this.cr.renderer; if (c === undefined) { // clear screen context.clearRect(0, 0, this.cr.canvas.width, this.cr.canvas.height); } else { // evaluate runtime errors this.colorRangeError(c); // draw the selected color on screen context.fillStyle = this.palette[c]; context.fillRect(0, 0, this.cr.canvas.width, this.cr.canvas.height); } // update ticks this.passedTicks += 1; } /******************************************************************** * Draw one pixel at a specific 2D location (x and y). * @param x0 [x coordinate of the pixel] * @param y0 [y coordinate of the pixel] * @param color [index of the color in the palette] ********************************************************************/ public pix(x0: number, y0: number, c: number): void { // evaluate runtime errors this.colorRangeError(c); this.cr.renderer.fillStyle = this.palette[c]; this.cr.renderer.fillRect( x0 * this.cr.options.scaleFactor, y0 * this.cr.options.scaleFactor, this.cr.options.scaleFactor, this.cr.options.scaleFactor ); } /******************************************************************** * Create a circle outline with the Bresenham's circle algorithm. * @param x [x coordinate of the center of the circle] * @param y [y coordinate of the center of the circle] * @param r [radius of the circle] * @param c [index of the color in the palette] ********************************************************************/ public circb(x0: number, y0: number, r: number, c: number): void { // evaluate runtime errors this.colorRangeError(c); let x = 0; let y = r; let p = 3 - 2 * r; this.circbPixGroup(x0, y0, x, y, c); while (x < y) { x++; if (p < 0) { p = p + 4 * x + 6; } else { y--; p = p + 4 * (x - y) + 10; } this.circbPixGroup(x0, y0, x, y, c); } } /******************************************************************** * [pixel description] * @param xc [description] * @param yc [description] * @param x [description] * @param y [description] * @param c [description] ********************************************************************/ private circbPixGroup( x0: number, y0: number, x: number, y: number, c: number ): void { this.pix(x0 + x, y0 + y, c); this.pix(x0 + x, y0 - y, c); this.pix(x0 - x, y0 + y, c); this.pix(x0 - x, y0 - y, c); this.pix(x0 + y, y0 + x, c); this.pix(x0 + y, y0 - x, c); this.pix(x0 - y, y0 + x, c); this.pix(x0 - y, y0 - x, c); } /******************************************************************** * Filled Circle (100%) * Create a filled circle with the Bresenham's circle algorithm. * @param x0 [x coordinate of the center of the circle] * @param y0 [y coordinate of the center of the circle] * @param r [radius of the circle] * @param c [index of the color in the palette] ********************************************************************/ public circ(x0: number, y0: number, r: number, c: number): void { // evaluate runtime errors this.colorRangeError(c); // draw filled circle let x = 0; let y = r; let p = 3 - 2 * r; this.circPixGroup(x0, y0, x, y, c); while (x < y) { x++; if (p < 0) { p = p + 4 * x + 6; } else { y--; p = p + 4 * (x - y) + 10; } this.circPixGroup(x0, y0, x, y, c); } } /******************************************************************** * Group of pixel lines (100%) * Draws a group of lines, used for drawing a filled circle. * @param x0 [first x coordinate] * @param y0 [first y coordinate] * @param x [second x coordinate] * @param y [second y coordinate] * @param c [index of the color in the palette] ********************************************************************/ private circPixGroup( x0: number, y0: number, x: number, y: number, c: number ): void { this.line(x0 + x, y0 + y, x0 - x, y0 + y, c); this.line(x0 + x, y0 - y, x0 - x, y0 - y, c); this.line(x0 + y, y0 + x, x0 - y, y0 + x, c); this.line(x0 + y, y0 - x, x0 - y, y0 - x, c); } /******************************************************************** * Create a line with the Bresenham's line algorithm. * @param x0 [the starting x position] * @param y0 [the starting y position] * @param x1 [the ending x position] * @param y1 [the ending y position] * @param c [index of the color in the palette] ********************************************************************/ public line(x0: number, y0: number, x1: number, y1: number, c: number): void { // evaluate runtime errors this.colorRangeError(c); x0 = Math.ceil(x0); y0 = Math.ceil(y0); x1 = Math.ceil(x1); y1 = Math.ceil(y1); let dx = Math.abs(x1 - x0); let dy = Math.abs(y1 - y0); let sx = x0 < x1 ? 1 : -1; let sy = y0 < y1 ? 1 : -1; let err = dx - dy; for (let x = 0; x <= dx; x++) { for (let y = 0; y <= dy; y++) { this.pix(x0, y0, c); if (x0 == x1 && y0 == y1) { break; } let e2 = 2 * err; if (e2 >= -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } } } /******************************************************************** * Draw a filled rectangle. * @param x0 [the x position of the rectangle] * @param y0 [the y position of the rectangle] * @param w [the width of the rectangle] * @param h [the height of the rectangle] * @param c [index of the color in the palette] ********************************************************************/ public rect(x0: number, y0: number, w: number, h: number, c: number): void { // evaluate runtime errors if (w <= 0) { throw new RangeError("The width of a rectangle must be > 0. "); } else if (h <= 0) { throw new RangeError("The height of a rectangle must be > 0. "); } this.colorRangeError(c); this.cr.renderer.fillStyle = this.palette[c]; this.cr.renderer.fillRect( x0 * this.cr.options.scaleFactor, y0 * this.cr.options.scaleFactor, w * this.cr.options.scaleFactor, h * this.cr.options.scaleFactor ); } /******************************************************************** * Draw a rectangle outline. * @param x0 [the x position of the rectangle] * @param y0 [the y position of the rectangle] * @param w [the width of the rectangle] * @param h [the height of the rectangle] * @param c [index of the color in the palette] ********************************************************************/ public rectb(x0: number, y0: number, w: number, h: number, c: number): void { // evaluate runtime errors if (w <= 0) { throw new RangeError("The width of a rectangle must be > 0. "); } else if (h <= 0) { throw new RangeError("The height of a rectangle must be > 0. "); } this.colorRangeError(c); for (let x = 0; x < w; x++) { for (let y = 0; y < h; y++) { if (x === 0 || y === 0 || x === w - 1 || y === h - 1) { this.pix(x0 + x, y0 + y, c); } } } } /******************************************************************** * Print text. * @param s [string to print] * @param x [x position of the text] * @param y [y position of the text] * @param c [index of the color in the palette] * @param sc [scale factor of the text] ********************************************************************/ public print( s: string, x0: number, y0: number, c: number, a?: number, sc?: number ): void { // evaluate runtime errors if (sc !== undefined && sc < 1) { throw new RangeError("The font size cannot be smaller than 1. "); } else if (s.length === 0) { throw new RangeError("The font length must be longer than 0. "); } this.colorRangeError(c); let size = sc * 3 * this.cr.options.scaleFactor || 3 * this.cr.options.scaleFactor; this.cr.renderer.font = size + "px Juno"; this.cr.renderer.fillStyle = this.palette[c] + this.calculateAlphaHexCode(a || 1); this.cr.renderer.fillText( s, x0 * this.cr.options.scaleFactor, y0 * this.cr.options.scaleFactor + size ); } /******************************************************************** * Trace a string or a number => Alternative to console.log(). * @param s [the string or number to trace] ********************************************************************/ public trace(s: string | number): void { if (typeof s === "number") { s = s.toString(); } this.print(s, 0, 0, 12); } /******************************************************************** * Load your game files (f.e. spritesheet, tiles, json map). * Currently supported file formats: jpg, jpeg, png and json. * @param name [name of the file] * @param path [path of the file] * @param size [size of the sprites/tiles] ********************************************************************/ public load(name: string, path: string, size: number): void { let extension = path.substr(path.lastIndexOf(".") + 1); if ( extension === "png" || extension === "PNG" || extension === "jpg" || extension === "JPG" || extension === "jpeg" || extension === "JPEG" ) { this.spriteSize = size; let image = new Image(); image.src = path; this.images.set(name, image); } if (extension === "json" || extension === "JSON") { this.tileSize = size; var request = new XMLHttpRequest(); request.open("GET", path, false); request.send(null); this.parseJSONDataIntoObject(JSON.parse(request.responseText)); } } /******************************************************************** * Parse JSON data into JSON object. * Currently the following data is parsed: * Map width, Map height, Tile width, Tile height, Layers and Tilesets. * Only CSV as Tile level format is currently supported. * @param data [the data to parse] ********************************************************************/ private parseJSONDataIntoObject(data: any): void { let tiledMapData: ITiledMapJson = {} as ITiledMapJson; // get basic map data tiledMapData.mapWidth = data.width; tiledMapData.mapHeight = data.height; tiledMapData.tileWidth = data.tilewidth; tiledMapData.tileHeight = data.tileheight; // get layer data let layers = []; let layersLength = data.layers.length; for (let i = 0; i < layersLength; i++) { let layer: ILayer = {} as ILayer; layer.name = data.layers[i].name; layer.type = data.layers[i].type; layer.height = data.layers[i].height; layer.width = data.layers[i].width; let layerData = []; let j = 0; for (let y = 0; y < layer.height; y++) { layerData[y] = []; for (let x = 0; x < layer.width; x++) { /* correction with - 1 is needed, because indexation with Tiles starts with 1 and not with 0 */ layerData[y][x] = data.layers[i].data[j] - 1; j++; } } layer.data = layerData; if (data.layers[i].objects !== undefined) { let obj = []; let objLength = data.layers[i].objects.length; for (let j = 0; j < objLength; j++) { let object: IObject = {} as IObject; object.name = data.layers[i].objects[j].name; object.x = data.layers[i].objects[j].x; object.y = data.layers[i].objects[j].y; object.height = data.layers[i].objects[j].height; object.width = data.layers[i].objects[j].width; obj.push(object); } layer.objects = obj; } layer.visible = data.layers[i].visible; layer.opacity = data.layers[i].opacity; layer.x = data.layers[i].x; layer.y = data.layers[i].y; layers.push(layer); } tiledMapData.layers = []; tiledMapData.layers = layers; // get tilesets let tilesets = []; let tilesetsLength = data.tilesets.length; for (let i = 0; i < tilesetsLength; i++) { let tileset: ITileset = {} as ITileset; let tileProp: Map<string, any> = new Map(); tileset.name = data.tilesets[i].name; tileset.tileCount = data.tilesets[i].tilecount; for (let key in data.tilesets[i].tileproperties) { if (data.tilesets[i].tileproperties.hasOwnProperty(key)) { tileProp.set(key, data.tilesets[i].tileproperties[key]); } } tileset.tileProperties = tileProp; tilesets.push(tileset); } tiledMapData.tilesets = []; tiledMapData.tilesets = tilesets; this.mapData.push(tiledMapData); } /******************************************************************** * Draw map on the screen. * @param x [starting tile on x coordinate] * @param y [starting tile on y coordinate] * @param w [number of tiles to draw to the width] * @param h [number of tiles to draw to the height] * @param sx [the x coordinate to draw the map to] * @param sy [the y coordinate to draw the map to] ********************************************************************/ public map( x?: number, y?: number, w?: number, h?: number, sx?: number, sy?: number ): void { let mapArray = this.mapData[0].layers[0].data; let tileSize = this.mapData[0].tileHeight; let numberVerticalTiles = this.mapData[0].layers[0].height; let numberHorizontalTiles = this.mapData[0].layers[0].width; let width = w || numberHorizontalTiles; let height = h || numberVerticalTiles; this.mapAdjustments.x = x || 0; this.mapAdjustments.y = y || 0; let x1 = sx || 0; let y1 = sy || 0; // evaluate runtime errors if (this.mapAdjustments.x < 0 || this.mapAdjustments.y < 0) { throw new RangeError("map(): Starting tile cannot be negative!. "); } for (let y0 = this.mapAdjustments.y; y0 < height; y0++) { for (let x0 = this.mapAdjustments.x; x0 < width; x0++) { this.spr( mapArray[y0][x0], x1 + (x0 - this.mapAdjustments.x) * tileSize, y1 + (y0 - this.mapAdjustments.y) * tileSize ); } } } /******************************************************************** * Get the map tile index at a specific 2D coordinate. * @param x [the x coordinate of the tile] * @param y [the y coordinate of the tile] * @return [the map tile index] ********************************************************************/ public mget(x: number, y: number): number { // get the actual coordinates. Depends on the tile size. // Use of floor to round downward to its nearest integer let x0 = Math.floor(x / this.tileSize) + this.mapAdjustments.x; let y0 = Math.floor(y / this.tileSize) + this.mapAdjustments.y; // evaluate runtime errors // math.floor rounds down f.e. x = 0.99 to 0 -> means < 0 not allowed // width & height must than be corrected with (-1) since we start from 0 if ( x0 < 0 || x0 > this.mapData[0].layers[0].width - 1 || y0 < 0 || y0 > this.mapData[0].layers[0].height - 1 ) { throw new RangeError( "mget(): Tile coordinate: " + x + " / " + y + " is out of the range. " ); } return this.mapData[0].layers[0].data[y0][x0]; } /******************************************************************** * Set the map tile index at a specific 2D coordinate. * @param id [the map tile index to set] * @param x [the x position of the tile to set] * @param y [the y position of the tile to set] ********************************************************************/ public mset(id: number, x: number, y: number): void { if (id < 0 || id > this.mapData[0].tilesets[0].tileCount) { throw new RangeError( "mset(): Tile id: " + id + " is out of the range / does not exist. " ); } else if ( x < 0 || x >= this.mapData[0].layers[0].width || y < 0 || y >= this.mapData[0].layers[0].height ) { throw new RangeError( "mset(): Tile coordinate: " + x + " / " + y + " is out of the range. " ); } this.mapData[0].layers[0].data[y][x] = id; } public mobj(): void { let obj = this.mapData[0].layers[1].objects; let objLength = this.mapData[0].layers[1].objects.length; for (let i = 0; i < objLength; i++) { this.spr(26, obj[i].x, obj[i].y); } } /******************************************************************** * Create a sprite from spritesheet. * @param s [the choosen sprite] * @param x0 [x position of the sprite] * @param y0 [y position of the sprite] ********************************************************************/ public spr(s: number, x0: number, y0: number): void { this.cr.renderer.mozImageSmoothingEnabled = false; this.cr.renderer.webkitImageSmoothingEnabled = false; this.cr.renderer.imageSmoothingEnabled = false; let amountFieldsHorizontal = this.images.values().next().value.width / this.spriteSize; let yPos = Math.floor(s / amountFieldsHorizontal); let xPos = s - amountFieldsHorizontal * yPos; this.cr.renderer.drawImage( this.images.values().next().value, xPos * this.spriteSize, yPos * this.spriteSize, 8, 8, x0 * this.cr.options.scaleFactor, y0 * this.cr.options.scaleFactor, this.spriteSize * this.cr.options.scaleFactor, this.spriteSize * this.cr.options.scaleFactor ); } /******************************************************************** * Get pixel color index from 2D position. * @param x0 [x position of the pixel] * @param y0 [y position of the pixel] * @return [the color index of the pixel] ********************************************************************/ public pget(x0: number, y0: number): number { let p = this.cr.renderer.getImageData( Math.round(x0) * this.cr.options.scaleFactor, Math.round(y0) * this.cr.options.scaleFactor, this.cr.options.scaleFactor, this.cr.options.scaleFactor ).data; let hex = this.rgbToHex(p[0], p[1], p[2]); let l = this.palette.length; for (let p = 0; p < l; p++) { if (this.palette[p] === hex) { return p; } } } /******************************************************************** * Converts each primary color to corresponding hex value. * @param c [the color to convert] * @return [the hex value as a string] ********************************************************************/ private componentToHex(c: number): string { let hex = c.toString(16); return hex.length === 1 ? "0" + hex : hex; } /******************************************************************** * Converts rgb to hex. * @param r [r value] * @param g [g value] * @param b [b value] * @return [the final hex-string] ********************************************************************/ private rgbToHex(r: number, g: number, b: number): string { return ( "#" + this.componentToHex(r) + this.componentToHex(g) + this.componentToHex(b) ); } /******************************************************************** * Get status of key code passed * Return true if key is pressed * @param code [key code passed] * @return [true or false] ********************************************************************/ public key(code: number): boolean { switch (code) { case 0: { if (this.inputs.isDown(KEY.UP)) { return true; } break; } case 1: { if (this.inputs.isDown(KEY.DOWN)) { return true; } break; } case 2: { if (this.inputs.isDown(KEY.LEFT)) { return true; } break; } case 3: { if (this.inputs.isDown(KEY.RIGHT)) { return true; } break; } case 4: { if (this.inputs.isDown(KEY.A)) { return true; } break; } case 5: { if (this.inputs.isDown(KEY.B)) { return true; } break; } case 6: { if (this.inputs.isDown(KEY.X)) { return true; } break; } case 7: { if (this.inputs.isDown(KEY.Z)) { return true; } break; } default: { break; } } } /******************************************************************** * Get status of key code passed * Return only true if pressed now and not in previous frame * @param code [Key code passed] * @return [true or false] ********************************************************************/ public keyp(code: number): boolean { switch (code) { case 0: { if (this.inputs.justDown(KEY.UP)) { return true; } break; } case 1: { if (this.inputs.justDown(KEY.DOWN)) { return true; } break; } case 2: { if (this.inputs.justDown(KEY.LEFT)) { return true; } break; } case 3: { if (this.inputs.justDown(KEY.RIGHT)) { return true; } break; } case 4: { if (this.inputs.justDown(KEY.A)) { return true; } break; } case 5: { if (this.inputs.justDown(KEY.B)) { return true; } break; } case 6: { if (this.inputs.justDown(KEY.X)) { return true; } break; } case 7: { if (this.inputs.justDown(KEY.Z)) { return true; } break; } default: { break; } } } public sfx(): void { let ctx = new AudioContext(); let lfo = ctx.createOscillator(); lfo.frequency.value = 1.0; // Create the high frequency oscillator to be modulated let hfo = ctx.createOscillator(); hfo.frequency.value = 440.0; // Create a gain node whose gain determines the amplitude of the modulation signal let modulationGain = ctx.createGain(); modulationGain.gain.value = 50; // Configure the graph and start the oscillators lfo.connect(modulationGain); modulationGain.connect(hfo.detune); hfo.connect(ctx.destination); hfo.start(0); lfo.start(0); } /******************************************************************** * Return mouse informations. * @return [IMouse Interface: Coordinates and Button pressed] ********************************************************************/ public mouse(): IMouse { return this.inputs.getMouse(); } /******************************************************************** * Get the game width in pixels * @return [game width] ********************************************************************/ public ggw(): number { return this.cr.canvas.width / this.cr.options.scaleFactor; } /******************************************************************** * Get the game height in pixels * @return [game height] ********************************************************************/ public ggh(): number { return this.cr.canvas.height / this.cr.options.scaleFactor; } public ticks(): number { return this.passedTicks; } public rnd(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1) + min); } /******************************************************************** * Color Range Error (100%) * Check if the selected color is between 0 and 15. * @param c [the color to check] ********************************************************************/ private colorRangeError(c: number): void { if (c < 0 || c > 15) { throw new RangeError( "You have selected an incorrect color index (" + c + "). The color must be between 0-15" ); } } /******************************************************************** * SPECIAL API FUNCTIONS ********************************************************************/ public crc(c, r): boolean { let circleDistanceX = Math.abs(c.x - r.x); let circleDistanceY = Math.abs(c.y - r.y); if (circleDistanceX > r.w / 2 + c.r) { return false; } if (circleDistanceY > r.h / 2 + c.r) { return false; } if (circleDistanceX <= r.w / 2) { return true; } if (circleDistanceY <= r.h / 2) { return true; } let cornerDistance_sq = (circleDistanceX - r.w / 2) * (circleDistanceX - r.w / 2) + (circleDistanceY - r.h / 2) * (circleDistanceY - r.h / 2); return cornerDistance_sq <= c.r * c.r; } public rrc(r1, r2): boolean { if ( r1.x < r2.x + r2.w && r1.x + r1.w > r2.x && r1.y < r2.y + r2.h && r1.y + r1.h > r2.y ) { return true; } return false; } public anim( object: any, startFrame: number, numberOfFrames: number, speed: number ): void { if (!object.a_ct) { object.a_ct = 0; } if (!object.a_st) { object.a_st = 0; } object.a_ct += 1; if (object.a_ct % (30 / speed) == 0) { object.a_st += 1; if (object.a_st == numberOfFrames) object.a_st = 0; } object.a_fr = startFrame + object.a_st; this.spr(object.a_fr, object.x, object.y); } private calculateAlphaHexCode(a: number): string { a = Math.round(a * 100) / 100; var alpha = Math.round(a * 255); var hex = (alpha + 0x10000) .toString(16) .substr(-2) .toUpperCase(); return hex; } }