dcl-npc-toolkit-ai-version
Version:
A collection of tools for creating Non-Player-Characters (NPCs). These are capable of having conversations with the player, and play different animations. AI usage is added atop of it
296 lines (234 loc) • 8.32 kB
text/typescript
import { OPERATION } from "../spec";
import { Schema } from "../Schema";
import { SchemaDefinition, FilterChildrenCallback } from "../annotations";
import { MapSchema } from "../types/MapSchema";
import { ArraySchema } from "../types/ArraySchema";
import { CollectionSchema } from "../types/CollectionSchema";
import { SetSchema } from "../types/SetSchema";
import { ReferenceTracker } from "./ReferenceTracker";
export type Ref = Schema
| ArraySchema
| MapSchema
| CollectionSchema
| SetSchema;
export interface ChangeOperation {
op: OPERATION,
index: number,
}
//
// FieldCache is used for @filter()
//
export interface FieldCache {
beginIndex: number;
endIndex: number;
}
export class ChangeTree {
ref: Ref;
refId: number;
root?: ReferenceTracker;
parent?: Ref;
parentIndex?: number;
indexes: {[index: string]: any};
changed: boolean = false;
changes = new Map<number, ChangeOperation>();
allChanges = new Set<number>();
// cached indexes for filtering
caches: {[field: number]: number[]} = {};
currentCustomOperation: number = 0;
constructor(ref: Ref, parent?: Ref, root?: ReferenceTracker) {
this.ref = ref;
this.setParent(parent, root);
}
setParent(
parent: Ref,
root?: ReferenceTracker,
parentIndex?: number,
) {
if (!this.indexes) {
this.indexes = (this.ref instanceof Schema)
? this.ref['_definition'].indexes
: {};
}
this.parent = parent;
this.parentIndex = parentIndex;
// avoid setting parents with empty `root`
if (!root) { return; }
this.root = root;
//
// assign same parent on child structures
//
if (this.ref instanceof Schema) {
const definition: SchemaDefinition = this.ref['_definition'];
for (let field in definition.schema) {
const value = this.ref[field];
if (value && value['$changes']) {
const parentIndex = definition.indexes[field];
(value['$changes'] as ChangeTree).setParent(
this.ref,
root,
parentIndex,
);
}
}
} else if (typeof (this.ref) === "object") {
this.ref.forEach((value, key) => {
if (value instanceof Schema) {
const changeTreee = value['$changes'];
const parentIndex = this.ref['$changes'].indexes[key];
changeTreee.setParent(
this.ref,
this.root,
parentIndex,
);
}
});
}
}
operation(op: ChangeOperation) {
this.changes.set(--this.currentCustomOperation, op);
}
change(fieldName: string | number, operation: OPERATION = OPERATION.ADD) {
const index = (typeof (fieldName) === "number")
? fieldName
: this.indexes[fieldName];
this.assertValidIndex(index, fieldName);
const previousChange = this.changes.get(index);
if (
!previousChange ||
previousChange.op === OPERATION.DELETE ||
previousChange.op === OPERATION.TOUCH // (mazmorra.io's BattleAction issue)
) {
this.changes.set(index, {
op: (!previousChange)
? operation
: (previousChange.op === OPERATION.DELETE)
? OPERATION.DELETE_AND_ADD
: operation,
// : OPERATION.REPLACE,
index
});
}
this.allChanges.add(index);
this.changed = true;
this.touchParents();
}
touch(fieldName: string | number) {
const index = (typeof (fieldName) === "number")
? fieldName
: this.indexes[fieldName];
this.assertValidIndex(index, fieldName);
if (!this.changes.has(index)) {
this.changes.set(index, { op: OPERATION.TOUCH, index });
}
this.allChanges.add(index);
// ensure touch is placed until the $root is found.
this.touchParents();
}
touchParents() {
if (this.parent) {
(this.parent['$changes'] as ChangeTree).touch(this.parentIndex);
}
}
getType(index?: number) {
if (this.ref['_definition']) {
const definition = (this.ref as Schema)['_definition'];
return definition.schema[ definition.fieldsByIndex[index] ];
} else {
const definition = (this.parent as Schema)['_definition'];
const parentType = definition.schema[ definition.fieldsByIndex[this.parentIndex] ];
//
// Get the child type from parent structure.
// - ["string"] => "string"
// - { map: "string" } => "string"
// - { set: "string" } => "string"
//
return Object.values(parentType)[0];
}
}
getChildrenFilter(): FilterChildrenCallback {
const childFilters = (this.parent as Schema)['_definition'].childFilters;
return childFilters && childFilters[this.parentIndex];
}
//
// used during `.encode()`
//
getValue(index: number) {
return this.ref['getByIndex'](index);
}
delete(fieldName: string | number) {
const index = (typeof (fieldName) === "number")
? fieldName
: this.indexes[fieldName];
if (index === undefined) {
console.warn(`/schema ${this.ref.constructor.name}: trying to delete non-existing index: ${fieldName} (${index})`);
return;
}
const previousValue = this.getValue(index);
// console.log("$changes.delete =>", { fieldName, index, previousValue });
this.changes.set(index, { op: OPERATION.DELETE, index });
this.allChanges.delete(index);
// delete cache
delete this.caches[index];
// remove `root` reference
if (previousValue && previousValue['$changes']) {
previousValue['$changes'].parent = undefined;
}
this.changed = true;
this.touchParents();
}
discard(changed: boolean = false, discardAll: boolean = false) {
//
// Map, Array, etc:
// Remove cached key to ensure ADD operations is unsed instead of
// REPLACE in case same key is used on next patches.
//
// TODO: refactor this. this is not relevant for Collection and Set.
//
if (!(this.ref instanceof Schema)) {
this.changes.forEach((change) => {
if (change.op === OPERATION.DELETE) {
const index = this.ref['getIndex'](change.index)
delete this.indexes[index];
}
});
}
this.changes.clear();
this.changed = changed;
if (discardAll) {
this.allChanges.clear();
}
// re-set `currentCustomOperation`
this.currentCustomOperation = 0;
}
/**
* Recursively discard all changes from this, and child structures.
*/
discardAll() {
this.changes.forEach((change) => {
const value = this.getValue(change.index);
if (value && value['$changes']) {
value['$changes'].discardAll();
}
});
this.discard();
}
// cache(field: number, beginIndex: number, endIndex: number) {
cache(field: number, cachedBytes: number[]) {
this.caches[field] = cachedBytes;
}
clone() {
return new ChangeTree(this.ref, this.parent, this.root);
}
ensureRefId() {
// skip if refId is already set.
if (this.refId !== undefined) {
return;
}
this.refId = this.root.getNextUniqueId();
}
protected assertValidIndex(index: number, fieldName: string | number) {
if (index === undefined) {
throw new Error(`ChangeTree: missing index for field "${fieldName}"`);
}
}
}