Archive commit

This commit is contained in:
Erik McClure
2019-08-28 21:15:02 -07:00
commit 735845746e
1435 changed files with 140724 additions and 0 deletions
@@ -0,0 +1,55 @@
import { tileWidth } from '../../common/constants';
import { Entity } from '../../common/interfaces';
import { entitiesIntersect } from '../../common/utils';
import { cloud } from '../../common/entities';
import * as sprites from '../../generated/sprites';
import { Controller, ServerMap, ServerEntity } from '../serverInterfaces';
import { World } from '../world';
import { updateEntityVelocity } from '../entityUtils';
import { timingEnd, timingStart } from '../timing';
const spriteWidth = sprites.cloud.shadow!.w / tileWidth;
const cloudVX = -0.5;
export class CloudController implements Controller {
private clouds: Entity[] = [];
private initialized = false;
constructor(private world: World, private map: ServerMap, private cloudCount: number) {
}
initialize() {
if (this.initialized)
return;
for (let i = 0; i < this.cloudCount; i++) {
this.addCloud(false, this.world.now / 1000);
}
this.initialized = true;
}
update(_: number, now: number) {
timingStart('CloudController.update()');
for (let i = this.clouds.length - 1; i >= 0; i--) {
const cloud = this.clouds[i];
if (cloud.x < -spriteWidth) {
this.clouds.splice(i, 1);
this.world.removeEntity(cloud, this.map);
}
}
if (this.clouds.length < this.cloudCount) {
this.addCloud(true, now);
}
timingEnd();
}
private addCloud(end: boolean, timestamp: number) {
const x = end ? this.map.width + spriteWidth : this.map.width * Math.random();
const y = this.map.height * Math.random();
const entity = cloud(x, y) as ServerEntity;
if (!this.clouds.some(c => entitiesIntersect(c, entity))) {
this.clouds.push(this.world.addEntity(entity, this.map));
updateEntityVelocity(entity, cloudVX, 0, timestamp);
}
}
}
@@ -0,0 +1,72 @@
import { remove, sample } from 'lodash';
import { Entity, CreateEntityMethod } from '../../common/interfaces';
import { IClient, Controller, ServerEntity, ServerMap } from '../serverInterfaces';
import { World } from '../world';
import { timingEnd, timingStart } from '../timing';
import { canPlaceItem, canBePickedByPlayer, pushRemoveEntityToClient } from '../entityUtils';
export function randomPosition(map: ServerMap) {
const x = Math.random() * map.width;
const y = Math.random() * map.height;
return { x, y };
}
export class CollectableController implements Controller {
private items: Entity[] = [];
constructor(
private world: World,
private map: ServerMap,
private ctors: CreateEntityMethod[],
public limit: number,
private pick: (client: IClient, entity: ServerEntity) => void,
private check: (client: IClient) => boolean = () => true,
private tries = 1,
private position = randomPosition,
private active = () => true
) {
}
initialize() {
}
update() {
timingStart('CollectableController.update()');
if (this.active()) {
for (let i = 0; i < this.tries; i++) {
if (this.items.length < this.limit) {
this.generateItem();
}
}
}
timingEnd();
}
private generateItem() {
const { world, map } = this;
const { x, y } = this.position(map);
const ctor = sample(this.ctors)!;
const entity = ctor(x, y) as ServerEntity;
if (!entity.interactRange) {
entity.interactRange = 1.5;
}
if (
x > 0 && y > 0 && x < map.width && y < map.height && canPlaceItem(map, entity) && !canBePickedByPlayer(map, entity)
) {
entity.interact = this.interact;
this.items.push(world.addEntity(entity, map));
}
}
private interact = (entity: Entity, client: IClient) => {
if (this.check(client)) {
if (client.shadowed) {
pushRemoveEntityToClient(client, entity);
} else {
remove(this.items, e => e === entity);
this.world.removeEntity(entity, this.map);
this.generateItem();
this.pick(client, entity);
}
}
}
}
@@ -0,0 +1,150 @@
import { sample } from 'lodash';
import {
createBinaryWriter, getWriterBuffer, resetWriter, resizeWriter, writeArrayHeader, writeUint8Array,
writeUint8
} from 'ag-sockets';
import { Controller, IClient } from '../serverInterfaces';
import { World } from '../world';
import { Character, Account } from '../db';
import { createClientAndPony } from '../playerUtils';
import { CounterService } from '../services/counter';
import { CharacterState, ServerConfig } from '../../common/adminInterfaces';
import { removeItem, times } from '../../common/utils';
import { timingStart, timingEnd } from '../timing';
interface Options {
count: number;
}
const mockCharacterStates = new CounterService<CharacterState>(0);
export class FakeClientsController implements Controller {
private clients: IClient[] = [];
private tokens: any[] = [];
private initialized = false;
constructor(private world: World, private server: ServerConfig, private options: Options) {
}
initialize() {
if (this.initialized)
return;
times(1000, async i => {
try {
const name = `perf-${i}`;
const account = await Account.findOne({ name }).exec();
if (!account)
throw new Error(`Missing debug account (${name})`);
const character = await Character.findOne({ account: account._id }).exec();
if (!character)
throw new Error(`Missing debug character (${name})`);
this.tokens.push({ id: name, account, character });
} catch (e) {
console.error(e);
}
});
this.initialized = true;
}
update() {
}
sparseUpdate() {
timingStart('FakeClientController.sparseUpdate()');
if (this.tokens.length) {
for (let i = this.clients.length - 1; i >= 0; i--) {
if (Math.random() < (10 / this.options.count)) {
this.leave(this.clients[i]);
}
}
if (this.clients.length < this.options.count) {
for (let i = 0; i < 10; i++) {
this.join();
}
}
}
timingEnd();
}
async join() {
try {
const token = sample(this.tokens)!;
if (!this.clients.some(c => c.tokenId === token.id)) {
const client = await joinFakeClient(token, this.server, this.world);
this.clients.push(client);
}
} catch (e) {
console.error(e);
}
}
async leave(client: IClient) {
this.world.leaveClient(client);
removeItem(this.clients, client);
}
}
const packetWriter = createBinaryWriter();
export let lastPacket: Uint8Array | undefined;
async function joinFakeClient(token: any, server: ServerConfig, world: World): Promise<IClient> {
const client: Partial<IClient> = {
tokenId: token.id,
tokenData: token,
disconnect() {
world.leaveClient(client as IClient);
},
queue() { },
left() { },
worldState() { },
mapState() { },
myEntity() { },
mapTest() { },
updateFriends() { },
actionParam() { },
update(_, subscribes, adds, datas) {
do {
try {
resetWriter(packetWriter);
writeUint8(packetWriter, 123);
if (writeArrayHeader(packetWriter, subscribes)) {
for (let i = 0; i < subscribes.length; i++) {
writeUint8Array(packetWriter, subscribes[i]);
}
}
writeUint8Array(packetWriter, adds);
if (writeArrayHeader(packetWriter, datas)) {
for (let i = 0; i < datas.length; i++) {
writeUint8Array(packetWriter, datas[i]);
}
}
break;
} catch (e) {
if (e instanceof RangeError || /DataView/.test(e.message)) {
resizeWriter(packetWriter);
} else {
throw e;
}
}
} while (true);
lastPacket = getWriterBuffer(packetWriter);
},
addNotification() { },
removeNotification() { },
};
createClientAndPony(client as IClient, [], [], server, world, mockCharacterStates);
world.joinClientToQueue(client as IClient);
return client as IClient;
}
@@ -0,0 +1,89 @@
import { sample } from 'lodash';
import { Entity, CreateEntityMethod, ServerFlags } from '../../common/interfaces';
import { Controller, ServerEntity, ServerMap } from '../serverInterfaces';
import { World } from '../world';
import { findClosestEntity, findEntities } from '../serverMap';
import { timingEnd, timingStart } from '../timing';
import { hasFlag, distanceXY } from '../../common/utils';
import { moveRandomly, findClosest, moveTowards } from '../entityUtils';
import { randomPosition } from './collectableController';
export class FlyingCritterController implements Controller {
private entities: Entity[] = [];
constructor(
private world: World, private map: ServerMap, private critter: CreateEntityMethod, private speed: number,
private limit: number, private isActive: () => boolean, private spawnOnStart = false
) {
}
initialize() {
if (this.spawnOnStart) {
for (let i = 0; i < this.limit; i++) {
const { x, y } = randomPosition(this.map);
this.entities.push(this.world.addEntity(this.critter(x, y), this.map));
}
}
}
update(_: number, now: number) {
timingStart('FlyingCritterController.update()');
updateTreehidingEntities(
this.entities, this.world, this.map, this.limit, this.speed, now, this.critter, this.isActive);
timingEnd();
}
}
function isTreeCrown(entity: ServerEntity) {
return hasFlag(entity.serverFlags || 0, ServerFlags.TreeCrown);
}
export function findClosestTree(map: ServerMap, x: number, y: number) {
return findClosestEntity(map, x, y, isTreeCrown);
}
export function findTrees(map: ServerMap) {
return findEntities(map, isTreeCrown);
}
interface TargetTree extends Entity {
targetTree?: Entity;
}
export function updateTreehidingEntities(
entities: TargetTree[], world: World, map: ServerMap, limit: number, speed: number, timestamp: number,
create: (x: number, y: number) => Entity, isActive: () => boolean
) {
const offsetY = -2;
if (isActive()) {
// release new critter
if (entities.length < limit && Math.random() < 0.1) {
const trees = findTrees(map);
const tree = sample(trees);
if (tree) {
const entity = create(tree.x, tree.y + offsetY);
entities.push(world.addEntity(entity, map));
moveRandomly(map, entity, speed, 1, timestamp);
}
}
for (const entity of entities) {
moveRandomly(map, entity, speed, 0.02, timestamp);
}
} else if (entities.length) {
// head to tree and disappear
const trees = findTrees(map);
for (let i = entities.length - 1; i >= 0; i--) {
const e = entities[i];
e.targetTree = e.targetTree || findClosest(e.x, e.y, trees);
if (distanceXY(e.x, e.y, e.targetTree.x, e.targetTree.y + offsetY) < 0.1) {
entities.splice(i, 1);
world.removeEntity(e, map);
} else {
moveTowards(e, e.targetTree.x, e.targetTree.y + offsetY, speed, timestamp);
}
}
}
}
+128
View File
@@ -0,0 +1,128 @@
import { range, compact } from 'lodash';
import { pony } from '../../common/entities';
import { Entity, EntityState, MessageType, EntityFlags } from '../../common/interfaces';
import { Controller, ServerEntity, IClient } from '../serverInterfaces';
import { World } from '../world';
import { Character } from '../db';
import { PONY_SPEED_TROT } from '../../common/constants';
import { setEntityName, updateEntityVelocity } from '../entityUtils';
import { encryptInfo } from '../characterUtils';
import { createCamera } from '../../common/camera';
import { shouldBeFacingRight } from '../../common/movementUtils';
import { timingEnd, timingStart } from '../timing';
import { sayToAll } from '../chat';
interface Options {
count: number;
moving: number;
saying?: boolean;
unique?: boolean;
spread?: boolean;
x?: number;
y?: number;
}
export class PerfController implements Controller {
private entities: Entity[] = [];
private limitLeft = 11;
private limitWidth = 30;
private limitTop = 9;
private limitHeight = 25;
private initialized = false;
constructor(private world: World, private options: Options) {
if (options.spread) {
this.limitWidth = 60;
this.limitHeight = 60;
}
if (options.x !== undefined) {
this.limitLeft = options.x;
}
if (options.y !== undefined) {
this.limitTop = options.y;
}
}
initialize() {
if (this.initialized)
return;
const world = this.world;
const map = world.getMainMap();
const names = [
'performance',
'performance 2',
];
const query = this.options.unique ?
Promise.resolve(Character.find({ account: '57ae2336a67f4dc52e123ed1' }).limit(this.options.count).exec()) :
Promise.all(names.map(name => Character.findOne({ name }).exec())).then(compact);
query
.then(characters => {
if (characters.length) {
this.entities = range(this.options.count).map(i => {
const character = characters[i % characters.length]!;
const name = character._id.toString();
const x = this.limitLeft + this.limitWidth * Math.random();
const y = this.limitTop + this.limitHeight * Math.random();
const p = pony(x, y) as ServerEntity;
setEntityName(p, name);
p.flags |= EntityFlags.CanCollide;
p.encryptedInfoSafe = encryptInfo(character.info || '');
p.client = {
pony: p,
accountId: 'foobar',
characterId: character._id.toString(),
ignores: new Set(),
hides: new Set(),
permaHides: new Set(),
account: {} as any,
regions: [],
camera: createCamera(),
updateRegion() { },
addEntity() { },
mapTest() { },
} as Partial<IClient> as any;
p.client!.camera.x = -10000;
p.vx = this.options.moving ? randomVelocity() : 0;
p.vy = this.options.moving ? randomVelocity() : 0;
p.state = shouldBeFacingRight(p) ? EntityState.FacingRight : EntityState.None;
return world.addEntity(p, map);
});
}
});
this.initialized = true;
}
update(_: number, now: number) {
timingStart('PerfController.update()');
const limitBottom = this.limitTop + this.limitHeight;
const limitRight = this.limitTop + this.limitHeight;
if (this.options.moving) {
for (const entity of this.entities) {
if ((entity.vy > 0 && entity.y > limitBottom) || (entity.vy < 0 && entity.y < this.limitTop)) {
updateEntityVelocity(entity, entity.vx, -entity.vy, now);
} else if ((entity.vx > 0 && entity.x > limitRight) || (entity.vx < 0 && entity.x < this.limitLeft)) {
updateEntityVelocity(entity, -entity.vx, entity.vy, now);
} else if (Math.random() < 0.1) {
updateEntityVelocity(entity, randomVelocity(), randomVelocity(), now);
}
if (this.options.saying && Math.random() < 0.01) {
sayToAll(entity, 'Hello World', 'Hello World', MessageType.Chat, {});
}
}
}
timingEnd();
}
}
function randomVelocity() {
const rand = Math.random();
return rand < 0.333 ? 0 : (rand < 0.666 ? -PONY_SPEED_TROT : +PONY_SPEED_TROT);
}
@@ -0,0 +1,89 @@
import { sample, random } from 'lodash';
import { Controller, ServerEntity, ServerMap, Interact } from '../serverInterfaces';
import { World } from '../world';
import { timingStart, timingEnd } from '../timing';
import { Rect, CreateEntityMethod, ServerFlags, TileType } from '../../common/interfaces';
import { removeItem, randomPoint } from '../../common/utils';
import { getTile } from '../../common/worldMap';
interface Plant extends ServerEntity {
plantStage: number;
plantStageNext: number;
}
export interface PlantConfig {
area: Rect;
count: number;
stages: CreateEntityMethod[][];
onPick?: Interact;
growOnlyOn?: TileType;
isActive?: () => boolean;
}
export class PlantController implements Controller {
private plants: Plant[] = [];
private interact: Interact = (entity, client) => {
this.world.removeEntity(entity, this.map);
removeItem(this.plants, entity);
this.config.onPick && this.config.onPick(entity, client);
}
private nextSpawn = 0;
constructor(private world: World, private map: ServerMap, private config: PlantConfig) {
}
initialize() {
}
update() {
}
sparseUpdate() {
timingStart('PlantController.sparseUpdate()');
const now = Date.now();
const maxStage = this.config.stages.length - 1;
if (
this.nextSpawn < now &&
(this.config.isActive === undefined || this.config.isActive()) &&
this.plants.length < this.config.count
) {
const { x, y } = randomPoint(this.config.area);
if (this.config.growOnlyOn === undefined || getTile(this.map, x, y) === this.config.growOnlyOn) {
this.addPlant(x, y, 0);
this.nextSpawn = now + random(10000, 20000);
}
}
const plantsToRemove: Plant[] = [];
for (const plant of this.plants) {
if (plant.plantStage < maxStage && plant.plantStageNext < now) {
plantsToRemove.push(plant);
this.addPlant(plant.x, plant.y, plant.plantStage + 1);
}
}
for (const plant of plantsToRemove) {
this.removePlant(plant);
}
timingEnd();
}
private removePlant(plant: Plant) {
removeItem(this.plants, plant);
this.world.removeEntity(plant, this.map);
}
private addPlant(x: number, y: number, stage: number) {
const create = sample(this.config.stages[stage])!;
const plant = create(x, y) as Plant;
plant.plantStage = stage;
plant.plantStageNext = Date.now() + random(15000, 40000);
plant.serverFlags = ServerFlags.DoNotSave;
if (stage === (this.config.stages.length - 1)) {
plant.interact = this.interact;
}
this.plants.push(plant);
this.world.addEntity(plant, this.map);
}
}
@@ -0,0 +1,87 @@
import { compact } from 'lodash';
import * as entities from '../../common/entities';
import { IClient, ServerEntity, Controller, ServerMap } from '../serverInterfaces';
import { World } from '../world';
import { Character } from '../db';
import { setEntityName } from '../entityUtils';
import { times } from '../../common/utils';
import { encryptInfo } from '../characterUtils';
import { createCamera } from '../../common/camera';
import { timingStart, timingEnd } from '../timing';
import { createBinaryWriter } from 'ag-sockets';
export class TestController implements Controller {
private clients: IClient[] = [];
private initialized = false;
constructor(private world: World, private map: ServerMap) {
}
initialize() {
if (this.initialized)
return;
const world = this.world;
const map = this.map;
if (DEVELOPMENT) {
Promise.all(times(10, i => `debug ${i + 1}`).map(name => Character.findOne({ name }).exec()))
.then(compact)
.then(items => items.forEach((item, i) => {
const name = item.name;
const tag = i === 0 ? 'mod' : (i === 2 ? 'sup2' : '');
const extraOptions = i === 0 ? {
site: {
provider: 'github',
name: 'Test name',
url: 'https://github.com/Microsoft/TypeScript',
}
} : undefined;
const p = entities.pony(57 + 1 * i, 47 + 1 * i) as ServerEntity;
p.options = { tag };
setEntityName(p, name);
p.encryptedInfoSafe = encryptInfo(item.info || '');
p.client = {
map,
accountSettings: {},
account: { id: 'foobar', name: 'Debug account' } as any,
country: 'XY',
regions: [],
saysQueue: { push() { }, length: 0 } as any,
notifications: [],
camera: createCamera(),
accountId: 'foobar',
characterId: '',
ignores: new Set(),
hides: new Set(),
permaHides: new Set(),
updateQueue: createBinaryWriter(1),
addEntity() { },
addNotification() { },
removeNotification() { },
updateParty() { },
mapUpdate() { },
} as Partial<IClient> as any;
p.client!.pony = p;
this.clients.push(p.client!);
p.extraOptions = extraOptions;
world.addEntity(p, map);
}));
}
this.initialized = true;
}
update() {
timingStart('TestController.update()');
timingEnd();
}
sparseUpdate() {
timingStart('TestController.sparseUpdate()');
for (const client of this.clients) {
for (const notification of client.notifications) {
notification.accept && notification.accept();
}
}
timingEnd();
}
}
@@ -0,0 +1,33 @@
import { isNight } from '../../common/timeUtils';
import { Controller, ServerEntity, ServerMap } from '../serverInterfaces';
import { World } from '../world';
import { timingStart, timingEnd } from '../timing';
import { updateLights } from '../controllerUtils';
import { hasFlag } from '../../common/utils';
import { EntityFlags } from '../../common/interfaces';
export class TorchController implements Controller {
private lights: ServerEntity[] = [];
constructor(private world: World, private map: ServerMap) {
}
initialize() {
this.lights = [];
for (const region of this.map.regions) {
for (const entity of region.entities) {
if (hasFlag(entity.flags, EntityFlags.OnOff)) {
this.lights.push(entity);
}
}
}
}
update() {
timingStart('TorchController.update()');
timingEnd();
}
sparseUpdate() {
timingStart('TorchController.sparseUpdate()');
updateLights(this.lights, isNight(this.world.time));
timingEnd();
}
}
@@ -0,0 +1,28 @@
import { Controller, ServerEntity, ServerMap } from '../serverInterfaces';
import { timingStart, timingEnd } from '../timing';
export class UpdateController implements Controller {
private updatables: ServerEntity[] = [];
constructor(private map: ServerMap) {
}
initialize() {
this.updatables = [];
for (const region of this.map.regions) {
for (const entity of region.entities) {
if (entity.serverUpdate) {
this.updatables.push(entity);
}
}
}
}
update(delta: number, now: number) {
timingStart('TorchController.update()');
for (const entity of this.updatables) {
entity.serverUpdate!(delta, now);
}
timingEnd();
}
}
+178
View File
@@ -0,0 +1,178 @@
import { fromByteArray, toByteArray } from 'base64-js';
import { Controller, ServerMap } from '../serverInterfaces';
import { World } from '../world';
import { Entity, TileType } from '../../common/interfaces';
import { tileHeight } from '../../common/constants';
import { array } from '../../common/utils';
import { Walls } from '../../common/entities';
const createGetAt = (width: number, height: number) => <T>(items: T[], x: number, y: number) => {
return (x < 0 || y < 0 || x >= width || y >= height) ? undefined : items[x + y * width];
};
const createSetAt = (width: number, height: number) => <T>(items: T[], x: number, y: number, value: T) => {
if (x >= 0 && y >= 0 && x < width && y < height) {
items[x + y * width] = value;
}
};
export class WallController implements Controller {
top = 0;
isTall = (_x: number, _y: number) => false;
lockOuterWalls = false;
private lockedTiles = new Set<string>();
private hWalls: (Entity | undefined)[];
private vWalls: (Entity | undefined)[];
constructor(world: World, map: ServerMap, walls: Walls) {
const width = map.width + 1;
const height = map.height + 1;
const getAt = createGetAt(width, height);
const setAt = createSetAt(width, height);
const hWalls = this.hWalls = array<Entity | undefined>(width * height, undefined);
const vWalls = this.vWalls = array<Entity | undefined>(width * height, undefined);
const cWalls = array<Entity | undefined>(width * height, undefined);
const yOffset = 3 / tileHeight;
const { wallHShort, wallVShort, wallH, wallV, wallCorners, wallCornersShort, wallCutR, wallCutL } = walls;
const calcCorner = (x: number, y: number) => {
// top right bottom left
return (getAt(vWalls, x, y - 1) ? 8 : 0)
+ (getAt(hWalls, x, y) ? 4 : 0)
+ (getAt(vWalls, x, y) ? 2 : 0)
+ (getAt(hWalls, x - 1, y) ? 1 : 0);
};
const updateCorner = (x: number, y: number) => {
if (x < 0 || y < 0 || x >= width || y >= height)
return;
const top = this.top;
const isOutside = x === 0 || y <= top || x === map.width || this.isTall(x, y);
const corners = isOutside ? wallCorners : wallCornersShort;
const current = getAt(cWalls, x, y);
const calc = calcCorner(x, y);
if (!current || current.type !== corners[calc].type) {
if (current) {
world.removeEntity(current, map);
}
setAt(cWalls, x, y, calc ? world.addEntity(corners[calc](x, y + yOffset), map) : undefined);
}
};
this.toggleWall = (x, y, type) => {
if (x < 0 || y < 0 || x >= width || y >= height)
return;
if (this.lockedTiles.has(`${x},${y}:${type}`))
return;
const walls = type === TileType.WallH ? hWalls : vWalls;
const entity = getAt(walls, x, y);
const top = this.top;
if (type === TileType.WallH && x === (width - 1))
return;
if (type === TileType.WallV && y === (height - 1))
return;
if (this.lockOuterWalls) {
if (type === TileType.WallH && (y <= top || y === (width - 1)))
return;
if (type === TileType.WallV && (x === 0 || x === (height - 1) || y < top))
return;
}
if (entity) {
world.removeEntity(entity, map);
setAt(walls, x, y, undefined);
} else {
if (type === TileType.WallH) {
const ctor = (y <= top || this.isTall(x, y)) ?
wallH : (x === 0 ? wallCutL : (x === (width - 2) ? wallCutR : wallHShort));
setAt(walls, x, y, world.addEntity(ctor(x + 0.5, y + yOffset), map));
} else {
const ctor = (x === 0 || x === (width - 1) || this.isTall(x, y)) ? wallV : wallVShort;
setAt(walls, x, y, world.addEntity(ctor(x, y + 0.5), map));
}
}
updateCorner(x, y);
updateCorner(x + 1, y);
updateCorner(x, y + 1);
};
}
initialize() {
}
update() {
}
toggleWall?: (x: number, y: number, type: TileType) => void;
lockWall(x: number, y: number, type: TileType.WallH | TileType.WallV) {
this.lockedTiles.add(`${x},${y}:${type}`);
}
serialize() {
const data = new Uint8Array(Math.ceil(this.vWalls.length / 8) + Math.ceil(this.hWalls.length / 8));
let offset = 0;
for (let i = 0; i < this.vWalls.length; i += 8, offset++) {
let value = 0;
for (let j = 0; j < 8; j++) {
if (this.vWalls[i + j]) {
value |= (1 << j);
}
}
data[offset] = value;
}
for (let i = 0; i < this.hWalls.length; i += 8, offset++) {
let value = 0;
for (let j = 0; j < 8; j++) {
if (this.hWalls[i + j]) {
value |= (1 << j);
}
}
data[offset] = value;
}
return fromByteArray(data);
}
deserialize(width: number, height: number, serialized: string) {
const data = toByteArray(serialized);
const size = (width + 1) * (height + 1);
let offset = 0;
for (let i = 0; i < size; i += 8, offset++) {
let value = data[offset];
for (let j = 0; j < 8; j++) {
if ((!!this.vWalls[i + j]) !== ((value & (1 << j)) !== 0)) {
const x = (i + j) % (width + 1);
const y = Math.floor((i + j) / (width + 1));
this.toggleWall!(x, y, TileType.WallV);
}
}
}
for (let i = 0; i < size; i += 8, offset++) {
let value = data[offset];
for (let j = 0; j < 8; j++) {
if ((!!this.hWalls[i + j]) !== ((value & (1 << j)) !== 0)) {
const x = (i + j) % (width + 1);
const y = Math.floor((i + j) / (width + 1));
this.toggleWall!(x, y, TileType.WallH);
}
}
}
}
}