diff --git a/Core/GDCore/Extensions/Builtin/BaseObjectExtension.cpp b/Core/GDCore/Extensions/Builtin/BaseObjectExtension.cpp index 390f8332da30..ff538f8fd853 100644 --- a/Core/GDCore/Extensions/Builtin/BaseObjectExtension.cpp +++ b/Core/GDCore/Extensions/Builtin/BaseObjectExtension.cpp @@ -576,7 +576,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension( .AddParameter("object", _("Object")) .AddParameter("objectvar", _("Variable")) .AddParameter("trueorfalse", _("Check if the value is")) - .SetDefaultValue("true") + .SetDefaultValue("True") // This parameter allows to keep the operand expression // when the editor switch between variable instructions. .AddCodeOnlyParameter("yesorno", _("Value")) @@ -887,7 +887,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension( .AddParameter("object", _("Object")) .AddParameter("objectvar", _("Variable")) .AddParameter("trueorfalse", _("Check if the value is")) - .SetDefaultValue("true") + .SetDefaultValue("True") .SetHelpPath("/all-features/variables/object-variables/") .SetRelevantForFunctionEventsOnly(); diff --git a/Core/GDCore/Extensions/Builtin/VariablesExtension.cpp b/Core/GDCore/Extensions/Builtin/VariablesExtension.cpp index 632b902a5dd5..d7380aa5f7df 100644 --- a/Core/GDCore/Extensions/Builtin/VariablesExtension.cpp +++ b/Core/GDCore/Extensions/Builtin/VariablesExtension.cpp @@ -61,7 +61,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension( "res/conditions/var.png") .AddParameter("variableOrPropertyOrParameter", _("Variable")) .AddParameter("trueorfalse", _("Check if the value is")) - .SetDefaultValue("true") + .SetDefaultValue("True") // This parameter allows to keep the operand expression // when the editor switch between variable instructions. .AddCodeOnlyParameter("trueorfalse", ""); @@ -318,7 +318,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension( "res/conditions/var.png") .AddParameter("scenevar", _("Variable")) .AddParameter("trueorfalse", _("Check if the value is")) - .SetDefaultValue("true") + .SetDefaultValue("True") .SetRelevantForFunctionEventsOnly() .SetHidden(); @@ -407,7 +407,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension( "res/conditions/var.png") .AddParameter("globalvar", _("Variable")) .AddParameter("trueorfalse", _("Check if the value is")) - .SetDefaultValue("true") + .SetDefaultValue("True") .SetRelevantForFunctionEventsOnly() .SetHidden(); diff --git a/Core/GDCore/IDE/Events/EventsVariableInstructionTypeSwitcher.cpp b/Core/GDCore/IDE/Events/EventsVariableInstructionTypeSwitcher.cpp index 974c3f3cc094..bc72975aaa00 100644 --- a/Core/GDCore/IDE/Events/EventsVariableInstructionTypeSwitcher.cpp +++ b/Core/GDCore/IDE/Events/EventsVariableInstructionTypeSwitcher.cpp @@ -92,9 +92,14 @@ bool EventsVariableInstructionTypeSwitcher::DoVisitInstruction(gd::Instruction& lastObjectName == groupName) { if (typeChangedVariableNames.find(variableName) != typeChangedVariableNames.end()) { + const gd::String previousType = instruction.GetType(); gd::VariableInstructionSwitcher:: SwitchBetweenUnifiedInstructionIfNeeded( platform, GetProjectScopedContainers(), instruction); + if (instruction.GetType() != previousType) { + gd::VariableInstructionSwitcher::ResetParametersAfterSwitch( + instruction); + } } } }); diff --git a/Core/GDCore/IDE/VariableInstructionSwitcher.cpp b/Core/GDCore/IDE/VariableInstructionSwitcher.cpp index c18edc2a485b..4df520a1c3b7 100644 --- a/Core/GDCore/IDE/VariableInstructionSwitcher.cpp +++ b/Core/GDCore/IDE/VariableInstructionSwitcher.cpp @@ -238,4 +238,76 @@ void VariableInstructionSwitcher::SwitchBetweenUnifiedInstructionIfNeeded( } } +void VariableInstructionSwitcher::ResetParametersAfterSwitch( + gd::Instruction &instruction) { + const gd::String &newType = instruction.GetType(); + + // Reset the operator parameter at `paramIndex` to `defaultValue` if its + // current value is not in `validValues`. + auto resetIfInvalid = [&instruction]( + std::size_t paramIndex, + std::initializer_list validValues, + const char *defaultValue) { + if (instruction.GetParametersCount() <= paramIndex) return; + const gd::String &value = + instruction.GetParameter(paramIndex).GetPlainString(); + for (const char *v : validValues) { + if (value == v) return; // value is valid, nothing to do + } + instruction.SetParameter(paramIndex, gd::Expression(defaultValue)); + }; + + // Helper: clear param at paramIndex if it holds a boolean-like value that is + // not a valid expression (left by a PushBoolean switch). + auto clearIfBooleanLiteral = [&instruction](std::size_t paramIndex) { + if (instruction.GetParametersCount() <= paramIndex) return; + const gd::String &value = + instruction.GetParameter(paramIndex).GetPlainString(); + if (value == "True" || value == "False") { + instruction.SetParameter(paramIndex, gd::Expression("")); + } + }; + + // Scene variable conditions (operator at param 1) + if (newType == "BooleanVariable") { + resetIfInvalid(1, {"True", "False"}, "True"); + } else if (newType == "NumberVariable") { + resetIfInvalid(1, {"=", "<", ">", "<=", ">=", "!="}, "="); + } else if (newType == "StringVariable") { + resetIfInvalid(1, {"=", "!=", "startsWith", "endsWith", "contains"}, "="); + // Scene variable Set actions (operator at param 1) + } else if (newType == "SetBooleanVariable") { + resetIfInvalid(1, {"True", "False", "Toggle"}, "True"); + } else if (newType == "SetNumberVariable") { + resetIfInvalid(1, {"=", "+", "-", "*", "/"}, "="); + } else if (newType == "SetStringVariable") { + resetIfInvalid(1, {"=", "+"}, "="); + // Scene variable Push actions (expression at param 1) + } else if (newType == "PushBoolean") { + resetIfInvalid(1, {"True", "False"}, "True"); + } else if (newType == "PushNumber" || newType == "PushString") { + clearIfBooleanLiteral(1); + // Object variable conditions (operator at param 2) + } else if (newType == "BooleanObjectVariable") { + resetIfInvalid(2, {"True", "False"}, "True"); + } else if (newType == "NumberObjectVariable") { + resetIfInvalid(2, {"=", "<", ">", "<=", ">=", "!="}, "="); + } else if (newType == "StringObjectVariable") { + resetIfInvalid(2, {"=", "!=", "startsWith", "endsWith", "contains"}, "="); + // Object variable Set actions (operator at param 2) + } else if (newType == "SetBooleanObjectVariable") { + resetIfInvalid(2, {"True", "False", "Toggle"}, "True"); + } else if (newType == "SetNumberObjectVariable") { + resetIfInvalid(2, {"=", "+", "-", "*", "/"}, "="); + } else if (newType == "SetStringObjectVariable") { + resetIfInvalid(2, {"=", "+"}, "="); + // Object variable Push actions (expression at param 2) + } else if (newType == "PushBooleanToObjectVariable") { + resetIfInvalid(2, {"True", "False"}, "True"); + } else if (newType == "PushNumberToObjectVariable" || + newType == "PushStringToObjectVariable") { + clearIfBooleanLiteral(2); + } +} + } // namespace gd diff --git a/Core/GDCore/IDE/VariableInstructionSwitcher.h b/Core/GDCore/IDE/VariableInstructionSwitcher.h index 872cd84a860d..4f2e73ab3cc5 100644 --- a/Core/GDCore/IDE/VariableInstructionSwitcher.h +++ b/Core/GDCore/IDE/VariableInstructionSwitcher.h @@ -79,6 +79,15 @@ class GD_CORE_API VariableInstructionSwitcher { const gd::ProjectScopedContainers &projectScopedContainers, gd::Instruction &instruction); + /** + * \brief After a type switch, fix the operator parameter if its current value + * is no longer valid for the new instruction type. Call this when the type + * actually changed. Covers all switchable variable instruction variants: + * conditions, Set actions, and Push (array) actions, for both scene and + * object variables. + */ + static void ResetParametersAfterSwitch(gd::Instruction &instruction); + private: static const gd::String variableGetterIdentifier; static const gd::String variableSetterIdentifier; diff --git a/newIDE/app/src/EventsSheet/InlineParameterEditor.js b/newIDE/app/src/EventsSheet/InlineParameterEditor.js index 45e2a056fcfd..727e1164739b 100644 --- a/newIDE/app/src/EventsSheet/InlineParameterEditor.js +++ b/newIDE/app/src/EventsSheet/InlineParameterEditor.js @@ -126,26 +126,42 @@ const InlineParameterEditor = ({ // When the parameter is done being edited, ensure the instruction parameters // are properly set up. For example, it's possible that the object name was // changed, and so the associated behavior should be updated. - if (instruction && instructionMetadata) { - const objectParameterIndex = getObjectParameterIndex( - instructionMetadata - ); - setupInstructionParameters( - project, - projectScopedContainersAccessor, - instruction, - instructionMetadata, - objectParameterIndex !== -1 - ? instruction.getParameter(objectParameterIndex).getPlainString() - : null - ); + // Use the current instruction type to get metadata — the type may have + // switched during editing (e.g. boolean → number variable), so the + // metadata stored in state when the editor opened may be stale and using + // it would corrupt the operator parameter. + if (instruction) { + const currentType = instruction.getType(); + const currentInstructionMetadata = isCondition + ? gd.MetadataProvider.getConditionMetadata( + project.getCurrentPlatform(), + currentType + ) + : gd.MetadataProvider.getActionMetadata( + project.getCurrentPlatform(), + currentType + ); + if (currentInstructionMetadata) { + const objectParameterIndex = getObjectParameterIndex( + currentInstructionMetadata + ); + setupInstructionParameters( + project, + projectScopedContainersAccessor, + instruction, + currentInstructionMetadata, + objectParameterIndex !== -1 + ? instruction.getParameter(objectParameterIndex).getPlainString() + : null + ); + } } onApply(); }, [ instruction, - instructionMetadata, + isCondition, onApply, project, projectScopedContainersAccessor, diff --git a/newIDE/app/src/EventsSheet/InstructionEditor/InstructionParametersEditor.js b/newIDE/app/src/EventsSheet/InstructionEditor/InstructionParametersEditor.js index 842361ae0c81..7fa3cc8053bb 100644 --- a/newIDE/app/src/EventsSheet/InstructionEditor/InstructionParametersEditor.js +++ b/newIDE/app/src/EventsSheet/InstructionEditor/InstructionParametersEditor.js @@ -27,7 +27,10 @@ import { getObjectParameterIndex } from '../../InstructionOrExpression/Enumerate import Text from '../../UI/Text'; import { getInstructionMetadata } from './InstructionEditor'; import { ColumnStackLayout } from '../../UI/Layout'; -import { setupInstructionParameters } from '../../InstructionOrExpression/SetupInstructionParameters'; +import { + setupInstructionParameters, + resetParametersAfterSwitch, +} from '../../InstructionOrExpression/SetupInstructionParameters'; import ScrollView from '../../UI/ScrollView'; import { getInstructionTutorialIds } from '../../Utils/GDevelopServices/Tutorial'; import useForceUpdate from '../../Utils/UseForceUpdate'; @@ -241,11 +244,15 @@ const InstructionParametersEditor: React.ComponentType<{ [focus, focusOnMount] ); + const typeBeforeSwitch = instruction.getType(); gd.VariableInstructionSwitcher.switchBetweenUnifiedInstructionIfNeeded( project.getCurrentPlatform(), projectScopedContainersAccessor.get(), instruction ); + if (instruction.getType() !== typeBeforeSwitch) { + resetParametersAfterSwitch(instruction); + } const instructionType = instruction.getType(); diff --git a/newIDE/app/src/EventsSheet/index.js b/newIDE/app/src/EventsSheet/index.js index 633ce5a9f247..5578b6d5fddf 100644 --- a/newIDE/app/src/EventsSheet/index.js +++ b/newIDE/app/src/EventsSheet/index.js @@ -124,6 +124,7 @@ import { unregisterOnResourceExternallyChangedCallback, } from '../MainFrame/ResourcesWatcher'; import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope'; +import { resetParametersAfterSwitch } from '../InstructionOrExpression/SetupInstructionParameters'; import LocalVariablesDialog from '../VariablesList/LocalVariablesDialog'; import GlobalAndSceneVariablesDialog from '../VariablesList/GlobalAndSceneVariablesDialog'; import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton'; @@ -2670,11 +2671,15 @@ export class EventsSheetComponentWithoutHandle extends React.Component< } instruction.setParameter(parameterIndex, value); + const typeBeforeSwitch = instruction.getType(); gd.VariableInstructionSwitcher.switchBetweenUnifiedInstructionIfNeeded( project.getCurrentPlatform(), editedParameterProjectScopedContainersAccessor.get(), instruction ); + if (instruction.getType() !== typeBeforeSwitch) { + resetParametersAfterSwitch(instruction); + } // Ask the component to re-render, so that the new parameter // set for the instruction in the state diff --git a/newIDE/app/src/InstructionOrExpression/SetupInstructionParameters.js b/newIDE/app/src/InstructionOrExpression/SetupInstructionParameters.js index 5aae7bcf93b6..1b81cfc99f45 100644 --- a/newIDE/app/src/InstructionOrExpression/SetupInstructionParameters.js +++ b/newIDE/app/src/InstructionOrExpression/SetupInstructionParameters.js @@ -45,23 +45,163 @@ export const setupInstructionParameters = ( for (let i = 0; i < instructionMetadata.getParametersCount(); i++) { const parameterMetadata = instructionMetadata.getParameter(i); - if (!instruction.getParameter(i).getPlainString()) { - const type = parameterMetadata.getType(); - const extraInfo = parameterMetadata.getExtraInfo(); - if (type === 'operator') { - const operators = - mapTypeToOperators[extraInfo] || mapTypeToOperators.unknown; + if (parameterMetadata.isCodeOnly()) continue; + const type = parameterMetadata.getType(); + const currentValue = instruction.getParameter(i).getPlainString(); + const extraInfo = parameterMetadata.getExtraInfo(); + if (type === 'operator') { + const operators = + mapTypeToOperators[extraInfo] || mapTypeToOperators.unknown; + if (!currentValue || !operators.includes(currentValue)) { instruction.setParameter(i, operators[0]); hasChangeAnyParameterValue = true; - } else if (type === 'relationalOperator') { - const operators = - mapTypeToRelationalOperators[extraInfo] || - mapTypeToRelationalOperators.unknown; + } + } else if (type === 'relationalOperator') { + const operators = + mapTypeToRelationalOperators[extraInfo] || + mapTypeToRelationalOperators.unknown; + if (!currentValue || !operators.includes(currentValue)) { instruction.setParameter(i, operators[0]); hasChangeAnyParameterValue = true; } + } else if ( + type === 'trueorfalse' && + currentValue !== 'True' && + currentValue !== 'False' + ) { + // Any invalid value (e.g. "=" left by the variable instruction switcher, + // or "true" from old casing) evaluated as false in the runtime and showed + // False in the UI, so preserve that behavior. + // Note: when a real type switch just happened, resetParametersAfterSwitch + // already set the value to "True" before this runs, so this branch only + // fires for old saved conditions that were never properly initialized. + instruction.setParameter(i, 'False'); + hasChangeAnyParameterValue = true; } } return hasChangeAnyParameterValue; }; + +/** + * Describes the "operator" parameter for each switchable variable instruction + * type — the slot whose valid values differ between instruction variants. + * Object variants shift the param index to 2 (extra object param at 0). + * Push variants use 'expression' as the paramType: there are no valid-value + * constraints, but "True"/"False" left by a PushBoolean switch must be cleared. + */ +const variableInstructionOperatorParam: { + [string]: {| index: number, paramType: string, extraInfo: string |}, +} = { + // Scene variable conditions + BooleanVariable: { index: 1, paramType: 'trueorfalse', extraInfo: '' }, + NumberVariable: { + index: 1, + paramType: 'relationalOperator', + extraInfo: 'number', + }, + StringVariable: { + index: 1, + paramType: 'relationalOperator', + extraInfo: 'string', + }, + // Scene variable Set actions + SetBooleanVariable: { index: 1, paramType: 'operator', extraInfo: 'boolean' }, + SetNumberVariable: { index: 1, paramType: 'operator', extraInfo: 'number' }, + SetStringVariable: { index: 1, paramType: 'operator', extraInfo: 'string' }, + // Scene variable Push (array) actions + PushBoolean: { index: 1, paramType: 'trueorfalse', extraInfo: '' }, + PushNumber: { index: 1, paramType: 'expression', extraInfo: '' }, + PushString: { index: 1, paramType: 'expression', extraInfo: '' }, + // Object variable conditions + BooleanObjectVariable: { index: 2, paramType: 'trueorfalse', extraInfo: '' }, + NumberObjectVariable: { + index: 2, + paramType: 'relationalOperator', + extraInfo: 'number', + }, + StringObjectVariable: { + index: 2, + paramType: 'relationalOperator', + extraInfo: 'string', + }, + // Object variable Set actions + SetBooleanObjectVariable: { + index: 2, + paramType: 'operator', + extraInfo: 'boolean', + }, + SetNumberObjectVariable: { + index: 2, + paramType: 'operator', + extraInfo: 'number', + }, + SetStringObjectVariable: { + index: 2, + paramType: 'operator', + extraInfo: 'string', + }, + // Object variable Push (array) actions + PushBooleanToObjectVariable: { + index: 2, + paramType: 'trueorfalse', + extraInfo: '', + }, + PushNumberToObjectVariable: { + index: 2, + paramType: 'expression', + extraInfo: '', + }, + PushStringToObjectVariable: { + index: 2, + paramType: 'expression', + extraInfo: '', + }, +}; + +/** + * After switchBetweenUnifiedInstructionIfNeeded changes the instruction type, + * fix the operator parameter if its current value is no longer valid for the + * new type (e.g. "<" is not a valid string relational operator, "True" is not + * a valid number operator). + * + * Must be called right after switchBetweenUnifiedInstructionIfNeeded, not + * inside setupInstructionParameters: setupInstructionParameters normalizes + * invalid trueorfalse values to "False" (backward compat for old saved files), + * so when switching TO boolean the reset to "True" must happen here first. + */ +export const resetParametersAfterSwitch = ( + instruction: gdInstruction +): void => { + const newType = instruction.getType(); + const paramInfo = variableInstructionOperatorParam[newType]; + if (!paramInfo) return; + + const { index, paramType, extraInfo } = paramInfo; + if (instruction.getParametersCount() <= index) return; + const currentValue = instruction.getParameter(index).getPlainString(); + + if (paramType === 'trueorfalse') { + if (currentValue !== 'True' && currentValue !== 'False') { + instruction.setParameter(index, 'True'); + } + } else if (paramType === 'relationalOperator') { + const operators = + mapTypeToRelationalOperators[extraInfo] || + mapTypeToRelationalOperators.unknown; + if (!currentValue || !operators.includes(currentValue)) { + instruction.setParameter(index, operators[0]); + } + } else if (paramType === 'operator') { + const operators = + mapTypeToOperators[extraInfo] || mapTypeToOperators.unknown; + if (!currentValue || !operators.includes(currentValue)) { + instruction.setParameter(index, operators[0]); + } + } else if (paramType === 'expression') { + // "True"/"False" left by a PushBoolean switch are not valid expressions. + if (currentValue === 'True' || currentValue === 'False') { + instruction.setParameter(index, ''); + } + } +}; diff --git a/newIDE/app/src/InstructionOrExpression/SetupInstructionParameters.spec.js b/newIDE/app/src/InstructionOrExpression/SetupInstructionParameters.spec.js index 5225c566e5e5..952879b92aed 100644 --- a/newIDE/app/src/InstructionOrExpression/SetupInstructionParameters.spec.js +++ b/newIDE/app/src/InstructionOrExpression/SetupInstructionParameters.spec.js @@ -3,11 +3,23 @@ import { enumerateObjectAndBehaviorsInstructions, enumerateFreeInstructions, } from './EnumerateInstructions'; -import { setupInstructionParameters } from './SetupInstructionParameters'; +import { + setupInstructionParameters, + resetParametersAfterSwitch, +} from './SetupInstructionParameters'; import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope'; const gd: libGDevelop = global.gd; +// Helper: build an instruction with the given type and parameter values. +const makeInstruction = (type: string, params: Array) => { + const instruction = new gd.Instruction(); + instruction.setType(type); + instruction.setParametersCount(params.length); + params.forEach((p, i) => instruction.setParameter(i, p)); + return instruction; +}; + // $FlowFixMe[incompatible-type] // $FlowFixMe[missing-local-annot] // $FlowFixMe[cannot-resolve-name] @@ -288,6 +300,92 @@ describe('setupInstructionParameters', () => { ); }); + it('normalizes an invalid relationalOperator to the first valid one', () => { + // $FlowFixMe[invalid-constructor] + const project = new gd.ProjectHelper.createNewGDJSProject(); + const layout = project.insertNewLayout('Scene', 0); + const projectScopedContainersAccessor = new ProjectScopedContainersAccessor( + { project, layout } + ); + const instructionMetadata = gd.MetadataProvider.getConditionMetadata( + project.getCurrentPlatform(), + 'NumberVariable' + ); + + // Simulate a NumberVariable instruction that has a stale string operator + // (e.g. "startsWith") left after switching from StringVariable. + const instruction = makeInstruction('NumberVariable', [ + 'myVar', + 'startsWith', + '0', + ]); + setupInstructionParameters( + project, + projectScopedContainersAccessor, + instruction, + instructionMetadata + ); + expect(instruction.getParameter(1).getPlainString()).toBe('='); + + project.delete(); + }); + + it('normalizes an invalid operator to the first valid one', () => { + // $FlowFixMe[invalid-constructor] + const project = new gd.ProjectHelper.createNewGDJSProject(); + const layout = project.insertNewLayout('Scene', 0); + const projectScopedContainersAccessor = new ProjectScopedContainersAccessor( + { project, layout } + ); + const instructionMetadata = gd.MetadataProvider.getActionMetadata( + project.getCurrentPlatform(), + 'SetStringVariable' + ); + + // Simulate a SetStringVariable instruction with "-" left from SetNumberVariable. + const instruction = makeInstruction('SetStringVariable', [ + 'myVar', + '-', + '"hello"', + ]); + setupInstructionParameters( + project, + projectScopedContainersAccessor, + instruction, + instructionMetadata + ); + expect(instruction.getParameter(1).getPlainString()).toBe('='); + + project.delete(); + }); + + it('normalizes an invalid trueorfalse value to "False" (backward compat)', () => { + // $FlowFixMe[invalid-constructor] + const project = new gd.ProjectHelper.createNewGDJSProject(); + const layout = project.insertNewLayout('Scene', 0); + const projectScopedContainersAccessor = new ProjectScopedContainersAccessor( + { project, layout } + ); + const instructionMetadata = gd.MetadataProvider.getConditionMetadata( + project.getCurrentPlatform(), + 'BooleanVariable' + ); + + // A saved BooleanVariable condition that still has "=" from before the + // C++ fixer was in place: it evaluated as False in the runtime and showed + // False in the UI, so we preserve that behavior. + const instruction = makeInstruction('BooleanVariable', ['myVar', '=']); + setupInstructionParameters( + project, + projectScopedContainersAccessor, + instruction, + instructionMetadata + ); + expect(instruction.getParameter(1).getPlainString()).toBe('False'); + + project.delete(); + }); + it('sets the proper parameters for a behavior, letting an existing behavior name if it is valid', () => { // $FlowFixMe[invalid-constructor] const project = new gd.ProjectHelper.createNewGDJSProject(); @@ -349,4 +447,488 @@ describe('setupInstructionParameters', () => { 'OtherPlatformerObject' ); }); + + it('preserves valid "True" for trueorfalse parameter (not overwritten to "False")', () => { + // $FlowFixMe[invalid-constructor] + const project = new gd.ProjectHelper.createNewGDJSProject(); + const layout = project.insertNewLayout('Scene', 0); + const projectScopedContainersAccessor = new ProjectScopedContainersAccessor( + { project, layout } + ); + const instructionMetadata = gd.MetadataProvider.getConditionMetadata( + project.getCurrentPlatform(), + 'BooleanVariable' + ); + + const instruction = makeInstruction('BooleanVariable', ['myVar', 'True']); + setupInstructionParameters( + project, + projectScopedContainersAccessor, + instruction, + instructionMetadata + ); + expect(instruction.getParameter(1).getPlainString()).toBe('True'); + + project.delete(); + }); + + it('preserves valid "Toggle" for SetBooleanVariable operator parameter', () => { + // $FlowFixMe[invalid-constructor] + const project = new gd.ProjectHelper.createNewGDJSProject(); + const layout = project.insertNewLayout('Scene', 0); + const projectScopedContainersAccessor = new ProjectScopedContainersAccessor( + { project, layout } + ); + const instructionMetadata = gd.MetadataProvider.getActionMetadata( + project.getCurrentPlatform(), + 'SetBooleanVariable' + ); + + const instruction = makeInstruction('SetBooleanVariable', [ + 'myVar', + 'Toggle', + '', + ]); + setupInstructionParameters( + project, + projectScopedContainersAccessor, + instruction, + instructionMetadata + ); + expect(instruction.getParameter(1).getPlainString()).toBe('Toggle'); + + project.delete(); + }); + + it('reset then setup: switching to BooleanVariable with stale "=" ends up as "True"', () => { + // $FlowFixMe[invalid-constructor] + const project = new gd.ProjectHelper.createNewGDJSProject(); + const layout = project.insertNewLayout('Scene', 0); + const projectScopedContainersAccessor = new ProjectScopedContainersAccessor( + { project, layout } + ); + const instructionMetadata = gd.MetadataProvider.getConditionMetadata( + project.getCurrentPlatform(), + 'BooleanVariable' + ); + + // Simulate the type-switch flow: resetParametersAfterSwitch runs first + // (sets "=" → "True"), then setupInstructionParameters runs and must leave + // "True" intact rather than normalizing it back to "False". + const instruction = makeInstruction('BooleanVariable', ['myVar', '=']); + resetParametersAfterSwitch(instruction); + setupInstructionParameters( + project, + projectScopedContainersAccessor, + instruction, + instructionMetadata + ); + expect(instruction.getParameter(1).getPlainString()).toBe('True'); + + project.delete(); + }); +}); + +describe('resetParametersAfterSwitch', () => { + describe('scene variable conditions', () => { + it('BooleanVariable: resets stale relational operator to "True"', () => { + const instruction = makeInstruction('BooleanVariable', ['myVar', '=']); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('True'); + }); + + it('BooleanVariable: keeps valid "True"', () => { + const instruction = makeInstruction('BooleanVariable', ['myVar', 'True']); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('True'); + }); + + it('BooleanVariable: keeps valid "False"', () => { + const instruction = makeInstruction('BooleanVariable', [ + 'myVar', + 'False', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('False'); + }); + + it('NumberVariable: resets "True" (stale from boolean) to "="', () => { + const instruction = makeInstruction('NumberVariable', [ + 'myVar', + 'True', + '0', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('='); + }); + + it('NumberVariable: resets "startsWith" (stale from string) to "="', () => { + const instruction = makeInstruction('NumberVariable', [ + 'myVar', + 'startsWith', + '0', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('='); + }); + + it('NumberVariable: keeps valid "="', () => { + const instruction = makeInstruction('NumberVariable', [ + 'myVar', + '=', + '42', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('='); + }); + + it('NumberVariable: keeps valid "<"', () => { + const instruction = makeInstruction('NumberVariable', [ + 'myVar', + '<', + '42', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('<'); + }); + + it('StringVariable: resets "True" (stale from boolean) to "="', () => { + const instruction = makeInstruction('StringVariable', [ + 'myVar', + 'True', + '"hi"', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('='); + }); + + it('StringVariable: resets "<" (invalid for string) to "="', () => { + const instruction = makeInstruction('StringVariable', [ + 'myVar', + '<', + '"hi"', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('='); + }); + + it('StringVariable: keeps valid "startsWith"', () => { + const instruction = makeInstruction('StringVariable', [ + 'myVar', + 'startsWith', + '"hi"', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('startsWith'); + }); + + it('StringVariable: keeps valid "!="', () => { + const instruction = makeInstruction('StringVariable', [ + 'myVar', + '!=', + '"hi"', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('!='); + }); + }); + + describe('scene variable Set actions', () => { + it('SetBooleanVariable: resets "=" (stale from number) to "True"', () => { + const instruction = makeInstruction('SetBooleanVariable', [ + 'myVar', + '=', + '', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('True'); + }); + + it('SetBooleanVariable: keeps valid "True"', () => { + const instruction = makeInstruction('SetBooleanVariable', [ + 'myVar', + 'True', + '', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('True'); + }); + + it('SetBooleanVariable: keeps valid "Toggle"', () => { + const instruction = makeInstruction('SetBooleanVariable', [ + 'myVar', + 'Toggle', + '', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('Toggle'); + }); + + it('SetNumberVariable: resets "True" (stale from boolean) to "="', () => { + const instruction = makeInstruction('SetNumberVariable', [ + 'myVar', + 'True', + '0', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('='); + }); + + it('SetNumberVariable: keeps valid "+"', () => { + const instruction = makeInstruction('SetNumberVariable', [ + 'myVar', + '+', + '1', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('+'); + }); + + it('SetStringVariable: resets "True" (stale from boolean) to "="', () => { + const instruction = makeInstruction('SetStringVariable', [ + 'myVar', + 'True', + '"hi"', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('='); + }); + + it('SetStringVariable: resets "-" (invalid for string) to "="', () => { + const instruction = makeInstruction('SetStringVariable', [ + 'myVar', + '-', + '"hi"', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('='); + }); + + it('SetStringVariable: keeps valid "+"', () => { + const instruction = makeInstruction('SetStringVariable', [ + 'myVar', + '+', + '"hi"', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('+'); + }); + }); + + describe('scene variable Push actions', () => { + it('PushBoolean: resets stale expression to "True"', () => { + const instruction = makeInstruction('PushBoolean', ['myVar', '42']); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('True'); + }); + + it('PushBoolean: keeps valid "False"', () => { + const instruction = makeInstruction('PushBoolean', ['myVar', 'False']); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('False'); + }); + + it('PushNumber: clears "True" (stale from PushBoolean)', () => { + const instruction = makeInstruction('PushNumber', ['myVar', 'True']); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe(''); + }); + + it('PushNumber: clears "False" (stale from PushBoolean)', () => { + const instruction = makeInstruction('PushNumber', ['myVar', 'False']); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe(''); + }); + + it('PushNumber: keeps a valid expression', () => { + const instruction = makeInstruction('PushNumber', ['myVar', '42']); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('42'); + }); + + it('PushString: clears "True" (stale from PushBoolean)', () => { + const instruction = makeInstruction('PushString', ['myVar', 'True']); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe(''); + }); + + it('PushString: keeps a valid expression', () => { + const instruction = makeInstruction('PushString', ['myVar', '"hello"']); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(1).getPlainString()).toBe('"hello"'); + }); + }); + + describe('object variable conditions', () => { + it('BooleanObjectVariable: resets stale operator at param 2 to "True"', () => { + const instruction = makeInstruction('BooleanObjectVariable', [ + 'MyObj', + 'myVar', + '=', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(2).getPlainString()).toBe('True'); + }); + + it('BooleanObjectVariable: keeps valid "False" at param 2', () => { + const instruction = makeInstruction('BooleanObjectVariable', [ + 'MyObj', + 'myVar', + 'False', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(2).getPlainString()).toBe('False'); + }); + + it('NumberObjectVariable: resets "True" (stale from boolean) to "=" at param 2', () => { + const instruction = makeInstruction('NumberObjectVariable', [ + 'MyObj', + 'myVar', + 'True', + '0', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(2).getPlainString()).toBe('='); + }); + + it('NumberObjectVariable: keeps valid "<" at param 2', () => { + const instruction = makeInstruction('NumberObjectVariable', [ + 'MyObj', + 'myVar', + '<', + '0', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(2).getPlainString()).toBe('<'); + }); + + it('StringObjectVariable: resets "<" (invalid for string) to "=" at param 2', () => { + const instruction = makeInstruction('StringObjectVariable', [ + 'MyObj', + 'myVar', + '<', + '"hi"', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(2).getPlainString()).toBe('='); + }); + + it('StringObjectVariable: keeps valid "contains" at param 2', () => { + const instruction = makeInstruction('StringObjectVariable', [ + 'MyObj', + 'myVar', + 'contains', + '"hi"', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(2).getPlainString()).toBe('contains'); + }); + }); + + describe('object variable Set actions', () => { + it('SetBooleanObjectVariable: resets "=" (stale from number) to "True" at param 2', () => { + const instruction = makeInstruction('SetBooleanObjectVariable', [ + 'MyObj', + 'myVar', + '=', + '', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(2).getPlainString()).toBe('True'); + }); + + it('SetBooleanObjectVariable: keeps valid "Toggle" at param 2', () => { + const instruction = makeInstruction('SetBooleanObjectVariable', [ + 'MyObj', + 'myVar', + 'Toggle', + '', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(2).getPlainString()).toBe('Toggle'); + }); + + it('SetNumberObjectVariable: resets "True" to "=" at param 2', () => { + const instruction = makeInstruction('SetNumberObjectVariable', [ + 'MyObj', + 'myVar', + 'True', + '0', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(2).getPlainString()).toBe('='); + }); + + it('SetStringObjectVariable: resets "-" (invalid for string) to "=" at param 2', () => { + const instruction = makeInstruction('SetStringObjectVariable', [ + 'MyObj', + 'myVar', + '-', + '"hi"', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(2).getPlainString()).toBe('='); + }); + }); + + describe('object variable Push actions', () => { + it('PushBooleanToObjectVariable: resets stale expression to "True" at param 2', () => { + const instruction = makeInstruction('PushBooleanToObjectVariable', [ + 'MyObj', + 'myVar', + '42', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(2).getPlainString()).toBe('True'); + }); + + it('PushBooleanToObjectVariable: keeps valid "False" at param 2', () => { + const instruction = makeInstruction('PushBooleanToObjectVariable', [ + 'MyObj', + 'myVar', + 'False', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(2).getPlainString()).toBe('False'); + }); + + it('PushNumberToObjectVariable: clears "True" (stale from PushBoolean) at param 2', () => { + const instruction = makeInstruction('PushNumberToObjectVariable', [ + 'MyObj', + 'myVar', + 'True', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(2).getPlainString()).toBe(''); + }); + + it('PushStringToObjectVariable: clears "False" (stale from PushBoolean) at param 2', () => { + const instruction = makeInstruction('PushStringToObjectVariable', [ + 'MyObj', + 'myVar', + 'False', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(2).getPlainString()).toBe(''); + }); + + it('PushNumberToObjectVariable: keeps a valid expression at param 2', () => { + const instruction = makeInstruction('PushNumberToObjectVariable', [ + 'MyObj', + 'myVar', + '42', + ]); + resetParametersAfterSwitch(instruction); + expect(instruction.getParameter(2).getPlainString()).toBe('42'); + }); + }); + + describe('non-switchable instruction types', () => { + it('does nothing for an unknown instruction type', () => { + const instruction = makeInstruction('PlayMusic', ['', '', '', '', '']); + resetParametersAfterSwitch(instruction); + // No throw, and params unchanged + expect(instruction.getParameter(0).getPlainString()).toBe(''); + }); + }); });