UNPKG

jrpg-stellar-engine

Version:
502 lines (401 loc) 14.7 kB
/** * Level map with all tiles, items and actors */ class Level { /** * Constructor * @param args.view: Object - view refernece */ constructor(args) { // Center of the coordinate system correction (this is constant not scroll) this.offset = {x: 0, y: 0}; // The furthest tiles in world coordinates (counted automatically) this.bounds = {left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity}; // Scale this.scale = 1; // Tileset definitions {'tileset id': {ref: TileSet object reference, first: Number of index offset}, ...} this.tilesets = {}; // Tile size this.tile = { w: 0, h: 0 }; // Environment layers [{name: 'string', class: 'colliders|empty', map: [[]]}, ...] this.layers = []; // [class Layer] // Custom renderers this.renderers = {}; // Actors {type: {'name': object, ...}, ...} for items, chars, npcs, mobs, mounts, vehicles, etc. this.actors = {}; // Spawn points {'player-1': [ SpawnPoint, ...], ...} this.spawnpoints = {}; // Respawn points [RespawnPoint, ...] this.respawnpoints = []; // Stairs [{x1, y1, x2, y2, x3, y3, x4, y4}, ...] from left-top clockwise in world coordinates this.stairs = []; // Portals to other maps [{map, spawn, left, top, right, bottom}, ...] this.portals = []; // Generic masks [{name, left, top, right, bottom}, ...] this.masks = []; // Generic shapes [{name, x, y, points: [{x, y}, ...], properties: {}}, ...] this.shapes = []; // Texts [{name, x, y, align, text}, ...] this.texts = []; // Map global properties this.properties = {}; // Id generation counter this.idGen = 0; // Loaders this.loader = { acx: new LoaderACX() }; // View reference this.view = args.view; } /** * Generate unique id */ genId() { return ++this.idGen; } /** * Returns list of all colliders */ getColliders(margin = 0) { const colliders = []; const tileset = Object.values(this.tilesets).length ? Object.values(this.tilesets)[0] : null; if (tileset) { this.layers.forEach(layer => { if (layer.class == 'colliders') { colliders.push(...tileset.ref.getColliders(layer.map, this.offset.x, this.offset.y, tileset.first, margin)); } }); } return colliders; } /** * Returns list of all stairs/slopes */ getStairs() { return this.stairs; } /** * Returns a random spawn point of type */ getSpawnPoint(type, fallback = {x: 0, y: 0}) { if (type in this.spawnpoints && this.spawnpoints[type].length > 0) { const spawnpoint = this.spawnpoints[type][randomRangeInt(0, this.spawnpoints[type].length - 1)]; return {x: spawnpoint.x, y: spawnpoint.y}; } return fallback; } /** * Returns all spawn point for given type */ getSpawnPoints(type, fallback = [{x: 0, y: 0}]) { if (type in this.spawnpoints && this.spawnpoints[type].length > 0) { return this.spawnpoints[type]; } return fallback; } /** * Check respawn points and spawn if necessary */ async respawn() { // Iterate through all respawn types for (const point of this.respawnpoints) { const spawnArgs = point.spawnArgs(); if (spawnArgs) await this.spawn({ ...spawnArgs, point }); } } /** * Spawn an actor directly * @param args.id: string - (optional) custom id * @param args.type: string - actor group 'mob', 'vehicle' etc. * @param args.actor.xml: string - acx as an xml string [optional] | @param args.actor.data: string - use actor.serialize() data [optional] * @param args.actor.scale: Number - sprite scale * @param args.actor.properties: Object - actor properties * @param args.layer: string - layer name of the level in which to spawn * @param args.x: Number - x coordinate to spawn * @param args.y: Number - y coordinate to spawn * @param args.w: Number - width of the random spawn area * @param args.h: Number - height of the random spawn area * @param args.point: RespawnPoint - (optional) point reference to know where to respawn again * @returns {Promise<ActorInstance>} Promise resolving to the spawned actor */ async spawn(args) { // Position in the area range const transform = { x: args.x + (args.w / 2) + Math.floor(Math.random() * (args.w + 1) - (args.w / 2)), y: args.y + (args.h / 2) + Math.floor(Math.random() * (args.h + 1) - (args.h / 2)) }; // Create instance (use await!) let actorInstance; if ('xml' in args.actor) { actorInstance = await this.loader.acx.parseActor({ ...args.actor, type: args.type, transform }); } else { // Sprawdź czy createActor też jest async actorInstance = await this.loader.acx.createActor({ ...args.actor.data, type: args.type, transform }); } if ('point' in args) actorInstance.spawn = args.point; if (!actorInstance) { console.error('Error spawning ACX', args); return null; } // Assign references actorInstance.level = this; actorInstance.view = this.view; // Actor's id if ('id' in args) actorInstance.id = args.id; if (!actorInstance.id) actorInstance.id = `${args.layer}.${actorInstance.name}.${this.genId()}`; // Add to layer registry const objectLayer = this.layers.find(layer => layer.name === args.layer); if (objectLayer && objectLayer.class === 'objects') { if (!objectLayer.actors) objectLayer.actors = []; if (!objectLayer.actors.includes(actorInstance.id)) objectLayer.actors.push(actorInstance.id); } // Add a type to global actors registry if (!(args.type in this.actors)) this.actors[args.type] = {}; // Add an actor this.actors[args.type][actorInstance.id] = actorInstance; // Returns new actor instance return actorInstance; } /** * Despawn an actor * @param id: string - actor's unique id */ despawn(id) { // Remove from layer for (const layer of this.layers) { if (('actors' in layer) && Array.isArray(layer.actors)) { const idx = layer.actors.indexOf(id); if (idx !== -1) { layer.actors.splice(idx, 1); break; } } } // Remove from actor types for (const type in this.actors) { if (id in this.actors[type]) { if ('spawn' in this.actors[type][id]) this.actors[type][id].spawn.decrease(); delete this.actors[type][id]; break; } } } /** * Returns list of all objects with class 'portal' */ getPortals() { return this.portals; } /** * Returns list of all objects with class 'mask' */ getMasks() { return this.masks; } /** * Find layer */ getLayer(name) { for (const layer of this.layers) { if (layer.name == name) return layer; } return null; } /** * Returns list of all layers */ getLayers() { return this.layers; } /** * Register custom layers with actor loaders and renderers */ addCustomRenderLayer(name, actors) { this.renderers[name] = actors; } /** * Update all actors */ update(deltaTime) { // Update tilesets for (const tileset of Object.values(this.tilesets)) { tileset.ref.update(deltaTime); } // Gather level colliders const colliders = this.getColliders(); // Update all actors Object.values(this.actors).forEach(actors => { Object.values(actors).forEach(actor => { actor.update({ deltaTime, colliders }); }); }); } /** * Render layers (from list or just all of them) */ render(view, layers = null) { // Iterate layers this.layers.forEach(layer => { if (layers && !layers.includes(layer.name)) return; // Render backgrounds/foregrounds if (layer.class == 'image') this.renderImageLayer(view, layer); // Render actors else if (layer.class == 'objects') this.renderObjectsLayer(view, layer); // Render tiles else if (layer.class == 'tiles' || layer.class == 'colliders') this.renderTilesLayer(view, layer); // Render custom else this.renderCustomLayer(view, layer); }); } /** * Render image layer */ renderImageLayer(view, layer) { view.background( layer.src, {x: layer.x * this.scale, y: layer.y * this.scale}, {w: layer.w * this.scale, h: layer.h * this.scale}, layer.repeat, layer.parallax, layer.coordinates ); } /** * Render objects layer */ renderObjectsLayer(view, layer) { // Visible actors const actors = []; // Collect culled actors Object.values(this.actors).forEach(actorsGroup => { // Iterate actors in group Object.entries(actorsGroup).forEach(([actorId, actor]) => { // Iterate actor IDs in the current layer layer.actors.forEach(actorIdOnLayer => { if (actorIdOnLayer == actorId) { const pos = view.world2Screen({ x: actor.transform.x - actor.tile.scaled.halfWidth, y: actor.transform.y - actor.tile.scaled.halfHeight }); if (pos.x > -actor.tile.scaled.width && pos.x < view.canvas.width + actor.tile.scaled.width && pos.y > -actor.tile.scaled.height && pos.y < view.canvas.height + actor.tile.scaled.height) actors.push(actor); } }); }); }); // Sort actors actors.sort(function(a, b) { return (a.transform.y - a.origin.y + a.tile.scaled.halfHeight) - (b.transform.y - b.origin.y + b.tile.scaled.halfHeight); }); // Render actors actors.forEach(actor => { actor.render(view); }); } /** * Render tiles layer */ renderTilesLayer(view, layer) { for (const tileset of Object.values(this.tilesets)) { tileset.ref.render(view, layer.map, this.offset.x - layer.offset.x, this.offset.y - layer.offset.y, tileset.first); } } /** * Render custom layer */ renderCustomLayer(view, layer) { if (layer.class in this.renderers) { layer.actors.forEach(actor => { if (actor.type in this.renderers[layer.class]) this.renderers[layer.class][actor.type](actor); }); } } /** * Render debug info */ debug(view) { // Iterate layers this.layers.forEach(layer => { // Colliders for (const tileset of Object.values(this.tilesets)) { if (layer.class == 'colliders') tileset.ref.debug(view, layer.map, this.offset.x, this.offset.y, tileset.first); } }); // Center view correction const ox = view.center.x + view.offset.x; const oy = view.center.y + view.offset.y; // Spawn points Object.entries(this.spawnpoints).forEach(([name, points]) => { points.forEach(point => { // Arrow view.ctx.fillStyle = 'rgba(0,255,0,0.8)'; view.ctx.beginPath(); const ax = point.x + ox; const ay = point.y + oy; view.ctx.moveTo(0 + ax, 0 + ay); view.ctx.lineTo(-8 + ax, -16 + ay); view.ctx.lineTo(8 + ax, -16 + ay); view.ctx.fill(); // Name view.ctx.font = "14px sans-serif"; const txtc = view.ctx.measureText(name).width / 2; view.ctx.fillText(name, ax - txtc, ay + 16); }); }); // Stairs view.ctx.fillStyle = 'rgba(0,255,255,0.5)'; this.stairs.forEach(shape => { // Draw path view.ctx.beginPath(); view.ctx.moveTo(shape.x1 + ox, shape.y1 + oy); view.ctx.lineTo(shape.x2 + ox, shape.y2 + oy); view.ctx.lineTo(shape.x3 + ox, shape.y3 + oy); view.ctx.lineTo(shape.x4 + ox, shape.y4 + oy); view.ctx.fill(); }); // Portals view.ctx.fillStyle = 'rgba(50,0,50,0.5)'; this.portals.forEach(shape => { view.ctx.fillRect( shape.left + ox, shape.top + oy, shape.right - shape.left, shape.bottom - shape.top ); }); // Masks view.ctx.fillStyle = 'rgba(3, 131, 61, 0.5)'; this.masks.forEach(shape => { view.ctx.fillRect( shape.left + ox, shape.top + oy, shape.right - shape.left, shape.bottom - shape.top ); }); // Actors Object.values(this.actors).forEach(actorsGroup => { // Iterate actors in group Object.values(actorsGroup).forEach(actor => { actor.debug(view); }); }); // Bounds if (this.bounds.left !== Infinity) { view.ctx.strokeStyle = 'rgba(255, 255, 0, 0.8)'; view.ctx.lineWidth = 2; view.ctx.strokeRect( this.bounds.left + ox, this.bounds.top + oy, this.bounds.right - this.bounds.left, this.bounds.bottom - this.bounds.top ); } // View if (view.debugEnabled) view.debug(); } }