mirror of
https://github.com/Heretek-AI/GDevelop.git
synced 2026-07-22 01:55:25 -04:00
bc606ed1be
* When launching the Debugger to inspect a game, open the Console to see internal messages sent by the game, JavaScript code or the game engine. * This is an advanced feature that is useful to find issues in your game or to see if your game is displaying any internal error.
585 lines
17 KiB
TypeScript
585 lines
17 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;
|
|
|
|
/**
|
|
* @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) arr.push(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.
|
|
*
|
|
* If the variable has not the specified child, an empty variable with the specified name
|
|
* is added as child.
|
|
* @returns The child variable
|
|
*/
|
|
getChild(childName: string): gdjs.Variable {
|
|
// Make sure the variable is a collection
|
|
if (this.isPrimitive()) this.castTo('structure');
|
|
|
|
if (this._type === 'array')
|
|
return this.getChildAt(parseInt(childName, 10) || 0);
|
|
|
|
if (
|
|
this._children[childName] === undefined ||
|
|
this._children[childName] === null
|
|
)
|
|
this._children[childName] = new gdjs.Variable();
|
|
return this._children[childName];
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
// Make sure this is a 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, 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;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
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) {
|
|
this.castTo('array');
|
|
this._childrenArray.push(variable.clone());
|
|
}
|
|
|
|
/**
|
|
* Pushes a value into the array.
|
|
*/
|
|
pushValue(value: string | float | boolean) {
|
|
this.castTo('array');
|
|
this._childrenArray.push(
|
|
new gdjs.Variable({
|
|
type: typeof value as 'string' | 'number' | 'boolean',
|
|
value,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
}
|