refactor: nix layers

This commit is contained in:
cha0s 2021-05-09 18:13:18 -05:00
parent 6dae625953
commit 2a8ae10ec7
27 changed files with 358 additions and 816 deletions

View File

@ -28,7 +28,10 @@ export default (latus) => {
if ('create' === s13nType) {
const {Entity} = latus.get('%resources');
const {id} = packet.data.synchronized;
this.addEntity(this.synchronized(Entity.resourceId, id));
const entity = this.synchronized(Entity.resourceId, id);
if (entity) {
this.addEntity(entity);
}
}
}
@ -72,6 +75,9 @@ export default (latus) => {
async load(json = []) {
await super.load(json);
if (0 === json.length) {
return;
}
const {Entity} = latus.get('%resources');
const entityInstances = await Promise.all(json.map((entity) => Entity.load(entity)));
for (let i = 0; i < entityInstances.length; i++) {

View File

@ -0,0 +1,60 @@
import {Rectangle, Vector} from '@avocado/math';
import {JsonResource} from '@avocado/resource';
import Image from './image';
export default class Atlas extends JsonResource {
#image;
#size = [0, 0];
#subimages = [];
destroy() {
this.#subimages.forEach((subimage) => {
subimage.destroy();
});
this.#subimages = [];
}
get image() {
return this.#image;
}
async load(json = {}) {
await super.load(json);
this.destroy();
const {imageUri = json.uri?.replace('.atlas.json', '.png'), type = 'grid'} = json;
if (!imageUri) {
return;
}
this.#image = await Image.load(imageUri);
switch (type) {
case 'grid': {
const {size} = json;
if (Vector.isNull(size)) {
return;
}
const grid = Vector.div(this.#image.size, size);
const rectangle = Rectangle.compose([0, 0], size);
for (let j = 0; j < grid[1]; ++j) {
for (let i = 0; i < grid[0]; ++i) {
const subimage = this.#image.subimage(rectangle);
this.#subimages.push(subimage);
rectangle[0] += size[0];
}
rectangle[0] = 0;
rectangle[1] += size[1];
}
break;
}
default:
}
}
subimage(index) {
return this.#subimages[index];
}
}

View File

@ -1,7 +1,7 @@
import {Vector} from '@avocado/math';
import {Class, compose} from '@latus/core';
import {Image} from './resources/image';
import Image from './image';
const decorate = compose(
Vector.Mixin('size', 'width', 'height', {

View File

@ -40,6 +40,12 @@ export default class Container extends Renderable {
this.container.addChild(child.internal);
}
addChildren(children) {
for (let i = 0; i < children.length; i++) {
this.addChild(children[i]);
}
}
addFilter(id, type, options = {}) {
if (this.container.isFake) {
return;

View File

@ -15,7 +15,7 @@ const cache = 'production' === process.env.NODE_ENV
set: () => {},
};
export class Image extends Resource {
export default class Image extends Resource {
constructor() {
super();
@ -110,5 +110,3 @@ export class Image extends Resource {
}
}
export default () => Image;

View File

@ -2,11 +2,12 @@ import {gatherWithLatus} from '@latus/core';
import './init';
export {default as Atlas} from './atlas';
export {default as Canvas} from './canvas';
export {default as Color} from './color';
export {default as Stage} from './components/stage';
export {default as Container} from './container';
export {Image} from './resources/image';
export {default as Image} from './image';
export {default as Primitives} from './primitives';
export {default as Renderable} from './renderable';
export {default as Renderer} from './renderer';
@ -15,9 +16,6 @@ export {default as Text} from './text';
export default {
hooks: {
'@avocado/resource/resources': gatherWithLatus(
require.context('./resources', false, /\.js$/),
),
'@avocado/traits/traits': gatherWithLatus(
require.context('./traits', false, /\.js$/),
),

View File

@ -3,6 +3,7 @@ import {StateProperty, Trait} from '@avocado/traits';
import {Rectangle, Vector} from '@avocado/math';
import {compose} from '@latus/core';
import Image from '../image';
import Sprite from '../sprite';
const decorate = compose(
@ -11,7 +12,7 @@ const decorate = compose(
}),
);
export default (latus) => class Pictured extends decorate(Trait) {
export default () => class Pictured extends decorate(Trait) {
#currentImage = '';
@ -161,7 +162,6 @@ export default (latus) => class Pictured extends decorate(Trait) {
.filter(([, {uri}]) => !!uri),
);
this.#currentImage = this.state.currentImage;
const {Image} = latus.get('%resources');
this.#images = await mapValuesAsync(images, ({uri}) => Image.load(uri));
this.#sprites = await mapValuesAsync(this.#images, async (image) => new Sprite(image));
Object.keys(this.#sprites).forEach((key) => {

View File

@ -58,9 +58,7 @@ export default () => class Rastered extends Trait {
},
onYChanged: () => {
if (this.#usingAutoZIndex) {
this.#container.zIndex = this.entity.y;
}
this.onYChanged();
},
opacityChanged: () => {
@ -77,16 +75,6 @@ export default () => class Rastered extends Trait {
this.#container.rotation = this.entity.rotation;
},
traitAdded: () => {
if (this.#container) {
this.#container.alpha = this.entity.opacity;
this.#container.rotation = this.entity.rotation;
this.#container.scale = this.entity.visibleScale;
}
this.synchronizePosition();
this.onZIndexChanged(this.entity.zIndex);
},
visibleScaleChanged: () => {
if (this.#container) {
this.#container.scale = this.entity.visibleScale;
@ -103,6 +91,13 @@ export default () => class Rastered extends Trait {
async load(json) {
await super.load(json);
if ('client' === process.env.SIDE) {
if (this.#container) {
this.#container.alpha = this.entity.opacity;
this.#container.rotation = this.entity.rotation;
this.#container.scale = this.entity.visibleScale;
}
this.onZIndexChanged(this.entity.zIndex);
this.renderTick();
const {filter} = this.params;
if (filter) {
this.#container.setFilter(filter);
@ -150,21 +145,28 @@ export default () => class Rastered extends Trait {
}
}
onYChanged() {
if (this.#usingAutoZIndex) {
this.#container.zIndex = this.entity.y;
}
}
onZIndexChanged(zIndex) {
this.#usingAutoZIndex = AUTO_ZINDEX === zIndex;
if (!this.#container) {
return;
}
this.#usingAutoZIndex = AUTO_ZINDEX === zIndex;
if (!this.#usingAutoZIndex) {
this.#container.zIndex = zIndex;
}
else {
this.#container.zIndex = this.entity.y;
this.onYChanged();
}
}
renderTick() {
this.synchronizePosition();
this.synchronizeZIndex();
}
synchronizePosition() {
@ -172,7 +174,10 @@ export default () => class Rastered extends Trait {
return;
}
this.#container.position = this.entity.position;
if (this.#usingAutoZIndex) {
}
synchronizeZIndex() {
if (this.#container && this.#usingAutoZIndex) {
this.#container.zIndex = this.entity.y;
}
}

View File

@ -1,7 +1,7 @@
import {assert, expect} from 'chai';
import {validate} from 'uuid';
import {Image} from '../src/resources/image';
import Image from '../src/image';
Image.root = 'test/fixtures';

View File

@ -1,5 +1,24 @@
export default (EntityList) => class PhysicsEntityList extends EntityList {
#world;
constructor() {
super();
this.on('entityAdded', this.onEntityAdded, this);
this.on('entityRemoved', this.onEntityRemoved, this);
}
onEntityAdded(entity) {
// eslint-disable-next-line no-param-reassign
entity.world = this.#world;
}
// eslint-disable-next-line class-methods-use-this
onEntityRemoved(entity) {
// eslint-disable-next-line no-param-reassign
entity.world = null;
}
set world(world) {
const entities = Object.values(this.entities);
for (let i = 0; i < entities.length; i++) {
@ -8,6 +27,7 @@ export default (EntityList) => class PhysicsEntityList extends EntityList {
entity.world = world;
}
}
this.#world = world;
}
};

View File

@ -1,77 +0,0 @@
import {
Vector,
Vertice,
} from '@avocado/math';
import PolygonShape from '../../shape/polygon';
export default (Layer) => class PhysicsLayer extends Layer {
#impassable = {};
#tileBodies = [];
#world;
addTileBodies() {
if (!this.#world) {
return;
}
const {tileset} = this;
if (!tileset) {
return;
}
const hulls = this.tiles.indexHulls(this.#impassable);
if (0 === hulls.length) {
return;
}
for (let j = 0; j < hulls.length; ++j) {
const scaled = [];
for (let k = 0; k < hulls[j].length; ++k) {
scaled.push(Vector.mul(hulls[j][k], tileset.tileSize));
}
const [vertices, position] = Vertice.localize(scaled);
const shape = new PolygonShape({position, vertices});
const body = this.#world.createBody(shape);
body.static = true;
this.#tileBodies.push(body);
this.#world.addBody(body);
}
}
async load(json = {}) {
await super.load(json);
const {impassable = []} = json;
this.#impassable = new Set(impassable);
}
onTilesUpdate() {
this.removeTileBodies();
this.addTileBodies();
}
removeTileBodies() {
if (this.#tileBodies.length > 0 && this.#world) {
for (let i = 0; i < this.#tileBodies.length; i++) {
this.#world.removeBody(this.#tileBodies[i]);
}
this.#tileBodies = [];
}
}
setTiles(tiles) {
if (this.tiles) {
this.tiles.off('update', this.onTilesUpdate);
}
super.setTiles(tiles);
this.tiles.on('update', this.onTilesUpdate, this);
}
set world(world) {
this.removeTileBodies();
this.#world = world;
this.entityList.world = world;
this.addTileBodies();
}
};

View File

@ -1,9 +0,0 @@
export default (Layers) => class PhysicsLayers extends Layers {
set world(world) {
for (let index = 0; index < this.layers.length; index++) {
this.layers[index].world = world;
}
}
};

View File

@ -22,7 +22,11 @@ export default (Room) => class PhysicsRoom extends Room {
const world = new World();
world.stepTime = 1 / 60;
this.world = world;
this.layers.world = world;
this.entityList.world = world;
this.tiles.forEach((tiles) => {
// eslint-disable-next-line no-param-reassign
tiles.world = world;
});
if (Vector.isZero(this.size)) {
return;
}

View File

@ -0,0 +1,65 @@
import {
Vector,
Vertice,
} from '@avocado/math';
import PolygonShape from '../../shape/polygon';
export default (Tiles) => class PhysicsTiles extends Tiles {
#bodies = [];
#impassable = {};
#world;
constructor() {
super();
this.on('update', this.onTilesUpdate, this);
}
addBodies() {
if (!this.#world) {
return;
}
const hulls = this.indexHulls(this.#impassable);
for (let i = 0; i < hulls.length; ++i) {
const scaled = [];
for (let j = 0; j < hulls[i].length; ++j) {
scaled.push(Vector.mul(hulls[i][j], this.tileSize));
}
const [vertices, position] = Vertice.localize(scaled);
const body = this.#world.createBody(new PolygonShape({position, vertices}));
body.static = true;
this.#bodies.push(body);
this.#world.addBody(body);
}
}
async load(json = {}) {
await super.load(json);
const {impassable = []} = json;
this.#impassable = new Set(impassable);
}
onTilesUpdate() {
this.removeBodies();
this.addBodies();
}
removeBodies() {
if (this.#bodies.length > 0 && this.#world) {
for (let i = 0; i < this.#bodies.length; i++) {
this.#world.removeBody(this.#bodies[i]);
}
this.#bodies = [];
}
}
set world(world) {
this.removeBodies();
this.#world = world;
this.addBodies();
}
};

View File

@ -3,7 +3,6 @@ import {gatherWithLatus} from '@latus/core';
export {default as Room} from './components/room';
export {default as Camera} from './camera';
export {default as LayerView} from './layer-view';
export {default as RoomView} from './room-view';
export default {

View File

@ -1,52 +0,0 @@
import {EntityListView} from '@avocado/entity';
import {Container} from '@avocado/graphics';
import TilesView from './tiles-view';
const views = new Set();
export default class LayerView extends Container {
constructor(layer, renderer) {
super();
this.entityListView = new EntityListView(layer.entityList);
this.tilesView = new TilesView(layer.tiles, layer.tileset, renderer);
this.lastExtent = [0, 0, 0, 0];
this.layer = layer;
this.renderer = renderer;
this.addChild(this.tilesView);
this.addChild(this.entityListView);
if (module.hot) {
views.add(this);
}
}
destroy() {
super.destroy();
if (module.hot) {
views.delete(this);
}
}
renderChunksForExtent(extent) {
this.lastExtent = extent;
this.tilesView.renderChunksForExtent(extent);
}
}
if (module.hot) {
module.hot.accept('./tiles-view', () => {
const it = views.values();
for (let value = it.next(); value.done !== true; value = it.next()) {
const {value: view} = value;
const {lastExtent} = view;
view.removeChild(view.entityListView);
view.removeChild(view.tilesView);
view.tilesView = new TilesView(view.layer.tiles, view.layer.tileset, view.renderer);
view.renderChunksForExtent(lastExtent);
view.addChild(view.tilesView);
view.addChild(view.entityListView);
}
});
}

View File

@ -1,39 +0,0 @@
import {Container} from '@avocado/graphics';
import LayerView from './layer-view';
export default class LayersView extends Container {
constructor(layers, renderer) {
super();
this.layers = layers;
this.layerViews = [];
this.renderer = renderer;
this.layers.on('layerAdded', this.onLayerAdded, this);
for (let i = 0; i < this.layers.layers.length; i++) {
this.onLayerAdded(this.layers.layers[i], i);
}
this.layers.on('layerRemoved', this.onLayerRemoved, this);
}
onLayerAdded(layer, i) {
const layerView = new LayerView(layer, this.renderer);
this.layerViews[i] = layerView;
this.addChild(layerView);
}
onLayerRemoved(layer, i) {
const layerView = this.layerViews[i];
if (!layerView) {
return;
}
this.removeChild(layerView);
}
renderChunksForExtent(extent) {
for (let i = 0; i < this.layerViews.length; i++) {
this.layerViews[i].renderChunksForExtent(extent);
}
}
}

View File

@ -1,157 +0,0 @@
import {Property} from '@avocado/core';
import {JsonResource} from '@avocado/resource';
import {Synchronized} from '@avocado/s13n';
import {compose, EventEmitter} from '@latus/core';
export default (latus) => {
const decorate = compose(
EventEmitter,
Property('tileset', {
track: true,
}),
Synchronized(latus),
);
return class Layer extends decorate(JsonResource) {
#index;
constructor() {
super();
const {EntityList, Tiles} = latus.get('%resources');
this.setEntityList(new EntityList());
this.setTiles(new Tiles());
}
addEntity(entity) {
this.entityList.addEntity(entity);
}
destroy() {
this.entityList.destroy();
this.entityList.off('entityAdded', this.onEntityAddedToLayer);
this.entityList.off('entityRemoved', this.onEntityRemovedFromLayer);
if (this.tileset) {
this.tileset.destroy();
}
}
get entities() {
return this.entityList.entities;
}
findEntity(uuid) {
return this.entityList.findEntity(uuid);
}
get index() {
return this.#index;
}
set index(index) {
this.#index = index;
}
async load(json = {}) {
await super.load(json);
const {
entities,
tiles,
tilesetUri,
} = json;
const {EntityList, Tileset} = latus.get('%resources');
await this.tiles.load(tiles);
this.tileset = tilesetUri
? await Tileset.load({extends: tilesetUri})
: new Tileset();
this.setEntityList(
entities
? await EntityList.load(entities)
: new EntityList(),
);
}
async onEntityAddedToLayer(entity) {
await entity.addTrait('Layered');
// eslint-disable-next-line no-param-reassign
entity.layer = this;
entity.emit('addedToLayer');
this.emit('entityAdded', entity);
}
onEntityRemovedFromLayer(entity) {
// eslint-disable-next-line no-param-reassign
entity.layer = null;
entity.emit('removedFromLayer', this);
entity.removeTrait('Layered');
this.emit('entityRemoved', entity);
}
removeEntity(entity) {
this.entityList.removeEntity(entity);
}
get s13nId() {
return this.#index;
}
setEntityList(entityList) {
if (this.entityList) {
this.stopSynchronizing(this.entityList);
Object.values(this.entityList.entities).forEach((entity) => {
this.onEntityRemovedFromLayer(entity);
});
this.entityList.off('entityRemoved', this.onEntityRemovedFromLayer);
this.entityList.off('entityAdded', this.onEntityAddedToLayer);
}
this.entityList = entityList;
this.entityList.on('entityAdded', this.onEntityAddedToLayer, this);
this.entityList.on('entityRemoved', this.onEntityRemovedFromLayer, this);
Object.values(this.entityList.entities).forEach((entity) => {
this.onEntityAddedToLayer(entity);
});
this.startSynchronizing(this.entityList);
}
setTileAt(position, tile) {
this.tiles.setTileAt(position, tile);
}
setTiles(tiles) {
if (this.tiles) {
this.stopSynchronizing(this.tiles);
}
this.tiles = tiles;
this.startSynchronizing(tiles);
}
stampAt([x, y, w, h], tiles) {
this.tiles.stampAt([x, y, w, h], tiles);
}
tick(elapsed) {
this.entityList.tick(elapsed);
this.tiles.tick();
}
tileAt(position) {
return this.tiles.tileAt(position);
}
toJSON() {
return {
entities: this.entityList.toJSON(),
tilesetUri: this.tileset.uri,
tiles: this.tiles.toJSON(),
};
}
toNetwork(informed) {
return {
entities: this.entityList.toNetwork(informed),
tilesetUri: this.tileset.uri,
tiles: this.tiles.toNetwork(informed),
};
}
};
};

View File

@ -1,128 +0,0 @@
import {compose, EventEmitter} from '@latus/core';
import {JsonResource} from '@avocado/resource';
import {Synchronized} from '@avocado/s13n';
export default (latus) => {
const decorate = compose(
EventEmitter,
Synchronized(latus),
);
return class Layers extends decorate(JsonResource) {
constructor() {
super();
this.layers = [];
}
addEntityToLayer(entity, layerIndex) {
const layer = this.layers[layerIndex];
if (!layer) {
return;
}
layer.addEntity(entity);
}
addLayer(layer) {
// eslint-disable-next-line no-param-reassign
layer.index = this.layers.length;
this.startSynchronizing(layer);
layer.on('entityAdded', this.onEntityAddedToLayers, this);
layer.on('entityRemoved', this.onEntityRemovedFromLayers, this);
this.layers.push(layer);
this.emit('layerAdded', layer);
}
destroy() {
for (let i = 0; i < this.layers.length; i++) {
const layer = this.layers[i];
this.removeLayer(layer);
layer.destroy();
}
}
get entities() {
const entities = {};
for (let i = 0; i < this.layers.length; i++) {
const layerEntities = Object.entries(this.layers[i].entities);
for (let j = 0; j < layerEntities.length; j++) {
const [uuid, entity] = layerEntities[j];
entities[uuid] = entity;
}
}
return entities;
}
findEntity(uuid) {
for (let i = 0; i < this.layers.length; i++) {
const foundEntity = this.layers[i].findEntity(uuid);
if (foundEntity) {
return foundEntity;
}
}
return undefined;
}
layer(index) {
return this.layers[index];
}
async load(json = []) {
await super.load(json);
this.removeAllLayers();
const {Layer} = latus.get('%resources');
const layers = await Promise.all(json.map((layer) => Layer.load(layer)));
for (let i = 0; i < layers.length; i++) {
this.addLayer(layers[i]);
}
}
onEntityAddedToLayers(entity) {
this.emit('entityAdded', entity);
}
onEntityRemovedFromLayers(entity) {
this.emit('entityRemoved', entity);
}
removeAllLayers() {
for (let i = 0; i < this.layers.length; i++) {
this.removeLayer(this.layers[i]);
}
}
removeEntityFromLayer(entity, layerIndex) {
const layer = this.layers[layerIndex];
if (!layer) {
return;
}
layer.removeEntity(entity);
}
removeLayer(layer) {
const index = this.layers.indexOf(layer);
if (-1 === index) {
return;
}
layer.off('entityAdded', this.onEntityAddedToLayers);
layer.off('entityRemoved', this.onEntityRemovedFromLayers);
this.layers.splice(index, 1);
this.emit('layerRemoved', layer);
this.stopSynchronizing(layer);
}
tick(elapsed) {
for (let i = 0; i < this.layers.length; i++) {
this.layers[i].tick(elapsed);
}
}
toJSON() {
return this.layers.map((layer) => layer.toJSON());
}
toNetwork(informed) {
return this.layers.map((layer) => layer.toNetwork(informed));
}
};
};

View File

@ -15,84 +15,68 @@ export default (latus) => {
);
return class Room extends decorate(JsonResource) {
entityList;
tiles = [];
constructor() {
super();
this._s13nId = s13nId++;
const {Layers} = latus.get('%resources');
this.setLayers(new Layers());
const {EntityList} = latus.get('%resources');
this.entityList = new EntityList();
this.entityList.on('entityAdded', this.onEntityAdded, this);
this.entityList.on('entityRemoved', this.onEntityRemoved, this);
}
addEntityToLayer(entity, layerIndex = 0) {
this.layers.addEntityToLayer(entity, layerIndex);
}
destroy() {
super.destroy();
this.layers.destroy();
this.layers.off('entityAdded', this.onEntityAddedToRoom);
this.layers.off('entityRemoved', this.onEntityRemovedFromRoom);
addEntity(entity) {
this.entityList.addEntity(entity);
}
get entities() {
return this.layers.entities;
return this.entityList.entities;
}
findEntity(uuid) {
return this.layers.findEntity(uuid);
}
layer(index) {
return this.layers.layer(index);
return this.entityList.findEntity(uuid);
}
async load(json = {}) {
await super.load(json);
const {layers, size} = json;
this.size = size || [0, 0];
const {Layers} = latus.get('%resources');
this.setLayers(
layers
? await Layers.load(layers)
: new Layers(),
);
const {Tiles} = latus.get('%resources');
const {
entities,
size = [0, 0],
tiles,
} = json;
if (entities) {
await this.entityList.load(entities);
}
this.startSynchronizing(this.entityList);
this.size = size;
if (tiles) {
this.tiles = await Promise.all(tiles.map((tiles, i) => Tiles.load({s13nId: i, ...tiles})));
this.tiles.forEach((tiles) => {
this.startSynchronizing(tiles);
});
}
}
onEntityAddedToRoom(entity) {
onEntityAdded(entity) {
// eslint-disable-next-line no-param-reassign
entity.room = this;
entity.emit('addedToRoom');
this.emit('entityAdded', entity);
}
onEntityRemovedFromRoom(entity) {
onEntityRemoved(entity) {
// eslint-disable-next-line no-param-reassign
entity.room = null;
entity.emit('removedFromRoom', this);
this.emit('entityRemoved', entity);
}
removeEntityFromLayer(entity, layerIndex) {
this.layers.removeEntityFromLayer(entity, layerIndex);
}
setLayers(layers) {
if (this.layers) {
this.stopSynchronizing(this.layers);
const entities = Object.values(this.layers.entities);
for (let i = 0; i < entities.length; i++) {
this.onEntityRemovedFromRoom(entities[i]);
}
this.layers.off('entityAdded', this.onEntityAddedToRoom);
this.layers.off('entityRemoved', this.onEntityRemovedFromRoom);
}
this.layers = layers;
this.layers.on('entityRemoved', this.onEntityRemovedFromRoom, this);
this.layers.on('entityAdded', this.onEntityAddedToRoom, this);
const entities = Object.values(this.layers.entities);
for (let i = 0; i < entities.length; i++) {
this.onEntityAddedToRoom(entities[i]);
}
this.startSynchronizing(this.layers);
removeEntity(entity) {
this.entityList.removeEntity(entity);
}
get s13nId() {
@ -100,20 +84,25 @@ export default (latus) => {
}
tick(elapsed) {
this.layers.tick(elapsed);
this.entityList.tick(elapsed);
for (let i = 0; i < this.tiles.length; i++) {
this.tiles[i].tick(elapsed);
}
}
toJSON() {
return {
layer: this.layers.toJSON(),
entities: this.entityList.toJSON(),
size: this.size,
tiles: this.tiles.map((tiles) => tiles.toJSON()),
};
}
toNetwork(informed) {
return {
layers: this.layers.toNetwork(informed),
entities: this.entityList.toNetwork(informed),
size: this.size,
tiles: this.tiles.map((tiles) => tiles.toNetwork(informed)),
};
}

View File

@ -1,38 +1,49 @@
import {Atlas} from '@avocado/graphics';
import {
floodwalk2D,
Rectangle,
Vector,
Vertice,
} from '@avocado/math';
import {JsonResource} from '@avocado/resource';
import {Synchronized} from '@avocado/s13n';
import {
Class,
compose,
deflate,
EventEmitter,
inflate,
} from '@latus/core';
import {Synchronized} from '@avocado/s13n';
// TODO: rows, with nulls
export default (latus) => {
const decorate = compose(
EventEmitter,
Vector.Mixin('size', 'width', 'height', {
Vector.Mixin('area', 'width', 'height', {
default: [0, 0],
}),
Synchronized(latus),
);
return class Tiles extends decorate(Class) {
return class Tiles extends decorate(JsonResource) {
#data = [];
#atlas = new Atlas();
#data = new Uint16Array();
#hasUpdates = false;
#packets = [];
#s13nId = 0;
#tileImageUri;
#tileSize = [0, 0];
#updates = [];
#zIndex = 0;
acceptPacket(packet) {
if ('TilesUpdate' === packet.constructor.type) {
const {position: [x, y], size: [w, h], tiles} = packet.data;
@ -46,6 +57,10 @@ export default (latus) => {
this.#updates = [];
}
destroy() {
this.#atlas.destroy();
}
static indexHulls(indices, data, size) {
const hulls = [];
const seen = [];
@ -108,18 +123,43 @@ export default (latus) => {
}
indexHulls(indices) {
return this.constructor.indexHulls(indices, this.#data, this.size);
return this.constructor.indexHulls(indices, this.#data, this.area);
}
async load(json) {
const {data, size} = json;
if (size) {
this.size = size;
const {
area,
data,
s13nId,
tileImageUri,
tileSize,
zIndex,
} = json;
if (area) {
this.area = area;
}
if (data) {
const {buffer, byteOffset, length} = inflate(Buffer.from(data, 'base64'));
this.#data = new Uint16Array(buffer, byteOffset, length / 2);
}
else if (area) {
this.#data = new Uint16Array(Vector.area(area));
}
if (tileImageUri && tileSize) {
this.#tileSize = tileSize;
this.#tileImageUri = tileImageUri;
await this.#atlas.load({
imageUri: tileImageUri,
type: 'grid',
size: tileSize,
});
}
if (s13nId) {
this.#s13nId = s13nId;
}
if (zIndex > 0) {
this.#zIndex = zIndex;
}
}
packetsFor() {
@ -144,11 +184,15 @@ export default (latus) => {
}
get rectangle() {
return Rectangle.compose([0, 0], this.size);
return Rectangle.compose([0, 0], this.area);
}
get s13nId() {
return this.#s13nId;
}
setTileAt([x, y], tile) {
const [w, h] = this.size;
const [w, h] = this.area;
const index = y * w + x;
if (x < 0 || x >= w || y < 0 || y >= h || this.#data[index] === tile) {
return;
@ -165,7 +209,7 @@ export default (latus) => {
if (w <= 0 || h <= 0) {
return [];
}
const [fw, fh] = this.size;
const [fw, fh] = this.area;
const n = fw - w;
const slice = new Array(w * h);
let i = y * fw + x;
@ -189,7 +233,7 @@ export default (latus) => {
if (0 === w || 0 === h || 0 === tiles.length) {
return;
}
const [fw, fh] = this.size;
const [fw, fh] = this.area;
if (x < 0 || x >= fw || y < 0 || y >= fh) {
return;
}
@ -219,6 +263,10 @@ export default (latus) => {
}
}
subimage(index) {
return this.#atlas.subimage(index);
}
tick() {
if (this.#hasUpdates) {
this.emit('update');
@ -227,20 +275,33 @@ export default (latus) => {
}
tileAt([x, y]) {
const [w, h] = this.size;
const [w, h] = this.area;
return x < 0 || x >= w || y < 0 || y >= h ? undefined : this.#data[y * w + x];
}
get tileSize() {
return this.#tileSize;
}
toNetwork() {
return this.toJSON();
return {
...this.toJSON(),
s13nId: this.#s13nId,
};
}
toJSON() {
return {
size: this.size,
area: this.area,
data: deflate(this.#data).toString('base64'),
tileSize: this.#tileSize,
...(this.#tileImageUri ? {tileImageUri: this.#tileImageUri} : []),
};
}
get zIndex() {
return this.#zIndex;
}
};
};

View File

@ -1,86 +0,0 @@
import {Property} from '@avocado/core';
import {Image} from '@avocado/graphics';
import {Rectangle, Vector} from '@avocado/math';
import {JsonResource} from '@avocado/resource';
import {compose, EventEmitter} from '@latus/core';
const decorate = compose(
EventEmitter,
Property('image', {
track: true,
}),
Vector.Mixin('tileSize', 'tileWidth', 'tileHeight', {
default: [0, 0],
}),
);
export default () => class Tileset extends decorate(JsonResource) {
constructor() {
super();
this.subimages = [];
}
destroy() {
this.subimages.forEach((subimage) => {
subimage.destroy();
});
this.subimages = [];
}
get image() {
return super.image;
}
set image(image) {
this.recalculateSubimages(image);
super.image = image;
}
async load(json = {}) {
await super.load(json);
const {imageUri, tileSize} = json;
if (imageUri) {
this.image = await Image.load(imageUri);
}
if (tileSize) {
this.tileSize = tileSize;
}
}
recalculateSubimages(image) {
this.destroy();
if (!image) {
return;
}
const {tileSize} = this;
if (Vector.isNull(tileSize)) {
return;
}
const grid = Vector.div(image.size, tileSize);
const rectangle = Rectangle.compose([0, 0], tileSize);
for (let j = 0; j < grid[1]; ++j) {
for (let i = 0; i < grid[0]; ++i) {
const subimage = image.subimage(rectangle);
this.subimages.push(subimage);
rectangle[0] += tileSize[0];
}
rectangle[0] = 0;
rectangle[1] += tileSize[1];
}
}
subimage(index) {
return this.subimages[index];
}
get tileSize() {
return super.tileSize;
}
set tileSize(tileSize) {
super.tileSize = tileSize;
this.recalculateSubimages(this.image);
}
};

View File

@ -1,19 +1,55 @@
import {EntityListView} from '@avocado/entity';
import {Container} from '@avocado/graphics';
import LayersView from './layers-view';
import TilesView from './tiles-view';
const views = new Set();
export default class RoomView extends Container {
layers = [];
constructor(room, renderer) {
super();
this.room = room;
this.layersView = new LayersView(room.layers, renderer);
this.addChild(this.layersView);
this.lastExtent = [0, 0, 0, 0];
this.renderer = renderer;
this.entityListView = new EntityListView(room.entityList);
this.tilesViews = room.tiles.map((tiles) => new TilesView(tiles, renderer));
this.addChildren(this.tilesViews);
this.addChild(this.entityListView);
this.sort();
if (module.hot) {
views.add(this);
}
}
destroy() {
super.destroy();
if (module.hot) {
views.delete(this);
}
}
renderChunksForExtent(extent) {
this.layersView.renderChunksForExtent(extent);
this.tilesViews.forEach((tilesView) => {
tilesView.renderChunksForExtent(extent);
});
}
}
if (module.hot) {
module.hot.accept('./tiles-view', () => {
const it = views.values();
for (let value = it.next(); value.done !== true; value = it.next()) {
const {value: view} = value;
const {lastExtent} = view;
view.removeChild(view.entityListView);
view.removeChild(view.tilesView);
view.tilesView = new TilesView(view.room.tiles, view.renderer);
view.renderChunksForExtent(lastExtent);
view.addChild(view.tilesView);
view.addChild(view.entityListView);
}
});
}

View File

@ -15,19 +15,18 @@ export default class TilesView extends Container {
static CHUNK_SIZE = [8, 8];
constructor(tiles, tileset, renderer) {
constructor(tiles, renderer) {
super();
this.wrapper = new Container();
this.addChild(this.wrapper);
this.tiles = tiles;
this.tiles.on('update', this.onTilesUpdate, this);
this.tileset = tileset;
this.renderer = renderer;
this.rendered = [];
}
chunksForExtent([x, y, w, h]) {
const [tw, th] = this.tileset.tileSize;
const [tw, th] = this.tiles.tileSize;
/* eslint-disable no-param-reassign */
x /= tw;
w /= tw;
@ -38,11 +37,11 @@ export default class TilesView extends Container {
}
chunksForUnitExtent([x, y, w, h]) {
const [fw, fh] = this.tiles.size;
/* eslint-disable no-param-reassign */
const [fw, fh] = this.tiles.area;
if (x >= fw || y >= fh) {
return [];
}
/* eslint-disable no-param-reassign */
if (x < 0) {
w += x;
x = 0;
@ -109,7 +108,7 @@ export default class TilesView extends Container {
if (cux < 0 || cuy < 0) {
return;
}
const [fw, fh] = this.tiles.size;
const [fw, fh] = this.tiles.area;
const [cw, ch] = this.constructor.CHUNK_SIZE;
const [cx, cy] = Vector.mul([cux, cuy], [cw, ch]);
if (cx >= fw || cy >= fh) {
@ -119,7 +118,7 @@ export default class TilesView extends Container {
cx + cw > fw ? (cx + cw) - fw : cw,
cy + ch > fh ? (cy + ch) - fh : ch,
];
const [tw, th] = this.tileset.tileSize;
const [tw, th] = this.tiles.tileSize;
const slice = this.tiles.slice([cx - 1, cy - 1, sw + 2, sh + 2]);
const container = new Container();
const mask = this.renderMask(slice, [sw + 2, sh + 2]);
@ -132,7 +131,7 @@ export default class TilesView extends Container {
for (let i = 0; i < slice.length; ++i) {
const index = slice[i];
if (index > 0) {
const subimage = this.tileset.subimage(index);
const subimage = this.tiles.subimage(index);
if (subimage) {
const sprite = new Sprite(subimage);
sprite.anchor = [0, 0];
@ -186,8 +185,7 @@ export default class TilesView extends Container {
if (0 === hulls.length) {
return undefined;
}
const {tileSize} = this.tileset;
const canvasSize = Vector.mul(size, tileSize);
const canvasSize = Vector.mul(size, this.tiles.tileSize);
const canvas = window.document.createElement('canvas');
[canvas.width, canvas.height] = canvasSize;
const ctx = canvas.getContext('2d');

View File

@ -27,14 +27,6 @@ export default () => class Followed extends Trait {
};
}
destroy() {
super.destroy();
const {room} = this.entity;
if (room) {
room.off('sizeChanged', this.onRoomSizeChanged);
}
}
get camera() {
return this.#camera;
}
@ -50,16 +42,6 @@ export default () => class Followed extends Trait {
addedToRoom: () => {
this.onRoomSizeChanged();
this.entity.room.on('sizeChanged', this.onRoomSizeChanged, this);
},
removedFromRoom: (room) => {
room.off('sizeChanged', this.onRoomSizeChanged);
},
traitAdded: () => {
this.onRoomSizeChanged();
this.updatePosition();
},
};

View File

@ -1,105 +0,0 @@
import {Vector} from '@avocado/math';
import {Trait} from '@avocado/traits';
export default () => class Layered extends Trait {
#tile = [0, 0];
#tileOffset = [0, 0];
static behaviorTypes() {
return {
layer: {
type: 'layer',
label: 'Layer',
},
};
}
static dependencies() {
return [
'Positioned',
];
}
destroy() {
super.destroy();
this.detachFromLayer(this.entity.layer);
}
detachFromLayer(layer) {
layer?.off('tilesetChanged', this.onLayerTilesetChanged);
}
hotJson() {
return {
tile: this.#tile,
tileOffset: this.#tileOffset,
};
}
listeners() {
return {
addedToLayer: () => {
this.entity.layer.on('tilesetChanged', this.onLayerTilesetChanged, this);
this.setCurrentTileFromLayer();
},
positionChanged: () => {
this.setCurrentTileFromLayer();
},
removedFromLayer: (layer) => {
this.detachFromLayer(layer);
},
};
}
async load(json) {
await super.load(json);
this.entity.layer = null;
this.setCurrentTileFromLayer();
}
onLayerTilesetChanged() {
this.setCurrentTileFromLayer();
}
setCurrentTileFromLayer() {
const {layer} = this.entity;
if (!layer) {
return;
}
const {tileset} = layer;
if (!tileset) {
return;
}
const oldTile = Vector.copy(this.#tile);
this.#tile = Vector.div(
this.entity.position,
tileset.tileSize,
);
if (!Vector.equals(oldTile, this.#tile)) {
this.entity.emit('tileChanged', oldTile, this.#tile);
}
const oldTileOffset = Vector.copy(this.#tileOffset);
this.#tileOffset = Vector.mod(
this.entity.position,
tileset.tileSize,
);
if (!Vector.equals(oldTileOffset, this.#tileOffset)) {
this.entity.emit('tileOffsetChanged', oldTileOffset, this.#tileOffset);
}
}
get tile() {
return this.#tile;
}
get tileOffset() {
return this.#tileOffset;
}
};

View File

@ -1,32 +0,0 @@
import {Image} from '@avocado/graphics';
import {Latus} from '@latus/core';
import {expect} from 'chai';
Image.root = 'test/fixtures';
describe('Tileset', () => {
let latus;
beforeEach(async () => {
latus = Latus.mock({
'@avocado/resource': require('@avocado/resource'),
'@avocado/topdown': require('../src'),
});
await Promise.all(latus.invokeFlat('@latus/core/starting'));
const {Tileset} = latus.get('%resources');
Tileset.root = 'test/fixtures';
});
it("has sane defaults", async () => {
const {Tileset} = latus.get('%resources');
const tileset = new Tileset();
expect(tileset.tileSize).to.deep.equal([0, 0]);
expect(tileset.image).to.equal(undefined);
});
it("can load", async () => {
const {Tileset} = latus.get('%resources');
const tileset = await Tileset.load({extends: '/test.tileset.json'});
expect(tileset.tileSize).to.deep.equal([4, 4]);
expect(tileset.image.size).to.deep.equal([16, 16]);
expect(tileset.subimages.length).to.equal(16);
expect(tileset.subimage(0).size).to.deep.equal([4, 4]);
});
});