mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
660 lines
20 KiB
TypeScript
660 lines
20 KiB
TypeScript
/*
|
|
* GDevelop JS Platform
|
|
* Copyright 2013-2016 Florian Rival (Florian.Rival@gmail.com). All rights reserved.
|
|
* This project is released under the MIT License.
|
|
*/
|
|
namespace gdjs {
|
|
const logger = new gdjs.Logger('Variables');
|
|
|
|
/**
|
|
* Children of a structure.
|
|
*/
|
|
type Children = Record<string, gdjs.Variable>;
|
|
|
|
/**
|
|
* A Variable is an object storing a value (number or a string) or children variables.
|
|
*/
|
|
export class Variable {
|
|
// TODO: convert this to an integer to speed up the type checks at runtime.
|
|
_type: VariableType = 'number';
|
|
_value: float = 0;
|
|
_str: string = '0';
|
|
_bool: boolean = false;
|
|
_children: Children = {};
|
|
_childrenArray: gdjs.Variable[] = [];
|
|
_undefinedInContainer: boolean = false;
|
|
|
|
// When synchronised over the network, this defines which player is the owner of the variable.
|
|
// Default is 0, meaning that the variable is owned by the host.
|
|
// If null, the variable is not synchronised over the network.
|
|
_playerNumber: number | null = 0;
|
|
|
|
/**
|
|
* @param [varData] The optional initial content of the variable.
|
|
*/
|
|
constructor(varData?: VariableData) {
|
|
this.reinitialize(varData);
|
|
}
|
|
|
|
reinitialize(varData?: VariableData | undefined) {
|
|
this._type = 'number';
|
|
this._value = 0;
|
|
this._str = '0';
|
|
this._bool = false;
|
|
this._children = {};
|
|
this._childrenArray = [];
|
|
this._undefinedInContainer = false;
|
|
|
|
if (varData !== undefined) {
|
|
this._type = varData.type || 'number';
|
|
if (this._type === 'number') {
|
|
this._value = parseFloat((varData.value as string) || '0');
|
|
// Protect against NaN.
|
|
if (this._value !== this._value) this._value = 0;
|
|
} else if (this._type === 'string') {
|
|
this._str = '' + varData.value || '0';
|
|
} else if (this._type === 'boolean') {
|
|
this._bool = !!varData.value;
|
|
} else if (this._type === 'structure') {
|
|
if (varData.children !== undefined) {
|
|
for (var i = 0, len = varData.children.length; i < len; ++i) {
|
|
var childData = varData.children[i];
|
|
if (childData.name === undefined) continue;
|
|
this._children[childData.name] = new gdjs.Variable(childData);
|
|
}
|
|
}
|
|
} else if (this._type === 'array' && varData.children) {
|
|
for (const childData of varData.children)
|
|
this._childrenArray.push(new gdjs.Variable(childData));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return true if the variable type is a primitive type.
|
|
*/
|
|
static isPrimitive(
|
|
type: VariableType
|
|
): type is 'string' | 'number' | 'boolean' {
|
|
return type === 'string' || type === 'number' || type === 'boolean';
|
|
}
|
|
|
|
/**
|
|
* Deeply copies a variable into another.
|
|
* @param source The source variable.
|
|
* @param target The target variable.
|
|
* @param merge Should the target be merged with the source, or be an exact copy?
|
|
* @returns The target variable.
|
|
*/
|
|
static copy(
|
|
source: gdjs.Variable,
|
|
target: gdjs.Variable,
|
|
merge?: boolean
|
|
): gdjs.Variable {
|
|
if (!merge) target.clearChildren();
|
|
target.castTo(source.getType());
|
|
if (source.isPrimitive()) {
|
|
target.setValue(source.getValue());
|
|
} else if (source.getType() === 'structure') {
|
|
const children = source.getAllChildren();
|
|
for (const p in children) {
|
|
if (children.hasOwnProperty(p))
|
|
target.addChild(p, children[p].clone());
|
|
}
|
|
} else if (source.getType() === 'array') {
|
|
for (const p of source.getAllChildrenArray())
|
|
target.pushVariableCopy(p);
|
|
}
|
|
return target;
|
|
}
|
|
|
|
/**
|
|
* Converts a JavaScript object into a value compatible
|
|
* with GDevelop variables and store it inside this variable.
|
|
* @param obj - The value to convert.
|
|
*/
|
|
fromJSObject(obj: any): this {
|
|
if (obj === null) {
|
|
this.setString('null');
|
|
} else if (typeof obj === 'number') {
|
|
if (Number.isNaN(obj)) {
|
|
logger.warn('Variables cannot be set to NaN, setting it to 0.');
|
|
this.setNumber(0);
|
|
} else {
|
|
this.setNumber(obj);
|
|
}
|
|
} else if (typeof obj === 'string') {
|
|
this.setString(obj);
|
|
} else if (typeof obj === 'undefined') {
|
|
// Do not modify the variable, as there is no value to set it to.
|
|
} else if (typeof obj === 'boolean') {
|
|
this.setBoolean(obj);
|
|
} else if (Array.isArray(obj)) {
|
|
this.castTo('array');
|
|
this.clearChildren();
|
|
for (const i in obj) this.getChild(i).fromJSObject(obj[i]);
|
|
} else if (typeof obj === 'object') {
|
|
this.castTo('structure');
|
|
this.clearChildren();
|
|
for (var p in obj)
|
|
if (obj.hasOwnProperty(p)) this.getChild(p).fromJSObject(obj[p]);
|
|
} else if (typeof obj === 'symbol') {
|
|
this.setString(obj.toString());
|
|
} else if (typeof obj === 'bigint') {
|
|
if (obj > Number.MAX_SAFE_INTEGER)
|
|
logger.warn(
|
|
'Error while converting JS variable to GDevelop variable: Integers bigger than ' +
|
|
Number.MAX_SAFE_INTEGER +
|
|
" aren't supported by GDevelop variables, it will be reduced to that size."
|
|
);
|
|
// @ts-ignore
|
|
variable.setNumber(parseInt(obj, 10));
|
|
} else if (typeof obj === 'function') {
|
|
logger.error(
|
|
'Error while converting JS variable to GDevelop variable: Impossible to set variable value to a function.'
|
|
);
|
|
} else {
|
|
logger.error(
|
|
'Error while converting JS variable to GDevelop variable: Cannot identify type of object ' +
|
|
obj
|
|
);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Unserialize a JSON string into this variable.
|
|
*
|
|
* This just logs an error if the JSON is invalid.
|
|
*
|
|
* @param json - A JSON string.
|
|
*/
|
|
fromJSON(json: string): this {
|
|
try {
|
|
var obj = JSON.parse(json);
|
|
} catch (e) {
|
|
logger.error('Unable to parse JSON: ' + json + e);
|
|
return this;
|
|
}
|
|
this.fromJSObject(obj);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Converts this variable into an equivalent JavaScript object.
|
|
* @returns A JavaScript object equivalent to the variable.
|
|
*/
|
|
toJSObject(): any {
|
|
switch (this._type) {
|
|
case 'string':
|
|
return this.getAsString();
|
|
case 'number':
|
|
return this.getAsNumber();
|
|
case 'boolean':
|
|
return this.getAsBoolean();
|
|
case 'structure':
|
|
const obj = {};
|
|
for (const name in this._children)
|
|
obj[name] = this._children[name].toJSObject();
|
|
return obj;
|
|
case 'array':
|
|
const arr: any[] = [];
|
|
for (const item of this._childrenArray) {
|
|
// item can be undefined if there are empty elements in the array.
|
|
// A variable can have empty items in its children array if one inserts
|
|
// a variable at an index greater than highest index. All the array elements
|
|
// in the gap will be empty elements.
|
|
arr.push(item === undefined ? undefined : item.toJSObject());
|
|
}
|
|
return arr;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return true if the type of the variable is a primitive type.
|
|
*/
|
|
isPrimitive() {
|
|
return gdjs.Variable.isPrimitive(this._type);
|
|
}
|
|
|
|
/**
|
|
* Returns a deep copy of this variable.
|
|
*/
|
|
clone(): gdjs.Variable {
|
|
return gdjs.Variable.copy(this, new gdjs.Variable());
|
|
}
|
|
|
|
/**
|
|
* Used (usually by gdjs.VariablesContainer) to set that the variable must be
|
|
* considered as not existing in the container.
|
|
*/
|
|
setUndefinedInContainer() {
|
|
this._undefinedInContainer = true;
|
|
}
|
|
|
|
/**
|
|
* Check if the variable must be considered as not existing in its container
|
|
* (usually a gdjs.VariablesContainer).
|
|
* @return true if the container must consider that the variable does not exist.
|
|
*/
|
|
isUndefinedInContainer(): boolean {
|
|
return this._undefinedInContainer;
|
|
}
|
|
|
|
/**
|
|
* Converts the variable into another type.
|
|
* @param newType The new type of the variable
|
|
*/
|
|
castTo(newType: VariableType) {
|
|
if (newType === 'string') this.setString(this.getAsString());
|
|
else if (newType === 'number') this.setNumber(this.getAsNumber());
|
|
else if (newType === 'boolean') this.setBoolean(this.getAsBoolean());
|
|
else if (newType === 'structure') {
|
|
if (this._type === 'structure') return;
|
|
this._children = this.getAllChildren();
|
|
this._type = 'structure';
|
|
} else if (newType === 'array') {
|
|
if (this._type === 'array') return;
|
|
this._childrenArray = this.getAllChildrenArray();
|
|
this._type = 'array';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the child with the specified name or at the specified index.
|
|
*
|
|
* If the variable is an array, prefer `getChildAt`.
|
|
* If the variable is a structure, prefer `getChildNamed`.
|
|
*
|
|
* If the variable has not the specified child, an empty variable with the specified name
|
|
* (or index) is added as child.
|
|
*
|
|
* @returns The child variable
|
|
*/
|
|
getChild(childName: string | number): gdjs.Variable {
|
|
if (this._type === 'array')
|
|
return this.getChildAt(
|
|
// @ts-ignore
|
|
Number.isInteger(childName) ? childName : parseInt(childName, 10) || 0
|
|
);
|
|
else {
|
|
if (this._type !== 'structure') this.castTo('structure');
|
|
return this.getChildNamed('' + childName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the child with the specified name.
|
|
*
|
|
* If the variable has not the specified child, an empty variable with the specified name
|
|
* is added as child.
|
|
*
|
|
* @returns The child variable
|
|
*/
|
|
getChildNamed(childName: string): gdjs.Variable {
|
|
const child = this._children[childName];
|
|
if (child === undefined || child === null)
|
|
return (this._children[childName] = new gdjs.Variable());
|
|
|
|
return child;
|
|
}
|
|
|
|
/**
|
|
* Add a child variable with the specified name.
|
|
*
|
|
* If there is an existing child variable with this name, it is erased.
|
|
* @param childName The name of the variable to add
|
|
* @param childVariable The variable to add as a child
|
|
* @returns The variable (for chaining calls)
|
|
*/
|
|
addChild(childName: string, childVariable: gdjs.Variable): this {
|
|
if (this._type !== 'structure') this.castTo('structure');
|
|
|
|
this._children[childName] = childVariable;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Return the child in a variable.
|
|
*
|
|
* Check if the variable has the specified children
|
|
* @return true if variable has the children with the specified name
|
|
*/
|
|
hasChild(childName: string): boolean {
|
|
return (
|
|
this._type === 'structure' && this._children.hasOwnProperty(childName)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Remove the child with the specified name.
|
|
*
|
|
* If the variable has not the specified child, nothing is done.
|
|
* @param childName The name of the child to be removed
|
|
*/
|
|
removeChild(childName: string) {
|
|
if (this._type !== 'structure') return;
|
|
delete this._children[childName];
|
|
}
|
|
|
|
/**
|
|
* Remove all the children.
|
|
*/
|
|
clearChildren() {
|
|
this._children = {};
|
|
this._childrenArray = [];
|
|
}
|
|
|
|
/**
|
|
* Replaces all the children with a new map of children.
|
|
* @param newChildren The map of new children.
|
|
*/
|
|
replaceChildren(newChildren: Children) {
|
|
this._type = 'structure';
|
|
this._children = newChildren;
|
|
}
|
|
|
|
/**
|
|
* Replaces all the children with a new array of children.
|
|
* @param newChildren The array of new children.
|
|
*/
|
|
replaceChildrenArray(newChildren: gdjs.Variable[]) {
|
|
this._type = 'array';
|
|
this._childrenArray = newChildren;
|
|
}
|
|
|
|
/**
|
|
* Get the value of the variable, considered as a number
|
|
* @return The number stored in the variable
|
|
*/
|
|
getAsNumber(): float {
|
|
if (this._type !== 'number') {
|
|
let number = 0;
|
|
if (this._type === 'string') number = parseFloat(this._str);
|
|
else if (this._type === 'boolean') number = this._bool ? 1 : 0;
|
|
|
|
return number === number ? number : 0; //Ensure NaN is not returned as a value.
|
|
}
|
|
|
|
return this._value;
|
|
}
|
|
|
|
/**
|
|
* Change the value of the variable, considered as a number
|
|
* @param newValue The new value to be set
|
|
*/
|
|
setNumber(newValue: float) {
|
|
this._type = 'number';
|
|
//@ts-ignore parseFloat does accept numbers.
|
|
newValue = parseFloat(newValue);
|
|
this._value = newValue === newValue ? newValue : 0; // Prevent NaN
|
|
}
|
|
|
|
/**
|
|
* Get the value of the variable, considered as a string
|
|
* @return The string stored in the variable
|
|
*/
|
|
getAsString(): string {
|
|
if (this._type !== 'string') {
|
|
if (this._type === 'number') return this._value.toString();
|
|
else if (this._type === 'boolean') return this._bool ? 'true' : 'false';
|
|
else if (this._type === 'structure') return '[Structure]';
|
|
else if (this._type === 'array') return '[Array]';
|
|
else return '';
|
|
}
|
|
|
|
return this._str;
|
|
}
|
|
|
|
/**
|
|
* Change the value of the variable, considered as a string
|
|
* @param newValue The new string to be set
|
|
*/
|
|
setString(newValue: string): void {
|
|
this._type = 'string';
|
|
this._str = '' + newValue;
|
|
}
|
|
|
|
/**
|
|
* Get the value of the variable, as a number if it's one,
|
|
* or as a string (if it's a string or something else)
|
|
*
|
|
* In most cases, prefer calling `getAsNumber` or `getAsString` directly.
|
|
* This is a fallback in case a variable type can't be known statically for sure,
|
|
* like `getValue`.
|
|
*
|
|
* @private
|
|
*/
|
|
getAsNumberOrString(): number | string {
|
|
if (this._type === 'number') return this._value;
|
|
return this.getAsString();
|
|
}
|
|
|
|
/**
|
|
* Get the value of the variable, considered as a boolean
|
|
* @return The boolean value of the variable.
|
|
*/
|
|
getAsBoolean(): boolean {
|
|
if (this._type !== 'boolean') {
|
|
if (this._type === 'number') return this._value !== 0;
|
|
else if (this._type === 'string')
|
|
return this._str !== '0' && this._str !== '' && this._str !== 'false';
|
|
else return true;
|
|
}
|
|
|
|
return this._bool;
|
|
}
|
|
|
|
/**
|
|
* Change the value of the variable, considered as a boolean
|
|
* @param newValue The new boolean to be set.
|
|
*/
|
|
setBoolean(newValue: boolean) {
|
|
this._type = 'boolean';
|
|
this._bool = !!newValue;
|
|
}
|
|
|
|
/**
|
|
* Toggle the value of the variable, considered as a boolean.
|
|
*/
|
|
toggle() {
|
|
this.setBoolean(!this.getAsBoolean());
|
|
}
|
|
|
|
/**
|
|
* Sets the primitive value using the setter of the current type.
|
|
* @param newValue The primitive value of the variable.
|
|
*/
|
|
setValue(newValue: string | float | boolean) {
|
|
if (this._type === 'string') this.setString(newValue as string);
|
|
else if (this._type === 'number') this.setNumber(newValue as float);
|
|
else if (this._type === 'boolean') this.setBoolean(newValue as boolean);
|
|
}
|
|
|
|
/**
|
|
* Gets the primitive value using the getter of the current type.
|
|
*/
|
|
getValue(): string | float | boolean {
|
|
return this._type === 'number'
|
|
? this.getAsNumber()
|
|
: this._type === 'boolean'
|
|
? this.getAsBoolean()
|
|
: this.getAsString();
|
|
}
|
|
|
|
/**
|
|
* Return true if the variable is a structure.
|
|
* @return true if the variable is a structure.
|
|
* @deprecated Use {@link gdjs.Variable.getType} instead.
|
|
*/
|
|
isStructure(): boolean {
|
|
return this._type === 'structure';
|
|
}
|
|
|
|
/**
|
|
* Return true if the variable is a number.
|
|
* @return true if the variable is a number.
|
|
* @deprecated Use {@link gdjs.Variable.getType} instead.
|
|
*/
|
|
isNumber(): boolean {
|
|
return this._type === 'number';
|
|
}
|
|
|
|
/**
|
|
* Returns the type of the variable.
|
|
*/
|
|
getType(): VariableType {
|
|
return this._type;
|
|
}
|
|
|
|
/**
|
|
* Return the object containing all the children of the variable.
|
|
* @return All the children of the variable
|
|
*/
|
|
getAllChildren(): Children {
|
|
return this._type === 'structure'
|
|
? this._children
|
|
: this._type === 'array'
|
|
? ((Object.assign({}, this._childrenArray) as unknown) as Children)
|
|
: {};
|
|
}
|
|
|
|
/**
|
|
* Return an Array containing all the children of the variable.
|
|
*/
|
|
getAllChildrenArray(): gdjs.Variable[] {
|
|
return this._type === 'structure'
|
|
? Object.values(this._children)
|
|
: this._type === 'array'
|
|
? this._childrenArray
|
|
: [];
|
|
}
|
|
|
|
/**
|
|
* Return the length of the collection.
|
|
*/
|
|
getChildrenCount(): integer {
|
|
return this._type === 'structure'
|
|
? Object.keys(this._children).length
|
|
: this._type === 'array'
|
|
? this._childrenArray.length
|
|
: 0;
|
|
}
|
|
|
|
/**
|
|
* Add the given number to the variable value
|
|
* @param val the number to add
|
|
*/
|
|
add(val: float) {
|
|
this.setNumber(this.getAsNumber() + val);
|
|
}
|
|
|
|
/**
|
|
* Subtract the given number to the variable value
|
|
* @param val the number to subtract
|
|
*/
|
|
sub(val: float) {
|
|
this.setNumber(this.getAsNumber() - val);
|
|
}
|
|
|
|
/**
|
|
* Multiply the variable value by the given number
|
|
* @param val the factor
|
|
*/
|
|
mul(val: float) {
|
|
this.setNumber(this.getAsNumber() * val);
|
|
}
|
|
|
|
/**
|
|
* Divide the variable value by the given number
|
|
* @param val the divisor
|
|
*/
|
|
div(val: float) {
|
|
this.setNumber(this.getAsNumber() / val);
|
|
}
|
|
|
|
/**
|
|
* Concatenate the given string at the end of the variable value
|
|
* @param str the string to append
|
|
*/
|
|
concatenateString(str: string) {
|
|
this.setString(this.getAsString() + str);
|
|
}
|
|
|
|
/**
|
|
* @deprecated
|
|
* @private
|
|
* @alias concatenateString
|
|
*/
|
|
concatenate(str: string) {
|
|
this.setString(this.getAsString() + str);
|
|
}
|
|
|
|
/**
|
|
* Get a variable at a given index of the array.
|
|
*/
|
|
getChildAt(index: integer) {
|
|
if (this._type !== 'array') this.castTo('array');
|
|
|
|
if (
|
|
this._childrenArray[index] === undefined ||
|
|
this._childrenArray[index] === null
|
|
)
|
|
this._childrenArray[index] = new gdjs.Variable();
|
|
return this._childrenArray[index];
|
|
}
|
|
|
|
/**
|
|
* Removes a variable at a given index of the array.
|
|
*/
|
|
removeAtIndex(index: integer) {
|
|
if (this._type === 'array') this._childrenArray.splice(index, 1);
|
|
}
|
|
|
|
/**
|
|
* Pushes a copy of a variable into the array.
|
|
*/
|
|
pushVariableCopy(variable: gdjs.Variable) {
|
|
if (this._type !== 'array') this.castTo('array');
|
|
|
|
this._childrenArray.push(variable.clone());
|
|
}
|
|
|
|
/**
|
|
* Pushes a variable into the array without duplicating it first.
|
|
* This should only be used by generated code.
|
|
*/
|
|
_pushVariable(variable: gdjs.Variable) {
|
|
if (this._type !== 'array') this.castTo('array');
|
|
|
|
this._childrenArray.push(variable);
|
|
}
|
|
|
|
/**
|
|
* Pushes a value into the array.
|
|
*/
|
|
pushValue(value: string | float | boolean) {
|
|
if (this._type !== 'array') this.castTo('array');
|
|
|
|
this._childrenArray.push(
|
|
new gdjs.Variable({
|
|
type: typeof value as 'string' | 'number' | 'boolean',
|
|
value,
|
|
})
|
|
);
|
|
}
|
|
|
|
getPlayerOwnership(): number | null {
|
|
return this._playerNumber;
|
|
}
|
|
|
|
setPlayerOwnership(playerNumber: number) {
|
|
this._playerNumber = playerNumber;
|
|
}
|
|
|
|
disableSynchronization() {
|
|
this._playerNumber = null;
|
|
}
|
|
}
|
|
}
|