import ChunkMgr from "./chunk/chunk_mgr.js";
import Vec2 from "../vector/vec2.js";
import ChunkUnit from "./chunk/unit/chunk_unit.js";
import PathBuilder from "./path/path_builder.js";
import Color from "../../util/color/color.js";
import Bounds from "../vector/bounds.js";
import EventHandler from "../../event/event_handler.js";
/**
* @module Space
* @fileoverview Contains Space class.
*/
/**
* Default fill color for new units.
* @type {Color}
*/
const FILL_COLOR = new Color(250,47,77);
/**
* @class
* Root class for holding all chunks and actors.
*/
class Space {
/**
* Create a new space.
* @param {App} app Parent application
* @param {number} chunkSize Number of units per chunk
* @param {number} unitSize Number of pixels per unit
* @constructor
*/
constructor(app, chunkSize = 16, unitSize = 16) {
this.app = app;
this.id = "";
this.width = $$.width;
this.height = $$.height;
this._camera = null;
/// The background color of the space
this.background = "rgb(100, 150, 255)";
this.chunkSize = chunkSize;
this.unitSize = unitSize;
this.chunks = new ChunkMgr(this, chunkSize, unitSize);
this._globalIDs = new Map();
this._events = new EventHandler();
}
//
// Getters
//
/**
* Get the space's event handler.
* @returns {EventHandler} Event handler
*/
get events() {
return this._events;
}
/**
* Get the camera of the space.
* @returns {Camera|null} Current camera
*/
get camera() {
return this._camera;
}
/**
* Get the mouse position relative to the camera and its zoom.
* @returns {Vec2} Mouse position in space
*/
getMousePos() {
let pos = $$.input.mouse.pos.copy().divv(this.camera.calcZoom());
if (this.camera !== null)
pos = pos.add(this.camera.getOffsetWithZoom());
return pos;
}
//
// Setters
//
/**
* Set the camera of the space.
* @param {Camera|null} camera New current camera
*/
set camera(camera) {
this._camera = camera;
this.addActor(camera);
}
//
// Update & Draw
//
/**
* Update the space and its actors.
* @param {number} elapsed Time since last update
*/
update(elapsed) {
this.chunks.update(elapsed);
}
/**
* Draw the space and its actors.
* @param {CanvasRenderingContext2D} ctx The canvas context to draw on
*/
draw(ctx) {
this.drawBackground(ctx);
if (this.camera !== null) {
this.chunks.draw(ctx);
this.camera.drawFilter(ctx);
}
}
/**
* Draw the background color of the space.
* @param {CanvasRenderingContext2D} ctx The canvas context to draw on
*/
drawBackground(ctx) {
if (this.background instanceof Color)
ctx.fillStyle = this.background.get();
else
ctx.fillStyle = this.background;
ctx.fillRect(0, 0, $$.width, $$.height);
}
//
// Actors
//
/**
* Add an actor to the space.
* @param {Actor} actor The actor to add
*/
addActor(actor) {
this.chunks.addActor(actor);
return actor;
}
/**
* Add an actor's global identifier to the space (do not call manually).
* @param {Actor} actor Actor to register identifier for
*/
addGlobalID(actor) {
this._globalIDs.set(actor.globalID, actor);
}
/**
* Get an actor with specified global identifier.
* @param {string} id Identifier to search for
* @returns {Actor|null} Actor with specified identifier, or null if it does not exist.
*/
find(id) {
if (!this._globalIDs.has(id))
return null;
return this._globalIDs.get(id);
}
/**
* Get all actors near a specified position and within a radius.
* @param {Actor} ignore Actor to ignore
* @param {Vec2} pos Point to find actors around
* @param {number} radius Radius to find actors within
* @returns {Actor[]} Array of actors near point within at radius
*/
getNearbyActors(ignore, pos, radius) {
return this.chunks.getNearbyActors(ignore, pos, radius);
}
checkUnitCollision(actor, pos) {
let bounds = new Bounds();
for (const comp of actor.components.getCollisions())
if (comp.layers.has("default"))
bounds = bounds.merge(comp.bounds);
bounds = bounds.scale(actor.scale).add(actor.pos);
for (const unit of this.getUnitsIn(bounds, true)) {
const check = unit.checkCollision(actor, pos);
if (check.result)
return check;
}
return {
result: false
};
}
/**
* Check collision with all other actors.
* @param {Actor} actor The actor to check collision for
* @param {Vec2} pos The position to attempt to move to
* @returns {Object} Object containing collision result data
*/
checkCollision(actor, pos) {
const actors = this.getNearbyActors(actor, pos, 256);
for (const other of actors) {
if (other.collision === null)
continue;
const result = actor.checkCollision(other.collision, pos);
if (result.result) {
return result;
}
}
return {
result: false
};
}
/**
* Generate a new path in the space.
* @param {Vec2} start Starting position
* @param {Vec2} end Final position
* @returns {Path|null} Generated path, or null if unsuccessful.
*/
generatePath(start, end) {
const path = new PathBuilder(this, start, end);
return path.get();
}
//
// Chunks
//
/**
* Handle a chunk just being added.
* @param {Chunk} chunk Chunk that was just added
*/
chunkAdded(chunk) {}
//
// Units
//
/**
* Get a unit at a specified position.
* @param {number} x Position along x-axis
* @param {number} y Position along y-axis
* @returns {ChunkUnit|null} Chunk unit at specified position, or null if there is none.
*/
getUnitAt(x, y) {
const size = this.chunkSize * this.unitSize;
const chunk = this.chunks.valChunk(Math.floor(x / size), Math.floor(y / size));
return chunk.getUnitAt(x, y);
}
getUnitsIn(bounds, filledOnly = false) {
const units = [];
const width = Math.floor(bounds.width / this.unitSize) + 1;
const height = Math.floor(bounds.height / this.unitSize) + 1;
for (let x = -1; x <= width + 1; x++) {
for (let y = -1; y <= height + 1; y++) {
const pos = bounds.min.addv(x * this.unitSize, y * this.unitSize);
const unit = this.getUnitAt(pos.x, pos.y);
if (unit === null)
continue;
if (filledOnly && unit.canWalkOn)
continue;
units.push(unit);
}
}
return units;
}
/**
* Get a unit at specified indices.
* @param {number} x Index along x-axis
* @param {number} y Index along y-axis
* @returns {ChunkUnit|null} Chunk unit at specified indices, or null if there is none.
*/
getUnit(x, y) {
const chunk = this.chunks.valChunk(Math.floor(x / this.chunkSize), Math.floor(y / this.chunkSize));
return chunk.getUnit(x - (this.chunkSize * chunk.x), y - (this.chunkSize * chunk.y));
}
/**
* Set a unit at a specified position.
* @param {number} x Position along x-axis
* @param {number} y Position along y-axis
* @param {ChunkUnit} unit Chunk unit to set at specified position
* @param {boolean} regenCache Whether to regenerate cached units image
* @private
*/
_setUnit(x, y, unit, regenCache = true) {
const chunk = this.chunks.valChunk(Math.floor(x / this.chunkSize), Math.floor(y / this.chunkSize));
chunk.setUnit(x - (this.chunkSize * chunk.x), y - (this.chunkSize * chunk.y), unit);
chunk.cachedUnits = null;
if (regenCache && $$.flags.chunks.cache) {
$$.createImage(ctx => {
for (let x = 0; x < chunk.units.length; x++) {
for (let y = 0; y < chunk.units[x].length; y++) {
const unit = chunk.units[x][y];
if (unit !== null)
unit.draw(ctx);
}
}
}, image => chunk.cachedUnits = image, this.chunkSize, this.chunkSize);
}
}
/**
* Fill a single unit at a specified position.
* @param {number} x Position along x-axis
* @param {number} y Position along y-axis
* @param {boolean} fill Whether to fill unit or clear it
* @param {Color|string|Sprite} color Color, sprite, or sprite identifier for unit
* @param {boolean} walkOn Whether to allow walking on unit
* @param {boolean} persistent Whether to make parent chunk persistent
* @param {boolean} regenCache Whether to regenerate cached units image
*/
fillUnit(x, y, fill = true, color = FILL_COLOR, walkOn = false,
persistent = true, regenCache = true) {
const size = this.chunkSize * this.unitSize;
const chunk = this.chunks.valChunkAt(new Vec2(x * size, y * size));
let unit = null;
if (fill) {
unit = new ChunkUnit({
chunk: chunk,
x: x,
y: y,
sprite: color
});
chunk.persistent = persistent;
unit.canWalkOn = walkOn;
}
this._setUnit(x, y, unit, regenCache);
}
/**
*
* @param {number} x Position along x-axis
* @param {number} y Position along y-axis
* @param {string} pattern Pattern to use for generation
* @param {Color|string|Sprite} color Color, sprite, or sprite identifier for unit
* @param {string} unfilled Character to use for unfilled units
* @param {string} filled Character to use for filled units
*/
setUnitPattern(x = 0, y = 0, pattern = "", color= FILL_COLOR,
unfilled = '0', filled = '1') {
if (pattern.length === 0)
return;
const lines = pattern.split("\n");
let yi = 0;
for (const line of lines) {
let xi = 0;
for (const c of line) {
if (c === filled) {
const pos = new Vec2(x + xi, y + yi);
const ind = this.chunks.getIndex(pos);
const chunk = this.chunks.valChunk(ind.x, ind.y);
const unit = new ChunkUnit({
chunk: chunk,
x: pos.x,
y: pos.y,
sprite: color
});
unit.canWalkOn = false;
this._setUnit(pos.x, pos.y, unit);
}
xi++;
}
yi++;
}
}
/**
* Fill an area at a specified position.
* @param {number} x1 First position along x-axis
* @param {number} y1 First position along y-axis
* @param {number} x2 Second position along x-axis
* @param {number} y2 Second position along y-axis
* @param {boolean} fill Whether to fill unit or clear it
* @param {Color|string|Sprite} color Color, sprite, or sprite identifier for unit
* @param {boolean} walkOn Whether to allow walking on unit
*/
fillUnits(x1, y1, x2, y2, fill = true,
color = FILL_COLOR, walkOn = false) {
for (let x = x1; x < x2; x++) {
for (let y = y1; y < y2; y++) {
const pos = new Vec2(x, y);
const ind = this.chunks.getIndex(pos);
const chunk = this.chunks.valChunk(ind.x, ind.y);
let unit = null;
if (fill) {
unit = new ChunkUnit({
chunk: chunk,
x: pos.x,
y: pos.y,
sprite: color
});
unit.canWalkOn = walkOn;
}
this._setUnit(pos.x, pos.y, unit);
}
}
}
/**
* Fill walls around an area at a specified position.
* @param {number} x1 First position along x-axis
* @param {number} y1 First position along y-axis
* @param {number} x2 Second position along x-axis
* @param {number} y2 Second position along y-axis
* @param {boolean} fill Whether to fill unit or clear it
* @param {Color|string|Sprite} color Color, sprite, or sprite identifier for unit
* @param {boolean} walkOn Whether to allow walking on unit
*/
fillUnitsWalls(x1, y1, x2, y2, fill = true,
color = FILL_COLOR, walkOn = false) {
for (let x = x1; x < x2; x++) {
for (let y = y1; y < y2; y++) {
if ((y !== y1 && y !== y2 - 1) && (x !== x1 && x !== x2 - 1))
continue;
const pos = new Vec2(x, y);
const ind = this.chunks.getIndex(pos.mulv(this.unitSize));
const chunk = this.chunks.valChunk(ind.x, ind.y);
chunk.persistent = true;
let unit = null;
if (fill) {
unit = new ChunkUnit({
chunk: chunk,
x: pos.x,
y: pos.y,
sprite: color
});
unit.canWalkOn = walkOn;
}
this._setUnit(pos.x, pos.y, unit);
}
}
}
/**
* Fill a line from one point to another.
* @param {number} x1 First position along x-axis
* @param {number} y1 First position along y-axis
* @param {number} x2 Second position along x-axis
* @param {number} y2 Second position along y-axis
* @param {boolean} fill Whether to fill unit or clear it
* @param {Color|string|Sprite} color Color, sprite, or sprite identifier for unit
* @param {boolean} walkOn Whether to allow walking on unit
*/
fillUnitsLine(x1, y1, x2, y2, fill = true,
color = FILL_COLOR, walkOn = false) {
const p1 = new Vec2(x1, y1);
const p2 = new Vec2(x2, y2);
const norm = p2.sub(p1).norm();
const length = Math.floor(p1.dist(p2));
for (let i = 0; i < length; i++) {
const pos = p1.add(norm.mulv(i));
const ind = this.chunks.getIndex(pos);
const chunk = this.chunks.valChunk(ind.x, ind.y);
chunk.persistent = true;
let unit = null;
if (fill) {
unit = new ChunkUnit({
chunk: chunk,
x: pos.x,
y: pos.y,
sprite: color
});
unit.canWalkOn = false;
}
this._setUnit(pos.x, pos.y, unit);
}
}
placePrefab(id, x = 0, y = 0) {
x = Math.floor(x);
y = Math.floor(y);
$$.reg.prefab.get(id).place(this, x, y);
}
}
export default Space;