mirror of
https://github.com/Heretek-AI/GDevelop.git
synced 2026-07-21 01:25:32 -04:00
6bb56c6e5b
- "For Each Child Variable" now also properly support choosing local variables for the variable to iterate on, and for the variable where to store the child and the optional variable where to store the name. - The loop counter starts from 0 and is automatically increased by 1 each time the event is repeated.
1511 lines
48 KiB
JavaScript
1511 lines
48 KiB
JavaScript
const initializeGDevelopJs = require('../../Binaries/embuild/GDevelop.js/libGD.js');
|
||
const { makeMinimalGDJSMock } = require('../TestUtils/GDJSMocks');
|
||
const {
|
||
generateCompiledEventsFromSerializedEvents,
|
||
} = require('../TestUtils/CodeGenerationHelpers.js');
|
||
|
||
describe('libGD.js - GDJS Loops Code Generation integration tests', function () {
|
||
let gd = null;
|
||
beforeAll(async () => {
|
||
gd = await initializeGDevelopJs();
|
||
});
|
||
|
||
it('can generate a While event', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::Standard',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Counter', '=', '0'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
{
|
||
infiniteLoopWarning: true,
|
||
type: 'BuiltinCommonInstructions::While',
|
||
whileConditions: [
|
||
{
|
||
type: { value: 'VarScene' },
|
||
parameters: ['Counter', '<', '4'],
|
||
},
|
||
],
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Counter', '+', '1'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
|
||
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(4);
|
||
});
|
||
|
||
it('can generate nested While events', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::Standard',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Counter', '=', '0'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
{
|
||
type: 'BuiltinCommonInstructions::Standard',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['CellY', '=', '0'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
{
|
||
infiniteLoopWarning: true,
|
||
type: 'BuiltinCommonInstructions::While',
|
||
whileConditions: [
|
||
{
|
||
type: { value: 'VarScene' },
|
||
parameters: ['CellY', '<', '4'],
|
||
},
|
||
],
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['CellX', '=', '0'],
|
||
},
|
||
],
|
||
events: [
|
||
{
|
||
infiniteLoopWarning: true,
|
||
type: 'BuiltinCommonInstructions::While',
|
||
whileConditions: [
|
||
{
|
||
type: { value: 'VarScene' },
|
||
parameters: ['CellX', '<', '5'],
|
||
},
|
||
],
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['CellX', '+', '1'],
|
||
},
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Counter', '+', '1'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
{
|
||
type: 'BuiltinCommonInstructions::Standard',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['CellY', '+', '1'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
|
||
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(20);
|
||
});
|
||
|
||
it('can generate a Repeat event', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::Repeat',
|
||
repeatExpression: '4',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Counter', '+', '1'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
|
||
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(4);
|
||
});
|
||
|
||
it('can generate a nested Repeat event', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::Repeat',
|
||
repeatExpression: '4',
|
||
conditions: [],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
type: 'BuiltinCommonInstructions::Repeat',
|
||
repeatExpression: '5',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Counter', '+', '1'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
|
||
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(20);
|
||
});
|
||
|
||
it('can generate a "for each object" event', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'MyObject',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Counter', '+', 'MyObject.Variable(MyVariable)'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: {
|
||
MyObject: 'object',
|
||
},
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
|
||
// Create 4 objects with variable values from 1 to 4.
|
||
const objectLists = new gdjs.Hashtable();
|
||
const myObjects = [];
|
||
objectLists.put('MyObject', myObjects);
|
||
for (let index = 1; index <= 4; index++) {
|
||
const myObject = runtimeScene.createObject('MyObject');
|
||
myObject.getVariables().get('MyVariable').setNumber(index);
|
||
myObjects.push(myObject);
|
||
}
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [objectLists]);
|
||
|
||
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
|
||
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(
|
||
1 + 2 + 3 + 4
|
||
);
|
||
});
|
||
|
||
it('can generate a nested "for each object" event', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'MyObjectA',
|
||
conditions: [],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'MyObjectB',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: [
|
||
'Counter',
|
||
'+',
|
||
'MyObjectA.Variable(MyVariable) * MyObjectB.Variable(MyVariable)',
|
||
],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: {
|
||
MyObjectA: 'object',
|
||
MyObjectB: 'object',
|
||
},
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
|
||
// Create 4 objects with variable values from 1 to 4.
|
||
const objectALists = new gdjs.Hashtable();
|
||
const myObjectsA = [];
|
||
objectALists.put('MyObjectA', myObjectsA);
|
||
for (let index = 1; index <= 4; index++) {
|
||
const myObjectA = runtimeScene.createObject('MyObjectA');
|
||
myObjectA.getVariables().get('MyVariable').setNumber(index);
|
||
myObjectsA.push(myObjectA);
|
||
}
|
||
// Create 3 objects with variable values from 5 to 7.
|
||
const objectBLists = new gdjs.Hashtable();
|
||
const myObjectsB = [];
|
||
objectBLists.put('MyObjectB', myObjectsB);
|
||
for (let index = 1; index <= 3; index++) {
|
||
const myObjectB = runtimeScene.createObject('MyObjectB');
|
||
myObjectB
|
||
.getVariables()
|
||
.get('MyVariable')
|
||
.setNumber(4 + index);
|
||
myObjectsB.push(myObjectB);
|
||
}
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [objectALists, objectBLists]);
|
||
|
||
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
|
||
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(
|
||
(1 + 2 + 3 + 4) * (5 + 6 + 7)
|
||
);
|
||
});
|
||
|
||
it('can generate a "for each child variable" event with undeclared variables', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEachChildVariable',
|
||
iterableVariableName: 'MyVariable',
|
||
valueIteratorVariableName: 'child',
|
||
keyIteratorVariableName: '',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Counter', '+', 'Variable(child)'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
|
||
// Create 4 child-variables with values from 1 to 4.
|
||
const myVariable = runtimeScene.getVariables().get('MyVariable');
|
||
for (let index = 1; index <= 4; index++) {
|
||
myVariable.getChild('Child' + index).setNumber(index);
|
||
}
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
|
||
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(
|
||
1 + 2 + 3 + 4
|
||
);
|
||
});
|
||
|
||
it('can generate a nested "for each child variable" event with undeclared variables', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEachChildVariable',
|
||
iterableVariableName: 'MyVariableA',
|
||
valueIteratorVariableName: 'childA',
|
||
keyIteratorVariableName: '',
|
||
conditions: [],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEachChildVariable',
|
||
iterableVariableName: 'MyVariableB',
|
||
valueIteratorVariableName: 'childB',
|
||
keyIteratorVariableName: '',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: [
|
||
'Counter',
|
||
'+',
|
||
'Variable(childA) * Variable(childB)',
|
||
],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
|
||
// Create 4 child-variables with values from 1 to 4.
|
||
const myVariableA = runtimeScene.getVariables().get('MyVariableA');
|
||
for (let index = 1; index <= 4; index++) {
|
||
myVariableA.getChild('Child' + index).setNumber(index);
|
||
}
|
||
// Create 4 child-variables with values from 5 to 7.
|
||
const myVariableB = runtimeScene.getVariables().get('MyVariableB');
|
||
for (let index = 1; index <= 3; index++) {
|
||
myVariableB.getChild('Child' + index).setNumber(4 + index);
|
||
}
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
|
||
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(
|
||
(1 + 2 + 3 + 4) * (5 + 6 + 7)
|
||
);
|
||
});
|
||
|
||
it('can generate a While event with local variables', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::While',
|
||
whileConditions: [
|
||
{
|
||
type: { value: 'NumberVariable' },
|
||
parameters: ['LocalCounter', '<', '3'],
|
||
},
|
||
],
|
||
variables: [{ name: 'LocalCounter', type: 'number', value: 0 }],
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'SetNumberVariable' },
|
||
parameters: ['LocalCounter', '+', '1'],
|
||
},
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Counter', '+', 'LocalCounter'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(6);
|
||
expect(runtimeScene.getVariables().has('LocalCounter')).toBe(false);
|
||
});
|
||
|
||
it('can generate a While event with an index variable', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::While',
|
||
whileConditions: [
|
||
{
|
||
type: { value: 'NumberVariable' },
|
||
parameters: ['LoopIndex', '<', '4'],
|
||
},
|
||
],
|
||
variables: [{ name: 'LoopIndex', type: 'number', value: 0 }],
|
||
loopIndexVariable: 'LoopIndex',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Sum', '+', 'LoopIndex'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().get('Sum').getAsNumber()).toBe(6);
|
||
expect(runtimeScene.getVariables().has('LoopIndex')).toBe(false);
|
||
});
|
||
|
||
it('can generate a Repeat event with local variables', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::Repeat',
|
||
repeatExpression: '3',
|
||
variables: [{ name: 'LocalValue', type: 'number', value: 1 }],
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'SetNumberVariable' },
|
||
parameters: ['LocalValue', '+', '1'],
|
||
},
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Sum', '+', 'LocalValue'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().get('Sum').getAsNumber()).toBe(9);
|
||
expect(runtimeScene.getVariables().has('LocalValue')).toBe(false);
|
||
});
|
||
|
||
it('can generate a Repeat event with an index variable', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::Repeat',
|
||
repeatExpression: '5',
|
||
variables: [{ name: 'LoopIndex', type: 'number', value: 0 }],
|
||
loopIndexVariable: 'LoopIndex',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Sum', '+', 'LoopIndex'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().get('Sum').getAsNumber()).toBe(10);
|
||
expect(runtimeScene.getVariables().has('LoopIndex')).toBe(false);
|
||
});
|
||
|
||
it('can generate a ForEach event with local variables', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'MyObject',
|
||
variables: [{ name: 'Multiplier', type: 'number', value: 2 }],
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: [
|
||
'Counter',
|
||
'+',
|
||
'MyObject.Variable(MyVariable) * Multiplier',
|
||
],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: {
|
||
MyObject: 'object',
|
||
},
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
const objectLists = new gdjs.Hashtable();
|
||
const myObjects = [];
|
||
objectLists.put('MyObject', myObjects);
|
||
for (let index = 1; index <= 4; index++) {
|
||
const myObject = runtimeScene.createObject('MyObject');
|
||
myObject.getVariables().get('MyVariable').setNumber(index);
|
||
myObjects.push(myObject);
|
||
}
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [objectLists]);
|
||
|
||
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(20);
|
||
expect(runtimeScene.getVariables().has('Multiplier')).toBe(false);
|
||
});
|
||
|
||
it('can generate a ForEach event with an index variable', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'MyObject',
|
||
variables: [{ name: 'LoopIndex', type: 'number', value: 0 }],
|
||
loopIndexVariable: 'LoopIndex',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Sum', '+', 'LoopIndex'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: {
|
||
MyObject: 'object',
|
||
},
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
const objectLists = new gdjs.Hashtable();
|
||
const myObjects = [];
|
||
objectLists.put('MyObject', myObjects);
|
||
for (let index = 1; index <= 4; index++) {
|
||
const myObject = runtimeScene.createObject('MyObject');
|
||
myObjects.push(myObject);
|
||
}
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [objectLists]);
|
||
|
||
expect(runtimeScene.getVariables().get('Sum').getAsNumber()).toBe(6);
|
||
expect(runtimeScene.getVariables().has('LoopIndex')).toBe(false);
|
||
});
|
||
|
||
it('can generate a "for each child variable" event with local variables', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEachChildVariable',
|
||
iterableVariableName: 'MyStructure',
|
||
valueIteratorVariableName: 'CurrentChild',
|
||
keyIteratorVariableName: '',
|
||
variables: [
|
||
{
|
||
name: 'MyStructure',
|
||
type: 'structure',
|
||
children: [
|
||
{ name: 'Child1', type: 'number', value: 1 },
|
||
{ name: 'Child2', type: 'number', value: 2 },
|
||
{ name: 'Child3', type: 'number', value: 3 },
|
||
],
|
||
},
|
||
{ name: 'CurrentChild', type: 'number', value: 0 },
|
||
],
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Sum', '+', 'CurrentChild'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().get('Sum').getAsNumber()).toBe(6);
|
||
expect(runtimeScene.getVariables().has('MyStructure')).toBe(false);
|
||
expect(runtimeScene.getVariables().has('CurrentChild')).toBe(false);
|
||
});
|
||
|
||
it('can generate a "for each child variable" event with an index variable', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEachChildVariable',
|
||
iterableVariableName: 'MyVariable',
|
||
valueIteratorVariableName: '',
|
||
keyIteratorVariableName: '',
|
||
variables: [{ name: 'LoopIndex', type: 'number', value: 0 }],
|
||
loopIndexVariable: 'LoopIndex',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Sum', '+', 'LoopIndex'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
const myVariable = runtimeScene.getVariables().get('MyVariable');
|
||
for (let index = 1; index <= 4; index++) {
|
||
myVariable.getChild('Child' + index).setNumber(index);
|
||
}
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().get('Sum').getAsNumber()).toBe(6);
|
||
expect(runtimeScene.getVariables().has('LoopIndex')).toBe(false);
|
||
});
|
||
|
||
it('can generate a "for each child variable" event with local iterator variables', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEachChildVariable',
|
||
iterableVariableName: 'MyArray',
|
||
valueIteratorVariableName: 'CurrentValue',
|
||
keyIteratorVariableName: 'CurrentKey',
|
||
variables: [
|
||
{
|
||
name: 'MyArray',
|
||
type: 'array',
|
||
children: [
|
||
{ name: '0', type: 'number', value: 5 },
|
||
{ name: '1', type: 'number', value: 7 },
|
||
],
|
||
},
|
||
{ name: 'CurrentValue', type: 'number', value: 0 },
|
||
{ name: 'CurrentKey', type: 'number', value: 0 },
|
||
],
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['ValueSum', '+', 'CurrentValue'],
|
||
},
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['KeySum', '+', 'CurrentKey'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().get('ValueSum').getAsNumber()).toBe(12);
|
||
expect(runtimeScene.getVariables().get('KeySum').getAsNumber()).toBe(1);
|
||
});
|
||
|
||
it('can generate nested loops with index variables of different names', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::Repeat',
|
||
repeatExpression: '3',
|
||
loopIndexVariable: 'OuterIndex',
|
||
variables: [{ name: 'OuterIndex', type: 'number', value: 0 }],
|
||
conditions: [],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
type: 'BuiltinCommonInstructions::Repeat',
|
||
repeatExpression: '2',
|
||
loopIndexVariable: 'InnerIndex',
|
||
variables: [{ name: 'InnerIndex', type: 'number', value: 0 }],
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Sum', '+', 'OuterIndex * 10 + InnerIndex'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().get('Sum').getAsNumber()).toBe(63);
|
||
expect(runtimeScene.getVariables().has('OuterIndex')).toBe(false);
|
||
expect(runtimeScene.getVariables().has('InnerIndex')).toBe(false);
|
||
});
|
||
|
||
it('can generate nested loops where an index variable shadows another one', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::Repeat',
|
||
repeatExpression: '3',
|
||
variables: [{ name: 'LoopIndex', type: 'number', value: 0 }],
|
||
loopIndexVariable: 'LoopIndex',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Sum', '+', 'LoopIndex'],
|
||
},
|
||
],
|
||
events: [
|
||
{
|
||
type: 'BuiltinCommonInstructions::Repeat',
|
||
repeatExpression: '2',
|
||
loopIndexVariable: 'LoopIndex',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Sum', '+', 'LoopIndex'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().get('Sum').getAsNumber()).toBe(6);
|
||
expect(runtimeScene.getVariables().has('LoopIndex')).toBe(false);
|
||
});
|
||
|
||
it('can generate deeper nested loops mixing local variables and index variable shadowing', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::Repeat',
|
||
repeatExpression: '2',
|
||
loopIndexVariable: 'I',
|
||
variables: [{ name: 'I', type: 'number', value: 0 }],
|
||
conditions: [],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
type: 'BuiltinCommonInstructions::Repeat',
|
||
repeatExpression: '3',
|
||
variables: [{ name: 'InnerConstant', type: 'number', value: 2 }],
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Sum', '+', 'I + InnerConstant'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
{
|
||
type: 'BuiltinCommonInstructions::Repeat',
|
||
repeatExpression: '2',
|
||
loopIndexVariable: 'J',
|
||
variables: [{ name: 'J', type: 'number', value: 0 }],
|
||
conditions: [],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
type: 'BuiltinCommonInstructions::Repeat',
|
||
repeatExpression: '2',
|
||
loopIndexVariable: 'I',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Sum', '+', 'I + J'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().get('Sum').getAsNumber()).toBe(23);
|
||
expect(runtimeScene.getVariables().has('InnerConstant')).toBe(false);
|
||
expect(runtimeScene.getVariables().has('I')).toBe(false);
|
||
expect(runtimeScene.getVariables().has('J')).toBe(false);
|
||
});
|
||
|
||
it('can generate a "for each child variable" event with scene key and value iterators', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEachChildVariable',
|
||
iterableVariableName: 'MyArray',
|
||
valueIteratorVariableName: 'child',
|
||
keyIteratorVariableName: 'ChildName',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['ValueSum', '+', 'Variable(child)'],
|
||
},
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['KeySum', '+', 'Variable(ChildName)'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{ logCode: false }
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
const myArray = runtimeScene.getVariables().get('MyArray');
|
||
myArray.getChild('0').setNumber(2);
|
||
myArray.getChild('1').setNumber(4);
|
||
myArray.getChild('2').setNumber(6);
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().get('ValueSum').getAsNumber()).toBe(12);
|
||
expect(runtimeScene.getVariables().get('KeySum').getAsNumber()).toBe(3);
|
||
});
|
||
|
||
it('can generate a "for each child variable" event with scene iterable and local value iterator', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEachChildVariable',
|
||
iterableVariableName: 'MyArray',
|
||
valueIteratorVariableName: 'LocalChildValue',
|
||
keyIteratorVariableName: 'ChildName',
|
||
variables: [{ name: 'LocalChildValue', type: 'number', value: 0 }],
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['ValueSum', '+', 'LocalChildValue'],
|
||
},
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['KeySum', '+', 'Variable(ChildName)'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{ logCode: false }
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
const myArray = runtimeScene.getVariables().get('MyArray');
|
||
myArray.getChild('0').setNumber(2);
|
||
myArray.getChild('1').setNumber(4);
|
||
myArray.getChild('2').setNumber(6);
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().get('ValueSum').getAsNumber()).toBe(12);
|
||
expect(runtimeScene.getVariables().get('KeySum').getAsNumber()).toBe(3);
|
||
expect(runtimeScene.getVariables().has('LocalChildValue')).toBe(false);
|
||
});
|
||
|
||
it('can generate a "for each child variable" event with scene iterable and both local iterators', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEachChildVariable',
|
||
iterableVariableName: 'MyArray',
|
||
valueIteratorVariableName: 'LocalChildValue',
|
||
keyIteratorVariableName: 'LocalChildName',
|
||
variables: [
|
||
{ name: 'LocalChildName', type: 'string', value: '' },
|
||
{ name: 'LocalChildValue', type: 'number', value: 0 },
|
||
],
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['ValueSum', '+', 'LocalChildValue'],
|
||
},
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['KeySum', '+', 'LocalChildName'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{ logCode: false }
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
const myArray = runtimeScene.getVariables().get('MyArray');
|
||
myArray.getChild('0').setNumber(2);
|
||
myArray.getChild('1').setNumber(4);
|
||
myArray.getChild('2').setNumber(6);
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().get('ValueSum').getAsNumber()).toBe(12);
|
||
expect(runtimeScene.getVariables().get('KeySum').getAsNumber()).toBe(3);
|
||
expect(runtimeScene.getVariables().has('LocalChildValue')).toBe(false);
|
||
expect(runtimeScene.getVariables().has('LocalChildName')).toBe(false);
|
||
});
|
||
|
||
it('can generate a "for each child variable" event with local iterable declared in a parent event', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::Standard',
|
||
variables: [
|
||
{
|
||
name: 'LocalUpperArray',
|
||
type: 'array',
|
||
children: [
|
||
{ type: 'number', value: 9 },
|
||
{ type: 'number', value: 10 },
|
||
{ type: 'number', value: 11 },
|
||
],
|
||
},
|
||
],
|
||
conditions: [],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEachChildVariable',
|
||
iterableVariableName: 'LocalUpperArray',
|
||
valueIteratorVariableName: 'LocalChildValue',
|
||
keyIteratorVariableName: 'LocalChildName',
|
||
variables: [
|
||
{ name: 'LocalChildName', type: 'string', value: '' },
|
||
{ name: 'LocalChildValue', type: 'number', value: 0 },
|
||
],
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['ValueSum', '+', 'LocalChildValue'],
|
||
},
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['KeySum', '+', 'LocalChildName'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{ logCode: false }
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().get('ValueSum').getAsNumber()).toBe(30);
|
||
expect(runtimeScene.getVariables().get('KeySum').getAsNumber()).toBe(3);
|
||
expect(runtimeScene.getVariables().has('LocalUpperArray')).toBe(false);
|
||
expect(runtimeScene.getVariables().has('LocalChildValue')).toBe(false);
|
||
expect(runtimeScene.getVariables().has('LocalChildName')).toBe(false);
|
||
});
|
||
|
||
it('can iterate over a local array containing mixed types (number + structure) using ToJSON', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEachChildVariable',
|
||
iterableVariableName: 'MixedArray',
|
||
valueIteratorVariableName: 'LocalChildValue',
|
||
keyIteratorVariableName: 'LocalChildName',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: ['Content', '+', 'ToJSON(LocalChildValue)'],
|
||
},
|
||
],
|
||
variables: [
|
||
{ name: 'LocalChildName', type: 'string', value: '' },
|
||
{
|
||
name: 'MixedArray',
|
||
type: 'array',
|
||
children: [
|
||
{ type: 'number', value: 9 },
|
||
{
|
||
type: 'structure',
|
||
children: [
|
||
{ name: 'a', type: 'number', value: 1 },
|
||
{ name: 'b', type: 'string', value: 'two' },
|
||
],
|
||
},
|
||
{ type: 'number', value: 11 },
|
||
],
|
||
},
|
||
{ name: 'LocalChildValue', type: 'number', value: 0 },
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{ logCode: false }
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
runtimeScene.getVariables().get('Content').setString('');
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
expect(runtimeScene.getVariables().get('Content').getAsString()).toBe(
|
||
'9{"a":1,"b":"two"}11'
|
||
);
|
||
expect(runtimeScene.getVariables().has('MixedArray')).toBe(false);
|
||
expect(runtimeScene.getVariables().has('LocalChildValue')).toBe(false);
|
||
});
|
||
|
||
it('can generate a Repeat event where a local index variable shadows a parent local variable', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::Standard',
|
||
variables: [{ name: 'SomeLocalVariable', type: 'number', value: 50 }],
|
||
conditions: [],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
type: 'BuiltinCommonInstructions::Repeat',
|
||
repeatExpression: '5',
|
||
loopIndexVariable: 'SomeLocalVariable',
|
||
variables: [
|
||
{ name: 'SomeLocalVariable', type: 'number', value: 0 },
|
||
],
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Sum', '+', 'SomeLocalVariable'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
// After the loop, read the parent's SomeLocalVariable (should still be 50).
|
||
{
|
||
type: 'BuiltinCommonInstructions::Standard',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['ParentValue', '=', 'SomeLocalVariable'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
// Inner loop: 0+1+2+3+4 = 10
|
||
expect(runtimeScene.getVariables().get('Sum').getAsNumber()).toBe(10);
|
||
// Parent's variable is unaffected by the inner loop's index.
|
||
expect(runtimeScene.getVariables().get('ParentValue').getAsNumber()).toBe(
|
||
50
|
||
);
|
||
expect(runtimeScene.getVariables().has('SomeLocalVariable')).toBe(false);
|
||
});
|
||
|
||
it('can generate nested While events with same-named local index variables', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
infiniteLoopWarning: true,
|
||
type: 'BuiltinCommonInstructions::While',
|
||
whileConditions: [
|
||
{
|
||
type: { value: 'NumberVariable' },
|
||
parameters: ['LoopIndex', '<', '3'],
|
||
},
|
||
],
|
||
loopIndexVariable: 'LoopIndex',
|
||
variables: [{ name: 'LoopIndex', type: 'number', value: 0 }],
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['OuterSum', '+', 'LoopIndex'],
|
||
},
|
||
],
|
||
events: [
|
||
{
|
||
infiniteLoopWarning: true,
|
||
type: 'BuiltinCommonInstructions::While',
|
||
whileConditions: [
|
||
{
|
||
type: { value: 'NumberVariable' },
|
||
parameters: ['LoopIndex', '<', '3'],
|
||
},
|
||
],
|
||
loopIndexVariable: 'LoopIndex',
|
||
variables: [{ name: 'LoopIndex', type: 'number', value: 0 }],
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['InnerSum', '+', 'LoopIndex'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
runCompiledEvents(gdjs, runtimeScene, []);
|
||
|
||
// Outer loop: 0+1+2 = 3
|
||
expect(runtimeScene.getVariables().get('OuterSum').getAsNumber()).toBe(3);
|
||
// Inner loop runs 3 times per outer iteration: 3*(0+1+2) = 9
|
||
expect(runtimeScene.getVariables().get('InnerSum').getAsNumber()).toBe(9);
|
||
expect(runtimeScene.getVariables().has('LoopIndex')).toBe(false);
|
||
});
|
||
|
||
it('can generate a stress test with deeply nested loops of all types, local variables, indexes and variable shadowing', function () {
|
||
// This test nests all four loop types (for-each-child, repeat, while,
|
||
// for-each-object) four levels deep, with local variables at every level,
|
||
// multiple loop index variables, and a variable "ShadowedIdx" that is
|
||
// shadowed at three different scopes.
|
||
//
|
||
// Scoping hierarchy:
|
||
// Level 0 (Standard): ShadowedIdx = 100
|
||
// Level 1 (ForEachChildVariable): ChildValue, ChildKey, ChildLoopIdx, DataMap
|
||
// Level 2 (Repeat): RepeatIdx, ShadowedIdx = 0 (shadows L0)
|
||
// Level 3 (While): WhileIdx
|
||
// Level 4 (ForEach object): ShadowedIdx = loop index (shadows L2)
|
||
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
// Level 0: Standard event declaring ShadowedIdx = 100
|
||
type: 'BuiltinCommonInstructions::Standard',
|
||
variables: [{ name: 'ShadowedIdx', type: 'number', value: 100 }],
|
||
conditions: [],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
// Level 1: ForEachChildVariable over a local structure
|
||
type: 'BuiltinCommonInstructions::ForEachChildVariable',
|
||
iterableVariableName: 'DataMap',
|
||
valueIteratorVariableName: 'ChildValue',
|
||
keyIteratorVariableName: 'ChildKey',
|
||
loopIndexVariable: 'ChildLoopIdx',
|
||
variables: [
|
||
{
|
||
name: 'DataMap',
|
||
type: 'structure',
|
||
children: [
|
||
{ name: 'X', type: 'number', value: 10 },
|
||
{ name: 'Y', type: 'number', value: 20 },
|
||
],
|
||
},
|
||
{ name: 'ChildValue', type: 'number', value: 0 },
|
||
{ name: 'ChildKey', type: 'string', value: '' },
|
||
{ name: 'ChildLoopIdx', type: 'number', value: 0 },
|
||
],
|
||
conditions: [],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
// Level 2: Repeat 3 times, with local ShadowedIdx=0
|
||
// that shadows Level 0's ShadowedIdx=100.
|
||
type: 'BuiltinCommonInstructions::Repeat',
|
||
repeatExpression: '3',
|
||
loopIndexVariable: 'RepeatIdx',
|
||
variables: [
|
||
{ name: 'RepeatIdx', type: 'number', value: 0 },
|
||
{ name: 'ShadowedIdx', type: 'number', value: 0 },
|
||
],
|
||
conditions: [],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
// Level 3: While loop (WhileIdx < 2 → 2 iterations)
|
||
infiniteLoopWarning: true,
|
||
type: 'BuiltinCommonInstructions::While',
|
||
whileConditions: [
|
||
{
|
||
type: { value: 'NumberVariable' },
|
||
parameters: ['WhileIdx', '<', '2'],
|
||
},
|
||
],
|
||
loopIndexVariable: 'WhileIdx',
|
||
variables: [{ name: 'WhileIdx', type: 'number', value: 0 }],
|
||
conditions: [],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
// Level 4: ForEach object whose loop index variable
|
||
// is "ShadowedIdx", shadowing Level 2's ShadowedIdx.
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'MyObject',
|
||
loopIndexVariable: 'ShadowedIdx',
|
||
variables: [
|
||
{
|
||
name: 'ShadowedIdx',
|
||
type: 'number',
|
||
value: 0,
|
||
},
|
||
],
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: [
|
||
'Sum',
|
||
'+',
|
||
'ChildValue + RepeatIdx + WhileIdx + ShadowedIdx + MyObject.Variable(MyVar)',
|
||
],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
// After repeat, still inside for-each-child: read indexes
|
||
// and verify shadowing restores to the correct scope.
|
||
{
|
||
type: 'BuiltinCommonInstructions::Standard',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
// ChildLoopIdx should still be the for-each-child index.
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['ChildIdxSum', '+', 'ChildLoopIdx'],
|
||
},
|
||
{
|
||
// ShadowedIdx here should be Level 0's value (100)
|
||
// because Level 2's local is now out of scope.
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['ShadowAfterRepeat', '+', 'ShadowedIdx'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
// After for-each-child, inside Level 0 Standard:
|
||
{
|
||
type: 'BuiltinCommonInstructions::Standard',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
// ShadowedIdx should be Level 0's original value (100).
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['OriginalShadow', '=', 'ShadowedIdx'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: { MyObject: 'object' },
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
|
||
// Create 2 objects with MyVar values 1 and 2.
|
||
const objectLists = new gdjs.Hashtable();
|
||
const myObjects = [];
|
||
objectLists.put('MyObject', myObjects);
|
||
for (let index = 1; index <= 2; index++) {
|
||
const myObject = runtimeScene.createObject('MyObject');
|
||
myObject.getVariables().get('MyVar').setNumber(index);
|
||
myObjects.push(myObject);
|
||
}
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [objectLists]);
|
||
|
||
// --- Verify Sum ---
|
||
// Total inner iterations:
|
||
// 2 (for-each-child: X,Y) × 3 (repeat) × 2 (while) × 2 (for-each-object) = 24
|
||
// Each addend appears in 24/(number of its distinct values) iterations:
|
||
// ChildValue ∈ {10,20} → 12 each → 12*(10+20) = 360
|
||
// RepeatIdx ∈ {0,1,2} → 8 each → 8*(0+1+2) = 24
|
||
// WhileIdx ∈ {0,1} → 12 each → 12*(0+1) = 12
|
||
// ShadowedIdx∈ {0,1} → 12 each → 12*(0+1) = 12
|
||
// MyVar ∈ {1,2} → 12 each → 12*(1+2) = 36
|
||
// Total Sum = 360 + 24 + 12 + 12 + 36 = 444
|
||
expect(runtimeScene.getVariables().get('Sum').getAsNumber()).toBe(444);
|
||
|
||
// --- Verify for-each-child loop index survives the inner loops ---
|
||
// Runs once per for-each-child iteration (2 iterations): 0 + 1 = 1
|
||
expect(runtimeScene.getVariables().get('ChildIdxSum').getAsNumber()).toBe(
|
||
1
|
||
);
|
||
|
||
// --- Verify variable shadowing restores correctly ---
|
||
// After the repeat exits, its local ShadowedIdx=0 is out of scope,
|
||
// so ShadowedIdx resolves to Level 0's value (100).
|
||
// Runs once per for-each-child iteration (2): 100 + 100 = 200
|
||
expect(
|
||
runtimeScene.getVariables().get('ShadowAfterRepeat').getAsNumber()
|
||
).toBe(200);
|
||
|
||
// After the for-each-child exits, ShadowedIdx is still Level 0's 100.
|
||
expect(
|
||
runtimeScene.getVariables().get('OriginalShadow').getAsNumber()
|
||
).toBe(100);
|
||
|
||
// --- Verify no local variable leaked to the scene ---
|
||
expect(runtimeScene.getVariables().has('ShadowedIdx')).toBe(false);
|
||
expect(runtimeScene.getVariables().has('DataMap')).toBe(false);
|
||
expect(runtimeScene.getVariables().has('ChildValue')).toBe(false);
|
||
expect(runtimeScene.getVariables().has('ChildKey')).toBe(false);
|
||
expect(runtimeScene.getVariables().has('ChildLoopIdx')).toBe(false);
|
||
expect(runtimeScene.getVariables().has('RepeatIdx')).toBe(false);
|
||
expect(runtimeScene.getVariables().has('WhileIdx')).toBe(false);
|
||
});
|
||
});
|